yaxa-svelte 2.0.3 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,7 +32,7 @@
32
32
  | 'decorator';
33
33
  }
34
34
 
35
- // Zero-WASM Multi-Language Pure JavaScript Tokenizer with Svelte 5 Rune Recognition
35
+ // Zero-WASM Multi-Language Pure JavaScript Tokenizer with Svelte 5 Rune Recognition & Multi-Line State
36
36
  export function tokenizeCode(code: string, language: string = 'typescript'): Token[][] {
37
37
  const lines = code.split('\n');
38
38
  const lang = language.toLowerCase();
@@ -41,41 +41,144 @@
41
41
  const isLatex = ['latex', 'tex', 'math'].includes(lang);
42
42
  const isVue = lang === 'vue';
43
43
  const isJsx = ['tsx', 'jsx', 'react'].includes(lang);
44
+ const isTs = ['typescript', 'ts', 'tsx'].includes(lang);
45
+ const isJs = ['javascript', 'js', 'jsx', 'mjs', 'cjs'].includes(lang);
44
46
  const isSvelte = ['svelte', 'svelte5', 'sv'].includes(lang);
45
47
  const isHtml = ['html', 'xml', 'svg'].includes(lang);
46
48
  const isPython = ['python', 'py'].includes(lang);
47
49
  const isSql = lang === 'sql';
48
50
  const isBash = ['bash', 'sh', 'zsh', 'shell'].includes(lang);
51
+ const isJson = ['json', 'jsonc', 'json5'].includes(lang);
52
+ const isCss = ['css', 'scss', 'postcss', 'tailwind', 'tailwindcss'].includes(lang);
53
+
54
+ let multilineState:
55
+ | null
56
+ | { type: 'comment'; delimiter: '/*' | '<!--' }
57
+ | { type: 'string'; delimiter: '"""' | "'''" | '`' } = null;
49
58
 
50
59
  return lines.map((line) => {
51
- if (!line) return [{ text: '', type: 'plain' }];
60
+ if (!line) {
61
+ if (multilineState !== null) {
62
+ return [{ text: '', type: multilineState.type }];
63
+ }
64
+ return [{ text: '', type: 'plain' }];
65
+ }
66
+
67
+ const tokens: Token[] = [];
68
+ let remaining = line;
52
69
 
53
- // Special full-line rule: Markdown Headings (# Header)
54
- if (isMarkdown) {
55
- const headingMatch = line.match(/^(#{1,6}\s+)(.*)$/);
70
+ // Handle multi-line comment or string continuation from previous line
71
+ if (multilineState !== null) {
72
+ if (multilineState.type === 'comment') {
73
+ const closeDelim = multilineState.delimiter === '/*' ? '*/' : '-->';
74
+ const closeIdx = remaining.indexOf(closeDelim);
75
+ if (closeIdx !== -1) {
76
+ const endIdx = closeIdx + closeDelim.length;
77
+ tokens.push({ text: remaining.slice(0, endIdx), type: 'comment' });
78
+ remaining = remaining.slice(endIdx);
79
+ multilineState = null;
80
+ } else {
81
+ tokens.push({ text: remaining, type: 'comment' });
82
+ return tokens;
83
+ }
84
+ } else if (multilineState.type === 'string') {
85
+ const delim = multilineState.delimiter;
86
+ if (delim === '`') {
87
+ const closeMatch = remaining.match(/^(?:[^`\\]|\\.)*?`/);
88
+ if (closeMatch) {
89
+ tokens.push({ text: closeMatch[0], type: 'string' });
90
+ remaining = remaining.slice(closeMatch[0].length);
91
+ multilineState = null;
92
+ } else {
93
+ tokens.push({ text: remaining, type: 'string' });
94
+ return tokens;
95
+ }
96
+ } else {
97
+ // Python triple quotes """ or '''
98
+ const closeIdx = remaining.indexOf(delim);
99
+ if (closeIdx !== -1) {
100
+ const endIdx = closeIdx + delim.length;
101
+ tokens.push({ text: remaining.slice(0, endIdx), type: 'string' });
102
+ remaining = remaining.slice(endIdx);
103
+ multilineState = null;
104
+ } else {
105
+ tokens.push({ text: remaining, type: 'string' });
106
+ return tokens;
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ // Special full-line Markdown rules
113
+ if (isMarkdown && remaining === line) {
114
+ // Fenced code blocks (```lang or ~~~lang)
115
+ const fenceMatch = remaining.match(/^(\s*(?:`{3,}|~{3,})[\w-]*\s*)$/);
116
+ if (fenceMatch) {
117
+ return [{ text: fenceMatch[1], type: 'macro' }];
118
+ }
119
+ // Frontmatter & Horizontal rule (--- or ***)
120
+ const hrMatch = remaining.match(/^(\s*(?:---|___|\*\*\*)\s*)$/);
121
+ if (hrMatch) {
122
+ return [{ text: hrMatch[1], type: 'punctuation' }];
123
+ }
124
+ // Markdown Headings (# Header)
125
+ const headingMatch = remaining.match(/^(#{1,6}\s+)(.*)$/);
56
126
  if (headingMatch) {
57
127
  return [
58
128
  { text: headingMatch[1], type: 'punctuation' },
59
129
  { text: headingMatch[2], type: 'heading' }
60
130
  ];
61
131
  }
62
- const blockquoteMatch = line.match(/^(\s*>\s+)(.*)$/);
132
+ // Blockquotes (> Quote)
133
+ const blockquoteMatch = remaining.match(/^(\s*>\s+)(.*)$/);
63
134
  if (blockquoteMatch) {
64
135
  return [
65
136
  { text: blockquoteMatch[1], type: 'punctuation' },
66
137
  { text: blockquoteMatch[2], type: 'italic' }
67
138
  ];
68
139
  }
140
+ // Lists & Task checkboxes (- [x] Item, * Item, 1. Item)
141
+ const listMatch = remaining.match(/^(\s*(?:[-*+]|\d+\.)(?:\s+\[[ xX]\])?\s+)(.*)$/);
142
+ if (listMatch) {
143
+ return [
144
+ { text: listMatch[1], type: 'punctuation' },
145
+ { text: listMatch[2], type: 'plain' }
146
+ ];
147
+ }
69
148
  }
70
149
 
71
- const tokens: Token[] = [];
72
- let remaining = line;
73
-
74
150
  while (remaining.length > 0) {
75
151
  // 1. Comments
152
+ if (!isLatex && remaining.startsWith('/*')) {
153
+ const closeIdx = remaining.indexOf('*/', 2);
154
+ if (closeIdx !== -1) {
155
+ const endIdx = closeIdx + 2;
156
+ tokens.push({ text: remaining.slice(0, endIdx), type: 'comment' });
157
+ remaining = remaining.slice(endIdx);
158
+ continue;
159
+ } else {
160
+ tokens.push({ text: remaining, type: 'comment' });
161
+ multilineState = { type: 'comment', delimiter: '/*' };
162
+ break;
163
+ }
164
+ }
165
+
166
+ if ((isMarkdown || isHtml || isSvelte || isVue) && remaining.startsWith('<!--')) {
167
+ const closeIdx = remaining.indexOf('-->', 4);
168
+ if (closeIdx !== -1) {
169
+ const endIdx = closeIdx + 3;
170
+ tokens.push({ text: remaining.slice(0, endIdx), type: 'comment' });
171
+ remaining = remaining.slice(endIdx);
172
+ continue;
173
+ } else {
174
+ tokens.push({ text: remaining, type: 'comment' });
175
+ multilineState = { type: 'comment', delimiter: '<!--' };
176
+ break;
177
+ }
178
+ }
179
+
76
180
  if (
77
- (!isLatex && (remaining.startsWith('//') || remaining.startsWith('/*'))) ||
78
- ((isMarkdown || isHtml || isSvelte || isVue) && remaining.startsWith('<!--')) ||
181
+ (!isLatex && remaining.startsWith('//')) ||
79
182
  ((isPython || isBash || lang === 'env' || lang === 'yaml' || lang === 'yml') &&
80
183
  remaining.startsWith('#')) ||
81
184
  (isLatex && remaining.startsWith('%')) ||
@@ -85,7 +188,86 @@
85
188
  break;
86
189
  }
87
190
 
88
- // 2. LaTeX Math Macros and Math Blocks
191
+ // 2. Python Triple-Quoted Strings (Docstrings)
192
+ if (isPython && (remaining.startsWith('"""') || remaining.startsWith("'''"))) {
193
+ const delim = remaining.startsWith('"""') ? '"""' : "'''";
194
+ const closeIdx = remaining.indexOf(delim, 3);
195
+ if (closeIdx !== -1) {
196
+ const endIdx = closeIdx + 3;
197
+ tokens.push({ text: remaining.slice(0, endIdx), type: 'string' });
198
+ remaining = remaining.slice(endIdx);
199
+ continue;
200
+ } else {
201
+ tokens.push({ text: remaining, type: 'string' });
202
+ multilineState = { type: 'string', delimiter: delim };
203
+ break;
204
+ }
205
+ }
206
+
207
+ // 3. Multi-line Template Literals (Backticks) for JS / TS / Svelte / Vue
208
+ if ((isJs || isTs || isSvelte || isVue) && remaining.startsWith('`')) {
209
+ const fullBacktickMatch = remaining.match(/^(`(?:[^`\\]|\\.)*`)/);
210
+ if (fullBacktickMatch) {
211
+ tokens.push({ text: fullBacktickMatch[0], type: 'string' });
212
+ remaining = remaining.slice(fullBacktickMatch[0].length);
213
+ continue;
214
+ } else {
215
+ tokens.push({ text: remaining, type: 'string' });
216
+ multilineState = { type: 'string', delimiter: '`' };
217
+ break;
218
+ }
219
+ }
220
+
221
+ // 4. JSON Object Keys ("key":)
222
+ if (isJson) {
223
+ const jsonKeyMatch = remaining.match(/^("(?:[^"\\]|\\.)*")(?=\s*:)/);
224
+ if (jsonKeyMatch) {
225
+ tokens.push({ text: jsonKeyMatch[1], type: 'attr' });
226
+ remaining = remaining.slice(jsonKeyMatch[1].length);
227
+ continue;
228
+ }
229
+ }
230
+
231
+ // 5. CSS & Tailwind CSS v4 At-Rules, Variables & Colors
232
+ if (isCss) {
233
+ // Tailwind v4 & CSS At-rules (@theme, @utility, @source, @plugin, @import, @custom-variant, etc.)
234
+ const atRuleMatch = remaining.match(
235
+ /^(@(?:theme|utility|custom-variant|source|plugin|import|apply|keyframes|media|supports|container|layer|font-face|page|starting-style))\b/
236
+ );
237
+ if (atRuleMatch) {
238
+ tokens.push({ text: atRuleMatch[0], type: 'directive' });
239
+ remaining = remaining.slice(atRuleMatch[0].length);
240
+ continue;
241
+ }
242
+
243
+ // CSS Custom Properties / Design Tokens (--color-primary-500, etc.)
244
+ const cssVarMatch = remaining.match(/^(--[\w-]+)/);
245
+ if (cssVarMatch) {
246
+ tokens.push({ text: cssVarMatch[0], type: 'macro' });
247
+ remaining = remaining.slice(cssVarMatch[0].length);
248
+ continue;
249
+ }
250
+
251
+ // Hex Color Literals (#ff3e00, #18181b, etc.)
252
+ const hexColorMatch = remaining.match(
253
+ /^#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})\b/
254
+ );
255
+ if (hexColorMatch) {
256
+ tokens.push({ text: hexColorMatch[0], type: 'number' });
257
+ remaining = remaining.slice(hexColorMatch[0].length);
258
+ continue;
259
+ }
260
+
261
+ // CSS Properties before colon (e.g. display:, background-color:)
262
+ const cssPropMatch = remaining.match(/^([\w-]+)(?=\s*:)/);
263
+ if (cssPropMatch && !cssPropMatch[0].startsWith('-')) {
264
+ tokens.push({ text: cssPropMatch[0], type: 'keyword' });
265
+ remaining = remaining.slice(cssPropMatch[0].length);
266
+ continue;
267
+ }
268
+ }
269
+
270
+ // 6. LaTeX Math Macros and Math Blocks
89
271
  if (isLatex) {
90
272
  const macroMatch = remaining.match(/^(\\[a-zA-Z]+|\\\[|\\\]|\\\(|\\\))/);
91
273
  if (macroMatch) {
@@ -101,7 +283,7 @@
101
283
  }
102
284
  }
103
285
 
104
- // 3. Markdown Formatting (Bold, Italic, Code, Links)
286
+ // 7. Markdown Formatting (Bold, Italic, Code, Links)
105
287
  if (isMarkdown) {
106
288
  const codeMatch = remaining.match(/^(`[^`]+`)/);
107
289
  if (codeMatch) {
@@ -124,17 +306,7 @@
124
306
  }
125
307
  }
126
308
 
127
- // 4. Strings
128
- const strMatch = remaining.match(
129
- /^("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/
130
- );
131
- if (strMatch) {
132
- tokens.push({ text: strMatch[0], type: 'string' });
133
- remaining = remaining.slice(strMatch[0].length);
134
- continue;
135
- }
136
-
137
- // 5. Python Decorators (@decorator)
309
+ // 8. Python Decorators (@decorator)
138
310
  if (isPython) {
139
311
  const decMatch = remaining.match(/^(@\w+)/);
140
312
  if (decMatch) {
@@ -144,7 +316,70 @@
144
316
  }
145
317
  }
146
318
 
147
- // 6. Vue / Svelte / JSX Directives & Tags
319
+ // 9. Python Built-in Types (int, str, dict, list, bool, etc.)
320
+ if (isPython) {
321
+ const pyTypeMatch = remaining.match(
322
+ /^(str|int|float|bool|dict|list|tuple|set|bytes|Any|Optional|Union|Callable|Literal)\b/
323
+ );
324
+ if (pyTypeMatch) {
325
+ tokens.push({ text: pyTypeMatch[0], type: 'macro' });
326
+ remaining = remaining.slice(pyTypeMatch[0].length);
327
+ continue;
328
+ }
329
+ }
330
+
331
+ // 10. TypeScript / JavaScript Primitive Types & Type Operators (TS 5.x / TS 6 & ES2026)
332
+ if (isTs || isJs) {
333
+ const tsTypeMatch = remaining.match(
334
+ /^(string|number|boolean|unknown|never|any|void|readonly|satisfies|keyof|infer|Promise|Record|Array|Partial|Omit|Pick|Awaited|ReturnType|Parameters|ConstructorParameters|InstanceType|Extract|Exclude|NonNullable|Uppercase|Lowercase|Capitalize|Uncapitalize|Disposable|AsyncDisposable|BigInt|Symbol)\b/
335
+ );
336
+ if (tsTypeMatch) {
337
+ tokens.push({ text: tsTypeMatch[0], type: 'macro' });
338
+ remaining = remaining.slice(tsTypeMatch[0].length);
339
+ continue;
340
+ }
341
+ }
342
+
343
+ // 11. Bash / Shell Variables & CLI Flags
344
+ if (isBash) {
345
+ const bashVarMatch = remaining.match(/^(\$\{?[\w#?@*!$-]+\}?)/);
346
+ if (bashVarMatch) {
347
+ tokens.push({ text: bashVarMatch[0], type: 'macro' });
348
+ remaining = remaining.slice(bashVarMatch[0].length);
349
+ continue;
350
+ }
351
+ const bashFlagMatch = remaining.match(/^(--?[\w-]+)/);
352
+ if (bashFlagMatch) {
353
+ tokens.push({ text: bashFlagMatch[0], type: 'attr' });
354
+ remaining = remaining.slice(bashFlagMatch[0].length);
355
+ continue;
356
+ }
357
+ }
358
+
359
+ // 12. Svelte 5 Native Runes & Template Logic Blocks
360
+ if (isSvelte) {
361
+ // Svelte 5 Runes ($state, $derived, $props, $effect, etc.)
362
+ const svelteRuneMatch = remaining.match(
363
+ /^(\$(?:state(?:\.raw|\.snapshot)?|derived(?:\.by)?|effect(?:\.pre|\.root)?|props|bindable|inspect(?:\.trace)?|host))\b/
364
+ );
365
+ if (svelteRuneMatch) {
366
+ tokens.push({ text: svelteRuneMatch[0], type: 'rune' });
367
+ remaining = remaining.slice(svelteRuneMatch[0].length);
368
+ continue;
369
+ }
370
+
371
+ // Svelte Control Flow & Snippets ({#if}, {#each}, {#snippet}, {@render}, {@html}, etc.)
372
+ const svelteBlockMatch = remaining.match(
373
+ /^(\{(?:#(?:if|each|await|snippet|key)|:(?:else\s+if|else|then|catch)|\/(?:if|each|await|snippet|key)|@(?:render|html|const|debug)))\b/
374
+ );
375
+ if (svelteBlockMatch) {
376
+ tokens.push({ text: svelteBlockMatch[0], type: 'directive' });
377
+ remaining = remaining.slice(svelteBlockMatch[0].length);
378
+ continue;
379
+ }
380
+ }
381
+
382
+ // 13. HTML / Vue / Svelte / JSX Tags & Attributes
148
383
  if (isVue || isSvelte || isHtml || isJsx) {
149
384
  const tagMatch = remaining.match(/^(<\/?[\w.:-]+|\/>|>)/);
150
385
  if (tagMatch) {
@@ -170,19 +405,19 @@
170
405
  }
171
406
  }
172
407
 
173
- // 7. Svelte 5 Native Runes ($state, $state.raw, $derived, $derived.by, $effect, $props, $bindable, etc.)
174
- const svelteRuneMatch = remaining.match(
175
- /^(\$(?:state(?:\.raw|\.snapshot)?|derived(?:\.by)?|effect(?:\.pre|\.root)?|props|bindable|inspect(?:\.trace)?|host))\b/
408
+ // 14. Standard Strings (Single & Double Quoted)
409
+ const strMatch = remaining.match(
410
+ /^("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/
176
411
  );
177
- if (svelteRuneMatch) {
178
- tokens.push({ text: svelteRuneMatch[0], type: 'rune' });
179
- remaining = remaining.slice(svelteRuneMatch[0].length);
412
+ if (strMatch) {
413
+ tokens.push({ text: strMatch[0], type: 'string' });
414
+ remaining = remaining.slice(strMatch[0].length);
180
415
  continue;
181
416
  }
182
417
 
183
- // 8. Keywords Across Languages
418
+ // 15. Keywords Across Languages (including ES2026 and TS6)
184
419
  const kwMatch = remaining.match(
185
- /^(import|export|from|default|const|let|var|function|return|async|await|class|interface|type|extends|implements|new|this|typeof|instanceof|if|else|switch|case|break|for|while|try|catch|finally|throw|as|in|of|true|false|null|undefined|fn|mut|struct|enum|impl|trait|pub|use|mod|crate|where|loop|unsafe|package|func|defer|go|chan|select|iota|def|self|yield|lambda|elif|except|with|None|True|False|pass|query|mutation|subscription|schema|scalar|fragment|directive|SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|GROUP|ORDER|BY|LIMIT|pnpm|npm|npx|bun|yarn|git|docker)\b/i
420
+ /^(import|export|from|default|const|let|var|function|return|async|await|class|interface|type|extends|implements|new|this|typeof|instanceof|if|else|switch|case|break|for|while|try|catch|finally|throw|as|in|of|true|false|null|undefined|fn|mut|struct|enum|impl|trait|pub|use|mod|crate|where|loop|unsafe|package|func|defer|go|chan|select|iota|def|self|yield|lambda|elif|except|with|None|True|False|pass|assert|raise|match|when|using|accessor|override|declare|namespace|asserts|is|del|global|nonlocal|query|mutation|subscription|schema|scalar|fragment|directive|SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|GROUP|ORDER|BY|LIMIT|pnpm|npm|npx|bun|yarn|git|docker|curl|wget|mkdir|echo|cat|rm|cp|mv|chmod|chown|sudo|cd|ls|pwd|grep|sed|awk)\b/i
186
421
  );
187
422
  if (kwMatch) {
188
423
  tokens.push({ text: kwMatch[0], type: 'keyword' });
@@ -190,7 +425,7 @@
190
425
  continue;
191
426
  }
192
427
 
193
- // 9. React Hooks / Generic Macro tokens
428
+ // 16. React Hooks / Generic Macro tokens
194
429
  const macroMatch = remaining.match(/^(use[A-Z]\w+)\b/);
195
430
  if (macroMatch) {
196
431
  tokens.push({ text: macroMatch[0], type: 'macro' });
@@ -198,7 +433,7 @@
198
433
  continue;
199
434
  }
200
435
 
201
- // 10. Function invocations (funcName())
436
+ // 17. Function Invocations (funcName())
202
437
  const fnMatch = remaining.match(/^(\$?\w+)(?=\s*\()/);
203
438
  if (fnMatch) {
204
439
  tokens.push({ text: fnMatch[0], type: 'function' });
@@ -206,15 +441,17 @@
206
441
  continue;
207
442
  }
208
443
 
209
- // 11. Numbers
210
- const numMatch = remaining.match(/^\b(\d+(?:\.\d+)?(?:px|rem|em|%|vh|vw|s|ms)?)\b/);
444
+ // 18. Numbers (integers, floats, scientific, BigInt, hex, binary, octal & CSS units)
445
+ const numMatch = remaining.match(
446
+ /^(?:0[xX][0-9a-fA-F_]+n?|0[bB][01_]+n?|0[oO][0-7_]+n?|\b\d[\d_]*(?:\.[\d_]+)?(?:[eE][+-]?[\d_]+)?n?(?:px|rem|em|%|vh|vw|dvh|dvw|s|ms)?)\b/
447
+ );
211
448
  if (numMatch) {
212
449
  tokens.push({ text: numMatch[0], type: 'number' });
213
450
  remaining = remaining.slice(numMatch[0].length);
214
451
  continue;
215
452
  }
216
453
 
217
- // 12. Punctuation & Math Operators
454
+ // 19. Punctuation & Math Operators
218
455
  const punctMatch = remaining.match(/^([{}()[\].,;:?!=<>+\-*/%&|^~_^\\]+)/);
219
456
  if (punctMatch) {
220
457
  tokens.push({ text: punctMatch[0], type: 'punctuation' });
@@ -222,7 +459,7 @@
222
459
  continue;
223
460
  }
224
461
 
225
- // 13. Plain text / whitespace
462
+ // 20. Plain text / whitespace
226
463
  const plainMatch = remaining.match(/^([\w$]+|\s+)/);
227
464
  if (plainMatch) {
228
465
  tokens.push({ text: plainMatch[0], type: 'plain' });
@@ -23,4 +23,131 @@ describe('tokenizeCode', () => {
23
23
  expect(tokens).toHaveLength(3);
24
24
  expect(tokens[1]).toEqual([{ text: '', type: 'plain' }]);
25
25
  });
26
+ it('tokenizes python keywords, types, operators, and multiline docstrings', () => {
27
+ const pyCode = `def add(x: int = 1) -> str:
28
+ """
29
+ Multi-line Python docstring
30
+ """
31
+ assert x > 0
32
+ return str(x)`;
33
+ const tokens = tokenizeCode(pyCode, 'python');
34
+ expect(tokens).toHaveLength(6);
35
+ // Line 0: def add(x: int = 1) -> str:
36
+ const line0Types = tokens[0].map((t) => ({ text: t.text, type: t.type }));
37
+ expect(line0Types).toContainEqual({ text: 'def', type: 'keyword' });
38
+ expect(line0Types).toContainEqual({ text: 'add', type: 'function' });
39
+ expect(line0Types).toContainEqual({ text: 'int', type: 'macro' });
40
+ expect(line0Types).toContainEqual({ text: 'str', type: 'macro' });
41
+ expect(line0Types.some((t) => t.text.includes('->'))).toBe(true);
42
+ expect(line0Types.some((t) => t.text.includes('='))).toBe(true);
43
+ // Docstring continuation
44
+ expect(tokens[1].some((t) => t.type === 'string')).toBe(true);
45
+ expect(tokens[2][0].type).toBe('string');
46
+ expect(tokens[3].some((t) => t.type === 'string')).toBe(true);
47
+ // Line 4: assert x > 0
48
+ const line4 = tokens[4].map((t) => ({ text: t.text, type: t.type }));
49
+ expect(line4).toContainEqual({ text: 'assert', type: 'keyword' });
50
+ });
51
+ it('tokenizes tailwind css v4 at-rules, variables and hex colors', () => {
52
+ const cssCode = `@theme {
53
+ --color-primary-500: #ff3e00;
54
+ display: flex;
55
+ }`;
56
+ const tokens = tokenizeCode(cssCode, 'css');
57
+ expect(tokens).toHaveLength(4);
58
+ const line0 = tokens[0].map((t) => ({ text: t.text, type: t.type }));
59
+ expect(line0).toContainEqual({ text: '@theme', type: 'directive' });
60
+ const line1 = tokens[1].map((t) => ({ text: t.text, type: t.type }));
61
+ expect(line1).toContainEqual({ text: '--color-primary-500', type: 'macro' });
62
+ expect(line1).toContainEqual({ text: '#ff3e00', type: 'number' });
63
+ const line2 = tokens[2].map((t) => ({ text: t.text, type: t.type }));
64
+ expect(line2).toContainEqual({ text: 'display', type: 'keyword' });
65
+ });
66
+ it('tokenizes svelte 5 runes and control flow blocks', () => {
67
+ const svelteCode = `let count = $state(0);
68
+ {#if count > 0}
69
+ <span>Positive</span>
70
+ {/if}`;
71
+ const tokens = tokenizeCode(svelteCode, 'svelte');
72
+ expect(tokens).toHaveLength(4);
73
+ const line0 = tokens[0].map((t) => ({ text: t.text, type: t.type }));
74
+ expect(line0).toContainEqual({ text: '$state', type: 'rune' });
75
+ const line1 = tokens[1].map((t) => ({ text: t.text, type: t.type }));
76
+ expect(line1).toContainEqual({ text: '{#if', type: 'directive' });
77
+ const line3 = tokens[3].map((t) => ({ text: t.text, type: t.type }));
78
+ expect(line3).toContainEqual({ text: '{/if', type: 'directive' });
79
+ });
80
+ it('tokenizes json object keys separately from string values', () => {
81
+ const jsonCode = '{\n "version": "2.0.2"\n}';
82
+ const tokens = tokenizeCode(jsonCode, 'json');
83
+ expect(tokens).toHaveLength(3);
84
+ const line1 = tokens[1].map((t) => ({ text: t.text, type: t.type }));
85
+ expect(line1).toContainEqual({ text: '"version"', type: 'attr' });
86
+ expect(line1).toContainEqual({ text: '"2.0.2"', type: 'string' });
87
+ });
88
+ it('tokenizes bash environment variables and flags', () => {
89
+ const bashCode = 'curl -s -L $API_URL --output file.txt';
90
+ const tokens = tokenizeCode(bashCode, 'bash');
91
+ expect(tokens).toHaveLength(1);
92
+ const line0 = tokens[0].map((t) => ({ text: t.text, type: t.type }));
93
+ expect(line0).toContainEqual({ text: 'curl', type: 'keyword' });
94
+ expect(line0).toContainEqual({ text: '$API_URL', type: 'macro' });
95
+ expect(line0).toContainEqual({ text: '--output', type: 'attr' });
96
+ });
97
+ it('maintains multiline block comments in typescript', () => {
98
+ const tsCode = `/*
99
+ * Description of function
100
+ */
101
+ function test() {}`;
102
+ const tokens = tokenizeCode(tsCode, 'typescript');
103
+ expect(tokens).toHaveLength(4);
104
+ expect(tokens[0][0].type).toBe('comment');
105
+ expect(tokens[1][0].type).toBe('comment');
106
+ expect(tokens[2][0].type).toBe('comment');
107
+ expect(tokens[3].some((t) => t.text === 'function' && t.type === 'keyword')).toBe(true);
108
+ });
109
+ it('supports ES2026 and TypeScript 6 keywords, utility types, and modern numeric separators', () => {
110
+ const modernCode = `import defer * as mod from './heavy-module';
111
+ using file = openFile();
112
+ class Service {
113
+ accessor count = 0;
114
+ override process() {}
115
+ }
116
+ function check(val: unknown): asserts val is string {
117
+ if (typeof val !== 'string') throw new Error();
118
+ }
119
+ const amount = 1_000_000n;
120
+ const mask = 0xFF_AA_00;
121
+ type TaskResult = Awaited<Promise<Record<string, unknown>>>;`;
122
+ const tokens = tokenizeCode(modernCode, 'typescript');
123
+ expect(tokens.length).toBeGreaterThan(5);
124
+ // Line 0: import defer
125
+ const line0 = tokens[0].map((t) => ({ text: t.text, type: t.type }));
126
+ expect(line0).toContainEqual({ text: 'import', type: 'keyword' });
127
+ expect(line0).toContainEqual({ text: 'defer', type: 'keyword' });
128
+ // Line 1: using file
129
+ const line1 = tokens[1].map((t) => ({ text: t.text, type: t.type }));
130
+ expect(line1).toContainEqual({ text: 'using', type: 'keyword' });
131
+ // Line 3: accessor count
132
+ const line3 = tokens[3].map((t) => ({ text: t.text, type: t.type }));
133
+ expect(line3).toContainEqual({ text: 'accessor', type: 'keyword' });
134
+ // Line 4: override process
135
+ const line4 = tokens[4].map((t) => ({ text: t.text, type: t.type }));
136
+ expect(line4).toContainEqual({ text: 'override', type: 'keyword' });
137
+ // Line 6: asserts val is string
138
+ const line6 = tokens[6].map((t) => ({ text: t.text, type: t.type }));
139
+ expect(line6).toContainEqual({ text: 'asserts', type: 'keyword' });
140
+ expect(line6).toContainEqual({ text: 'is', type: 'keyword' });
141
+ expect(line6).toContainEqual({ text: 'unknown', type: 'macro' });
142
+ expect(line6).toContainEqual({ text: 'string', type: 'macro' });
143
+ // Line 9: amount = 1_000_000n
144
+ const line9 = tokens[9].map((t) => ({ text: t.text, type: t.type }));
145
+ expect(line9).toContainEqual({ text: '1_000_000n', type: 'number' });
146
+ // Line 10: mask = 0xFF_AA_00
147
+ const line10 = tokens[10].map((t) => ({ text: t.text, type: t.type }));
148
+ expect(line10).toContainEqual({ text: '0xFF_AA_00', type: 'number' });
149
+ // Line 11: Awaited utility type
150
+ const line11 = tokens[11].map((t) => ({ text: t.text, type: t.type }));
151
+ expect(line11).toContainEqual({ text: 'Awaited', type: 'macro' });
152
+ });
26
153
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaxa-svelte",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "The Intuitive Svelte UI Library — Nuxt UI v4 equivalent for SvelteKit 2 & 3 & Svelte 5 with automated SEO, dynamic OpenGraph cards, sitemaps, robots.txt, and Tailwind CSS v4.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://yaxa.vercel.app",