Pages: render bullet and numbered lists as Markdown#41
Conversation
Body paragraphs whose list style (StorageArchive field 7 → TSWP.ListStyleArchive) marks them as list items now render as Markdown lists instead of flat paragraphs. The marker comes from the list style's per-level marker-type array (field 11), read at the base level: 2 = bullet (`-`), 3 = ordered (`1.` with a running counter that resets when the list style changes or a non-list paragraph interrupts it); other values render as plain body text. BodyStorage gains the field-7 list-style runs and a pre-resolved marker per style. renderParagraph now reports heading vs body so renderParagraphs (which holds the counter) can apply markers; consecutive items of one list render tight (single newline) while lists are separated from surrounding blocks by a blank line. List membership follows the file's styles, so "Plain paragraph before list", which the fixture styles as List Bullet, is a bullet. Nested-list indentation isn't resolved yet (items render flat); noted as a follow-up. Validated against sample.pages: a 2-item bullet list and a 3-item numbered list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mxnj6jvuLX9VS9JsJJbjmf
There was a problem hiding this comment.
Code Review
This pull request adds support for rendering bulleted and numbered lists from Apple iWork Pages documents into Markdown, including tight spacing for consecutive list items. The feedback highlights two issues: first, the Protobuf parser should handle both packed and unpacked representations of field 11 to ensure robust list marker detection; second, empty plain paragraphs should reset the ordered list counter to prevent consecutive lists of the same style from incorrectly merging their counters.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Two fixes from review: - listMarker only handled an unpacked field 11 (one varint per level). A packed encoding (all levels in one length-delimited field) would arrive as .length and be skipped, silently dropping list markers. Handle both wire forms, reading level 0 either from the first varint field or the first varint of the packed bytes (firstPackedVarint). - An empty non-list paragraph (a blank line) between two ordered lists of the same style left the ordered run active, so the second list continued the first's numbering. Reset the counter when a skipped (empty) paragraph isn't itself a list item; an empty list item still preserves the run. Both are defensive against inputs the sample fixture doesn't exercise (packed field 11, blank-line-separated same-style lists); existing tests are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mxnj6jvuLX9VS9JsJJbjmf
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2fabbaf669
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| parts.append(("- " + text, listStyle)) | ||
| case .ordered?: | ||
| if listStyle != orderedList { counter = 0; orderedList = listStyle } | ||
| counter += 1 | ||
| parts.append(("\(counter). " + text, listStyle)) |
There was a problem hiding this comment.
Indent wrapped list item lines
These prefixes are applied to the rendered paragraph string as a whole, but renderInline intentionally preserves soft line breaks for PagesConverter.normalize to turn into \n. For a Pages list item containing a manual line break, only the first line gets the Markdown marker; the following line is unindented, and the repo parser only attaches continuation lines that start with two spaces (DocumentRenderer.swift lines 511-512), so HTML/plaintext rendering splits one list item into a list item plus a separate paragraph.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch, but I'm deferring this one rather than half-fixing it. A 2-space continuation indent won't survive PagesConverter.normalize, which trims every line's leading whitespace — making multi-line items stick would need normalize to preserve list continuations, a broader change. The fixture's items are all single-line, so I've noted "multi-line list items" as a follow-up in the scope comment. Happy to prioritize it if you'd like it in this PR.
Generated by Claude Code
| if listStyle != orderedList { counter = 0; orderedList = listStyle } | ||
| counter += 1 | ||
| parts.append(("\(counter). " + text, listStyle)) |
There was a problem hiding this comment.
Preserve explicit ordered-list starts
The ordered number is synthesized from a local counter that resets only when the list-style id changes. For Pages ordered lists that explicitly restart or start from a non-1 value while reusing the same list style, the converter rewrites the visible numbering because it never consults the document's per-paragraph numbering state; for example, a second adjacent list restarted at 1 is emitted as the next count from the previous list.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid limitation — the counter is synthesized and ignores explicit start/restart values. Reproducing the document's numbering needs the per-paragraph numbering state, which I haven't decoded yet (the fixture's list starts at 1, so it isn't exercised). Noted "explicit list-start numbers" as a follow-up in the scope comment; can tackle it separately if it matters for your inputs.
Generated by Claude Code
Two further list fixes from review: - An empty paragraph between two same-style lists reset the ordered counter but the join still merged the items tight (1. a + blank + 1. b rendered as one list). renderParagraphs now records per-paragraph tightness at build time from the immediately preceding list paragraph, and an empty non-list paragraph clears it — so adjacent same-style lists restart their numbering and are set off by a blank line. - listMarker scanned only the child ListStyle. A local override that inherits its marker array (field 11) from a parent lost its marker and fell back to plain text. It now resolves through the parent chain, the same visited-guarded walk styleName/characterTraits use. Multi-line list-item continuation indentation and explicit list-start numbers are noted as follow-ups (normalize trims continuation indents; per-paragraph numbering state isn't decoded — neither is exercised by the fixture). Existing tests and the fixture output are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mxnj6jvuLX9VS9JsJJbjmf
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bf179ce0e4
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Body paragraphs whose list style (StorageArchive field 7 → TSWP.ListStyleArchive)
marks them as list items now render as Markdown lists instead of flat paragraphs.
The marker comes from the list style's per-level marker-type array (field 11), read
at the base level: 2 = bullet (
-), 3 = ordered (1.with a running counter thatresets when the list style changes or a non-list paragraph interrupts it); other
values render as plain body text.
BodyStorage gains the field-7 list-style runs and a pre-resolved marker per style.
renderParagraph now reports heading vs body so renderParagraphs (which holds the
counter) can apply markers; consecutive items of one list render tight (single
newline) while lists are separated from surrounding blocks by a blank line.
List membership follows the file's styles, so "Plain paragraph before list", which
the fixture styles as List Bullet, is a bullet. Nested-list indentation isn't
resolved yet (items render flat); noted as a follow-up. Validated against
sample.pages: a 2-item bullet list and a 3-item numbered list.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01Mxnj6jvuLX9VS9JsJJbjmf