Precompiled Rust NIFs for rendering Typst documents from Elixir.
All rendering goes through a persistent AshTypst.Context, which loads fonts
once and keeps the compiled document in memory so you can render pages, export
PDFs, or re-compile after a markup change without repeating expensive setup.
Architecture
graph TB
subgraph Elixir
direction LR
C[AshTypst.Code] -->|encode| A[AshTypst.Context] -->|NIF calls| N[AshTypst.NIF]
end
N --> W
N --> VF
N --> IN
N --> F
subgraph "Rust -- TypstContext resource"
subgraph "SystemWorld (persistent)"
direction LR
W[Markup]
VF[Virtual Files]
IN[sys.inputs]
F[Fonts + FontBook]
FS["File Slots -- disk cache"]
end
W -->|compile| PD
VF -->|import| PD
IN -->|sys.inputs| PD
F -->|font resolve| PD
FS -->|import pkg| PD
subgraph "Compiled Output"
PD["PagedDocument (cached)"]
end
PD -->|render_svg| SVG[SVG string]
PD -->|export_pdf| PDF[PDF binary]
W -->|export_html| HTML[HTML string]
endKey points:
- The
TypstContextis a Rust NIF resource held as an opaque reference in Elixir. - Fonts are scanned once at context creation and reused for every compile.
compile/1stores aPagedDocument;render_svg/2andexport_pdf/2read from it without recompiling.export_html/1performs its own compilation (HTML uses a different document type internally).- Virtual files and
sys.inputspersist across compiles until explicitly changed.
Quick start
# Create a context (fonts scanned once)
{:ok, ctx} = AshTypst.Context.new(root: "/path/to/templates")
# Set markup and compile
:ok = AshTypst.Context.set_markup(ctx, "= Hello World")
{:ok, %AshTypst.CompileResult{page_count: 1}} = AshTypst.Context.compile(ctx)
# Render any page as SVG
{:ok, svg} = AshTypst.Context.render_svg(ctx, page: 0)
# Export the full document as PDF
{:ok, pdf_binary} = AshTypst.Context.export_pdf(ctx)Data injection
You can feed Elixir data into templates in two ways:
Virtual files
Create in-memory .typ files that your template can #import:
AshTypst.Context.set_virtual_file(ctx, "data.typ", ~s(#let title = "Q4 Report"))
AshTypst.Context.set_markup(ctx, ~s(#import "data.typ": title\n= \#title))For large datasets, stream records in batches to keep Elixir memory flat:
AshTypst.Context.stream_virtual_file(ctx, "rows.typ", records_stream,
variable_name: "rows",
context: %{timezone: "America/New_York"}
)sys.inputs
Pass simple string key/value pairs accessible via #sys.inputs in templates:
AshTypst.Context.set_inputs(ctx, %{"theme" => "dark", "locale" => "en"})Data encoding
The AshTypst.Code protocol converts Elixir values to Typst source syntax.
It handles maps, lists, dates, decimals, Ash resources, and more.
See AshTypst.Code.encode/2 for the full type mapping.
Live editing
The context is designed for iterative workflows. Only the markup (or virtual file) that changed needs to be re-set before re-compiling; fonts and other state stay hot:
:ok = AshTypst.Context.set_markup(ctx, updated_template)
{:ok, _} = AshTypst.Context.compile(ctx)
{:ok, svg} = AshTypst.Context.render_svg(ctx, page: current_page)
Summary
Functions
List all font families available to Typst.
Functions
@spec font_families(AshTypst.FontOptions.t()) :: [String.t()]
List all font families available to Typst.
This is a standalone operation that does not require a context.
For fonts loaded in a context, use AshTypst.Context.font_families/1.