golden-luckyの日記

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

Markdownで書籍を作るとは

昨日まで何回かにわたり、多様なドキュメント形式の変換アプリケーションであるPandocのコアとなる仕組みを説明してきました。 特に、Pandoc構造とそれを生成するReader、生成されたPandoc構造を変換するPandocフィルターについて、少し時間をかけて紹介しました。

では、PandocのReaderとフィルターについて理解したところで、Pandocを使って本は作れるでしょうか?

いままでの説明には登場しませんでしたが、Pandocの出力側を担うWriterには、「PDF生成のためのLaTeX Writer」や「EPUB Writer」など、「本」を作るのに使えそうなものがあります。 それらWriterを制御するためのコマンドラインオプションはいろいろ用意されており、独自のテンプレートを指定することも可能です。

ただ正直なところ、これらのWriterは、吊るしの状態では売り物の本を作れるほどには洗練されていません。 本にはPandoc構造では表現できないより大きな構造があり、コマンドラインオプションやテンプレートでそれを完全に制御するのは難しいからです。 今日は、そのような書籍のマクロ構造を手なずける必要がある例として、書籍の前付けと本文とを区別する話をします。

Pandocで書籍のマクロ構造に対処する3つの方針

「ドキュメント」と一口にいっても、その用途に応じていろいろ形が決まっています。 その形を定める国際的な規格が成立しているものもあれば、長年の慣習でなんとなく定型ができているものもあります。 いわゆる書籍については、これといった公的な規格はありませんが、かなり長い年月にわたって受け継がれてきた形があり、それがマクロな構造を形成しているといえるでしょう。

ほとんどの日本の書籍は先頭付近に扉絵やクレジットがあり、序文などに続いて目次、それから本文、巻末にあとがきや参考文献、索引、奥付で終わるという構造になっています。 一方、Pandoc構造で与えられるのは、HeaderParaのようなブロック構造、あるいはそれらを構成するインライン構造だけです。 Pandocで本を作るときには、どうやってマクロな構造を組み立てればいいでしょうか?

さまざまな方針はあるでしょうが、おおまかには次の3つに区分できると思います。

  1. PandocのWriterもしくはフィルターでやる
  2. Pandocのメタデータとテンプレートでやる
  3. Pandocでやらない

1つめの方針は、昨日までの記事でだいたい想像がつくと思います。 マクロな構造をある程度決め打ちにし、必要があればフィルターで整えて、Writerで一気に出力するというものです。 EPUB Writerはこの方針だといえるでしょう。

2つめの方針は、LaTeXやConTeXtを介したPDF出力が該当します。

想像に難くないと思いますが、ぼくが採用しているのは3つめの方針です。 全体のワークフローを示すとこんな感じになります。

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

マクロな構造は完全にLaTeXで用意しておき、PandocはあくまでもコンテンツをMarkdownからLaTeXの章や節に変換するのに使っています。 この方法の利点は、書籍のマクロ構造に関して完全に枯れた技術であるLaTeXのパワーを余すところなく利用できることです。 欠点は、Pandocで完結しないのでMakefileのような仕組みが必要になることです。 もっとも、この欠点は高い柔軟性という利点でもあります。

前付けの\section\section*にする3つの方針

書籍のマクロ構造についてPandocの側では気にせず、LaTeX側で面倒を見るという方針を取るのはいいとして、PandocでLaTeXへと変換したコンテンツが、そのままの形でLaTeX側で要求する書籍のマクロ構造の要件に合致するとは限りません。 たとえば、書籍では通常は章や節に連番を振りますが、前付けの「序文」などでは連番を振りたくありません。 この両者を区別するのに、LaTeXでは \section{...} コマンドと \section*{...} コマンドを使い分けます。 LaTeXで書籍を作るとき、つまり書籍のマクロ構造を組み立てるときには、「前付けでは見出しに \section*{...} を使い、本文では見出しに\section{...}を使う」という使い分けを、人間がやっています。 これをPandocの出力で切り替えることは、標準ではできないはずです(それともできたっけ?)。

なお、もし書籍のマクロ構造についてもPandocで組み立てる、という方針にしたならば、連番そのものをPandocに作らせるという手はあります。 その場合には、pandocコマンドに--number-sectionsを付けたり付けなかったり、あるいはヘッダに{.unnumbered}という属性を付けたり付けなかったりすることで、前付けと本文での \section の使い分けが可能です(実際には全部が \section のままで、unnumberedの場合はテンプレートで「\setcounter{secnumdepth}{0}」になる)。 しかし、こうしたマクロな構造は、LaTeXの側で対処したいものです。Pandocの標準的な挙動に頼らない別の方法を考える必要があるでしょう。

別の方法としては次の3つの方針がありえると思います。

  1. Pandocフィルターでなんとかする
  2. 出力された.texファイルに対してsedrubyの変換スクリプトを走らせる
  3. TeX言語でやる

投げやりですが、好きな方法で対応すればいいと思います。

まとめ

今日の記事で言いたかったことは、「Pandocは書籍を作るためのツールではないので書籍が簡単に作れるわけではない」ということの再確認です。 特に、書籍のマクロ構造については、そのためにPandocが裏で使っているツール(TeX/LaTeXなど)まで下りてくる必要があります。 Pandocのメタ情報などを駆使してある程度まではパッケージ化できると思いますが、完全な柔軟性がほしい場合もあるでしょう。

なお、Re:VIEWとかSphinxのような「本を作ることをもともと考えている仕組み」には、書籍のマクロ構造を扱う標準的な方法が用意されています。 それらをMarkdown原稿から利用するのもいいでしょう。 ただ、Re:VIEWにしろSphinxにしろ、その書籍のマクロ構造を扱う部分のデフォルトはLaTeXのエコシステムに対するラッパーなので、そのラッパーではカバーしきれない範囲については、やはりLaTeXに下りてくるしかありません。 さらに、LaTeXTeXのアプリケーションのひとつなので、LaTeXで不満な部分についてはTeX言語まで下りてくるしかない、というのはPandocと事情は同じです。

一口に「Markdownで本を書く」といっても、Pandocで可能な範囲でやる、LaTeXなどの仕組みまで下りてくる、TeX言語まで下りてくる、というふうに「自分がどこで何をやっているのか」を意識すると、どこでどういう対策が可能かの選択肢も見えてくるだろう、というのが今日の話のひとつのテーマでした。

明日は「未知の記法を前にして人はどうするか」という話をします。 以降はすべて余談です。

前付けの\section\section*相当にする(TeXで)

そろそろPandocフィルターの話も飽きてきたと思うので、最後におまけとしてTeXでやる方法を考えてみます。 最終的なコードは示しますが、これで常に使える出力が安全に得られるとは限らないと思うので、注意してください。

さて、こういう要求が生じた場合、まずはlatex.ltxを見ます。 そこでは\sectionがこんなふうに定義されています。

\def\section{\secdef\@section\@ssection}

番号あり見出しが\@section、番号なしの見出しが\@ssectionで定義されており、これをスター*の有無で切り替えるのが\secdefというマクロです。 ということは、\secdefの2つの引数の順番を逆にすれば、\section{...}\section*{...}の機能を切り替えられそうだな、とわかります。 いま組版してるのが本文なのかそれ以外なのかは\if@mainmatterで判別できるので、こんな感じに書けばよさそうですね。

% うごかない
\def\section{%
  \if@mainmatter
    \secdef\@ssubsection\@subsection
  \else
    \secdef\@subsection\@ssubsection
  \fi}

もちろんこれは動きません。それがなぜかを知るには、もう少しまじめにTeX言語を読むことになります。

とりあえず関連するマクロの定義をlatex.ltxから書き出してみます。

\def\secdef#1#2{\@ifstar{#2}{\@dblarg{#1}}}
\def\@ifstar#1{\@ifnextchar *{\@firstoftwo{#1}}}
\long\def\@ifnextchar#1#2#3{%
  \let\reserved@d=#1%
  \def\reserved@a{#2}%
  \def\reserved@b{#3}%
  \futurelet\@let@token\@ifnch}
\let\kernel@ifnextchar\@ifnextchar
\def\@ifnch{%
  \ifx\@let@token\@sptoken
    \let\reserved@c\@xifnch
  \else
    \ifx\@let@token\reserved@d
      \let\reserved@c\reserved@a
    \else
      \let\reserved@c\reserved@b
    \fi
  \fi
  \reserved@c}
\long\def\@firstoftwo#1#2{#1}

これらの定義に従って、\section*{...}の展開がどう進行するかを確認してみましょう。

\section*{}\secdef{\@section}{\@ssection}*{}
    ↓
\@ifstar{\@ssection}{\@dblarg{\@section}}*{}
    ↓
\@ifnextchar *{\@firstoftwo{\@ssection}}{\@dblarg{\@section}}*{}

ここまでは単純な定義による置き換えだけです。 ここで\@ifnextcharの定義により、次のような代入が起こります。

  • \reserved@d*
  • \reserved@a{\@firstoftwo{\@ssection}}
  • \reserved@b{\@dblarg{\@section}}
  • \@let@token*

最後のは\futureletによる代入で、これにより残されるのは\@ifnch*{あ}です。 そこで次は\@ifnch*{あ}の展開を考えます。

\@ifnchの定義をみると、\@let@tokenの値に応じた条件分岐になっています。 いま、\@let@tokenは「*」になっています。 一方、\@ifnextcharの定義による代入で、\reserved@d*になっています。 そのため、この条件分岐の結果として残るのは、\let\reserved@c\reserved@aの部分です。 これは次のような代入です。

  • \reserved@c\reserved@a

そのうえで\reserved@cが展開されます。 先の代入により、\reserved@a\@firstoftwo{\@ssubsection}になっているので、結果的に\reserved@cもそうなっています。

\@firstoftwo{\@ssection}*{}\@ssection{}

これでようやく、「 \section*{...}という命令で\@ssectionが選択されてるまでには、こんな具合に展開が起こる」ということがわかりました。

TeX言語のつらさは、ここで話が終わらないことです。 ここまでの流れを注意して読むと、\@ifstarの挙動を逆にするためには、\@ifstarに渡される1つめの引数の展開を先に済ませておく必要がある、ということに気付きます。 したがって、こんな感じに \section を定義し直してあげれば、スターの有無の挙動を本文以外では反転できることになるでしょう。

\def\section{%
  \edef\stared@section{%
    \if@mainmatter\noexpand\@dblarg{\noexpand\@section}\else
    \noexpand\@ssection\fi}
  \@ifstar{\@dblarg{\@section}}{\stared@section}}

繰り返しになりますが、これをコピペして使うことは推奨しないので、この話はこれで終わりです。

なお、こういう感じに「LaTeXレベルの挙動を変えるのにTeX言語をもってする」ことは、「TeX on LaTeX」と呼ばれています。 これは「Ruby on Rails」のもじりですが、単なるもじりでそう言っているわけではなく、実際にこのRuby on RailsにおけるRubyRailsの役割が、TeXおよびLaTeXにそっくりそのまま対応していることによります。

どういうことかというと、RailsRubyで作られたアプリケーションのひとつであり、そのRailsでしか通用しない話を、まるでRubyの機能のように説明することはできません。 逆に、せっかくRailsを採用したのに、そのやり方に逆らって素のRubyだけでWebアプリケーションを作ろうとするのは悪手です。 TeXLaTeXにも、それらの関係に近い、いやむしろほとんど同じ関係を言うことができます。 「TeX on LaTeX」という表現には、「LaTeXの文脈で済む話をわざわざTeXでやり直すのは悪手である」という気持ちがあります。

あるフレームワークでドキュメントを作るということは、そのフレームワークに備わっている機能の範囲でドキュメントを作るということです。 Pandocを使うということはPandocが想定している使い方を受け入れるということであり、LaTeXを使うということはLaTeXが想定している使い方を受け入れるということです。 その範囲に収まらないことをしたい場合にはHaskellなりTeX言語なりまで下りてくるしかない、というのが今日までの話のひとつの結論だったような気がします。