golden-luckyの日記

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

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を吐き出すアプリケーションを書くときにエスケープどうするか、という小ネタの予定です。