You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Aug 20, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Aug 20, 2026. It is now read-only.
New sibling package: build a heading-based hierarchy over ContentDocument #14
Proposing a new sibling package, not a feature of this one — see the reasoning below for why it doesn't belong here or in documents.js.
None of ContentDocument's wordprocessing shape groups content by heading. A section is blocks: ContentBlock[], a flat, ordered list — a heading paragraph sits in that list like any other, distinguished only by headingLevel (once #13 lands). Building an actual nested tree — everything under a Heading 2 becomes that heading's children, recursively — is a real, separate transform every consumer of this ecosystem currently has to write for itself if they need it (chunking a document for retrieval/search, generating a table of contents, structural diffing).
Proposed: a new package, document-outline (name open to bikeshedding), depending on document-schema.js only:
An OutlineNode schema — recursive (heading text/level, children: (OutlineNode | ContentBlock)[]). Worth trying z.lazy() for this from the start rather than the z.custom() type-guard workaround ContentBlock/ContentEmbeddedObject need — this package's own README documents a throwaway spike confirming z.lazy() works now (against the pinned zod version), with one annotation gotcha; that fix postdates when those two were built.
buildWordprocessingOutline(doc): OutlineNode[] — walks sections[].blocks, opens a new group at each headingLevel, and every non-heading block (paragraph, table, image) becomes a leaf child of whichever group is currently open.
A small set of tree-walking helpers alongside it: flatten-to-leaves, a leaf's own text, a stable per-leaf content hash — the operations every consumer of a grouped tree ends up needing regardless of what they do with it.
Scope for v1 is wordprocessing only, deliberately. Presentation has no natural deep hierarchy the way headings give wordprocessing one — a slide isn't "under" another slide — so "one group per slide" vs. something else is a real product decision, not assumed here. Spreadsheet/drawing/formula don't have an obvious equivalent at all. Leaving those unaddressed rather than guessing at a shape nobody's validated yet.
Why a new package rather than adding this here or to documents.js:
This package is a dependency of every codec and everything downstream of them. Its own sibling-release automation means every dependent gets an auto-bump PR on every release here — bundling in an algorithm that's still finding its own shape (open questions on presentation/spreadsheet scope, likely to grow toward TOC-extraction/heading-search later) means every iteration on it forces that cascade through the whole ecosystem for zero change to anyone's actual read/write logic.
documents.js is the wrong home for the opposite reason: it already depends on every codec (ooxml.js, odf.js, pdf-codec, markdown-codec, byte-codec) for its actual job, format conversion. This transform needs none of that — it only ever touches an already-produced ContentDocument, regardless of which codec made it. Forcing a consumer who wants only the outline builder to pull in every codec's dependency tree buys them nothing.
A new package gets the same single-dependency shape as every existing codec here, and the same sibling-release wiring, for free.
Blocked by #13 — there's nothing to group by until headingLevel exists.
Expanding scope from the original wordprocessing-only v1: the outline hierarchy is built for every ContentDocument kind, not just wordprocessing. Per-kind shape:
wordprocessing — nests by headingLevel with stack semantics: each heading nests under the deepest open group with a shallower level and pops shallower groups closed, so an H4 following an H2 becomes its direct child (no synthetic intermediate levels) and an H1 after an H3 pops back to the root. Non-heading blocks become leaves of the currently open group. headingLevel is the only heading signal read — no styleId fallback; it is the canonical field this ecosystem's readers now all populate.
presentation — one group per slide, labelled Slide N (matching the Markdown renderer's own per-slide heading convention), children = the slide's shape blocks in document order. Slides have no deeper native hierarchy, and inventing title heuristics from shape order would be guessing.
spreadsheet — one group per sheet, labelled with the sheet name, children = the sheet's images and embedded objects. Cells are addressable data, not outline content.
drawing — one group per page, labelled Page N, children = the page's shape blocks and vectors. Vectors carry no text but stay as leaves so structural diffing still sees them.
formula — a single node: a standalone equation has no hierarchy to build, but chunking consumers still want it as one retrievable unit.
Two corrections to the original body: the package name becomes document-outline.js (bare document-outline is taken on npm; the .js suffix matches this family's naming), and the z.lazy() note is stale — ContentBlock, ContentEmbeddedObject, and MathExpression all use the z.custom hand-written-guard pattern because z.lazy() collapses to unknown for recursive children in the pinned zod 4, so OutlineNode follows the same pattern.
The tree-walking helper set (flatten-to-leaves, a leaf's own text, stable per-leaf content hash) stays kind-agnostic as originally specified. Unblocked now that headingLevel has shipped.
Correction to the presentation shape above (and it widens scope slightly): slides DO carry deeper hierarchical content, and the outline must surface it rather than flattening.
PowerPoint's own outline view is the model: the slide's body paragraphs each sit at an outline level (a:pPr/@lvl in OOXML, text:list nesting depth in ODF), and the outline nests them — level 0 paragraphs under the slide group, level 1 under the last level 0, and so on. The original flat design above threw that away.
The level signal does not fully exist yet, so this issue now spans four repos:
document-schema.js — ContentListMembershipSchema.numId becomes optional: OOXML drawing paragraphs carry only a level, no numbering identity, and a fabricated numId would be a lie in the data. Additive, CONTENT_FORMAT_VERSION stays 3.
ooxml.js — the pptx reader populates list: { level } from a:pPr/@lvl (it already tracks levels internally for style inheritance at src/typed/pptx/inherit.ts; today the value is consumed and dropped, exactly the headingLevel/text:outline-level pattern this ecosystem has fixed twice before).
odf.js — the odp reader populates the same from text:list nesting inside slide text frames, mirroring the odt reader's own structural approach (src/typed/odt/read.ts's numId-minting walk).
document-outline.js — the presentation handler nests a slide's paragraphs by list.level under the slide group; non-paragraph blocks attach as leaves at the current depth without changing it. The same rule applies uniformly in wordprocessing: heading groups nest by headingLevel, and list content inside a group nests further by list.level — an outline that nests headings and list levels, matching how both PowerPoint's outline view and Word's navigation pane present structure.
Implemented as the new sibling package ExaDev/document-outline.js per this issue's expanded scope and both design comments — all five ContentDocument kinds, with slides nesting body content by outline level via the readers this change set shipped alongside it (ooxml.js 2.17.0 emits a:pPr/@lvl as list.level; odf.js 3.1.0 emits text:list nesting depth for odp slides; schema 3.3.0 made numId optional so level-only formats need no fabricated identity). Wordprocessing composes both depth signals — heading groups by headingLevel, list content nesting further by list.level. PR: ExaDev/document-outline.js#1
Activity
Mearman commentedon Aug 17, 2026
Expanding scope from the original wordprocessing-only v1: the outline hierarchy is built for every ContentDocument kind, not just wordprocessing. Per-kind shape:
headingLevelwith stack semantics: each heading nests under the deepest open group with a shallower level and pops shallower groups closed, so an H4 following an H2 becomes its direct child (no synthetic intermediate levels) and an H1 after an H3 pops back to the root. Non-heading blocks become leaves of the currently open group.headingLevelis the only heading signal read — no styleId fallback; it is the canonical field this ecosystem's readers now all populate.Slide N(matching the Markdown renderer's own per-slide heading convention), children = the slide's shape blocks in document order. Slides have no deeper native hierarchy, and inventing title heuristics from shape order would be guessing.Page N, children = the page's shape blocks and vectors. Vectors carry no text but stay as leaves so structural diffing still sees them.Two corrections to the original body: the package name becomes
document-outline.js(baredocument-outlineis taken on npm; the.jssuffix matches this family's naming), and thez.lazy()note is stale —ContentBlock,ContentEmbeddedObject, andMathExpressionall use thez.customhand-written-guard pattern becausez.lazy()collapses tounknownfor recursive children in the pinned zod 4, soOutlineNodefollows the same pattern.The tree-walking helper set (flatten-to-leaves, a leaf's own text, stable per-leaf content hash) stays kind-agnostic as originally specified. Unblocked now that
headingLevelhas shipped.Mearman commentedon Aug 17, 2026
Correction to the presentation shape above (and it widens scope slightly): slides DO carry deeper hierarchical content, and the outline must surface it rather than flattening.
PowerPoint's own outline view is the model: the slide's body paragraphs each sit at an outline level (
a:pPr/@lvlin OOXML,text:listnesting depth in ODF), and the outline nests them — level 0 paragraphs under the slide group, level 1 under the last level 0, and so on. The original flat design above threw that away.The level signal does not fully exist yet, so this issue now spans four repos:
ContentListMembershipSchema.numIdbecomes optional: OOXML drawing paragraphs carry only a level, no numbering identity, and a fabricated numId would be a lie in the data. Additive,CONTENT_FORMAT_VERSIONstays 3.list: { level }froma:pPr/@lvl(it already tracks levels internally for style inheritance atsrc/typed/pptx/inherit.ts; today the value is consumed and dropped, exactly theheadingLevel/text:outline-levelpattern this ecosystem has fixed twice before).text:listnesting inside slide text frames, mirroring the odt reader's own structural approach (src/typed/odt/read.ts's numId-minting walk).list.levelunder the slide group; non-paragraph blocks attach as leaves at the current depth without changing it. The same rule applies uniformly in wordprocessing: heading groups nest byheadingLevel, and list content inside a group nests further bylist.level— an outline that nests headings and list levels, matching how both PowerPoint's outline view and Word's navigation pane present structure.Mearman commentedon Aug 17, 2026
Implemented as the new sibling package ExaDev/document-outline.js per this issue's expanded scope and both design comments — all five ContentDocument kinds, with slides nesting body content by outline level via the readers this change set shipped alongside it (ooxml.js 2.17.0 emits
a:pPr/@lvlaslist.level; odf.js 3.1.0 emitstext:listnesting depth for odp slides; schema 3.3.0 madenumIdoptional so level-only formats need no fabricated identity). Wordprocessing composes both depth signals — heading groups byheadingLevel, list content nesting further bylist.level. PR: ExaDev/document-outline.js#1