2024‐08‐18 Introducing coderunner on Pandoc Night - atusy/pandoc-coderunner GitHub Wiki
PandocとLuaフィルタで作るプログラマブルな文書
文書作成をプログラミングできるフレームワークいろいろ
LaTeX
言わずとしれたやつ
knitr
汎用動的文書作成ツール
- Pros
- 任意の言語を実行可能
- 様々な入力形式をサポート
- MarkdownやLaTeX、HTML、AsciiDocなどを拡張
- QuartoやR Markdownと組み合わせることで、様々な形式に変換できる
- 内部でPandocを使っているので出力の自由度は高い
- この場合の入力形式はMarkdown拡張の
qmd
やRmd
- Cons
-
依存が多い
- R + Pandoc
- R + Quarto (QuartoがPandocを内蔵)
-
独自の文法でパースにつらみ
Pandoc's Markdownの自然な拡張だったらよかったのに......
# チャンク ```{r} #| echo: false # コードを非表示にするオプション x <- 1:10 x ** 2 ``` # インラインコード `r x[1]`は`1`になる
-
Jupyter Notebook
- Pros
- 任意の言語を実行可能
- Quartoと組み合わせれば、様々な形式に変換できる
- Cons
- ソースファイルの閲覧は対応エディタが必要
- Python依存
Typst
組版向けの文書作成ツール
- Pros
- 基本のシンタックスが平易
- コンパイルが速い
- Language Serverがある......! https://github.com/nvarner/typst-lsp
- Cons
- 出力がpdf, png, svgのみ
- Pandocを使えば他の形式にも出力できるし、typstのインストールも不要
- 色んな言語を実行したいかも?
- 出力がpdf, png, svgのみ
MDX
Markdownの中にReactやVueのコンポーネントを埋め込める
- Pros
- フロントエンドエンジニアに馴染み深い
- Cons
- 出力がHTMLに限定される
- Node.js依存で、閲覧者にもNode.jsが必要
欲しいもの
Typstでいいのでは......?
- 文書作成をプログラミングできる
- Pandocさえあれば様々な出力形式に対応でき、Typstのインストールは不要
- Language Serverがあるので、エラーの診断などをしやすそう
人の欲望は尽きない
- デファクトの形式や好みの形式を使いたい
- 様々な言語を実行したい(とりあえず今回はLuaのみ)
Luaフィルタでなんとかしよう
Luaフィルタは、Pandocで文書を変換する際に、文書の内部表現(抽象構文木; Abstract Syntax Tree; AST)を操作できる仕組み。
CodeBlockやCodeを発見したら、その中身を実行して、その結果をASTに反映することができる......!
要件
eval=true
なコードの実行結果を反映できる
Markdownに限らずdjotなども対応
-
コードブロック
```{.lua eval=true} x = "hoge" return x ```
-
インラインコード
コードブロックで定義した変数を`x`{.lua eval=true}で展開
実行結果として文字列とPandoc's ASTをサポート
-
文字列は簡単
出力がMarkdownならMarkdown形式の文字列を返せばOK
```{.lua eval=true} return "- foo\ - bar" ```
-
Pandoc's ASTは汎用
出力形式に依存しないし、インデントの深さやバッククォートの数を気にしなくていい
```{.lua eval=true} return pandoc.BulletList({ pandoc.Plain({ pandoc.Str("foo") }), pandoc.Plain({ pandoc.Str("bar") }) }) ```
コード間で変数を共有できる
-
定義
```{.lua eval=true} x = "foo" function f() return "foo" end ```
-
利用
- `return x`{.lua eval=true} - `return f()`{.lua eval=true}
入力したドキュメントにアクセスできる
-
メタデータ(YAML Frontmatter)の定義
--- title: "PandocとLuaフィルタで作るプログラマブルな文書" ---
-
利用
`return ctx.meta.title`{.lua eval=true}
PoC
30行程度で作れる
local function eval(el)
-- PoCではLua言語で且つeval属性がtrueの場合のみ処理する
if el.classes[1] ~= "lua" or el.attributes.eval ~= "true" then
return el
end
-- コードの実行
local result = assert(load(el.text))() or {}
-- 実行結果がtableならPandoc's ASTと見做す
if type(result) == "table" then
return result
end
-- 実行結果がtableでない場合は文字列として扱う
-- RawBlockやRawInlineを使ってPandoc's ASTに変換することで、意図せぬエスケープを防ぐ
---@diagnostic disable-next-line: undefined-global
return pandoc[el.t == "CodeBlock" and "RawBlock" or "RawInline"](FORMAT, tostring(result))
end
return {
{
Pandoc = function(doc)
-- コードがYAMLフロントマターなどのコンテキストにアクセスできるようにする
_G.ctx = { meta = doc.meta }
-- コードの実行結果を反映する
-- 実行順序をtopdownにすることで、CodeBlockとCodeの依存関係を保証する
return doc:walk({
traverse = "topdown",
CodeBlock = eval,
Code = eval,
})
end,
},
}
Brushup
グローバル汚染対策
コード実行によるLuaフィルタのグローバル汚染を防ぐべし
- たとえば
type = nil
すると、attempt to call a nil value (global 'type')
のエラーになる - でも便利なAPIは使いたい
コードの実行はload
関数で行っており、env
引数に適切な環境を渡せばOK
- 素朴には
_G
のdeepcopyを渡す - グローバル環境を読むが、グローバル環境に書き込まないようなテーブルを渡す
- 例えば
setmetatable({}, { __index = function(_, key) _G[key] end })
- 上記は
_G.math.min
などを上書きできてしまうため、もう一工夫必要
- 例えば
多言語対応
Lua以外の言語でもコンテキストの共有とか、複数のコードブロック間で変数を共有するとかしたい......けど難しい
Jupyter Kernelとか使えるとよさそうだけど、ZeroMQなるライブラリが必要で依存が増える
素朴な対応のデフォルト提供
eval=true
属性を持つコードブロックに対して、言語名のコマンドを呼び、コードブロックの中身をファイルで渡し、標準出力を受け取る
```{.python eval=true}
for i in range(10):
print(i)
```
0 1 2 3 4 5 6 7 8 9
ユーザーによるエンジン追加
catエンジン
↓みたいので外部ファイルを取り込めると面白そう
```{.cat eval=true}
example.txt
```
実装
```{.lua eval=true}
ctx.opts.engines.cat = function(code)
local p = io.popen("cat " .. code.text)
local result = p:read("*a")
p:close()
return result
end
```
文字列でも定義可能
%s
がコードの内容を含む一時ファイルのパスに置換される
```{.lua eval=true}
ctx.opts.engines.cat = [bash -c 'cat "$(cat %s)"'](/atusy/pandoc-coderunner/wiki/bash--c-'cat-"$(cat-%s)"')
```
hoge
exprエンジン
lua expressionを評価してreturnするエンジン
たとえば計算を簡単にできるようにする
- 一日は`24 * 60 * 60`{.expr}秒
- 一日は`return 24 * 60 * 60`{.lua eval=true}秒
- 一日は86400秒
- 一日は86400秒
たとえばリンクを簡単に作れるようにする
- `gh("atusy")`{.expr}
- `gh("niszet")`{.expr}
実装
```{.lua eval=true}
-- exprエンジンの実装
ctx.opts.engines.expr = function(code, env)
code.text = "return " .. code.text
return ctx.opts.engines.lua(code, env)
end
-- exprエンジンは常に評価する
ctx.opts.eval.expr = true
-- epxrエンジンで使いたい関数の定義
function gh(x)
return pandoc.Link(x, "https://github.com/" .. x)
end
```
templateエンジン
PandocのTemplateを展開するエンジン
```{.template params='{ "hoge": "fuga", "array": ["a", "b", "c"] }'}
$hoge$
$for(array)$
- $array$
$endfor$
```
fuga
- a
- b
- c
実装
``` {.lua eval=true}
ctx.opts.engines.template = function(code)
return pandoc.template.apply(
pandoc.template.compile(code.text),
pandoc.json.decode(code.attributes.params)
):render()
end
ctx.opts.eval.template = true
```
補足
これだけだと弱いけど、も少し強化して、定義済みのテンプレートを再利用可能にすると便利かも?
```{.template #hoge eval=false}
$foo$
```
```{.template ref="hoge" params='{ "foo": "bar" }'}
```
```{.template ref="hoge" params='{ "foo": "buzz" }'}
```
ENJOY
コードが実行できると世界が広がりそうですね