@@ -70,6 +70,90 @@ let isDividerOnlyEdge = (edgeChars: array<cellChar>): bool => {
7070 ! hasHLineOrChar
7171}
7272
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+
73157/**
74158 * Trace a box starting from the top-left corner position.
75159 *
@@ -134,38 +218,22 @@ let traceBox = (grid: Grid.t, topLeft: Position.t): traceResult => {
134218 // Calculate top width
135219 let topWidth = topRight .col - topLeft .col
136220
137- // Step 4: Scan down from top-right to find bottom-right corner
138- let rightEdgeScan = Grid .scanDown (grid , topRight , isValidVerticalChar )
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 )
139224
140- let bottomRightOpt = {
141- let lastCorner = ref (None )
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 )
150227
151228 switch bottomRightOpt {
152229 | None => {
153- // Right edge scan failed - could be width mismatch
154- // Try tracing from left side to detect width mismatch
155- let leftEdgeScan = Grid .scanDown (grid , topLeft , isValidVerticalChar )
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 )
166233
167- switch bottomLeftOpt {
234+ switch bottomLeftViaLeft {
168235 | None =>
236+ // No bottom-left corner found either - unclosed box
169237 Error (
170238 ErrorTypes .makeSimple (
171239 ErrorTypes .UncloseBox ({
@@ -212,7 +280,7 @@ let traceBox = (grid: Grid.t, topLeft: Position.t): traceResult => {
212280 ),
213281 )
214282 } else {
215- // Widths match but right edge still failed - unclosed box
283+ // Widths match but right edge corners don't align - unclosed box
216284 Error (
217285 ErrorTypes .makeSimple (
218286 ErrorTypes .UncloseBox ({
@@ -372,3 +440,80 @@ let traceBox = (grid: Grid.t, topLeft: Position.t): traceResult => {
372440 )
373441 }
374442}
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+ }
0 commit comments