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)
|
|
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
|
-
//
|
|
54
|
-
if (
|
|
55
|
-
|
|
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
|
-
|
|
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 &&
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
174
|
-
const
|
|
175
|
-
/^(
|
|
408
|
+
// 14. Standard Strings (Single & Double Quoted)
|
|
409
|
+
const strMatch = remaining.match(
|
|
410
|
+
/^("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/
|
|
176
411
|
);
|
|
177
|
-
if (
|
|
178
|
-
tokens.push({ text:
|
|
179
|
-
remaining = remaining.slice(
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
210
|
-
const numMatch = remaining.match(
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
+
"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",
|