数式を含むasciidocファイルをxetexでpdf出力する

# 要約

* `asciidoctor-pdf` だと綺麗に出力されない数式に直面（`\bigcup`や`\bigcap`）
* latexにおけるtheorem environment等を`asciidoctor-pdf`へ追加する方法がわからない
* 上記2点を解決するにあたり、 **asciidocファイル内でなるべく不自然な記述はしたくない**
* 以下の手順でpdfを生成する手順をまとめた
    * `asciidoc => (asciidoctor) => docbook => (dblatex with xetex) => pdf`

## 手っ取り早く使いたい人へ

[このリポジトリ](https://gitlab.com/nobuta05/mathproofs)をクローンした後、 `mathproofs.adoc` というファイルを編集して、"READMEのローカルでの実行"に記載されている手順を実行してください。 `~/public/` ディレクトリ配下に[mathproofs.pdf](https://keishi-sandbox.s3-ap-northeast-1.amazonaws.com/mathproofs.pdf)が作成されます。

## 手っ取り早く処理を知りたい人へ

1. 定理や証明として強調したい部分に `[.theorem]` や `[.proof]` を追記して目印をつける
2. `asciidoc => (asciidoctor) => docbook` 処理時は特に何もしない
3. `docbook => (dblatex with xetex) => pdf` 処理時に、修正・自作したxsltを与えて、xml => tex の変換規則を変える
4. 私が準備したtheorem環境やproof環境以外を使いたい場合は、同リポジトリの `styles/mytheme.xsl` と `styles/mytheme.sty` に変換規則と環境定義をそれぞれ追加してください。


# 前置き

はじめに、最終的な成果物は[ここ](https://gitlab.com/nobuta05/mathproofs)にあげています。記事の最後に簡単な使い方を記述します。

上記のリポジトリを利用する場合は依存コマンドがそちらに記述されています。ローカルで実行される場合は下記のコマンドを想定します。

* 日本語フォント（xetexで指定できれば良いので何でもいいですが、本記事ではNoto CJK JPフォントを想定します。）
* 本記事で提示する手順に必須のコマンド
    * asciidoctor
    * dblatex
    * xetex
* Gitlabリポジトリを実行するのに必要なコマンド（本記事では不要）
    * gitlab-runner
    * docker
* 記事全体を手元で動かすために、追加で必要なコマンド
    * asciidoctor-pdf
    * asciidoctor-pdf-cjk
    * asciidoctor-mathematical

# 課題1: 一部の数式が綺麗に出力できない

asciidocからpdfへの出力にあたり、これまで`asciidoctor-pdf`を使っていたのですが、綺麗に出力できない状況に直面しました。

```asciidoc:input.adoc
:stem: latexmath
:sectnums:

= てすと

== 見出し

以下が正しく表示されることを期待しています。

[latexmath]
++++
\begin{aligned}
  \bigcup_{n\in\mathbb{N}} A_n = \liminf_{n} A_n
  .
\end{aligned}
++++


以下では、定理としてblock要素となることを期待します。

.とある性質
[.theorem]
--
この文章はblock要素の中に記述されることを期待しています。また以下の数式も同様です。

[latexmath]
++++
\begin{aligned}
  \lambda = \bigcap_{n\in\mathbb{N}} A_n
\end{aligned}
++++

これは締めの文章です。
--

上記は正しく表示されているでしょうか？
```

一般的によく知られる`asciidoctor-pdf`を用いてpdf化してみます。下記のコマンドでpdfを出力した結果が[こちら](https://github.com/nobuta05/asciidoctor_example/blob/master/public/output_asciidoctor-pdf.pdf)となります。

```sh
asciidoctor-pdf -r asciidoctor-pdf-cjk -a scripts=cjk -a pdf-theme=default-with-fallback-font -r asciidoctor-mathematical -a mathematical-format=svg input.adoc
```
ご覧の通り、$\displaystyle \bigcup_{n\in\mathbb{N}}$等の位置がずれてしまい、良い見栄えとは言えません。


# 課題2: 定理・証明環境のためのスタイルがない

課題2では文章構造に着目します。AsciiDocにはblock要素を表すための記法が存在し、特に`NOTE, TIP, WARNING`といった一般的なものには標準でスタイルが準備されています。例えば下記の入力を先程の `asciidoctor-pdf` へ与えると、良しなに表示してくれることがわかります。

```asciidoc:input2.adoc
= てすと

[WARNING]
====
こちらが注意事項の1つ目の段落となります。

こちらが2つ目の段落となります。`====`で全体を囲うことにより、複数の段落や要素を1つのwarning block要素として処理してくれます。
====
```

一方で、LaTeXの[amsthm](https://www.ctan.org/pkg/amsthm)パッケージにおける定理環境のためのBlock要素は、当然ながらAsciiDocでは提供されていません。HTML出力する場合は、AsciiDocのOpen Block要素に対して `[.myclass]` といった記述で属性を付与して、CSSでスタイルを適用する、という方法があります。しかし `asciidoctor-pdf` によるpdf出力の場合は、付与した属性に対してスタイルを適用する方法は（私の調べた限り）ありません。

[asciidoctor-latex](https://github.com/asciidoctor/asciidoctor-latex)というものもありますが、メンテナンスがされていなかったりするため、今回は選択肢から外しています。

# 課題

以上をまとめると、以降では次の問題を解決するアプローチを提示します。

* 課題1. 多様な数式を綺麗に出力したい
* 課題2. pdf出力した際も定理・証明環境に対してスタイルを適用したい



# 解決方針と詳細

ざっくりとしたアプローチとして、texで処理できれば解決するのでは？と考え、実際にそのための手順を提示します。

問題の依存関係的に、課題1を解決して、その後に課題2を解決します。

## 課題1の解決策

結論から述べると、`asciidoctor -b docbook`で、asciidocファイルを一旦docbook形式へ変換し、その後`dblatex -b xetex`でpdf出力します。しかし、何も考えず `input.adoc` を変換しても一部綺麗に表示されません。

### pdf出力の基本的な流れ


一部綺麗にされない点は一旦保留して、ひとまずpdfへ変換することを考えます。はじめにadocファイルをdocbookファイルへ変換します。

```shell
asciidoctor -b docbook input.adoc -o input.xml
```

次にdocbookファイルをpdfへ変換します。dblatex処理時のxetexで、日本語を表示させるためのプリアンブルを追記する必要があります。dblatexに含まれるスタイルファイル（私の場合は `/usr/share/dblatex/latex/style/docbook.sty` にありました）を作業ディレクトリへコピー（ここでは `styles/mytheme.sty` とします）し、必要な追記をします。

```latex:styles/mytheme.sty
%%
%% Default dblatex DocBook style
%%                                                                
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{docbook}[2007/04/04 DocBook Style]
...
% 以降追記部分
\usepackage{amsfonts}
\setmainfont{Noto Serif CJK JP}
\setsansfont{Noto Sans CJK JP}
\setmonofont{Noto Sans Mono CJK JP}

\XeTeXlinebreaklocale "ja"
```

下記のコマンドを実行すればpdf出力が得られます。

```shell
dblatex --texstyle=styles/mytheme.sty -b xetex input.xml -o out.pdf
```

### 残り一部を綺麗に表示

先程得られたpdf出力を見ると、ディスプレイモードとして記述した数式箇所が、インラインになっています。これを修正することを考えます。dblatexに含まれるxslファイルの記述を一部流用（私の場合は `/usr/share/dblatex/xsl/equation.xsl` ）して下記の `styles/mytheme.xsl` を準備します。

```xml:styles/mytheme.xsl
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

  <xsl:template match="alt" mode="latex">
  <xsl:variable name="delim">
    <xsl:call-template name="pi.texmath_delimiters"/>
  </xsl:variable>

  <xsl:variable name="tex">
    <xsl:variable name="text" select="normalize-space(.)"/>
    <xsl:variable name="len" select="string-length($text)"/>
    <xsl:choose>
    <xsl:when test="$delim='user'">
      <xsl:copy-of select="."/>
    </xsl:when>
    <xsl:when test="ancestor::equation[not(child::title)]">
      <!-- Remove any math mode in an equation environment -->
      <xsl:choose>
      <xsl:when test="starts-with($text,'$') and
                      substring($text,$len,$len)='$'">
        <xsl:copy-of select="substring($text, 2, $len - 2)"/>
      </xsl:when>
      <xsl:when test="(starts-with($text,'\[') and
                       substring($text,$len - 1,$len)='\]') or
                      (starts-with($text,'\(') and
                       substring($text,$len - 1,$len)='\)')">
        <xsl:copy-of select="substring($text, 3, $len - 4)"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:copy-of select="."/>
      </xsl:otherwise>
      </xsl:choose>
    </xsl:when>
    <!-- Test to be DB5 compatible, where <alt> can be in other elements -->
    <xsl:when test="ancestor::equation or
                    ancestor::informalequation or
                    ancestor::inlineequation">
      <!-- Keep the specified math mode... -->
      <xsl:variable name="displayed_eq_ed_envs" select="'
        aligned alignedat cases gathered split
      '"/>
      <xsl:variable name="env_name" select="substring-before(
        substring($text, string-length('\begin{') + 1),
        '}'
      )"/>
      <xsl:variable name="expected_closing_tag" select="substring(
        $text,
        $len - string-length(concat('\end{', $env_name, '}')) + 1
      )"/>

      <xsl:choose>
      <xsl:when test="(starts-with($text,'\[') and
                       substring($text,$len - 1,$len)='\]') or
                      (starts-with($text,'\(') and
                       substring($text,$len - 1,$len)='\)') or
                      (starts-with($text,'$') and
                       substring($text,$len,$len)='$')">
        <xsl:copy-of select="$text"/>
      </xsl:when>
      <xsl:when test="(starts-with($text,concat('\begin{',$env_name,'}')) and
                       $expected_closing_tag = concat('\end{',$env_name,'}') and
                       contains($displayed_eq_ed_envs, $env_name))">
        <xsl:copy-of select="concat('$$', $text, '$$')"/>
      </xsl:when>
      <!-- ...Or wrap in default math mode -->
      <xsl:otherwise>
        <xsl:copy-of select="concat('$', $text, '$')"/>
      </xsl:otherwise>
      </xsl:choose>
    </xsl:when>
    <xsl:otherwise/>
    </xsl:choose>
  </xsl:variable>
  <!-- Encode it properly -->
  <xsl:call-template name="scape-encode">
    <xsl:with-param name="string" select="$tex"/>
  </xsl:call-template>
</xsl:template>

</xsl:stylesheet>
```

これを用いて後述のコマンドを実行すると、ディスプレイモードで数式が表示されます。

```shell
dblatex --texstyle=styles/mytheme.sty --xsl-user=styles/mytheme.xsl -b xetex input.xml -o out.pdf
```

## 課題2の解決策

課題1の解決策によって綺麗な数式が得られました。残るはスタイルの適用方法です。イメージとしては、LaTeXにおいて定理用の環境を定義し、adocファイルの該当する部分をそれに適用するよう変換する流れです。

はじめに先程準備したスタイルファイルに追記します。

```latex:styles/mytheme.sty(修正)
%%
%% Default dblatex DocBook style
%%                                                                
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{docbook}[2007/04/04 DocBook Style]
...
% 以降追記部分
\usepackage{bm}
\usepackage{mathptmx}

\usepackage{amsmath}
\usepackage{amsfonts}
\usepackage{amssymb}
\usepackage{mathtools}
% \setmainfont{IPA Pゴシック}
% \setsansfont{IPA P明朝}
% \setmonofont{IPAゴシック}
\setmainfont{Noto Serif CJK JP}
\setsansfont{Noto Sans CJK JP}
\setmonofont{Noto Sans Mono CJK JP}

\usepackage{amsthm} %this package should be loaded before thmtools
\usepackage{thmtools}
\usepackage{mdframed}
\theoremstyle{break}
\declaretheoremstyle[
  spaceabove=1.2em,
  spacebelow=1.2em,
  headfont=\normalfont\bfseries,
  notefont=\normalfont,
  notebraces={(}{)},
  bodyfont=\normalfont,
  headpunct={},
  preheadhook={
    \begin{mdframed}[innertopmargin=0pt,splittopskip=\topskip,topline=true,bottomline=true,leftline=true,rightline=true]
  },
  prefoothook=\end{mdframed}
]{basic_style}

\declaretheoremstyle[
  spaceabove=1.2em,
  spacebelow=1.2em,
  headfont=\normalfont\bfseries,
  notefont=\normalfont,
  notebraces={(}{)},
  bodyfont=\normalfont,
  headpunct={},
  preheadhook={
    \begin{mdframed}[innertopmargin=0pt,splittopskip=\topskip,topline=false,bottomline=false,leftline=true,rightline=false]
  },
  prefoothook=\end{mdframed}
]{proof_style}

\declaretheorem[
  title=定理,
  style=basic_style
]{theorem}

\declaretheorem[
  title=証明,
  style=proof_style,
  numbered=no
]{prf}

\XeTeXlinebreaklocale "ja"
```

上記を見てわかるように、adocファイルを（dblatexの処理内部で）texファイルへ変換した際に、該当部分が `\begin{theorem}...\end{theorem}` や `\begin{prf}...\end{prf}` で囲まれるように変換してほしいというイメージです。

前述のイメージの通り変換されるように、dblatexのデフォルトの変換規則の一部を変更します。先程作成した `styles/mytheme.xsl` を下記のように修正します。

```xml:styles/mytheme.xsl(修正)
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

  <xsl:template match="formalpara">
    <xsl:choose>
      <xsl:when test="@role = 'theorem'">
        <xsl:text>&#10;\begin{theorem}</xsl:text>
        <xsl:apply-templates select="title" mode="format.title"/>
        <xsl:text>&#10;</xsl:text>

        <xsl:apply-templates select="*[not(self::title)]"/>
        <xsl:text>&#10;\end{theorem}&#10;</xsl:text>
      </xsl:when>
      <xsl:when test="@role = 'proof'">
        <xsl:text>\begin{prf}</xsl:text>
        <xsl:apply-templates select="title" mode="format.title"/>
        <xsl:text>&#10;</xsl:text>

        <xsl:apply-templates select="*[not(self::title)]"/>

        <xsl:text>&#10;\end{prf}&#10;</xsl:text>
      </xsl:when>
      <xsl:otherwise>
        <xsl:text>&#10;{\bf </xsl:text>
        <xsl:call-template name="normalize-scape">
          <xsl:with-param name="string" select="title"/>
        </xsl:call-template>
        <xsl:text>} </xsl:text>
        <xsl:call-template name="label.id"/>
        <xsl:apply-templates/>
        <xsl:text>&#10;</xsl:text>
        <xsl:text>&#10;</xsl:text>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

  <!--
    setting for display mode delimiters
  -->


  <xsl:template match="alt" mode="latex">
  <xsl:variable name="delim">
    <xsl:call-template name="pi.texmath_delimiters"/>
  </xsl:variable>

  <xsl:variable name="tex">
    <xsl:variable name="text" select="normalize-space(.)"/>
    <xsl:variable name="len" select="string-length($text)"/>
    <xsl:choose>
    <xsl:when test="$delim='user'">
      <xsl:copy-of select="."/>
    </xsl:when>
    <xsl:when test="ancestor::equation[not(child::title)]">
      <!-- Remove any math mode in an equation environment -->
      <xsl:choose>
      <xsl:when test="starts-with($text,'$') and
                      substring($text,$len,$len)='$'">
        <xsl:copy-of select="substring($text, 2, $len - 2)"/>
      </xsl:when>
      <xsl:when test="(starts-with($text,'\[') and
                       substring($text,$len - 1,$len)='\]') or
                      (starts-with($text,'\(') and
                       substring($text,$len - 1,$len)='\)')">
        <xsl:copy-of select="substring($text, 3, $len - 4)"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:copy-of select="."/>
      </xsl:otherwise>
      </xsl:choose>
    </xsl:when>
    <!-- Test to be DB5 compatible, where <alt> can be in other elements -->
    <xsl:when test="ancestor::equation or
                    ancestor::informalequation or
                    ancestor::inlineequation">
      <!-- Keep the specified math mode... -->
      <xsl:variable name="displayed_eq_ed_envs" select="'
        aligned alignedat cases gathered split
      '"/>
      <xsl:variable name="env_name" select="substring-before(
        substring($text, string-length('\begin{') + 1),
        '}'
      )"/>
      <xsl:variable name="expected_closing_tag" select="substring(
        $text,
        $len - string-length(concat('\end{', $env_name, '}')) + 1
      )"/>

      <xsl:choose>
      <xsl:when test="(starts-with($text,'\[') and
                       substring($text,$len - 1,$len)='\]') or
                      (starts-with($text,'\(') and
                       substring($text,$len - 1,$len)='\)') or
                      (starts-with($text,'$') and
                       substring($text,$len,$len)='$')">
        <xsl:copy-of select="$text"/>
      </xsl:when>
      <xsl:when test="(starts-with($text,concat('\begin{',$env_name,'}')) and
                       $expected_closing_tag = concat('\end{',$env_name,'}') and
                       contains($displayed_eq_ed_envs, $env_name))">
        <xsl:copy-of select="concat('$$', $text, '$$')"/>
      </xsl:when>
      <!-- ...Or wrap in default math mode -->
      <xsl:otherwise>
        <xsl:copy-of select="concat('$', $text, '$')"/>
      </xsl:otherwise>
      </xsl:choose>
    </xsl:when>
    <xsl:otherwise/>
    </xsl:choose>
  </xsl:variable>
  <!-- Encode it properly -->
  <xsl:call-template name="scape-encode">
    <xsl:with-param name="string" select="$tex"/>
  </xsl:call-template>
</xsl:template>

</xsl:stylesheet>
```

これらを用いて下記のコマンドを実行すると、定理部分を枠で囲ったスタイルが適用されたpdfが得られます。

```shell
 dblatex --texstyle=styles/mytheme.sty --xsl-user=styles/mytheme.xsl -b xetex input.xml -o out.pdf
```

# まとめ

今回は、AsciiDocからpdf出力を得る際に、綺麗な数式を描画しつつ、自分好みのスタイルを反映させる方法を提示しました。ここに記載されていない拡張をしたい際にはXSLTの知識が多少要求されますが、数日画面とにらめっこすれば書けるようになると思います。

# 付録1. 雑談

本当はdblatexから足を洗ってpandoc等を使ってdocbookからpdf/texを生成したかったのですが、dblatexでいうところのxsltのような、変換規則を修正するための方法がわかりませんでした。結果としてdblatexでゴリ押しになりました。。。

それと、<font color="red">AsciiDocにおける、数式記述用ブロック内部の扱いについて認識違いしてました。</font>例えば、

```asciidoc:input.adoc
[latexmath]
++++
\lambda = \beta
++++
```
と記述されるAsciiDocファイルをtexファイルへ変換した場合、期待する結果とはどのようなものでしょうか？これは恐らく大多数が一致し、

```latex:out.tex
$$
\lambda = \beta
$$
```
が期待されます。それでは、次の場合はどうでしょうか？

```asciidoc:input.adoc
[latexmath]
++++
\begin{align}
  \lambda = \beta
\end{align}
++++
```
当初の私は、 `$$` といった区切り文字等を特に加えたりせず、そのまま吐き出してくれることを期待しました。しかしpandocにおけるmarkdown2latexの挙動等をみると、どうもその認識は誤りらしいという結論に達しました。ブロック内はmath modeとして扱うのが適切のようです。つまり上記の記述は誤りで、類似の記述だと下記のようになります。

```asciidoc:input.adoc
[latexmath]
++++
\begin{aligned}
  \lambda = \beta
\end{aligned}
++++
```
あまりスッキリしませんが、パーサ等に手を加える気はないので受け入れる他ないですね。


[^1]: これは`dblatex`に付属する`style/docbook.sty`へ、日本語や数式表示のために最後数行書き加えたものになっています。