This repository was archived by the owner on Aug 20, 2026. It is now read-only.
feat: build per-kind outlines over any ContentDocument - #1
Merged
Conversation
…uard, semantic-release, CI)
… guard OutlineNode is a group (text/level/children) whose children mix nested groups and leaf payloads drawn from every ContentDocument kind: block flow, sheet images, embedded documents, vectors, and the formula. The recursion lives in a plain function guard behind z.custom -- the same pattern document-schema's ContentBlock uses -- because z.lazy() collapses a recursive schema's static type to unknown in the pinned zod 4. Leaf validation delegates to document-schema's own exported schemas so the leaf shapes stay owned by the schema package.
buildOutline dispatches on doc.kind and returns the root scope's children. Wordprocessing headings nest by headingLevel with stack semantics (H4 after H2 nests directly, H1 after H3 pops to root, headingLevel is the only heading signal read); list paragraphs nest inside a heading scope by list.level on the same semantics, and a plain paragraph sits flat at its scope, closing the list nesting so flattened order stays document order. Presentations get one group per slide (Slide N, matching the Markdown renderer) with paragraphs nested by list.level and non-paragraph blocks attached at the current depth. Spreadsheets get one group per sheet over images and embedded objects, drawings one group per page over shape blocks and vectors, and a formula document a single node whose leaf is the ContentFormula. Test fixtures are asserted against ContentDocumentSchema so the shapes tested are the real schema 3.3.0 shapes.
flattenOutline walks the tree depth-first and yields every leaf in document order; outlineLeafText returns a leaf's own text (paragraph runs, table cell paragraphs, image altText, formula LaTeX, empty for the textless leaves); leafContentHash canonicalises key order, JSON-serialises, UTF-8 encodes, and digests with a hand-rolled SHA-256 -- Worker-isomorphic, since node:crypto is banned in runtime src and SubtleCrypto is async -- pinned against the FIPS 180-4 vectors. The public barrel lands here, with the whole surface present.
Runs buildOutline for all five kinds plus the helpers under @cloudflare/vitest-pool-workers, turning the zero-Node-API design into a runtime-checked fact: any node:* or Buffer use in the module graph (including the zod and document-schema dependencies and the hand-rolled SHA-256) would make the isolate throw instead of the tests pass.
The README records what each kind's hierarchy looks like, the level semantics (source signal carried verbatim, render from nesting), the flat-and-close rule that keeps flattened order equal to document order, and every step of the published hash recipe. The agent instruction entry points symlink README.md, matching the family convention.
Mearman
force-pushed
the
feat/outline-all-kinds
branch
from
August 17, 2026 21:53
74f9309 to
6791a5a
Compare
…alias The schema repo publishes five alias names alongside its primary document-schema.js, and the family's sibling-released automation bumps dependents by running pnpm add document-schema.js@$VERSION. Against the alias this repo imported, that bump would install a second copy under the canonical name and never update the imported module, so the dependency, every import specifier, and the README now use document-schema.js, matching ooxml.js, odf.js, and markdown-codec.
A schema release dispatches sibling-released to every dependent repo, which needs a repository_dispatch listener here to receive it: without this stub the dispatch finds no workflow, so no bump PR can ever open and a new document-schema.js would strand this package on its pinned version. The three files are ooxml.js's verbatim: the listener stub delegating to ExaDev/.github's reusable workflow, the dependabot config with the sibling cooldown exemption, and the auto-merge policy whose sibling list already names document-schema.js. The dependabot-ignore rule commitlint.config.ts already carries now backs a workflow that exists here instead of pointing at nothing.
…mptions minimumReleaseAge: 0 disabled the freshness gate for every dependency, broader than the same-day-sibling need that motivated it, and deviated from the uniform 60 every family repo carries. 60 minutes keeps the third-party gate while a same-day schema release ages past the window within the hour; the family-wide exemption list covers the sibling packages on the pnpm add path the sibling-dependency-update workflow bumps through.
…anonicaliser with a guard The spreadsheet, drawing, and formula children spreads infer (ContentSheetImage | ContentEmbeddedObject)[], (ContentBlock | ContentVector)[], and ContentFormula[] -- each already assignable to OutlineChild[], so the as OutlineChild[] annotations asserted nothing the compiler did not know. canonicalise narrowed its object branch with a cast instead; it now uses the same isRecord type guard node.ts and document-schema.js's own content guards use for exactly this narrowing.
…el edges The suite covered jumps deeper (H2 to H4) and pops to the root (H3 to H1) but not a step back by one: H2 after H1/H3 must pop exactly the H3 and land beside it as H1's child, the case a refactor of the stack loop is most likely to regress by popping too much or too little. The schema leaves headingLevel unbounded, so a headingLevel 10 under an H2 must nest as its direct child with the level carried verbatim -- the builder must not clamp to the format conventions (ODF's ten levels, Word's nine) that the schema deliberately does not enforce.
|
🎉 This PR is included in version 1.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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Resolves the expanded scope of ExaDev/document-schema.js#14: a new sibling package,
document-outline.js, depending only ondocument-schema.js(andzod), that builds a hierarchical outline over anyContentDocument— all five kinds — plus the kind-agnostic tree-walking helpers.Closes ExaDev/document-schema.js#14
Per-kind hierarchy
headingLevelwith stack semantics: a heading nests under the deepest open group with a strictly shallower level and pops equal-or-deeper groups closed, so an H4 after an H2 becomes its direct child (no synthetic intermediates) and an H1 after an H3 pops to the root.headingLevelis the only heading signal read — a Heading style without it does not group. Inside a group, paragraphs carrying list membership nest further bylist.levelon the same stack semantics (level 0 items are the group's children, level 1 under the last level 0, a 0→2 jump nests directly under the 0). Non-paragraph blocks attach as leaves at the current depth without changing it; a plain paragraph sits flat at its scope and closes the list nesting — the rule that keeps flattened leaf order identical to document order.Slide N(1-based, matching the Markdown renderer's convention); the slide's paragraphs, across its shapes in shape order, nest under the group bylist.level(PowerPoint-outline-view semantics).headingLevelis not read here: slides have no heading hierarchy of their own. Non-paragraph blocks attach at the current depth; a slide with no list levels is flat under its group.Page N; children are the page's shape blocks in shape order, then its vectors (textless leaves so structural diffing still sees them).ContentFormulaitself, labelled with its LaTeX linearisation when present.API and helpers
buildOutline(doc): OutlineChild[]— the root scope's children; the root is deliberately not a synthetic node, so pre-heading content appears as leaves directly in the returned array.OutlineNode(text/level/children) withOutlineNodeSchemaas az.customover a hand-written recursive guard — the family's established pattern, sincez.lazy()collapses recursive schemas tounknownin the pinned zod 4.levelcarries the source signal verbatim (headingLevel 1-based, list.level 0-based, synthetic groups 1); render indentation from the nesting, not fromlevel.flattenOutline(leaves in document order),outlineLeafText(a leaf's own text: paragraph runs, table cell paragraphs, image altText, formula LaTeX),leafContentHash(key-order canonicalisation → JSON → UTF-8 → hand-rolled SHA-256 → lowercase hex; Worker-isomorphic, deterministic, pinned against the FIPS 180-4 vectors).Notes for review
ContentDocumentSchema.parsefirst, so the tested shapes are the real schema 3.3.0 shapes (this caught a 0–1 float colour channel during development).workerdisolate, complementing the ESLint Worker-isomorphism guard on runtimesrc/.sibling-dependency-update.yml(the ExaDev/.github reusable-workflow stub),dependabot.yml, anddependabot-auto-merge.ymlare ooxml.js's verbatim, so a schema release dispatchingsibling-releasedopens and lands a bump PR here like in every other dependent repo.pnpm-workspace.yamlcarries the family's uniformminimumReleaseAge: 60with the family-wide sibling exemption list: a same-day schema release ages past the window within the hour (a frozen-lockfile install inside it fails once, the retry passes), and third-party dependencies stay behind the freshness gate.Generated by Claude Code