Skip to main content
The slurp-compiler crate is the CLI and the library. The CLI is a thin wrapper over the functions below, which are what a host server calls per request.
Slurp is 0.1.0, pre-1.0. This API will change without a deprecation cycle until 1.0. Pin an exact version if you depend on it.

Adding the dependency

Cargo.toml
serde_json is not optional in practice: the render context is a serde_json::Value and every schema extraction returns one. The minimum supported Rust version is 1.88.

Minimal example

Two shapes of entry point

There are two families.

Result-returning

compile_template, compile_with_options. Return Result<String, CompileError>, giving you the first error and nothing else. Good for a request handler that either serves a page or serves a 500.

Diagnostic-returning

compile, compile_with_registry, compile_with_render_options, render*. Return (String, ErrorAccumulator), giving you every error and every warning. Good for a build, a publish gate, or anything that reports to a human.
Warnings are where the truncations live. A Result-returning call that succeeds reports nothing about whether a loop was capped at 1,000 items or the output budget cut the page in half. Only the accumulator carries that. See Limits.

compile_template

Compiles one template string in BuildMode::Production. The file name recorded on every diagnostic is the literal "template", since there is no path to use. CompileError implements Display as [Code] message at file:line:column (severity), so this prints as ... at template:3:12 (error). It runs, in order: parse, the compile-time security walk, then render. Any of the three failing returns the first error.

CompileOptions

It derives Default, so ..Default::default() is the normal way to build one.
Option<String>
default:"None"
The render context, as a JSON string rather than a serde_json::Value.Invalid JSON is silently treated as {}. There is no error for it. Every path in the template then resolves to null and the page renders as though the data were missing, which looks exactly like a template bug. Validate the JSON yourself before calling, or use compile instead, which takes a &serde_json::Value and cannot have this problem.
bool
default:"false"
Compile under middleware rules. They are inverted, not merely looser: request.*, {redirect} and {next} are legal only when this is true, and every other path root except env and loop is illegal when it is.It is a compile option rather than a path convention, because a host may keep middleware anywhere. A caller with no way to know passes false, which is what the CLI does.
Option<HashMap<String, String>>
default:"None"
A virtual file system for multi-file compilation, used only by compile_with_options below. compile_template ignores it.
Option<String>
default:"None"
Which key in virtual_files is the entry point. Defaults to "entry.slurp". compile_template ignores it.
MemoryBudget
default:"MemoryBudget::DEFAULT"
Per-render allocation caps: 8 MiB per filter value, 64 MiB of cumulative filter work, 16 MiB of output. See Limits for what each one defends against and how to size it.

compile_with_options

Compiles from options.virtual_files, starting at options.entry_file. Circular imports are detected up front, before anything is compiled.
With no virtual_files, or with an entry_file that is not a key in the map, this compiles the empty string and returns Ok(""). It is not an error.
Note that virtual_files drives circular-import detection, not component resolution. Resolving <Card /> against a set of files is the file registry, which is a separate mechanism.

compile

The entry point the CLI uses, and usually the right one for a host. It takes a real serde_json::Value, a real file name for diagnostics, and returns every diagnostic rather than the first.
On any error, html is empty. Check diag.has_errors() before serving it, or you will serve a blank page instead of a 500.

BuildMode

Development renders {debug expr} nodes as a visible <pre data-slurp-debug> element and records two extra advisory warnings: CSS-structural characters stripped from a style value, and an un-annotated interpolation in a JavaScript-evaluated attribute. Production strips {debug} entirely and stays silent about both. The budget diagnostics are not among the development-only pair. Loop truncation, the output-byte budget and the render-depth budget are recorded in both modes, because each one silently removes content from the page.

Components and layouts: the file registry

compile on its own resolves nothing. A <Card /> whose import cannot be resolved renders a placeholder and does not fail:
To resolve imports, supply a registry mapping paths to source:
The registry key is exactly the path written in the template, so @components/Card for using "@components/Card" and @layouts/base for <layout src="@layouts/base">. A filesystem-shaped key such as components/Card.slurp does not resolve, and the failure is the silent placeholder above rather than an error.A host that also does route resolution by file path usually needs both key forms in the map. Insert both; they are cheap and they answer different questions.
An unresolvable layout has a different signature, a comment wrapper around an unfilled <slot>:
Grep your output for data-slurp-component and <!-- layout: in tests. They are the only evidence that resolution failed. The registry is an Arc<HashMap<..>> so it can be built once per theme and cloned per request. Parsed component ASTs are memoised per render, so a component used a hundred times in a loop is parsed once.

RenderOptions

compile_with_registry cannot set a memory budget and compile cannot set a registry. RenderOptions carries both.
compile and compile_with_registry both delegate here with MemoryBudget::DEFAULT, so adding options to an existing call site cannot change behaviour by accident.

The security walk

The walk enforces rules that live outside the parser, so a file that parses cleanly can still be refused:
  • an unfiltered ${ } in a <script> body,
  • a | js slot in JavaScript statement position, where escaping quote characters cannot contain the value,
  • env.SLURP_SECRET_*, in any file, including the bracket form env["..."] and any non-constant env[k],
  • request.* outside middleware, and anything but request, env and loop inside it,
  • {redirect} and {next} outside middleware.

Which entry points run it

The three compile_with_* functions hardcode is_middleware = false. Every entry point that reaches them compiles a page; passing true would relax the page rules, and taking a caller-supplied flag would let a page opt out of them. A middleware file is never compiled at all: the CLI checks it with check_security(.., true) and stops there, and a server resolves it at request time.

Running the walk manually

If you parse once and render many times, run the walk after parsing and before the first render.
The walk returns on its first violation. It is a Result, not an accumulator, so at most one security diagnostic comes back per call. Fix the reported one and run it again to see the next.
is_middleware must match the value the eventual compile will use, because the rules differ.

Rendering a parsed document

render takes three arguments and has no file parameter, so its diagnostics carry an empty file name. Like compile, it returns diagnostics rather than a Result, because rendering is total: it always produces a string, and every failure mode is either an empty value or a truncation. parse is the other half:
Err means lexing failed outright. Ok with a non-empty diag.errors means the parser recovered enough to build an AST but the source is still broken. Check both.

Error handling

ErrorCode is #[non_exhaustive], so match on it with a catch-all arm. See Error codes for the full list, including the eight codes that are declared for host integrations and that the compiler itself never emits.

ErrorAccumulator

  • push(e) files by severity: Error into errors, Warning and Info into warnings.
  • has_errors() is the gate before serving the HTML.
  • all() iterates errors then warnings.
  • MAX_DIAGNOSTICS is 1,000 combined. Past that, push is a no-op, because a pathological input can raise a diagnostic per token. If you see exactly 1,000, assume there are more.
A practical handler:

Sections and theme blocks

Sections are the editor-facing half of the language: a template declares a typed schema in its frontmatter, a host stores per-instance state as JSON, and the two are merged at render time.
Rendering one section, in four variants of increasing capability:
The merge is tolerant: unknown setting keys are dropped, out-of-range numbers are clamped to the schema’s min and max, and nothing is rejected, so saved state survives a theme update.

@theme requires a block catalog

Without a catalog, a @theme-targeted block is an unknown type and is dropped at merge time, while surviving intact in storage. The page renders as though nothing was added and nothing is lost, so it presents as an editor that saves and does not display.Build the catalog from the theme’s blocks/<name>.slurp files with theme_block_type and catalog_entry, then use merged_section_value_with and render_section_with_registry_and_blocks. The catalog-free variants are correct only for a theme that uses no @theme targeting.
Blocks are also capped: nesting is merged to depth 64, and a type with no declared max is capped at 200 instances. Both drop silently. See Limits.

Other exports

The same ammonia-backed sanitizer {html expr sanitize} uses, exposed so a host can sanitize on its own boundary with the identical allow-list.
Returns (variable_name, evaluated_url) for every {fetch} node in a document. The dev server uses it to pre-fetch API data before SSR. It walks the body and all three side branches, and stops descending past 512 levels, silently. Also re-exported: Document, parse, the sections::* family, SECTIONS_HTML_KEY and section_group_html_key, which are the reserved context keys a host uses to inject pre-rendered section HTML for {sections} and {sections "name"}.

Next

Limits

Every budget, and how to size MemoryBudget for your host.

Error codes

What each ErrorCode means, and which ones never occur.

JavaScript API

The same compiler through WASM, for editors and browsers.

Sections and blocks

The schema language these functions consume.