@matthesketh/utopia-compiler 0.0.1
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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/index.cjs +834 -0
- package/dist/index.d.cts +155 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +801 -0
- package/package.json +48 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
SFCParseError: () => SFCParseError,
|
|
24
|
+
compile: () => compile,
|
|
25
|
+
compileStyle: () => compileStyle,
|
|
26
|
+
compileTemplate: () => compileTemplate,
|
|
27
|
+
generateScopeId: () => generateScopeId,
|
|
28
|
+
parse: () => parse,
|
|
29
|
+
parseTemplate: () => parseTemplate
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
|
|
33
|
+
// src/parser.ts
|
|
34
|
+
function parse(source, filename = "anonymous.utopia") {
|
|
35
|
+
const descriptor = {
|
|
36
|
+
template: null,
|
|
37
|
+
script: null,
|
|
38
|
+
style: null,
|
|
39
|
+
filename
|
|
40
|
+
};
|
|
41
|
+
const blockRe = /<(template|script|style)([\s][^>]*)?\s*>/g;
|
|
42
|
+
let match;
|
|
43
|
+
while ((match = blockRe.exec(source)) !== null) {
|
|
44
|
+
const tagName = match[1];
|
|
45
|
+
const attrString = match[2] || "";
|
|
46
|
+
const openTagStart = match.index;
|
|
47
|
+
const openTagEnd = openTagStart + match[0].length;
|
|
48
|
+
const attrs = parseAttributes(attrString);
|
|
49
|
+
const closeTag = `</${tagName}>`;
|
|
50
|
+
const closeIndex = source.indexOf(closeTag, openTagEnd);
|
|
51
|
+
if (closeIndex === -1) {
|
|
52
|
+
throw new SFCParseError(
|
|
53
|
+
`Unclosed <${tagName}> block \u2014 expected </${tagName}>`,
|
|
54
|
+
filename,
|
|
55
|
+
positionAt(source, openTagStart)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const content = source.slice(openTagEnd, closeIndex);
|
|
59
|
+
const blockEnd = closeIndex + closeTag.length;
|
|
60
|
+
const block = {
|
|
61
|
+
content,
|
|
62
|
+
attrs,
|
|
63
|
+
start: openTagStart,
|
|
64
|
+
end: blockEnd
|
|
65
|
+
};
|
|
66
|
+
if (descriptor[tagName] !== null) {
|
|
67
|
+
throw new SFCParseError(
|
|
68
|
+
`Duplicate <${tagName}> block \u2014 only one is allowed per component`,
|
|
69
|
+
filename,
|
|
70
|
+
positionAt(source, openTagStart)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
descriptor[tagName] = block;
|
|
74
|
+
blockRe.lastIndex = blockEnd;
|
|
75
|
+
}
|
|
76
|
+
return descriptor;
|
|
77
|
+
}
|
|
78
|
+
var ATTR_RE = /([a-zA-Z_][\w-]*)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
79
|
+
function parseAttributes(raw) {
|
|
80
|
+
const attrs = {};
|
|
81
|
+
let m;
|
|
82
|
+
ATTR_RE.lastIndex = 0;
|
|
83
|
+
while ((m = ATTR_RE.exec(raw)) !== null) {
|
|
84
|
+
const name = m[1];
|
|
85
|
+
const value = m[2] ?? m[3] ?? m[4] ?? true;
|
|
86
|
+
attrs[name] = value;
|
|
87
|
+
}
|
|
88
|
+
return attrs;
|
|
89
|
+
}
|
|
90
|
+
function positionAt(source, offset) {
|
|
91
|
+
let line = 1;
|
|
92
|
+
let column = 0;
|
|
93
|
+
for (let i = 0; i < offset; i++) {
|
|
94
|
+
if (source[i] === "\n") {
|
|
95
|
+
line++;
|
|
96
|
+
column = 0;
|
|
97
|
+
} else {
|
|
98
|
+
column++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { line, column };
|
|
102
|
+
}
|
|
103
|
+
var SFCParseError = class extends Error {
|
|
104
|
+
filename;
|
|
105
|
+
position;
|
|
106
|
+
constructor(message, filename, position) {
|
|
107
|
+
super(`${filename}:${position.line}:${position.column} ${message}`);
|
|
108
|
+
this.name = "SFCParseError";
|
|
109
|
+
this.filename = filename;
|
|
110
|
+
this.position = position;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/template-compiler.ts
|
|
115
|
+
function compileTemplate(template, options = {}) {
|
|
116
|
+
const ast = parseTemplate(template);
|
|
117
|
+
return generate(ast, options);
|
|
118
|
+
}
|
|
119
|
+
var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
120
|
+
"area",
|
|
121
|
+
"base",
|
|
122
|
+
"br",
|
|
123
|
+
"col",
|
|
124
|
+
"embed",
|
|
125
|
+
"hr",
|
|
126
|
+
"img",
|
|
127
|
+
"input",
|
|
128
|
+
"link",
|
|
129
|
+
"meta",
|
|
130
|
+
"param",
|
|
131
|
+
"source",
|
|
132
|
+
"track",
|
|
133
|
+
"wbr"
|
|
134
|
+
]);
|
|
135
|
+
var TemplateParser = class {
|
|
136
|
+
source;
|
|
137
|
+
pos;
|
|
138
|
+
constructor(source) {
|
|
139
|
+
this.source = source;
|
|
140
|
+
this.pos = 0;
|
|
141
|
+
}
|
|
142
|
+
parse() {
|
|
143
|
+
return this.parseChildren(null);
|
|
144
|
+
}
|
|
145
|
+
// ---- Children (text / interpolation / elements) -------------------------
|
|
146
|
+
parseChildren(parentTag) {
|
|
147
|
+
const nodes = [];
|
|
148
|
+
while (this.pos < this.source.length) {
|
|
149
|
+
if (parentTag !== null && this.lookingAt(`</${parentTag}`)) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
if (this.lookingAt("<!--")) {
|
|
153
|
+
nodes.push(this.parseComment());
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (this.lookingAt("<") && this.peekTagStart()) {
|
|
157
|
+
nodes.push(this.parseElement());
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const textOrInterp = this.parseTextOrInterpolation(parentTag);
|
|
161
|
+
if (textOrInterp.length > 0) {
|
|
162
|
+
nodes.push(...textOrInterp);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return nodes;
|
|
166
|
+
}
|
|
167
|
+
// ---- Comments -----------------------------------------------------------
|
|
168
|
+
parseComment() {
|
|
169
|
+
this.expect("<!--");
|
|
170
|
+
const endIdx = this.source.indexOf("-->", this.pos);
|
|
171
|
+
if (endIdx === -1) {
|
|
172
|
+
throw this.error("Unterminated comment");
|
|
173
|
+
}
|
|
174
|
+
const content = this.source.slice(this.pos, endIdx);
|
|
175
|
+
this.pos = endIdx + 3;
|
|
176
|
+
return { type: 4 /* Comment */, content: content.trim() };
|
|
177
|
+
}
|
|
178
|
+
// ---- Elements -----------------------------------------------------------
|
|
179
|
+
parseElement() {
|
|
180
|
+
this.expect("<");
|
|
181
|
+
const tag = this.readTagName();
|
|
182
|
+
const attrs = [];
|
|
183
|
+
const directives = [];
|
|
184
|
+
this.parseAttributeList(attrs, directives);
|
|
185
|
+
this.skipWhitespace();
|
|
186
|
+
let selfClosing = false;
|
|
187
|
+
if (this.lookingAt("/>")) {
|
|
188
|
+
this.pos += 2;
|
|
189
|
+
selfClosing = true;
|
|
190
|
+
} else {
|
|
191
|
+
this.expect(">");
|
|
192
|
+
if (VOID_ELEMENTS.has(tag.toLowerCase())) {
|
|
193
|
+
selfClosing = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
let children = [];
|
|
197
|
+
if (!selfClosing) {
|
|
198
|
+
children = this.parseChildren(tag);
|
|
199
|
+
this.expect(`</${tag}`);
|
|
200
|
+
this.skipWhitespace();
|
|
201
|
+
this.expect(">");
|
|
202
|
+
}
|
|
203
|
+
return { type: 1 /* Element */, tag, attrs, directives, children, selfClosing };
|
|
204
|
+
}
|
|
205
|
+
// ---- Attributes & Directives --------------------------------------------
|
|
206
|
+
parseAttributeList(attrs, directives) {
|
|
207
|
+
while (true) {
|
|
208
|
+
this.skipWhitespace();
|
|
209
|
+
if (this.pos >= this.source.length) break;
|
|
210
|
+
if (this.lookingAt(">") || this.lookingAt("/>")) break;
|
|
211
|
+
const name = this.readAttributeName();
|
|
212
|
+
if (!name) break;
|
|
213
|
+
let value = null;
|
|
214
|
+
this.skipWhitespace();
|
|
215
|
+
if (this.lookingAt("=")) {
|
|
216
|
+
this.pos++;
|
|
217
|
+
this.skipWhitespace();
|
|
218
|
+
value = this.readAttributeValue();
|
|
219
|
+
}
|
|
220
|
+
const dir = classifyDirective(name, value);
|
|
221
|
+
if (dir) {
|
|
222
|
+
directives.push(dir);
|
|
223
|
+
} else {
|
|
224
|
+
attrs.push({ name, value });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// ---- Text & Interpolation -----------------------------------------------
|
|
229
|
+
parseTextOrInterpolation(parentTag) {
|
|
230
|
+
const nodes = [];
|
|
231
|
+
let textBuf = "";
|
|
232
|
+
const flush = () => {
|
|
233
|
+
if (textBuf) {
|
|
234
|
+
nodes.push({ type: 2 /* Text */, content: textBuf });
|
|
235
|
+
textBuf = "";
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
while (this.pos < this.source.length) {
|
|
239
|
+
if (this.lookingAt("<")) {
|
|
240
|
+
if (parentTag !== null && this.lookingAt(`</${parentTag}`)) break;
|
|
241
|
+
if (this.lookingAt("<!--") || this.peekTagStart()) break;
|
|
242
|
+
textBuf += this.source[this.pos++];
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (this.lookingAt("{{")) {
|
|
246
|
+
flush();
|
|
247
|
+
this.pos += 2;
|
|
248
|
+
const endIdx = this.source.indexOf("}}", this.pos);
|
|
249
|
+
if (endIdx === -1) throw this.error("Unterminated interpolation {{ }}");
|
|
250
|
+
const expression = this.source.slice(this.pos, endIdx).trim();
|
|
251
|
+
nodes.push({ type: 3 /* Interpolation */, expression });
|
|
252
|
+
this.pos = endIdx + 2;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
textBuf += this.source[this.pos++];
|
|
256
|
+
}
|
|
257
|
+
flush();
|
|
258
|
+
return nodes;
|
|
259
|
+
}
|
|
260
|
+
// ---- Low-level helpers --------------------------------------------------
|
|
261
|
+
readTagName() {
|
|
262
|
+
const start = this.pos;
|
|
263
|
+
while (this.pos < this.source.length && /[a-zA-Z0-9\-_]/.test(this.source[this.pos])) {
|
|
264
|
+
this.pos++;
|
|
265
|
+
}
|
|
266
|
+
const name = this.source.slice(start, this.pos);
|
|
267
|
+
if (!name) throw this.error("Expected tag name");
|
|
268
|
+
return name;
|
|
269
|
+
}
|
|
270
|
+
readAttributeName() {
|
|
271
|
+
const start = this.pos;
|
|
272
|
+
while (this.pos < this.source.length && /[a-zA-Z0-9\-_:@.]/.test(this.source[this.pos])) {
|
|
273
|
+
this.pos++;
|
|
274
|
+
}
|
|
275
|
+
return this.source.slice(start, this.pos);
|
|
276
|
+
}
|
|
277
|
+
readAttributeValue() {
|
|
278
|
+
const quote = this.source[this.pos];
|
|
279
|
+
if (quote === '"' || quote === "'") {
|
|
280
|
+
this.pos++;
|
|
281
|
+
const start2 = this.pos;
|
|
282
|
+
const endIdx = this.source.indexOf(quote, this.pos);
|
|
283
|
+
if (endIdx === -1) throw this.error(`Unterminated attribute value (expected ${quote})`);
|
|
284
|
+
const value = this.source.slice(start2, endIdx);
|
|
285
|
+
this.pos = endIdx + 1;
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
288
|
+
const start = this.pos;
|
|
289
|
+
while (this.pos < this.source.length && !/[\s/>]/.test(this.source[this.pos])) {
|
|
290
|
+
this.pos++;
|
|
291
|
+
}
|
|
292
|
+
return this.source.slice(start, this.pos);
|
|
293
|
+
}
|
|
294
|
+
skipWhitespace() {
|
|
295
|
+
while (this.pos < this.source.length && /\s/.test(this.source[this.pos])) {
|
|
296
|
+
this.pos++;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
lookingAt(str) {
|
|
300
|
+
return this.source.startsWith(str, this.pos);
|
|
301
|
+
}
|
|
302
|
+
/** Returns true when the char after `<` looks like the start of a tag name. */
|
|
303
|
+
peekTagStart() {
|
|
304
|
+
const next = this.source[this.pos + 1];
|
|
305
|
+
return next !== void 0 && /[a-zA-Z]/.test(next);
|
|
306
|
+
}
|
|
307
|
+
expect(str) {
|
|
308
|
+
if (!this.lookingAt(str)) {
|
|
309
|
+
throw this.error(`Expected "${str}" but found "${this.source.slice(this.pos, this.pos + 20)}"`);
|
|
310
|
+
}
|
|
311
|
+
this.pos += str.length;
|
|
312
|
+
}
|
|
313
|
+
error(message) {
|
|
314
|
+
let line = 1;
|
|
315
|
+
let col = 1;
|
|
316
|
+
for (let i = 0; i < this.pos && i < this.source.length; i++) {
|
|
317
|
+
if (this.source[i] === "\n") {
|
|
318
|
+
line++;
|
|
319
|
+
col = 1;
|
|
320
|
+
} else {
|
|
321
|
+
col++;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return new Error(`Template parse error at ${line}:${col} \u2014 ${message}`);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
function parseTemplate(source) {
|
|
328
|
+
return new TemplateParser(source).parse();
|
|
329
|
+
}
|
|
330
|
+
function classifyDirective(name, value) {
|
|
331
|
+
const expression = value ?? "";
|
|
332
|
+
if (name.startsWith("@")) {
|
|
333
|
+
const parts = name.slice(1).split(".");
|
|
334
|
+
return {
|
|
335
|
+
kind: "on",
|
|
336
|
+
arg: parts[0],
|
|
337
|
+
expression,
|
|
338
|
+
modifiers: parts.slice(1)
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (name.startsWith(":")) {
|
|
342
|
+
const parts = name.slice(1).split(".");
|
|
343
|
+
return {
|
|
344
|
+
kind: "bind",
|
|
345
|
+
arg: parts[0],
|
|
346
|
+
expression,
|
|
347
|
+
modifiers: parts.slice(1)
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
if (name.startsWith("u-")) {
|
|
351
|
+
const withoutPrefix = name.slice(2);
|
|
352
|
+
const colonIdx = withoutPrefix.indexOf(":");
|
|
353
|
+
let kind;
|
|
354
|
+
let arg = null;
|
|
355
|
+
const modifiers = [];
|
|
356
|
+
if (colonIdx !== -1) {
|
|
357
|
+
kind = withoutPrefix.slice(0, colonIdx);
|
|
358
|
+
const rest = withoutPrefix.slice(colonIdx + 1);
|
|
359
|
+
const parts = rest.split(".");
|
|
360
|
+
arg = parts[0];
|
|
361
|
+
modifiers.push(...parts.slice(1));
|
|
362
|
+
} else {
|
|
363
|
+
const parts = withoutPrefix.split(".");
|
|
364
|
+
kind = parts[0];
|
|
365
|
+
modifiers.push(...parts.slice(1));
|
|
366
|
+
}
|
|
367
|
+
if (!isDirectiveKind(kind)) return null;
|
|
368
|
+
return { kind, arg, expression, modifiers };
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
function isDirectiveKind(s) {
|
|
373
|
+
return s === "on" || s === "bind" || s === "if" || s === "for" || s === "model";
|
|
374
|
+
}
|
|
375
|
+
var CodeGenerator = class {
|
|
376
|
+
constructor(options) {
|
|
377
|
+
this.options = options;
|
|
378
|
+
this.scopeId = options.scopeId;
|
|
379
|
+
}
|
|
380
|
+
code = [];
|
|
381
|
+
varCounter = 0;
|
|
382
|
+
helpers = /* @__PURE__ */ new Set();
|
|
383
|
+
scopeId;
|
|
384
|
+
generate(ast) {
|
|
385
|
+
const scope = /* @__PURE__ */ new Set();
|
|
386
|
+
const rootElements = ast.filter(
|
|
387
|
+
(n) => n.type === 1 /* Element */ || n.type === 3 /* Interpolation */ || n.type === 2 /* Text */ && n.content.trim() !== ""
|
|
388
|
+
);
|
|
389
|
+
if (rootElements.length === 0) {
|
|
390
|
+
this.helpers.add("createElement");
|
|
391
|
+
this.emit(`const _root = createElement('div')`);
|
|
392
|
+
this.emit(`return _root`);
|
|
393
|
+
} else if (rootElements.length === 1 && rootElements[0].type === 1 /* Element */) {
|
|
394
|
+
const rootVar = this.genNode(rootElements[0], scope);
|
|
395
|
+
this.emit(`return ${rootVar}`);
|
|
396
|
+
} else {
|
|
397
|
+
this.helpers.add("createElement");
|
|
398
|
+
const fragVar = this.freshVar();
|
|
399
|
+
this.emit(`const ${fragVar} = createElement('div')`);
|
|
400
|
+
if (this.scopeId) {
|
|
401
|
+
this.helpers.add("setAttr");
|
|
402
|
+
this.emit(`setAttr(${fragVar}, '${this.scopeId}', '')`);
|
|
403
|
+
}
|
|
404
|
+
for (const node of ast) {
|
|
405
|
+
const childVar = this.genNode(node, scope);
|
|
406
|
+
if (childVar) {
|
|
407
|
+
this.helpers.add("appendChild");
|
|
408
|
+
this.emit(`appendChild(${fragVar}, ${childVar})`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
this.emit(`return ${fragVar}`);
|
|
412
|
+
}
|
|
413
|
+
const helperList = Array.from(this.helpers).sort();
|
|
414
|
+
const importLine = helperList.length > 0 ? `import { ${helperList.join(", ")} } from '@matthesketh/utopia-runtime'
|
|
415
|
+
|
|
416
|
+
` : "";
|
|
417
|
+
const fnBody = this.code.map((l) => ` ${l}`).join("\n");
|
|
418
|
+
const moduleCode = `${importLine}export default function render(_ctx) {
|
|
419
|
+
${fnBody}
|
|
420
|
+
}
|
|
421
|
+
`;
|
|
422
|
+
return { code: moduleCode, helpers: this.helpers };
|
|
423
|
+
}
|
|
424
|
+
// ---- Node generation ----------------------------------------------------
|
|
425
|
+
genNode(node, scope) {
|
|
426
|
+
switch (node.type) {
|
|
427
|
+
case 1 /* Element */:
|
|
428
|
+
return this.genElement(node, scope);
|
|
429
|
+
case 2 /* Text */:
|
|
430
|
+
return this.genText(node);
|
|
431
|
+
case 3 /* Interpolation */:
|
|
432
|
+
return this.genInterpolation(node, scope);
|
|
433
|
+
case 4 /* Comment */:
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// ---- Element generation -------------------------------------------------
|
|
438
|
+
genElement(node, scope) {
|
|
439
|
+
const ifDir = node.directives.find((d) => d.kind === "if");
|
|
440
|
+
if (ifDir) {
|
|
441
|
+
return this.genIf(node, ifDir, scope);
|
|
442
|
+
}
|
|
443
|
+
const forDir = node.directives.find((d) => d.kind === "for");
|
|
444
|
+
if (forDir) {
|
|
445
|
+
return this.genFor(node, forDir, scope);
|
|
446
|
+
}
|
|
447
|
+
if (isComponentTag(node.tag)) {
|
|
448
|
+
return this.genComponent(node, scope);
|
|
449
|
+
}
|
|
450
|
+
this.helpers.add("createElement");
|
|
451
|
+
const elVar = this.freshVar();
|
|
452
|
+
this.emit(`const ${elVar} = createElement('${node.tag}')`);
|
|
453
|
+
if (this.scopeId) {
|
|
454
|
+
this.helpers.add("setAttr");
|
|
455
|
+
this.emit(`setAttr(${elVar}, '${this.scopeId}', '')`);
|
|
456
|
+
}
|
|
457
|
+
for (const attr of node.attrs) {
|
|
458
|
+
this.helpers.add("setAttr");
|
|
459
|
+
if (attr.value === null) {
|
|
460
|
+
this.emit(`setAttr(${elVar}, '${escapeStr(attr.name)}', '')`);
|
|
461
|
+
} else {
|
|
462
|
+
this.emit(`setAttr(${elVar}, '${escapeStr(attr.name)}', '${escapeStr(attr.value)}')`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
for (const dir of node.directives) {
|
|
466
|
+
if (dir.kind === "if" || dir.kind === "for") continue;
|
|
467
|
+
this.genDirective(elVar, dir, scope);
|
|
468
|
+
}
|
|
469
|
+
for (const child of node.children) {
|
|
470
|
+
const childVar = this.genNode(child, scope);
|
|
471
|
+
if (childVar) {
|
|
472
|
+
this.helpers.add("appendChild");
|
|
473
|
+
this.emit(`appendChild(${elVar}, ${childVar})`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return elVar;
|
|
477
|
+
}
|
|
478
|
+
// ---- Text & Interpolation -----------------------------------------------
|
|
479
|
+
genText(node) {
|
|
480
|
+
if (!node.content) return null;
|
|
481
|
+
this.helpers.add("createTextNode");
|
|
482
|
+
const v = this.freshVar();
|
|
483
|
+
this.emit(`const ${v} = createTextNode(${JSON.stringify(node.content)})`);
|
|
484
|
+
return v;
|
|
485
|
+
}
|
|
486
|
+
genInterpolation(node, scope) {
|
|
487
|
+
this.helpers.add("createTextNode");
|
|
488
|
+
this.helpers.add("createEffect");
|
|
489
|
+
this.helpers.add("setText");
|
|
490
|
+
const textVar = this.freshVar();
|
|
491
|
+
const expr = this.resolveExpression(node.expression, scope);
|
|
492
|
+
this.emit(`const ${textVar} = createTextNode('')`);
|
|
493
|
+
this.emit(`createEffect(() => setText(${textVar}, String(${expr})))`);
|
|
494
|
+
return textVar;
|
|
495
|
+
}
|
|
496
|
+
// ---- Directives ---------------------------------------------------------
|
|
497
|
+
genDirective(elVar, dir, scope) {
|
|
498
|
+
switch (dir.kind) {
|
|
499
|
+
case "on":
|
|
500
|
+
this.genOn(elVar, dir, scope);
|
|
501
|
+
break;
|
|
502
|
+
case "bind":
|
|
503
|
+
this.genBind(elVar, dir, scope);
|
|
504
|
+
break;
|
|
505
|
+
case "model":
|
|
506
|
+
this.genModel(elVar, dir, scope);
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
genOn(elVar, dir, scope) {
|
|
511
|
+
this.helpers.add("addEventListener");
|
|
512
|
+
const event = dir.arg ?? "click";
|
|
513
|
+
const handler = this.resolveExpression(dir.expression, scope);
|
|
514
|
+
this.emit(`addEventListener(${elVar}, '${escapeStr(event)}', ${handler})`);
|
|
515
|
+
}
|
|
516
|
+
genBind(elVar, dir, scope) {
|
|
517
|
+
this.helpers.add("setAttr");
|
|
518
|
+
this.helpers.add("createEffect");
|
|
519
|
+
const attrName = dir.arg ?? "value";
|
|
520
|
+
const expr = this.resolveExpression(dir.expression, scope);
|
|
521
|
+
this.emit(`createEffect(() => setAttr(${elVar}, '${escapeStr(attrName)}', ${expr}))`);
|
|
522
|
+
}
|
|
523
|
+
genModel(elVar, dir, scope) {
|
|
524
|
+
this.helpers.add("setAttr");
|
|
525
|
+
this.helpers.add("addEventListener");
|
|
526
|
+
this.helpers.add("createEffect");
|
|
527
|
+
const signalRef = this.resolveExpression(dir.expression, scope);
|
|
528
|
+
this.emit(`createEffect(() => setAttr(${elVar}, 'value', ${signalRef}()))`);
|
|
529
|
+
this.emit(
|
|
530
|
+
`addEventListener(${elVar}, 'input', (e) => ${signalRef}.set(e.target.value))`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
// ---- Structural: u-if ---------------------------------------------------
|
|
534
|
+
genIf(node, dir, scope) {
|
|
535
|
+
this.helpers.add("createIf");
|
|
536
|
+
const anchorVar = this.freshVar();
|
|
537
|
+
this.helpers.add("createComment");
|
|
538
|
+
this.emit(`const ${anchorVar} = createComment('u-if')`);
|
|
539
|
+
const condition = this.resolveExpression(dir.expression, scope);
|
|
540
|
+
const strippedNode = {
|
|
541
|
+
...node,
|
|
542
|
+
directives: node.directives.filter((d) => d.kind !== "if")
|
|
543
|
+
};
|
|
544
|
+
const trueFnVar = this.freshVar();
|
|
545
|
+
const savedCode = this.code;
|
|
546
|
+
this.code = [];
|
|
547
|
+
const innerVar = this.genElement(strippedNode, scope);
|
|
548
|
+
const innerLines = [...this.code];
|
|
549
|
+
this.code = savedCode;
|
|
550
|
+
this.emit(`const ${trueFnVar} = () => {`);
|
|
551
|
+
for (const line of innerLines) {
|
|
552
|
+
this.emit(` ${line}`);
|
|
553
|
+
}
|
|
554
|
+
this.emit(` return ${innerVar}`);
|
|
555
|
+
this.emit(`}`);
|
|
556
|
+
this.emit(`createIf(${anchorVar}, () => Boolean(${condition}), ${trueFnVar})`);
|
|
557
|
+
return anchorVar;
|
|
558
|
+
}
|
|
559
|
+
// ---- Structural: u-for --------------------------------------------------
|
|
560
|
+
genFor(node, dir, scope) {
|
|
561
|
+
this.helpers.add("createFor");
|
|
562
|
+
const anchorVar = this.freshVar();
|
|
563
|
+
this.helpers.add("createComment");
|
|
564
|
+
this.emit(`const ${anchorVar} = createComment('u-for')`);
|
|
565
|
+
const forMatch = dir.expression.match(/^\s*(\w+)\s+in\s+(.+)$/);
|
|
566
|
+
if (!forMatch) {
|
|
567
|
+
throw new Error(`Invalid u-for expression: "${dir.expression}"`);
|
|
568
|
+
}
|
|
569
|
+
const itemName = forMatch[1];
|
|
570
|
+
const listExpr = this.resolveExpression(forMatch[2].trim(), scope);
|
|
571
|
+
const innerScope = new Set(scope);
|
|
572
|
+
innerScope.add(itemName);
|
|
573
|
+
const strippedNode = {
|
|
574
|
+
...node,
|
|
575
|
+
directives: node.directives.filter((d) => d.kind !== "for")
|
|
576
|
+
};
|
|
577
|
+
const savedCode = this.code;
|
|
578
|
+
this.code = [];
|
|
579
|
+
const innerVar = this.genElement(strippedNode, innerScope);
|
|
580
|
+
const innerLines = [...this.code];
|
|
581
|
+
this.code = savedCode;
|
|
582
|
+
const renderFnVar = this.freshVar();
|
|
583
|
+
this.emit(`const ${renderFnVar} = (${itemName}, _index) => {`);
|
|
584
|
+
for (const line of innerLines) {
|
|
585
|
+
this.emit(` ${line}`);
|
|
586
|
+
}
|
|
587
|
+
this.emit(` return ${innerVar}`);
|
|
588
|
+
this.emit(`}`);
|
|
589
|
+
this.emit(`createFor(${anchorVar}, () => ${listExpr}, ${renderFnVar})`);
|
|
590
|
+
return anchorVar;
|
|
591
|
+
}
|
|
592
|
+
// ---- Component generation -----------------------------------------------
|
|
593
|
+
genComponent(node, scope) {
|
|
594
|
+
const compVar = this.freshVar();
|
|
595
|
+
const propEntries = [];
|
|
596
|
+
for (const a of node.attrs) {
|
|
597
|
+
if (a.value !== null) {
|
|
598
|
+
propEntries.push(`${a.name}: '${escapeStr(a.value)}'`);
|
|
599
|
+
} else {
|
|
600
|
+
propEntries.push(`${a.name}: true`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
for (const d of node.directives) {
|
|
604
|
+
if (d.kind === "bind" && d.arg) {
|
|
605
|
+
propEntries.push(`${d.arg}: ${this.resolveExpression(d.expression, scope)}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const propsStr = propEntries.length > 0 ? `{ ${propEntries.join(", ")} }` : "{}";
|
|
609
|
+
this.emit(`const ${compVar} = _ctx.${node.tag}(${propsStr})`);
|
|
610
|
+
return compVar;
|
|
611
|
+
}
|
|
612
|
+
// ---- Expression resolution ----------------------------------------------
|
|
613
|
+
/**
|
|
614
|
+
* Resolve a template expression to a JS expression.
|
|
615
|
+
*
|
|
616
|
+
* If the leading identifier is in `scope` (a u-for item variable), it is
|
|
617
|
+
* emitted as a bare variable reference. Otherwise it is prefixed with
|
|
618
|
+
* `_ctx.` to access the component context.
|
|
619
|
+
*/
|
|
620
|
+
resolveExpression(expr, scope) {
|
|
621
|
+
const trimmed = expr.trim();
|
|
622
|
+
if (!trimmed) return "''";
|
|
623
|
+
const leadIdMatch = trimmed.match(/^([a-zA-Z_$][\w$]*)/);
|
|
624
|
+
if (!leadIdMatch) {
|
|
625
|
+
return trimmed;
|
|
626
|
+
}
|
|
627
|
+
const leadId = leadIdMatch[1];
|
|
628
|
+
if (scope.has(leadId)) {
|
|
629
|
+
return trimmed;
|
|
630
|
+
}
|
|
631
|
+
if (trimmed.includes("=>")) {
|
|
632
|
+
return trimmed;
|
|
633
|
+
}
|
|
634
|
+
return `_ctx.${trimmed}`;
|
|
635
|
+
}
|
|
636
|
+
// ---- Utilities ----------------------------------------------------------
|
|
637
|
+
freshVar() {
|
|
638
|
+
return `_el${this.varCounter++}`;
|
|
639
|
+
}
|
|
640
|
+
emit(line) {
|
|
641
|
+
this.code.push(line);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
function isComponentTag(tag) {
|
|
645
|
+
return /^[A-Z]/.test(tag);
|
|
646
|
+
}
|
|
647
|
+
function escapeStr(s) {
|
|
648
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
649
|
+
}
|
|
650
|
+
function generate(ast, options) {
|
|
651
|
+
const gen = new CodeGenerator(options);
|
|
652
|
+
return gen.generate(ast);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/style-compiler.ts
|
|
656
|
+
function compileStyle(options) {
|
|
657
|
+
const { source, filename, scoped, scopeId: overrideScopeId } = options;
|
|
658
|
+
if (!scoped) {
|
|
659
|
+
return { css: source, scopeId: null };
|
|
660
|
+
}
|
|
661
|
+
const scopeId = overrideScopeId ?? generateScopeId(filename);
|
|
662
|
+
const css = scopeSelectors(source, scopeId);
|
|
663
|
+
return { css, scopeId };
|
|
664
|
+
}
|
|
665
|
+
function generateScopeId(filename) {
|
|
666
|
+
let hash = 5381;
|
|
667
|
+
for (let i = 0; i < filename.length; i++) {
|
|
668
|
+
hash = (hash << 5) + hash + filename.charCodeAt(i) >>> 0;
|
|
669
|
+
}
|
|
670
|
+
return `data-u-${hash.toString(16).padStart(8, "0")}`;
|
|
671
|
+
}
|
|
672
|
+
function scopeSelectors(css, scopeId) {
|
|
673
|
+
const result = [];
|
|
674
|
+
let pos = 0;
|
|
675
|
+
while (pos < css.length) {
|
|
676
|
+
if (/\s/.test(css[pos])) {
|
|
677
|
+
result.push(css[pos]);
|
|
678
|
+
pos++;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (css[pos] === "/" && css[pos + 1] === "*") {
|
|
682
|
+
const endIdx = css.indexOf("*/", pos + 2);
|
|
683
|
+
if (endIdx === -1) {
|
|
684
|
+
result.push(css.slice(pos));
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
result.push(css.slice(pos, endIdx + 2));
|
|
688
|
+
pos = endIdx + 2;
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (css[pos] === "@") {
|
|
692
|
+
const atResult = consumeAtRule(css, pos, scopeId);
|
|
693
|
+
result.push(atResult.text);
|
|
694
|
+
pos = atResult.end;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (css[pos] === "}") {
|
|
698
|
+
result.push("}");
|
|
699
|
+
pos++;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
const ruleResult = consumeRuleSet(css, pos, scopeId);
|
|
703
|
+
result.push(ruleResult.text);
|
|
704
|
+
pos = ruleResult.end;
|
|
705
|
+
}
|
|
706
|
+
return result.join("");
|
|
707
|
+
}
|
|
708
|
+
function consumeAtRule(css, pos, scopeId) {
|
|
709
|
+
const start = pos;
|
|
710
|
+
let depth = 0;
|
|
711
|
+
let headerEnd = -1;
|
|
712
|
+
for (let i = pos; i < css.length; i++) {
|
|
713
|
+
if (css[i] === "{") {
|
|
714
|
+
headerEnd = i;
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
if (css[i] === ";") {
|
|
718
|
+
return { text: css.slice(start, i + 1), end: i + 1 };
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (headerEnd === -1) {
|
|
722
|
+
return { text: css.slice(start), end: css.length };
|
|
723
|
+
}
|
|
724
|
+
const header = css.slice(start, headerEnd);
|
|
725
|
+
const isKeyframes = /^@(?:-\w+-)?keyframes\b/.test(header.trim());
|
|
726
|
+
depth = 1;
|
|
727
|
+
let bodyStart = headerEnd + 1;
|
|
728
|
+
let bodyEnd = headerEnd + 1;
|
|
729
|
+
for (let i = bodyStart; i < css.length && depth > 0; i++) {
|
|
730
|
+
if (css[i] === "{") depth++;
|
|
731
|
+
else if (css[i] === "}") depth--;
|
|
732
|
+
bodyEnd = i;
|
|
733
|
+
}
|
|
734
|
+
const body = css.slice(bodyStart, bodyEnd);
|
|
735
|
+
let scopedBody;
|
|
736
|
+
if (isKeyframes) {
|
|
737
|
+
scopedBody = body;
|
|
738
|
+
} else {
|
|
739
|
+
scopedBody = scopeSelectors(body, scopeId);
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
text: `${header}{${scopedBody}}`,
|
|
743
|
+
end: bodyEnd + 1
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function consumeRuleSet(css, pos, scopeId) {
|
|
747
|
+
const braceIdx = css.indexOf("{", pos);
|
|
748
|
+
if (braceIdx === -1) {
|
|
749
|
+
return { text: css.slice(pos), end: css.length };
|
|
750
|
+
}
|
|
751
|
+
const selectorText = css.slice(pos, braceIdx);
|
|
752
|
+
let depth = 1;
|
|
753
|
+
let endIdx = braceIdx + 1;
|
|
754
|
+
for (; endIdx < css.length && depth > 0; endIdx++) {
|
|
755
|
+
if (css[endIdx] === "{") depth++;
|
|
756
|
+
else if (css[endIdx] === "}") depth--;
|
|
757
|
+
}
|
|
758
|
+
const declarations = css.slice(braceIdx + 1, endIdx - 1);
|
|
759
|
+
const scopedSelectors = selectorText.split(",").map((sel) => scopeSingleSelector(sel.trim(), scopeId)).join(", ");
|
|
760
|
+
return {
|
|
761
|
+
text: `${scopedSelectors} {${declarations}}`,
|
|
762
|
+
end: endIdx
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function scopeSingleSelector(selector, scopeId) {
|
|
766
|
+
if (!selector) return selector;
|
|
767
|
+
const attr = `[${scopeId}]`;
|
|
768
|
+
const pseudoRe = /(?:::?[\w-]+(?:\([^)]*\))?)+$/;
|
|
769
|
+
const pseudoMatch = selector.match(pseudoRe);
|
|
770
|
+
if (pseudoMatch) {
|
|
771
|
+
const beforePseudo = selector.slice(0, pseudoMatch.index);
|
|
772
|
+
return `${beforePseudo}${attr}${pseudoMatch[0]}`;
|
|
773
|
+
}
|
|
774
|
+
return `${selector}${attr}`;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/index.ts
|
|
778
|
+
function compile(source, options = {}) {
|
|
779
|
+
const filename = options.filename ?? "anonymous.utopia";
|
|
780
|
+
const descriptor = parse(source, filename);
|
|
781
|
+
let css = "";
|
|
782
|
+
let scopeId = null;
|
|
783
|
+
if (descriptor.style) {
|
|
784
|
+
const isScoped = "scoped" in descriptor.style.attrs;
|
|
785
|
+
const styleResult = compileStyle({
|
|
786
|
+
source: descriptor.style.content,
|
|
787
|
+
filename,
|
|
788
|
+
scoped: isScoped,
|
|
789
|
+
scopeId: options.scopeId
|
|
790
|
+
});
|
|
791
|
+
css = styleResult.css;
|
|
792
|
+
scopeId = styleResult.scopeId;
|
|
793
|
+
}
|
|
794
|
+
let renderModule = "";
|
|
795
|
+
if (descriptor.template) {
|
|
796
|
+
const templateResult = compileTemplate(descriptor.template.content, {
|
|
797
|
+
scopeId: scopeId ?? void 0
|
|
798
|
+
});
|
|
799
|
+
renderModule = templateResult.code;
|
|
800
|
+
}
|
|
801
|
+
const { imports, body } = splitModuleParts(renderModule);
|
|
802
|
+
const scriptContent = descriptor.script?.content ?? "";
|
|
803
|
+
const parts = [];
|
|
804
|
+
if (imports) {
|
|
805
|
+
parts.push(imports);
|
|
806
|
+
}
|
|
807
|
+
if (scriptContent.trim()) {
|
|
808
|
+
parts.push(scriptContent.trim());
|
|
809
|
+
}
|
|
810
|
+
if (body) {
|
|
811
|
+
parts.push(body);
|
|
812
|
+
}
|
|
813
|
+
const code = parts.join("\n\n") + "\n";
|
|
814
|
+
return { code, css };
|
|
815
|
+
}
|
|
816
|
+
function splitModuleParts(moduleCode) {
|
|
817
|
+
const idx = moduleCode.indexOf("\n\n");
|
|
818
|
+
if (idx === -1) {
|
|
819
|
+
return { imports: "", body: moduleCode };
|
|
820
|
+
}
|
|
821
|
+
const imports = moduleCode.slice(0, idx).trim();
|
|
822
|
+
const body = moduleCode.slice(idx).trim();
|
|
823
|
+
return { imports, body };
|
|
824
|
+
}
|
|
825
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
826
|
+
0 && (module.exports = {
|
|
827
|
+
SFCParseError,
|
|
828
|
+
compile,
|
|
829
|
+
compileStyle,
|
|
830
|
+
compileTemplate,
|
|
831
|
+
generateScopeId,
|
|
832
|
+
parse,
|
|
833
|
+
parseTemplate
|
|
834
|
+
});
|