Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,9 @@ One `SyntaxLang` lexer per language, registered via `add_lang`:
(`xml`/`ssml`/`atom`/`rss`: plain tag scanning), one shared scanner
parameterized by `MarkupLexMode`
- `lexer_svelte.ts` - Svelte: the markup scanner in svelte mode (script→ts,
no special attrs) plus the `{…}` expression lexer (blocks, each/await
splits, at-directives, directive modifiers)
no special attrs, js-style comments between attributes) plus the `{…}`
expression lexer (blocks, each/await splits, at-directives, directive
modifiers, `{const …}`/`{let …}` declaration tags)
- `lexer_md.ts` - Markdown: line-oriented block scan (fences with exact-word
info matching and any-length closers, headings, blockquotes, lists, hr)
with a per-block inline scan (emphasis, inline code, links, entities, raw
Expand Down
28 changes: 28 additions & 0 deletions src/lib/lexer_markup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
is_hex_digit,
is_space,
matches_ci,
scan_to_line_end,
skip_space,
token_type,
type Lexer,
Expand Down Expand Up @@ -91,6 +92,12 @@ export interface MarkupLexMode {
* `style=`/`on*=` attribute embedding (html only).
*/
special_attrs: boolean;
/**
* JS-style line and block comments between attributes emit `comment` leaves
* (svelte only — in html/xml a `//` inside a tag is ordinary bogus attribute
* text, not a comment).
*/
attr_comments: boolean;
/**
* Lexes a `{…}` expression at `from`, returning the position after it —
* the svelte hook, null for html/xml. `full` enables block/each forms
Expand All @@ -108,6 +115,7 @@ export const MARKUP_MODE_HTML: MarkupLexMode = {
script_container: T_LANG_JS,
rcdata: true,
special_attrs: true,
attr_comments: false,
lex_expression: null,
};

Expand All @@ -116,6 +124,7 @@ const MODE_XML: MarkupLexMode = {
script_container: 0,
rcdata: false,
special_attrs: false,
attr_comments: false,
lex_expression: null,
};

Expand Down Expand Up @@ -366,6 +375,25 @@ const lex_markup_tag = (
tag_end = j + 2;
break;
}
if (mode.attr_comments && c === 47 && j + 1 < end) {
// js-style comment between attributes (svelte); a `//` line comment
// runs to the newline, a block comment to its closer, and either
// extends to the window end when unterminated (editor-style)
const c1 = text.charCodeAt(j + 1);
if (c1 === 47) {
const line_end = scan_to_line_end(text, j, end);
l.leaf(T_COMMENT, j, line_end);
j = line_end;
continue;
}
if (c1 === 42) {
const close = text.indexOf('*/', j + 2);
const comment_end = close === -1 || close + 2 > end ? end : close + 2;
l.leaf(T_COMMENT, j, comment_end);
j = comment_end;
continue;
}
}
if (c === 47 || c === 61) {
// stray `/` or `=` — plain text inside the tag
j++;
Expand Down
39 changes: 33 additions & 6 deletions src/lib/lexer_svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import {lex_markup_window, type MarkupLexMode} from './lexer_markup.ts';
* in svelte mode plus the `{…}` expression lexer.
*
* Emits markup's tag/attr/comment/entity structure (no `special_attr` —
* svelte handles `style=`/`on*=` as ordinary attributes), `script`+`lang_ts`
* svelte handles `style=`/`on*=` as ordinary attributes; js-style line and
* block comments between attributes emit `comment` leaves), `script`+`lang_ts`
* and `style`+`lang_css` regions (svelte scripts are always TypeScript), and
* `svelte_expression` containers whose forms nest a same-span `block`
* (`{#if}`/`{:else if}`/`{/each}`/…), `each` (`{#each list as item}`), or
* `at_directive` (`{@render}`/`{@const}`/…) container with `special_keyword`/
* `at_keyword`/`keyword` leaves and `lang_ts` interiors.
* (`{#if}`/`{:else if}`/`{/each}`/…), `each` (`{#each list as item}`),
* `at_directive` (`{@render}`/`{@const}`/…), or `declaration_tag`
* (`{const …}`/`{let …}`) container with `special_keyword`/`at_keyword`/
* `keyword` leaves and `lang_ts` interiors.
*
* Notable behavior: expression braces balance to any depth and skip
* strings/templates/comments (`{'}'}` works); expression-valued attributes
Expand All @@ -38,6 +40,7 @@ import {lex_markup_window, type MarkupLexMode} from './lexer_markup.ts';

const T_SVELTE_EXPRESSION = token_type('svelte_expression');
const T_AT_DIRECTIVE = token_type('at_directive');
const T_DECLARATION_TAG = token_type('declaration_tag');
const T_BLOCK = token_type('block');
const T_EACH = token_type('each');
const T_AT_KEYWORD = token_type('at_keyword');
Expand Down Expand Up @@ -105,10 +108,21 @@ const find_top_level_word = (text: string, from: number, to: number, word: strin
return -1;
};

/**
* Length of a `const`/`let` declaration-tag keyword at `i` (with an identifier
* boundary after it), or 0 — the `{const …}`/`{let …}` recognizer. These are
* sigil-less declaration tags, the modern replacement for `{@const …}`.
*/
const declaration_keyword_len = (text: string, i: number, end: number): number => {
if (i + 5 <= end && text.startsWith('const', i) && !is_ident(text.charCodeAt(i + 5))) return 5;
if (i + 3 <= end && text.startsWith('let', i) && !is_ident(text.charCodeAt(i + 3))) return 3;
return 0;
};

/**
* Lexes a `{…}` expression at `from`, returning the position after it.
* `full` enables the block/each forms (top level); tag and attribute
* contexts pass false (at-directives like `{@attach …}` work in both).
* `full` enables the block/each and declaration-tag forms (top level); tag and
* attribute contexts pass false (at-directives like `{@attach …}` work in both).
*/
const lex_svelte_expression = (l: Lexer, from: number, end: number, full: boolean): number => {
const {text} = l;
Expand All @@ -119,6 +133,9 @@ const lex_svelte_expression = (l: Lexer, from: number, end: number, full: boolea

l.open(T_SVELTE_EXPRESSION, from);
const sigil = from + 1 < end ? text.charCodeAt(from + 1) : 0;
// `{const …}`/`{let …}` — only at top level; never collides with the
// `@`/`#`/`:`/`/` sigil forms since those first chars can't spell a keyword
const decl_len = full ? declaration_keyword_len(text, from + 1, inner_end) : 0;
if (sigil === 64 && is_ident(text.charCodeAt(from + 2))) {
// {@word …}
let word_end = from + 2;
Expand Down Expand Up @@ -178,6 +195,15 @@ const lex_svelte_expression = (l: Lexer, from: number, end: number, full: boolea
if (closed) l.leaf(T_PUNCTUATION, close, expr_end);
l.close(expr_end);
}
} else if (decl_len !== 0) {
// {const …} / {let …} — the keyword leads, the rest is a ts interior
const kw_end = from + 1 + decl_len;
l.open(T_DECLARATION_TAG, from);
l.leaf(T_PUNCTUATION, from, from + 1);
l.leaf(T_KEYWORD, from + 1, kw_end);
lex_ts_interior(l, kw_end, inner_end);
if (closed) l.leaf(T_PUNCTUATION, close, expr_end);
l.close(expr_end);
} else {
lex_svelte_expression_plain(l, from, inner_end, close, expr_end, closed);
}
Expand All @@ -203,6 +229,7 @@ const SVELTE_MODE: MarkupLexMode = {
script_container: T_LANG_TS,
rcdata: false,
special_attrs: false,
attr_comments: true,
lex_expression: lex_svelte_expression,
};

Expand Down
13 changes: 11 additions & 2 deletions src/routes/samples/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,11 +333,20 @@ export const complex_regex = /^(?:\\/\\*.*?\\*\\/|\\/\\/.*|[^/])+$/;

{#each thing_keys as [k, { t, u }] (f(k))}
{@const v = Math.round(t[k + f(u)])}
{const doubled = v * 2}
{let label = \`\${k}: \${doubled}\`}
{f(v)}
{label}
{/each}

{#if f(c)}
<Thing string_prop="a {f('s')} b" number_prop={f(1)} />
<Thing
string_prop="a {f('s')} b"
// interpolated string prop
/* number prop is
computed at runtime */
number_prop={f(1)}
/>
{:else if f(a > 0)}
bigger
{:else}
Expand All @@ -349,7 +358,7 @@ export const complex_regex = /^(?:\\/\\*.*?\\*\\/|\\/\\/.*|[^/])+$/;
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html '<strong>raw html</strong>'}

<input bind:value type="text" class:active={c} />
<input bind:value type="text" /* two-way bound */ class:active={c} />

<span {@attach attachment('param', f(42))}>...</span>

Expand Down
13 changes: 11 additions & 2 deletions src/test/fixtures/generated/svelte/svelte_complex.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,20 @@

<span class="token_svelte_expression"><span class="token_each"><span class="token_punctuation">{</span><span class="token_special_keyword">#each</span> <span class="token_lang_ts">thing_keys</span> <span class="token_keyword">as</span> <span class="token_lang_ts"><span class="token_punctuation">[</span>k<span class="token_punctuation">,</span> <span class="token_punctuation">{</span> t<span class="token_punctuation">,</span> u <span class="token_punctuation">}]</span> <span class="token_punctuation">(</span><span class="token_function">f</span><span class="token_punctuation">(</span>k<span class="token_punctuation">))</span></span><span class="token_punctuation">}</span></span></span>
<span class="token_svelte_expression"><span class="token_at_directive"><span class="token_punctuation">{</span><span class="token_at_keyword">@const</span> <span class="token_lang_ts">v <span class="token_operator">=</span> <span class="token_capitalized_identifier token_class_name">Math</span><span class="token_punctuation">.</span><span class="token_function">round</span><span class="token_punctuation">(</span>t<span class="token_punctuation">[</span>k <span class="token_operator">+</span> <span class="token_function">f</span><span class="token_punctuation">(</span>u<span class="token_punctuation">)])</span></span><span class="token_punctuation">}</span></span></span>
<span class="token_svelte_expression"><span class="token_declaration_tag"><span class="token_punctuation">{</span><span class="token_keyword">const</span> <span class="token_lang_ts">doubled <span class="token_operator">=</span> v <span class="token_operator">*</span> <span class="token_number">2</span></span><span class="token_punctuation">}</span></span></span>
<span class="token_svelte_expression"><span class="token_declaration_tag"><span class="token_punctuation">{</span><span class="token_keyword">let</span> <span class="token_lang_ts">label <span class="token_operator">=</span> <span class="token_template_string"><span class="token_template_punctuation token_string">`</span><span class="token_interpolation"><span class="token_interpolation_punctuation token_punctuation">${</span>k<span class="token_interpolation_punctuation token_punctuation">}</span></span><span class="token_string">: </span><span class="token_interpolation"><span class="token_interpolation_punctuation token_punctuation">${</span>doubled<span class="token_interpolation_punctuation token_punctuation">}</span></span><span class="token_template_punctuation token_string">`</span></span></span><span class="token_punctuation">}</span></span></span>
<span class="token_svelte_expression"><span class="token_punctuation">{</span><span class="token_lang_ts"><span class="token_function">f</span><span class="token_punctuation">(</span>v<span class="token_punctuation">)</span></span><span class="token_punctuation">}</span></span>
<span class="token_svelte_expression"><span class="token_punctuation">{</span><span class="token_lang_ts">label</span><span class="token_punctuation">}</span></span>
<span class="token_svelte_expression"><span class="token_block"><span class="token_punctuation">{</span><span class="token_special_keyword">/each</span><span class="token_punctuation">}</span></span></span>

<span class="token_svelte_expression"><span class="token_block"><span class="token_punctuation">{</span><span class="token_special_keyword">#if</span> <span class="token_lang_ts"><span class="token_function">f</span><span class="token_punctuation">(</span>c<span class="token_punctuation">)</span></span><span class="token_punctuation">}</span></span></span>
<span class="token_tag"><span class="token_tag"><span class="token_punctuation token_tag_punctuation">&lt;</span>Thing</span> <span class="token_attr_name">string_prop</span><span class="token_attr_value"><span class="token_punctuation token_attr_equals">=</span><span class="token_punctuation token_attr_quote">"</span>a <span class="token_svelte_expression"><span class="token_punctuation">{</span><span class="token_lang_ts"><span class="token_function">f</span><span class="token_punctuation">(</span><span class="token_string">'s'</span><span class="token_punctuation">)</span></span><span class="token_punctuation">}</span></span> b<span class="token_punctuation token_attr_quote">"</span></span> <span class="token_attr_name">number_prop</span><span class="token_attr_value"><span class="token_punctuation token_attr_equals">=</span><span class="token_svelte_expression"><span class="token_punctuation">{</span><span class="token_lang_ts"><span class="token_function">f</span><span class="token_punctuation">(</span><span class="token_number">1</span><span class="token_punctuation">)</span></span><span class="token_punctuation">}</span></span></span> <span class="token_punctuation token_tag_punctuation">/></span></span>
<span class="token_tag"><span class="token_tag"><span class="token_punctuation token_tag_punctuation">&lt;</span>Thing</span>
<span class="token_attr_name">string_prop</span><span class="token_attr_value"><span class="token_punctuation token_attr_equals">=</span><span class="token_punctuation token_attr_quote">"</span>a <span class="token_svelte_expression"><span class="token_punctuation">{</span><span class="token_lang_ts"><span class="token_function">f</span><span class="token_punctuation">(</span><span class="token_string">'s'</span><span class="token_punctuation">)</span></span><span class="token_punctuation">}</span></span> b<span class="token_punctuation token_attr_quote">"</span></span>
<span class="token_comment">// interpolated string prop</span>
<span class="token_comment">/* number prop is
computed at runtime */</span>
<span class="token_attr_name">number_prop</span><span class="token_attr_value"><span class="token_punctuation token_attr_equals">=</span><span class="token_svelte_expression"><span class="token_punctuation">{</span><span class="token_lang_ts"><span class="token_function">f</span><span class="token_punctuation">(</span><span class="token_number">1</span><span class="token_punctuation">)</span></span><span class="token_punctuation">}</span></span></span>
<span class="token_punctuation token_tag_punctuation">/></span></span>
<span class="token_svelte_expression"><span class="token_block"><span class="token_punctuation">{</span><span class="token_special_keyword">:else if</span> <span class="token_lang_ts"><span class="token_function">f</span><span class="token_punctuation">(</span>a <span class="token_operator">></span> <span class="token_number">0</span><span class="token_punctuation">)</span></span><span class="token_punctuation">}</span></span></span>
bigger
<span class="token_svelte_expression"><span class="token_block"><span class="token_punctuation">{</span><span class="token_special_keyword">:else</span><span class="token_punctuation">}</span></span></span>
Expand All @@ -59,7 +68,7 @@
<span class="token_comment">&lt;!-- eslint-disable-next-line svelte/no-at-html-tags --></span>
<span class="token_svelte_expression"><span class="token_at_directive"><span class="token_punctuation">{</span><span class="token_at_keyword">@html</span> <span class="token_lang_ts"><span class="token_string">'&lt;strong>raw html&lt;/strong>'</span></span><span class="token_punctuation">}</span></span></span>

<span class="token_tag"><span class="token_tag"><span class="token_punctuation token_tag_punctuation">&lt;</span>input</span> <span class="token_attr_name"><span class="token_namespace">bind:</span>value</span> <span class="token_attr_name">type</span><span class="token_attr_value"><span class="token_punctuation token_attr_equals">=</span><span class="token_punctuation token_attr_quote">"</span>text<span class="token_punctuation token_attr_quote">"</span></span> <span class="token_attr_name"><span class="token_namespace">class:</span>active</span><span class="token_attr_value"><span class="token_punctuation token_attr_equals">=</span><span class="token_svelte_expression"><span class="token_punctuation">{</span><span class="token_lang_ts">c</span><span class="token_punctuation">}</span></span></span> <span class="token_punctuation token_tag_punctuation">/></span></span>
<span class="token_tag"><span class="token_tag"><span class="token_punctuation token_tag_punctuation">&lt;</span>input</span> <span class="token_attr_name"><span class="token_namespace">bind:</span>value</span> <span class="token_attr_name">type</span><span class="token_attr_value"><span class="token_punctuation token_attr_equals">=</span><span class="token_punctuation token_attr_quote">"</span>text<span class="token_punctuation token_attr_quote">"</span></span> <span class="token_comment">/* two-way bound */</span> <span class="token_attr_name"><span class="token_namespace">class:</span>active</span><span class="token_attr_value"><span class="token_punctuation token_attr_equals">=</span><span class="token_svelte_expression"><span class="token_punctuation">{</span><span class="token_lang_ts">c</span><span class="token_punctuation">}</span></span></span> <span class="token_punctuation token_tag_punctuation">/></span></span>

<span class="token_tag"><span class="token_tag"><span class="token_punctuation token_tag_punctuation">&lt;</span>span</span> <span class="token_svelte_expression"><span class="token_at_directive"><span class="token_punctuation">{</span><span class="token_at_keyword">@attach</span> <span class="token_lang_ts"><span class="token_function">attachment</span><span class="token_punctuation">(</span><span class="token_string">'param'</span><span class="token_punctuation">,</span> <span class="token_function">f</span><span class="token_punctuation">(</span><span class="token_number">42</span><span class="token_punctuation">))</span></span><span class="token_punctuation">}</span></span></span><span class="token_punctuation token_tag_punctuation">></span></span>...<span class="token_tag"><span class="token_tag"><span class="token_punctuation token_tag_punctuation">&lt;/</span>span</span><span class="token_punctuation token_tag_punctuation">></span></span>

Expand Down
Loading