feat!: fuse ContentDocument/LayoutDocument into a unified DocumentPackage tree and add canonical headingLevel - #16
Merged
Conversation
…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.
|
🎉 This PR is included in version 3.0.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
#12 — fused DocumentPackage tree.
DocumentPackageSchemano longer pairs aContentDocumentwith a separate, optionalLayoutDocumentcorrelated only by matchingsourcePathstrings. Instead, every content-kind leaf that used to carry just asourcePath—ContentRun,ContentParagraph,ContentImageBlock,ContentPageBreak,ContentTable,ContentTableCell,ContentEmbeddedObjectBlock,ContentShape, everyContentVectorvariant, andContentSheetCell— now also carries an optionalframes?: LayoutFrame[]field: that node's own rendered page position(s) (pageIndexplus PDF user-spacexPt/yPt/widthPt/heightPt, matchingLayoutItem'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 exportedFusedNode<T>utility type.DocumentPackageSchemaitself now carriescontent(required) pluspages(optional): each rendered page's own size, indexed to match every node's ownframes[].pageIndex. The oldlayout: LayoutDocumentfield is gone.LayoutDocumentSchema/LayoutItemare untouched and still exported standalone — they remain the right shape for pdf-codec's ownreadPdf/writePdf, which read and write a PDF's pages of positioned items directly with noContentDocumentin the loop at all.#13 — canonical
headingLevel.ContentParagraphSchemagains an optional, format-agnosticheadingLevel: numberfield (1 = outermost heading) alongside the existing round-trip-onlystyleIdstring. A new exportedclampHeadingLevel(level): numberhelper 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 → 3DOCUMENT_PACKAGE_FORMAT_VERSION: 1 → 2LAYOUT_FORMAT_VERSIONis unchanged —LayoutDocumentSchema's own shape didn't change.Other updates
documentPackageWithSchema/documentFromJson/schemaUriForneeded no code changes (they're version/kind-agnostic), but existing tests and README examples referencing the old{ content, layout }shape are updated.scripts/generate-json-schemas.mjs, and their hand-authored$defsfragments incontent-json-schema-defs.tsare updated to match (including a newLayoutFramefragment, held to the same livez.toJSONSchema()regression test as every other leaf fragment).DocumentPackage/ContentParagraphliteral is updated to the new shape, plus new tests forheadingLevel,clampHeadingLevel, and theframesfusion pattern (including a node with multiple frames, and negative/malformed-frame rejection).test/smoke.test.mjsandtest/workers/document-schema.test.tsare 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.jsconsume it via their own npm dependency and will be updated in separate follow-up work once this publishes.DocumentPackageSchema/ContentDocumentSchemahere are internally consistent and richly tested on their own.Verification
pnpm lint,pnpm typecheck,pnpm test,pnpm test:workers,pnpm build, andpnpm test:smoke(which rebuildsdist//schemas/and includes the JSON-schema regression test comparing hand-authored$defsfragments against livez.toJSONSchema()output) all pass locally.Closes #12
Closes #13
Generated by Claude Code