@canvasengine/compiler 2.0.0-beta.5 → 2.0.0-beta.51
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/grammar.pegjs +1119 -0
- package/dist/grammar2.pegjs +995 -0
- package/dist/index.d.ts +35 -2
- package/dist/index.js +335 -10
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
- package/grammar.pegjs +0 -186
- package/index.ts +0 -167
- package/tests/compiler.spec.ts +0 -352
- package/tsconfig.json +0 -29
- package/tsup.config.ts +0 -14
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
{
|
|
2
|
+
function generateError(message, location) {
|
|
3
|
+
const { start, end } = location;
|
|
4
|
+
const errorMessage = `${message}\n` +
|
|
5
|
+
`at line ${start.line}, column ${start.column} to line ${end.line}, column ${end.column}`;
|
|
6
|
+
throw new Error(errorMessage);
|
|
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
|
+
const voidElements = new Set([
|
|
43
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta',
|
|
44
|
+
'param', 'source', 'track', 'wbr'
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const eventAttributes = new Set([
|
|
48
|
+
'click', 'tap', 'pointertap', 'pointerdown', 'pointerup', 'pointermove',
|
|
49
|
+
'pointerover', 'pointerout', 'pointerupoutside', 'mousedown', 'mouseup',
|
|
50
|
+
'mousemove', 'mouseover', 'mouseout', 'touchstart', 'touchend', 'touchmove',
|
|
51
|
+
'touchcancel', 'rightclick', 'keydown', 'keyup', 'keypress'
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
// DisplayObject special attributes that should not be in attrs
|
|
55
|
+
const displayObjectAttributes = new Set([
|
|
56
|
+
'x', 'y', 'scale', 'anchor', 'skew', 'tint', 'rotation', 'angle',
|
|
57
|
+
'zIndex', 'roundPixels', 'cursor', 'visible', 'alpha', 'pivot', 'filters', 'maskOf',
|
|
58
|
+
'blendMode', 'filterArea', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
|
|
59
|
+
'aspectRatio', 'flexGrow', 'flexShrink', 'flexBasis', 'rowGap', 'columnGap',
|
|
60
|
+
'positionType', 'top', 'right', 'bottom', 'left', 'objectFit', 'objectPosition',
|
|
61
|
+
'transformOrigin', 'flexDirection', 'justifyContent', 'alignItems', 'alignContent',
|
|
62
|
+
'alignSelf', 'margin', 'padding', 'border', 'gap', 'blur', 'shadow'
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
function isDOMElement(tagName) {
|
|
66
|
+
// Don't transform framework components to DOM elements
|
|
67
|
+
if (frameworkComponents.has(tagName)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return domElements.has(tagName.toLowerCase());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isVoidElement(tagName) {
|
|
74
|
+
return voidElements.has(tagName.toLowerCase());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatAttributes(attributes) {
|
|
78
|
+
if (attributes.length === 0) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if there's exactly one attribute and it's a spread attribute
|
|
83
|
+
if (attributes.length === 1 && attributes[0].startsWith('...')) {
|
|
84
|
+
// Return the identifier directly, removing the '...'
|
|
85
|
+
return attributes[0].substring(3);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Otherwise, format as an object literal
|
|
89
|
+
const formattedAttrs = attributes.map(attr => {
|
|
90
|
+
// If it's a spread attribute, keep it as is
|
|
91
|
+
if (attr.startsWith('...')) {
|
|
92
|
+
return attr;
|
|
93
|
+
}
|
|
94
|
+
// If it's a standalone attribute (doesn't contain ':'), format as shorthand property 'name'
|
|
95
|
+
if (!attr.includes(':')) {
|
|
96
|
+
return attr; // JS object literal shorthand
|
|
97
|
+
}
|
|
98
|
+
// Otherwise (key: value), keep it as is
|
|
99
|
+
return attr;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return `{ ${formattedAttrs.join(', ')} }`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatDOMElement(tagName, attributes) {
|
|
106
|
+
if (attributes.length === 0) {
|
|
107
|
+
return `h(DOMElement, { element: "${tagName}" })`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { domAttrs, displayObjectAttrs } = splitAttributes(attributes);
|
|
111
|
+
|
|
112
|
+
// Build the result
|
|
113
|
+
const parts = [`element: "${tagName}"`];
|
|
114
|
+
|
|
115
|
+
if (domAttrs.length > 0) {
|
|
116
|
+
parts.push(`attrs: { ${domAttrs.join(', ')} }`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (displayObjectAttrs.length > 0) {
|
|
120
|
+
parts.push(...displayObjectAttrs);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return `h(DOMElement, { ${parts.join(', ')} })`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function splitAttributes(attributes) {
|
|
127
|
+
const domAttrs = [];
|
|
128
|
+
const displayObjectAttrs = [];
|
|
129
|
+
const classValues = [];
|
|
130
|
+
let classInsertIndex = null;
|
|
131
|
+
|
|
132
|
+
attributes.forEach(attr => {
|
|
133
|
+
// Handle spread attributes
|
|
134
|
+
if (attr.startsWith('...')) {
|
|
135
|
+
displayObjectAttrs.push(attr);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Extract attribute name and value (if present)
|
|
140
|
+
let attrName;
|
|
141
|
+
let attrValue;
|
|
142
|
+
if (attr.includes(':')) {
|
|
143
|
+
const colonIndex = attr.indexOf(':');
|
|
144
|
+
attrName = attr.slice(0, colonIndex).trim().replace(/['"]/g, '');
|
|
145
|
+
attrValue = attr.slice(colonIndex + 1).trim();
|
|
146
|
+
} else {
|
|
147
|
+
// Standalone attribute
|
|
148
|
+
attrName = attr.replace(/['"]/g, '');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check if it's a DisplayObject attribute
|
|
152
|
+
if (displayObjectAttributes.has(attrName)) {
|
|
153
|
+
displayObjectAttrs.push(attr);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (attrName === 'class' && attrValue !== undefined) {
|
|
158
|
+
classValues.push(attrValue);
|
|
159
|
+
if (classInsertIndex === null) {
|
|
160
|
+
classInsertIndex = domAttrs.length;
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
domAttrs.push(attr);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (classValues.length > 0) {
|
|
169
|
+
const mergedClass = classValues.length === 1
|
|
170
|
+
? `class: ${classValues[0]}`
|
|
171
|
+
: `class: [${classValues.join(', ')}]`;
|
|
172
|
+
if (classInsertIndex === null) {
|
|
173
|
+
domAttrs.push(mergedClass);
|
|
174
|
+
} else {
|
|
175
|
+
domAttrs.splice(classInsertIndex, 0, mergedClass);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { domAttrs, displayObjectAttrs };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatDOMContainerAttributes(attributes) {
|
|
183
|
+
if (attributes.length === 0) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const propsEntries = [];
|
|
188
|
+
const domAttrs = [];
|
|
189
|
+
const classValues = [];
|
|
190
|
+
let classInsertIndex = null;
|
|
191
|
+
let attrsInsertIndex = null;
|
|
192
|
+
let attrsIndex = null;
|
|
193
|
+
let attrsValue = null;
|
|
194
|
+
|
|
195
|
+
attributes.forEach(attr => {
|
|
196
|
+
if (attr.startsWith('...')) {
|
|
197
|
+
propsEntries.push(attr);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let attrName;
|
|
202
|
+
let attrValue;
|
|
203
|
+
if (attr.includes(':')) {
|
|
204
|
+
const colonIndex = attr.indexOf(':');
|
|
205
|
+
attrName = attr.slice(0, colonIndex).trim().replace(/['"]/g, '');
|
|
206
|
+
attrValue = attr.slice(colonIndex + 1).trim();
|
|
207
|
+
} else {
|
|
208
|
+
attrName = attr.replace(/['"]/g, '');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (attrName === 'class' && attrValue !== undefined) {
|
|
212
|
+
classValues.push(attrValue);
|
|
213
|
+
if (classInsertIndex === null) {
|
|
214
|
+
classInsertIndex = domAttrs.length;
|
|
215
|
+
}
|
|
216
|
+
if (attrsInsertIndex === null) {
|
|
217
|
+
attrsInsertIndex = propsEntries.length;
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (attrName === 'style') {
|
|
223
|
+
domAttrs.push(attr);
|
|
224
|
+
if (attrsInsertIndex === null) {
|
|
225
|
+
attrsInsertIndex = propsEntries.length;
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (attrName === 'attrs' && attrValue !== undefined) {
|
|
231
|
+
attrsValue = attrValue;
|
|
232
|
+
attrsIndex = propsEntries.length;
|
|
233
|
+
propsEntries.push(null);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
propsEntries.push(attr);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (classValues.length > 0) {
|
|
241
|
+
const mergedClass = classValues.length === 1
|
|
242
|
+
? `class: ${classValues[0]}`
|
|
243
|
+
: `class: [${classValues.join(', ')}]`;
|
|
244
|
+
if (classInsertIndex === null) {
|
|
245
|
+
domAttrs.push(mergedClass);
|
|
246
|
+
} else {
|
|
247
|
+
domAttrs.splice(classInsertIndex, 0, mergedClass);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let attrsEntry = null;
|
|
252
|
+
if (attrsValue && domAttrs.length > 0) {
|
|
253
|
+
attrsEntry = `attrs: { ...${attrsValue}, ${domAttrs.join(', ')} }`;
|
|
254
|
+
} else if (attrsValue) {
|
|
255
|
+
attrsEntry = `attrs: ${attrsValue}`;
|
|
256
|
+
} else if (domAttrs.length > 0) {
|
|
257
|
+
attrsEntry = `attrs: { ${domAttrs.join(', ')} }`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (attrsEntry) {
|
|
261
|
+
if (attrsIndex !== null) {
|
|
262
|
+
propsEntries[attrsIndex] = attrsEntry;
|
|
263
|
+
} else if (attrsInsertIndex !== null) {
|
|
264
|
+
propsEntries.splice(attrsInsertIndex, 0, attrsEntry);
|
|
265
|
+
} else {
|
|
266
|
+
propsEntries.unshift(attrsEntry);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const filteredEntries = propsEntries.filter(entry => entry !== null);
|
|
271
|
+
if (filteredEntries.length === 0) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (filteredEntries.length === 1 && filteredEntries[0].startsWith('...')) {
|
|
276
|
+
return filteredEntries[0].substring(3);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return `{ ${filteredEntries.join(', ')} }`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function hasFunctionCall(value) {
|
|
283
|
+
return /[a-zA-Z_][a-zA-Z0-9_]*\s*\(/.test(value);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function hasIdentifier(value) {
|
|
287
|
+
return /[a-zA-Z_]/.test(value);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function isSimpleAccessor(value) {
|
|
291
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/.test(value.trim());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function formatObjectLiteralSpacing(value) {
|
|
295
|
+
const trimmed = value.trim();
|
|
296
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
300
|
+
return `{ ${inner} }`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function transformBareIdentifiersToSignals(value) {
|
|
304
|
+
return value.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g, (match, name, offset) => {
|
|
305
|
+
if (['true', 'false', 'null'].includes(name)) {
|
|
306
|
+
return match;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const beforeMatch = value.substring(0, offset);
|
|
310
|
+
const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length;
|
|
311
|
+
const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;
|
|
312
|
+
if (singleQuotesBefore % 2 === 1 || doubleQuotesBefore % 2 === 1) {
|
|
313
|
+
return match;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const charBefore = offset > 0 ? value[offset - 1] : '';
|
|
317
|
+
const charAfter = offset + match.length < value.length ? value[offset + match.length] : '';
|
|
318
|
+
|
|
319
|
+
if (charBefore === '.' || charAfter === '.') {
|
|
320
|
+
return match;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const afterSlice = value.slice(offset + match.length);
|
|
324
|
+
if (/^\s*\(/.test(afterSlice)) {
|
|
325
|
+
return match;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return `${name}()`;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
start
|
|
334
|
+
= _ elements:(element)* _ {
|
|
335
|
+
if (elements.length === 1) {
|
|
336
|
+
return elements[0];
|
|
337
|
+
}
|
|
338
|
+
return `[${elements.join(',')}]`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
element "component or control structure"
|
|
342
|
+
= forLoop
|
|
343
|
+
/ ifCondition
|
|
344
|
+
/ svgElement
|
|
345
|
+
/ domElementWithText
|
|
346
|
+
/ domElementWithMixedContent
|
|
347
|
+
/ selfClosingElement
|
|
348
|
+
/ voidElement
|
|
349
|
+
/ openCloseElement
|
|
350
|
+
/ openUnclosedTag
|
|
351
|
+
/ comment
|
|
352
|
+
|
|
353
|
+
selfClosingElement "self-closing component tag"
|
|
354
|
+
= _ "<" _ tagName:tagName _ attributes:attributes _ "/>" _ {
|
|
355
|
+
// Check if it's a DOM element
|
|
356
|
+
if (isDOMElement(tagName)) {
|
|
357
|
+
return formatDOMElement(tagName, attributes);
|
|
358
|
+
}
|
|
359
|
+
if (tagName === 'DOMContainer') {
|
|
360
|
+
const attrsString = formatDOMContainerAttributes(attributes);
|
|
361
|
+
return attrsString ? `h(DOMContainer, ${attrsString})` : `h(DOMContainer)`;
|
|
362
|
+
}
|
|
363
|
+
// Otherwise, treat as regular component
|
|
364
|
+
const attrsString = formatAttributes(attributes);
|
|
365
|
+
return attrsString ? `h(${tagName}, ${attrsString})` : `h(${tagName})`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
voidElement "void DOM element tag"
|
|
369
|
+
= _ "<" _ tagName:tagName &{ return isVoidElement(tagName); } _ attributes:attributes _ ">" _ {
|
|
370
|
+
return formatDOMElement(tagName, attributes);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
domElementWithText "DOM element with text content"
|
|
374
|
+
= "<" _ tagName:tagName &{ return !isVoidElement(tagName); } _ attributes:attributes _ ">" _ text:simpleTextContent _ "</" _ closingTagName:tagName _ ">" _ {
|
|
375
|
+
if (tagName !== closingTagName) {
|
|
376
|
+
generateError(
|
|
377
|
+
`Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
|
|
378
|
+
location()
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (isDOMElement(tagName)) {
|
|
383
|
+
if (attributes.length === 0) {
|
|
384
|
+
return `h(DOMElement, { element: "${tagName}", textContent: ${text} })`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const { domAttrs, displayObjectAttrs } = splitAttributes(attributes);
|
|
388
|
+
|
|
389
|
+
// Build the result
|
|
390
|
+
const parts = [`element: "${tagName}"`];
|
|
391
|
+
|
|
392
|
+
if (domAttrs.length > 0) {
|
|
393
|
+
parts.push(`attrs: { ${domAttrs.join(', ')} }`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
parts.push(`textContent: ${text}`);
|
|
397
|
+
|
|
398
|
+
if (displayObjectAttrs.length > 0) {
|
|
399
|
+
parts.push(...displayObjectAttrs);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return `h(DOMElement, { ${parts.join(', ')} })`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// If not a DOM element, fall back to regular parsing
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
domElementWithMixedContent "DOM element with mixed content"
|
|
410
|
+
= "<" _ tagName:tagName &{ return isDOMElement(tagName) && !isVoidElement(tagName); } _ attributes:attributes _ ">" _ children:domContent _ "</" _ closingTagName:tagName _ ">" _ {
|
|
411
|
+
if (tagName !== closingTagName) {
|
|
412
|
+
generateError(
|
|
413
|
+
`Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
|
|
414
|
+
location()
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const childrenContent = children ? children : null;
|
|
419
|
+
|
|
420
|
+
if (attributes.length === 0) {
|
|
421
|
+
if (childrenContent) {
|
|
422
|
+
return `h(DOMElement, { element: "${tagName}" }, ${childrenContent})`;
|
|
423
|
+
} else {
|
|
424
|
+
return `h(DOMElement, { element: "${tagName}" })`;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const { domAttrs, displayObjectAttrs } = splitAttributes(attributes);
|
|
429
|
+
|
|
430
|
+
// Build the result
|
|
431
|
+
const parts = [`element: "${tagName}"`];
|
|
432
|
+
|
|
433
|
+
if (domAttrs.length > 0) {
|
|
434
|
+
parts.push(`attrs: { ${domAttrs.join(', ')} }`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (displayObjectAttrs.length > 0) {
|
|
438
|
+
parts.push(...displayObjectAttrs);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (childrenContent) {
|
|
442
|
+
return `h(DOMElement, { ${parts.join(', ')} }, ${childrenContent})`;
|
|
443
|
+
} else {
|
|
444
|
+
return `h(DOMElement, { ${parts.join(', ')} })`;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
simpleTextContent "simple text content"
|
|
449
|
+
= parts:(simpleDynamicPart / simpleTextPart)+ {
|
|
450
|
+
const validParts = parts.filter(p => p !== null);
|
|
451
|
+
if (validParts.length === 0) return null;
|
|
452
|
+
if (validParts.length === 1) return validParts[0];
|
|
453
|
+
|
|
454
|
+
// Multiple parts - need to concatenate
|
|
455
|
+
const normalizedParts = validParts.map(part => {
|
|
456
|
+
if (typeof part === 'string' && part.startsWith('computed(() => ') && part.endsWith(')')) {
|
|
457
|
+
return part.slice('computed(() => '.length, -1);
|
|
458
|
+
}
|
|
459
|
+
return part;
|
|
460
|
+
});
|
|
461
|
+
const hasSignals = normalizedParts.some(part => part && part.includes && part.includes('()'));
|
|
462
|
+
if (hasSignals) {
|
|
463
|
+
return `computed(() => ${normalizedParts.join(' + ')})`;
|
|
464
|
+
}
|
|
465
|
+
return normalizedParts.join(' + ');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
simpleTextPart "simple text part"
|
|
469
|
+
= !("@for" / "@if") text:$([^<{@]+) {
|
|
470
|
+
const trimmed = text.trim();
|
|
471
|
+
if (!trimmed) return null;
|
|
472
|
+
const escaped = text
|
|
473
|
+
.replace(/\\/g, '\\\\')
|
|
474
|
+
.replace(/'/g, "\\'")
|
|
475
|
+
.replace(/\r/g, '\\r')
|
|
476
|
+
.replace(/\n/g, '\\n')
|
|
477
|
+
.replace(/\t/g, '\\t');
|
|
478
|
+
return `'${escaped}'`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
simpleDynamicPart "simple dynamic part"
|
|
482
|
+
= "{{" _ expr:attributeValue _ "}}" {
|
|
483
|
+
const trimmedExpr = expr.trim();
|
|
484
|
+
if (!trimmedExpr) {
|
|
485
|
+
return trimmedExpr;
|
|
486
|
+
}
|
|
487
|
+
if (hasFunctionCall(trimmedExpr)) {
|
|
488
|
+
return `computed(() => ${trimmedExpr})`;
|
|
489
|
+
}
|
|
490
|
+
return trimmedExpr;
|
|
491
|
+
}
|
|
492
|
+
/ "{" _ expr:attributeValue _ "}" {
|
|
493
|
+
const trimmedExpr = expr.trim();
|
|
494
|
+
if (!trimmedExpr) {
|
|
495
|
+
return trimmedExpr;
|
|
496
|
+
}
|
|
497
|
+
if (hasFunctionCall(trimmedExpr)) {
|
|
498
|
+
return `computed(() => ${trimmedExpr})`;
|
|
499
|
+
}
|
|
500
|
+
return trimmedExpr;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
openCloseElement "component with content"
|
|
504
|
+
= "<" _ tagName:tagName _ attributes:attributes _ ">" _ content:content _ "</" _ closingTagName:tagName _ ">" _ {
|
|
505
|
+
if (tagName !== closingTagName) {
|
|
506
|
+
generateError(
|
|
507
|
+
`Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
|
|
508
|
+
location()
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const children = content ? content : null;
|
|
513
|
+
|
|
514
|
+
// Check if it's a DOM element
|
|
515
|
+
if (isDOMElement(tagName)) {
|
|
516
|
+
if (attributes.length === 0) {
|
|
517
|
+
if (children) {
|
|
518
|
+
return `h(DOMElement, { element: "${tagName}" }, ${children})`;
|
|
519
|
+
} else {
|
|
520
|
+
return `h(DOMElement, { element: "${tagName}" })`;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const { domAttrs, displayObjectAttrs } = splitAttributes(attributes);
|
|
525
|
+
|
|
526
|
+
// Build the result
|
|
527
|
+
const parts = [`element: "${tagName}"`];
|
|
528
|
+
|
|
529
|
+
if (domAttrs.length > 0) {
|
|
530
|
+
parts.push(`attrs: { ${domAttrs.join(', ')} }`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (displayObjectAttrs.length > 0) {
|
|
534
|
+
parts.push(...displayObjectAttrs);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (children) {
|
|
538
|
+
return `h(DOMElement, { ${parts.join(', ')} }, ${children})`;
|
|
539
|
+
} else {
|
|
540
|
+
return `h(DOMElement, { ${parts.join(', ')} })`;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Otherwise, treat as regular component
|
|
545
|
+
if (tagName === 'DOMContainer') {
|
|
546
|
+
const attrsString = formatDOMContainerAttributes(attributes);
|
|
547
|
+
if (attrsString && children) {
|
|
548
|
+
return `h(DOMContainer, ${attrsString}, ${children})`;
|
|
549
|
+
} else if (attrsString) {
|
|
550
|
+
return `h(DOMContainer, ${attrsString})`;
|
|
551
|
+
} else if (children) {
|
|
552
|
+
return `h(DOMContainer, null, ${children})`;
|
|
553
|
+
} else {
|
|
554
|
+
return `h(DOMContainer)`;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const attrsString = formatAttributes(attributes);
|
|
559
|
+
if (attrsString && children) {
|
|
560
|
+
return `h(${tagName}, ${attrsString}, ${children})`;
|
|
561
|
+
} else if (attrsString) {
|
|
562
|
+
return `h(${tagName}, ${attrsString})`;
|
|
563
|
+
} else if (children) {
|
|
564
|
+
return `h(${tagName}, null, ${children})`;
|
|
565
|
+
} else {
|
|
566
|
+
return `h(${tagName})`;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
attributes "component attributes"
|
|
571
|
+
= attrs:(attribute (_ attribute)*)? {
|
|
572
|
+
return attrs
|
|
573
|
+
? [attrs[0]].concat(attrs[1].map(a => a[1]))
|
|
574
|
+
: [];
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
attribute "attribute"
|
|
578
|
+
= staticAttribute
|
|
579
|
+
/ dynamicAttribute
|
|
580
|
+
/ eventHandler
|
|
581
|
+
/ spreadAttribute
|
|
582
|
+
/ unclosedQuote
|
|
583
|
+
/ unclosedBrace
|
|
584
|
+
|
|
585
|
+
spreadAttribute "spread attribute"
|
|
586
|
+
= "..." expr:(functionCallExpr / dotNotation) {
|
|
587
|
+
return "..." + expr;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
functionCallExpr "function call"
|
|
591
|
+
= name:dotNotation "(" args:functionArgs? ")" {
|
|
592
|
+
return `${name}(${args || ''})`;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
dotNotation "property access"
|
|
596
|
+
= first:identifier rest:("." identifier)* {
|
|
597
|
+
return text();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
eventHandler "event handler"
|
|
601
|
+
= "@" eventName:identifier _ "=" _ "{" _ handlerName:attributeValue _ "}" {
|
|
602
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(eventName);
|
|
603
|
+
const formattedName = needsQuotes ? `'${eventName}'` : eventName;
|
|
604
|
+
return `${formattedName}: ${handlerName}`;
|
|
605
|
+
}
|
|
606
|
+
/ "@" eventName:attributeName _ {
|
|
607
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(eventName);
|
|
608
|
+
return needsQuotes ? `'${eventName}'` : eventName;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
dynamicAttribute "dynamic attribute"
|
|
612
|
+
= attributeName:attributeName _ "=" _ "{" _ attributeValue:attributeValue _ "}" {
|
|
613
|
+
// Check if attributeName needs to be quoted (contains dash or other invalid JS identifier chars)
|
|
614
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
|
|
615
|
+
const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
|
|
616
|
+
|
|
617
|
+
if (eventAttributes.has(attributeName)) {
|
|
618
|
+
return `${formattedName}: ${attributeValue}`;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// If it's a complex object with strings, preserve it as is
|
|
622
|
+
if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}') &&
|
|
623
|
+
(attributeValue.includes('"') || attributeValue.includes("'"))) {
|
|
624
|
+
return `${formattedName}: ${attributeValue}`;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// If it's a template string, keep it as-is
|
|
628
|
+
if (attributeValue.trim().startsWith('`') && attributeValue.trim().endsWith('`')) {
|
|
629
|
+
return formattedName + ': ' + attributeValue;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Handle other types of values
|
|
633
|
+
if (attributeValue.startsWith('h(') || attributeValue.includes('=>')) {
|
|
634
|
+
return `${formattedName}: ${attributeValue}`;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const trimmedValue = attributeValue.trim();
|
|
638
|
+
if (trimmedValue.match(/^[a-zA-Z_]\w*$/)) {
|
|
639
|
+
return `${formattedName}: ${attributeValue}`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (/^\d+(\.\d+)?$/.test(trimmedValue) || ['true', 'false', 'null'].includes(trimmedValue)) {
|
|
643
|
+
return `${formattedName}: ${attributeValue}`;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (isSimpleAccessor(trimmedValue)) {
|
|
647
|
+
return `${formattedName}: ${attributeValue}`;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const isObjectLiteral = trimmedValue.startsWith('{') && trimmedValue.endsWith('}');
|
|
651
|
+
const isArrayLiteral = trimmedValue.startsWith('[') && trimmedValue.endsWith(']');
|
|
652
|
+
if (isObjectLiteral) {
|
|
653
|
+
const formattedObject = formatObjectLiteralSpacing(attributeValue);
|
|
654
|
+
if (hasFunctionCall(trimmedValue)) {
|
|
655
|
+
return `${formattedName}: computed(() => (${formattedObject}))`;
|
|
656
|
+
}
|
|
657
|
+
return `${formattedName}: ${formattedObject}`;
|
|
658
|
+
}
|
|
659
|
+
if (isArrayLiteral) {
|
|
660
|
+
if (hasFunctionCall(trimmedValue)) {
|
|
661
|
+
return `${formattedName}: computed(() => ${attributeValue})`;
|
|
662
|
+
}
|
|
663
|
+
return `${formattedName}: ${attributeValue}`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (hasFunctionCall(trimmedValue)) {
|
|
667
|
+
return `${formattedName}: computed(() => ${attributeValue})`;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (!hasIdentifier(trimmedValue)) {
|
|
671
|
+
return `${formattedName}: ${attributeValue}`;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const computedValue = transformBareIdentifiersToSignals(attributeValue);
|
|
675
|
+
return `${formattedName}: computed(() => ${computedValue})`;
|
|
676
|
+
}
|
|
677
|
+
/ attributeName:attributeName _ {
|
|
678
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
|
|
679
|
+
return needsQuotes ? `'${attributeName}'` : attributeName;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
attributeValue "attribute value"
|
|
683
|
+
= element
|
|
684
|
+
/ functionWithElement
|
|
685
|
+
/ objectLiteral
|
|
686
|
+
/ $([^{}]* ("{" [^{}]* "}" [^{}]*)*) {
|
|
687
|
+
return text().trim()
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
objectLiteral "object literal"
|
|
691
|
+
= "{" _ objContent:objectContent _ "}" {
|
|
692
|
+
return `{ ${objContent} }`;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
objectContent
|
|
696
|
+
= prop:objectProperty rest:(_ "," _ objectProperty)* {
|
|
697
|
+
return [prop].concat(rest.map(r => r[3])).join(', ');
|
|
698
|
+
}
|
|
699
|
+
/ "" { return ""; }
|
|
700
|
+
|
|
701
|
+
objectProperty
|
|
702
|
+
= key:identifier _ ":" _ value:propertyValue {
|
|
703
|
+
return `${key}: ${value}`;
|
|
704
|
+
}
|
|
705
|
+
/ key:identifier {
|
|
706
|
+
return key;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
propertyValue
|
|
710
|
+
= nestedObject
|
|
711
|
+
/ element
|
|
712
|
+
/ functionWithElement
|
|
713
|
+
/ stringLiteral
|
|
714
|
+
/ number
|
|
715
|
+
/ identifier
|
|
716
|
+
|
|
717
|
+
nestedObject
|
|
718
|
+
= "{" _ objContent:objectContent _ "}" {
|
|
719
|
+
return `{ ${objContent} }`;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
stringLiteral
|
|
723
|
+
= '"' chars:[^"]* '"' { return text(); }
|
|
724
|
+
/ "'" chars:[^']* "'" { return text(); }
|
|
725
|
+
|
|
726
|
+
functionWithElement "function expression"
|
|
727
|
+
= "(" _ params:functionParams? _ ")" _ "=>" _ elem:element {
|
|
728
|
+
return `${params ? `(${params}) =>` : '() =>'} ${elem}`;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
functionParams
|
|
732
|
+
= destructuredParams
|
|
733
|
+
/ simpleParams
|
|
734
|
+
|
|
735
|
+
destructuredParams
|
|
736
|
+
= "{" _ param:identifier rest:(_ "," _ identifier)* _ "}" {
|
|
737
|
+
return `{${[param].concat(rest.map(r => r[3])).join(', ')}}`;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
simpleParams
|
|
741
|
+
= param:identifier rest:(_ "," _ identifier)* {
|
|
742
|
+
return [param].concat(rest.map(r => r[3])).join(', ');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
staticAttribute "static attribute"
|
|
746
|
+
= attributeName:attributeName _ "=" _ "\"" attributeValue:staticValue "\"" {
|
|
747
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
|
|
748
|
+
const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
|
|
749
|
+
return `${formattedName}: ${attributeValue}`;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
eventAttribute
|
|
753
|
+
= "(" _ eventName:eventName _ ")" _ "=" _ "\"" eventAction:eventAction "\"" {
|
|
754
|
+
return `${eventName}: () => { ${eventAction} }`;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
staticValue
|
|
758
|
+
= [^"]+ {
|
|
759
|
+
var val = text();
|
|
760
|
+
return `'${val}'`
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
content "component content"
|
|
764
|
+
= elements:(element)* {
|
|
765
|
+
const filteredElements = elements.filter(el => el !== null);
|
|
766
|
+
if (filteredElements.length === 0) return null;
|
|
767
|
+
if (filteredElements.length === 1) return filteredElements[0];
|
|
768
|
+
return `[${filteredElements.join(', ')}]`;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
domContent "DOM content"
|
|
772
|
+
= elements:(domContentPart)* {
|
|
773
|
+
const filteredElements = elements.filter(el => el !== null);
|
|
774
|
+
if (filteredElements.length === 0) return null;
|
|
775
|
+
if (filteredElements.length === 1) return filteredElements[0];
|
|
776
|
+
return `[${filteredElements.join(', ')}]`;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
domContentPart
|
|
780
|
+
= element
|
|
781
|
+
/ simpleTextContent
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
textNode
|
|
786
|
+
= text:$([^<]+) {
|
|
787
|
+
const trimmed = text.trim();
|
|
788
|
+
return trimmed ? `'${trimmed}'` : null;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
textElement
|
|
792
|
+
= text:[^<>]+ {
|
|
793
|
+
const trimmed = text.join('').trim();
|
|
794
|
+
return trimmed ? JSON.stringify(trimmed) : null;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
forLoop "for loop"
|
|
798
|
+
= _ "@for" _ "(" _ variableName:(tupleDestructuring / identifier) _ "of" _ iterable:iterable _ ")" _ "{" _ content:content _ "}" _ {
|
|
799
|
+
return `loop(${iterable}, ${variableName} => ${content})`;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
tupleDestructuring "destructuring pattern"
|
|
803
|
+
= "(" _ first:identifier _ "," _ second:identifier _ ")" {
|
|
804
|
+
return `(${first}, ${second})`;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
ifCondition "if condition"
|
|
808
|
+
= _ "@if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ elseIfs:elseIfClause* elseClause:elseClause? _ {
|
|
809
|
+
let result = `cond(${condition}, () => ${content}`;
|
|
810
|
+
|
|
811
|
+
// Add else if clauses
|
|
812
|
+
elseIfs.forEach(elseIf => {
|
|
813
|
+
result += `, [${elseIf.condition}, () => ${elseIf.content}]`;
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// Add else clause if present
|
|
817
|
+
if (elseClause) {
|
|
818
|
+
result += `, () => ${elseClause}`;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
result += ')';
|
|
822
|
+
return result;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
elseIfClause "else if clause"
|
|
826
|
+
= _ "@else" _ "if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ {
|
|
827
|
+
return { condition, content };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
elseClause "else clause"
|
|
831
|
+
= _ "@else" _ "{" _ content:content _ "}" _ {
|
|
832
|
+
return content;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
tagName "tag name"
|
|
836
|
+
= tagExpression
|
|
837
|
+
|
|
838
|
+
tagExpression "tag expression"
|
|
839
|
+
= first:tagPart rest:("." tagPart)* {
|
|
840
|
+
return text();
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
tagPart "tag part"
|
|
844
|
+
= name:[a-zA-Z][a-zA-Z0-9]* args:("(" functionArgs? ")")? {
|
|
845
|
+
return text();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
attributeName "attribute name"
|
|
849
|
+
= [a-zA-Z][a-zA-Z0-9-]* { return text(); }
|
|
850
|
+
|
|
851
|
+
eventName
|
|
852
|
+
= [a-zA-Z][a-zA-Z0-9-]* { return text(); }
|
|
853
|
+
|
|
854
|
+
variableName
|
|
855
|
+
= [a-zA-Z_][a-zA-Z0-9_]* { return text(); }
|
|
856
|
+
|
|
857
|
+
iterable "iterable expression"
|
|
858
|
+
= id:identifier "(" _ args:functionArgs? _ ")" { // Direct function call
|
|
859
|
+
return `${id}(${args || ''})`;
|
|
860
|
+
}
|
|
861
|
+
/ first:identifier "." rest:dotFunctionChain { // Dot notation possibly with function call
|
|
862
|
+
return `${first}.${rest}`;
|
|
863
|
+
}
|
|
864
|
+
/ id:identifier { return id; }
|
|
865
|
+
|
|
866
|
+
dotFunctionChain
|
|
867
|
+
= segment:identifier "(" _ args:functionArgs? _ ")" rest:("." dotFunctionChain)? {
|
|
868
|
+
const restStr = rest ? `.${rest[1]}` : '';
|
|
869
|
+
return `${segment}(${args || ''})${restStr}`;
|
|
870
|
+
}
|
|
871
|
+
/ segment:identifier rest:("." dotFunctionChain)? {
|
|
872
|
+
const restStr = rest ? `.${rest[1]}` : '';
|
|
873
|
+
return `${segment}${restStr}`;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
condition "condition expression"
|
|
877
|
+
= text:$(conditionChunk*) {
|
|
878
|
+
const originalText = text.trim();
|
|
879
|
+
if (!originalText) {
|
|
880
|
+
return originalText;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const hasOperator = /[!<>=&|]/.test(originalText);
|
|
884
|
+
if (hasOperator) {
|
|
885
|
+
return `computed(() => ${originalText})`;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return originalText;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
conditionChunk
|
|
892
|
+
= "(" conditionChunk* ")"
|
|
893
|
+
/ [^()]
|
|
894
|
+
|
|
895
|
+
functionCall "function call"
|
|
896
|
+
= name:identifier "(" args:functionArgs? ")" {
|
|
897
|
+
return `${name}(${args || ''})`;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
functionCallWithArgs "function call with complex args"
|
|
901
|
+
= name:identifier "(" args:complexFunctionArgs? ")" {
|
|
902
|
+
return `${name}(${args || ''})`;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
functionArgs
|
|
906
|
+
= arg:functionArg rest:("," _ functionArg)* {
|
|
907
|
+
return [arg].concat(rest.map(r => r[2])).join(', ');
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
complexFunctionArgs
|
|
911
|
+
= arg:complexFunctionArg rest:("," _ complexFunctionArg)* {
|
|
912
|
+
return [arg].concat(rest.map(r => r[2])).join(', ');
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
functionArg
|
|
916
|
+
= _ value:(identifier / number / string) _ {
|
|
917
|
+
return value;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
complexFunctionArg "complex function argument"
|
|
921
|
+
= _ value:complexArgExpression _ {
|
|
922
|
+
return value.trim();
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
complexArgExpression "complex argument expression"
|
|
926
|
+
= $([^,)]* ("(" [^)]* ")" [^,)]*)*) {
|
|
927
|
+
return text().trim();
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
number
|
|
931
|
+
= [0-9]+ ("." [0-9]+)? { return text(); }
|
|
932
|
+
|
|
933
|
+
string
|
|
934
|
+
= '"' chars:[^"]* '"' { return text(); }
|
|
935
|
+
/ "'" chars:[^']* "'" { return text(); }
|
|
936
|
+
|
|
937
|
+
eventAction
|
|
938
|
+
= [^"]* { return text(); }
|
|
939
|
+
|
|
940
|
+
_ 'whitespace'
|
|
941
|
+
= [ \t\n\r]*
|
|
942
|
+
|
|
943
|
+
identifier
|
|
944
|
+
= [a-zA-Z_][a-zA-Z0-9_]* { return text(); }
|
|
945
|
+
|
|
946
|
+
comment
|
|
947
|
+
= singleComment+ {
|
|
948
|
+
return null
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
singleComment
|
|
952
|
+
= "<!--" _ content:((!("-->") .)* "-->") _ {
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Add a special error detection rule for unclosed tags
|
|
957
|
+
openUnclosedTag "unclosed tag"
|
|
958
|
+
= "<" _ tagName:tagName &{ return !isVoidElement(tagName); } _ attributes:attributes _ ">" _ content:content _ !("</" _ closingTagName:tagName _ ">") {
|
|
959
|
+
generateError(
|
|
960
|
+
`Unclosed tag: <${tagName}> is missing its closing tag`,
|
|
961
|
+
location()
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Add error detection for unclosed quotes in static attributes
|
|
966
|
+
unclosedQuote "unclosed string"
|
|
967
|
+
= attributeName:attributeName _ "=" _ "\"" [^"]* !("\"") {
|
|
968
|
+
generateError(
|
|
969
|
+
`Missing closing quote in attribute '${attributeName}'`,
|
|
970
|
+
location()
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Add error detection for unclosed braces in dynamic attributes
|
|
975
|
+
unclosedBrace "unclosed brace"
|
|
976
|
+
= attributeName:attributeName _ "=" _ "{" !("}" / _ "}") [^{}]* {
|
|
977
|
+
generateError(
|
|
978
|
+
`Missing closing brace in dynamic attribute '${attributeName}'`,
|
|
979
|
+
location()
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
svgElement "SVG element"
|
|
984
|
+
= "<svg" attrs:([^>]*) ">" content:svgInnerContent "</svg>" _ {
|
|
985
|
+
const attributes = attrs.join('').trim();
|
|
986
|
+
// Clean up the content by removing extra whitespace and newlines
|
|
987
|
+
const cleanContent = content.replace(/\s+/g, ' ').trim();
|
|
988
|
+
const rawContent = `<svg${attributes ? ' ' + attributes : ''}>${cleanContent}</svg>`;
|
|
989
|
+
return `h(Svg, { content: \`${rawContent}\` })`;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
svgInnerContent "SVG inner content"
|
|
993
|
+
= content:$((!("</svg>") .)*) {
|
|
994
|
+
return content;
|
|
995
|
+
}
|