Pandocで湯婆婆

>   これは「[Pandoc Advent Caleandar 2020](https://adventar.org/calendars/4963)」の12日目の記事です。

なんか最近のアドベントカレンダーの[記事][atusy]を[イロイロ][monaqa]読んでいるうちに、やらなければいけない気がしてきたので :upside_down: 

## Pandocで湯婆婆するLuaフィルタ

* [yubaba.lua][gist]

<details>
<summary>コードリストを表示</summary>
<div>

```lua:yubaba.lua
-- yubaba.lua
--
-- @copyright 2020 Takayuki YATO (aka. "ZR")
--   GitHub:   https://github.com/zr-tex8r
--   Twitter:  @zr_tex8r
--   This program is distributed under the MIT License.
--
local filter_name = 'yubaba'
---------------------------------------- 設定

-- 湯婆婆処理対象のクラス名
local yubaba_class = 'yubaba'

---------------------------------------- 補助

local utils = utils or require 'pandoc.utils'

-- 乱数種の設定
math.randomseed(os.time())

--- 名前を1文字に短縮する.
-- ※名前が空ならエラー.
-- @param name 名前(文字列)
-- @return 短縮した名前(文字列)
local function compact(name)
  local ucs = { utf8.codepoint(name, 1, #name) }
  if #ucs == 0 then -- 名前が空である
    error("name is empty")
  end
  return utf8.char(ucs[math.random(#ucs)])
end

--- Pandocオブジェクトならクラス名, それ以外ならLua型名を返す.
-- @return 型名(文字列)
local function ptype(v)
  local t = type(v)
  return t == 'table' and v.t or t
end

--- 複数のテーブルを連結する.
-- @return 結果(テーブル)
local function merge(...)
  local dst, srcs = {}, {...}
  for i = 1, #srcs do
    local src = srcs[i]
    for j = 1, #src do dst[#dst+1] = src[j] end
  end
  return dst
end

--- オブジェクトが湯婆婆処理対象のSpanであるか.
-- @return 真偽値
local function is_yubaba_span(el)
  return ptype(el) == 'Span' and el.classes:includes(yubaba_class)
end

--- オブジェクトが湯婆婆処理対象のParaであるか.
-- @return 真偽値
local function is_yubaba_para(el)
  -- Paraの内容が"湯婆婆処理対象のSpan"のみであるかを調べる.
  if ptype(el) == 'Para' and #el.content == 1 then
    return is_yubaba_span(el.content[1])
  end
  return false
end

---------------------------------------- フェーズ'main'
-- 湯婆婆対象のParaを変換する.

--- Paraに対する処理.
local function main_Para(el)
  if is_yubaba_para(el) then -- 湯婆婆対象の場合
    local yspan = el.content[1] -- 湯婆婆対象のSpan
    -- 新しい名前を得る. utils.stringify() は"文字列化"の関数.
    local newname = compact(utils.stringify(yspan))
    -- 変換後の内容を返す.
    return { -- 複数ブロックを返していることに注意
      pandoc.Para {
        pandoc.Str "「契約書だよ。そこに名前を書きな。」"
      },
      pandoc.BlockQuote {
        pandoc.Para(yspan.content) -- 元の名前
      },
      pandoc.Para(merge(
        { pandoc.Str "「フン。" },
        yspan.content, -- 元の名前
        {
          pandoc.Str "というのかい。贅沢な名だねえ。」";
          pandoc.LineBreak();
          pandoc.Str "「今からお前の名前は";
          pandoc.Str(newname); -- 新しい名前
          pandoc.Str "だ。いいかい、";
          pandoc.Str(newname); -- 新しい名前
          pandoc.Str "だよ。分かったら返事をするんだ、";
          pandoc.Str(newname); -- 新しい名前
          pandoc.Str "！」";
        }
      ))
    }
  end -- 湯婆婆対象以外は変更しない
end

---------------------------------------- フェーズ'check'
-- 湯婆婆対象のSpanが残っていないことを確認する.
-- 残っている場合は"湯婆婆対象のSpanの記述条件"に反しているのでエラー.

--- Spanに対する処理.
local function check_Span(el)
  if is_yubaba_span(el) then
    error("bad yubaba span found")
  end
end

---------------------------------------- フィルタ定義
return {
  {-- フェーズ'main'
    Para = main_Para;
  };
  {-- フェーズ'check'
    Span = check_Span;
  };
}
---------------------------------------- おしまい
```

</div>
</details>

### 仕様

[例によって][noujolais]、入力のマークアップの書式として「クラス付のSpan」を利用しています。

`yubaba`クラスの付いたSpan要素**のみ**からなる段落がある場合、その要素の内容のテキストを「契約者の名前」とした場合の湯婆婆の台詞に変換されます。

```markdown:Markdown入力の場合
[契約者の名前]{.yubaba}
```

※`yubaba`クラスのSpan要素のある段落に他のテキストが混在している場合は不正でありエラーになります。

### 実行例

例えば、次のようなPandoc’s Markdownの入力ファイルを用意します。

```md:sample-1.md
# Pandocで湯婆婆してみた

[荻野千尋]{.yubaba}
```

これをHTMLに変換する際にyubabaフィルタを適用します。

```bash
pandoc sample-1.md -L yubaba.lua -o sample-1.html
```

以下のHTMLが得られました。レンダリングした結果も一緒に載せておきます。

```html:sample-1.html
<h1 id="pandocで湯婆婆してみた">Pandocで湯婆婆してみた</h1>
<p>「契約書だよ。そこに名前を書きな。」</p>
<blockquote>
<p>荻野千尋</p>
</blockquote>
<p>「フン。荻野千尋というのかい。贅沢な名だねえ。」<br />
「今からお前の名前は千だ。いいかい、千だよ。分かったら返事をするんだ、千！」</p>
```

![image-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/5cb1d896-c9fb-3f36-6d8e-d447b92ba88e.png)

#### フン。获野千尋というのかい。

※[元ネタを参照][monaqa-1]

もちろん、元の名前がBMP外の文字を含んでいてもOKです。

```md:sample-2.md
[获野千尋]{.yubaba}
```

変換方法は先ほどと全く同じで、以下の出力が得られます。

```bash
pandoc sample-2.md -L yubaba.lua -o sample-2.html
```

![image-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/19e7e146-86aa-ccc3-d5cd-aa30081d142f.png)

※「获」だけフォントが違う問題は、適切なフォントをCSSで指定することで解決できます。

#### フン。\*\*获野千尋\*\*というのかい。

※[元ネタを参照][monaqa-2]

契約書に太字で[^strong]名前を書いた場合もOKです。

[^strong]: フン。Markdownの`**…**`は「太字」じゃなくて云々、ていいたいのかい。贅沢なツッコミだねえ。

```md:sample-3.md
[**获野千尋**]{.yubaba}
```

変換方法と結果は以下の通りです。

```bash
pandoc sample-3.md -L yubaba.lua -o sample-3.html
```

![image-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/eb7a2c27-da97-ce31-149a-06d47922bdfc.png)

#### フン。というのかい。

最後に、元の名前が空の場合も試してみましょう。

```console
> pandoc sample-4.md -L yubaba.lua -o sample-4.html
Error running filter yubaba.lua:
yubaba.lua:28: name is empty
stack traceback:
        [C]: in function 'error'
        yubaba.lua:28: in upvalue 'compact'
        yubaba.lua:75: in function <yubaba.lua:71>
```

ちゃんとクラッシュしましたね:upside_down: 

### まとめ

[Pandoc Advent Calendar 2020][advent]、まだまだ空き枠だらけでトッテモ寂しいので、皆さんの奮っての参加をお待ちしています:bow: 

[advent]: https://adventar.org/calendars/4963
[atusy]: https://blog.atusy.net/2020/12/07/lua-filter-is-hot/
[monaqa]: https://qiita.com/monaqa/items/33fe5be7d5f884f1840f
[monaqa-1]: https://qiita.com/monaqa/items/33fe5be7d5f884f1840f#%E3%81%B5%E3%82%93%E8%8E%B7%E9%87%8E%E5%8D%83%E5%B0%8B%E3%81%A8%E3%81%84%E3%81%86%E3%81%AE%E3%81%8B%E3%81%84
[monaqa-2]: https://qiita.com/monaqa/items/33fe5be7d5f884f1840f#%E3%81%B5%E3%82%93textbf%E8%8E%B7%E9%87%8E%E5%8D%83%E5%B0%8B%E3%81%A8%E3%81%84%E3%81%86%E3%81%AE%E3%81%8B%E3%81%84
[gist]: https://gist.github.com/zr-tex8r/43090955d778d68528b420c661657a14
[noujolais]: https://qiita.com/zr_tex8r/items/efbf8d747e7144efcf15