${ } normally comes out escaped for wherever it landed. Attributes and
<script> bodies are where that stops being uniform: a browser reads those two
places with different parsers, and no single escaper is correct in both.
For a fix to error[UnsafeScriptInterpolation], skip to
Choosing between | js and | json.
Ordinary attributes need nothing
Any quoted attribute interpolates, and the value is escaped for an attribute value on the way out.data- attribute is the simplest way to hand a value to client-side code:
publish it in markup, read it back with dataset in your own script. Nothing on
this page applies to it.
slurp build minifies its output, so the HTML on disk may drop quotes and
decode entities that are unambiguous in position (data-product-id=42). The
examples here show what the compiler emits, before that pass. Both forms parse
identically.Interpolation in a <script> body
Write an interpolation in a script body with no filter and the build fails:
<script> is a raw text element: the HTML parser hands its contents to the
JavaScript engine without decoding character references. So the entity escaping
that makes ${ } safe in ordinary text does nothing at all there, and every
JavaScript metacharacter except < passes through untouched. There is no single
correct escaper either, because a value can land inside a string literal or in
code position, and those need different treatments. So the compiler refuses
rather than picking one.
Two filters satisfy the rule, | js and | json. | unsafe_js does not.
Choosing between | js and | json
One question decides it:
Did you write quotes around the slot?
Yes: use | js
The value goes inside a string literal you wrote.
| js backslash-escapes
the JavaScript metacharacters and adds no quotes of its own.No: use | json
The value stands alone in code position.
| json emits a complete,
self-delimiting JSON literal, so a string arrives with its own quotes.| json also
handles values that are not strings, which | js does not:
Using | js in code position
| js in code position compiles, because the filter is present and that is all
the script-body rule checks. What it emits is a bare, unquoted value:
var n = ${ product.id | js } renders var n = 42; and
works fine right up until the field is a string.
A JavaScript template literal cannot be written in a script body
Slurp claims the${ sequence itself, so there is no way to write a backtick
template in a script body. The compiler says so as part of the same error:
let t = hours + "h";.
Passing a whole object to the page
For anything larger than a value or two, do not build JavaScript at all. Emit a JSON payload and parse it:cart.js:
.js file where an editor and a linter can see
it, and the template generates only data. {$let name = expr}
does the same thing with slurp’s own spelling, emitting a
<script type="application/json" data-slurp-let="name"> payload for the browser
runtime to pick up.
JavaScript-evaluated attributes
An attribute counts as JavaScript-evaluated if it is a nativeon* handler or
one of the Alpine forms. The full set:
- anything starting with
@orx-on: - anything starting with
:orx-bind: - any name longer than two characters starting with
on x-datax-initx-effectx-showx-textx-htmlx-ifx-forx-model
x-model.lazy is recognised.
Here the compiler does not refuse, because it can read the author’s own text
around the slot and work out where the value lands. Position decides the
filter, and the same question from the last section answers it:
| js is a claim about context, and a declared slot is then passed through
verbatim. A false claim turns the automatic escaper off and puts nothing in its
place, so it is refused rather than ignored.
Omitting the filter
An undeclared slot in a JavaScript-evaluated attribute is not an error. The compiler escapes it for the position it found:');alert(1);(') neutralised inside the string it
was written into. The other two are the ordinary numeric case. Leaving the
filter off stays legal because comparing against a count or a loop index is a
normal thing to write and cannot be proved safe or unsafe statically.
In development mode the renderer records an advisory on every undeclared slot,
naming the attribute and the filter that would state the intent. It is silent in
slurp build, which always renders in production mode.
| unsafe_js is the escape hatch for the opposite case: JavaScript the theme
itself wrote, passed in as a prop and composed into a handler. Escaping that
would corrupt working code. It means “this is code, not data”, so never route
a value from outside the theme through it.A style value loses its punctuation
There is no useful escape for CSS, so a slot interpolated into style,
:style or x-bind:style has the structural characters removed:
; { } ( ) " ' \ @ * < > and NUL.
path set to a);color:red;x:url(b:
: and / are kept, because neither can open a new declaration on its own and
both are needed by the background:url(...) shape.
The consequence for ordinary use is that a value carrying legitimate punctuation
arrives mangled. A CSS custom property set from a schema color setting is
fine; anything that needs a function call, a quoted font name or a semicolon
belongs in a class instead. Development mode warns when the strip actually
changed a value.
Two silent failures
JavaScript template literals in attributes
${ } in attribute values itself, so this slot is evaluated
against the server render context rather than the browser’s. If item is a
client-side variable it resolves to nothing and the link renders as
`/product/`, with no runtime error. The page looks built and carries dead
links.
This is diagnosed as a JsTemplateLiteralInAttribute warning, which slurp validate --warnings and slurp build -v print. It is a warning rather than an
error because interpolating a genuine server value inside backticks is legal,
and the compiler cannot tell the two apart.
A nested double quote ends the attribute
What catches what
Next
How escaping works
Every escaping context, and the unescaped forms.
Security model
The threat model behind these rules, with the enforcing symbol named for
each one.