Skip to content

feat(ooxml.js): wire the new document-schema.js fields into docx, pptx, and xlsx - #1216

Merged
Mearman merged 6 commits into
mainfrom
feat/ooxml-schema-field-wiring
Sep 11, 2026
Merged

feat(ooxml.js): wire the new document-schema.js fields into docx, pptx, and xlsx#1216
Mearman merged 6 commits into
mainfrom
feat/ooxml-schema-field-wiring

Conversation

@Mearman

@Mearman Mearman commented Sep 11, 2026

Copy link
Copy Markdown
Member

Six schema fields landed on main with no ooxml.js consumer; this wires each one, as separate atomic commits that each pass the package gate (pnpm --dir packages/ooxml.js lint && typecheck && test, plus test:workers).

What lands

xlsx per-cell font (ContentSheetCell.font, read+write). The reader resolves each cell's <cellXfs> xf through <fonts> and diffs the entry its fontId names against the table's own entry 0, stating only genuine differences — mirroring xls-codec's default-diffing policy for BIFF8's identical font-table-and-index mechanism (its per-cell-fonts PR is the reference; verified against its contentFontOf/xfFontFieldsOf both-directions shape). The writer widens the cell-format interning tuple from (number format, decoration) to (number format, font, decoration): the established single Calibri-11 font stays fixed at index 0, one further <font> per distinct cell font, applyFont on a fonted xf, and a ContentFont normalising back to entry 0 references it and mints nothing. CT_Font members with no ContentFont field (vertAlign, outline, shadow, family, charset, scheme) are read past; theme/indexed/auto colours resolve to no colour statement, matching the fill/border policy.

xlsx defined names (names, read+write). readWorkbookNames reads every workbook.xml <definedName> — the _xlnm built-ins included — with refersTo verbatim and localSheetId mapped onto scopeSheetIndex. The writer emits the array verbatim and in its own order (so a same-format round trip reproduces the file's own definedName order, and a multi-area print range's refersTo survives verbatim), with the print-settings derivation filling in only the two reserved print names the array does not carry. The namedRange half of the tree root's definitions table is retired — its content is exactly what names now carries across the flat/tree boundary natively, and feeding both channels would double-write every general definedName; the definitions table/option keep Table/List objects. A name's refersTo passes the same sheet-qualified-internal-A1 security gate a namedRange entry always did (refused by name, never written into live formula context).

docx verticalAlign + direction. w:vertAlign lands on ContentRun.verticalAlign and w:rtl on ContentRun.direction, resolved through the same style cascade bold rides; w:bidi — verified against ECMA-376 Part 1's own "Right to Left Paragraph Layout" definition as the genuine WordprocessingML paragraph-level spelling — lands on ContentParagraph.direction through the paragraph cascade. vertAlign val="baseline" is the cascade's explicit override spelling: it wins the merge and then disappears, since the schema models baseline as the field's absence. Writers spell an explicitly left-to-left value with the off form, mirroring bold: false.

pptx/xlsx origin. The chart cached-table reader sets origin: 'chart' on its ContentTable (pptx) and on the xlsx chart ContentEmbeddedObject; a SmartArt graphic frame's shape carries origin: 'diagram'. Checked git log/open PRs first: #1202's schema half is merged and no codec wiring is landing elsewhere, so this is the only origin wiring.

docx image anchors. Every lifted image records anchorRunIndex/anchorOffset per the schema's boundary rule: the run whose text it followed and the character position within that run after which it sat. A paragraph that splits at a mid-run page break leaves its images unanchored (the split re-indexes both halves — no clean encoding, so dropped rather than mis-encoded). The writer re-inlines a lifted image as its containing paragraph's last run, so end-of-paragraph anchors are round-trip-stable and earlier ones migrate to the end — pinned by a test.

README. The docx/pptx/xlsx gotcha bullets state all of the above; the same docx bullet's stale "floating wp:anchor position not recorded" claim is corrected to name floatPosition, which the reader has recorded since that widening.

Deliberately out of scope

  • ContentSection.watermarks — the schema field has not landed on main (verified by grep at origin/main); wpd's wiring is another PR's, and no ooxml page-furniture change was in this task's items.
  • The docx writer does not yet place a lifted image back at its anchored position (it appends as the paragraph's last run); the anchors are recorded for the consumers that want them, per the field's stated purpose.

@Mearman
Mearman force-pushed the feat/ooxml-schema-field-wiring branch from 8be7ed8 to f269f9a Compare September 11, 2026 04:26
…t table

ContentSheetCell gains its uniform font on both directions of the xlsx
pair. The reader resolves each cell's xf through <fonts> and diffs the
entry its fontId names against the table's own entry 0, stating only
genuine differences -- mirroring xls-codec's default-diffing policy for
BIFF8's identical font-table-and-index mechanism, since xlsx gives a
cell no way to say "no font", only an index, and entry 0 is what the
format's default concretely means for a given file.

The write side widens the cell-format interning tuple from (number
format, decoration) to (number format, font, decoration): the writer's
own single Calibri-11 font stays fixed at index 0 as the reserved
scaffolding entry, one further <font> is interned per distinct cell
font, and a ContentFont normalising back to the entry-0 fields
references index 0 and mints nothing -- an explicit bold:false from a
file whose default was bold is a restatement of this writer's own
not-bold default, not a new font. A fonted xf carries fontId plus
applyFont, the identical flag discipline applyFill and applyBorder
already draw.

What CT_Font states that ContentFont has no member for (vertAlign's
superscript/subscript, outline, shadow, condense, extend, family,
charset, scheme) is read past rather than half-modelled, and a colour
carried only as theme/indexed/auto resolves to no colour statement,
matching the fill and border colour policy.
…ways

The workbook's defined names now ride the ContentDocument's own names
field: readWorkbookNames reads every xl/workbook.xml <definedName>
including the _xlnm built-ins, refersTo verbatim in Excel's own formula
language, with localSheetId mapped onto scopeSheetIndex in the same
sheets document order the reader resolves sheets in.

The write side emits the names array verbatim and in its own order --
the file's own definedName order is the only order a same-format round
trip can reproduce, and the array's refersTo (a multi-area print range,
a quoted sheet name) is the higher-fidelity spelling of exactly the two
_xlnm print names a structured printRange can restate. The
print-settings derivation now fills in only the print names the array
does not already carry, so a hand-built document stating a structured
printRange still gets its reserved definedName while a read-back
workbook writes each name exactly once, from the array.

The namedRange half of the tree root's definitions table is retired:
its content was exactly what names now carries across the flat/tree
boundary natively, and a tree reader feeding both channels would
double-write every general definedName on the way back out. The
definitions option and table keep carrying Table/List objects, which
genuinely have no flat spelling.

A name's refersTo passes the same security gate a namedRange entry
always did -- sheet-qualified internal A1 references only, refused by
name -- since it is the identical live-formula surface.
w:vertAlign lands on ContentRun.verticalAlign and w:rtl on
ContentRun.direction, both resolved through the same style cascade
bold and italic already ride (docDefaults through the character-style
chain to direct formatting), since both are ordinary rPr members a
style can state and inherit. The one wrinkle is vertAlign val=
"baseline": that is the explicit none-of-the-above a producer writes
to turn an inherited position off, so it wins the cascade merge as a
value of its own and then disappears -- ContentRun models baseline as
the field's absence, and the writer materialises the resolved position
as direct formatting, so the override survives as plain text.

w:bidi (the genuine WordprocessingML paragraph-level spelling, right-
to-left paragraph layout rather than run direction) lands on
ContentParagraph.direction through the paragraph cascade the same way.

Both writers spell an explicitly left-to-right value with the off form
(w:rtl/w:bidi val="0"), mirroring how bold: false is written: an
absent element is "inherit" to the read-side cascade, not "off", so a
resolved ltr must be stated rather than omitted. Element placement
follows CT_PPrBase (bidi between numPr and spacing) and CT_RPr
(vertAlign and rtl after u).
The origin annotation channel names what a node's content IS, and the
two graphic-frame kinds that establish it now state it: a chart's
cached-model table (pptx) and embedded chart object (xlsx) carry
origin: 'chart', and a SmartArt graphic frame's shape carries origin:
'diagram' -- distinguishing a native chart part's exact numbers and a
diagram's own node text from the identical-looking content a pasted
screenshot or freeform text box would produce, which no other field
on those nodes says.

Stated only where the reader genuinely knows it: an authored a:tbl, a
plain text shape, an OLE fallback picture, and a sheet's own cells
carry no origin at all, the channel's absent-means-ordinary-prose
convention.
…ream

Every image the docx reader lifts out of a paragraph's run stream now
carries anchorRunIndex/anchorOffset: the run whose text the image
originally followed, and the character position within that run after
which it sat. The run walk records each w:drawing/w:object that is a
direct child of an emitted run at exactly the position it occupied --
the same run-and-character indexing the mid-run page-break event
already collects -- and the lifting pass resolves the boundary rule
from it: an image mid-run anchors inside its own run at the preceding
text's length, an image in a run of its own anchors to the previous
run at that run's full length, and an image at the paragraph's very
start anchors to (0, 0). A drawing nested inside a w:object (the
AlternateContent preview spelling) anchors to the position the object
itself sat at, the only position the reader honestly knows for it.

An element the walk never emitted a run for (inside field code, or a
shape the run walk cannot see) carries no anchor -- the schema's
absent-when-unknown spelling rather than a guess. A paragraph that
splits at a mid-run page break leaves its lifted images unanchored
too: the split re-indexes both halves' run arrays, so a pre-split
index would name one half or the other ambiguously, the identical
no-clean-encoding rule the split applies to a construct spanning the
break.

The writer re-inlines a lifted image as the last run of its containing
paragraph, so an image that already sat at the paragraph's end keeps
its anchor through a round trip while one lifted from earlier in the
text migrates to the end -- pinned by a test, and the honest statement
of a writer that does not yet place runs by anchor.
…igin, and anchor coverage

The xlsx bullet now describes the names field's both-ways ride (every
definedName including the _xlnm built-ins, refersTo verbatim, the
print-derivation fallback, and the internal-A1 security gate), the
retirement of the namedRange half of the definitions table, and the
per-cell font's default-diffed read and interned-table write. The docx
bullet gains the vertAlign/rtl/bidi mappings, the lifted-image anchors
and their end-of-paragraph-stable round trip, and the pptx and xlsx
bullets name the chart/diagram origin classifications.

Also corrects the same bullet's stale claim that a floating wp:anchor
position is not recorded: floatPosition has been read since the
anchor-position widening, and the parenthetical now says so.
@Mearman
Mearman force-pushed the feat/ooxml-schema-field-wiring branch from f269f9a to 89deff9 Compare September 11, 2026 05:07
@Mearman
Mearman marked this pull request as ready for review September 11, 2026 05:07
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 11, 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-11T05:19:50.071758Z 89deff9 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 merged commit c5bec41 into main Sep 11, 2026
22 checks passed
@Mearman
Mearman deleted the feat/ooxml-schema-field-wiring branch September 11, 2026 05:12
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 8.14.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