これは「Pandoc Advent Calendar 2019」の20日目の記事です。
(19日目は skyy_writing さん です。)
(21日目は hid_alma1026 さん です。)
あるいは、「Pandoc’s MarkdownのSpan記法はトッテモ便利」という話。
ヌジョレーボーボーとは
コレ→「ヌジョレーボーボー」。
要するに、「ボジョレーヌーボー」の文字をランダムに入れ替えて「ヌジョレーボーボー」とか「ジョボヌーボーレー」とかに変換するやつです。
もう少し厳密に定義するとこんな感じでしょうか。(※あまり厳密に定義する気はないので先に進みましょう)
- 入力は日本語の仮名(ひらがな・カタカナ)の文字列とする。
- 仮名のうち「ァ ィ ゥ ェ ォ ャ ュ ョ ヮ」(および対応するひらがな)を「拗音文字」と呼ぶ。「ー ッ ン」(および対応するひらがな)を「特殊拍文字」と呼ぶ。
- 入力文字列において、拗音文字は特殊拍文字の直後および先頭には来ないものとする。
- 拗音文字以外の文字、およびそれに拗音文字の列を連ねたものを「拍」と呼ぶ。
- 入力文字列を拍の列に分解し、特殊拍文字の拍の位置は固定してそれ以外の拍をランダムに並べ替えて、新しい文字列を作る。
Luaで実装してみる
“ヌジョレーボーボー変換”をLuaで実装してみましょう1。
-- 準備
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
実装てきたようなので動作を試してみましょう。
print(noujolais('ヌジョレーボーボー')) -->レボジョーボーヌー
print(noujolais('キーパーソン')) -->パーソーキン
print(noujolais('きゃりーぱみゅぱみゅ')) -->りぱーみゅぱきゃみゅ
print(noujolais('ファンキーモンキーベイビーズ')) -->ビンベーキンモーファキズーイ
バッチリですね!2
Pandocでヌジョレーボーボーしたい
ヌジョレーボーボーがLuaで実装できたとなると、今度は「PandocでヌジョレーボーボーをするLuaフィルタ」を作りたくなる、というのは当然の要求でしょう。というわけで、次のようなLuaフィルタを実装しましょう。
- Markdown文書において、“特別にマークアップされた”文字列に対してヌジョレーボーボー変換を適用する。
Markdownにまた新しい記法を追加しない話
さて、ここで問題になるのは記法をどうするかということです。つまり、Markdown文書中にボジョレーヌーボー
という文字列があってこれにヌジョレーボーボー変換を適用したい場合、どのように書く規則にすればいいでしょうか?
一説によると「Pandoc界隈はわりと異様に記法に拘る[要出典]」ようなので、もしかしたら
㋦ボジョレーヌーボー㋦
みたいなのを考えた人もいるかもしれません。でもPandocを使う場合にはもっとスマートな方法があります。
[ボジョレーヌーボー]{.noujolais}
これはSpan記法と呼ばれているもので、ちょうどHTMLにおける
<span class="noujolais">ボジョレーヌーボー</span>
に対応するものです(つまりnoujolais
はクラス名)。HTMLのspan要素はよく“独自のマークアップ”のために用いられますが、Pandocでも同じ目的でSpan記法を用いることができるわけです。
このような「クラスつきのSpan記法」を利用することは(例えば㋦ボジョレーヌーボー㋦
のような)真に独自の記法を持ち込むことと比較して次のようなメリットがあります。
- 既存の記法の「利用」に過ぎないので、新しい記法に関する細則を決める(およびフィルタ利用者が習得する)必要がない。
- 例えば
[ボジョレーヌーボー]{.noujolais}
を「そのまま出力」したいのであれば、単純にPandoc’s Markdownの規則に従って[
をエスケープ(\[
)すれば済む。対して、㋦ボジョレーヌーボー㋦
という記法の場合は「そのまま出力」する方法について何らかの考慮(および実装)を行う必要がある3。
- 例えば
- 既存の記法であるため「その記法の部分を切り出す」ためのパーザの実装が不要である。
- マークアップされた部分のインラインが既に切り出されている。
- 例えばマークアップ部分が別のマークアップを含む(例えば
[ボジョレー**ヌー**ボー]{.noujolais}
とか)でも勝手に対応されている。(もっとも今回はそのような入力はサポート対象外だが。)
以下では、Span記法を採用する場合のLuaフィルタの実装手順を簡単に説明します。
※実は、Pandoc標準のマークアップ機能でも、Markdownでの記法が「クラスつきのSpan記法」であるものが存在します。例えば「スモールキャプス」は[Some text]{.smallcaps}
のように記述します4。
※Span記法の{ }
の中にはクラス(.class
)の他にもID(#id
)や属性(key=value
)を書くことができます。
Pandocでヌジョレーボーボーする
マークアップとして「クラスつきのSpan記法」を採用した場合、それを実際に処理する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
関数)を内容の文字列に適用したい、という場合は以下のようにすればいいわけです。
span.content[1].text = noujolais(span.content[1].text)
「Spanの内容が単一の文字列である」ことのチェックも入れた完全なSpan
関数の実装は以下の通りです。
-- 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
関数の実装の後に5書いて、完全なLuaフィルタコードが完成しました!
- noujolais.lua (Gist:zr-tex8r)
Pandocでヌジョレーボーボーできた
適当にMarkdown文書を作って、それをWord文書(docx)に変換してみましょう。
「今年の[ボジョレーヌーボー]{.noujolais}は令和になって最高の出来栄え。」
pandoc test1.md --lua-filter noujolais.lua -o test1.docx
出力のtest1.docxをWordで開くと……
完璧ですね!
おまけ:HTMLをヌジョレーボーボーする
先ほど述べたように、Pandoc’s MarkdownのSpan記法はHTMLのspan要素に対応するものです。なので、変換元をHTML文書にしてその中にspan要素を書いた場合もPandocの解釈結果としては同値となります。ということは、noujolais.luaフィルタは実は変換元がHTMLである場合でも使えます。
<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
</head><body>
<ul> <!-- "noujolais"クラスつきのspan要素を書く -->
<li>「今年の<span class="noujolais">ボジョレーヌーボー</span>は令和になって最高の出来栄え。」</li>
</ul>
</body></html>
pandoc test2.html --lua-filter noujolais.lua -o test2.tex
出力結果:
\begin{itemize}
\tightlist
\item
「今年の{ジョヌボーレーボー}は令和になって最高の出来栄え。」
\end{itemize}
まとめ
- Markdownからの変換をフィルタで拡張する際に、“独自のマークアップ記法”が必要になった場合はPandoc’s MarkdownのSpan記法を利用しましょう!
-
LuaはUTF-8対応がアレでしかも正規表現がアレアレなのでかなりつらい…… ↩
-
入力が正当であるかのチェックは行っていません。正当でない文字列の場合は変換結果は何らかの文字列になりますがそれ以上のことは未定義です ↩
-
ちなみに「
㋦
をエスケープして\㋦
とする」という規則は、もちろんPandoc’s Markdownの規則にはないわけですが、実は実装することも困難です。なぜかというと、Pandoc’s Markdownの規則で\㋦
は文字「㋦」と解釈されて、従ってフィルタの入力の時点で㋦
との区別がつかなくなっているからです。文字によっては\
を前置するとエスケープにならずにTeX記法が発動することもあります。何れにしても「Pandoc’s Markdownの標準の記法とエスケープの規則が統一できない」というのは大きな欠点といえるでしょう。 ↩ -
{.smallcaps}
つきのSpan記法の解釈結果はSpan
の値ではなくSmallCaps
の値となります。 ↩ -
noujolais
関数はlocalで定義しているので、スコープはその後に限られます。 ↩