@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 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: { source: "cell" }
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, output);
2779
+ this.renderPanelBorder(node, pos, containerGroup);
1902
2780
  }
1903
2781
  if (node.containerType === "card") {
1904
- this.renderCardBorder(node, pos, output);
2782
+ this.renderCardBorder(node, pos, containerGroup);
1905
2783
  }
1906
2784
  node.children.forEach((childRef) => {
1907
- this.renderNode(childRef.ref, output);
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 = "<g>";
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 = "<g>";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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