wyreframe 0.2.2 → 0.4.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.
@@ -0,0 +1,567 @@
1
+ // Fixer.res
2
+ // Auto-fix functionality for common wireframe errors and warnings
3
+
4
+ open FixTypes
5
+
6
+ // ============================================================================
7
+ // String Manipulation Helpers
8
+ // ============================================================================
9
+
10
+ /**
11
+ * Split text into lines (preserving empty lines)
12
+ */
13
+ let splitLines = (text: string): array<string> => {
14
+ text->String.split("\n")
15
+ }
16
+
17
+ /**
18
+ * Join lines back into text
19
+ */
20
+ let joinLines = (lines: array<string>): string => {
21
+ lines->Array.join("\n")
22
+ }
23
+
24
+ /**
25
+ * Get a specific line from text (0-indexed)
26
+ */
27
+ let getLine = (lines: array<string>, row: int): option<string> => {
28
+ lines->Array.get(row)
29
+ }
30
+
31
+ /**
32
+ * Replace a specific line in the array (0-indexed)
33
+ */
34
+ let replaceLine = (lines: array<string>, row: int, newLine: string): array<string> => {
35
+ lines->Array.mapWithIndex((line, idx) => {
36
+ if idx === row {
37
+ newLine
38
+ } else {
39
+ line
40
+ }
41
+ })
42
+ }
43
+
44
+ /**
45
+ * Insert characters at a specific position in a string
46
+ */
47
+ let insertAt = (str: string, col: int, chars: string): string => {
48
+ let before = str->String.slice(~start=0, ~end=col)
49
+ let after = str->String.sliceToEnd(~start=col)
50
+ before ++ chars ++ after
51
+ }
52
+
53
+ /**
54
+ * Remove characters at a specific position in a string
55
+ */
56
+ let removeAt = (str: string, col: int, count: int): string => {
57
+ let before = str->String.slice(~start=0, ~end=col)
58
+ let after = str->String.sliceToEnd(~start=col + count)
59
+ before ++ after
60
+ }
61
+
62
+ /**
63
+ * Replace a character at a specific position
64
+ */
65
+ let replaceCharAt = (str: string, col: int, char: string): string => {
66
+ let before = str->String.slice(~start=0, ~end=col)
67
+ let after = str->String.sliceToEnd(~start=col + 1)
68
+ before ++ char ++ after
69
+ }
70
+
71
+ // ============================================================================
72
+ // Fix Strategies for Each Error Type
73
+ // ============================================================================
74
+
75
+ /**
76
+ * Fix MisalignedPipe - adjust column position of '|' character
77
+ *
78
+ * Before: | content | (pipe at wrong column)
79
+ * After: | content | (pipe at correct column)
80
+ */
81
+ let fixMisalignedPipe = (text: string, error: ErrorTypes.t): option<(string, fixedIssue)> => {
82
+ switch error.code {
83
+ | MisalignedPipe({position, expectedCol, actualCol}) => {
84
+ let lines = splitLines(text)
85
+
86
+ switch getLine(lines, position.row) {
87
+ | None => None
88
+ | Some(line) => {
89
+ // Calculate how many spaces to add or remove
90
+ let diff = expectedCol - actualCol
91
+
92
+ let newLine = if diff > 0 {
93
+ // Need to add spaces before the pipe
94
+ insertAt(line, actualCol, String.repeat(" ", diff))
95
+ } else {
96
+ // Need to remove spaces before the pipe
97
+ let removeCount = Js.Math.abs_int(diff)
98
+ // Make sure we're removing spaces, not content
99
+ let beforePipe = line->String.slice(~start=actualCol + diff, ~end=actualCol)
100
+ if beforePipe->String.trim === "" {
101
+ removeAt(line, actualCol + diff, removeCount)
102
+ } else {
103
+ line // Can't safely remove non-space characters
104
+ }
105
+ }
106
+
107
+ if newLine !== line {
108
+ let newLines = replaceLine(lines, position.row, newLine)
109
+ let fixedText = joinLines(newLines)
110
+
111
+ Some((
112
+ fixedText,
113
+ {
114
+ original: error,
115
+ description: `Aligned pipe at line ${Int.toString(position.row + 1)} to column ${Int.toString(expectedCol + 1)}`,
116
+ line: position.row + 1,
117
+ column: expectedCol + 1,
118
+ },
119
+ ))
120
+ } else {
121
+ None
122
+ }
123
+ }
124
+ }
125
+ }
126
+ | _ => None
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Fix MisalignedClosingBorder - adjust closing '|' position
132
+ */
133
+ let fixMisalignedClosingBorder = (text: string, error: ErrorTypes.t): option<(string, fixedIssue)> => {
134
+ switch error.code {
135
+ | MisalignedClosingBorder({position, expectedCol, actualCol}) => {
136
+ let lines = splitLines(text)
137
+
138
+ switch getLine(lines, position.row) {
139
+ | None => None
140
+ | Some(line) => {
141
+ let diff = expectedCol - actualCol
142
+
143
+ let newLine = if diff > 0 {
144
+ // Need to add spaces before the closing pipe
145
+ insertAt(line, actualCol, String.repeat(" ", diff))
146
+ } else {
147
+ // Need to remove spaces before the closing pipe
148
+ let removeCount = Js.Math.abs_int(diff)
149
+ let beforePipe = line->String.slice(~start=actualCol + diff, ~end=actualCol)
150
+ if beforePipe->String.trim === "" {
151
+ removeAt(line, actualCol + diff, removeCount)
152
+ } else {
153
+ line
154
+ }
155
+ }
156
+
157
+ if newLine !== line {
158
+ let newLines = replaceLine(lines, position.row, newLine)
159
+ let fixedText = joinLines(newLines)
160
+
161
+ Some((
162
+ fixedText,
163
+ {
164
+ original: error,
165
+ description: `Aligned closing border at line ${Int.toString(position.row + 1)} to column ${Int.toString(expectedCol + 1)}`,
166
+ line: position.row + 1,
167
+ column: expectedCol + 1,
168
+ },
169
+ ))
170
+ } else {
171
+ None
172
+ }
173
+ }
174
+ }
175
+ }
176
+ | _ => None
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Fix UnusualSpacing - replace tabs with spaces
182
+ */
183
+ let fixUnusualSpacing = (text: string, error: ErrorTypes.t): option<(string, fixedIssue)> => {
184
+ switch error.code {
185
+ | UnusualSpacing({position, issue}) => {
186
+ // Check if the issue is about tabs
187
+ if issue->String.includes("tab") || issue->String.includes("Tab") {
188
+ let lines = splitLines(text)
189
+
190
+ switch getLine(lines, position.row) {
191
+ | None => None
192
+ | Some(line) => {
193
+ // Replace tabs with 2 spaces (common convention)
194
+ let newLine = line->String.replaceAll("\t", " ")
195
+
196
+ if newLine !== line {
197
+ let newLines = replaceLine(lines, position.row, newLine)
198
+ let fixedText = joinLines(newLines)
199
+
200
+ Some((
201
+ fixedText,
202
+ {
203
+ original: error,
204
+ description: `Replaced tabs with spaces at line ${Int.toString(position.row + 1)}`,
205
+ line: position.row + 1,
206
+ column: position.col + 1,
207
+ },
208
+ ))
209
+ } else {
210
+ None
211
+ }
212
+ }
213
+ }
214
+ } else {
215
+ None
216
+ }
217
+ }
218
+ | _ => None
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Fix UnclosedBracket - add closing ']' at end of line
224
+ */
225
+ let fixUnclosedBracket = (text: string, error: ErrorTypes.t): option<(string, fixedIssue)> => {
226
+ switch error.code {
227
+ | UnclosedBracket({opening}) => {
228
+ let lines = splitLines(text)
229
+
230
+ switch getLine(lines, opening.row) {
231
+ | None => None
232
+ | Some(line) => {
233
+ // Find the content after '[' and close it
234
+ let trimmedLine = line->String.trimEnd
235
+
236
+ // Only add ']' if the line doesn't already end with it
237
+ if !(trimmedLine->String.endsWith("]")) {
238
+ // Add ' ]' with proper spacing
239
+ let newLine = trimmedLine ++ " ]"
240
+ let newLines = replaceLine(lines, opening.row, newLine)
241
+ let fixedText = joinLines(newLines)
242
+
243
+ Some((
244
+ fixedText,
245
+ {
246
+ original: error,
247
+ description: `Added closing bracket at line ${Int.toString(opening.row + 1)}`,
248
+ line: opening.row + 1,
249
+ column: String.length(trimmedLine) + 2,
250
+ },
251
+ ))
252
+ } else {
253
+ None
254
+ }
255
+ }
256
+ }
257
+ }
258
+ | _ => None
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Fix MismatchedWidth - extend the shorter border to match the longer one
264
+ */
265
+ let fixMismatchedWidth = (text: string, error: ErrorTypes.t): option<(string, fixedIssue)> => {
266
+ switch error.code {
267
+ | MismatchedWidth({topLeft, topWidth, bottomWidth}) => {
268
+ let lines = splitLines(text)
269
+ let diff = topWidth - bottomWidth
270
+
271
+ if diff === 0 {
272
+ None
273
+ } else {
274
+ // Find the bottom border line
275
+ // We need to trace down from topLeft to find the bottom
276
+ let rec findBottomRow = (row: int): option<int> => {
277
+ if row >= Array.length(lines) {
278
+ None
279
+ } else {
280
+ switch getLine(lines, row) {
281
+ | None => None
282
+ | Some(line) => {
283
+ // Check if this line has a '+' at the same column as topLeft
284
+ let col = topLeft.col
285
+ if col < String.length(line) {
286
+ let char = line->String.charAt(col)
287
+ if char === "+" && row > topLeft.row {
288
+ Some(row)
289
+ } else {
290
+ findBottomRow(row + 1)
291
+ }
292
+ } else {
293
+ findBottomRow(row + 1)
294
+ }
295
+ }
296
+ }
297
+ }
298
+ }
299
+
300
+ switch findBottomRow(topLeft.row + 1) {
301
+ | None => None
302
+ | Some(bottomRow) => {
303
+ switch getLine(lines, bottomRow) {
304
+ | None => None
305
+ | Some(bottomLine) => {
306
+ if diff > 0 {
307
+ // Bottom is shorter, need to extend it
308
+ // Find the closing '+' and add dashes before it
309
+ let closingPlusCol = topLeft.col + bottomWidth - 1
310
+ if closingPlusCol >= 0 && closingPlusCol < String.length(bottomLine) {
311
+ let before = bottomLine->String.slice(~start=0, ~end=closingPlusCol)
312
+ let after = bottomLine->String.sliceToEnd(~start=closingPlusCol)
313
+ let dashes = String.repeat("-", diff)
314
+ let newLine = before ++ dashes ++ after
315
+
316
+ let newLines = replaceLine(lines, bottomRow, newLine)
317
+ let fixedText = joinLines(newLines)
318
+
319
+ Some((
320
+ fixedText,
321
+ {
322
+ original: error,
323
+ description: `Extended bottom border at line ${Int.toString(bottomRow + 1)} by ${Int.toString(diff)} characters`,
324
+ line: bottomRow + 1,
325
+ column: closingPlusCol + 1,
326
+ },
327
+ ))
328
+ } else {
329
+ None
330
+ }
331
+ } else {
332
+ // Top is shorter, need to extend it
333
+ // This is trickier as it affects content alignment
334
+ // For now, we extend the top border
335
+ switch getLine(lines, topLeft.row) {
336
+ | None => None
337
+ | Some(topLine) => {
338
+ let closingPlusCol = topLeft.col + topWidth - 1
339
+ if closingPlusCol >= 0 && closingPlusCol < String.length(topLine) {
340
+ let before = topLine->String.slice(~start=0, ~end=closingPlusCol)
341
+ let after = topLine->String.sliceToEnd(~start=closingPlusCol)
342
+ let dashes = String.repeat("-", Js.Math.abs_int(diff))
343
+ let newLine = before ++ dashes ++ after
344
+
345
+ let newLines = replaceLine(lines, topLeft.row, newLine)
346
+ let fixedText = joinLines(newLines)
347
+
348
+ Some((
349
+ fixedText,
350
+ {
351
+ original: error,
352
+ description: `Extended top border at line ${Int.toString(topLeft.row + 1)} by ${Int.toString(Js.Math.abs_int(diff))} characters`,
353
+ line: topLeft.row + 1,
354
+ column: closingPlusCol + 1,
355
+ },
356
+ ))
357
+ } else {
358
+ None
359
+ }
360
+ }
361
+ }
362
+ }
363
+ }
364
+ }
365
+ }
366
+ }
367
+ }
368
+ }
369
+ | _ => None
370
+ }
371
+ }
372
+
373
+ // ============================================================================
374
+ // Main Fix Function
375
+ // ============================================================================
376
+
377
+ /**
378
+ * List of all fix strategies in order of application
379
+ */
380
+ let fixStrategies: array<(string, ErrorTypes.errorCode => bool, (string, ErrorTypes.t) => option<(string, fixedIssue)>)> = [
381
+ ("MisalignedPipe", code => {
382
+ switch code {
383
+ | MisalignedPipe(_) => true
384
+ | _ => false
385
+ }
386
+ }, fixMisalignedPipe),
387
+ ("MisalignedClosingBorder", code => {
388
+ switch code {
389
+ | MisalignedClosingBorder(_) => true
390
+ | _ => false
391
+ }
392
+ }, fixMisalignedClosingBorder),
393
+ ("UnusualSpacing", code => {
394
+ switch code {
395
+ | UnusualSpacing(_) => true
396
+ | _ => false
397
+ }
398
+ }, fixUnusualSpacing),
399
+ ("UnclosedBracket", code => {
400
+ switch code {
401
+ | UnclosedBracket(_) => true
402
+ | _ => false
403
+ }
404
+ }, fixUnclosedBracket),
405
+ ("MismatchedWidth", code => {
406
+ switch code {
407
+ | MismatchedWidth(_) => true
408
+ | _ => false
409
+ }
410
+ }, fixMismatchedWidth),
411
+ ]
412
+
413
+ /**
414
+ * Try to fix a single error using available strategies
415
+ */
416
+ let tryFixError = (text: string, error: ErrorTypes.t): option<(string, fixedIssue)> => {
417
+ // Find the first strategy that can fix this error
418
+ fixStrategies->Array.reduce(None, (acc, (_, canFix, apply)) => {
419
+ switch acc {
420
+ | Some(_) => acc // Already found a fix
421
+ | None => {
422
+ if canFix(error.code) {
423
+ apply(text, error)
424
+ } else {
425
+ None
426
+ }
427
+ }
428
+ }
429
+ })
430
+ }
431
+
432
+ /**
433
+ * Check if an error code is fixable
434
+ */
435
+ let isFixable = (code: ErrorTypes.errorCode): bool => {
436
+ fixStrategies->Array.some(((_, canFix, _)) => canFix(code))
437
+ }
438
+
439
+ /**
440
+ * Main fix function - attempts to fix all errors in the text
441
+ *
442
+ * This function:
443
+ * 1. Parses the text to get errors/warnings
444
+ * 2. Attempts to fix each fixable error
445
+ * 3. Re-parses after each fix to get updated positions
446
+ * 4. Returns the fixed text and list of applied fixes
447
+ *
448
+ * @param text The wireframe markdown text
449
+ * @returns FixResult with fixed text and details
450
+ */
451
+ @genType
452
+ let fix = (text: string): fixResult => {
453
+ // Maximum iterations to prevent infinite loops
454
+ let maxIterations = 100
455
+
456
+ // Recursive fix loop
457
+ let rec fixLoop = (currentText: string, fixedSoFar: array<fixedIssue>, iteration: int): fixResult => {
458
+ if iteration >= maxIterations {
459
+ // Too many iterations, return what we have
460
+ Ok({
461
+ text: currentText,
462
+ fixed: fixedSoFar,
463
+ remaining: [],
464
+ })
465
+ } else {
466
+ // Parse current text to get errors
467
+ let parseResult = Parser.parse(currentText)
468
+
469
+ switch parseResult {
470
+ | Ok((_ast, warnings)) => {
471
+ // Parse succeeded, only warnings remain
472
+ // Try to fix warnings
473
+ let fixableWarnings = warnings->Array.filter(w => isFixable(w.code))
474
+
475
+ if Array.length(fixableWarnings) === 0 {
476
+ // No more fixable issues
477
+ Ok({
478
+ text: currentText,
479
+ fixed: fixedSoFar,
480
+ remaining: warnings->Array.filter(w => !isFixable(w.code)),
481
+ })
482
+ } else {
483
+ // Try to fix the first fixable warning
484
+ switch fixableWarnings->Array.get(0) {
485
+ | None => Ok({
486
+ text: currentText,
487
+ fixed: fixedSoFar,
488
+ remaining: warnings,
489
+ })
490
+ | Some(warning) => {
491
+ switch tryFixError(currentText, warning) {
492
+ | None => {
493
+ // Couldn't fix, move on
494
+ Ok({
495
+ text: currentText,
496
+ fixed: fixedSoFar,
497
+ remaining: warnings,
498
+ })
499
+ }
500
+ | Some((newText, fixedIssue)) => {
501
+ // Fixed! Continue with remaining
502
+ let newFixed = fixedSoFar->Array.concat([fixedIssue])
503
+ fixLoop(newText, newFixed, iteration + 1)
504
+ }
505
+ }
506
+ }
507
+ }
508
+ }
509
+ }
510
+ | Error(errors) => {
511
+ // Parse failed, try to fix errors
512
+ let fixableErrors = errors->Array.filter(e => isFixable(e.code))
513
+
514
+ if Array.length(fixableErrors) === 0 {
515
+ // No fixable errors, return what we have
516
+ Ok({
517
+ text: currentText,
518
+ fixed: fixedSoFar,
519
+ remaining: errors->Array.filter(e => !isFixable(e.code)),
520
+ })
521
+ } else {
522
+ // Try to fix the first fixable error
523
+ switch fixableErrors->Array.get(0) {
524
+ | None => Ok({
525
+ text: currentText,
526
+ fixed: fixedSoFar,
527
+ remaining: errors,
528
+ })
529
+ | Some(error) => {
530
+ switch tryFixError(currentText, error) {
531
+ | None => {
532
+ // Couldn't fix, return remaining errors
533
+ Ok({
534
+ text: currentText,
535
+ fixed: fixedSoFar,
536
+ remaining: errors,
537
+ })
538
+ }
539
+ | Some((newText, fixedIssue)) => {
540
+ // Fixed! Continue fixing
541
+ let newFixed = fixedSoFar->Array.concat([fixedIssue])
542
+ fixLoop(newText, newFixed, iteration + 1)
543
+ }
544
+ }
545
+ }
546
+ }
547
+ }
548
+ }
549
+ }
550
+ }
551
+ }
552
+
553
+ // Start the fix loop
554
+ fixLoop(text, [], 0)
555
+ }
556
+
557
+ /**
558
+ * Convenience function - fix and return just the fixed text
559
+ * Returns the original text if nothing was fixed
560
+ */
561
+ @genType
562
+ let fixOnly = (text: string): string => {
563
+ switch fix(text) {
564
+ | Ok({text: fixedText, _}) => fixedText
565
+ | Error(_) => text
566
+ }
567
+ }
@@ -11,12 +11,19 @@ import type {t as ErrorTypes_t} from '../../src/parser/Errors/ErrorTypes.gen';
11
11
 
12
12
  export abstract class DomBindings_element { protected opaque!: any }; /* simulate opaque types */
13
13
 
14
+ /** * Scene change callback type.
15
+ * Called when navigating between scenes.
16
+ * @param fromScene The scene ID navigating from (None if initial)
17
+ * @param toScene The scene ID navigating to */
18
+ export type onSceneChangeCallback = (_1:(undefined | string), _2:string) => void;
19
+
14
20
  /** * Configuration for the rendering process. */
15
21
  export type renderOptions = {
16
22
  readonly theme: (undefined | string);
17
23
  readonly interactive: boolean;
18
24
  readonly injectStyles: boolean;
19
- readonly containerClass: (undefined | string)
25
+ readonly containerClass: (undefined | string);
26
+ readonly onSceneChange: (undefined | onSceneChangeCallback)
20
27
  };
21
28
 
22
29
  /** * Scene management interface returned by render function. */
@@ -12,7 +12,8 @@ let defaultOptions = {
12
12
  theme: undefined,
13
13
  interactive: true,
14
14
  injectStyles: true,
15
- containerClass: undefined
15
+ containerClass: undefined,
16
+ onSceneChange: undefined
16
17
  };
17
18
 
18
19
  let defaultStyles = `
@@ -280,7 +281,7 @@ function renderScene(scene, onAction) {
280
281
  return sceneEl;
281
282
  }
282
283
 
283
- function createSceneManager(scenes) {
284
+ function createSceneManager(scenes, onSceneChange) {
284
285
  let currentScene = {
285
286
  contents: undefined
286
287
  };
@@ -290,10 +291,11 @@ function createSceneManager(scenes) {
290
291
  let forwardStack = {
291
292
  contents: []
292
293
  };
293
- let switchToScene = id => {
294
- let currentId = currentScene.contents;
295
- if (currentId !== undefined) {
296
- let el = scenes.get(currentId);
294
+ let switchToScene = (id, notifyOpt) => {
295
+ let notify = notifyOpt !== undefined ? notifyOpt : true;
296
+ let previousScene = currentScene.contents;
297
+ if (previousScene !== undefined) {
298
+ let el = scenes.get(previousScene);
297
299
  if (el !== undefined) {
298
300
  Primitive_option.valFromOption(el).classList.remove("active");
299
301
  }
@@ -302,7 +304,15 @@ function createSceneManager(scenes) {
302
304
  if (el$1 !== undefined) {
303
305
  Primitive_option.valFromOption(el$1).classList.add("active");
304
306
  currentScene.contents = id;
305
- return;
307
+ if (notify && (previousScene === undefined || previousScene !== id)) {
308
+ if (onSceneChange !== undefined) {
309
+ return onSceneChange(previousScene, id);
310
+ } else {
311
+ return;
312
+ }
313
+ } else {
314
+ return;
315
+ }
306
316
  }
307
317
  };
308
318
  let goto = id => {
@@ -311,7 +321,7 @@ function createSceneManager(scenes) {
311
321
  historyStack.contents = historyStack.contents.concat([currentId]);
312
322
  forwardStack.contents = [];
313
323
  }
314
- switchToScene(id);
324
+ switchToScene(id, undefined);
315
325
  };
316
326
  let back = () => {
317
327
  let history = historyStack.contents;
@@ -328,7 +338,7 @@ function createSceneManager(scenes) {
328
338
  forwardStack.contents = forwardStack.contents.concat([currentId]);
329
339
  }
330
340
  historyStack.contents = history.slice(0, len - 1 | 0);
331
- switchToScene(prevId);
341
+ switchToScene(prevId, undefined);
332
342
  };
333
343
  let forward = () => {
334
344
  let fwdStack = forwardStack.contents;
@@ -345,7 +355,7 @@ function createSceneManager(scenes) {
345
355
  historyStack.contents = historyStack.contents.concat([currentId]);
346
356
  }
347
357
  forwardStack.contents = fwdStack.slice(0, len - 1 | 0);
348
- switchToScene(nextId);
358
+ switchToScene(nextId, undefined);
349
359
  };
350
360
  let refresh = () => {
351
361
  let id = currentScene.contents;
@@ -451,7 +461,7 @@ function render(ast, options) {
451
461
  app.appendChild(sceneEl);
452
462
  sceneMap.set(scene.id, sceneEl);
453
463
  });
454
- let manager = createSceneManager(sceneMap);
464
+ let manager = createSceneManager(sceneMap, opts.onSceneChange);
455
465
  gotoRef.contents = manager.goto;
456
466
  backRef.contents = manager.back;
457
467
  forwardRef.contents = manager.forward;