Template Developer Guide
A template is a compiled executable that communicates with Presto via stdin/stdout. The user's Markdown content is passed through standard input, and the template converts it into Typst source code output to standard output. Presto then invokes the Typst compiler to generate the final PDF.
For the detailed communication specification, see the Binary Protocol. For core terminology, see the Glossary.
Part 1: 5-Minute Quick Start
Scaffolding (Recommended)
npx create-presto-templateThe interactive wizard will ask 7 questions:
- Template name (kebab-case, e.g.,
my-report) - Display name (e.g., "My Report Template")
- Description (one sentence describing its purpose)
- Development language (Go recommended / Rust / TypeScript)
- Category (up to 20 characters, e.g., "official-doc", "resume")
- GitHub username
- License (MIT / Apache-2.0 / GPL-3.0)
Manual Setup
Fork the corresponding language Starter repository:
- Go:
presto-template-starter-go - Rust:
presto-template-starter-rust - TypeScript:
presto-template-starter-typescript
Project Structure (Go Example)
my-template/
main.go # Entry point: protocol implementation + Markdown to Typst conversion
manifest.json # Template metadata
example.md # Example document
template_head.typ # Typst template header (optional)
Makefile # Build / test / preview commands
CLAUDE.md # AI development configurationBuild and Preview
make build # Compile the binary
make preview # Install to Presto and previewPart 2: Binary Protocol Quick Reference
For the full specification, see binary-protocol.md. Below is a practical summary.
Four Invocation Methods
| Command | stdin | stdout | Purpose |
|---|---|---|---|
cat input.md | ./binary | Markdown | Typst source | Core conversion |
./binary --manifest | None | manifest JSON | Metadata query |
./binary --example | None | Example Markdown | Example document |
./binary --version | None | Version number | Version query |
No flags outside the protocol may be added.
Three-Language Implementation Comparison
| Feature | Go | Rust | TypeScript |
|---|---|---|---|
| Markdown parser | goldmark | pulldown-cmark | marked |
| Parsing mode | AST traversal | Event-driven | Token traversal |
| Embedding method | //go:embed | include_str!() | import with { type: "text" } |
| Build command | go build | cargo build --release | bun build --compile |
manifest.json Field Reference
| Field | Required | Description |
|---|---|---|
name | Yes | Template unique identifier, kebab-case |
displayName | Yes | User-visible template name |
version | Yes | Semantic version number (semver) |
description | No | Template purpose description |
author | No | Author information |
license | No | Open-source license |
category | No | Category tag, up to 20 characters |
keywords | No | Search keywords array |
minPrestoVersion | No | Minimum compatible Presto version |
requiredFonts | No | Required font list (array of objects with name, displayName, url) |
frontmatterSchema | No | User-configurable frontmatter field definitions |
Types supported by frontmatterSchema:
| type | Description | Example default |
|---|---|---|
"string" | Text | "Untitled Paper" |
"boolean" | Boolean | false |
"number" | Number | 1.5 |
The optional format field provides a hint for UI rendering (e.g., "date" displays a date picker).
Example 1: Starter Default Manifest
{
"name": "my-template",
"displayName": "My Template",
"description": "Replace with your template description",
"version": "0.1.0",
"author": "your-github-username",
"license": "MIT",
"category": "general",
"keywords": ["template"],
"minPrestoVersion": "0.1.0",
"requiredFonts": [],
"frontmatterSchema": {
"title": { "type": "string", "default": "Enter title" }
}
}Example 2: gongwen Official Template Manifest
{
"name": "gongwen",
"displayName": "Official Document Template",
"description": "Official document typesetting following the GB/T 9704-2012 standard",
"version": "1.0.0",
"author": "Presto-io",
"license": "MIT",
"category": "official-doc",
"keywords": ["official document", "notice", "report", "government", "GB/T 9704"],
"minPrestoVersion": "0.1.0",
"requiredFonts": [
{ "name": "FZXiaoBiaoSong-B05", "displayName": "FZ XiaoBiaoSong", "url": "https://www.foundertype.com/..." },
{ "name": "STHeiti", "displayName": "STHeiti", "url": "https://www.foundertype.com/..." },
{ "name": "STFangsong", "displayName": "STFangsong", "url": "https://www.foundertype.com/..." },
{ "name": "STKaiti", "displayName": "STKaiti", "url": "https://www.foundertype.com/..." },
{ "name": "STSong", "displayName": "STSong", "url": "https://www.foundertype.com/..." }
],
"frontmatterSchema": {
"title": { "type": "string", "default": "Enter text" },
"author": { "type": "string", "default": "Enter text" },
"date": { "type": "string", "format": "YYYY-MM-DD" },
"signature": { "type": "boolean", "default": false }
}
}Part 3: Typst Quick Reference
Typst is the underlying typesetting engine used by Presto. The output of a template is Typst source code. Below are the most commonly used syntax elements for template development.
Page Setup
#set page(paper: "a4", margin: (top: 2.54cm, bottom: 2.54cm, left: 2.58cm, right: 2.08cm))Fonts and Paragraphs
#set text(font: "SimSun", size: 12pt, lang: "zh")
#set par(leading: 1.5em, first-line-indent: 2em)Common Functions
| Function | Purpose | Example |
|---|---|---|
page | Page setup | #set page(paper: "a4") |
text | Font style | #set text(font: "SimSun", size: 12pt) |
par | Paragraph | #set par(leading: 1.5em) |
heading | Heading | #heading(level: 1)[Title] |
table | Table | #table(columns: 3, [...], [...]) |
image | Image | #image("photo.png", width: 80%) |
v | Vertical space | #v(1em) |
h | Horizontal space | #h(2em) |
align | Alignment | #align(center)[content] |
box | Inline box | #box(width: 5cm)[...] |
block | Block box | #block(fill: luma(230))[...] |
grid | Grid layout | #grid(columns: 2, gutter: 1em, [...], [...]) |
pagebreak | Page break | #pagebreak() |
line | Line | #line(length: 100%) |
Chinese Font Mapping
| Typst Font Name | Chinese Name |
|---|---|
| SimSun | Song (宋体) |
| SimHei | Hei (黑体) |
| FangSong | FangSong (仿宋) |
| KaiTi | Kai (楷体) |
| Microsoft YaHei | Microsoft YaHei (微软雅黑) |
| FZXiaoBiaoSong-B05 | FZ XiaoBiaoSong (方正小标宋) |
| FZFangSong-Z02 | FZ FangSong (方正仿宋) |
Markdown to Typst Conversion Reference
| Markdown | Typst |
|---|---|
| frontmatter field | #let variable = "value" |
# heading | #heading(level: 1)[...] |
**bold** | #strong[...] |
*italic* | #emph[...] |
`code` | #raw("...") |
- item | - item |
--- | #line(length: 100%) |
Part 4: Pattern Recipes
The following 6 patterns are all extracted from real code in official templates.
Recipe 1: Custom Heading Numbering
Requirement: Official document headings use four levels of numbering in Chinese format.
The gongwen template defines 5-level counters in template_head.typ:
#let h2-counter = counter("h2")
#let h3-counter = counter("h3")
#let h4-counter = counter("h4")
#let h5-counter = counter("h5")
#let custom-heading(level, body, numbering: auto) = {
if level == 2 {
h2-counter.step()
h3-counter.update(0)
text(font: FONT_HEI, size: zh(3))[#context h2-counter.display("一、")#body]
} else if level == 3 {
h3-counter.step()
text(font: FONT_KAI, size: zh(3))[#context h3-counter.display("(一)")#body]
} else if level == 4 {
h4-counter.step()
let number = h4-counter.get().first()
text(size: zh(3))[#number. #body]
} else if level == 5 {
h5-counter.step()
let number = h5-counter.get().first()
text(size: zh(3))[(#number)#body]
}
}
#show heading: it => {
custom-heading(it.level, it.body, numbering: it.numbering)
}The Go code in the template converts Markdown headings to Typst heading syntax, and the #show heading rule automatically applies the numbering format.
Recipe 2: Table Layout
Requirement: The jiaoan-shicao template uses landscape A4 + fixed column widths + rowspan merging.
Typst page setup:
#set page(paper: "a4", flipped: true, margin: (top: 2.54cm, bottom: 2.54cm, left: 2.58cm, right: 2.08cm))
#table(
columns: (2.3cm, 4.2cm, auto, auto, 2.2cm, 1.1cm),
stroke: 0.5pt,
align: center + horizon,
// rowspan merging
table.cell(rowspan: 3)[Activity Name],
[Content 1], [Content 2], [Content 3], [Method], [Hours],
)The Go code implements rowspan merging by detecting a "ditto" marker: when a cell's content is the ditto marker, it automatically merges with the cell above, outputting table.cell(rowspan: N)[...].
Recipe 3: Font Declaration
Requirement: Declare dependent fonts in the manifest and use them in Typst.
Declare in manifest.json:
{
"requiredFonts": [
{ "name": "FZXiaoBiaoSong-B05", "displayName": "FZ XiaoBiaoSong", "url": "https://..." },
{ "name": "STFangsong", "displayName": "STFangsong", "url": "https://..." }
]
}Define font constants and use them in Typst:
#let FONT_XBS = "FZXiaoBiaoSong-B05"
#let FONT_FS = "STFangsong"
#set text(font: FONT_FS, size: zh(3))
// Use XiaoBiaoSong for titles
#text(font: FONT_XBS, size: zh(2), weight: "bold")[Title content]When Presto installs a template, it reads requiredFonts and prompts the user to install any missing fonts.
Recipe 4: Image Scaling
Requirement: Smart image scaling in the gongwen template (max 13.4cm), with grid layout for multiple images.
Single image scaling (Typst code generated by Go):
#figure(
context {
let img = image("photo.png")
let img-size = measure(img)
let x = img-size.width
let y = img-size.height
let max-size = 13.4cm
let new-x = x
let new-y = y
if x > max-size {
let scale = max-size / x
new-x = max-size
new-y = y * scale
}
if new-y > max-size {
let scale = max-size / new-y
new-x = new-x * scale
new-y = max-size
}
image("photo.png", width: new-x, height: new-y)
},
caption: [photo],
)For multiple images, grid is used for automatic arrangement, calculating how many fit per row based on aspect ratio.
Recipe 5: Odd/Even Page Footers
Requirement: In the gongwen template, odd page numbers are right-aligned, even page numbers are left-aligned, formatted as "--- N ---".
#set page(
footer-descent: 7mm,
footer: context {
let page-num = here().page()
let is-even = calc.even(page-num)
let num = str(page-num)
let pm = text(font: FONT_SONG, size: zh(4))[— #num —]
if is-even {
align(left, [#h(1em) #pm])
} else {
align(right, [#pm #h(1em)])
}
},
)Key point: Use context to get the current page number and calc.even() to determine odd/even.
Recipe 6: Custom Markers
Requirement: Use special markers in Markdown to control typesetting.
The gongwen template supports three custom markers:
| Markdown Marker | Effect | Generated Typst |
|---|---|---|
{v} or {v:N} | Insert N blank lines | #linebreak(justify: false) |
{pagebreak} | Force page break | #pagebreak() |
{.noindent} | Remove paragraph indent | #set par(first-line-indent: 0pt) |
Processing logic in Go code:
// processMarker checks whether the text is a standalone marker
func processMarker(text string) (string, bool) {
text = strings.TrimSpace(text)
if m := vMarkerRe.FindStringSubmatch(text); m != nil {
count := 1
if m[1] != "" {
count, _ = strconv.Atoi(m[1])
}
var lines []string
for i := 0; i < count; i++ {
lines = append(lines, "#linebreak(justify: false)")
}
return strings.Join(lines, "\n") + "\n", true
}
if text == "{pagebreak}" {
return "#pagebreak()\n", true
}
return "", false
}{.noindent} can be used as a trailing marker on a paragraph, or with the Pandoc-style ::: {.noindent} block syntax to wrap multiple paragraphs.
Part 5: Development Workflow
Local Development Loop
make build # Compile the binary
make test # Run tests (including security checks)
make preview # Install to Presto and previewTest Coverage
make test automatically performs the following checks:
- Whether manifest is valid JSON
- Example round-trip: whether
--exampleoutput can be correctly converted by the template - Whether
--versionoutputs correctly - Category field validation (non-empty, max 20 characters, valid characters)
- Three-layer security testing:
- Static analysis: checks for imports of prohibited packages
- Network isolation: runs the template in a sandbox to confirm no network access
- Output validation: confirms stdout contains only valid Typst code
AI-Assisted Development
Starter repositories come pre-configured with CLAUDE.md, containing project rules, tech stack constraints, and development workflows. For detailed AI workflow patterns, see ai-workflow-patterns.md.
Recommended four-step approach:
- Provide reference documents (PDF/DOCX) to AI for analyzing typesetting characteristics
- Confirm typesetting parameters and configurable options
- Generate code step by step (manifest first, then conversion logic, finally edge case handling)
- Run
make previewto visually inspect the PDF output
Security Essentials
Templates run in a strict sandbox (30-second timeout, no network, no file writes). During development, be aware of Typst injection protection:
- When embedding user input in Typst strings (
"..."), escape\,",# - When embedding user input in Typst content blocks (
[...]), escape\,],#
Go's internal/typst package provides two escape functions:
typst.EscapeString(s) // For "..." context
typst.EscapeContent(s) // For [...] contextProhibited packages: net, net/*, os/exec, plugin, debug/*.
Part 6: Release Checklist
Pre-Release Checks
- [ ]
make testpasses completely (including security tests) - [ ]
manifest.jsonversionfield has been updated - [ ]
example.mdconverts correctly - [ ] Preview in Presto looks correct
- [ ]
categoryis 20 characters or fewer
Release Steps
# 1. Update the version in manifest.json
# 2. Commit the changes
git add manifest.json
git commit -m "chore: bump version to 1.1.0"
# 3. Tag and push
git tag v1.1.0
git push origin main --tagsCI will automatically perform 6-platform builds (darwin/linux/windows x arm64/amd64) and create a GitHub Release. For the detailed release process and CI configuration, see release-process.md.
Registry Listing
- Add the
presto-templatetopic to your GitHub repository settings - The registry automatically scans daily at UTC 08:00
- You will automatically receive the
communitytrust level - For
verifiedlevel, submit a PR to theverified-templates.jsonfile in thetemplate-registryrepository
