Skip to content

Commit 48e99b9

Browse files
committed
Fix #88, #93: Strip markdown prefixes before table prettification
Bordered tables inside blockquotes (>) and list markers (1., -, *, +) are now correctly found and prettified. Added MarkdownPrefixStripper that strips markdown context prefixes before the pipeline runs and restores them after, with zero changes to the existing table finding/prettification components. - New: src/modelFactory/markdownPrefixStripper.ts - Modified: src/prettyfiers/multiTablePrettyfier.ts (4 lines) - Added system tests for both scenarios - Added 23 unit tests for the prefix stripper
1 parent 09992a8 commit 48e99b9

8 files changed

Lines changed: 314 additions & 3 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
1111

1212
### Fixed
1313
- Issue #85: Fixed markdown spec compliance for unbordered tables with center/right aligned first columns. First column now uses left padding logic regardless of alignment to prevent invalid markdown output.
14+
- Issue #88: Fixed bordered tables inside numbered/bullet lists having their list marker incorporated as a data column and losing their right border.
15+
- Issue #93: Fixed tables inside blockquotes not being prettified.
1416

1517
### Changed
1618
- **BREAKING**: Updated NPM package compilation target from ES5 to ES2022. Requires Node.js 16.11+.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export class MarkdownPrefixStripper {
2+
3+
public strip(text: string): { strippedText: string; prefixes: string[] } {
4+
const lines = text.match(/[^\n]*\n|[^\n]+/g) || [""];
5+
const prefixes: string[] = [];
6+
const strippedLines: string[] = [];
7+
8+
for (const line of lines) {
9+
const prefix = this.detectPrefix(line);
10+
prefixes.push(prefix);
11+
strippedLines.push(line.substring(prefix.length));
12+
}
13+
14+
return { strippedText: strippedLines.join(""), prefixes };
15+
}
16+
17+
public restore(text: string, prefixes: string[]): string {
18+
const lines = text.match(/[^\n]*\n|[^\n]+/g) || [""];
19+
const result: string[] = [];
20+
21+
for (let i = 0; i < lines.length; i++) {
22+
const prefix = i < prefixes.length ? prefixes[i] : "";
23+
result.push(prefix + lines[i]);
24+
}
25+
26+
return result.join("");
27+
}
28+
29+
private detectPrefix(line: string): string {
30+
let prefix = "";
31+
let remaining = line;
32+
33+
// Layer 1: Blockquote markers (always strip - unambiguous Markdown syntax)
34+
const bqMatch = remaining.match(/^(\s*(?:>\s*)+)/);
35+
if (bqMatch) {
36+
prefix += bqMatch[1];
37+
remaining = remaining.substring(bqMatch[1].length);
38+
}
39+
40+
// Layer 2: List markers (only strip when followed by whitespace + |, i.e., bordered tables)
41+
const listMatch = remaining.match(/^(\s*(?:\d+[.)]|[-*+]))(?=\s+\|)/);
42+
if (listMatch) {
43+
prefix += listMatch[1];
44+
}
45+
46+
return prefix;
47+
}
48+
}

src/prettyfiers/multiTablePrettyfier.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { Range } from "../models/doc/range";
33
import { TableFinder } from "../tableFinding/tableFinder";
44
import { SizeLimitChecker } from "./sizeLimit/sizeLimitChecker";
55
import { SingleTablePrettyfier } from "./singleTablePrettyfier";
6+
import { MarkdownPrefixStripper } from "../modelFactory/markdownPrefixStripper";
67

78
export class MultiTablePrettyfier {
89
constructor(
910
private readonly _tableFinder: TableFinder,
1011
private readonly _singleTablePrettyfier: SingleTablePrettyfier,
11-
private readonly _sizeLimitChecker: SizeLimitChecker
12+
private readonly _sizeLimitChecker: SizeLimitChecker,
13+
private readonly _prefixStripper: MarkdownPrefixStripper = new MarkdownPrefixStripper()
1214
) { }
1315

1416
public formatTables(input: string): string
@@ -17,7 +19,9 @@ export class MultiTablePrettyfier {
1719
return input;
1820
}
1921

20-
let document = new Document(input);
22+
const { strippedText, prefixes } = this._prefixStripper.strip(input);
23+
24+
let document = new Document(strippedText);
2125
let tableRange: Range | null = null;
2226
let tableSearchStartLine = 0;
2327

@@ -27,6 +31,6 @@ export class MultiTablePrettyfier {
2731
tableSearchStartLine = tableRange.endLine + 1;
2832
}
2933

30-
return document.getText();
34+
return this._prefixStripper.restore(document.getText(), prefixes);
3135
}
3236
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
> | Original | Bitmask |
2+
> |---------:|--------:|
3+
> | 010110 | 101101 |
4+
5+
Some text between tables
6+
7+
>> | col1 | col2 | col3 |
8+
>> |------|------|------|
9+
>> | a | b | c |
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
> | Original | Bitmask |
2+
> |---------:|--------:|
3+
> | 010110 | 101101 |
4+
5+
Some text between tables
6+
7+
>> |col1|col2|col3|
8+
>> |-|-|-|
9+
>> |a|b|c|
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
1. | Overflow | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | Base-10 Value |
2+
|-------------:|----:|---:|---:|---:|--:|--:|--:|--:|--------------:|
3+
| Inapplicable | | | | | | | | | 20 |
4+
| | | | | | | | | | 40 |
5+
| | | | | | | | | | 80 |
6+
7+
2. | col1 | col2 |
8+
|------|------|
9+
| a | b |
10+
11+
- | col1 | col2 |
12+
|------|------|
13+
| x | y |
14+
15+
* | col1 | col2 |
16+
|------|------|
17+
| x | y |
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
1. |Overflow|128|64|32|16|8|4|2|1|Base-10 Value|
2+
|-:|-:|-:|-:|-:|-:|-:|-:|-:|-:|
3+
|Inapplicable| | | | | | | | |20|
4+
| | | | | | | | | |40|
5+
| | | | | | | | | |80|
6+
7+
2. |col1|col2|
8+
|-|-|
9+
|a|b|
10+
11+
- |col1|col2|
12+
|-|-|
13+
|x|y|
14+
15+
* |col1|col2|
16+
|-|-|
17+
|x|y|
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import * as assert from "assert";
2+
import { MarkdownPrefixStripper } from "../../../src/modelFactory/markdownPrefixStripper";
3+
4+
suite("MarkdownPrefixStripper tests", () => {
5+
6+
suite("strip()", () => {
7+
8+
test("strips blockquote prefix from all lines", () => {
9+
const input = "> | col1 | col2 |\n> |------|------|\n> | a | b |";
10+
const result = createSut().strip(input);
11+
12+
assert.strictEqual(result.strippedText, "| col1 | col2 |\n|------|------|\n| a | b |");
13+
assert.deepStrictEqual(result.prefixes, ["> ", "> ", "> "]);
14+
});
15+
16+
test("strips nested blockquote prefix from all lines", () => {
17+
const input = ">> | col1 | col2 |\n>> |------|\n>> | a |";
18+
const result = createSut().strip(input);
19+
20+
assert.strictEqual(result.strippedText, "| col1 | col2 |\n|------|\n| a |");
21+
assert.deepStrictEqual(result.prefixes, [">> ", ">> ", ">> "]);
22+
});
23+
24+
test("strips blockquote prefix with spaces before >", () => {
25+
const input = " > | col1 |\n > |------|\n > | a |";
26+
const result = createSut().strip(input);
27+
28+
assert.strictEqual(result.strippedText, "| col1 |\n|------|\n| a |");
29+
assert.deepStrictEqual(result.prefixes, [" > ", " > ", " > "]);
30+
});
31+
32+
test("strips numbered list marker when followed by whitespace and pipe", () => {
33+
const input = "1.\t|col1|col2|\n\t|-|-|\n\t|a|b|";
34+
const result = createSut().strip(input);
35+
36+
assert.strictEqual(result.strippedText, "\t|col1|col2|\n\t|-|-|\n\t|a|b|");
37+
assert.deepStrictEqual(result.prefixes, ["1.", "", ""]);
38+
});
39+
40+
test("strips numbered list marker with closing parenthesis", () => {
41+
const input = "1) |col1|\n |-|\n |a|";
42+
const result = createSut().strip(input);
43+
44+
assert.strictEqual(result.strippedText, " |col1|\n |-|\n |a|");
45+
assert.deepStrictEqual(result.prefixes, ["1)", "", ""]);
46+
});
47+
48+
test("strips bullet list marker (-) when followed by whitespace and pipe", () => {
49+
const input = "- |col1|col2|\n |-|-|\n |x|y|";
50+
const result = createSut().strip(input);
51+
52+
assert.strictEqual(result.strippedText, " |col1|col2|\n |-|-|\n |x|y|");
53+
assert.deepStrictEqual(result.prefixes, ["-", "", ""]);
54+
});
55+
56+
test("strips bullet list marker (*) when followed by whitespace and pipe", () => {
57+
const input = "* |col1|col2|\n |-|-|\n |x|y|";
58+
const result = createSut().strip(input);
59+
60+
assert.strictEqual(result.strippedText, " |col1|col2|\n |-|-|\n |x|y|");
61+
assert.deepStrictEqual(result.prefixes, ["*", "", ""]);
62+
});
63+
64+
test("strips bullet list marker (+) when followed by whitespace and pipe", () => {
65+
const input = "+ |col1|col2|\n |-|-|\n |x|y|";
66+
const result = createSut().strip(input);
67+
68+
assert.strictEqual(result.strippedText, " |col1|col2|\n |-|-|\n |x|y|");
69+
assert.deepStrictEqual(result.prefixes, ["+", "", ""]);
70+
});
71+
72+
test("does not strip list marker when no pipe follows", () => {
73+
const input = "1. Just a list item\n- Another item\n* Third item";
74+
const result = createSut().strip(input);
75+
76+
assert.strictEqual(result.strippedText, input);
77+
assert.deepStrictEqual(result.prefixes, ["", "", ""]);
78+
});
79+
80+
test("strips combined blockquote and list marker", () => {
81+
const input = "> 1. |col1|\n> |-|\n> |a|";
82+
const result = createSut().strip(input);
83+
84+
assert.strictEqual(result.strippedText, " |col1|\n|-|\n|a|");
85+
assert.deepStrictEqual(result.prefixes, ["> 1.", "> ", "> "]);
86+
});
87+
88+
test("does not strip content that merely starts with whitespace", () => {
89+
const input = "\t|col1|col2|\n\t|-|-|\n\t|a|b|";
90+
const result = createSut().strip(input);
91+
92+
assert.strictEqual(result.strippedText, input);
93+
assert.deepStrictEqual(result.prefixes, ["", "", ""]);
94+
});
95+
96+
test("handles mixed prefixed and non-prefixed lines", () => {
97+
const input = "Some text\n> |col1|\n> |-|\n> |a|\nMore text";
98+
const result = createSut().strip(input);
99+
100+
assert.strictEqual(result.strippedText, "Some text\n|col1|\n|-|\n|a|\nMore text");
101+
assert.deepStrictEqual(result.prefixes, ["", "> ", "> ", "> ", ""]);
102+
});
103+
104+
test("is no-op for plain table without prefix", () => {
105+
const input = "|col1|col2|\n|-|-|\n|a|b|";
106+
const result = createSut().strip(input);
107+
108+
assert.strictEqual(result.strippedText, input);
109+
assert.deepStrictEqual(result.prefixes, ["", "", ""]);
110+
});
111+
112+
test("preserves line endings", () => {
113+
const input = "> |col1|\r\n> |-|\r\n> |a|";
114+
const result = createSut().strip(input);
115+
116+
assert.strictEqual(result.strippedText, "|col1|\r\n|-|\r\n|a|");
117+
});
118+
119+
test("strips multi-digit numbered list marker", () => {
120+
const input = "10. |col1|\n |-|\n |a|";
121+
const result = createSut().strip(input);
122+
123+
assert.strictEqual(result.strippedText, " |col1|\n |-|\n |a|");
124+
assert.deepStrictEqual(result.prefixes, ["10.", "", ""]);
125+
});
126+
});
127+
128+
suite("restore()", () => {
129+
130+
test("prepends stored prefixes to each line", () => {
131+
const text = "| col1 |\n|------|\n| a |";
132+
const prefixes = ["> ", "> ", "> "];
133+
const result = createSut().restore(text, prefixes);
134+
135+
assert.strictEqual(result, "> | col1 |\n> |------|\n> | a |");
136+
});
137+
138+
test("handles mixed prefixes", () => {
139+
const text = "\t| col1 |\n\t|------|\n\t| a |";
140+
const prefixes = ["1.", "", ""];
141+
const result = createSut().restore(text, prefixes);
142+
143+
assert.strictEqual(result, "1.\t| col1 |\n\t|------|\n\t| a |");
144+
});
145+
146+
test("handles empty prefixes as no-op", () => {
147+
const text = "|col1|\n|-|\n|a|";
148+
const prefixes = ["", "", ""];
149+
const result = createSut().restore(text, prefixes);
150+
151+
assert.strictEqual(result, text);
152+
});
153+
154+
test("preserves line endings", () => {
155+
const text = "|col1|\r\n|-|\r\n|a|";
156+
const prefixes = ["> ", "> ", "> "];
157+
const result = createSut().restore(text, prefixes);
158+
159+
assert.strictEqual(result, "> |col1|\r\n> |-|\r\n> |a|");
160+
});
161+
});
162+
163+
suite("roundtrip", () => {
164+
165+
test("strip() + restore() preserves original text with blockquote prefix", () => {
166+
const original = "> | col1 | col2 |\n> |------|------|\n> | a | b |";
167+
const sut = createSut();
168+
const { strippedText, prefixes } = sut.strip(original);
169+
const restored = sut.restore(strippedText, prefixes);
170+
171+
assert.strictEqual(restored, original);
172+
});
173+
174+
test("strip() + restore() preserves original text with list marker prefix", () => {
175+
const original = "1.\t|col1|col2|\n\t|-|-|\n\t|a|b|";
176+
const sut = createSut();
177+
const { strippedText, prefixes } = sut.strip(original);
178+
const restored = sut.restore(strippedText, prefixes);
179+
180+
assert.strictEqual(restored, original);
181+
});
182+
183+
test("strip() + restore() preserves text without any prefix", () => {
184+
const original = "|col1|col2|\n|-|-|\n|a|b|";
185+
const sut = createSut();
186+
const { strippedText, prefixes } = sut.strip(original);
187+
const restored = sut.restore(strippedText, prefixes);
188+
189+
assert.strictEqual(restored, original);
190+
});
191+
192+
test("strip() + restore() preserves text with CRLF endings", () => {
193+
const original = "> |col1|\r\n> |-|\r\n> |a|";
194+
const sut = createSut();
195+
const { strippedText, prefixes } = sut.strip(original);
196+
const restored = sut.restore(strippedText, prefixes);
197+
198+
assert.strictEqual(restored, original);
199+
});
200+
});
201+
202+
function createSut(): MarkdownPrefixStripper {
203+
return new MarkdownPrefixStripper();
204+
}
205+
});

0 commit comments

Comments
 (0)