Skip to content

Commit 3ac05aa

Browse files
kmelveclaude
andauthored
fix: prevent padding of multi-pipe connector lines (#6)
Lines with multiple │ characters that form connectors between side-by-side boxes were incorrectly classified as content lines and padded, breaking diagrams like: │ │ │ Add isConnectorLine() function to detect lines with more than 2 vertical bars where all segments between them are whitespace-only. Modify isContentLine() to exclude these connector lines. Fixes issue reported in soderlind/admin-coach-tours@4c0d1de Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ae1102b commit 3ac05aa

File tree

3 files changed

+106
-2
lines changed

3 files changed

+106
-2
lines changed

src/diagram-detector.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,48 @@ export function isBoundaryLine(line: string): boolean {
101101
return false;
102102
}
103103

104+
/**
105+
* Check if a line is a connector line between multiple boxes
106+
* Connector lines have more than 2 vertical bars with only whitespace between them
107+
* Example: "│ │ │" (connector) vs "│ foo │ bar │" (table row with content)
108+
*/
109+
export function isConnectorLine(line: string): boolean {
110+
const trimmed = line.trim();
111+
if (trimmed.length < 3) return false;
112+
113+
const verticalChars: string[] = [...BOX_CHARS.vertical, ...BOX_CHARS.asciiVertical];
114+
115+
// Split by vertical chars and collect segments
116+
const segments: string[] = [];
117+
let current = "";
118+
119+
for (const char of trimmed) {
120+
if (verticalChars.includes(char)) {
121+
segments.push(current);
122+
current = "";
123+
} else {
124+
current += char;
125+
}
126+
}
127+
segments.push(current); // last segment after final pipe
128+
129+
// Count vertical chars (segments.length - 1)
130+
const pipeCount = segments.length - 1;
131+
132+
// Need more than 2 vertical chars to be a connector line
133+
if (pipeCount <= 2) return false;
134+
135+
// Check interior segments (skip first and last as they can be empty)
136+
// All interior segments must be whitespace-only
137+
for (let i = 1; i < segments.length - 1; i++) {
138+
if (segments[i].trim() !== "") {
139+
return false; // Has non-whitespace content
140+
}
141+
}
142+
143+
return true;
144+
}
145+
104146
/**
105147
* Check if a line is a content line that should be normalized
106148
* Content lines both START and END with a vertical border character
@@ -118,6 +160,11 @@ export function isContentLine(line: string): boolean {
118160
const firstChar = trimmed[0];
119161
const lastChar = trimmed[trimmed.length - 1];
120162

163+
// Exclude connector lines (multiple pipes with only whitespace between)
164+
if (isConnectorLine(line)) {
165+
return false;
166+
}
167+
121168
return verticalChars.includes(firstChar) && verticalChars.includes(lastChar);
122169
}
123170

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55

66
export { boxfix, boxfixDiagram } from "./boxfix.js";
77
export { boxfixMarkdown } from "./markdown.js";
8-
export { isDiagram, isBoundaryLine, isContentLine, isTreeLine } from "./diagram-detector.js";
8+
export { isDiagram, isBoundaryLine, isContentLine, isConnectorLine, isTreeLine } from "./diagram-detector.js";
99
export { getDisplayWidth, expandTabs } from "./width.js";
1010
export type { BoxfixResult, BoxfixStats, CodeBlock } from "./types.js";

test/boxfix.test.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
22
import { boxfix, boxfixDiagram } from "../src/boxfix.js";
33
import { boxfixMarkdown } from "../src/markdown.js";
44
import { getDisplayWidth, expandTabs, sliceByDisplayColumn, mapDisplayColumnToCharIndex } from "../src/width.js";
5-
import { isDiagram, isBoundaryLine, isContentLine, isTreeLine, hasInnerBoundary } from "../src/diagram-detector.js";
5+
import { isDiagram, isBoundaryLine, isContentLine, isTreeLine, hasInnerBoundary, isConnectorLine } from "../src/diagram-detector.js";
66
import { extractFilePath } from "../src/cli.js";
77

88
describe("getDisplayWidth", () => {
@@ -76,6 +76,37 @@ describe("isBoundaryLine", () => {
7676
});
7777
});
7878

79+
describe("isConnectorLine", () => {
80+
it("detects connector line with 3 pipes and whitespace", () => {
81+
expect(isConnectorLine("│ │ │")).toBe(true);
82+
});
83+
84+
it("detects connector line with spaces and indentation", () => {
85+
expect(isConnectorLine(" │ │ │")).toBe(true);
86+
});
87+
88+
it("detects ASCII connector line", () => {
89+
expect(isConnectorLine("| | |")).toBe(true);
90+
});
91+
92+
it("rejects table row with content between pipes", () => {
93+
expect(isConnectorLine("│ foo │ bar │")).toBe(false);
94+
});
95+
96+
it("rejects line with only 2 pipes (normal content)", () => {
97+
expect(isConnectorLine("│ content │")).toBe(false);
98+
});
99+
100+
it("rejects line with only 2 pipes and whitespace", () => {
101+
expect(isConnectorLine("│ │")).toBe(false);
102+
});
103+
104+
it("rejects short lines", () => {
105+
expect(isConnectorLine("│")).toBe(false);
106+
expect(isConnectorLine("││")).toBe(false);
107+
});
108+
});
109+
79110
describe("isContentLine", () => {
80111
it("detects line ending with │", () => {
81112
expect(isContentLine("│ content │")).toBe(true);
@@ -88,6 +119,11 @@ describe("isContentLine", () => {
88119
it("rejects boundary line", () => {
89120
expect(isContentLine("┌───┐")).toBe(false);
90121
});
122+
123+
it("rejects connector line with multiple pipes", () => {
124+
expect(isConnectorLine("│ │ │")).toBe(true);
125+
expect(isContentLine("│ │ │")).toBe(false);
126+
});
91127
});
92128

93129
describe("hasInnerBoundary", () => {
@@ -289,6 +325,27 @@ describe("boxfixDiagram", () => {
289325
const { result } = boxfixDiagram(input);
290326
expect(result).toBe(expected);
291327
});
328+
329+
it("does not modify connector lines between side-by-side boxes", () => {
330+
// Regression test: connector lines with multiple pipes should not be padded
331+
const input = `┌─────────────┐ ┌─────────────┐ ┌─────────────┐
332+
│ Box 1 │ │ Box 2 │ │ Box 3 │
333+
└─────────────┘ └─────────────┘ └─────────────┘
334+
│ │ │
335+
└─────────────┴─────────────┘`;
336+
const { result, linesFixed } = boxfixDiagram(input);
337+
expect(result).toBe(input);
338+
expect(linesFixed).toBe(0);
339+
});
340+
341+
it("does not modify spaced connector lines (real-world bug)", () => {
342+
// Regression test from soderlind/admin-coach-tours
343+
// The line "│ │ │" should not be padded
344+
const input = ` │ │ │`;
345+
// This line should not be treated as a content line
346+
expect(isContentLine(input)).toBe(false);
347+
expect(isConnectorLine(input)).toBe(true);
348+
});
292349
});
293350

294351
describe("boxfix", () => {

0 commit comments

Comments
 (0)