@thyn/core 0.0.344 → 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,213 @@
1
+ export function extractParts(code: string) {
2
+ const scriptMatch = code.match(/<script([^>]*?)>([\s\S]*?)<\/script>/);
3
+ const styleMatch = code.match(/<style[^>]*>([\s\S]*?)<\/style>/);
4
+ const html = code
5
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/, "")
6
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/, "")
7
+ .trim();
8
+
9
+ let scriptLang = "js";
10
+ if (scriptMatch && scriptMatch[1]) {
11
+ const langMatch = scriptMatch[1].match(/lang\s*=\s*["']([^"']+)["']/);
12
+ if (langMatch) {
13
+ scriptLang = langMatch[1];
14
+ }
15
+ }
16
+
17
+ return {
18
+ script: scriptMatch?.[2]?.trim() ?? "",
19
+ scriptLang,
20
+ style: styleMatch?.[1]?.trim() ?? "",
21
+ html,
22
+ };
23
+ };
24
+
25
+ export function escapeTemplateLiteral(text: string): string {
26
+ return text
27
+ .replace(/\\/g, '\\\\') // Escape backslashes
28
+ .replace(/`/g, '\\`') // Escape backticks
29
+ .replace(/\$/g, '\\$'); // Escape interpolation
30
+ }
31
+
32
+ export function escapeHtml(text: string): string {
33
+ return text
34
+ .replace(/&/g, "&amp;")
35
+ .replace(/</g, "&lt;")
36
+ .replace(/>/g, "&gt;")
37
+ .replace(/"/g, "&quot;")
38
+ .replace(/'/g, "&#039;");
39
+ }
40
+
41
+ export function splitScript(script: string) {
42
+ if (!script || typeof script !== "string") {
43
+ return { imports: [], body: [] };
44
+ }
45
+
46
+ const lines = script.split("\n");
47
+ const imports = [];
48
+ const body = [];
49
+ let currentImport = [];
50
+ let inImport = false;
51
+ let braceCount = 0;
52
+ let inString = false;
53
+ let stringChar = "";
54
+ let inMultiLineComment = false;
55
+
56
+ // Helper function to check if import is complete without semicolon
57
+ function isImportComplete(line, braceCount, inString) {
58
+ // If we have balanced braces and not in a string, check if next non-empty line starts a new statement
59
+ if (braceCount === 0 && !inString) {
60
+ // Look ahead to see if next line starts a new statement/declaration
61
+ const nextLineIndex = lines.indexOf(line) + 1;
62
+ for (let i = nextLineIndex; i < lines.length; i++) {
63
+ const nextLine = lines[i].trim();
64
+ if (
65
+ !nextLine || nextLine.startsWith("//") || nextLine.startsWith("/*")
66
+ ) {
67
+ continue; // Skip empty lines and comments
68
+ }
69
+ // If next line starts with typical JS keywords/patterns, current import is complete
70
+ return /^(const|let|var|function|class|export|if|for|while|switch|try|return|\w+\s*[=:]|\w+\()/
71
+ .test(nextLine);
72
+ }
73
+ // If we reached end of file, import is complete
74
+ return true;
75
+ }
76
+ return false;
77
+ }
78
+
79
+ for (let i = 0; i < lines.length; i++) {
80
+ const line = lines[i];
81
+ const trimmed = line.trim();
82
+
83
+ // Handle multi-line comments
84
+ if (inMultiLineComment) {
85
+ if (inImport) {
86
+ currentImport.push(line);
87
+ } else {
88
+ body.push(line);
89
+ }
90
+
91
+ if (line.includes("*/")) {
92
+ inMultiLineComment = false;
93
+ }
94
+ continue;
95
+ }
96
+
97
+ // Check for start of multi-line comment
98
+ if (line.includes("/*") && !inString) {
99
+ inMultiLineComment = true;
100
+ if (inImport) {
101
+ currentImport.push(line);
102
+ } else {
103
+ body.push(line);
104
+ }
105
+
106
+ if (!line.includes("*/")) {
107
+ continue;
108
+ } else {
109
+ inMultiLineComment = false;
110
+ }
111
+ }
112
+
113
+ // Skip single-line comments when not in import
114
+ if (trimmed.startsWith("//") && !inImport) {
115
+ body.push(line);
116
+ continue;
117
+ }
118
+
119
+ // Skip empty lines when not in import
120
+ if (!trimmed && !inImport) {
121
+ body.push(line);
122
+ continue;
123
+ }
124
+
125
+ // Start of import statement
126
+ if (!inImport && trimmed.startsWith("import")) {
127
+ inImport = true;
128
+ currentImport = [line];
129
+ braceCount = 0;
130
+ inString = false;
131
+
132
+ // Count braces and track strings in the import line
133
+ for (let j = 0; j < line.length; j++) {
134
+ const char = line[j];
135
+
136
+ if (inString) {
137
+ if (char === stringChar && line[j - 1] !== "\\") {
138
+ inString = false;
139
+ stringChar = "";
140
+ }
141
+ } else {
142
+ if (char === '"' || char === "'" || char === "`") {
143
+ inString = true;
144
+ stringChar = char;
145
+ } else if (char === "{") {
146
+ braceCount++;
147
+ } else if (char === "}") {
148
+ braceCount--;
149
+ }
150
+ }
151
+ }
152
+
153
+ // Check if import is complete
154
+ if (
155
+ (trimmed.endsWith(";") ||
156
+ isImportComplete(trimmed, braceCount, inString)) &&
157
+ braceCount === 0 && !inString
158
+ ) {
159
+ imports.push(currentImport.join("\n"));
160
+ currentImport = [];
161
+ inImport = false;
162
+ }
163
+ } // Continue import statement
164
+ else if (inImport) {
165
+ currentImport.push(line);
166
+
167
+ // Count braces and track strings in the current line
168
+ for (let j = 0; j < line.length; j++) {
169
+ const char = line[j];
170
+
171
+ if (inString) {
172
+ if (char === stringChar && line[j - 1] !== "\\") {
173
+ inString = false;
174
+ stringChar = "";
175
+ }
176
+ } else {
177
+ if (char === '"' || char === "'" || char === "`") {
178
+ inString = true;
179
+ stringChar = char;
180
+ } else if (char === "{") {
181
+ braceCount++;
182
+ } else if (char === "}") {
183
+ braceCount--;
184
+ }
185
+ }
186
+ }
187
+
188
+ // Check if import is complete
189
+ if (
190
+ (trimmed.endsWith(";") ||
191
+ isImportComplete(trimmed, braceCount, inString)) &&
192
+ braceCount === 0 && !inString
193
+ ) {
194
+ imports.push(currentImport.join("\n"));
195
+ currentImport = [];
196
+ inImport = false;
197
+ }
198
+ } // Regular body content
199
+ else {
200
+ body.push(line);
201
+ }
202
+ }
203
+
204
+ // Handle unterminated import (likely malformed)
205
+ if (currentImport.length > 0) {
206
+ imports.push(currentImport.join("\n"));
207
+ }
208
+
209
+ return {
210
+ imports: imports.filter((imp) => imp.trim()),
211
+ body: body.length > 0 ? body : [""],
212
+ };
213
+ }
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import Bind from "./Bind.thyn";
3
+
4
+ describe("Bind component", () => {
5
+ it("has bound class name", async () => {
6
+ const root = Bind();
7
+ expect(root.className).toBe("bar");
8
+ });
9
+
10
+ it("does not touch text content", async () => {
11
+ const root = Bind();
12
+ expect(root.textContent).toBe("const n={run:t,deps:new Set,td:null}");
13
+ });
14
+ });
@@ -0,0 +1,7 @@
1
+ <script>
2
+ const foo = $signal("bar");
3
+ </script>
4
+
5
+ <div class={foo() === 'foo' ? 'danger' : 'bar'}>
6
+ const n={run:t,deps:new Set,td:null}
7
+ </div>
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import ConsecInterps from "./ConsecInterps.thyn";
3
+
4
+ describe("ConsecInterps component", () => {
5
+ it("renders", async () => {
6
+ const root = ConsecInterps();
7
+ expect(root.textContent).toBe("00");
8
+ });
9
+ });
@@ -0,0 +1,9 @@
1
+ <script>
2
+ const foo = $signal(0);
3
+ const bar = $signal(0);
4
+ </script>
5
+
6
+ <div>
7
+ {{ foo() }}{{ bar() }}
8
+ </div>
9
+
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import Counter from "./Counter.thyn";
3
+
4
+ describe("Counter component", () => {
5
+ it("increments on click", async () => {
6
+ const root = Counter();
7
+ expect(root.textContent).toBe("Count: 0");
8
+ root.click();
9
+ await Promise.resolve();
10
+ expect(root.textContent).toBe("Count: 1");
11
+ });
12
+ });
@@ -0,0 +1,7 @@
1
+ <script>
2
+ const count = $signal(0);
3
+ </script>
4
+
5
+ <button onclick={() => count(c => c + 1)}>
6
+ Count: {{ count() }}
7
+ </button>
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import DoubleQuotes from "./DoubleQuotes.thyn";
3
+
4
+ describe("DoubleQuotes component", () => {
5
+ it("handles double quotes in bound js expression", async () => {
6
+ const root = DoubleQuotes();
7
+ expect(root.textContent).toBe("abc");
8
+ });
9
+ });
@@ -0,0 +1,3 @@
1
+ <div #for={char in ["a", "b", "c"]}>
2
+ {{ char }}
3
+ </div>
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import Escape from "./Escape.thyn";
3
+
4
+ describe("Escape component", () => {
5
+ it("escapes curly braces", async () => {
6
+ const root = Escape();
7
+ expect(root.textContent).toBe("{{ foo }}");
8
+ });
9
+ });
@@ -0,0 +1,3 @@
1
+ <div>
2
+ \{{ foo \}}
3
+ </div>
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import EscapeDollar from "./EscapeDollar.thyn";
3
+
4
+ describe("EscapeDollar component", () => {
5
+ it("escapes dollar sign", async () => {
6
+ const root = EscapeDollar();
7
+ expect(root.textContent).toBe("$foo");
8
+ });
9
+ });
@@ -0,0 +1,5 @@
1
+ <script>
2
+ const foo = $signal("foo");
3
+ </script>
4
+
5
+ <div>${{ foo() }}</div>
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import EventPipes from "./EventPipes.thyn";
3
+
4
+ describe("EventPipes component", () => {
5
+ it("pipes event handlers", async () => {
6
+ const root = EventPipes();
7
+ expect(root.textContent).toBe("00");
8
+ root.querySelector(".inner").click();
9
+ await Promise.resolve();
10
+ await Promise.resolve();
11
+ expect(root.textContent).toBe("10");
12
+ });
13
+ });
@@ -0,0 +1,11 @@
1
+ <script>
2
+ const foo = $signal(0);
3
+ const bar = $signal(0);
4
+ </script>
5
+
6
+ <div onclick={() => bar(c => c + 1)}>
7
+ <div class="inner" onclick.stop={() => foo(c => c + 1)}>
8
+ <span>{{ foo() }}</span><span>{{ bar() }}</span>
9
+ </div>
10
+ </div>
11
+
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import List from "./List.thyn";
3
+
4
+ describe("List component", () => {
5
+ it("appends on click", async () => {
6
+ const root = List();
7
+ expect(root.textContent).toBe("start012end");
8
+ root.click();
9
+ await Promise.resolve();
10
+ await Promise.resolve();
11
+ expect(root.textContent).toBe("start0123end");
12
+ root.click();
13
+ await Promise.resolve();
14
+ await Promise.resolve();
15
+ expect(root.textContent).toBe("startend");
16
+ root.click();
17
+ await Promise.resolve();
18
+ await Promise.resolve();
19
+ expect(root.textContent).toBe("start0end");
20
+ });
21
+ });
@@ -0,0 +1,15 @@
1
+ <script>
2
+ const items = $signal([0, 1, 2]);
3
+ </script>
4
+
5
+ <div onclick={() => items(v => v.length === 4 ? [] : [...v, v.length])}>
6
+ <p>
7
+ start
8
+ </p>
9
+ <p #for={item in items()}>
10
+ {{ item }}
11
+ </p>
12
+ <p>
13
+ end
14
+ </p>
15
+ </div>
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import List from "./ListV2.thyn";
3
+
4
+ describe("List component", () => {
5
+ it("appends on click", async () => {
6
+ const root = List();
7
+ expect(root.textContent).toBe("start012end");
8
+ expect(root.querySelector(".selected").id).toBe("1");
9
+ root.click();
10
+ await Promise.resolve();
11
+ await Promise.resolve();
12
+ expect(root.textContent).toBe("start0123end");
13
+ expect(root.querySelector(".selected").id).toBe("1");
14
+ root.click();
15
+ await Promise.resolve();
16
+ await Promise.resolve();
17
+ expect(root.textContent).toBe("startend");
18
+ expect(root.querySelector(".selected")).toBeFalsy();
19
+ });
20
+ });
@@ -0,0 +1,16 @@
1
+ <script>
2
+ const items = $signal([$signal(0), $signal(1), $signal(2)]);
3
+ const selected = $signal(1);
4
+ </script>
5
+
6
+ <div onclick={() => items(v => v.length === 4 ? [] : [...v, $signal(v.length)])}>
7
+ <p>
8
+ start
9
+ </p>
10
+ <p #for={item in items()} class={selected() === item() ? 'selected' : undefined} id={item()}>
11
+ {{ item() }}
12
+ </p>
13
+ <p>
14
+ end
15
+ </p>
16
+ </div>
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import MixElemAndText from "./MixElemAndText.thyn";
3
+
4
+ describe("MixElemAndText component", () => {
5
+ it("renders", async () => {
6
+ const root = MixElemAndText();
7
+ expect(root.textContent).toBe("00");
8
+ });
9
+ });
@@ -0,0 +1,12 @@
1
+ <script>
2
+ const foo = $signal(0);
3
+ const bar = $signal(0);
4
+ </script>
5
+
6
+ <div>
7
+ {{ foo() }}
8
+ <span>
9
+ {{ bar() }}
10
+ </span>
11
+ </div>
12
+
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import Show from "./Show.thyn";
3
+
4
+ describe("Show component", () => {
5
+ it("swaps on click", async () => {
6
+ const root = Show();
7
+ expect(root.textContent).toBe("foo");
8
+ root.click();
9
+ await Promise.resolve();
10
+ await Promise.resolve();
11
+ expect(root.textContent).toBe("bar");
12
+ });
13
+ });
@@ -0,0 +1,8 @@
1
+ <script>
2
+ const show = $signal(true);
3
+ </script>
4
+
5
+ <div onclick={() => show(c => !c)}>
6
+ <p #if={show()}>foo</p>
7
+ <p #else>bar</p>
8
+ </div>
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import Template from "./Template.thyn";
3
+
4
+ describe("Template component", () => {
5
+ it("finds nested reactive node", async () => {
6
+ const root = Template();
7
+ expect(root.textContent).toBe("foo0");
8
+ });
9
+ });
@@ -0,0 +1,8 @@
1
+ <script>
2
+ const foo = $signal(0);
3
+ </script>
4
+
5
+ <div>
6
+ <h1>foo</h1>
7
+ <p>{{ foo() }}</p>
8
+ </div>