XMLをつぶす機械を作る機械を作る
昨日は、ドキュメントの構造をプログラムのように実行できるというアイデアの話をしました。
具体的には、「ドキュメントの構造をS式で表現し(SXML)、そのタグをLispの関数と見立て、それを要素に関数適用する」というアプローチです。
たとえば、XMLで表したときに段落を意味する<para>
のようなタグに対する変換処理は、こんな感じのLispの関数として定義できます。
(define (para arg) (print arg "\n\n"))
今日は、これをもうちょっと真面目に定義する部分と、これを評価する部分、それに実用的に使うためのフレームワークについて書きます。 以降、Lispの処理系としては、GaucheというSchemeの実装を使います。
ドキュメントをS式で書くの?
まず、そもそもドキュメントを書くときにS式で書くのか、という点に答えておきます。 べつにS式で書きたければ書いてもいいんですが、実際にはXMLをSXMLに変換して使うほうが便利でしょう。
というのは、いま本当にやりたいのは、「何らかの方法で手に入れた、スキーマが明らかとは限らないXMLなドキュメントを、LaTeXなどの他のマークアップへと変換する」ことです。 SXMLでドキュメントを書いていくためのエコシステムが欲しいわけではありません。
いまは「記法」の話をしていないことを思い出してください。 プログラミング言語でいえば、抽象構文木を評価して最終的な実行可能ファイルを得ることを考えています。 記法と構造の関係については、いまのところ別のシリーズで言及するつもりです。
なので、XMLをSXMLに変換して使います。 そのための方法は、いわゆるSAXパーサのSXML版の標準的な実装を使います。
- S-exp-based XML parsing/query/conversion http://ssax.sourceforge.net/
これはGaucheにも ssax:xml->sxml
という関数で組み込まれています。
- Gaucheリファレンスマニュアル:11.29 sxml.ssax - 関数的なXMLパーザ http://quruli.ivory.ne.jp/document/gauche_0.9/gauche-refj_146.html
これをありがたく使うだけで、基本的には(整形式の)XMLからSXMLが得られます。
「XMLのタグ」を関数適用できるようにする
いくらLisp族の言語といえども、リテラルなS式を自由自在に評価させてくれるわけではありません。 そういうLispの実装を自分で作るか、そのためのバックドアがある実装を使う必要があります。
幸い、GaucheというSchemeの実装には、型を指定して好きなモノを適用可能にできるobject-apply
という汎関数があります。
- Gaucheリファレンスマニュアル:6.18.6 適用可能なオブジェクト
https://practical-scheme.net/gauche/man/gauche-refj/Shou-Sok-kitoJi-Sok-.html#g_t_9069_7528_53ef_80fd_306a_30aa_30d6_30b8_30a7_30af_30c8
このobject-apply
を使って、こんなふうにすると、 (<XMLのタグ> <XMLの要素>)
という形をしたリストを評価できるようになります。
(define-method object-apply ((tag <symbol>) (body <list>)) ((global-variable-ref 'user tag) body))
あとは、個々のタグをどういうふうに変換したいか、関数として定義していくだけです。
たとえば para
タグをLaTeXに変換する処理であれば、まず改行を出力して、それから中の子要素を再帰的に処理して、最後に改行を2つ出力したいので、こんな感じに定義します。
(define (para body) (begin (print "\n") (map (lambda (b) (cond ((bがリストの場合) ((car b) (cdr b))) ((bが文字列の場合) (print b)) (else '()))) body) (print "\n\n")))
b
タグだったらこんな感じ。
(define (b body) (begin (print "\\textbf{") (map (lambda (b) (cond ((bがリストの場合) ((car b) (cdr b))) ((bが文字列の場合) (print b)) (else '()))) body) (print "}")))
「「XMLのタグ」を関数として定義する」を抽象化する
こんなノリでLaTeXへのコンバーターを何回か作ってみると気づきますが、やることは毎回だいたい同じです。
要するに、最初にやること、間にやること、最後にやること、の3つをタグごとに決めるだけです。 実際、ほとんどの変換処理は、「この3つをルールとしてタグに与える」で書けます。
いま、「この3つをルールとして定義する」と「それをタグに関連付ける」を、それぞれdefine-rule
およびdefine-tag
として用意できたとしましょう。
これらを使うことで、たとえば「段落タグpara
用の処理ルール」をこんな感じに書こうという算段です。
(define-tag para (define-rule "\n" エスケープ処理用の関数 "\n\n")
「太字タグb
用の処理ルール」であればこんな感じ。
(define-tag para (define-rule "\\textbf{" エスケープ処理用の関数 "}")
ちなみに、define-rule
およびdefine-tag
を分離しているのは、ルールにはタグ間で共通するものも多いからです。
たとえば、b
タグ用のルールをemph
タグでも共通して使う、みたいなことをしたいと思ったら、ルール部分だけlatex-bf-cmd
とかの名前で定義しておいて、タグだけ関連づけれることが考えられます。
(define-tag b latex-bf-cmd) (define-tag emph latex-bf-cmd)
このような抽象化は、define-rule
およびdefine-tag
をLispのマクロとして定義することで簡単に実装できます。
さらに、Lispマクロのパワーで、「タグの名前からルールを自動生成する」とか「XMLの属性をルールの中で定数として扱えるようにする」といったこともできます。 特に後者はOnLispに出てくるアナフォリックマクロの応用で、それ自体がだいぶ面白いと思うんですが、Lispの話は無限にできてしまう割に読者をさらに選ぶので、このへんでやめておきます。
車輪は自分のために再発明する
ここまでに説明した仕掛けは、xml2tex
という名前で公開しています。
- k16shikano/xml2tex
https://github.com/k16shikano/xml2tex
詳しくは、TeX界のTUGboatというジャーナルに投稿した記事もあるので、気になる人は読んでみて。
- xml2tex: An easy way to define XML-to-LATEX converters
https://tug.org/TUGboat/tb35-2/tb110shikano.pdf
xml2tex
という名前を見ると、「XMLからTeXへの変換器」を想像するかもしれません。
実際、最初はTeXへの変換器を作るバックエンドとして開発したのですが、作っているうちに、xml2tex
の実際の機能は「「XMLをシリアライズして他のマークアップ原稿へと変換するコンバーター」を作るフレームワーク」であることに気付きました。
過去には、FrameMakerというアプリケーションのSGML(XMLの親分みたいなやつ)からRe:VIEW原稿を作ったりするのにも使っています。
ところで、SXMLとXMLはシンタックスが違うだけなので、ふつうのXMLでも同様のことが実現できないはずがないんですよね。 どういうことかというと、山かっこのXMLに対する変換処理を山かっこのシンタックスで書き、山かっこのXMLを実行するようなプログラミング言語が作れるだろう、ということです。
で、昨日の記事を書いていて初めて気が付いたんですが、XSLTという関数型言語がまさにそれに相当するような気がしました。 ドキュメント全体をS式として表現したことで「ドキュメントの構造をそのまま実行する」という策が見えてきた気がしていたけれど、その先にあったのはやはり車輪の再発明でしかなかったというわけです。
でも、車輪の再発明、常套だと思っています。 XSLTは、XMLの世界観では完成度が高い仕組みだけれど、やはりふつうのプログラミング言語と同じ感覚では書きにくいし、処理系の機能も限られます。 そこいくとLispはやはり自由度が高い。 自分の直観にあう方法で変換器をぽんぽん生成できる仕組みを作ったことは個人的にはとても気に入っています。
明日は、このxml2tex
を使うことで、DocBookの原書データを活用した翻訳版の本をLaTeX経由で作っている話をします。