@wire-dsl/engine 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1138 -36
- package/dist/index.d.cts +475 -1
- package/dist/index.d.ts +475 -1
- package/dist/index.js +1132 -36
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -23,11 +23,17 @@ __export(index_exports, {
|
|
|
23
23
|
IRGenerator: () => IRGenerator,
|
|
24
24
|
LayoutEngine: () => LayoutEngine,
|
|
25
25
|
SVGRenderer: () => SVGRenderer,
|
|
26
|
+
SourceMapBuilder: () => SourceMapBuilder,
|
|
27
|
+
SourceMapResolver: () => SourceMapResolver,
|
|
26
28
|
buildSVG: () => buildSVG,
|
|
27
29
|
calculateLayout: () => calculateLayout,
|
|
28
30
|
createSVGElement: () => createSVGElement,
|
|
29
31
|
generateIR: () => generateIR,
|
|
32
|
+
generateStableNodeId: () => generateStableNodeId,
|
|
33
|
+
getTypeFromNodeId: () => getTypeFromNodeId,
|
|
34
|
+
isValidNodeId: () => isValidNodeId,
|
|
30
35
|
parseWireDSL: () => parseWireDSL,
|
|
36
|
+
parseWireDSLWithSourceMap: () => parseWireDSLWithSourceMap,
|
|
31
37
|
renderToSVG: () => renderToSVG,
|
|
32
38
|
resolveGridPosition: () => resolveGridPosition,
|
|
33
39
|
version: () => version
|
|
@@ -36,6 +42,418 @@ module.exports = __toCommonJS(index_exports);
|
|
|
36
42
|
|
|
37
43
|
// src/parser/index.ts
|
|
38
44
|
var import_chevrotain = require("chevrotain");
|
|
45
|
+
|
|
46
|
+
// src/sourcemap/builder.ts
|
|
47
|
+
var SourceMapBuilder = class {
|
|
48
|
+
// Counter per type-subtype
|
|
49
|
+
constructor(filePath = "<input>", sourceCode = "") {
|
|
50
|
+
this.entries = [];
|
|
51
|
+
this.parentStack = [];
|
|
52
|
+
// Stack of parent nodeIds for hierarchy tracking
|
|
53
|
+
this.counters = /* @__PURE__ */ new Map();
|
|
54
|
+
this.filePath = filePath;
|
|
55
|
+
this.sourceCode = sourceCode;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Add a node to the SourceMap
|
|
59
|
+
* Generates semantic IDs like: project, screen-0, component-button-1, layout-stack-0
|
|
60
|
+
*
|
|
61
|
+
* @param type - Type of AST node
|
|
62
|
+
* @param tokens - Captured tokens from parser
|
|
63
|
+
* @param metadata - Optional metadata (name, layoutType, componentType)
|
|
64
|
+
* @returns Generated nodeId
|
|
65
|
+
*/
|
|
66
|
+
addNode(type, tokens, metadata) {
|
|
67
|
+
const range = this.calculateRange(tokens);
|
|
68
|
+
const nodeId = this.generateNodeId(type, metadata);
|
|
69
|
+
const parentId = this.parentStack.length > 0 ? this.parentStack[this.parentStack.length - 1] : null;
|
|
70
|
+
const keywordRange = tokens.keyword ? this.tokenToRange(tokens.keyword) : void 0;
|
|
71
|
+
const nameRange = tokens.name ? this.tokenToRange(tokens.name) : void 0;
|
|
72
|
+
const bodyRange = tokens.body ? this.calculateBodyRange(tokens.body) : void 0;
|
|
73
|
+
const entry = {
|
|
74
|
+
nodeId,
|
|
75
|
+
type,
|
|
76
|
+
range,
|
|
77
|
+
filePath: this.filePath,
|
|
78
|
+
parentId,
|
|
79
|
+
keywordRange,
|
|
80
|
+
nameRange,
|
|
81
|
+
bodyRange,
|
|
82
|
+
...metadata
|
|
83
|
+
// Spread name, layoutType, componentType, isUserDefined if provided
|
|
84
|
+
};
|
|
85
|
+
this.entries.push(entry);
|
|
86
|
+
return nodeId;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Generate semantic node ID based on type and subtype
|
|
90
|
+
* Format: {type}-{subtype}-{counter} or {type}-{counter}
|
|
91
|
+
*
|
|
92
|
+
* Examples:
|
|
93
|
+
* - project → "project"
|
|
94
|
+
* - theme → "theme"
|
|
95
|
+
* - mocks → "mocks"
|
|
96
|
+
* - colors → "colors"
|
|
97
|
+
* - screen → "screen-0", "screen-1"
|
|
98
|
+
* - component Button → "component-button-0", "component-button-1"
|
|
99
|
+
* - layout stack → "layout-stack-0", "layout-stack-1"
|
|
100
|
+
* - cell → "cell-0", "cell-1"
|
|
101
|
+
* - component-definition → "define-MyButton"
|
|
102
|
+
*/
|
|
103
|
+
generateNodeId(type, metadata) {
|
|
104
|
+
switch (type) {
|
|
105
|
+
case "project":
|
|
106
|
+
return "project";
|
|
107
|
+
case "theme":
|
|
108
|
+
return "theme";
|
|
109
|
+
case "mocks":
|
|
110
|
+
return "mocks";
|
|
111
|
+
case "colors":
|
|
112
|
+
return "colors";
|
|
113
|
+
case "screen":
|
|
114
|
+
const screenIdx = this.counters.get("screen") || 0;
|
|
115
|
+
this.counters.set("screen", screenIdx + 1);
|
|
116
|
+
return `screen-${screenIdx}`;
|
|
117
|
+
case "component": {
|
|
118
|
+
const componentType = metadata?.componentType || "unknown";
|
|
119
|
+
const key = `component-${componentType.toLowerCase()}`;
|
|
120
|
+
const idx = this.counters.get(key) || 0;
|
|
121
|
+
this.counters.set(key, idx + 1);
|
|
122
|
+
return `${key}-${idx}`;
|
|
123
|
+
}
|
|
124
|
+
case "layout": {
|
|
125
|
+
const layoutType = metadata?.layoutType || "unknown";
|
|
126
|
+
const key = `layout-${layoutType.toLowerCase()}`;
|
|
127
|
+
const idx = this.counters.get(key) || 0;
|
|
128
|
+
this.counters.set(key, idx + 1);
|
|
129
|
+
return `${key}-${idx}`;
|
|
130
|
+
}
|
|
131
|
+
case "cell": {
|
|
132
|
+
const idx = this.counters.get("cell") || 0;
|
|
133
|
+
this.counters.set("cell", idx + 1);
|
|
134
|
+
return `cell-${idx}`;
|
|
135
|
+
}
|
|
136
|
+
case "component-definition":
|
|
137
|
+
return `define-${metadata?.name || "unknown"}`;
|
|
138
|
+
default:
|
|
139
|
+
return `${type}-0`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Add a property to an existing node in the SourceMap
|
|
144
|
+
* Captures precise ranges for property name and value for surgical editing
|
|
145
|
+
*
|
|
146
|
+
* @param nodeId - ID of the node that owns this property
|
|
147
|
+
* @param propertyName - Name of the property (e.g., "text", "direction")
|
|
148
|
+
* @param propertyValue - Parsed value of the property
|
|
149
|
+
* @param tokens - Captured tokens for the property
|
|
150
|
+
* @returns The PropertySourceMap entry created
|
|
151
|
+
*/
|
|
152
|
+
addProperty(nodeId, propertyName, propertyValue, tokens) {
|
|
153
|
+
const entry = this.entries.find((e) => e.nodeId === nodeId);
|
|
154
|
+
if (!entry) {
|
|
155
|
+
throw new Error(`Cannot add property to non-existent node: ${nodeId}`);
|
|
156
|
+
}
|
|
157
|
+
if (!entry.properties) {
|
|
158
|
+
entry.properties = {};
|
|
159
|
+
}
|
|
160
|
+
let nameRange;
|
|
161
|
+
let valueRange;
|
|
162
|
+
let fullRange;
|
|
163
|
+
if (tokens.name && tokens.value) {
|
|
164
|
+
nameRange = {
|
|
165
|
+
start: this.getTokenStart(tokens.name),
|
|
166
|
+
end: this.getTokenEnd(tokens.name)
|
|
167
|
+
};
|
|
168
|
+
valueRange = {
|
|
169
|
+
start: this.getTokenStart(tokens.value),
|
|
170
|
+
end: this.getTokenEnd(tokens.value)
|
|
171
|
+
};
|
|
172
|
+
fullRange = {
|
|
173
|
+
start: nameRange.start,
|
|
174
|
+
end: valueRange.end
|
|
175
|
+
};
|
|
176
|
+
} else if (tokens.full) {
|
|
177
|
+
fullRange = {
|
|
178
|
+
start: this.getTokenStart(tokens.full),
|
|
179
|
+
end: this.getTokenEnd(tokens.full)
|
|
180
|
+
};
|
|
181
|
+
nameRange = fullRange;
|
|
182
|
+
valueRange = fullRange;
|
|
183
|
+
} else {
|
|
184
|
+
throw new Error(`Invalid tokens for property ${propertyName}: need either name+value or full`);
|
|
185
|
+
}
|
|
186
|
+
const propertySourceMap = {
|
|
187
|
+
name: propertyName,
|
|
188
|
+
value: propertyValue,
|
|
189
|
+
range: fullRange,
|
|
190
|
+
nameRange,
|
|
191
|
+
valueRange
|
|
192
|
+
};
|
|
193
|
+
entry.properties[propertyName] = propertySourceMap;
|
|
194
|
+
return propertySourceMap;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Push a parent onto the stack (when entering a container node)
|
|
198
|
+
*/
|
|
199
|
+
pushParent(nodeId) {
|
|
200
|
+
this.parentStack.push(nodeId);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Pop a parent from the stack (when exiting a container node)
|
|
204
|
+
*/
|
|
205
|
+
popParent() {
|
|
206
|
+
this.parentStack.pop();
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get the current parent nodeId (or null if at root)
|
|
210
|
+
*/
|
|
211
|
+
getCurrentParent() {
|
|
212
|
+
return this.parentStack.length > 0 ? this.parentStack[this.parentStack.length - 1] : null;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Build and return the final SourceMap
|
|
216
|
+
*/
|
|
217
|
+
build() {
|
|
218
|
+
this.calculateAllInsertionPoints();
|
|
219
|
+
return this.entries;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Calculate insertionPoints for all container nodes
|
|
223
|
+
* Container nodes: project, screen, layout, cell, component-definition
|
|
224
|
+
*/
|
|
225
|
+
calculateAllInsertionPoints() {
|
|
226
|
+
const containerTypes = [
|
|
227
|
+
"project",
|
|
228
|
+
"screen",
|
|
229
|
+
"layout",
|
|
230
|
+
"cell",
|
|
231
|
+
"component-definition"
|
|
232
|
+
];
|
|
233
|
+
for (const entry of this.entries) {
|
|
234
|
+
if (containerTypes.includes(entry.type)) {
|
|
235
|
+
entry.insertionPoint = this.calculateInsertionPoint(entry.nodeId);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Calculate CodeRange from captured tokens
|
|
241
|
+
* Finds the earliest start and latest end among all tokens
|
|
242
|
+
*/
|
|
243
|
+
calculateRange(tokens) {
|
|
244
|
+
const positions = [];
|
|
245
|
+
if (tokens.keyword) {
|
|
246
|
+
positions.push(this.getTokenStart(tokens.keyword));
|
|
247
|
+
positions.push(this.getTokenEnd(tokens.keyword));
|
|
248
|
+
}
|
|
249
|
+
if (tokens.name) {
|
|
250
|
+
positions.push(this.getTokenStart(tokens.name));
|
|
251
|
+
positions.push(this.getTokenEnd(tokens.name));
|
|
252
|
+
}
|
|
253
|
+
if (tokens.paramList) {
|
|
254
|
+
positions.push(this.getTokenStart(tokens.paramList));
|
|
255
|
+
positions.push(this.getTokenEnd(tokens.paramList));
|
|
256
|
+
}
|
|
257
|
+
if (tokens.body) {
|
|
258
|
+
positions.push(this.getTokenStart(tokens.body));
|
|
259
|
+
positions.push(this.getTokenEnd(tokens.body));
|
|
260
|
+
}
|
|
261
|
+
if (tokens.properties && tokens.properties.length > 0) {
|
|
262
|
+
tokens.properties.forEach((prop) => {
|
|
263
|
+
positions.push(this.getTokenStart(prop));
|
|
264
|
+
positions.push(this.getTokenEnd(prop));
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (positions.length === 0) {
|
|
268
|
+
const fallbackToken = tokens.keyword || tokens.name;
|
|
269
|
+
return {
|
|
270
|
+
start: this.getTokenStart(fallbackToken),
|
|
271
|
+
end: this.getTokenEnd(fallbackToken)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
positions.sort((a, b) => {
|
|
275
|
+
if (a.line !== b.line) return a.line - b.line;
|
|
276
|
+
return a.column - b.column;
|
|
277
|
+
});
|
|
278
|
+
const start = positions[0];
|
|
279
|
+
const end = positions[positions.length - 1];
|
|
280
|
+
return { start, end };
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Convert a single token to CodeRange
|
|
284
|
+
*/
|
|
285
|
+
tokenToRange(token) {
|
|
286
|
+
return {
|
|
287
|
+
start: this.getTokenStart(token),
|
|
288
|
+
end: this.getTokenEnd(token)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Calculate body range from closing brace token
|
|
293
|
+
* Body range typically spans from opening brace to closing brace
|
|
294
|
+
*/
|
|
295
|
+
calculateBodyRange(closingBrace) {
|
|
296
|
+
return this.tokenToRange(closingBrace);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Extract the first real token from a CST node (earliest by offset)
|
|
300
|
+
* Recursively searches through children to find the token with smallest offset
|
|
301
|
+
*/
|
|
302
|
+
getFirstToken(cstNode) {
|
|
303
|
+
if (!cstNode?.children) {
|
|
304
|
+
return cstNode;
|
|
305
|
+
}
|
|
306
|
+
let earliestToken = null;
|
|
307
|
+
let earliestOffset = Infinity;
|
|
308
|
+
for (const childArray of Object.values(cstNode.children)) {
|
|
309
|
+
if (Array.isArray(childArray)) {
|
|
310
|
+
for (const child of childArray) {
|
|
311
|
+
if (!child) continue;
|
|
312
|
+
let token;
|
|
313
|
+
if (child.children) {
|
|
314
|
+
token = this.getFirstToken(child);
|
|
315
|
+
} else {
|
|
316
|
+
token = child;
|
|
317
|
+
}
|
|
318
|
+
if (token?.startOffset !== void 0 && token.startOffset < earliestOffset) {
|
|
319
|
+
earliestToken = token;
|
|
320
|
+
earliestOffset = token.startOffset;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return earliestToken;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Extract the last real token from a CST node (latest by offset)
|
|
329
|
+
* Recursively searches through children to find the token with largest offset
|
|
330
|
+
*/
|
|
331
|
+
getLastToken(cstNode) {
|
|
332
|
+
if (!cstNode?.children) {
|
|
333
|
+
return cstNode;
|
|
334
|
+
}
|
|
335
|
+
let latestToken = null;
|
|
336
|
+
let latestOffset = -1;
|
|
337
|
+
for (const childArray of Object.values(cstNode.children)) {
|
|
338
|
+
if (Array.isArray(childArray)) {
|
|
339
|
+
for (const child of childArray) {
|
|
340
|
+
if (!child) continue;
|
|
341
|
+
let token;
|
|
342
|
+
if (child.children) {
|
|
343
|
+
token = this.getLastToken(child);
|
|
344
|
+
} else {
|
|
345
|
+
token = child;
|
|
346
|
+
}
|
|
347
|
+
const tokenOffset = token?.endOffset ?? token?.startOffset;
|
|
348
|
+
if (tokenOffset !== void 0 && tokenOffset > latestOffset) {
|
|
349
|
+
latestToken = token;
|
|
350
|
+
latestOffset = tokenOffset;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return latestToken;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Extract start position from a Chevrotain token or CST node
|
|
359
|
+
*/
|
|
360
|
+
getTokenStart(token) {
|
|
361
|
+
if (token?.children) {
|
|
362
|
+
const firstToken = this.getFirstToken(token);
|
|
363
|
+
if (firstToken) {
|
|
364
|
+
return this.getTokenStart(firstToken);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
line: token.startLine || 1,
|
|
369
|
+
column: token.startColumn !== void 0 ? token.startColumn - 1 : 0,
|
|
370
|
+
// Chevrotain is 1-based, we want 0-based
|
|
371
|
+
offset: token.startOffset
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Extract end position from a Chevrotain token or CST node
|
|
376
|
+
*/
|
|
377
|
+
getTokenEnd(token) {
|
|
378
|
+
if (token?.children) {
|
|
379
|
+
const lastToken = this.getLastToken(token);
|
|
380
|
+
if (lastToken) {
|
|
381
|
+
return this.getTokenEnd(lastToken);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
line: token.endLine || token.startLine || 1,
|
|
386
|
+
column: token.endColumn !== void 0 ? token.endColumn : token.startColumn || 0,
|
|
387
|
+
// Chevrotain columns are 1-based
|
|
388
|
+
offset: token.endOffset
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Reset the builder (for reuse)
|
|
393
|
+
*/
|
|
394
|
+
reset(filePath = "<input>", sourceCode = "") {
|
|
395
|
+
this.entries = [];
|
|
396
|
+
this.filePath = filePath;
|
|
397
|
+
this.sourceCode = sourceCode;
|
|
398
|
+
this.parentStack = [];
|
|
399
|
+
this.counters.clear();
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Calculate insertion point for adding new children to a container node
|
|
403
|
+
*
|
|
404
|
+
* Strategy:
|
|
405
|
+
* - If node has children: insert after last child, preserve indentation
|
|
406
|
+
* - If node is empty: insert inside body, use parent indentation + 2 spaces
|
|
407
|
+
*
|
|
408
|
+
* @param nodeId - ID of the container node
|
|
409
|
+
* @returns InsertionPoint with line, column, indentation, and optional after
|
|
410
|
+
*/
|
|
411
|
+
calculateInsertionPoint(nodeId) {
|
|
412
|
+
const node = this.entries.find((e) => e.nodeId === nodeId);
|
|
413
|
+
if (!node) {
|
|
414
|
+
return void 0;
|
|
415
|
+
}
|
|
416
|
+
const children = this.entries.filter((e) => e.parentId === nodeId);
|
|
417
|
+
if (children.length > 0) {
|
|
418
|
+
const lastChild = children[children.length - 1];
|
|
419
|
+
const insertLine = lastChild.range.end.line;
|
|
420
|
+
const indentation2 = this.extractIndentation(lastChild.range.start.line);
|
|
421
|
+
return {
|
|
422
|
+
line: insertLine,
|
|
423
|
+
column: 0,
|
|
424
|
+
// Start of next line
|
|
425
|
+
indentation: indentation2,
|
|
426
|
+
after: lastChild.nodeId
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const bodyEndLine = node.range.end.line;
|
|
430
|
+
const parentIndentation = this.extractIndentation(node.range.start.line);
|
|
431
|
+
const indentation = parentIndentation + " ";
|
|
432
|
+
return {
|
|
433
|
+
line: bodyEndLine,
|
|
434
|
+
// Insert right before closing brace
|
|
435
|
+
column: 0,
|
|
436
|
+
indentation
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Extract indentation (leading whitespace) from a line
|
|
441
|
+
*/
|
|
442
|
+
extractIndentation(lineNumber) {
|
|
443
|
+
if (!this.sourceCode) {
|
|
444
|
+
return "";
|
|
445
|
+
}
|
|
446
|
+
const lines = this.sourceCode.split("\n");
|
|
447
|
+
if (lineNumber < 1 || lineNumber > lines.length) {
|
|
448
|
+
return "";
|
|
449
|
+
}
|
|
450
|
+
const line = lines[lineNumber - 1];
|
|
451
|
+
const match = line.match(/^(\s*)/);
|
|
452
|
+
return match ? match[1] : "";
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// src/parser/index.ts
|
|
39
457
|
var Project = (0, import_chevrotain.createToken)({ name: "Project", pattern: /project/ });
|
|
40
458
|
var Screen = (0, import_chevrotain.createToken)({ name: "Screen", pattern: /screen/ });
|
|
41
459
|
var Layout = (0, import_chevrotain.createToken)({ name: "Layout", pattern: /layout/ });
|
|
@@ -503,6 +921,420 @@ var WireDSLVisitor = class extends BaseCstVisitor {
|
|
|
503
921
|
return params;
|
|
504
922
|
}
|
|
505
923
|
};
|
|
924
|
+
var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
|
|
925
|
+
constructor(sourceMapBuilder) {
|
|
926
|
+
super();
|
|
927
|
+
this.definedComponentNames = /* @__PURE__ */ new Set();
|
|
928
|
+
this.sourceMapBuilder = sourceMapBuilder;
|
|
929
|
+
}
|
|
930
|
+
project(ctx) {
|
|
931
|
+
const projectName = ctx.projectName[0].image.slice(1, -1);
|
|
932
|
+
const theme = {};
|
|
933
|
+
const mocks = {};
|
|
934
|
+
const colors = {};
|
|
935
|
+
const definedComponents = [];
|
|
936
|
+
const screens = [];
|
|
937
|
+
const tokens = {
|
|
938
|
+
keyword: ctx.Project[0],
|
|
939
|
+
name: ctx.projectName[0],
|
|
940
|
+
body: ctx.RCurly[0]
|
|
941
|
+
};
|
|
942
|
+
const ast = {
|
|
943
|
+
type: "project",
|
|
944
|
+
name: projectName,
|
|
945
|
+
theme: {},
|
|
946
|
+
mocks: {},
|
|
947
|
+
colors: {},
|
|
948
|
+
definedComponents: [],
|
|
949
|
+
// Will be filled after push
|
|
950
|
+
screens: []
|
|
951
|
+
// Will be filled after push
|
|
952
|
+
};
|
|
953
|
+
if (this.sourceMapBuilder) {
|
|
954
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
955
|
+
"project",
|
|
956
|
+
tokens,
|
|
957
|
+
{ name: projectName }
|
|
958
|
+
);
|
|
959
|
+
ast._meta = { nodeId };
|
|
960
|
+
this.sourceMapBuilder.pushParent(nodeId);
|
|
961
|
+
}
|
|
962
|
+
if (ctx.themeDecl && ctx.themeDecl.length > 0) {
|
|
963
|
+
const themeBlock = this.visit(ctx.themeDecl[0]);
|
|
964
|
+
Object.assign(ast.theme, themeBlock);
|
|
965
|
+
}
|
|
966
|
+
if (ctx.mocksDecl && ctx.mocksDecl.length > 0) {
|
|
967
|
+
const mocksBlock = this.visit(ctx.mocksDecl[0]);
|
|
968
|
+
Object.assign(ast.mocks, mocksBlock);
|
|
969
|
+
}
|
|
970
|
+
if (ctx.colorsDecl && ctx.colorsDecl.length > 0) {
|
|
971
|
+
const colorsBlock = this.visit(ctx.colorsDecl[0]);
|
|
972
|
+
Object.assign(ast.colors, colorsBlock);
|
|
973
|
+
}
|
|
974
|
+
if (ctx.definedComponent) {
|
|
975
|
+
ctx.definedComponent.forEach((comp) => {
|
|
976
|
+
ast.definedComponents.push(this.visit(comp));
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
if (ctx.screen) {
|
|
980
|
+
ctx.screen.forEach((screen) => {
|
|
981
|
+
ast.screens.push(this.visit(screen));
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
return ast;
|
|
985
|
+
}
|
|
986
|
+
screen(ctx) {
|
|
987
|
+
const params = ctx.paramList ? this.visit(ctx.paramList[0]) : {};
|
|
988
|
+
const screenName = ctx.screenName[0].image;
|
|
989
|
+
const tokens = {
|
|
990
|
+
keyword: ctx.Screen[0],
|
|
991
|
+
name: ctx.screenName[0],
|
|
992
|
+
paramList: ctx.paramList?.[0],
|
|
993
|
+
body: ctx.RCurly[0]
|
|
994
|
+
};
|
|
995
|
+
const ast = {
|
|
996
|
+
type: "screen",
|
|
997
|
+
name: screenName,
|
|
998
|
+
params,
|
|
999
|
+
layout: {}
|
|
1000
|
+
// Will be filled after push
|
|
1001
|
+
};
|
|
1002
|
+
if (this.sourceMapBuilder) {
|
|
1003
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1004
|
+
"screen",
|
|
1005
|
+
tokens,
|
|
1006
|
+
{ name: screenName }
|
|
1007
|
+
);
|
|
1008
|
+
ast._meta = { nodeId };
|
|
1009
|
+
this.sourceMapBuilder.pushParent(nodeId);
|
|
1010
|
+
}
|
|
1011
|
+
ast.layout = this.visit(ctx.layout[0]);
|
|
1012
|
+
if (this.sourceMapBuilder) {
|
|
1013
|
+
this.sourceMapBuilder.popParent();
|
|
1014
|
+
}
|
|
1015
|
+
return ast;
|
|
1016
|
+
}
|
|
1017
|
+
layout(ctx) {
|
|
1018
|
+
const layoutType = ctx.layoutType[0].image;
|
|
1019
|
+
const params = {};
|
|
1020
|
+
if (ctx.paramList) {
|
|
1021
|
+
const paramResult = this.visit(ctx.paramList);
|
|
1022
|
+
Object.assign(params, paramResult);
|
|
1023
|
+
}
|
|
1024
|
+
const tokens = {
|
|
1025
|
+
keyword: ctx.Layout[0],
|
|
1026
|
+
name: ctx.layoutType[0],
|
|
1027
|
+
paramList: ctx.paramList?.[0],
|
|
1028
|
+
body: ctx.RCurly[0]
|
|
1029
|
+
};
|
|
1030
|
+
const ast = {
|
|
1031
|
+
type: "layout",
|
|
1032
|
+
layoutType,
|
|
1033
|
+
params,
|
|
1034
|
+
children: []
|
|
1035
|
+
// Will be filled after push
|
|
1036
|
+
};
|
|
1037
|
+
if (this.sourceMapBuilder) {
|
|
1038
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1039
|
+
"layout",
|
|
1040
|
+
tokens,
|
|
1041
|
+
{ layoutType }
|
|
1042
|
+
);
|
|
1043
|
+
ast._meta = { nodeId };
|
|
1044
|
+
if (ctx.paramList && ctx.paramList[0]?.children?.property) {
|
|
1045
|
+
ctx.paramList[0].children.property.forEach((propCtx) => {
|
|
1046
|
+
const propResult = this.visit(propCtx);
|
|
1047
|
+
this.sourceMapBuilder.addProperty(
|
|
1048
|
+
nodeId,
|
|
1049
|
+
propResult.key,
|
|
1050
|
+
propResult.value,
|
|
1051
|
+
{
|
|
1052
|
+
name: propCtx.children.propKey[0],
|
|
1053
|
+
value: propCtx.children.propValue[0]
|
|
1054
|
+
}
|
|
1055
|
+
);
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
this.sourceMapBuilder.pushParent(nodeId);
|
|
1059
|
+
}
|
|
1060
|
+
const childNodes = [];
|
|
1061
|
+
if (ctx.component) {
|
|
1062
|
+
ctx.component.forEach((comp) => {
|
|
1063
|
+
const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
|
|
1064
|
+
childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
if (ctx.layout) {
|
|
1068
|
+
ctx.layout.forEach((layout) => {
|
|
1069
|
+
const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
|
|
1070
|
+
childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
if (ctx.cell) {
|
|
1074
|
+
ctx.cell.forEach((cell) => {
|
|
1075
|
+
const startToken = cell.children?.Cell?.[0];
|
|
1076
|
+
childNodes.push({ type: "cell", node: cell, index: startToken.startOffset });
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
childNodes.sort((a, b) => a.index - b.index);
|
|
1080
|
+
childNodes.forEach((item) => {
|
|
1081
|
+
ast.children.push(this.visit(item.node));
|
|
1082
|
+
});
|
|
1083
|
+
if (this.sourceMapBuilder) {
|
|
1084
|
+
this.sourceMapBuilder.popParent();
|
|
1085
|
+
}
|
|
1086
|
+
return ast;
|
|
1087
|
+
}
|
|
1088
|
+
cell(ctx) {
|
|
1089
|
+
const props = {};
|
|
1090
|
+
if (ctx.property) {
|
|
1091
|
+
ctx.property.forEach((prop) => {
|
|
1092
|
+
const result = this.visit(prop);
|
|
1093
|
+
props[result.key] = result.value;
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
const tokens = {
|
|
1097
|
+
keyword: ctx.Cell[0],
|
|
1098
|
+
properties: ctx.property || [],
|
|
1099
|
+
body: ctx.RCurly[0]
|
|
1100
|
+
};
|
|
1101
|
+
const ast = {
|
|
1102
|
+
type: "cell",
|
|
1103
|
+
props,
|
|
1104
|
+
children: []
|
|
1105
|
+
// Will be filled after push
|
|
1106
|
+
};
|
|
1107
|
+
if (this.sourceMapBuilder) {
|
|
1108
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1109
|
+
"cell",
|
|
1110
|
+
tokens
|
|
1111
|
+
);
|
|
1112
|
+
ast._meta = { nodeId };
|
|
1113
|
+
if (ctx.property) {
|
|
1114
|
+
ctx.property.forEach((propCtx) => {
|
|
1115
|
+
const propResult = this.visit(propCtx);
|
|
1116
|
+
this.sourceMapBuilder.addProperty(
|
|
1117
|
+
nodeId,
|
|
1118
|
+
propResult.key,
|
|
1119
|
+
propResult.value,
|
|
1120
|
+
{
|
|
1121
|
+
name: propCtx.children.propKey[0],
|
|
1122
|
+
value: propCtx.children.propValue[0]
|
|
1123
|
+
}
|
|
1124
|
+
);
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
this.sourceMapBuilder.pushParent(nodeId);
|
|
1128
|
+
}
|
|
1129
|
+
const childNodes = [];
|
|
1130
|
+
if (ctx.component) {
|
|
1131
|
+
ctx.component.forEach((comp) => {
|
|
1132
|
+
const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
|
|
1133
|
+
childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
if (ctx.layout) {
|
|
1137
|
+
ctx.layout.forEach((layout) => {
|
|
1138
|
+
const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
|
|
1139
|
+
childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
childNodes.sort((a, b) => a.index - b.index);
|
|
1143
|
+
childNodes.forEach((item) => {
|
|
1144
|
+
ast.children.push(this.visit(item.node));
|
|
1145
|
+
});
|
|
1146
|
+
if (this.sourceMapBuilder) {
|
|
1147
|
+
this.sourceMapBuilder.popParent();
|
|
1148
|
+
}
|
|
1149
|
+
return ast;
|
|
1150
|
+
}
|
|
1151
|
+
component(ctx) {
|
|
1152
|
+
const tokens = {
|
|
1153
|
+
keyword: ctx.Component[0],
|
|
1154
|
+
name: ctx.componentType[0],
|
|
1155
|
+
properties: ctx.property || []
|
|
1156
|
+
};
|
|
1157
|
+
const ast = super.component(ctx);
|
|
1158
|
+
if (this.sourceMapBuilder) {
|
|
1159
|
+
const isUserDefined = this.definedComponentNames.has(ast.componentType);
|
|
1160
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1161
|
+
"component",
|
|
1162
|
+
tokens,
|
|
1163
|
+
{
|
|
1164
|
+
componentType: ast.componentType,
|
|
1165
|
+
isUserDefined
|
|
1166
|
+
}
|
|
1167
|
+
);
|
|
1168
|
+
ast._meta = { nodeId };
|
|
1169
|
+
if (ctx.property) {
|
|
1170
|
+
ctx.property.forEach((propCtx) => {
|
|
1171
|
+
const propResult = this.visit(propCtx);
|
|
1172
|
+
this.sourceMapBuilder.addProperty(
|
|
1173
|
+
nodeId,
|
|
1174
|
+
propResult.key,
|
|
1175
|
+
propResult.value,
|
|
1176
|
+
{
|
|
1177
|
+
name: propCtx.children.propKey[0],
|
|
1178
|
+
value: propCtx.children.propValue[0]
|
|
1179
|
+
}
|
|
1180
|
+
);
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return ast;
|
|
1185
|
+
}
|
|
1186
|
+
definedComponent(ctx) {
|
|
1187
|
+
const name = ctx.componentName[0].image.slice(1, -1);
|
|
1188
|
+
this.definedComponentNames.add(name);
|
|
1189
|
+
const tokens = {
|
|
1190
|
+
keyword: ctx.Define[0],
|
|
1191
|
+
name: ctx.componentName[0],
|
|
1192
|
+
body: ctx.RCurly[0]
|
|
1193
|
+
};
|
|
1194
|
+
let body;
|
|
1195
|
+
const ast = {
|
|
1196
|
+
type: "definedComponent",
|
|
1197
|
+
name,
|
|
1198
|
+
body: {}
|
|
1199
|
+
// Will be filled after push
|
|
1200
|
+
};
|
|
1201
|
+
if (this.sourceMapBuilder) {
|
|
1202
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1203
|
+
"component-definition",
|
|
1204
|
+
tokens,
|
|
1205
|
+
{ name }
|
|
1206
|
+
);
|
|
1207
|
+
ast._meta = { nodeId };
|
|
1208
|
+
this.sourceMapBuilder.pushParent(nodeId);
|
|
1209
|
+
}
|
|
1210
|
+
if (ctx.layout && ctx.layout.length > 0) {
|
|
1211
|
+
body = this.visit(ctx.layout[0]);
|
|
1212
|
+
} else if (ctx.component && ctx.component.length > 0) {
|
|
1213
|
+
body = this.visit(ctx.component[0]);
|
|
1214
|
+
} else {
|
|
1215
|
+
throw new Error(`Defined component "${name}" must contain either a layout or component`);
|
|
1216
|
+
}
|
|
1217
|
+
ast.body = body;
|
|
1218
|
+
if (this.sourceMapBuilder) {
|
|
1219
|
+
this.sourceMapBuilder.popParent();
|
|
1220
|
+
}
|
|
1221
|
+
return ast;
|
|
1222
|
+
}
|
|
1223
|
+
// Override themeDecl to capture theme block in SourceMap
|
|
1224
|
+
themeDecl(ctx) {
|
|
1225
|
+
const theme = {};
|
|
1226
|
+
const tokens = {
|
|
1227
|
+
keyword: ctx.Theme[0],
|
|
1228
|
+
body: ctx.RCurly[0]
|
|
1229
|
+
};
|
|
1230
|
+
if (this.sourceMapBuilder) {
|
|
1231
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1232
|
+
"theme",
|
|
1233
|
+
tokens,
|
|
1234
|
+
{ name: "theme" }
|
|
1235
|
+
);
|
|
1236
|
+
if (ctx.themeProperty) {
|
|
1237
|
+
ctx.themeProperty.forEach((propCtx) => {
|
|
1238
|
+
const { key, value } = this.visit(propCtx);
|
|
1239
|
+
theme[key] = value;
|
|
1240
|
+
this.sourceMapBuilder.addProperty(
|
|
1241
|
+
nodeId,
|
|
1242
|
+
key,
|
|
1243
|
+
value,
|
|
1244
|
+
{
|
|
1245
|
+
name: propCtx.children.themeKey[0],
|
|
1246
|
+
value: propCtx.children.themeValue[0]
|
|
1247
|
+
}
|
|
1248
|
+
);
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
} else {
|
|
1252
|
+
if (ctx.themeProperty) {
|
|
1253
|
+
ctx.themeProperty.forEach((prop) => {
|
|
1254
|
+
const { key, value } = this.visit(prop);
|
|
1255
|
+
theme[key] = value;
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return theme;
|
|
1260
|
+
}
|
|
1261
|
+
// Override mocksDecl to capture mocks block in SourceMap
|
|
1262
|
+
mocksDecl(ctx) {
|
|
1263
|
+
const mocks = {};
|
|
1264
|
+
const tokens = {
|
|
1265
|
+
keyword: ctx.Mocks[0],
|
|
1266
|
+
body: ctx.RCurly[0]
|
|
1267
|
+
};
|
|
1268
|
+
if (this.sourceMapBuilder) {
|
|
1269
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1270
|
+
"mocks",
|
|
1271
|
+
tokens,
|
|
1272
|
+
{ name: "mocks" }
|
|
1273
|
+
);
|
|
1274
|
+
if (ctx.mockEntry) {
|
|
1275
|
+
ctx.mockEntry.forEach((entryCtx) => {
|
|
1276
|
+
const { key, value } = this.visit(entryCtx);
|
|
1277
|
+
mocks[key] = value;
|
|
1278
|
+
this.sourceMapBuilder.addProperty(
|
|
1279
|
+
nodeId,
|
|
1280
|
+
key,
|
|
1281
|
+
value,
|
|
1282
|
+
{
|
|
1283
|
+
name: entryCtx.children.mockKey[0],
|
|
1284
|
+
value: entryCtx.children.mockValue[0]
|
|
1285
|
+
}
|
|
1286
|
+
);
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
} else {
|
|
1290
|
+
if (ctx.mockEntry) {
|
|
1291
|
+
ctx.mockEntry.forEach((entry) => {
|
|
1292
|
+
const { key, value } = this.visit(entry);
|
|
1293
|
+
mocks[key] = value;
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return mocks;
|
|
1298
|
+
}
|
|
1299
|
+
// Override colorsDecl to capture colors block in SourceMap
|
|
1300
|
+
colorsDecl(ctx) {
|
|
1301
|
+
const colors = {};
|
|
1302
|
+
const tokens = {
|
|
1303
|
+
keyword: ctx.Colors[0],
|
|
1304
|
+
body: ctx.RCurly[0]
|
|
1305
|
+
};
|
|
1306
|
+
if (this.sourceMapBuilder) {
|
|
1307
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1308
|
+
"colors",
|
|
1309
|
+
tokens,
|
|
1310
|
+
{ name: "colors" }
|
|
1311
|
+
);
|
|
1312
|
+
if (ctx.colorEntry) {
|
|
1313
|
+
ctx.colorEntry.forEach((entryCtx) => {
|
|
1314
|
+
const { key, value } = this.visit(entryCtx);
|
|
1315
|
+
colors[key] = value;
|
|
1316
|
+
this.sourceMapBuilder.addProperty(
|
|
1317
|
+
nodeId,
|
|
1318
|
+
key,
|
|
1319
|
+
value,
|
|
1320
|
+
{
|
|
1321
|
+
name: entryCtx.children.colorKey[0],
|
|
1322
|
+
value: entryCtx.children.colorValue[0]
|
|
1323
|
+
}
|
|
1324
|
+
);
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
} else {
|
|
1328
|
+
if (ctx.colorEntry) {
|
|
1329
|
+
ctx.colorEntry.forEach((entry) => {
|
|
1330
|
+
const { key, value } = this.visit(entry);
|
|
1331
|
+
colors[key] = value;
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return colors;
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
506
1338
|
var visitor = new WireDSLVisitor();
|
|
507
1339
|
function parseWireDSL(input) {
|
|
508
1340
|
const lexResult = WireDSLLexer.tokenize(input);
|
|
@@ -520,6 +1352,30 @@ ${parserInstance.errors.map((e) => e.message).join("\n")}`);
|
|
|
520
1352
|
validateComponentDefinitionCycles(ast);
|
|
521
1353
|
return ast;
|
|
522
1354
|
}
|
|
1355
|
+
function parseWireDSLWithSourceMap(input, filePath = "<input>") {
|
|
1356
|
+
const lexResult = WireDSLLexer.tokenize(input);
|
|
1357
|
+
if (lexResult.errors.length > 0) {
|
|
1358
|
+
throw new Error(`Lexer errors:
|
|
1359
|
+
${lexResult.errors.map((e) => e.message).join("\n")}`);
|
|
1360
|
+
}
|
|
1361
|
+
parserInstance.input = lexResult.tokens;
|
|
1362
|
+
const cst = parserInstance.project();
|
|
1363
|
+
if (parserInstance.errors.length > 0) {
|
|
1364
|
+
throw new Error(`Parser errors:
|
|
1365
|
+
${parserInstance.errors.map((e) => e.message).join("\n")}`);
|
|
1366
|
+
}
|
|
1367
|
+
const sourceMapBuilder = new SourceMapBuilder(filePath, input);
|
|
1368
|
+
const visitorWithSourceMap = new WireDSLVisitorWithSourceMap(sourceMapBuilder);
|
|
1369
|
+
const ast = visitorWithSourceMap.visit(cst);
|
|
1370
|
+
validateComponentDefinitionCycles(ast);
|
|
1371
|
+
const sourceMap = sourceMapBuilder.build();
|
|
1372
|
+
return {
|
|
1373
|
+
ast,
|
|
1374
|
+
sourceMap,
|
|
1375
|
+
errors: []
|
|
1376
|
+
// No errors if we got here (errors throw exceptions)
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
523
1379
|
function validateComponentDefinitionCycles(ast) {
|
|
524
1380
|
if (!ast.definedComponents || ast.definedComponents.length === 0) {
|
|
525
1381
|
return;
|
|
@@ -620,7 +1476,8 @@ var IRStyleSchema = import_zod.z.object({
|
|
|
620
1476
|
background: import_zod.z.string().optional()
|
|
621
1477
|
});
|
|
622
1478
|
var IRMetaSchema = import_zod.z.object({
|
|
623
|
-
source: import_zod.z.string().optional()
|
|
1479
|
+
source: import_zod.z.string().optional(),
|
|
1480
|
+
nodeId: import_zod.z.string().optional()
|
|
624
1481
|
});
|
|
625
1482
|
var IRContainerNodeSchema = import_zod.z.object({
|
|
626
1483
|
id: import_zod.z.string(),
|
|
@@ -866,7 +1723,10 @@ Define these components with: define Component "Name" { ... }`
|
|
|
866
1723
|
params: this.cleanParams(layout.params),
|
|
867
1724
|
children: childRefs,
|
|
868
1725
|
style,
|
|
869
|
-
meta: {
|
|
1726
|
+
meta: {
|
|
1727
|
+
nodeId: layout._meta?.nodeId
|
|
1728
|
+
// Pass SourceMap nodeId from AST
|
|
1729
|
+
}
|
|
870
1730
|
};
|
|
871
1731
|
this.nodes[nodeId] = containerNode;
|
|
872
1732
|
return nodeId;
|
|
@@ -892,7 +1752,11 @@ Define these components with: define Component "Name" { ... }`
|
|
|
892
1752
|
children: childRefs,
|
|
893
1753
|
style: { padding: "none" },
|
|
894
1754
|
// Cells have no padding by default - grid gap handles spacing
|
|
895
|
-
meta: {
|
|
1755
|
+
meta: {
|
|
1756
|
+
source: "cell",
|
|
1757
|
+
nodeId: cell._meta?.nodeId
|
|
1758
|
+
// Pass SourceMap nodeId from AST
|
|
1759
|
+
}
|
|
896
1760
|
};
|
|
897
1761
|
this.nodes[nodeId] = containerNode;
|
|
898
1762
|
return nodeId;
|
|
@@ -944,7 +1808,10 @@ Define these components with: define Component "Name" { ... }`
|
|
|
944
1808
|
componentType: component.componentType,
|
|
945
1809
|
props: component.props,
|
|
946
1810
|
style: {},
|
|
947
|
-
meta: {
|
|
1811
|
+
meta: {
|
|
1812
|
+
nodeId: component._meta?.nodeId
|
|
1813
|
+
// Pass SourceMap nodeId from AST
|
|
1814
|
+
}
|
|
948
1815
|
};
|
|
949
1816
|
this.nodes[nodeId] = componentNode;
|
|
950
1817
|
return nodeId;
|
|
@@ -1897,15 +2764,30 @@ var SVGRenderer = class {
|
|
|
1897
2764
|
if (!node || !pos) return;
|
|
1898
2765
|
this.renderedNodeIds.add(nodeId);
|
|
1899
2766
|
if (node.kind === "container") {
|
|
2767
|
+
const containerGroup = [];
|
|
2768
|
+
const hasNodeId = node.meta?.nodeId;
|
|
2769
|
+
if (hasNodeId) {
|
|
2770
|
+
containerGroup.push(`<g${this.getDataNodeId(node)}>`);
|
|
2771
|
+
}
|
|
2772
|
+
const needsClickableArea = hasNodeId && node.containerType !== "panel" && node.containerType !== "card";
|
|
2773
|
+
if (needsClickableArea) {
|
|
2774
|
+
containerGroup.push(
|
|
2775
|
+
`<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="transparent" stroke="none" pointer-events="all"/>`
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
1900
2778
|
if (node.containerType === "panel") {
|
|
1901
|
-
this.renderPanelBorder(node, pos,
|
|
2779
|
+
this.renderPanelBorder(node, pos, containerGroup);
|
|
1902
2780
|
}
|
|
1903
2781
|
if (node.containerType === "card") {
|
|
1904
|
-
this.renderCardBorder(node, pos,
|
|
2782
|
+
this.renderCardBorder(node, pos, containerGroup);
|
|
1905
2783
|
}
|
|
1906
2784
|
node.children.forEach((childRef) => {
|
|
1907
|
-
this.renderNode(childRef.ref,
|
|
2785
|
+
this.renderNode(childRef.ref, containerGroup);
|
|
1908
2786
|
});
|
|
2787
|
+
if (hasNodeId) {
|
|
2788
|
+
containerGroup.push("</g>");
|
|
2789
|
+
}
|
|
2790
|
+
output.push(...containerGroup);
|
|
1909
2791
|
} else if (node.kind === "component") {
|
|
1910
2792
|
const componentSvg = this.renderComponent(node, pos);
|
|
1911
2793
|
if (componentSvg) {
|
|
@@ -1982,7 +2864,7 @@ var SVGRenderer = class {
|
|
|
1982
2864
|
renderHeading(node, pos) {
|
|
1983
2865
|
const text = String(node.props.text || "Heading");
|
|
1984
2866
|
const fontSize = 20;
|
|
1985
|
-
return `<g>
|
|
2867
|
+
return `<g${this.getDataNodeId(node)}>
|
|
1986
2868
|
<text x="${pos.x}" y="${pos.y + pos.height / 2 + 6}"
|
|
1987
2869
|
font-family="system-ui, -apple-system, sans-serif"
|
|
1988
2870
|
font-size="${fontSize}"
|
|
@@ -2000,7 +2882,7 @@ var SVGRenderer = class {
|
|
|
2000
2882
|
const fontSizeMap = { "sm": 12, "md": 14, "lg": 16 };
|
|
2001
2883
|
const fontSize = fontSizeMap[size] || 14;
|
|
2002
2884
|
const buttonWidth = Math.max(pos.width, 60);
|
|
2003
|
-
return `<g>
|
|
2885
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2004
2886
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2005
2887
|
width="${buttonWidth}" height="${pos.height}"
|
|
2006
2888
|
rx="6"
|
|
@@ -2018,7 +2900,7 @@ var SVGRenderer = class {
|
|
|
2018
2900
|
renderInput(node, pos) {
|
|
2019
2901
|
const label = String(node.props.label || "");
|
|
2020
2902
|
const placeholder = String(node.props.placeholder || "");
|
|
2021
|
-
return `<g>
|
|
2903
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2022
2904
|
${label ? `<text x="${pos.x + 8}" y="${pos.y - 6}"
|
|
2023
2905
|
font-family="system-ui, -apple-system, sans-serif"
|
|
2024
2906
|
font-size="12"
|
|
@@ -2050,7 +2932,7 @@ var SVGRenderer = class {
|
|
|
2050
2932
|
} else {
|
|
2051
2933
|
titleY = pos.y + pos.height / 2 + titleLineHeight / 2 - 4;
|
|
2052
2934
|
}
|
|
2053
|
-
let svg = `<g>
|
|
2935
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
2054
2936
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2055
2937
|
width="${pos.width}" height="${pos.height}"
|
|
2056
2938
|
fill="${this.renderTheme.cardBg}"
|
|
@@ -2183,7 +3065,7 @@ var SVGRenderer = class {
|
|
|
2183
3065
|
});
|
|
2184
3066
|
mockRows.push(row);
|
|
2185
3067
|
}
|
|
2186
|
-
let svg = `<g>
|
|
3068
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
2187
3069
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2188
3070
|
width="${pos.width}" height="${pos.height}"
|
|
2189
3071
|
rx="8"
|
|
@@ -2287,7 +3169,7 @@ var SVGRenderer = class {
|
|
|
2287
3169
|
}
|
|
2288
3170
|
renderChartPlaceholder(node, pos) {
|
|
2289
3171
|
const type = String(node.props.type || "bar");
|
|
2290
|
-
return `<g>
|
|
3172
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2291
3173
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2292
3174
|
width="${pos.width}" height="${pos.height}"
|
|
2293
3175
|
rx="8"
|
|
@@ -2307,7 +3189,7 @@ var SVGRenderer = class {
|
|
|
2307
3189
|
renderText(node, pos) {
|
|
2308
3190
|
const text = String(node.props.content || "Text content");
|
|
2309
3191
|
const fontSize = 14;
|
|
2310
|
-
return `<g>
|
|
3192
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2311
3193
|
<text x="${pos.x}" y="${pos.y + 16}"
|
|
2312
3194
|
font-family="system-ui, -apple-system, sans-serif"
|
|
2313
3195
|
font-size="${fontSize}"
|
|
@@ -2316,7 +3198,7 @@ var SVGRenderer = class {
|
|
|
2316
3198
|
}
|
|
2317
3199
|
renderLabel(node, pos) {
|
|
2318
3200
|
const text = String(node.props.text || "Label");
|
|
2319
|
-
return `<g>
|
|
3201
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2320
3202
|
<text x="${pos.x}" y="${pos.y + 12}"
|
|
2321
3203
|
font-family="system-ui, -apple-system, sans-serif"
|
|
2322
3204
|
font-size="12"
|
|
@@ -2325,7 +3207,7 @@ var SVGRenderer = class {
|
|
|
2325
3207
|
}
|
|
2326
3208
|
renderCode(node, pos) {
|
|
2327
3209
|
const code = String(node.props.code || "const x = 42;");
|
|
2328
|
-
return `<g>
|
|
3210
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2329
3211
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2330
3212
|
width="${pos.width}" height="${pos.height}"
|
|
2331
3213
|
rx="4"
|
|
@@ -2344,7 +3226,7 @@ var SVGRenderer = class {
|
|
|
2344
3226
|
renderTextarea(node, pos) {
|
|
2345
3227
|
const label = String(node.props.label || "");
|
|
2346
3228
|
const placeholder = String(node.props.placeholder || "Enter text...");
|
|
2347
|
-
return `<g>
|
|
3229
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2348
3230
|
${label ? `<text x="${pos.x}" y="${pos.y - 6}"
|
|
2349
3231
|
font-family="system-ui, -apple-system, sans-serif"
|
|
2350
3232
|
font-size="12"
|
|
@@ -2364,7 +3246,7 @@ var SVGRenderer = class {
|
|
|
2364
3246
|
renderSelect(node, pos) {
|
|
2365
3247
|
const label = String(node.props.label || "");
|
|
2366
3248
|
const placeholder = String(node.props.placeholder || "Select...");
|
|
2367
|
-
return `<g>
|
|
3249
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2368
3250
|
${label ? `<text x="${pos.x}" y="${pos.y - 6}"
|
|
2369
3251
|
font-family="system-ui, -apple-system, sans-serif"
|
|
2370
3252
|
font-size="12"
|
|
@@ -2390,7 +3272,7 @@ var SVGRenderer = class {
|
|
|
2390
3272
|
const checked = String(node.props.checked || "false").toLowerCase() === "true";
|
|
2391
3273
|
const checkboxSize = 18;
|
|
2392
3274
|
const checkboxY = pos.y + pos.height / 2 - checkboxSize / 2;
|
|
2393
|
-
return `<g>
|
|
3275
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2394
3276
|
<rect x="${pos.x}" y="${checkboxY}"
|
|
2395
3277
|
width="${checkboxSize}" height="${checkboxSize}"
|
|
2396
3278
|
rx="4"
|
|
@@ -2413,7 +3295,7 @@ var SVGRenderer = class {
|
|
|
2413
3295
|
const checked = String(node.props.checked || "false").toLowerCase() === "true";
|
|
2414
3296
|
const radioSize = 16;
|
|
2415
3297
|
const radioY = pos.y + pos.height / 2 - radioSize / 2;
|
|
2416
|
-
return `<g>
|
|
3298
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2417
3299
|
<circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
|
|
2418
3300
|
r="${radioSize / 2}"
|
|
2419
3301
|
fill="${this.renderTheme.cardBg}"
|
|
@@ -2434,7 +3316,7 @@ var SVGRenderer = class {
|
|
|
2434
3316
|
const toggleWidth = 40;
|
|
2435
3317
|
const toggleHeight = 20;
|
|
2436
3318
|
const toggleY = pos.y + pos.height / 2 - toggleHeight / 2;
|
|
2437
|
-
return `<g>
|
|
3319
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2438
3320
|
<rect x="${pos.x}" y="${toggleY}"
|
|
2439
3321
|
width="${toggleWidth}" height="${toggleHeight}"
|
|
2440
3322
|
rx="10"
|
|
@@ -2466,7 +3348,7 @@ var SVGRenderer = class {
|
|
|
2466
3348
|
const itemHeight = 40;
|
|
2467
3349
|
const padding = 16;
|
|
2468
3350
|
const titleHeight = 40;
|
|
2469
|
-
let svg = `<g>
|
|
3351
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
2470
3352
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2471
3353
|
width="${pos.width}" height="${pos.height}"
|
|
2472
3354
|
fill="${this.renderTheme.cardBg}"
|
|
@@ -2501,7 +3383,7 @@ var SVGRenderer = class {
|
|
|
2501
3383
|
const itemsStr = String(node.props.items || "");
|
|
2502
3384
|
const tabs = itemsStr ? itemsStr.split(",").map((t) => t.trim()) : ["Tab 1", "Tab 2", "Tab 3"];
|
|
2503
3385
|
const tabWidth = pos.width / tabs.length;
|
|
2504
|
-
let svg = `<g>
|
|
3386
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
2505
3387
|
<!-- Tab headers -->`;
|
|
2506
3388
|
tabs.forEach((tab, i) => {
|
|
2507
3389
|
const tabX = pos.x + i * tabWidth;
|
|
@@ -2530,7 +3412,7 @@ var SVGRenderer = class {
|
|
|
2530
3412
|
return svg;
|
|
2531
3413
|
}
|
|
2532
3414
|
renderDivider(node, pos) {
|
|
2533
|
-
return `<g>
|
|
3415
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2534
3416
|
<line x1="${pos.x}" y1="${pos.y + pos.height / 2}"
|
|
2535
3417
|
x2="${pos.x + pos.width}" y2="${pos.y + pos.height / 2}"
|
|
2536
3418
|
stroke="${this.renderTheme.border}"
|
|
@@ -2550,7 +3432,7 @@ var SVGRenderer = class {
|
|
|
2550
3432
|
success: "#10B981"
|
|
2551
3433
|
};
|
|
2552
3434
|
const bgColor = typeColors[type] || typeColors.info;
|
|
2553
|
-
return `<g>
|
|
3435
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2554
3436
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2555
3437
|
width="${pos.width}" height="${pos.height}"
|
|
2556
3438
|
rx="6"
|
|
@@ -2572,7 +3454,7 @@ var SVGRenderer = class {
|
|
|
2572
3454
|
const variant = String(node.props.variant || "default");
|
|
2573
3455
|
const bgColor = variant === "primary" ? this.renderTheme.primary : this.renderTheme.border;
|
|
2574
3456
|
const textColor = variant === "primary" ? "white" : this.renderTheme.text;
|
|
2575
|
-
return `<g>
|
|
3457
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2576
3458
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2577
3459
|
width="${pos.width}" height="${pos.height}"
|
|
2578
3460
|
rx="${pos.height / 2}"
|
|
@@ -2593,7 +3475,7 @@ var SVGRenderer = class {
|
|
|
2593
3475
|
const overlayHeight = Math.max(this.options.height, this.calculateContentHeight());
|
|
2594
3476
|
const modalX = (this.options.width - pos.width) / 2;
|
|
2595
3477
|
const modalY = Math.max(40, (overlayHeight - pos.height) / 2);
|
|
2596
|
-
return `<g>
|
|
3478
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2597
3479
|
<!-- Modal backdrop -->
|
|
2598
3480
|
<rect x="0" y="0"
|
|
2599
3481
|
width="${this.options.width}" height="${overlayHeight}"
|
|
@@ -2646,7 +3528,7 @@ var SVGRenderer = class {
|
|
|
2646
3528
|
const padding = 12;
|
|
2647
3529
|
const itemHeight = 36;
|
|
2648
3530
|
const titleHeight = title ? 40 : 0;
|
|
2649
|
-
let svg = `<g>
|
|
3531
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
2650
3532
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2651
3533
|
width="${pos.width}" height="${pos.height}"
|
|
2652
3534
|
rx="8"
|
|
@@ -2681,7 +3563,7 @@ var SVGRenderer = class {
|
|
|
2681
3563
|
return svg;
|
|
2682
3564
|
}
|
|
2683
3565
|
renderGenericComponent(node, pos) {
|
|
2684
|
-
return `<g>
|
|
3566
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2685
3567
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2686
3568
|
width="${pos.width}" height="${pos.height}"
|
|
2687
3569
|
rx="4"
|
|
@@ -2715,7 +3597,7 @@ var SVGRenderer = class {
|
|
|
2715
3597
|
const titleY = innerY + topGap + titleSize;
|
|
2716
3598
|
const valueY = titleY + valueGap + valueSize;
|
|
2717
3599
|
const captionY = valueY + captionGap + captionSize;
|
|
2718
|
-
let svg = `<g>
|
|
3600
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
2719
3601
|
<!-- StatCard Background -->
|
|
2720
3602
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2721
3603
|
width="${pos.width}" height="${pos.height}"
|
|
@@ -2769,7 +3651,7 @@ var SVGRenderer = class {
|
|
|
2769
3651
|
const offsetX = pos.x + (pos.width - iconWidth) / 2;
|
|
2770
3652
|
const offsetY = pos.y + (pos.height - iconHeight) / 2;
|
|
2771
3653
|
let svgContent = "";
|
|
2772
|
-
let svg = `<g>
|
|
3654
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
2773
3655
|
<!-- Image Background -->
|
|
2774
3656
|
<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="#E8E8E8"/>`;
|
|
2775
3657
|
if (["landscape", "portrait", "square"].includes(placeholder)) {
|
|
@@ -2842,7 +3724,7 @@ var SVGRenderer = class {
|
|
|
2842
3724
|
const separatorWidth = 20;
|
|
2843
3725
|
const itemSpacing = 8;
|
|
2844
3726
|
let currentX = pos.x;
|
|
2845
|
-
let svg =
|
|
3727
|
+
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
2846
3728
|
items.forEach((item, index) => {
|
|
2847
3729
|
const isLast = index === items.length - 1;
|
|
2848
3730
|
const textColor = isLast ? this.renderTheme.text : this.renderTheme.textMuted;
|
|
@@ -2875,7 +3757,7 @@ var SVGRenderer = class {
|
|
|
2875
3757
|
const itemHeight = 40;
|
|
2876
3758
|
const fontSize = 14;
|
|
2877
3759
|
const activeIndex = Number(node.props.active || 0);
|
|
2878
|
-
let svg =
|
|
3760
|
+
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
2879
3761
|
items.forEach((item, index) => {
|
|
2880
3762
|
const itemY = pos.y + index * itemHeight;
|
|
2881
3763
|
const isActive = index === activeIndex;
|
|
@@ -2919,7 +3801,7 @@ var SVGRenderer = class {
|
|
|
2919
3801
|
const size = String(node.props.size || "md");
|
|
2920
3802
|
const iconSvg = getIcon(iconType);
|
|
2921
3803
|
if (!iconSvg) {
|
|
2922
|
-
return `<g>
|
|
3804
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2923
3805
|
<!-- Icon not found: ${iconType} -->
|
|
2924
3806
|
<circle cx="${pos.x + pos.width / 2}" cy="${pos.y + pos.height / 2}" r="${Math.min(pos.width, pos.height) / 2 - 2}" fill="none" stroke="rgba(100, 116, 139, 0.4)" stroke-width="1"/>
|
|
2925
3807
|
<text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="rgba(100, 116, 139, 0.6)" text-anchor="middle">?</text>
|
|
@@ -2930,7 +3812,7 @@ var SVGRenderer = class {
|
|
|
2930
3812
|
const iconColor = "rgba(30, 41, 59, 0.75)";
|
|
2931
3813
|
const offsetX = pos.x + (pos.width - iconSize) / 2;
|
|
2932
3814
|
const offsetY = pos.y + (pos.height - iconSize) / 2;
|
|
2933
|
-
const wrappedSvg = `<g transform="translate(${offsetX}, ${offsetY})">
|
|
3815
|
+
const wrappedSvg = `<g${this.getDataNodeId(node)} transform="translate(${offsetX}, ${offsetY})">
|
|
2934
3816
|
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2935
3817
|
${this.extractSvgContent(iconSvg)}
|
|
2936
3818
|
</svg>
|
|
@@ -2965,7 +3847,7 @@ var SVGRenderer = class {
|
|
|
2965
3847
|
const sizeMap = { "sm": 28, "md": 32, "lg": 40 };
|
|
2966
3848
|
const buttonSize = sizeMap[size] || 32;
|
|
2967
3849
|
const radius = 6;
|
|
2968
|
-
let svg = `<g opacity="${opacity}">
|
|
3850
|
+
let svg = `<g${this.getDataNodeId(node)} opacity="${opacity}">
|
|
2969
3851
|
<!-- IconButton background -->
|
|
2970
3852
|
<rect x="${pos.x}" y="${pos.y}" width="${buttonSize}" height="${buttonSize}" rx="${radius}" fill="${bgColor}" stroke="${borderColor}" stroke-width="1"/>`;
|
|
2971
3853
|
if (iconSvg) {
|
|
@@ -3007,6 +3889,13 @@ var SVGRenderer = class {
|
|
|
3007
3889
|
escapeXml(text) {
|
|
3008
3890
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3009
3891
|
}
|
|
3892
|
+
/**
|
|
3893
|
+
* Get data-node-id attribute string for SVG elements
|
|
3894
|
+
* Enables bidirectional selection between code and canvas
|
|
3895
|
+
*/
|
|
3896
|
+
getDataNodeId(node) {
|
|
3897
|
+
return node.meta.nodeId ? ` data-node-id="${node.meta.nodeId}"` : "";
|
|
3898
|
+
}
|
|
3010
3899
|
};
|
|
3011
3900
|
function renderToSVG(ir, layout, options) {
|
|
3012
3901
|
const renderer = new SVGRenderer(ir, layout, options);
|
|
@@ -3024,6 +3913,213 @@ function buildSVG(component) {
|
|
|
3024
3913
|
return createSVGElement(component.tag, component.attrs, children);
|
|
3025
3914
|
}
|
|
3026
3915
|
|
|
3916
|
+
// src/sourcemap/hash.ts
|
|
3917
|
+
function simpleHash(str) {
|
|
3918
|
+
let hash = 5381;
|
|
3919
|
+
for (let i = 0; i < str.length; i++) {
|
|
3920
|
+
const char = str.charCodeAt(i);
|
|
3921
|
+
hash = (hash << 5) + hash + char;
|
|
3922
|
+
hash = hash & hash;
|
|
3923
|
+
}
|
|
3924
|
+
return Math.abs(hash);
|
|
3925
|
+
}
|
|
3926
|
+
function generateStableNodeId(type, filePath, line, column, indexInParent, name) {
|
|
3927
|
+
const content = [
|
|
3928
|
+
filePath,
|
|
3929
|
+
`${line}:${column}`,
|
|
3930
|
+
type,
|
|
3931
|
+
`idx:${indexInParent}`,
|
|
3932
|
+
name || ""
|
|
3933
|
+
].join("|");
|
|
3934
|
+
const hashNum = simpleHash(content);
|
|
3935
|
+
const hashStr = hashNum.toString(36);
|
|
3936
|
+
return `node-${hashStr}-${type}`;
|
|
3937
|
+
}
|
|
3938
|
+
function isValidNodeId(id) {
|
|
3939
|
+
return /^node-[a-z0-9]+-[a-z-]+$/.test(id);
|
|
3940
|
+
}
|
|
3941
|
+
function getTypeFromNodeId(nodeId) {
|
|
3942
|
+
const match = nodeId.match(/^node-[a-z0-9]+-(.+)$/);
|
|
3943
|
+
if (!match) return null;
|
|
3944
|
+
return match[1];
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
// src/sourcemap/resolver.ts
|
|
3948
|
+
var SourceMapResolver = class {
|
|
3949
|
+
constructor(sourceMap) {
|
|
3950
|
+
this.nodeMap = /* @__PURE__ */ new Map();
|
|
3951
|
+
this.childrenMap = /* @__PURE__ */ new Map();
|
|
3952
|
+
this.positionIndex = sourceMap;
|
|
3953
|
+
for (const entry of sourceMap) {
|
|
3954
|
+
this.nodeMap.set(entry.nodeId, entry);
|
|
3955
|
+
}
|
|
3956
|
+
for (const entry of sourceMap) {
|
|
3957
|
+
if (entry.parentId) {
|
|
3958
|
+
const siblings = this.childrenMap.get(entry.parentId) || [];
|
|
3959
|
+
siblings.push(entry);
|
|
3960
|
+
this.childrenMap.set(entry.parentId, siblings);
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
/**
|
|
3965
|
+
* Find node by ID (Canvas → Code)
|
|
3966
|
+
*
|
|
3967
|
+
* @example
|
|
3968
|
+
* // User clicks SVG element with data-node-id="component-button-0"
|
|
3969
|
+
* const node = resolver.getNodeById("component-button-0");
|
|
3970
|
+
* editor.revealRange(node.range); // Jump to code
|
|
3971
|
+
*/
|
|
3972
|
+
getNodeById(nodeId) {
|
|
3973
|
+
return this.nodeMap.get(nodeId) || null;
|
|
3974
|
+
}
|
|
3975
|
+
/**
|
|
3976
|
+
* Find node at position (Code → Canvas)
|
|
3977
|
+
* Returns the most specific (deepest) node containing the position
|
|
3978
|
+
*
|
|
3979
|
+
* @example
|
|
3980
|
+
* // User clicks code at line 5, column 10
|
|
3981
|
+
* const node = resolver.getNodeByPosition(5, 10);
|
|
3982
|
+
* canvas.highlightElement(node.nodeId); // Highlight in canvas
|
|
3983
|
+
*/
|
|
3984
|
+
getNodeByPosition(line, column) {
|
|
3985
|
+
const candidates = [];
|
|
3986
|
+
for (const entry of this.positionIndex) {
|
|
3987
|
+
if (this.containsPosition(entry, line, column)) {
|
|
3988
|
+
const depth = this.calculateDepth(entry);
|
|
3989
|
+
candidates.push({ ...entry, depth });
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
if (candidates.length === 0) {
|
|
3993
|
+
return null;
|
|
3994
|
+
}
|
|
3995
|
+
candidates.sort((a, b) => b.depth - a.depth);
|
|
3996
|
+
return candidates[0];
|
|
3997
|
+
}
|
|
3998
|
+
/**
|
|
3999
|
+
* Get all child nodes of a parent
|
|
4000
|
+
*
|
|
4001
|
+
* @example
|
|
4002
|
+
* const children = resolver.getChildren("layout-stack-0");
|
|
4003
|
+
* // Returns: [component-button-0, component-input-0, ...]
|
|
4004
|
+
*/
|
|
4005
|
+
getChildren(nodeId) {
|
|
4006
|
+
return this.childrenMap.get(nodeId) || [];
|
|
4007
|
+
}
|
|
4008
|
+
/**
|
|
4009
|
+
* Get parent node
|
|
4010
|
+
*
|
|
4011
|
+
* @example
|
|
4012
|
+
* const parent = resolver.getParent("component-button-0");
|
|
4013
|
+
* // Returns: layout-stack-0
|
|
4014
|
+
*/
|
|
4015
|
+
getParent(nodeId) {
|
|
4016
|
+
const node = this.nodeMap.get(nodeId);
|
|
4017
|
+
if (!node || !node.parentId) {
|
|
4018
|
+
return null;
|
|
4019
|
+
}
|
|
4020
|
+
return this.nodeMap.get(node.parentId) || null;
|
|
4021
|
+
}
|
|
4022
|
+
/**
|
|
4023
|
+
* Get all nodes in the SourceMap
|
|
4024
|
+
*/
|
|
4025
|
+
getAllNodes() {
|
|
4026
|
+
return this.positionIndex;
|
|
4027
|
+
}
|
|
4028
|
+
/**
|
|
4029
|
+
* Get all nodes of a specific type
|
|
4030
|
+
*
|
|
4031
|
+
* @example
|
|
4032
|
+
* const buttons = resolver.getNodesByType("component", "Button");
|
|
4033
|
+
*/
|
|
4034
|
+
getNodesByType(type, subtype) {
|
|
4035
|
+
return this.positionIndex.filter((entry) => {
|
|
4036
|
+
if (entry.type !== type) return false;
|
|
4037
|
+
if (subtype) {
|
|
4038
|
+
if (type === "component" && entry.componentType !== subtype) return false;
|
|
4039
|
+
if (type === "layout" && entry.layoutType !== subtype) return false;
|
|
4040
|
+
}
|
|
4041
|
+
return true;
|
|
4042
|
+
});
|
|
4043
|
+
}
|
|
4044
|
+
/**
|
|
4045
|
+
* Get siblings of a node (nodes with same parent)
|
|
4046
|
+
*/
|
|
4047
|
+
getSiblings(nodeId) {
|
|
4048
|
+
const node = this.nodeMap.get(nodeId);
|
|
4049
|
+
if (!node || !node.parentId) {
|
|
4050
|
+
return [];
|
|
4051
|
+
}
|
|
4052
|
+
const siblings = this.getChildren(node.parentId);
|
|
4053
|
+
return siblings.filter((s) => s.nodeId !== nodeId);
|
|
4054
|
+
}
|
|
4055
|
+
/**
|
|
4056
|
+
* Get path from root to node (breadcrumb)
|
|
4057
|
+
*
|
|
4058
|
+
* @example
|
|
4059
|
+
* const path = resolver.getPath("component-button-0");
|
|
4060
|
+
* // Returns: [project, screen-0, layout-stack-0, component-button-0]
|
|
4061
|
+
*/
|
|
4062
|
+
getPath(nodeId) {
|
|
4063
|
+
const path = [];
|
|
4064
|
+
let current = this.nodeMap.get(nodeId);
|
|
4065
|
+
while (current) {
|
|
4066
|
+
path.unshift(current);
|
|
4067
|
+
current = current.parentId ? this.nodeMap.get(current.parentId) : void 0;
|
|
4068
|
+
}
|
|
4069
|
+
return path;
|
|
4070
|
+
}
|
|
4071
|
+
/**
|
|
4072
|
+
* Check if a position is within a node's range
|
|
4073
|
+
*/
|
|
4074
|
+
containsPosition(entry, line, column) {
|
|
4075
|
+
const { range } = entry;
|
|
4076
|
+
if (line < range.start.line || line > range.end.line) {
|
|
4077
|
+
return false;
|
|
4078
|
+
}
|
|
4079
|
+
if (range.start.line === range.end.line) {
|
|
4080
|
+
return column >= range.start.column && column <= range.end.column;
|
|
4081
|
+
}
|
|
4082
|
+
if (line === range.start.line) {
|
|
4083
|
+
return column >= range.start.column;
|
|
4084
|
+
}
|
|
4085
|
+
if (line === range.end.line) {
|
|
4086
|
+
return column <= range.end.column;
|
|
4087
|
+
}
|
|
4088
|
+
return true;
|
|
4089
|
+
}
|
|
4090
|
+
/**
|
|
4091
|
+
* Calculate depth of a node in the tree (0 = root)
|
|
4092
|
+
*/
|
|
4093
|
+
calculateDepth(entry) {
|
|
4094
|
+
let depth = 0;
|
|
4095
|
+
let current = entry;
|
|
4096
|
+
while (current.parentId) {
|
|
4097
|
+
depth++;
|
|
4098
|
+
const parent = this.nodeMap.get(current.parentId);
|
|
4099
|
+
if (!parent) break;
|
|
4100
|
+
current = parent;
|
|
4101
|
+
}
|
|
4102
|
+
return depth;
|
|
4103
|
+
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Get statistics about the SourceMap
|
|
4106
|
+
*/
|
|
4107
|
+
getStats() {
|
|
4108
|
+
const byType = {};
|
|
4109
|
+
let maxDepth = 0;
|
|
4110
|
+
for (const entry of this.positionIndex) {
|
|
4111
|
+
byType[entry.type] = (byType[entry.type] || 0) + 1;
|
|
4112
|
+
const depth = this.calculateDepth(entry);
|
|
4113
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
4114
|
+
}
|
|
4115
|
+
return {
|
|
4116
|
+
totalNodes: this.positionIndex.length,
|
|
4117
|
+
byType,
|
|
4118
|
+
maxDepth
|
|
4119
|
+
};
|
|
4120
|
+
}
|
|
4121
|
+
};
|
|
4122
|
+
|
|
3027
4123
|
// src/index.ts
|
|
3028
4124
|
var version = "0.0.1";
|
|
3029
4125
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -3031,11 +4127,17 @@ var version = "0.0.1";
|
|
|
3031
4127
|
IRGenerator,
|
|
3032
4128
|
LayoutEngine,
|
|
3033
4129
|
SVGRenderer,
|
|
4130
|
+
SourceMapBuilder,
|
|
4131
|
+
SourceMapResolver,
|
|
3034
4132
|
buildSVG,
|
|
3035
4133
|
calculateLayout,
|
|
3036
4134
|
createSVGElement,
|
|
3037
4135
|
generateIR,
|
|
4136
|
+
generateStableNodeId,
|
|
4137
|
+
getTypeFromNodeId,
|
|
4138
|
+
isValidNodeId,
|
|
3038
4139
|
parseWireDSL,
|
|
4140
|
+
parseWireDSLWithSourceMap,
|
|
3039
4141
|
renderToSVG,
|
|
3040
4142
|
resolveGridPosition,
|
|
3041
4143
|
version
|