@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.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: { source: "cell" }
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, output);
2737
+ this.renderPanelBorder(node, pos, containerGroup);
1866
2738
  }
1867
2739
  if (node.containerType === "card") {
1868
- this.renderCardBorder(node, pos, output);
2740
+ this.renderCardBorder(node, pos, containerGroup);
1869
2741
  }
1870
2742
  node.children.forEach((childRef) => {
1871
- this.renderNode(childRef.ref, output);
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 = "<g>";
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 = "<g>";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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