@thyn/core 0.0.343 → 0.0.346

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.
Files changed (105) hide show
  1. package/.github/workflows/static.yml +48 -0
  2. package/.github/workflows/test.yml +39 -0
  3. package/LICENSE +21 -0
  4. package/README.md +50 -0
  5. package/dist/{element.js → core/element.js} +14 -36
  6. package/dist/core/index.d.ts +1 -0
  7. package/dist/core/index.js +1 -0
  8. package/dist/index.d.ts +5 -2
  9. package/dist/index.js +5 -2
  10. package/dist/plugin/html-parser.d.ts +31 -0
  11. package/dist/plugin/html-parser.js +275 -0
  12. package/dist/plugin/index.d.ts +24 -0
  13. package/dist/plugin/index.js +1009 -0
  14. package/dist/plugin/utils.d.ts +12 -0
  15. package/dist/plugin/utils.js +194 -0
  16. package/docs/CNAME +1 -0
  17. package/docs/index.html +18 -0
  18. package/docs/package-lock.json +980 -0
  19. package/docs/package.json +15 -0
  20. package/docs/public/thyn.png +0 -0
  21. package/docs/public/thyn.svg +1 -0
  22. package/docs/src/App.thyn +10 -0
  23. package/docs/src/components/Button.thyn +3 -0
  24. package/docs/src/docs/GettingStarted.thyn +8 -0
  25. package/docs/src/main.css +17 -0
  26. package/docs/src/main.js +5 -0
  27. package/docs/src/pages/Home.thyn +147 -0
  28. package/docs/vite.config.js +7 -0
  29. package/package.json +18 -10
  30. package/src/{element.ts → core/element.ts} +14 -34
  31. package/src/core/index.ts +1 -0
  32. package/src/{signals.ts → core/signals.ts} +1 -1
  33. package/src/index.ts +5 -15
  34. package/src/plugin/html-parser.ts +332 -0
  35. package/src/plugin/index.ts +1127 -0
  36. package/src/plugin/utils.ts +213 -0
  37. package/tests/Bind.test.ts +14 -0
  38. package/tests/Bind.thyn +7 -0
  39. package/tests/ConsecInterps.test.ts +9 -0
  40. package/tests/ConsecInterps.thyn +9 -0
  41. package/tests/Counter.test.ts +12 -0
  42. package/tests/Counter.thyn +7 -0
  43. package/tests/DoubleQuotes.test.ts +9 -0
  44. package/tests/DoubleQuotes.thyn +3 -0
  45. package/tests/Escape.test.ts +9 -0
  46. package/tests/Escape.thyn +3 -0
  47. package/tests/EscapeDollar.test.ts +9 -0
  48. package/tests/EscapeDollar.thyn +5 -0
  49. package/tests/EventPipes.test.ts +13 -0
  50. package/tests/EventPipes.thyn +11 -0
  51. package/tests/List.test.ts +21 -0
  52. package/tests/List.thyn +15 -0
  53. package/tests/ListV2.test.ts +20 -0
  54. package/tests/ListV2.thyn +16 -0
  55. package/tests/MixElemAndText.test.ts +9 -0
  56. package/tests/MixElemAndText.thyn +12 -0
  57. package/tests/Show.test.ts +13 -0
  58. package/tests/Show.thyn +8 -0
  59. package/tests/Template.test.ts +9 -0
  60. package/tests/Template.thyn +8 -0
  61. package/tests/list/comprehensive.test.ts +659 -0
  62. package/tests/list/operations/ChildrenAppend.thyn +11 -0
  63. package/tests/list/operations/ChildrenFilter.thyn +11 -0
  64. package/tests/list/operations/ChildrenInsert.thyn +11 -0
  65. package/tests/list/operations/ChildrenNoneToSome.thyn +11 -0
  66. package/tests/list/operations/ChildrenPrepend.thyn +11 -0
  67. package/tests/list/operations/ChildrenRemove.thyn +11 -0
  68. package/tests/list/operations/ChildrenReplaceAll.thyn +11 -0
  69. package/tests/list/operations/ChildrenSomeToNone.thyn +11 -0
  70. package/tests/list/operations/ChildrenSort.thyn +11 -0
  71. package/tests/list/operations/IsolatedAppend.thyn +10 -0
  72. package/tests/list/operations/IsolatedFilter.thyn +16 -0
  73. package/tests/list/operations/IsolatedInsert.thyn +10 -0
  74. package/tests/list/operations/IsolatedMove.thyn +16 -0
  75. package/tests/list/operations/IsolatedNoneToSome.thyn +16 -0
  76. package/tests/list/operations/IsolatedPrepend.thyn +10 -0
  77. package/tests/list/operations/IsolatedRemove.thyn +17 -0
  78. package/tests/list/operations/IsolatedReplaceAll.thyn +10 -0
  79. package/tests/list/operations/IsolatedSomeToNone.thyn +10 -0
  80. package/tests/list/operations/IsolatedSort.thyn +16 -0
  81. package/tests/list/operations/TerminalAppend.thyn +12 -0
  82. package/tests/list/operations/TerminalFilter.thyn +12 -0
  83. package/tests/list/operations/TerminalInsert.thyn +12 -0
  84. package/tests/list/operations/TerminalNoneToSome.thyn +12 -0
  85. package/tests/list/operations/TerminalPrepend.thyn +12 -0
  86. package/tests/list/operations/TerminalRemove.thyn +12 -0
  87. package/tests/list/operations/TerminalReplaceAll.thyn +12 -0
  88. package/tests/list/operations/TerminalSomeToNone.thyn +12 -0
  89. package/tests/list/operations/TerminalSort.thyn +12 -0
  90. package/tests/tsconfig.json +14 -0
  91. package/tsconfig.json +11 -6
  92. package/types/thyn.d.ts +4 -0
  93. package/vitest.config.ts +7 -2
  94. package/tests/fx.test.ts +0 -31
  95. package/tests/lists.test.ts +0 -184
  96. package/tests/router.test.ts +0 -69
  97. package/tests/show.test.ts +0 -66
  98. package/tests/utils.ts +0 -3
  99. package/tsconfig.tsbuildinfo +0 -1
  100. /package/dist/{element.d.ts → core/element.d.ts} +0 -0
  101. /package/dist/{router.d.ts → core/router.d.ts} +0 -0
  102. /package/dist/{router.js → core/router.js} +0 -0
  103. /package/dist/{signals.d.ts → core/signals.d.ts} +0 -0
  104. /package/dist/{signals.js → core/signals.js} +0 -0
  105. /package/src/{router.ts → core/router.ts} +0 -0
@@ -0,0 +1,1009 @@
1
+ import * as acorn from "acorn";
2
+ import * as acornwalk from "acorn-walk";
3
+ import MagicString from "magic-string";
4
+ import postcss from 'postcss';
5
+ import selectorParser from 'postcss-selector-parser';
6
+ import { parseHTML } from "./html-parser.js";
7
+ import { escapeHtml, escapeTemplateLiteral, extractParts, splitScript } from "./utils.js";
8
+ const esbuildModule = import("esbuild");
9
+ async function scopeSelectors(css, scopeId) {
10
+ const result = await postcss([
11
+ {
12
+ postcssPlugin: 'postcss-scope-thyn',
13
+ Rule(rule) {
14
+ if (!rule.selector)
15
+ return;
16
+ const processor = selectorParser((selectors) => {
17
+ selectors.walkClasses(() => { }); // just to trigger full parse
18
+ selectors.each((sel) => {
19
+ sel.append(selectorParser.className({ value: scopeId }));
20
+ });
21
+ });
22
+ rule.selector = processor.processSync(rule.selector);
23
+ },
24
+ },
25
+ ]).process(css, { from: undefined });
26
+ return result.css;
27
+ }
28
+ const DIRECTIVES = ["#for", "#if", "#then", "#else", "#else-if", "#props"];
29
+ function isReactiveExpression(expr) {
30
+ const ast = acorn.parseExpressionAt(expr, 0, { ecmaVersion: 2022 });
31
+ if (["ArrowFunctionExpression", "FunctionExpression"].includes(ast.type)) {
32
+ return false;
33
+ }
34
+ let isReactive = false;
35
+ acornwalk.simple(ast, {
36
+ CallExpression(node) {
37
+ isReactive = true;
38
+ },
39
+ });
40
+ return isReactive;
41
+ }
42
+ const DOUBLE_QUOTE = "__THYN__DOUBLE_QUOTE__";
43
+ const PLACEHOLDER_CHAR = "\uE000";
44
+ function parseAttributes(el) {
45
+ const result = {};
46
+ for (const attr of el.attributes) {
47
+ let { name, value } = attr;
48
+ name = name.replace(/__thyn_attribute_(:?[a-z-]+)/g, (match, kebabName) => {
49
+ const camelCase = kebabName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
50
+ return camelCase;
51
+ });
52
+ if (name.startsWith(":")) {
53
+ if (name.startsWith(":on")) {
54
+ let pipes;
55
+ [name, ...pipes] = name.split(".");
56
+ for (const pipe of pipes) {
57
+ if (pipe === "stop") {
58
+ value = `(e) => { e.stopPropagation(); (${value})(e); }`;
59
+ }
60
+ else if (pipe === "prevent") {
61
+ value = `(e) => { e.preventDefault(); (${value})(e); }`;
62
+ }
63
+ else if (pipe === "enter") {
64
+ value = `(e) => { if (e.key === 'Enter') (${value})(e); }`;
65
+ }
66
+ else if (pipe === "ctrl") {
67
+ value = `(e) => { if (e.ctrlKey) (${value})(e); }`;
68
+ }
69
+ else if (pipe === "meta") {
70
+ value = `(e) => { if (e.metaKey) (${value})(e); }`;
71
+ }
72
+ else if (pipe === "alt") {
73
+ value = `(e) => { if (e.altKey) (${value})(e); }`;
74
+ }
75
+ else if (pipe === "shift") {
76
+ value = `(e) => { if (e.shiftKey) (${value})(e); }`;
77
+ }
78
+ else {
79
+ throw new Error(`Unknown event modifier: ${pipe}`);
80
+ }
81
+ }
82
+ }
83
+ const reactive = value && isReactiveExpression(value);
84
+ if (reactive && name !== ":#for" && name !== ":#if" &&
85
+ name !== ":#else-if" && name !== ":#else" && name !== ":#props") {
86
+ value = `() => ${value}`;
87
+ result[name.slice(1)] = { raw: value.replace(new RegExp(DOUBLE_QUOTE, "g"), '"') };
88
+ }
89
+ else {
90
+ result[name.slice(1)] = { raw: value.replace(new RegExp(DOUBLE_QUOTE, "g"), '"') };
91
+ }
92
+ }
93
+ else {
94
+ result[name] = { quoted: value };
95
+ }
96
+ }
97
+ return result;
98
+ }
99
+ function parseTextContent(text) {
100
+ text = text.trim();
101
+ // First, handle escaped braces by temporarily replacing them
102
+ const escapedOpenBrace = '\u0001'; // Use control character as placeholder
103
+ const escapedCloseBrace = '\u0002';
104
+ // Replace escaped braces with placeholders
105
+ text = text.replace(/\\(\{\{|\}\})/g, (match, braces) => {
106
+ return braces === '{{' ? escapedOpenBrace : escapedCloseBrace;
107
+ });
108
+ const regex = /\{\{([^}]+)\}\}/g;
109
+ let lastIndex = 0;
110
+ let match;
111
+ const parts = [];
112
+ let hasReactive = false;
113
+ let hasInterpolations = false;
114
+ while ((match = regex.exec(text)) !== null) {
115
+ const staticText = text.slice(lastIndex, match.index);
116
+ if (staticText) {
117
+ // Restore escaped braces in static text
118
+ const restoredText = staticText
119
+ .replace(new RegExp(escapedOpenBrace, 'g'), '{{')
120
+ .replace(new RegExp(escapedCloseBrace, 'g'), '}}');
121
+ parts.push(restoredText);
122
+ }
123
+ const expr = match[1].trim();
124
+ const isReactive = isReactiveExpression(expr);
125
+ hasReactive || (hasReactive = isReactive);
126
+ hasInterpolations = true;
127
+ parts.push({ expr, isReactive });
128
+ lastIndex = regex.lastIndex;
129
+ }
130
+ if (lastIndex < text.length) {
131
+ // Restore escaped braces in remaining text
132
+ const remainingText = text.slice(lastIndex)
133
+ .replace(new RegExp(escapedOpenBrace, 'g'), '{{')
134
+ .replace(new RegExp(escapedCloseBrace, 'g'), '}}');
135
+ parts.push(remainingText);
136
+ }
137
+ if (!hasInterpolations) {
138
+ // Restore escaped braces if no interpolations
139
+ const finalText = text
140
+ .replace(new RegExp(escapedOpenBrace, 'g'), '{{')
141
+ .replace(new RegExp(escapedCloseBrace, 'g'), '}}');
142
+ return {
143
+ code: `document.createTextNode(\`${escapeTemplateLiteral(finalText)}\`)`,
144
+ hasReactive: false,
145
+ };
146
+ }
147
+ const interpolated = parts.map((part) => {
148
+ if (typeof part === "string") {
149
+ return escapeTemplateLiteral(part);
150
+ }
151
+ return `$\{${part.expr}\}`;
152
+ }).join("");
153
+ if (hasReactive) {
154
+ let code = `__THYN__CORE__.createReactiveTextNode(() => \`${interpolated}\`)`;
155
+ if (parts.length === 1) {
156
+ const ast = acorn.parseExpressionAt(interpolated.slice(2, -1), 0, {
157
+ ecmaVersion: 2022,
158
+ });
159
+ if (ast.type === "CallExpression" && !ast.arguments.length) {
160
+ code = `__THYN__CORE__.createReactiveTextNode(${interpolated.slice(2, -1).replace(/\(\s*\)\s*$/, "")})`;
161
+ }
162
+ }
163
+ return {
164
+ code,
165
+ hasReactive,
166
+ };
167
+ }
168
+ if (parts.length === 1) {
169
+ return {
170
+ code: `document.createTextNode(${interpolated.slice(2, -1)})`,
171
+ hasReactive,
172
+ };
173
+ }
174
+ return {
175
+ code: `document.createTextNode(\`${interpolated}\`)`,
176
+ hasReactive,
177
+ };
178
+ }
179
+ function generateTextContentTemplate(text, parent, prevSibling) {
180
+ text = text.trim();
181
+ // First, handle escaped braces by temporarily replacing them
182
+ const escapedOpenBrace = '\u0001'; // Use control character as placeholder
183
+ const escapedCloseBrace = '\u0002';
184
+ // Replace escaped braces with placeholders
185
+ text = text.replace(/\\(\{\{|\}\})/g, (match, braces) => {
186
+ return braces === '{{' ? escapedOpenBrace : escapedCloseBrace;
187
+ });
188
+ const regex = /\{\{([^}]+)\}\}/g;
189
+ let lastIndex = 0;
190
+ let match;
191
+ const parts = [];
192
+ let hasReactive = false;
193
+ let hasInterpolations = false;
194
+ while ((match = regex.exec(text)) !== null) {
195
+ const staticText = text.slice(lastIndex, match.index);
196
+ if (staticText) {
197
+ // Restore escaped braces in static text
198
+ const restoredText = staticText
199
+ .replace(new RegExp(escapedOpenBrace, 'g'), '{{')
200
+ .replace(new RegExp(escapedCloseBrace, 'g'), '}}');
201
+ parts.push(restoredText);
202
+ }
203
+ const expr = match[1].trim();
204
+ const isReactive = isReactiveExpression(expr);
205
+ hasReactive || (hasReactive = isReactive);
206
+ hasInterpolations = true;
207
+ parts.push({ expr, isReactive });
208
+ lastIndex = regex.lastIndex;
209
+ }
210
+ if (lastIndex < text.length) {
211
+ // Restore escaped braces in remaining text
212
+ const remainingText = text.slice(lastIndex)
213
+ .replace(new RegExp(escapedOpenBrace, 'g'), '{{')
214
+ .replace(new RegExp(escapedCloseBrace, 'g'), '}}');
215
+ parts.push(remainingText);
216
+ }
217
+ const root = makeVariable();
218
+ if (!hasInterpolations) {
219
+ // Restore escaped braces if no interpolations
220
+ const finalText = text
221
+ .replace(new RegExp(escapedOpenBrace, 'g'), '{{')
222
+ .replace(new RegExp(escapedCloseBrace, 'g'), '}}');
223
+ return {
224
+ static: escapeHtml(finalText),
225
+ dynamic: "",
226
+ root: "",
227
+ staticRoot: root,
228
+ };
229
+ }
230
+ const interpolated = parts.map((part) => {
231
+ if (typeof part === "string") {
232
+ return escapeTemplateLiteral(part);
233
+ }
234
+ return `$\{${part.expr}\}`;
235
+ }).join("");
236
+ const textNode = prevSibling
237
+ ? `${prevSibling}.nextSibling`
238
+ : `${parent}.firstChild`;
239
+ if (hasReactive) {
240
+ let fn = `\`${interpolated}\``;
241
+ if (parts.length === 1) {
242
+ const ast = acorn.parseExpressionAt(interpolated.slice(2, -1), 0, {
243
+ ecmaVersion: 2022,
244
+ });
245
+ if (ast.type === "CallExpression" && !ast.arguments.length) {
246
+ fn = interpolated.slice(2, -1).replace(/\(\s*\)\s*$/, "") + "()";
247
+ }
248
+ else {
249
+ fn = interpolated.slice(2, -1);
250
+ }
251
+ }
252
+ // Use a space as placeholder for the text node
253
+ const stat = PLACEHOLDER_CHAR;
254
+ const dynamic = `__THYN__CORE__.staticEffect(() => {
255
+ ${textNode}.nodeValue = ${fn};
256
+ });\n`;
257
+ return {
258
+ static: stat,
259
+ dynamic,
260
+ root: "",
261
+ staticRoot: root,
262
+ };
263
+ }
264
+ if (parts.length === 1) {
265
+ return {
266
+ dynamic: `${textNode}.nodeValue = ${interpolated.slice(2, -1)};\n`,
267
+ static: PLACEHOLDER_CHAR,
268
+ root: "",
269
+ staticRoot: root,
270
+ };
271
+ }
272
+ return {
273
+ dynamic: `${textNode}.nodeValue = \`${interpolated}\`;\n`,
274
+ static: PLACEHOLDER_CHAR,
275
+ root: "",
276
+ staticRoot: root,
277
+ };
278
+ }
279
+ const NAMESPACE = "__THYN__";
280
+ const HOIST_PREFIX = `${NAMESPACE}HOIST__`;
281
+ function cloneIfNeeded(code) {
282
+ if (code.startsWith(HOIST_PREFIX)) {
283
+ return `${code}.cloneNode()`;
284
+ }
285
+ return code;
286
+ }
287
+ function createHoisting(code, hoist) {
288
+ const existing = hoist.find((h) => h.endsWith(` = ${code};`));
289
+ if (existing) {
290
+ return existing.slice(6, existing.indexOf(" = "));
291
+ }
292
+ const id = `${HOIST_PREFIX}${hoist.length}`;
293
+ hoist.push(`const ${id} = ${code};`);
294
+ return id;
295
+ }
296
+ function createObjectCode(obj) {
297
+ const keys = Object.keys(obj);
298
+ if (!keys.length)
299
+ return "null";
300
+ return `{${keys.map((k) => `'${k}': ${obj[k]}`).join(",")}}`;
301
+ }
302
+ let varId = 0;
303
+ function makeVariable() {
304
+ return `${NAMESPACE}${varId++}`;
305
+ }
306
+ function makeTemplate(node, parent, prevSibling) {
307
+ if (node.nodeType === 3) {
308
+ const text = node.textContent;
309
+ return generateTextContentTemplate(text, parent, prevSibling);
310
+ }
311
+ const tag = node.tagName.toLowerCase();
312
+ const attrs = parseAttributes(node);
313
+ let statRoot = makeVariable();
314
+ let template = `<${tag}`;
315
+ if (!parent) {
316
+ statRoot = "__THYN__template";
317
+ }
318
+ let code = "";
319
+ let dynRoot = makeVariable();
320
+ const childNodes = Array.from(node.childNodes).filter((n) => n.nodeType !== 3 || n.textContent.trim());
321
+ const children = [];
322
+ let ps = undefined;
323
+ for (let i = 0; i < childNodes.length; i++) {
324
+ const cn = childNodes[i];
325
+ const ch = makeTemplate(cn, dynRoot, ps);
326
+ children.push(ch);
327
+ ps = ch.root || `${dynRoot}.childNodes[${i}]`;
328
+ }
329
+ if (!parent) {
330
+ code = `const ${dynRoot} = __THYN__template_generate();\n`;
331
+ }
332
+ else if (!prevSibling) {
333
+ code = `const ${dynRoot} = ${parent}.firstChild;\n`;
334
+ }
335
+ else {
336
+ code = `const ${dynRoot} = ${prevSibling}.nextSibling;\n`;
337
+ }
338
+ for (const [key, val] of Object.entries(attrs)) {
339
+ if (DIRECTIVES.includes(key))
340
+ continue;
341
+ if ("quoted" in val) {
342
+ if (key === "class") {
343
+ template += ` class="${escapeHtml(val.quoted)}"`;
344
+ }
345
+ else {
346
+ template += ` ${key}="${escapeHtml(val.quoted)}"`;
347
+ }
348
+ }
349
+ }
350
+ template += ">";
351
+ for (const [key, val] of Object.entries(attrs)) {
352
+ if (DIRECTIVES.includes(key))
353
+ continue;
354
+ if (!("raw" in val))
355
+ continue;
356
+ if (key.startsWith("on")) {
357
+ code += `${dynRoot}.${key} = ${val.raw};\n`;
358
+ continue;
359
+ }
360
+ const reactive = isReactiveExpression(val.raw.replace(/^\(\) => /, ""));
361
+ if (reactive) {
362
+ val.raw = val.raw.replace(/^\(\) => /, "");
363
+ if (key === "class") {
364
+ const prev = makeVariable();
365
+ code += `let ${prev};\n`;
366
+ code += `__THYN__CORE__.staticEffect(() => {
367
+ const val = ${val.raw};
368
+ if (val !== ${prev}) {
369
+ if (${prev} = val) ${dynRoot}.className = val;
370
+ else ${dynRoot}.removeAttribute("class");
371
+ }
372
+ });\n`;
373
+ continue;
374
+ }
375
+ if (key.includes("-")) {
376
+ const ran = makeVariable();
377
+ code += `let ${ran};\n`;
378
+ code += `__THYN__CORE__.staticEffect(() => {
379
+ const val = ${val.raw};
380
+ if (val === undefined) {
381
+ if (${ran} && ${dynRoot}.hasAttribute("${key}")) {
382
+ ${dynRoot}.removeAttribute("${key}");
383
+ }
384
+ } else {
385
+ ${dynRoot}.setAttribute("${key}", val);\n
386
+ }
387
+ ${ran} = true;
388
+ });\n`;
389
+ }
390
+ else {
391
+ const ran = makeVariable();
392
+ code += `let ${ran} = false;\n`;
393
+ code += `__THYN__CORE__.staticEffect(() => {
394
+ const val = ${val.raw};
395
+ if (val || ${ran}) {
396
+ ${dynRoot}.${key} = val;\n
397
+ }
398
+ ${ran} = true;
399
+ });\n`;
400
+ }
401
+ continue;
402
+ }
403
+ if (key === "class") {
404
+ code += `${dynRoot}.className = ${val.raw};\n`;
405
+ continue;
406
+ }
407
+ if (key.includes("-")) {
408
+ code += `${dynRoot}.setAttribute("${key}", ${val.raw});\n`;
409
+ }
410
+ else {
411
+ code += `${dynRoot}.${key} = ${val.raw};\n`;
412
+ }
413
+ }
414
+ if (children.length) {
415
+ for (let i = 0; i < children.length; i++) {
416
+ const ch = children[i];
417
+ if (ch.static) {
418
+ template += ch.static;
419
+ }
420
+ if (ch.dynamic) {
421
+ code += ch.dynamic;
422
+ }
423
+ }
424
+ }
425
+ template += `</${tag}>`;
426
+ return {
427
+ root: dynRoot,
428
+ staticRoot: statRoot,
429
+ static: template,
430
+ dynamic: code,
431
+ };
432
+ }
433
+ function walkConditionChain(nodes, i) {
434
+ const chain = [];
435
+ for (; i < nodes.length; i++) {
436
+ const node = nodes[i];
437
+ if (node.nodeType !== 1)
438
+ continue;
439
+ const el = node;
440
+ const attrs = parseAttributes(el);
441
+ if ("#if" in attrs || "#else-if" in attrs || "#else" in attrs) {
442
+ chain.push({ node, attrs });
443
+ }
444
+ else {
445
+ break;
446
+ }
447
+ if ("#else" in attrs)
448
+ break;
449
+ }
450
+ return chain;
451
+ }
452
+ function walk(node, hoist, siblings, index) {
453
+ if (node.nodeType === 3) {
454
+ const text = node.textContent;
455
+ if (!text.trim())
456
+ return null;
457
+ const result = parseTextContent(text);
458
+ return {
459
+ code: result.code,
460
+ isComponent: false,
461
+ hasReactive: result.hasReactive,
462
+ };
463
+ }
464
+ if (node.nodeType !== 1)
465
+ return null;
466
+ const el = node;
467
+ const tag = el.tagName.toLowerCase();
468
+ let isComponent = el.hasAttribute("__thyn_component");
469
+ const makeArg = isComponent
470
+ ? el.getAttribute("__thyn_component")
471
+ : `"${tag}"`;
472
+ el.removeAttribute("__thyn_component");
473
+ const attrs = parseAttributes(el);
474
+ if (isComponent)
475
+ el.setAttribute("__thyn_component", makeArg);
476
+ const children = [];
477
+ let skip = 0;
478
+ for (let i = 0; i < node.childNodes.length; i++) {
479
+ const result = walk(node.childNodes[i], hoist, node.childNodes, i);
480
+ if (result) {
481
+ if (skip) {
482
+ skip--;
483
+ continue;
484
+ }
485
+ children.push(result);
486
+ if ("skip" in result) {
487
+ skip = result.skip;
488
+ }
489
+ }
490
+ }
491
+ const hasReactiveChildren = children.some((c) => c.hasReactive);
492
+ const hasComponentChildren = children.some((c) => c.isComponent);
493
+ let hasReactive = hasReactiveChildren || hasComponentChildren;
494
+ if (tag === "slot") {
495
+ return {
496
+ code: `...$props.slot ?? ${children.map((c) => cloneIfNeeded(c.code)).join(", ") || "[]"}`,
497
+ isComponent: false,
498
+ hasReactive,
499
+ };
500
+ }
501
+ let code = "";
502
+ let hasOwnEffects = false;
503
+ if (isComponent) {
504
+ const props = {};
505
+ let propsDirective = null;
506
+ let propCount = 0;
507
+ for (const [key, val] of Object.entries(attrs)) {
508
+ if (["#for", "#if", "#then", "#else", "#else-if"].includes(key)) {
509
+ continue;
510
+ }
511
+ if (key === "#props") {
512
+ propsDirective = "raw" in val ? val.raw : JSON.stringify(val.quoted);
513
+ continue;
514
+ }
515
+ const value = "raw" in val ? val.raw : JSON.stringify(val.quoted);
516
+ props[key] = value;
517
+ propCount++;
518
+ }
519
+ if (children.length) {
520
+ props.slot = `[${children.map((c) => cloneIfNeeded(c.code)).join(", ")}]`;
521
+ propCount++;
522
+ }
523
+ let propsCode;
524
+ if (propsDirective) {
525
+ propsCode = propsDirective;
526
+ }
527
+ else {
528
+ propsCode = createObjectCode(props);
529
+ }
530
+ code = `__THYN__CORE__.${hasComponentChildren ? 'component' : 'fixedComponent'}(${makeArg}, ${propsCode})`;
531
+ }
532
+ else {
533
+ code = createHoisting(`document.createElement(${makeArg})`, hoist);
534
+ for (const [key, val] of Object.entries(attrs)) {
535
+ if (DIRECTIVES.includes(key))
536
+ continue;
537
+ if ("quoted" in val) {
538
+ if (key === "class" || key.includes("-")) {
539
+ code = createHoisting(`__THYN__CORE__.setAttribute(${cloneIfNeeded(code)}, "${key}", "${val.quoted}")`, hoist);
540
+ }
541
+ else {
542
+ code = createHoisting(`__THYN__CORE__.setProperty(${cloneIfNeeded(code)}, "${key}", "${val.quoted}")`, hoist);
543
+ }
544
+ }
545
+ }
546
+ for (const [key, val] of Object.entries(attrs)) {
547
+ if (DIRECTIVES.includes(key))
548
+ continue;
549
+ if (!("raw" in val))
550
+ continue;
551
+ if (key.startsWith("on")) {
552
+ code = `__THYN__CORE__.setProperty(${cloneIfNeeded(code)}, "${key}", ${val.raw})`;
553
+ continue;
554
+ }
555
+ const reactive = isReactiveExpression(val.raw.replace(/^\(\) => /, ""));
556
+ if (reactive) {
557
+ hasOwnEffects = true;
558
+ hasReactive = true;
559
+ if (key === "class" || key.includes("-")) {
560
+ code = `__THYN__CORE__.setReactiveAttribute(${cloneIfNeeded(code)}, "${key}", ${val.raw})`;
561
+ }
562
+ else {
563
+ code = `__THYN__CORE__.setReactiveProperty(${cloneIfNeeded(code)}, "${key}", ${val.raw})`;
564
+ }
565
+ continue;
566
+ }
567
+ if (key === "class" || key.includes("-")) {
568
+ code = `__THYN__CORE__.setAttribute(${cloneIfNeeded(code)}, "${key}", ${val.raw})`;
569
+ }
570
+ else {
571
+ code = `__THYN__CORE__.setProperty(${cloneIfNeeded(code)}, "${key}", ${val.raw})`;
572
+ }
573
+ }
574
+ if (children.length) {
575
+ code = `__THYN__CORE__.addChildren(${cloneIfNeeded(code)}, [${children.map((c) => cloneIfNeeded(c.code)).join(", ")}])`;
576
+ }
577
+ if (!hasOwnEffects && hasReactiveChildren) {
578
+ code = `__THYN__CORE__.markAsReactive(${cloneIfNeeded(code)})`;
579
+ }
580
+ }
581
+ if ("#for" in attrs && "raw" in attrs["#for"]) {
582
+ const isolated = siblings ? Array.from(siblings).filter((n) => n.nodeType !== 3 || n.textContent.trim()).length === 1 : true;
583
+ const forAttr = attrs["#for"].raw;
584
+ const [item, iterable] = forAttr.split(" in ").map((s) => s.trim());
585
+ code = `__THYN__CORE__.component(${hasComponentChildren
586
+ ? "__THYN__CORE__.list"
587
+ : (isolated ? "__THYN__CORE__.isolatedTerminalList" : "__THYN__CORE__.terminalList")}, {
588
+ items: () => ${iterable},
589
+ render: (${item}) => ${code},
590
+ })`;
591
+ isComponent = true;
592
+ }
593
+ if ("#if" in attrs) {
594
+ const chain = walkConditionChain(siblings, index);
595
+ const branches = [];
596
+ for (const entry of chain) {
597
+ const subEl = entry.node;
598
+ const subAttrs = entry.attrs;
599
+ subEl.removeAttribute(":#if");
600
+ subEl.removeAttribute(":#else-if");
601
+ subEl.removeAttribute(":#else");
602
+ const ch = walk(subEl, hoist);
603
+ if ("#if" in subAttrs || "#else-if" in subAttrs) {
604
+ const cond = subAttrs["#if"]?.raw ?? subAttrs["#else-if"].raw;
605
+ branches.push(`{ if: () => ${cond}, then: () => ${ch.code} }`);
606
+ }
607
+ else if ("#else" in subAttrs) {
608
+ branches.push(`{ then: () => ${ch.code} }`);
609
+ }
610
+ }
611
+ code = `__THYN__CORE__.component(__THYN__CORE__.show, [\n${branches.join(",\n")}\n])`;
612
+ return {
613
+ code,
614
+ isComponent: true,
615
+ hasReactive: true,
616
+ skip: chain.length - 1,
617
+ };
618
+ }
619
+ return {
620
+ code,
621
+ isComponent,
622
+ hasReactive: hasOwnEffects || isComponent || hasReactiveChildren,
623
+ };
624
+ }
625
+ function hasComponentChildren(node) {
626
+ if (node.nodeType === 3) {
627
+ return false;
628
+ }
629
+ if (node.nodeType !== 1)
630
+ return false;
631
+ const tag = node.tagName.toLowerCase();
632
+ if (tag === "slot")
633
+ return true;
634
+ let isComponent = node.hasAttribute("__thyn_component");
635
+ if (isComponent)
636
+ return true;
637
+ for (const attr of node.attributes) {
638
+ if ([":#for", ":#if", ":#else", ":#else-if"].includes(attr.name)) {
639
+ return true;
640
+ }
641
+ }
642
+ return Array.from(node.childNodes).some((n) => hasComponentChildren(n));
643
+ }
644
+ const COMPONENT_TAG_REGEX = /<\/?([A-Z][a-zA-Z0-9]*)(\s(?:[^"'<>\/]|"[^"]*"|'[^']*')*)?(\/?)>/g;
645
+ function convertToColonBindings(html) {
646
+ let result = '';
647
+ let i = 0;
648
+ while (i < html.length) {
649
+ const start = html.indexOf('={', i);
650
+ if (start === -1) {
651
+ result += html.slice(i);
652
+ break;
653
+ }
654
+ // Check if we're inside escaped HTML (&lt; ... &gt;)
655
+ let beforeStart = html.slice(0, start);
656
+ let lastLt = beforeStart.lastIndexOf('<');
657
+ let lastAmpLt = beforeStart.lastIndexOf('&lt;');
658
+ // If the last &lt; is more recent than the last <, we're in escaped HTML
659
+ if (lastAmpLt > lastLt) {
660
+ result += html.slice(i, start + 2);
661
+ i = start + 2;
662
+ continue;
663
+ }
664
+ // Find the most recent < and > before our position
665
+ let lastTagOpen = beforeStart.lastIndexOf('<');
666
+ let lastTagClose = beforeStart.lastIndexOf('>');
667
+ // If there's no unclosed tag (last > is more recent than last <), skip
668
+ if (lastTagClose >= lastTagOpen) {
669
+ result += html.slice(i, start + 2);
670
+ i = start + 2;
671
+ continue;
672
+ }
673
+ // Find the attribute name by going backwards
674
+ let attrEnd = start - 1;
675
+ while (attrEnd >= 0 && /\s/.test(html[attrEnd]))
676
+ attrEnd--;
677
+ let attrStart = attrEnd;
678
+ // Updated to include dots in attribute names
679
+ while (attrStart >= 0 && /[a-zA-Z0-9_#.-]/.test(html[attrStart])) {
680
+ attrStart--;
681
+ }
682
+ attrStart++; // Move to first character of attribute name
683
+ if (attrStart > attrEnd) {
684
+ // No valid attribute name found
685
+ result += html.slice(i, start + 2);
686
+ i = start + 2;
687
+ continue;
688
+ }
689
+ const attrName = html.slice(attrStart, attrEnd + 1);
690
+ // Updated validation to allow dots in attribute names
691
+ if (!/^[#a-zA-Z_][\w\-\.]*$/.test(attrName)) {
692
+ result += html.slice(i, start + 2);
693
+ i = start + 2;
694
+ continue;
695
+ }
696
+ // Parse balanced {}
697
+ let braceCount = 1;
698
+ let j = start + 2;
699
+ while (j < html.length && braceCount > 0) {
700
+ if (html[j] === '{')
701
+ braceCount++;
702
+ else if (html[j] === '}')
703
+ braceCount--;
704
+ j++;
705
+ }
706
+ if (braceCount !== 0) {
707
+ // Unbalanced braces, treat as raw
708
+ result += html.slice(i, j);
709
+ i = j;
710
+ continue;
711
+ }
712
+ const jsExpr = html.slice(start + 2, j - 1).trim();
713
+ const prefix = attrName.startsWith('#') ? ':#' + attrName.slice(1) : ':' + attrName;
714
+ const beforeAttr = html.slice(i, attrStart);
715
+ result += beforeAttr + `${prefix}="${jsExpr.replace(/"/g, DOUBLE_QUOTE)}"`;
716
+ i = j;
717
+ }
718
+ return result;
719
+ }
720
+ function preprocessHTML(html) {
721
+ html = convertToColonBindings(html);
722
+ html = addComponentAttributes(html);
723
+ html = preserveCamelCaseAttributes(html);
724
+ return html;
725
+ }
726
+ function preserveCamelCaseAttributes(html) {
727
+ return html.replace(/<([^>]+)>/g, (match, tagContent) => {
728
+ const processedContent = tagContent.replace(/\s+(:?[a-z][a-zA-Z]*[A-Z][a-zA-Z]*)\s*=/g, (_attrMatch, attrName) => {
729
+ const kebabCase = attrName.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
730
+ return ` __thyn_attribute_${kebabCase}=`;
731
+ });
732
+ return `<${processedContent}>`;
733
+ });
734
+ }
735
+ function addComponentAttributes(html) {
736
+ let processedHTML = html;
737
+ processedHTML = processedHTML.replace(COMPONENT_TAG_REGEX, (componentMatch, componentName, attributes, selfClose) => {
738
+ const isClosing = componentMatch.startsWith("</");
739
+ return isClosing
740
+ ? "</div>"
741
+ : selfClose
742
+ ? `<div${attributes || ""} __thyn_component="${componentName}"></div>`
743
+ : `<div${attributes || ""} __thyn_component="${componentName}">`;
744
+ });
745
+ return processedHTML;
746
+ }
747
+ function addScopeId(el, scopeId) {
748
+ el.classList.add(scopeId);
749
+ for (const child of el.children) {
750
+ addScopeId(child, scopeId);
751
+ }
752
+ }
753
+ function removeUnusedThynVars(code) {
754
+ const varDeclRE = /const (\__THYN__\d+) = ([^\n;]+);/g;
755
+ const varToExpr = new Map();
756
+ const exprDeps = new Map();
757
+ const allVars = new Set();
758
+ const directlyUsed = new Set();
759
+ // 1. Collect variable declarations
760
+ let match;
761
+ while ((match = varDeclRE.exec(code))) {
762
+ const name = match[1];
763
+ const expr = match[2];
764
+ varToExpr.set(name, expr);
765
+ allVars.add(name);
766
+ // Track dependencies (i.e., const a = b.c → b is a dependency)
767
+ const deps = new Set();
768
+ for (const dep of expr.match(/\__THYN__\d+/g) || []) {
769
+ deps.add(dep);
770
+ }
771
+ exprDeps.set(name, deps);
772
+ }
773
+ // 2. Detect any variable used outside its own declaration
774
+ for (const name of allVars) {
775
+ const usageRE = new RegExp(`\\b${name}\\b`, "g");
776
+ while ((match = usageRE.exec(code))) {
777
+ const idx = match.index;
778
+ const lineStart = code.lastIndexOf("\n", idx);
779
+ const line = code.slice(lineStart + 1, code.indexOf("\n", idx + 1));
780
+ if (!line.startsWith(`const ${name} =`)) {
781
+ directlyUsed.add(name);
782
+ break;
783
+ }
784
+ }
785
+ }
786
+ // 3. Add any variable that is `return`ed
787
+ const returnRE = /return (\__THYN__\d+);/g;
788
+ while ((match = returnRE.exec(code))) {
789
+ directlyUsed.add(match[1]);
790
+ }
791
+ // 4. Walk transitive dependency graph
792
+ const keep = new Set(directlyUsed);
793
+ const stack = [...directlyUsed];
794
+ while (stack.length) {
795
+ const current = stack.pop();
796
+ const deps = exprDeps.get(current);
797
+ if (!deps)
798
+ continue;
799
+ for (const dep of deps) {
800
+ if (!keep.has(dep)) {
801
+ keep.add(dep);
802
+ stack.push(dep);
803
+ }
804
+ }
805
+ }
806
+ // 5. Remove any declaration not in the keep set
807
+ const cleaned = code.replace(varDeclRE, (decl, name) => {
808
+ return keep.has(name) ? decl : "";
809
+ });
810
+ return cleaned;
811
+ }
812
+ async function transformHTMLtoJSX(html, style) {
813
+ const scopeId = `thyn-${(styleId++).toString(36)}`;
814
+ const processedHTML = preprocessHTML(html);
815
+ const template = parseHTML("<template>" + processedHTML + "</template>");
816
+ const rootElement = template.content.firstElementChild;
817
+ let scopedStyle = null;
818
+ if (style) {
819
+ addScopeId(rootElement, scopeId);
820
+ scopedStyle = await scopeSelectors(style, scopeId);
821
+ }
822
+ if (hasComponentChildren(rootElement)) {
823
+ const hoist = [];
824
+ const { code } = walk(rootElement, hoist);
825
+ const root = makeVariable();
826
+ return [root, `const ${root} = ${code};`, hoist, scopedStyle];
827
+ }
828
+ const { root, static: tmpl, dynamic } = makeTemplate(rootElement);
829
+ const hoist = [`
830
+ let __THYN__template;
831
+ function __THYN__template_generate() {
832
+ if (!__THYN__template) {
833
+ const t = document.createElement("template");
834
+ t.innerHTML = \`${escapeTemplateLiteral(tmpl)}\`;
835
+ const w = document.createTreeWalker(t.content, 4);
836
+ while(w.nextNode()) {
837
+ if (w.currentNode.nodeValue === "\\uE000") w.currentNode.nodeValue = "";
838
+ }
839
+ __THYN__template = t.content.firstChild;
840
+ }
841
+ return __THYN__template.cloneNode(true);
842
+ }`];
843
+ return [root, dynamic, hoist, scopedStyle];
844
+ }
845
+ async function transformTypeScript(code, id) {
846
+ try {
847
+ const esbuild = await esbuildModule;
848
+ const result = await esbuild.transform(code, {
849
+ loader: "ts",
850
+ target: "es2022",
851
+ format: "esm",
852
+ sourcemap: true,
853
+ sourcefile: id,
854
+ });
855
+ return {
856
+ code: result.code,
857
+ map: result.map,
858
+ };
859
+ }
860
+ catch (error) {
861
+ throw new Error(`TypeScript compilation failed for ${id}: ${error.message}`);
862
+ }
863
+ }
864
+ export async function transformSFC(source, id) {
865
+ const name = id.split("/").pop()?.replace(/\.thyn$/, "");
866
+ const { script, scriptLang, html, style } = extractParts(source);
867
+ const { imports, body } = splitScript(script);
868
+ const s = new MagicString("");
869
+ if (!imports.some((imp) => imp.includes("$signal"))) {
870
+ s.prepend("import { $signal } from '@thyn/core';\n");
871
+ }
872
+ if (!imports.some((imp) => imp.includes("$effect"))) {
873
+ s.prepend("import { $effect } from '@thyn/core';\n");
874
+ }
875
+ s.prepend("import * as __THYN__CORE__ from '@thyn/core';\n");
876
+ s.append(imports.join("\n") + "\n");
877
+ let [root, transformed, hoist, scopedStyle] = await transformHTMLtoJSX(html, style);
878
+ s.append(hoist.join("\n") + "\n");
879
+ s.append([
880
+ "",
881
+ `export default function ${name}($props) {`,
882
+ ...body.map((l) => " " + l),
883
+ removeUnusedThynVars(` ${transformed} return ${root};`),
884
+ `}`,
885
+ ].join("\n"));
886
+ let output = s.toString();
887
+ let sourceMap = s.generateMap({
888
+ source: id,
889
+ includeContent: true,
890
+ hires: true,
891
+ });
892
+ return { output, sourceMap, scopedStyle, scriptLang };
893
+ }
894
+ export async function compileSFC(source, id) {
895
+ let { scriptLang, output, sourceMap, scopedStyle } = await transformSFC(source, id);
896
+ if (scriptLang === "ts" || scriptLang === "typescript") {
897
+ const tsResult = await transformTypeScript(output, id);
898
+ output = tsResult.code;
899
+ if (tsResult.map) {
900
+ // @ts-expect-error
901
+ sourceMap = tsResult.map;
902
+ }
903
+ }
904
+ return {
905
+ js: output,
906
+ css: scopedStyle || null,
907
+ cssModuleId: scopedStyle ? `${id}.css` : null,
908
+ map: sourceMap,
909
+ };
910
+ }
911
+ async function compileThynScript(source, id) {
912
+ const s = new MagicString(source);
913
+ const { imports } = splitScript(source);
914
+ if (!imports.some((imp) => imp.includes("$signal"))) {
915
+ s.prepend("import { $signal } from '@thyn/core';\n");
916
+ }
917
+ if (!imports.some((imp) => imp.includes("$effect"))) {
918
+ s.prepend("import { $effect } from '@thyn/core';\n");
919
+ }
920
+ let output = s.toString();
921
+ let sourceMap = s.generateMap({
922
+ source: id,
923
+ includeContent: true,
924
+ hires: true,
925
+ });
926
+ if (id.endsWith(".thyn.ts")) {
927
+ const tsResult = await transformTypeScript(output, id);
928
+ output = tsResult.code;
929
+ if (tsResult.map) {
930
+ // @ts-expect-error
931
+ sourceMap = tsResult.map;
932
+ }
933
+ }
934
+ return {
935
+ code: output,
936
+ map: sourceMap,
937
+ };
938
+ }
939
+ let styleId = 0;
940
+ export default function thyn() {
941
+ const collectedCSS = [];
942
+ let isDev = false;
943
+ return {
944
+ name: "thyn",
945
+ enforce: "pre",
946
+ configResolved(config) {
947
+ isDev = config.command === "serve";
948
+ },
949
+ buildStart() {
950
+ collectedCSS.length = 0;
951
+ },
952
+ async transform(code, id) {
953
+ if (id.endsWith(".thyn.js") || id.endsWith(".thyn.ts")) {
954
+ return await compileThynScript(code, id);
955
+ }
956
+ if (!id.endsWith(".thyn"))
957
+ return;
958
+ const { js, css, map } = await compileSFC(code, id);
959
+ let finalCode = js;
960
+ if (isDev && css) {
961
+ const escapedCSS = JSON.stringify(css);
962
+ finalCode += `\n
963
+ if (typeof document !== 'undefined') {
964
+ const style = document.createElement('style');
965
+ style.textContent = ${escapedCSS};
966
+ document.head.appendChild(style);
967
+ }`;
968
+ }
969
+ else if (css) {
970
+ collectedCSS.push(css);
971
+ }
972
+ return {
973
+ code: finalCode,
974
+ map,
975
+ };
976
+ },
977
+ transformIndexHtml(html) {
978
+ if (collectedCSS.length === 0)
979
+ return html;
980
+ const s = new MagicString(html);
981
+ const headCloseIndex = html.indexOf("</head>");
982
+ if (headCloseIndex !== -1) {
983
+ if (isDev) {
984
+ const combinedCSS = collectedCSS.join("\n");
985
+ s.appendLeft(headCloseIndex, ` <style>\n${combinedCSS}\n </style>\n`);
986
+ }
987
+ else {
988
+ s.appendLeft(headCloseIndex, ' <link rel="stylesheet" href="/main.css">\n');
989
+ }
990
+ }
991
+ return s.toString();
992
+ },
993
+ async generateBundle() {
994
+ if (isDev || collectedCSS.length === 0)
995
+ return;
996
+ const combinedCSS = collectedCSS.join("\n");
997
+ const esbuild = await esbuildModule;
998
+ const result = await esbuild.transform(combinedCSS, {
999
+ loader: "css",
1000
+ minify: true,
1001
+ });
1002
+ this.emitFile({
1003
+ type: "asset",
1004
+ fileName: "main.css",
1005
+ source: result.code,
1006
+ });
1007
+ },
1008
+ };
1009
+ }