defmodule AshTypst do
@moduledoc """
Precompiled Rust NIFs for rendering [Typst](https://typst.app) 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
```mermaid
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]
end
```
**Key points:**
- The `TypstContext` is a Rust NIF resource held as an opaque reference in Elixir.
- Fonts are scanned once at context creation and reused for every compile.
- `compile/1` stores a `PagedDocument`; `render_svg/2` and `export_pdf/2` read from it without recompiling.
- `export_html/1` performs its own compilation (HTML uses a different document type internally).
- Virtual files and `sys.inputs` persist 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)
"""
@doc """
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`.
"""
def font_families(%AshTypst.FontOptions{} = opts \\ %AshTypst.FontOptions{}) do
AshTypst.NIF.font_families(opts)
end
end