この記事は、Zennにも投稿しています。
最近話題の組版システムのTypstですが、プラグインシステムを備えておりWASMを使って拡張することが可能です。
Plugin Type – Typst Documentation
Documentation for the Plugin type.
typst.app
プラグインを使うことで従来のTypst言語のみでは難しかった様々な処理を行うことができます。
公式のパッケージリストに掲載されているパッケージの中にも内部でWASMプラグインを使用しているものがあります。例えばQuickJSを利用してJavaScriptを実行する「Jogs」やMarkdownをTypstに変換する「cmarker」、さらにはLaTeXをTypst構文に変換して表示する(!)「mitex」なんていうものもあります。
この記事ではRustを使ってTypstのプラグインを作成します。Typst自体Rustで作られているためRustの環境がよく整備されていますが、もちろんWASMにコンパイルできる言語であればどのような言語も使用可能です。ただし、WASIはサポートされていないため、WASIが必須な言語やライブラリを使用する際にはwasi-stubを使用する必要があります。
ZigとCの例がここにある他、その他の言語でもTypstのwasm protocolに従って関数をエクスポートすることでTypstプラグインを作成できます。
プラグインとパッケージ
Typstでのプラグインというのは
wasm
ファイルのことを指します。一方、パッケージはプラグインをロードする処理などが記述されたtyp
ファイルなどを含んだ一連のファイル群のことを指します。パッケージはwasm
ファイルを含んでいる必要はありません。Greetプラグイン
まずは
Hello {name}
という文字列を出力するだけのプラグインを作ってみましょう。を実行してRustのプロジェクトを作り、wasmにビルドできるようにしておきましょう。
次に
次に
Cargo.toml
に以下を追記します。crate-type = ["cdylib"]
はwasmにビルドするのに必要です。また、wasm-minimal-protocol
クレートでは関数をTypstから呼び出すのに必要な諸々をやってくれます。また、
.cargo/config.toml
ファイルを作成し、デフォルトでwasmがコンパイルされるようにしておきます。次に
lib.rs
を以下のように書き換えます。initiate_protocol!()
を実行した後、wasm_func
アトリビュートを関数に付与することで関数をTypstにエクスポートすることができます。エクスポートされた
greet
関数では、バイト列として受け取った引数にHello,
を付加してそれをやはりバイト列として返しています。Rustには文字列を表すString
型がありますが、Vec<u8>
を返しているのは、Typstのプラグインができることは「バイト列を受け取ってバイト列を返すこと」だからです。(ただし将来的にはマクロを使って型の自動変換ぐらいはしてくれるようになるかも?例えばwasm-minimal-protocol
のサンプルにあるようにResult
型は今でも使えるようです)では実行するために以下のコマンドでビルドします。
target/wasm32-unknown-unknown/release
ディレクトリ内にwasmが生成されたはずです。では実際にTypstで読み込んでみましょう。作成プロジェクトのルートに以下のようなTypstファイルを作成します。wasmファイルは
plugin
関数で読み込みこまれ、後は通常のメソッドのように使うことができます。ただし、バイト列を渡して受け取ることには注意が必要です。これを
でコンパイルすれば…
このようなpdfファイルが生成されているはずです。非常に簡単ですね。
このようなpdfファイルが生成されているはずです。非常に簡単ですね。
Excel読み込みプラグイン
これだけでは面白くないのでもう少し実用的なものを作りましょう。
自分は表を作る時に雑にExcelで作ることが多いのですが、Typstでxlsxファイルは読み込めないのでCSVにいちいち変換しなければならず面倒です。これを簡略化するためにxlsxファイルを直接読み込むTypstプラグインを作ってみましょう。
当然イチからxlsxファイルを読み込む処理を実装するのは非常に大変ですが、プラグインを作るのにRustが使えるということは当然Rustのエコシステムを使えるということです。Rustのエコシステムは結構豊富で、今回の目的にドンピシャなcalamineというクレートを見つけました。Cのラッパーとかでもないのでビルドも難しい所はありません。
ということで実際にプラグインを作っていきましょう。まずは前節と同様に
cargo new
でRustプロジェクトを作成してからで依存関係を追加し、以下のコードを
lib.rs
に書きます。get_table
関数が実際の処理内容で、calamineクレートでバイト列として受け取ったxlsxファイルの内容を解析し、引数として指定された範囲の内容を読み取っています。読み取ったデータはTypstで解析できるようにtsvの文字列として返しています。Typstの表データとして直接返せれば良いのですがそのような方法は今は無いようです。
そしてこちらが上のRustコードから生成されたプラグインWASMを実行するためのファイルです。Rust内の
get_table
関数にxlsxファイルの内容と引数を渡して実行し、tsvとしてパースすることでxlsxファイル内の値を表示しています。では試しに以下のような
するとこのように期待通りの表が得られました!
今回始めて知ったのですがxlsxって計算結果もファイルの中に保持してあるんですね。数式を取りたければコード内の
Book1.xlsx
を作ってtypst compile
を実行してみましょう。するとこのように期待通りの表が得られました!
今回始めて知ったのですがxlsxって計算結果もファイルの中に保持してあるんですね。数式を取りたければコード内の
worksheet_range
をworksheet_formula
に変えればよさそうです。ファイルをプラグインから読み込めない理由
このセクションは別に読まなくてもいいです。
先程のコードでは、わざわざTypstからファイルをバイト列として渡していましたが、Rustから直接IOできたりすれば便利なのではないでしょうか?
そのようなことができない実際的な理由としては、「Typstがサポートしているwasm環境向けターゲットの
wasm32-unknown-unknown
ではファイルを読み込めないから」です。しかしながら、TypstがWASIなどをサポートしておらず、フリースタンディングなwasmターゲットしかないのは意図的なものだと思われます。というのもtypstの関数は純粋でなければならないからです。これによりドキュメントの再現性が確保され、高速な差分コンパイルが実現されています。ここでもしプラグインから外部にアクセスできてしまうと全く純粋ではなくなってしまうわけですね。
なので、プラグイン内で状態を保持することもできません。例えば以下のようなコードを考えてみましょう。
unsafeなどはとりあえず無視して頂くとこれは
count
が呼び出される度にCOUNTERを加算するコードなのですが、これをTypstから何回呼び出しても1が表示されます。とまあ長々と書きましたが、実はこのことはTypstのドキュメントページに全部書いてあります。
最後に
いかがでしたか?とても簡単にTypstのプラグインを作成できることがお分かり頂けたかと思います。既存の言語のエコシステムを使って比較的容易に複雑なプラグインを開発することができることはTypstの大きな利点だと思います。
ちなみに、パッケージを作ったら
typst/packages
リポジトリにPRを送ることで公式のプラグインリストに載り、import "@preview/..."
でインポートできるようになります。参考記事:
Typstの日本語Lipsumパッケージを作ってみた件
zenn.dev
また、今回使用したソースコードは
playground/other/typst-plugin at c0fb192f71e71fbbaafcc57673bdc4e931f3dd39 · nazo6/playground
Contribute to nazo6/playground development by creating an account on GitHub.
github.com
で公開しています。
是非みなさんもTypstプラグインを作ってみてください。