Skip to content

feat(doc-codec): read a paragraph's own numbering definitions from PlfLst/PlfLfo - #913

Merged
Mearman merged 4 commits into
mainfrom
feat/doc-numbering-definitions
Sep 4, 2026
Merged

feat(doc-codec): read a paragraph's own numbering definitions from PlfLst/PlfLfo#913
Mearman merged 4 commits into
mainfrom
feat/doc-numbering-definitions

Conversation

@Mearman

@Mearman Mearman commented Sep 4, 2026

Copy link
Copy Markdown
Member

A paragraph's listId/listLevel (sprmPIlfo/sprmPIlvl) says which list it belongs to and at what depth, but nothing about what that list actually looks like -- the glyph/format, level-text template, and start-at value live in PlfLst (the list definitions: LSTF plus each one's appended array of LVLs) and PlfLfo (which list a paragraph's own ilfo actually refers to), neither of which this reader touched.

list/numbering.ts's readNumberingDefinitions resolves both into NumberingDefinitions, keyed by the same listId string ContentListMembership.numId already carries. The shape and field values deliberately mirror ooxml.js's own docx numbering reader: NumberingLevel.format is the identical ECMA-376 ST_NumberFormat string MSONFC's own values are documented as mapping to ([MS-OSHARED] 2.2.1.3), and NumberingLevel.text is the identical '%1.'-style placeholder convention. NumberingDefinitions sits outside document-schema.js for the same reason ooxml.js's own numbering definitions do -- a document-level resource keyed by id has no business being copied onto every paragraph that shares it.

readDocContent now returns DocContent, a ContentDocument widened by one further field (numbering) -- an intersection type, so every existing caller expecting a plain ContentDocument is unaffected (confirmed: full workspace typecheck/lint/test/test:workers green, one documents.js fixture updated to expect the new field).

Read-only, matching ooxml.js's own docx writer exactly: writeDocContent does not attempt to write PlfLst/PlfLfo back out. LFOLVL overrides, grpprlPapx/grpprlChpx, and legal numbering (fLegal) are deliberately not resolved -- each a genuine further layer of the format, not an oversight.

Verified against real LibreOffice 26.2.5.2: a .doc built from a hand-authored .fodt declaring a real numbered list and a separate bulleted list reads back with the exact ODF-authored decimal template and the real Private Use Area bullet glyph (U+F0B7) LibreOffice wrote for it, confirmed against the raw PlfLst/LVL bytes directly.

Part of #816

@Mearman
Mearman marked this pull request as ready for review September 4, 2026 13:01
…fLst/PlfLfo

A paragraph's listId/listLevel (sprmPIlfo/sprmPIlvl) says which list
it belongs to and at what depth, but nothing about what that list
actually looks like -- the glyph/format, level-text template, and
start-at value live in PlfLst (the list definitions: LSTF plus each
one's appended array of LVLs) and PlfLfo (which list a paragraph's own
ilfo actually refers to), neither of which this reader touched.

list/numbering.ts's readNumberingDefinitions resolves both into
NumberingDefinitions, keyed by the same listId string
ContentListMembership.numId already carries. The shape and field
values deliberately mirror ooxml.js's own docx numbering reader:
NumberingLevel.format is the identical ECMA-376 ST_NumberFormat string
MSONFC's own values are documented as mapping to ([MS-OSHARED]
2.2.1.3), and NumberingLevel.text is the identical '%1.'-style
placeholder convention, decoded from Xst's own raw-level-index
character encoding via rgbxchNums. NumberingDefinitions sits outside
document-schema.js for the same reason ooxml.js's own numbering
definitions do: ContentListMembership is shared verbatim across every
codec, and a document-level resource keyed by id has no business being
copied onto every paragraph that shares it.

read.ts's readDocContent now returns DocContent, a ContentDocument
widened by one further field (numbering) -- an intersection type, so
every existing caller expecting a plain ContentDocument is unaffected.

Read-only, matching ooxml.js's own docx writer exactly: writeDocContent
does not attempt to write PlfLst/PlfLfo back out, and LFOLVL overrides,
grpprlPapx/grpprlChpx, and legal numbering (fLegal) are deliberately
not resolved -- each a genuine further layer of the format, not an
oversight.
…lfLfo fixtures

Bytes assembled directly from [MS-DOC] 2.9.226 (PlfLst)/2.9.191
(LSTF)/2.9.196 (LVL)/2.9.148 (LVLF)/2.9.343 (Xst)/2.9.225
(PlfLfo)/2.9.181 (LFO)'s own field tables, independently of
numbering.ts's own reader -- so a test asserting against these bytes
checks the reader's understanding of the spec, not agreement with a
second copy of the same layout, the identical convention
table/decoration.test.ts's own hand-built Brc80/Shd80 fixtures state.

Covers: the common case of no PlfLst/PlfLfo at all, a simple one-level
bulleted list, a decimal list with a real '%1.' placeholder template,
a nine-level multi-level list with mixed formats and placeholder
levels, ilvlRestartLim resolving only when fNoRestart is set, the
nfc=0xFF "no number sequence" sentinel, resolution keyed by the
one-based ilfo (not by lsid, proven by an LFO array whose order
deliberately does not match its LSTF array), and a thrown
DocFormatError for an unrecognised MSONFC value.

Also adds list/numbering.js to the deep-import smoke test's own
module list, matching the family convention of covering every new
src module there.
Adds a Numbering definitions section explaining what readNumberingDefinitions
resolves, why it deliberately mirrors ooxml.js's own numbering shape and
vocabulary rather than document-schema.js, and what it deliberately does not
resolve (LFOLVL overrides, grpprlPapx/grpprlChpx, legal numbering). Removes
the "Numbering definitions" row from the "not built on either side" table
now that the read side genuinely resolves it, and adds a read-side status
bullet.

Verified against real LibreOffice 26.2.5.2: a .doc built from a
hand-authored .fodt declaring a real numbered list and a separate
bulleted list reads back with the exact ODF-authored decimal template
and the real Private Use Area bullet glyph (U+F0B7) LibreOffice wrote
for it, confirmed against the raw PlfLst/LVL bytes directly.
…c round trip

readDocContent's return type widened by one field (numbering,
doc-codec's own read-only list-formatting definitions keyed by
listId) -- this fixture declares no lists, so the round trip's own
numbering now resolves to {} rather than being absent from the result
entirely, which the strict content equality check needs to expect.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-04T13:18:05.204894Z 36cecd6 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Mearman
Mearman force-pushed the feat/doc-numbering-definitions branch from 36cecd6 to 8f09463 Compare September 4, 2026 13:02
@Mearman
Mearman merged commit 28905d3 into main Sep 4, 2026
17 checks passed
@Mearman
Mearman deleted the feat/doc-numbering-definitions branch September 4, 2026 13:05
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 6.5.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant