1- import { BoxfixResult , BoxfixStats , RIGHT_BORDER_CHARS } from "./types.js" ;
2- import { getDisplayWidth , expandTabs , padBeforeLastChar } from "./width.js" ;
1+ import { BoxfixResult , BoxfixStats , RIGHT_BORDER_CHARS , BoxRegion } from "./types.js" ;
2+ import {
3+ getDisplayWidth ,
4+ expandTabs ,
5+ padBeforeLastChar ,
6+ sliceByDisplayColumn ,
7+ replaceByDisplayColumn ,
8+ } from "./width.js" ;
39import {
410 isDiagram ,
511 isBoundaryLine ,
612 isContentLine ,
713 isTreeLine ,
14+ hasInnerBoundary ,
15+ findInnerTopCornerColumn ,
16+ hasBottomCornerAtColumn ,
17+ findBoundaryEndColumn ,
818} from "./diagram-detector.js" ;
919
20+ /**
21+ * Find all inner box regions within a diagram
22+ * An inner box is detected by finding a top-left corner (┌ or +) not at column 0,
23+ * then tracking until we find the matching bottom-left corner (└ or +) at the same column
24+ */
25+ function findInnerBoxRegions ( lines : string [ ] ) : BoxRegion [ ] {
26+ const regions : BoxRegion [ ] = [ ] ;
27+
28+ for ( let lineIdx = 0 ; lineIdx < lines . length ; lineIdx ++ ) {
29+ const line = lines [ lineIdx ] ;
30+
31+ // Look for inner top boundaries in this line
32+ if ( ! hasInnerBoundary ( line ) ) {
33+ continue ;
34+ }
35+
36+ // Find all inner top corners in this line
37+ let searchCol = 0 ;
38+ while ( true ) {
39+ const startCol = findInnerTopCornerColumn ( line , searchCol ) ;
40+ if ( startCol === - 1 ) {
41+ break ;
42+ }
43+
44+ // Find the end column of this boundary
45+ const endCol = findBoundaryEndColumn ( line , startCol ) ;
46+
47+ // Look for the matching bottom boundary
48+ let endLine = - 1 ;
49+ for ( let bottomIdx = lineIdx + 1 ; bottomIdx < lines . length ; bottomIdx ++ ) {
50+ if ( hasBottomCornerAtColumn ( lines [ bottomIdx ] , startCol ) ) {
51+ // Verify it's a complete bottom boundary by checking end column too
52+ const bottomEndCol = findBoundaryEndColumn ( lines [ bottomIdx ] , startCol ) ;
53+ if ( bottomEndCol === endCol ) {
54+ endLine = bottomIdx ;
55+ break ;
56+ }
57+ }
58+ }
59+
60+ if ( endLine !== - 1 ) {
61+ regions . push ( {
62+ startLine : lineIdx ,
63+ endLine,
64+ startCol,
65+ endCol,
66+ } ) ;
67+ }
68+
69+ // Continue searching for more inner boxes on this line
70+ searchCol = endCol ;
71+ }
72+ }
73+
74+ return regions ;
75+ }
76+
77+ /**
78+ * Result of extracting an inner box
79+ */
80+ interface ExtractedBox {
81+ content : string ;
82+ /** Width of each extracted line (may differ from boundary width for content lines) */
83+ lineWidths : number [ ] ;
84+ }
85+
86+ /**
87+ * Extract an inner box as a standalone diagram
88+ * For content lines, finds the actual right border position to avoid including outer box padding
89+ * Returns null if the extraction doesn't produce a valid box
90+ */
91+ function extractInnerBox ( lines : string [ ] , region : BoxRegion ) : ExtractedBox | null {
92+ const extractedLines : string [ ] = [ ] ;
93+ const lineWidths : number [ ] = [ ] ;
94+ const verticalChars = [ "│" , "┃" , "|" ] ;
95+
96+ for ( let i = region . startLine ; i <= region . endLine ; i ++ ) {
97+ const line = lines [ i ] ;
98+ let extracted = sliceByDisplayColumn ( line , region . startCol , region . endCol ) ;
99+ let extractedWidth = getDisplayWidth ( extracted ) ;
100+
101+ // For content lines (not top/bottom boundary), trim to the actual right border
102+ if ( i !== region . startLine && i !== region . endLine ) {
103+ // Find the last vertical border character in the extracted content
104+ let lastBorderIdx = - 1 ;
105+ for ( let j = extracted . length - 1 ; j >= 0 ; j -- ) {
106+ if ( verticalChars . includes ( extracted [ j ] ) ) {
107+ lastBorderIdx = j ;
108+ break ;
109+ }
110+ }
111+ if ( lastBorderIdx !== - 1 && lastBorderIdx < extracted . length - 1 ) {
112+ // Trim to end at the border character and track the actual width
113+ extracted = extracted . slice ( 0 , lastBorderIdx + 1 ) ;
114+ extractedWidth = getDisplayWidth ( extracted ) ;
115+ }
116+ }
117+
118+ extractedLines . push ( extracted ) ;
119+ lineWidths . push ( extractedWidth ) ;
120+ }
121+
122+ const content = extractedLines . join ( "\n" ) ;
123+
124+ // Validate that the extraction produced a valid box
125+ // The first line should be a boundary line (start with corner character)
126+ const firstLine = extractedLines [ 0 ] ;
127+ const topCorners = [ "┌" , "+" ] ;
128+ if ( ! firstLine || ! topCorners . includes ( firstLine [ 0 ] ) ) {
129+ // Extraction didn't produce a valid box (misaligned inner box)
130+ return null ;
131+ }
132+
133+ // Also validate that content lines start with vertical border
134+ // (they might be offset if the inner box is misaligned)
135+ for ( let i = 1 ; i < extractedLines . length - 1 ; i ++ ) {
136+ const contentLine = extractedLines [ i ] ;
137+ if ( contentLine && ! verticalChars . includes ( contentLine [ 0 ] ) ) {
138+ // Content line doesn't start with border - misaligned inner box
139+ return null ;
140+ }
141+ }
142+
143+ return { content, lineWidths } ;
144+ }
145+
146+ /**
147+ * Reinsert a fixed inner box back at its original position
148+ * Uses the original extracted widths and adjusts for width changes to maintain outer spacing
149+ */
150+ function reinsertInnerBox (
151+ lines : string [ ] ,
152+ region : BoxRegion ,
153+ fixedInner : string ,
154+ originalWidths : number [ ]
155+ ) : string [ ] {
156+ const fixedLines = fixedInner . split ( "\n" ) ;
157+ const result = [ ...lines ] ;
158+
159+ for ( let i = 0 ; i < fixedLines . length ; i ++ ) {
160+ const lineIdx = region . startLine + i ;
161+ if ( lineIdx < result . length ) {
162+ const originalWidth = originalWidths [ i ] || 0 ;
163+ const fixedWidth = getDisplayWidth ( fixedLines [ i ] ) ;
164+ const widthDiff = fixedWidth - originalWidth ;
165+
166+ // Extend the end column to consume trailing space if the fixed content is wider
167+ // This maintains the total line width by taking space from after the inner box
168+ const endCol = region . startCol + originalWidth + Math . max ( 0 , widthDiff ) ;
169+
170+ result [ lineIdx ] = replaceByDisplayColumn (
171+ result [ lineIdx ] ,
172+ region . startCol ,
173+ endCol ,
174+ fixedLines [ i ]
175+ ) ;
176+ }
177+ }
178+
179+ return result ;
180+ }
181+
10182/**
11183 * Fix a single ASCII diagram by correcting right border alignment
12184 *
13185 * Algorithm:
14- * 1. Process the diagram top to bottom, tracking current box context
15- * 2. When we see a boundary line, update the target width for subsequent lines
16- * 3. For content lines shorter than target (by small margin), pad them
17- * 4. This handles nested/stacked boxes correctly by using local context
186+ * 1. Find and recursively fix any inner boxes first
187+ * 2. Process the diagram top to bottom, tracking current box context
188+ * 3. When we see a boundary line, update the target width for subsequent lines
189+ * 4. For content lines shorter than target (by small margin), pad them
190+ * 5. This handles nested/stacked boxes correctly by using local context
18191 */
19192export function boxfixDiagram ( content : string ) : {
20193 result : string ;
21194 linesFixed : number ;
22195} {
23196 // Expand tabs first
24197 const expanded = expandTabs ( content ) ;
25- const lines = expanded . split ( "\n" ) ;
198+ let lines = expanded . split ( "\n" ) ;
199+ let totalLinesFixed = 0 ;
200+
201+ // Phase 1: Find and recursively fix inner boxes
202+ const regions = findInnerBoxRegions ( lines ) ;
203+
204+ // Sort regions: process right-to-left (higher startCol first) to avoid column invalidation
205+ // Also process deeper boxes first (those that start on later lines)
206+ regions . sort ( ( a , b ) => {
207+ if ( a . startCol !== b . startCol ) {
208+ return b . startCol - a . startCol ; // Right to left
209+ }
210+ return b . startLine - a . startLine ; // Later lines first
211+ } ) ;
212+
213+ for ( const region of regions ) {
214+ const extracted = extractInnerBox ( lines , region ) ;
215+ // Skip if extraction didn't produce a valid box (misaligned inner box)
216+ if ( extracted === null ) {
217+ continue ;
218+ }
219+ const { result : fixedInner , linesFixed : innerFixed } = boxfixDiagram ( extracted . content ) ;
220+ totalLinesFixed += innerFixed ;
221+ lines = reinsertInnerBox ( lines , region , fixedInner , extracted . lineWidths ) ;
222+ }
223+
224+ // Phase 2: Fix outer box (existing logic)
26225
27226 // Find all boundary line widths to use as reference
28227 const boundaryWidths = new Set < number > ( ) ;
@@ -32,13 +231,13 @@ export function boxfixDiagram(content: string): {
32231 }
33232 }
34233
35- // If no boundary lines found, return unchanged
234+ // If no boundary lines found, return with just inner fixes
36235 if ( boundaryWidths . size === 0 ) {
37- return { result : content , linesFixed : 0 } ;
236+ return { result : lines . join ( "\n" ) , linesFixed : totalLinesFixed } ;
38237 }
39238
40239 // Process each line, tracking current box context
41- let linesFixed = 0 ;
240+ let outerLinesFixed = 0 ;
42241 let currentTargetWidth = 0 ;
43242
44243 const fixedLines = lines . map ( ( line ) => {
@@ -91,7 +290,7 @@ export function boxfixDiagram(content: string): {
91290 const { padded, spacesAdded } = padBeforeLastChar ( trimmed , targetWidth ) ;
92291
93292 if ( spacesAdded > 0 ) {
94- linesFixed ++ ;
293+ outerLinesFixed ++ ;
95294 return padded + trailingWhitespace ;
96295 }
97296
@@ -100,7 +299,7 @@ export function boxfixDiagram(content: string): {
100299
101300 return {
102301 result : fixedLines . join ( "\n" ) ,
103- linesFixed,
302+ linesFixed : totalLinesFixed + outerLinesFixed ,
104303 } ;
105304}
106305
0 commit comments