PandocでPDFを作成する時に表の枠線を格子状にする

カテゴリ: pandoc

R Markdownユーザーは素直にgtやflextable、huxtableなどのパッケージを使いましょう。

Pandoc’s Markdownで記述した表をPDFに出力すると、↓のような1行目の前後と最下部に横線の入った表になります。

時には以下のような格子状の枠線がどうしても欲しい場合もあるでしょう。 Pandocでやるにはどうすればいいのでしょうか?

普通にMarkdown -> LaTeXしてみる

以下の表を含むMarkdownファイルをフツーにLaTeX化けしてみましょう。

---
title: example.md
---

# Header

| foo | bar |
|-----|-----|
|  1  |  2  |
|  a  |  b  |

以下のコマンドで変換すると、表は\begin{longtable}[]{@{}[email protected]{}}という行から始まることを確認できます。こいつにどうにかして枠線をいれたい。

pandoc example.md -t latex
\hypertarget{header}{%
\section{Header}\label{header}}

\begin{longtable}[]{@{}[email protected]{}}
\toprule
foo & bar \\
\midrule
\endhead
1 & 2 \\
a & b \\
\bottomrule
\end{longtable}

Stack Exchangeの例は最近のPandocでは動かない

https://tex.stackexchange.com/a/596005

少なくともPandoc 2.14.0.2はダメっぽいです。

しかし、先の表をどうすればいいか指針はわかります。

  • {@{}[email protected]{}}が列の書式設定で、lは左揃えを示し、その両脇に|を配置すると縦線が入る
  • 行間に\hlineを挟むと横線が入る

これを自動化する方法も載っていますが、コメントにある通り、arrayパッケージを読み込むと動きません。今のPandocはlongtableパッケージと同時にarrayパッケージを読み込むので、絶望ですね(https://github.com/jgm/pandoc-templates/blob/466c90ed5bb489d9cafe41b59fff47a3c5eb858c/default.latex#L276)。

作戦

  1. LaTeX頑張る
  2. PandocにはTeXの出力までを任せて、TeXファイルを文字列置換した後、lualatexなどのコマンドでPDF化
  3. Pandocフィルタ頑張る

1が真っ当ですね。私にはできません。

2もシンプルそうですね。ただ、Pandocで--resource-pathなどを指定していると死ぬ畏れありです。

3も一見真っ当そうですね。 Pandocは入力したファイルをまずASTに変換します。 ASTは、どこが表だとかどこが行だとかをプログラムで扱いやすくしてくれます。 ASTに対してはユーザーが必要に応じてフィルタ処理を噛ませられます。弄ったASTをPandocに返すとよしなに出力形式に変換してくれます。なにそれよさそうって感じがしますね。やってみるとしんどいです。

結果としては横線に1と、縦線に3を組み合わせて頑張ります。

真っ当 + 真っ当 = 真っ当……?

ちなみにPandocフィルタには、2種類あります。

  • JSONを受けとってJSONを返すフィルタ
  • 内蔵のLuaインタプリタを利用して内部表現ASTを受け取ってASTを返すフィルタ

後者の方が内蔵されているだけあって便利なAPIが用意されていたり、高速だったりします。

残念ながら今回は両方使います。

実装

Pandocフィルタで縦線を入れる

フィルタはASTを処理すると説明したところでした。 ASTってどんな感じかというと、JSONで表現するならこんな感じ。

pandoc example.md -t json
{"pandoc-api-version":[1,22],"meta":{"title":{"t":"MetaInlines","c":[{"t":"Str","c":"example.md"}]}},"blocks":[{"t":"Header","c":[1,["header",[],[]],[{"t":"Str","c":"Header"}]]},{"t":"Table","c":[["",[],[]],[null,[]],[[{"t":"AlignDefault"},{"t":"ColWidthDefault"}],[{"t":"AlignDefault"},{"t":"ColWidthDefault"}]],[["",[],[]],[[["",[],[]],[[["",[],[]],{"t":"AlignDefault"},1,1,[{"t":"Plain","c":[{"t":"Str","c":"foo"}]}]],[["",[],[]],{"t":"AlignDefault"},1,1,[{"t":"Plain","c":[{"t":"Str","c":"bar"}]}]]]]]],[[["",[],[]],0,[],[[["",[],[]],[[["",[],[]],{"t":"AlignDefault"},1,1,[{"t":"Plain","c":[{"t":"Str","c":"1"}]}]],[["",[],[]],{"t":"AlignDefault"},1,1,[{"t":"Plain","c":[{"t":"Str","c":"2"}]}]]]],[["",[],[]],[[["",[],[]],{"t":"AlignDefault"},1,1,[{"t":"Plain","c":[{"t":"Str","c":"a"}]}]],[["",[],[]],{"t":"AlignDefault"},1,1,[{"t":"Plain","c":[{"t":"Str","c":"b"}]}]]]]]]],[["",[],[]],[]]]}]}
整形したJSONを見る
pandoc example.md -t json | jq .
{
  "pandoc-api-version": [
    1,
    22
  ],
  "meta": {
    "title": {
      "t": "MetaInlines",
      "c": [
        {
          "t": "Str",
          "c": "example.md"
        }
      ]
    }
  },
  "blocks": [
    {
      "t": "Header",
      "c": [
        1,
        [
          "header",
          [],
          []
        ],
        [
          {
            "t": "Str",
            "c": "Header"
          }
        ]
      ]
    },
    {
      "t": "Table",
      "c": [
        [
          "",
          [],
          []
        ],
        [
          null,
          []
        ],
        [
          [
            {
              "t": "AlignDefault"
            },
            {
              "t": "ColWidthDefault"
            }
          ],
          [
            {
              "t": "AlignDefault"
            },
            {
              "t": "ColWidthDefault"
            }
          ]
        ],
        [
          [
            "",
            [],
            []
          ],
          [
            [
              [
                "",
                [],
                []
              ],
              [
                [
                  [
                    "",
                    [],
                    []
                  ],
                  {
                    "t": "AlignDefault"
                  },
                  1,
                  1,
                  [
                    {
                      "t": "Plain",
                      "c": [
                        {
                          "t": "Str",
                          "c": "foo"
                        }
                      ]
                    }
                  ]
                ],
                [
                  [
                    "",
                    [],
                    []
                  ],
                  {
                    "t": "AlignDefault"
                  },
                  1,
                  1,
                  [
                    {
                      "t": "Plain",
                      "c": [
                        {
                          "t": "Str",
                          "c": "bar"
                        }
                      ]
                    }
                  ]
                ]
              ]
            ]
          ]
        ],
        [
          [
            [
              "",
              [],
              []
            ],
            0,
            [],
            [
              [
                [
                  "",
                  [],
                  []
                ],
                [
                  [
                    [
                      "",
                      [],
                      []
                    ],
                    {
                      "t": "AlignDefault"
                    },
                    1,
                    1,
                    [
                      {
                        "t": "Plain",
                        "c": [
                          {
                            "t": "Str",
                            "c": "1"
                          }
                        ]
                      }
                    ]
                  ],
                  [
                    [
                      "",
                      [],
                      []
                    ],
                    {
                      "t": "AlignDefault"
                    },
                    1,
                    1,
                    [
                      {
                        "t": "Plain",
                        "c": [
                          {
                            "t": "Str",
                            "c": "2"
                          }
                        ]
                      }
                    ]
                  ]
                ]
              ],
              [
                [
                  "",
                  [],
                  []
                ],
                [
                  [
                    [
                      "",
                      [],
                      []
                    ],
                    {
                      "t": "AlignDefault"
                    },
                    1,
                    1,
                    [
                      {
                        "t": "Plain",
                        "c": [
                          {
                            "t": "Str",
                            "c": "a"
                          }
                        ]
                      }
                    ]
                  ],
                  [
                    [
                      "",
                      [],
                      []
                    ],
                    {
                      "t": "AlignDefault"
                    },
                    1,
                    1,
                    [
                      {
                        "t": "Plain",
                        "c": [
                          {
                            "t": "Str",
                            "c": "b"
                          }
                        ]
                      }
                    ]
                  ]
                ]
              ]
            ]
          ]
        ],
        [
          [
            "",
            [],
            []
          ],
          []
        ]
      ]
    }
  ]
}

表のAST辛いですねー。ここから \begin{longtable}...といった記法に持っていくコードは書きたくない。

ではどうするか?一旦、ASTの内、表に相当する部分をLaTeX化してしまいましょう。

ASTの表だけをLaTeX化するLuaフィルタ

grid-table.luaになります。大まかな手順は以下の通り。

  1. ドキュメント中に表を探す
  2. 表ごとに部分的なドキュメントを作成する
  3. 部分的なドキュメントをJSONフィルタに渡して表の部分をLaTeX化したJSONを受けとる
  4. LaTeX部分の文字列を良い感じに置換して、元のドキュメントのASTに組込む

表ごとに小規模なドキュメントを作成するため、親ドキュメントのメタデータも渡せるよう工夫しています。

-- grid-table.lua

-- 親ドキュメントのメタデータを抽出
local METADATA = {}
function Meta(meta)
  METADATA = meta
end

-- 表ごとに小規模なドキュメントを作成、LaTeX化・文字列置換の後にASTに組込む
function Table(tbl)
  -- JSONフィルタを使って表をLaTeX化し該当のASTを抽出する
  -- JSONフィルタの内容は後述
  latex = pandoc.utils.run_json_filter(
    pandoc.Pandoc({tbl}, METADATA),
    "latexify.bash"
  ).blocks[1]

  -- 正規表現を使った文字列の置換を行う
  -- pattern変数を使ってLaTeXを3つのブロックに分割
  -- 2番目の`([lcr]+)`にマッチする部分では文字を`|`区切りにする
  local pattern = table.concat({
    "(.*\\begin%{longtable%}%[%]%{@%{%})", -- 表の開始を宣言する部分
    "([lcr]+)",                            -- 文字の揃え方
    "(@%{%}%}.*)"                          -- 残り
  })
  local text = ""
  for i = 1, 3 do
    match, _ = latex.text:gsub(pattern, string.format("%%%s", i))
    text = text .. (i ~= 2 and match or match:gsub("(.)", "|%1") .. "|")
  end
  latex.text = text

  -- 成果物を返す
  return latex
end

-- 処理の順序の定義
-- メタデータ、表の順に処理を行う
return {
  {Meta = Meta},
  {Table = Table}
}

Luaフィルタから受け取った部分的なドキュメントをLaTeX化して返すJSONフィルタ

latexify.bashになります。外部コマンドとしてpandocの他にjqのインストールが必要です。

ざっくりとした流れは

  1. 標準入力としてJSONを受け取る
  2. Pandocを使ってJSONをLaTeX化
  3. JSONのblocksキーをLaTeXに置換して返す

JSONの構造の説明は省略。 pandoc -t jsonで色んなファイルをJSON化したら自然と分かります。ただ、出力は人に優しくない見た目なので、pandoc -t json example.md | jq .といった感じでjqコマンドで整形すると良いです。

#!/bin/bash
# latexify.bash

# 標準入力としてJSONを受け取る
JSON="$( cat - )"

# JSONに対してpandocを使ってLaTeX化する
# 後でJSONとして返しやすいように適宜エスケープする
LATEX="$(
  echo "$JSON" \
    | pandoc -f json -t latex \
    | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
)"

# jqコマンドを使ってJSONを更新して返す
# 具体的にはblocksキーをLaTeXのRawBlockにする
QUERY="$(cat <<EOF
.blocks[]|={
  "t": "RawBlock",
  "c": [
    "latex",
    "$LATEX"
  ]
}
EOF
)"
echo "$JSON" | jq "$QUERY"

作成したフィルタは実行可能にしておきましょう。

chmod +x latexify.bash # JSONフィルタを実行可能にしておく。

フィルタを使ってMarkdown -> Markdownしてみる

本来はPDFに出力しますが、まずはちゃんとそれっぽいソースを吐けるか、markdown化して確認します。

pandoc example.md -t markdown --lua-filter grid-table.lua
# Header

```{=latex}
\begin{longtable}[]{@{}|l|l|@{}}
\toprule
foo & bar \\
\midrule
\endhead
1 & 2 \\
a & b \\
\bottomrule
\end{longtable}
```

よさそうですね。

プリアンブルで横線を入れる

ここはStack Exchangeの投稿を参考にミニマルに実装します。割りとシンプル。

%preamble.tex

% 表の行ごとに \hline が入るようにする
\makeatletter
\apptocmd{\[email protected]}{\hline}{}{}
\makeatother

% ついでに横線と縦線の間に生じる空間を消しておく
\setlength{\aboverulesep}{0pt}
\setlength{\belowrulesep}{0pt}
\renewcommand{\arraystretch}{1.3}

実行

やっと準備ができました。作ったLuaフィルタとプリアンブルを読んでPDF化しましょう。

ここで注意点として、--metadata=tables:trueを足してください。 Pandocはドキュメント中に表があるかどうかを判定して自動的に必要なパッケージを読み込みます。しかし、表をすべてLaTeXで表現している場合は判定に失敗するので、明示的にパッケージを読み込みます。

pandoc example.md \
    -o example.pdf \
    --lua-filter grid-table.lua \
    --include-in-header preamble.tex \
    --metadata=tables:true 

以下、結果です。

注意

画像を挿入できません

ナゾのエラーが出て死にます。\hlineを足しているのが悪いらしいです。誰かタスケテ。

! Undefined control sequence. l.83 a & \includegraphics

改行とかもできません

カジュアルにMarkdownの中でLaTeXを使うと\\を使って強制改行できそうな気がしますが、レイアウトが崩れます。

| foo              | bar |
|------------------|-----|
| 1 `\\`{=latex} 2 |  3  |
| 4                |  5  |

Pandoc’s Markdownの場合、Grid Tableという記法を使うと表の中で改行、改段落、箇条書きなど自由な記述が可能です。

しかし今回紹介した方法では対応できません。

というのも今回は列の揃え方に{@{}[email protected]{}}という書き方を想定していたのですが、出力は異なります。

列ごとの書式の定義は >{\raggedright\arraybackslash}... といった行で記述しているようですね。

ようわからんので宿題です。

cat <<EOF > example2.md
: Sample grid table.

+---------------+---------------+--------------------+
| Fruit         | Price         | Advantages         |
+===============+===============+====================+
| Bananas       | \$1.34         | - built-in wrapper |
|               |               | - bright color     |
+---------------+---------------+--------------------+
| Oranges       | \$2.10         | - cures scurvy     |
|               |               | - tasty            |
+---------------+---------------+--------------------+
EOF

pandoc example2.md -t latex
## \begin{longtable}[]{@{}
##   >{\raggedright\arraybackslash}p{(\columnwidth - 4\tabcolsep) * \real{0.22}}
##   >{\raggedright\arraybackslash}p{(\columnwidth - 4\tabcolsep) * \real{0.22}}
##   >{\raggedright\arraybackslash}p{(\columnwidth - 4\tabcolsep) * \real{0.29}}@{}}
## \caption{Sample grid table.}\tabularnewline
## \toprule
## Fruit & Price & Advantages \\
## \midrule
## \endfirsthead
## \toprule
## Fruit & Price & Advantages \\
## \midrule
## \endhead
## Bananas & \$1.34 & \begin{minipage}[t]{\linewidth}\raggedright
## \begin{itemize}
## \tightlist
## \item
##   built-in wrapper
## \item
##   bright color
## \end{itemize}
## \end{minipage} \\
## Oranges & \$2.10 & \begin{minipage}[t]{\linewidth}\raggedright
## \begin{itemize}
## \tightlist
## \item
##   cures scurvy
## \item
##   tasty
## \end{itemize}
## \end{minipage} \\
## \bottomrule
## \end{longtable}

ちなみに無理にLuaフィルタを適用すると愉快なことになります。

pandoc example2.md -t latex --lua-filter grid-table.lua
## \begin{longtable}[]{@{}
##   >{\raggedright\arraybackslash}p{(\columnwidth - 4\tabcolsep) * \real{0.22}}
##   >{\raggedright\arraybackslash}p{(\columnwidth - 4\tabcolsep) * \real{0.22}}
##   >{\raggedright\arraybackslash}p{(\columnwidth - 4\tabcolsep) * \real{0.29}}@{}}
## \caption{Sample grid table.}\tabularnewline
## \toprule
## Fruit & Price & Advantages \\
## \midrule
## \endfirsthead
## \toprule
## Fruit & Price & Advantages \\
## \midrule
## \endhead
## Bananas & \$1.34 & \begin{minipage}[t]{\linewidth}\raggedright
## \begin{itemize}
## \tightlist
## \item
##   built-in wrapper
## \item
##   bright color
## \end{itemize}
## \end{minipage} \\
## Oranges & \$2.10 & \begin{minipage}[t]{\linewidth}\raggedright
## \begin{itemize}
## \tightlist
## \item
##   cures scurvy
## \item
##   tasty
## \end{itemize}
## \end{minipage} \\
## \bottomrule
## \end{longtable}|\|b|e|g|i|n|{|l|o|n|g|t|a|b|l|e|}|[|]|{|@|{|}|
## | | |>|{|\|r|a|g|g|e|d|r|i|g|h|t|\|a|r|r|a|y|b|a|c|k|s|l|a|s|h|}|p|{|(|\|c|o|l|u|m|n|w|i|d|t|h| |-| |4|\|t|a|b|c|o|l|s|e|p|)| |*| |\|r|e|a|l|{|0|.|2|2|}|}|
## | | |>|{|\|r|a|g|g|e|d|r|i|g|h|t|\|a|r|r|a|y|b|a|c|k|s|l|a|s|h|}|p|{|(|\|c|o|l|u|m|n|w|i|d|t|h| |-| |4|\|t|a|b|c|o|l|s|e|p|)| |*| |\|r|e|a|l|{|0|.|2|2|}|}|
## | | |>|{|\|r|a|g|g|e|d|r|i|g|h|t|\|a|r|r|a|y|b|a|c|k|s|l|a|s|h|}|p|{|(|\|c|o|l|u|m|n|w|i|d|t|h| |-| |4|\|t|a|b|c|o|l|s|e|p|)| |*| |\|r|e|a|l|{|0|.|2|9|}|}|@|{|}|}|
## |\|c|a|p|t|i|o|n|{|S|a|m|p|l|e| |g|r|i|d| |t|a|b|l|e|.|}|\|t|a|b|u|l|a|r|n|e|w|l|i|n|e|
## |\|t|o|p|r|u|l|e|
## |F|r|u|i|t| |&| |P|r|i|c|e| |&| |A|d|v|a|n|t|a|g|e|s| |\|\|
## |\|m|i|d|r|u|l|e|
## |\|e|n|d|f|i|r|s|t|h|e|a|d|
## |\|t|o|p|r|u|l|e|
## |F|r|u|i|t| |&| |P|r|i|c|e| |&| |A|d|v|a|n|t|a|g|e|s| |\|\|
## |\|m|i|d|r|u|l|e|
## |\|e|n|d|h|e|a|d|
## |B|a|n|a|n|a|s| |&| |\|$|1|.|3|4| |&| |\|b|e|g|i|n|{|m|i|n|i|p|a|g|e|}|[|t|]|{|\|l|i|n|e|w|i|d|t|h|}|\|r|a|g|g|e|d|r|i|g|h|t|
## |\|b|e|g|i|n|{|i|t|e|m|i|z|e|}|
## |\|t|i|g|h|t|l|i|s|t|
## |\|i|t|e|m|
## | | |b|u|i|l|t|-|i|n| |w|r|a|p|p|e|r|
## |\|i|t|e|m|
## | | |b|r|i|g|h|t| |c|o|l|o|r|
## |\|e|n|d|{|i|t|e|m|i|z|e|}|
## |\|e|n|d|{|m|i|n|i|p|a|g|e|}| |\|\|
## |O|r|a|n|g|e|s| |&| |\|$|2|.|1|0| |&| |\|b|e|g|i|n|{|m|i|n|i|p|a|g|e|}|[|t|]|{|\|l|i|n|e|w|i|d|t|h|}|\|r|a|g|g|e|d|r|i|g|h|t|
## |\|b|e|g|i|n|{|i|t|e|m|i|z|e|}|
## |\|t|i|g|h|t|l|i|s|t|
## |\|i|t|e|m|
## | | |c|u|r|e|s| |s|c|u|r|v|y|
## |\|i|t|e|m|
## | | |t|a|s|t|y|
## |\|e|n|d|{|i|t|e|m|i|z|e|}|
## |\|e|n|d|{|m|i|n|i|p|a|g|e|}| |\|\|
## |\|b|o|t|t|o|m|r|u|l|e|
## |\|e|n|d|{|l|o|n|g|t|a|b|l|e|}|\begin{longtable}[]{@{}
##   >{\raggedright\arraybackslash}p{(\columnwidth - 4\tabcolsep) * \real{0.22}}
##   >{\raggedright\arraybackslash}p{(\columnwidth - 4\tabcolsep) * \real{0.22}}
##   >{\raggedright\arraybackslash}p{(\columnwidth - 4\tabcolsep) * \real{0.29}}@{}}
## \caption{Sample grid table.}\tabularnewline
## \toprule
## Fruit & Price & Advantages \\
## \midrule
## \endfirsthead
## \toprule
## Fruit & Price & Advantages \\
## \midrule
## \endhead
## Bananas & \$1.34 & \begin{minipage}[t]{\linewidth}\raggedright
## \begin{itemize}
## \tightlist
## \item
##   built-in wrapper
## \item
##   bright color
## \end{itemize}
## \end{minipage} \\
## Oranges & \$2.10 & \begin{minipage}[t]{\linewidth}\raggedright
## \begin{itemize}
## \tightlist
## \item
##   cures scurvy
## \item
##   tasty
## \end{itemize}
## \end{minipage} \\
## \bottomrule
## \end{longtable}

ENJOY