@relevate/katachi 0.1.0
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/CONTRIBUTING.md +60 -0
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/bin/katachi.mjs +30 -0
- package/dist/api/index.d.ts +54 -0
- package/dist/api/index.js +45 -0
- package/dist/api/jsx.d.ts +26 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +77 -0
- package/dist/core/ast.d.ts +115 -0
- package/dist/core/ast.js +51 -0
- package/dist/core/build.d.ts +15 -0
- package/dist/core/build.js +107 -0
- package/dist/core/compiler.d.ts +9 -0
- package/dist/core/compiler.js +9 -0
- package/dist/core/example-fixtures.d.ts +5 -0
- package/dist/core/example-fixtures.js +54 -0
- package/dist/core/parser.d.ts +5 -0
- package/dist/core/parser.js +637 -0
- package/dist/core/types.d.ts +65 -0
- package/dist/core/types.js +1 -0
- package/dist/core/verify.d.ts +25 -0
- package/dist/core/verify.js +270 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/targets/askama.d.ts +11 -0
- package/dist/targets/askama.js +122 -0
- package/dist/targets/index.d.ts +5 -0
- package/dist/targets/index.js +60 -0
- package/dist/targets/react.d.ts +4 -0
- package/dist/targets/react.js +28 -0
- package/dist/targets/shared.d.ts +36 -0
- package/dist/targets/shared.js +278 -0
- package/dist/targets/static-jsx.d.ts +4 -0
- package/dist/targets/static-jsx.js +28 -0
- package/dist/verify-examples.d.ts +1 -0
- package/dist/verify-examples.js +14 -0
- package/docs/architecture.md +122 -0
- package/docs/getting-started.md +154 -0
- package/docs/syntax.md +236 -0
- package/docs/targets.md +53 -0
- package/examples/basic/README.md +67 -0
- package/examples/basic/components/badge-chip.html +3 -0
- package/examples/basic/components/comparison-table.html +24 -0
- package/examples/basic/components/glyph.html +6 -0
- package/examples/basic/components/hover-note.html +6 -0
- package/examples/basic/components/media-frame.html +15 -0
- package/examples/basic/components/notice-panel.html +24 -0
- package/examples/basic/components/resource-tile.html +24 -0
- package/examples/basic/components/stack-shell.html +3 -0
- package/examples/basic/dist/askama/badge-chip.rs +18 -0
- package/examples/basic/dist/askama/comparison-table.rs +47 -0
- package/examples/basic/dist/askama/glyph.rs +21 -0
- package/examples/basic/dist/askama/hover-note.rs +19 -0
- package/examples/basic/dist/askama/includes/badge-chip.html +5 -0
- package/examples/basic/dist/askama/includes/comparison-table.html +34 -0
- package/examples/basic/dist/askama/includes/glyph.html +6 -0
- package/examples/basic/dist/askama/includes/hover-note.html +6 -0
- package/examples/basic/dist/askama/includes/media-frame.html +23 -0
- package/examples/basic/dist/askama/includes/notice-panel.html +34 -0
- package/examples/basic/dist/askama/includes/resource-tile.html +42 -0
- package/examples/basic/dist/askama/includes/stack-shell.html +5 -0
- package/examples/basic/dist/askama/media-frame.rs +37 -0
- package/examples/basic/dist/askama/notice-panel.rs +49 -0
- package/examples/basic/dist/askama/resource-tile.rs +59 -0
- package/examples/basic/dist/askama/stack-shell.rs +17 -0
- package/examples/basic/dist/jsx-static/badge-chip.tsx +18 -0
- package/examples/basic/dist/jsx-static/comparison-table.tsx +53 -0
- package/examples/basic/dist/jsx-static/glyph.tsx +21 -0
- package/examples/basic/dist/jsx-static/hover-note.tsx +19 -0
- package/examples/basic/dist/jsx-static/media-frame.tsx +41 -0
- package/examples/basic/dist/jsx-static/notice-panel.tsx +53 -0
- package/examples/basic/dist/jsx-static/resource-tile.tsx +63 -0
- package/examples/basic/dist/jsx-static/stack-shell.tsx +17 -0
- package/examples/basic/dist/react/badge-chip.tsx +18 -0
- package/examples/basic/dist/react/comparison-table.tsx +53 -0
- package/examples/basic/dist/react/glyph.tsx +21 -0
- package/examples/basic/dist/react/hover-note.tsx +19 -0
- package/examples/basic/dist/react/media-frame.tsx +41 -0
- package/examples/basic/dist/react/notice-panel.tsx +53 -0
- package/examples/basic/dist/react/resource-tile.tsx +63 -0
- package/examples/basic/dist/react/stack-shell.tsx +17 -0
- package/examples/basic/src/templates/badge-chip.template.tsx +18 -0
- package/examples/basic/src/templates/comparison-table.template.tsx +35 -0
- package/examples/basic/src/templates/glyph.template.tsx +17 -0
- package/examples/basic/src/templates/hover-note.template.tsx +17 -0
- package/examples/basic/src/templates/media-frame.template.tsx +25 -0
- package/examples/basic/src/templates/notice-panel.template.tsx +40 -0
- package/examples/basic/src/templates/resource-tile.template.tsx +51 -0
- package/examples/basic/src/templates/stack-shell.template.tsx +13 -0
- package/examples/basic/tsconfig.json +10 -0
- package/package.json +69 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import { and, classList, componentNode, elementNode, eq, exprAttr, forNode, intrinsic, ifNode, n, neq, not, or, printNode, raw, s, slotNode, textAttr, textNode, v, } from "./ast.js";
|
|
2
|
+
/**
|
|
3
|
+
* Handwritten parser for Katachi's current restricted TSX subset.
|
|
4
|
+
*
|
|
5
|
+
* This parser exists to prove the authoring model and compiler pipeline. The
|
|
6
|
+
* long-term direction is a real TSX AST parser, but the handwritten approach is
|
|
7
|
+
* still useful while the syntax surface is changing quickly.
|
|
8
|
+
*/
|
|
9
|
+
function isTagNameChar(char) {
|
|
10
|
+
return /[A-Za-z0-9_-]/.test(char);
|
|
11
|
+
}
|
|
12
|
+
function isAttributeNameChar(char) {
|
|
13
|
+
return /[A-Za-z0-9_:@.-]/.test(char);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Splits a string on a separator while respecting nested brackets, braces,
|
|
17
|
+
* parentheses, and quoted strings.
|
|
18
|
+
*/
|
|
19
|
+
function splitTopLevel(input, separator) {
|
|
20
|
+
const parts = [];
|
|
21
|
+
let current = "";
|
|
22
|
+
let depthParen = 0;
|
|
23
|
+
let depthBracket = 0;
|
|
24
|
+
let depthBrace = 0;
|
|
25
|
+
let quote = null;
|
|
26
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
27
|
+
const char = input[index];
|
|
28
|
+
const next = input[index + 1];
|
|
29
|
+
if (quote) {
|
|
30
|
+
current += char;
|
|
31
|
+
if (char === "\\" && next) {
|
|
32
|
+
current += next;
|
|
33
|
+
index += 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (char === quote) {
|
|
37
|
+
quote = null;
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (char === '"' || char === "'") {
|
|
42
|
+
quote = char;
|
|
43
|
+
current += char;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (char === "(")
|
|
47
|
+
depthParen += 1;
|
|
48
|
+
if (char === ")")
|
|
49
|
+
depthParen -= 1;
|
|
50
|
+
if (char === "[")
|
|
51
|
+
depthBracket += 1;
|
|
52
|
+
if (char === "]")
|
|
53
|
+
depthBracket -= 1;
|
|
54
|
+
if (char === "{")
|
|
55
|
+
depthBrace += 1;
|
|
56
|
+
if (char === "}")
|
|
57
|
+
depthBrace -= 1;
|
|
58
|
+
if (char === separator &&
|
|
59
|
+
depthParen === 0 &&
|
|
60
|
+
depthBracket === 0 &&
|
|
61
|
+
depthBrace === 0) {
|
|
62
|
+
parts.push(current.trim());
|
|
63
|
+
current = "";
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
current += char;
|
|
67
|
+
}
|
|
68
|
+
if (current.trim()) {
|
|
69
|
+
parts.push(current.trim());
|
|
70
|
+
}
|
|
71
|
+
return parts;
|
|
72
|
+
}
|
|
73
|
+
function findTopLevelOperator(input, operator) {
|
|
74
|
+
let depthParen = 0;
|
|
75
|
+
let depthBracket = 0;
|
|
76
|
+
let depthBrace = 0;
|
|
77
|
+
let quote = null;
|
|
78
|
+
for (let index = 0; index <= input.length - operator.length; index += 1) {
|
|
79
|
+
const char = input[index];
|
|
80
|
+
const next = input[index + 1];
|
|
81
|
+
if (quote) {
|
|
82
|
+
if (char === "\\" && next) {
|
|
83
|
+
index += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (char === quote) {
|
|
87
|
+
quote = null;
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (char === '"' || char === "'") {
|
|
92
|
+
quote = char;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (char === "(")
|
|
96
|
+
depthParen += 1;
|
|
97
|
+
if (char === ")")
|
|
98
|
+
depthParen -= 1;
|
|
99
|
+
if (char === "[")
|
|
100
|
+
depthBracket += 1;
|
|
101
|
+
if (char === "]")
|
|
102
|
+
depthBracket -= 1;
|
|
103
|
+
if (char === "{")
|
|
104
|
+
depthBrace += 1;
|
|
105
|
+
if (char === "}")
|
|
106
|
+
depthBrace -= 1;
|
|
107
|
+
if (depthParen === 0 &&
|
|
108
|
+
depthBracket === 0 &&
|
|
109
|
+
depthBrace === 0 &&
|
|
110
|
+
input.slice(index, index + operator.length) === operator) {
|
|
111
|
+
return index;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return -1;
|
|
115
|
+
}
|
|
116
|
+
function unquote(value) {
|
|
117
|
+
const trimmed = value.trim();
|
|
118
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
119
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
120
|
+
return trimmed.slice(1, -1);
|
|
121
|
+
}
|
|
122
|
+
return trimmed;
|
|
123
|
+
}
|
|
124
|
+
function parseTopLevelCall(input) {
|
|
125
|
+
const match = input.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
|
|
126
|
+
if (!match) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const name = match[1];
|
|
130
|
+
const openParenIndex = input.indexOf("(", name.length);
|
|
131
|
+
if (openParenIndex === -1 || !input.endsWith(")")) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
let depthParen = 0;
|
|
135
|
+
let depthBracket = 0;
|
|
136
|
+
let depthBrace = 0;
|
|
137
|
+
let quote = null;
|
|
138
|
+
for (let index = openParenIndex; index < input.length; index += 1) {
|
|
139
|
+
const char = input[index];
|
|
140
|
+
const next = input[index + 1];
|
|
141
|
+
if (quote) {
|
|
142
|
+
if (char === "\\" && next) {
|
|
143
|
+
index += 1;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (char === quote) {
|
|
147
|
+
quote = null;
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (char === '"' || char === "'") {
|
|
152
|
+
quote = char;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (char === "(")
|
|
156
|
+
depthParen += 1;
|
|
157
|
+
if (char === ")")
|
|
158
|
+
depthParen -= 1;
|
|
159
|
+
if (char === "[")
|
|
160
|
+
depthBracket += 1;
|
|
161
|
+
if (char === "]")
|
|
162
|
+
depthBracket -= 1;
|
|
163
|
+
if (char === "{")
|
|
164
|
+
depthBrace += 1;
|
|
165
|
+
if (char === "}")
|
|
166
|
+
depthBrace -= 1;
|
|
167
|
+
if (depthParen === 0 && index < input.length - 1) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (depthParen !== 0 || depthBracket !== 0 || depthBrace !== 0) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const argsBody = input.slice(openParenIndex + 1, -1).trim();
|
|
175
|
+
return {
|
|
176
|
+
name,
|
|
177
|
+
args: argsBody ? splitTopLevel(argsBody, ",") : [],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function parseExpr(source) {
|
|
181
|
+
const input = source.trim();
|
|
182
|
+
if (!input) {
|
|
183
|
+
return raw("");
|
|
184
|
+
}
|
|
185
|
+
if (input === "true" || input === "false") {
|
|
186
|
+
return { kind: "bool", value: input === "true" };
|
|
187
|
+
}
|
|
188
|
+
if (/^-?\d+(\.\d+)?$/.test(input)) {
|
|
189
|
+
return n(Number(input));
|
|
190
|
+
}
|
|
191
|
+
if (input.startsWith("!(") && input.endsWith(")")) {
|
|
192
|
+
return not(parseExpr(input.slice(2, -1)));
|
|
193
|
+
}
|
|
194
|
+
if (input.startsWith("!") && !input.startsWith("!=")) {
|
|
195
|
+
return not(parseExpr(input.slice(1)));
|
|
196
|
+
}
|
|
197
|
+
for (const operator of ["||", "&&", "===", "!==", "==", "!="]) {
|
|
198
|
+
const operatorIndex = findTopLevelOperator(input, operator);
|
|
199
|
+
if (operatorIndex !== -1) {
|
|
200
|
+
const left = parseExpr(input.slice(0, operatorIndex));
|
|
201
|
+
const right = parseExpr(input.slice(operatorIndex + operator.length));
|
|
202
|
+
if (operator === "||")
|
|
203
|
+
return or(left, right);
|
|
204
|
+
if (operator === "&&")
|
|
205
|
+
return and(left, right);
|
|
206
|
+
if (operator === "===" || operator === "==")
|
|
207
|
+
return eq(left, right);
|
|
208
|
+
return neq(left, right);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if ((input.startsWith('"') && input.endsWith('"')) ||
|
|
212
|
+
(input.startsWith("'") && input.endsWith("'"))) {
|
|
213
|
+
return s(unquote(input));
|
|
214
|
+
}
|
|
215
|
+
if (/^[A-Za-z_][A-Za-z0-9_.]*$/.test(input)) {
|
|
216
|
+
return v(input);
|
|
217
|
+
}
|
|
218
|
+
const call = parseTopLevelCall(input);
|
|
219
|
+
if (call && call.args.length === 1) {
|
|
220
|
+
if (call.name === "len") {
|
|
221
|
+
return intrinsic("len", parseExpr(call.args[0] ?? ""));
|
|
222
|
+
}
|
|
223
|
+
if (call.name === "isEmpty") {
|
|
224
|
+
return intrinsic("isEmpty", parseExpr(call.args[0] ?? ""));
|
|
225
|
+
}
|
|
226
|
+
if (call.name === "isSome") {
|
|
227
|
+
return intrinsic("isSome", parseExpr(call.args[0] ?? ""));
|
|
228
|
+
}
|
|
229
|
+
if (call.name === "isNone") {
|
|
230
|
+
return intrinsic("isNone", parseExpr(call.args[0] ?? ""));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return raw(input);
|
|
234
|
+
}
|
|
235
|
+
function parseClassList(source) {
|
|
236
|
+
const input = source.trim();
|
|
237
|
+
const listBody = input.slice(1, -1);
|
|
238
|
+
const items = splitTopLevel(listBody, ",").map((item) => {
|
|
239
|
+
if (item.includes("&&")) {
|
|
240
|
+
const parts = item.split("&&");
|
|
241
|
+
const test = parts[0] ?? "";
|
|
242
|
+
const value = parts.slice(1).join("&&");
|
|
243
|
+
return {
|
|
244
|
+
kind: "when",
|
|
245
|
+
test: parseExpr(test),
|
|
246
|
+
value: unquote(value),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
kind: "static",
|
|
251
|
+
value: unquote(item),
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
return classList(...items);
|
|
255
|
+
}
|
|
256
|
+
function parseAttrValue(name, source) {
|
|
257
|
+
const input = source.trim();
|
|
258
|
+
if (input.startsWith("{") && input.endsWith("}")) {
|
|
259
|
+
const inner = input.slice(1, -1).trim();
|
|
260
|
+
if ((name === "class" || name === "className") &&
|
|
261
|
+
inner.startsWith("[") &&
|
|
262
|
+
inner.endsWith("]")) {
|
|
263
|
+
return parseClassList(inner);
|
|
264
|
+
}
|
|
265
|
+
return exprAttr(parseExpr(inner));
|
|
266
|
+
}
|
|
267
|
+
return textAttr(unquote(input));
|
|
268
|
+
}
|
|
269
|
+
function parseTag(openTagSource) {
|
|
270
|
+
let index = 1;
|
|
271
|
+
while (index < openTagSource.length && /\s/.test(openTagSource[index]))
|
|
272
|
+
index += 1;
|
|
273
|
+
let tagName = "";
|
|
274
|
+
while (index < openTagSource.length && isTagNameChar(openTagSource[index])) {
|
|
275
|
+
tagName += openTagSource[index];
|
|
276
|
+
index += 1;
|
|
277
|
+
}
|
|
278
|
+
const attrs = {};
|
|
279
|
+
while (index < openTagSource.length) {
|
|
280
|
+
while (index < openTagSource.length && /\s/.test(openTagSource[index]))
|
|
281
|
+
index += 1;
|
|
282
|
+
if (openTagSource[index] === "/" || openTagSource[index] === ">")
|
|
283
|
+
break;
|
|
284
|
+
let attrName = "";
|
|
285
|
+
while (index < openTagSource.length &&
|
|
286
|
+
isAttributeNameChar(openTagSource[index])) {
|
|
287
|
+
attrName += openTagSource[index];
|
|
288
|
+
index += 1;
|
|
289
|
+
}
|
|
290
|
+
while (index < openTagSource.length && /\s/.test(openTagSource[index]))
|
|
291
|
+
index += 1;
|
|
292
|
+
if (openTagSource[index] !== "=") {
|
|
293
|
+
throw new Error(`Expected "=" after attribute ${attrName}`);
|
|
294
|
+
}
|
|
295
|
+
index += 1;
|
|
296
|
+
while (index < openTagSource.length && /\s/.test(openTagSource[index]))
|
|
297
|
+
index += 1;
|
|
298
|
+
let value = "";
|
|
299
|
+
if (openTagSource[index] === '"' || openTagSource[index] === "'") {
|
|
300
|
+
const quote = openTagSource[index];
|
|
301
|
+
value += quote;
|
|
302
|
+
index += 1;
|
|
303
|
+
while (index < openTagSource.length && openTagSource[index] !== quote) {
|
|
304
|
+
value += openTagSource[index];
|
|
305
|
+
index += 1;
|
|
306
|
+
}
|
|
307
|
+
value += quote;
|
|
308
|
+
index += 1;
|
|
309
|
+
}
|
|
310
|
+
else if (openTagSource[index] === "{") {
|
|
311
|
+
let depth = 0;
|
|
312
|
+
while (index < openTagSource.length) {
|
|
313
|
+
const char = openTagSource[index];
|
|
314
|
+
value += char;
|
|
315
|
+
if (char === "{")
|
|
316
|
+
depth += 1;
|
|
317
|
+
if (char === "}") {
|
|
318
|
+
depth -= 1;
|
|
319
|
+
if (depth === 0) {
|
|
320
|
+
index += 1;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
index += 1;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
while (index < openTagSource.length &&
|
|
329
|
+
!/\s/.test(openTagSource[index]) &&
|
|
330
|
+
openTagSource[index] !== "/" &&
|
|
331
|
+
openTagSource[index] !== ">") {
|
|
332
|
+
value += openTagSource[index];
|
|
333
|
+
index += 1;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
attrs[attrName] = parseAttrValue(attrName, value);
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
tagName,
|
|
340
|
+
attrs,
|
|
341
|
+
selfClosing: openTagSource.trim().endsWith("/>"),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function normalizeElementAttrs(attrs) {
|
|
345
|
+
const normalized = {};
|
|
346
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
347
|
+
normalized[name === "className" ? "class" : name] = value;
|
|
348
|
+
}
|
|
349
|
+
return normalized;
|
|
350
|
+
}
|
|
351
|
+
function readOpenTag(source, startIndex) {
|
|
352
|
+
let index = startIndex;
|
|
353
|
+
let quote = null;
|
|
354
|
+
let braceDepth = 0;
|
|
355
|
+
while (index < source.length) {
|
|
356
|
+
const char = source[index];
|
|
357
|
+
const next = source[index + 1];
|
|
358
|
+
if (quote) {
|
|
359
|
+
if (char === "\\" && next) {
|
|
360
|
+
index += 2;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (char === quote)
|
|
364
|
+
quote = null;
|
|
365
|
+
index += 1;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (char === '"' || char === "'") {
|
|
369
|
+
quote = char;
|
|
370
|
+
index += 1;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (char === "{")
|
|
374
|
+
braceDepth += 1;
|
|
375
|
+
if (char === "}")
|
|
376
|
+
braceDepth -= 1;
|
|
377
|
+
if (char === ">" && braceDepth === 0) {
|
|
378
|
+
return source.slice(startIndex, index + 1);
|
|
379
|
+
}
|
|
380
|
+
index += 1;
|
|
381
|
+
}
|
|
382
|
+
throw new Error("Unterminated tag");
|
|
383
|
+
}
|
|
384
|
+
function parseNodes(source, startIndex = 0, untilTagName = null) {
|
|
385
|
+
const nodes = [];
|
|
386
|
+
let index = startIndex;
|
|
387
|
+
while (index < source.length) {
|
|
388
|
+
if (source.startsWith("</", index)) {
|
|
389
|
+
const end = source.indexOf(">", index);
|
|
390
|
+
const closingTag = source.slice(index + 2, end).trim();
|
|
391
|
+
if (untilTagName && closingTag === untilTagName) {
|
|
392
|
+
return { nodes, nextIndex: end + 1 };
|
|
393
|
+
}
|
|
394
|
+
throw new Error(`Unexpected closing tag: ${closingTag}`);
|
|
395
|
+
}
|
|
396
|
+
if (source[index] === "<") {
|
|
397
|
+
const openTagSource = readOpenTag(source, index);
|
|
398
|
+
const { tagName, attrs, selfClosing } = parseTag(openTagSource);
|
|
399
|
+
index += openTagSource.length;
|
|
400
|
+
if (selfClosing) {
|
|
401
|
+
if (/^[A-Z]/.test(tagName)) {
|
|
402
|
+
nodes.push(componentNode(tagName, attrs));
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
nodes.push(elementNode(tagName, normalizeElementAttrs(attrs)));
|
|
406
|
+
}
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const parsedChildren = parseNodes(source, index, tagName);
|
|
410
|
+
index = parsedChildren.nextIndex;
|
|
411
|
+
if (tagName === "if" || tagName === "If") {
|
|
412
|
+
const test = attrs.test?.kind === "expr" ? attrs.test.expr : null;
|
|
413
|
+
if (!test) {
|
|
414
|
+
throw new Error("<If> requires a test expression");
|
|
415
|
+
}
|
|
416
|
+
nodes.push(ifNode(test, parsedChildren.nodes));
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (tagName === "for" || tagName === "For") {
|
|
420
|
+
const each = attrs.each?.kind === "expr" ? attrs.each.expr : null;
|
|
421
|
+
const item = attrs.as?.kind === "text" ? attrs.as.value : null;
|
|
422
|
+
const indexName = attrs.index?.kind === "text" ? attrs.index.value : null;
|
|
423
|
+
if (!each || !item) {
|
|
424
|
+
throw new Error('<For> requires `each={...}` and `as="..."`');
|
|
425
|
+
}
|
|
426
|
+
nodes.push(forNode(item, each, parsedChildren.nodes, indexName));
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (/^[A-Z]/.test(tagName)) {
|
|
430
|
+
nodes.push(componentNode(tagName, attrs, parsedChildren.nodes));
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
nodes.push(elementNode(tagName, normalizeElementAttrs(attrs), parsedChildren.nodes));
|
|
434
|
+
}
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (source[index] === "{") {
|
|
438
|
+
let depth = 0;
|
|
439
|
+
let end = index;
|
|
440
|
+
while (end < source.length) {
|
|
441
|
+
const char = source[end];
|
|
442
|
+
if (char === "{")
|
|
443
|
+
depth += 1;
|
|
444
|
+
if (char === "}") {
|
|
445
|
+
depth -= 1;
|
|
446
|
+
if (depth === 0)
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
end += 1;
|
|
450
|
+
}
|
|
451
|
+
const inner = source.slice(index + 1, end).trim();
|
|
452
|
+
if (inner) {
|
|
453
|
+
if (inner === "children") {
|
|
454
|
+
nodes.push(slotNode(inner));
|
|
455
|
+
}
|
|
456
|
+
else if (inner.startsWith("safe(") && inner.endsWith(")")) {
|
|
457
|
+
nodes.push(printNode(parseExpr(inner.slice(5, -1)), true));
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
nodes.push(printNode(parseExpr(inner)));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
index = end + 1;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
let end = index;
|
|
467
|
+
while (end < source.length && source[end] !== "<" && source[end] !== "{") {
|
|
468
|
+
end += 1;
|
|
469
|
+
}
|
|
470
|
+
const text = source.slice(index, end);
|
|
471
|
+
if (text.trim()) {
|
|
472
|
+
nodes.push(textNode(text.trim()));
|
|
473
|
+
}
|
|
474
|
+
index = end;
|
|
475
|
+
}
|
|
476
|
+
if (untilTagName) {
|
|
477
|
+
throw new Error(`Missing closing tag: ${untilTagName}`);
|
|
478
|
+
}
|
|
479
|
+
return { nodes, nextIndex: index };
|
|
480
|
+
}
|
|
481
|
+
function normalizePropType(type) {
|
|
482
|
+
if (type === "ReactNode" ||
|
|
483
|
+
type === "React.ReactNode" ||
|
|
484
|
+
type === "TemplateNode") {
|
|
485
|
+
return "children";
|
|
486
|
+
}
|
|
487
|
+
if (type === "ClassValue") {
|
|
488
|
+
return "string";
|
|
489
|
+
}
|
|
490
|
+
if (type.includes("|")) {
|
|
491
|
+
return "string";
|
|
492
|
+
}
|
|
493
|
+
if (type === "string") {
|
|
494
|
+
return "string";
|
|
495
|
+
}
|
|
496
|
+
if (type === "boolean") {
|
|
497
|
+
return "bool";
|
|
498
|
+
}
|
|
499
|
+
return type;
|
|
500
|
+
}
|
|
501
|
+
function parseProps(source) {
|
|
502
|
+
const propsMatch = source.match(/export\s+type\s+Props\s*=\s*\{([\s\S]*?)\}/);
|
|
503
|
+
if (!propsMatch) {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
return propsMatch[1]
|
|
507
|
+
.split("\n")
|
|
508
|
+
.map((line) => line.trim())
|
|
509
|
+
.filter(Boolean)
|
|
510
|
+
.map((line) => {
|
|
511
|
+
const normalized = line.replace(/;$/, "");
|
|
512
|
+
const colonIndex = normalized.indexOf(":");
|
|
513
|
+
if (colonIndex === -1) {
|
|
514
|
+
throw new Error(`Could not parse prop declaration: ${line}`);
|
|
515
|
+
}
|
|
516
|
+
const left = normalized.slice(0, colonIndex).trim();
|
|
517
|
+
const right = normalized.slice(colonIndex + 1).trim();
|
|
518
|
+
const optional = left.endsWith("?");
|
|
519
|
+
const name = optional ? left.slice(0, -1) : left;
|
|
520
|
+
return {
|
|
521
|
+
name,
|
|
522
|
+
type: normalizePropType(right),
|
|
523
|
+
optional,
|
|
524
|
+
};
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
function parseImports(source) {
|
|
528
|
+
return Array.from(source.matchAll(/import\s+([A-Z][A-Za-z0-9_]*)\s+from\s+["'](.+?)["'];?/g)).map((match) => ({
|
|
529
|
+
localName: match[1],
|
|
530
|
+
source: match[2],
|
|
531
|
+
}));
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Parses a template module into the portable compiler representation.
|
|
535
|
+
*/
|
|
536
|
+
export function parseTemplateFile(source) {
|
|
537
|
+
const imports = parseImports(source);
|
|
538
|
+
const nameMatch = source.match(/export\s+default\s+function\s+([A-Za-z0-9_]+)/) ??
|
|
539
|
+
source.match(/export\s+function\s+([A-Za-z0-9_]+)/);
|
|
540
|
+
if (!nameMatch) {
|
|
541
|
+
throw new Error("Template must export a named function component");
|
|
542
|
+
}
|
|
543
|
+
const props = parseProps(source);
|
|
544
|
+
const returnIndex = source.indexOf("return");
|
|
545
|
+
if (returnIndex === -1) {
|
|
546
|
+
throw new Error("Template must return JSX");
|
|
547
|
+
}
|
|
548
|
+
let exprStart = returnIndex + "return".length;
|
|
549
|
+
while (/\s/.test(source[exprStart])) {
|
|
550
|
+
exprStart += 1;
|
|
551
|
+
}
|
|
552
|
+
let markup = "";
|
|
553
|
+
if (source[exprStart] === "(") {
|
|
554
|
+
let depth = 0;
|
|
555
|
+
let parenEnd = -1;
|
|
556
|
+
for (let index = exprStart; index < source.length; index += 1) {
|
|
557
|
+
const char = source[index];
|
|
558
|
+
if (char === "(")
|
|
559
|
+
depth += 1;
|
|
560
|
+
if (char === ")") {
|
|
561
|
+
depth -= 1;
|
|
562
|
+
if (depth === 0) {
|
|
563
|
+
parenEnd = index;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (parenEnd === -1) {
|
|
569
|
+
throw new Error("Could not find JSX return end");
|
|
570
|
+
}
|
|
571
|
+
markup = source.slice(exprStart + 1, parenEnd).trim();
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
let index = exprStart;
|
|
575
|
+
let depthParen = 0;
|
|
576
|
+
let depthBracket = 0;
|
|
577
|
+
let depthBrace = 0;
|
|
578
|
+
let quote = null;
|
|
579
|
+
let exprEnd = -1;
|
|
580
|
+
while (index < source.length) {
|
|
581
|
+
const char = source[index];
|
|
582
|
+
const next = source[index + 1];
|
|
583
|
+
if (quote) {
|
|
584
|
+
if (char === "\\" && next) {
|
|
585
|
+
index += 2;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (char === quote) {
|
|
589
|
+
quote = null;
|
|
590
|
+
}
|
|
591
|
+
index += 1;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (char === '"' || char === "'") {
|
|
595
|
+
quote = char;
|
|
596
|
+
index += 1;
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (char === "(")
|
|
600
|
+
depthParen += 1;
|
|
601
|
+
if (char === ")")
|
|
602
|
+
depthParen -= 1;
|
|
603
|
+
if (char === "[")
|
|
604
|
+
depthBracket += 1;
|
|
605
|
+
if (char === "]")
|
|
606
|
+
depthBracket -= 1;
|
|
607
|
+
if (char === "{")
|
|
608
|
+
depthBrace += 1;
|
|
609
|
+
if (char === "}")
|
|
610
|
+
depthBrace -= 1;
|
|
611
|
+
if (char === ";" &&
|
|
612
|
+
depthParen === 0 &&
|
|
613
|
+
depthBracket === 0 &&
|
|
614
|
+
depthBrace === 0) {
|
|
615
|
+
exprEnd = index;
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
index += 1;
|
|
619
|
+
}
|
|
620
|
+
if (exprEnd === -1) {
|
|
621
|
+
throw new Error("Could not find JSX return end");
|
|
622
|
+
}
|
|
623
|
+
markup = source.slice(exprStart, exprEnd).trim();
|
|
624
|
+
}
|
|
625
|
+
const parsed = parseNodes(markup);
|
|
626
|
+
if (parsed.nodes.length !== 1) {
|
|
627
|
+
throw new Error("Template body must contain exactly one root node");
|
|
628
|
+
}
|
|
629
|
+
const name = nameMatch[1];
|
|
630
|
+
return {
|
|
631
|
+
name,
|
|
632
|
+
fileName: name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(),
|
|
633
|
+
imports,
|
|
634
|
+
props,
|
|
635
|
+
template: parsed.nodes[0],
|
|
636
|
+
};
|
|
637
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Node } from "./ast.js";
|
|
2
|
+
/**
|
|
3
|
+
* Describes a component import discovered in a template source file.
|
|
4
|
+
*/
|
|
5
|
+
export interface TemplateImport {
|
|
6
|
+
localName: string;
|
|
7
|
+
source: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Represents one prop from an exported `Props` type in a template module.
|
|
11
|
+
*/
|
|
12
|
+
export interface TemplateProp {
|
|
13
|
+
name: string;
|
|
14
|
+
type: string;
|
|
15
|
+
optional: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Maps a component name used in authoring input to per-target resolution data.
|
|
19
|
+
*/
|
|
20
|
+
export interface ComponentRegistration {
|
|
21
|
+
reactImport: string;
|
|
22
|
+
include: string;
|
|
23
|
+
}
|
|
24
|
+
export type ComponentRegistry = Record<string, ComponentRegistration>;
|
|
25
|
+
/**
|
|
26
|
+
* Parser output before filesystem metadata and import resolution are attached.
|
|
27
|
+
*/
|
|
28
|
+
export interface ParsedTemplate {
|
|
29
|
+
name: string;
|
|
30
|
+
fileName: string;
|
|
31
|
+
imports: TemplateImport[];
|
|
32
|
+
props: TemplateProp[];
|
|
33
|
+
template: Node;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Project-level template representation after the build step resolves imports.
|
|
37
|
+
*/
|
|
38
|
+
export interface BuildTemplate extends ParsedTemplate {
|
|
39
|
+
sourcePath: string;
|
|
40
|
+
relativePath: string;
|
|
41
|
+
componentRegistry: ComponentRegistry;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* One file emitted by a target.
|
|
45
|
+
*/
|
|
46
|
+
export interface TargetOutputFile {
|
|
47
|
+
fileName: string;
|
|
48
|
+
content: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Contract implemented by a concrete output format.
|
|
52
|
+
*/
|
|
53
|
+
export interface OutputTarget {
|
|
54
|
+
id: string;
|
|
55
|
+
outputSubdir: string;
|
|
56
|
+
extension: string;
|
|
57
|
+
emitFiles(template: BuildTemplate): TargetOutputFile[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Used by the parser while recursively reading a template body.
|
|
61
|
+
*/
|
|
62
|
+
export interface ParseNodesResult {
|
|
63
|
+
nodes: Node[];
|
|
64
|
+
nextIndex: number;
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|