@canvasengine/compiler 2.0.0-beta.3 → 2.0.0-beta.31
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/dist/index.d.ts +35 -2
- package/dist/index.js +48 -7
- package/dist/index.js.map +1 -1
- package/grammar.pegjs +736 -55
- package/index.ts +96 -9
- package/package.json +2 -2
- package/tests/compiler.spec.ts +838 -25
package/grammar.pegjs
CHANGED
|
@@ -5,6 +5,134 @@
|
|
|
5
5
|
`at line ${start.line}, column ${start.column} to line ${end.line}, column ${end.column}`;
|
|
6
6
|
throw new Error(errorMessage);
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
/*——— Custom error handler for syntax errors ———*/
|
|
10
|
+
function parseError(error) {
|
|
11
|
+
// error.expected : array of { type, description }
|
|
12
|
+
// error.found : string | null
|
|
13
|
+
// error.location : { start, end }
|
|
14
|
+
const { expected, found, location } = error;
|
|
15
|
+
|
|
16
|
+
// Group expected items by description to avoid duplicates
|
|
17
|
+
const uniqueExpected = [...new Set(expected.map(e => e.description))];
|
|
18
|
+
|
|
19
|
+
// Format the expected values in a more readable way
|
|
20
|
+
const expectedDesc = uniqueExpected
|
|
21
|
+
.map(desc => `'${desc}'`)
|
|
22
|
+
.join(' or ');
|
|
23
|
+
|
|
24
|
+
const foundDesc = found === null ? 'end of input' : `'${found}'`;
|
|
25
|
+
|
|
26
|
+
generateError(
|
|
27
|
+
`Syntax error: expected ${expectedDesc} but found ${foundDesc}`,
|
|
28
|
+
location
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// List of standard HTML DOM elements
|
|
33
|
+
const domElements = new Set([
|
|
34
|
+
'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 'samp', 's', 'script', 'section', 'select', 'slot', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr'
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// Framework components that should NOT be transformed to DOM elements
|
|
38
|
+
const frameworkComponents = new Set([
|
|
39
|
+
'Canvas', 'Container', 'Sprite', 'Text', 'DOMElement', 'Svg', 'Button'
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
// DisplayObject special attributes that should not be in attrs
|
|
43
|
+
const displayObjectAttributes = new Set([
|
|
44
|
+
'x', 'y', 'scale', 'anchor', 'skew', 'tint', 'rotation', 'angle',
|
|
45
|
+
'zIndex', 'roundPixels', 'cursor', 'visible', 'alpha', 'pivot', 'filters', 'maskOf',
|
|
46
|
+
'blendMode', 'filterArea', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
|
|
47
|
+
'aspectRatio', 'flexGrow', 'flexShrink', 'flexBasis', 'rowGap', 'columnGap',
|
|
48
|
+
'positionType', 'top', 'right', 'bottom', 'left', 'objectFit', 'objectPosition',
|
|
49
|
+
'transformOrigin', 'flexDirection', 'justifyContent', 'alignItems', 'alignContent',
|
|
50
|
+
'alignSelf', 'margin', 'padding', 'border', 'gap', 'blur', 'shadow'
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
function isDOMElement(tagName) {
|
|
54
|
+
// Don't transform framework components to DOM elements
|
|
55
|
+
if (frameworkComponents.has(tagName)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return domElements.has(tagName.toLowerCase());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatAttributes(attributes) {
|
|
62
|
+
if (attributes.length === 0) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check if there's exactly one attribute and it's a spread attribute
|
|
67
|
+
if (attributes.length === 1 && attributes[0].startsWith('...')) {
|
|
68
|
+
// Return the identifier directly, removing the '...'
|
|
69
|
+
return attributes[0].substring(3);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Otherwise, format as an object literal
|
|
73
|
+
const formattedAttrs = attributes.map(attr => {
|
|
74
|
+
// If it's a spread attribute, keep it as is
|
|
75
|
+
if (attr.startsWith('...')) {
|
|
76
|
+
return attr;
|
|
77
|
+
}
|
|
78
|
+
// If it's a standalone attribute (doesn't contain ':'), format as shorthand property 'name'
|
|
79
|
+
if (!attr.includes(':')) {
|
|
80
|
+
return attr; // JS object literal shorthand
|
|
81
|
+
}
|
|
82
|
+
// Otherwise (key: value), keep it as is
|
|
83
|
+
return attr;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return `{ ${formattedAttrs.join(', ')} }`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatDOMElement(tagName, attributes) {
|
|
90
|
+
if (attributes.length === 0) {
|
|
91
|
+
return `h(DOMElement, { element: "${tagName}" })`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Separate DisplayObject attributes from DOM attributes
|
|
95
|
+
const domAttrs = [];
|
|
96
|
+
const displayObjectAttrs = [];
|
|
97
|
+
|
|
98
|
+
attributes.forEach(attr => {
|
|
99
|
+
// Handle spread attributes
|
|
100
|
+
if (attr.startsWith('...')) {
|
|
101
|
+
displayObjectAttrs.push(attr);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Extract attribute name
|
|
106
|
+
let attrName;
|
|
107
|
+
if (attr.includes(':')) {
|
|
108
|
+
// Format: "name: value" or "'name': value"
|
|
109
|
+
attrName = attr.split(':')[0].trim().replace(/['"]/g, '');
|
|
110
|
+
} else {
|
|
111
|
+
// Standalone attribute
|
|
112
|
+
attrName = attr.replace(/['"]/g, '');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check if it's a DisplayObject attribute
|
|
116
|
+
if (displayObjectAttributes.has(attrName)) {
|
|
117
|
+
displayObjectAttrs.push(attr);
|
|
118
|
+
} else {
|
|
119
|
+
domAttrs.push(attr);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Build the result
|
|
124
|
+
const parts = [`element: "${tagName}"`];
|
|
125
|
+
|
|
126
|
+
if (domAttrs.length > 0) {
|
|
127
|
+
parts.push(`attrs: { ${domAttrs.join(', ')} }`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (displayObjectAttrs.length > 0) {
|
|
131
|
+
parts.push(...displayObjectAttrs);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return `h(DOMElement, { ${parts.join(', ')} })`;
|
|
135
|
+
}
|
|
8
136
|
}
|
|
9
137
|
|
|
10
138
|
start
|
|
@@ -15,30 +143,246 @@ start
|
|
|
15
143
|
return `[${elements.join(',')}]`;
|
|
16
144
|
}
|
|
17
145
|
|
|
18
|
-
element
|
|
146
|
+
element "component or control structure"
|
|
19
147
|
= forLoop
|
|
20
148
|
/ ifCondition
|
|
149
|
+
/ svgElement
|
|
150
|
+
/ domElementWithText
|
|
21
151
|
/ selfClosingElement
|
|
22
152
|
/ openCloseElement
|
|
23
|
-
/
|
|
153
|
+
/ openUnclosedTag
|
|
154
|
+
/ comment
|
|
24
155
|
|
|
25
|
-
selfClosingElement
|
|
156
|
+
selfClosingElement "self-closing component tag"
|
|
26
157
|
= _ "<" _ tagName:tagName _ attributes:attributes _ "/>" _ {
|
|
27
|
-
|
|
28
|
-
|
|
158
|
+
// Check if it's a DOM element
|
|
159
|
+
if (isDOMElement(tagName)) {
|
|
160
|
+
return formatDOMElement(tagName, attributes);
|
|
161
|
+
}
|
|
162
|
+
// Otherwise, treat as regular component
|
|
163
|
+
const attrsString = formatAttributes(attributes);
|
|
164
|
+
return attrsString ? `h(${tagName}, ${attrsString})` : `h(${tagName})`;
|
|
29
165
|
}
|
|
30
166
|
|
|
31
|
-
|
|
32
|
-
= "<" _ tagName:tagName _ attributes:attributes _ ">" _
|
|
167
|
+
domElementWithText "DOM element with text content"
|
|
168
|
+
= "<" _ tagName:tagName _ attributes:attributes _ ">" _ text:simpleTextContent _ "</" _ closingTagName:tagName _ ">" _ {
|
|
33
169
|
if (tagName !== closingTagName) {
|
|
34
|
-
|
|
170
|
+
generateError(
|
|
171
|
+
`Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
|
|
172
|
+
location()
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (isDOMElement(tagName)) {
|
|
177
|
+
if (attributes.length === 0) {
|
|
178
|
+
return `h(DOMElement, { element: "${tagName}", textContent: ${text} })`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Separate DisplayObject attributes from DOM attributes
|
|
182
|
+
const domAttrs = [];
|
|
183
|
+
const displayObjectAttrs = [];
|
|
184
|
+
|
|
185
|
+
attributes.forEach(attr => {
|
|
186
|
+
// Handle spread attributes
|
|
187
|
+
if (attr.startsWith('...')) {
|
|
188
|
+
displayObjectAttrs.push(attr);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Extract attribute name
|
|
193
|
+
let attrName;
|
|
194
|
+
if (attr.includes(':')) {
|
|
195
|
+
// Format: "name: value" or "'name': value"
|
|
196
|
+
attrName = attr.split(':')[0].trim().replace(/['"]/g, '');
|
|
197
|
+
} else {
|
|
198
|
+
// Standalone attribute
|
|
199
|
+
attrName = attr.replace(/['"]/g, '');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check if it's a DisplayObject attribute
|
|
203
|
+
if (displayObjectAttributes.has(attrName)) {
|
|
204
|
+
displayObjectAttrs.push(attr);
|
|
205
|
+
} else {
|
|
206
|
+
domAttrs.push(attr);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Build the result
|
|
211
|
+
const parts = [`element: "${tagName}"`];
|
|
212
|
+
|
|
213
|
+
if (domAttrs.length > 0) {
|
|
214
|
+
parts.push(`attrs: { ${domAttrs.join(', ')} }`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
parts.push(`textContent: ${text}`);
|
|
218
|
+
|
|
219
|
+
if (displayObjectAttrs.length > 0) {
|
|
220
|
+
parts.push(...displayObjectAttrs);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return `h(DOMElement, { ${parts.join(', ')} })`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// If not a DOM element, fall back to regular parsing
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
simpleTextContent "simple text content"
|
|
231
|
+
= parts:(simpleDynamicPart / simpleTextPart)+ {
|
|
232
|
+
const validParts = parts.filter(p => p !== null);
|
|
233
|
+
if (validParts.length === 0) return null;
|
|
234
|
+
if (validParts.length === 1) return validParts[0];
|
|
235
|
+
|
|
236
|
+
// Multiple parts - need to concatenate
|
|
237
|
+
const hasSignals = validParts.some(part => part && part.includes && part.includes('()'));
|
|
238
|
+
if (hasSignals) {
|
|
239
|
+
return `computed(() => ${validParts.join(' + ')})`;
|
|
240
|
+
}
|
|
241
|
+
return validParts.join(' + ');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
simpleTextPart "simple text part"
|
|
245
|
+
= !("@for" / "@if") text:$([^<{@]+) {
|
|
246
|
+
const trimmed = text.trim();
|
|
247
|
+
return trimmed ? `'${trimmed}'` : null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
simpleDynamicPart "simple dynamic part"
|
|
251
|
+
= "{{" _ expr:attributeValue _ "}}" {
|
|
252
|
+
// Handle double brace expressions like {{ object.x }} or {{ @object.x }} or {{ @object.@x }}
|
|
253
|
+
if (expr.trim().match(/^(@?[a-zA-Z_][a-zA-Z0-9_]*)(\.@?[a-zA-Z_][a-zA-Z0-9_]*)*$/)) {
|
|
254
|
+
let foundSignal = false;
|
|
255
|
+
let hasLiterals = false;
|
|
256
|
+
|
|
257
|
+
// Split by dots to handle each part separately
|
|
258
|
+
const parts = expr.split('.');
|
|
259
|
+
const allLiterals = parts.every(part => part.trim().startsWith('@'));
|
|
260
|
+
|
|
261
|
+
let computedValue;
|
|
262
|
+
|
|
263
|
+
if (allLiterals) {
|
|
264
|
+
// All parts are literals, just remove @ prefixes
|
|
265
|
+
computedValue = parts.map(part => part.replace('@', '')).join('.');
|
|
266
|
+
hasLiterals = true;
|
|
267
|
+
} else {
|
|
268
|
+
// Transform each part individually
|
|
269
|
+
computedValue = parts.map(part => {
|
|
270
|
+
const trimmedPart = part.trim();
|
|
271
|
+
if (trimmedPart.startsWith('@')) {
|
|
272
|
+
hasLiterals = true;
|
|
273
|
+
return trimmedPart.substring(1); // Remove @ prefix for literals
|
|
274
|
+
} else {
|
|
275
|
+
// Don't transform keywords
|
|
276
|
+
if (['true', 'false', 'null'].includes(trimmedPart)) {
|
|
277
|
+
return trimmedPart;
|
|
278
|
+
}
|
|
279
|
+
foundSignal = true;
|
|
280
|
+
return `${trimmedPart}()`;
|
|
281
|
+
}
|
|
282
|
+
}).join('.');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (foundSignal && !allLiterals) {
|
|
286
|
+
return `computed(() => ${computedValue})`;
|
|
287
|
+
}
|
|
288
|
+
return computedValue;
|
|
289
|
+
}
|
|
290
|
+
return expr;
|
|
291
|
+
}
|
|
292
|
+
/ "{" _ expr:attributeValue _ "}" {
|
|
293
|
+
// Handle single brace expressions like {item.name} or {@text}
|
|
294
|
+
if (expr.trim().match(/^@?[a-zA-Z_][a-zA-Z0-9_.]*$/)) {
|
|
295
|
+
let foundSignal = false;
|
|
296
|
+
const computedValue = expr.replace(/@?[a-zA-Z_][a-zA-Z0-9_]*(?!:)/g, (match) => {
|
|
297
|
+
if (match.startsWith('@')) {
|
|
298
|
+
return match.substring(1);
|
|
299
|
+
}
|
|
300
|
+
foundSignal = true;
|
|
301
|
+
return `${match}()`;
|
|
302
|
+
});
|
|
303
|
+
if (foundSignal) {
|
|
304
|
+
return `computed(() => ${computedValue})`;
|
|
305
|
+
}
|
|
306
|
+
return computedValue;
|
|
307
|
+
}
|
|
308
|
+
return expr;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
openCloseElement "component with content"
|
|
312
|
+
= "<" _ tagName:tagName _ attributes:attributes _ ">" _ content:content _ "</" _ closingTagName:tagName _ ">" _ {
|
|
313
|
+
if (tagName !== closingTagName) {
|
|
314
|
+
generateError(
|
|
315
|
+
`Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
|
|
316
|
+
location()
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check if it's a DOM element
|
|
321
|
+
if (isDOMElement(tagName)) {
|
|
322
|
+
const children = content ? content : null;
|
|
323
|
+
|
|
324
|
+
if (attributes.length === 0) {
|
|
325
|
+
if (children) {
|
|
326
|
+
return `h(DOMElement, { element: "${tagName}" }, ${children})`;
|
|
327
|
+
} else {
|
|
328
|
+
return `h(DOMElement, { element: "${tagName}" })`;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Separate DisplayObject attributes from DOM attributes
|
|
333
|
+
const domAttrs = [];
|
|
334
|
+
const displayObjectAttrs = [];
|
|
335
|
+
|
|
336
|
+
attributes.forEach(attr => {
|
|
337
|
+
// Handle spread attributes
|
|
338
|
+
if (attr.startsWith('...')) {
|
|
339
|
+
displayObjectAttrs.push(attr);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Extract attribute name
|
|
344
|
+
let attrName;
|
|
345
|
+
if (attr.includes(':')) {
|
|
346
|
+
// Format: "name: value" or "'name': value"
|
|
347
|
+
attrName = attr.split(':')[0].trim().replace(/['"]/g, '');
|
|
348
|
+
} else {
|
|
349
|
+
// Standalone attribute
|
|
350
|
+
attrName = attr.replace(/['"]/g, '');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Check if it's a DisplayObject attribute
|
|
354
|
+
if (displayObjectAttributes.has(attrName)) {
|
|
355
|
+
displayObjectAttrs.push(attr);
|
|
356
|
+
} else {
|
|
357
|
+
domAttrs.push(attr);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Build the result
|
|
362
|
+
const parts = [`element: "${tagName}"`];
|
|
363
|
+
|
|
364
|
+
if (domAttrs.length > 0) {
|
|
365
|
+
parts.push(`attrs: { ${domAttrs.join(', ')} }`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (displayObjectAttrs.length > 0) {
|
|
369
|
+
parts.push(...displayObjectAttrs);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (children) {
|
|
373
|
+
return `h(DOMElement, { ${parts.join(', ')} }, ${children})`;
|
|
374
|
+
} else {
|
|
375
|
+
return `h(DOMElement, { ${parts.join(', ')} })`;
|
|
376
|
+
}
|
|
35
377
|
}
|
|
36
|
-
|
|
378
|
+
|
|
379
|
+
// Otherwise, treat as regular component
|
|
380
|
+
const attrsString = formatAttributes(attributes);
|
|
37
381
|
const children = content ? content : null;
|
|
38
|
-
if (
|
|
39
|
-
return `h(${tagName}, ${
|
|
40
|
-
} else if (
|
|
41
|
-
return `h(${tagName}, ${
|
|
382
|
+
if (attrsString && children) {
|
|
383
|
+
return `h(${tagName}, ${attrsString}, ${children})`;
|
|
384
|
+
} else if (attrsString) {
|
|
385
|
+
return `h(${tagName}, ${attrsString})`;
|
|
42
386
|
} else if (children) {
|
|
43
387
|
return `h(${tagName}, null, ${children})`;
|
|
44
388
|
} else {
|
|
@@ -46,59 +390,243 @@ openCloseElement
|
|
|
46
390
|
}
|
|
47
391
|
}
|
|
48
392
|
|
|
49
|
-
|
|
50
|
-
generateError("Syntaxe d'élément invalide", location());
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
attributes
|
|
393
|
+
attributes "component attributes"
|
|
54
394
|
= attrs:(attribute (_ attribute)*)? {
|
|
55
395
|
return attrs
|
|
56
396
|
? [attrs[0]].concat(attrs[1].map(a => a[1]))
|
|
57
397
|
: [];
|
|
58
398
|
}
|
|
59
399
|
|
|
60
|
-
attribute
|
|
400
|
+
attribute "attribute"
|
|
61
401
|
= staticAttribute
|
|
62
402
|
/ dynamicAttribute
|
|
63
403
|
/ eventHandler
|
|
404
|
+
/ spreadAttribute
|
|
405
|
+
/ unclosedQuote
|
|
406
|
+
/ unclosedBrace
|
|
407
|
+
|
|
408
|
+
spreadAttribute "spread attribute"
|
|
409
|
+
= "..." expr:(functionCallExpr / dotNotation) {
|
|
410
|
+
return "..." + expr;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
functionCallExpr "function call"
|
|
414
|
+
= name:dotNotation "(" args:functionArgs? ")" {
|
|
415
|
+
return `${name}(${args || ''})`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
dotNotation "property access"
|
|
419
|
+
= first:identifier rest:("." identifier)* {
|
|
420
|
+
return text();
|
|
421
|
+
}
|
|
64
422
|
|
|
65
|
-
eventHandler
|
|
423
|
+
eventHandler "event handler"
|
|
66
424
|
= "@" eventName:identifier _ "=" _ "{" _ handlerName:attributeValue _ "}" {
|
|
67
|
-
|
|
425
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(eventName);
|
|
426
|
+
const formattedName = needsQuotes ? `'${eventName}'` : eventName;
|
|
427
|
+
return `${formattedName}: ${handlerName}`;
|
|
68
428
|
}
|
|
69
429
|
/ "@" eventName:attributeName _ {
|
|
70
|
-
|
|
430
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(eventName);
|
|
431
|
+
return needsQuotes ? `'${eventName}'` : eventName;
|
|
71
432
|
}
|
|
72
433
|
|
|
73
|
-
dynamicAttribute
|
|
434
|
+
dynamicAttribute "dynamic attribute"
|
|
74
435
|
= attributeName:attributeName _ "=" _ "{" _ attributeValue:attributeValue _ "}" {
|
|
75
|
-
if (
|
|
76
|
-
|
|
436
|
+
// Check if attributeName needs to be quoted (contains dash or other invalid JS identifier chars)
|
|
437
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
|
|
438
|
+
const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
// If it's a complex object with strings, preserve it as is
|
|
442
|
+
if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}') &&
|
|
443
|
+
(attributeValue.includes('"') || attributeValue.includes("'"))) {
|
|
444
|
+
return `${formattedName}: ${attributeValue}`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// If it's a template string, transform expressions inside ${}
|
|
448
|
+
if (attributeValue.trim().startsWith('`') && attributeValue.trim().endsWith('`')) {
|
|
449
|
+
// Transform expressions inside ${} in template strings
|
|
450
|
+
let transformedTemplate = attributeValue;
|
|
451
|
+
|
|
452
|
+
// Find and replace ${expression} patterns
|
|
453
|
+
let startIndex = 0;
|
|
454
|
+
while (true) {
|
|
455
|
+
const dollarIndex = transformedTemplate.indexOf('${', startIndex);
|
|
456
|
+
if (dollarIndex === -1) break;
|
|
457
|
+
|
|
458
|
+
const braceIndex = transformedTemplate.indexOf('}', dollarIndex);
|
|
459
|
+
if (braceIndex === -1) break;
|
|
460
|
+
|
|
461
|
+
const expr = transformedTemplate.substring(dollarIndex + 2, braceIndex);
|
|
462
|
+
const trimmedExpr = expr.trim();
|
|
463
|
+
|
|
464
|
+
let replacement;
|
|
465
|
+
if (trimmedExpr.startsWith('@')) {
|
|
466
|
+
// Remove @ prefix for literals
|
|
467
|
+
replacement = '${' + trimmedExpr.substring(1) + '}';
|
|
468
|
+
} else if (trimmedExpr.match(/^[a-zA-Z_][a-zA-Z0-9_.]*$/)) {
|
|
469
|
+
// Transform identifiers to signals
|
|
470
|
+
replacement = '${' + trimmedExpr + '()}';
|
|
471
|
+
} else {
|
|
472
|
+
// Keep as is for complex expressions
|
|
473
|
+
replacement = '${' + expr + '}';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
transformedTemplate = transformedTemplate.substring(0, dollarIndex) +
|
|
477
|
+
replacement +
|
|
478
|
+
transformedTemplate.substring(braceIndex + 1);
|
|
479
|
+
|
|
480
|
+
startIndex = dollarIndex + replacement.length;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return formattedName + ': ' + transformedTemplate;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Handle other types of values
|
|
487
|
+
if (attributeValue.startsWith('h(') || attributeValue.includes('=>')) {
|
|
488
|
+
return `${formattedName}: ${attributeValue}`;
|
|
489
|
+
} else if (attributeValue.trim().match(/^[a-zA-Z_]\w*$/)) {
|
|
490
|
+
return `${formattedName}: ${attributeValue}`;
|
|
77
491
|
} else {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
492
|
+
// Check if this is an object or array literal
|
|
493
|
+
const isObjectLiteral = attributeValue.trim().startsWith('{ ') && attributeValue.trim().endsWith(' }');
|
|
494
|
+
const isArrayLiteral = attributeValue.trim().startsWith('[') && attributeValue.trim().endsWith(']');
|
|
495
|
+
|
|
496
|
+
let foundSignal = false;
|
|
497
|
+
let hasLiterals = false;
|
|
498
|
+
let computedValue = attributeValue;
|
|
499
|
+
|
|
500
|
+
// For simple object and array literals (like {x: x, y: 20} or [x, 20]),
|
|
501
|
+
// don't use computed() at all and don't transform identifiers
|
|
502
|
+
if ((isObjectLiteral || isArrayLiteral) && !attributeValue.includes('()')) {
|
|
503
|
+
// Don't transform anything, return as is
|
|
504
|
+
foundSignal = false;
|
|
505
|
+
computedValue = attributeValue;
|
|
506
|
+
} else {
|
|
507
|
+
// Apply signal transformation for other values
|
|
508
|
+
computedValue = attributeValue.replace(/@?([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*:)/g, (match, p1, offset) => {
|
|
509
|
+
// Don't transform keywords, numbers, or if we're inside quotes
|
|
510
|
+
if (['true', 'false', 'null'].includes(p1) || /^\d+(\.\d+)?$/.test(p1)) {
|
|
511
|
+
return match;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Check if we're inside a string literal
|
|
515
|
+
const beforeMatch = attributeValue.substring(0, offset);
|
|
516
|
+
const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length;
|
|
517
|
+
const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;
|
|
518
|
+
|
|
519
|
+
// If we're inside quotes, don't transform
|
|
520
|
+
if (singleQuotesBefore % 2 === 1 || doubleQuotesBefore % 2 === 1) {
|
|
521
|
+
return match;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (match.startsWith('@')) {
|
|
525
|
+
hasLiterals = true;
|
|
526
|
+
return p1; // Remove @ prefix
|
|
527
|
+
}
|
|
528
|
+
foundSignal = true;
|
|
529
|
+
return `${p1}()`;
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Check if any values already contain signals (ending with ())
|
|
533
|
+
if (attributeValue.includes('()')) {
|
|
534
|
+
foundSignal = true;
|
|
81
535
|
}
|
|
82
|
-
|
|
83
|
-
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (foundSignal) {
|
|
539
|
+
// For objects, wrap in parentheses
|
|
540
|
+
if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}')) {
|
|
541
|
+
// Remove spaces for objects in parentheses
|
|
542
|
+
const cleanedObject = computedValue.replace(/{ /g, '{').replace(/ }/g, '}');
|
|
543
|
+
return `${formattedName}: computed(() => (${cleanedObject}))`;
|
|
544
|
+
}
|
|
545
|
+
return `${formattedName}: computed(() => ${computedValue})`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// If only literals (all @), don't use computed
|
|
549
|
+
if (hasLiterals && !foundSignal) {
|
|
550
|
+
return `${formattedName}: ${computedValue}`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// For static objects and arrays, return as is without parentheses
|
|
554
|
+
return `${formattedName}: ${computedValue}`;
|
|
84
555
|
}
|
|
85
556
|
}
|
|
86
557
|
/ attributeName:attributeName _ {
|
|
87
|
-
|
|
558
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
|
|
559
|
+
return needsQuotes ? `'${attributeName}'` : attributeName;
|
|
88
560
|
}
|
|
89
561
|
|
|
90
|
-
attributeValue
|
|
91
|
-
=
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
562
|
+
attributeValue "attribute value"
|
|
563
|
+
= element
|
|
564
|
+
/ functionWithElement
|
|
565
|
+
/ objectLiteral
|
|
566
|
+
/ $([^{}]* ("{" [^{}]* "}" [^{}]*)*) {
|
|
567
|
+
return text().trim()
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
objectLiteral "object literal"
|
|
571
|
+
= "{" _ objContent:objectContent _ "}" {
|
|
572
|
+
return `{ ${objContent} }`;
|
|
97
573
|
}
|
|
98
574
|
|
|
99
|
-
|
|
575
|
+
objectContent
|
|
576
|
+
= prop:objectProperty rest:(_ "," _ objectProperty)* {
|
|
577
|
+
return [prop].concat(rest.map(r => r[3])).join(', ');
|
|
578
|
+
}
|
|
579
|
+
/ "" { return ""; }
|
|
580
|
+
|
|
581
|
+
objectProperty
|
|
582
|
+
= key:identifier _ ":" _ value:propertyValue {
|
|
583
|
+
return `${key}: ${value}`;
|
|
584
|
+
}
|
|
585
|
+
/ key:identifier {
|
|
586
|
+
return key;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
propertyValue
|
|
590
|
+
= nestedObject
|
|
591
|
+
/ element
|
|
592
|
+
/ functionWithElement
|
|
593
|
+
/ stringLiteral
|
|
594
|
+
/ number
|
|
595
|
+
/ identifier
|
|
596
|
+
|
|
597
|
+
nestedObject
|
|
598
|
+
= "{" _ objContent:objectContent _ "}" {
|
|
599
|
+
return `{ ${objContent} }`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
stringLiteral
|
|
603
|
+
= '"' chars:[^"]* '"' { return text(); }
|
|
604
|
+
/ "'" chars:[^']* "'" { return text(); }
|
|
605
|
+
|
|
606
|
+
functionWithElement "function expression"
|
|
607
|
+
= "(" _ params:functionParams? _ ")" _ "=>" _ elem:element {
|
|
608
|
+
return `${params ? `(${params}) =>` : '() =>'} ${elem}`;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
functionParams
|
|
612
|
+
= destructuredParams
|
|
613
|
+
/ simpleParams
|
|
614
|
+
|
|
615
|
+
destructuredParams
|
|
616
|
+
= "{" _ param:identifier rest:(_ "," _ identifier)* _ "}" {
|
|
617
|
+
return `{${[param].concat(rest.map(r => r[3])).join(', ')}}`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
simpleParams
|
|
621
|
+
= param:identifier rest:(_ "," _ identifier)* {
|
|
622
|
+
return [param].concat(rest.map(r => r[3])).join(', ');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
staticAttribute "static attribute"
|
|
100
626
|
= attributeName:attributeName _ "=" _ "\"" attributeValue:staticValue "\"" {
|
|
101
|
-
|
|
627
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
|
|
628
|
+
const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
|
|
629
|
+
return `${formattedName}: ${attributeValue}`;
|
|
102
630
|
}
|
|
103
631
|
|
|
104
632
|
eventAttribute
|
|
@@ -109,10 +637,10 @@ eventAttribute
|
|
|
109
637
|
staticValue
|
|
110
638
|
= [^"]+ {
|
|
111
639
|
var val = text();
|
|
112
|
-
return
|
|
640
|
+
return `'${val}'`
|
|
113
641
|
}
|
|
114
642
|
|
|
115
|
-
content
|
|
643
|
+
content "component content"
|
|
116
644
|
= elements:(element)* {
|
|
117
645
|
const filteredElements = elements.filter(el => el !== null);
|
|
118
646
|
if (filteredElements.length === 0) return null;
|
|
@@ -120,6 +648,8 @@ content
|
|
|
120
648
|
return `[${filteredElements.join(', ')}]`;
|
|
121
649
|
}
|
|
122
650
|
|
|
651
|
+
|
|
652
|
+
|
|
123
653
|
textNode
|
|
124
654
|
= text:$([^<]+) {
|
|
125
655
|
const trimmed = text.trim();
|
|
@@ -132,20 +662,58 @@ textElement
|
|
|
132
662
|
return trimmed ? JSON.stringify(trimmed) : null;
|
|
133
663
|
}
|
|
134
664
|
|
|
135
|
-
forLoop
|
|
136
|
-
= _ "@for" _ "(" _ variableName:identifier _ "of" _ iterable:
|
|
137
|
-
return `loop(${iterable},
|
|
665
|
+
forLoop "for loop"
|
|
666
|
+
= _ "@for" _ "(" _ variableName:(tupleDestructuring / identifier) _ "of" _ iterable:iterable _ ")" _ "{" _ content:content _ "}" _ {
|
|
667
|
+
return `loop(${iterable}, ${variableName} => ${content})`;
|
|
138
668
|
}
|
|
139
669
|
|
|
140
|
-
|
|
141
|
-
=
|
|
142
|
-
return `
|
|
670
|
+
tupleDestructuring "destructuring pattern"
|
|
671
|
+
= "(" _ first:identifier _ "," _ second:identifier _ ")" {
|
|
672
|
+
return `(${first}, ${second})`;
|
|
143
673
|
}
|
|
144
674
|
|
|
145
|
-
|
|
146
|
-
=
|
|
675
|
+
ifCondition "if condition"
|
|
676
|
+
= _ "@if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ elseIfs:elseIfClause* elseClause:elseClause? _ {
|
|
677
|
+
let result = `cond(${condition}, () => ${content}`;
|
|
678
|
+
|
|
679
|
+
// Add else if clauses
|
|
680
|
+
elseIfs.forEach(elseIf => {
|
|
681
|
+
result += `, [${elseIf.condition}, () => ${elseIf.content}]`;
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// Add else clause if present
|
|
685
|
+
if (elseClause) {
|
|
686
|
+
result += `, () => ${elseClause}`;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
result += ')';
|
|
690
|
+
return result;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
elseIfClause "else if clause"
|
|
694
|
+
= _ "@else" _ "if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ {
|
|
695
|
+
return { condition, content };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
elseClause "else clause"
|
|
699
|
+
= _ "@else" _ "{" _ content:content _ "}" _ {
|
|
700
|
+
return content;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
tagName "tag name"
|
|
704
|
+
= tagExpression
|
|
705
|
+
|
|
706
|
+
tagExpression "tag expression"
|
|
707
|
+
= first:tagPart rest:("." tagPart)* {
|
|
708
|
+
return text();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
tagPart "tag part"
|
|
712
|
+
= name:[a-zA-Z][a-zA-Z0-9]* args:("(" functionArgs? ")")? {
|
|
713
|
+
return text();
|
|
714
|
+
}
|
|
147
715
|
|
|
148
|
-
attributeName
|
|
716
|
+
attributeName "attribute name"
|
|
149
717
|
= [a-zA-Z][a-zA-Z0-9-]* { return text(); }
|
|
150
718
|
|
|
151
719
|
eventName
|
|
@@ -154,11 +722,83 @@ eventName
|
|
|
154
722
|
variableName
|
|
155
723
|
= [a-zA-Z_][a-zA-Z0-9_]* { return text(); }
|
|
156
724
|
|
|
157
|
-
iterable
|
|
158
|
-
=
|
|
725
|
+
iterable "iterable expression"
|
|
726
|
+
= id:identifier "(" _ args:functionArgs? _ ")" { // Direct function call
|
|
727
|
+
return `${id}(${args || ''})`;
|
|
728
|
+
}
|
|
729
|
+
/ first:identifier "." rest:dotFunctionChain { // Dot notation possibly with function call
|
|
730
|
+
return `${first}.${rest}`;
|
|
731
|
+
}
|
|
732
|
+
/ id:identifier { return id; }
|
|
159
733
|
|
|
160
|
-
|
|
161
|
-
=
|
|
734
|
+
dotFunctionChain
|
|
735
|
+
= segment:identifier "(" _ args:functionArgs? _ ")" rest:("." dotFunctionChain)? {
|
|
736
|
+
const restStr = rest ? `.${rest[1]}` : '';
|
|
737
|
+
return `${segment}(${args || ''})${restStr}`;
|
|
738
|
+
}
|
|
739
|
+
/ segment:identifier rest:("." dotFunctionChain)? {
|
|
740
|
+
const restStr = rest ? `.${rest[1]}` : '';
|
|
741
|
+
return `${segment}${restStr}`;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
condition "condition expression"
|
|
745
|
+
= functionCall
|
|
746
|
+
/ text_condition:$([^)]*) {
|
|
747
|
+
const originalText = text_condition.trim();
|
|
748
|
+
|
|
749
|
+
// Transform simple identifiers to function calls like "foo" to "foo()"
|
|
750
|
+
// This regex matches identifiers not followed by an opening parenthesis.
|
|
751
|
+
// This transformation should only apply if we are wrapping in 'computed'.
|
|
752
|
+
if (originalText.includes('!') || originalText.includes('&&') || originalText.includes('||') ||
|
|
753
|
+
originalText.includes('>=') || originalText.includes('<=') || originalText.includes('===') ||
|
|
754
|
+
originalText.includes('!==') || originalText.includes('==') || originalText.includes('!=') ||
|
|
755
|
+
originalText.includes('>') || originalText.includes('<')) {
|
|
756
|
+
const transformedText = originalText.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g, (match, p1, offset) => {
|
|
757
|
+
// Do not transform keywords (true, false, null) or numeric literals
|
|
758
|
+
if (['true', 'false', 'null'].includes(match) || /^\d+(\.\d+)?$/.test(match)) {
|
|
759
|
+
return match;
|
|
760
|
+
}
|
|
761
|
+
// Check if the match is inside quotes
|
|
762
|
+
const beforeMatch = originalText.substring(0, offset);
|
|
763
|
+
const afterMatch = originalText.substring(offset + match.length);
|
|
764
|
+
const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length;
|
|
765
|
+
const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;
|
|
766
|
+
|
|
767
|
+
// If we're inside quotes, don't transform
|
|
768
|
+
if (singleQuotesBefore % 2 === 1 || doubleQuotesBefore % 2 === 1) {
|
|
769
|
+
return match;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return `${match}()`;
|
|
773
|
+
});
|
|
774
|
+
return `computed(() => ${transformedText})`;
|
|
775
|
+
}
|
|
776
|
+
// For simple conditions (no !, &&, ||), return the original text as is.
|
|
777
|
+
// Cases like `myFunction()` are handled by the `functionCall` rule.
|
|
778
|
+
return originalText;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
functionCall "function call"
|
|
782
|
+
= name:identifier "(" args:functionArgs? ")" {
|
|
783
|
+
return `${name}(${args || ''})`;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
functionArgs
|
|
787
|
+
= arg:functionArg rest:("," _ functionArg)* {
|
|
788
|
+
return [arg].concat(rest.map(r => r[2])).join(', ');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
functionArg
|
|
792
|
+
= _ value:(identifier / number / string) _ {
|
|
793
|
+
return value;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
number
|
|
797
|
+
= [0-9]+ ("." [0-9]+)? { return text(); }
|
|
798
|
+
|
|
799
|
+
string
|
|
800
|
+
= '"' chars:[^"]* '"' { return text(); }
|
|
801
|
+
/ "'" chars:[^']* "'" { return text(); }
|
|
162
802
|
|
|
163
803
|
eventAction
|
|
164
804
|
= [^"]* { return text(); }
|
|
@@ -177,4 +817,45 @@ comment
|
|
|
177
817
|
singleComment
|
|
178
818
|
= "<!--" _ content:((!("-->") .)* "-->") _ {
|
|
179
819
|
return null;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Add a special error detection rule for unclosed tags
|
|
823
|
+
openUnclosedTag "unclosed tag"
|
|
824
|
+
= "<" _ tagName:tagName _ attributes:attributes _ ">" _ content:content _ !("</" _ closingTagName:tagName _ ">") {
|
|
825
|
+
generateError(
|
|
826
|
+
`Unclosed tag: <${tagName}> is missing its closing tag`,
|
|
827
|
+
location()
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Add error detection for unclosed quotes in static attributes
|
|
832
|
+
unclosedQuote "unclosed string"
|
|
833
|
+
= attributeName:attributeName _ "=" _ "\"" [^"]* !("\"") {
|
|
834
|
+
generateError(
|
|
835
|
+
`Missing closing quote in attribute '${attributeName}'`,
|
|
836
|
+
location()
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Add error detection for unclosed braces in dynamic attributes
|
|
841
|
+
unclosedBrace "unclosed brace"
|
|
842
|
+
= attributeName:attributeName _ "=" _ "{" !("}" / _ "}") [^{}]* {
|
|
843
|
+
generateError(
|
|
844
|
+
`Missing closing brace in dynamic attribute '${attributeName}'`,
|
|
845
|
+
location()
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
svgElement "SVG element"
|
|
850
|
+
= "<svg" attrs:([^>]*) ">" content:svgInnerContent "</svg>" _ {
|
|
851
|
+
const attributes = attrs.join('').trim();
|
|
852
|
+
// Clean up the content by removing extra whitespace and newlines
|
|
853
|
+
const cleanContent = content.replace(/\s+/g, ' ').trim();
|
|
854
|
+
const rawContent = `<svg${attributes ? ' ' + attributes : ''}>${cleanContent}</svg>`;
|
|
855
|
+
return `h(Svg, { content: \`${rawContent}\` })`;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
svgInnerContent "SVG inner content"
|
|
859
|
+
= content:$((!("</svg>") .)*) {
|
|
860
|
+
return content;
|
|
180
861
|
}
|