Skip to content

fix(markdown): lift under-indented fence bodies into numeric list items - #232

Open
detail-app[bot] wants to merge 1 commit into
devfrom
detail/bug-fix/fix-markdown-lift-under-indented-fence-bodies-into-acc7f7
Open

fix(markdown): lift under-indented fence bodies into numeric list items#232
detail-app[bot] wants to merge 1 commit into
devfrom
detail/bug-fix/fix-markdown-lift-under-indented-fence-bodies-into-acc7f7

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Detail bug report: View on Detail

Closes #219

Bug

nestNumericListFences (the streamed-markdown preprocessor in frontend/src/lib/services/numericListFenceNesting.ts) computes a single fixed bodyIndentation prefix for every fence body line as max(0, continuationColumn − openingFenceColumn). When the opening fence rails are already indented to (or beyond) the list-item continuation column but the literal code body sits at a lower column (e.g. rails at column 3 under 1., body flush at column 0), that prefix collapses to empty, so the under-indented body lines pass through unchanged.

marked then closes the list item mid-fence, producing user-facing corruption:

  • The nested <pre><code> inside the list item renders empty.
  • The code body leaks out as a loose <p> paragraph.
  • The following numbered step (e.g. 2. Step two) is rendered as a top-level indented code block instead of an <ol><li>, breaking the rest of the list.

The defect is a logic gap, not a by-design refusal: the existing suite already rescued an identical flush-left body when the opening fence was at column 0 (bodyIndentation = 3), but failed it solely because the opening fence moved to the continuation column (bodyIndentation = 0).

Fix

Re-indent each fence body line based on its own leading spaces so the emitted line reaches at least the list-item continuation column, instead of relying on one fixed prefix derived only from the opening fence's position:

const lineIndentation = leadingSpaceCount(markdownLine);
const padding = Math.max(0, targetIndentation - bodyIndentation.length - lineIndentation);
nestedLines.push(`${" ".repeat(padding)}${bodyIndentation}${markdownLine}`);

The padding is layered on top of the existing uniform-shift bodyIndentation prefix and clamps to 0 whenever the old formula already placed the line at or beyond the continuation column, so it only changes lines the old formula left below the continuation boundary. This is a deliberate surgical clamp rather than the max(continuation, lineIndent) re-placement suggested in the report, which would compress already over-indented bodies (e.g. a 5-space body under a flush-left fence would drop to 2 spaces) and regress existing behavior — a regression guard for that alternative is included in the tests.

Testing

  • Unit tests, typecheck, lint, and build all pass. The targeted numericListFenceNesting.test.ts suite (12 tests) and the full frontend vitest suite (356 tests across 31 files) are green; svelte-check reports 0 errors/0 warnings; oxlint/eslint/ast-grep lint is clean; oxfmt format check passes; the vite production build succeeds.
  • Four regression tests were added to numericListFenceNesting.test.ts, each guarding a distinct footgun: the exact bug reproducer (rails at the continuation column, body flush-left, across both fence markers, LF/CRLF/CR, and streaming + non-streaming), a mixed-indentation body (some lines below the rails, one above) in a single block, the opening-column symmetry (the same flush-left body is rescued whether the opening fence is at column 0 or at the continuation column), and an over-indented body preserved unchanged under a flush-left fence (pins the surgical fix against the naive clamp alternative).
  • End-to-end verification against a running app could not be completed. The Spring Boot jar boots and serves the frontend (HTTP 200) and the CSRF handshake succeeds, but the chat stream dies at the retrieval stage with stream.provider.fatal-error before any markdown is generated: retrieval requires Qdrant collections that are absent locally and an embedding gateway that returns 401: Invalid API Key for a dummy credential, and populating those collections needs a real OPENAI_API_KEY for the private gateway, which is not available in this environment. Even with a valid key, the model's markdown output is non-deterministic so the exact reproducer shape cannot be forced through the chat UI. The committed renderer-level tests feed the exact reproducer markdown directly into parseMarkdown (preprocess → marked → DOMPurify) and assert the expected <ol><li>…<pre><code>…</code></pre></li><li>…</li></ol> DOM, which is the deterministic substitute.

Automatic Fixes PRs can be configured here.

@detail-app
detail-app Bot requested a review from WilliamAGH September 6, 2026 13:58
@detail-app detail-app Bot added the bug Something isn't working label Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Detail Bug] Chat markdown: fenced code blocks in numbered lists render empty and break subsequent list items

1 participant