wyreframe 0.2.0 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wyreframe",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "ASCII wireframe + interaction DSL to HTML converter with scene transitions",
5
5
  "author": "wickedev",
6
6
  "repository": {
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Types from "../Core/Types.mjs";
4
+ import * as Core__Option from "@rescript/core/src/Core__Option.mjs";
4
5
  import * as ErrorContext from "./ErrorContext.mjs";
5
6
 
6
7
  function getSeverity(code) {
@@ -95,6 +96,99 @@ function getCodeName(code) {
95
96
  }
96
97
  }
97
98
 
99
+ function adjustCodeLineOffset(code, offset) {
100
+ let adjustPos = pos => Types.Position.make(pos.row + offset | 0, pos.col);
101
+ switch (code.TAG) {
102
+ case "InvalidInput" :
103
+ return code;
104
+ case "InvalidStartPosition" :
105
+ return {
106
+ TAG: "InvalidStartPosition",
107
+ _0: adjustPos(code._0)
108
+ };
109
+ case "UncloseBox" :
110
+ return {
111
+ TAG: "UncloseBox",
112
+ corner: adjustPos(code.corner),
113
+ direction: code.direction
114
+ };
115
+ case "MismatchedWidth" :
116
+ return {
117
+ TAG: "MismatchedWidth",
118
+ topLeft: adjustPos(code.topLeft),
119
+ topWidth: code.topWidth,
120
+ bottomWidth: code.bottomWidth
121
+ };
122
+ case "MisalignedPipe" :
123
+ return {
124
+ TAG: "MisalignedPipe",
125
+ position: adjustPos(code.position),
126
+ expectedCol: code.expectedCol,
127
+ actualCol: code.actualCol
128
+ };
129
+ case "OverlappingBoxes" :
130
+ return {
131
+ TAG: "OverlappingBoxes",
132
+ box1Name: code.box1Name,
133
+ box2Name: code.box2Name,
134
+ position: adjustPos(code.position)
135
+ };
136
+ case "InvalidElement" :
137
+ return {
138
+ TAG: "InvalidElement",
139
+ content: code.content,
140
+ position: adjustPos(code.position)
141
+ };
142
+ case "UnclosedBracket" :
143
+ return {
144
+ TAG: "UnclosedBracket",
145
+ opening: adjustPos(code.opening)
146
+ };
147
+ case "EmptyButton" :
148
+ return {
149
+ TAG: "EmptyButton",
150
+ position: adjustPos(code.position)
151
+ };
152
+ case "InvalidInteractionDSL" :
153
+ return {
154
+ TAG: "InvalidInteractionDSL",
155
+ message: code.message,
156
+ position: Core__Option.map(code.position, adjustPos)
157
+ };
158
+ case "UnusualSpacing" :
159
+ return {
160
+ TAG: "UnusualSpacing",
161
+ position: adjustPos(code.position),
162
+ issue: code.issue
163
+ };
164
+ case "DeepNesting" :
165
+ return {
166
+ TAG: "DeepNesting",
167
+ depth: code.depth,
168
+ position: adjustPos(code.position)
169
+ };
170
+ case "MisalignedClosingBorder" :
171
+ return {
172
+ TAG: "MisalignedClosingBorder",
173
+ position: adjustPos(code.position),
174
+ expectedCol: code.expectedCol,
175
+ actualCol: code.actualCol
176
+ };
177
+ }
178
+ }
179
+
180
+ function adjustLineOffset(error, offset) {
181
+ if (offset === 0) {
182
+ return error;
183
+ } else {
184
+ return {
185
+ code: adjustCodeLineOffset(error.code, offset),
186
+ severity: error.severity,
187
+ context: error.context
188
+ };
189
+ }
190
+ }
191
+
98
192
  export {
99
193
  getSeverity,
100
194
  make,
@@ -104,5 +198,7 @@ export {
104
198
  isWarning,
105
199
  isError,
106
200
  getCodeName,
201
+ adjustCodeLineOffset,
202
+ adjustLineOffset,
107
203
  }
108
204
  /* Types Not a pure module */
@@ -174,3 +174,61 @@ let getCodeName = (code: errorCode): string => {
174
174
  | MisalignedClosingBorder(_) => "MisalignedClosingBorder"
175
175
  }
176
176
  }
177
+
178
+ /**
179
+ * Adjust the line offset in an error code's position.
180
+ * This is used to correct line numbers when scene directive lines
181
+ * are stripped before grid processing.
182
+ *
183
+ * @param code - The error code to adjust
184
+ * @param offset - The number of lines to add to the row (e.g., number of directive lines stripped)
185
+ * @returns A new error code with adjusted position
186
+ */
187
+ let adjustCodeLineOffset = (code: errorCode, offset: int): errorCode => {
188
+ let adjustPos = (pos: Position.t): Position.t => {
189
+ Position.make(pos.row + offset, pos.col)
190
+ }
191
+
192
+ switch code {
193
+ | InvalidInput(_) as c => c
194
+ | InvalidStartPosition(pos) => InvalidStartPosition(adjustPos(pos))
195
+ | UncloseBox({corner, direction}) => UncloseBox({corner: adjustPos(corner), direction})
196
+ | MismatchedWidth({topLeft, topWidth, bottomWidth}) =>
197
+ MismatchedWidth({topLeft: adjustPos(topLeft), topWidth, bottomWidth})
198
+ | MisalignedPipe({position, expectedCol, actualCol}) =>
199
+ MisalignedPipe({position: adjustPos(position), expectedCol, actualCol})
200
+ | OverlappingBoxes({box1Name, box2Name, position}) =>
201
+ OverlappingBoxes({box1Name, box2Name, position: adjustPos(position)})
202
+ | InvalidElement({content, position}) =>
203
+ InvalidElement({content, position: adjustPos(position)})
204
+ | UnclosedBracket({opening}) => UnclosedBracket({opening: adjustPos(opening)})
205
+ | EmptyButton({position}) => EmptyButton({position: adjustPos(position)})
206
+ | InvalidInteractionDSL({message, position}) =>
207
+ InvalidInteractionDSL({message, position: position->Option.map(adjustPos)})
208
+ | UnusualSpacing({position, issue}) =>
209
+ UnusualSpacing({position: adjustPos(position), issue})
210
+ | DeepNesting({depth, position}) => DeepNesting({depth, position: adjustPos(position)})
211
+ | MisalignedClosingBorder({position, expectedCol, actualCol}) =>
212
+ MisalignedClosingBorder({position: adjustPos(position), expectedCol, actualCol})
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Adjust the line offset in an error's position.
218
+ * Creates a new error with the position adjusted by the given offset.
219
+ *
220
+ * @param error - The error to adjust
221
+ * @param offset - The number of lines to add to the row
222
+ * @returns A new error with adjusted position
223
+ */
224
+ let adjustLineOffset = (error: t, offset: int): t => {
225
+ if offset === 0 {
226
+ error
227
+ } else {
228
+ {
229
+ code: adjustCodeLineOffset(error.code, offset),
230
+ severity: error.severity,
231
+ context: error.context,
232
+ }
233
+ }
234
+ }
@@ -69,7 +69,7 @@ function mergeInteractionsIntoAST(ast, sceneInteractions) {
69
69
  }
70
70
  }
71
71
 
72
- function parseSingleScene(sceneContent, sceneMetadata, errors) {
72
+ function parseSingleScene(sceneContent, sceneMetadata, lineOffset, errors) {
73
73
  let gridResult = scanGrid(sceneContent);
74
74
  if (gridResult.TAG === "Ok") {
75
75
  let grid = gridResult._0;
@@ -78,12 +78,14 @@ function parseSingleScene(sceneContent, sceneMetadata, errors) {
78
78
  if (shapesResult.TAG === "Ok") {
79
79
  let match = shapesResult._0;
80
80
  match[1].forEach(w => {
81
- errors.push(w);
81
+ let adjusted = ErrorTypes.adjustLineOffset(w, lineOffset);
82
+ errors.push(adjusted);
82
83
  });
83
84
  shapes = match[0];
84
85
  } else {
85
86
  shapesResult._0.forEach(err => {
86
- errors.push(err);
87
+ let adjusted = ErrorTypes.adjustLineOffset(err, lineOffset);
88
+ errors.push(adjusted);
87
89
  });
88
90
  shapes = [];
89
91
  }
@@ -110,7 +112,8 @@ function parseSingleScene(sceneContent, sceneMetadata, errors) {
110
112
  };
111
113
  }
112
114
  gridResult._0.forEach(err => {
113
- errors.push(err);
115
+ let adjusted = ErrorTypes.adjustLineOffset(err, lineOffset);
116
+ errors.push(adjusted);
114
117
  });
115
118
  }
116
119
 
@@ -132,9 +135,9 @@ function parseInternal(wireframe, interactions) {
132
135
  let scenes = [];
133
136
  sceneBlocks.forEach(block => {
134
137
  let lines = block.split("\n");
135
- let match = SemanticParser.parseSceneDirectives(lines);
136
- let sceneContent = match[1].join("\n");
137
- let scene = parseSingleScene(sceneContent, match[0], allIssues);
138
+ let match = SemanticParser.parseSceneDirectivesWithOffset(lines);
139
+ let sceneContent = match.contentLines.join("\n");
140
+ let scene = parseSingleScene(sceneContent, match.metadata, match.lineOffset, allIssues);
138
141
  if (scene !== undefined) {
139
142
  scenes.push(scene);
140
143
  return;
@@ -191,12 +191,14 @@ let mergeInteractionsIntoAST = (
191
191
  *
192
192
  * @param sceneContent ASCII wireframe content for one scene (without directives)
193
193
  * @param sceneMetadata Scene metadata from directives
194
+ * @param lineOffset Number of directive lines stripped (for adjusting error line numbers)
194
195
  * @param errors Accumulator for errors
195
196
  * @returns Parsed scene or None if parsing failed
196
197
  */
197
198
  let parseSingleScene = (
198
199
  sceneContent: string,
199
200
  sceneMetadata: SemanticParser.sceneMetadata,
201
+ lineOffset: int,
200
202
  errors: array<ErrorTypes.t>,
201
203
  ): option<Types.scene> => {
202
204
  // Stage 1: Grid Scanner
@@ -204,7 +206,11 @@ let parseSingleScene = (
204
206
 
205
207
  switch gridResult {
206
208
  | Error(gridErrors) => {
207
- gridErrors->Array.forEach(err => errors->Array.push(err)->ignore)
209
+ // Adjust line numbers by offset before adding to errors
210
+ gridErrors->Array.forEach(err => {
211
+ let adjusted = ErrorTypes.adjustLineOffset(err, lineOffset)
212
+ errors->Array.push(adjusted)->ignore
213
+ })
208
214
  None
209
215
  }
210
216
  | Ok(grid) => {
@@ -213,12 +219,20 @@ let parseSingleScene = (
213
219
 
214
220
  let shapes = switch shapesResult {
215
221
  | Error(shapeErrors) => {
216
- shapeErrors->Array.forEach(err => errors->Array.push(err)->ignore)
222
+ // Adjust line numbers by offset before adding to errors
223
+ shapeErrors->Array.forEach(err => {
224
+ let adjusted = ErrorTypes.adjustLineOffset(err, lineOffset)
225
+ errors->Array.push(adjusted)->ignore
226
+ })
217
227
  []
218
228
  }
219
229
  | Ok((boxes, warnings)) => {
220
230
  // Collect warnings (non-fatal issues like misaligned borders)
221
- warnings->Array.forEach(w => errors->Array.push(w)->ignore)
231
+ // Adjust line numbers by offset before adding to errors
232
+ warnings->Array.forEach(w => {
233
+ let adjusted = ErrorTypes.adjustLineOffset(w, lineOffset)
234
+ errors->Array.push(adjusted)->ignore
235
+ })
222
236
  boxes
223
237
  }
224
238
  }
@@ -294,15 +308,16 @@ let parseInternal = (wireframe: string, interactions: option<string>): parseResu
294
308
  let scenes = []
295
309
 
296
310
  sceneBlocks->Array.forEach(block => {
297
- // Parse scene directives
311
+ // Parse scene directives and get line offset
298
312
  let lines = block->String.split("\n")
299
- let (metadata, contentLines) = SemanticParser.parseSceneDirectives(lines)
313
+ let {metadata, contentLines, lineOffset} = SemanticParser.parseSceneDirectivesWithOffset(lines)
300
314
 
301
315
  // Rejoin content lines (without directives)
302
316
  let sceneContent = contentLines->Array.join("\n")
303
317
 
304
318
  // Parse this scene through 3-stage pipeline
305
- switch parseSingleScene(sceneContent, metadata, allIssues) {
319
+ // Pass lineOffset to adjust error/warning line numbers
320
+ switch parseSingleScene(sceneContent, metadata, lineOffset, allIssues) {
306
321
  | Some(scene) => scenes->Array.push(scene)->ignore
307
322
  | None => () // Scene parsing failed, errors already collected
308
323
  }
@@ -141,7 +141,7 @@ function defaultSceneMetadata() {
141
141
  };
142
142
  }
143
143
 
144
- function parseSceneDirectives(lines) {
144
+ function parseSceneDirectivesWithOffset(lines) {
145
145
  let sceneId = {
146
146
  contents: undefined
147
147
  };
@@ -155,7 +155,10 @@ function parseSceneDirectives(lines) {
155
155
  contents: undefined
156
156
  };
157
157
  let contentLines = [];
158
- lines.forEach(line => {
158
+ let firstContentLineIndex = {
159
+ contents: undefined
160
+ };
161
+ lines.forEach((line, lineIndex) => {
159
162
  let trimmed = line.trim();
160
163
  if (trimmed.startsWith("@scene:")) {
161
164
  let id = trimmed.replace("@scene:", "").trim();
@@ -176,6 +179,9 @@ function parseSceneDirectives(lines) {
176
179
  if (trimmed.startsWith("@")) {
177
180
  return;
178
181
  } else {
182
+ if (firstContentLineIndex.contents === undefined) {
183
+ firstContentLineIndex.contents = lineIndex;
184
+ }
179
185
  contentLines.push(line);
180
186
  return;
181
187
  }
@@ -204,9 +210,20 @@ function parseSceneDirectives(lines) {
204
210
  transition: finalTransition,
205
211
  device: finalDevice
206
212
  };
213
+ let idx = firstContentLineIndex.contents;
214
+ let lineOffset = idx !== undefined ? idx : 0;
215
+ return {
216
+ metadata: metadata,
217
+ contentLines: contentLines,
218
+ lineOffset: lineOffset
219
+ };
220
+ }
221
+
222
+ function parseSceneDirectives(lines) {
223
+ let result = parseSceneDirectivesWithOffset(lines);
207
224
  return [
208
- metadata,
209
- contentLines
225
+ result.metadata,
226
+ result.contentLines
210
227
  ];
211
228
  }
212
229
 
@@ -824,6 +841,7 @@ export {
824
841
  hasContent,
825
842
  getContentLineCount,
826
843
  defaultSceneMetadata,
844
+ parseSceneDirectivesWithOffset,
827
845
  parseSceneDirectives,
828
846
  splitSceneBlocks,
829
847
  groupContentByScenes,
@@ -261,7 +261,32 @@ let defaultSceneMetadata = (): sceneMetadata => {
261
261
  * ```
262
262
  * Returns: ({id: "login", title: "Login Page", transition: "slide"}, ["+--Login--+", ...])
263
263
  */
264
- let parseSceneDirectives = (lines: array<string>): (sceneMetadata, array<string>) => {
264
+ /**
265
+ * Result type for parseSceneDirectives that includes line offset information.
266
+ * The lineOffset indicates how many lines were removed from the beginning
267
+ * before the first content line, which is needed to correctly report
268
+ * line numbers in warnings and errors.
269
+ */
270
+ type directiveParseResult = {
271
+ metadata: sceneMetadata,
272
+ contentLines: array<string>,
273
+ lineOffset: int, // Number of directive lines stripped from the beginning
274
+ }
275
+
276
+ /**
277
+ * Parse scene directives and track line offset.
278
+ * Returns metadata, content lines, and the number of lines stripped from the beginning.
279
+ *
280
+ * The lineOffset is calculated as the index of the first content line in the original
281
+ * input. This offset is needed to convert grid row numbers back to original file line numbers.
282
+ *
283
+ * Example:
284
+ * - Input: ["@scene: login", "", "+---+", ...]
285
+ * - Output: contentLines = ["", "+---+", ...], lineOffset = 1
286
+ *
287
+ * Grid row 0 corresponds to original line (0 + lineOffset + 1) = line 2 (1-indexed)
288
+ */
289
+ let parseSceneDirectivesWithOffset = (lines: array<string>): directiveParseResult => {
265
290
  // Use mutable refs to accumulate directive values
266
291
  let sceneId = ref(None)
267
292
  let title = ref(None)
@@ -269,7 +294,10 @@ let parseSceneDirectives = (lines: array<string>): (sceneMetadata, array<string>
269
294
  let device = ref(None)
270
295
  let contentLines = []
271
296
 
272
- lines->Array.forEach(line => {
297
+ // Track the index of the first content line (for line offset calculation)
298
+ let firstContentLineIndex = ref(None)
299
+
300
+ lines->Array.forEachWithIndex((line, lineIndex) => {
273
301
  let trimmed = line->String.trim
274
302
 
275
303
  if trimmed->String.startsWith("@scene:") {
@@ -301,6 +329,10 @@ let parseSceneDirectives = (lines: array<string>): (sceneMetadata, array<string>
301
329
  ()
302
330
  } else {
303
331
  // Non-directive line - add to content
332
+ // Track the index of the first content line
333
+ if firstContentLineIndex.contents === None {
334
+ firstContentLineIndex := Some(lineIndex)
335
+ }
304
336
  contentLines->Array.push(line)
305
337
  }
306
338
  })
@@ -334,7 +366,27 @@ let parseSceneDirectives = (lines: array<string>): (sceneMetadata, array<string>
334
366
  device: finalDevice,
335
367
  }
336
368
 
337
- (metadata, contentLines)
369
+ // Calculate line offset: index of first content line
370
+ // If no content lines, offset is 0
371
+ let lineOffset = switch firstContentLineIndex.contents {
372
+ | Some(idx) => idx
373
+ | None => 0
374
+ }
375
+
376
+ {
377
+ metadata,
378
+ contentLines,
379
+ lineOffset,
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Parse scene directives from an array of lines.
385
+ * This is the original API that returns just (metadata, contentLines) for backward compatibility.
386
+ */
387
+ let parseSceneDirectives = (lines: array<string>): (sceneMetadata, array<string>) => {
388
+ let result = parseSceneDirectivesWithOffset(lines)
389
+ (result.metadata, result.contentLines)
338
390
  }
339
391
 
340
392
  /**