カランのブログ

Kalan 頭像照片,在淡水拍攝,淺藍背景

四零二曜日電子報上線啦!訂閱訂起來

ソフトウェアエンジニア / 台湾人 / 福岡生活
このブログはRSS Feed をサポートしています。RSSリンクをクリックして設定してください。技術に関する記事はコードがあるのでブログで閲覧することをお勧めします。

今のモード ライト

我會把一些不成文的筆記或是最近的生活雜感放在短筆記,如果有興趣的話可以來看看唷!

記事のタイトルや概要は自動翻訳であるため(中身は翻訳されてない場合が多い)、変な言葉が出たり、意味伝わらない場合がございます。空いてる時間で翻訳します。

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

part1では、JSONパーサーの作成方法と文字列の解析機能の実装について説明しました。次に、他の関数を補完します。(実際には基本原理さえわかっていれば、残りの関数の実装は単純なものです)

Number

json-grammer

数値の実装も難しくありませんが、小数点、負数、浮動小数点数、指数表記(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)

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;
}
  • 最初の部分では、[で始まるかどうかを確認します。

    • ]が出現すると、空の配列です。
  • 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も実装する必要があります。

カスタム機能: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を使用してレンダリングすることを試してみます。

次の記事

JSON パーサーを一から作成する (1)

前の記事

テクノロジーは常に人類から生まれる(スヴェルト社会:質問質問メモ)

この文章が役に立つと思うなら、下のリンクで応援してくれると大変嬉しいです✨

Buy me a coffee