part1では、JSONパーサーの作成方法と文字列の解析機能の実装について説明しました。次に、他の関数を補完します。(実際には基本原理さえわかっていれば、残りの関数の実装は単純なものです)
Number
数値の実装も難しくありませんが、小数点、負数、浮動小数点数、指数表記(1e6)などの部分を見落としやすいです。(ちなみに、今気づいたのですが、E
でも使えるんですね)
function number(parser) {
let str = "";
if (parser.current() === "-") {
str += "-";
parser.index += 1;
}
let curr = "";
while (((curr = parser.current()), curr >= "0" && curr <= "9")) {
str += curr;
parser.index += 1;
}
let isFloat = false;
// float number
if (parser.next(".")) {
str += ".";
isFloat = true;
while (((curr = parser.current()), curr >= "0" && curr <= "9")) {
str += curr;
parser.index += 1;
}
}
// exponential expression
let expo = "";
if (parser.next("e")) {
curr = "";
if (parser.next("-")) {
expo += "-";
}
while (((curr = parser.current()), curr >= "0" && curr <= "9")) {
expo += curr;
parser.index += 1;
}
}
if (expo) {
return isFloat
? parseFloat(str, 10) * Math.pow(10, +expo)
: parseInt(str, 10) * Math.pow(10, +expo);
}
return isFloat ? parseFloat(str, 10) : parseInt(str, 10);
}
-
最初の部分では、負数かどうかを確認します。
-
2番目の部分では、whileループを実行して数字の部分を追加します。
-
3番目の部分では、小数点があるかどうかを判定します。
- 小数点がある場合は、もう一度数字を追加します。
-
4番目の部分では、指数表記(大文字または小文字の
e
)があるかどうかを判定します。- 指数表記がある場合は、もう一度数字を追加します。
-
5番目の部分:文字列を数値に変換します(parseIntまたはparseFloatを使用)
キーワード(true, false, null)
function keyword(parser) {
if (parser.next("true")) {
return true;
} else if (parser.next("false")) {
return false;
} else if (parser.next("null")) {
return null;
}
}
この部分は非常に簡単で、値が一致するかどうかを確認するだけです。
配列(Array)
function array(parser) {
const arr = [];
if (parser.current() === "[") {
parser.next("[");
parser.skip();
if (parser.next("]")) {
return arr;
}
let i = 0;
while (parser.current()) {
const val = value(parser);
arr.push(val);
parser.skip();
if (parser.current() === "]") {
parser.next("]");
return arr;
}
parser.next(",");
parser.skip();
}
}
return arr;
}
-
最初の部分では、
[
で始まるかどうかを確認します。]
が出現すると、空の配列です。
-
2番目の部分では、
value
関数を実行し、その結果を配列に追加します。 -
]
が出現すると、配列の終わりです。配列を返します。 -
カンマが出現すると、次の要素があることを意味し、続行します。
これでほぼ完成です。詳細な実装については、コードの実装を確認するためにリポジトリを参照してください。テストでは、"specical-character"というテストが失敗しています。これは、文字列にエスケープ文字が含まれる可能性があるため、実装してみましょう。
const escape = {
'"': '"',
t: "\t",
r: "\r",
"\\": "\\",
};
while (((curr = parser.current()), curr)) {
if (parser.next('"')) {
return str;
} else if (curr === "\\") {
parser.index += 1;
const escapeCh = parser.current();
if (escape[escapeCh]) {
str += escape[escapeCh];
}
} else {
str += curr;
}
parser.index += 1;
}
エスケープ文字のテーブルを作成し、対応する実装に置き換えます。ここでは、\t
と\r
の実装のみを行っています。これで、基本的なJSONのテストをパスしました🍻。ただし、エスケープ文字には上記で説明した以外にも、ユニコードを表す\u
も実装する必要があります。
カスタム機能:
パーサーを自分で作成しているので、新しい構文を自分で追加することもできます!たとえば、\$\$
で囲まれた変数は、渡されたオブジェクトで置き換えられます。例えば:
{
"name": $name$
}
次のようになります:
new Parser(string, { name: 'kalan' }).parse();
// { name: "kalan" }
実装
function template(parser) {
parser.skip();
if (parser.next("$")) {
parser.skip();
if (parser.next("$")) {
throw new Error("template can not be empty");
}
let curr = "";
let key = "";
while (((curr = parser.current()), curr)) {
if (parser.next("$")) {
return parser.variables[key];
}
key += curr;
parser.index += 1;
}
}
}
- 最初に
$
をマッチさせます。 - 内容を読み取り、
$
が見つかるまで続けます。 $
が見つかったら、while
ループを停止し、変数でテンプレート変数を置き換えて結果を返します。
詳細な実装については、templateブランチを確認してください。テスト結果を確認してみてください(test/template
フォルダ内にあります)。
まとめ
パーサーを自分で作成することで、複雑な実装をより簡潔に表現することができます。また、既存の文法(この場合はJSON)にも拡張を追加することができます。パーサー言語そのものは重要で興味深いですが、パーサー言語だけでは一歩足りません。たとえば、JSXを純粋なJavaScriptコードに変換しても、Reactのサポートがなければ役に立ちません。SQLを抽象構文木に変換しても、データベースの実装がなければあまり意味がありません。パーサー言語の目的は、後続の処理(クエリの実行、DOMへのレンダリングなど)を容易にするためです。
実際には、既に多くのライブラリが存在し、パーサーの作成を省略してくれます。有名な例としては、BisonやPEG.jsがあります。これらのツールは、BNFに似た構文を使用して安定したパーサーを自動生成するため、パーサーの作成にかかる時間を節約し、言語の実装に集中することができます。
今回のJSONパーサーは、抽象構文木に変換して最終結果を生成していないため、次のステップでは簡単なHTMLを解析し、構文木に変換してJavaScriptのDOM APIを使用してレンダリングすることを試してみます。