Skip to content

feat!: DocumentPackage-native readMarkdown/writeMarkdown - #71

Merged
Mearman merged 8 commits into
mainfrom
feat/document-package-api
Aug 19, 2026
Merged

feat!: DocumentPackage-native readMarkdown/writeMarkdown#71
Mearman merged 8 commits into
mainfrom
feat/document-package-api

Conversation

@Mearman

@Mearman Mearman commented Aug 19, 2026

Copy link
Copy Markdown
Member

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.

readMarkdown now returns a DocumentPackage and writeMarkdown accepts one; markdownCodec follows, decoding to DocumentPackageSchema. The old flat behaviour is untouched under readMarkdownContent / writeMarkdownContent / markdownContentCodec — same signatures, same options, same diagnostics, same errors — mirroring the readXlsx/readXlsxContent naming 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 bare decompose: decompose alone hands back only the children array and leaves the envelope splice and the styles-minting pass to the caller, while assemblePackage is all three, and document-schema.js's own barrel calls it "the one helper a construction site calls". A codec is a construction site. No pages argument, 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.ts pins that the two pairs render identical text, so the flat measurement speaks for both.

Breaking

  • readMarkdown returns { documentPackage: DocumentPackage, diagnostics }, not { document: ContentDocument, diagnostics }. The field is documentPackage because package is a reserved word in strict mode — const { package } = readMarkdown(src) does not parse.
  • writeMarkdown accepts a DocumentPackage.
  • markdownCodec decodes to a DocumentPackage.
  • ReadMarkdownResult now describes the tree result; the old shape is ReadMarkdownContentResult.

Rename to the *Content names for the previous behaviour.

Tests

src/package.test.ts covers the three properties worth pinning: that the tree functions really are assemblePackage/flattenPackage composed 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 an indentLeftPt tuple is the one construct this package's lowering produces that minting actually hoists onto a styles-table entry, which is exactly what separates assemblePackage from decompose plus 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

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
Mearman force-pushed the feat/document-package-api branch from 3af51ab to 8cd3a64 Compare August 19, 2026 06:21
…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.
@Mearman
Mearman merged commit ac07cb8 into main Aug 19, 2026
11 checks passed
@Mearman
Mearman deleted the feat/document-package-api branch August 19, 2026 06:51
@github-actions

Copy link
Copy Markdown
Contributor

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

1 participant