wyreframe 0.3.0 → 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
+ }