hex.pmhex.pmDocumentationhex.pmgithub.com

AshTypst

Precompiled Rust NIFs for rendering Typst templates via an extensible data-encoding protocol with built-in Ash support. Compile markup to SVG, PDF, or HTML with persistent contexts that keep fonts and compiled state in memory for fast, iterative rendering.

Features

Installation

Add ash_typst to your dependencies in mix.exs:

def deps do
[
{:ash_typst, "~> 0.0.1"}
]
end

Precompiled NIF binaries are downloaded automatically for common targets. To compile from source, add {:rustler, "~> 0.35"} as an optional dependency and set RUSTLER_PRECOMPILATION_EXAMPLE_FORCE_BUILD=1.

Quick start

# 1. Create a context — fonts loaded once, reused for all operations
{:ok, ctx} = AshTypst.Context.new(root: "/path/to/templates")
# 2. Set the main template
:ok = AshTypst.Context.set_markup(ctx, """
#import "data.typ": records
= Invoice \#sys.inputs.at("invoice_id")
#for r in records [- \#r.name: \#r.amount]
""")
# 3. Inject data
AshTypst.Context.set_inputs(ctx, %{"invoice_id" => "INV-42"})
AshTypst.Context.stream_virtual_file(ctx, "data.typ", line_items,
variable_name: "records"
)
# 4. Compile
{:ok, %AshTypst.CompileResult{page_count: n}} = AshTypst.Context.compile(ctx)
# 5. Render
{:ok, svg} = AshTypst.Context.render_svg(ctx, page: 0)
{:ok, pdf_binary} = AshTypst.Context.export_pdf(ctx, pages: "1-3", pdf_standards: [:pdf_a_2b])
{:ok, html} = AshTypst.Context.export_html(ctx)

Context API

All rendering is done through AshTypst.Context.

FunctionPurpose
new/1Create a context with root path and font options
set_markup/2Set the main Typst template (invalidates compiled doc)
compile/1Compile markup into a paged document
render_svg/2Render a page as SVG
export_pdf/2Export the document as a PDF binary
export_html/1Export as HTML (separate compilation pass)
set_virtual_file/3Set an in-memory file importable by templates
stream_virtual_file/4Stream an enumerable into a virtual file
append_virtual_file/3Append a chunk to a virtual file
clear_virtual_file/2Remove a virtual file
set_input/3Set a single sys.inputs entry
set_inputs/2Replace all sys.inputs entries
font_families/1List fonts loaded in this context

Data encoding

The AshTypst.Code protocol converts Elixir values into Typst source syntax:

Elixir typeTypst type
Mapdictionary
Listarray
Integerint(n)
Floatfloat(n)
Decimaldecimal(n)
String"str"
DateTime / NaiveDateTime / Date / Timedatetime(...)
true / falsetrue / false
nilnone
Ash resourcedictionary of public fields

Implement AshTypst.Code for your own structs to control how they serialize.

Ash Resource Extension

AshTypst.Resource is a Spark DSL extension that lets you declare Typst templates and render actions directly on your Ash resources. Each render action becomes a standard Ash generic action that returns an AshTypst.Document struct.

defmodule MyApp.Invoice do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshTypst.Resource]
typst do
root "priv/typst"
template :invoice do
source "invoice.typ"
inputs %{"company" => "Acme Corp"}
end
template :receipt do
# ~TYPST sigil is auto-imported inside template blocks
markup ~TYPST"""
#import "data.typ": record, args
= Receipt \#args.receipt_number
*Customer:* \#record.name
"""
end
render :generate_pdf do
template :invoice
format :pdf
argument :invoice_id, :string, allow_nil?: false
read :one do
filter expr(id == ^arg(:invoice_id))
load [:line_items, :customer]
end
pdf_options do
pdf_standards [:pdf_a_2b]
end
end
end
end

Call the action like any other Ash generic action:

input = Ash.ActionInput.for_action(MyApp.Invoice, :generate_pdf, %{invoice_id: "123"})
{:ok, %AshTypst.Document{format: :pdf, data: pdf_binary}} = Ash.run_action(input)

Data is injected into a virtual file (data.typ by default) that your template can #import. Depending on the read cardinality, your template receives record (single), records (list), and/or args (action arguments).

For the complete DSL reference, see the AshTypst.Resource DSL cheatsheet.

Live editing

The context is designed for iterative workflows. After the initial setup, only the changed markup or data needs to be re-set before re-compiling:

# Initial render
:ok = AshTypst.Context.set_markup(ctx, template_v1)
{:ok, _} = AshTypst.Context.compile(ctx)
{:ok, svg} = AshTypst.Context.render_svg(ctx)
# User edits template — only re-set what changed
:ok = AshTypst.Context.set_markup(ctx, template_v2)
{:ok, _} = AshTypst.Context.compile(ctx)
{:ok, svg} = AshTypst.Context.render_svg(ctx)

Fonts, virtual files, and sys.inputs all persist across re-compilations.