wyreframe 0.1.5 → 0.2.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/README.md +9 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +34 -4
- package/src/parser/Detector/BoxTracer.mjs +122 -36
- package/src/parser/Detector/BoxTracer.res +172 -27
- package/src/parser/Detector/ShapeDetector.mjs +27 -9
- package/src/parser/Detector/ShapeDetector.res +15 -8
- package/src/parser/Errors/ErrorMessages.mjs +25 -0
- package/src/parser/Errors/ErrorMessages.res +17 -0
- package/src/parser/Errors/ErrorTypes.mjs +3 -0
- package/src/parser/Errors/ErrorTypes.res +8 -1
- package/src/parser/Parser.gen.tsx +3 -2
- package/src/parser/Parser.mjs +22 -10
- package/src/parser/Parser.res +27 -18
- package/src/renderer/Renderer.gen.tsx +7 -1
- package/src/renderer/Renderer.mjs +126 -25
- package/src/renderer/Renderer.res +116 -12
|
@@ -70,6 +70,90 @@ let isDividerOnlyEdge = (edgeChars: array<cellChar>): bool => {
|
|
|
70
70
|
!hasHLineOrChar
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Find the last Corner character below a given position in the same column.
|
|
75
|
+
* Simple version used for fallback width mismatch detection.
|
|
76
|
+
*
|
|
77
|
+
* @param grid - The 2D character grid
|
|
78
|
+
* @param startPos - Starting position (searches below this row)
|
|
79
|
+
* @returns Option of position where the last corner was found
|
|
80
|
+
*/
|
|
81
|
+
let findLastCornerInColumn = (grid: Grid.t, startPos: Position.t): option<Position.t> => {
|
|
82
|
+
let lastCorner = ref(None)
|
|
83
|
+
for row in startPos.row + 1 to grid.height - 1 {
|
|
84
|
+
let pos = Position.make(row, startPos.col)
|
|
85
|
+
switch Grid.get(grid, pos) {
|
|
86
|
+
| Some(Corner) => lastCorner := Some(pos)
|
|
87
|
+
| _ => ()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
lastCorner.contents
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if a row has any VLine character between left and right columns (inclusive).
|
|
95
|
+
* Used to determine if a row is part of a box (even with misaligned borders).
|
|
96
|
+
*/
|
|
97
|
+
let rowHasVLineInRange = (grid: Grid.t, row: int, leftCol: int, rightCol: int): bool => {
|
|
98
|
+
let found = ref(false)
|
|
99
|
+
let col = ref(leftCol)
|
|
100
|
+
while col.contents <= rightCol && !found.contents {
|
|
101
|
+
switch Grid.get(grid, Position.make(row, col.contents)) {
|
|
102
|
+
| Some(VLine) => found := true
|
|
103
|
+
| _ => ()
|
|
104
|
+
}
|
|
105
|
+
col := col.contents + 1
|
|
106
|
+
}
|
|
107
|
+
found.contents
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Find the bottom-right corner of a box by scanning down from top-right.
|
|
112
|
+
*
|
|
113
|
+
* This is more tolerant than the strict scanDown approach:
|
|
114
|
+
* - Continues through rows with misaligned VLines (records for warnings)
|
|
115
|
+
* - Stops at rows with no VLine at all in the box's column range (box boundary)
|
|
116
|
+
* - Handles internal dividers (+=====+) correctly by finding the last corner
|
|
117
|
+
*
|
|
118
|
+
* @param grid - The 2D character grid
|
|
119
|
+
* @param topLeft - Position of top-left corner (for determining left boundary)
|
|
120
|
+
* @param topRight - Position of top-right corner (starting point for scan)
|
|
121
|
+
* @returns Option of position where the bottom-right corner was found
|
|
122
|
+
*/
|
|
123
|
+
let findBottomRightCorner = (grid: Grid.t, topLeft: Position.t, topRight: Position.t): option<Position.t> => {
|
|
124
|
+
let lastCorner = ref(None)
|
|
125
|
+
let row = ref(topRight.row + 1)
|
|
126
|
+
let continue = ref(true)
|
|
127
|
+
|
|
128
|
+
while row.contents < grid.height && continue.contents {
|
|
129
|
+
let pos = Position.make(row.contents, topRight.col)
|
|
130
|
+
|
|
131
|
+
switch Grid.get(grid, pos) {
|
|
132
|
+
| Some(Corner) => {
|
|
133
|
+
// Found a corner at the expected column - remember it
|
|
134
|
+
lastCorner := Some(pos)
|
|
135
|
+
row := row.contents + 1
|
|
136
|
+
}
|
|
137
|
+
| Some(VLine) => {
|
|
138
|
+
// Found a VLine at the expected column - continue scanning
|
|
139
|
+
row := row.contents + 1
|
|
140
|
+
}
|
|
141
|
+
| _ => {
|
|
142
|
+
// No VLine/Corner at expected column - check if row is part of this box
|
|
143
|
+
if rowHasVLineInRange(grid, row.contents, topLeft.col, topRight.col) {
|
|
144
|
+
// Row has a VLine somewhere (misaligned) - continue scanning
|
|
145
|
+
row := row.contents + 1
|
|
146
|
+
} else {
|
|
147
|
+
// No VLine in this row's box range - we've reached the end of the box
|
|
148
|
+
continue := false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
lastCorner.contents
|
|
155
|
+
}
|
|
156
|
+
|
|
73
157
|
/**
|
|
74
158
|
* Trace a box starting from the top-left corner position.
|
|
75
159
|
*
|
|
@@ -134,38 +218,22 @@ let traceBox = (grid: Grid.t, topLeft: Position.t): traceResult => {
|
|
|
134
218
|
// Calculate top width
|
|
135
219
|
let topWidth = topRight.col - topLeft.col
|
|
136
220
|
|
|
137
|
-
// Step 4:
|
|
138
|
-
|
|
221
|
+
// Step 4: Find bottom-right corner by searching the column
|
|
222
|
+
// Use tolerant search that ignores interior rows with misaligned VLines
|
|
223
|
+
let bottomRightOpt = findBottomRightCorner(grid, topLeft, topRight)
|
|
139
224
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
Array.forEach(rightEdgeScan, ((pos, cell)) => {
|
|
143
|
-
switch cell {
|
|
144
|
-
| Corner if !Position.equals(pos, topRight) => lastCorner := Some(pos)
|
|
145
|
-
| _ => ()
|
|
146
|
-
}
|
|
147
|
-
})
|
|
148
|
-
lastCorner.contents
|
|
149
|
-
}
|
|
225
|
+
// Store rightEdgeScan for later validation (after we know the box is valid)
|
|
226
|
+
let rightEdgeScan = Grid.scanDown(grid, topRight, isValidVerticalChar)
|
|
150
227
|
|
|
151
228
|
switch bottomRightOpt {
|
|
152
229
|
| None => {
|
|
153
|
-
//
|
|
154
|
-
// Try
|
|
155
|
-
let
|
|
156
|
-
let bottomLeftOpt = {
|
|
157
|
-
let lastCorner = ref(None)
|
|
158
|
-
Array.forEach(leftEdgeScan, ((pos, cell)) => {
|
|
159
|
-
switch cell {
|
|
160
|
-
| Corner if !Position.equals(pos, topLeft) => lastCorner := Some(pos)
|
|
161
|
-
| _ => ()
|
|
162
|
-
}
|
|
163
|
-
})
|
|
164
|
-
lastCorner.contents
|
|
165
|
-
}
|
|
230
|
+
// No corner found in the right column at topRight.col
|
|
231
|
+
// Try to detect width mismatch by finding bottom-left via left edge
|
|
232
|
+
let bottomLeftViaLeft = findLastCornerInColumn(grid, topLeft)
|
|
166
233
|
|
|
167
|
-
switch
|
|
234
|
+
switch bottomLeftViaLeft {
|
|
168
235
|
| None =>
|
|
236
|
+
// No bottom-left corner found either - unclosed box
|
|
169
237
|
Error(
|
|
170
238
|
ErrorTypes.makeSimple(
|
|
171
239
|
ErrorTypes.UncloseBox({
|
|
@@ -212,7 +280,7 @@ let traceBox = (grid: Grid.t, topLeft: Position.t): traceResult => {
|
|
|
212
280
|
),
|
|
213
281
|
)
|
|
214
282
|
} else {
|
|
215
|
-
// Widths match but right edge
|
|
283
|
+
// Widths match but right edge corners don't align - unclosed box
|
|
216
284
|
Error(
|
|
217
285
|
ErrorTypes.makeSimple(
|
|
218
286
|
ErrorTypes.UncloseBox({
|
|
@@ -372,3 +440,80 @@ let traceBox = (grid: Grid.t, topLeft: Position.t): traceResult => {
|
|
|
372
440
|
)
|
|
373
441
|
}
|
|
374
442
|
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Validate interior row alignment for a successfully traced box.
|
|
446
|
+
*
|
|
447
|
+
* Checks each row between the top and bottom borders to ensure:
|
|
448
|
+
* - The closing '|' character is at the expected column (bounds.right)
|
|
449
|
+
* - Generates MisalignedClosingBorder warnings for any misaligned rows
|
|
450
|
+
*
|
|
451
|
+
* This validation runs AFTER successful box tracing to detect visual
|
|
452
|
+
* alignment issues that don't prevent parsing but should be warned about.
|
|
453
|
+
*
|
|
454
|
+
* @param grid - The 2D character grid
|
|
455
|
+
* @param bounds - The bounds of the traced box
|
|
456
|
+
* @returns Array of warnings for any misaligned closing borders
|
|
457
|
+
*/
|
|
458
|
+
let validateInteriorAlignment = (grid: Grid.t, bounds: Bounds.t): array<ErrorTypes.t> => {
|
|
459
|
+
let warnings = []
|
|
460
|
+
|
|
461
|
+
// Check each interior row (excluding top and bottom border rows)
|
|
462
|
+
for row in bounds.top + 1 to bounds.bottom - 1 {
|
|
463
|
+
// Check if there's a VLine at the expected right border column
|
|
464
|
+
let expectedRightCol = bounds.right
|
|
465
|
+
let rightCell = Grid.get(grid, Position.make(row, expectedRightCol))
|
|
466
|
+
|
|
467
|
+
switch rightCell {
|
|
468
|
+
| Some(VLine) => () // Properly aligned, no warning needed
|
|
469
|
+
| Some(_) | None => {
|
|
470
|
+
// The expected column doesn't have a VLine
|
|
471
|
+
// Try to find where the actual closing '|' is on this row
|
|
472
|
+
// Search in both directions from the expected position
|
|
473
|
+
let actualCol = ref(None)
|
|
474
|
+
|
|
475
|
+
// First, search to the RIGHT of expectedRightCol (for pipes beyond the expected boundary)
|
|
476
|
+
// Search up to 50 chars beyond to catch misaligned pipes
|
|
477
|
+
let maxSearchRight = expectedRightCol + 50
|
|
478
|
+
let colRight = ref(expectedRightCol + 1)
|
|
479
|
+
while colRight.contents <= maxSearchRight && Option.isNone(actualCol.contents) {
|
|
480
|
+
switch Grid.get(grid, Position.make(row, colRight.contents)) {
|
|
481
|
+
| Some(VLine) => actualCol := Some(colRight.contents)
|
|
482
|
+
| _ => ()
|
|
483
|
+
}
|
|
484
|
+
colRight := colRight.contents + 1
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// If not found to the right, search to the LEFT (for pipes before the expected boundary)
|
|
488
|
+
// But only if it's not the opening pipe (bounds.left)
|
|
489
|
+
if Option.isNone(actualCol.contents) {
|
|
490
|
+
let col = ref(expectedRightCol - 1)
|
|
491
|
+
while col.contents > bounds.left && Option.isNone(actualCol.contents) {
|
|
492
|
+
switch Grid.get(grid, Position.make(row, col.contents)) {
|
|
493
|
+
| Some(VLine) => actualCol := Some(col.contents)
|
|
494
|
+
| _ => ()
|
|
495
|
+
}
|
|
496
|
+
col := col.contents - 1
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// If we found a VLine at a different position, generate a warning
|
|
501
|
+
switch actualCol.contents {
|
|
502
|
+
| Some(foundCol) if foundCol !== expectedRightCol => {
|
|
503
|
+
let warning = ErrorTypes.makeSimple(
|
|
504
|
+
ErrorTypes.MisalignedClosingBorder({
|
|
505
|
+
position: Position.make(row, foundCol),
|
|
506
|
+
expectedCol: expectedRightCol,
|
|
507
|
+
actualCol: foundCol,
|
|
508
|
+
}),
|
|
509
|
+
)
|
|
510
|
+
warnings->Array.push(warning)->ignore
|
|
511
|
+
}
|
|
512
|
+
| _ => () // No VLine found or it's at the correct position (already handled above)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
warnings
|
|
519
|
+
}
|
|
@@ -47,10 +47,16 @@ function detect(grid) {
|
|
|
47
47
|
let corners = grid.cornerIndex;
|
|
48
48
|
let boxes = [];
|
|
49
49
|
let traceFailures = [];
|
|
50
|
+
let warnings = [];
|
|
50
51
|
corners.forEach(corner => {
|
|
51
52
|
let box = BoxTracer.traceBox(grid, corner);
|
|
52
53
|
if (box.TAG === "Ok") {
|
|
53
|
-
|
|
54
|
+
let box$1 = box._0;
|
|
55
|
+
boxes.push(box$1);
|
|
56
|
+
let alignmentWarnings = BoxTracer.validateInteriorAlignment(grid, box$1.bounds);
|
|
57
|
+
alignmentWarnings.forEach(w => {
|
|
58
|
+
warnings.push(w);
|
|
59
|
+
});
|
|
54
60
|
return;
|
|
55
61
|
}
|
|
56
62
|
traceFailures.push(box._0);
|
|
@@ -59,7 +65,10 @@ function detect(grid) {
|
|
|
59
65
|
if (corners.length === 0) {
|
|
60
66
|
return {
|
|
61
67
|
TAG: "Ok",
|
|
62
|
-
_0: [
|
|
68
|
+
_0: [
|
|
69
|
+
[],
|
|
70
|
+
[]
|
|
71
|
+
]
|
|
63
72
|
};
|
|
64
73
|
} else if (traceFailures.length > 0) {
|
|
65
74
|
return {
|
|
@@ -69,7 +78,10 @@ function detect(grid) {
|
|
|
69
78
|
} else {
|
|
70
79
|
return {
|
|
71
80
|
TAG: "Ok",
|
|
72
|
-
_0: [
|
|
81
|
+
_0: [
|
|
82
|
+
[],
|
|
83
|
+
[]
|
|
84
|
+
]
|
|
73
85
|
};
|
|
74
86
|
}
|
|
75
87
|
}
|
|
@@ -78,7 +90,10 @@ function detect(grid) {
|
|
|
78
90
|
if (rootBoxes.TAG === "Ok") {
|
|
79
91
|
return {
|
|
80
92
|
TAG: "Ok",
|
|
81
|
-
_0:
|
|
93
|
+
_0: [
|
|
94
|
+
rootBoxes._0,
|
|
95
|
+
warnings
|
|
96
|
+
]
|
|
82
97
|
};
|
|
83
98
|
}
|
|
84
99
|
let parseError = hierarchyErrorToParseError(rootBoxes._0);
|
|
@@ -101,26 +116,29 @@ function flattenBoxes(boxes) {
|
|
|
101
116
|
|
|
102
117
|
function getStats(result) {
|
|
103
118
|
if (result.TAG === "Ok") {
|
|
104
|
-
let
|
|
119
|
+
let match = result._0;
|
|
120
|
+
let boxes = match[0];
|
|
105
121
|
let rootCount = boxes.length;
|
|
106
122
|
let totalCount = countBoxes(boxes);
|
|
123
|
+
let warningCount = match[1].length;
|
|
107
124
|
return `Shape Detection Success:
|
|
108
125
|
Root boxes: ` + rootCount.toString() + `
|
|
109
|
-
Total boxes (including nested): ` + totalCount.toString()
|
|
126
|
+
Total boxes (including nested): ` + totalCount.toString() + `
|
|
127
|
+
Warnings: ` + warningCount.toString();
|
|
110
128
|
}
|
|
111
129
|
let errors = result._0;
|
|
112
130
|
let errorCount = errors.length;
|
|
113
|
-
let warningCount = Core__Array.reduce(errors, 0, (acc, err) => {
|
|
131
|
+
let warningCount$1 = Core__Array.reduce(errors, 0, (acc, err) => {
|
|
114
132
|
if (ErrorTypes.isWarning(err)) {
|
|
115
133
|
return acc + 1 | 0;
|
|
116
134
|
} else {
|
|
117
135
|
return acc;
|
|
118
136
|
}
|
|
119
137
|
});
|
|
120
|
-
let realErrorCount = errorCount - warningCount | 0;
|
|
138
|
+
let realErrorCount = errorCount - warningCount$1 | 0;
|
|
121
139
|
return `Shape Detection Failed:
|
|
122
140
|
Errors: ` + realErrorCount.toString() + `
|
|
123
|
-
Warnings: ` + warningCount.toString();
|
|
141
|
+
Warnings: ` + warningCount$1.toString();
|
|
124
142
|
}
|
|
125
143
|
|
|
126
144
|
export {
|
|
@@ -19,10 +19,10 @@ open Types
|
|
|
19
19
|
/**
|
|
20
20
|
* Result type for shape detection.
|
|
21
21
|
* Returns either:
|
|
22
|
-
* - Ok(boxes): Array of root-level boxes with nested hierarchy
|
|
22
|
+
* - Ok((boxes, warnings)): Array of root-level boxes with nested hierarchy, plus any warnings
|
|
23
23
|
* - Error(errors): Array of all errors encountered during detection
|
|
24
24
|
*/
|
|
25
|
-
type detectResult = result<array<BoxTracer.box>, array<ErrorTypes.t>>
|
|
25
|
+
type detectResult = result<(array<BoxTracer.box>, array<ErrorTypes.t>), array<ErrorTypes.t>>
|
|
26
26
|
|
|
27
27
|
// ============================================================================
|
|
28
28
|
// Helper Functions
|
|
@@ -121,12 +121,17 @@ let detect = (grid: Grid.t): detectResult => {
|
|
|
121
121
|
// Only corners that are actually top-left of a box will trace successfully.
|
|
122
122
|
let boxes = []
|
|
123
123
|
let traceFailures = [] // Keep track of failures
|
|
124
|
+
let warnings = [] // Collect alignment warnings
|
|
124
125
|
|
|
125
126
|
corners->Array.forEach(corner => {
|
|
126
127
|
switch BoxTracer.traceBox(grid, corner) {
|
|
127
128
|
| Ok(box) => {
|
|
128
129
|
// Successfully traced a box - add to collection
|
|
129
130
|
boxes->Array.push(box)
|
|
131
|
+
|
|
132
|
+
// Validate interior alignment and collect warnings
|
|
133
|
+
let alignmentWarnings = BoxTracer.validateInteriorAlignment(grid, box.bounds)
|
|
134
|
+
alignmentWarnings->Array.forEach(w => warnings->Array.push(w)->ignore)
|
|
130
135
|
}
|
|
131
136
|
| Error(traceError) => {
|
|
132
137
|
// This corner failed to trace as a top-left corner.
|
|
@@ -141,14 +146,14 @@ let detect = (grid: Grid.t): detectResult => {
|
|
|
141
146
|
// No boxes traced successfully
|
|
142
147
|
if Array.length(corners) === 0 {
|
|
143
148
|
// No corners at all - truly empty wireframe
|
|
144
|
-
Ok([])
|
|
149
|
+
Ok(([], []))
|
|
145
150
|
} else if Array.length(traceFailures) > 0 {
|
|
146
151
|
// Had corners but all traces failed - this indicates malformed boxes
|
|
147
152
|
// Return all trace failures as errors
|
|
148
153
|
Error(traceFailures)
|
|
149
154
|
} else {
|
|
150
155
|
// No corners and no failures - empty result
|
|
151
|
-
Ok([])
|
|
156
|
+
Ok(([], []))
|
|
152
157
|
}
|
|
153
158
|
} else {
|
|
154
159
|
// Step 4: Detect dividers for each successfully traced box
|
|
@@ -160,8 +165,8 @@ let detect = (grid: Grid.t): detectResult => {
|
|
|
160
165
|
// Step 6: Build hierarchy using HierarchyBuilder
|
|
161
166
|
switch HierarchyBuilder.buildHierarchy(uniqueBoxes) {
|
|
162
167
|
| Ok(rootBoxes) => {
|
|
163
|
-
// Hierarchy built successfully - return the boxes
|
|
164
|
-
Ok(rootBoxes)
|
|
168
|
+
// Hierarchy built successfully - return the boxes with any warnings
|
|
169
|
+
Ok((rootBoxes, warnings))
|
|
165
170
|
}
|
|
166
171
|
| Error(hierarchyError) => {
|
|
167
172
|
// Hierarchy building failed (e.g., overlapping boxes)
|
|
@@ -210,12 +215,14 @@ let rec flattenBoxes = (boxes: array<BoxTracer.box>): array<BoxTracer.box> => {
|
|
|
210
215
|
*/
|
|
211
216
|
let getStats = (result: detectResult): string => {
|
|
212
217
|
switch result {
|
|
213
|
-
| Ok(boxes) => {
|
|
218
|
+
| Ok((boxes, warnings)) => {
|
|
214
219
|
let rootCount = Array.length(boxes)
|
|
215
220
|
let totalCount = countBoxes(boxes)
|
|
221
|
+
let warningCount = Array.length(warnings)
|
|
216
222
|
`Shape Detection Success:
|
|
217
223
|
Root boxes: ${Int.toString(rootCount)}
|
|
218
|
-
Total boxes (including nested): ${Int.toString(totalCount)}
|
|
224
|
+
Total boxes (including nested): ${Int.toString(totalCount)}
|
|
225
|
+
Warnings: ${Int.toString(warningCount)}`
|
|
219
226
|
}
|
|
220
227
|
| Error(errors) => {
|
|
221
228
|
let errorCount = Array.length(errors)
|
|
@@ -218,6 +218,31 @@ Consider simplifying the structure:
|
|
|
218
218
|
|
|
219
219
|
This is a warning - parsing will continue.`
|
|
220
220
|
};
|
|
221
|
+
case "MisalignedClosingBorder" :
|
|
222
|
+
let actualCol$1 = code.actualCol;
|
|
223
|
+
let expectedCol$1 = code.expectedCol;
|
|
224
|
+
return {
|
|
225
|
+
title: "⚠️ Misaligned closing border",
|
|
226
|
+
message: `The closing '|' at ` + formatPosition(code.position) + ` is not aligned with the box border:
|
|
227
|
+
• Expected column: ` + (expectedCol$1 + 1 | 0).toString() + `
|
|
228
|
+
• Actual column: ` + (actualCol$1 + 1 | 0).toString() + `
|
|
229
|
+
• Off by: ` + Math.abs(expectedCol$1 - actualCol$1 | 0).toString() + ` ` + (
|
|
230
|
+
Math.abs(expectedCol$1 - actualCol$1 | 0) === 1 ? "space" : "spaces"
|
|
231
|
+
) + `
|
|
232
|
+
|
|
233
|
+
The closing border character on this row is not aligned with the right edge of the box.`,
|
|
234
|
+
solution: `💡 Solution:
|
|
235
|
+
Align the closing '|' to column ` + (expectedCol$1 + 1 | 0).toString() + `:
|
|
236
|
+
• ` + (
|
|
237
|
+
expectedCol$1 > actualCol$1 ? "Add" : "Remove"
|
|
238
|
+
) + ` ` + Math.abs(expectedCol$1 - actualCol$1 | 0).toString() + ` space` + (
|
|
239
|
+
Math.abs(expectedCol$1 - actualCol$1 | 0) === 1 ? "" : "s"
|
|
240
|
+
) + ` before the closing '|'
|
|
241
|
+
• Use a monospace font editor to ensure proper alignment
|
|
242
|
+
• Check that all rows in this box have the closing '|' at the same column
|
|
243
|
+
|
|
244
|
+
This is a warning - parsing will continue with the detected box structure.`
|
|
245
|
+
};
|
|
221
246
|
}
|
|
222
247
|
}
|
|
223
248
|
|
|
@@ -198,6 +198,23 @@ Consider simplifying the structure:
|
|
|
198
198
|
This is a warning - parsing will continue.`,
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
| MisalignedClosingBorder({position, expectedCol, actualCol}) => {
|
|
202
|
+
title: "⚠️ Misaligned closing border",
|
|
203
|
+
message: `The closing '|' at ${formatPosition(position)} is not aligned with the box border:
|
|
204
|
+
• Expected column: ${Int.toString(expectedCol + 1)}
|
|
205
|
+
• Actual column: ${Int.toString(actualCol + 1)}
|
|
206
|
+
• Off by: ${Int.toString(Js.Math.abs_int(expectedCol - actualCol))} ${Js.Math.abs_int(expectedCol - actualCol) === 1 ? "space" : "spaces"}
|
|
207
|
+
|
|
208
|
+
The closing border character on this row is not aligned with the right edge of the box.`,
|
|
209
|
+
solution: `💡 Solution:
|
|
210
|
+
Align the closing '|' to column ${Int.toString(expectedCol + 1)}:
|
|
211
|
+
• ${expectedCol > actualCol ? "Add" : "Remove"} ${Int.toString(Js.Math.abs_int(expectedCol - actualCol))} space${Js.Math.abs_int(expectedCol - actualCol) === 1 ? "" : "s"} before the closing '|'
|
|
212
|
+
• Use a monospace font editor to ensure proper alignment
|
|
213
|
+
• Check that all rows in this box have the closing '|' at the same column
|
|
214
|
+
|
|
215
|
+
This is a warning - parsing will continue with the detected box structure.`,
|
|
216
|
+
}
|
|
217
|
+
|
|
201
218
|
| InvalidInput({message}) => {
|
|
202
219
|
title: "❌ Invalid input",
|
|
203
220
|
message: `${message}
|
|
@@ -7,6 +7,7 @@ function getSeverity(code) {
|
|
|
7
7
|
switch (code.TAG) {
|
|
8
8
|
case "UnusualSpacing" :
|
|
9
9
|
case "DeepNesting" :
|
|
10
|
+
case "MisalignedClosingBorder" :
|
|
10
11
|
return "Warning";
|
|
11
12
|
default:
|
|
12
13
|
return "Error";
|
|
@@ -89,6 +90,8 @@ function getCodeName(code) {
|
|
|
89
90
|
return "UnusualSpacing";
|
|
90
91
|
case "DeepNesting" :
|
|
91
92
|
return "DeepNesting";
|
|
93
|
+
case "MisalignedClosingBorder" :
|
|
94
|
+
return "MisalignedClosingBorder";
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
|
|
@@ -59,6 +59,11 @@ type errorCode =
|
|
|
59
59
|
depth: int,
|
|
60
60
|
position: Position.t,
|
|
61
61
|
}) // Nesting depth exceeds recommended level
|
|
62
|
+
| MisalignedClosingBorder({
|
|
63
|
+
position: Position.t,
|
|
64
|
+
expectedCol: int,
|
|
65
|
+
actualCol: int,
|
|
66
|
+
}) // Closing border '|' is not aligned with the box edge (REQ-7)
|
|
62
67
|
|
|
63
68
|
/**
|
|
64
69
|
* Complete parse error with error code, severity, and context
|
|
@@ -75,7 +80,7 @@ type t = {
|
|
|
75
80
|
*/
|
|
76
81
|
let getSeverity = (code: errorCode): severity => {
|
|
77
82
|
switch code {
|
|
78
|
-
| UnusualSpacing(_) | DeepNesting(_) => Warning
|
|
83
|
+
| UnusualSpacing(_) | DeepNesting(_) | MisalignedClosingBorder(_) => Warning
|
|
79
84
|
| _ => Error
|
|
80
85
|
}
|
|
81
86
|
}
|
|
@@ -130,6 +135,7 @@ let getPosition = (code: errorCode): option<Position.t> => {
|
|
|
130
135
|
| InvalidInteractionDSL({position}) => position
|
|
131
136
|
| UnusualSpacing({position}) => Some(position)
|
|
132
137
|
| DeepNesting({position}) => Some(position)
|
|
138
|
+
| MisalignedClosingBorder({position}) => Some(position)
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
|
|
@@ -165,5 +171,6 @@ let getCodeName = (code: errorCode): string => {
|
|
|
165
171
|
| InvalidInteractionDSL(_) => "InvalidInteractionDSL"
|
|
166
172
|
| UnusualSpacing(_) => "UnusualSpacing"
|
|
167
173
|
| DeepNesting(_) => "DeepNesting"
|
|
174
|
+
| MisalignedClosingBorder(_) => "MisalignedClosingBorder"
|
|
168
175
|
}
|
|
169
176
|
}
|
|
@@ -11,10 +11,11 @@ import type {sceneInteractions as Types_sceneInteractions} from '../../src/parse
|
|
|
11
11
|
|
|
12
12
|
import type {t as ErrorTypes_t} from '../../src/parser/Errors/ErrorTypes.gen';
|
|
13
13
|
|
|
14
|
-
/** * Parse result type - either a successful AST or an array of parse errors.
|
|
14
|
+
/** * Parse result type - either a successful (AST, warnings) tuple or an array of parse errors.
|
|
15
|
+
* Warnings are non-fatal issues like misaligned borders that don't prevent parsing.
|
|
15
16
|
* This Result type is compatible with TypeScript through GenType. */
|
|
16
17
|
export type parseResult =
|
|
17
|
-
{ TAG: "Ok"; _0: Types_ast }
|
|
18
|
+
{ TAG: "Ok"; _0: Types_ast; _1: ErrorTypes_t[] }
|
|
18
19
|
| { TAG: "Error"; _0: ErrorTypes_t[] };
|
|
19
20
|
|
|
20
21
|
/** * Interaction parse result type. */
|
package/src/parser/Parser.mjs
CHANGED
|
@@ -76,7 +76,11 @@ function parseSingleScene(sceneContent, sceneMetadata, errors) {
|
|
|
76
76
|
let shapesResult = ShapeDetector.detect(grid);
|
|
77
77
|
let shapes;
|
|
78
78
|
if (shapesResult.TAG === "Ok") {
|
|
79
|
-
|
|
79
|
+
let match = shapesResult._0;
|
|
80
|
+
match[1].forEach(w => {
|
|
81
|
+
errors.push(w);
|
|
82
|
+
});
|
|
83
|
+
shapes = match[0];
|
|
80
84
|
} else {
|
|
81
85
|
shapesResult._0.forEach(err => {
|
|
82
86
|
errors.push(err);
|
|
@@ -111,15 +115,18 @@ function parseSingleScene(sceneContent, sceneMetadata, errors) {
|
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
function parseInternal(wireframe, interactions) {
|
|
114
|
-
let
|
|
118
|
+
let allIssues = [];
|
|
115
119
|
let sceneBlocks = SemanticParser.splitSceneBlocks(wireframe);
|
|
116
120
|
let trimmed = wireframe.trim();
|
|
117
121
|
if (sceneBlocks.length === 0 && trimmed === "") {
|
|
118
122
|
return {
|
|
119
123
|
TAG: "Ok",
|
|
120
|
-
_0:
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
_0: [
|
|
125
|
+
{
|
|
126
|
+
scenes: []
|
|
127
|
+
},
|
|
128
|
+
[]
|
|
129
|
+
]
|
|
123
130
|
};
|
|
124
131
|
}
|
|
125
132
|
let scenes = [];
|
|
@@ -127,7 +134,7 @@ function parseInternal(wireframe, interactions) {
|
|
|
127
134
|
let lines = block.split("\n");
|
|
128
135
|
let match = SemanticParser.parseSceneDirectives(lines);
|
|
129
136
|
let sceneContent = match[1].join("\n");
|
|
130
|
-
let scene = parseSingleScene(sceneContent, match[0],
|
|
137
|
+
let scene = parseSingleScene(sceneContent, match[0], allIssues);
|
|
131
138
|
if (scene !== undefined) {
|
|
132
139
|
scenes.push(scene);
|
|
133
140
|
return;
|
|
@@ -143,23 +150,28 @@ function parseInternal(wireframe, interactions) {
|
|
|
143
150
|
finalAst = mergeInteractionsIntoAST(baseAst, interactionsResult._0);
|
|
144
151
|
} else {
|
|
145
152
|
interactionsResult._0.forEach(err => {
|
|
146
|
-
|
|
153
|
+
allIssues.push(err);
|
|
147
154
|
});
|
|
148
155
|
finalAst = baseAst;
|
|
149
156
|
}
|
|
150
157
|
} else {
|
|
151
158
|
finalAst = baseAst;
|
|
152
159
|
}
|
|
160
|
+
let errors = allIssues.filter(ErrorTypes.isError);
|
|
161
|
+
let warnings = allIssues.filter(ErrorTypes.isWarning);
|
|
153
162
|
let totalElements = Core__Array.reduce(finalAst.scenes, 0, (acc, scene) => acc + scene.elements.length | 0);
|
|
154
|
-
if (
|
|
163
|
+
if (errors.length > 0 && totalElements === 0) {
|
|
155
164
|
return {
|
|
156
165
|
TAG: "Error",
|
|
157
|
-
_0:
|
|
166
|
+
_0: allIssues
|
|
158
167
|
};
|
|
159
168
|
} else {
|
|
160
169
|
return {
|
|
161
170
|
TAG: "Ok",
|
|
162
|
-
_0:
|
|
171
|
+
_0: [
|
|
172
|
+
finalAst,
|
|
173
|
+
warnings
|
|
174
|
+
]
|
|
163
175
|
};
|
|
164
176
|
}
|
|
165
177
|
}
|