Skip to content

feat(fields): add SECTIONPAGES field support with section-aware numbering (SD-3349 and SD-3027)#3605

Merged
harbournick merged 64 commits into
mainfrom
luccas/sd-3349-bug-numbering-issues
Jun 4, 2026
Merged

feat(fields): add SECTIONPAGES field support with section-aware numbering (SD-3349 and SD-3027)#3605
harbournick merged 64 commits into
mainfrom
luccas/sd-3349-bug-numbering-issues

Conversation

@luccas-harbour
Copy link
Copy Markdown
Contributor

Summary

This branch builds on the PAGE/NUMPAGES field work from luccas/sd-3006-feature-page-number-fields and adds full support for Word's SECTIONPAGES field — the count of physical pages in the current section (as opposed to NUMPAGES, which counts the whole document). It threads a per-section page count through the entire import → layout → paint → export pipeline, and through the header/footer/shape editing surfaces.

Along the way it extends PAGE field-local number formatting (\* roman, etc.) into the live/editable header-footer and shape-text paths, fixes a header/footer layout-height fidelity bug, and corrects a couple of malformed parseDOM selectors.

What changed

New section-page-count node + field plumbing

  • New PM node section-page-count (page-number/page-number.js) with an addSectionPageCount command (header/footer only), node attributes (instruction, pageNumberFormat, importedCachedText, resolvedText), node view, and CSS styling (page-number.css).
  • Registered as an atomic inline token across the adapter (constants.ts ATOMIC_INLINE_TYPES / TOKEN_INLINE_TYPES), inline-node fallback (is-inline-node.js), extension index, and node-attribute typings (node-attributes.ts, miscellaneous-commands.ts).

Import (DOCX → PM)

  • New section-pages-preprocessor.js converts SECTIONPAGES field codes into sd:sectionPageCount nodes, preserving cached display text and run properties. Registered in both the main pipeline (fld-preprocessors/index.js) and the header/footer-only path (preProcessPageFieldsOnly.js).
  • parsePageInstruction generalized to accept an expectedKeyword so it parses value-format switches for both PAGE and SECTIONPAGES.
  • New v3 sectionPageCount-translator.js (encode/decode) + importer wiring (autoPageNumberImporter.js, docxImporter.js, handlers/index.js).

Export (PM → DOCX)

  • sectionPageCount-translator decode rebuilds the w:fldChar complex-field structure (instruction, cached text, marks) and is registered in exporter.js.

Layout / paint

  • Contract: TextRun.token and TextPart.fieldType gain sectionPageCount/SECTIONPAGES; TextPart now carries pageNumberFormat.
  • pageNumbering.ts computes a per-section page count and exposes it on DisplayPageInfo.
  • resolvePageTokens.ts, renderer.ts, and text-run.ts resolve the new token (with optional format override) for body, shape, and decoration rendering.
  • layout-bridge (layoutHeaderFooter.ts, resolveHeaderFooterTokens.ts, incrementalLayout.ts, HeaderFooterPerRidLayout.ts) resolves SECTIONPAGES tokens per page and disables page-bucketing when the content contains them (since the value can differ per page).

Header/footer & shape editing surfaces

  • sectionPageCount, currentPageNumberText, and currentPageDisplayNumber are threaded through the story-editor chain: HeaderFooterSessionManager, HeaderFooterRegistry (with validation + DOM refresh that re-resolves pageNumberFormat via posAtDOM), PresentationEditor, story-editor-factory, pagination-helpers, EditorConfig, and session types.
  • Shape/textbox text rendering (svg-utils.js, ShapeGroupView.js, VectorShapeView.js, encode-image-node-helpers.js) resolves SECTIONPAGES (and now formats PAGE) in SVG foreignObject text, preserving cached values when no live count is available.

Field refresh

  • F9 (field-update.js) and the Document API fields.rebuild (field-wrappers.ts + new section-page-count.ts helper, field-resolver.ts) now recompute SECTIONPAGES values, replacing the node so text content and resolvedText stay in sync.

Notable fidelity fixes

  • Header/footer height: the final HeaderFooterLayout now derives minY/maxY/renderHeight/height from the widest bounds across all per-page layouts instead of only the first page — previously per-page differences (now common with SECTIONPAGES) could mis-size the region.
  • parseDOM selectors: fixed missing ] in the page-number and total-page-number selectors (span[data-id="auto-page-number"…"]), which previously made those selectors invalid.
  • PAGE field-local formatting now also applies in editable header/footer DOM and shape text, not just static paint.

Testing

New/updated unit tests accompany each layer: pageNumbering, resolvePageNumberTokens, painter index/text-run, layout-bridge headerFooterLayout/resolveHeaderFooterTokens, importer preprocessors (section-pages-preprocessor, preProcessPageFieldsOnly, preProcessNodesForFldChar), sectionPageCount-translator, encode-image-node-helpers, svg-utils, field-update, page-number, field-resolver, field-wrappers.section-pages, HeaderFooterRegistry, HeaderFooterSessionManager, and story-editor-factory.

@luccas-harbour luccas-harbour requested a review from a team as a code owner June 2, 2026 16:41
@linear-code
Copy link
Copy Markdown

linear-code Bot commented Jun 2, 2026

SD-3349

SD-3027

@luccas-harbour luccas-harbour changed the base branch from main to luccas/sd-3006-feature-page-number-fields June 2, 2026 16:41
@luccas-harbour luccas-harbour self-assigned this Jun 2, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

The ecma-spec MCP permission isn't being granted in this session, so I verified against my own knowledge of ECMA-376 Part 1 §17.16 (field codes) rather than the live spec. Flagging that caveat up front.


Status: PASS

This PR adds a SECTIONPAGES field type plus PAGE/SECTIONPAGES value-format switches, and the OOXML-facing parts hold up against ECMA-376. The bulk of the diff is layout-engine/painter plumbing (not OOXML); the spec-relevant surface is the field preprocessors, the sd:* translators, and the exported w:fldChar/w:instrText runs. Notes:

  • SECTIONPAGES is a real field. It's documented in §17.16.5 ("number of pages in the current section"), alongside PAGE and NUMPAGES. Good — not an invented keyword. (spec)

  • Format-switch mapping is correct. In page-instruction.js, the \* general formatting switches map the way Word does: Roman/ROMAN→upper, roman→lower, ALPHABETIC→upper, alphabetic→lower, Arabic→decimal, ArabicDash→numberInDash. The case-of-keyword → case-of-output behavior matches §17.16.4.2. (spec)

  • The numberInDash fix is actually more correct. Changing -1-- 1 - matches Word's \* ArabicDash output, which includes the surrounding spaces.

  • Case-insensitive field dispatch is spec-aligned. Field type names are case-insensitive in OOXML, so routing page/Page/PAGE to the same preprocessor (via extractFieldKeyword) is a correctness improvement, not a regression. Same for the ^\s*HYPERLINK anchored/case-insensitive regex.

  • Export structure is well-formed. buildComplexFieldRuns emits w:fldChar begin/separate/end with w:instrText in between, and w:dirty="true" is a valid CT_FldChar attribute. Instruction text carries the conventional leading space ( SECTIONPAGES \* Roman \* MERGEFORMAT). The sd:autoPageNumber / sd:sectionPageCount nodes are SuperDoc's internal namespace (round-tripped back to w: field runs), so they're correctly not treated as OOXML elements.

Two minor, non-blocking robustness gaps (not spec violations):

  • The switch table has no arabic (lowercase) or Alphabetic (initial-cap) entries. Word accepts those; here they fall through to the default (decimal / no local format), which is harmless but slightly lossy on re-export.
  • Worth confirming w:instrText is emitted with xml:space="preserve" so the leading space in the instruction survives a strict reader — though this mirrors the pre-existing PAGE path, so it's not introduced here.

Net: no non-existent elements/attributes, no missing required attributes, no incorrect defaults. OOXML-compliant.

@luccas-harbour luccas-harbour changed the title feat(fields): add SECTIONPAGES field support with section-aware numbering (SD-3349 and SD-3029) feat(fields): add SECTIONPAGES field support with section-aware numbering (SD-3349 and SD-3027) Jun 2, 2026
@chatgpt-codex-connector
Copy link
Copy Markdown

💡 Codex Review

} else if (block.kind === 'list') {
const list = block as ListBlock;
for (const item of list.items ?? []) {
if (paragraphHasPageToken(item.paragraph)) return true;
}

P2 Badge Resolve list-item page tokens before measuring

This new list branch makes header/footer variants with PAGE/NUMPAGES/SECTIONPAGES inside list items take the token-aware layout path, but resolveHeaderFooterTokens() only visits top-level paragraphs and tables, and cloneHeaderFooterBlock() still shallow-copies lists. In that scenario the list item run keeps the import placeholder/cached text during measureBlock, so a footer list like SECTIONPAGES changing from 0/1 to a wider value can be measured and positioned with the wrong width even though the painter later renders the real value.

ℹ️ 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".

@luccas-harbour
Copy link
Copy Markdown
Contributor Author

💡 Codex Review

} else if (block.kind === 'list') {
const list = block as ListBlock;
for (const item of list.items ?? []) {
if (paragraphHasPageToken(item.paragraph)) return true;
}

P2 Badge Resolve list-item page tokens before measuring
This new list branch makes header/footer variants with PAGE/NUMPAGES/SECTIONPAGES inside list items take the token-aware layout path, but resolveHeaderFooterTokens() only visits top-level paragraphs and tables, and cloneHeaderFooterBlock() still shallow-copies lists. In that scenario the list item run keeps the import placeholder/cached text during measureBlock, so a footer list like SECTIONPAGES changing from 0/1 to a wider value can be measured and positioned with the wrong width even though the painter later renders the real value.

ℹ️ About Codex in GitHub

ListBlock-related code is obsolete. It is a remnant from when we treated list items separately from paragraphs. It doesn't have to be updated.

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

cubic analysis

2 issues found across 65 files

Linked issue analysis

Linked issue: SD-3349: Bug: Numbering issues

Status Acceptance criteria Notes
Import DOCX SECTIONPAGES fields into the pipeline (create sd:sectionPageCount nodes, preserve cached text/run props) New SECTIONPAGES preprocessor and importer wiring are present and covered by unit tests that assert creation and preservation of cached text/run properties.
Compute and expose per-section page counts during layout (DisplayPageInfo / numbering context) Layout code now computes sectionPageCount and threads it through page resolvers and DisplayPageInfo; incremental layout and layoutHeaderFooter were updated and corresponding tests added.
Resolve SECTIONPAGES tokens (and formatting) at render/layout time (body/header/footer/shape) Token resolution updated to accept sectionPageCount and apply pageNumberFormat; resolvePageTokens, resolveHeaderFooterTokens, text-run resolution, and unit tests were added/updated to assert formatted SECTIONPAGES output.
Header/footer and shape editing surfaces render and expose section-aware values (editor options and DOM/SVG rendering) Editor plumbing now threads sectionPageCount/current-page display values through HeaderFooterSessionManager, HeaderFooterRegistry, PresentationEditor and story editor factory; shape and SVG rendering consume sectionPageCount and tests assert behaviour and options.
Export reconstructs SECTIONPAGES complex-field w:fldChar structure (encode/decode) A sectionPageCount translator was added and wired into exporter/translators; translator has tests for encode/decode.
Field refresh (F9 and fields.rebuild) recomputes SECTIONPAGES values and updates resolved text Field update extension now treats SECTIONPAGES as updatable; document API field-wrappers and a section-pages-specific rebuild path were added with tests verifying F9/fields.rebuild behaviour.
Header/footer layout fidelity when SECTIONPAGES varies per-page (use largest per-page metrics; disable page-bucketing) Header/footer layout logic was changed to derive heights across per-page layouts and paragraph token checks now detect sectionPageCount to avoid page-bucketing; tests were added to validate choosing largest metrics.
End-to-end rendering in DOM and vector shapes (formatted PAGE/SECTIONPAGES values appear in painted DOM/SVG) Painter/renderer changes handle SECTIONPAGES and field-local formatting in shape text; tests assert formatted PAGE/SECTIONPAGES in DOM and drawing text.

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/layout-engine/contracts/src/index.ts
Comment thread packages/layout-engine/painters/dom/src/runs/text-run.ts Outdated
@luccas-harbour luccas-harbour force-pushed the luccas/sd-3006-feature-page-number-fields branch from 97ef352 to 2416c1e Compare June 2, 2026 17:05
@luccas-harbour luccas-harbour force-pushed the luccas/sd-3349-bug-numbering-issues branch from 9cd0404 to efbe1b2 Compare June 2, 2026 17:06
@luccas-harbour luccas-harbour force-pushed the luccas/sd-3349-bug-numbering-issues branch from c9b8acd to 01fcb64 Compare June 2, 2026 17:19
… parity

OOXML (ECMA-376 §17.10.1) selects even/odd headers based on the printed page
number — which respects per-section numbering restarts and offsets — not the
physical page index. Track the post-restart/offset value as `displayNumber` on
each page and thread it through pagination, header/footer resolution, and the
HeaderFooterSessionManager so a section that starts at page 2 picks the `even`
variant on its first page.
Parse `\*` general-format and `\#` numeric-picture switches when
importing PAGE/NUMPAGES fields and thread the requested format
(roman/alphabetic/zero-padded decimal/etc.) plus the section-aware
numeric page value through the converter, pm-adapter, layout engine,
and DOM painter so page-number fields render in the format Word stored
rather than always decimal. The original instruction is preserved on
the node so export round-trips back to the same field code.
Replace the third positional `_docxOrFieldRunRPr` argument with a
generic field-preprocessing `options` object (docx, instructionTokens,
fieldRunRPr), keeping backward-compat with the legacy positional w:rPr.
Update cache-invalidation test fixtures to the `pageNumberFieldFormat`
run shape.
@luccas-harbour luccas-harbour force-pushed the luccas/sd-3349-bug-numbering-issues branch from 25957c8 to e2a88d6 Compare June 3, 2026 16:58
Base automatically changed from luccas/sd-2990-feature-headerfooter-page-numbers to main June 3, 2026 23:43
luccas-harbour and others added 2 commits June 3, 2026 17:58
…3620)

* feat(sections): add chapter numbering to section page numbering

Extend section page numbering with `chapterStyle` (w:chapStyle) and
`chapterSeparator` (w:chapSep) support across the stack:

- document-api: add fields to the setPageNumbering contract (anyOf now
  accepts chapterStyle/chapterSeparator), schemas, types, and validation
- layout contracts: extend SectionNumbering with chapter fields and
  reuse PageNumberFormat/PageNumberChapterSeparator types
- layout-adapter: extract w:chapStyle/w:chapSep from pgNumType, validate
  positive integers and known separators, and compare them in section
  signatures
- sections-xml helpers: read/write chapter attributes on w:pgNumType
- regenerate Document API reference docs

* feat(layout): render chapter-prefixed page numbers

Resolve and paint chapter number prefixes (e.g. "1-1", "3:V") for
sections that enable chapter numbering, deriving the chapter from the
nearest numbered Heading N marker.

- contracts: add formatSectionPageNumberText helper, chapter/format
  fields on Page and HeaderFooterPage, and headingLevel/listLevelOrdinal
  paragraph attrs
- pageNumbering: add buildChapterContextByPage to track the active
  chapter per physical page, normalizeChapterMarkerText to accept only
  clean single-token markers, and thread chapter context through
  computeDisplayPageNumber
- layout-bridge: build/cache chapter context across PAGE-token
  convergence, apply it to body pages, and disable header/footer digit
  bucketing when chapter prefixes can vary the rendered width
- resolvePageTokens / text-run / renderer: preserve the chapter prefix
  when a run-local PAGE format switch applies
- layout-adapter: resolve built-in heading level from style metadata
  (incl. localized names) and expose the structured list ordinal

* fix(page-number): fall back chapter headings

* fix(page-number): use no-break chapter hyphen

* fix(page-number): clear stale chapter children

* fix(layout): stamp section page format

* fix(page-number): accept nested chapter markers

* fix(layout): converge page tokens by output

* fix(header-footer): disable buckets for restarts

* fix(page-number): preserve body token metadata

* fix(header-footer): pass per-rid chapter context

* fix(layout): key chapter cache by fragments

* fix(page-number): allow safe chapter separators

* fix(header-footer): measure chapter tokens in prelayout

* fix(header-footer): preserve chapter page prefix in editor

* fix(header-footer): clear stale chapter editor context

* fix(header-footer): prelayout resolved headings

* fix(header-footer): prelayout heading ordinal fallback

* fix(header-footer): prelayout two digit page tokens

* fix(layout-bridge): keep numpages headers bucketed
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

@harbournick harbournick left a comment

Choose a reason for hiding this comment

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

LGTM

@harbournick harbournick merged commit 4cf6d78 into main Jun 4, 2026
74 checks passed
@harbournick harbournick deleted the luccas/sd-3349-bug-numbering-issues branch June 4, 2026 03:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants