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.
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.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
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
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
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.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
serde_json::Value, a real file name for diagnostics, and returns every
diagnostic rather than the first.
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:
<slot>:
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
| jsslot in JavaScript statement position, where escaping quote characters cannot contain the value, env.SLURP_SECRET_*, in any file, including the bracket formenv["..."]and any non-constantenv[k],request.*outside middleware, and anything butrequest,envandloopinside 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.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:Errorintoerrors,WarningandInfointowarnings.has_errors()is the gate before serving the HTML.all()iterates errors then warnings.MAX_DIAGNOSTICSis 1,000 combined. Past that,pushis a no-op, because a pathological input can raise a diagnostic per token. If you see exactly 1,000, assume there are more.
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.min and max, and nothing is rejected, so saved
state survives a theme update.
@theme requires a block catalog
max is capped at 200 instances. Both drop silently. See
Limits.
Other exports
{html expr sanitize} uses, exposed so a host
can sanitize on its own boundary with the identical allow-list.
(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.