@canvasengine/compiler 2.0.0-beta.2 → 2.0.0-beta.20
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 +42 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/grammar.pegjs +559 -45
- package/index.ts +94 -9
- package/package.json +1 -1
- package/tests/compiler.spec.ts +565 -20
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', 'DOMContainer', 'Svg'
|
|
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(DOMContainer, { 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(DOMContainer, { ${parts.join(', ')} })`;
|
|
135
|
+
}
|
|
8
136
|
}
|
|
9
137
|
|
|
10
138
|
start
|
|
@@ -15,30 +143,205 @@ 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
|
+
);
|
|
35
174
|
}
|
|
36
|
-
|
|
175
|
+
|
|
176
|
+
if (isDOMElement(tagName)) {
|
|
177
|
+
if (attributes.length === 0) {
|
|
178
|
+
return `h(DOMContainer, { 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(DOMContainer, { ${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 dynamic expressions like {item.name}
|
|
253
|
+
if (expr.trim().match(/^[a-zA-Z_][a-zA-Z0-9_.]*$/)) {
|
|
254
|
+
let foundSignal = false;
|
|
255
|
+
const computedValue = expr.replace(/@?[a-zA-Z_][a-zA-Z0-9_]*(?!:)/g, (match) => {
|
|
256
|
+
if (match.startsWith('@')) {
|
|
257
|
+
return match.substring(1);
|
|
258
|
+
}
|
|
259
|
+
foundSignal = true;
|
|
260
|
+
return `${match}()`;
|
|
261
|
+
});
|
|
262
|
+
if (foundSignal) {
|
|
263
|
+
return `computed(() => ${computedValue})`;
|
|
264
|
+
}
|
|
265
|
+
return computedValue;
|
|
266
|
+
}
|
|
267
|
+
return expr;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
openCloseElement "component with content"
|
|
271
|
+
= "<" _ tagName:tagName _ attributes:attributes _ ">" _ content:content _ "</" _ closingTagName:tagName _ ">" _ {
|
|
272
|
+
if (tagName !== closingTagName) {
|
|
273
|
+
generateError(
|
|
274
|
+
`Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
|
|
275
|
+
location()
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check if it's a DOM element
|
|
280
|
+
if (isDOMElement(tagName)) {
|
|
281
|
+
const children = content ? content : null;
|
|
282
|
+
|
|
283
|
+
if (attributes.length === 0) {
|
|
284
|
+
if (children) {
|
|
285
|
+
return `h(DOMContainer, { element: "${tagName}" }, ${children})`;
|
|
286
|
+
} else {
|
|
287
|
+
return `h(DOMContainer, { element: "${tagName}" })`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Separate DisplayObject attributes from DOM attributes
|
|
292
|
+
const domAttrs = [];
|
|
293
|
+
const displayObjectAttrs = [];
|
|
294
|
+
|
|
295
|
+
attributes.forEach(attr => {
|
|
296
|
+
// Handle spread attributes
|
|
297
|
+
if (attr.startsWith('...')) {
|
|
298
|
+
displayObjectAttrs.push(attr);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Extract attribute name
|
|
303
|
+
let attrName;
|
|
304
|
+
if (attr.includes(':')) {
|
|
305
|
+
// Format: "name: value" or "'name': value"
|
|
306
|
+
attrName = attr.split(':')[0].trim().replace(/['"]/g, '');
|
|
307
|
+
} else {
|
|
308
|
+
// Standalone attribute
|
|
309
|
+
attrName = attr.replace(/['"]/g, '');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check if it's a DisplayObject attribute
|
|
313
|
+
if (displayObjectAttributes.has(attrName)) {
|
|
314
|
+
displayObjectAttrs.push(attr);
|
|
315
|
+
} else {
|
|
316
|
+
domAttrs.push(attr);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Build the result
|
|
321
|
+
const parts = [`element: "${tagName}"`];
|
|
322
|
+
|
|
323
|
+
if (domAttrs.length > 0) {
|
|
324
|
+
parts.push(`attrs: { ${domAttrs.join(', ')} }`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (displayObjectAttrs.length > 0) {
|
|
328
|
+
parts.push(...displayObjectAttrs);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (children) {
|
|
332
|
+
return `h(DOMContainer, { ${parts.join(', ')} }, ${children})`;
|
|
333
|
+
} else {
|
|
334
|
+
return `h(DOMContainer, { ${parts.join(', ')} })`;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Otherwise, treat as regular component
|
|
339
|
+
const attrsString = formatAttributes(attributes);
|
|
37
340
|
const children = content ? content : null;
|
|
38
|
-
if (
|
|
39
|
-
return `h(${tagName}, ${
|
|
40
|
-
} else if (
|
|
41
|
-
return `h(${tagName}, ${
|
|
341
|
+
if (attrsString && children) {
|
|
342
|
+
return `h(${tagName}, ${attrsString}, ${children})`;
|
|
343
|
+
} else if (attrsString) {
|
|
344
|
+
return `h(${tagName}, ${attrsString})`;
|
|
42
345
|
} else if (children) {
|
|
43
346
|
return `h(${tagName}, null, ${children})`;
|
|
44
347
|
} else {
|
|
@@ -46,49 +349,88 @@ openCloseElement
|
|
|
46
349
|
}
|
|
47
350
|
}
|
|
48
351
|
|
|
49
|
-
|
|
50
|
-
generateError("Syntaxe d'élément invalide", location());
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
attributes
|
|
352
|
+
attributes "component attributes"
|
|
54
353
|
= attrs:(attribute (_ attribute)*)? {
|
|
55
354
|
return attrs
|
|
56
355
|
? [attrs[0]].concat(attrs[1].map(a => a[1]))
|
|
57
356
|
: [];
|
|
58
357
|
}
|
|
59
358
|
|
|
60
|
-
attribute
|
|
359
|
+
attribute "attribute"
|
|
61
360
|
= staticAttribute
|
|
62
361
|
/ dynamicAttribute
|
|
63
362
|
/ eventHandler
|
|
363
|
+
/ spreadAttribute
|
|
364
|
+
/ unclosedQuote
|
|
365
|
+
/ unclosedBrace
|
|
64
366
|
|
|
65
|
-
|
|
367
|
+
spreadAttribute "spread attribute"
|
|
368
|
+
= "..." expr:(functionCallExpr / dotNotation) {
|
|
369
|
+
return "..." + expr;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
functionCallExpr "function call"
|
|
373
|
+
= name:dotNotation "(" args:functionArgs? ")" {
|
|
374
|
+
return `${name}(${args || ''})`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
dotNotation "property access"
|
|
378
|
+
= first:identifier rest:("." identifier)* {
|
|
379
|
+
return text();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
eventHandler "event handler"
|
|
66
383
|
= "@" eventName:identifier _ "=" _ "{" _ handlerName:attributeValue _ "}" {
|
|
67
|
-
|
|
384
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(eventName);
|
|
385
|
+
const formattedName = needsQuotes ? `'${eventName}'` : eventName;
|
|
386
|
+
return `${formattedName}: ${handlerName}`;
|
|
68
387
|
}
|
|
69
388
|
/ "@" eventName:attributeName _ {
|
|
70
|
-
|
|
389
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(eventName);
|
|
390
|
+
return needsQuotes ? `'${eventName}'` : eventName;
|
|
71
391
|
}
|
|
72
392
|
|
|
73
|
-
dynamicAttribute
|
|
393
|
+
dynamicAttribute "dynamic attribute"
|
|
74
394
|
= attributeName:attributeName _ "=" _ "{" _ attributeValue:attributeValue _ "}" {
|
|
75
|
-
if (
|
|
76
|
-
|
|
395
|
+
// Check if attributeName needs to be quoted (contains dash or other invalid JS identifier chars)
|
|
396
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
|
|
397
|
+
const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
|
|
398
|
+
|
|
399
|
+
// If it's a complex object literal starting with curly braces, preserve it as is
|
|
400
|
+
if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}')) {
|
|
401
|
+
return `${formattedName}: ${attributeValue}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Handle other types of values
|
|
405
|
+
if (attributeValue.startsWith('h(') || attributeValue.includes('=>')) {
|
|
406
|
+
return `${formattedName}: ${attributeValue}`;
|
|
407
|
+
} else if (attributeValue.trim().match(/^[a-zA-Z_]\w*$/)) {
|
|
408
|
+
return `${formattedName}: ${attributeValue}`;
|
|
77
409
|
} else {
|
|
78
|
-
|
|
410
|
+
let foundSignal = false;
|
|
411
|
+
const computedValue = attributeValue.replace(/@?[a-zA-Z_][a-zA-Z0-9_]*(?!:)/g, (match) => {
|
|
79
412
|
if (match.startsWith('@')) {
|
|
80
413
|
return match.substring(1);
|
|
81
414
|
}
|
|
415
|
+
foundSignal = true;
|
|
82
416
|
return `${match}()`;
|
|
83
|
-
})
|
|
417
|
+
});
|
|
418
|
+
if (foundSignal) {
|
|
419
|
+
return `${formattedName}: computed(() => ${computedValue})`;
|
|
420
|
+
}
|
|
421
|
+
return `${formattedName}: ${computedValue}`;
|
|
84
422
|
}
|
|
85
423
|
}
|
|
86
424
|
/ attributeName:attributeName _ {
|
|
87
|
-
|
|
425
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
|
|
426
|
+
return needsQuotes ? `'${attributeName}'` : attributeName;
|
|
88
427
|
}
|
|
89
428
|
|
|
90
|
-
attributeValue
|
|
91
|
-
=
|
|
429
|
+
attributeValue "attribute value"
|
|
430
|
+
= element
|
|
431
|
+
/ functionWithElement
|
|
432
|
+
/ objectLiteral
|
|
433
|
+
/ $([^{}]* ("{" [^{}]* "}" [^{}]*)*) {
|
|
92
434
|
const t = text().trim()
|
|
93
435
|
if (t.startsWith("{") && t.endsWith("}")) {
|
|
94
436
|
return `(${t})`;
|
|
@@ -96,9 +438,65 @@ attributeValue
|
|
|
96
438
|
return t
|
|
97
439
|
}
|
|
98
440
|
|
|
99
|
-
|
|
441
|
+
objectLiteral "object literal"
|
|
442
|
+
= "{" _ objContent:objectContent _ "}" {
|
|
443
|
+
return `{ ${objContent} }`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
objectContent
|
|
447
|
+
= prop:objectProperty rest:(_ "," _ objectProperty)* {
|
|
448
|
+
return [prop].concat(rest.map(r => r[3])).join(', ');
|
|
449
|
+
}
|
|
450
|
+
/ "" { return ""; }
|
|
451
|
+
|
|
452
|
+
objectProperty
|
|
453
|
+
= key:identifier _ ":" _ value:propertyValue {
|
|
454
|
+
return `${key}: ${value}`;
|
|
455
|
+
}
|
|
456
|
+
/ key:identifier {
|
|
457
|
+
return key;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
propertyValue
|
|
461
|
+
= nestedObject
|
|
462
|
+
/ element
|
|
463
|
+
/ functionWithElement
|
|
464
|
+
/ stringLiteral
|
|
465
|
+
/ identifier
|
|
466
|
+
|
|
467
|
+
nestedObject
|
|
468
|
+
= "{" _ objContent:objectContent _ "}" {
|
|
469
|
+
return `{ ${objContent} }`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
stringLiteral
|
|
473
|
+
= '"' chars:[^"]* '"' { return text(); }
|
|
474
|
+
/ "'" chars:[^']* "'" { return text(); }
|
|
475
|
+
|
|
476
|
+
functionWithElement "function expression"
|
|
477
|
+
= "(" _ params:functionParams? _ ")" _ "=>" _ elem:element {
|
|
478
|
+
return `${params ? `(${params}) =>` : '() =>'} ${elem}`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
functionParams
|
|
482
|
+
= destructuredParams
|
|
483
|
+
/ simpleParams
|
|
484
|
+
|
|
485
|
+
destructuredParams
|
|
486
|
+
= "{" _ param:identifier rest:(_ "," _ identifier)* _ "}" {
|
|
487
|
+
return `{${[param].concat(rest.map(r => r[3])).join(', ')}}`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
simpleParams
|
|
491
|
+
= param:identifier rest:(_ "," _ identifier)* {
|
|
492
|
+
return [param].concat(rest.map(r => r[3])).join(', ');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
staticAttribute "static attribute"
|
|
100
496
|
= attributeName:attributeName _ "=" _ "\"" attributeValue:staticValue "\"" {
|
|
101
|
-
|
|
497
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
|
|
498
|
+
const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
|
|
499
|
+
return `${formattedName}: ${attributeValue}`;
|
|
102
500
|
}
|
|
103
501
|
|
|
104
502
|
eventAttribute
|
|
@@ -109,10 +507,10 @@ eventAttribute
|
|
|
109
507
|
staticValue
|
|
110
508
|
= [^"]+ {
|
|
111
509
|
var val = text();
|
|
112
|
-
return
|
|
510
|
+
return `'${val}'`
|
|
113
511
|
}
|
|
114
512
|
|
|
115
|
-
content
|
|
513
|
+
content "component content"
|
|
116
514
|
= elements:(element)* {
|
|
117
515
|
const filteredElements = elements.filter(el => el !== null);
|
|
118
516
|
if (filteredElements.length === 0) return null;
|
|
@@ -120,6 +518,8 @@ content
|
|
|
120
518
|
return `[${filteredElements.join(', ')}]`;
|
|
121
519
|
}
|
|
122
520
|
|
|
521
|
+
|
|
522
|
+
|
|
123
523
|
textNode
|
|
124
524
|
= text:$([^<]+) {
|
|
125
525
|
const trimmed = text.trim();
|
|
@@ -132,20 +532,35 @@ textElement
|
|
|
132
532
|
return trimmed ? JSON.stringify(trimmed) : null;
|
|
133
533
|
}
|
|
134
534
|
|
|
135
|
-
forLoop
|
|
136
|
-
= _ "@for" _ "(" _ variableName:identifier _ "of" _ iterable:
|
|
137
|
-
return `loop(${iterable},
|
|
535
|
+
forLoop "for loop"
|
|
536
|
+
= _ "@for" _ "(" _ variableName:(tupleDestructuring / identifier) _ "of" _ iterable:iterable _ ")" _ "{" _ content:content _ "}" _ {
|
|
537
|
+
return `loop(${iterable}, ${variableName} => ${content})`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
tupleDestructuring "destructuring pattern"
|
|
541
|
+
= "(" _ first:identifier _ "," _ second:identifier _ ")" {
|
|
542
|
+
return `(${first}, ${second})`;
|
|
138
543
|
}
|
|
139
544
|
|
|
140
|
-
ifCondition
|
|
545
|
+
ifCondition "if condition"
|
|
141
546
|
= _ "@if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ {
|
|
142
547
|
return `cond(${condition}, () => ${content})`;
|
|
143
548
|
}
|
|
144
549
|
|
|
145
|
-
tagName
|
|
146
|
-
=
|
|
550
|
+
tagName "tag name"
|
|
551
|
+
= tagExpression
|
|
552
|
+
|
|
553
|
+
tagExpression "tag expression"
|
|
554
|
+
= first:tagPart rest:("." tagPart)* {
|
|
555
|
+
return text();
|
|
556
|
+
}
|
|
147
557
|
|
|
148
|
-
|
|
558
|
+
tagPart "tag part"
|
|
559
|
+
= name:[a-zA-Z][a-zA-Z0-9]* args:("(" functionArgs? ")")? {
|
|
560
|
+
return text();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
attributeName "attribute name"
|
|
149
564
|
= [a-zA-Z][a-zA-Z0-9-]* { return text(); }
|
|
150
565
|
|
|
151
566
|
eventName
|
|
@@ -154,11 +569,69 @@ eventName
|
|
|
154
569
|
variableName
|
|
155
570
|
= [a-zA-Z_][a-zA-Z0-9_]* { return text(); }
|
|
156
571
|
|
|
157
|
-
iterable
|
|
158
|
-
=
|
|
572
|
+
iterable "iterable expression"
|
|
573
|
+
= id:identifier "(" _ args:functionArgs? _ ")" { // Direct function call
|
|
574
|
+
return `${id}(${args || ''})`;
|
|
575
|
+
}
|
|
576
|
+
/ first:identifier "." rest:dotFunctionChain { // Dot notation possibly with function call
|
|
577
|
+
return `${first}.${rest}`;
|
|
578
|
+
}
|
|
579
|
+
/ id:identifier { return id; }
|
|
580
|
+
|
|
581
|
+
dotFunctionChain
|
|
582
|
+
= segment:identifier "(" _ args:functionArgs? _ ")" rest:("." dotFunctionChain)? {
|
|
583
|
+
const restStr = rest ? `.${rest[1]}` : '';
|
|
584
|
+
return `${segment}(${args || ''})${restStr}`;
|
|
585
|
+
}
|
|
586
|
+
/ segment:identifier rest:("." dotFunctionChain)? {
|
|
587
|
+
const restStr = rest ? `.${rest[1]}` : '';
|
|
588
|
+
return `${segment}${restStr}`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
condition "condition expression"
|
|
592
|
+
= functionCall
|
|
593
|
+
/ text_condition:$([^)]*) {
|
|
594
|
+
const originalText = text_condition.trim();
|
|
595
|
+
|
|
596
|
+
// Transform simple identifiers to function calls like "foo" to "foo()"
|
|
597
|
+
// This regex matches identifiers not followed by an opening parenthesis.
|
|
598
|
+
// This transformation should only apply if we are wrapping in 'computed'.
|
|
599
|
+
if (originalText.includes('!') || originalText.includes('&&') || originalText.includes('||')) {
|
|
600
|
+
const transformedText = originalText.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g, (match) => {
|
|
601
|
+
// Do not transform keywords (true, false, null) or numeric literals
|
|
602
|
+
if (['true', 'false', 'null'].includes(match) || /^\d+(\.\d+)?$/.test(match)) {
|
|
603
|
+
return match;
|
|
604
|
+
}
|
|
605
|
+
return `${match}()`;
|
|
606
|
+
});
|
|
607
|
+
return `computed(() => ${transformedText})`;
|
|
608
|
+
}
|
|
609
|
+
// For simple conditions (no !, &&, ||), return the original text as is.
|
|
610
|
+
// Cases like `myFunction()` are handled by the `functionCall` rule.
|
|
611
|
+
return originalText;
|
|
612
|
+
}
|
|
159
613
|
|
|
160
|
-
|
|
161
|
-
=
|
|
614
|
+
functionCall "function call"
|
|
615
|
+
= name:identifier "(" args:functionArgs? ")" {
|
|
616
|
+
return `${name}(${args || ''})`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
functionArgs
|
|
620
|
+
= arg:functionArg rest:("," _ functionArg)* {
|
|
621
|
+
return [arg].concat(rest.map(r => r[2])).join(', ');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
functionArg
|
|
625
|
+
= _ value:(identifier / number / string) _ {
|
|
626
|
+
return value;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
number
|
|
630
|
+
= [0-9]+ ("." [0-9]+)? { return text(); }
|
|
631
|
+
|
|
632
|
+
string
|
|
633
|
+
= '"' chars:[^"]* '"' { return text(); }
|
|
634
|
+
/ "'" chars:[^']* "'" { return text(); }
|
|
162
635
|
|
|
163
636
|
eventAction
|
|
164
637
|
= [^"]* { return text(); }
|
|
@@ -177,4 +650,45 @@ comment
|
|
|
177
650
|
singleComment
|
|
178
651
|
= "<!--" _ content:((!("-->") .)* "-->") _ {
|
|
179
652
|
return null;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Add a special error detection rule for unclosed tags
|
|
656
|
+
openUnclosedTag "unclosed tag"
|
|
657
|
+
= "<" _ tagName:tagName _ attributes:attributes _ ">" _ content:content _ !("</" _ closingTagName:tagName _ ">") {
|
|
658
|
+
generateError(
|
|
659
|
+
`Unclosed tag: <${tagName}> is missing its closing tag`,
|
|
660
|
+
location()
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Add error detection for unclosed quotes in static attributes
|
|
665
|
+
unclosedQuote "unclosed string"
|
|
666
|
+
= attributeName:attributeName _ "=" _ "\"" [^"]* !("\"") {
|
|
667
|
+
generateError(
|
|
668
|
+
`Missing closing quote in attribute '${attributeName}'`,
|
|
669
|
+
location()
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Add error detection for unclosed braces in dynamic attributes
|
|
674
|
+
unclosedBrace "unclosed brace"
|
|
675
|
+
= attributeName:attributeName _ "=" _ "{" !("}" / _ "}") [^{}]* {
|
|
676
|
+
generateError(
|
|
677
|
+
`Missing closing brace in dynamic attribute '${attributeName}'`,
|
|
678
|
+
location()
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
svgElement "SVG element"
|
|
683
|
+
= "<svg" attrs:([^>]*) ">" content:svgInnerContent "</svg>" _ {
|
|
684
|
+
const attributes = attrs.join('').trim();
|
|
685
|
+
// Clean up the content by removing extra whitespace and newlines
|
|
686
|
+
const cleanContent = content.replace(/\s+/g, ' ').trim();
|
|
687
|
+
const rawContent = `<svg${attributes ? ' ' + attributes : ''}>${cleanContent}</svg>`;
|
|
688
|
+
return `h(Svg, { content: \`${rawContent}\` })`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
svgInnerContent "SVG inner content"
|
|
692
|
+
= content:$((!("</svg>") .)*) {
|
|
693
|
+
return content;
|
|
180
694
|
}
|