Pandocのテンプレート機能でYAMLから本の奥付を自動生成する


(この記事は Pandoc Advent Calendar 2019 の1日目です)

奥付、それは技術同人誌入稿のラスボス四天王その1。

本文とは扱い異なる上に、記述をミスるといろいろややこしい。
できれば早めにスタイルだけ作って、最後はデータを流し込むだけにしたい……!


この記事ではPandocのテンプレート機能を紹介します。(中級者向け)
そしてテンプレートの応用として、冊子本(特に技術同人誌)をつくる上で欠かせない奥付をYAMLファイルから生成する方法も説明します。

前提

下記をインストールした状態で進めます。

  • Pandoc 2.8(現時点の最新版!)
    • 基本的なテンプレート機能は、Pandoc 2.8以前でも利用できます
  • TeX Live 2018以降(古すぎないのバージョン)
    • 主に LuaLaTeX と bxjscls を使用します

Pandocのテンプレート機能

Pandocにおけるテンプレートは、(一部の例外を除く)各出力形式ごとに存在するテキストファイルです。
Pandocはテンプレートにデータを流し込むことで、出力文書を生成します。

つまりテンプレートは、本文や各データとは独立して文書の形式やスタイルを規定するために利用できます。

ちなみに先ほど「一部の例外」は、docxとpptx形式です。
この2つの書式は複雑すぎてプレーンテキストのテンプレートでは表現できないので、
style reference (--reference-doc) という仕組みでスタイルをカスタマイズします。

Templates以外の節でも 変数 (variables) の説明が頻繁に表れます。
Pandocの世界で「variable」といえば、9割以上はテンプレート内で使われる変数(テンプレート変数)を意味します。

テンプレートの解説は、Pandoc User's Guideの「Templates」にあります。

テンプレートの例

ある出力形式のテンプレートは pandoc -D 出力形式名 で得られます。

たとえば、HTML出力 (-t html) のテンプレートは次のコマンドで出力できます。

$ pandoc -D html

実行すると、次のようにテンプレートの内容が表示されます。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="$lang$" xml:lang="$lang$"$if(dir)$ dir="$dir$"$endif$>
<head>
  <meta charset="utf-8" />
(中略:meta部分)
  <title>$if(title-prefix)$$title-prefix$ – $endif$$pagetitle$</title>
  <style>
    $styles.html()$
  </style>
$for(css)$
  <link rel="stylesheet" href="$css$" />
$endfor$
(中略)
$for(header-includes)$
  $header-includes$
$endfor$
</head>
<body>
$for(include-before)$
$include-before$
$endfor$
(中略:タイトルや目次など)
$body$
$for(include-after)$
$include-after$
$endfor$
</body>
</html>

ここで $ ... $ のようにドル記号で囲まれている部分が、Pandocテンプレート独自の構文・変数です。
次のように簡易的なマクロ言語が備わっています。

  • for: 繰り返し構文
    • $for(変数名)$ $変数名$ $endfor$
    • 前者の 変数名 はリスト(と解釈できる)変数、後者の $変数名$ はそのリストの要素
  • if: 条件分岐構文
    • $if(変数名)$ 記述 $endif$
    • 変数名 が非空白文字列の場合に、記述 が出力される
    • else もある。elseif はPandoc 2.8で追加された(後述)
  • 構文や予約語ではないキーワード: 変数
    • Pandocがデフォルトで用意している変数がたくさんある
    • その他にもユーザが独自に定義できる(後述)

ちなみに $body$ が本文にあたります。
Pandocは入力文書を内部で独自の文書形式 (Pandoc AST) に変換した上で、この $body$ に本文データを流し込みます。

$body$ の中身はテンプレートで制御できないことに注意してください。
(制御したい場合は、コマンドラインオプションの使用やフィルタによるPandoc ASTの加工が必要です)

Pandoc 2.8 の新機能(テンプレート編)

つい先日(2019/11/23)にリリースされたPandoc 2.8では、テンプレートに関して多くの新機能が入りました。

Pandoc templates now support a number of new features that have been added in doctemplates: notably, elseif, it, partials, filters, and syntax to control nesting and reflowing of text. These changes make pandoc more suitable out of the box for generating plain-text documents from data in YAML metadata. It can create enumerated lists and even tabular structures.

Release pandoc 2.8 · jgm/pandoc

すべてを紹介できませんが、新機能の中でさっそく次の機能を使ってみます。

  • structured variable values
    • 変数をkey-valueの入れ子構造(連想配列)で利用できる
    • YAMLメタデータでは、YAMLのマッピング(連想配列)で定義できる
    • 連想配列の変数 foo に対して、「キー bar の値」に foo.bar でアクセスできる
  • it キーワード
    • for ループの中で使う。リストの要素に it でアクセスできる

Pandocにおけるメタデータと変数の違い

変数を扱う上で注意することは、メタデータとの違いです。

ざっくりとした意味あいとしては、次のような違いがあります。

  • メタデータ:文書自体が持つべきメタデータ(タイトル、著者名、スタイル定義など)
    • 文書に付随するもの
  • 変数:テンプレートで利用するための変数
    • 原則としてテンプレートのみで利用するもの

Pandoc上の扱いとしては、次の違いがあります。

  1. すべてのメタデータは、テンプレートにおける変数としても利用できる
    • テンプレートから見ると、両者の区別なくメタデータと変数の両方にアクセスできる
  2. Pandocは入力文書からメタデータを取得し、出力文書にメタデータを付加しようとする
    • 入力文書から読み取れるメタデータは、Pandocが解釈して内部文書 (AST) で保持している
    • このメタデータは、既定のテンプレートに記述されている通りに、出力文書のメタデータにも出力される
      • テンプレートからメタデータ部分(=変数)を削除したら、出力文書のメタデータも削除される
  3. Pandocフィルタはメタデータにアクセスできるが、テンプレートの変数にはアクセスできない
  4. メタデータはYAML形式のテキストまたはファイルで記述できる
    • この記述をYAMLメタデータブロックと呼ぶ

YAMLメタデータブロックは次のように書きます。
(詳細: Pandoc - Pandoc User’s Guide)

---
title: タイトル
author: 
- 著者1
- 著者2
abstract: |
  概要を
  複数行に書ける。
---

通常、YAMLメタデータブロックには区切りがあります。
--- で開始し、--- または ... で終了します。(個人的には両方 --- が好み)

YAMLの仕様としてはリストや連想配列、コメント、複数行などが可能です。
(参考: プログラマーのための YAML 入門 (初級編)

YAMLメタデータブロックをPandocに与える場合は、次の選択肢があります。

  1. コマンドラインオプション: -M / --metadata=
    • -M key=value--metadata=key:value みたいに与える (:= は両者で使える)
  2. 入力ファイルにPandoc's Markdownの一部として与える
    • ファイル形式は markdown 扱い
    • 上記のようにメタデータブロックをMarkdown文書の中に直接埋め込む
    • コマンドライン上でMarkdownファイルと並列して、YAMLファイルとして与えてもよい (内部でcatされるため、前後の区切りが必要)
  3. --metadata-file オプションで与える
    • YAMLファイル(とJSONファイル)を与えられる
    • 入力形式が markdown 以外でも利用できる
    • 前後の区切りはあってもなくてもよい(つまり厳密なYAML形式として与えてもよい)

一方、「メタデータではない変数」をPandocに与える場合には、実質的に次の方法しかありません。

  1. コマンドラインオプション: -V / --variable=
    • -v key=value--variable=key:value みたいに与える

(追記03:43 Pandoc 2.8では新機能の Default file (-d / --defaults) により、変数を外部ファイル (YAML) から与えることも可能になりました。メタデータも同様です)

このような性質から、実用上はメタデータとして使うつもりがなくても
「テンプレート変数を与えるために、YAMLメタデータブロックで変数(=メタデータ)を定義する」というテクニックが便利です。

YAMLメタデータから本の奥付を自動生成する

ようやく本題です。本の最後に下記の奥付を入れたいとします。

今回はLaTeXによるPDF出力を例にします。

(もっと基本的な話はこちらも参照: メモ: Pandoc+LaTeXで気軽に日本語PDFを出力する - Qiita

ファイル構成は下記の通りです。

.
├── book.pdf                   # 最終的に生成するPDF
├── okuduke.tex                # Pandocから生成された奥付用texファイル
└── src
    ├── body.md                # 本文Markdownファイル
    ├── metadata.yaml          # YAMLメタデータファイル
    └── template-okuduke.tex   # 奥付生成用のテンプレート

次の2段階でPDFを生成します。

  1. Pandocで奥付texファイル okuduke.tex を生成する
  2. 本文を含めたPDF book.pdf を生成する

具体的なコマンドではこうなります。(見やすくするために \ で改行してます)

# Step 1
$ pandoc -f markdown+raw_tex src/metadata.yaml --template=src/template-okuduke.tex -o okuduke.tex

# Step 2
$ pandoc -s -N -f markdown+raw_tex --pdf-engine=lualatex \
  -A ./okuduke.tex --metadata-file=src/metadata.yaml \
  src/body.md -o book.pdf
  • オプションスイッチのない引数は、入力ファイル扱い
    • Step 1の入力ファイルは src/metadata.yaml
    • Step 2の入力ファイルは src/body.md
  • -f markdown+raw_tex: Pandoc's Markdownを入力形式とする、ただしTeXコマンドはそのままLaTeXエンジンに渡す (+raw_tex 拡張)
  • --template=: テンプレートファイルの指定
  • -o: 出力ファイル
  • -N: 見出しに番号を付ける
  • --pdf-engine=lualatex: PDF生成のためにLuaLaTeXを利用する(日本語組版のために必要)
  • -A (--include-after-body): 本文の最後にファイルを挿入する
    • LaTeXでいうと \end{document} の手前
  • --metadata-file=: YAMLメタデータファイルを明示的に指定
  • -s: ヘッダ・フッタ付きの完全な文書を出力(standaloneモード)
    • Step 1では未指定(本文のみ、ヘッダ・フッタなし)
    • Step 2では指定しているが、実はなくてもよい(PDF出力では自動的に指定される)

Step 1: Pandocで奥付texファイル okuduke.tex を生成する

$ pandoc -f markdown+raw_tex src/metadata.yaml --template=src/template-okuduke.tex -o okuduke.tex

このコマンドを実行することで、奥付のデータが入った okuduke.tex というLaTeXファイルが生成されます。

Step 1では、本の奥付を生成するために、次の2つのファイルを入力に与えます。

  • YAMLメタデータファイル src/metadata.yaml
    • 奥付のデータ部分(+Step 2のメタデータ・変数)
  • 奥付生成用のテンプレート template-okuduke.tex
    • 奥付のスタイル部分

このように分離すると、「奥付のデータのみをYAMLで定義する」ことが可能になります。

ポイントは metadata.yaml を、Markdown文書 (-f markdown) だと思って入力に与えている ことです。
YAMLファイルのことを「本文が空でメタデータだけのMarkdown文書」という扱いにします。

(編集2019-12-03: classoptionb5jb5 に変更。コメント参照)
(編集2019-12-04: classoption ではなく papersizeb5 を記述するように変更。コメント参照)

metadata.yaml
---
title: すごい本
subtitle: すごいサブタイトル
author: 藤原 惟

# LaTeX用の設定
documentclass: bxjsbook
papersize: b5
classoption: pandoc
header-includes: |
  \usepackage{booktabs}

# 奥付用の設定 (LaTeXの書式で書く)
okuduke:
  header: |
    \thispagestyle{empty}
  message: |
    \begin{itemize}
    \item 引用・私的利用の範囲を超える無断転載・複製・複写・インターネット上への掲載(SNS・ネットオークション・フリマアプリ含む)を禁じます。
    \item 落丁・乱丁の場合は下記連絡先までご一報ください。
    \end{itemize}
  rev:
    - revision: 初版
      date: 2019年11月30日
  publisher: ソラソルファ
  contact: \verb|[email protected]|
  others:
    - name: Twitter
      content: \verb|@skyy_writing|
    - name: Webサイト
      content: \verb|https://example.com/|
  printing: 株式会社○○
---
template-okuduke.tex
\backmatter
$okuduke.header$

$for(okuduke.message)$
$okuduke.message$
$endfor$

\vspace*{\stretch{1}}

\begin{center}
\textsf{$title$}
$if(subtitle)$

$subtitle$
$endif$

\begin{tabular}{ll}
$for(okuduke.rev)$
$it.revision$ & $it.date$ \\
$endfor$
\end{tabular}

\begin{tabular}{ll} \toprule
    発行      & $okuduke.publisher$ \\
    著者      & $author$ \\
    連絡先    & $okuduke.contact$ \\
    $for(okuduke.others)$
    $it.name$ & $it.content$ \\
    $endfor$
    印刷      & $okuduke.printing$  \\ \bottomrule
\end{tabular}
\end{center}

余談:Pandoc 2.8による変更点

ちなみに、Pandoc 2.8 のstructured variable valuesによって

okuduke:
  header: |
    \thispagestyle{empty}
  publisher: ソラソルファ
  contact: \verb|[email protected]|

のように、入れ子(連想配列)として変数を記述できるようになりました。

これまでは次のように、それぞれ別の変数としてベタに書く必要がありました。

okuduke-header: |
  \thispagestyle{empty}
okuduke-publisher: ソラソルファ
okuduke-contact: \verb|[email protected]|

また同様に、it キーワードによって次のように書けるようになりました。

$for(okuduke.rev)$
$it.revision$ & $it.date$ \\
$endfor$

従来は、次のように書く必要がありました。(okuduke.revも以前の仕様に直します)

$for(okuduke-rev)$
$okuduke-rev.revision$ & $okuduke-rev.date$ \\
$endfor$

Step 2: 本文を含めたPDF book.pdf を生成する

$ pandoc -s -N -f markdown+raw_tex --pdf-engine=lualatex \
  -A ./okuduke.tex --metadata-file=src/metadata.yaml \
  src/body.md -o book.pdf

この段階では「Markdown文書を入力とし、LuaLaTeXを内部で用いてPDF文書を出力する」処理を行います。

Step 1の生成物(奥付のLaTeXファイル)である okuduke.tex を、-Aオプションで読み込みます。

これにより、自動生成された奥付を含めて、完全な本の形でPDFを生成できます。やったね!

※ ただし、実際に同人誌を印刷所に入稿する際には、「通しノンブルを付ける」などもう少し面倒なことがあります。
この例は奥付のページ番号を「なし」(\thispagestyle{empty})にしているので注意してください。

ソースコードあります

GitHubにソースコードを上げてます。

  • Pandoc 2.8以前の仕様のファイルもアップしてます
    • metadata-old.yaml
    • template-okuduke-old.tex

以上です。