Skip to content

fix(ui): render numerals for ordered list items#520

Merged
backnotprop merged 5 commits intomainfrom
fix/mkdown-numbers
Apr 8, 2026
Merged

fix(ui): render numerals for ordered list items#520
backnotprop merged 5 commits intomainfrom
fix/mkdown-numbers

Conversation

@backnotprop
Copy link
Copy Markdown
Owner

Summary

The branch name promised numbered-list rendering, but the prior commit (d850b78) only fixed list-continuation merging. Ordered lists like 1.10. were still rendered as bullet points because the data model had no concept of "ordered" — the parser collapsed *, -, and \d+. into a single list-item block type and the renderer hardcoded // by indent level.

This PR fixes the actual numbering bug:

  • Parser (packages/ui/utils/parser.ts): captures the numeric marker via a second capture group; sets ordered + orderedStart on list-item blocks. Switched from a second replace() to slice(listMatch[0].length) to avoid coupling to capture-group ordering.
  • Block type (packages/ui/types.ts): two optional fields, ordered?: boolean and orderedStart?: number. Optional so every existing fixture is byte-identical.
  • computeListIndices() (new pure helper in parser.ts): walks a list group and assigns each ordered item a CommonMark-correct display number. Handles sequential renumbering (1./2./99. → 1,2,3), streak break/restart on unordered items, deeper-level state truncation when siblings change, and top-level numbering survival across nested children.
  • Renderer (packages/ui/components/Viewer.tsx): branches the list-item marker span on block.ordered and renders ${index}. with tabular-nums + a 1.5rem min-width so 9 → 10 doesn't jitter. groupBlocks is untouched — mixed nested lists still share a single data-pinpoint-group="list" hover wrapper, so annotation anchoring is unaffected. Checkbox items still take precedence over numerals.
  • Tests: 21 new unit tests across parseMarkdownToBlocks and computeListIndices covering the tricky cases — sub-bullets between ordered items keeping the top-level streak alive, nested ordered sublists numbering independently, source orderedStart honored on restart, numeric checkboxes setting both ordered and checked, 1.5 second not matching, ### 1. Foo staying a heading.
  • Manual fixture: tests/test-fixtures/06-ordered-list-plan.md — the real-world Slice 6 plan whose 10-item ## Verification section originally surfaced the bug.

Test plan

  • bun test packages/ui/utils/parser.test.ts — 43 pass, 0 fail (22 existing + 21 new)
  • Full bun test — only failure is a pre-existing pi-extension module-resolution error unrelated to this branch
  • bun run --cwd apps/review build && bun run build:hook — bundles rebuilt
  • Manual: served all 6 fixtures (01..06) via bun run apps/hook/server/index.ts annotate <file>; ordered lists render with numerals, bullets unchanged, no regressions in continuation merging / hard line breaks / real-world plans
  • Manual: annotated items inside an ordered list — web-highlighter anchoring works, numeral isn't selectable (parent span carries select-none)

Out of scope

  • Plan diff view (PlanCleanDiffView) renders diff lines without going through BlockRenderer, so ordered-list changes still show bullets in diff view. Pre-existing limitation, not a new regression — worth a follow-up.
  • Code review editor (apps/review) uses a different renderer that doesn't share Viewer.tsx.
  • Inline ordered markers like - 1. nested (where 1. is content of an unordered item) aren't detected because parsing is line-level.

For provenance purposes, this PR was AI assisted.

The block parser collapsed `*`, `-`, and `\d+.` markers into a single
`list-item` block type, discarding ordered/unordered status. Add
`ordered` + `orderedStart` to Block, capture the numeric marker in the
list regex, and introduce `computeListIndices()` — a pure helper that
walks a list group and assigns each ordered item a CommonMark-correct
display number (sequential renumbering, streak break/restart on
unordered items, deeper-level state truncation, top-level numbering
preserved across nested children).

21 new unit tests cover both the parser changes and the indexing
helper, including the tricky cases: `1./2./99.` renumbers as 1,2,3;
sub-bullets between ordered items keep the top-level streak alive;
nested ordered sublists number independently and reset between
siblings; numeric checkboxes set both `ordered` and `checked`.

For provenance purposes, this commit was AI assisted.
Branch the list-item marker span on `block.ordered`: render
`${index}.` (with `tabular-nums` and a 1.5rem min-width to keep
columns stable across single- and double-digit numerals) when the
source marker was numeric, otherwise fall through to the existing
`•`/`◦`/`▪` bullet symbols. Indices come from `computeListIndices()`
called once per list group; `groupBlocks` is unchanged so mixed
nested lists still share a single `data-pinpoint-group="list"`
hover wrapper and annotation anchoring is unaffected. Checkbox
items still take precedence over numerals.

Adds a real-world manual fixture (06-ordered-list-plan.md) whose
`## Verification` section exercises a 10-item ordered list, the
case that originally surfaced the bug.

For provenance purposes, this commit was AI assisted.
Each `>` line was emitted as its own blockquote block, so the
renderer's `my-4` margin produced visible gaps between every line
of a multi-line quote (the parser had a literal TODO comment:
"Check if previous was blockquote, if so, merge? No, separate for
now"). Fix: append to the previous block when it's a blockquote
and the prior line wasn't blank, mirroring the list-continuation
pattern. A blank line still breaks the quote so two `>` runs
separated by a blank line stay distinct.

Adds 5 unit tests (merge, blank-line break, paragraph boundaries,
single-line) and a manual fixture (07-blockquotes.md) covering the
bug case, the blank-line-break case, sandwich-between-paragraphs,
and inline markdown across merged lines.

For provenance purposes, this commit was AI assisted.
…graphs

Three issues surfaced by PR review #520:

1. **Diff view flattened ordered lists to bullets.** PlanCleanDiffView's
   SimpleBlockRenderer duplicated Viewer's list-item JSX with hardcoded
   bullet symbols, so a denied+resubmitted plan with numbered steps
   showed numerals in the main view but `•` in the diff view — exactly
   the screen where "which step changed?" matters most. Fixed by
   threading computeListIndices through MarkdownChunk and sharing the
   marker rendering via a new ListMarker component used by both
   renderers, which also removes the root-cause duplication.

2. **Ordered task lists dropped their numbers.** `1. [ ] step` set both
   `ordered=true` and `checked=false` in the parser, but the renderer's
   checkbox branch took precedence and the numeral was never shown.
   GitHub renders `1. [ ]` as numeral + checkbox side by side; we now
   match that by rendering both glyphs in ListMarker when an ordered
   task list item appears.

3. **Multi-paragraph blockquotes collapsed.** After the blockquote-merge
   fix in the previous commit, `> a\n>\n> b` produced content
   `"a\n\nb"` but the renderer passed it straight to InlineMarkdown,
   which renders `\n\n` as whitespace — so two quoted paragraphs
   mashed into one line. Fixed by splitting blockquote content on
   `/\n\n+/` in both Viewer and PlanCleanDiffView and emitting one
   `<p>` child per paragraph.

Adds one unit test for the multi-paragraph blockquote content shape and
a manual fixture (08-ordered-edge-cases.md) covering ordered task lists,
multi-paragraph quotes, nested-bullet counter preservation, double-digit
alignment, and start-at-N numbering.

The fourth review comment — loose ordered lists with intervening non-list
blocks restarting numbering — is deferred. It requires parser-level
loose-list detection (CommonMark's indented-continuation rule) and the
bug only fires when users rely on lazy `1./1./1.` markers across a
break. Tracked as a follow-up.

For provenance purposes, this commit was AI assisted.
Round-two review flagged a regression: `> 1. foo\n> 2. bar\n> 3. baz` was
merging into one blockquote whose content was `"1. foo\n2. bar\n3. baz"`.
The renderer split on `\n\n+` (paragraph breaks), found none, and emitted
a single `<p>` — so `\n` collapsed to whitespace in HTML and the user
saw `"1. foo 2. bar 3. baz"` as one run-on line. Worse than the pre-PR
behavior (which at least kept each line in its own box).

Pragmatic fix: don't merge a `>` line whose stripped content starts with
a block-level marker (`*`, `-`, `\d+.`, `#`, `` ``` ``, `>`). Those stay
as separate blockquote blocks so each marker line is visually distinct
(legible, matching pre-PR behavior for quoted lists). Wrapped prose
quotes — the original motivating case — still merge correctly because
prose lines don't start with markers.

Also check the PREVIOUS block's content for markers so a trailing prose
line after a `> 1. item` doesn't glue onto the list-item block.

7 new unit tests cover: quoted ordered list stays separate, quoted
unordered list stays separate, quoted heading stays separate, quoted
code fence stays separate, nested blockquote stays separate, wrapped
prose quote still merges (regression guard), and mixed prose+list where
prose merges and list lines stay separate.

Adds tests/test-fixtures/09-quoted-list-regression.md as a manual repro.

Known follow-ups (tracked separately, not in this PR):
- Consecutive separate blockquote blocks still get individual `my-4`
  margins, so a quoted list shows as stacked boxes with gaps between
  lines. The proper fix is recursive blockquote parsing (render the
  content as its own Block[] tree with an actual nested list inside
  the quote). Deferred — requires `children?: Block[]` on Block,
  parser rework, and exportAnnotations traversal changes.
- Clean diff view renumbers ordered lists from the start of each diff
  chunk when users rely on CommonMark's lazy `1./1./1.` markers. Same
  power-user population as the earlier deferred loose-list case.
- Pure code-hygiene items from the second review (non-list-block
  handling in computeListIndices, BULLET_BY_LEVEL modulo cycle,
  <ListGroup> extraction, CLAUDE.md Block interface drift,
  splitBlockquoteParagraphs helper) — batch into a follow-up cleanup.

For provenance purposes, this commit was AI assisted.
@backnotprop backnotprop merged commit b3fc1f7 into main Apr 8, 2026
7 checks passed
@backnotprop backnotprop deleted the fix/mkdown-numbers branch April 8, 2026 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant