Pandocでヌジョレーボーボー

> これは「[Pandoc Advent Calendar 2019](https://adventar.org/calendars/4338)」の20日目の記事です。
（19日目は [skyy_writing さん](https://qiita.com/sky_y/items/3c5c46ebd319490907e8) です。）
（21日目は [hid_alma1026 さん](https://laughingandgrief.hatenablog.com/entry/2019/12/21/233441) です。）

あるいは、「**Pandoc’s MarkdownのSpan記法はトッテモ便利**」という話。

# ヌジョレーボーボーとは

コレ→[「ヌジョレーボーボー」][noujolais]。

[noujolais]: https://qiita.com/mattn/items/ae764601862b0073071e

要するに、「ボジョレーヌーボー」の文字をランダムに入れ替えて「ヌジョレーボーボー」とか「ジョボヌーボーレー」とかに変換するやつです。

もう少し厳密に定義するとこんな感じでしょうか。（※あまり厳密に定義する気はないので先に進みましょう:upside_down:）

* 入力は日本語の仮名（ひらがな・カタカナ）の文字列とする。
* 仮名のうち「ァ ィ ゥ ェ ォ ャ ュ ョ ヮ」（および対応するひらがな）を「拗音文字」と呼ぶ。「ー ッ ン」（および対応するひらがな）を「特殊拍文字」と呼ぶ。
* 入力文字列において、拗音文字は特殊拍文字の直後および先頭には来ないものとする。
* 拗音文字以外の文字、およびそれに拗音文字の列を連ねたものを「拍」と呼ぶ。
* 入力文字列を拍の列に分解し、特殊拍文字の拍の位置は固定してそれ以外の拍をランダムに並べ替えて、新しい文字列を作る。

## Luaで実装してみる

“ヌジョレーボーボー変換”をLuaで実装してみましょう[^lua-are]。

[^lua-are]: LuaはUTF-8対応がアレでしかも正規表現がアレアレなのでかなりつらい……:frowning2:

```lua
-- 準備
math.randomseed(os.time())
local kana_type = {}
local function kana_type_assign(str, kt)
  for p, uc in utf8.codes(str) do
    kana_type[utf8.char(uc)] = kt
  end
end
kana_type_assign('ぁぃぅぇぉゃゅょゎァィゥェォャュョヮ', 1)
kana_type_assign('ーっんッン', 2)

--- noujolais(str)
-- 文字列strに"ヌジョレーボーボー変換"を施した結果の文字列を返す.
local function noujolais(str)
  local parts, idxs = {}, {}
  for p, uc in utf8.codes(str) do
    local ch = utf8.char(uc)
    local kt = kana_type[ch] or 0
    if #parts > 0 and kt == 1 then
      parts[#parts] = parts[#parts]..ch
    else
      parts[#parts + 1] = ch
      if kt ~= 2 then
        idxs[#idxs + 1] = #parts
      end
    end
  end
  for k = #idxs, 1, -1 do
    local p = math.random(k)
    parts[idxs[p]], parts[idxs[k]] = parts[idxs[k]], parts[idxs[p]]
    idxs[p], idxs[k] = idxs[k], idxs[p]
  end
  return table.concat(parts)
end
```

実装てきたようなので動作を試してみましょう。

```lua
print(noujolais('ヌジョレーボーボー'))           -->レボジョーボーヌー
print(noujolais('キーパーソン'))                 -->パーソーキン
print(noujolais('きゃりーぱみゅぱみゅ'))         -->りぱーみゅぱきゃみゅ
print(noujolais('ファンキーモンキーベイビーズ')) -->ビンベーキンモーファキズーイ
```

バッチリですね！[^no-validate]

[^no-validate]: 入力が正当であるかのチェックは行っていません。正当でない文字列の場合は変換結果は何らかの文字列になりますがそれ以上のことは未定義です:smiley:

# Pandocでヌジョレーボーボーしたい

ヌジョレーボーボーがLuaで実装できたとなると、今度は「PandocでヌジョレーボーボーをするLuaフィルタ」を作りたくなる、というのは**当然の要求**でしょう。というわけで、次のようなLuaフィルタを実装しましょう。

* Markdown文書において、“特別にマークアップされた”文字列に対してヌジョレーボーボー変換を適用する。

## Markdownにまた新しい記法を追加しない話

さて、ここで問題になるのは**記法をどうするか**ということです。つまり、Markdown文書中に`ボジョレーヌーボー`という文字列があってこれにヌジョレーボーボー変換を適用したい場合、どのように書く規則にすればいいでしょうか？

一説によると「Pandoc界隈はわりと異様に記法に拘る<sup>[要出典]</sup>」ようなので、もしかしたら

```text
㋦ボジョレーヌーボー㋦
```

みたいなのを考えた人もいるかもしれません。でもPandocを使う場合にはもっとスマートな方法があります。

```text
[ボジョレーヌーボー]{.noujolais}
```

これは**Span記法**と呼ばれているもので、ちょうどHTMLにおける

```html
<span class="noujolais">ボジョレーヌーボー</span>
```

に対応するものです（つまり`noujolais`は**クラス名**）。HTMLのspan要素はよく“独自のマークアップ”のために用いられますが、Pandocでも同じ目的でSpan記法を用いることができるわけです。

このような「クラスつきのSpan記法」を利用することは（例えば`㋦ボジョレーヌーボー㋦`のような）真に独自の記法を持ち込むことと比較して次のようなメリットがあります。

* 既存の記法の「利用」に過ぎないので、新しい記法に関する細則を決める（およびフィルタ利用者が習得する）必要がない。
    * 例えば`[ボジョレーヌーボー]{.noujolais}`を「そのまま出力」したいのであれば、単純にPandoc’s Markdownの規則に従って`[`をエスケープ（`\[`）すれば済む。対して、`㋦ボジョレーヌーボー㋦`という記法の場合は「そのまま出力」する方法について何らかの考慮（および実装）を行う必要がある[^escape]。
* 既存の記法であるため「その記法の部分を切り出す」ためのパーザの実装が不要である。
    * マークアップされた部分のインラインが既に切り出されている。
    * 例えばマークアップ部分が別のマークアップを含む（例えば`[ボジョレー**ヌー**ボー]{.noujolais}`とか）でも勝手に対応されている。（もっとも今回はそのような入力はサポート対象外だが。）

[^escape]: ちなみに「`㋦`をエスケープして`\㋦`とする」という規則は、もちろんPandoc’s Markdownの規則にはないわけですが、実は実装することも困難です。なぜかというと、Pandoc’s Markdownの規則で`\㋦`は文字「㋦」と解釈されて、従ってフィルタの入力の時点で`㋦`との区別がつかなくなっているからです。文字によっては`\`を前置するとエスケープにならずにTeX記法が発動することもあります。何れにしても「Pandoc’s Markdownの標準の記法とエスケープの規則が統一できない」というのは大きな欠点といえるでしょう。

以下では、Span記法を採用する場合のLuaフィルタの実装手順を簡単に説明します。

※実は、Pandoc標準のマークアップ機能でも、Markdownでの記法が「クラスつきのSpan記法」であるものが存在します。例えば「スモールキャプス」は`[Some text]{.smallcaps}`のように記述します[^smallcaps]。

[^smallcaps]: `{.smallcaps}`つきのSpan記法の解釈結果は`Span`の値ではなく`SmallCaps`の値となります。

※Span記法の`{ }`の中にはクラス（`.class`）の他にもID（`#id`）や属性（`key=value`）を書くことができます。

# Pandocでヌジョレーボーボーする

マークアップとして「クラスつきのSpan記法」を採用した場合、それを実際に処理するLuaフィルタの実装のコードは次のような形になります。

```lua
-- 'Span'という名前の関数をグローバルに定義すればよい
function Span(span)
  if span.classes:includes('クラス名', 1) then -- 目的のクラスである
    -- ここでspanに対して必要な変更を加える.
    return span
  end
  -- 目的のクラスでない場合は何もreturnしない.
  -- この場合, 当該のSpan要素は何も変更されない.
end
```

Spanの内容（`[]`の中身）が単一の文字列である場合（今回はこれに対応するだけでよい）は、`span`の中身は（少し単純化して）次のようなテーブル値になっています。

```
{
  t = "Span"; -- なお't'キーには'tag'でもアクセスできる
  classes = {"クラス名1", "クラス名2", ...}; -- クラス名のリスト
  content = { -- 内容(インラインのリスト)
    {t = "Str"; text = "文字列" };
  }
}
```

従って、先ほど実装したヌジョレーボーボー変換（`noujolais`関数）を内容の文字列に適用したい、という場合は以下のようにすればいいわけです。

```lua
span.content[1].text = noujolais(span.content[1].text)
```

「Spanの内容が単一の文字列である」ことのチェックも入れた完全な`Span`関数の実装は以下の通りです。

```lua
-- Span要素に対する処理.
function Span(span)
  -- Spanがnoujolaisクラスつきならば
  if span.classes:includes('noujolais', 1) then 
    -- 内容が単一のStrであるならば
    if #span.content == 1 and span.content[1].t == 'Str' then
      local str = span.content[1] -- Str要素
      str.text = noujolais(str.text)
      -- spanを返すことを忘れずに!
      return span
    else -- それ以外はエラー
      error('bad noujolais span')
    end
  end
end
```

この`Span`関数のコードを先ほどの`noujolais`関数の実装の後に[^after]書いて、完全なLuaフィルタコードが完成しました！

[^after]: `noujolais`関数はlocalで定義しているので、スコープはその後に限られます。

* [noujolais.lua](https://gist.github.com/zr-tex8r/884826a99342cee7ec301d6dfa2be1c7) （Gist:zr-tex8r）

# Pandocでヌジョレーボーボーできた

適当にMarkdown文書を作って、それをWord文書（docx）に変換してみましょう。

```markdown:test1.md
「今年の[ボジョレーヌーボー]{.noujolais}は令和になって最高の出来栄え。」
```

```bash:コマンド行
pandoc test1.md --lua-filter noujolais.lua -o test1.docx
```

出力のtest1.docxをWordで開くと……

![image-noujolais1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/f9269211-630d-aad9-a274-925b97098197.png)

**完璧ですね！**:smiley:

## おまけ：HTMLをヌジョレーボーボーする

先ほど述べたように、Pandoc’s MarkdownのSpan記法はHTMLのspan要素に対応するものです。なので、変換元をHTML文書にしてその中にspan要素を書いた場合もPandocの解釈結果としては同値となります。ということは、noujolais.luaフィルタは実は変換元がHTMLである場合でも使えます。

```html:test2.html
<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
</head><body>
<ul> <!-- "noujolais"クラスつきのspan要素を書く -->
<li>「今年の<span class="noujolais">ボジョレーヌーボー</span>は令和になって最高の出来栄え。」</li>
</ul>
</body></html>
```

```bash:コマンド行
pandoc test2.html --lua-filter noujolais.lua -o test2.tex
```

出力結果：

```latex:test2.tex
\begin{itemize}
\tightlist
\item
  「今年の{ジョヌボーレーボー}は令和になって最高の出来栄え。」
\end{itemize}
```

# まとめ

* Markdownからの変換をフィルタで拡張する際に、“独自のマークアップ記法”が必要になった場合は**Pandoc’s MarkdownのSpan記法**を利用しましょう！:information_desk_person: