Skip to content

feat: number a list, and write its markers as text a copy keeps - #673

Merged
andiwand merged 4 commits into
mainfrom
feat/list-markers
Aug 9, 2026
Merged

feat: number a list, and write its markers as text a copy keeps#673
andiwand merged 4 commits into
mainfrom
feat/list-markers

Conversation

@andiwand

@andiwand andiwand commented Aug 9, 2026

Copy link
Copy Markdown
Member

🤖 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, a
pseudo-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_list wrote <ul> unconditionally
and the model carried nothing about list kind or numbering, so 1. 2. 3.
rendered as discs — 591 <ul> and zero <ol> across the whole reference
output. Underneath sat a plain bug: the ODF style index was keyed on
style:list-style, but those elements are text:-prefixed, so the index had
been 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:numId that resolves through numbering.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_numbering is the shared middle: a level is a format plus
a label template where %N names level N's counter — Word's w:lvlText
verbatim, and what ODF's prefix / suffix / text:display-levels lower onto.
Number formats (decimal, zero-padded, alphabetic, roman) live there too.

Why div role="list" and not ul

Keeping <ul> with list-style:none regressed rich-text paste: the macOS
importer ignores list-style however it is spelled, draws its own marker, and
turns 1. One into 1. 1. One. And the label is document-defined text —
1.2.3., a), — that no HTML marker reproduces, so a live list on paste
would 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 ListCounter
and the number formats, integration tests over odt and docx. Reference output
regenerated and pinned: 55 files, plus document.css. Clipboard checked for
real on macOS — plain text and rich text agree, no doubling.

Known gaps

Documented in the module docs: a w:numPr inherited through w:pStyle is not
detected; a run of list paragraphs with differing w:numId still becomes one
list; ODF text:outline-style is indexed but not applied; image bullets fall
back to a character. List::type() is not consumed by the renderer — it stays
as document information for API consumers. Word on Windows is untested.

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
andiwand force-pushed the feat/list-markers branch from e6c27d2 to 3dc5a37 Compare August 9, 2026 13:36
@andiwand andiwand changed the title feat: resolve list markers and write them as copyable text feat: number a list, and write its markers as text a copy keeps Aug 9, 2026
@andiwand
andiwand marked this pull request as ready for review August 9, 2026 13:38

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/odr/document_element.hpp
andiwand and others added 3 commits August 9, 2026 16:05
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
andiwand force-pushed the feat/list-markers branch from 3dc5a37 to e42fdaf Compare August 9, 2026 14:05
@andiwand
andiwand enabled auto-merge (squash) August 9, 2026 14:15
@andiwand
andiwand merged commit 5f47d6a into main Aug 9, 2026
36 checks passed
@andiwand
andiwand deleted the feat/list-markers branch August 9, 2026 14:16
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