昨年(2025年)のQiitaのアドベントカレンダー企画において「Hello World あたたた Advent Calendar 2025」が実施されました。これは「Hello World あたたたた」というプログラミング課題を様々なプログラム言語で解いていくというもので、最終的に60件以上の記事が登録されました。
今回はPandocで「Hello World あたたたた」を実装して解説していきます。
Pandocとは
Pandocとは一言でいうと「Markdownをピボットとする多機能な文書形式コンバータ」です。変換対象として60種類以上の文書形式がサポートされていて、「MarkdownからHTMLへ」「reStructuredTextからLaTeXへ」「LaTeXからWordへ」のように入力と出力の形式を自由に選んで変換ができます。特徴的なのは、変換の内部において構造化文書の内容を(拡張された)Markdownに対応する抽象的表現で表していることです。つまりPandocは飽くまで「構造化文書の構造部分の文書形式変換」に特化したツールになっています。
Pandocでの文書形式の変換では間にフィルタを挟んで文書内容を加工することができて、この機能は例えば「Markdownへの独自記法の追加」(よくありますね
)を実現するのに使用されます。フィルタは外部プログラム(実行ファイル)として用意できますが、今のPandocではLua処理系が搭載されていて、Luaプログラムで手軽にフィルタ(Luaフィルタ)を実装できます。
今回は文書中に出現する「HelloWorld」という文字列を全て「Hello World あたたたた」の出力テキストに置き換えるようなLuaフィルタを実装します。
コーディング例
-- 乱数の初期化
math.randomseed(os.time())
-- Hello World あたたたの出力を文字列として得る.
local function hello_world_atatatata()
local hako = "" -- 出力した文字をためていく変数
local running = true -- プログラムを続けるかどうかのフラグ
local out -- 最終的に出力すべき文字列
while running do
-- 0 または 1 をランダムに生成
local x = math.random(0, 1)
-- 条件に応じて「あ」または「た」を選ぶ
local char = (x == 0) and "あ" or "た"
-- 出力した文字を hako に追加
hako = hako..char
-- 最後の 5 文字が「あたたたた」なら終了
if hako:find("あたたたた$") then
-- out に出力を詰め込む
out = hako.."\nお前はもう死んでいる"
running = false
end
end
return out
end
-- Str要素に適用するフィルタ.
function Str(element)
local text = element.text -- 内容の文字列
local out = {} -- 出力すべき要素のリスト
-- outに文字列を空でなければ追加する.
local function add_Str(str)
if str ~= "" then
table.insert(out, pandoc.Str(str))
end
end
-- "HelloWorld"の複数回出現を考慮して置換処理を反復する
while true do
-- "HelloWorld"の出現を探す
local left, right = text:find("HelloWorld")
-- left, rightは出現の左端と右端のオフセット (バイト単位)
if left == nil then -- もう出現しない
add_Str(text) -- そのまま出力する
break
end
-- "あたたたた"の結果を得る
local atata = hello_world_atatatata()
-- "HelloWorld"の前側の文字列を出力
add_Str(text:sub(1, left - 1))
-- "あたたたた"の結果を出力
for line in atata:gmatch("[^\n]+") do
add_Str(line)
table.insert(out, pandoc.LineBreak()) -- 改行要素
end
table.remove(out) -- 最後の改行は不要
-- "HelloWorld"の後側の文字列に更新してループ
text = text:sub(right + 1)
end
return out
end
実行例
入力として以下のようなMarkdownファイル(文字しかないですが)を用意して、これを前掲のLuaフィルタhwata.luaを適用した状態で「プレーンテキスト形式に変換して標準出力に出力」してみます。
HelloWorld
コマンドラインは以下の通りです。-t plainは出力形式の指定で、-Lで適用するLuaフィルタのパス名を指定します。
pandoc -L hwata.lua -t plain input.md
例えば以下のような出力が得られます。
たああたあたあたたたああああたあたあたたたああたあたあたたたた
お前はもう死んでいる
もう少し複雑な入力の例として、以下のMarkdownファイルをHTML形式に変換してみます。
# サンプル
隣のHelloWorldはよく柿食うHelloWorldだ。
- **強いHelloWorld**
コマンドラインは以下の通りです。今回はファイルに出力したいので-oでパス名1を指定します。
pandoc -L hwata.lua input2.md -o output2.html
例えば以下のようなHTMLファイルが出力されます。
<h1 id="サンプル">サンプル</h1>
<p>隣のたあたたたた<br />
お前はもう死んでいるはよく柿食うたああたたたた<br />
お前はもう死んでいるだ。</p>
<ul>
<li><strong>強いたあたああたたたあたああたああたたたあたあたあたあたあたあああたあたたあたたたあたあたああああたあたたああああああああたたあたあたあああたあたあたたたあたたたた<br />
お前はもう死んでいる</strong></li>
</ul>
コードと文法の解説
フィルタプログラムの構成
Luaフィルタプログラムの構成には複数の方式がありますが、ここでは簡単な「グローバル関数を定義する」方式を採用しています。プログラムではStrという名前のグローバル関数を定義しています2が、これは文書の抽象構文木(AST)の中の文字列要素(Str要素)のノードに対する変換を規定します。他の要素についても同様で、例えばHeadingというグローバル関数は節見出し要素(Heading要素)のノード変換を規定します。
- Pandocの実行時に入力文書の全てのStr要素について
Str関数が実行されます。 - 今回のフィルタの目的は
HelloWorldを「あたたたた」の出力に変換することですが、この出力には改行が含まれます。PandocのASTにおいて改行は文字列(Str要素)の一種ではなく独自の要素(LineBreak要素)であるため、フィルタの出力は「Str要素とLineBreak要素が混在するリスト」となります。 - 「文書中の
HelloWorldを全て置き換える」という仕様を満たすため、単一のStr要素にHelloWorldが複数回出現する場合も考慮する必要があります。
「Hello World あたたたた」の実装部分
プログラム中のhello_world_atatatata関数が「Hello World あたたたた」の出力を生成している部分です。この実装は過去のLuaの記事にあるものとほぼ同じなので詳細の説明は省略しますが、相違点について述べておきます。
-
「出力」の扱いについて、その場で標準出力に出力するのではなく「出力すべき文字列」を関数の戻り値としています。
-
問題のロジック通りに「
hakoがあたたたたで終わるか」を判定した方がよい(気がする
)ので、Luaのパターン(正規表現モドキ)を用いた検索を利用しています。-- hakoが "あたたたた" で終わるか ('$'は末尾アンカー) hako:find("あたたたた$")なお、UTF-8のマルチバイト文字を構成するバイト(128以上)はASCII文字5(127以下)とは完全に分離しているので、パターンにマルチバイト文字を含める6ことは問題になりません。
個人的なコメント
せっかくネタのプログラムを作っていたので記事にしてみた![]()
-
-oでパス名を指定した場合はその拡張子から文書形式を自動的に決めるため、-tで出力形式(今の場合はhtml)を指定する必要はありません。なお入力についても同様で、今は入力のパス名があるので入力の文書形式の指定(するなら-f markdown)は不要です。 ↩ -
プログラム中の
Str以外のトップレベル変数は全てローカルであり、これは全てStrの実装を補助するためのものです。 ↩ -
リストは「Pandoc独自のList型のインスタンス」または「Luaのシーケンス」で表します。 ↩
-
0要素(つまり空)のリストを返すことで当該の要素を削除することもできます。 ↩
-
パターンのメタ文字(
$や+等)は全てASCII文字なので、128以上のバイトがメタ文字になることはありません。 ↩ -
もちろん「パターンの解釈はバイト単位で行われる」ため、例えば「
たの1回以上の繰り返し」を"た+"と書くことはできません。("た+"は"\xE3\x81\x9F+"と同値です。)しかもLuaのパターンは「グループに対する繰り返し」(例えば"(た)+")をサポートしないため、結局「たの1回以上の繰り返し」を表現できません。ただしPandocのLua処理系はLPegモジュールを組み込んでいるため、LPegの機能を利用するという手段はあります。 ↩