@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.
- package/.github/workflows/static.yml +48 -0
- package/.github/workflows/test.yml +39 -0
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/{element.js → core/element.js} +14 -36
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/plugin/html-parser.d.ts +31 -0
- package/dist/plugin/html-parser.js +275 -0
- package/dist/plugin/index.d.ts +24 -0
- package/dist/plugin/index.js +1009 -0
- package/dist/plugin/utils.d.ts +12 -0
- package/dist/plugin/utils.js +194 -0
- package/docs/CNAME +1 -0
- package/docs/index.html +18 -0
- package/docs/package-lock.json +980 -0
- package/docs/package.json +15 -0
- package/docs/public/thyn.png +0 -0
- package/docs/public/thyn.svg +1 -0
- package/docs/src/App.thyn +10 -0
- package/docs/src/components/Button.thyn +3 -0
- package/docs/src/docs/GettingStarted.thyn +8 -0
- package/docs/src/main.css +17 -0
- package/docs/src/main.js +5 -0
- package/docs/src/pages/Home.thyn +147 -0
- package/docs/vite.config.js +7 -0
- package/package.json +18 -10
- package/src/{element.ts → core/element.ts} +14 -34
- package/src/core/index.ts +1 -0
- package/src/{signals.ts → core/signals.ts} +1 -1
- package/src/index.ts +5 -15
- package/src/plugin/html-parser.ts +332 -0
- package/src/plugin/index.ts +1127 -0
- package/src/plugin/utils.ts +213 -0
- package/tests/Bind.test.ts +14 -0
- package/tests/Bind.thyn +7 -0
- package/tests/ConsecInterps.test.ts +9 -0
- package/tests/ConsecInterps.thyn +9 -0
- package/tests/Counter.test.ts +12 -0
- package/tests/Counter.thyn +7 -0
- package/tests/DoubleQuotes.test.ts +9 -0
- package/tests/DoubleQuotes.thyn +3 -0
- package/tests/Escape.test.ts +9 -0
- package/tests/Escape.thyn +3 -0
- package/tests/EscapeDollar.test.ts +9 -0
- package/tests/EscapeDollar.thyn +5 -0
- package/tests/EventPipes.test.ts +13 -0
- package/tests/EventPipes.thyn +11 -0
- package/tests/List.test.ts +21 -0
- package/tests/List.thyn +15 -0
- package/tests/ListV2.test.ts +20 -0
- package/tests/ListV2.thyn +16 -0
- package/tests/MixElemAndText.test.ts +9 -0
- package/tests/MixElemAndText.thyn +12 -0
- package/tests/Show.test.ts +13 -0
- package/tests/Show.thyn +8 -0
- package/tests/Template.test.ts +9 -0
- package/tests/Template.thyn +8 -0
- package/tests/list/comprehensive.test.ts +659 -0
- package/tests/list/operations/ChildrenAppend.thyn +11 -0
- package/tests/list/operations/ChildrenFilter.thyn +11 -0
- package/tests/list/operations/ChildrenInsert.thyn +11 -0
- package/tests/list/operations/ChildrenNoneToSome.thyn +11 -0
- package/tests/list/operations/ChildrenPrepend.thyn +11 -0
- package/tests/list/operations/ChildrenRemove.thyn +11 -0
- package/tests/list/operations/ChildrenReplaceAll.thyn +11 -0
- package/tests/list/operations/ChildrenSomeToNone.thyn +11 -0
- package/tests/list/operations/ChildrenSort.thyn +11 -0
- package/tests/list/operations/IsolatedAppend.thyn +10 -0
- package/tests/list/operations/IsolatedFilter.thyn +16 -0
- package/tests/list/operations/IsolatedInsert.thyn +10 -0
- package/tests/list/operations/IsolatedMove.thyn +16 -0
- package/tests/list/operations/IsolatedNoneToSome.thyn +16 -0
- package/tests/list/operations/IsolatedPrepend.thyn +10 -0
- package/tests/list/operations/IsolatedRemove.thyn +17 -0
- package/tests/list/operations/IsolatedReplaceAll.thyn +10 -0
- package/tests/list/operations/IsolatedSomeToNone.thyn +10 -0
- package/tests/list/operations/IsolatedSort.thyn +16 -0
- package/tests/list/operations/TerminalAppend.thyn +12 -0
- package/tests/list/operations/TerminalFilter.thyn +12 -0
- package/tests/list/operations/TerminalInsert.thyn +12 -0
- package/tests/list/operations/TerminalNoneToSome.thyn +12 -0
- package/tests/list/operations/TerminalPrepend.thyn +12 -0
- package/tests/list/operations/TerminalRemove.thyn +12 -0
- package/tests/list/operations/TerminalReplaceAll.thyn +12 -0
- package/tests/list/operations/TerminalSomeToNone.thyn +12 -0
- package/tests/list/operations/TerminalSort.thyn +12 -0
- package/tests/tsconfig.json +14 -0
- package/tsconfig.json +11 -6
- package/types/thyn.d.ts +4 -0
- package/vitest.config.ts +7 -2
- package/tests/fx.test.ts +0 -31
- package/tests/lists.test.ts +0 -184
- package/tests/router.test.ts +0 -69
- package/tests/show.test.ts +0 -66
- package/tests/utils.ts +0 -3
- package/tsconfig.tsbuildinfo +0 -1
- /package/dist/{element.d.ts → core/element.d.ts} +0 -0
- /package/dist/{router.d.ts → core/router.d.ts} +0 -0
- /package/dist/{router.js → core/router.js} +0 -0
- /package/dist/{signals.d.ts → core/signals.d.ts} +0 -0
- /package/dist/{signals.js → core/signals.js} +0 -0
- /package/src/{router.ts → core/router.ts} +0 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
interface Node {
|
|
2
|
+
nodeType: number;
|
|
3
|
+
nodeName: string;
|
|
4
|
+
textContent: string;
|
|
5
|
+
childNodes: Node[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Element extends Node {
|
|
9
|
+
tagName: string;
|
|
10
|
+
attributes: Array<{ name: string; value: string }>;
|
|
11
|
+
children: Element[];
|
|
12
|
+
firstElementChild: Element | null;
|
|
13
|
+
hasAttribute(name: string): boolean;
|
|
14
|
+
getAttribute(name: string): string | null;
|
|
15
|
+
setAttribute(name: string, value: string): void;
|
|
16
|
+
removeAttribute(name: string): void;
|
|
17
|
+
classList: { add(className: string): void };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface DocumentFragment {
|
|
21
|
+
childNodes: Node[];
|
|
22
|
+
firstElementChild: Element | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TemplateElement extends Element {
|
|
26
|
+
content: DocumentFragment;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseAttributes(attrStr: string): Array<{ name: string; value: string }> {
|
|
30
|
+
const attrs: Array<{ name: string; value: string }> = []
|
|
31
|
+
let i = 0;
|
|
32
|
+
|
|
33
|
+
while (i < attrStr.length) {
|
|
34
|
+
// Skip whitespace
|
|
35
|
+
while (i < attrStr.length && /\s/.test(attrStr[i])) i++;
|
|
36
|
+
if (i >= attrStr.length) break;
|
|
37
|
+
|
|
38
|
+
// Parse attribute name
|
|
39
|
+
let name = "";
|
|
40
|
+
while (i < attrStr.length && !/[\s=]/.test(attrStr[i])) {
|
|
41
|
+
name += attrStr[i];
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!name) break;
|
|
46
|
+
|
|
47
|
+
// Skip whitespace
|
|
48
|
+
while (i < attrStr.length && /\s/.test(attrStr[i])) i++;
|
|
49
|
+
|
|
50
|
+
let value = "";
|
|
51
|
+
if (i < attrStr.length && attrStr[i] === "=") {
|
|
52
|
+
i++; // skip '='
|
|
53
|
+
// Skip whitespace
|
|
54
|
+
while (i < attrStr.length && /\s/.test(attrStr[i])) i++;
|
|
55
|
+
|
|
56
|
+
if (i < attrStr.length) {
|
|
57
|
+
const quote = attrStr[i];
|
|
58
|
+
if (quote === '"' || quote === "'") {
|
|
59
|
+
i++; // skip opening quote
|
|
60
|
+
while (i < attrStr.length && attrStr[i] !== quote) {
|
|
61
|
+
value += attrStr[i];
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
if (i < attrStr.length) i++; // skip closing quote
|
|
65
|
+
} else {
|
|
66
|
+
// Unquoted value - take until whitespace
|
|
67
|
+
while (i < attrStr.length && !/\s/.test(attrStr[i])) {
|
|
68
|
+
value += attrStr[i];
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
attrs.push({ name, value });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return attrs;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createTextNode(text: string): Node {
|
|
82
|
+
return {
|
|
83
|
+
nodeType: 3,
|
|
84
|
+
nodeName: "#text",
|
|
85
|
+
textContent: text,
|
|
86
|
+
childNodes: [],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createElement(tagName: string, attributes: Array<{ name: string; value: string }> = []): Element {
|
|
91
|
+
const children: Element[] = [];
|
|
92
|
+
const childNodes: Node[] = [];
|
|
93
|
+
|
|
94
|
+
const element: Element = {
|
|
95
|
+
nodeType: 1,
|
|
96
|
+
nodeName: tagName.toUpperCase(),
|
|
97
|
+
tagName: tagName.toUpperCase(),
|
|
98
|
+
textContent: "",
|
|
99
|
+
attributes: [...attributes],
|
|
100
|
+
children,
|
|
101
|
+
childNodes,
|
|
102
|
+
firstElementChild: null,
|
|
103
|
+
hasAttribute(name: string): boolean {
|
|
104
|
+
return this.attributes.some((attr) => attr.name === name);
|
|
105
|
+
},
|
|
106
|
+
getAttribute(name: string): string | null {
|
|
107
|
+
const attr = this.attributes.find((attr) => attr.name === name);
|
|
108
|
+
return attr ? attr.value : null;
|
|
109
|
+
},
|
|
110
|
+
setAttribute(name: string, value: string): void {
|
|
111
|
+
const existing = this.attributes.find((attr) => attr.name === name);
|
|
112
|
+
if (existing) {
|
|
113
|
+
existing.value = value;
|
|
114
|
+
} else {
|
|
115
|
+
this.attributes.push({ name, value });
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
removeAttribute(name: string): void {
|
|
119
|
+
this.attributes = this.attributes.filter((attr) => attr.name !== name);
|
|
120
|
+
},
|
|
121
|
+
classList: {
|
|
122
|
+
add: (className: string) => {
|
|
123
|
+
const existing = element.getAttribute("class");
|
|
124
|
+
const classes = existing ? existing.split(" ").filter(Boolean) : [];
|
|
125
|
+
if (!classes.includes(className)) {
|
|
126
|
+
classes.push(className);
|
|
127
|
+
element.setAttribute("class", classes.join(" "));
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return element;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Find next tag position, properly handling quoted strings
|
|
137
|
+
function findNextTag(html: string, startIndex: number): { index: number; endIndex: number; isClose: boolean; tagName: string; attrs: string; isSelfClose: boolean } | null {
|
|
138
|
+
let i = startIndex;
|
|
139
|
+
|
|
140
|
+
while (i < html.length) {
|
|
141
|
+
// Find the next '<'
|
|
142
|
+
while (i < html.length && html[i] !== '<') {
|
|
143
|
+
i++;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (i >= html.length) return null;
|
|
147
|
+
|
|
148
|
+
const tagStart = i;
|
|
149
|
+
i++; // skip '<'
|
|
150
|
+
|
|
151
|
+
// Check if it's a closing tag
|
|
152
|
+
const isClose = i < html.length && html[i] === '/';
|
|
153
|
+
if (isClose) i++;
|
|
154
|
+
|
|
155
|
+
// Parse tag name
|
|
156
|
+
let tagName = '';
|
|
157
|
+
while (i < html.length && /[a-zA-Z0-9-]/.test(html[i])) {
|
|
158
|
+
tagName += html[i];
|
|
159
|
+
i++;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!tagName) {
|
|
163
|
+
// Not a valid tag, continue searching
|
|
164
|
+
i = tagStart + 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Parse attributes, respecting quotes
|
|
169
|
+
let attrs = '';
|
|
170
|
+
let inQuote: string | null = null;
|
|
171
|
+
let tagEnd = -1;
|
|
172
|
+
|
|
173
|
+
while (i < html.length) {
|
|
174
|
+
const char = html[i];
|
|
175
|
+
|
|
176
|
+
if (inQuote) {
|
|
177
|
+
attrs += char;
|
|
178
|
+
if (char === inQuote) {
|
|
179
|
+
inQuote = null;
|
|
180
|
+
}
|
|
181
|
+
i++;
|
|
182
|
+
} else if (char === '"' || char === "'") {
|
|
183
|
+
attrs += char;
|
|
184
|
+
inQuote = char;
|
|
185
|
+
i++;
|
|
186
|
+
} else if (char === '>') {
|
|
187
|
+
tagEnd = i + 1; // Include the '>'
|
|
188
|
+
i++;
|
|
189
|
+
break;
|
|
190
|
+
} else {
|
|
191
|
+
attrs += char;
|
|
192
|
+
i++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (tagEnd === -1) {
|
|
197
|
+
// Malformed tag (no closing >), continue searching
|
|
198
|
+
i = tagStart + 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check for self-closing
|
|
203
|
+
const trimmedAttrs = attrs.trim();
|
|
204
|
+
const isSelfClose = trimmedAttrs.endsWith('/');
|
|
205
|
+
const finalAttrs = isSelfClose ? trimmedAttrs.slice(0, -1).trim() : trimmedAttrs;
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
index: tagStart,
|
|
209
|
+
endIndex: tagEnd,
|
|
210
|
+
isClose,
|
|
211
|
+
tagName,
|
|
212
|
+
attrs: finalAttrs,
|
|
213
|
+
isSelfClose
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function parseHTML(html: string): TemplateElement {
|
|
221
|
+
const match = html.match(/<template([^>]*)>([\s\S]*)<\/template>/i);
|
|
222
|
+
|
|
223
|
+
if (!match) {
|
|
224
|
+
throw new Error("No <template> tag found in HTML");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const content = match[2].trim();
|
|
228
|
+
const stack: Element[] = [];
|
|
229
|
+
const textChunks: string[] = [];
|
|
230
|
+
const fragmentChildren: Node[] = [];
|
|
231
|
+
const fragmentElements: Element[] = [];
|
|
232
|
+
|
|
233
|
+
let pos = 0;
|
|
234
|
+
|
|
235
|
+
const flushText = () => {
|
|
236
|
+
if (textChunks.length > 0) {
|
|
237
|
+
const text = textChunks.join("");
|
|
238
|
+
textChunks.length = 0;
|
|
239
|
+
const textNode = createTextNode(text);
|
|
240
|
+
if (stack.length > 0) {
|
|
241
|
+
const parent = stack[stack.length - 1];
|
|
242
|
+
parent.childNodes.push(textNode);
|
|
243
|
+
} else {
|
|
244
|
+
fragmentChildren.push(textNode);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
while (pos < content.length) {
|
|
250
|
+
const tagInfo = findNextTag(content, pos);
|
|
251
|
+
|
|
252
|
+
if (!tagInfo) {
|
|
253
|
+
// No more tags, add remaining as text
|
|
254
|
+
if (pos < content.length) {
|
|
255
|
+
textChunks.push(content.slice(pos));
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Add text before this tag
|
|
261
|
+
if (tagInfo.index > pos) {
|
|
262
|
+
textChunks.push(content.slice(pos, tagInfo.index));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const { isClose, tagName, attrs, isSelfClose, endIndex } = tagInfo;
|
|
266
|
+
|
|
267
|
+
if (isClose) {
|
|
268
|
+
flushText();
|
|
269
|
+
if (stack.length > 0) {
|
|
270
|
+
const closedElement = stack.pop()!;
|
|
271
|
+
if (closedElement.tagName.toLowerCase() !== tagName.toLowerCase()) {
|
|
272
|
+
throw new Error(`Mismatched tags: expected </${closedElement.tagName}>, got </${tagName}>`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Update parent's firstElementChild if needed
|
|
276
|
+
const parent = stack.length > 0 ? stack[stack.length - 1] : null;
|
|
277
|
+
if (parent && !parent.firstElementChild) {
|
|
278
|
+
parent.firstElementChild = closedElement;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
flushText();
|
|
283
|
+
const attributes = parseAttributes(attrs);
|
|
284
|
+
const element = createElement(tagName, attributes);
|
|
285
|
+
|
|
286
|
+
if (stack.length === 0) {
|
|
287
|
+
// Top-level element
|
|
288
|
+
fragmentChildren.push(element);
|
|
289
|
+
fragmentElements.push(element);
|
|
290
|
+
} else {
|
|
291
|
+
const parent = stack[stack.length - 1];
|
|
292
|
+
parent.children.push(element);
|
|
293
|
+
parent.childNodes.push(element);
|
|
294
|
+
if (!parent.firstElementChild) {
|
|
295
|
+
parent.firstElementChild = element;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!isSelfClose) {
|
|
300
|
+
stack.push(element);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Move position past this tag
|
|
305
|
+
pos = endIndex;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Flush any remaining text
|
|
309
|
+
if (stack.length === 0) {
|
|
310
|
+
flushText();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (stack.length > 0) {
|
|
314
|
+
throw new Error(`Unclosed tags remain: ${stack.map(e => e.tagName).join(', ')}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const fragment: DocumentFragment = {
|
|
318
|
+
childNodes: fragmentChildren,
|
|
319
|
+
firstElementChild: fragmentElements[0] || null,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const templateAttrs = parseAttributes(match[1].trim());
|
|
323
|
+
const templateElement: TemplateElement = {
|
|
324
|
+
...createElement("template", templateAttrs),
|
|
325
|
+
content: fragment,
|
|
326
|
+
};
|
|
327
|
+
templateElement.childNodes = [...fragmentChildren];
|
|
328
|
+
templateElement.children = [...fragmentElements];
|
|
329
|
+
templateElement.firstElementChild = fragmentElements[0] || null;
|
|
330
|
+
|
|
331
|
+
return templateElement;
|
|
332
|
+
}
|