
Typst
- 1.3k installs
- 112 repo stars
- Updated August 3, 2026
- lucifer1004/claude-skill-typst
typst provides documented workflows for Typst document creation and package development. Use when: (1) Working with .typ files, (2) User mentions typst, typst.toml, or typst-cli, (3) Creating or using
About
The typst skill typst document creation and package development. Use when: (1) Working with .typ files, (2) User mentions typst, typst.toml, or typst-cli, (3) Creating or using Typst packages, (4) Developing document templates, (5) Converting Markdown/LaTeX to Typst # Typst This skill targets Typst 0.15+ by default. For Typst 0.14.2, use the `typst-0.14.2` repository tag for the previous skill snapshot, `typst query` for CLI introspection, or `--channel 0.14.2` with API search. ## Compilation ```bash typst compile document.typ # compile once → PDF typst compile document.typ output.pdf # explicit output path typst compile document.typ -f png # export as PNG image typst compile src/main.typ --root . # set project root for /path imports typst watch document.typ # recompile on change typst eval --in document.typ 'query(heading).len()' # Typst 0.15+ introspection ``` For command options beyond this quick reference, see [cli.md](cli.md).
- **typst CLI 0.15+ recommended**: Install from https://typst.app or via package manager
- macOS: `brew install typst`
- Linux: `cargo install typst-cli`
- Windows: `winget install typst`
- **pdftotext** (optional): For text-level output verification
Typst by the numbers
- 1,336 all-time installs (skills.sh)
- +46 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #183 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
typst capabilities & compatibility
- Capabilities
- **typst cli 0.15+ recommended**: install from ht · macos: `brew install typst` · linux: `cargo install typst cli` · windows: `winget install typst` · **pdftotext** (optional): for text level output
- Use cases
- documentation
What typst says it does
# Typst This skill targets Typst 0.15+ by default.
For Typst 0.14.2, use the `typst-0.14.2` repository tag for the previous skill snapshot, `typst query` for CLI introspection, or `--channel 0.14.2` with API search.
npx skills add https://github.com/lucifer1004/claude-skill-typst --skill typstAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.3k |
|---|---|
| repo stars | ★ 112 |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 3, 2026 |
| Repository | lucifer1004/claude-skill-typst ↗ |
How do I use typst for the task described in its SKILL.md triggers?
Typst document creation and package development. Use when: (1) Working with .typ files, (2) User mentions typst, typst.toml, or typst-cli, (3) Creating or using Typst packages, (4) Developing documen.
Who is it for?
Teams invoking typst when the user request matches documented triggers and prerequisites.
Skip if: Skip when cached docs are missing, the request is a negative trigger, or another sibling skill owns the workflow.
When should I use this skill?
Typst document creation and package development. Use when: (1) Working with .typ files, (2) User mentions typst, typst.toml, or typst-cli, (3) Creating or using Typst packages, (4) Developing document templates, (5) Conv
What you get
Step-by-step guidance grounded in typst documentation and reference files.
- .typ source file
- formatted academic paper
- technical report
By the numbers
- Splits guidance across styling.md, template.md, and advanced.md reference files
- Default layout uses US-letter paper with 1in margins and 12pt body text
Files
Typst
This skill targets Typst 0.15+ by default. For Typst 0.14.2, use the typst-0.14.2 repository tag for the previous skill snapshot, typst query for CLI introspection, or --channel 0.14.2 with API search.
Compilation
typst compile document.typ # compile once → PDF
typst compile document.typ output.pdf # explicit output path
typst compile document.typ -f png # export as PNG image
typst compile src/main.typ --root . # set project root for /path imports
typst watch document.typ # recompile on change
typst eval --in document.typ 'query(heading).len()' # Typst 0.15+ introspectionFor command options beyond this quick reference, see cli.md.
Agent verification — choose by what you need to check (see debug.md for details):
| Method | Command | Best for |
|---|---|---|
| HTML export | typst compile doc.typ /dev/stdout -f html --features html 2>/dev/null | Text content, structure, headings, tables |
| PNG export | typst compile doc.typ page-{p}.png -f png | Visual layout, alignment, spacing, fonts |
| pdftotext | typst compile doc.typ && pdftotext doc.pdf - | Fallback for page-specific content |
Minimal Document
#set page(paper: "a4", margin: 2cm)
#set text(size: 11pt)
= Title
Content goes here.Writing Documents
Starting a new document? Copy the closest recipe from Examples below — it's faster than starting blank and each row names the docs to read next.
| When you need to... | Read |
|---|---|
| Learn syntax, imports, functions, control flow | basics.md |
| Learn data types, operators, string/array methods | types.md |
| Style pages, headings, figures, layout | styling.md |
| Tables, grids, cell spans, borders, data tables | tables.md |
| Academic papers, bibliography, theorems, equations | academic.md |
| Convert from Markdown or LaTeX | conversion.md |
| Use Typst CLI commands and build options | cli.md |
| Extract data from documents, multi-pass builds | query.md |
Developing Packages and Templates
| When you need to... | Read |
|---|---|
State, counters, in-document query(), XML | advanced.md |
| CLI introspection, metadata export, multi-pass | query.md |
| Create a reusable template function | template.md |
| Create or publish a package | package.md |
| Verify output (HTML/PNG/pdftotext, repr) | debug.md |
| Profile performance (--timings, hotspots) | perf.md |
basics.md and types.md are also the foundation for developers.
Finding Packages
Search the embedded index of Typst Universe packages (updated weekly):
python3 scripts/search-packages.py "what you need"
python3 scripts/search-packages.py "chart" --category visualization
python3 scripts/search-packages.py --category cv --top 5
python3 scripts/search-packages.py --list-categoriesCommon Errors
| Error | Cause | Fix |
|---|---|---|
| "unknown variable" | Undefined identifier | Check spelling, ensure #let before use |
| "expected X, found Y" | Type mismatch | Check function signature in docs |
| "file not found" | Bad import path | Paths resolve relative to current file |
| "unknown font" | Font not installed | Use system fonts or web-safe alternatives |
| "maximum function call depth exceeded" | Deep recursion | Use iteration instead |
| "can only be used when context is known" | Missing context wrapper | Wrap in context { ... } |
| "unexpected argument" | = instead of : for args | Named args use : syntax: func(name: value) |
| "variables from outside are read-only" | Mutating captured variable | Use loop accumulation or state() — see advanced.md |
| "expected content, found string" (or vice versa) | Content/string type mismatch | Use [#str-var] to embed string in content |
| set/show rule has no effect | Rule placed after content | Place set/show rules before the content they target |
Examples
Copy the closest starter, adjust, compile. For CVs, letters, or slides, search packages: python3 scripts/search-packages.py --category cv (or letter, presentation).
| Example | Start here when you want... | Next read |
|---|---|---|
| basic-document.typ | A short note or memo | basics.md, styling.md |
| styled-document.typ | A multi-section report with page styling | styling.md, tables.md |
| template-report.typ | A reusable template for a series | template.md |
| tables-showcase.typ | A data-heavy doc (tables, CSV/JSON) | tables.md, types.md |
| academic-paper.typ | A paper with citations, theorems, math | academic.md |
| query-export.typ | Metadata export or multi-pass builds | query.md |
| package-example/ | A publishable package | package.md |
Dependencies
- typst CLI 0.15+ recommended: Install from https://typst.app or via package manager
- macOS:
brew install typst - Linux:
cargo install typst-cli - Windows:
winget install typst - pdftotext (optional): For text-level output verification
- Python 3.10+ (optional): For package search and validation scripts
- jq (optional): For parsing JSON output from
typst evalortypst queryin shell scripts
API Reference Search
Search the embedded index of Typst API functions, methods, and constructors:
python3 scripts/search-api.py "image width fit"
python3 scripts/search-api.py "color lighten" --kind method
python3 scripts/search-api.py --name str.position -v
python3 scripts/search-api.py "rightarrow" --kind symbol # LaTeX names work
python3 scripts/search-api.py "path" --channel 0.14.2 # legacy Typst 0.14 API
python3 scripts/search-api.py --list-categoriesEcosystem Tools
Ecosystem tools: tinymist (LSP/editor), typstyle (formatter), typst-package-check (package validator), tytanic (visual test runner). For package tooling details, see package.md.
Academic Writing
For page layout, see styling.md. For reusable templates, see template.md. For state and counters, see advanced.md.
Paper Structure
#set page(paper: "us-letter", margin: 1in)
#set text(font: "New Computer Modern", size: 12pt)
#set par(justify: true, leading: 0.65em, first-line-indent: 0.5in)
#set heading(numbering: "1.1")
// Title block
#align(center)[
#text(17pt, weight: "bold")[Paper Title]
#v(0.5em)
Author Name \
_Institution_ \
#link("mailto:email@example.edu")
#v(1em)
]
// Abstract
#heading(outlined: false, numbering: none)[Abstract]
#par(first-line-indent: 0pt)[
This paper presents...
]
#v(1em)
// Body
= Introduction
The rest of the paper...Bibliography and Citations
Setup
Place a .bib file (BibTeX or BibLaTeX format) in your project:
// At the end of your document
#bibliography("refs.bib")Citation Syntax
As shown by @smith2024. // Smith (2024) — narrative
This was proven @smith2024. // (Smith, 2024) — parenthetical
See @smith2024[pp. 12-15]. // (Smith, 2024, pp. 12-15) — with supplement
Multiple sources @smith2024 @doe2023.Citation Styles
#bibliography("refs.bib", style: "ieee") // [1], [2], ...
#bibliography("refs.bib", style: "apa") // (Author, Year)
#bibliography("refs.bib", style: "chicago-author-date")
#bibliography("refs.bib", style: "mla")
#bibliography("refs.bib", style: "chicago-notes") // Footnote styleFull list: https://typst.app/docs/reference/model/bibliography/
Bibliography Title
#bibliography("refs.bib", title: [References], style: "ieee")
#bibliography("refs.bib", title: none) // No headingMultiple .bib Files
#bibliography(("primary.bib", "secondary.bib"), style: "apa")Multiple Bibliographies (Typst 0.15+)
Use target or group to produce separate reference lists, for example primary sources vs. secondary sources. Keep a single bibliography when the venue expects one reference section.
#bibliography("refs.bib", title: [Primary Sources], target: <primary>)
#bibliography("refs.bib", title: [Secondary Sources], target: <secondary>)Equations
Inline and Display Math
The equation $E = m c^2$ is well known.
The quadratic formula is:
$ x = (-b plus.minus sqrt(b^2 - 4 a c)) / (2 a) $Equation Numbering
#set math.equation(numbering: "(1)")
$ integral_0^infinity e^(-x^2) dif x = sqrt(pi) / 2 $ <eq:gaussian>
As shown in @eq:gaussian...Aligned Equations
$ a &= b + c \
&= d + e + f $Theorem Environments
Typst has no built-in theorem environment, but you can build one with counters and show rules.
Simple Theorem Block
#let theorem-counter = counter("theorem")
#let theorem(body, name: none) = {
theorem-counter.step()
block(
width: 100%,
inset: 10pt,
stroke: (left: 2pt + black),
{
context {
let num = theorem-counter.display()
[*Theorem #num*]
if name != none [ _(#name)_]
[*.* ]
}
emph(body)
},
)
}
#theorem[Every even integer greater than 2 is the sum of two primes.]
#theorem(name: "Fermat's Last")[No three positive integers satisfy $a^n + b^n = c^n$ for $n > 2$.]Proof Block
#let proof(body) = block(
width: 100%,
inset: (left: 10pt),
{
[_Proof._ ]
body
h(1fr)
$square$
},
)
#proof[
By contradiction, assume...
Therefore the statement holds.
]Shared Counter for Theorem-Like Environments
#let thm-counter = counter("theorem")
#let make-env(kind) = (body, name: none) => {
thm-counter.step()
block(
width: 100%,
inset: 10pt,
stroke: (left: 2pt + black),
{
context {
let num = thm-counter.display()
[*#kind #num*]
if name != none [ _(#name)_]
[*.* ]
}
if kind == "Theorem" or kind == "Lemma" { emph(body) } else { body }
},
)
}
#let theorem = make-env("Theorem")
#let lemma = make-env("Lemma")
#let definition = make-env("Definition")
#let corollary = make-env("Corollary")
#definition[A group is a set $G$ with a binary operation...]
#theorem[Every finite group of prime order is cyclic.]
#corollary[Every group of order 2 is isomorphic to $ZZ slash 2 ZZ$.]Chapter-Linked Numbering
#let thm-counter = counter("theorem")
#show heading.where(level: 1): it => {
thm-counter.update(0)
it
}
#let theorem(body) = {
thm-counter.step()
context {
let ch = counter(heading).get().first()
let n = thm-counter.get().first()
block(
width: 100%,
inset: 10pt,
stroke: (left: 2pt + black),
[*Theorem #ch.#n.* ] + emph(body),
)
}
}Figure and Table Numbering
// Number figures as "Figure 1", tables as "Table 1"
#set figure(numbering: "1")
// Reference: "see Figure 1", "see Table 2"
#figure(table(columns: 2, [A], [B]), caption: [Sample data]) <tab:data>
See @tab:data for results.Supplemental Figures
#set figure.caption(separator: [. ])
#figure(
image("plot.png", width: 80%),
caption: [Results of experiment A],
supplement: [Fig.],
) <fig:results>For two-column layout and full-width elements in multi-column documents, see styling.md.
Common Academic Patterns
| Pattern | Code |
|---|---|
| Double spacing | #set par(leading: 1.3em) |
| Numbered sections | #set heading(numbering: "1.1") |
| Running header | #set page(header: context { ... }) — see styling.md |
| Abstract indent | #pad(x: 2em)[Abstract text...] |
| Keywords line | *Keywords:* word1, word2, word3 |
| Acknowledgments | #heading(numbering: none)[Acknowledgments] |
| Line numbers | Not built-in — use @preview/lineno package |
| Footnotes | Text#footnote[Note content]. — auto-numbered |
| Subfigures | Not built-in — use @preview/subpar package |
| Appendix | See "Appendix" in styling.md |
Advanced Typst Patterns
For language basics (syntax, imports, functions), see basics.md. For data types and operators, see types.md. For labels, references, and everyday styling, see styling.md.
XML Parsing
Typst has built-in XML parsing:
```typst #let xml-content = `xml <root> <item name="first">Value 1</item> <item name="second">Value 2</item> </root> ``.text
#let doc = xml(xml-content) // doc is an array of nodes
// Navigate structure #let root = doc.first() // Root element #let children = root.children // Child nodes #let attrs = root.attrs // Attributes dictionary
// Find elements by tag #let find-child(node, tag) = { node.children.find(c => ( type(c) == dictionary and c.at("tag", default: "") == tag )) }
#let find-children(node, tag) = { node.children.filter(c => ( type(c) == dictionary and c.at("tag", default: "") == tag )) }
// Get text content (handles nested text) #let get-text(node) = { if type(node) == str { return node } if type(node) != dictionary { return "" } node.children.map(c => { if type(c) == str { c } else { get-text(c) } }).join("") } ````
XML Node Structure
// Element node
(
tag: "element-name",
attrs: (attr1: "value1", attr2: "value2"),
children: (/* child nodes or strings */),
)
// Text nodes are plain strings in the children arrayState and Context
State allows tracking information across a document. Requires context to read.
Basic State
#let counter = state("my-counter", 0)
// Update state
#counter.update(n => n + 1)
// Read state (must be in context)
#context counter.get()
// Display with context
#context [Count: #counter.get()]Custom Counters
#let example-counter = counter("example")
#let example(body) = {
example-counter.step()
block[*Example #context example-counter.display():* #body]
}State for Headers
#let chapter-title = state("chapter", none)
#show heading.where(level: 1): it => {
chapter-title.update(it.body)
it
}
#set page(header: context { chapter-title.get() })Final Values
// Get final value (at document end)
#let my-counter = state("my-counter", 0)
#context {
let final-count = my-counter.final()
[Total: #final-count]
}Tracking Across Document
// Track citations
#let _citations = state("citations", (:))
#let cite-marker(key) = {
[#metadata((key: key)) <my-cite>]
_citations.update(c => {
if key not in c { c.insert(key, 0) }
c.at(key) += 1
c
})
}
// At document end
#context {
let data = _citations.final()
// Process collected data...
}Query System
Query finds elements in the document. Requires context. For CLI introspection with typst eval (Typst 0.15+) or typst query (0.14 fallback), see query.md.
By Label
// Place metadata markers
#metadata((key: "item1", value: 42)) <marker>
// Query all markers
#context {
let items = query(<marker>)
for item in items {
let data = item.value
[Key: #data.key, Value: #data.value]
}
}By Selector
// Query all headings
#context {
let headings = query(heading)
for h in headings { [- #h.body] }
}
// Query specific heading level
#context {
let h1s = query(heading.where(level: 1))
}Within an Ancestor (Typst 0.15+)
Use selector.within to restrict matches to a section, figure, or other ancestor. This is especially useful for multi-document HTML bundles.
= Methods <sec:methods>
== Setup
#context {
let local = query(heading.where(level: 2).within(<sec:methods>))
[Method subsections: #local.len()]
}By Label String
#context {
let target = query(label("ref-mykey"))
if target.len() > 0 {
[Found at page #target.first().location().page()]
}
}Location-Based
#context {
let items = query(<marker>)
let here-loc = here()
// Find items before current location
let before = items.filter(i => (
i.location().position().y < here-loc.position().y
))
}Closure Workarounds
Closures cannot mutate captured variables (see basics.md "Mutability in Closures"). Beyond the loop accumulation pattern, two more options:
Fold for Accumulation
// Build dictionary from array
#let dict = items.fold((:), (acc, item) => {
acc.insert(item.key, item.value)
acc
})State for Cross-Document
#let _data = state("data", ())
#let add-item(item) = {
_data.update(d => { d.push(item); d })
}
// Read accumulated data
#context {
let all-items = _data.final()
}Content Introspection
Content elements (headings, text, figures, etc.) can be inspected and decomposed programmatically. This is essential for advanced show rules and template development.
Core Methods
= Hello *World*
// func() — the element's constructor (for type comparison)
#context {
let h = query(heading).first()
let is-heading = h.func() == heading // true
let is-text = h.func() == text // false
}
// fields() — dictionary of all field values
#context {
let h = query(heading).first()
let f = h.fields()
// f.keys() → ("level", "depth", "offset", "numbering", "supplement",
// "outlined", "bookmarked", "hanging-indent", "body")
}
// has() / at() — check and access fields
#context {
let h = query(heading).first()
let has-body = h.has("body") // true
let level = h.at("level") // 1
}Content Tree Structure
Content is a tree. Compound elements have children; leaf elements have text. Whitespace is a separate space node:
// [Hello *World*] decomposes to:
// sequence
// ├── text("Hello")
// ├── space
// └── strong
// └── text("World")
// Access children via fields()
#let body = [Hello *World*]
#let f = body.fields()
// "children" in f → true
// f.children → (text("Hello"), space, strong(...))Show Rule Decomposition Pattern
Intercept an element, decompose its content, transform parts, reassemble:
// Bold text before first colon in list items
#show list.item: it => {
let fields = it.body.fields()
if "text" in fields and ":" in fields.text {
let idx = fields.text.position(":")
list.item[*#(fields.text.slice(0, idx)):*#(fields.text.slice(idx + 1))]
} else if "children" in fields {
let (before, after, found) = ((), (), false)
for child in fields.children {
if found { after.push(child) }
else if type(child) != str and child.func() == text and ":" in child.text {
let idx = child.text.position(":")
before.push(child.text.slice(0, idx))
found = true
let post = child.text.slice(idx + 1)
if post.len() > 0 { after.push(post) }
} else { before.push(child) }
}
if found { list.item[*#(before.join()):*#(after.join())] } else { it }
} else { it }
}
- Name: John Doe
- Age: 25
- No colon hereRecursive Plain-Text Extraction
Extract plain text from any content element (useful for metadata export, see query.md):
#let plain-text(content) = {
let fields = content.fields()
if "text" in fields {
fields.text
} else if "children" in fields {
fields.children.map(c => {
if type(c) == str { c }
else if c.func() == [ ].func() { " " } // space element
else { plain-text(c) }
}).join()
} else if "body" in fields {
plain-text(fields.body)
} else if "child" in fields {
plain-text(fields.child)
} else { "" }
}Common Element Fields
| Element | Key fields |
|---|---|
heading | level, body, numbering, outlined |
text | text (leaf — the actual string) |
strong | body |
emph | body |
list.item | body |
enum.item | body, number |
figure | body, caption, kind, supplement |
sequence | children (array of child elements) |
space | (no fields — check with c.func() == [ ].func()) |
For performance profiling and optimization, see perf.md.
You are a Typst package QA agent. You run the full validation and testing suite on a Typst package to ensure it is ready for publishing.
Prerequisites
The package directory must contain a typst.toml manifest. If it doesn't, report this and stop.
QA Pipeline
Run these checks in order. Stop on critical failures.
1. Manifest Validation
typst-package-check check .If typst-package-check is not installed, check typst.toml manually for required fields: name, version, entrypoint, authors, license, description.
2. Compilation
typst compile <entrypoint>The entrypoint is specified in typst.toml. Verify it compiles without errors.
3. Formatting
find . -name '*.typ' | xargs typstyle --checkIf typstyle is not installed, skip and note it.
4. Visual Tests
tt runIf tytanic (tt) is not installed or no tests/ directory exists, skip and note it.
5. Content Verification
Use HTML export on the entrypoint directly to verify it produces expected output:
typst compile <entrypoint> /dev/stdout -f html --features html 2>/dev/null6. Checklist Audit
Verify each item:
- [ ]
typst.tomlhas all required fields - [ ]
entrypointfile exists and compiles - [ ]
LICENSEfile exists - [ ]
README.mdexists with usage examples - [ ] No compilation warnings
- [ ] Formatting passes (if typstyle available)
- [ ] Visual tests pass (if tytanic available)
- [ ] Package size is reasonable (< 10MB total)
Output Format
## Package QA: <package-name> v<version>
### Results
| Check | Status | Details |
|-------|--------|---------|
| typst-package-check | PASS/FAIL/SKIP | ... |
| Compilation | PASS/FAIL | ... |
| Formatting | PASS/FAIL/SKIP | ... |
| Visual tests | PASS/FAIL/SKIP | ... |
| LICENSE | PASS/FAIL | ... |
| README | PASS/FAIL | ... |
### Issues
- [CRITICAL] ...
- [WARNING] ...
### Verdict
READY / NOT READY for Typst Universe submissionRules
- Run all available checks. Skip gracefully when tools are missing — note the skip, don't fail.
- Critical failures: compilation errors, missing
typst.toml, missing entrypoint,typst-package-checkerrors. - Warnings: missing README, missing LICENSE, no tests, formatting issues.
- If the package is a template (has
[template]section intypst.toml), also verify the template entrypoint compiles. - Report the exact commands you ran so the author can reproduce.
You are a Typst document verification agent. You systematically verify that a compiled Typst document meets its requirements using the appropriate verification method for each claim.
Verification Methods
You have three methods. Choose by what you need to check — use multiple when needed:
| Method | Command | Checks |
|---|---|---|
| HTML export | typst compile <file> /dev/stdout -f html --features html 2>/dev/null | Text content, headings, tables, figures, cross-references |
| PNG export | typst compile <file> "page-{p}.png" -f png | Visual layout, alignment, spacing, fonts, page breaks, headers/footers |
typst eval | typst eval --in <file> 'query(heading).len()' | Typst 0.15+ element counts, metadata, structured data, page numbers |
typst query | typst query <file> "heading" or typst query <file> "<label>" | Typst 0.14 fallback for element counts and metadata |
HTML export is experimental and ignores page-specific features. PNG requires multimodal capability (read the image file). typst eval --in is the default introspection path on Typst 0.15+; use typst query when verifying against Typst 0.14. Element selectors (heading, figure) work on any document; labeled metadata queries require the source to contain metadata elements.
Process
1. Compile — Run typst compile <file> first. If it fails, report the error and stop.
2. Identify claims — What does the document need to satisfy? Extract from:
- User's requirements (explicit)
- Document structure expectations (headings, sections)
- Content correctness (text, data, citations)
- Layout requirements (margins, fonts, columns, page numbers)
3. Select methods — For each claim, pick the cheapest sufficient method:
- "Has section X" → HTML export, grep for
<h2>/<h3> - "Table has correct data" → HTML export, check
<table>content - "Page numbers show" → PNG export (HTML ignores page features)
- "Correct metadata" →
typst eval --inwith label - "Layout looks right" → PNG export, read the image
- "N figures exist" →
typst eval --inwithfigureselector, or HTML grep for<figure>
4. Execute — Run verifications in parallel where possible.
5. Report — For each claim, report PASS/FAIL with evidence.
Output Format
## Verification: <document>
**Compiled**: yes/no (exit code)
| # | Claim | Method | Status | Evidence |
|---|-------|--------|--------|----------|
| 1 | ... | HTML | PASS | `<h2>Introduction</h2>` found |
| 2 | ... | PNG | PASS | Page 1 shows correct layout |
| 3 | ... | query | FAIL | Expected 5 figures, found 3 |
**Verdict**: PASS / FAILRules
- Run verification commands yourself. Do not trust claims without output.
- Use the cheapest method that answers the question. Don't export PNG to check if a heading exists.
- If a requirement is ambiguous, state what you checked and what remains unverifiable.
- If HTML export shows warnings about ignored features, note which claims may need PNG verification.
- Element selectors do not require
metadata(); usetypst eval --in <file> 'query(heading).len()'for structure counts on Typst 0.15+, ortypst query <file> "heading"on 0.14. Labeled metadata queries require the source to contain matchingmetadata()elements.
Source Formatting (optional)
When the user also asks for formatting hygiene on edited .typ files:
1. Skip this section if command -v typstyle returns nothing. 2. Run typstyle --check <file> for each file the current task created or edited. 3. On failure, inspect with typstyle --diff <file> before deciding. 4. Apply with typstyle -i <file> only when every changed line is yours. If the diff touches pre-existing code you did not edit, stop and ask the user before formatting. 5. Report formatted files separately from output verification — they are distinct claims.
Typst Language Fundamentals
For data types, operators, and built-in functions, see types.md.
Modes
Typst has two modes that determine how text is interpreted:
Markup Mode
Default mode at document top level. Text is rendered as content:
Hello *bold* and _italic_ text.
= Heading
- List itemCode Mode
Entered with #. Expressions and statements:
#let x = 1 + 2
#if condition { [content] }
#for i in range(5) { [Item #i] }Switching Between Modes
// Code → Markup: use [ ]
#let greeting = [Hello *world*]
// Markup → Code: use #
The answer is #(1 + 2).Imports and Paths
Import Syntax
// Import from local file (relative to current file)
#import "utils.typ": helper, format
#import "lib/core.typ": *
// Import from Typst Universe packages
#import "@preview/package-name:0.1.0": func1, func2
// Import from local packages
#import "@local/my-package:0.1.0": *Path Resolution Rules
| Path Type | Example | Resolves To |
|---|---|---|
| Relative | "utils.typ" | Relative to current file's directory |
| Root-relative | "/src/lib.typ" | Relative to project root |
| Package | "@preview/pkg:1.0" | Typst Universe or local packages |
// File structure:
// project/
// ├── main.typ
// └── src/
// ├── lib.typ
// └── utils.typ
// In main.typ:
#import "src/lib.typ": * // ✅ Relative to main.typ
#import "/src/lib.typ": * // ✅ Root-relative (same result)
// In src/lib.typ:
#import "utils.typ": * // ✅ Relative to lib.typ (finds src/utils.typ)
#import "/src/utils.typ": * // ✅ Root-relative
#import "../main.typ": * // ✅ Parent directoryProject Root (--root)
The project root controls:
1. Where /-prefixed paths resolve from 2. Security boundary (files outside root cannot be accessed)
# Default: root is the main file's directory
typst compile src/main.typ
# Root = src/, so "/lib.typ" looks for src/lib.typ
# Explicit root: set project root to current directory
typst compile src/main.typ --root .
# Root = ., so "/lib.typ" looks for ./lib.typ
# Common pattern for multi-file projects
typst compile document.typ --root .Common Path Errors
| Error | Cause | Fix |
|---|---|---|
| "file not found" | Wrong relative path | Check path relative to current file, not project root |
"file not found" with / path | Root not set correctly | Use --root . or adjust path |
| "access denied" | File outside project root | Move file inside root or adjust --root |
Image and Data Files
// Images use the same path rules
#image("images/diagram.png") // Relative to current file
#image("/assets/logo.png") // Relative to project root
// Reading data files
#let data = json("data/config.json")
#let content = read("templates/header.typ")Include vs Import
// import: brings symbols into scope
#import "utils.typ": helper
#helper()
// include: directly inserts file content as-is
#include "chapter1.typ" // Content appears hereScope difference:
// chapters/intro.typ (content file)
This is chapter 1.
// vars.typ (module file)
#let shared-title = "Intro"
// main.typ
#include "chapters/intro.typ"
#shared-title // ❌ Error! Variables defined in included files
// do NOT leak to parent scope
// To share variables, use import from a module file:
#import "vars.typ": shared-title
#shared-title // ✅ WorksUse include for document content, import for reusable functions/variables.
Variables
// Immutable binding
#let name = "Alice"
#let count = 42
#let items = (1, 2, 3)
// Destructuring
#let (a, b) = (1, 2)
#let (first, ..rest) = (1, 2, 3, 4)
// Dictionary destructuring
#let (name: n, age: a) = (name: "Bob", age: 30)Data Types, Operators, and Built-ins
See types.md for the full reference. Quick summary:
- Primitives:
int,float,str,bool,none - Collections: arrays
(1, 2, 3), dictionaries(key: val) - Content:
[Hello *world*]
Functions
Basic Functions
#let greet(name) = [Hello, #name!]
#greet("Alice") // Hello, Alice!Default Parameters
#let greet(name, greeting: "Hello") = [#greeting, #name!]
#greet("Bob") // Hello, Bob!
#greet("Bob", greeting: "Hi") // Hi, Bob!Variadic Arguments
#let sum(..nums) = {
let total = 0
for n in nums.pos() {
total += n
}
total
}
#sum(1, 2, 3) // 6Named and Positional Args
#let format(..args) = {
let positional = args.pos() // Array
let named = args.named() // Dictionary
// ...
}Anonymous Functions (Lambdas)
#let double = x => x * 2
#let add = (a, b) => a + b
#(1, 2, 3).map(x => x * 2) // (2, 4, 6)Control Flow
Conditionals
#if x > 0 {
[Positive]
} else if x < 0 {
[Negative]
} else {
[Zero]
}
// Inline conditional (returns value)
#let sign = if x > 0 { "+" } else { "-" }Loops
// For loop
#for item in items {
[- #item]
}
#for (i, item) in items.enumerate() {
[#i: #item]
}
#for (key, value) in dict {
[#key = #value]
}
// While loop
#let i = 0
#while i < 5 {
[#i ]
i += 1
}Loop Control
#for item in items {
if item == "skip" { continue }
if item == "stop" { break }
[#item]
}Common Pitfalls
Mutability in Closures
Closures cannot modify captured variables:
// ❌ WRONG
#let results = ()
#let add(x) = { results.push(x) } // Error!
// ✅ CORRECT - Modify in loop
#let results = ()
#for item in items {
results.push(item)
}None Returns
Functions without explicit return value return none:
#let maybe(x) = {
if x > 0 { x }
// Returns none if x <= 0
}
// Handle none
#let result = maybe(-1)
#if result != none {
[Got: #result]
} else {
[No result]
}Content vs String
// Content brackets are literal text — code is not evaluated inside
[1 + 2] // Shows literal "1 + 2"
[Result: #(1 + 2)] // Shows "Result: 3"
// Concatenation differs by type
#let result = [#prefix#body#suffix] // content
#let combined = prefix-str + body-str // string
// Check if "empty"
#let is-empty(x) = { x == none or x == "" or x == [] }Spacing
// Adjacent code blocks merge without space
#[A]#[B] // "AB"
// Add explicit space
#[A] #[B] // "A B"
#[A]#h(1em)#[B] // "A B" (1em space)Error Handling
Use assert(condition, message: "...") for preconditions and panic("...") for unreachable states. For assertion patterns and debug techniques, see debug.md.
Typst CLI
For full option lists, run typst <command> --help. This page is a routing index plus a few CLI gotchas that are not already covered elsewhere.
What This Adds
- Pointers to the right existing reference doc.
- CI/export flag combinations that do not fit a language or debugging guide.
- Pitfalls around PDF standards, bundle export, dependency files, package caches, templates, and page-numbered outputs.
Command Choice
| Task | Use |
|---|---|
| Validate or inspect rendered output | debug.md |
Use typst eval / typst query, metadata export, or multi-pass builds | query.md |
Fix project-root or /absolute/path import issues | basics.md |
Configure fonts, variable axes, typst fonts --variants, or --font-path | styling.md |
Profile with --timings | perf.md |
| Develop or publish packages | package.md |
| Initialize from a template, produce PDF/A or PDF/UA, emit dependency files, bundle HTML, or pin caches | This page |
CI Export Recipe
typst compile doc.typ out.pdf --root . \
--creation-timestamp "$SOURCE_DATE_EPOCH" \
--package-cache-path ./.typst-cache \
--deps deps.json --deps-format jsonUse this shape when comparing artifacts across machines. Pin the root, timestamp, and package cache; record dependencies for rebuild logic. In Typst 0.15, JSON deps include outputs. --deps-format make is for Makefiles; zero handles paths that cannot be represented as Unicode.
PDF Standards
typst compile doc.typ out.pdf --pdf-standard a-4
typst compile doc.typ out.pdf --pdf-standard ua-1
typst compile doc.typ out.pdf --pdf-standard a-4,ua-1Use CLI values like 1.7, 2.0, a-4, or ua-1; do not write pdf/a-4 or pdf/ua-1. Comma-separated standards such as a-4,ua-1 are valid. --no-pdf-tags is an explicit size/compatibility tradeoff.
Bundle Export
typst compile site.typ dist -f bundle --features bundle,htmlBundle export is experimental in Typst 0.15. Use document("page.html")[...] for pages and asset("robots.txt", "...") for extra files.
Template Init
typst init @preview/charged-ieee
typst init @preview/charged-ieee:0.1.0 my-paper
typst init @local/my-template:0.1.0 draft --package-path ./packagesUse @local/... with --package-path when testing templates before publication.
Gotchas
-means stdin for input and stdout for output, but writing PDF/PNG bytes to a terminal is rarely useful.- Multi-page PNG/SVG outputs need a template such as
page-{p}.png;{0p}pads page numbers and{t}inserts total page count. --pagesuses one-indexed physical page numbers, not the document's printed page counter.watchis for human feedback loops; usecompile,eval, or 0.14queryin CI.typst completions <shell>exists for shell setup, but agents normally do not need it.
0.14 Compatibility
- Use
typst queryinstead oftypst eval; see query.md. - Search legacy APIs with
python3 scripts/search-api.py "path" --channel 0.14.2. - The
typst-0.14.2repository tag freezes the old skill snapshot.
When to Read Help
Use typst compile --help, typst watch --help, typst eval --help, typst query --help, or typst init --help when you need the complete current option list. This page is intentionally not a mirror of those outputs.
Converting Documents to Typst
For Typst language fundamentals (modes, functions), see basics.md. For types and operators, see types.md. For advanced table features, see tables.md.
Basic Formatting
| Effect | Markdown | LaTeX | Typst |
|---|---|---|---|
| Bold | **text** | \textbf{text} | *text* |
| Italic | *text* | \textit{text} | _text_ |
| Code | ` code ` | \texttt{code} | ` code ` |
| Link | [text](url) | \href{url}{text} | #link("url")[text] |
| Heading | # Title | \section{Title} | = Title |
| List item | - item | \item item | - item |
| Numbered | 1. item | \item item | + item |
For full Typst syntax details on headings, lists, links, and references, see basics.md.
From LaTeX: Package and Concept Map
Typst is "batteries included" — most common LaTeX packages are built in:
| LaTeX package | Typst equivalent |
|---|---|
graphicx, svg | image() function |
tabularx, tabularray | table(), grid() |
amsmath, amssymb | Built into math mode; see academic.md |
hyperref | link() function |
biblatex, natbib | cite(), bibliography() — see academic.md |
geometry, fancyhdr | #set page(margin: ..., header: ..., footer: ...) |
xcolor | #set text(fill: rgb("#...")), luma(), etc. |
babel, polyglossia | #set text(lang: "zh") |
lstlisting, minted | raw() function, markup |
caption | figure(caption: ...) |
enumitem | list(), enum(), terms() parameters |
parskip | #set par(spacing: ..., first-line-indent: ...) |
nicefrac | frac(a, b, style: "horizontal") or "skewed" |
csquotes | Smart quotes auto-active; set text(lang: ...) |
Concept mappings
| LaTeX | Typst |
|---|---|
\documentclass{article} | #show: template.with(...) (from a template) |
\newcommand{\foo}{...} | #let foo = ... or #let foo(x) = ... |
\textbf{x} (style-only, no tag) | #text(weight: "bold")[x] — style only |
| Semantic strong emphasis | *x* or #strong[x] — tagged for a11y |
\emph{x} (semantic) | _x_ or #emph[x] |
\textit{x} (style-only) | #text(style: "italic")[x] |
\bfseries (declaration-style) | #set text(weight: "bold") in current scope |
\textsc{x} | #smallcaps[x] |
\left( ... \right) | Auto-scaling in math; use lr(( )) to force |
\label{foo} / \ref{foo} | <foo> / @foo |
Set rules act like LaTeX declarations scoped to the current block; direct function calls act like argument-style commands.
"LaTeX look" starter
Reproduces the Computer Modern / justified / tight-leading look of a classic LaTeX article:
#set page(margin: 1.75in)
#set par(leading: 0.55em, spacing: 0.55em, first-line-indent: 1.8em, justify: true)
#set text(font: "New Computer Modern")
#show raw: set text(font: "New Computer Modern Mono")
#show heading: set block(above: 1.4em, below: 1em)Math Conversion
Inline vs Display Math
// Inline math
The formula $a + b = c$ is simple.
// Display math
$ integral_0^infinity e^(-x) dif x = 1 $Common Conversions
| LaTeX | Typst |
|---|---|
\frac{a}{b} | frac(a, b) |
\sqrt{x} | sqrt(x) |
\sum_{i=1}^{n} | sum_(i=1)^n |
\int_a^b | integral_a^b |
\alpha, \beta | alpha, beta |
\mathbf{x} | bold(x) |
\text{word} | "word" |
\left( \right) | auto (use lr(( )) to force) |
\begin{matrix} | mat(...) |
\begin{cases} | cases(...) |
\citet{key}, \textcite{key} | #cite(<key>, form: "prose") |
\arrow, alt forms | arrow.r.squiggly, arrow.l.long, etc. (symbol modifiers) |
Math Examples
// Fraction
$ frac(a + b, c) $
// Matrix
$ mat(1, 2; 3, 4) $
// Cases
$ f(x) = cases(
x^2 "if" x > 0,
0 "otherwise"
) $
// Aligned equations
$ a &= b + c \
&= d + e $Using mitex for LaTeX Math
For complex LaTeX math, use the mitex package:
#import "@preview/mitex:0.2.6": mitex, mi
// Display math
#mitex(`\frac{\partial f}{\partial x}`)
// Inline math
The value is #mi(`\alpha + \beta`).Code Blocks
Inline code uses backticks (same as Markdown). Fenced code blocks use triple backticks with language name. For programmatic raw content:
#raw("print('hello')", lang: "python", block: true)Tables
#table(
columns: (auto, 1fr, 1fr),
align: (left, center, right),
// Header row
[*Name*], [*Value*], [*Unit*],
// Data rows
[Length], [10], [cm],
[Width], [5], [cm],
)From Markdown Tables
Markdown:
| Name | Value |
| ---- | ----- |
| A | 1 |
| B | 2 |Typst:
#table(
columns: 2,
[*Name*], [*Value*],
[A], [1],
[B], [2],
)Figures and Images
#figure(
image("diagram.png", width: 80%),
caption: [A diagram showing the process],
) <fig:diagram>
// Reference
See @fig:diagram for details.Block Elements
Quotes
#quote(block: true)[
To be or not to be.
]
// With attribution
#quote(block: true, attribution: [Shakespeare])[
To be or not to be.
]Admonitions / Callouts
// Simple box
#block(
fill: luma(240),
inset: 1em,
radius: 4pt,
)[
*Note:* Important information here.
]
// Custom admonition function
#let note(body) = block(
fill: rgb("#e8f4f8"),
inset: 1em,
radius: 4pt,
width: 100%,
)[*Note:* #body]
#note[Remember to save your work.]Escaping Rules
Special Characters
Characters requiring escape with backslash:
| Character | Escape | Purpose |
|---|---|---|
* | \* | Bold marker |
_ | \_ | Italic marker |
# | \# | Code mode |
$ | \$ | Math mode |
@ | \@ | Reference |
< | \< | Label start |
> | \> | Label end |
/ | \/ | Term list |
` `` | ` \ `` | Raw text |
\ | \\ | Escape char |
In Raw Strings
Inside #raw("..."), only escape:
\→\\"→\"
#raw("path\\to\\file", lang: "text")Document Structure
From LaTeX
LaTeX:
\documentclass{article}
\title{My Document}
\author{Author Name}
\begin{document}
\maketitle
\section{Introduction}
Content here.
\end{document}Typst:
#set document(title: "My Document", author: "Author Name")
#set page(paper: "a4")
#align(center, text(20pt)[*My Document*])
#align(center)[Author Name]
= Introduction
Content here.From Markdown
Markdown:
---
title: My Document
author: Author Name
---
# Introduction
Some **bold** and _italic_ text.
- List item 1
- List item 2Typst:
#set document(title: "My Document", author: "Author Name")
= Introduction
Some *bold* and _italic_ text.
- List item 1
- List item 2Current Limitations vs LaTeX
- Plotting ecosystem: LaTeX has mature PGF/TikZ. Typst's
cetzis catching up but narrower. See package search for alternatives. - Mid-page margin changes:
#set page(margin: ...)forces a page break. For local stretching, usepad()with negative padding. - Change bars / track-changes workflows: No first-class equivalent yet.
- `\input` with partial scope: Typst
includeevaluates a whole file; scoping differs from TeX's\input. - Some niche journal templates may not yet be on Typst Universe — check before committing a submission to Typst-only.
Using Pandoc for Conversion
Pandoc (since v2.18) supports Typst as an output format.
pandoc -f markdown -t typst input.md -o output.typ # Markdown → Typst
pandoc -f latex -t typst input.tex -o output.typ # LaTeX → Typst
pandoc input.md -o output.pdf --pdf-engine=typst # Markdown → PDF via TypstCommon Options
pandoc input.md -t typst -o output.typ \
-V papersize=a4 -V fontsize=12pt -V mainfont="Libertinus Serif" \
-V section-numbering="1.1" --tocKey -V variables: title, author, papersize, margin, fontsize, mainfont/mathfont/codefont, section-numbering, page-numbering, columns, linestretch, linkcolor. These can also be set via YAML frontmatter.
Custom templates: pandoc -D typst > template.typ, then pandoc input.md --template=template.typ -o output.typ.
Known Limitations
- Citations:
@refin Markdown →#cite(<ref>)in Typst. Escape literal@with\@. - Complex tables: Cell merging needs manual adjustment.
- Raw Typst blocks: Use ```
`{=typst}``` fenced blocks for unsupported features.
Review and refine Pandoc output — custom styling and advanced layout usually need manual adjustment.
Typst Debugging Techniques
For language basics, see basics.md. For type inspection (type(), repr()), see types.md. For state/context debugging, see advanced.md.
Agent Verification Methods
Agents cannot preview PDFs directly. Three methods, choose by what you need to check:
HTML Export — Text and Structure
Outputs semantic HTML (headings → <h2>, tables → <table>, figures → <figure>). Best for verifying content, structure, and data correctness.
typst compile document.typ /dev/stdout -f html --features html 2>/dev/null
typst compile document.typ /dev/stdout -f html --features html 2>/dev/null | grep -i "expected text"HTML export is experimental and ignores page-specific features (headers, footers, page numbers).
PNG Export — Visual Layout
Exports rendered pages as images. Use when layout matters — alignment, spacing, font rendering, page breaks, multi-column, headers/footers. Requires a multimodal agent.
# Export all pages ({p} = page number, required for multi-page documents)
typst compile document.typ "page-{p}.png" -f png
# Export specific pages only
typst compile document.typ "page-{p}.png" -f png --pages 1-3
# Higher resolution (default: 144 PPI)
typst compile document.typ "page-{p}.png" -f png --ppi 288Then read the PNG file(s) to visually inspect the rendered output.
pdftotext — Fallback
Plain text extraction. Use when HTML export fails or for quick page-count checks.
typst compile document.typ && pdftotext document.pdf -Object Inspection with repr
Use repr() to inspect complex objects during development:
// Basic inspection
#repr(some-variable)
// Inspect function arguments
#let my-func(..args) = {
[DEBUG: #repr(args.pos()) | #repr(args.named())]
// actual logic...
}
// Inspect content structure
#let c = [Hello *world*]
#repr(c) // Shows internal content structure
// Inspect dictionary/array
#let data = (name: "test", items: (1, 2, 3))
#repr(data) // "(name: "test", items: (1, 2, 3))"Type + Repr Pattern
// Full debug info
#let debug-value(v) = {
text(fill: red, size: 8pt)[
[#type(v)] #repr(v)
]
}
#debug-value((a: 1, b: (2, 3)))
// Output: [dictionary] (a: 1, b: (2, 3))Conditional Debug Output
#let DEBUG = true
#let debug(label, value) = if DEBUG {
block(
fill: yellow.lighten(80%),
inset: 4pt,
radius: 2pt,
text(size: 8pt, fill: red)[#label: #repr(value)]
)
}
// Usage
#debug("config", config)
#debug("items count", items.len())Layout Debugging with measure
Use measure() to debug sizing and spacing issues. Requires context.
Basic Measurement
#context {
let size = measure([Hello World])
[Width: #size.width, Height: #size.height]
}
// Output: Width: 52.5pt, Height: 10ptMeasure + Repr + Place Pattern
For debugging layout issues, combine measurement with visual markers:
// Debug helper: shows measurement overlay
#let debug-measure(content, label: none) = context {
let size = measure(content)
let lbl = if label != none { label } else { "" }
box[
#content
#place(
top + left,
dx: size.width,
text(size: 6pt, fill: red)[
#lbl #repr(size.width) × #repr(size.height)
]
)
]
}
// Usage
#debug-measure([Some content], label: "box1")Visual Boundary Boxes
// Show element boundaries
#let debug-box(content) = context {
let size = measure(content)
box(
stroke: 0.5pt + red,
inset: 0pt,
)[
#content
#place(
bottom + right,
text(size: 5pt, fill: red)[#repr(size)]
)
]
}
#debug-box[This text has visible boundaries]Spacing Debug
// Visualize spacing between elements
#let debug-spacing(a, b, gap: 1em) = context {
let size-a = measure(a)
let size-b = measure(b)
box[
#a
#h(gap)
#place(
dx: size-a.width,
text(size: 6pt, fill: blue)[← #repr(gap) →]
)
#b
]
}
#debug-spacing([Left], [Right], gap: 2em)Page Position Debug
// Show current position on page
#let debug-position() = context {
let pos = here().position()
place(
dx: -20pt,
text(size: 5pt, fill: gray)[
(#repr(pos.x), #repr(pos.y))
]
)
}
Some content #debug-position()
More content #debug-position()State Debugging
#let my-state = state("debug-example", 0)
// Track state changes
#let debug-state-change(label) = context {
let val = my-state.get()
text(size: 7pt, fill: purple)[
[#label] state = #repr(val)
]
}
#debug-state-change("before")
#my-state.update(n => n + 1)
#debug-state-change("after")Query Debugging
// Debug query results
#context {
let headings = query(heading)
block(
fill: luma(240),
inset: 8pt,
width: 100%,
)[
*Query Debug: #headings.len() headings found*
#for (i, h) in headings.enumerate() {
[
#(i + 1). Level #h.level: #repr(h.body)
]
}
]
}Assertion-Based Debugging
// Fail fast with clear messages
#let validate-config(cfg) = {
assert(type(cfg) == dictionary, message: "Config must be dictionary")
assert("name" in cfg, message: "Config missing required 'name' field")
assert(cfg.at("size", default: 10) > 0, message: "Size must be positive")
}
#validate-config((name: "test", size: 12))Production Cleanup
Remove debug code before publishing:
// Single flag controls all debug output
#let DEBUG = false // Set to true during development
#let debug(..args) = if DEBUG { /* debug logic */ }
#let debug-box(c) = if DEBUG { /* with borders */ } else { c }
#let debug-measure(c, ..) = if DEBUG { /* with overlay */ } else { c }Or use conditional compilation:
# Compile with debug flag via CLI (requires wrapper)
typst compile document.typ --input debug=true// In document
#let DEBUG = sys.inputs.at("debug", default: "false") == "true"// Academic paper demonstrating built-in features.
// Compile: typst compile examples/academic-paper.typ
#set page(paper: "us-letter", margin: 1in, numbering: "1")
#set text(font: "New Computer Modern", size: 12pt)
#set par(justify: true, leading: 0.65em, first-line-indent: 0.5in)
#set heading(numbering: "1.1")
#set math.equation(numbering: "(1)")
// --- Theorem environments ---
#let thm-counter = counter("theorem")
#let make-env(kind) = (body, name: none) => {
thm-counter.step()
block(
width: 100%,
inset: 10pt,
stroke: (left: 2pt + black),
{
context {
let num = thm-counter.display()
[*#kind #num*]
if name != none [ _(#name)_]
[*.* ]
}
if kind == "Theorem" or kind == "Lemma" { emph(body) } else { body }
},
)
}
#let theorem = make-env("Theorem")
#let lemma = make-env("Lemma")
#let definition = make-env("Definition")
#let proof(body) = block(
width: 100%,
inset: (left: 10pt),
{
[_Proof._ ]
body
h(1fr)
$square$
},
)
// --- Title block ---
#align(center)[
#text(17pt, weight: "bold")[On the Properties of Example Numbers]
#v(0.5em)
Jane Doe #h(2em) John Smith \
_University of Typst_ \
#v(0.3em)
#text(size: 10pt, style: "italic")[March 2025]
#v(1em)
]
// --- Abstract ---
#heading(outlined: false, numbering: none)[Abstract]
#par(first-line-indent: 0pt)[
We investigate the properties of example numbers and demonstrate several
foundational results. Our main contribution is a proof that all example
numbers are positive, along with a classification theorem. We present
supporting data in tabular form and discuss implications.
]
#v(0.5em)
*Keywords:* example numbers, positivity, classification
#v(1em)
// --- Body ---
= Introduction
#par(first-line-indent: 0pt)[
Example numbers arise naturally in the study of
typesetting systems. In this paper we establish their basic properties
and present a classification.
]
The fundamental equation governing example numbers is:
$ E(n) = sum_(k=1)^n k^2 = (n(n+1)(2n+1)) / 6 $ <eq:sum>
As shown in @eq:sum, the sum grows cubically.
= Definitions and Preliminaries
#definition[
An _example number_ is a positive integer $n$ such that $E(n) > n$.
]
#lemma[
For all $n >= 1$, we have $E(n) >= 1$.
]
#proof[
Since $E(n) = sum_(k=1)^n k^2$ and each term $k^2 >= 1$, the sum
contains at least one positive term. Thus $E(n) >= 1^2 = 1$.
]
= Main Results
#theorem(name: "Positivity")[
Every example number is positive.
]
#proof[
By definition, an example number $n$ satisfies $E(n) > n > 0$.
Therefore $n$ is positive.
]
We summarize the first several values in @tab:values.
#figure(
table(
columns: (1fr, 1fr, 1fr),
align: (center, center, center),
table.header([*$n$*], [*$E(n)$*], [*Example?*]),
[1], [1], [No],
[2], [5], [Yes],
[3], [14], [Yes],
[4], [30], [Yes],
[5], [55], [Yes],
),
caption: [Values of $E(n)$ for small $n$.],
) <tab:values>
= Discussion
The data in @tab:values confirms that most small integers are
example numbers. The sole exception is $n = 1$, where $E(1) = 1 = n$.
A natural question is whether the density of example numbers
approaches 1. We leave this as an open problem.
== Future Work
- Extend the classification to negative integers.
- Investigate connections to other number-theoretic sequences.
// --- Acknowledgments ---
#heading(numbering: none)[Acknowledgments]
The authors thank the Typst community for helpful discussions.
// --- Appendix ---
#pagebreak()
#counter(heading).update(0)
#set heading(numbering: "A.1")
= Proof Details
== Extended Computation
For completeness, we verify @eq:sum for $n = 5$:
$ E(5) = 1 + 4 + 9 + 16 + 25 = 55 = (5 dot 6 dot 11) / 6 $
This confirms the closed-form expression.
// Basic Typst Document Example
// Demonstrates: document setup, headings, lists, math, code, figures
// === Document Settings ===
#set document(title: "Basic Document", author: "Author Name")
#set page(paper: "a4", margin: 2cm, numbering: "1")
#set text(font: "Libertinus Serif", size: 11pt)
#set par(justify: true, leading: 0.65em)
#set heading(numbering: "1.1")
// === Title Page ===
#align(center + horizon)[
#text(24pt, weight: "bold")[Basic Document]
#v(2em)
#text(14pt)[Author Name]
#v(1em)
#datetime.today().display()
]
#pagebreak()
// === Table of Contents ===
#outline(title: [Contents], indent: auto, depth: 2)
#pagebreak()
// === Content ===
= Introduction
This document demonstrates basic Typst features. Typst is a modern typesetting
system with a simple syntax and fast compilation.
== Text Formatting
Basic formatting: *bold text*, _italic text_, and `inline code`.
You can also use #strong[functional syntax] for #emph[emphasis].
== Lists
Unordered list:
- First item
- Second item
- Nested item
- Another nested
Ordered list:
+ Step one
+ Step two
+ Step three
Term list:
/ Typst: A modern typesetting system
/ LaTeX: A traditional typesetting system
= Mathematics
== Inline Math
The quadratic formula is $x = (-b plus.minus sqrt(b^2 - 4a c)) / (2a)$.
== Display Math
The Gaussian integral:
$ integral_(-infinity)^infinity e^(-x^2) dif x = sqrt(pi) $
A matrix example:
$
mat(
1, 2, 3;
4, 5, 6;
7, 8, 9
)
$
= Code
Python example:
```python
def fibonacci(n):
"""Calculate the nth Fibonacci number."""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # Output: 55
```
= Figures and Tables
== Figure
#figure(
rect(width: 6cm, height: 4cm, fill: luma(230), radius: 4pt)[
#align(center + horizon)[Placeholder Image]
],
caption: [A sample figure with caption.],
) <fig:sample>
Reference: See @fig:sample for the sample figure.
== Table
#figure(
table(
columns: 3,
align: (left, center, right),
stroke: 0.5pt,
[*Name*], [*Value*], [*Unit*],
[Length], [10.5], [cm],
[Width], [5.2], [cm],
[Height], [3.8], [cm],
),
caption: [Sample measurements],
) <tab:measurements>
= Conclusion
This document covered the essential Typst features:
- Document and page setup
- Text formatting and lists
- Mathematical formulas
- Code blocks with syntax highlighting
- Figures and tables with captions
For more advanced features, see the template and package examples.
// Example Utils Package
// A minimal example package demonstrating Typst package structure
// === Re-exports from submodules ===
#import "src/boxes.typ": callout, note
#import "src/formatting.typ": badge, iso-date
// === Package-level functions ===
/// Creates a horizontal divider line.
///
/// - width (length): Line width (default: 100%)
/// - stroke (stroke): Line style (default: 0.5pt + gray)
/// -> content
#let divider(width: 100%, stroke: 0.5pt + luma(180)) = {
v(0.5em)
line(length: width, stroke: stroke)
v(0.5em)
}
/// Wraps content in a centered block with optional width.
///
/// - body (content): Content to center
/// - width (length): Container width (default: auto)
/// -> content
#let centered(body, width: auto) = {
align(center, block(width: width, body))
}
example-utils
A minimal example Typst package demonstrating package structure and conventions.
Installation
Local Development
Copy to your local packages directory:
# Linux/macOS
mkdir -p ~/.local/share/typst/packages/local/example-utils/0.1.0
cp -r . ~/.local/share/typst/packages/local/example-utils/0.1.0/
# Then import in your document
#import "@local/example-utils:0.1.0": *From Typst Universe (after publishing)
#import "@preview/example-utils:0.1.0": *Usage
#import "@local/example-utils:0.1.0": note, callout, badge, iso-date, divider
// Note boxes
#note[This is an info note.]
#note(type: "warning")[Be careful!]
#note(type: "error", title: "Error")[Something went wrong.]
#note(type: "tip", title: "Pro Tip")[This is helpful.]
// Callout box
#callout(title: "Important")[
This is a callout with a colored header.
]
// Badge
Status: #badge[Active] #badge(color: red)[Deprecated]
// Date formatting
Today: #iso-date(datetime.today())
// Divider
#divider()API Reference
note(body, type: "info", title: none)
Creates a styled note box.
body: Content to displaytype: One of "info", "warning", "error", "tip"title: Optional title
callout(body, title: [Note], color: rgb("#3b82f6"))
Creates a callout box with colored header.
badge(label, color: rgb("#3b82f6"), text-color: white)
Creates a colored badge/tag.
iso-date(d)
Formats a datetime as YYYY-MM-DD.
divider(width: 100%, stroke: 0.5pt + gray)
Creates a horizontal divider line.
Package Structure
example-utils/
├── typst.toml # Package manifest
├── lib.typ # Public API entrypoint
├── README.md # Documentation
└── src/
├── boxes.typ # Box components (note, callout)
└── formatting.typ # Formatting utilities (badge, iso-date)License
MIT
// Box components: note, callout
/// Creates a styled note box.
///
/// - body (content): The content to display
/// - type (string): Box type - "info", "warning", "error", or "tip"
/// - title (content, none): Optional title for the box
/// -> content
#let note(body, type: "info", title: none) = {
let styles = (
info: (fill: rgb("#e0f2fe"), border: rgb("#0ea5e9"), icon: "ℹ"),
warning: (fill: rgb("#fef3c7"), border: rgb("#f59e0b"), icon: "⚠"),
error: (fill: rgb("#fee2e2"), border: rgb("#ef4444"), icon: "✕"),
tip: (fill: rgb("#dcfce7"), border: rgb("#22c55e"), icon: "✓"),
)
let style = styles.at(type, default: styles.info)
block(
fill: style.fill,
stroke: (left: 3pt + style.border),
inset: 1em,
radius: (right: 4pt),
width: 100%,
)[
#if title != none {
text(weight: "bold")[#style.icon #title]
v(0.3em)
}
#body
]
}
/// Creates a callout box with a colored header.
///
/// - body (content): The content to display
/// - title (content): The callout title
/// - color (color): Header background color
/// -> content
#let callout(body, title: [Note], color: rgb("#3b82f6")) = {
block(
stroke: 1pt + color,
radius: 4pt,
width: 100%,
clip: true,
)[
#block(
fill: color,
width: 100%,
inset: 0.7em,
)[
#text(fill: white, weight: "bold")[#title]
]
#block(
inset: 1em,
width: 100%,
)[#body]
]
}
// Formatting utilities: dates, badges
/// Formats a datetime in ISO format (YYYY-MM-DD).
///
/// - d (datetime): The date to format
/// -> string
#let iso-date(d) = {
d.display("[year]-[month padding:zero]-[day padding:zero]")
}
/// Creates a colored badge/tag.
///
/// - text (string, content): Badge text
/// - color (color): Background color (default: blue)
/// - text-color (color): Text color (default: white)
/// -> content
#let badge(label, color: rgb("#3b82f6"), text-color: white) = {
box(
fill: color,
inset: (x: 0.5em, y: 0.25em),
radius: 3pt,
)[
#text(fill: text-color, size: 0.85em, weight: "medium")[#label]
]
}
/// Formats a keyboard shortcut.
///
/// - keys (string): Keyboard shortcut (e.g., "Ctrl+S")
/// -> content
#let kbd(keys) = {
let parts = keys.split("+")
parts
.map(k => box(
fill: luma(240),
stroke: 0.5pt + luma(200),
inset: (x: 0.4em, y: 0.2em),
radius: 3pt,
)[#text(size: 0.9em, font: "monospace")[#k]])
.join([+])
}
[package]
name = "example-utils"
version = "0.1.0"
entrypoint = "lib.typ"
authors = ["Your Name <@github-username>"]
license = "MIT"
description = "Example utility package with common helper functions"
repository = "https://github.com/username/example-utils"
keywords = ["utility", "helpers", "example"]
categories = ["utility"]
compiler = "0.12.0"
exclude = ["tests/*", "docs/*"]
#set page(paper: "a4", margin: 2cm)
#set text(size: 11pt)
= Typst Perf Test
#let make-table(rows) = {
let cells = ()
for r in rows {
cells.push([#r])
cells.push([#(r * 2)])
cells.push([#(r * r)])
}
table(
columns: (auto, 1fr, 1fr),
[*Index*], [*Value*], [*Square*],
..cells,
)
}
#let section(i) = [
== Section #i #label("sec-" + str(i))
#lorem(80)
#let items = range(1, 12).map(n => n + i)
#make-table(items)
#let sum = items.fold(0, (acc, x) => acc + x)
Total: #sum
#if calc.rem(i, 3) == 0 {
[*Note:* This section triggers an extra block.]
}
]
#for i in range(1, 40) {
section(i)
pagebreak(weak: true)
}
== Summary
#context {
let hs = query(heading.where(level: 2))
[Total sections: #hs.len()]
}
// Query export example — demonstrates metadata export for CLI introspection.
//
// Usage:
// typst eval --in examples/query-export.typ 'query(<doc-info>).first().value' --pretty
// typst eval --in examples/query-export.typ 'query(<doc-stats>).first().value' --pretty
// typst eval --in examples/query-export.typ 'query(<task>).map(it => it.value)' --pretty
// typst eval --in examples/query-export.typ 'query(heading).map(it => it.body)' --pretty
// typst compile examples/query-export.typ /dev/null -f pdf
//
// Multi-pass (inject total page count):
// PAGES=$(typst eval --in examples/query-export.typ 'query(<page-count>).first().value')
// typst compile examples/query-export.typ --input "total-pages=$PAGES"
// --- Document metadata (plain, no context needed) ---
#metadata((
title: "Query Export Demo",
version: "1.0.0",
authors: ("Alice", "Bob"),
status: "draft",
)) <doc-info>
// --- Task tracking (multiple elements with same label) ---
#let task(name, status, priority: "medium") = {
metadata((name: name, status: status, priority: priority))
}
#task("Design API", "done", priority: "high") <task>
#task("Write tests", "in-progress") <task>
#task("Deploy", "pending", priority: "low") <task>
// --- Page setup ---
#let total = sys.inputs.at("total-pages", default: none)
#set page(paper: "a4", margin: 2cm, footer: context {
let current = counter(page).get().first()
if total != none [Page #current of #total] else [Page #current]
})
#set heading(numbering: "1.1")
= Introduction
#lorem(100)
== Background
#lorem(80)
= Methods
#lorem(120)
= Results
#lorem(100)
// --- Computed exports (require context) ---
// Label goes on metadata INSIDE the context block.
#context {
let stats = (
headings: query(heading).len(),
pages: counter(page).final().first(),
)
[#metadata(stats) <doc-stats>]
}
#context [#metadata(counter(page).final().first()) <page-count>]
// Styled Document Example
// Demonstrates: set rules, show rules, page layout, counters, headings, figures, labels, multi-region
// Compile: typst compile examples/styled-document.typ
// === Set Rules ===
#set document(title: "Styled Document Example", author: "Author Name")
#set text(size: 11pt, lang: "en")
#set par(justify: true)
#set heading(numbering: "1.1")
// === Show Rules ===
#show heading.where(level: 1): it => {
pagebreak(weak: true)
align(center, text(16pt, strong(it.body)))
v(0.5em)
}
#show heading.where(level: 2): set text(size: 13pt)
#show link: set text(fill: blue)
#show raw: set text(size: 9pt)
// === Front Matter (Roman numerals) ===
#set page(paper: "a4", margin: 2cm, numbering: "i")
#align(center + horizon)[
#text(24pt, strong[Styled Document Example])
#v(1em)
Author Name
#v(0.5em)
#datetime.today().display("[month repr:long] [day], [year]")
]
#pagebreak()
#outline(title: [Contents], indent: auto, depth: 3)
#pagebreak()
// === Main Matter (Arabic, reset counter) ===
#set page(
numbering: "1",
header: context {
let page = counter(page).get().first()
if page > 1 {
[Styled Document Example #h(1fr) Page #page]
}
},
)
#counter(page).update(1)
= Introduction <intro>
This document demonstrates the styling patterns from `styling.md`: set rules, show rules, page layout with headers, heading numbering, figures with labels, and multi-region documents.
== Motivation
Typst uses *set rules* for defaults and *show rules* for transformations. This separation keeps documents clean and maintainable.
== Scope
See @results for the main content and @fig:demo for a sample figure.
= Results <results>
Here we demonstrate figures, labels, and references working together.
#figure(
table(
columns: 3,
[*Item*], [*Value*], [*Unit*],
[Length], [42], [cm],
[Width], [18], [cm],
[Height], [7], [cm],
),
caption: [Sample measurements],
) <fig:demo>
As shown in @fig:demo, the table uses standard Typst formatting. For more details, refer back to @intro.
== Additional Notes
Built-in counters track pages and headings automatically:
- Current page: #context counter(page).display()
- Current heading: #context counter(heading).display()
// === Appendix ===
#counter(heading).update(0)
#set heading(numbering: "A.1")
= Appendix
Supporting material goes here. The heading numbering switches to letter-based format for appendix sections.
== Data Sources
All measurements in @fig:demo are illustrative.
// Table features showcase.
// Compile: typst compile examples/tables-showcase.typ
#set page(paper: "a4", margin: 2cm)
#set text(size: 11pt)
= Tables Showcase
== Basic Table
#table(
columns: 3,
[*Name*], [*Age*], [*City*],
[Alice], [30], [Berlin],
[Bob], [25], [Tokyo],
[Carol], [28], [Paris],
)
== Column Sizing
#table(
columns: (auto, 1fr, 2fr),
inset: 8pt,
[*Label*], [*Description*], [*Details*],
[A], [Short], [This column gets twice the remaining space],
[B], [Medium], [Fractional units distribute leftover width],
)
== Header and Footer
#table(
columns: (1fr, 1fr, 1fr),
table.header([*Product*], [*Q1*], [*Q2*]),
[Widget], [100], [150],
[Gadget], [200], [180],
[Gizmo], [50], [75],
table.footer([*Total*], [*350*], [*405*]),
)
== Cell Spanning
#table(
columns: 4,
align: center,
table.cell(colspan: 4)[*Annual Sales Report*],
[*Region*], [*Q1*], [*Q2*], [*Total*],
table.cell(rowspan: 2)[Americas], [120], [150], [270],
[80], [95], [175],
table.cell(rowspan: 2)[Europe], [200], [210], [410],
[90], [100], [190],
)
== Styled: Zebra Stripes
#table(
columns: (auto, 1fr, auto),
fill: (_, y) => if calc.odd(y) { luma(240) },
inset: 8pt,
table.header([*ID*], [*Task*], [*Status*]),
[1], [Design], [Done],
[2], [Implement], [In progress],
[3], [Test], [Pending],
[4], [Deploy], [Pending],
)
== Styled: Header Highlight + Minimal Lines
#show table.cell.where(y: 0): strong
#table(
columns: 3,
stroke: none,
inset: 8pt,
fill: (_, y) => if y == 0 { blue.lighten(80%) },
table.hline(stroke: 1.5pt),
[Language], [Paradigm], [Year],
table.hline(stroke: 0.5pt),
[Rust], [Systems], [2010],
[Typst], [Typesetting], [2023],
[Python], [General], [1991],
table.hline(stroke: 1.5pt),
)
== Grid Layout (No Borders)
#grid(
columns: (1fr, 1fr),
gutter: 16pt,
[
=== Left Column
Grids share the same API as tables but have no default strokes. Use them for layout rather than data display.
],
[
=== Right Column
Columns, rows, alignment, gutter, and cell spanning all work identically.
],
)
== Generated from Data
#let data = (
(lang: "Rust", stars: "95k", license: "MIT/Apache"),
(lang: "Typst", stars: "38k", license: "Apache 2.0"),
(lang: "Zig", stars: "35k", license: "MIT"),
)
#table(
columns: 3,
[*Language*], [*GitHub Stars*], [*License*],
..data.map(r => ([#r.lang], [#r.stars], [#r.license])).flatten(),
)
== Figure-Wrapped Table
#figure(
table(
columns: (1fr, 1fr),
table.header([*Input*], [*Output*]),
[$x$], [$x^2$],
[1], [1],
[2], [4],
[3], [9],
),
caption: [Squared values.],
) <tab:squared>
See @tab:squared for the mapping.
// Reusable Report Template Example
// Demonstrates: template function, set/show rules, counter, state, multi-region document
// === Template Definition ===
#let report(
title: none,
author: none,
date: datetime.today(),
abstract: none,
body,
) = {
// Document metadata
set document(title: title, author: author)
// Page setup with dynamic header
set page(
paper: "a4",
margin: (top: 2.5cm, bottom: 2cm, x: 2cm),
header: context {
let page-num = counter(page).get().first()
if page-num > 1 {
text(size: 9pt, fill: luma(100))[
#title
#h(1fr)
Page #page-num
]
v(-0.3em)
line(length: 100%, stroke: 0.5pt + luma(200))
}
},
footer: context {
let page-num = counter(page).get().first()
if page-num > 1 {
align(center, text(size: 9pt)[#page-num])
}
},
)
// Text and paragraph settings
set text(font: "Libertinus Serif", size: 11pt, lang: "en")
set par(justify: true, leading: 0.65em)
// Heading settings
set heading(numbering: "1.1")
// === Show Rules ===
// Level 1 heading: page break + centered
show heading.where(level: 1): it => {
pagebreak(weak: true)
v(1em)
text(16pt, weight: "bold")[
#if it.numbering != none {
counter(heading).display()
h(0.5em)
}
#it.body
]
v(0.8em)
}
// Level 2 heading
show heading.where(level: 2): it => {
v(0.8em)
text(13pt, weight: "bold")[
#if it.numbering != none {
counter(heading).display()
h(0.5em)
}
#it.body
]
v(0.5em)
}
// Figure caption styling
show figure.caption: it => text(size: 9pt, style: "italic", it)
// Link styling
show link: it => text(fill: rgb("#2563eb"), it)
// === Title Page ===
align(center + horizon)[
#text(28pt, weight: "bold")[#title]
#v(3em)
#text(14pt)[#author]
#v(1.5em)
#text(12pt, fill: luma(100))[#date.display(
"[month repr:long] [day], [year]",
)]
]
pagebreak()
// === Abstract (if provided) ===
if abstract != none {
heading(level: 1, numbering: none)[Abstract]
text(style: "italic")[#abstract]
pagebreak()
}
// === Table of Contents ===
heading(level: 1, numbering: none)[Contents]
outline(title: none, indent: auto, depth: 2)
pagebreak()
// === Main Content ===
body
}
// === Custom Components ===
/// Note box with customizable type
#let note(body, type: "info") = {
let styles = (
info: (fill: rgb("#e0f2fe"), border: rgb("#0ea5e9")),
warning: (fill: rgb("#fef3c7"), border: rgb("#f59e0b")),
error: (fill: rgb("#fee2e2"), border: rgb("#ef4444")),
)
let style = styles.at(type, default: styles.info)
block(
fill: style.fill,
stroke: (left: 3pt + style.border),
inset: 1em,
width: 100%,
)[#body]
}
/// Example counter and block
#let example-counter = counter("example")
#let example(body) = {
example-counter.step()
block(
fill: luma(245),
inset: 1em,
radius: 4pt,
width: 100%,
)[
*Example #context example-counter.display():*
#v(0.3em)
#body
]
}
// ============================================================
// Usage: Apply the template
// ============================================================
#show: report.with(
title: "Technical Report Template",
author: "Research Team",
abstract: [
This document demonstrates a reusable Typst template with custom styling,
dynamic headers, and helper functions. It serves as a starting point for
technical reports, papers, and documentation.
],
)
= Introduction
This template provides a professional layout for technical documents. It includes:
- Automatic page numbering with headers
- Styled headings at multiple levels
- Custom note boxes and example blocks
- Figure and table formatting
#note[
This is an informational note. Use it to highlight important information.
]
== Motivation
Typst offers a simpler alternative to LaTeX while maintaining professional output quality.
== Document Structure
The template automatically generates:
+ Title page with metadata
+ Abstract section (optional)
+ Table of contents
+ Numbered sections
= Features
== Note Boxes
Different types of notes for various purposes:
#note(type: "info")[
*Info:* General information for the reader.
]
#note(type: "warning")[
*Warning:* Something to be careful about.
]
#note(type: "error")[
*Error:* Critical issues or common mistakes.
]
== Examples
The `example` function provides numbered examples:
#example[
Calculate $integral_0^1 x^2 dif x$:
$ integral_0^1 x^2 dif x = [x^3 / 3]_0^1 = 1/3 $
]
#example[
Solve $x^2 - 5x + 6 = 0$:
$ x = (5 plus.minus sqrt(25 - 24)) / 2 = (5 plus.minus 1) / 2 $
Therefore $x = 3$ or $x = 2$.
]
== Figures
#figure(
rect(width: 8cm, height: 5cm, fill: luma(240), radius: 4pt)[
#align(center + horizon)[Architecture Diagram]
],
caption: [System architecture overview],
) <fig:arch>
As shown in @fig:arch, the system consists of multiple components.
== Tables
#figure(
table(
columns: 4,
align: (left, center, center, center),
stroke: 0.5pt,
[*Method*], [*Precision*], [*Recall*], [*F1*],
[Baseline], [0.82], [0.78], [0.80],
[Proposed], [0.91], [0.89], [0.90],
[Enhanced], [0.94], [0.92], [0.93],
),
caption: [Performance comparison of different methods],
)
= Conclusion
This template demonstrates key Typst features for document creation:
- Template functions with parameters
- Set and show rules for styling
- Custom counters for numbered elements
- Reusable component functions
Customize this template by modifying the `report` function parameters and styles.
// === Appendix ===
#counter(heading).update(0)
#set heading(numbering: "A.1")
= Appendix: Additional Notes
Appendices use letter numbering (A.1, A.2, etc.) by resetting the heading counter
and changing the numbering format.
== Configuration Options
The template supports these parameters:
- `title`: Document title (required)
- `author`: Author name(s)
- `date`: Publication date (defaults to today)
- `abstract`: Optional abstract content
MIT License
Copyright (c) 2026 lucifer1004
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Typst Package Development
For Typst language basics (syntax, functions), see basics.md. For types and operators, see types.md.
Complete example: See examples/package-example/ for a minimal publishable package with submodules.
Package Structure
my-package/
├── typst.toml # Package manifest (required)
├── lib.typ # Public API entrypoint
├── src/ # Internal modules
│ ├── core.typ
│ └── utils.typ
├── LICENSE
└── README.mdtypst.toml Manifest
[package]
name = "my-package"
version = "0.1.0"
entrypoint = "lib.typ"
authors = ["Your Name <@github-username>"]
license = "MIT"
description = "Short description"
repository = "https://github.com/user/my-package"
keywords = ["keyword1", "keyword2"]
categories = ["utility"]
compiler = "0.12.0"
exclude = ["tests/*", "docs/*"]Optional Template Section
[template]
path = "template"
entrypoint = "main.typ"
thumbnail = "thumbnail.png"Valid Categories
| Type | Categories |
|---|---|
| Document | book, report, paper, thesis, poster, flyer, presentation, cv, office |
| Function | components, visualization, model, layout, text, scripting, integration, utility, fun |
Module System
Import Syntax
// From Typst Universe
#import "@preview/package:0.1.0": func1, func2
#import "@preview/package:0.1.0": *
// From local file
#import "src/core.typ": main-func
#import "utils.typ": long-name as shortEntrypoint Pattern (lib.typ)
// Re-export public API only
#import "src/core.typ": main-func, Config
#import "src/utils.typ": helperPath Resolution in Packages
Important: Inside a package, the root path (/) resolves to the package directory itself, not the user's project root.
// Package structure:
// my-package/
// ├── lib.typ (entrypoint)
// └── src/
// ├── core.typ
// └── assets/
// └── icon.svg
// In lib.typ:
#import "/src/core.typ": * // ✅ Resolves to my-package/src/core.typ
// In src/core.typ:
#import "/src/assets/icon.svg" // ✅ Resolves to my-package/src/assets/icon.svg
#import "assets/icon.svg" // ✅ Same result (relative to core.typ)This isolation ensures packages are self-contained and don't depend on the user's file structure.
Modules must form a DAG (no circular imports).
API Design
Function Documentation
/// Creates a styled note box.
/// - body (content): The content to display
/// - type (string): "info", "warning", or "error"
/// -> content
#let note(body, type: "info") = { ... }For configuration patterns (default dictionaries, overrides), see template.md.
Local Development
Local Package Path
| OS | Path |
|---|---|
| Linux/macOS | ~/.local/share/typst/packages/local/ |
| Windows | %APPDATA%\typst\packages\local\ |
Install locally:
mkdir -p ~/.local/share/typst/packages/local/my-package/0.1.0
cp -r . ~/.local/share/typst/packages/local/my-package/0.1.0/Testing Locally
#import "@local/my-package:0.1.0": *
#my-func(test-input)Visual Regression Testing with tytanic
tytanic (tt) compiles test files, renders pages to PNG, and diffs against stored references.
cargo install tytanicDirectory layout:
my-package/
├── typst.toml
├── lib.typ
└── tests/
└── basic/
├── test.typ # test document
└── ref/
└── 1.png # reference image (one per page)Test file (tests/basic/test.typ):
#import "/lib.typ": *
#my-func("test input")Commands:
tt run # compile and compare all tests against refs
tt run basic # run a specific test
tt update # accept current output as new references
tt list # list discovered testsCommit ref/ images to version control so CI can detect regressions. See the tytanic guide for ephemeral references and advanced test modes.
Formatting with typstyle
cargo install typstyle
typstyle -i lib.typ src/*.typ # format in place
typstyle --check lib.typ src/*.typ # CI check (exit 1 if unformatted)Publishing
Validate with typst-package-check
Run the official validator before submitting to Typst Universe:
cargo install typst-package-check
typst-package-check check .Checks: typst.toml schema, entrypoint exists, package compiles, license valid, file size limits.
To Typst Universe
1. Run typst-package-check check . and fix any errors 2. Fork https://github.com/typst/packages 3. Add package to packages/preview/my-package/0.1.0/ 4. Create pull request
Versioning
| Change | Version |
|---|---|
| Bug fixes | 0.1.0 → 0.1.1 |
| New features (compatible) | 0.1.0 → 0.2.0 |
| Breaking changes | 0.1.0 → 1.0.0 |
Checklist
- [ ]
typst.tomlcomplete - [ ]
entrypointfile exports public API - [ ]
LICENSEincluded - [ ]
README.mdwith usage examples - [ ]
typst-package-check check .passes - [ ]
typstyle --checkpasses - [ ]
tt runpasses (if visual tests exist)
Best Practices
1. Minimal exports: Only expose what users need 2. Sensible defaults: All optional parameters have defaults 3. Document API: Use /// comments for all public functions 4. Semantic versioning: Follow semver strictly 5. No breaking changes: Deprecate before removing
Typst Performance Profiling
Use Typst's built-in timing trace to find slow stages and hotspots.
Generate Timing Trace
# Emit Chrome trace-style JSON
typst compile document.typ output.pdf --root . --timings build/timings.json 2>&1Notes:
--timingswrites trace events in Chrome trace format.--rootshould match your project root for correct imports.
View the Trace
Open in any trace viewer:
- Chrome:
chrome://tracing - Perfetto: https://ui.perfetto.dev/
Load build/timings.json to explore event timelines.
Aggregate Hotspots (Top N)
Use the bundled script to summarize total time by event name:
python3 scripts/perf-timings.py build/timings.jsonOutput includes a summary header (event count, threads, wall time) followed by a table with count, total, avg, and max per event.
CLI Examples
# Top 5 rows
python3 scripts/perf-timings.py build/timings.json --top 5
# Self time (excluding children) — reveals true hotspots vs containers
python3 scripts/perf-timings.py build/timings.json --self-time
# Show source file:line locations from trace args
python3 scripts/perf-timings.py build/timings.json --source --top 5
# Per-thread breakdown — check if work is balanced across threads
python3 scripts/perf-timings.py build/timings.json --by-thread --contains layout
# Only entries with total >= 50ms
python3 scripts/perf-timings.py build/timings.json --min-ms 50
# Filter by substring
python3 scripts/perf-timings.py build/timings.json --contains layout --top 3
# Sort by count instead of total time
python3 scripts/perf-timings.py build/timings.json --sort count --top 3
# JSON output for tooling (includes all fields: total, self, avg, max, sources)
python3 scripts/perf-timings.py build/timings.json --json --top 2Interpreting Self Time
Total time includes children. Self time subtracts direct child durations.
| Event | Total | Self | Interpretation |
|---|---|---|---|
page run | 340ms | 9ms | Container — time is in children |
block | 317ms | 88ms | Mix — significant own work plus children |
prepare | 85ms | 82ms | Leaf-like — most time is its own work |
Use --self-time to sort by self time and find the actual bottlenecks.
Example
Run the bundled perf test:
typst compile examples/perf-test.typ build/perf-test.pdf --root . --timings build/timings.json
python3 scripts/perf-timings.py build/timings.jsonPractical Tips
- Use
--self-timefirst to find real bottlenecks, not just wrapper events. - Use
--sourceto map hotspots back to specific lines in your.typfiles. - Use
--by-threadto check if one thread is bottlenecked while others are idle. - Re-run with the same inputs to compare timing deltas.
- Use
--font-pathif your project relies on non-system fonts. - Large
query()orstate()usage can dominate timelines; optimize those first.
CLI Introspection (typst eval / typst query)
For the in-document query() function, see advanced.md. For language basics, see basics.md.
Use typst eval --in <file> on Typst 0.15+. It evaluates a Typst expression in a document context and serializes the result as JSON or YAML. typst query is deprecated in Typst 0.15, but remains the 0.14 fallback.
Version Check
typst --version| Version | Preferred command |
|---|---|
| 0.15+ | typst eval --in doc.typ '...' |
| 0.14.x | typst query doc.typ "<selector>" |
Typst 0.15+: typst eval
typst eval --in doc.typ 'query(heading).len()'
typst eval --in doc.typ 'query(<doc-info>).first().value' --pretty
typst eval --in doc.typ 'query(<task>).map(it => it.value)' --pretty| Option | Effect |
|---|---|
--in <FILE> | Evaluate in a document context |
| `--format json\ | yaml` |
--pretty | Pretty-print JSON |
--input key=value | Pass string to sys.inputs (repeatable) |
--root <DIR> | Set project root for /path imports |
Use typst eval for expression probes too:
typst eval '1 + 2'
typst eval 'type("hi")'0.14 Fallback: typst query
typst query [OPTIONS] <INPUT> <SELECTOR>typst query accepts element selectors and labels:
typst query doc.typ "heading"
typst query doc.typ "figure"
typst query doc.typ "<doc-info>" --field value --one --prettyThe useful 0.14 options are --field, --one, --format json|yaml, --pretty, --input, and --root.
Selectors
Element type
typst eval --in doc.typ 'query(heading).len()'
typst eval --in doc.typ 'query(figure).len()'
typst eval --in doc.typ 'query(math.equation).len()'Label
typst eval --in doc.typ 'query(<my-label>).first().value'Filtered with .where()
typst eval --in doc.typ 'query(heading.where(level: 1)).len()'
typst eval --in doc.typ 'query(figure.where(kind: image)).len()'
typst eval --in doc.typ 'query(figure.where(kind: table)).len()'Restricted with .within() (Typst 0.15+)
typst eval --in doc.typ 'query(heading.where(level: 2).within(<methods>)).len()'metadata() Export
metadata(value) creates invisible content that holds any Typst value. Attach a label, then inspect it from the CLI.
#metadata("1.0.0") <version>
#metadata((title: "Report", status: "draft")) <doc-info>typst eval --in doc.typ 'query(<version>).first().value'
# -> "1.0.0"
typst eval --in doc.typ 'query(<doc-info>).first().value' --pretty
# -> {"title": "Report", "status": "draft"}0.14 equivalent
typst query doc.typ "<version>" --field value --one
typst query doc.typ "<doc-info>" --field value --one --prettyType Mapping
| Typst | JSON |
|---|---|
str | string |
int | number |
float | number |
bool | boolean |
none | null |
array | array |
dictionary | object |
| content | nested object with "func" key |
Label Placement in context
The label must go on metadata() itself, inside the context block:
// CORRECT: label on metadata
#context {
let data = query(heading).len()
[#metadata(data) <heading-count>]
}
// WRONG: label on context block, returns context content with no value field
#context {
metadata(query(heading).len())
} <heading-count>Patterns
Extract document metadata
// doc.typ
#metadata((
title: "Product Spec",
version: "2.1.0",
authors: ("Alice", "Bob"),
status: "final",
)) <doc-info>typst eval --in doc.typ 'query(<doc-info>).first().value' --pretty
VERSION=$(typst eval --in doc.typ 'query(<doc-info>).first().value.at("version")' | jq -r .)Export document statistics
#context {
let stats = (
headings: query(heading).len(),
figures: query(figure).len(),
equations: query(math.equation).len(),
pages: counter(page).final().first(),
)
[#metadata(stats) <doc-stats>]
}typst eval --in doc.typ 'query(<doc-stats>).first().value' --pretty
# -> {"headings":5,"figures":3,"equations":12,"pages":8}Export TOC with page numbers
Heading bodies are content, not strings. Use the plain-text helper from advanced.md (Content Introspection section) to extract text.
#let plain-text(value) = repr(value)
#context {
let toc = query(heading).map(h => {
let pg = counter(page).at(h.location()).first()
(level: h.level, title: plain-text(h.body), page: pg)
})
[#metadata(toc) <toc-export>]
}typst eval --in doc.typ 'query(<toc-export>).first().value' --pretty
# -> [{"level":1,"title":"Introduction","page":1}, ...]Multi-pass compilation
Query in pass 1, feed back via --input in pass 2. Example: "Page X of N" footer.
// main.typ
#let total = sys.inputs.at("total-pages", default: none)
#set page(footer: context {
let current = counter(page).get().first()
if total != none [Page #current of #total] else [Page #current]
})
= Chapter One
#lorem(200)
= Chapter Two
#lorem(300)
#context [#metadata(counter(page).final().first()) <page-count>]PAGES=$(typst eval --in main.typ 'query(<page-count>).first().value')
typst compile main.typ --input "total-pages=$PAGES"Conditional metadata with sys.inputs
Label must be on metadata() inside the if, not on the if block.
#let mode = sys.inputs.at("mode", default: "normal")
#if mode == "ci" [
#metadata((
version: "1.0.0",
packages: ("cetz", "tablex"),
)) <ci-meta>
]typst eval --in doc.typ 'query(<ci-meta>).first().value' --input mode=ci --prettyStructured task/status tracking
Multiple elements can share a label. query(<task>) returns all matching metadata elements.
#let task(name, status, priority: "medium") = {
metadata((name: name, status: status, priority: priority))
}
#task("Design API", "done", priority: "high") <task>
#task("Write tests", "in-progress") <task>
#task("Deploy", "pending", priority: "low") <task>typst eval --in doc.typ 'query(<task>).map(it => it.value)' --pretty
# -> [{"name":"Design API","status":"done","priority":"high"}, ...]CI version gate
#!/bin/bash
EXPECTED="2.1.0"
ACTUAL=$(typst eval --in doc.typ 'query(<version>).first().value' | tr -d '"')
if [ "$ACTUAL" != "$EXPECTED" ]; then
echo "Version mismatch: expected $EXPECTED, got $ACTUAL" >&2
exit 1
fiBatch validation
for f in docs/*.typ; do
typst eval --in "$f" 'query(<doc-info>).first().value' > /dev/null 2>&1 \
|| echo "MISSING metadata: $f" >&2
doneAgent Workflow
Use typst eval --in to verify document structure without opening a PDF:
typst eval --in doc.typ 'query(<expected-section>).len() > 0' | grep true
typst eval --in doc.typ 'query(figure).len()' # figure count
typst eval --in doc.typ 'query(<doc-info>).first().value.at("status")' | jq -e '. == "final"'See query-export.typ for a runnable example.
Fileless Probe
For a raw expression:
typst eval '1 + 2'
# -> 3For document-context probes from stdin:
printf '#metadata(1 + 2) <probe>\n' | typst eval --in - 'query(<probe>).first().value'
# -> 3Useful when docs or search are ambiguous about return types or runtime behavior. Exit code 1 on compile failure; stderr carries the error.
#!/usr/bin/env python3
"""Summarize Typst --timings trace by event name.
Parses Chrome trace-format JSON emitted by `typst compile --timings` and
aggregates duration-pair (B/E) events into a sorted hotspot table.
Usage:
python3 scripts/perf-timings.py build/timings.json
python3 scripts/perf-timings.py build/timings.json --top 5
python3 scripts/perf-timings.py build/timings.json --min-ms 50
python3 scripts/perf-timings.py build/timings.json --contains layout --top 3
python3 scripts/perf-timings.py build/timings.json --sort count --top 3
python3 scripts/perf-timings.py build/timings.json --self-time
python3 scripts/perf-timings.py build/timings.json --by-thread --top 5
python3 scripts/perf-timings.py build/timings.json --source
python3 scripts/perf-timings.py build/timings.json --json --top 2
"""
import argparse
import json
import sys
from collections import defaultdict
def parse_events(events):
"""Match B/E pairs into structured records with nesting info.
Returns list of dicts: {name, tid, start, dur, depth, source}.
Events are sorted by start time per thread.
"""
begins = {} # (name, tid) -> [(ts, depth, args)]
depth = defaultdict(int) # tid -> current depth
records = []
for event in events:
name = event.get("name", "")
ph = event.get("ph", "")
ts = event.get("ts", 0)
tid = event.get("tid", 0)
key = (name, tid)
if ph == "B":
d = depth[tid]
depth[tid] += 1
begins.setdefault(key, []).append((ts, d, event.get("args")))
elif ph == "E" and begins.get(key):
start_ts, d, args = begins[key].pop()
depth[tid] -= 1
source = None
if args and isinstance(args, dict):
f = args.get("file")
ln = args.get("line")
if f and ln:
source = f"{f}:{ln}"
records.append(
{
"name": name,
"tid": tid,
"start": start_ts,
"dur": ts - start_ts,
"depth": d,
"source": source,
}
)
return records
def compute_self_time(records):
"""Compute self time by subtracting direct children's duration.
For each event, subtract durations of events that are one depth level
deeper and nested within its time range on the same thread.
"""
# Group by thread, sort by start time
by_thread = defaultdict(list)
for r in records:
by_thread[r["tid"]].append(r)
self_times = {} # id(record) -> self_us
for tid, thread_records in by_thread.items():
thread_records.sort(key=lambda r: r["start"])
# Stack of active parents: [(record, child_sum)]
stack = []
for r in thread_records:
# Pop finished parents
while stack and r["start"] >= stack[-1][0]["start"] + stack[-1][0]["dur"]:
parent, child_sum = stack.pop()
self_times[id(parent)] = parent["dur"] - child_sum
# Add our duration to parent's child_sum
if stack:
stack[-1] = (stack[-1][0], stack[-1][1] + r["dur"])
stack.append((r, 0))
# Flush remaining
while stack:
parent, child_sum = stack.pop()
self_times[id(parent)] = parent["dur"] - child_sum
return self_times
def aggregate(records, self_times=None):
"""Aggregate records by name. Returns {name: {count, total, self, avg, max, sources}}."""
agg = defaultdict(
lambda: {"count": 0, "total": 0, "self": 0, "max": 0, "sources": set()}
)
for r in records:
name = r["name"]
dur = r["dur"]
a = agg[name]
a["count"] += 1
a["total"] += dur
if self_times:
a["self"] += self_times.get(id(r), dur)
else:
a["self"] += dur
if dur > a["max"]:
a["max"] = dur
if r["source"]:
a["sources"].add(r["source"])
return agg
def aggregate_by_thread(records, self_times=None):
"""Aggregate records by (name, tid). Returns {(name, tid): stats}."""
agg = defaultdict(
lambda: {"count": 0, "total": 0, "self": 0, "max": 0, "sources": set()}
)
for r in records:
key = (r["name"], r["tid"])
dur = r["dur"]
a = agg[key]
a["count"] += 1
a["total"] += dur
if self_times:
a["self"] += self_times.get(id(r), dur)
else:
a["self"] += dur
if dur > a["max"]:
a["max"] = dur
if r["source"]:
a["sources"].add(r["source"])
return agg
def sort_key(sort_by, use_self=False):
field = "self" if use_self else "total"
if sort_by == "count":
return lambda item: item[1]["count"]
if sort_by == "name":
return lambda item: (
item[0] if isinstance(item[0], str) else item[0][0]
).lower()
return lambda item: item[1][field]
def us_to_ms(us):
return round(us / 1000, 2)
def main():
parser = argparse.ArgumentParser(
description="Summarize Typst --timings trace by event name.",
)
parser.add_argument("timings", help="Path to timings JSON file")
parser.add_argument(
"-n",
"--top",
type=int,
default=10,
help="Number of rows to show (default: 10)",
)
parser.add_argument(
"--min-ms",
type=float,
default=0.0,
help="Filter out entries with total < min-ms (default: 0)",
)
parser.add_argument(
"--sort",
choices=("total", "count", "name"),
default="total",
help="Sort by total, count, or name (default: total)",
)
parser.add_argument(
"--contains",
default="",
help="Only include event names containing this substring",
)
parser.add_argument(
"--self-time",
action="store_true",
help="Show self time (excluding children) and sort by it",
)
parser.add_argument(
"--by-thread",
action="store_true",
help="Break down timings per thread",
)
parser.add_argument(
"--source",
action="store_true",
help="Show source file:line locations from trace args",
)
parser.add_argument(
"--json",
dest="json_output",
action="store_true",
help="Output as JSON instead of table",
)
args = parser.parse_args()
try:
with open(args.timings, encoding="utf-8") as f:
events = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Error reading {args.timings}: {e}", file=sys.stderr)
sys.exit(1)
records = parse_events(events)
if not records:
print("No duration events found in trace.", file=sys.stderr)
sys.exit(1)
# Compute self time if requested or if JSON (include all fields)
self_times = None
if args.self_time or args.json_output:
self_times = compute_self_time(records)
# Summary header
tids = set(r["tid"] for r in records)
wall_us = max(r["start"] + r["dur"] for r in records) - min(
r["start"] for r in records
)
if not args.json_output:
print(
f"Trace: {len(records)} events, {len(tids)} threads, "
f"{us_to_ms(wall_us)}ms wall time"
)
print()
# Aggregate
if args.by_thread:
agg = aggregate_by_thread(records, self_times)
else:
agg = aggregate(records, self_times)
items = list(agg.items())
if args.contains:
if args.by_thread:
items = [i for i in items if args.contains in i[0][0]]
else:
items = [i for i in items if args.contains in i[0]]
use_self = args.self_time
items = sorted(
items, key=sort_key(args.sort, use_self), reverse=args.sort != "name"
)
if args.min_ms > 0:
min_us = args.min_ms * 1000
field = "self" if use_self else "total"
items = [i for i in items if i[1][field] >= min_us]
items = items[: args.top]
# JSON output
if args.json_output:
out = []
for key, stats in items:
entry = {
"name": key[0] if args.by_thread else key,
"count": stats["count"],
"total_ms": us_to_ms(stats["total"]),
"self_ms": us_to_ms(stats["self"]),
"avg_ms": us_to_ms(stats["total"] / stats["count"]),
"max_ms": us_to_ms(stats["max"]),
}
if args.by_thread:
entry["tid"] = key[1]
if stats["sources"]:
entry["sources"] = sorted(stats["sources"])
out.append(entry)
print(json.dumps(out, indent=2))
return
# Table output
if args.by_thread:
name_hdr = f"{'Name':<40} {'TID':>4}"
else:
name_hdr = f"{'Name':<50}"
if use_self:
hdr = (
f"{name_hdr} {'Count':>7} {'Total':>10} {'Self':>10} {'Avg':>9} {'Max':>9}"
)
else:
hdr = f"{name_hdr} {'Count':>7} {'Total':>10} {'Avg':>9} {'Max':>9}"
src_hdr = " Source" if args.source else ""
print(hdr + src_hdr)
print("-" * len(hdr) + ("-" * 40 if args.source else ""))
for key, stats in items:
if args.by_thread:
name, tid = key
name_col = f"{name[:40]:<40} {tid:>4}"
else:
name_col = f"{key[:50]:<50}"
count = stats["count"]
total_ms = us_to_ms(stats["total"])
avg_ms = us_to_ms(stats["total"] / count)
max_ms = us_to_ms(stats["max"])
if use_self:
self_ms = us_to_ms(stats["self"])
row = f"{name_col} {count:>7} {total_ms:>9.2f}ms {self_ms:>9.2f}ms {avg_ms:>8.2f}ms {max_ms:>8.2f}ms"
else:
row = f"{name_col} {count:>7} {total_ms:>9.2f}ms {avg_ms:>8.2f}ms {max_ms:>8.2f}ms"
if args.source and stats["sources"]:
row += " " + ", ".join(sorted(stats["sources"]))
print(row)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Search Typst API using a pre-computed BM25 index.
Usage:
python3 scripts/search-api.py "position slice string"
python3 scripts/search-api.py "image width" --top 5
python3 scripts/search-api.py "query heading" --kind method
python3 scripts/search-api.py "query heading" --channel 0.15.0
python3 scripts/search-api.py "query heading" --channel 0.14.2
python3 scripts/search-api.py "query heading" --channel main
python3 scripts/search-api.py "color" --kind type --json
python3 scripts/search-api.py --name str.position
python3 scripts/search-api.py --list-categories
"""
import argparse
import json
import os
import re
import sys
from collections import defaultdict
def tokenize(text):
"""Lowercase, split on non-alphanumeric, keep all tokens including 1-char."""
return [t for t in re.split(r"[^a-z0-9]+", text.lower()) if t]
def load_json(path):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def resolve_data_dir(override):
if override:
return override
script_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(script_dir, "..", "data")
def resolve_index_paths(data_dir, channel):
"""Return (api_path, bm25_path) for an API data channel."""
stems = {
"stable": "api",
"0.15.0": "api-0.15.0",
"0.14.2": "api-0.14.2",
"main": "api-main",
}
stem = stems[channel]
return (
os.path.join(data_dir, f"{stem}.json"),
os.path.join(data_dir, f"{stem}-bm25.json"),
)
def bm25_search(query_tokens, bm25, top_n=20):
"""Score documents against query using BM25."""
scores = defaultdict(float)
meta = bm25["meta"]
avg_dl = meta["avg_dl"]
k1 = meta["k1"]
b = meta["b"]
for token in query_tokens:
idf_val = bm25["idf"].get(token, 0)
if idf_val == 0:
continue
for doc_id_str, tf in bm25["postings"].get(token, []):
doc_id = int(doc_id_str) if isinstance(doc_id_str, str) else doc_id_str
dl = bm25["doc_lengths"].get(
str(doc_id), bm25["doc_lengths"].get(doc_id, avg_dl)
)
# Cap doc length to prevent over-penalizing long documents
dl = min(dl, avg_dl * 3)
numerator = tf * (k1 + 1)
denominator = tf + k1 * (1 - b + b * dl / avg_dl)
scores[doc_id] += idf_val * numerator / denominator
ranked = sorted(scores.items(), key=lambda x: -x[1])
return ranked[:top_n]
def format_params(params):
"""Format parameter list for display."""
parts = []
for p in params:
types = "|".join(p.get("types", []))
name = p["name"]
if p.get("required"):
parts.append(f"{name}: {types}")
else:
default = p.get("default", "")
if default:
parts.append(f"{name}: {types} = {default}")
else:
parts.append(f"{name}: {types}?")
return ", ".join(parts)
def format_entry(entry, verbose=False):
"""Format a single API entry for display."""
if entry.get("kind") == "symbol":
value = entry.get("value", "")
lines = [f"{entry['name']} {value}"]
parts = []
if entry.get("mathShorthand"):
parts.append(f"math: {entry['mathShorthand']}")
if entry.get("markupShorthand"):
parts.append(f"markup: {entry['markupShorthand']}")
if parts:
lines.append(f" shorthand: {', '.join(parts)}")
return "\n".join(lines)
params = format_params(entry.get("params", []))
returns = "|".join(entry.get("returns", []))
sig = f"{entry['name']}({params})"
if returns:
sig += f" -> {returns}"
lines = [sig]
lines.append(f" {entry.get('oneliner', '')}")
if verbose:
lines.append(
f" [{entry['kind']}] category: {entry['category']} | docs: https://typst.app/docs{entry.get('route', '')}"
)
if entry.get("contextual"):
lines.append(" requires context")
if entry.get("deprecated"):
lines.append(f" DEPRECATED: {entry['deprecated']}")
# Show enum values for string params
for p in entry.get("params", []):
if p.get("strings"):
lines.append(
f" {p['name']} values: {', '.join(repr(s) for s in p['strings'])}"
)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Search Typst API reference.")
parser.add_argument("query", nargs="?", default="", help="Search query")
parser.add_argument("--top", type=int, default=10, help="Number of results")
parser.add_argument(
"--kind",
choices=["function", "method", "constructor", "type", "symbol"],
help="Filter by kind",
)
parser.add_argument(
"--category", help="Filter by category (e.g., Foundations, Layout)"
)
parser.add_argument("--name", help="Exact name lookup (e.g., str.position)")
parser.add_argument("--json", action="store_true", help="JSON output")
parser.add_argument(
"--verbose", "-v", action="store_true", help="Show more details"
)
parser.add_argument(
"--list-categories", action="store_true", help="List all categories"
)
parser.add_argument("--data-dir", help="Override data directory")
parser.add_argument(
"--channel",
choices=["stable", "0.15.0", "0.14.2", "main"],
default="stable",
help="API data channel: stable release alias, legacy 0.14.2, or upstream main preview",
)
args = parser.parse_args()
data_dir = resolve_data_dir(args.data_dir)
api_path, bm25_path = resolve_index_paths(data_dir, args.channel)
api = load_json(api_path)
if args.list_categories:
cats = sorted(set(e["category"] for e in api))
for c in cats:
count = sum(1 for e in api if e["category"] == c)
print(f" {c} ({count})")
return
if args.name:
matches = [e for e in api if e["name"] == args.name]
if not matches:
# Try partial match
matches = [e for e in api if args.name in e["name"]]
if not matches:
print(f"No entry found for '{args.name}'", file=sys.stderr)
sys.exit(1)
if args.json:
print(json.dumps(matches, indent=2))
else:
for e in matches:
print(format_entry(e, verbose=True))
print()
return
if not args.query:
parser.print_help()
sys.exit(1)
bm25 = load_json(bm25_path)
tokens = tokenize(args.query)
if not tokens:
print("No searchable terms in query", file=sys.stderr)
sys.exit(1)
# Pre-filter by kind/category before BM25 to avoid missing matches
candidate_ids = set(range(len(api)))
if args.kind:
candidate_ids = {i for i in candidate_ids if api[i]["kind"] == args.kind}
if args.category:
candidate_ids = {
i
for i in candidate_ids
if args.category.lower() in api[i]["category"].lower()
}
results = bm25_search(tokens, bm25, top_n=len(api))
# Use explicit --top if set, otherwise default higher for symbols
top_n = args.top
filtered = []
for doc_id, score in results:
if doc_id not in candidate_ids:
continue
# Apply category weight multiplier
weight = api[doc_id].get("weight", 1.0)
filtered.append((api[doc_id], score * weight))
if len(filtered) >= top_n * 3:
break
# Re-sort after weight adjustment
filtered.sort(key=lambda x: -x[1])
filtered = filtered[:top_n]
if not filtered:
print("No results found", file=sys.stderr)
sys.exit(1)
if args.json:
print(json.dumps([e for e, _ in filtered], indent=2))
else:
for i, (entry, score) in enumerate(filtered, 1):
print(f"{i:2}. {format_entry(entry, verbose=args.verbose)}")
print()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Extract and validate inline Typst code blocks from skill documentation.
Extracts ```typst fenced code blocks from .md files using a CommonMark parser,
wraps snippets with a standard preamble to supply common undefined variables,
and attempts to compile each block with `typst compile`.
Requires: markdown-it-py (pip install markdown-it-py)
Usage:
python3 scripts/validate-examples.py # validate all .md files
python3 scripts/validate-examples.py basics.md types.md # specific files
python3 scripts/validate-examples.py --json # JSON output
python3 scripts/validate-examples.py --keep # keep temp files for debugging
"""
import argparse
import json
import re
import subprocess
import sys
import tempfile
from pathlib import Path
try:
from markdown_it import MarkdownIt
except ImportError:
print(
"ERROR: markdown-it-py is required. Install with: pip install markdown-it-py",
file=sys.stderr,
)
sys.exit(2)
# Standard preamble injected before each snippet to define common variables
# used across documentation examples.
PREAMBLE = r"""
// === Validation preamble (auto-injected) ===
#let items = ("alpha", "beta", "gamma")
#let condition = true
#let x = 5
#let config = (color: blue, size: 12pt, name: "test")
#let dict = (name: "Alice", age: 30, city: "NYC")
#let key = "name"
#let default = "N/A"
#let body = [Sample body content]
#let prefix = [Pre]
#let suffix = [Suf]
#let prefix-str = "Pre"
#let body-str = "Body"
#let suffix-str = "Suf"
#let transform(x) = x
// === End preamble ===
"""
# Patterns that indicate a block is intentionally non-compilable
SKIP_PATTERNS = [
r"^\s*//\s*\.\.\.", # // ... placeholder
r"\.\.\.", # any ... ellipsis
r'#import\s+"[^@]', # relative imports (file not present)
r'#import\s+"@local/', # local packages (not installed)
r'#import\s+"@preview/', # preview packages may require network/cache
r'#import\s+"@preview/package-name', # placeholder package names
r'#include\s+"', # include (file not present)
r"image\(", # image() references (with or without #)
r"#bibliography\(", # bibliography references
r"#read\(", # read() file references
r"#csv\(", # csv() file references
r"#json\(", # json() file references but allow xml()
r"#yaml\(", # yaml() file references
r'#raw\("', # raw() with string (often partial)
r"item\.key", # assumes dict-shaped items
r"```xml", # embedded raw XML blocks
r"@\w+\d{4}", # citation references (require .bib file)
]
def extract_blocks(md_path: Path) -> list[dict]:
"""Extract ```typst code blocks with line numbers using CommonMark parsing."""
text = md_path.read_text()
md = MarkdownIt()
tokens = md.parse(text)
blocks = []
for token in tokens:
if token.type == "fence" and token.info.strip() == "typst":
# token.map is [start_line, end_line] (0-indexed)
# Content starts on the line after the opening fence
line = token.map[0] + 2 # +1 for 0-index, +1 for fence line
blocks.append(
{
"file": str(md_path),
"line": line,
"code": token.content,
}
)
return blocks
def should_skip(code: str) -> str | None:
"""Return a reason string if this block should be skipped, else None."""
for pattern in SKIP_PATTERNS:
if re.search(pattern, code):
return f"matches skip pattern: {pattern}"
return None
def compile_block(
code: str, preamble: bool = True, keep: bool = False
) -> tuple[bool, str]:
"""Try to compile a Typst code block. Returns (success, stderr)."""
full_code = (PREAMBLE + "\n" + code) if preamble else code
with tempfile.NamedTemporaryFile(
suffix=".typ", mode="w", delete=not keep, dir="."
) as f:
f.write(full_code)
f.flush()
tmp_path = f.name
try:
result = subprocess.run(
["typst", "compile", tmp_path, "/dev/null", "-f", "pdf"],
capture_output=True,
text=True,
timeout=10,
)
return result.returncode == 0, result.stderr.strip()
except FileNotFoundError:
return False, "typst not found in PATH"
except subprocess.TimeoutExpired:
return False, "compilation timed out (10s)"
def main():
parser = argparse.ArgumentParser(
description="Validate inline Typst examples from skill .md files",
)
parser.add_argument(
"files",
nargs="*",
help="Specific .md files to validate (default: all *.md in skill dir)",
)
parser.add_argument(
"--json",
action="store_true",
dest="json_output",
help="Output results as JSON",
)
parser.add_argument(
"--keep",
action="store_true",
help="Keep temporary .typ files for debugging",
)
parser.add_argument(
"--no-preamble",
action="store_true",
help="Skip injecting the standard variable preamble",
)
parser.add_argument(
"--include-skipped",
action="store_true",
help="Also attempt to compile blocks that would normally be skipped",
)
args = parser.parse_args()
# Resolve skill directory
script_dir = Path(__file__).resolve().parent
skill_dir = script_dir.parent
if args.files:
md_files = [skill_dir / f for f in args.files]
else:
md_files = sorted(skill_dir.rglob("*.md"))
results = []
counts = {"pass": 0, "fail": 0, "skip": 0}
for md_file in md_files:
if not md_file.exists():
print(f"WARNING: {md_file} not found, skipping", file=sys.stderr)
continue
blocks = extract_blocks(md_file)
for block in blocks:
skip_reason = should_skip(block["code"])
rel_path = md_file.relative_to(skill_dir).as_posix()
if skip_reason and not args.include_skipped:
counts["skip"] += 1
results.append(
{
"file": rel_path,
"line": block["line"],
"status": "skip",
"reason": skip_reason,
}
)
continue
ok, stderr = compile_block(
block["code"],
preamble=not args.no_preamble,
keep=args.keep,
)
if ok:
counts["pass"] += 1
status = "pass"
else:
counts["fail"] += 1
status = "fail"
entry = {
"file": rel_path,
"line": block["line"],
"status": status,
}
if not ok:
# Show first line of error for context
first_error = stderr.split("\n")[0] if stderr else "unknown error"
entry["error"] = first_error
results.append(entry)
# Output
if args.json_output:
print(json.dumps({"counts": counts, "results": results}, indent=2))
else:
for r in results:
icon = {"pass": "OK", "fail": "FAIL", "skip": "SKIP"}[r["status"]]
loc = f"{r['file']}:{r['line']}"
msg = r.get("error", r.get("reason", ""))
suffix = f" {msg}" if msg else ""
print(f"{icon:>4} {loc:<30}{suffix}")
print()
total = counts["pass"] + counts["fail"] + counts["skip"]
print(
f"Total: {total} Pass: {counts['pass']} Fail: {counts['fail']} Skip: {counts['skip']}"
)
sys.exit(1 if counts["fail"] > 0 else 0)
if __name__ == "__main__":
main()
Related skills
How it compares
Choose typst over LaTeX skills when you want simpler markup syntax with comparable academic typography and faster iteration in Typst.
FAQ
What does typst do?
Typst document creation and package development. Use when: (1) Working with .typ files, (2) User mentions typst, typst.toml, or typst-cli, (3) Creating or using Typst packages, (4) Developing document templates, (5) Conv
When should I use typst?
Typst document creation and package development. Use when: (1) Working with .typ files, (2) User mentions typst, typst.toml, or typst-cli, (3) Creating or using Typst packages, (4) Developing document templates, (5) Conv
What are common prerequisites?
--- name: typst description: 'Typst document creation and package development.
Is Typst safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.