JSONパーサーをゼロから作成する (2)

作成者:カランカラン
💡

質問やフィードバックがありましたら、フォームからお願いします

本文は台湾華語で、ChatGPT で翻訳している記事なので、不確かな部分や間違いがあるかもしれません。ご了承ください

part1 では JSON パーサーの書き方と文字列を解析する機能について触れましたが、次に残りの関数を追加します。(実際には基本原理を理解していれば、残りの関数の実装はそれほど難しくありません)

Number

json-grammer

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);
}
  • 第一部分、負数かどうかを確認します。

  • 第二部分、while ループを実行して数字部分を取り込みます。

  • 第三部分、小数点があるかどうかを判断します。

    • 小数点があれば、再度数字を取得します。
  • 第四部分、指数表現(大文字または小文字の e)があるかどうかを判断します。

    • もしあれば、再度数字を取得します。
  • 第五部分:文字列を数字に変換します(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)

json-grammer

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;
}
  • 第一部分、[ で始まっているか確認します。

    • ] に遭遇した場合、それは空の配列を意味します。
  • 第二部分、while ループを実行して value 関数を呼び出し、配列に値を追加します。

  • ] に遭遇した場合、配列の終わりを意味し、配列を返します。

  • コンマに遭遇した場合、次の要素があることを意味し、処理を続けます。

これでほぼ完成です。 Repository にあるコード実装を確認することができ、テスト中に 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 が必要で、これはユニコードを表します。この機能は非常に重要です。

カスタム機能:templatetemplate

解析部分は自分で書いているので、新しい構文を追加することもできます!たとえば、$$ で囲まれた変数は、渡されたオブジェクトで置き換えられます。以下のように:

{
  "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 へのレンダリング)を簡便にするためです。

実際、すでに多くのライブラリがあり、パーサーの部分を省略することができます。例えば、著名な BisonPEG.js などは、BNF に似た構文を使用して自動的に安定したパーサーを生成し、解析にかかる時間を省き、言語の実装に集中できるようにしてくれます。

今回の JSON パーサーは抽象構文木に変換して最終結果を生成することはしていませんので、次のステージでは、シンプルな HTML を解析し、構文木に変換して JavaScript の DOM API を使用してレンダリングすることを試みます。


この翻訳が役立つことを願っています!

この記事が役に立ったと思ったら、下のリンクからコーヒーを奢ってくれると嬉しいです ☕ 私の普通の一日が輝かしいものになります ✨

Buy me a coffee