golden-luckyの日記

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

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経由で作っている話をします。

XMLをLisp評価器で実行する

昨日は、ドキュメントとは木であり、その木はXML、さらにいうとXMLアプリケーションとして形作られる、という話をしました。 一般にドキュメントは、生のままの構造として読み手に与えられるものではありません。 ドキュメントの構造が何らかのXMLアプリケーションであれば、本来はそのスキーマに従って解釈し、しかるべきスタイルを適用することになります。

ここで、しばしば厄介になるのは、XMLアプリケーションのスキーマを正しく扱うのはなかなか大変だということです。 そもそも、プロプラなXMLアプリケーションだと、スキーマの定義が手に入らない場合もあります。 そういった「野良XMLをどうやって扱うか」が今日からの話題です。

でもその前に、昨日の記事の最後で紹介したSXMLについて雑に補足しておきます。

XMLをSXMLに引き写すことで見える景色

さて、昨日はSXMLという技術を紹介しました。 SXMLのSはS式のSです。 簡単に言うと、こういうXMLで構造が表されている書籍は…、

<book>
  <info>
    <author>著者</author>
    <title>本タイトル</title>
  </info>
  <chapter>
    <title>章タイトル</title>
    <para>段落の本文</para>
  </chapter>
</book>

こういうS式で表せます。

(book
  (info
    (author "著者")
    (title "本タイトル"))
  (chapter
    (title "章タイトル")
    (para "段落の本文" (b "ここは太字") )))

ところで、みなさんご存じのように、S式はLispというプログラミング言語で(現代では)主に採用されています。 Lispというプログラミング言語におけるS式は、Lispシンタックスであると同時に、抽象構文木でもあります。 Lispでは、S式で表された抽象構文木をそのまま評価器で実行でき、しかもマクロでDSLを作れます。

さて、ここで、一昨日の記事を思い出してください。 そこでは、「ドキュメントの構造はプログラミング言語における抽象構文木に対応している」と述べました。 そしてSXMLでは、ドキュメントの構造がS式で表現されています。

ということは、ドキュメントの構造をそのままLispで実行できそうに思えませんか?

XMLをわざわざS式で表現することには、見た目の雰囲気を変えただけでなく、「ドキュメントの構造をプログラミング言語の抽象構文木と同じように評価できる」ことを思い出させてくれる効能があったのです(ただしLispプログラマーにとって)。 スキーマ言語XMLアプリケーションを作るのと事実上同じことが、SXMLであればLispのマクロとして、より直観的に実現できると言ってもいいでしょう。

SXMLをLispで「評価」する

野良XMLLispマクロで手なずける前に、「LispでSXMLを評価する」ことについて、Lispを少ししか知らない人向けに補足しておきます。

先ほど例として示した書籍の構造を表すSXMLの一部には、次のようなS式が含まれていました。

(para "段落の本文")

このSXMLは「paraという関数を引数に適用する」と見なせます。 この場合の引数は"段落の本文"という文字列です。

山かっこのXMLだと「段落要素を表す<para>タグ」にしか見えなかったものが、SXMLだと関数適用に見えるのがわかるでしょうか。 たとえば、(para ...)というSXMLをLaTeXの段落として出力したければ、関数paraを「引数の中身をLaTeXの段落として出力する」というふうに定義すればよいでしょう。

(define (para arg)
  (print arg "\n\n"))

同様に、関数bを「引数の中身をLaTeXの太字として出力する」というふうに定義すれば、(b ...)というSXMLの項を評価した結果はLaTeXの太字になります。

(define (b arg)
  (print "\\textbf{" arg "}"))

こんな感じで元のXMLに出現するタグと同じ名前の関数にしかるべき定義を与えてあげれば、あとはSXMLの全体をLispの評価器で実行するだけで、「元のXMLアプリケーションを処理した結果」が得られるという寸法です。 画期的ですね。

(define (book arg)
  ("\\documentclass{jsbook}\n\\begin{document}" arg "\\end{document}"))

ただ、これだけだと、argの中身が再帰的に評価されないのでうまくいきません。 そのへん、もうちょっとちゃんと定義してあげる必要があります。

また、XML名前空間や属性の扱いなども、それなりに対処してあげる必要があります。 SXMLでこれらを扱うのは、XMLスキーマを書くときと同様に、わりと大変です。 そもそも、素性のわからない野良XMLの中身を見ながら「元のXMLに出現するタグと同じ名前の関数にしかるべき定義を与えてあげる」という作業はかなりうっとうしい。

SXMLを評価することで野良XMLに意味を与える、というアイデアを実用的なものにするには、これらの細かい部分を抽象化してくれるフレームワークが欲しくなります。

明日は、そういうフレームワークを実装する話をします。

なんでドキュメントといったらXMLが出てくるのか

昨日は、ドキュメントにおける構造というのはセマンティックな構造である、という話をしました。 今日は、そのセマンティックな構造をどう扱うか、という話です。

ドキュメントの構造は一般にXMLを使って表されている

結論から言うと、ドキュメントの構造は、XMLで扱うのが一般的です。 ドキュメントの構造を表すのにXMLがよく使われているのには理由があって、それは、ドキュメントが木構造だからです。

本当はここで「XMLとは何か」みたいな話をする必要があると思うんですが、ここではXMLというのは「木構造のデータを表現するときの標準的な構文」くらいの意味で使います。 つまり、表現する「木構造のデータが具体的にどんなか」については別の問題ということにして、木構造で表せるようなデータにとって共通で必要そうな構文だけを定めたものが、(ここでいう)XMLです。

ちなみに、「木構造のデータが具体的にどんなか」のほうは、「XMLアプリケーション」と呼ばれます。 XMLアプリケーションとしては、HTML(正確にはXHTML)とかMathMLとかDocBookとかがあります。 XMLアプリケーションを定義するには、一般にはDTDとかXML SchemaとかRELAX NGとかいった「スキーマ」を使います。 上の段落で言いたいことは、そういったスキーマを使って定義されたXMLアプリケーションが何であるかを本記事では気にしない、ということです。

ドキュメントの木

よくわからないと思うので、ドキュメントの木構造を例に説明します。 ドキュメントの木構造といって想像するのは、たぶんこんなのでしょう(適当に描いたのでツッコミはなしで)。

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

緑色の葉に相当するのがインライン要素、そのインラインの要素が集まったものが茶色の節に相当するブロック要素、ブロック要素が集まってドキュメントになる、という感じです。

ただ、この単純なイメージだと、「インライン要素にどんなものがあるか」、「ブロック要素にどんなものがあるか」、「それらをどう組み合わせていいか」といった具体的な木の形状までは説明できてません。 実際に必要な要素が何であるか、要素の組み合わせとして何を認めるかは、ドキュメントの用途や種類によってまちまちです。

とはいえある程度までは、ドキュメントが必要とされる分野ごとに「汎用性のある要素の組み合わせ」みたいなものは考えられるでしょう。 そういう「汎用性のある要素の組み合わせ」を考えるということは、木構造を制限するということです。 その手段がXMLアプリケーションです。

言い換えると、ドキュメントがどんな形状の木になりうるかをXMLアプリケーションとして制限する、という世界観です。 たとえば、技術書のために必要な制限を課したXMLアプリケーションとしては、DocBookがあります。

木構造ではないドキュメントもありうるとは思うんですが、それはここではドキュメントではないものとします。どう考えても木構造とみなすのが適切でなさそうなドキュメントがあったとして、それをコンピューターでどう扱えばいいかという話も面白そうだけど、そういう話はどこかにあるのかなあ。)

XMLは山かっこでなくてもいい

ここまでの話を整理すると、こうなります。

  • ドキュメントは木構造
  • どんなドキュメントかに応じて木構造を制限したい
  • それに都合がいい仕組みとしてXMLがある

どうでもいい話に聞こえるかもしれませんね。 なんでわざわざこんなことをくどくど書いてるかというと、これらの話にはXMLの象徴である「山かっこの記法」が出てこないことを強調したいからです。 実際、こういう枠組みを実現するのに、記法が山かっこタグである必要はありません。

XMLで表せる木構造は、文字通り「構造」であり、記法とはレイヤが違います。 XMLの世界観だと、「XMLアプリケーションごとにスキーマで定義した構文」のほうが記法に相当します。

もっとも、この「XMLアプリケーションごとにスキーマで定義された構文」もXMLと見た目は同じ、つまり、通常は山かっこタグになります。 記法についてはドキュメントの構造とは別に考えるべきだけど、XMLでは両方とも同じ記法を採用している、みたいな感じです。 ドキュメント屋さんとして、XMLを使うときはこの辺りの事実からは逃れられない感じです。

ただ、ぶっちゃけ木構造を表すなら、山かっこタグよりも優れたシンタックスがあります。そう、S式です。 S式をシンタックスとするXMLをSXMLといいます。

というわけで、明日はLispの時間です。

ドキュメント技術とプログラミング言語の相似について

よく知られているように、ドキュメントには「構造」があります。 WebページではHTMLとCSSにより構造とスタイルを分離するべきとか、Wordでは書式設定をスタイルとして定義して使うことで構造とスタイルを分離するべきとか、ドキュメントの「べき」論で必ず言及される「構造とスタイルの分離」における「構造」です。

昨日までの話ではPDFにもドキュメント構造というのが出てきました。あれは、この「構造とスタイルの分離」というときの「構造」とは別物なので注意してください。 たぶん、PDFのドキュメント構造には、「ドキュメントを表すデータ構造」くらいの意味合いくらいしかありません。

一方、ドキュメントの話において「構造とスタイルの分離」というときの「構造」は、もうちょっとこうなんていうか、セマンティックな話です。 データをどう構成するかではなく、ドキュメントで表したい意味をどう構成するか、という話。

したがって、ドキュメントの話をするときは、「ドキュメントで表したい内容」を「ドキュメントの最終的な見た目」みたいなフワフワから切り離すことで、前者の可搬性を高めていくことが目指されます。

話はちょっとずれるんですが、コンピューターで扱うドキュメントの話をするときって、「可搬性を持たせたい部分がどこか」という点がわりと曖昧なままになってることがあるので、その点に注意して聞くといいと思います。 PDFは、見た目の可搬性から出発していました。 「構造とスタイルの分離」の話では、構造の可搬性を目標にします。まあ、あんまりこっちの話をするときは「可搬性」という用語は使わず、再利用可能性とかいうことが多いので、混乱はないと思うけれど。

構造とスタイルと記法

コンピューターでドキュメントを扱う話をするときは、「構造とスタイルの分離」がどれくらいかっちりできてるかで、技術の素性の良さみたいなのが語られがちです。 つまり、構造とスタイルの分離っていうのは、ドキュメント技術においてはある種の金科玉条です。

まあ最近だと、「金科玉条でした」と過去形で書くほうが正解なのかもしれませんね。 いまの流行りは構造とスタイルの分離でなく、書式とか記法、つまりシンタックスに移っているようなので。

しかしドキュメントにおけるシンタックスって、結局のところ、ドキュメントのセマンティックな構造を暗に埋め込むためにどんな表現を使うか、という話なんですよね。

ドキュメントの構造を暗に表現するシンタックスの話については、このアドベントカレンダーでいつか話す予定です。 いまは何が言いたいかというと、構造とスタイルの分離を重視する立場だと、「人間の直観を裏切らない記法」みたいな観点はそれほど重視されません。 一方で、人間が編集しやすい記法が何かという話に注力してしまうと、それはそれでドキュメントにおける構造とスタイルの分離の伝統とは違う話をしてしまう可能性が高い。

なにが言いたいかというと、これからのドキュメント技術について語るときの前提は、「記法」「構造」「見た目」の3つのレイヤを意識したモデルに依拠するといいのかな、ということです。

ドキュメント技術の3階層モデル

だんだん与太話っぽくなってきましたが、「シンタックス(記法)」→「セマンティクス(構造)」→「スタイル(見た目)」という3つの階層でいろんなドキュメント技術を考えるのは、わりと建設的なモデルなんじゃないかなと個人的には考えています。

個人的にこのモデルが特に気に入ってるのは、プログラミング言語における「ソースコード」→「抽象構文木」→「実行ファイル」の関係によく似ている点です。 プログラミング言語に似ているということは、コンピューターでドキュメントを扱う方法について考察するときに都合がいい。

たとえば、プログラミング言語だと、この階層の矢印を逆方向にたどるのが無理筋だということをみんながよく知っています。 ふつうの人が直接触れるのは最下層のみだけど、階層を下にいくにつれて、その中身は人間が理解しにくいモノになっていく。 これがドキュメント技術の話になると、どの階層を見ても人間が読む文字が見えるので、似たような階層があることに気が付きにくい。 意識的に層の違いを強調してあげる必要があると思うんですが、そこで「プログラミング言語における「ソースコード」→「抽象構文木」→「バイナリ」の階層みたいな感じ」といえば、なんとなく伝わりそうな気がします。

階層を下に降りるほど編集の自由度がなくなるという点も、ドキュメントの3階層がプログラミング言語に似ているところだと思います。 PDFを直接いじることが煩雑かつ(ドキュメントの可搬性にとって)危険であることは何となく昨日までの記事で伝わってると思うんですが、これはドキュメントの「シンタックス(記法)」→「セマンティクス(構造)」→「スタイル(見た目)」という階層で考えれば「プログラムの実行ファイルを直接いじる」のと同じ話なわけで、なんとなく当然の成り行きであることが伝わりやすい気がします。 大変かつ危険だけど、条件によっては安全に実行するすべがないわけでもないので、その必要がある場合にはそのためのPDF編集ツールを導入してください、という話がしやすいでしょう。

編集者の仕事は各階層をそれぞれがんばること

この見方でポジショントーク的に説明しやすいことがもう一つあって、それは、プログラミング言語で実行環境に相当する部分はドキュメント技術では人間の脳である、という点です。 そう考えると、編集者の仕事っていうのは、「人間の脳におけるドキュメントの実行を最適化するために各階層で手を尽くすこと」に見えてこないでしょうか?

プログラミングにおいては、シンタックス、セマンティクス、スタイルの各階層だけでなく、その上、つまりアルゴリズムの改良とかデータ構造の工夫も重要です。 たぶん、ドキュメントの仕事でそれに近いのは、文章のリライトとか、いわゆるトンマナの調整、それに校正なんかなんでしょう。

さらに上の階層、そもそも現実の問題をどんなアプリケーションとして作ればいいのかを含めた設計に相当する部分は、ドキュメントでいうと企画ですね。

最後はちょっと強引に編集者の仕事論っぽい話をしてみました。 明日は構造化文書の本丸、XMLの話です。