HN 日本語サマリー

← 一覧へ戻る
プログラミング

パーシング・エクスプレッション・グラマー対正規表現:LispでのOrgパーサー構築、HTMLエクスポート

Parsing Expression Grammar vs. Regexes: Building Org Parser in Lisp, Export HTML (jointhefreeworld.org)

126 pointsby jjba2320 コメント

要約

この記事では、Guile SchemeのPEG(パーシング・エクスプレッション・グラマー)ライブラリである(ice-9 peg)を使用してOrgモードのパーサーを構築し、それをSXML経由でHTMLに変換するプロジェクト「OrgWebAlchemy」について解説しています。複雑なOrgモードの構文解析に正規表現の限界を感じた著者が、PEGの利点と実装の詳細、そしてHTML出力のカスタマイズ性について説明しています。

全文翻訳

皆さん、こんにちは。この記事では、Guile SchemeのPEG(パーシング・エクスプレッション・グラマー)である(ice-9 peg)を使ってOrgモードを解析し、SXML経由でHTMLに変換する「OrgWebAlchemy」というプロジェクトについて、冒険のような旅にご案内したいと思います。これは私がしばらく取り組んできたものです。最初はOrgモードのコンテンツを解析するために単純な正規表現を使っていましたが、すぐに目標を達成するにはそれ以上のスマートなものが必要だと気づきました。GNUの素晴らしい(ice-9 peg)モジュールとチュートリアルのおかげで、PEGが何ができるのかについて、ようやく知識を深めることができました。Lisp、Scheme、パーシング、Orgモード、あるいは私がメタ・メタ・メタ・プログラミングと呼ぶ一般的なアイデアを楽しむ人たちにとって、興味深いかもしれないと思いました。AIがPEGの理解やデバッグに役立ったことは認めますが、OrgWebAlchemyの開発は「私自身のスパゲッティ」であり、単体テストや手動検証(そしてASTの多くの美しいプリティプリント)が、かなり良い実装へと私を導いてくれました(自分で言うのもなんですが)。 プロジェクトのソースコードはCodebergにあります:https://codeberg.org/jjba23/orgwebalchemy OrgWebAlchemyは、OrgモードのドキュメントをAST(抽象構文木)に解析し、それをHTMLとしてレンダリングするためのGuile Schemeライブラリです。私の主なユースケースは、Emacsを必要とせずにOrgをHTMLにエクスポートすること、そしてこの機能をいくつかのプロジェクトに統合して、Orgモードで記述したものをきれいにレンダリングできるようにすることです。 基本的なアイデアは非常にシンプルです: (use-modules (orgwebalchemy html)) (org->html "This is ~test~ code.") は、以下のようなものになります: This is <code>test</code> code. しかし、興味深いのはその間のプロセスです。 Orgドキュメント v パーシング・エクスプレッション・グラマー v AST v SXML -> HTML OrgWebAlchemyがLucidPlanプロジェクトでOrgモードをHTMLにきれいにレンダリングできるようにする方法を示す例をこちらでご覧ください。 その他のリソース PEGに関するWikipediaページをご覧ください:https://en.wikipedia.org/wiki/Parsing_expression_grammar GNU/Emacs Lisp向けのチュートリアルをご覧ください(学習ソースとしても良いです):https://www.gnu.org/software/emacs/manual/html_node/elisp/Parsing-Expression-Grammars.html GNU/Guile向けのチュートリアルをご覧ください:https://doc.guix.gnu.org/guile/3.0.9/en/html_node/PEG-Parsing.html PEG対大量の正規表現? Orgモードは、実際に解析しようとするまではシンプルに見えます。見出しは簡単です。段落も簡単です。リストも簡単です(いや、待てよ、これは私を汗だくにした)。そして突然、以下のようなものが出てきます: ネストされたリスト 順序付き、順序なし、説明リスト 異なるインデントレベル インラインマークアップ 説明を含むリンク ソースブロック 例ブロック 引用ブロック テーブル 入力の消費を正確な場所で停止しなければならないエスケープ構文 この時点で、別の正規表現を追加するという通常のやり方は…冒険的になり始めます。:-) 以下のようなものになります: これをマッチさせるが、それが続かない場合は除く、このブロックの中では除く、それが説明でない限りは除く、ただし改行は消費しない、ただし前の行がリスト項目でなかった場合は除く… それはもはや言語を記述しているのではなく、パーサーのバグの歴史を記述しているようなものです。 そこでOrgWebAlchemyは、Guileの優れた(ice-9 peg)モジュールを通じて、PEG(パーシング・エクスプレッション・グラマー)を使用します。 例: (define-peg-pattern element body (or empty-line heading separator table src-block quote-block example-block export-html-block description-list unordered-list ordered-list paragraph)) これは非常に良いです。なぜなら、グラマー自体が言語のドキュメントのように見え始めるからです。そしてGuileでは、PEGをS式として直接表現できるため(あるいは、気に入らない場合はより伝統的な構文を使用することもできます)、私のLisp愛好家としての心を非常に喜ばせてくれます。 このアプローチで特に気に入っている点は、疎結合であり、SXMLを生成し、その後HTMLをレンダリングするという詳細が「プレゼンテーションの懸念事項」であることです。これにより、後でMarkdownや他の形式にエクスポートする可能性が開かれます。 例えば: - name :: Josep - project :: orgwebalchemy - language :: Scheme は、以下のようなASTになります: (description-list (unordered-item (desc-key "name") (line-content "Josep")) ...) 私はまだ正確な表現と、すべてを正しく行うことに忙しくしています。しかし、現時点ではv1.0はある程度の安定性を持っています:-) ここにいる賢明な人々からのプロジェクトへのフィードバックをぜひ聞きたいです。もちろん、Orgモードは(素晴らしい)巨大なソフトウェアの一部なので、すべての機能をサポートするには程遠いですが、いくつかのコアで重要な構造は含まれています: 見出し(n * で始まる行) 段落(「非特殊」なテキスト) ネストレベルを問わない順序なし、順序付き、説明リスト イタリック、ボールド、インラインコード 説明の有無にかかわらずリンク(ネストされた解析あり) 水平区切り線(---–—)5つ以上のダッシュ テーブル #+begin_src #+begin_example #+begin_quote (ネストされた解析あり) #+begin_export html : Org構文はPEGグラマーによって解析されますが、生のHTMLエクスポートブロックはOrgインラインパーサーをバイパスし、信頼できるリテラル出力として発行されます。 ヤヤヤ、再帰的なリスト Orgのネストされたリストを正しく扱うことは、楽しい部分の一つでした。以下のようなもの: - Item 1 - Item 1.1 - Item 1.2 - Item 2 は、 quasi-tree になるはずです。 パーサーは最初はリスト項目のフラットなシーケンスを生成し、AST処理フェーズがインデントをネスト構造に変換します。 HTMLレンダラーは、自然に以下のようなものを生成できます: <ul> <li> Item 1 <ul> <li>Item 1.1</li> <li>Item 1.2</li> </ul> </li> <li>Item 2</li> </ul> </li> </ul> まだ小さな問題が一つあります。それは、ネストされた方法で異なるリストタイプが混在することです。これは微妙なバグで修正されることを願っています。 HTML側はSXMLを使用しています。なぜなら、すでにLispを書いているなら、HTMLもLispデータとして表現する方が良いからです。:-) それは本当に大いに役立ち、マークアップツリーの構築をはるかに楽にします。 出力HTMLの完全なカスタマイズを可能にするように注意しました(Guileパラメータ経由)。これにより、レンダラーが特定のウェブサイトのHTMLの見た目に関する考え方にハードコードされることがなくなります。ほとんどは単純なクラスのリストですが、見出しレベルごとのカスタマイズはもう少し柔軟です: (heading-classes (lambda (level) (case level ((1) '("text-4xl" "font-bold")) ((2) '("text-2xl" "font-semibold")) (else '("text-base"))))) なぜこれを作っているのか? 一部は私がそれを望んでいたからです。挑戦が好きで、パーシング、ASTなどを扱うのは非常に楽しいです…この仕事をするためにEmacsを使ってもよかったのですが、Orgの実装ほど優れたものはありません。しかし、このプロジェクトの進め方から、他の人々が拡張やカスタマイズ(おそらくより多くのレンダラーやOrg機能を追加する)できる、小さく、ハック可能で、フリーなOrgパーサーを持つというアイデアが気に入っています。 フリーソフトウェア OrgWebAlchemyは、GNU LGPL v3以降でライセンスされています。このプロジェクトは、まもなくGNU Guixにguile-orgwebalchemyとしてパッケージ化される予定です。リポジトリにはテストスイートもあり、パーサーができることの安全網とショーケースとしてすでに役立っています。 締めくくりの考え 皆さんの考え、特にグラマー、ASTデザイン、パーサーアーキテクチャ、あるいは私がまだ扱っていない興味深いOrg構造について、ぜひ聞かせていただけると嬉しいです。ハッピーハッキング!✨