Skip to content

feat!: fuse ContentDocument/LayoutDocument into a unified DocumentPackage tree and add canonical headingLevel - #16

Merged
Mearman merged 1 commit into
mainfrom
feat/document-package-fusion
Aug 17, 2026
Merged

feat!: fuse ContentDocument/LayoutDocument into a unified DocumentPackage tree and add canonical headingLevel#16
Mearman merged 1 commit into
mainfrom
feat/document-package-fusion

Conversation

@Mearman

@Mearman Mearman commented Aug 17, 2026

Copy link
Copy Markdown
Member

What changed

#12 — fused DocumentPackage tree. DocumentPackageSchema no longer pairs a ContentDocument with a separate, optional LayoutDocument correlated only by matching sourcePath strings. Instead, every content-kind leaf that used to carry just a sourcePathContentRun, ContentParagraph, ContentImageBlock, ContentPageBreak, ContentTable, ContentTableCell, ContentEmbeddedObjectBlock, ContentShape, every ContentVector variant, and ContentSheetCell — now also carries an optional frames?: LayoutFrame[] field: that node's own rendered page position(s) (pageIndex plus PDF user-space xPt/yPt/widthPt/heightPt, matching LayoutItem's existing convention). A node can carry more than one frame — appearing at more than one rendered position (a paragraph wrapping across a page boundary is the running example) — without the content itself being split or duplicated. This pattern is named once as the exported FusedNode<T> utility type.

DocumentPackageSchema itself now carries content (required) plus pages (optional): each rendered page's own size, indexed to match every node's own frames[].pageIndex. The old layout: LayoutDocument field is gone.

LayoutDocumentSchema/LayoutItem are untouched and still exported standalone — they remain the right shape for pdf-codec's own readPdf/writePdf, which read and write a PDF's pages of positioned items directly with no ContentDocument in the loop at all.

#13 — canonical headingLevel. ContentParagraphSchema gains an optional, format-agnostic headingLevel: number field (1 = outermost heading) alongside the existing round-trip-only styleId string. A new exported clampHeadingLevel(level): number helper clamps an arbitrary level to 1-6, for a writer whose own vocabulary tops out there (markdown-codec's private clamp-to-6 logic is the motivating case, to be switched over in a follow-up).

Why one PR

Both changes reshape the same schema files and both are breaking, so landing them together (per #13's own suggestion) means one major version bump instead of two.

Version bumps

  • CONTENT_FORMAT_VERSION: 2 → 3
  • DOCUMENT_PACKAGE_FORMAT_VERSION: 1 → 2
  • LAYOUT_FORMAT_VERSION is unchanged — LayoutDocumentSchema's own shape didn't change.

Other updates

  • documentPackageWithSchema/documentFromJson/schemaUriFor needed no code changes (they're version/kind-agnostic), but existing tests and README examples referencing the old { content, layout } shape are updated.
  • The three published JSON Schema files are regenerated via scripts/generate-json-schemas.mjs, and their hand-authored $defs fragments in content-json-schema-defs.ts are updated to match (including a new LayoutFrame fragment, held to the same live z.toJSONSchema() regression test as every other leaf fragment).
  • Every existing test constructing a DocumentPackage/ContentParagraph literal is updated to the new shape, plus new tests for headingLevel, clampHeadingLevel, and the frames fusion pattern (including a node with multiple frames, and negative/malformed-frame rejection).
  • test/smoke.test.mjs and test/workers/document-schema.test.ts are updated to match the new generated schema shape.

Scope note

This package has no consumers to update in this same PR — ooxml.js/odf.js/documents.js consume it via their own npm dependency and will be updated in separate follow-up work once this publishes. DocumentPackageSchema/ContentDocumentSchema here are internally consistent and richly tested on their own.

Verification

pnpm lint, pnpm typecheck, pnpm test, pnpm test:workers, pnpm build, and pnpm test:smoke (which rebuilds dist//schemas/ and includes the JSON-schema regression test comparing hand-authored $defs fragments against live z.toJSONSchema() output) all pass locally.

Closes #12
Closes #13


Generated by Claude Code

…d canonical headingLevel

Replace DocumentPackage's separate content/layout pivots -- correlated
only by a fragile sourcePath string -- with a single fused tree: every
content-kind leaf that used to carry just a sourcePath (run, paragraph,
image, page break, table, table cell, embedded object, shape, every
vector variant, spreadsheet cell) now also carries an optional
`frames: LayoutFrame[]` field of its own rendered page position(s), via
the new FusedNode<T> pattern. DocumentPackage itself keeps only `pages`
(each rendered page's own size) alongside `content`. LayoutDocument is
untouched and still exported standalone, since pdf-codec's own
readPdf/writePdf read and write it directly with no ContentDocument
involved.

Also add ContentParagraph's canonical, format-agnostic `headingLevel`
field alongside the existing round-trip-only `styleId`, plus a shared
`clampHeadingLevel` helper for a writer whose own heading vocabulary
tops out at 6 (markdown-codec's private clamp-to-6 logic is the
motivating case).

Bumps CONTENT_FORMAT_VERSION (2 -> 3) and DOCUMENT_PACKAGE_FORMAT_VERSION
(1 -> 2), regenerates the three published JSON Schema files, and updates
their hand-authored $defs fragments (content-json-schema-defs.ts) to
match, including a new LayoutFrame fragment held to the same live
z.toJSONSchema() regression test as every other leaf fragment. Updates
every existing test that constructs a DocumentPackage/ContentParagraph
literal, and the build-output smoke test, to match the new shape.

BREAKING CHANGE: DocumentPackageSchema no longer has a `layout` field.
A DocumentPackage produced against DOCUMENT_PACKAGE_FORMAT_VERSION 1
must be rebuilt: move each rendered position onto its own content
node's new `frames` field and replace the old `layout` value with a
`pages` array of page sizes. ContentDocumentSchema's own
CONTENT_FORMAT_VERSION moves from 2 to 3 for the new `frames` and
`headingLevel` fields.
@Mearman
Mearman merged commit 74f1f6b into main Aug 17, 2026
11 checks passed
@Mearman
Mearman deleted the feat/document-package-fusion branch August 17, 2026 12:14
@github-actions

Copy link
Copy Markdown

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

Add a canonical headingLevel field to the paragraph content model Make DocumentPackage the unified content+layout tree, not a sibling container

1 participant