feat: number a list, and write its markers as text a copy keeps - #673
Merged
Conversation
A run of `w:numPr` paragraphs was rebuilt into a tree by creating one fresh list per level for *every* item, so consecutive items of the same nested level each landed in a list of their own, hanging off the enclosing list rather than off the item above them. Track the open level instead: one list per level, reused while the level stays, and nested under the last item of the level above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ACZ1RcX9pxoMZWBaTdDRU
andiwand
force-pushed
the
feat/list-markers
branch
from
August 9, 2026 13:36
e6c27d2 to
3dc5a37
Compare
andiwand
marked this pull request as ready for review
August 9, 2026 13:38
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3dc5a37785
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
The model knew a list only as `list` / `list_item`, with no notion of what labels them, so every list rendered as bullets and a numbered one lost its numbers outright. Both engines had the gap on their open-work list. ODF and OOXML disagree on nearly everything here — ODF nests the XML and hangs the format off a list style per level, Word writes a flat run of paragraphs that name a `w:numId` and resolve through `numbering.xml` — so the seam is the *resolved* label: each engine stamps every item at load time, in document order, and the model exposes `List::type` plus `ListItem::marker` / `::number`. `common/list_numbering` is the shared middle: a level is a format plus a label template in which `%N` names level N's counter (Word's `w:lvlText` verbatim, ODF's prefix / suffix / `text:display-levels` lowered onto it), and a `ListCounter` expands one against the running counters. Number formats — decimal, zero-padded, alphabetic, roman — live there too. ODF resolves through the list-style stack, honouring `text:start-value` and `text:continue-numbering`, and treating a `text:list-header` as unlabelled. This also fixes the style index, which had been keyed on `style:list-style` and `style:outline-style`: the elements are `text:`-prefixed, so the index it filled was always empty. OOXML resolves `w:numFmt` / `w:lvlText` / `w:start` through `w:abstractNum`, `w:numStyleLink` and `w:lvlOverride`. Counters are kept per `w:numId`, which is what lets a numbered list resume after a bullet list interrupts it. Word's symbol-font bullets arrive as private-use code points that render only in Symbol or Wingdings; they map to Unicode where the shape is recognisable and to the level's default bullet otherwise. The pptx adapter's `ListItemAdapter` went away with this: that parser never produces a list item, so the implementation was unreachable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ACZ1RcX9pxoMZWBaTdDRU
Bullets were drawn by `list-style`, which makes them a `::marker`
pseudo-element — outside the DOM text stream, and so absent from every copied
selection. Users reported bullets and numbers going missing on paste; numbers
were worse off still, since every list was a `<ul>`.
Write the label as real text instead, as an `x-s` carrying the resolved marker.
It goes *inside* the item's first paragraph, not beside it — as a sibling of
that block the serializer would break the line between label and text, giving
"•\nOne" instead of "•\tOne".
The list itself becomes `div role="list"` / `div role="listitem"` rather than
`ul`/`li`. The label is document-defined text — "1.2.3.", "a)", a symbol — that
no HTML list marker reproduces, so an application that draws its own marker
next to ours shows both: the macOS rich-text importer behind TextEdit, Mail and
Notes does exactly that, ignoring `list-style:none` however it is spelled, and
turns "1. One" into "1. 1. One" on paste. Divs give it nothing to generate, and
the ARIA roles keep the semantics a screen reader needs. It also matches the
rest of this renderer, which already prefers `x-p`/`x-s` over semantic tags.
The marker hangs into the item's padding, so a wrapped line still aligns under
the text, and it grows past its 2em box rather than colliding with the text
when the label is long ("1.1.1."). It carries its own font size because `x-p`
collapses to `font-size:0`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ACZ1RcX9pxoMZWBaTdDRU
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nk1S12YmjBsmksSJ4tVB3X
andiwand
force-pushed
the
feat/list-markers
branch
from
August 9, 2026 14:05
3dc5a37 to
e42fdaf
Compare
andiwand
enabled auto-merge (squash)
August 9, 2026 14:15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Generated with Claude Code
From a user report that bullets could not be copied. True — and the smaller of
two problems.
Markers were CSS.
<ul><li>leaves the bullet to::marker, apseudo-element outside the DOM text stream, so no plain-text copy could contain
it. Selecting a list in Chrome gave
Alpha\nBeta.Numbered lists did not exist.
translate_listwrote<ul>unconditionallyand the model carried nothing about list kind or numbering, so
1. 2. 3.rendered as discs — 591
<ul>and zero<ol>across the whole referenceoutput. Underneath sat a plain bug: the ODF style index was keyed on
style:list-style, but those elements aretext:-prefixed, so the index hadbeen empty since it was written.
The seam
ODF nests the XML and hangs formatting off a list style per level; Word writes a
flat run of paragraphs naming a
w:numIdthat resolves throughnumbering.xml.Rather than teach the renderer both, each engine stamps every item with its
resolved label at load, and the model exposes
ListItem::marker()/::number(). The divergence stays inside the engines.internal/common/list_numberingis the shared middle: a level is a format plusa label template where
%Nnames level N's counter — Word'sw:lvlTextverbatim, and what ODF's prefix / suffix /
text:display-levelslower onto.Number formats (decimal, zero-padded, alphabetic, roman) live there too.
Why
div role="list"and notulKeeping
<ul>withlist-style:noneregressed rich-text paste: the macOSimporter ignores
list-stylehowever it is spelled, draws its own marker, andturns
1. Oneinto1. 1. One. And the label is document-defined text —1.2.3.,a),✔— that no HTML marker reproduces, so a live list on pastewould be wrong for most documents anyway. Divs give an importer nothing to
generate; the ARIA roles keep what a screen reader needs.
The marker goes inside the item's first paragraph — as a sibling of that block
it would copy onto a line of its own.
Testing
Full suite: 835 passed, 8 skipped, no failures. New unit tests for
ListCounterand the number formats, integration tests over odt and docx. Reference output
regenerated and pinned: 55 files, plus
document.css. Clipboard checked forreal on macOS — plain text and rich text agree, no doubling.
Known gaps
Documented in the module docs: a
w:numPrinherited throughw:pStyleis notdetected; a run of list paragraphs with differing
w:numIdstill becomes onelist; ODF
text:outline-styleis indexed but not applied; image bullets fallback to a character.
List::type()is not consumed by the renderer — it staysas document information for API consumers. Word on Windows is untested.