マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

Pandocで節見出しの“番号の書式”を変えたい話

元ネタはこの辺。

要するに「節見出しの中の番号の書式を変えるにはどうすればよいか」という話。

やりたいこと

Markdown文書をPandocでLaTeXやHTMLに変換する場合に、「節見出しの中の番号の書式」を以下のようにしたい。

  • 章(レベル1)については「第1章」とする。
  • 節(レベル2)については「第1節」とする(章番号はつけない)。

例えばこんなMarkdown文書があったとする。

# まえがき {-}
# ゆきだるま
## ゆきだるまは素敵
## ゆきだるまは本質的
## TeXとの関係 {-}

{-}は節見出しにunnumberedクラスを指定する記法({.unnumbered}と等価)で、番号のない節見出しであることを表す。

この文書の変換結果は以下のようになってほしい。

まえがき
第1章  ゆきだるま
第1節  ゆきだるまは素敵
第2節  ゆきだるまは本質的
TeXとの関係

LaTeXする場合

LaTeX脳で考える

LaTeXの思想に依って考えると、「節見出しの中の番号の書式」は文書クラスの専管事項ということになる。従って、書式を自分好みに変えたいのであれば、そういう文書クラスをつくればよい、という単純な話になる。

ただ、「単純な話」といっても「簡単な話」では決してない。文書クラスを作製(改変)するにはTeX言語の知識が多少とも必要となるからである。そこで、文書クラスによっては多少のカスタマイズを可能としていることがある。例えば、章(\chapter)については、多くの和文文書クラスにおいて、「章の数字(カウンタ表示)の前後に付く文字列」はマクロ(\prechaptername\postchaptername)になっていて変更できる。なので、要件によっては既存のクラスのカスタマイズで済む場合もあるだろう。

この辺りの話は、「PandocでLaTeX文書を生成する」場合であっても基本的には変わらない。Pandocの思想では原則的に「文書の構造を含めた内容だけを変換対象対象とし、文書のレイアウトについては変換先のソフトウェアに委ねる」ことになっているからである。ただしここでも例外があり、(主に複数の文書形式の間で統一的に扱う目的で)一部のレイアウト設定はPandoc側で制御できる。今回の件に関しては、LaTeX出力で標準のテンプレートを適用する場合は以下の機能が存在する。

  • --number-sectionsオプション(短縮名-N)が指定されない場合、節番号の表示を抑止するためにsecnumdepthカウンタの値に負数を設定する。

従って、要件を実現するには、「--number-sectionsを指定した上で、LaTeXの側で節番号表示に対してカスタマイズを行う」という方針をとることになる。

やってみる

いつも通り、BXJSクラスのPandocモードを利用する。今回は章がある文書なのでbxjsbookを使うことになる。

章番号の設定については、bxjsbookクラスも他の和文クラスと同様に\prechaptername\postchapternameマクロで行う。

\renewcommand{\prechaptername}{}
\renewcommand{\postchaptername}{}

ただし、(これも多くの和文クラスと同様で)上に示した値は既定値になっているので実際には何も設定しなくてよい。

節(\section)の番号については、BXJSクラスの独自機能(詳細は過去の記事を参照されたい)を利用する。すなわち、クラスオプションにlabel-section=modernを指定すた上で、以下のようにマクロを再定義する。

% 節のカウンタの表示に章の番号を入れない
\renewcommand{\thesection}{\arabic{section}}
% 節番号の出力を"第1節"とする
\renewcommand{\labelsection}{\thesection}

その他諸々の設定と合わせて、Pandocのデフォルトファイルの形にまとめたものを以下に示す。

to: latex-smart
standalone: true
pdf-engine: lualatex

top-level-division: chapter
number-sections: true

variables:
  papersize: b5
  secnumdepth: 2
  documentclass: bxjsbook
  classoption:
    - pandoc
    - label-section=modern
  header-includes: |
    \renewcommand{\thesection}{\arabic{section}}
    \renewcommand{\labelsection}{第\thesection 節}

f:id:zrbabbler:20200814181729p:plain
LaTeXに変換したやつ(第1章の部分)

HTMLする場合

Pandocで節番号するとアレ

Pandocで--number-sections付きでHTMLに変換した場合、以下のような出力が得られる。

<h1 class="unnumbered" id="まえがき">まえがき</h1>
<h1 data-number="1" id="ゆきだるま"><span class="header-section-number">1</span> ゆきだるま</h1>
<h2 data-number="1.1" id="ゆきだるまは素敵"><span class="header-section-number">1.1</span> ゆきだるまは素敵</h2>
<h2 data-number="1.2" id="ゆきだるまは本質的"><span class="header-section-number">1.2</span> ゆきだるまは本質的</h2>
<h2 class="unnumbered" id="texとの関係">TeXとの関係</h2>
<h2 data-number="1.3" id="マフラー"><span class="header-section-number">1.3</span> マフラー</h2>
<h2 data-number="1.4" id="ホウキ"><span class="header-section-number">1.4</span> ホウキ</h2>

ここで注意すべきなのは、「出力のHTMLの中に節番号が記述されている」ということである。つまり、LaTeX変換の場合と異なり、Pandoc側が番号を生成しているわけであり、先述の「Pandocの思想」にも反している。

※もちろん、--number-sectionsを付けない場合は節番号は入らない。

“思想に合わない”ということもあってか、Pandocの「節番号の自動生成」の機能は中途半端で、番号の書式を変えるためのインタフェースは用意されていないようである。つまり、HTML出力の場合は、Pandoc本体の機能だけでは要件は実現できないのである。

HTMLをLaTeX脳で考える

「Pandocの思想」に沿って考えるならば、「節番号の書式」については“HTMLの側”で対処すべきであろう。HTMLの世界で“文書クラス”に相当するものはCSSである。実際、CSSには「節番号を自動生成して節見出しに表示する」ための機能が存在する。

  • 節番号の生成はCSSカウンタを利用するとできる。
  • 節番号の表示は擬似要素を利用する。この際に、番号の書式を自由に設定できる。

従って、Pandocの側では節番号を生成せず(--number-sectionsを指定しない)、代わりに、CSSを利用して所望の書式の節番号を出力する、というのが“Pandoc的に真っ当な”方法といえるだろう。

今の場合、節番号の生成のためのスタイル指定は以下のようになる。

[sample.css]
/** 章のカウンタ"chapter"と節のカウンタ"section"を用意する **/
body {
  counter-reset: chapter, section;
}
/** 章見出しではchapterを増やしてsectionをリセット **/
h1 {
  counter-increment: chapter;
  counter-reset: section;
}
/** 節見出しではsectionを増やす **/
h2 {
  counter-increment: section;
}
/** "unnumbered"クラスの見出しでは何もしない **/
.unnumbered {
  counter-increment: none;
}

節番号の表示は以下のようになる。

[sample.css(続き)]
/** 章見出しの内容の前に"第1章"の形式で章番号を挿入 **/
h1::before {
  content: "第" counter(chapter) "章 ";
}
/** 節見出しの内容の前に"第1節"の形式で節番号を挿入 **/
h2::before {
  content: "第" counter(section) "節 ";
}
/** "unnumbered"クラスの見出しでは出力しない **/
.unnumbered::before {
  content: ""
}

Pandoc側の設定をデフォルトファイルで表すと以下の通り。

standalone: true
css: sample.css
number-sections: false

f:id:zrbabbler:20200814194512p:plain
HTMLに変換したやつ(第1章の部分)

実際には、「既定のテンプレートを指定した場合、文書のタイトルが“titleクラス付きのh1要素”で出力されるので、そのための対処が必要」といった問題がある。(まあ、実用で既定テンプレートを指定することはあまりないのかも知れないが。)その辺りの対処も加えたサンプル一式をGistに置いた。

厳しい現実の話

自分としては、これで満足なのであるが、しかし現実問題としては、CSSの素敵な機能を使おうとする場合には常に「その機能は世の中で十分にサポートされているのか」ということに留意しておく必要がある。

CSSの素敵な機能がサポートされていない環境のことも考慮する必要があるのなら、残念ながら結局、「Pandoc側で節番号を生成してHTMLのソースに入れておく」しかないことになる。

どうにかしてどうにかする

結局、Pandocが生成する節番号の書式を設定する機能はないのであるが、だとしても、節見出しがもつ「節番号表示のデータ」をフィルタを使って上書きすることはできないか。いろいろ調べてみた結果、次のことがわかった。

  • 節見出しのオブジェクト(Header型)の属性(attributes)のnumberに文字列を設定しておくと、writerの処理において、Pandocが生成した文字列の代わりにnumberの文字列が「節番号表示の文字列」として使用される1

ということは、「自分で好きな節番号文字列を生成してそれをnumber属性に格納する」ようなフィルタを作れば解決することになる。

[numbering.lua]
local numbers = {}
-- 節番号のスタイル
local number_styles = {
  -- この配列のn番目の要素に"レベルnの節番号表示"を返す関数を置く.
  -- 関数の実行時に, 配列numbersには現在の節番号の値が入っている.
  -- 例えば3.5節であればnumbersの値は {3,5} である.
  function() return ("第%s章"):format(numbers[1]) end;
  function() return ("第%s節"):format(numbers[2]) end;
}

function Header(el)
  -- 子カウンタをリセット
  if numbers[el.level+1] then
    numbers[el.level+1] = 0
  end
  -- .unnumbered なら変更しない
  if el.classes:includes("unnumbered") then
    return -- 変更しない
  end
  -- カウンタをインクリメント
  numbers[el.level] = (numbers[el.level] or 0) + 1
  -- 番号を生成し'number'属性に設定する
  el.attributes['number'] = number_styles[el.level]()
  return el
end

このフィルタを用いる場合のPandoc側の設定は以下の通り。

filters:
  - numbering.lua
standalone: true
number-sections: true

※今度は--number-sectionsを有効にする必要があることに注意。

この設定を適用してHTMLに変換した結果(のbody内)を示す。所望の節番号表示が入っていることがわかる。

<h1 class="unnumbered" id="まえがき">まえがき</h1>
<h1 data-number="第1章" id="ゆきだるま"><span class="header-section-number">第1章</span> ゆきだるま</h1>
<h2 data-number="第1節" id="ゆきだるまは素敵"><span class="header-section-number">第1節</span> ゆきだるまは素敵</h2>
<h2 data-number="第2節" id="ゆきだるまは本質的"><span class="header-section-number">第2節</span> ゆきだるまは本質的</h2>
<h2 class="unnumbered" id="texとの関係">TeXとの関係</h2>
<h2 data-number="第3節" id="マフラー"><span class="header-section-number">第3節</span> マフラー</h2>
<h2 data-number="第4節" id="ホウキ"><span class="header-section-number">第4節</span> ホウキ</h2>

実際にブラウザで表示したときの結果は前のもの(CSSを利用したもの)と全く同じになるので省略する。

まとめ


  1. Pandoc要素に属性を設定すると、HTMLに出力した際に、対応するHTML要素のデータ属性として値が現れる。先に挙げた「PandocのHTML出力」のコードをよくみると、h1・h2要素のdata-number属性に節番号文字列が入っていることがわかる。これは「writerの処理時にPandocのHeader要素のnumber属性に節番号文字列が入っていた」ことを意味している。ただし、節番号の生成はまさにwriterの処理の中で行われるため、フィルタ処理の中でnumber属性を読みだしても何も入っていない。