Skip to content
This repository was archived by the owner on Aug 20, 2026. It is now read-only.
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

Closed
ExaDev/document-outline.js
#1

Description

@Mearman

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.

Activity

  1. added theissue type on Aug 17, 2026
  2. Mearman commented on Aug 17, 2026

    @Mearman
    MemberAuthor

    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.

  3. Mearman commented on Aug 17, 2026

    @Mearman
    MemberAuthor

    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:

    1. 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.
    2. 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).
    3. 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).
    4. 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.
  4. Mearman commented on Aug 17, 2026

    @Mearman
    MemberAuthor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions