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拡張のqmdRmd
  • 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
  • Cons
    • 出力がpdf, png, svgのみ
      • Pandocを使えば他の形式にも出力できるし、typstのインストールも不要
    • 色んな言語を実行したいかも?

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

コードが実行できると世界が広がりそうですね