Conversation
Meta-languages now declare *which parts* of their token tree are code in
the inner language, instead of a custom tokenizer or placeholder tricks:
$inner: { select: ['unchanged, deleted, diff', 'unchanged, inserted, diff'] }
A selector names containers (token names, matched by type or alias, and
`:text` for the unmatched text). The strings directly inside the selected
containers are concatenated, highlighted as one whole with the inner
language, and the tokens are put back. Several selectors are highlighted
separately, later ones winning where they overlap.
- `diff` uses two selectors, the versions before and after the change, so
multi-line constructs are highlighted correctly across changed and
unchanged lines. Its grammar is back to a plain declaration.
- The default selector is `:text`, which is what templating languages
need; nothing changes for them. Placeholders are gone: non-selected
tokens simply vanish from the highlighted document, and inner tokens
that span them stay whole with the tokens inserted by position.
- `src/util/token-stream.js` exports the two primitives this is built on,
`splitTokenStream()` and `insertTokens()`, for reuse by everything else
that merges one tokenized structure with another (keep-markup,
command-line, doc-comment prefixes, …).
- `$placeholder` and the `inner` grammar option are removed; `templating()`
is replaced by `src/core/tokenize/embed.js`.
Three expectations change where a template expression was an entire
unquoted attribute value or tag name (handlebars, latte, soy): markup no
longer sees a placeholder there.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbhmLXUybTbxCBSwGa2YZr
✅ Deploy Preview for dev-prismjs-com ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
`isInnerSpec` keyed on `select` alone, so `$inner: { language: X }` — the
long form that `types.d.ts` documents a bare grammar reference as shorthand
for — was read as an inline grammar and crashed in `matchPattern`. Decide on
the key set instead, keeping the `select` type check so a one-token grammar
named `select` still works inline.
A selector naming no container (`''`, `' , '`, `[]`) silently disabled inner
highlighting. Throw instead, above the `!inner` early exit, so the same
malformed grammar fails the same way with or without an inner language.
Follow-up to #4110, whose code both bugs are in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Two corrections to the description, found while reviewing this PR together with #4112 and #4114. Both are about the text, not the code.
There is a third position, and two of the three changed fixtures in this PR are examples of it: an unquoted attribute name. // django
'<div {{ x }}>a</div>'
v2 [tag [punctuation <][tag div] [attr-name [django …]][punctuation >]]
new [tag [punctuation <][tag div] [django …][punctuation >]]
The three positions differ in severity, which the one-line summary flattens:
Only the first loses a whole construct. Quoted values are unaffected, as the description says. Measured across every markup-templating language — django, twig, handlebars, liquid, ejs, erb, etlua, tt2, smarty, ftl, latte, soy, php — all 13 change in all three positions. Filed as #4119 so it is tracked somewhere other than this description; it is recorded there as an accepted trade against the #4107 placeholder bugs, not as an argument to revert.
This does not reproduce. The repo pins TypeScript 5.8.3 ( |
`isInnerSpec` keyed on `select` alone, so `$inner: { language: X }` — the
long form that `types.d.ts` documents a bare grammar reference as shorthand
for — was read as an inline grammar and crashed in `matchPattern`. Decide on
the key set instead, keeping the `select` type check so a one-token grammar
named `select` still works inline.
A selector naming no container (`''`, `' , '`, `[]`) silently disabled inner
highlighting. Throw instead, above the `!inner` early exit, so the same
malformed grammar fails the same way with or without an inner language.
Follow-up to #4110, whose code both bugs are in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`container.splice(start, n, ...segment)` in `embed` and `stream.splice(i, 1, ...parts)` in `insertTokens` pass every item as a call argument, which overflows the stack once a document has more inner tokens than the engine takes arguments — about 460 KB of markup through any templating language, where `v2` was fine. Both now go through `replaceRange()`, which truncates and pushes instead. A root-level `ignore` token is dissolved into the `:text` run, but the walk then took it again, so a selector naming both `:text` and that token fed its text to the inner language twice, and the second copy changed how the first one parsed. Also in `token-stream.js`: the two halves of a split token shared the original's alias array, so `addAlias` on one reached the other and the original; `insertTokens` silently sliced text away when offsets were not ascending, and now throws; and `splitTokenStream` no longer claims an isolation it does not give — segments share their unsplit tokens with the input. Tests for each, plus the `diff` behaviours nothing pinned: two selectors rather than one, the normal-diff `<`/`>` blocks, and the single-alias branch of `tokenMatches`. Follow-up to #4110 and #4112, whose code these are in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Split token streams in one pass `splitTokenStream` copied the remainder of the stream for every offset (`stream.slice(i)`, `[b, ...stream.slice(i + 1)]`), and `embed` passes one offset per selected run, i.e. one per diff line. A 12k-line `diff:javascript` took 3197ms; `--cpu-prof` put 80% of samples in `splitAt`. Walk the stream once instead, carrying the straddling item into the next segment. `diff:javascript`, this branch vs its base: 3k lines 189ms -> 103ms, 12k lines 3197ms -> 307ms, 48k lines 1290ms, i.e. linear again. Two details the one-pass form has to preserve, both easy to get wrong and both covered by new tests: - A zero-length item sitting exactly on an offset belongs to the segment that starts there, not the one that ends there. `splitAt` got this from checking `pos >= offset` before consuming the item; without the `offsets[next] === start` clause, `diff:markdown` drops the empty `code-block` token that markdown emits for an empty fenced block. - With no offsets there is nothing to split. That is the common path — `:text` is the default selector, so every templating language takes it — so it must not walk the stream measuring items. 200k random streams split both ways against the previous implementation: no divergences. `splitTokenStream` and `insertTokens` are public API via `shared.js` and had no direct tests; add them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Re-point the #2008 test at what it now guards The test's note described a placeholder that no longer exists, so it could no longer fail for the reason it was written. The input still covers the embedding it was about: the CSS around a PHP block in a `style` attribute. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`isInnerSpec` keyed on `select` alone, so `$inner: { language: X }` — the
long form that `types.d.ts` documents a bare grammar reference as shorthand
for — was read as an inline grammar and crashed in `matchPattern`. Decide on
the key set instead, keeping the `select` type check so a one-token grammar
named `select` still works inline.
A selector naming no container (`''`, `' , '`, `[]`) silently disabled inner
highlighting. Throw instead, above the `!inner` early exit, so the same
malformed grammar fails the same way with or without an inner language.
Follow-up to #4110, whose code both bugs are in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to #4105 / #4107, per the review discussion there: meta-languages now declare which parts of their token tree are code in the inner language, instead of a custom tokenizer or placeholder tricks.
Selectors
$inneris either a grammar reference (as before) or{ language?, select? }. A selector names containers, separated by commas: token names (matched by type or alias, since both become classes, sodeletedcoversdeleted-signanddeleted-arrow) and:textfor the unmatched text. The plain strings directly inside the selected containers are concatenated in document order, tokenized as one whole with the inner language, and the tokens are put back where the strings were.:text, which is exactly what templating languages need, so they are unchanged (inner: markupand nothing else).'foo, :text'highlights foo's content together with the unmatched text as one document;['foo, :text', 'bar, :text']does that twice, separately, later selectors winning where they overlap.:textis a pseudo-name becausetextis already a token in pug, haml, mermaid, plant-uml, textile and inform7; token names can never contain:.diff is now fully declarative (66 lines, the original grammar plus the
$innerline) and better than the old plugin: the two selectors are the before and after versions of the file, so a comment opened on an unchanged line and closed after a change is highlighted correctly on the deleted and the inserted lines.tests/core/embedding.jscovers that; the five existing snapshots (diff:javascript,django:css,diff:django:css,php:none, …) are byte-identical tov2.No placeholders
Non-selected tokens simply vanish from the highlighted document. That removes
$placeholder, the collision handling, and the class of bugs #4107 found (the placeholder being markdown emphasis / a JS constant / split byfalse). Three expectations change, all the same case: a template expression that is an entire unquoted attribute value or tag name (<span data-has-name={{x}}>,<h{$level}>) is no longer parsed as a tag by markup. Quoted values (href="{{url}}") are unaffected.The helper
src/util/token-stream.js(exported fromshared.js) has the two primitives this is built on, for everything else in the repo that merges one tokenized structure with another:splitTokenStream(stream, offsets)— split at text offsets, cloning tokens that straddle a boundary.insertTokens(stream, [[offset, token], …])— insert tokens by position, nesting into spanning tokens.Consumers to migrate are listed in the follow-up issue (keep-markup's DOM
Rangejuggling, command-line's line stashing on the HTML string, shell-session, the////*doc-comment prefixes in xml-doc/javadoclike/jsdoc, markdown blockquotes, the per-stringtokenizeStringsplugins, jsx/xquery's plain-text flattening).Removed
templating()(replaced bysrc/core/tokenize/embed.js),$placeholder, theinnergrammar option (nothing needs it: php's<?check now just re-tokenizes withwithoutTokenize(grammar)).Full suite green, lint clean,
tscclean (with--moduleResolution bundler, since the checked-in config is broken by TS 6).🤖 Generated with Claude Code
https://claude.ai/code/session_01RbhmLXUybTbxCBSwGa2YZr
Generated by Claude Code