Skip to content

Commit 5cdfa2e

Browse files
committed
fix(markdownlint-rules): preserve nested fenced blocks
Track complete opening fence markers and require closing markers to use the same character with at least the opening length. Prevent shorter nested example fences from exposing code to prose checks and producing false one-sentence-per-line violations. Add focused regression coverage for the shared prose iterator and cross-line sentence scan.
1 parent 338a601 commit 5cdfa2e

3 files changed

Lines changed: 38 additions & 4 deletions

File tree

‎markdownlint-rules/utils.js‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,11 @@ function* iterateNonFencedLines(lines) {
6767

6868
const fenceMatch = trimmed.match(/^(```+|~~~+)/);
6969
if (fenceMatch) {
70-
const marker = fenceMatch[1][0] === "`" ? "```" : "~~~";
70+
const marker = fenceMatch[1];
7171
if (!inFence) {
7272
inFence = true;
7373
fenceMarker = marker;
74-
} else if (fenceMarker === marker) {
74+
} else if (fenceMarker[0] === marker[0] && marker.length >= fenceMarker.length) {
7575
inFence = false;
7676
fenceMarker = null;
7777
}
@@ -496,12 +496,12 @@ function* iterateLinesWithFenceInfo(lines) {
496496
const fenceMatch = trimmed.match(/^(```+|~~~+)/);
497497

498498
if (fenceMatch) {
499-
const marker = fenceMatch[1][0] === "`" ? "```" : "~~~";
499+
const marker = fenceMatch[1];
500500
if (!inFence) {
501501
inFence = true;
502502
fenceMarker = marker;
503503
blockType = parseFenceInfo(line);
504-
} else if (fenceMarker === marker) {
504+
} else if (fenceMarker[0] === marker[0] && marker.length >= fenceMarker.length) {
505505
inFence = false;
506506
fenceMarker = null;
507507
blockType = "";

‎test/markdownlint-rules/one-sentence-per-line.test.js‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,22 @@ describe("one-sentence-per-line", () => {
182182
assert.strictEqual(errors.length, 0);
183183
});
184184

185+
it("keeps shorter nested backtick fences inside the outer fenced block", () => {
186+
const lines = [
187+
"````markdown",
188+
"```go",
189+
"func estimateTokens(msgs []inference.Message) int {",
190+
" for _, message := range msgs {",
191+
" }",
192+
"}",
193+
"```",
194+
"````",
195+
];
196+
const config = { "one-sentence-per-line": { checkCrossLine: true } };
197+
const errors = runRule(rule, lines, config);
198+
assert.strictEqual(errors.length, 0);
199+
});
200+
185201
it("skips ATX headings", () => {
186202
const lines = ["## Heading with. Multiple parts."];
187203
const errors = runRule(rule, lines);

‎test/markdownlint-rules/utils.test.js‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,24 @@ describe("utils", () => {
412412
assert.strictEqual(blocks[1][0].lineNumber, 6);
413413
});
414414

415+
it("does not close an outer backtick fence with a shorter nested fence", () => {
416+
const lines = [
417+
"Before fence.",
418+
"````markdown",
419+
"```go",
420+
"Inside nested fence.",
421+
"```",
422+
"````",
423+
"After fence.",
424+
];
425+
const blocks = Array.from(iterateProseBlocks(lines));
426+
assert.strictEqual(blocks.length, 2);
427+
assert.deepStrictEqual(blocks.map((block) => block[0].line), [
428+
"Before fence.",
429+
"After fence.",
430+
]);
431+
});
432+
415433
it("preserves absolute line numbers", () => {
416434
const lines = [
417435
"",

0 commit comments

Comments
 (0)