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.
Refresh font discovery after fonts are installed or removed.
Functions
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.
Refresh font discovery after fonts are installed or removed.
Font scan results are cached process-wide (per font configuration) and reused by every new context, so font changes on the system are not visible automatically. Calling this clears that cache and drops all pooled render contexts — new contexts and render actions then re-scan.
Contexts you are still holding keep the fonts they were created with; recreate them to pick up the changes.