golden-luckyの日記

ツイッターより長くなるやつ

PandocをreSTのリストテーブルに対応させる

Python界隈でよく見かける構造化文書のための記法として、reStructuredText(以降はreSTと書きます)があります。

軽量マークアップ言語などと呼ばれることもありますが、reSTはかなり高度な表現力がある記法です。 その記法をパースするために標準で使われているのはDocutilsという仕組みです。ただ、DocutilsはreST専用ではなく、他の記法のパーサを実装することもできるらしいです。 その意味でDocutilsは、Pandocと同じく、内部の抽象的なデータ構造へと記法を押し込めるツールだといえる気がします。

Docutilsについては『マスタリングDocutils』に詳しいので興味がある方は購入しましょう。

今日はDocutilsのことは忘れて、reSTの記法をPandocで読み込み、Pandocの抽象データ型(Pandoc構造)に押し込める話をします。

reSTのリストテーブル

reSTにはリストテーブル(List Table)という記法があります。 正確に言うとこれはreST本来の記法ではなく、reSTに備わっているディレクティブという仕組みで定義された拡張です。 ちなみにディレクティブとは、思いっきり単純にいうと、「ドキュメントの構造を作り出す局所ローカルな記法」を定義するためにreSTに用意されている仕掛けです。

リストテーブルは、表(テーブル)を2階層の箇条書きで書けるという便利な記法です。 上記の公式ドキュメントにはこんな例が載っています。

.. list-table:: Frozen Delights!
   :widths: 15 10 30
   :header-rows: 1

   * - Treat
     - Quantity
     - Description
   * - Albatross
     - 2.99
     - On a stick!
   * - Crunchy Frog
     - 1.49
     - If we took the bones out, it wouldn't be
       crunchy, now would it?
   * - Gannet Ripple
     - 1.99
     - On a stick!

上記のリストテーブルは、こんな感じのシンプルなテーブルとして最終的にレンダリングされることが想定されています。

Frozen Delights!
Treat Quantity Description
Albatross 2.99 On a stick!
Crunchy Frog 1.49 If we took the bones out, it wouldn't be crunchy, now would it?
Gannet Ripple 1.99 On a stick!

PandocのreST Readerでリストテーブルは読めるか

さて、PandocにはreSTのReaderもあります。 わりといろいろなディレクティブにも対応しているのですが、リストテーブルについては2017年5月ごろまで長らく未対応でした。 試しに当時のpandocコマンド(バージョン1.17.2がたまたま手元で利用可能でした)で上記の例を読み込んでみると、こんな感じに無視されてしまいます。 (pandocでは、-t nativeとすることで、Haskellの代数的データ型(をshowしたもの)の生の姿を見られます)

$ pandoc -f rst -t native temp.rst
pandoc: ignoring unknown directive: list-table "source" (line 19, column 1)
[]

一方、最近のpandocコマンドでは、こんなふうにPandoc構造として読み取ってくれます。

$ pandoc -f rst -t native temp.rst
[Table [Str "Frozen",Space,Str "Delights!"] [AlignDefault,AlignDefault,AlignDefault] [0.0,0.0,0.0]
 [[Plain [Str "Treat"]]
 ,[Plain [Str "Quantity"]]
 ,[Plain [Str "Description"]]]
 [[[Plain [Str "Albatross"]]
  ,[Plain [Str "2.99"]]
  ,[Plain [Str "On",Space,Str "a",Space,Str "stick!"]]]
 ,[[Plain [Str "Crunchy",Space,Str "Frog"]]
  ,[Plain [Str "1.49"]]
  ,[Plain [Str "If",Space,Str "we",Space,Str "took",Space,Str "the",Space,Str "bones",Space,Str "out,",Space,Str "it",Space,Str "wouldn't",Space,Str "be",SoftBreak,Str "crunchy,",Space,Str "now",Space,Str "would",Space,Str "it?"]]]
 ,[[Plain [Str "Gannet",Space,Str "Ripple"]]
  ,[Plain [Str "1.99"]]
  ,[Plain [Str "On",Space,Str "a",Space,Str "stick!"]]]]]

しかし、これには実は制限があります。 リストテーブルでは、テーブルのヘッダとなる行の数を:header-rows:で指定できるのですが、これを2にしても、常に最初の1行だけがヘッダになります。 たとえば次のようなリストテーブルをPandocでMarkdownに変換してみると…、

.. list-table:: 
   :widths: 15 10 30
   :header-rows: 2

   * - Treat
     - Quantity
     - Description
   * - Albatross
     - 2.99
     - On a stick!

こうにしかなりません。

$ pandoc -f rst -t markdown temp.rst
  Treat           Quantity   Description
  --------------- ---------- -----------------------------------------------------
  Albatross       2.99       On a stick!

Markdownにヘッダ行が複数のテーブルがないからだろ、と思うかもしれませんが、HTMLでも同じです。

$ pandoc -f rst -t html temp.rst
<table>
<thead>
<tr class="header">
<th>Treat</th>
<th>Quantity</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Albatross</td>
<td>2.99</td>
<td>On a stick!</td>
</tr>
</tbody>
</table>

この連載をずっと読んでもらっていればわかると思いますが、これは「複数行ヘッダのテーブル」を表す型がPandoc構造にないからです。 どんなにがんばってReaderを実装しても、このリストテーブルをreSTの意図通りに読むこと、つまりDocutilsと同じように読むことはできないのです。

宣伝

最後に宣伝ですが、このPandocのreSTリストテーブル対応、ぼくがやりました。 何もコメントせずにいきなりPRを出したらjgmから瞬時に怒られが発生して青くなったのは懐かしい思い出です(最初はPRを出すつもりなかったけど操作ミスでこうなった)。

そもそもなんで実装したかといったら、『Goならわかるシステムプログラミング』の原稿がreSTで、アスキー.jp連載時はこれをPandocでHTMLにしてアスキーさんに入稿してたんですが、そのときにPandocがリストテーブル未対応で困ったからなのでした(なおSphinxを使わなかったのは、入稿仕様のHTMLにするPandocのテンプレートをすでに作っていて、それを使いまわしたかったからです)。

こんな感じに編集ツールを作るようなお仕事もできるので、そうしたお仕事があったらご連絡ください。

Pandocの抽象データ型

Pandocのいいところは、構造をさまざまな記法から暗黙に読み取ってくれる点です。 ただし、その構造はPandocの内部で定義された抽象データ型であり、利用者の目的に合わせて増改築することはできません。 XMLLaTeXでやるフルフルの構造化文書に比べると良い意味でも悪い意味でも制限があるので、昨日の記事ではPandocによる構造化文書の扱いを「ライトウェイト」と呼びました。

今日は、この「抽象データ型によるライトウェイト構造化文書」について、Pandocの抽象データ型を例にもうちょっと具体的に説明します。

Pandocの抽象データ型

なにはともあれ、Pandocの抽象データ型がどんなかを見てみましょう。

Pandocにおける文書とは、「ブロック要素」と「インライン要素」からなるデータです。 この概念の具体的な姿は、Haskellの代数的データ型という仕組みを使って、pandoc-typesというHaskellのパッケージで定義されています。

hackage.haskell.org

このパッケージで定義されているPandocの「型」を簡単に紹介します。 まず、これがPandocのインライン要素を表す代数的データ型です。

data Inline
    = Str Text              -- 単純な文字列
    | Emph [Inline]         -- 強調
    | Strong [Inline]       -- もっと強調
    | Strikeout [Inline]    -- 打ち消しの文字列
    | Superscript [Inline]  -- 上付き文字列
    | Subscript [Inline]    -- 下付き文字列
    | SmallCaps [Inline]    -- スモールキャプスの文字列
    | Quoted QuoteType [Inline] -- 引用の文字列
    | Cite [Citation]  [Inline] -- 文献参照
    | Code Attr Text        -- 等幅の文字列
    | Space                 -- 空白
    | SoftBreak             -- ソフト改行
    | LineBreak             -- ハード改行
    | Math MathType Text    -- TeX記法の数式
    | RawInline Format Text -- HTMLやLaTeXの生データ
    | Link Attr [Inline] Target  -- ハイパーリンク
    | Image Attr [Inline] Target -- 画像
    | Note [Block]          -- 脚注や後注
    | Span Attr [Inline]    -- 汎用のインライン要素

Pandocにおけるもっとも基本的な型のひとつは、素の文字列Textに対してStrという名前を付けたStr Textであり、これがインライン要素を表す型Inlineの代表選手です。 なお、Strのような名前のことを、Haskellでは「構成子(値構成子)」と呼びます。

複数のインライン要素Inlineを集めたものは[Inline]という型で表します。 上記の定義には、「[Inline]Emphなどの構成子を付けたインライン要素」もいくつかあることがわかります。

さらに、いくつかの構成子にはAttrという型の引数もあります。 これは、HTMLとかXMLにおける「属性」に相当する型です。 StrAttrを取らない構成子なので、素の文字列には属性が付けられません。 こうした要素に属性を付けたい場合や、いくつかのインライン要素を束ねて特に意味を持たないインライン要素にしたい場合には、Spanを使います。 これはHTMLの<span>に相当します(余談ですが古代のPandocにはSpanがなくてわりと苦労しました)。

次は、Pandocのブロック要素を表す代数的データ型です。

data Block
    = Plain [Inline]        -- 段落ではないブロック
    | Para [Inline]         -- 段落
    | LineBlock [[Inline]]  -- 複数行からなるブロック
    | CodeBlock Attr Text   -- コードブロック
    | RawBlock Format Text  -- HTMLやLaTeXの生データ
    | BlockQuote [Block]    -- 引用のブロック
    | OrderedList ListAttributes [[Block]]  -- 順序付き箇条書き
    | BulletList [[Block]]                  -- 箇条書き
    | DefinitionList [([Inline],[[Block]])] -- 見出し付き箇条書き
    | Header Int Attr [Inline] -- 見出し
    | HorizontalRule           -- 水平線
    | Table [Inline] [Alignment] [Double] [TableCell] [[TableCell]]  -- 表
    | Div Attr [Block]       -- 汎用のブロック要素
    | Null                   -- 何も含まないブロック

多くのブロック要素は、「インライン要素の集まり[Inline]に構成子を付けたもの」として定義されているのが見て取れると思います。 箇条書き(OrderdListとかBulletListとか)のように、他のブロック要素の集まり[Block]によって構成されているブロック要素もありますね。

以降では、このBlockInlineで定義されたPandocの抽象データ型のことを、「Pandoc構造」と呼びます。

Pandoc構造に当てはまらないものはどうなるか

原稿をPandocで読み込むと、その原稿に含まれている内容は、元の原稿の記法が何であれ、すべてPandoc構造として保持されます。

しかし、たとえば元の原稿がDocBookであったとして、DocBookのスキーマには、このPandocの代数的データ型では表現できないような構造もたくさんあります。 たとえば、初出の用語を表す目的で用意されている構造としてDocBookには<firstterm>というものがありますが、これに相当する構成子はPandocのInline型にはありません。 組版された表示結果で何らかの強調をするといった効果を期待したいだけならEmphマッピングできそうな気もしますが、元の構造は索引項目の抽象などにも使えるように意図されているので、どうやっても別ものにしかならないでしょう。

また、<sidebar>というブロック要素もPandoc構造には対応するものがありません。 これは、いわゆる「コラム」のように、本文とは切り離された余談などを記述するために使うXML要素です。

では、DocBookの原稿に出てくる<firstterm><sidebar>で囲まれた部分は、Pandocではどのように扱われるのでしょうか? 残念ながら、Pandocでは、そもそもこれらのタグを認識してくれません。 単純に要素が無視されるわけではなく、知らないタグがスルーされます。 つまり、その中に出てくる文字列や他のXML要素は読み取って、それをできるだけPandoc構造にマッピングしようと試みます。

なお、上記では未知のタグはスルーされると言いましたが、これはDocbookを読むときの話で、他の記法を読むときにもスルーされるとは限りません。 Pandocでは入力の記法をパースして代数的データ型として読み込む仕組みのことを「Reader」と呼んでおり、さまざまな記法ごとにReaderが用意されています。 そして、そのReaderの実装によって、記法からPandoc構造への対応も異なるからです。 DocBookのReaderの方針がたまたま「知らないタグはスルー」なのであって、他のReaderがどういう方針で実装されているかはまちまちです。 たとえばreSTのReaderは未知のディレクティブを無視します(記法で定まる構造を無視することと、記法をスルーすることは、違う)。

Readerの方針を知るには、実装を見るしかありません。 DocBookのReaderの実装は以下で参照できます。

DocBookの場合には、このReaderの冒頭に、対応するタグと対応しないタグを示したチェックシートが並んでいます。 それを見るとわかるように、ほとんどのタグは未対応のままです。 たぶん今後も完全に対応することはないでしょう。

PandocではDocBookに対応しているとされていますが、このReaderの実装を見ると、処理結果が元のDocBookの原稿で意図されていたものになるとは限らない(むしろならないことのほうが多い)と考えるほうがよさそうだとわかると思います。

構造化文書から記法を分離しないとPandocの話がしにくい

DocBookは、OASISという団体が管理しているガチガチの構造化文書の仕組みです。

昨日の話で構造化文書から記法を分離し、「Pandocは記法の変換」という話をしたのは、今日の話のような状況、つまりPandocが何らかの構造化文書に対応しているといってもそれは標準化されたフルフルの規格に対応しているという意味ではなく、あくまでも「記法からPandoc構造が得られるよ」、もしくは「Pandoc構造から記法が得られるよ」という意味であり、ライトウェイトに構造化文書をやる仕組みであることを強調したかったからです(余談ですが、プログラミング言語の型がライトウェイトな形式手法と呼ばれていることを意識してライトウェイトと言っています)。

ここで、昨日の最後の図を再掲します。 この図は、それぞれの構造化文書の標準的な利用方法を横方向の流れで示したとして、Pandocにおける「対応」はあくまでも「Pandoc構造という抽象データ型に照らして記法を読み書きできる」という意味だ、というふう読んでほしかったものです。

f:id:golden-lucky:20191214182222p:plain

ところで、いままでWriterの話をしてませんが、実はWriterの実装について気にすることはあまりありません。 というのは、Pandocにはフィルターという仕組みがあり、これによって「いったんPandoc構造になったものを何らかの記法で出力する」際にはカスタマイズが可能だからです。 もちろん、Pandoc構造として読み取られていない元の原稿の記法にあった情報を復元することはできませんが、読み取られた範囲で好きなように出力を組み立てることは容易です。

逆に、ある記法の原稿が「Pandoc構造として読み取られる方法」を変えたい場合には、Readerを改造して対処するしかないとも言えます。 明日はそのような事例を紹介します。

ライトウェイト構造化文書

このアドベントカレンダーでは、先週まで、主にページメディアにおける「PDF」と「XML」の話をしてきました。 この2つ、それぞれ「Webブラウザでのレンダリング」と「HTML」に言い換えると、ウェブメディアの世界観と似ている気がしてこないでしょうか。

実際のところ、ウェブメディアとページメディアって互いに遠い存在ではなく、「平面に文字などを配置することを目的とした構造化文書」という視点に立てば、むしろ兄弟みたいなものです。

Webブラウザへの表示がHTMLだけで済むわけないだろ、CSSJavaScriptの役割を知らないのか」という指摘が聞こえてきそうですが、ページメディアにもそれらに相当するものは存在します。 というか、いろいろありすぎて、もはや収拾がついていません。 ページメディアでわりと標準っぽいのは、CSSの役割がXSL-FO、JavaScriptの役割がXSLTという、いわゆるXML組版的な世界観でしょう。

ちょっと話は逸れますが、個人的にこの世界観で面白いなと思うのは、「すべてを同じ構文で済ませよう」とする点です。 XSL-FOでも、XSLTでも、山かっこXML構文が採用されています。 特別なGUIアプリを使わない限り入力も山かっこXML構文なので、おはようからおやすみまですべてがXMLになります。

もっとも、TeXにしても入力から表示制御まですべてを同一の記法で完結させているので、むしろ面白いのは、HTMLとCSSJavaScriptという異なる記法の技術を当然のように組み合わせているウェブの世界観のほうなのかもしれません。 仕組みを考える側からしても、入力も変換も表示制御もすべてを同じ構文で済ませるほうが、周辺ツールなどを統一的に扱えるので理想的でしょう。 とくにXMLだと構文エラーが機械的に判別しやすく、さらにスキーマを適切に扱えば妥当性検証のようなご利益がもれなくついてくるので、そのような傾向が強いのかもしれません。

ただ、ぼくには、XML組版TeXのように単一の記法ですべてを完結させる世界観はちょっと馴染めませんでした。 馴染めなかったというよりは、そこまでの腕力がなかっただけともいえます。 そこでウェブメディアにおいてCSSが担っている役割はLaTeXJavaScriptが担っている役割はSchemeにして、商品としての本を作れていますよというのが、先週の話です。 「ふつうの書籍くらいの規模であればXML組版でなくても構造化文書からの自動組版は十分に可能である」という話だったと思ってもらってもかまいません。 (もちろんこれはXML組版の否定ではなく、たとえばDITAの導入が必要といった具体的な目的意識がある状況であれば、この連載のようなXML組版に対する態度が役に立たないのは言うまでもありません。)

意味の構造、見た目の構造、表示結果

前置きが長くなりましたが、「平面に文字などを配置することを目的とした構造化文書」から実際にコンテンツを平面に配置するまでの流れ的なものを整理しておきます。 いろんなモデル化が考えられると思いますが、ここではまず「意味の木構造」、「見た目の構造」、「表示結果」の3段階で考えます。 これはWebブラウザのモデルに沿ったモデルで、「意味の木構造」は入力されたHTMLもしくはJavaScriptでいじれるDOMツリー、「見た目の構造」はそれにCSSを適用したレンダーツリー、「表示結果」はWebブラウザが表示するレンダリング結果に相当します。 このブログ的には、「意味の木構造」はTeX記法やXML記法の原稿、「見た目の構造」はPDFのドキュメント構造、「表示結果」はPDFビューワーが表示するレンダリング結果です。

f:id:golden-lucky:20191214181923p:plain
3段階モデル

XML組版は、これの最初の2つの段階にかかわる要素技術をすべてXMLでやろうというアプローチだといえます。

f:id:golden-lucky:20191214181948p:plain
XML組版の世界観

LaTeX組版の場合、そこまでとんがった使い方をしたがる人は実際には少なく、見た目の構造にはDVIドライバの存在もあったりしてTeX構文でない世界になりますが、このモデルに当てはめるとだいたいXML組版と同じようなノリにはなります。

f:id:golden-lucky:20191214182012p:plain
LaTeXの世界観

構造は記法とは別

今日の本題はここからです。 先ほどの3段階モデルには、実際にはその前に「記法」があると考えるのが自然でしょう。

f:id:golden-lucky:20191214182031p:plain
記法を分離

XML組版LaTeX組版だと記法がXMLTeXそのものなので意識されにくいんですが、人間が読み書きする記法を「意味の木構造」の生の姿と同一視する必要はないので、ここは区別して考えられるはずです。

f:id:golden-lucky:20191214182103p:plain
XMLLaTeXだと記法と意味の木構造が同一視されがち

ここの同一視を明確に区別して考えると、いろいろ説明がしやすくなります。 たとえば、Re:VIEW(処理系)やSphinxは、Re:VIEW(記法)やreStructuredTextを「記法」としてそれぞれが内部で利用する「意味の木構造」を作り、RubyPythonで調整しつつLaTeXやHTML/CSSによる「見た目の構造」へと変換し、それらの仕組みによって表示結果を得る仕組み、と説明できます。 CSS組版なんかも同じように説明できるでしょう。

f:id:golden-lucky:20191214182142p:plain
ひろがる可能性

さらに、記法を意味の木構造と区別すれば、ある記法から非標準的な方法で表示結果を得ることもできます。 実際、当社の『プロフェッショナルSSL/TLS』の制作フローでは、記法としては原書と同じXMLを使っていながら、それを標準的なXML組版の流れでなく、LaTeXによる「見た目の構造」へとSchemeで変換することで最終的な表示結果を得ています。

Pandocの構造は構造化文書の構造ではない

「記法」と「構造」、とくにこのモデルでいう「意味の木構造」を明確に区別しようという話は、先週の記事でもときどき触れていました。 なんでこれをそんなに強調しているかというと、この区別をつけないと「構造化文書」の話が「記法」の好き嫌いの話になってしまいやすいからです。 XML組版TeXは「記法」と「構造」に見た目上の区別があんまりないので、「記法」の好き嫌いで選んでしまうと、他の部分の特性を見失います。 また、部分の特性ばかりを強調すると、「記法」がつらくても我慢して使えみたいな話になり、これはこれであまり健全ではないと思います。

さらに、「記法」と「構造」を混ぜて話すと、HTMLベースの「意味の木構造」に対する「記法」として生まれながら後で「記法」の部分だけが独り立ちしたMarkdownの可能性が見えにくいでしょう。

もうひとつ、この混同を放置したままだと可能性をうまく生かせなくなる技術があると思っていて、それがPandocです。 Pandocの価値の中核は「記法」レベルでの変換であり、変換対象の「記法」がもともと想定している「意味の木構造」より後ろの工程は、Pandocにとってはおまけでしかないからです。

f:id:golden-lucky:20191214182222p:plain
Pandocの核は記法の変換と抽象データ型の提供

各「記法」が想定している「意味の木構造」の代わりに、Pandocでは独自の抽象データ型を利用します。 その抽象データ型から、さまざまな「意味の木構造」のサブセットが作り出せるようになっています。 そして、この抽象データ型を暗黙に構成するための「記法」として、PandocのMarkdown記法があります。

ちなみに上の図にも示しましたが、このPandocと同じ世界観の仕組みとして、Sphinxがバックエンドで利用しているDocutilsがあります (Docutilsをこのようなモデルに合わせて使ったことがないのですが、『マスタリングDocutils』を読んだ限りでは、たぶんそう)。

もうひとつちなみに、このような「記法から何らかの抽象データ型を暗に読み取り、そこから後段の処理系が自由になるような意味の木構造を生成し、そっちの処理系で表示結果を得る」という戦略は、謎の記法の原稿に対峙するときの基本技でもあります。 そのための武器としてPandocやDocutilsを使ってもいいし、自分でパーサを書いてもいいでしょう。

そんなわけで、構造化文書だからといってXMLLaTeXのような「意味の木構造」のための従来のフレームに縛られる必要はなく、「記法とそれに対する抽象データ型」でライトウェイトに構造化文書をやっていくのがトレンドになるといいなと考えています。

GitHubで「コメントの一覧」を取得したい

近年、出版社でも原稿管理にGitの導入が進んでおり[要出典]GitHubのようなWebサービスへの需要が高まっている[要出典]。 これに伴い、WebブラウザGitHub上の原稿に対する特定のコミットを開き、そこに行コメントを残すといった利用も増えている[要出典]。 以下に例を挙げる。

f:id:golden-lucky:20191213225444p:plain

この「特定のコミットに対して行コメントを残す」機能は、ワープロソフトの編集履歴ツールと同じ感覚で原稿に局所的なツッコミを入れられるという点で大変に使い勝手が良い。 しかし難点が一つあって、GitHubのWebページではこのコメントを一覧で表示できない。 そのため、コメントに気付かずスルーしてしまうという、文書の編集において最悪の事態を招くことがある。

ただ、一覧表示する術がまったくないかというとそういうわけでもなく、GitHubが公開しているREST API v3経由で取得できる。

Go言語であれば、このREST APIを使って、以下の要領でコメントの本文をすべて取得できる。

package main

import (
    "encoding/json"
    "io/ioutil"
    "net/http"
    "fmt"
)

type Comment struct {
    Body string `json:"body"`
}

func main () {

    req, err := http.NewRequest("GET",
        "https://api.github.com/repos/〈アカウント〉/〈リポジトリ〉/comments", nil)

    if err != nil {
        panic(err)
    }
    req.Header.Set("Authorization", "token 〈好きなトークンを指定しよう〉")
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }

    var comments []Comment
    err = json.Unmarshal(body, &comments)
    if err != nil {
        fmt.Println(err.Error())
        return
    }
 
    fmt.Println(comments)
 
    defer resp.Body.Close()
}

おなじみ、http.NewRequestでリクエストを作ればいいのだが、そのヘッダには"Authorization"エントリでGitHubから取得したトークンを指定する必要がある。 "Content-Type""application/json"を指定しているのは、GitHub API v3のマニュアルに従ったものである。

そうやって作ったHTTPリクエストをhttp.DefaultClient.Doで発行し、ioutil.ReadAllでレスポンスを読み込んで、それをjson.Unmarshalする。 ここでは事前に定義したComment型の構造体に、コメントの各エントリを取り込むようにしている。 Comment型の構造体としては、とりあえずコメントの本文を表す"body"エントリだけを残すようにしてみた。

さっそく実行してみよう。〈アカウント〉に当社のGitHubアカウント、〈リポジトリ〉にとあるn月刊ラムダノートの記事のリポジトリを指定し、go buildして実行みると、現時点では2つのコメントが残されているっぽいことが判明した。

$ ./github-comments
[{Common Lispと見出しレベルが揃ってなかったので揃えました。目次に影響ありそう} {ありがとうございます。これどうしようかちょっと迷ってたところでした。}]

もちろん、これだけだと誰のコメントなのかわからないので、もうちょっとComment型を作りこむ必要があるだろう。 また、いつ書き込まれたコメントかわからないのは不便なので、日時くらいは取得するようにしたい。

さらに、せっかくならWebブラウザで関係者が閲覧できるようにして、該当のコメントが残されているコミットへのリンクを貼り、クリックすれば前後の本文の状況を確かめられるようにしたい。 そこで、JSONから取得したコメントを"html/template"モジュールを使ってHTMLのテーブルに流し込むようにし、HerokuかどこかでWebサーバとして動かすようにしよう。

ようするにこういうことである。

package main

import (
    "os"
    "encoding/json"
    "html/template"
    "io/ioutil"
    "net/http"
    "fmt"
)

type Comment struct {
    Body string `json:"body"`
    Date string `json:"created_at"`
    URL string `json:"html_url"`
    Author struct {
        Login string `json:"login"`
    } `json:"user"`   
}

func getComments (w http.ResponseWriter, r *http.Request) {

    req, err := http.NewRequest("GET",
        "https://api.github.com/repos/〈アカウント〉/〈リポジトリ〉/comments", nil)

    if err != nil {
        panic(err)
    }
    req.Header.Set("Authorization", "token 〈好きなトークンを指定しよう〉")
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }

    var comments []Comment
    err = json.Unmarshal(body, &comments)
    if err != nil {
        fmt.Println(err.Error())
        return
    }
 
    t := template.New("template.tpl")
    t, _ = t.ParseFiles("template.tpl")
    err = t.Execute(w, comments)
    if err != nil {
        panic(err)
    }

    defer resp.Body.Close()
}

func main () {
    port := os.Getenv("PORT")
    http.HandleFunc("/", getComments)
    http.ListenAndServe(":"+port, nil)
}

template.tplは適当に用意しよう。 それらをHerokuに挙げてWebブラウザから閲覧するとこんな感じになる。

f:id:golden-lucky:20191213230445p:plain

日時の欄をクリックすると、コメントがあるコミットのページに飛ぶようになっている。 net/httpにあるBasicAuth()を使った簡単な認証をかけて、このページを関係者と共有してもいいだろう。

LaTeXソースを出力するときのエスケープ

昨日までの記事では、XMLの構文で書かれた原稿を他のマークアップにどうやって変換しているかを紹介しました。 こういった変換をするときに一般に悩みの種になるのが、変換先の記法で特殊な意味を持つ文字の扱いです。

たとえばTeXでは、次の10種類の文字は「原稿の入力にそのまま使えない」とされています。 最終的な印字結果にこれらを出力したい場合には、原稿上で何らかの「処置」が必要です。

\ { } $ & # ^ _ % ~

今日は、これらをLaTeXのソースでどうエスケープしたらいいか、という話です。

TeXエスケープ文字は文字のエスケープをする文字ではない

プログラミング言語などで特殊な文字を入力したい場合、一般には「エスケープ文字を前置する」という方法を使います。 エスケープ文字としてお馴染みなのは、バックスラッシュ記号「\」でしょう。 "\n"とか、"\\"とか、"\""といったやつです。

TeXにも「エスケープ文字」があり、それは一般にバックスラッシュとされています。 そのため、上記の特殊な文字にとりあえずバックスラッシュを前置し、意図した文字と違う文字が出て悩んだ人も多いのではないでしょうか?

TeXでバックスラッシュを特殊な文字に前置しても意図した文字が印字されるとは限らないのは、 これがCやJavaScriptなどの汎用のプログラミング言語で「文字」をエスケープするための「エスケープ文字」とはちょっと毛色が違うからです。

とはいえ、通常のTeXでもバックスラッシュは確かに「エスケープ文字」なので、そりゃあ混乱しますよね。

実はTeXのバックスラッシュは、「後続の特殊な文字をその文字そのものにする」という意味でエスケープ文字なのではなく、「後続の文字たちを命令として扱う」ためのエスケープ文字です。 要するに、TeXの字句解析では、エスケープ文字に続く文字を文字としては読み取らず、常に命令の名前を構成する先頭の要素として読みます。 結果、命令の名前がたまたま1文字で、その命令がその文字を表すような印字結果を導くものであれば、いわゆる「エスケープ文字」に期待される動作になります。 そうでなければ、そうなりません。

なので、TeXのソースを吐き出す仕組みでは、わりとみんな苦労して「エスケープ」の問題にあたっているようです。 よく見るのは、「ふつうにバックスラッシュでエスケープできない文字は強引に1文字の名前の命令にする」方法でしょう。 \\^{}のように、波カッコの空ブロックを後置した文字列を変換結果の出力とする、という手法です。 また、TeXの側の仕組みであらかじめ定義された「文字を表す命令」を出力するという手もあります。

たとえばPandocのLaTeXライターは、上記の10個の文字を、それぞれこんな感じに現状ではエスケープしているようです。

case x of
  '{' -> "\\{" ++ rest
  '}' -> "\\}" ++ rest
  '$' | not isUrl -> "\\$" ++ rest
  '%' -> "\\%" ++ rest
  '&' -> "\\&" ++ rest
  '_' | not isUrl -> "\\_" ++ rest
  '#' -> "\\#" ++ rest
  '~' | not isUrl -> "\\textasciitilde{}" ++ rest
  '^' -> "\\^{}" ++ rest
  '\\'| isUrl     -> '/' : rest
      | otherwise -> "\\textbackslash{}" ++ rest

どんな環境で出現するかによって処理が変わっているのでごちゃごちゃしてますが、ノリはわかると思います。 にしても、URLの中だとバックスラッシュを「/」に変換してしまうというのは、やりすぎな気もしますが…。

\symbolを使う

マークアップにおいて特殊な文字を使うのに、エスケープ文字の前置以外の手法が用意されている場合があります。 HTML文字実体参照とか、ASCIIエスケープシーケンスみたいなやつです。 この言い方が正しいか微妙ですが、特殊な文字のための専用の文法がマークアップ側に用意されていると考えてもいいと思います。

で、LaTeXにもそれに近い機能の命令があります。それは\symbol{数字}です。 数字の部分に、そのときのフォントエンコーディングにおける文字を示す数字を指定することで、その文字のグリフが印字される仕組みです。

昨日までの記事で紹介したxml2texでは、この方法でLaTeXマークアップにおける特殊な文字を「エスケープ」しています。 具体的には、TeXのソースにあると扱いが面倒な記号を、すべてTeX\symbol{数字}に変換しています。 こんな感じです。

(define (tex-escape str)
  (regexp-replace-all 
   #/[(]symbol (\d{1,3})[)]/
   (regexp-replace-all* str
      #/\\/ "(symbol 92)"
      #/{/  "(symbol 123)"
      #/}/  "(symbol 125)"
      #/\#/ "(symbol 35)"
      #/\$/ "(symbol 36)"
      #/\%/ "(symbol 37)"
      #/\&/ "(symbol 38)"
      #/\_/ "(symbol 95)"
      #/\^/ "(symbol 94)"
      #/\~/ "(symbol 126)")
   "{\\\\symbol{\\1}}"))

正規表現を2段階にしているのは、\symbol{数字}の中に出現するバックスラッシュと波カッコが再置換されるのを防ぐためです。 元の原稿に「(symbol 92)」のような文字列が出現する場合のことは、このtex-escapeという関数では想定していません。 その可能性がある場面では別のエスケープ用の関数を定義して使います。

注意が必要なのは、この数字はフォントエンコーディングによって異なるということです。 そのときのフォントエンコーディングLaTeXの層の話なので、XMLからLaTeXへの変換では決められません。 逆にいうと、何かしら決め打ちしてるということです。 xml2texの場合は、T1というエンコーディングに決め打ちしています。 ここで昨日の記事のデフォルトの出力結果の最初の3行を思い出してもいいでしょう。

\documentclass{book}
\usepackage[T1]{fontenc}
\usepackage{alltt}

デフォルトがこうなっていたのは、このへんの事情によります。 allttをデフォルトで呼び出しているのも、古き良きverbatimだと\symbolによる特殊文字の印字が使えないからです。

このようにいくつか制約はありますが、経験上、この方法でLaTeXへの変換におけるエスケープをするのがいちばん困ることが少ないように思います。 ちなみに自分は、Pandocでも、コードブロックの構造に対して以下のような上記と同等なエスケープ処理を施すようにしています。

escapeTeX :: String -> String
escapeTeX s = 
  let re  = R.mkRegex "[(]symbol ([0-9]{1,3})[)]"
  in R.subRegex re (at $ sh $ bs $ lp $ rp $ us $ ps $ dl $ ad s) "{\\symbol{\\1}}"
  where bs s = R.subRegex (R.mkRegex "[\\]") s "(symbol 92)"
        lp s = R.subRegex (R.mkRegex "{") s "(symbol 123)"
        rp s = R.subRegex (R.mkRegex "}") s "(symbol 125)"
        at s = R.subRegex (R.mkRegex "[@]") s "(symbol 64)"
        sh s = R.subRegex (R.mkRegex "[#]") s "(symbol 35)"
        us s = R.subRegex (R.mkRegex "[_]") s "(symbol 95)"
        ps s = R.subRegex (R.mkRegex "[%]") s "(symbol 37)"
        dl s = R.subRegex (R.mkRegex "[$]") s "(symbol 36)"
        ad s = R.subRegex (R.mkRegex "[&]") s "(symbol 38)"

Pandocでは、Writerの前段にPandocフィルターをかませることができます。 最初に示したPandoc標準のエスケープ処理のコードはLaTeX用のWriterのものなので、コードブロックの構造に対する処理をフィルターで奪うことにより、上記の独自エスケープ処理を済ませた結果が吐き出されるという仕掛けです。

明日からはPandocまわりの話を始めるつもりです。 が、まだ構想が決まってないので、もう少し小ネタを挟むかかも。

XMLのつぶし方

昨日までの話を整理します。

  • ドキュメントのXMLによる表現は、プログラムの抽象構文木に相当し、ドキュメントの意味構造を示したものであった
  • なので、XMLの構文をS式で表せた
  • すると、XMLの要素名がLispにおける関数、要素がその関数への引数に見えた
  • そこで、要素を材料としてシリアライズした文字列を返すように、要素名で関数を定義した。その際、要素の中には別の要素名を持つ要素が入れ子になっていることがあるので、それらは再帰的に処理するように定義した。
  • こうして、ドキュメントのXMLLispの評価器で直接実行できた

そして、そのためのフレームワークとして、xml2texという自作のアプリケーションを紹介しました。 XMLからTeXを生成する専用機に見える名前が付いているけど、これは命名を失敗したと思っていて、xml2texは、いわば、XMLをつぶす機械を作る機械です。 XMLをつぶして好きなようにコンテンツをマークアップし直したいときに使えます。

github.com

今日はこのxml2texを使って、DocBook(のサブセット)からLaTeXへの変換器を作っていきます。

追記: XML組版をするといった場合、ふつうはXMLのエコシステムを使うことが想定されています。 具体的には、XMLの構文を扱うのにXSLTを使い、組版にはXSL-FOを使うというエコシステムです。 しかし、完成度の問題から組版にはTeXのエコシステムを使いたいので、ここではXMLのエコシステムは無視しています。 また、XSLTLaTeXマークアップに変換する手法を採用しているシステムもありますが、XSLTXMLからXMLへ変換したりする用途には向いてるものの、XML構文をアドホックに他のマークアップに変換する用途には向いていないというのがぼくの持論です。(XMLからXMLへの変換でさえHXTのほうが強力かつ直観的だと思います。)

(追記ここまで)

DocBookからLaTeXへの変換器をxml2texで作る

ここに『プロフェッショナルSSL/TLS』という当社の出版物があります。 この本は翻訳書で、原書出版社ではXMLで原稿を執筆、管理しています。

https://www.lambdanote.com/collections/tlswww.lambdanote.com f:id:golden-lucky:20191211131405p:plain

原書出版社が使っているスキーマはDocBookです。 DocBookはれっきとしたXMLアプリケーションなので、スキーマがあります。 しかも、DocBookのWebサイトを見ればわかるように、いくつもの方法でスキーマが定義されていて至れり尽くせりです。 いずれか好きなスキーマ定義を見て、「どんな要素があるか」「どんな属性があるか」「どんな要素がどんな要素の親になれるか」「どんな属性がどんな要素で使えるか」などを掌握し、しかるべき変換器を開発してLaTeXマークアップに変換する、もしくは、誰かが開発してくれた変換器を使ってLaTeXマークアップされたファイルを手に入れる、というのがXMLの正しいやり方です。

しかし、現実には以下のような困難があります。

  • 自分で変換器を作るとして、汎用のものが必要なのではなく、この本を表現するのに使われているDocBookのサブセットさえLaTeXマークアップに変換できればいい
  • 既存の変換器を使うとして、それらが吐き出すLaTeXマークアップが自分にとって使い物になるとは限らない
  • DocBookの場合にはスキーマがオープンで公開されているが、変換したいXML文書のスキーマがいつも手に入るとは限らない

そこでxml2texでは、たとえスキーマがあったとしてもスキーマはまったく使わずに、探索的に必要最小限の変換器を作るというアプローチを採用しています。 具体的には、素性がよくわからない野良XML文書から「関数として定義が必要なXMLの要素」を洗い出し、それをどんなLaTeXマークアップに変換したいか、その際に要素の親子関係や属性をどう使いたいかを、必要なだけ変換ルールを追加することで組み上げていきます。

どういうことかというと、たとえば本の原稿がこんなXMLだったとします。

<book>
  <info>
    <author>著者</author>
    <title>本タイトル</title>
  </info>
  <chapter>
    <title>章タイトル</title>
    <para>本文1</para>
    <section>
      <title>節タイトル</title>
      <para>本文2</para>
      <para>本文3</para>
    </section>
  </chapter>
</book>

このXMLファイルをxml2texにかけると、xml2texは「とりあえずデフォルトの変換ルール」を適用して変換を試み、こんな感じの結果を端末に返してきます。


$ gosh -I. xml2tex.scm  test.xml > test.tex
Not knowing tha LaTeX syntax for <book>, ... applyed (through).
Not knowing tha LaTeX syntax for <info>, ... applyed (through).
Not knowing tha LaTeX syntax for <author>, ... applyed (through).
Not knowing tha LaTeX syntax for <title>, ... applyed (through).
Not knowing tha LaTeX syntax for <chapter>, ... applyed (through).
Not knowing tha LaTeX syntax for <title>, ... applyed (through).
Not knowing tha LaTeX syntax for <para>, ... applyed (through).
Not knowing tha LaTeX syntax for <section>, ... applyed (through).
Not knowing tha LaTeX syntax for <title>, ... applyed (through).
Not knowing tha LaTeX syntax for <para>, ... applyed (through).
Not knowing tha LaTeX syntax for <para>, ... applyed (through).
Not knowing tha LaTeX syntax for <para>, ... applyed (through).

デフォルトでは、「すべての要素を文字列として印字する」という変換ルールが適用されることになっているので、変換結果そのものはこんな感じになります。


$ cat test.tex
\documentclass{book}
\usepackage[T1]{fontenc}
\usepackage{alltt}


    著者
    本タイトル


    章タイトル
    本文1

      節タイトル
      本文2
      本文3

先頭の3行は、「XML全体を表したS式」に対するデフォルトの変換ルールで生成されたものです。 通常はLaTeXへの変換に使うので、デフォルトはこんな3行が出力されるようにしています。 (もちろん、これから説明する方法でこのデフォルトのルールは上書きできるので、別の種類の出力にも使えます。)

さて、ここで注目してほしいのは、変換結果ではなく、端末への出力のほうです。 この端末の出力に、「関数として未定義のXML要素」が洗い出されているのがわかるでしょうか。 この端末の出力を見ながら、未定義のXML要素に対する関数を、昨日の記事で紹介したLispマクロを使って定義していきます。

簡単な要素から片づけましょう。 <book>LaTeXdocument環境にすればよさそうです。 <para>は段落になればいいので、改行を付加するだけでよさそうです。 それぞれ次のような変換ルールとして定義できます。

(define-tag book
  (define-rule 
    "\\begin{document}"
    trim
    "\\end{document}\n"))

(define-tag para
  (define-rule 
    ""
    trim
    "\n"))

これらの変換ルールをファイルに保存し、test.rulesとでも名前を付けておきます。 この変換ルールを書き溜めたルールファイルが、xml2texでは設定ファイル的な役割を果たします。

ルールファイルを-rオプションに指定して、再びxml2texを実行すると、今度は端末への出力結果がこう変わります。


$ gosh -I. xml2tex.scm -r test.rules test.xml > test.tex
Not knowing tha LaTeX syntax for <info>, ... applyed (through).
Not knowing tha LaTeX syntax for <author>, ... applyed (through).
Not knowing tha LaTeX syntax for <title>, ... applyed (through).
Not knowing tha LaTeX syntax for <chapter>, ... applyed (through).
Not knowing tha LaTeX syntax for <title>, ... applyed (through).
Not knowing tha LaTeX syntax for <section>, ... applyed (through).
Not knowing tha LaTeX syntax for <title>, ... applyed (through).

<book><para>に対応する変換ルールを用意したので、端末の出力からこれらに相当する行が消えてますね。

変換結果のほうも見てみましょう。こんな状態になりました。


$ cat test.tex
\documentclass{book}
\usepackage[T1]{fontenc}
\usepackage{alltt}
\begin{document}
    著者
    本タイトル

    章タイトル
    本文1


      節タイトル
      本文2

      本文3
\end{document}

続いて、<info>とその子要素<author>に対する変換ルールを考えます。 が、<info>LaTeXのほうには不要そうなので、要素ごと無視することにします。 それにはこう書きます。

(define-simple-rules ignore info)

これをルールファイルに追加して再びxml2texを実行すると、<info>に相当する行が端末の出力からなくなります。 変換結果は次のようになり、<info>要素に含まれていた文字列が印字されなくなりました。


$ cat test.tex
\documentclass{book}
\usepackage[T1]{fontenc}
\usepackage{alltt}
\begin{document}
    章タイトル
    本文1


      節タイトル
      本文2

      本文3

\end{document}

最後に、<title>の処理を考えましょう。 <title>要素には、親要素が<chapter>の場合と<section>の場合があり、LaTeXではそれぞれを\chapterおよび\sectionコマンドに引き当てる必要があります。 今までの例ではルールに指定する出力結果が文字列だけで済んでいましたが、もうちょっと複雑なルールを書く必要があるということです。

そのような場合のために、xml2texでは、Schemeのプロシージャをルール内に書けるようになっています。 「親要素が<chapter>の場合には\chapter<section>の場合には\sectionを印字するようなルール」はこんなふうに書けます。

(define-tag title
  (define-rule
    (lambda ()
      (list
       (cond ((eq? ($parent) 'chapter) "\\chapter{")
             ((eq? ($parent) 'section) "\\section{"))))
     trim
     (list "}\n")))

(define-simple-rules through chapter section)

最後の行は、「<chapter><section>に対しては特に何もすることがないのでスルーする」という意味です。

<title>に対するルールで使っている$parentという変数は、このルールを適用している最中の親要素を表すシンボル(へと評価されるプロシージャ)に束縛されています。 こういう技が可能なのがLispマクロの強さで、おかげでそこそこ直観的にXML要素に対する変換ルールを定義できるようになっています。

これらの変換ルールをルールファイルに追記してxml2texを実行すると、無事にこんな感じのLaTeXソースが得られました。


$ cat test.tex
\documentclass{book}
\usepackage[T1]{fontenc}
\usepackage{alltt}
\begin{document}
    \chapter{章タイトル}

    本文1

      \section{節タイトル}

      本文2

      本文3

\end{document}

ついでに、もう一つだけ込み入った例を紹介します。

実は、DocBookでは<section>の階層をどんどん深くできます。 LaTeX\subsectionに当たるものが、2階層ある<section>の子要素の<title>を変換した結果、ということです。 この挙動を反映して<title>に対するルールを拡張するとこうなります。

(define-tag title
  (define-rule
    (lambda ()
      (list
       (cond ((eq? ($parent) 'chapter)  "\\chapter{")
             ((eq? ($parent) 'section)
              (let1 depth ($ancestors 'section)
                (cond ((= (length depth) 0) "\\section{")
                      ((= (length depth) 1) "\\subsection{")))))))
     trim
     (list "}\n")))

($ancestors 'section)が、$parentと同様にLispマクロの力により、「祖先にいる'sectionたちのリスト」に束縛されてます。 その個数を数えれば階層の深さがわかるというわけです。

原著と同じ構造化文書を翻訳の原稿でも使うメリット

今日は、xml2texで変換器を作るときの基本的な流れを紹介しました。 つまり、次の工程を繰り返すことで、そのXML文書に対する変換ルール集を練り上げていきます。

  1. とりあえず実行してみる
  2. 端末に未定義の要素が指示される
  3. 端末に残っている要素の様子を対象のXMLファイルで眺める
  4. その要素をどういうマークアップへ変換したいか考えて、test.rulesに追加する
  5. 再び実行してみて、出力結果のマークアップが希望どおりか確認する
  6. 端末で指示された要素がなくなるまでこれを繰り返す

『プロフェッショナルSSL/TLS』では、このxml2texを使うことで、原書のDocBookソースで使われている主な要素に対する変換ルールを1日もかからずに作り出しました (それに対するLaTeXのスタイルを作ったり、細かい部分の調整にはもっと時間をかけています)。 さらに、英日対訳ができるように<para>などの構造にはlang属性を追加し、それをそのまま翻訳に使っています。

このおかげで、『プロフェッショナルSSL/TLS』では、原書の改訂にも最低限の労力で追随できるようになっています。 世間にはかなり高機能なXMLの編集ツールがあり、それを使って構造を考慮した差分を取りながら、修正があった部分に対する再翻訳や新規訳出をしている感じです。

ちなみに、そのような高機能なXMLの編集ツールとしては、oXygenというやつが有名です(有償)。

たぶん、oXygenとxml2texがなければ、原書の翻訳に追随できる制作体制を維持することはもっと大変だったと思います。 あとは原書の改訂原稿がもっと順調にくればいいのですが…。

閑話休題。 明日は、xml2texの落穂拾い的な話として、LaTeXを吐き出すアプリケーションを書くときにエスケープどうするか、という小ネタの予定です。

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版の標準的な実装を使います。

これはGaucheにも ssax:xml->sxml という関数で組み込まれています。

これをありがたく使うだけで、基本的には(整形式の)XMLからSXMLが得られます。

XMLのタグ」を関数適用できるようにする

いくらLisp族の言語といえども、リテラルなS式を自由自在に評価させてくれるわけではありません。 そういうLispの実装を自分で作るか、そのためのバックドアがある実装を使う必要があります。

幸い、GaucheというSchemeの実装には、型を指定して好きなモノを適用可能にできるobject-applyという汎関数があります。

この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へのコンバーターを何回か作ってみると気づきますが、やることは毎回だいたい同じです。

  1. 先頭にLaTeXのコマンド名とか開き波カッコとか改行とかを配置
  2. 中身がリストだったら再帰的に処理、文字列だったら必要な加工をして配置
  3. 後ろに閉じ波カッコとか改行とかを配置

要するに、最初にやること、間にやること、最後にやること、の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-tagLispのマクロとして定義することで簡単に実装できます。

さらに、Lispマクロのパワーで、「タグの名前からルールを自動生成する」とか「XMLの属性をルールの中で定数として扱えるようにする」といったこともできます。 特に後者はOnLispに出てくるアナフォリックマクロの応用で、それ自体がだいぶ面白いと思うんですが、Lispの話は無限にできてしまう割に読者をさらに選ぶので、このへんでやめておきます。

車輪は自分のために再発明する

ここまでに説明した仕掛けは、xml2texという名前で公開しています。

詳しくは、TeX界のTUGboatというジャーナルに投稿した記事もあるので、気になる人は読んでみて。

xml2texという名前を見ると、「XMLからTeXへの変換器」を想像するかもしれません。 実際、最初はTeXへの変換器を作るバックエンドとして開発したのですが、作っているうちに、xml2texの実際の機能は「「XMLシリアライズして他のマークアップ原稿へと変換するコンバーター」を作るフレームワーク」であることに気付きました。 過去には、FrameMakerというアプリケーションのSGMLXMLの親分みたいなやつ)からRe:VIEW原稿を作ったりするのにも使っています。

ところで、SXMLとXMLシンタックスが違うだけなので、ふつうのXMLでも同様のことが実現できないはずがないんですよね。 どういうことかというと、山かっこのXMLに対する変換処理を山かっこのシンタックスで書き、山かっこのXMLを実行するようなプログラミング言語が作れるだろう、ということです。

で、昨日の記事を書いていて初めて気が付いたんですが、XSLTという関数型言語がまさにそれに相当するような気がしました。 ドキュメント全体をS式として表現したことで「ドキュメントの構造をそのまま実行する」という策が見えてきた気がしていたけれど、その先にあったのはやはり車輪の再発明でしかなかったというわけです。

でも、車輪の再発明、常套だと思っています。 XSLTは、XMLの世界観では完成度が高い仕組みだけれど、やはりふつうのプログラミング言語と同じ感覚では書きにくいし、処理系の機能も限られます。 そこいくとLispはやはり自由度が高い。 自分の直観にあう方法で変換器をぽんぽん生成できる仕組みを作ったことは個人的にはとても気に入っています。

明日は、このxml2texを使うことで、DocBookの原書データを活用した翻訳版の本をLaTeX経由で作っている話をします。