AsciiDoc+Pandoc+LaTeXで技術同人誌を作る方法を検証したが断念した記録
動機
次のコミケで出す技術同人誌をつくるときの手順を検証したいというのが動機。今まで数冊の同人誌をLaTeXでつくっており、Markdownで原稿を書いてPandocでTeXファイルに変換して最終的にLuaLaTeXで組版してPDFにするフローは確立されている。
ただ、前回の同人誌で次のような問題があった。
キャラクターの台詞があって対話するような本をつくるためには、「台詞の吹き出し」のような、普通ではない要素を使う必要が出てくる。このようなカスタム要素は、LaTeXのレベルでは環境(environment)を使えば実現できるが、それを原稿たるMarkdownで表現する方法が問題になってくる。
実は、PandocではMarkdownにLaTeXのコマンドをそのまま書いてもよい。ただ、ここで問題がある。
\begin{kaiwa}
ここに [リンク](http://example.com) をつけます。
\end{kaiwa}
のように書いた際に、\begin{kaiwa}
と\end{kaiwa}
に囲まれた部分はMarkdown記法ではなくTeX記法で書かれていると想定されてしまう。つまり、台詞枠といった特殊な枠の中でMarkdown記法が使えない。
PandocのMarkdownではLaTeX記法だけでなくHTML記法も書けるのであるが、HTMLのほうについては markdown_in_html_blocks
という、その名の通りHTMLで囲まれた中でもMarkdown記法を使えるようにするオプションが存在する。が、何故かLaTeX記法のほうにはそういうオプションがない。
その対応として、前回は
【会話始】
ここに [リンク](http://example.com) をつけます。
【会話終】
のように書いて、Pandocで生成されたTeXファイルに更にPerlで正規表現置換(【会話始】
を\begin{kaiwa}
に置換する等)をかけることで実現した。
が、あまりにもアドホックで綺麗な手段とはいいがたい。
なので、AsciiDocでできないかと考えた。AsciiDoc入門- Qiita に、AsciiDocでは「記法を拡張する方法が言語仕様に定められている」と書いてあったので、そこに期待した。
想定アーキテクチャ
基本的には、今までの
- Markdown --(Pandoc)--> TeXファイル --(LuaLaTeX)--> PDFファイル
の流れのうち、後半部分は踏襲する方向で考える。AsciiDoctorにもPDFを出力する機能があるようだが、まともな品質のPDFを(特に日本語で)つくろうとするのであれば、基本的にLaTeXを使わないと厳しいのは経験で学んでいる。
したがって、以下の流れで変換することを考えた。
- AsciiDocファイル --(AsciiDoctor)--> DocBookファイル --(Pandoc)--> TeXファイル --(LuaLaTeX)--> PDFファイル
AsciiDocファイルの入力にPandocは対応していないので、一旦DocBookファイルを経由してPandocに持っていくことにする。DocBookファイルはXMLの一種で、たしかオライリーの本を組むのに使われている。AsciiDoctorは他にHTMLでも出力できるのだが、HTMLだと不要なヘッダなどがついてしまって面倒そうだし、DocBookのほうが紙の書籍に向いていそうだということで、Docbook形式を経由することにした。
結果
やりたかったことについて書いたので、ここで結果を書くと、「AsciiDocで定義した台詞枠のような特殊な枠を、DocBook形式を経由してPandocでTeX形式にすることはできない(そのような枠であるという情報が落とされてしまう)」らしいことが判明した。
というのは、AsciiDocで特殊な枠のようなものを定義するのには「ロール」というものを使うのだが、このロールの情報が、AsciiDocファイル --> DocBookファイルの変換では保たれるものの、DocBookファイル --> LaTeXファイルの変換の際に落とされてしまう。
この問題は https://github.com/jgm/pandoc/issues/9089 でIssueが建てられており、まだOpenな状態。Pandocのフィルタでなんとかならないか見てみたものの、PandocのASTになる時点でロールの状態が消えており(これは pandoc -f docbook -t native sample.xml
で確認できる)、フィルタでは無理そう。そうなると、Pandocのカスタムリーダを書く(Luaで書けるが、たぶん一からDocBookのパース処理を書く必要がある)か、PandocにあるDocBookのリーダ(Haskellで書かれている)を改造してPandocを再ビルドするようなことになる。これは茨の道である。
一方、AsciiDocを使おうと考えたきっかけであるところの特殊な枠の記載方法であるが、AsciiDocを使おうとした調査の副産物で、Markdownであっても実現できそうだという目処がたった。というのも、PandocのフィルタがLuaで簡単に書けることが判明したので、おそらく <div class="kaiwa">ここが会話</div>
をMarkdownに書いて、フィルタでこれをLaTeXのkaiwa
環境に変換するといった処理を書けば、やりたかったことが実現できそうなのである。Markdownの fenced_divs
拡張 を使えば、たぶんHTMLを書く必要もない。
というように、AsciiDocでやりたかったことをやるのが厳しそうと判明し、Markdownで実現できそうなことが分かったため、AsciiDocの導入は断念する。なお、ファイルの結合はTeXファイルレベルで行っているなど、他にAsciiDocの利点とされる部分はMarkdownでも現状困っていないという事情を付記しておく。
ここからは、試したことを記録しておく。今回の私の用途には適さないという判断になったが、特殊な枠が必要ない場合や将来的に問題が解決した場合には、AsciiDocからPDFを作る方法の事例として役立つかもしれないので。
環境構築:DevContainer
LaTeXのほかに、Pandoc(Haskell製)とAsciiDoctor(Ruby製)を入れる必要があり、やや環境構築が面倒である。
WindowsデスクトップとMacノートの両方で執筆できたほうが嬉しいので、今回は全面的にコンテナを使うことにした。更に、VSCodeの拡張機能の設定を環境ごとで行う面倒さの解消も意図して、DevContainerを導入してみた。
基本的には https://github.com/Paperist/texlive-ja を使わせていただくが、DevContainerでは mcr.microsoft.com/devcontainers/base:bookworm ベースのものを使ったほうがよさそうな気がするので、texlive-ja のコンテナから必要なファイルだけをコピーする形にした。なお、Make代わりに今回は Taskfile を使うことにしたので、Taskfileのインストール用にGoのコンテナも用意した。一応全部Debian 12 (Bookworm)のコンテナで統一してある。
{
"name": "Debian",
"build": {
"dockerfile": "Dockerfile"
},
"customizations": {
"vscode": {
"extensions": [
"EditorConfig.EditorConfig",
"asciidoctor.asciidoctor-vscode"
]
}
}
}
# TexLiveインストール用コンテナ
# cf. https://github.com/Paperist/texlive-ja/blob/main/debian/Dockerfile
FROM ghcr.io/paperist/texlive-ja:debian AS texlive-installer
# Go系ツールインストール用コンテナ
FROM golang:1.23-bookworm AS golang
RUN go install github.com/go-task/task/v3/cmd/task@latest
# メインのコンテナ
FROM mcr.microsoft.com/devcontainers/base:bookworm
WORKDIR /workdir
ENV PATH /usr/local/bin/texlive:$PATH
COPY /usr/local/texlive /usr/local/texlive
COPY /go/bin/task /usr/local/bin/task
RUN ln -sf /usr/local/texlive/*/bin/* /usr/local/bin/texlive
RUN apt-get update
RUN apt-get install -y asciidoctor pandoc
基本的な流れの説明
まずは、AsciiDocからPDFまでの流れを簡単に書く。
AsciiDocファイル
最初に、AsciiDocのファイルを用意する。
== サンプルの世界
ここは、どこでしょうか?
**不思議の国**です。ここでは、どんな突飛な文章も許されます。
ええ、そうです。あなたはここでは自由です。
NOTE: 脚注段落は補足情報を示すものです。
段落冒頭のラベルによって脚注の種類を使い分けることができます。
[.yomi]
====
これはカスタムブロックです。詠ちゃんのためのブロックです。
====
ソースコードを書いてみましょう。まずは、Rubyのプログラムを示してしてみます。
[source,ruby]
----
require 'sinatra'
get '/hi' do
"Hello World!"
end
----
AsciiDoc --> DocBook の変換
これを、AsciiDoctorでDocBook形式に変換する。
asciidoctor -b docbook sample.adoc
変換して出てくるDocBookファイルは以下のようなものである。なお、拡張子は.xml
となる。
<?xml version="1.0" encoding="UTF-8"?>
<?asciidoc-toc?>
<?asciidoc-numbered?>
<article xmlns="http://docbook.org/ns/docbook" xmlns:xl="http://www.w3.org/1999/xlink" version="5.0" xml:lang="en">
<info>
<title>サンプルの世界</title>
<date>2024-09-14</date>
</info>
<section xml:id="_サンプルの世界">
<title>サンプルの世界</title>
<simpara>ここは、どこでしょうか?</simpara>
<simpara><emphasis role="strong">不思議の国</emphasis>です。ここでは、どんな突飛な文章も許されます。
ええ、そうです。あなたはここでは自由です。</simpara>
<note>
<simpara>脚注段落は補足情報を示すものです。
段落冒頭のラベルによって脚注の種類を使い分けることができます。</simpara>
</note>
<informalexample role="yomi">
<simpara>これはカスタムブロックです。詠ちゃんのためのブロックです。</simpara>
</informalexample>
<simpara>ソースコードを書いてみましょう。まずは、Rubyのプログラムを示してしてみます。</simpara>
<programlisting language="ruby" linenumbering="unnumbered">require 'sinatra'
get '/hi' do
"Hello World!"
end</programlisting>
</section>
</article>
DocBook --> LaTeX の変換
これを、PandocでLaTeX形式に変換する。
なお、AsciiDocの原稿を章ごとに用意して、それを親である別のTeXファイルから読み取って使う想定なので、--standalone
オプションはつけない。Pandocテンプレート:デフォルトのやつを使うべきか否か という記事がこのあたりについて説明してくれているのだが、この記事でいうところの (c) の方法である。私もこの (c) の方法が一番だと思う(技術同人誌をつくるなら)。
Pandocのコマンドは以下のようになる。
pandoc -f docbook -t latex \
--top-level-division=chapter \
-o sample.tex sample.xml
すると、以下のようなTeXファイルが生成される。
\hypertarget{_ux30b5ux30f3ux30d7ux30ebux306eux4e16ux754c}{%
\chapter{サンプルの世界}\label{_ux30b5ux30f3ux30d7ux30ebux306eux4e16ux754c}}
ここは、どこでしょうか?
\textbf{不思議の国}です。ここでは、どんな突飛な文章も許されます。
ええ、そうです。あなたはここでは自由です。
脚注段落は補足情報を示すものです。
段落冒頭のラベルによって脚注の種類を使い分けることができます。
これはカスタムブロックです。詠ちゃんのためのブロックです。
ソースコードを書いてみましょう。まずは、Rubyのプログラムを示してしてみます。
\begin{Shaded}
\begin{Highlighting}[]
\FunctionTok{require} \VerbatimStringTok{\textquotesingle{}sinatra\textquotesingle{}}
\NormalTok{get }\VerbatimStringTok{\textquotesingle{}/hi\textquotesingle{}} \ControlFlowTok{do}
\StringTok{"Hello World!"}
\ControlFlowTok{end}
\end{Highlighting}
\end{Shaded}
LaTeX --> PDF の変換
さて、さっきのsample.tex
だけではLaTeXでコンパイルできないので、これを読み込むための親となるTeXファイルを用意する。名前はmain.tex
とする。
\PassOptionsToPackage{unicode}{hyperref}
\PassOptionsToPackage{hyphens}{url}
% jlreqを使う
\documentclass[book,paper=a5paper,jafontsize=12Q]{jlreq}
\usepackage{longtable,booktabs,array}
\usepackage{calc}
\usepackage[]{tcolorbox}
\tcbuselibrary{breakable,theorems,skins}
\usepackage{xcolor}
\IfFileExists{xurl.sty}{\usepackage{xurl}}{} % add URL line breaks if available
\IfFileExists{bookmark.sty}{\usepackage{bookmark}}{\usepackage{hyperref}}
\hypersetup{
hidelinks,
pdfcreator={LaTeX via pandoc}}
\urlstyle{same} % disable monospaced font for URLs
\usepackage{color}
\usepackage{fancyvrb}
% ここからソースコードを挿入するための設定
\newcommand{\VerbBar}{|}
\newcommand{\VERB}{\Verb[commandchars=\\\{\}]}
\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\{\}}
% Add ',fontsize=\small' for more characters per line
\usepackage{framed}
\definecolor{shadecolor}{RGB}{248,248,248}
\newenvironment{Shaded}{\begin{snugshade}}{\end{snugshade}}
\newcommand{\AlertTok}[1]{\textcolor[rgb]{0.94,0.16,0.16}{#1}}
\newcommand{\AnnotationTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{\textbf{\textit{#1}}}}
\newcommand{\AttributeTok}[1]{\textcolor[rgb]{0.77,0.63,0.00}{#1}}
\newcommand{\BaseNTok}[1]{\textcolor[rgb]{0.00,0.00,0.81}{#1}}
\newcommand{\BuiltInTok}[1]{#1}
\newcommand{\CharTok}[1]{\textcolor[rgb]{0.31,0.60,0.02}{#1}}
\newcommand{\CommentTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{\textit{#1}}}
\newcommand{\CommentVarTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{\textbf{\textit{#1}}}}
\newcommand{\ConstantTok}[1]{\textcolor[rgb]{0.00,0.00,0.00}{#1}}
\newcommand{\ControlFlowTok}[1]{\textcolor[rgb]{0.13,0.29,0.53}{\textbf{#1}}}
\newcommand{\DataTypeTok}[1]{\textcolor[rgb]{0.13,0.29,0.53}{#1}}
\newcommand{\DecValTok}[1]{\textcolor[rgb]{0.00,0.00,0.81}{#1}}
\newcommand{\DocumentationTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{\textbf{\textit{#1}}}}
\newcommand{\ErrorTok}[1]{\textcolor[rgb]{0.64,0.00,0.00}{\textbf{#1}}}
\newcommand{\ExtensionTok}[1]{#1}
\newcommand{\FloatTok}[1]{\textcolor[rgb]{0.00,0.00,0.81}{#1}}
\newcommand{\FunctionTok}[1]{\textcolor[rgb]{0.00,0.00,0.00}{#1}}
\newcommand{\ImportTok}[1]{#1}
\newcommand{\InformationTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{\textbf{\textit{#1}}}}
\newcommand{\KeywordTok}[1]{\textcolor[rgb]{0.13,0.29,0.53}{\textbf{#1}}}
\newcommand{\NormalTok}[1]{#1}
\newcommand{\OperatorTok}[1]{\textcolor[rgb]{0.81,0.36,0.00}{\textbf{#1}}}
\newcommand{\OtherTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{#1}}
\newcommand{\PreprocessorTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{\textit{#1}}}
\newcommand{\RegionMarkerTok}[1]{#1}
\newcommand{\SpecialCharTok}[1]{\textcolor[rgb]{0.00,0.00,0.00}{#1}}
\newcommand{\SpecialStringTok}[1]{\textcolor[rgb]{0.31,0.60,0.02}{#1}}
\newcommand{\StringTok}[1]{\textcolor[rgb]{0.31,0.60,0.02}{#1}}
\newcommand{\VariableTok}[1]{\textcolor[rgb]{0.00,0.00,0.00}{#1}}
\newcommand{\VerbatimStringTok}[1]{\textcolor[rgb]{0.31,0.60,0.02}{#1}}
\newcommand{\WarningTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{\textbf{\textit{#1}}}}
\setlength{\emergencystretch}{3em} % prevent overfull lines
% ここまでソースコードを挿入するための設定
% ここから本文
\begin{document}
\input{sample}
\end{document}
見ての通り、かなり長い。sample.tex
の読み込みをしているのは最後の3行なのであるが、sample.tex
のようなPandcで生成されたTeXファイルをコンパイルするには様々なパッケージや設定をプリアンブルでいれないといけない(このあたりはAsciiDocとは関係なく、MarkdownからLaTeXを生成するとしても必要な作業である)。
当然手で書くと大変なので、Pandocのデフォルトのテンプレートから持ってくるとよい。以下のようなコマンドで、雑なテキストを--standalone -t latex
で処理すると、Pandocのデフォルトのテンプレートが吐き出される。なお、ソースコード関係のスタイルは --highlight-style
の指定で変更できる。Pandoc Syntax Highlighting Examplesというサイトが各スタイルのサンプル画像を載せてくれている。
echo -e "~~~c\nint\n~~~" | pandoc --standalone -t latex --pdf-engine=lualatex --highlight-style=pygments
あとは、この main.tex
をLuaLaTeXで普通にコンパイルする。
lualatex main.tex
これで、以下のような main.pdf
が生成される。
これで、AsciiDocからPDFを生成するまでの流れは一旦検証できた。
Taskfileによる自動化
以上のようなPDFをつくるまでの流れをTaskfileで自動化する。細かい説明は省くが、何かの役に立つかもしれないので書いたものをそのまま貼っておく。
version: '3'
vars:
# AsciiDocファイルの格納先
SRC_DIR: src
# LaTeXファイルの格納先
LATEX_SRC_DIR: latex
# ビルド結果の出力先
BUILD_DIR: build
# 親となるLaTeXファイルの名前
MAIN_LATEX_BASE_NAME: main
# 生成したPDFファイルの名前
PDF_FILE_NAME: main
tasks:
build:
desc: |
AsciiDoc から PDF を生成する
vars:
# ソースファイルの拡張子を除いた名前
SOURCE_FILE_NAMES:
sh: perl -le 'for (glob("./src/*.adoc")) { s!.*/!!; s/\.adoc$//; print }'
cmds:
# AsciiDoc から DocBook への変換
- task: make-docbook-from-asciidoc
for: { var: SOURCE_FILE_NAMES }
vars:
BASENAME: "{{.ITEM}}"
# DocBook から LaTeX への変換
- task: make-latex-from-docbook
for: { var: SOURCE_FILE_NAMES }
vars:
BASENAME: "{{.ITEM}}"
# LaTeX から PDF への変換
- task: make-pdf-from-latex
make-docbook-from-asciidoc:
requires:
vars: [BASENAME]
cmds:
- mkdir -p {{.BUILD_DIR}}/docbook
- asciidoctor -b docbook -D {{.BUILD_DIR}}/docbook {{.SRC_DIR}}/{{.BASENAME}}.adoc
make-latex-from-docbook:
requires:
vars: [BASENAME]
cmds:
- mkdir -p {{.BUILD_DIR}}/latex
- |
pandoc -f docbook -t latex \
--top-level-division=chapter \
--lua-filter=pandoc/admotion_filter.lua \
--lua-filter=pandoc/custom_block_filter.lua \
-o {{.BUILD_DIR}}/latex/{{.BASENAME}}.tex {{.BUILD_DIR}}/docbook/{{.BASENAME}}.xml
make-pdf-from-latex:
cmds:
- mkdir -p {{.BUILD_DIR}}/pdf
- cp latex/{{.MAIN_LATEX_BASE_NAME}}.tex {{.BUILD_DIR}}/latex/{{.MAIN_LATEX_BASE_NAME}}.tex
- cd {{.BUILD_DIR}}/latex && lualatex {{.MAIN_LATEX_BASE_NAME}}.tex
- cp {{.BUILD_DIR}}/latex/{{.MAIN_LATEX_BASE_NAME}}.pdf {{.BUILD_DIR}}/pdf/{{.PDF_FILE_NAME}}.pdf
clean:
desc: |
ビルド結果を削除する
cmds:
- rm -rf {{.BUILD_DIR}}
このように書いておくと、
task build
だけでPDFが生成されるようになる。
なお、--lua-filter
についてはまだ説明していないが、最終的には使うことになるので含めておいた。また、Taskfileを使うのであれば - defer: rm -rf {{.BUILD_DIR}}/docbook
とか書いて中間ファイルを削除するのが(たぶん)定石なのだが、デバッグ用に中間ファイルも見たいので今回は task clean
で明示的に削除することにした。
ディレクトリ構成
上記Taskfileは以下のようなディレクトリ構成をとることを想定している。
% tree
.
├── Taskfile.yml
├── build
│ ├── docbook
│ │ └── sample.xml
│ ├── latex
│ │ ├── sample.tex
│ │ ├── main.aux
│ │ ├── main.log
│ │ ├── main.out
│ │ ├── main.pdf
│ │ └── main.tex
│ └── pdf
│ └── main.pdf
├── latex
│ └── main.tex
├── pandoc
│ ├── admotion_filter.lua
│ └── custom_block_filter.lua
└── src
└── sample.adoc
自分で用意するファイルは、以下のように各フォルダに格納する想定である。
-
src
: AsciiDocの原稿ファイルを入れる。 -
latex
: 親となるTeXファイルや、その他スタイルファイルなどを入れる。 -
pandoc
: Pandocのフィルタを書いたらここに入れる。
実際に本をつくるなら更に画像フォルダなどが必要になるだろうが、今回の検証ではそこまでやっていないので省略する。
問題点
上記手順で一応PDFをつくることはできているのだが、少なくとも以下の2点に問題がある。
-
NOTE:
の記述が効いていない - カスタムブロックの記述が効いていない
それぞれを説明する。
NOTE:
の記述が効いていない
サンプルのAsciiDocでは以下のように書いている部分がある。
NOTE: 脚注段落は補足情報を示すものです。
段落冒頭のラベルによって脚注の種類を使い分けることができます。
ここの NOTE:
はただの文字追加ではなく、「ノートを記載する枠をつくる」ことを意図している。Asciidoctor 文法クイックリファレンス(日本語訳) の「1.4 脚注」のところの例を見れば分かるように、「ℹのような絵を入れた枠」を出すことを意図するマークアップである。
こういうのはAsciiDocの用語では admonition (警句)と呼ばれていて、以下の5種類がある。
NOTE
TIP
IMPORTANT
CAUTION
WARNING
このように、技術書でありがちな注意書きを書くのに便利そうな記法[1]なのだが、これがPDFの出力に一切反映されていない。
なぜ消えているのかを、中間出力を見て調べてみる。
<note>
<simpara>脚注段落は補足情報を示すものです。
段落冒頭のラベルによって脚注の種類を使い分けることができます。</simpara>
</note>
脚注段落は補足情報を示すものです。
段落冒頭のラベルによって脚注の種類を使い分けることができます。
上記を見て分かるように、DocBookの段階では<note>
タグとして残っているのが、LaTeXの段階で消えてしまっている。要するにPandocが情報を落としてしまっている。
そのため、Pandocの処理に介入すればなんとかなる可能性がある。実際、Pandocにはフィルタという機能があり、それを使うことで問題を解消できる(後述)。
カスタムブロックの記述が効いていない
サンプルのAsciiDocでは以下のように書いている部分がある。
[.yomi]
====
これはカスタムブロックです。詠ちゃんのためのブロックです。
====
これは、4つの=
で囲ったブロック(こういうの)に対して、AsciiDocのロール記法([.rolename]
を前置する)で名前をつけられるという記法である。どうも、AsciiDocではカスタムブロック(と呼ぶのか知らないが、最初に書いた「台詞枠」のような特殊な枠)をこの記法で作るのが流儀らしい。AsciiDoc入門- Qiita の「記法を拡張する方法が言語仕様に定められている」というのも、おそらくこの手法を指している。
なのであるが、これも先程のadmonitionと同様にPDF出力に反映されていない。こちらも中間出力を見てみる。
<informalexample role="yomi">
<simpara>これはカスタムブロックです。詠ちゃんのためのブロックです。</simpara>
</informalexample>
これはカスタムブロックです。詠ちゃんのためのブロックです。
というわけで、こちらもDocBook形式では残っていた情報がLaTeXに変換したときに落ちている。なので、admonitionと同様の方法で解決できる……かと思ったらできなかった。それがこの記録(スクラップ)の本題である。
-
最近はGitHub Flavored Markdownにも似たような記法が入っている。 ↩︎
問題点の解決策:Pandocのフィルタ
admonitionの反映(成功)
NOTE:
の記述つまりadmonitionの情報をPDFに反映するのは、Pandocのフィルタを使えばできた。
Pandocのフィルタとはなんぞやということは、pandocフィルターおぼえがき(前編) に出てくる図を見るのが一番早い。要するに、Pandocの抽象構文木(AST)の変換処理を書くことができる機能である。
昔はPandocフィルタはASTを示すJSONの変換スクリプトであったため、PythonなりRubyなりのプログラミング言語で書かれていたのだが、Pandoc 2.0以降ではLuaフィルタというものが追加された。PandocにはLua処理系が組み込まれているので、Luaで書けるものであればLuaフィルタで書いてしまうのが簡単である。
で、具体的にはどのようになるかであるが、DocBookの <note></note>
をLaTeXの \begin{admnote}\end{admnote}
に変換するフィルタは以下のように書ける[1]。
function Div(el)
if el.classes:includes('note') then
-- 'note' クラスが含まれている場合、それをLaTeXのadmnote環境に変換
return {
pandoc.RawBlock('latex', '\\begin{admnote}'),
el,
pandoc.RawBlock('latex', '\\end{admnote}')
}
end
end
なんとも簡単である。これを、以下のようにPandocでの処理時に --lua-filter
オプションで渡してやる。
pandoc -f docbook -t latex \
--top-level-division=chapter \
--lua-filter=pandoc/admotion_filter.lua \
-o sample.tex sample.xml
こうすると、LaTeXファイルの出力は以下のように変化する。
\begin{admnote}
脚注段落は補足情報を示すものです。
段落冒頭のラベルによって脚注の種類を使い分けることができます。
\end{admnote}
これで、admnote
環境を使うようになったので、あとは、main.tex
のプリアンブルでadmnote
環境の定義を行う。例えば、tcolorbox
パッケージを使って以下のように定義する。
\usepackage[]{tcolorbox}
\tcbuselibrary{breakable,theorems,skins}
\newtcolorbox{admnote}[1][]{enhanced,
detach title,before upper={{\hskip-1em《備考》}},
before skip=2mm,after skip=3mm,fontupper={\small},
boxrule=0.4pt,left=5mm,right=2mm,top=1mm,bottom=1mm,
colback=gray!20,
colframe=gray!10!black,
sharp corners,rounded corners=southeast,arc is angular,arc=3mm,
drop fuzzy shadow,#1}
これをコンパイルしてPDFにすると、以下のようにNOTE
が反映された出力が得られる。
カスタムブロックの反映(失敗)
では、同じようにカスタムブロックもフィルタでいいかんじにできないのか?
これは、現状できなそうである。というのも、そもそもPandocのASTにカスタムブロック実現に必要なロール情報が入っていないからである。
というのは、以下のようなコマンドでPandocのASTを出力すると確認できる。
pandoc -f docbook -t native sample.xml
これで出力されるのは、以下のようなテキストである。
[ Header
1
( "_\12469\12531\12503\12523\12398\19990\30028" , [] , [] )
[ Str "\12469\12531\12503\12523\12398\19990\30028" ]
, Para
[ Str
"\12371\12371\12399\12289\12393\12371\12391\12375\12423\12358\12363\65311"
]
, Para
[ Strong [ Str "\19981\24605\35696\12398\22269" ]
, Str
"\12391\12377\12290\12371\12371\12391\12399\12289\12393\12435\12394\31361\39131\12394\25991\31456\12418\35377\12373\12428\12414\12377\12290"
, SoftBreak
, Str
"\12360\12360\12289\12381\12358\12391\12377\12290\12354\12394\12383\12399\12371\12371\12391\12399\33258\30001\12391\12377\12290"
]
, Div
( "" , [ "note" ] , [] )
[ Para
[ Str
"\33050\27880\27573\33853\12399\35036\36275\24773\22577\12434\31034\12377\12418\12398\12391\12377\12290"
, SoftBreak
, Str
"\27573\33853\20882\38957\12398\12521\12505\12523\12395\12424\12387\12390\33050\27880\12398\31278\39006\12434\20351\12356\20998\12369\12427\12371\12392\12364\12391\12365\12414\12377\12290"
]
]
, Div
( "" , [ "informalexample" ] , [] )
[ Para
[ Str
"\12371\12428\12399\12459\12473\12479\12512\12502\12525\12483\12463\12391\12377\12290\35424\12385\12419\12435\12398\12383\12417\12398\12502\12525\12483\12463\12391\12377\12290"
]
]
, Para
[ Str
"\12477\12540\12473\12467\12540\12489\12434\26360\12356\12390\12415\12414\12375\12423\12358\12290\12414\12378\12399\12289Ruby\12398\12503\12525\12464\12521\12512\12434\31034\12375\12390\12375\12390\12415\12414\12377\12290"
]
, CodeBlock
( "" , [ "ruby" ] , [] )
"require 'sinatra'\n\nget '/hi' do\n \"Hello World!\"\nend"
]
これを見ると、admotionのところは以下のようにnote
というテキストが入っている。
, Div
( "" , [ "note" ] , [] )
一方、カスタムブロックに相当するところは以下のようになっており、ロール名(今回はyomi
)は存在しない。
, Div
( "" , [ "informalexample" ] , [] )
というわけで、PandocのASTをいじるものであるフィルタでは原理的にどうしようもない。
この状況を改善するために、PandocのASTにロール情報を含めてほしいという要望が https://github.com/jgm/pandoc/issues/9089 にあるのだが、2024年9月現在ではまだ実現されていない……。
-
note
だとLaTeXの別のコマンドと名前が衝突してエラーになるので、admonitionの略のadm
を前置してadmnote
という名前にしたした。 ↩︎
その他の解決策の案
フィルタでは無理だとして、DocBookのロール情報をどうにかしてLaTeXに出力できないのか?
pandocフィルターおぼえがき(前編)の図を見ると、ASTを作るのはPandocのリーダーの役目であることがわかる。では、リーダーをつくればいいことになる。が、それは相当大変そうである。
https://pandoc.org/custom-readers.html で説明されているように、Pandocにはリーダーを自分で書く方法もある。のだが、例を見る限りはおそらくDocBookの構文解析処理を全部Luaで書く必要がある。
別の方法は、既存のDocBookのリーダーを改造する方法である。
ファイル自体は https://github.com/jgm/pandoc/blob/main/src/Text/Pandoc/Readers/DocBook.hs にあるのだが、私はHaskellが読めないし、Pandocの自前ビルドも面倒そうだし、パッチを当てて使うようなことをするとバージョンアップで動かなくなる懸念も大きいのでできる限りやりたくない。
……と、実現方法として思い浮かぶ案にかかる労力を考えると、この道は一旦引き返したほうがよさそうだと判断した。
Markdownによる最初の問題の解決
と、断念する結果になったのでAsciiDocの検証は無駄だったかというとそうではない。
Pandocのフィルタを使えば、当初の課題(台詞のような特殊枠を書きたい)をMarkdownで解決できることが分かったのである。
というのも、以下のようにMarkdownを書けばいい。
## Markdownだよ
台詞枠はつぎのようにつくれる。
::::: {.yomi}
ここがわたしの台詞です。
:::::
いけた?
これは、PandocのMarkdownにある native_divs
という拡張(デフォルトで有効)で、上記のように書くとHTMLにしたときに<div class="yomi">
で囲むといった処理をしてくれる。
こう書いた場合のASTを見てみる。
pandoc -f markdown -t native sample.md
[ Header
2
( "markdown\12384\12424" , [] , [] )
[ Str "Markdown\12384\12424" ]
, Para
[ Str
"\21488\35422\26528\12399\12388\12366\12398\12424\12358\12395\12388\12367\12428\12427\12290"
]
, Div
( "" , [ "yomi" ] , [] )
[ Para
[ Str
"\12371\12371\12364\12431\12383\12375\12398\21488\35422\12391\12377\12290"
]
]
, Para [ Str "\12356\12369\12383\65311" ]
]
yomi
がちゃんと入っている!
というわけで、あとはnote
のときと同じようなフィルタを書けば好きにできる。これで万事解決!!
(実際に、このようにMarkdownで特殊枠がつくれることを確認した)
なお、この native_divs
を入れ子にできるのか等は不明だが、入れ子にできないとしてもそのときはHTMLでdivを書けばよい。 markdown_in_html_blocks
拡張で、HTMLの中には普通にMarkdownを書けるのだから。
感想
今回の検証結果をひとことでまとめれば、「PandocでDocBook形式を処理するには罠が多い」ということである。AsciiDocを導入しようとした結果遭遇した問題ではあるのだが、AsciiDoc自体には問題はない。
というわけで、Pandoc+LaTeXでやろうとするのであれば、入力にAsciiDocを使うのは難儀そうだ。AsciiDocを使うのであれば、HTMLに出力するか、直接PDFを出力する(asciidoctor-pdf)か、もしくは直接LaTeXに出力する(asciidoctor-latex)かを試したほうがいいかもしれない。ただ、それらの選択肢にもそれぞれの罠がありそうな予感がする。私は別にAsciiDocにこだわりがあるわけではないのでこれらの検証はしない(Markdownを使う)が、AsciiDocの記法や機能が好きな方は検証してみてほしい。
Pandocはいろいろ変換できるわけだが、やはりXMLよりもMarkdownやHTMLのほうがサポートがしっかりしているのだろう、という雑な感想を得た。