Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 98 additions & 1 deletion frontend/src/lib/services/numericListFenceNesting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,111 @@ describe("numeric list fence nesting", () => {

for (const isStreaming of [false, true]) {
const renderedContainer = renderMarkdown(markdown, isStreaming);
const nestedCodeBlocks = renderedContainer.querySelectorAll("ol > li pre > code");

expect(nestedCodeBlocks).toHaveLength(1);
expect(nestedCodeBlocks[0].textContent).toBe("{{name}}\n");
expect(renderedContainer.querySelectorAll(":scope > pre")).toHaveLength(0);
}
}
}
});

it("keeps column-zero and continuation-aligned fences nested with intact body", () => {
for (const fenceMarker of ["```", "~~~"]) {
for (const bodyIndent of [0, 1, 2, 3]) {
const indent = " ".repeat(bodyIndent);
const markdown = [
"1. Run this:",
`${indent}${fenceMarker}bash`,
`${indent}npm install`,
`${indent}${fenceMarker}`,
].join("\n");

for (const isStreaming of [false, true]) {
const renderedContainer = renderMarkdown(markdown, isStreaming);
const nestedCodeBlocks = renderedContainer.querySelectorAll("ol > li pre > code");

expect(nestedCodeBlocks).toHaveLength(1);
expect(nestedCodeBlocks[0].textContent).toBe("npm install\n");
expect(renderedContainer.querySelectorAll(":scope > pre")).toHaveLength(0);
}
}
}
});

it("renders over-indented fence body without spurious leading spaces", () => {
for (const fenceMarker of ["```", "~~~"]) {
for (const bodyIndent of [4, 5, 6]) {
const indent = " ".repeat(bodyIndent);
const markdown = [
"1. Run this:",
`${indent}${fenceMarker}bash`,
`${indent}npm install`,
`${indent}${fenceMarker}`,
].join("\n");

for (const isStreaming of [false, true]) {
const renderedContainer = renderMarkdown(markdown, isStreaming);
const nestedCodeBlocks = renderedContainer.querySelectorAll("ol > li pre > code");

expect(nestedCodeBlocks).toHaveLength(1);
expect(nestedCodeBlocks[0].textContent).toBe("npm install\n");
expect(renderedContainer.querySelectorAll(":scope > pre")).toHaveLength(0);
}
}
}
});

it("renders over-indented fences under multi-digit list markers without corruption", () => {
for (const numericListMarker of ["12.", "100."]) {
const requiredIndentation = " ".repeat(numericListMarker.length + 1);
for (const bodyIndent of [
requiredIndentation.length + 1,
requiredIndentation.length + 2,
requiredIndentation.length + 3,
]) {
const indent = " ".repeat(bodyIndent);
const markdown = [
`${numericListMarker} Run this:`,
`${indent}\`\`\`bash`,
`${indent}npm install`,
`${indent}\`\`\``,
].join("\n");

for (const isStreaming of [false, true]) {
const renderedContainer = renderMarkdown(markdown, isStreaming);
const nestedCodeBlocks = renderedContainer.querySelectorAll("ol > li pre > code");

expect(renderedContainer.querySelectorAll("ol > li pre > code")).toHaveLength(1);
expect(nestedCodeBlocks).toHaveLength(1);
expect(nestedCodeBlocks[0].textContent).toBe("npm install\n");
expect(renderedContainer.querySelectorAll(":scope > pre")).toHaveLength(0);
}
}
}
});

it("preserves ragged body line indentation under an over-indented opener", () => {
for (const fenceMarker of ["```", "~~~"]) {
const markdown = [
"1. Run this:",
` ${fenceMarker}bash`,
" lineA",
"lineB",
` ${fenceMarker}`,
].join("\n");

for (const isStreaming of [false, true]) {
const renderedContainer = renderMarkdown(markdown, isStreaming);
const nestedCodeBlocks = renderedContainer.querySelectorAll("ol > li pre > code");

expect(nestedCodeBlocks).toHaveLength(1);
expect(nestedCodeBlocks[0].textContent).toBe("lineA\nlineB\n");
expect(renderedContainer.querySelectorAll(":scope > pre")).toHaveLength(0);
}
}
});

it("accepts CommonMark closing indentation after a column-zero opening fence", () => {
for (const fenceMarker of ["```", "~~~"]) {
for (const closingOffset of [1, 2, 3, 4]) {
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/lib/services/numericListFenceNesting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export function nestNumericListFences(
const fenceState = new ListFenceState();
let awaitedContinuationIndentation: number | null = null;
let bodyIndentation = "";
let bodyDeindentation = 0;
let targetIndentation = 0;
let maximumSourceIndentation = COMMONMARK_MAX_FENCE_INDENTATION;

Expand All @@ -228,10 +229,13 @@ export function nestNumericListFences(
);
fenceState.close();
bodyIndentation = "";
bodyDeindentation = 0;
targetIndentation = 0;
maximumSourceIndentation = COMMONMARK_MAX_FENCE_INDENTATION;
} else {
nestedLines.push(`${bodyIndentation}${markdownLine}`);
const leadingSpaces = leadingSpaceCount(markdownLine);
const strippedLine = markdownLine.slice(Math.min(bodyDeindentation, leadingSpaces));
nestedLines.push(`${bodyIndentation}${strippedLine}`);
}
continue;
}
Expand Down Expand Up @@ -259,9 +263,9 @@ export function nestNumericListFences(
awaitedContinuationIndentation + COMMONMARK_MAX_FENCE_INDENTATION,
);
if (fenceCandidate) {
bodyIndentation = " ".repeat(
Math.max(0, awaitedContinuationIndentation - fenceCandidate.markerIndex),
);
const bodyShift = awaitedContinuationIndentation - fenceCandidate.markerIndex;
bodyIndentation = " ".repeat(Math.max(0, bodyShift));
bodyDeindentation = Math.max(0, -bodyShift);
targetIndentation = awaitedContinuationIndentation;
maximumSourceIndentation =
awaitedContinuationIndentation + COMMONMARK_MAX_FENCE_INDENTATION;
Expand Down