Skip to content

fix(markdown): preserve prose around nested inline lists - #239

Open
detail-app[bot] wants to merge 1 commit into
devfrom
detail/bug-fix/fix-markdown-preserve-prose-around-nested-inline-l-c046a9
Open

detail-app[bot] wants to merge 1 commit into
devfrom
detail/bug-fix/fix-markdown-preserve-prose-around-nested-inline-l-c046a9

Conversation

@detail-app

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

Copy link
Copy Markdown
Contributor

Detail bug report: View on Detail

Bug

Closes #226

InlineListParser rewrites flat <p> paragraphs containing inline list markers into <ol>/<ul> HTML lists, and extracts a sub-list from any list item whose text contains a colon followed by an inline list (e.g. 1. Parent: a. Child one b. Child two 2. Sibling).

When a list item's post-colon tail contained prose between the colon and the first nested-list marker and/or after the last nested-list marker, that prose was silently dropped. The nested-list consumers (tryConvert and renderNestedListsRecursively) iterated only nestedParse.primaryBlock().entryLabels() and never read nestedParse.leadingText() / nestedParse.trailingText(), even though Parse.tryParse captured both.

End-to-end repro via UnifiedMarkdownService.process:

  • Input: Key points: 1. Setup phase: First gather tools a. Knife b. Spoon 2. Final cleanup
  • Actual before fix: <p>Key points:</p><ol><li>Setup phase</li><li>Final cleanup</li></ol><ol><li>Knife</li><li>Spoon</li></ol>First gather tools is absent.
  • Trailing prose was dropped the same way: Steps: 1. Setup: Prepare phase a. Step A b. Step B. Then finish 2. Done lost both Prepare phase (nested leading) and Then finish (nested trailing).

The top-level Conversion.leadingText/trailingText were already surfaced; only the nested level omitted them.

Fix

InlineListParser.tryConvert and its recursive helper now emit each nested Parse.leadingText() and Parse.trailingText() as sibling <p> elements, mirroring how the top-level Conversion already surfaces prose. The two duplicated nested-rendering loops were consolidated into a single shared renderNestedParse (+ buildListElement) helper used by both tryConvert and the recursive case, so the nested Parse is rendered as: leading <p> → nested list → deeper nested lists → trailing <p>.

  • The MAX_NESTED_DEPTH = 3 gating is preserved (the recursion guard is unchanged in effect; only the previously-dropped prose is now emitted).
  • The colon-abutting shape (1. Parent: a. Child one b. Child two 2. Sibling) renders identically to before — no spurious paragraphs are introduced when nested leading/trailing prose is empty.
  • Prose is emitted via new Element("p").text(...), so it remains HTML-escaped and safe.

After the fix, the repro input renders as:
<p>Key points:</p><ol><li>Setup phase</li><li>Final cleanup</li></ol><p>First gather tools</p><ol><li>Knife</li><li>Spoon</li></ol>

Testing

A committed regression test (InlineListNestedTextDropReproTest) asserts at the UnifiedMarkdownService.process observable boundary that:

  • the nested leading prose (First gather tools) is preserved and precedes the nested list;
  • both nested leading (Prepare phase) and trailing (Then finish) prose are preserved in the correct sibling order around the nested list;
  • the colon-abutting shape introduces no spurious <p> paragraphs.

Verification performed (all green):

  • Targeted regression test and the existing InlineListParserTest (26 cases) pass; full markdown package (InlineListParserTest, the new repro test, MarkdownNormalizerTest, MarkdownAstUtilsTest, MarkdownFenceRenderingTest, EnrichmentPlaceholderizerTest, 110 tests total) passes.
  • MarkdownServiceTest, MarkdownPreprocessingTest, and ComprehensiveListFormattingTest (67 tests, exercising list rendering through the public MarkdownService facade) pass — no behavioral regression.
  • Full JVM test lane (./gradlew test): 141 classes, 1167 tests, 0 failures, 0 errors.
  • Compile, SpotBugs, PMD (main + test), Spotless/spotlessCheck, and build -x test all pass; make lint-ast (ast-grep + Ruby SSOT) clean.
  • End-to-end runtime smoke: started the Spring Boot app and POSTed the bug-report markdown to /api/markdown/render and /api/markdown/render/structured; both returned HTTP 200 with First gather tools present as a <p> and the nested Knife/Spoon list preserved.
  • A throwaway JShell probe against the compiled classes confirmed nested leading <p> / nested <ol> / nested trailing <p> render at each nested depth (the probe was run outside the repo and discarded, per repo convention).

Automatic Fixes PRs can be configured here.

@detail-app
detail-app Bot requested a review from WilliamAGH September 6, 2026 14:00
@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: Nested inline lists drop prose between/after nested markers

1 participant