はじめに
GitBookはMarkdownなどで記述したドキュメントをHTMLやPDF、EPUBなどへ変換するツールである。GitBookにはPDFを作成する機能が標準で備わっているが、それの組版のできがそれほどよくなかったので、今回はPandocとLaTeXなどを用いて美しい組版のPDFドキュメントを作成することを目指す。また、このドキュメントはTravis CI上でコンパイルができるようにする。
この記事にある内容を行うことで、次のようなPDFが作成できる。
また、今回の成果は次のリポジトリにある。
対象としたドキュメント
今回はScala TextというドキュメントのPDF版を作成することにした。このドキュメントはプログラム言語Scalaの入門テキストであるが、Markdownに埋め込まれたScalaコードをtutというプログラムで実行していることが特徴である。
変換
次のような流れでPDFへ変換する。
- sbtでtutを実行し、Scalaコードの実行結果が埋め込まれたMarkdownを生成
-
book.json
をコピー - 画像ファイルをコピー
- ソースコードをコピー
- SVG形式の画像をInkscapeでPDFへ変換
- PandocでLaTeXへ変換
- LuaLaTeXで
book.json
を解析し、目次と本文を生成 - LuaLaTeXファイルのコンパイル
この流れは、次のシェルスクリプトにまとまっている。
sbtの実行
これは次のようにsbt tut
をScala Textのルートディレクトリで実行するだけでよい。
$ cd ./scala_text
$ sbt tut
$ cd -
終了すると、./scala_text/gitbook/
にScalaのコードを実行した結果が埋め込まれたMarkdownファイルが生成される。
book.json
をコピー
book.json
とはGitBookのメタ情報を表わすJSONファイルであり、Scala Textでは次のようになっている。
{
"structure": {
"readme": "INTRODUCTION.md",
"summary": "SUMMARY.md"
},
"plugins": [
"-search",
"include-codeblock",
"japanese-support",
"footnote-string-to-number",
"anchors",
"regexplace"
],
"pluginsConfig": {
"regexplace": {
"substitutes": [
{"pattern": "<!-- begin answer id=\"(.*)\" style=\"(.*)\" -->", "flags": "g", "substitute": "<div><button type=\"button\" id=\"$1_show_answer_button\" style=\"display:block\" onclick=\"document.getElementById('$1').style.display='block'; document.getElementById('$1_show_answer_button').style.display='none'; document.getElementById('$1_hide_answer_button').style.display='block'; \">解答例を表示する</button><button type=\"button\" id=\"$1_hide_answer_button\" style=\"display:none\" onclick=\"document.getElementById('$1').style.display='none'; document.getElementById('$1_show_answer_button').style.display='block'; document.getElementById('$1_hide_answer_button').style.display='none'; \">解答例を隠す</button></div><div id=\"$1\" style=\"$2\">"},
{"pattern": "<!-- end answer -->", "flags": "g", "substitute": "</div>"}
]
}
},
"title": "Scala研修テキスト"
}
ここで重要なのは、目次を表わすstructure.summary
と序文を表すstructure.readme
である。このbook.json
をLaTeXファイルから読み込み、structure.summary
にある目次ファイルを利用して本文を作成する。
SVG形式の画像をInkscapeでPDFへ変換
LaTeXではSVG形式の画像を読み込むことができないので、Inkscapeを用いてPDFへ変換しそれを読み込むことにする。次のようなシェルスクリプトでSVG画像を変換する。
for f in ./img/*.svg
do
if [[ $f =~ \./img/(.*)\.svg ]]; then
inkscape -z -D --file=`pwd`/img/${BASH_REMATCH[1]}.svg --export-pdf=`pwd`/${BASH_REMATCH[1]}.pdf --export-latex=`pwd`/${BASH_REMATCH[1]}.pdf_tex
fi
doneB
PandocでLaTeXへ変換
GitBookのMarkdownを全てPandocを用いてTeXファイルへ変換する。
mkdir target
for f in ./scala_text/gitbook/*.md
do
if [[ $f =~ \./scala_text/gitbook/(.*)\.md ]]; then
cp $f ./target/
pandoc -o "./target/${BASH_REMATCH[1]}.tex" -f markdown_github+footnotes+header_attributes-hard_line_breaks-intraword_underscores --latex-engine=lualatex --chapters --listings --filter=filter.py $f
fi
done
まず、Pandocは多彩なMarkdown拡張に対応している。markdown_github+footnotes+header_attributes-hard_line_breaks-intraword_underscores
というのはGitHub Markdownに追加で次のようなものを有効にする。
- +footnote
- 脚注を書ける
- +header_attributes
-
# hoge { headers }
のような、ヘッダ情報を書ける - -hard_line_breaks
- 改行をパラグラフの終了とみなさない
- -intraword_underscores
- 単語内の
_
による強調を許可する
また、それ以外のオプションの意味は次のようになっている。
- --latex-engine=lualatex
- LaTeXのエンジンとしてLuaTeXを使う
- --chapters
- LaTeXのマークアップを`\chapter`からにする
- --listings
- ソースコードハイライティングにListingsを使う
そして、最後の--filter=filter.py
というのは、Pandocの汎用性の高さを示す機能のひとつであると考えている。これは、ソースファイルから一旦それのASTをJSON形式で出力し、それに対してfilter
で指定したスクリプトでツリーを操作し、操作されたJSONを最終的にターゲットへ変換する、ということが可能になっている。Pandocのマニュアルから図を引用する。
http://pandoc.org/scripting.html
source format
↓
(pandoc)
↓
JSON-formatted AST
↓
(filter)
↓
JSON-formatted AST
↓
(pandoc)
↓
target format
このfilter
では今回次のような仕事を行う。
- ListingsでScalaのスタイルを使うようにする
- GitBookのファイルのインクルード1機能に対応する
- 画像がURLの場合、ダウンロードする
- 波ダッシュ
〜
(U+301C)を全角チルダ~
(U+FF5E)へ変換する2
このような処理をするPythonスクリプトは次のようになる。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
from pandocfilters import toJSONFilter, CodeBlock, RawBlock, Str, RawInline
def mkListingsEnvironment(code):
return RawBlock('latex', "\\begin{lstlisting}[style=scala]\n" + code + "\n\\end{lstlisting}\n")
def mkInputListings(src):
return RawInline('latex', "\\lstinputlisting[style=scala]{" + src + "}")
def mkIncludegraphics(src):
return RawInline('latex', "\\includegraphics{img/" + src + "}")
def filter(key, value, fmt, meta):
if key == 'CodeBlock':
[[ident, classes, kvs], code] = value
if 'scala' in classes:
return mkListingsEnvironment(code)
elif key == 'Link':
[_, text, [href, _]] = value
if text == [Str("include")]:
return mkInputListings(href)
elif key == 'Image':
[_, _, [src, _]] = value
if src.startswith("http"):
fileName = src.split("/")[-1]
os.system("cd img && curl -O " + src)
return mkIncludegraphics(fileName)
elif key == 'Str':
return(Str(value.replace(u"〜", u"~")))
if __name__ == "__main__":
toJSONFilter(filter)
LuaLaTeXでbook.json
を解析し、目次と本文を生成
今回はLaTeXの処理系にLuaTeXを採用したため、この部分はLuaコードで次のように実装する。よいことに、LuaTeXではJSONパーザーが提供されているのでそれを使えばよく、目次のMarkdownは簡単な形式だったので今回はやや手抜きだが正規表現で抜き出すこととした。
require("lualibs-util-jsn.lua")
local target = "./target/"
local bookJsonFile = "book.json"
function read(file)
local handler = io.open(file, "rb")
local content = handler:read("*all")
handler:close()
return content
end
function readSummary(file)
local handler = io.open(file, "r")
local summary = {}
for l in handler:lines() do
local n = string.match(l, "%[.*%]%((.*)%.md%)")
table.insert(summary, n)
end
handler:close()
return summary
end
function getTeXFile(target, name)
return target .. name .. ".tex"
end
function getFileName(f)
return string.match(f, "(.*)%.md")
end
local bookJson = utilities.json.tolua(read(bookJsonFile))
local readmeFile = getTeXFile(target, getFileName(bookJson['structure']['readme']))
local summaryFile = target .. bookJson['structure']['summary']
tex.print("\\frontmatter")
tex.print("\\input{" .. readmeFile .. "}")
tex.print("\\tableofcontents")
tex.print("\\mainmatter")
for k, v in pairs(readSummary(summaryFile)) do
tex.print("\\input{" .. getTeXFile(target, v) .. "}")
end
LuaLaTeXファイルのコンパイル
LuaLaTeXファイルのコンパイルにはlatexmkとMakeを用いた。latexmkは簡単な設定ファイルを作成すると、それによって適切にLaTeXファイルをコンパイルするプログラムである。次のようなlatexmkの設定ファイルとMakefile
を用いた。
#!/usr/bin/env perl
$pdflatex = 'lualatex %O %S --interaction=nonstopmode';
$pdf_mode = 3;
$bibtex = 'pbibtex';
.PHONY : all clean
all : scala_text.pdf
clean :
latexmk -C
rm -r img target example_projects *.pdf *.pdf_tex book.json
scala_text.pdf :
latexmk -pdf scala_text.tex
Travis CI上によるコンパイル
さて、これらでGitBook MarkdownからPDFを作れるようになったので、Travis CI上でコンパイルできるようにする。LaTeXをTravis CI上でコンパイルすることに関しては以前の記事を参照して欲しい。
なお、今回の.tarivs.yml
は次のようになっている。
まとめ
このようにすることで、最終的にはある程度まともなPDFを作ることができた。もし、このPDFをよりよくする方法を思いついたり、知っていたりする場合は、この記事のコメントもしくはScala Text PDFのIssue、またはScala TextのIssueのどれでもよいので、どれかで教えて欲しいと思う。
-
GitBookはMarkdownのリンクを用いて、ファイルをインクルードする機能がある。 ↩
-
波ダッシュはLaTeXでは意図した見た目にならない。http://www1.pm.tokushima-u.ac.jp/~kohda/tex/texlive.html ↩