@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.js
CHANGED
|
@@ -1,5 +1,417 @@
|
|
|
1
1
|
// src/parser/index.ts
|
|
2
2
|
import { Lexer, createToken, CstParser } from "chevrotain";
|
|
3
|
+
|
|
4
|
+
// src/sourcemap/builder.ts
|
|
5
|
+
var SourceMapBuilder = class {
|
|
6
|
+
// Counter per type-subtype
|
|
7
|
+
constructor(filePath = "<input>", sourceCode = "") {
|
|
8
|
+
this.entries = [];
|
|
9
|
+
this.parentStack = [];
|
|
10
|
+
// Stack of parent nodeIds for hierarchy tracking
|
|
11
|
+
this.counters = /* @__PURE__ */ new Map();
|
|
12
|
+
this.filePath = filePath;
|
|
13
|
+
this.sourceCode = sourceCode;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Add a node to the SourceMap
|
|
17
|
+
* Generates semantic IDs like: project, screen-0, component-button-1, layout-stack-0
|
|
18
|
+
*
|
|
19
|
+
* @param type - Type of AST node
|
|
20
|
+
* @param tokens - Captured tokens from parser
|
|
21
|
+
* @param metadata - Optional metadata (name, layoutType, componentType)
|
|
22
|
+
* @returns Generated nodeId
|
|
23
|
+
*/
|
|
24
|
+
addNode(type, tokens, metadata) {
|
|
25
|
+
const range = this.calculateRange(tokens);
|
|
26
|
+
const nodeId = this.generateNodeId(type, metadata);
|
|
27
|
+
const parentId = this.parentStack.length > 0 ? this.parentStack[this.parentStack.length - 1] : null;
|
|
28
|
+
const keywordRange = tokens.keyword ? this.tokenToRange(tokens.keyword) : void 0;
|
|
29
|
+
const nameRange = tokens.name ? this.tokenToRange(tokens.name) : void 0;
|
|
30
|
+
const bodyRange = tokens.body ? this.calculateBodyRange(tokens.body) : void 0;
|
|
31
|
+
const entry = {
|
|
32
|
+
nodeId,
|
|
33
|
+
type,
|
|
34
|
+
range,
|
|
35
|
+
filePath: this.filePath,
|
|
36
|
+
parentId,
|
|
37
|
+
keywordRange,
|
|
38
|
+
nameRange,
|
|
39
|
+
bodyRange,
|
|
40
|
+
...metadata
|
|
41
|
+
// Spread name, layoutType, componentType, isUserDefined if provided
|
|
42
|
+
};
|
|
43
|
+
this.entries.push(entry);
|
|
44
|
+
return nodeId;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generate semantic node ID based on type and subtype
|
|
48
|
+
* Format: {type}-{subtype}-{counter} or {type}-{counter}
|
|
49
|
+
*
|
|
50
|
+
* Examples:
|
|
51
|
+
* - project → "project"
|
|
52
|
+
* - theme → "theme"
|
|
53
|
+
* - mocks → "mocks"
|
|
54
|
+
* - colors → "colors"
|
|
55
|
+
* - screen → "screen-0", "screen-1"
|
|
56
|
+
* - component Button → "component-button-0", "component-button-1"
|
|
57
|
+
* - layout stack → "layout-stack-0", "layout-stack-1"
|
|
58
|
+
* - cell → "cell-0", "cell-1"
|
|
59
|
+
* - component-definition → "define-MyButton"
|
|
60
|
+
*/
|
|
61
|
+
generateNodeId(type, metadata) {
|
|
62
|
+
switch (type) {
|
|
63
|
+
case "project":
|
|
64
|
+
return "project";
|
|
65
|
+
case "theme":
|
|
66
|
+
return "theme";
|
|
67
|
+
case "mocks":
|
|
68
|
+
return "mocks";
|
|
69
|
+
case "colors":
|
|
70
|
+
return "colors";
|
|
71
|
+
case "screen":
|
|
72
|
+
const screenIdx = this.counters.get("screen") || 0;
|
|
73
|
+
this.counters.set("screen", screenIdx + 1);
|
|
74
|
+
return `screen-${screenIdx}`;
|
|
75
|
+
case "component": {
|
|
76
|
+
const componentType = metadata?.componentType || "unknown";
|
|
77
|
+
const key = `component-${componentType.toLowerCase()}`;
|
|
78
|
+
const idx = this.counters.get(key) || 0;
|
|
79
|
+
this.counters.set(key, idx + 1);
|
|
80
|
+
return `${key}-${idx}`;
|
|
81
|
+
}
|
|
82
|
+
case "layout": {
|
|
83
|
+
const layoutType = metadata?.layoutType || "unknown";
|
|
84
|
+
const key = `layout-${layoutType.toLowerCase()}`;
|
|
85
|
+
const idx = this.counters.get(key) || 0;
|
|
86
|
+
this.counters.set(key, idx + 1);
|
|
87
|
+
return `${key}-${idx}`;
|
|
88
|
+
}
|
|
89
|
+
case "cell": {
|
|
90
|
+
const idx = this.counters.get("cell") || 0;
|
|
91
|
+
this.counters.set("cell", idx + 1);
|
|
92
|
+
return `cell-${idx}`;
|
|
93
|
+
}
|
|
94
|
+
case "component-definition":
|
|
95
|
+
return `define-${metadata?.name || "unknown"}`;
|
|
96
|
+
default:
|
|
97
|
+
return `${type}-0`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Add a property to an existing node in the SourceMap
|
|
102
|
+
* Captures precise ranges for property name and value for surgical editing
|
|
103
|
+
*
|
|
104
|
+
* @param nodeId - ID of the node that owns this property
|
|
105
|
+
* @param propertyName - Name of the property (e.g., "text", "direction")
|
|
106
|
+
* @param propertyValue - Parsed value of the property
|
|
107
|
+
* @param tokens - Captured tokens for the property
|
|
108
|
+
* @returns The PropertySourceMap entry created
|
|
109
|
+
*/
|
|
110
|
+
addProperty(nodeId, propertyName, propertyValue, tokens) {
|
|
111
|
+
const entry = this.entries.find((e) => e.nodeId === nodeId);
|
|
112
|
+
if (!entry) {
|
|
113
|
+
throw new Error(`Cannot add property to non-existent node: ${nodeId}`);
|
|
114
|
+
}
|
|
115
|
+
if (!entry.properties) {
|
|
116
|
+
entry.properties = {};
|
|
117
|
+
}
|
|
118
|
+
let nameRange;
|
|
119
|
+
let valueRange;
|
|
120
|
+
let fullRange;
|
|
121
|
+
if (tokens.name && tokens.value) {
|
|
122
|
+
nameRange = {
|
|
123
|
+
start: this.getTokenStart(tokens.name),
|
|
124
|
+
end: this.getTokenEnd(tokens.name)
|
|
125
|
+
};
|
|
126
|
+
valueRange = {
|
|
127
|
+
start: this.getTokenStart(tokens.value),
|
|
128
|
+
end: this.getTokenEnd(tokens.value)
|
|
129
|
+
};
|
|
130
|
+
fullRange = {
|
|
131
|
+
start: nameRange.start,
|
|
132
|
+
end: valueRange.end
|
|
133
|
+
};
|
|
134
|
+
} else if (tokens.full) {
|
|
135
|
+
fullRange = {
|
|
136
|
+
start: this.getTokenStart(tokens.full),
|
|
137
|
+
end: this.getTokenEnd(tokens.full)
|
|
138
|
+
};
|
|
139
|
+
nameRange = fullRange;
|
|
140
|
+
valueRange = fullRange;
|
|
141
|
+
} else {
|
|
142
|
+
throw new Error(`Invalid tokens for property ${propertyName}: need either name+value or full`);
|
|
143
|
+
}
|
|
144
|
+
const propertySourceMap = {
|
|
145
|
+
name: propertyName,
|
|
146
|
+
value: propertyValue,
|
|
147
|
+
range: fullRange,
|
|
148
|
+
nameRange,
|
|
149
|
+
valueRange
|
|
150
|
+
};
|
|
151
|
+
entry.properties[propertyName] = propertySourceMap;
|
|
152
|
+
return propertySourceMap;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Push a parent onto the stack (when entering a container node)
|
|
156
|
+
*/
|
|
157
|
+
pushParent(nodeId) {
|
|
158
|
+
this.parentStack.push(nodeId);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Pop a parent from the stack (when exiting a container node)
|
|
162
|
+
*/
|
|
163
|
+
popParent() {
|
|
164
|
+
this.parentStack.pop();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get the current parent nodeId (or null if at root)
|
|
168
|
+
*/
|
|
169
|
+
getCurrentParent() {
|
|
170
|
+
return this.parentStack.length > 0 ? this.parentStack[this.parentStack.length - 1] : null;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build and return the final SourceMap
|
|
174
|
+
*/
|
|
175
|
+
build() {
|
|
176
|
+
this.calculateAllInsertionPoints();
|
|
177
|
+
return this.entries;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Calculate insertionPoints for all container nodes
|
|
181
|
+
* Container nodes: project, screen, layout, cell, component-definition
|
|
182
|
+
*/
|
|
183
|
+
calculateAllInsertionPoints() {
|
|
184
|
+
const containerTypes = [
|
|
185
|
+
"project",
|
|
186
|
+
"screen",
|
|
187
|
+
"layout",
|
|
188
|
+
"cell",
|
|
189
|
+
"component-definition"
|
|
190
|
+
];
|
|
191
|
+
for (const entry of this.entries) {
|
|
192
|
+
if (containerTypes.includes(entry.type)) {
|
|
193
|
+
entry.insertionPoint = this.calculateInsertionPoint(entry.nodeId);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Calculate CodeRange from captured tokens
|
|
199
|
+
* Finds the earliest start and latest end among all tokens
|
|
200
|
+
*/
|
|
201
|
+
calculateRange(tokens) {
|
|
202
|
+
const positions = [];
|
|
203
|
+
if (tokens.keyword) {
|
|
204
|
+
positions.push(this.getTokenStart(tokens.keyword));
|
|
205
|
+
positions.push(this.getTokenEnd(tokens.keyword));
|
|
206
|
+
}
|
|
207
|
+
if (tokens.name) {
|
|
208
|
+
positions.push(this.getTokenStart(tokens.name));
|
|
209
|
+
positions.push(this.getTokenEnd(tokens.name));
|
|
210
|
+
}
|
|
211
|
+
if (tokens.paramList) {
|
|
212
|
+
positions.push(this.getTokenStart(tokens.paramList));
|
|
213
|
+
positions.push(this.getTokenEnd(tokens.paramList));
|
|
214
|
+
}
|
|
215
|
+
if (tokens.body) {
|
|
216
|
+
positions.push(this.getTokenStart(tokens.body));
|
|
217
|
+
positions.push(this.getTokenEnd(tokens.body));
|
|
218
|
+
}
|
|
219
|
+
if (tokens.properties && tokens.properties.length > 0) {
|
|
220
|
+
tokens.properties.forEach((prop) => {
|
|
221
|
+
positions.push(this.getTokenStart(prop));
|
|
222
|
+
positions.push(this.getTokenEnd(prop));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (positions.length === 0) {
|
|
226
|
+
const fallbackToken = tokens.keyword || tokens.name;
|
|
227
|
+
return {
|
|
228
|
+
start: this.getTokenStart(fallbackToken),
|
|
229
|
+
end: this.getTokenEnd(fallbackToken)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
positions.sort((a, b) => {
|
|
233
|
+
if (a.line !== b.line) return a.line - b.line;
|
|
234
|
+
return a.column - b.column;
|
|
235
|
+
});
|
|
236
|
+
const start = positions[0];
|
|
237
|
+
const end = positions[positions.length - 1];
|
|
238
|
+
return { start, end };
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Convert a single token to CodeRange
|
|
242
|
+
*/
|
|
243
|
+
tokenToRange(token) {
|
|
244
|
+
return {
|
|
245
|
+
start: this.getTokenStart(token),
|
|
246
|
+
end: this.getTokenEnd(token)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Calculate body range from closing brace token
|
|
251
|
+
* Body range typically spans from opening brace to closing brace
|
|
252
|
+
*/
|
|
253
|
+
calculateBodyRange(closingBrace) {
|
|
254
|
+
return this.tokenToRange(closingBrace);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Extract the first real token from a CST node (earliest by offset)
|
|
258
|
+
* Recursively searches through children to find the token with smallest offset
|
|
259
|
+
*/
|
|
260
|
+
getFirstToken(cstNode) {
|
|
261
|
+
if (!cstNode?.children) {
|
|
262
|
+
return cstNode;
|
|
263
|
+
}
|
|
264
|
+
let earliestToken = null;
|
|
265
|
+
let earliestOffset = Infinity;
|
|
266
|
+
for (const childArray of Object.values(cstNode.children)) {
|
|
267
|
+
if (Array.isArray(childArray)) {
|
|
268
|
+
for (const child of childArray) {
|
|
269
|
+
if (!child) continue;
|
|
270
|
+
let token;
|
|
271
|
+
if (child.children) {
|
|
272
|
+
token = this.getFirstToken(child);
|
|
273
|
+
} else {
|
|
274
|
+
token = child;
|
|
275
|
+
}
|
|
276
|
+
if (token?.startOffset !== void 0 && token.startOffset < earliestOffset) {
|
|
277
|
+
earliestToken = token;
|
|
278
|
+
earliestOffset = token.startOffset;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return earliestToken;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Extract the last real token from a CST node (latest by offset)
|
|
287
|
+
* Recursively searches through children to find the token with largest offset
|
|
288
|
+
*/
|
|
289
|
+
getLastToken(cstNode) {
|
|
290
|
+
if (!cstNode?.children) {
|
|
291
|
+
return cstNode;
|
|
292
|
+
}
|
|
293
|
+
let latestToken = null;
|
|
294
|
+
let latestOffset = -1;
|
|
295
|
+
for (const childArray of Object.values(cstNode.children)) {
|
|
296
|
+
if (Array.isArray(childArray)) {
|
|
297
|
+
for (const child of childArray) {
|
|
298
|
+
if (!child) continue;
|
|
299
|
+
let token;
|
|
300
|
+
if (child.children) {
|
|
301
|
+
token = this.getLastToken(child);
|
|
302
|
+
} else {
|
|
303
|
+
token = child;
|
|
304
|
+
}
|
|
305
|
+
const tokenOffset = token?.endOffset ?? token?.startOffset;
|
|
306
|
+
if (tokenOffset !== void 0 && tokenOffset > latestOffset) {
|
|
307
|
+
latestToken = token;
|
|
308
|
+
latestOffset = tokenOffset;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return latestToken;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Extract start position from a Chevrotain token or CST node
|
|
317
|
+
*/
|
|
318
|
+
getTokenStart(token) {
|
|
319
|
+
if (token?.children) {
|
|
320
|
+
const firstToken = this.getFirstToken(token);
|
|
321
|
+
if (firstToken) {
|
|
322
|
+
return this.getTokenStart(firstToken);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
line: token.startLine || 1,
|
|
327
|
+
column: token.startColumn !== void 0 ? token.startColumn - 1 : 0,
|
|
328
|
+
// Chevrotain is 1-based, we want 0-based
|
|
329
|
+
offset: token.startOffset
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Extract end position from a Chevrotain token or CST node
|
|
334
|
+
*/
|
|
335
|
+
getTokenEnd(token) {
|
|
336
|
+
if (token?.children) {
|
|
337
|
+
const lastToken = this.getLastToken(token);
|
|
338
|
+
if (lastToken) {
|
|
339
|
+
return this.getTokenEnd(lastToken);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
line: token.endLine || token.startLine || 1,
|
|
344
|
+
column: token.endColumn !== void 0 ? token.endColumn : token.startColumn || 0,
|
|
345
|
+
// Chevrotain columns are 1-based
|
|
346
|
+
offset: token.endOffset
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Reset the builder (for reuse)
|
|
351
|
+
*/
|
|
352
|
+
reset(filePath = "<input>", sourceCode = "") {
|
|
353
|
+
this.entries = [];
|
|
354
|
+
this.filePath = filePath;
|
|
355
|
+
this.sourceCode = sourceCode;
|
|
356
|
+
this.parentStack = [];
|
|
357
|
+
this.counters.clear();
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Calculate insertion point for adding new children to a container node
|
|
361
|
+
*
|
|
362
|
+
* Strategy:
|
|
363
|
+
* - If node has children: insert after last child, preserve indentation
|
|
364
|
+
* - If node is empty: insert inside body, use parent indentation + 2 spaces
|
|
365
|
+
*
|
|
366
|
+
* @param nodeId - ID of the container node
|
|
367
|
+
* @returns InsertionPoint with line, column, indentation, and optional after
|
|
368
|
+
*/
|
|
369
|
+
calculateInsertionPoint(nodeId) {
|
|
370
|
+
const node = this.entries.find((e) => e.nodeId === nodeId);
|
|
371
|
+
if (!node) {
|
|
372
|
+
return void 0;
|
|
373
|
+
}
|
|
374
|
+
const children = this.entries.filter((e) => e.parentId === nodeId);
|
|
375
|
+
if (children.length > 0) {
|
|
376
|
+
const lastChild = children[children.length - 1];
|
|
377
|
+
const insertLine = lastChild.range.end.line;
|
|
378
|
+
const indentation2 = this.extractIndentation(lastChild.range.start.line);
|
|
379
|
+
return {
|
|
380
|
+
line: insertLine,
|
|
381
|
+
column: 0,
|
|
382
|
+
// Start of next line
|
|
383
|
+
indentation: indentation2,
|
|
384
|
+
after: lastChild.nodeId
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const bodyEndLine = node.range.end.line;
|
|
388
|
+
const parentIndentation = this.extractIndentation(node.range.start.line);
|
|
389
|
+
const indentation = parentIndentation + " ";
|
|
390
|
+
return {
|
|
391
|
+
line: bodyEndLine,
|
|
392
|
+
// Insert right before closing brace
|
|
393
|
+
column: 0,
|
|
394
|
+
indentation
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Extract indentation (leading whitespace) from a line
|
|
399
|
+
*/
|
|
400
|
+
extractIndentation(lineNumber) {
|
|
401
|
+
if (!this.sourceCode) {
|
|
402
|
+
return "";
|
|
403
|
+
}
|
|
404
|
+
const lines = this.sourceCode.split("\n");
|
|
405
|
+
if (lineNumber < 1 || lineNumber > lines.length) {
|
|
406
|
+
return "";
|
|
407
|
+
}
|
|
408
|
+
const line = lines[lineNumber - 1];
|
|
409
|
+
const match = line.match(/^(\s*)/);
|
|
410
|
+
return match ? match[1] : "";
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// src/parser/index.ts
|
|
3
415
|
var Project = createToken({ name: "Project", pattern: /project/ });
|
|
4
416
|
var Screen = createToken({ name: "Screen", pattern: /screen/ });
|
|
5
417
|
var Layout = createToken({ name: "Layout", pattern: /layout/ });
|
|
@@ -467,6 +879,420 @@ var WireDSLVisitor = class extends BaseCstVisitor {
|
|
|
467
879
|
return params;
|
|
468
880
|
}
|
|
469
881
|
};
|
|
882
|
+
var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
|
|
883
|
+
constructor(sourceMapBuilder) {
|
|
884
|
+
super();
|
|
885
|
+
this.definedComponentNames = /* @__PURE__ */ new Set();
|
|
886
|
+
this.sourceMapBuilder = sourceMapBuilder;
|
|
887
|
+
}
|
|
888
|
+
project(ctx) {
|
|
889
|
+
const projectName = ctx.projectName[0].image.slice(1, -1);
|
|
890
|
+
const theme = {};
|
|
891
|
+
const mocks = {};
|
|
892
|
+
const colors = {};
|
|
893
|
+
const definedComponents = [];
|
|
894
|
+
const screens = [];
|
|
895
|
+
const tokens = {
|
|
896
|
+
keyword: ctx.Project[0],
|
|
897
|
+
name: ctx.projectName[0],
|
|
898
|
+
body: ctx.RCurly[0]
|
|
899
|
+
};
|
|
900
|
+
const ast = {
|
|
901
|
+
type: "project",
|
|
902
|
+
name: projectName,
|
|
903
|
+
theme: {},
|
|
904
|
+
mocks: {},
|
|
905
|
+
colors: {},
|
|
906
|
+
definedComponents: [],
|
|
907
|
+
// Will be filled after push
|
|
908
|
+
screens: []
|
|
909
|
+
// Will be filled after push
|
|
910
|
+
};
|
|
911
|
+
if (this.sourceMapBuilder) {
|
|
912
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
913
|
+
"project",
|
|
914
|
+
tokens,
|
|
915
|
+
{ name: projectName }
|
|
916
|
+
);
|
|
917
|
+
ast._meta = { nodeId };
|
|
918
|
+
this.sourceMapBuilder.pushParent(nodeId);
|
|
919
|
+
}
|
|
920
|
+
if (ctx.themeDecl && ctx.themeDecl.length > 0) {
|
|
921
|
+
const themeBlock = this.visit(ctx.themeDecl[0]);
|
|
922
|
+
Object.assign(ast.theme, themeBlock);
|
|
923
|
+
}
|
|
924
|
+
if (ctx.mocksDecl && ctx.mocksDecl.length > 0) {
|
|
925
|
+
const mocksBlock = this.visit(ctx.mocksDecl[0]);
|
|
926
|
+
Object.assign(ast.mocks, mocksBlock);
|
|
927
|
+
}
|
|
928
|
+
if (ctx.colorsDecl && ctx.colorsDecl.length > 0) {
|
|
929
|
+
const colorsBlock = this.visit(ctx.colorsDecl[0]);
|
|
930
|
+
Object.assign(ast.colors, colorsBlock);
|
|
931
|
+
}
|
|
932
|
+
if (ctx.definedComponent) {
|
|
933
|
+
ctx.definedComponent.forEach((comp) => {
|
|
934
|
+
ast.definedComponents.push(this.visit(comp));
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
if (ctx.screen) {
|
|
938
|
+
ctx.screen.forEach((screen) => {
|
|
939
|
+
ast.screens.push(this.visit(screen));
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
return ast;
|
|
943
|
+
}
|
|
944
|
+
screen(ctx) {
|
|
945
|
+
const params = ctx.paramList ? this.visit(ctx.paramList[0]) : {};
|
|
946
|
+
const screenName = ctx.screenName[0].image;
|
|
947
|
+
const tokens = {
|
|
948
|
+
keyword: ctx.Screen[0],
|
|
949
|
+
name: ctx.screenName[0],
|
|
950
|
+
paramList: ctx.paramList?.[0],
|
|
951
|
+
body: ctx.RCurly[0]
|
|
952
|
+
};
|
|
953
|
+
const ast = {
|
|
954
|
+
type: "screen",
|
|
955
|
+
name: screenName,
|
|
956
|
+
params,
|
|
957
|
+
layout: {}
|
|
958
|
+
// Will be filled after push
|
|
959
|
+
};
|
|
960
|
+
if (this.sourceMapBuilder) {
|
|
961
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
962
|
+
"screen",
|
|
963
|
+
tokens,
|
|
964
|
+
{ name: screenName }
|
|
965
|
+
);
|
|
966
|
+
ast._meta = { nodeId };
|
|
967
|
+
this.sourceMapBuilder.pushParent(nodeId);
|
|
968
|
+
}
|
|
969
|
+
ast.layout = this.visit(ctx.layout[0]);
|
|
970
|
+
if (this.sourceMapBuilder) {
|
|
971
|
+
this.sourceMapBuilder.popParent();
|
|
972
|
+
}
|
|
973
|
+
return ast;
|
|
974
|
+
}
|
|
975
|
+
layout(ctx) {
|
|
976
|
+
const layoutType = ctx.layoutType[0].image;
|
|
977
|
+
const params = {};
|
|
978
|
+
if (ctx.paramList) {
|
|
979
|
+
const paramResult = this.visit(ctx.paramList);
|
|
980
|
+
Object.assign(params, paramResult);
|
|
981
|
+
}
|
|
982
|
+
const tokens = {
|
|
983
|
+
keyword: ctx.Layout[0],
|
|
984
|
+
name: ctx.layoutType[0],
|
|
985
|
+
paramList: ctx.paramList?.[0],
|
|
986
|
+
body: ctx.RCurly[0]
|
|
987
|
+
};
|
|
988
|
+
const ast = {
|
|
989
|
+
type: "layout",
|
|
990
|
+
layoutType,
|
|
991
|
+
params,
|
|
992
|
+
children: []
|
|
993
|
+
// Will be filled after push
|
|
994
|
+
};
|
|
995
|
+
if (this.sourceMapBuilder) {
|
|
996
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
997
|
+
"layout",
|
|
998
|
+
tokens,
|
|
999
|
+
{ layoutType }
|
|
1000
|
+
);
|
|
1001
|
+
ast._meta = { nodeId };
|
|
1002
|
+
if (ctx.paramList && ctx.paramList[0]?.children?.property) {
|
|
1003
|
+
ctx.paramList[0].children.property.forEach((propCtx) => {
|
|
1004
|
+
const propResult = this.visit(propCtx);
|
|
1005
|
+
this.sourceMapBuilder.addProperty(
|
|
1006
|
+
nodeId,
|
|
1007
|
+
propResult.key,
|
|
1008
|
+
propResult.value,
|
|
1009
|
+
{
|
|
1010
|
+
name: propCtx.children.propKey[0],
|
|
1011
|
+
value: propCtx.children.propValue[0]
|
|
1012
|
+
}
|
|
1013
|
+
);
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
this.sourceMapBuilder.pushParent(nodeId);
|
|
1017
|
+
}
|
|
1018
|
+
const childNodes = [];
|
|
1019
|
+
if (ctx.component) {
|
|
1020
|
+
ctx.component.forEach((comp) => {
|
|
1021
|
+
const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
|
|
1022
|
+
childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
if (ctx.layout) {
|
|
1026
|
+
ctx.layout.forEach((layout) => {
|
|
1027
|
+
const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
|
|
1028
|
+
childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
if (ctx.cell) {
|
|
1032
|
+
ctx.cell.forEach((cell) => {
|
|
1033
|
+
const startToken = cell.children?.Cell?.[0];
|
|
1034
|
+
childNodes.push({ type: "cell", node: cell, index: startToken.startOffset });
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
childNodes.sort((a, b) => a.index - b.index);
|
|
1038
|
+
childNodes.forEach((item) => {
|
|
1039
|
+
ast.children.push(this.visit(item.node));
|
|
1040
|
+
});
|
|
1041
|
+
if (this.sourceMapBuilder) {
|
|
1042
|
+
this.sourceMapBuilder.popParent();
|
|
1043
|
+
}
|
|
1044
|
+
return ast;
|
|
1045
|
+
}
|
|
1046
|
+
cell(ctx) {
|
|
1047
|
+
const props = {};
|
|
1048
|
+
if (ctx.property) {
|
|
1049
|
+
ctx.property.forEach((prop) => {
|
|
1050
|
+
const result = this.visit(prop);
|
|
1051
|
+
props[result.key] = result.value;
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
const tokens = {
|
|
1055
|
+
keyword: ctx.Cell[0],
|
|
1056
|
+
properties: ctx.property || [],
|
|
1057
|
+
body: ctx.RCurly[0]
|
|
1058
|
+
};
|
|
1059
|
+
const ast = {
|
|
1060
|
+
type: "cell",
|
|
1061
|
+
props,
|
|
1062
|
+
children: []
|
|
1063
|
+
// Will be filled after push
|
|
1064
|
+
};
|
|
1065
|
+
if (this.sourceMapBuilder) {
|
|
1066
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1067
|
+
"cell",
|
|
1068
|
+
tokens
|
|
1069
|
+
);
|
|
1070
|
+
ast._meta = { nodeId };
|
|
1071
|
+
if (ctx.property) {
|
|
1072
|
+
ctx.property.forEach((propCtx) => {
|
|
1073
|
+
const propResult = this.visit(propCtx);
|
|
1074
|
+
this.sourceMapBuilder.addProperty(
|
|
1075
|
+
nodeId,
|
|
1076
|
+
propResult.key,
|
|
1077
|
+
propResult.value,
|
|
1078
|
+
{
|
|
1079
|
+
name: propCtx.children.propKey[0],
|
|
1080
|
+
value: propCtx.children.propValue[0]
|
|
1081
|
+
}
|
|
1082
|
+
);
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
this.sourceMapBuilder.pushParent(nodeId);
|
|
1086
|
+
}
|
|
1087
|
+
const childNodes = [];
|
|
1088
|
+
if (ctx.component) {
|
|
1089
|
+
ctx.component.forEach((comp) => {
|
|
1090
|
+
const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
|
|
1091
|
+
childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
if (ctx.layout) {
|
|
1095
|
+
ctx.layout.forEach((layout) => {
|
|
1096
|
+
const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
|
|
1097
|
+
childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
childNodes.sort((a, b) => a.index - b.index);
|
|
1101
|
+
childNodes.forEach((item) => {
|
|
1102
|
+
ast.children.push(this.visit(item.node));
|
|
1103
|
+
});
|
|
1104
|
+
if (this.sourceMapBuilder) {
|
|
1105
|
+
this.sourceMapBuilder.popParent();
|
|
1106
|
+
}
|
|
1107
|
+
return ast;
|
|
1108
|
+
}
|
|
1109
|
+
component(ctx) {
|
|
1110
|
+
const tokens = {
|
|
1111
|
+
keyword: ctx.Component[0],
|
|
1112
|
+
name: ctx.componentType[0],
|
|
1113
|
+
properties: ctx.property || []
|
|
1114
|
+
};
|
|
1115
|
+
const ast = super.component(ctx);
|
|
1116
|
+
if (this.sourceMapBuilder) {
|
|
1117
|
+
const isUserDefined = this.definedComponentNames.has(ast.componentType);
|
|
1118
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1119
|
+
"component",
|
|
1120
|
+
tokens,
|
|
1121
|
+
{
|
|
1122
|
+
componentType: ast.componentType,
|
|
1123
|
+
isUserDefined
|
|
1124
|
+
}
|
|
1125
|
+
);
|
|
1126
|
+
ast._meta = { nodeId };
|
|
1127
|
+
if (ctx.property) {
|
|
1128
|
+
ctx.property.forEach((propCtx) => {
|
|
1129
|
+
const propResult = this.visit(propCtx);
|
|
1130
|
+
this.sourceMapBuilder.addProperty(
|
|
1131
|
+
nodeId,
|
|
1132
|
+
propResult.key,
|
|
1133
|
+
propResult.value,
|
|
1134
|
+
{
|
|
1135
|
+
name: propCtx.children.propKey[0],
|
|
1136
|
+
value: propCtx.children.propValue[0]
|
|
1137
|
+
}
|
|
1138
|
+
);
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return ast;
|
|
1143
|
+
}
|
|
1144
|
+
definedComponent(ctx) {
|
|
1145
|
+
const name = ctx.componentName[0].image.slice(1, -1);
|
|
1146
|
+
this.definedComponentNames.add(name);
|
|
1147
|
+
const tokens = {
|
|
1148
|
+
keyword: ctx.Define[0],
|
|
1149
|
+
name: ctx.componentName[0],
|
|
1150
|
+
body: ctx.RCurly[0]
|
|
1151
|
+
};
|
|
1152
|
+
let body;
|
|
1153
|
+
const ast = {
|
|
1154
|
+
type: "definedComponent",
|
|
1155
|
+
name,
|
|
1156
|
+
body: {}
|
|
1157
|
+
// Will be filled after push
|
|
1158
|
+
};
|
|
1159
|
+
if (this.sourceMapBuilder) {
|
|
1160
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1161
|
+
"component-definition",
|
|
1162
|
+
tokens,
|
|
1163
|
+
{ name }
|
|
1164
|
+
);
|
|
1165
|
+
ast._meta = { nodeId };
|
|
1166
|
+
this.sourceMapBuilder.pushParent(nodeId);
|
|
1167
|
+
}
|
|
1168
|
+
if (ctx.layout && ctx.layout.length > 0) {
|
|
1169
|
+
body = this.visit(ctx.layout[0]);
|
|
1170
|
+
} else if (ctx.component && ctx.component.length > 0) {
|
|
1171
|
+
body = this.visit(ctx.component[0]);
|
|
1172
|
+
} else {
|
|
1173
|
+
throw new Error(`Defined component "${name}" must contain either a layout or component`);
|
|
1174
|
+
}
|
|
1175
|
+
ast.body = body;
|
|
1176
|
+
if (this.sourceMapBuilder) {
|
|
1177
|
+
this.sourceMapBuilder.popParent();
|
|
1178
|
+
}
|
|
1179
|
+
return ast;
|
|
1180
|
+
}
|
|
1181
|
+
// Override themeDecl to capture theme block in SourceMap
|
|
1182
|
+
themeDecl(ctx) {
|
|
1183
|
+
const theme = {};
|
|
1184
|
+
const tokens = {
|
|
1185
|
+
keyword: ctx.Theme[0],
|
|
1186
|
+
body: ctx.RCurly[0]
|
|
1187
|
+
};
|
|
1188
|
+
if (this.sourceMapBuilder) {
|
|
1189
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1190
|
+
"theme",
|
|
1191
|
+
tokens,
|
|
1192
|
+
{ name: "theme" }
|
|
1193
|
+
);
|
|
1194
|
+
if (ctx.themeProperty) {
|
|
1195
|
+
ctx.themeProperty.forEach((propCtx) => {
|
|
1196
|
+
const { key, value } = this.visit(propCtx);
|
|
1197
|
+
theme[key] = value;
|
|
1198
|
+
this.sourceMapBuilder.addProperty(
|
|
1199
|
+
nodeId,
|
|
1200
|
+
key,
|
|
1201
|
+
value,
|
|
1202
|
+
{
|
|
1203
|
+
name: propCtx.children.themeKey[0],
|
|
1204
|
+
value: propCtx.children.themeValue[0]
|
|
1205
|
+
}
|
|
1206
|
+
);
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
} else {
|
|
1210
|
+
if (ctx.themeProperty) {
|
|
1211
|
+
ctx.themeProperty.forEach((prop) => {
|
|
1212
|
+
const { key, value } = this.visit(prop);
|
|
1213
|
+
theme[key] = value;
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return theme;
|
|
1218
|
+
}
|
|
1219
|
+
// Override mocksDecl to capture mocks block in SourceMap
|
|
1220
|
+
mocksDecl(ctx) {
|
|
1221
|
+
const mocks = {};
|
|
1222
|
+
const tokens = {
|
|
1223
|
+
keyword: ctx.Mocks[0],
|
|
1224
|
+
body: ctx.RCurly[0]
|
|
1225
|
+
};
|
|
1226
|
+
if (this.sourceMapBuilder) {
|
|
1227
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1228
|
+
"mocks",
|
|
1229
|
+
tokens,
|
|
1230
|
+
{ name: "mocks" }
|
|
1231
|
+
);
|
|
1232
|
+
if (ctx.mockEntry) {
|
|
1233
|
+
ctx.mockEntry.forEach((entryCtx) => {
|
|
1234
|
+
const { key, value } = this.visit(entryCtx);
|
|
1235
|
+
mocks[key] = value;
|
|
1236
|
+
this.sourceMapBuilder.addProperty(
|
|
1237
|
+
nodeId,
|
|
1238
|
+
key,
|
|
1239
|
+
value,
|
|
1240
|
+
{
|
|
1241
|
+
name: entryCtx.children.mockKey[0],
|
|
1242
|
+
value: entryCtx.children.mockValue[0]
|
|
1243
|
+
}
|
|
1244
|
+
);
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
} else {
|
|
1248
|
+
if (ctx.mockEntry) {
|
|
1249
|
+
ctx.mockEntry.forEach((entry) => {
|
|
1250
|
+
const { key, value } = this.visit(entry);
|
|
1251
|
+
mocks[key] = value;
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return mocks;
|
|
1256
|
+
}
|
|
1257
|
+
// Override colorsDecl to capture colors block in SourceMap
|
|
1258
|
+
colorsDecl(ctx) {
|
|
1259
|
+
const colors = {};
|
|
1260
|
+
const tokens = {
|
|
1261
|
+
keyword: ctx.Colors[0],
|
|
1262
|
+
body: ctx.RCurly[0]
|
|
1263
|
+
};
|
|
1264
|
+
if (this.sourceMapBuilder) {
|
|
1265
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1266
|
+
"colors",
|
|
1267
|
+
tokens,
|
|
1268
|
+
{ name: "colors" }
|
|
1269
|
+
);
|
|
1270
|
+
if (ctx.colorEntry) {
|
|
1271
|
+
ctx.colorEntry.forEach((entryCtx) => {
|
|
1272
|
+
const { key, value } = this.visit(entryCtx);
|
|
1273
|
+
colors[key] = value;
|
|
1274
|
+
this.sourceMapBuilder.addProperty(
|
|
1275
|
+
nodeId,
|
|
1276
|
+
key,
|
|
1277
|
+
value,
|
|
1278
|
+
{
|
|
1279
|
+
name: entryCtx.children.colorKey[0],
|
|
1280
|
+
value: entryCtx.children.colorValue[0]
|
|
1281
|
+
}
|
|
1282
|
+
);
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
} else {
|
|
1286
|
+
if (ctx.colorEntry) {
|
|
1287
|
+
ctx.colorEntry.forEach((entry) => {
|
|
1288
|
+
const { key, value } = this.visit(entry);
|
|
1289
|
+
colors[key] = value;
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return colors;
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
470
1296
|
var visitor = new WireDSLVisitor();
|
|
471
1297
|
function parseWireDSL(input) {
|
|
472
1298
|
const lexResult = WireDSLLexer.tokenize(input);
|
|
@@ -484,6 +1310,30 @@ ${parserInstance.errors.map((e) => e.message).join("\n")}`);
|
|
|
484
1310
|
validateComponentDefinitionCycles(ast);
|
|
485
1311
|
return ast;
|
|
486
1312
|
}
|
|
1313
|
+
function parseWireDSLWithSourceMap(input, filePath = "<input>") {
|
|
1314
|
+
const lexResult = WireDSLLexer.tokenize(input);
|
|
1315
|
+
if (lexResult.errors.length > 0) {
|
|
1316
|
+
throw new Error(`Lexer errors:
|
|
1317
|
+
${lexResult.errors.map((e) => e.message).join("\n")}`);
|
|
1318
|
+
}
|
|
1319
|
+
parserInstance.input = lexResult.tokens;
|
|
1320
|
+
const cst = parserInstance.project();
|
|
1321
|
+
if (parserInstance.errors.length > 0) {
|
|
1322
|
+
throw new Error(`Parser errors:
|
|
1323
|
+
${parserInstance.errors.map((e) => e.message).join("\n")}`);
|
|
1324
|
+
}
|
|
1325
|
+
const sourceMapBuilder = new SourceMapBuilder(filePath, input);
|
|
1326
|
+
const visitorWithSourceMap = new WireDSLVisitorWithSourceMap(sourceMapBuilder);
|
|
1327
|
+
const ast = visitorWithSourceMap.visit(cst);
|
|
1328
|
+
validateComponentDefinitionCycles(ast);
|
|
1329
|
+
const sourceMap = sourceMapBuilder.build();
|
|
1330
|
+
return {
|
|
1331
|
+
ast,
|
|
1332
|
+
sourceMap,
|
|
1333
|
+
errors: []
|
|
1334
|
+
// No errors if we got here (errors throw exceptions)
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
487
1337
|
function validateComponentDefinitionCycles(ast) {
|
|
488
1338
|
if (!ast.definedComponents || ast.definedComponents.length === 0) {
|
|
489
1339
|
return;
|
|
@@ -584,7 +1434,8 @@ var IRStyleSchema = z.object({
|
|
|
584
1434
|
background: z.string().optional()
|
|
585
1435
|
});
|
|
586
1436
|
var IRMetaSchema = z.object({
|
|
587
|
-
source: z.string().optional()
|
|
1437
|
+
source: z.string().optional(),
|
|
1438
|
+
nodeId: z.string().optional()
|
|
588
1439
|
});
|
|
589
1440
|
var IRContainerNodeSchema = z.object({
|
|
590
1441
|
id: z.string(),
|
|
@@ -830,7 +1681,10 @@ Define these components with: define Component "Name" { ... }`
|
|
|
830
1681
|
params: this.cleanParams(layout.params),
|
|
831
1682
|
children: childRefs,
|
|
832
1683
|
style,
|
|
833
|
-
meta: {
|
|
1684
|
+
meta: {
|
|
1685
|
+
nodeId: layout._meta?.nodeId
|
|
1686
|
+
// Pass SourceMap nodeId from AST
|
|
1687
|
+
}
|
|
834
1688
|
};
|
|
835
1689
|
this.nodes[nodeId] = containerNode;
|
|
836
1690
|
return nodeId;
|
|
@@ -856,7 +1710,11 @@ Define these components with: define Component "Name" { ... }`
|
|
|
856
1710
|
children: childRefs,
|
|
857
1711
|
style: { padding: "none" },
|
|
858
1712
|
// Cells have no padding by default - grid gap handles spacing
|
|
859
|
-
meta: {
|
|
1713
|
+
meta: {
|
|
1714
|
+
source: "cell",
|
|
1715
|
+
nodeId: cell._meta?.nodeId
|
|
1716
|
+
// Pass SourceMap nodeId from AST
|
|
1717
|
+
}
|
|
860
1718
|
};
|
|
861
1719
|
this.nodes[nodeId] = containerNode;
|
|
862
1720
|
return nodeId;
|
|
@@ -908,7 +1766,10 @@ Define these components with: define Component "Name" { ... }`
|
|
|
908
1766
|
componentType: component.componentType,
|
|
909
1767
|
props: component.props,
|
|
910
1768
|
style: {},
|
|
911
|
-
meta: {
|
|
1769
|
+
meta: {
|
|
1770
|
+
nodeId: component._meta?.nodeId
|
|
1771
|
+
// Pass SourceMap nodeId from AST
|
|
1772
|
+
}
|
|
912
1773
|
};
|
|
913
1774
|
this.nodes[nodeId] = componentNode;
|
|
914
1775
|
return nodeId;
|
|
@@ -1861,15 +2722,30 @@ var SVGRenderer = class {
|
|
|
1861
2722
|
if (!node || !pos) return;
|
|
1862
2723
|
this.renderedNodeIds.add(nodeId);
|
|
1863
2724
|
if (node.kind === "container") {
|
|
2725
|
+
const containerGroup = [];
|
|
2726
|
+
const hasNodeId = node.meta?.nodeId;
|
|
2727
|
+
if (hasNodeId) {
|
|
2728
|
+
containerGroup.push(`<g${this.getDataNodeId(node)}>`);
|
|
2729
|
+
}
|
|
2730
|
+
const needsClickableArea = hasNodeId && node.containerType !== "panel" && node.containerType !== "card";
|
|
2731
|
+
if (needsClickableArea) {
|
|
2732
|
+
containerGroup.push(
|
|
2733
|
+
`<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="transparent" stroke="none" pointer-events="all"/>`
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
1864
2736
|
if (node.containerType === "panel") {
|
|
1865
|
-
this.renderPanelBorder(node, pos,
|
|
2737
|
+
this.renderPanelBorder(node, pos, containerGroup);
|
|
1866
2738
|
}
|
|
1867
2739
|
if (node.containerType === "card") {
|
|
1868
|
-
this.renderCardBorder(node, pos,
|
|
2740
|
+
this.renderCardBorder(node, pos, containerGroup);
|
|
1869
2741
|
}
|
|
1870
2742
|
node.children.forEach((childRef) => {
|
|
1871
|
-
this.renderNode(childRef.ref,
|
|
2743
|
+
this.renderNode(childRef.ref, containerGroup);
|
|
1872
2744
|
});
|
|
2745
|
+
if (hasNodeId) {
|
|
2746
|
+
containerGroup.push("</g>");
|
|
2747
|
+
}
|
|
2748
|
+
output.push(...containerGroup);
|
|
1873
2749
|
} else if (node.kind === "component") {
|
|
1874
2750
|
const componentSvg = this.renderComponent(node, pos);
|
|
1875
2751
|
if (componentSvg) {
|
|
@@ -1946,7 +2822,7 @@ var SVGRenderer = class {
|
|
|
1946
2822
|
renderHeading(node, pos) {
|
|
1947
2823
|
const text = String(node.props.text || "Heading");
|
|
1948
2824
|
const fontSize = 20;
|
|
1949
|
-
return `<g>
|
|
2825
|
+
return `<g${this.getDataNodeId(node)}>
|
|
1950
2826
|
<text x="${pos.x}" y="${pos.y + pos.height / 2 + 6}"
|
|
1951
2827
|
font-family="system-ui, -apple-system, sans-serif"
|
|
1952
2828
|
font-size="${fontSize}"
|
|
@@ -1964,7 +2840,7 @@ var SVGRenderer = class {
|
|
|
1964
2840
|
const fontSizeMap = { "sm": 12, "md": 14, "lg": 16 };
|
|
1965
2841
|
const fontSize = fontSizeMap[size] || 14;
|
|
1966
2842
|
const buttonWidth = Math.max(pos.width, 60);
|
|
1967
|
-
return `<g>
|
|
2843
|
+
return `<g${this.getDataNodeId(node)}>
|
|
1968
2844
|
<rect x="${pos.x}" y="${pos.y}"
|
|
1969
2845
|
width="${buttonWidth}" height="${pos.height}"
|
|
1970
2846
|
rx="6"
|
|
@@ -1982,7 +2858,7 @@ var SVGRenderer = class {
|
|
|
1982
2858
|
renderInput(node, pos) {
|
|
1983
2859
|
const label = String(node.props.label || "");
|
|
1984
2860
|
const placeholder = String(node.props.placeholder || "");
|
|
1985
|
-
return `<g>
|
|
2861
|
+
return `<g${this.getDataNodeId(node)}>
|
|
1986
2862
|
${label ? `<text x="${pos.x + 8}" y="${pos.y - 6}"
|
|
1987
2863
|
font-family="system-ui, -apple-system, sans-serif"
|
|
1988
2864
|
font-size="12"
|
|
@@ -2014,7 +2890,7 @@ var SVGRenderer = class {
|
|
|
2014
2890
|
} else {
|
|
2015
2891
|
titleY = pos.y + pos.height / 2 + titleLineHeight / 2 - 4;
|
|
2016
2892
|
}
|
|
2017
|
-
let svg = `<g>
|
|
2893
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
2018
2894
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2019
2895
|
width="${pos.width}" height="${pos.height}"
|
|
2020
2896
|
fill="${this.renderTheme.cardBg}"
|
|
@@ -2147,7 +3023,7 @@ var SVGRenderer = class {
|
|
|
2147
3023
|
});
|
|
2148
3024
|
mockRows.push(row);
|
|
2149
3025
|
}
|
|
2150
|
-
let svg = `<g>
|
|
3026
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
2151
3027
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2152
3028
|
width="${pos.width}" height="${pos.height}"
|
|
2153
3029
|
rx="8"
|
|
@@ -2251,7 +3127,7 @@ var SVGRenderer = class {
|
|
|
2251
3127
|
}
|
|
2252
3128
|
renderChartPlaceholder(node, pos) {
|
|
2253
3129
|
const type = String(node.props.type || "bar");
|
|
2254
|
-
return `<g>
|
|
3130
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2255
3131
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2256
3132
|
width="${pos.width}" height="${pos.height}"
|
|
2257
3133
|
rx="8"
|
|
@@ -2271,7 +3147,7 @@ var SVGRenderer = class {
|
|
|
2271
3147
|
renderText(node, pos) {
|
|
2272
3148
|
const text = String(node.props.content || "Text content");
|
|
2273
3149
|
const fontSize = 14;
|
|
2274
|
-
return `<g>
|
|
3150
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2275
3151
|
<text x="${pos.x}" y="${pos.y + 16}"
|
|
2276
3152
|
font-family="system-ui, -apple-system, sans-serif"
|
|
2277
3153
|
font-size="${fontSize}"
|
|
@@ -2280,7 +3156,7 @@ var SVGRenderer = class {
|
|
|
2280
3156
|
}
|
|
2281
3157
|
renderLabel(node, pos) {
|
|
2282
3158
|
const text = String(node.props.text || "Label");
|
|
2283
|
-
return `<g>
|
|
3159
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2284
3160
|
<text x="${pos.x}" y="${pos.y + 12}"
|
|
2285
3161
|
font-family="system-ui, -apple-system, sans-serif"
|
|
2286
3162
|
font-size="12"
|
|
@@ -2289,7 +3165,7 @@ var SVGRenderer = class {
|
|
|
2289
3165
|
}
|
|
2290
3166
|
renderCode(node, pos) {
|
|
2291
3167
|
const code = String(node.props.code || "const x = 42;");
|
|
2292
|
-
return `<g>
|
|
3168
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2293
3169
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2294
3170
|
width="${pos.width}" height="${pos.height}"
|
|
2295
3171
|
rx="4"
|
|
@@ -2308,7 +3184,7 @@ var SVGRenderer = class {
|
|
|
2308
3184
|
renderTextarea(node, pos) {
|
|
2309
3185
|
const label = String(node.props.label || "");
|
|
2310
3186
|
const placeholder = String(node.props.placeholder || "Enter text...");
|
|
2311
|
-
return `<g>
|
|
3187
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2312
3188
|
${label ? `<text x="${pos.x}" y="${pos.y - 6}"
|
|
2313
3189
|
font-family="system-ui, -apple-system, sans-serif"
|
|
2314
3190
|
font-size="12"
|
|
@@ -2328,7 +3204,7 @@ var SVGRenderer = class {
|
|
|
2328
3204
|
renderSelect(node, pos) {
|
|
2329
3205
|
const label = String(node.props.label || "");
|
|
2330
3206
|
const placeholder = String(node.props.placeholder || "Select...");
|
|
2331
|
-
return `<g>
|
|
3207
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2332
3208
|
${label ? `<text x="${pos.x}" y="${pos.y - 6}"
|
|
2333
3209
|
font-family="system-ui, -apple-system, sans-serif"
|
|
2334
3210
|
font-size="12"
|
|
@@ -2354,7 +3230,7 @@ var SVGRenderer = class {
|
|
|
2354
3230
|
const checked = String(node.props.checked || "false").toLowerCase() === "true";
|
|
2355
3231
|
const checkboxSize = 18;
|
|
2356
3232
|
const checkboxY = pos.y + pos.height / 2 - checkboxSize / 2;
|
|
2357
|
-
return `<g>
|
|
3233
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2358
3234
|
<rect x="${pos.x}" y="${checkboxY}"
|
|
2359
3235
|
width="${checkboxSize}" height="${checkboxSize}"
|
|
2360
3236
|
rx="4"
|
|
@@ -2377,7 +3253,7 @@ var SVGRenderer = class {
|
|
|
2377
3253
|
const checked = String(node.props.checked || "false").toLowerCase() === "true";
|
|
2378
3254
|
const radioSize = 16;
|
|
2379
3255
|
const radioY = pos.y + pos.height / 2 - radioSize / 2;
|
|
2380
|
-
return `<g>
|
|
3256
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2381
3257
|
<circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
|
|
2382
3258
|
r="${radioSize / 2}"
|
|
2383
3259
|
fill="${this.renderTheme.cardBg}"
|
|
@@ -2398,7 +3274,7 @@ var SVGRenderer = class {
|
|
|
2398
3274
|
const toggleWidth = 40;
|
|
2399
3275
|
const toggleHeight = 20;
|
|
2400
3276
|
const toggleY = pos.y + pos.height / 2 - toggleHeight / 2;
|
|
2401
|
-
return `<g>
|
|
3277
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2402
3278
|
<rect x="${pos.x}" y="${toggleY}"
|
|
2403
3279
|
width="${toggleWidth}" height="${toggleHeight}"
|
|
2404
3280
|
rx="10"
|
|
@@ -2430,7 +3306,7 @@ var SVGRenderer = class {
|
|
|
2430
3306
|
const itemHeight = 40;
|
|
2431
3307
|
const padding = 16;
|
|
2432
3308
|
const titleHeight = 40;
|
|
2433
|
-
let svg = `<g>
|
|
3309
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
2434
3310
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2435
3311
|
width="${pos.width}" height="${pos.height}"
|
|
2436
3312
|
fill="${this.renderTheme.cardBg}"
|
|
@@ -2465,7 +3341,7 @@ var SVGRenderer = class {
|
|
|
2465
3341
|
const itemsStr = String(node.props.items || "");
|
|
2466
3342
|
const tabs = itemsStr ? itemsStr.split(",").map((t) => t.trim()) : ["Tab 1", "Tab 2", "Tab 3"];
|
|
2467
3343
|
const tabWidth = pos.width / tabs.length;
|
|
2468
|
-
let svg = `<g>
|
|
3344
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
2469
3345
|
<!-- Tab headers -->`;
|
|
2470
3346
|
tabs.forEach((tab, i) => {
|
|
2471
3347
|
const tabX = pos.x + i * tabWidth;
|
|
@@ -2494,7 +3370,7 @@ var SVGRenderer = class {
|
|
|
2494
3370
|
return svg;
|
|
2495
3371
|
}
|
|
2496
3372
|
renderDivider(node, pos) {
|
|
2497
|
-
return `<g>
|
|
3373
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2498
3374
|
<line x1="${pos.x}" y1="${pos.y + pos.height / 2}"
|
|
2499
3375
|
x2="${pos.x + pos.width}" y2="${pos.y + pos.height / 2}"
|
|
2500
3376
|
stroke="${this.renderTheme.border}"
|
|
@@ -2514,7 +3390,7 @@ var SVGRenderer = class {
|
|
|
2514
3390
|
success: "#10B981"
|
|
2515
3391
|
};
|
|
2516
3392
|
const bgColor = typeColors[type] || typeColors.info;
|
|
2517
|
-
return `<g>
|
|
3393
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2518
3394
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2519
3395
|
width="${pos.width}" height="${pos.height}"
|
|
2520
3396
|
rx="6"
|
|
@@ -2536,7 +3412,7 @@ var SVGRenderer = class {
|
|
|
2536
3412
|
const variant = String(node.props.variant || "default");
|
|
2537
3413
|
const bgColor = variant === "primary" ? this.renderTheme.primary : this.renderTheme.border;
|
|
2538
3414
|
const textColor = variant === "primary" ? "white" : this.renderTheme.text;
|
|
2539
|
-
return `<g>
|
|
3415
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2540
3416
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2541
3417
|
width="${pos.width}" height="${pos.height}"
|
|
2542
3418
|
rx="${pos.height / 2}"
|
|
@@ -2557,7 +3433,7 @@ var SVGRenderer = class {
|
|
|
2557
3433
|
const overlayHeight = Math.max(this.options.height, this.calculateContentHeight());
|
|
2558
3434
|
const modalX = (this.options.width - pos.width) / 2;
|
|
2559
3435
|
const modalY = Math.max(40, (overlayHeight - pos.height) / 2);
|
|
2560
|
-
return `<g>
|
|
3436
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2561
3437
|
<!-- Modal backdrop -->
|
|
2562
3438
|
<rect x="0" y="0"
|
|
2563
3439
|
width="${this.options.width}" height="${overlayHeight}"
|
|
@@ -2610,7 +3486,7 @@ var SVGRenderer = class {
|
|
|
2610
3486
|
const padding = 12;
|
|
2611
3487
|
const itemHeight = 36;
|
|
2612
3488
|
const titleHeight = title ? 40 : 0;
|
|
2613
|
-
let svg = `<g>
|
|
3489
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
2614
3490
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2615
3491
|
width="${pos.width}" height="${pos.height}"
|
|
2616
3492
|
rx="8"
|
|
@@ -2645,7 +3521,7 @@ var SVGRenderer = class {
|
|
|
2645
3521
|
return svg;
|
|
2646
3522
|
}
|
|
2647
3523
|
renderGenericComponent(node, pos) {
|
|
2648
|
-
return `<g>
|
|
3524
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2649
3525
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2650
3526
|
width="${pos.width}" height="${pos.height}"
|
|
2651
3527
|
rx="4"
|
|
@@ -2679,7 +3555,7 @@ var SVGRenderer = class {
|
|
|
2679
3555
|
const titleY = innerY + topGap + titleSize;
|
|
2680
3556
|
const valueY = titleY + valueGap + valueSize;
|
|
2681
3557
|
const captionY = valueY + captionGap + captionSize;
|
|
2682
|
-
let svg = `<g>
|
|
3558
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
2683
3559
|
<!-- StatCard Background -->
|
|
2684
3560
|
<rect x="${pos.x}" y="${pos.y}"
|
|
2685
3561
|
width="${pos.width}" height="${pos.height}"
|
|
@@ -2733,7 +3609,7 @@ var SVGRenderer = class {
|
|
|
2733
3609
|
const offsetX = pos.x + (pos.width - iconWidth) / 2;
|
|
2734
3610
|
const offsetY = pos.y + (pos.height - iconHeight) / 2;
|
|
2735
3611
|
let svgContent = "";
|
|
2736
|
-
let svg = `<g>
|
|
3612
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
2737
3613
|
<!-- Image Background -->
|
|
2738
3614
|
<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="#E8E8E8"/>`;
|
|
2739
3615
|
if (["landscape", "portrait", "square"].includes(placeholder)) {
|
|
@@ -2806,7 +3682,7 @@ var SVGRenderer = class {
|
|
|
2806
3682
|
const separatorWidth = 20;
|
|
2807
3683
|
const itemSpacing = 8;
|
|
2808
3684
|
let currentX = pos.x;
|
|
2809
|
-
let svg =
|
|
3685
|
+
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
2810
3686
|
items.forEach((item, index) => {
|
|
2811
3687
|
const isLast = index === items.length - 1;
|
|
2812
3688
|
const textColor = isLast ? this.renderTheme.text : this.renderTheme.textMuted;
|
|
@@ -2839,7 +3715,7 @@ var SVGRenderer = class {
|
|
|
2839
3715
|
const itemHeight = 40;
|
|
2840
3716
|
const fontSize = 14;
|
|
2841
3717
|
const activeIndex = Number(node.props.active || 0);
|
|
2842
|
-
let svg =
|
|
3718
|
+
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
2843
3719
|
items.forEach((item, index) => {
|
|
2844
3720
|
const itemY = pos.y + index * itemHeight;
|
|
2845
3721
|
const isActive = index === activeIndex;
|
|
@@ -2883,7 +3759,7 @@ var SVGRenderer = class {
|
|
|
2883
3759
|
const size = String(node.props.size || "md");
|
|
2884
3760
|
const iconSvg = getIcon(iconType);
|
|
2885
3761
|
if (!iconSvg) {
|
|
2886
|
-
return `<g>
|
|
3762
|
+
return `<g${this.getDataNodeId(node)}>
|
|
2887
3763
|
<!-- Icon not found: ${iconType} -->
|
|
2888
3764
|
<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"/>
|
|
2889
3765
|
<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>
|
|
@@ -2894,7 +3770,7 @@ var SVGRenderer = class {
|
|
|
2894
3770
|
const iconColor = "rgba(30, 41, 59, 0.75)";
|
|
2895
3771
|
const offsetX = pos.x + (pos.width - iconSize) / 2;
|
|
2896
3772
|
const offsetY = pos.y + (pos.height - iconSize) / 2;
|
|
2897
|
-
const wrappedSvg = `<g transform="translate(${offsetX}, ${offsetY})">
|
|
3773
|
+
const wrappedSvg = `<g${this.getDataNodeId(node)} transform="translate(${offsetX}, ${offsetY})">
|
|
2898
3774
|
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2899
3775
|
${this.extractSvgContent(iconSvg)}
|
|
2900
3776
|
</svg>
|
|
@@ -2929,7 +3805,7 @@ var SVGRenderer = class {
|
|
|
2929
3805
|
const sizeMap = { "sm": 28, "md": 32, "lg": 40 };
|
|
2930
3806
|
const buttonSize = sizeMap[size] || 32;
|
|
2931
3807
|
const radius = 6;
|
|
2932
|
-
let svg = `<g opacity="${opacity}">
|
|
3808
|
+
let svg = `<g${this.getDataNodeId(node)} opacity="${opacity}">
|
|
2933
3809
|
<!-- IconButton background -->
|
|
2934
3810
|
<rect x="${pos.x}" y="${pos.y}" width="${buttonSize}" height="${buttonSize}" rx="${radius}" fill="${bgColor}" stroke="${borderColor}" stroke-width="1"/>`;
|
|
2935
3811
|
if (iconSvg) {
|
|
@@ -2971,6 +3847,13 @@ var SVGRenderer = class {
|
|
|
2971
3847
|
escapeXml(text) {
|
|
2972
3848
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2973
3849
|
}
|
|
3850
|
+
/**
|
|
3851
|
+
* Get data-node-id attribute string for SVG elements
|
|
3852
|
+
* Enables bidirectional selection between code and canvas
|
|
3853
|
+
*/
|
|
3854
|
+
getDataNodeId(node) {
|
|
3855
|
+
return node.meta.nodeId ? ` data-node-id="${node.meta.nodeId}"` : "";
|
|
3856
|
+
}
|
|
2974
3857
|
};
|
|
2975
3858
|
function renderToSVG(ir, layout, options) {
|
|
2976
3859
|
const renderer = new SVGRenderer(ir, layout, options);
|
|
@@ -2988,17 +3871,230 @@ function buildSVG(component) {
|
|
|
2988
3871
|
return createSVGElement(component.tag, component.attrs, children);
|
|
2989
3872
|
}
|
|
2990
3873
|
|
|
3874
|
+
// src/sourcemap/hash.ts
|
|
3875
|
+
function simpleHash(str) {
|
|
3876
|
+
let hash = 5381;
|
|
3877
|
+
for (let i = 0; i < str.length; i++) {
|
|
3878
|
+
const char = str.charCodeAt(i);
|
|
3879
|
+
hash = (hash << 5) + hash + char;
|
|
3880
|
+
hash = hash & hash;
|
|
3881
|
+
}
|
|
3882
|
+
return Math.abs(hash);
|
|
3883
|
+
}
|
|
3884
|
+
function generateStableNodeId(type, filePath, line, column, indexInParent, name) {
|
|
3885
|
+
const content = [
|
|
3886
|
+
filePath,
|
|
3887
|
+
`${line}:${column}`,
|
|
3888
|
+
type,
|
|
3889
|
+
`idx:${indexInParent}`,
|
|
3890
|
+
name || ""
|
|
3891
|
+
].join("|");
|
|
3892
|
+
const hashNum = simpleHash(content);
|
|
3893
|
+
const hashStr = hashNum.toString(36);
|
|
3894
|
+
return `node-${hashStr}-${type}`;
|
|
3895
|
+
}
|
|
3896
|
+
function isValidNodeId(id) {
|
|
3897
|
+
return /^node-[a-z0-9]+-[a-z-]+$/.test(id);
|
|
3898
|
+
}
|
|
3899
|
+
function getTypeFromNodeId(nodeId) {
|
|
3900
|
+
const match = nodeId.match(/^node-[a-z0-9]+-(.+)$/);
|
|
3901
|
+
if (!match) return null;
|
|
3902
|
+
return match[1];
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
// src/sourcemap/resolver.ts
|
|
3906
|
+
var SourceMapResolver = class {
|
|
3907
|
+
constructor(sourceMap) {
|
|
3908
|
+
this.nodeMap = /* @__PURE__ */ new Map();
|
|
3909
|
+
this.childrenMap = /* @__PURE__ */ new Map();
|
|
3910
|
+
this.positionIndex = sourceMap;
|
|
3911
|
+
for (const entry of sourceMap) {
|
|
3912
|
+
this.nodeMap.set(entry.nodeId, entry);
|
|
3913
|
+
}
|
|
3914
|
+
for (const entry of sourceMap) {
|
|
3915
|
+
if (entry.parentId) {
|
|
3916
|
+
const siblings = this.childrenMap.get(entry.parentId) || [];
|
|
3917
|
+
siblings.push(entry);
|
|
3918
|
+
this.childrenMap.set(entry.parentId, siblings);
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
/**
|
|
3923
|
+
* Find node by ID (Canvas → Code)
|
|
3924
|
+
*
|
|
3925
|
+
* @example
|
|
3926
|
+
* // User clicks SVG element with data-node-id="component-button-0"
|
|
3927
|
+
* const node = resolver.getNodeById("component-button-0");
|
|
3928
|
+
* editor.revealRange(node.range); // Jump to code
|
|
3929
|
+
*/
|
|
3930
|
+
getNodeById(nodeId) {
|
|
3931
|
+
return this.nodeMap.get(nodeId) || null;
|
|
3932
|
+
}
|
|
3933
|
+
/**
|
|
3934
|
+
* Find node at position (Code → Canvas)
|
|
3935
|
+
* Returns the most specific (deepest) node containing the position
|
|
3936
|
+
*
|
|
3937
|
+
* @example
|
|
3938
|
+
* // User clicks code at line 5, column 10
|
|
3939
|
+
* const node = resolver.getNodeByPosition(5, 10);
|
|
3940
|
+
* canvas.highlightElement(node.nodeId); // Highlight in canvas
|
|
3941
|
+
*/
|
|
3942
|
+
getNodeByPosition(line, column) {
|
|
3943
|
+
const candidates = [];
|
|
3944
|
+
for (const entry of this.positionIndex) {
|
|
3945
|
+
if (this.containsPosition(entry, line, column)) {
|
|
3946
|
+
const depth = this.calculateDepth(entry);
|
|
3947
|
+
candidates.push({ ...entry, depth });
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
if (candidates.length === 0) {
|
|
3951
|
+
return null;
|
|
3952
|
+
}
|
|
3953
|
+
candidates.sort((a, b) => b.depth - a.depth);
|
|
3954
|
+
return candidates[0];
|
|
3955
|
+
}
|
|
3956
|
+
/**
|
|
3957
|
+
* Get all child nodes of a parent
|
|
3958
|
+
*
|
|
3959
|
+
* @example
|
|
3960
|
+
* const children = resolver.getChildren("layout-stack-0");
|
|
3961
|
+
* // Returns: [component-button-0, component-input-0, ...]
|
|
3962
|
+
*/
|
|
3963
|
+
getChildren(nodeId) {
|
|
3964
|
+
return this.childrenMap.get(nodeId) || [];
|
|
3965
|
+
}
|
|
3966
|
+
/**
|
|
3967
|
+
* Get parent node
|
|
3968
|
+
*
|
|
3969
|
+
* @example
|
|
3970
|
+
* const parent = resolver.getParent("component-button-0");
|
|
3971
|
+
* // Returns: layout-stack-0
|
|
3972
|
+
*/
|
|
3973
|
+
getParent(nodeId) {
|
|
3974
|
+
const node = this.nodeMap.get(nodeId);
|
|
3975
|
+
if (!node || !node.parentId) {
|
|
3976
|
+
return null;
|
|
3977
|
+
}
|
|
3978
|
+
return this.nodeMap.get(node.parentId) || null;
|
|
3979
|
+
}
|
|
3980
|
+
/**
|
|
3981
|
+
* Get all nodes in the SourceMap
|
|
3982
|
+
*/
|
|
3983
|
+
getAllNodes() {
|
|
3984
|
+
return this.positionIndex;
|
|
3985
|
+
}
|
|
3986
|
+
/**
|
|
3987
|
+
* Get all nodes of a specific type
|
|
3988
|
+
*
|
|
3989
|
+
* @example
|
|
3990
|
+
* const buttons = resolver.getNodesByType("component", "Button");
|
|
3991
|
+
*/
|
|
3992
|
+
getNodesByType(type, subtype) {
|
|
3993
|
+
return this.positionIndex.filter((entry) => {
|
|
3994
|
+
if (entry.type !== type) return false;
|
|
3995
|
+
if (subtype) {
|
|
3996
|
+
if (type === "component" && entry.componentType !== subtype) return false;
|
|
3997
|
+
if (type === "layout" && entry.layoutType !== subtype) return false;
|
|
3998
|
+
}
|
|
3999
|
+
return true;
|
|
4000
|
+
});
|
|
4001
|
+
}
|
|
4002
|
+
/**
|
|
4003
|
+
* Get siblings of a node (nodes with same parent)
|
|
4004
|
+
*/
|
|
4005
|
+
getSiblings(nodeId) {
|
|
4006
|
+
const node = this.nodeMap.get(nodeId);
|
|
4007
|
+
if (!node || !node.parentId) {
|
|
4008
|
+
return [];
|
|
4009
|
+
}
|
|
4010
|
+
const siblings = this.getChildren(node.parentId);
|
|
4011
|
+
return siblings.filter((s) => s.nodeId !== nodeId);
|
|
4012
|
+
}
|
|
4013
|
+
/**
|
|
4014
|
+
* Get path from root to node (breadcrumb)
|
|
4015
|
+
*
|
|
4016
|
+
* @example
|
|
4017
|
+
* const path = resolver.getPath("component-button-0");
|
|
4018
|
+
* // Returns: [project, screen-0, layout-stack-0, component-button-0]
|
|
4019
|
+
*/
|
|
4020
|
+
getPath(nodeId) {
|
|
4021
|
+
const path = [];
|
|
4022
|
+
let current = this.nodeMap.get(nodeId);
|
|
4023
|
+
while (current) {
|
|
4024
|
+
path.unshift(current);
|
|
4025
|
+
current = current.parentId ? this.nodeMap.get(current.parentId) : void 0;
|
|
4026
|
+
}
|
|
4027
|
+
return path;
|
|
4028
|
+
}
|
|
4029
|
+
/**
|
|
4030
|
+
* Check if a position is within a node's range
|
|
4031
|
+
*/
|
|
4032
|
+
containsPosition(entry, line, column) {
|
|
4033
|
+
const { range } = entry;
|
|
4034
|
+
if (line < range.start.line || line > range.end.line) {
|
|
4035
|
+
return false;
|
|
4036
|
+
}
|
|
4037
|
+
if (range.start.line === range.end.line) {
|
|
4038
|
+
return column >= range.start.column && column <= range.end.column;
|
|
4039
|
+
}
|
|
4040
|
+
if (line === range.start.line) {
|
|
4041
|
+
return column >= range.start.column;
|
|
4042
|
+
}
|
|
4043
|
+
if (line === range.end.line) {
|
|
4044
|
+
return column <= range.end.column;
|
|
4045
|
+
}
|
|
4046
|
+
return true;
|
|
4047
|
+
}
|
|
4048
|
+
/**
|
|
4049
|
+
* Calculate depth of a node in the tree (0 = root)
|
|
4050
|
+
*/
|
|
4051
|
+
calculateDepth(entry) {
|
|
4052
|
+
let depth = 0;
|
|
4053
|
+
let current = entry;
|
|
4054
|
+
while (current.parentId) {
|
|
4055
|
+
depth++;
|
|
4056
|
+
const parent = this.nodeMap.get(current.parentId);
|
|
4057
|
+
if (!parent) break;
|
|
4058
|
+
current = parent;
|
|
4059
|
+
}
|
|
4060
|
+
return depth;
|
|
4061
|
+
}
|
|
4062
|
+
/**
|
|
4063
|
+
* Get statistics about the SourceMap
|
|
4064
|
+
*/
|
|
4065
|
+
getStats() {
|
|
4066
|
+
const byType = {};
|
|
4067
|
+
let maxDepth = 0;
|
|
4068
|
+
for (const entry of this.positionIndex) {
|
|
4069
|
+
byType[entry.type] = (byType[entry.type] || 0) + 1;
|
|
4070
|
+
const depth = this.calculateDepth(entry);
|
|
4071
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
4072
|
+
}
|
|
4073
|
+
return {
|
|
4074
|
+
totalNodes: this.positionIndex.length,
|
|
4075
|
+
byType,
|
|
4076
|
+
maxDepth
|
|
4077
|
+
};
|
|
4078
|
+
}
|
|
4079
|
+
};
|
|
4080
|
+
|
|
2991
4081
|
// src/index.ts
|
|
2992
4082
|
var version = "0.0.1";
|
|
2993
4083
|
export {
|
|
2994
4084
|
IRGenerator,
|
|
2995
4085
|
LayoutEngine,
|
|
2996
4086
|
SVGRenderer,
|
|
4087
|
+
SourceMapBuilder,
|
|
4088
|
+
SourceMapResolver,
|
|
2997
4089
|
buildSVG,
|
|
2998
4090
|
calculateLayout,
|
|
2999
4091
|
createSVGElement,
|
|
3000
4092
|
generateIR,
|
|
4093
|
+
generateStableNodeId,
|
|
4094
|
+
getTypeFromNodeId,
|
|
4095
|
+
isValidNodeId,
|
|
3001
4096
|
parseWireDSL,
|
|
4097
|
+
parseWireDSLWithSourceMap,
|
|
3002
4098
|
renderToSVG,
|
|
3003
4099
|
resolveGridPosition,
|
|
3004
4100
|
version
|