Skip to content

[Detail Bug] Markdown: Fenced code blocks in numeric lists can close early, dropping content after an indented ``` line #244

Description

@detail-app

Detail Bug Report

https://app.detail.dev/org_befd6425-a158-4e24-9d4d-1e5c08769515/bugs/bug_82ed997f-5a37-479d-9d2e-f5e12428c09c

Introduced in 724c810 by @WilliamAGH on Sep 1, 2026

Summary

  • Context: nestNumericListFences (in frontend/src/lib/services/numericListFenceNesting.ts) re-indents streamed/plain fenced code blocks so that CommonMark attaches them to a preceding numeric list item; parseMarkdown runs this on every message before lexing. All HTML evidence below is from marked@15.0.12 (the lock resolution of marked@^15.0.0), gfm: true, breaks: true.
  • Bug: In commit 724c810e the maximum indentation used to detect a closing fence during the open-fence pass was changed to awaitedContinuationIndentation + COMMONMARK_MAX_FENCE_INDENTATION and no longer subtracts the bodyIndentation that was applied to body lines, so a bare ``` (or ~~~) line used as code content inside a reparented fence is misclassified as the closing fence whenever its source indentation falls in the window (openingSourceIndentation + 3, awaitedContinuationIndentation + 3].
  • Actual vs. expected: The fence should remain open and the bare-fence line kept as literal content (the pre-724c810e behavior and the plain-marked behavior); instead the block is closed prematurely, the inner ``` and everything after it up to the real close are dropped/repurposed, and trailing content leaks out of the list item.
  • Impact: Code-fence content shown inside numeric list items in chat is silently dropped / split out of the list item. For deep numeric markers (e.g. 14., 123., …) the misclassification window widens, so even a moderately indented demonstration of nested fences loses content and produces stray empty <pre><code> blocks.

Code with Bug

frontend/src/lib/services/numericListFenceNesting.ts:

        bodyIndentation = " ".repeat(
          Math.max(0, awaitedContinuationIndentation - fenceCandidate.markerIndex),
        );
        targetIndentation = awaitedContinuationIndentation;
        maximumSourceIndentation =
          awaitedContinuationIndentation + COMMONMARK_MAX_FENCE_INDENTATION; // <-- BUG 🔴 ignores bodyIndentation shift applied to body lines
        fenceState.open(fenceCandidate.marker);

Explanation

nestNumericListFences shifts code-fence body lines by bodyIndentation to reparent a fence under a numeric list item while preserving indentation relative to the opening fence.

Closing-fence detection, however, still needs to use the source-frame CommonMark rule: a closing fence may be indented at most 3 spaces past the opening fence’s source column. Before 724c810e, maximumSourceIndentation effectively enforced this as openingSourceIndentation + 3 by subtracting bodyIndentation.length. After 724c810e, the cutoff became awaitedContinuationIndentation + 3 (larger whenever the opening fence is re-indented), so a bare fence line that is actually intended as code content (e.g. an indented ``` inside the block) falls into the widened window and is incorrectly treated as the closer.

Empirical confirmation (authority is plain marked@15.0.12 on the unmodified source):

Input:

1. Example of nested:
```text
    ```
more
```

Current behavior after preprocessing: the ``` line is consumed as the close, producing empty/stray blocks and leaking more out of the list item:

<ol>
<li>Example of nested:<pre><code class="language-text">
</code></pre>
</li>
</ol>
<p>more</p>
<pre><code>
</code></pre>

Plain marked@15.0.12 without preprocessing keeps ```\nmore as code-block content (closing fences can’t be indented >3 past a column-0 opener), demonstrating the preprocessing step is reinterpreting content boundaries rather than only reparenting structure.

Codebase Inconsistency

The in-tree test added with the change (numericListFenceNesting.test.ts:103-124) asserts that a bare fence at source columns 4–6 closes a block whose opening fence is at column 0. Plain marked@15.0.12 on the same source treats columns 4–6 as content, so the test codifies a non-CommonMark behavior and locks in the misclassification.

Recommended Fix

Restore the body-indentation correction so closing-fence detection remains relative to the opening fence’s source indentation:

maximumSourceIndentation =
  awaitedContinuationIndentation +
  COMMONMARK_MAX_FENCE_INDENTATION -
  bodyIndentation.length;

Update the test added in 724c810e to expect the bare fence at offsets 1–4 (columns 4–7) to be preserved as content (plain marked@15.0.12 keeps all of them as content).

History

This bug was introduced in commit 724c810. The change removed - bodyIndentation.length from the maximumSourceIndentation calculation in the open-fence pass, widening the closing-fence cutoff and causing bare fence markers used as content to be misclassified as closers; the commit also added a test that codifies this widened cutoff. The prior behavior (from e57f73b1) computed maximumSourceIndentation = awaitedContinuationIndentation + COMMONMARK_MAX_FENCE_INDENTATION - bodyIndentation.length, keeping the cutoff relative to the opening fence.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions