WASMでTypstプラグインを作ろう
最近なんだか話題ですね、Typst。その簡潔な構文や環境構築の容易さが注目されているのだと思います。
そんなTypstですが、プラグインシステムを備えており、WASMを使って拡張することが可能です。
公式のパッケージリストに掲載されているパッケージの中にも内部で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プラグインを作成できます。
基本的なRustの知識があることを前提にしています。
Greetプラグイン
まずはHello {name}
という文字列を出力するだけのプラグインを作ります。
cargo new --lib typst-greet
rustup target add wasm32-unknown-unknown
を実行してRustのプロジェクトを作り、wasmにビルドできるようにしておきましょう。
次にCargo.toml
に以下を追記します。
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-minimal-protocol = { git = "https://github.com/astrale-sharp/wasm-minimal-protocol/" }
crate-type = ["cdylib"]
はwasmにビルドするのに必要です。また、wasm-minimal-protocol
クレートでは、関数を先述したプロトコルに合うように変換するマクロを提供してくれています。
また、.cargo/config.toml
ファイルを作成し、デフォルトでwasmがコンパイルされるようにしておきます。
[build]
target = "wasm32-unknown-unknown"
次にlib.rs
を以下のように書き換えます。
use wasm_minimal_protocol::*;
initiate_protocol!();
#[wasm_func]
pub fn greet(name: &[u8]) -> Vec<u8> {
[b"Hello, ", name, b"!"].concat()
}
initiate_protocol!()
を実行した後、wasm_func
アノテーションを関数に付与することでマクロがよしなに関数をTypst向けにエクスポートしてくれます。
Typstプラグインは基本的にバイト列を受け取りバイト列を返すことしかできません。ただし戻り値にはResult
typeを使うこともできます。詳しくはwasm-minimal-protocol
のサンプルを見ていただければわかると思います。
最後に以下のコマンドでビルドします。
cargo build --release
target
ディレクトリ内にwasmが生成されたはずです。では実際にTypstで読み込んでみましょう。以下のようなTypstファイルを作成します。
#let plugin = plugin("./target/wasm32-unknown-unknown/release/typst_greet.wasm")
#let greet(name) = str(
plugin.greet(
bytes(name),
)
)
#greet("typst")
プラグインはバイト列を受け取りバイト列を返すので、引数はbytes()
で変換して渡し戻り値はstr()
関数で文字列に戻します。
これを
typst compile sample.typ
でコンパイルすれば…
このようなpdfファイルが生成されているはずです。非常に簡単ですね。
Excel読み込みプラグイン
これだけでは面白くないのでもう少し実用的なものを作りましょう。
自分は表を作る時に雑にExcelで作ることが多いのですが、Typstでxlsxファイルは読み込めないのでCSVにいちいち変換しなければなりません。
これは面倒なのでxlsxファイルを直接読み込んでくれるTypstプラグインを作ろうと思います。
xlsxファイルを読み込むのは大変そうだな…と思った方もいるかもしれませんがそこはRust、calamineという素晴らしいクレートが存在します。これを使うことで手軽にxlsxの解析が行えます。
まずは前節と同様にRustプロジェクトを作成してから
cargo add calamine
で依存関係を追加し、以下のコードをlib.rs
に書きます。
use calamine::{Reader, Xlsx, XlsxError};
use wasm_minimal_protocol::*;
initiate_protocol!();
fn parse_num(num: &[u8]) -> Result<usize, String> {
std::str::from_utf8(num)
.map_err(|e| format!("Invalid number: {e}"))?
.parse()
.map_err(|e| format!("Invalid number: {e}"))
}
#[wasm_func]
pub fn get_table(
data: &[u8],
sheet: &[u8],
col: &[u8],
row: &[u8],
width: &[u8],
height: &[u8],
) -> Result<Vec<u8>, String> {
let sheet = std::str::from_utf8(sheet).map_err(|e| format!("Invalid sheet name: {e}"))?;
let col = parse_num(col)?;
let row = parse_num(row)?;
let width = parse_num(width)?;
let height = parse_num(height)?;
let cursor = std::io::Cursor::new(data);
let mut workbook: Xlsx<_> = calamine::open_workbook_from_rs(cursor)
.map_err(|e: XlsxError| format!("Failed to open workbook: {e}"))?;
let sheet_range = workbook
.worksheet_range(sheet)
.map_err(|e| format!("Sheet read error: {e}"))?;
let mut lines = vec![];
for r in row..row + height {
let mut line = vec![];
for c in col..col + width {
if let Some(cell) =
sheet_range.get_value((r.try_into().unwrap(), c.try_into().unwrap()))
{
line.push(cell.to_string());
} else {
line.push("null".to_string());
}
}
lines.push(line.join("\t"));
}
Ok(lines.join("\n").into_bytes())
}
#let plugin = plugin("./target/wasm32-unknown-unknown/release/typst_excel.wasm")
#let get_table(file, sheet, col, row, width, height) = csv.decode(
plugin.get_table(
read(file, encoding: none),
bytes(sheet),
bytes(str(col)),
bytes(str(row)),
bytes(str(width)),
bytes(str(height))
),
delimiter: "\t"
)
#table(
columns: 4,
..get_table("Book1.xlsx", "Sheet1", 1, 1, 4, 10).flatten()
)
ソースコードを見てもらえれば大体何をしているのかわかると思います。
ポイントとしては、TypstのWASM環境は完全に外部と分離されているため、外部のファイルにアクセスするために.typ
ファイルのほうでxlsxファイルをバイト列として読み込みそれをプラグインに渡しています。
プラグイン側ではxlsxデータを読み込み、指定された範囲のデータをtsv形式で返しています。
CSVやTSVはTypstで読み込めるため後は自由にデータを弄ることができます。
では以下のようなBook1.xlsx
を作ってtypst compile
を実行してみましょう。
するとこのように期待通りの表が得られました!
少し意外だったのですがxlsxって計算結果もファイルの中に保持してあるんですね。これは嬉しい。数式を取りたければコード内のworksheet_range
をworksheet_formula
に変えればよさそうです。
最後に
いかがでしたか?とても簡単にTypstのプラグインを作成できることがお分かり頂けたかと思います。既存の言語のエコシステムを使って比較的容易に複雑なプラグインを開発することができることはTypstの大きな利点だと思います。
ちなみに、パッケージを作ったらtypst/packages
リポジトリにPRを送ることで公式のプラグインリストに載り、import "@preview/..."
でインポートできるようになります。
参考:
また、今回使用したソースコードは
で公開しています。
是非みなさんもTypstプラグインを作ってみてください。
この記事は https://note.nazo6.dev/blog/wasm-typst-plugin とのクロスポストです。
Discussion