Skip to content

fix(markdown): de-indent over-indented fence bodies under numeric lists - #231

Open
detail-app[bot] wants to merge 1 commit into
devfrom
detail/bug-fix/fix-markdown-de-indent-over-indented-fence-bodies-96bfbd
Open

detail-app[bot] wants to merge 1 commit into
devfrom
detail/bug-fix/fix-markdown-de-indent-over-indented-fence-bodies-96bfbd

Conversation

@detail-app

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

Copy link
Copy Markdown
Contributor

Detail bug report: View on Detail

Bug

Fenced code blocks nested inside ordered (numeric) list items rendered with spurious leading spaces on every body line when the fence was indented a few columns past the list item's continuation column. For a 1. item (continuation column 3), a fence at indent 4, 5, or 6 rendered codeText as " npm install\n", " npm install\n", " npm install\n" instead of "npm install\n". The corruption window scaled with marker width (12. → 5–7, 100. → 6–8) and affected both streaming and non-streaming renders identically.

Root cause: In nestNumericListFences (numericListFenceNesting.ts), the fence opener/closer are re-indented to the list's continuation column (targetIndentation), but the body shift was computed as Math.max(0, continuation − openerIndent). For over-indented openers this clamps to 0, so the body keeps its original larger indent while the opener is pulled left — and marked, which strips the opener's (now smaller) indent from each content line, leaves openerIndent − continuation extra spaces on every sufficiently-indented body line.

Fix

Apply the signed shift consistently to the opener and every body line, clipped at each body line's own leading-space count. In numericListFenceNesting.ts:

  • Store bodyShift = continuation − openerIndent at fence-open time, split into bodyIndentation (rightward prepend, for the existing under-indented D ≥ 0 case) and a new bodyDeindentation = max(0, −bodyShift) (leftward strip, for the over-indented D < 0 case).
  • Body-line emission now strips min(bodyDeindentation, leadingSpaceCount(line)) leading spaces from each line before prepending bodyIndentation, so the body moves by the same signed delta as the opener and the non-whitespace suffix is preserved. The closing-fence branch already re-indented to targetIndentation regardless, so it was already consistent.

This establishes the invariant: for a body line with k leading spaces and opener delta D = targetIndentation − openerIndent, the emitted line has max(0, k + D) leading spaces.

Testing

  • Unit tests (vitest): the existing numericListFenceNesting.test.ts and markdown.test.ts suites pass (12 + 49 tests). Added regression tests covering the over-indented bug window (indent 4–6 under 1.), multi-digit markers (12./100. at continuation+1..+3), a ragged body (opener/body at indent 4 with a 0-indent line in between rendered as lineA\nlineB\n), and column-zero/under-indented cases (indent 0–3) to guard against reintroducing the fence-escapes-list failure the preprocessor exists to fix. Extended the existing mixed-indentation test to assert exact textContent (it previously only checked structure, so the over-indented sub-case was silently corrupted). All assertions hold for both isStreaming=false and isStreaming=true, confirming streaming/non-streaming parity.
  • Routine checks: svelte-check typecheck, oxlint (incl. --type-aware), eslint, ast-grep, and oxfmt format check all pass with zero errors/warnings; the vite production build succeeds.
  • Browser/accessibility e2e (Playwright + axe-core): installed Chromium via playwright install --with-deps, then ran the existing answer-accessibility.spec.ts (renders the chat-answer DOM through parseMarkdown against a stubbed SSE stream, runs WCAG A/AA scans). 4/4 tests pass — the rendered ol > li pre > code structure is still well-formed.
  • Manual end-to-end smoke (live app): could not run. The Spring backend fail-fast blocks startup without OPENAI_API_KEY (Gateway embeddings require OPENAI_API_KEY to be configured, EmbeddingConfig.java:76), which isn't present in this environment. The Playwright e2e and vitest streaming-parity assertions cover the same production render path this smoke targets.

Closes #218


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] Markdown: ordered-list fenced code blocks render with extra leading spaces when fence is slightly over-indented

1 participant