#4119·prism

Removing $placeholder changes how an unquoted template attribute value, attribute name, or tag name parses (13 languages)

Author: DmitrySharabinCreated Sep 17, 2026Updated Sep 17, 2026

#4110 removed $placeholder: template tokens no longer leave an identifier-like stand-in in the code handed to the host grammar, they simply vanish. Its description records the consequence for three fixtures; this issue is to track it, since nothing else does.

Where a template expression is an entire unquoted value, the host grammar no longer sees a value there and stops parsing the construct around it. Quoted values are unaffected.

Repro

javascript
import { createInstance } from './tests/helper/prism-loader.js';

const prism = await createInstance(['php', 'markup', 'javascript', 'clike', 'css']);

console.log(prism.highlight('<span data-x=<?= $x ?>>a</span>', 'php'));
v2   [tag [punctuation <][tag span] [attr-name data-x][attr-value [punctuation =][php …]][punctuation >]]
new  &lt;span data-x=[php …]>a…                                  ← the whole opening tag is no longer a tag

Scope, measured

All 13 markup-templating languages — django, twig, handlebars, liquid, ejs, erb, etlua, tt2, smarty, ftl, latte, soy, php — in three positions:

position example severity
unquoted attribute value <span data-x={{x}}> the whole opening tag stops being a tag
tag name <h{$level}> tag survives, name is just h
unquoted attribute name <div {{on "click"}}> tag survives, loses the attr-name wrapper

Quoted values (href="{{url}}") are identical to v2.

Of these, only the first loses a whole construct. latte and soy's fixtures changed in #4110 because their stripped text still parses; handlebars' fixture shows the tag being lost.

Not worth chasing

A template expression between JS regex delimiters — <script>var re = /{{p}}/;</script> — collapses to // and comments out the rest of that line. Same root cause, but nobody writes that: interpolating into a regex literal breaks on the first / or backslash in the value, so real code uses new RegExp('{{p}}'). Checked against a corpus of realistic template-in-script snippets (JSON blob, quoted string, bare number, condition, CSS value) — all byte-identical to v2.

Why it is a reasonable trade

Placeholders caused their own class of bugs, found in #4107: the stand-in being parsed as markdown emphasis, as a JS constant, or split in two by the host grammar. This is recorded for tracking, not to argue #4110 should be reverted.