Renderer: align ordered list items on the widest marker of their list - #27
Conversation
The hanging indent of an ordered item was the width of that item's own prefix. The body font is not monospaced, so "1. " and "10. " are 18.47 pt and 31.00 pt wide at a 21 pt body, and each item's text started at a different x — a 12.5 pt step, visible at a glance on any list that crosses ten items. `render` now pre-scans the joined lines and, for each contiguous run of ordered items at one indent level, computes the widest prefix; every item of the run hangs under it. The run is known before any rendering, so the widest prefix is too. `testWiderOrderedMarkerHangsFurther` keeps passing, and for the right reason: it renders "1. alpha" and "10. alpha" as two separate one-item documents, so each still hangs under its own — and only — marker. Its stated intent, "a wider marker has to hang further, or 10. would overlap its own text", is preserved: within a list, every item now hangs under the widest one. Adds `testOrderedItemsOfOneListShareTheirHangingIndent`, which fails on unpatched main and passes here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ollow-up to #27) PR #27 gave every ordered item of a list the same `headIndent`, which aligns their WRAPPED lines. The first line, however, starts at `firstLineHeadIndent` plus the item's own marker glyphs, so "1. alpha" and "10. kappa" still began their text 12.5 pt apart — and the wrapped lines of "1." now hung 12.5 pt to the right of its own first line. - Push a narrower marker right by (widest − own) via `firstLineHeadIndent`: markers are right-aligned under the widest one and every item's text starts at the same x on the first line and on wrapped lines alike. - Choose the widest marker by measured width in the body font, not by character count ("9. " is wider than "1. "). - Blank lines and deeper-indented lines (loose lists, nested lists, continuation paragraphs) no longer end the run; nested ordered lists form their own runs. `orderedItem` normalises the number exactly like `renderListItem` ("007." → "7. "). - `applyHangingIndent(indent:firstLineLead:hangWidth:)` is the one mechanism; the string overload (definition lists, bullets, task items) passes lead 0. Tests: TextKit lays out the rendered string and reads the x of the first text glyph per item — the visual truth, independent of which indent field does the work. Four new tests fail on #27 alone and pass with this change; 191 total. Known limit (pre-existing parser structure): the parser slices text buffers into ≤30-line chunks, so a list that straddles a chunk boundary is two runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017oPbKHjsi1HB3RWgxxFTKz
|
Merged locally with One follow-up on top of it (7abb18c), because the shared Will ship in the next release with credit in the CHANGELOG. |
Graphics follow the zoom and open in a window-filling preview (#28/#29 @arvearve), ordered lists share one text edge (#27 @rosekanari), fenced ```svg blocks render natively, the 1.8.0 layout fallback is removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017oPbKHjsi1HB3RWgxxFTKz
|
Shipped in v1.10.0: https://github.com/b451c/quickmd/releases/tag/v1.10.0 - credited in the CHANGELOG and release notes. Thank you! |
Ordered list items each hang under their own marker, so their text starts at a different x. This aligns every item of a list on the widest marker of that list.
The problem
renderListItemcallsapplyListParagraphStyle(_:hangingUnder:)with that item’s own prefix, which delegates toapplyHangingIndent(_:indent:hangingUnder:):The body font is not monospaced, so prefixes of different items have different widths. Measured at a 21 pt body:
prefix | width | vs "1. " -- | -- | -- "1. " | 18.47 pt | — "9. " | 21.95 pt | +3.49 "10. " | 31.00 pt | +12.53 "100. " | 43.68 pt | +25.21A ten-item list is enough to see it: the text of item 10 starts 12.53 pt further right than that of item 1. Nothing overflows — no width is imposed — but nothing lines up either.
The change
renderpre-scans the joined lines and, for each contiguous run of ordered items at one indent level, computes the widest prefix; every item of the run hangs under it. The run is fully known before any item is rendered, so the widest prefix is too.renderLineandrenderListItemtake it as an optional parameter defaulting tonil, so unordered items, task items and every other caller are untouched.Two files, 69 insertions, 6 deletions.
Tests
testOrderedItemsOfOneListShareTheirHangingIndentis added. It renders a twelve-item list and asserts that all items share oneheadIndent. It fails on unpatched main and passes with this change. The full suite passes.testWiderOrderedMarkerHangsFurtherkeeps passing, and for the right reason: it renders"1. alpha"and"10. alpha"as two separate one-item documents, so each still hangs under its own — and only — marker. Its stated intent, “a wider marker has to hang further, or10.would overlap its own text”, is preserved: within a list, every item now hangs under the widest one, so the widest never overlaps its text.Notes
Verified against
28d86fd. Built and tested with:Written with Claude Code.
Happy to split, rename, or drop the added test if you would rather keep the suite as it is