feat!: DocumentPackage-native readMarkdown/writeMarkdown - #71
Merged
Conversation
readMarkdown now returns document-schema.js's tree-form DocumentPackage
(assemblePackage over the lowered content: decompose, envelope splice, then
styles minting) and writeMarkdown accepts one, flattening it back before
emission.
markdownCodec follows, moving from ContentDocumentSchema to
DocumentPackageSchema.
A codec is a construction site -- the point a document first exists -- and
assemblePackage is the helper every construction site in this family calls,
so the tree is what a caller gets from the unsuffixed names.
The previous flat behaviour survives unchanged under readMarkdownContent,
writeMarkdownContent, and markdownContentCodec, mirroring ooxml.js's
readXlsx/readXlsxContent pair.
The tree functions are exact compositions of the flat ones, so both render
identical markdown from the same source; the conformance suites stay on the
flat pair, since lowering and emission are where every conformance-relevant
decision is made.
BREAKING CHANGE: readMarkdown returns { documentPackage: DocumentPackage },
not { document: ContentDocument }; writeMarkdown accepts a DocumentPackage,
not a ContentDocument; and markdownCodec decodes to a DocumentPackage. A
caller that wants the previous flat behaviour should rename its calls to
readMarkdownContent, writeMarkdownContent, and markdownContentCodec, whose
signatures and behaviour are unchanged. The result field is documentPackage
rather than package because package is a reserved word in strict mode.
src/package.test.ts pins the three properties that make the tree-form functions trustworthy as the primary entry points: that they are exactly assemblePackage/flattenPackage composed onto the flat pair (so swapping assemblePackage for bare decompose, or forgetting to flatten before emitting, fails here), that the transform is transparent to the markdown itself, and that bytes survive decode -> encode -> decode to the identical package. The blockquote fixture is the one construct this package's lowering produces whose repeated indentLeftPt tuple minting actually hoists onto a styles-table entry, which is what separates assemblePackage from decompose plus an envelope. The workers suite runs both encodings inside the workerd isolate, so document-schema.js's own decompose/factorStyles/flattenPackage are checked for Worker isomorphism on the path this package puts them on. The smoke test exercises both encodings against each built bundle for the same reason.
A new "Two encodings" section names the DocumentPackage/ContentDocument split document-schema.js owns, tabulates the read/write/codec trio at each level, and states when a caller genuinely wants the flat pair: composing a package boundary by hand, feeding a ContentDocument-consuming builder, or stamping layout frames onto content before it is decomposed. The usage examples, status line, architecture entry, and footnote section follow the same split, and the package description names the tree form the unsuffixed functions now speak.
Mearman
force-pushed
the
feat/document-package-api
branch
from
August 19, 2026 06:21
3af51ab to
8cd3a64
Compare
…wn errors writeMarkdown now checks documentPackage.kind before calling flattenPackage, throwing MarkdownUnsupportedDocumentKindError directly rather than letting flattenPackage's own kind-specific validation (a formula package's single-node constraint, a spreadsheet sheet group's style-ref constraint) throw an untyped Error first for a package this function was never going to accept anyway. The one flattenPackage failure still reachable for a 'wordprocessing' package -- a group referencing a style the package's own styles table has no entry for -- is now caught and rewrapped as the new MarkdownPackageFlattenError, part of this package's own MarkdownWriteError hierarchy rather than a bare Error from the dependency. writeMarkdown also now reports a PACKAGE_TABLE_DROPPED diagnostic for each non-empty definitions/layers/attachments/destinations/pages table a package carries: flattenPackage's own envelope carries forward only metadata and symbolTable, so a caller composing one of those tables onto a DocumentPackage before calling writeMarkdown previously saw it vanish with no diagnostic at all.
…, and footnote shapes Covers the write.ts fix: PACKAGE_TABLE_DROPPED firing once per non-empty table (and not at all when none are present), MarkdownUnsupportedDocumentKindError replacing the bare Error a malformed formula package used to throw from inside flattenPackage, and MarkdownPackageFlattenError replacing the bare Error a style ref with no styles table used to throw. diagnostics/diagnostics.test.ts's own reachability sweep now exercises PACKAGE_TABLE_DROPPED too, so the dead-code check keeps covering every code in the table. Extends the tree-vs-flat equivalence src/package.test.ts pins over two hand-picked fixtures (flattenPackage(assemblePackage(document)) reproducing document, and writeMarkdown rendering what writeMarkdownContent renders) to run across every example in the vendored CommonMark 0.31.2 and tagged GFM corpora, in src/conformance.test.ts and src/gfm-conformance.test.ts. Adds a dedicated footnote-shape fixture set to src/package.test.ts -- bodyless, a duplicate label, a multi-paragraph body, and a definition following a list, a blockquote, and a heading -- exercising the construct-group promotion path over shapes the vendored corpora never contain a single footnote to test at all.
… round trip Documents writeMarkdown's full error contract: the kind check now runs ahead of flattening, a style ref with no styles table raises MarkdownPackageFlattenError rather than a bare Error, and a non-empty definitions/layers/attachments/destinations/pages table reports through PACKAGE_TABLE_DROPPED (added to the Gotchas list) instead of vanishing silently. Corrects the "assemblePackage and flattenPackage are inverses" claim: only flattenPackage(assemblePackage(document)) holds in general, checked here against the full CommonMark and GFM conformance corpora rather than asserted from two fixtures; assemblePackage(flattenPackage(documentPackage)) does not, for a package carrying any of the tables flattenPackage's own envelope drops. Rewrites the footnote-body rationale: DocumentPackage does carry a definitions-table root (unlike the flat ContentDocument), so the reason a footnote body still rides the construct's own extent rather than AnchorDescriptor.definition is that a definitions-table entry is a flat descriptor record rather than a container for block content, not that no root exists to carry the table.
Contributor
|
🎉 This PR is included in version 4.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.
This branch is rebased onto main after the automated 4.3.0 bump landed there, so it carries no dependency change of its own.
document-schema.js 4.3.0 exports the package-boundary transform (
decompose,flattenPackage,factorStyles,assemblePackage), so markdown-codec can finally speak the tree form directly instead of leaving every caller to assemble it.readMarkdownnow returns aDocumentPackageandwriteMarkdownaccepts one;markdownCodecfollows, decoding toDocumentPackageSchema. The old flat behaviour is untouched underreadMarkdownContent/writeMarkdownContent/markdownContentCodec— same signatures, same options, same diagnostics, same errors — mirroring thereadXlsx/readXlsxContentnaming already in ooxml.js. documents.js's pipeline consumes the flat pair, so it keeps working after a rename of its call sites.Which composition the read side uses is the one real design call here.
assemblePackage, not baredecompose:decomposealone hands back only the children array and leaves the envelope splice and the styles-minting pass to the caller, whileassemblePackageis all three, and document-schema.js's own barrel calls it "the one helper a construction site calls". A codec is a construction site. Nopagesargument, because markdown has no layout stage — same as the layoutless bridge conversions in documents.js.The tree functions are exact compositions of the flat ones, so the conformance suites stay on the flat pair: lowering and emission are where every conformance-relevant decision is made, and making the CommonMark corpus rate depend on a transform that has nothing to say about CommonMark would only muddy it.
src/package.test.tspins that the two pairs render identical text, so the flat measurement speaks for both.Breaking
readMarkdownreturns{ documentPackage: DocumentPackage, diagnostics }, not{ document: ContentDocument, diagnostics }. The field isdocumentPackagebecausepackageis a reserved word in strict mode —const { package } = readMarkdown(src)does not parse.writeMarkdownaccepts aDocumentPackage.markdownCodecdecodes to aDocumentPackage.ReadMarkdownResultnow describes the tree result; the old shape isReadMarkdownContentResult.Rename to the
*Contentnames for the previous behaviour.Tests
src/package.test.tscovers the three properties worth pinning: that the tree functions really areassemblePackage/flattenPackagecomposed onto the flat pair, that the transform is transparent to the emitted markdown, and that bytes survive decode → encode → decode to the identical package. The blockquote fixture is deliberate — two blockquote paragraphs sharing anindentLeftPttuple is the one construct this package's lowering produces that minting actually hoists onto a styles-table entry, which is exactly what separatesassemblePackagefromdecomposeplus an envelope.The workers suite runs both encodings inside workerd, so document-schema.js's own transform is checked for Worker isomorphism on the path this package puts it on. Smoke test does both encodings against each built bundle.
Green: typecheck, lint, test (1003), test:workers, test:smoke, build, publint, attw.
🤖 Generated with Claude Code