diff --git a/CLAUDE.md b/CLAUDE.md
index f1fba96a..a07283c3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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
diff --git a/src/lib/lexer_markup.ts b/src/lib/lexer_markup.ts
index 47cbb1f0..8121eb8c 100644
--- a/src/lib/lexer_markup.ts
+++ b/src/lib/lexer_markup.ts
@@ -5,6 +5,7 @@ import {
is_hex_digit,
is_space,
matches_ci,
+ scan_to_line_end,
skip_space,
token_type,
type Lexer,
@@ -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
@@ -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,
};
@@ -116,6 +124,7 @@ const MODE_XML: MarkupLexMode = {
script_container: 0,
rcdata: false,
special_attrs: false,
+ attr_comments: false,
lex_expression: null,
};
@@ -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++;
diff --git a/src/lib/lexer_svelte.ts b/src/lib/lexer_svelte.ts
index afdfe825..3ed6f10a 100644
--- a/src/lib/lexer_svelte.ts
+++ b/src/lib/lexer_svelte.ts
@@ -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
@@ -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');
@@ -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;
@@ -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;
@@ -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);
}
@@ -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,
};
diff --git a/src/routes/samples/all.ts b/src/routes/samples/all.ts
index 374722b8..f2994874 100644
--- a/src/routes/samples/all.ts
+++ b/src/routes/samples/all.ts
@@ -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)}
-
+
{:else if f(a > 0)}
bigger
{:else}
@@ -349,7 +358,7 @@ export const complex_regex = /^(?:\\/\\*.*?\\*\\/|\\/\\/.*|[^/])+$/;
{@html 'raw html '}
-
+
...
diff --git a/src/test/fixtures/generated/svelte/svelte_complex.html b/src/test/fixtures/generated/svelte/svelte_complex.html
index 260d403f..07c09bf1 100644
--- a/src/test/fixtures/generated/svelte/svelte_complex.html
+++ b/src/test/fixtures/generated/svelte/svelte_complex.html
@@ -43,11 +43,20 @@
{ #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"
+
+
+ number_prop = { f ( 1 ) }
+ />
{ :else if f ( a > 0 ) }
bigger
{ :else }
@@ -59,7 +68,7 @@
{ @html '<strong>raw html</strong>' }
-< input bind: value type = " text" class: active= { c } />
+< input bind: value type = " text" class: active= { c } />
< span { @attach attachment ( 'param' , f ( 42 )) } > ...</ span>
diff --git a/src/test/fixtures/generated/svelte/svelte_complex.txt b/src/test/fixtures/generated/svelte/svelte_complex.txt
index 8f062ad0..11b2215d 100644
--- a/src/test/fixtures/generated/svelte/svelte_complex.txt
+++ b/src/test/fixtures/generated/svelte/svelte_complex.txt
@@ -1,33 +1,37 @@
=== STATS ===
-Sample length: 2348 characters
-Total tokens: 647
+Sample length: 2517 characters
+Total tokens: 680
-Token types (34 unique):
- punctuation: 297
+Token types (39 unique):
+ punctuation: 303
tag: 78
- operator: 37
- lang_ts: 25
- svelte_expression: 25
+ operator: 40
+ lang_ts: 28
+ svelte_expression: 28
function: 24
attr_name: 19
attr_value: 16
special_keyword: 15
- keyword: 13
- string: 11
+ keyword: 15
+ string: 12
+ comment: 10
builtin: 10
capitalized_identifier: 8
property: 8
- comment: 7
block: 7
selector: 7
+ number: 6
at_directive: 6
at_keyword: 6
- number: 5
+ interpolation_punctuation: 4
script: 3
namespace: 3
constant: 2
boolean: 2
function_variable: 2
+ declaration_tag: 2
+ template_punctuation: 2
+ interpolation: 2
style: 2
lang_css: 2
import_type_keyword: 1
@@ -35,6 +39,7 @@ Token types (34 unique):
:: 1
type: 1
each: 1
+ template_string: 1
atrule: 1
rule: 1
@@ -255,434 +260,467 @@ Token types (34 unique):
849-850 punctuation (
851-854 punctuation )])
854-855 punctuation }
- 857-863 svelte_expression {f(v)}
+ 857-880 svelte_expression {const doubled = v * 2}
+ 857-880 declaration_tag {const doubled = v * 2}
857-858 punctuation {
- 858-862 lang_ts f(v)
- 858-859 function f
- 859-860 punctuation (
- 861-862 punctuation )
- 862-863 punctuation }
- 864-871 svelte_expression {/each}
- 864-871 block {/each}
- 864-865 punctuation {
- 865-870 special_keyword /each
- 870-871 punctuation }
- 873-883 svelte_expression {#if f(c)}
- 873-883 block {#if f(c)}
- 873-874 punctuation {
- 874-877 special_keyword #if
- 878-882 lang_ts f(c)
- 878-879 function f
- 879-880 punctuation (
- 881-882 punctuation )
- 882-883 punctuation }
- 885-940 tag
- 885-891 tag
- 941-960 svelte_expression {:else if f(a > 0)}
- 941-960 block {:else if f(a > 0)}
+ 858-863 keyword const
+ 864-879 lang_ts doubled = v * 2
+ 872-873 operator =
+ 876-877 operator *
+ 878-879 number 2
+ 879-880 punctuation }
+ 882-914 svelte_expression {let label = `${k}: ${doubled}`}
+ 882-914 declaration_tag {let label = `${k}: ${doubled}`}
+ 882-883 punctuation {
+ 883-886 keyword let
+ 887-913 lang_ts label = `${k}: ${doubled}`
+ 893-894 operator =
+ 895-913 template_string `${k}: ${doubled}`
+ 895-896 template_punctuation `
+ 896-900 interpolation ${k}
+ 896-898 interpolation_punctuation ${
+ 899-900 interpolation_punctuation }
+ 900-902 string :
+ 902-912 interpolation ${doubled}
+ 902-904 interpolation_punctuation ${
+ 911-912 interpolation_punctuation }
+ 912-913 template_punctuation `
+ 913-914 punctuation }
+ 916-922 svelte_expression {f(v)}
+ 916-917 punctuation {
+ 917-921 lang_ts f(v)
+ 917-918 function f
+ 918-919 punctuation (
+ 920-921 punctuation )
+ 921-922 punctuation }
+ 924-931 svelte_expression {label}
+ 924-925 punctuation {
+ 925-930 lang_ts label
+ 930-931 punctuation }
+ 932-939 svelte_expression {/each}
+ 932-939 block {/each}
+ 932-933 punctuation {
+ 933-938 special_keyword /each
+ 938-939 punctuation }
+ 941-951 svelte_expression {#if f(c)}
+ 941-951 block {#if f(c)}
941-942 punctuation {
- 942-950 special_keyword :else if
- 951-959 lang_ts f(a > 0)
- 951-952 function f
- 952-953 punctuation (
- 955-956 operator >
- 957-958 number 0
- 958-959 punctuation )
- 959-960 punctuation }
- 969-976 svelte_expression {:else}
- 969-976 block {:else}
- 969-970 punctuation {
- 970-975 special_keyword :else
- 975-976 punctuation }
- 978-1029 tag (c = f(!c))}>
- 978-984 tag (c = f(!c))}
-1008-1009 punctuation =
-1009-1028 svelte_expression {() => (c = f(!c))}
-1009-1010 punctuation {
-1010-1027 lang_ts () => (c = f(!c))
-1010-1012 punctuation ()
-1013-1015 operator =>
-1016-1017 punctuation (
-1019-1020 operator =
-1021-1022 function f
-1022-1023 punctuation (
-1023-1024 operator !
-1025-1027 punctuation ))
-1027-1028 punctuation }
-1028-1029 punctuation >
-1032-1052 svelte_expression {@render children()}
-1032-1052 at_directive {@render children()}
-1032-1033 punctuation {
-1033-1040 at_keyword @render
-1041-1051 lang_ts children()
-1041-1049 function children
-1049-1051 punctuation ()
-1051-1052 punctuation }
-1054-1062 tag
-1054-1061 tag
-1063-1068 svelte_expression {/if}
-1063-1068 block {/if}
-1063-1064 punctuation {
-1064-1067 special_keyword /if
-1067-1068 punctuation }
-1070-1126 comment
-1127-1162 svelte_expression {@html 'raw html '}
-1127-1162 at_directive {@html 'raw html '}
-1127-1128 punctuation {
-1128-1133 at_keyword @html
-1134-1161 lang_ts 'raw html '
-1134-1161 string 'raw html '
-1161-1162 punctuation }
-1164-1213 tag
-1164-1170 tag
-1215-1258 tag
-1215-1220 tag
-1261-1268 tag
-1261-1267 tag
-1270-1326 tag
{\n\t// function attachment arg\n})}>
-1270-1274 tag
{\n\t// function attachment arg\n})}
-1275-1325 at_directive {@attach f(() => {\n\t// function attachment arg\n})}
-1275-1276 punctuation {
-1276-1283 at_keyword @attach
-1284-1324 lang_ts f(() => {\n\t// function attachment arg\n})
-1284-1285 function f
-1285-1288 punctuation (()
-1289-1291 operator =>
-1292-1293 punctuation {
-1295-1321 comment // function attachment arg
-1322-1324 punctuation })
-1324-1325 punctuation }
-1325-1326 punctuation >
-1336-1342 tag
-1336-1341 tag
-1344-1369 svelte_expression {@render my_snippet('p')}
-1344-1369 at_directive {@render my_snippet('p')}
-1344-1345 punctuation {
-1345-1352 at_keyword @render
-1353-1368 lang_ts my_snippet('p')
-1353-1363 function my_snippet
-1363-1364 punctuation (
-1364-1367 string 'p'
-1367-1368 punctuation )
-1368-1369 punctuation }
-1371-1403 svelte_expression {#snippet my_snippet(p: string)}
-1371-1403 block {#snippet my_snippet(p: string)}
-1371-1372 punctuation {
-1372-1380 special_keyword #snippet
-1381-1402 lang_ts my_snippet(p: string)
-1381-1391 function my_snippet
-1391-1392 punctuation (
-1393-1394 operator :
-1395-1401 builtin string
-1401-1402 punctuation )
-1402-1403 punctuation }
-1405-1437 tag
-1405-1412 tag
+ 953-959 tag
+1090-1109 svelte_expression {:else if f(a > 0)}
+1090-1109 block {:else if f(a > 0)}
+1090-1091 punctuation {
+1091-1099 special_keyword :else if
+1100-1108 lang_ts f(a > 0)
+1100-1101 function f
+1101-1102 punctuation (
+1104-1105 operator >
+1106-1107 number 0
+1107-1108 punctuation )
+1108-1109 punctuation }
+1118-1125 svelte_expression {:else}
+1118-1125 block {:else}
+1118-1119 punctuation {
+1119-1124 special_keyword :else
+1124-1125 punctuation }
+1127-1178 tag (c = f(!c))}>
+1127-1133 tag (c = f(!c))}
+1157-1158 punctuation =
+1158-1177 svelte_expression {() => (c = f(!c))}
+1158-1159 punctuation {
+1159-1176 lang_ts () => (c = f(!c))
+1159-1161 punctuation ()
+1162-1164 operator =>
+1165-1166 punctuation (
+1168-1169 operator =
+1170-1171 function f
+1171-1172 punctuation (
+1172-1173 operator !
+1174-1176 punctuation ))
+1176-1177 punctuation }
+1177-1178 punctuation >
+1181-1201 svelte_expression {@render children()}
+1181-1201 at_directive {@render children()}
+1181-1182 punctuation {
+1182-1189 at_keyword @render
+1190-1200 lang_ts children()
+1190-1198 function children
+1198-1200 punctuation ()
+1200-1201 punctuation }
+1203-1211 tag
+1203-1210 tag
+1212-1217 svelte_expression {/if}
+1212-1217 block {/if}
+1212-1213 punctuation {
+1213-1216 special_keyword /if
+1216-1217 punctuation }
+1219-1275 comment
+1276-1311 svelte_expression {@html 'raw html '}
+1276-1311 at_directive {@html 'raw html '}
+1276-1277 punctuation {
+1277-1282 at_keyword @html
+1283-1310 lang_ts 'raw html '
+1283-1310 string 'raw html '
+1310-1311 punctuation }
+1313-1382 tag
+1313-1319 tag
+1384-1427 tag
+1384-1389 tag
+1430-1437 tag
+1430-1436 tag
-1437-1440 svelte_expression {p}
-1437-1438 punctuation {
-1438-1439 lang_ts p
-1439-1440 punctuation }
-1440-1449 tag
-1440-1448 tag
-1450-1460 svelte_expression {/snippet}
-1450-1460 block {/snippet}
-1450-1451 punctuation {
-1451-1459 special_keyword /snippet
-1459-1460 punctuation }
-1462-1511 tag
-1462-1464 tag
{\n\t// function attachment arg\n})}>
+1439-1443 tag
{\n\t// function attachment arg\n})}
+1444-1494 at_directive {@attach f(() => {\n\t// function attachment arg\n})}
+1444-1445 punctuation {
+1445-1452 at_keyword @attach
+1453-1493 lang_ts f(() => {\n\t// function attachment arg\n})
+1453-1454 function f
+1454-1457 punctuation (()
+1458-1460 operator =>
+1461-1462 punctuation {
+1464-1490 comment // function attachment arg
+1491-1493 punctuation })
+1493-1494 punctuation }
+1494-1495 punctuation >
+1505-1511 tag
+1505-1510 tag
-1518-1538 tag
-1518-1523 tag
-1542-1549 tag
-1542-1548 tag
-1550-1554 tag
-1550-1553 tag
-1556-1587 tag
-1556-1563 tag
-1597-1606 tag
-1597-1605 tag
+1574-1581 tag
-1608-1647 comment
-1648-1661 svelte_expression {b.repeat(2)}
-1648-1649 punctuation {
-1649-1660 lang_ts b.repeat(2)
-1650-1651 punctuation .
-1651-1657 function repeat
-1657-1658 punctuation (
-1658-1659 number 2
-1659-1660 punctuation )
-1660-1661 punctuation }
-1662-1669 svelte_expression {bound}
-1662-1663 punctuation {
-1663-1668 lang_ts bound
-1668-1669 punctuation }
-1671-1677 tag
-1671-1674 tag
-1679-1685 tag
-1679-1682 tag
-1687-1723 tag
-1687-1691 tag
+1609-1617 tag
+1619-1629 svelte_expression {/snippet}
+1619-1629 block {/snippet}
+1619-1620 punctuation {
+1620-1628 special_keyword /snippet
+1628-1629 punctuation }
+1631-1680 tag
+1631-1633 tag
+1687-1707 tag
+1687-1692 tag
-1725-1729 tag
+1711-1717 tag
+1719-1723 tag
+1719-1722 tag
+1725-1756 tag
+1725-1732 tag
-1731-1735 tag
-1731-1734 tag
-1746-1751 tag
-1746-1750 tag
-1753-1757 tag
-1753-1756 tag
-1768-1773 tag
-1768-1772 tag
-1774-1779 tag
-1774-1778 tag
-1781-1824 comment
-1825-1830 tag
-1825-1829 tag
-1832-1840 tag
-1878-1886 tag
-1889-1896 tag
-1930-1937 tag
-1939-1945 tag
-1939-1944 tag
-1947-1954 tag
-2339-2346 tag
+1733-1737 attr_name type
+1737-1746 attr_value ="button"
+1737-1738 punctuation =
+1738-1739 punctuation "
+1745-1746 punctuation "
+1747-1755 attr_name disabled
+1755-1756 punctuation >
+1766-1775 tag
+1766-1774 tag
+1777-1816 comment
+1817-1830 svelte_expression {b.repeat(2)}
+1817-1818 punctuation {
+1818-1829 lang_ts b.repeat(2)
+1819-1820 punctuation .
+1820-1826 function repeat
+1826-1827 punctuation (
+1827-1828 number 2
+1828-1829 punctuation )
+1829-1830 punctuation }
+1831-1838 svelte_expression {bound}
+1831-1832 punctuation {
+1832-1837 lang_ts bound
+1837-1838 punctuation }
+1840-1846 tag
+1840-1843 tag
+1848-1854 tag
+1848-1851 tag
+1856-1892 tag
+1856-1860 tag
+1894-1898 tag
+1894-1897 tag
+1900-1904 tag
+1900-1903 tag
+1915-1920 tag
+1915-1919 tag
+1922-1926 tag
+1922-1925 tag
+1937-1942 tag
+1937-1941 tag
+1943-1948 tag
+1943-1947 tag
+1950-1993 comment
+1994-1999 tag
+1994-1998 tag
+2001-2009 tag
+2047-2055 tag
+2058-2065 tag
+2099-2106 tag
+2108-2114 tag
+2108-2113 tag
+2116-2123 tag
+2508-2515 tag
diff --git a/src/test/fixtures/samples/sample_complex.svelte b/src/test/fixtures/samples/sample_complex.svelte
index 517e3d94..3dc6131c 100644
--- a/src/test/fixtures/samples/sample_complex.svelte
+++ b/src/test/fixtures/samples/sample_complex.svelte
@@ -43,11 +43,20 @@
{#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)}
-
+
{:else if f(a > 0)}
bigger
{:else}
@@ -59,7 +68,7 @@
{@html 'raw html '}
-
+
...
diff --git a/src/test/lexer_markup.test.ts b/src/test/lexer_markup.test.ts
index ab622a7e..bceda3de 100644
--- a/src/test/lexer_markup.test.ts
+++ b/src/test/lexer_markup.test.ts
@@ -91,6 +91,14 @@ describe('lexer_markup tags', () => {
assert.deepEqual(tokens[0], ['tag', text]);
assert.deepEqual(validate_syntax_events(syntax_styler_global.lex(text, 'html')), []);
});
+
+ test('js-style comments in tags stay bogus attributes (svelte-only feature)', () => {
+ // `//` and `/* */` between attributes are comments in svelte, but in
+ // html/xml they are ordinary bogus attribute text
+ assert.deepEqual(picked('', ['comment']), []);
+ assert.deepEqual(picked('
', ['attr_name']), [['attr_name', 'x']]);
+ assert.deepEqual(picked('
', ['comment'], 'xml'), []);
+ });
});
describe('lexer_markup entities', () => {
diff --git a/src/test/lexer_svelte.test.ts b/src/test/lexer_svelte.test.ts
index 17f3d5ff..f5facb31 100644
--- a/src/test/lexer_svelte.test.ts
+++ b/src/test/lexer_svelte.test.ts
@@ -164,6 +164,89 @@ describe('lexer_svelte at-directives', () => {
});
});
+describe('lexer_svelte declaration tags', () => {
+ test('`{const …}` and `{let …}` wrap a declaration_tag with a keyword lead', () => {
+ assert.deepEqual(tokens_of('{const area = w * h}').slice(0, 5), [
+ ['svelte_expression', '{const area = w * h}'],
+ ['declaration_tag', '{const area = w * h}'],
+ ['punctuation', '{'],
+ ['keyword', 'const'],
+ ['lang_ts', 'area = w * h'],
+ ]);
+ assert.deepEqual(picked('{let name = user.name}', ['declaration_tag', 'keyword']), [
+ ['declaration_tag', '{let name = user.name}'],
+ ['keyword', 'let'],
+ ]);
+ });
+
+ test('a `const`/`let`-prefixed identifier is not a declaration tag', () => {
+ assert.deepEqual(picked('{const_value + 1}', ['declaration_tag']), []);
+ assert.deepEqual(tokens_of('{const_value + 1}')[0], ['svelte_expression', '{const_value + 1}']);
+ });
+
+ test('declaration tags are top-level only, not in attribute position', () => {
+ // `full` is false in tag/attribute context — the leading `const` stays a
+ // plain ts keyword with no declaration_tag container
+ assert.deepEqual(picked('
', ['declaration_tag']), []);
+ assert.deepEqual(picked(' ', ['keyword']), [['keyword', 'const']]);
+ });
+
+ test('an unterminated declaration tag stays valid', () => {
+ const text = '{const x =';
+ assert.deepEqual(validate_syntax_events(syntax_styler_global.lex(text, 'svelte')), []);
+ assert.deepEqual(picked(text, ['declaration_tag', 'keyword']), [
+ ['declaration_tag', '{const x ='],
+ ['keyword', 'const'],
+ ]);
+ });
+
+ test('the legacy `{@const}` stays an at_directive, not a declaration tag', () => {
+ assert.deepEqual(picked('{@const x = 1}', ['at_directive', 'declaration_tag', 'at_keyword']), [
+ ['at_directive', '{@const x = 1}'],
+ ['at_keyword', '@const'],
+ ]);
+ });
+});
+
+describe('lexer_svelte attribute comments', () => {
+ test('line and block comments between attributes emit comment leaves', () => {
+ assert.deepEqual(picked('', ['comment', 'attr_name']), [
+ ['attr_name', 'a'],
+ ['comment', '// note'],
+ ['comment', '/* block */'],
+ ['attr_name', 'b'],
+ ]);
+ });
+
+ test('multiple inline block comments, and an unterminated one extends to the end', () => {
+ assert.deepEqual(picked('
', ['comment']), [
+ ['comment', '/* a */'],
+ ['comment', '/* b */'],
+ ]);
+ const text = '`', () => {
+ // js line-comment semantics — the tag closes on the next line
+ assert.deepEqual(picked('
\n>', ['comment', 'punctuation']), [
+ ['punctuation', '<'],
+ ['comment', '// c >'],
+ ['punctuation', '>'],
+ ]);
+ });
+
+ test('a block comment is opaque — an inner `>` does not close the tag', () => {
+ const text = '
b */ c="1">';
+ assert.deepEqual(validate_syntax_events(syntax_styler_global.lex(text, 'svelte')), []);
+ assert.deepEqual(picked(text, ['comment', 'attr_name']), [
+ ['comment', '/* a > b */'],
+ ['attr_name', 'c'],
+ ]);
+ });
+});
+
describe('lexer_svelte tags', () => {
test('an expression-valued attribute', () => {
assert.deepEqual(tokens_of('
').slice(3, 9), [
@@ -293,7 +376,15 @@ describe('lexer_svelte sample', () => {
const types = new Set(
syntax_events_to_tokens(syntax_styler_global.lex(content, 'svelte')).map((t) => t.type),
);
- for (const t of ['tag', 'svelte_expression', 'lang_ts', 'attr_name', 'block', 'at_directive']) {
+ for (const t of [
+ 'tag',
+ 'svelte_expression',
+ 'lang_ts',
+ 'attr_name',
+ 'block',
+ 'at_directive',
+ 'declaration_tag',
+ ]) {
assert.ok(types.has(t), `expected a ${t} token in the sample`);
}
});