document-outline.js 0.0.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joseph Mearman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # document-outline.js
2
+
3
+ [![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&logoColor=white)](https://github.com/ExaDev/document-outline.js) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](https://www.npmjs.com/package/document-outline.js) [![Release](https://img.shields.io/github/v/release/ExaDev/document-outline.js)](https://github.com/ExaDev/document-outline.js/releases/latest) [![CI](https://img.shields.io/github/actions/workflow/status/ExaDev/document-outline.js/ci.yml?branch=main)](https://github.com/ExaDev/document-outline.js/actions)
4
+
5
+ > Utilities for consumers holding a tree-form `DocumentPackage` (document-schema.js 4.0.0) — the table-of-contents projection, effective-property resolution, and the flatten-to-leaves / leaf-text / stable-hash helpers — without importing the producer that made it. The outline package for the [documents.js family](https://github.com/ExaDev). Worker-isomorphic: the same code runs under Node and inside a Cloudflare Workers isolate.
6
+
7
+ Created for [document-schema.js#14](https://github.com/ExaDev/document-schema.js/issues/14): none of the content shapes groups content by heading or list level — a heading paragraph sits in a flat `blocks` array like any other — so every consumer needing a nested tree (chunking a document for retrieval, generating a table of contents, structural diffing) had to rebuild the same nesting transform for itself. This package is that transform, once. It depends only on `document-schema.js` (plus `zod`): it never touches a codec, because it only ever operates on an already-produced package, regardless of which producer made it.
8
+
9
+ [document-schema.js#20](https://github.com/ExaDev/document-schema.js/issues/20) then made the tree the canonical form: since 4.0.0, `DocumentPackage` **is** the tree — a discriminated union of `{ node, children }` group wrappers (`SectionGroupNode`, `SlideGroupNode`, `SheetGroupNode`, `DrawPageGroupNode`, `ShapeGroupNode`, `HeadingGroupNode`, `ListGroupNode`, all imported from `document-schema.js` itself). With the tree vocabulary owned by the schema, this package's phase-1 `decompose`/`flatten` pair — the flat-to-tree transform and its bijection — moved wholesale into [documents.js](https://github.com/ExaDev/documents.js)'s package boundary ([document-outline.js#2](https://github.com/ExaDev/document-outline.js/issues/2), phase 2): one implementation, one authority, no second copy of the grouping semantics here. What remains — and what this major release re-charters the package around — is the artefact-utility surface: everything a consumer holding a serialised tree-form package JSON needs to project, resolve, and hash it, with `document-schema.js` as the only dependency. The removal is the release note: `decompose`, `flatten`, `documentEnvelope`, and the local `PackageNode` types are gone from this package's surface outright, not `@deprecated` — the tree types live in `document-schema.js`, the lossless tree↔flat pair lives in `documents.js`.
10
+
11
+ ## Getting started
12
+
13
+ Requires Node.js `>=20` and pnpm `11.6.0`.
14
+
15
+ ```sh
16
+ pnpm install
17
+ pnpm build # tsdown -> dist/ (ESM + CJS + .d.ts)
18
+ pnpm typecheck # tsc -p tsconfig.json && tsc -p tsconfig.node.json (dual tsconfig)
19
+ pnpm lint # eslint . --fix --cache --max-warnings 0
20
+ pnpm test # vitest run
21
+ pnpm test:watch # vitest
22
+ pnpm test:workers # vitest run --config vitest.workers.config.ts, inside a real Cloudflare Workers (workerd) isolate
23
+ ```
24
+
25
+ To run a single test file, pass its path to vitest directly, e.g. `pnpm exec vitest run src/outline/build.test.ts`.
26
+
27
+ ## What it provides
28
+
29
+ | Module | Exports |
30
+ |---|---|
31
+ | `outline/build` | `buildOutline` (per-kind TOC projection over a `DocumentPackage`) |
32
+ | `outline/effective` | `effectivePackage` (effective-property resolution) |
33
+ | `outline/node` | `OutlineNode`, `OutlineChild`, `OutlineLeaf`, `OutlineNodeSchema`, `isOutlineNode`, `isOutlineChild`, `isOutlineLeaf` |
34
+ | `outline/helpers` | `flattenOutline`, `outlineLeafText`, `leafContentHash` |
35
+
36
+ Every module in the table is re-exported from the package root, so its exports import from `'document-outline.js'` directly. `outline/hash` (the `stableContentHash`/`canonicalise`/`sha256` primitives behind `leafContentHash`'s published recipe) is deliberately not on the root entry — it stays reachable via the `document-outline.js/outline/hash` subpath, keeping the root surface small.
37
+
38
+ `buildOutline(pkg)` dispatches on `pkg.kind` and projects `pkg.children` into the root scope's children — `OutlineChild[]`, an ordered mix of this package's own `OutlineNode` groups and the schema's leaf payloads. The root is deliberately not itself a node (no synthetic "document" group), so a wordprocessing package's pre-heading content — or a package with no grouping signal at all — appears as leaves directly in the returned array.
39
+
40
+ This is the **TOC projection**, not a decomposition, and the difference is the charter: it deliberately re-groups across container boundaries — a wordprocessing package's sections flow into one tree, a slide's paragraphs are taken across its shapes in shape order — which is exactly the lossiness a table of contents wants, and exactly why the lossless container-boundary-respecting pair lives in `documents.js`'s package boundary instead.
41
+
42
+ An `OutlineNode` carries `text` (the group's own label), `level` (its source level signal, verbatim), and `children` (nested groups and leaf payloads in document order). `level` is the source signal, not tree depth: heading groups carry their anchor's `headingLevel` (1-based), list-item groups carry `list.level` (0-based), and the synthetic slide/sheet/page/formula groups are level 1. Render indentation from the nesting, never from `level` — a slide group (level 1) legitimately contains list items at levels 0, 1, 2… on the other scale.
43
+
44
+ ### Per-kind hierarchy
45
+
46
+ | Kind | Groups | Nesting | Leaves |
47
+ |---|---|---|---|
48
+ | wordprocessing | one per heading group, by `headingLevel` | stack semantics; list groups nest inside by `list.level` | non-list blocks, at the current depth |
49
+ | presentation | one per slide group, `Slide N` | slide paragraphs nest by `list.level`, across its shapes | non-paragraph blocks, at the current depth |
50
+ | spreadsheet | one per sheet group, the sheet's name | — | the sheet's images, then its embedded objects |
51
+ | drawing | one per page group, `Page N` | — | the page's shape contents flattened, then its vectors |
52
+ | formula | a single node | — | the `ContentFormula` itself |
53
+
54
+ Both nesting scales follow the same stack semantics, modelled on how Word's navigation pane and PowerPoint's outline view present structure: each new group nests under the deepest open group with a strictly shallower level and pops equal-or-deeper groups closed, so an H4 following an H2 becomes its direct child (no synthetic intermediates) and an H1 after an H3 pops to the root; list groups behave identically on `list.level`'s 0-based scale (a jump from level 0 to level 2 nests directly under the 0). Within a group the two compose: heading groups open scopes, list groups nest inside them, and non-paragraph blocks (tables, images, page breaks, embedded objects) attach as leaves at the current depth without changing it. A paragraph at a leaf position carries neither grouping signal in a well-formed tree, sits flat at its scope, and closes the list nesting — which is what keeps the flattened leaf order identical to document order. `headingLevel` is the only heading signal read — a Heading `styleId` without `headingLevel` does not group — and in presentations it is not read at all: slides have no heading hierarchy of their own, so `list.level` is the only depth signal they carry.
55
+
56
+ Slide and page labels (`Slide 1`, `Page 1`, …) are 1-based, matching the Markdown renderer's own per-slide/per-page heading convention; spreadsheet groups are labelled with the sheet's own name (cells are addressable data, not outline content, and never appear); drawing vectors stay in the tree as textless leaves so structural diffing still sees them.
57
+
58
+ ## Effective properties
59
+
60
+ A tree group may carry a `style` ref into the package's `styles` table ([document-schema.js#21](https://github.com/ExaDev/document-schema.js/issues/21)). `effectivePackage(pkg)` resolves those refs away using document-schema.js's own overlay helpers (`resolveStyleChain`, `applyParagraphStyleProperties`, `applyRunStyleProperties` — the mechanics are the schema's to own, the same single-authority rule that moved the tree vocabulary there) and returns the package with every ref consumed and the styles table dropped:
61
+
62
+ ```ts
63
+ import { effectivePackage } from 'document-outline.js';
64
+
65
+ const resolved = effectivePackage(pkg); // same tree, properties inlined, no styles table
66
+ ```
67
+
68
+ The semantics: a group's ref, plus every ancestor group's ref, overlays onto each paragraph in that group's subtree — group anchors (heading and list groups carry full `ContentParagraph` anchors) and bare paragraph leaves alike — with the chain ordered outermost-first so the nearest group's entry wins over further-out ones, and the paragraph's own direct properties win over everything (the schema's apply helpers fill gaps, never overwrite). The run half of a resolved entry applies to every run of each paragraph it resolved for. The walk's boundary is the block flow: a table leaf's cell paragraphs and an embedded document's own content are leaf-local payload this walk does not rewrite — an embedded document is its own whole document context.
69
+
70
+ Two guarantees worth depending on. First, `effectivePackage(factored)` deep-equals `effectivePackage(unfactored)`: a serialisation that factored properties into style refs and one that inlined them everywhere resolve to the same effective tree, so consumers comparing or hashing content never see the producer's compression choices. To get that property for hashes, resolve first — `leafContentHash` over the leaves of `buildOutline(effectivePackage(pkg))` names the document, not the factoring. Second, resolution runs loudly: a ref the styles table does not carry is malformed, and `resolveStyleChain` throws rather than silently skipping. A styles-free package is returned as the same object — nothing anywhere needs rewriting.
71
+
72
+ ## Helpers
73
+
74
+ ```ts
75
+ import { buildOutline, effectivePackage, flattenOutline, leafContentHash, outlineLeafText } from 'document-outline.js';
76
+
77
+ const outline = buildOutline(pkg); // OutlineChild[] — the TOC projection
78
+ const resolved = effectivePackage(pkg); // style refs consumed, table dropped
79
+ flattenOutline(outline); // every leaf payload, in document order
80
+ outlineLeafText(aLeaf); // the leaf's own text (paragraph runs, table cells,
81
+ // image altText, formula LaTeX; '' for textless leaves)
82
+ leafContentHash(aLeaf); // stable content hash — see the recipe below
83
+ ```
84
+
85
+ Heading and list paragraphs are represented by their group nodes and are not duplicated as leaves, so a tree of groups flattens to the non-paragraph content plus every unlevelled paragraph; a group's own text is always its `text` field. `leafContentHash` hashes the leaf as given and deliberately does not fold style resolution in — a leaf alone does not know its ancestor group refs, so effective-property resolution can only happen with the whole package in hand. The resolve-then-hash route is `effectivePackage(pkg)` first, then hash the resolved leaves; hash the raw leaf only when you truly mean the literal object.
86
+
87
+ ### The hash recipe
88
+
89
+ `leafContentHash` (via `stableContentHash`) is a published contract — changing any step changes every hash ever issued:
90
+
91
+ 1. Strip `$schema` keys recursively from the value (arrays mapped, plain objects rebuilt without the key). Serialised dumps carry a release-pinned `$schema` CDN URI stamped by `document-schema.js`'s serialisation helper; the label is transport metadata about which schema version produced the JSON, not content, and no content field is named `$schema` — so a dump and its parsed-then-rehashed original agree.
92
+ 2. Canonicalise the result: rebuild every plain object with its own keys sorted ascending by UTF-16 code unit (arrays keep their order, primitives pass through) — so independently constructed, structurally identical content is byte-identical from here on regardless of field-construction order.
93
+ 3. `JSON.stringify` the canonicalised value (no spacing; `undefined`-valued optional fields drop out, so "absent" and "explicitly undefined" hash the same).
94
+ 4. UTF-8 encode with `TextEncoder`.
95
+ 5. SHA-256, hand-rolled over `Uint8Array` (Worker-isomorphic; no `node:crypto`, no async `SubtleCrypto`) and pinned against the FIPS 180-4 example vectors in `hash.test.ts`.
96
+ 6. Hex-encode the digest, lowercase.
97
+
98
+ The result is deterministic across processes and platforms, equal exactly when the leaf's content is equal, and different for different content up to SHA-256 collision resistance.
99
+
100
+ ## Where decompose and flatten went
101
+
102
+ The phase-1 `decompose`/`flatten` pair and its property-tested bijection — the lossless tree↔flat transform this package once carried as the vehicle for the `DocumentPackage` promotion — now live in [documents.js](https://github.com/ExaDev/documents.js)'s package boundary. Schema 4.0.0 made `DocumentPackage` itself tree-form, so the grouping semantics have one home next to the codecs that produce and consume packages, and the tree types (`PackageNode`, `PackageGroup`, `SectionGroupNode`, …) import from `document-schema.js`. If you hold a flat `ContentDocument` and need the tree, or need the exact container-boundary-preserving inverse of the TOC projection above, that is documents.js's surface now.
103
+
104
+ ## Conventions
105
+
106
+ - Worker-isomorphic (see the [family-wide convention](https://github.com/ExaDev/documents.js/blob/main/README.md#conventions)): runtime `src/` must not import `node:*`, a bare Node builtin, or use the `Buffer` global — enforced by a `no-restricted-imports`/`no-restricted-globals` ESLint rule and exercised in CI by running a test suite inside an actual `workerd` isolate (`pnpm test:workers`).
107
+ - Only `src/index.ts` may be named `index.*` — a custom ESLint rule (`local/no-non-barrel-index`) rejects any other module using an `index` basename, since that would be a hidden entry point the `exports` map in `package.json` doesn't advertise.
108
+ - `OutlineNodeSchema` follows document-schema.js's `z.custom` hand-written-guard pattern (`ContentBlock` is the precedent): `z.lazy()` collapses recursive schemas' static type to `unknown` in the pinned zod 4, so the recursion lives in a plain function guard instead.
109
+ - Releases are fully automated: a push to `main` runs `semantic-release` in CI, which determines the version from Conventional Commit messages and publishes to npm via OIDC trusted publishing (no local `NPM_TOKEN` needed). There is no manual publish step.
110
+
111
+ ## Install
112
+
113
+ ```sh
114
+ pnpm add document-outline.js
115
+ # or
116
+ npm install document-outline.js
117
+ ```
118
+
119
+ ## License
120
+
121
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,509 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let document_schema_js = require("document-schema.js");
3
+ let zod = require("zod");
4
+ //#region src/outline/build.ts
5
+ function buildOutline(pkg) {
6
+ switch (pkg.kind) {
7
+ case "wordprocessing": return wordprocessingOutline(pkg.children);
8
+ case "presentation": return presentationOutline(pkg.children);
9
+ case "spreadsheet": return spreadsheetOutline(pkg.children);
10
+ case "drawing": return drawingOutline(pkg.children);
11
+ case "formula": return formulaOutline(pkg.children[0]);
12
+ }
13
+ }
14
+ function wordprocessingOutline(sections) {
15
+ const root = [];
16
+ const headingStack = [];
17
+ const listStack = [];
18
+ const headingScope = () => headingStack.at(-1)?.children ?? root;
19
+ const walk = (children) => {
20
+ for (const child of children) if ((0, document_schema_js.isHeadingGroupNode)(child)) {
21
+ listStack.length = 0;
22
+ const level = child.node.headingLevel;
23
+ for (let top = headingStack.at(-1); top !== void 0 && top.level >= level; top = headingStack.at(-1)) headingStack.pop();
24
+ const node = {
25
+ text: paragraphText(child.node),
26
+ level,
27
+ children: []
28
+ };
29
+ const parent = headingStack.at(-1);
30
+ (parent !== void 0 ? parent.children : root).push(node);
31
+ headingStack.push(node);
32
+ walk(child.children);
33
+ } else if ((0, document_schema_js.isListGroupNode)(child)) {
34
+ openListGroup(listStack, headingScope(), paragraphText(child.node), child.node.list.level);
35
+ walk(child.children);
36
+ } else if (child.kind === "paragraph") {
37
+ listStack.length = 0;
38
+ headingScope().push(child);
39
+ } else {
40
+ const parent = listStack.at(-1);
41
+ (parent !== void 0 ? parent.children : headingScope()).push(child);
42
+ }
43
+ };
44
+ for (const section of sections) walk(section.children);
45
+ return root;
46
+ }
47
+ function presentationOutline(slides) {
48
+ return slides.map((slide, index) => {
49
+ const group = {
50
+ text: `Slide ${String(index + 1)}`,
51
+ level: 1,
52
+ children: []
53
+ };
54
+ const listStack = [];
55
+ const walk = (children) => {
56
+ for (const child of children) if ((0, document_schema_js.isListGroupNode)(child)) {
57
+ openListGroup(listStack, group.children, paragraphText(child.node), child.node.list.level);
58
+ walk(child.children);
59
+ } else if (child.kind === "paragraph") {
60
+ listStack.length = 0;
61
+ group.children.push(child);
62
+ } else {
63
+ const parent = listStack.at(-1);
64
+ (parent !== void 0 ? parent.children : group.children).push(child);
65
+ }
66
+ };
67
+ for (const shape of slide.children) walk(shape.children);
68
+ return group;
69
+ });
70
+ }
71
+ function spreadsheetOutline(sheets) {
72
+ return sheets.map((sheet) => ({
73
+ text: sheet.node.name,
74
+ level: 1,
75
+ children: [...sheet.children]
76
+ }));
77
+ }
78
+ function drawingOutline(pages) {
79
+ return pages.map((page, index) => ({
80
+ text: `Page ${String(index + 1)}`,
81
+ level: 1,
82
+ children: page.children.flatMap((child) => (0, document_schema_js.isShapeGroupNode)(child) ? flattenShapeChildren(child) : [child])
83
+ }));
84
+ }
85
+ function flattenShapeChildren(group) {
86
+ return flattenListFlow(group.children);
87
+ }
88
+ function flattenListFlow(children) {
89
+ const leaves = [];
90
+ for (const child of children) if ((0, document_schema_js.isListGroupNode)(child)) leaves.push(child.node, ...flattenListFlow(child.children));
91
+ else leaves.push(child);
92
+ return leaves;
93
+ }
94
+ function formulaOutline(formula) {
95
+ return [{
96
+ text: formula.presentation?.latex ?? "",
97
+ level: 1,
98
+ children: [formula]
99
+ }];
100
+ }
101
+ function openListGroup(listStack, scopeChildren, text, level) {
102
+ for (let top = listStack.at(-1); top !== void 0 && top.level >= level; top = listStack.at(-1)) listStack.pop();
103
+ const node = {
104
+ text,
105
+ level,
106
+ children: []
107
+ };
108
+ const parent = listStack.at(-1);
109
+ (parent !== void 0 ? parent.children : scopeChildren).push(node);
110
+ listStack.push(node);
111
+ }
112
+ function paragraphText(paragraph) {
113
+ return paragraph.runs.map((run) => run.text).join("");
114
+ }
115
+ //#endregion
116
+ //#region src/outline/effective.ts
117
+ function effectivePackage(pkg) {
118
+ const styles = pkg.styles;
119
+ if (styles === void 0) return pkg;
120
+ switch (pkg.kind) {
121
+ case "wordprocessing": return withoutStyles({
122
+ ...pkg,
123
+ children: pkg.children.map((group) => resolveSectionGroup(styles, [], group))
124
+ });
125
+ case "presentation": return withoutStyles({
126
+ ...pkg,
127
+ children: pkg.children.map((group) => resolveSlideGroup(styles, [], group))
128
+ });
129
+ case "spreadsheet": return withoutStyles({
130
+ ...pkg,
131
+ children: pkg.children.map(resolveSheetGroup)
132
+ });
133
+ case "drawing": return withoutStyles({
134
+ ...pkg,
135
+ children: pkg.children.map((group) => resolveDrawPageGroup(styles, [], group))
136
+ });
137
+ case "formula": return withoutStyles({ ...pkg });
138
+ }
139
+ }
140
+ function withoutStyles(pkg) {
141
+ const copy = { ...pkg };
142
+ delete copy.styles;
143
+ return copy;
144
+ }
145
+ function chainWithRef(chain, group) {
146
+ return group.style === void 0 ? chain : [...chain, group.style];
147
+ }
148
+ function resolveSectionGroup(styles, chain, group) {
149
+ const children = resolveSectionChildren(styles, chainWithRef(chain, group), group.children);
150
+ if (group.style === void 0 && children === group.children) return group;
151
+ return {
152
+ node: group.node,
153
+ children
154
+ };
155
+ }
156
+ function resolveSlideGroup(styles, chain, group) {
157
+ const own = chainWithRef(chain, group);
158
+ let changed = group.style !== void 0;
159
+ const children = [];
160
+ for (const shape of group.children) {
161
+ const resolved = resolveShapeGroup(styles, own, shape);
162
+ changed ||= resolved !== shape;
163
+ children.push(resolved);
164
+ }
165
+ if (!changed) return group;
166
+ return {
167
+ node: group.node,
168
+ children
169
+ };
170
+ }
171
+ function resolveSheetGroup(group) {
172
+ return group.style === void 0 ? group : {
173
+ node: group.node,
174
+ children: group.children
175
+ };
176
+ }
177
+ function resolveDrawPageGroup(styles, chain, group) {
178
+ const own = chainWithRef(chain, group);
179
+ let changed = group.style !== void 0;
180
+ const children = [];
181
+ for (const child of group.children) {
182
+ const resolved = isShapeGroup(child) ? resolveShapeGroup(styles, own, child) : child;
183
+ changed ||= resolved !== child;
184
+ children.push(resolved);
185
+ }
186
+ if (!changed) return group;
187
+ return {
188
+ node: group.node,
189
+ children
190
+ };
191
+ }
192
+ function resolveShapeGroup(styles, chain, group) {
193
+ const children = resolveListChildren(styles, chainWithRef(chain, group), group.children);
194
+ if (group.style === void 0 && children === group.children) return group;
195
+ return {
196
+ node: group.node,
197
+ children
198
+ };
199
+ }
200
+ function resolveHeadingGroup(styles, chain, group) {
201
+ const own = chainWithRef(chain, group);
202
+ const entry = own.length > 0 ? (0, document_schema_js.resolveStyleChain)(styles, own) : void 0;
203
+ let anchor = group.node;
204
+ if (entry !== void 0) {
205
+ const applied = applyEntry(entry, group.node);
206
+ assertResolvedHeadingAnchor(applied);
207
+ anchor = applied;
208
+ }
209
+ const children = resolveSectionChildren(styles, own, group.children);
210
+ if (group.style === void 0 && anchor === group.node && children === group.children) return group;
211
+ return {
212
+ node: anchor,
213
+ children
214
+ };
215
+ }
216
+ function resolveListGroup(styles, chain, group) {
217
+ const own = chainWithRef(chain, group);
218
+ const entry = own.length > 0 ? (0, document_schema_js.resolveStyleChain)(styles, own) : void 0;
219
+ let anchor = group.node;
220
+ if (entry !== void 0) {
221
+ const applied = applyEntry(entry, group.node);
222
+ assertResolvedListAnchor(applied);
223
+ anchor = applied;
224
+ }
225
+ const children = resolveListChildren(styles, own, group.children);
226
+ if (group.style === void 0 && anchor === group.node && children === group.children) return group;
227
+ return {
228
+ node: anchor,
229
+ children
230
+ };
231
+ }
232
+ function assertResolvedHeadingAnchor(paragraph) {
233
+ if (paragraph.headingLevel === void 0) throw new Error("effectivePackage: resolution dropped a heading anchor's headingLevel");
234
+ }
235
+ function assertResolvedListAnchor(paragraph) {
236
+ if (paragraph.list === void 0) throw new Error("effectivePackage: resolution dropped a list anchor's list membership");
237
+ }
238
+ function resolveSectionChildren(styles, chain, children) {
239
+ let changed = false;
240
+ const out = [];
241
+ for (const child of children) {
242
+ let resolved;
243
+ if ((0, document_schema_js.isHeadingGroupNode)(child)) resolved = resolveHeadingGroup(styles, chain, child);
244
+ else if ((0, document_schema_js.isListGroupNode)(child)) resolved = resolveListGroup(styles, chain, child);
245
+ else if (child.kind === "paragraph") resolved = resolveParagraphLeaf(styles, chain, child);
246
+ else resolved = child;
247
+ changed ||= resolved !== child;
248
+ out.push(resolved);
249
+ }
250
+ return changed ? out : children;
251
+ }
252
+ function resolveListChildren(styles, chain, children) {
253
+ let changed = false;
254
+ const out = [];
255
+ for (const child of children) {
256
+ let resolved;
257
+ if ((0, document_schema_js.isListGroupNode)(child)) resolved = resolveListGroup(styles, chain, child);
258
+ else if (child.kind === "paragraph") resolved = resolveParagraphLeaf(styles, chain, child);
259
+ else resolved = child;
260
+ changed ||= resolved !== child;
261
+ out.push(resolved);
262
+ }
263
+ return changed ? out : children;
264
+ }
265
+ function resolveParagraphLeaf(styles, chain, leaf) {
266
+ if (chain.length === 0) return leaf;
267
+ return applyEntry((0, document_schema_js.resolveStyleChain)(styles, chain), leaf);
268
+ }
269
+ function applyEntry(entry, paragraph) {
270
+ const withParagraph = (0, document_schema_js.applyParagraphStyleProperties)(entry.paragraph, paragraph);
271
+ const runProperties = entry.run;
272
+ if (runProperties === void 0) return withParagraph;
273
+ return {
274
+ ...withParagraph,
275
+ runs: withParagraph.runs.map((run) => (0, document_schema_js.applyRunStyleProperties)(runProperties, run))
276
+ };
277
+ }
278
+ function isShapeGroup(child) {
279
+ return "node" in child;
280
+ }
281
+ //#endregion
282
+ //#region src/outline/node.ts
283
+ function isOutlineLeaf(value) {
284
+ return (0, document_schema_js.isPackageLeaf)(value);
285
+ }
286
+ function isOutlineChild(value) {
287
+ return isOutlineNode(value) || isOutlineLeaf(value);
288
+ }
289
+ function isOutlineNode(value) {
290
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
291
+ if (!("text" in value) || typeof value.text !== "string") return false;
292
+ if (!("level" in value) || typeof value.level !== "number" || !Number.isFinite(value.level)) return false;
293
+ if (!("children" in value) || !Array.isArray(value.children)) return false;
294
+ return value.children.every(isOutlineChild);
295
+ }
296
+ const OutlineNodeSchema = zod.z.custom(isOutlineNode);
297
+ //#endregion
298
+ //#region src/outline/hash.ts
299
+ function stableContentHash(value) {
300
+ return sha256Hex(new TextEncoder().encode(JSON.stringify(canonicalise(stripSchemaKeys(value)))));
301
+ }
302
+ function isRecord(value) {
303
+ return typeof value === "object" && value !== null && !Array.isArray(value);
304
+ }
305
+ function stripSchemaKeys(value) {
306
+ if (Array.isArray(value)) return value.map(stripSchemaKeys);
307
+ if (isRecord(value)) {
308
+ const stripped = {};
309
+ for (const [key, entry] of Object.entries(value)) {
310
+ if (key === "$schema") continue;
311
+ stripped[key] = stripSchemaKeys(entry);
312
+ }
313
+ return stripped;
314
+ }
315
+ return value;
316
+ }
317
+ function canonicalise(value) {
318
+ if (Array.isArray(value)) return value.map(canonicalise);
319
+ if (isRecord(value)) {
320
+ const sorted = {};
321
+ for (const key of Object.keys(value).sort()) sorted[key] = canonicalise(value[key]);
322
+ return sorted;
323
+ }
324
+ return value;
325
+ }
326
+ function sha256Hex(bytes) {
327
+ const digest = sha256(bytes);
328
+ let hex = "";
329
+ for (const byte of digest) hex += byte.toString(16).padStart(2, "0");
330
+ return hex;
331
+ }
332
+ const K = [
333
+ 1116352408,
334
+ 1899447441,
335
+ 3049323471,
336
+ 3921009573,
337
+ 961987163,
338
+ 1508970993,
339
+ 2453635748,
340
+ 2870763221,
341
+ 3624381080,
342
+ 310598401,
343
+ 607225278,
344
+ 1426881987,
345
+ 1925078388,
346
+ 2162078206,
347
+ 2614888103,
348
+ 3248222580,
349
+ 3835390401,
350
+ 4022224774,
351
+ 264347078,
352
+ 604807628,
353
+ 770255983,
354
+ 1249150122,
355
+ 1555081692,
356
+ 1996064986,
357
+ 2554220882,
358
+ 2821834349,
359
+ 2952996808,
360
+ 3210313671,
361
+ 3336571891,
362
+ 3584528711,
363
+ 113926993,
364
+ 338241895,
365
+ 666307205,
366
+ 773529912,
367
+ 1294757372,
368
+ 1396182291,
369
+ 1695183700,
370
+ 1986661051,
371
+ 2177026350,
372
+ 2456956037,
373
+ 2730485921,
374
+ 2820302411,
375
+ 3259730800,
376
+ 3345764771,
377
+ 3516065817,
378
+ 3600352804,
379
+ 4094571909,
380
+ 275423344,
381
+ 430227734,
382
+ 506948616,
383
+ 659060556,
384
+ 883997877,
385
+ 958139571,
386
+ 1322822218,
387
+ 1537002063,
388
+ 1747873779,
389
+ 1955562222,
390
+ 2024104815,
391
+ 2227730452,
392
+ 2361852424,
393
+ 2428436474,
394
+ 2756734187,
395
+ 3204031479,
396
+ 3329325298
397
+ ];
398
+ function rotr(x, n) {
399
+ return (x >>> n | x << 32 - n) >>> 0;
400
+ }
401
+ function sha256(bytes) {
402
+ const bitLength = bytes.length * 8;
403
+ const paddedLength = (bytes.length + 8 >> 6) + 1 << 6;
404
+ const padded = new Uint8Array(paddedLength);
405
+ padded.set(bytes);
406
+ padded[bytes.length] = 128;
407
+ const view = new DataView(padded.buffer);
408
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296));
409
+ view.setUint32(paddedLength - 4, bitLength >>> 0);
410
+ let h0 = 1779033703;
411
+ let h1 = 3144134277;
412
+ let h2 = 1013904242;
413
+ let h3 = 2773480762;
414
+ let h4 = 1359893119;
415
+ let h5 = 2600822924;
416
+ let h6 = 528734635;
417
+ let h7 = 1541459225;
418
+ const w = /* @__PURE__ */ new Uint32Array(64);
419
+ for (let offset = 0; offset < paddedLength; offset += 64) {
420
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
421
+ for (let i = 16; i < 64; i++) {
422
+ const w15 = w[i - 15];
423
+ const w2 = w[i - 2];
424
+ const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ w15 >>> 3;
425
+ const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ w2 >>> 10;
426
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
427
+ }
428
+ let a = h0;
429
+ let b = h1;
430
+ let c = h2;
431
+ let d = h3;
432
+ let e = h4;
433
+ let f = h5;
434
+ let g = h6;
435
+ let h = h7;
436
+ for (let i = 0; i < 64; i++) {
437
+ const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
438
+ const ch = e & f ^ ~e & g;
439
+ const temp1 = h + s1 + ch + K[i] + w[i] >>> 0;
440
+ const temp2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
441
+ h = g;
442
+ g = f;
443
+ f = e;
444
+ e = d + temp1 >>> 0;
445
+ d = c;
446
+ c = b;
447
+ b = a;
448
+ a = temp1 + temp2 >>> 0;
449
+ }
450
+ h0 = h0 + a >>> 0;
451
+ h1 = h1 + b >>> 0;
452
+ h2 = h2 + c >>> 0;
453
+ h3 = h3 + d >>> 0;
454
+ h4 = h4 + e >>> 0;
455
+ h5 = h5 + f >>> 0;
456
+ h6 = h6 + g >>> 0;
457
+ h7 = h7 + h >>> 0;
458
+ }
459
+ const digest = /* @__PURE__ */ new Uint8Array(32);
460
+ const out = new DataView(digest.buffer);
461
+ out.setUint32(0, h0);
462
+ out.setUint32(4, h1);
463
+ out.setUint32(8, h2);
464
+ out.setUint32(12, h3);
465
+ out.setUint32(16, h4);
466
+ out.setUint32(20, h5);
467
+ out.setUint32(24, h6);
468
+ out.setUint32(28, h7);
469
+ return digest;
470
+ }
471
+ //#endregion
472
+ //#region src/outline/helpers.ts
473
+ function flattenOutline(children) {
474
+ const leaves = [];
475
+ const walk = (subtree) => {
476
+ for (const child of subtree) if (isOutlineNode(child)) walk(child.children);
477
+ else leaves.push(child);
478
+ };
479
+ walk(children);
480
+ return leaves;
481
+ }
482
+ function outlineLeafText(leaf) {
483
+ if ("runs" in leaf) return leaf.runs.map((run) => run.text).join("");
484
+ if ("rows" in leaf) return leaf.rows.map((row) => row.cells.map((cell) => blockTexts(cell.blocks).join(" ")).join(" ")).join("\n");
485
+ if ("base64" in leaf) return leaf.altText ?? "";
486
+ if ("mathml" in leaf) return leaf.presentation?.latex ?? "";
487
+ return "";
488
+ }
489
+ function blockTexts(blocks) {
490
+ const parts = [];
491
+ for (const block of blocks) {
492
+ if (block.kind === "paragraph") parts.push(block.runs.map((run) => run.text).join(""));
493
+ if (block.kind === "table") parts.push(...block.rows.flatMap((row) => row.cells.map((cell) => blockTexts(cell.blocks).join(" "))));
494
+ }
495
+ return parts;
496
+ }
497
+ function leafContentHash(leaf) {
498
+ return stableContentHash(leaf);
499
+ }
500
+ //#endregion
501
+ exports.OutlineNodeSchema = OutlineNodeSchema;
502
+ exports.buildOutline = buildOutline;
503
+ exports.effectivePackage = effectivePackage;
504
+ exports.flattenOutline = flattenOutline;
505
+ exports.isOutlineChild = isOutlineChild;
506
+ exports.isOutlineLeaf = isOutlineLeaf;
507
+ exports.isOutlineNode = isOutlineNode;
508
+ exports.leafContentHash = leafContentHash;
509
+ exports.outlineLeafText = outlineLeafText;
@@ -0,0 +1,27 @@
1
+ import { DocumentPackage, PackageLeaf } from "document-schema.js";
2
+ import { z } from "zod";
3
+ //#region src/outline/node.d.ts
4
+ type OutlineLeaf = PackageLeaf;
5
+ type OutlineChild = OutlineNode | OutlineLeaf;
6
+ interface OutlineNode {
7
+ readonly text: string;
8
+ readonly level: number;
9
+ children: OutlineChild[];
10
+ }
11
+ declare function isOutlineLeaf(value: unknown): value is OutlineLeaf;
12
+ declare function isOutlineChild(value: unknown): value is OutlineChild;
13
+ declare function isOutlineNode(value: unknown): value is OutlineNode;
14
+ declare const OutlineNodeSchema: z.ZodCustom<OutlineNode, OutlineNode>;
15
+ //#endregion
16
+ //#region src/outline/build.d.ts
17
+ declare function buildOutline(pkg: DocumentPackage): OutlineChild[];
18
+ //#endregion
19
+ //#region src/outline/effective.d.ts
20
+ declare function effectivePackage(pkg: DocumentPackage): DocumentPackage;
21
+ //#endregion
22
+ //#region src/outline/helpers.d.ts
23
+ declare function flattenOutline(children: readonly OutlineChild[]): OutlineLeaf[];
24
+ declare function outlineLeafText(leaf: OutlineLeaf): string;
25
+ declare function leafContentHash(leaf: OutlineLeaf): string;
26
+ //#endregion
27
+ export { OutlineChild, OutlineLeaf, OutlineNode, OutlineNodeSchema, buildOutline, effectivePackage, flattenOutline, isOutlineChild, isOutlineLeaf, isOutlineNode, leafContentHash, outlineLeafText };
@@ -0,0 +1,27 @@
1
+ import { DocumentPackage, PackageLeaf } from "document-schema.js";
2
+ import { z } from "zod";
3
+ //#region src/outline/node.d.ts
4
+ type OutlineLeaf = PackageLeaf;
5
+ type OutlineChild = OutlineNode | OutlineLeaf;
6
+ interface OutlineNode {
7
+ readonly text: string;
8
+ readonly level: number;
9
+ children: OutlineChild[];
10
+ }
11
+ declare function isOutlineLeaf(value: unknown): value is OutlineLeaf;
12
+ declare function isOutlineChild(value: unknown): value is OutlineChild;
13
+ declare function isOutlineNode(value: unknown): value is OutlineNode;
14
+ declare const OutlineNodeSchema: z.ZodCustom<OutlineNode, OutlineNode>;
15
+ //#endregion
16
+ //#region src/outline/build.d.ts
17
+ declare function buildOutline(pkg: DocumentPackage): OutlineChild[];
18
+ //#endregion
19
+ //#region src/outline/effective.d.ts
20
+ declare function effectivePackage(pkg: DocumentPackage): DocumentPackage;
21
+ //#endregion
22
+ //#region src/outline/helpers.d.ts
23
+ declare function flattenOutline(children: readonly OutlineChild[]): OutlineLeaf[];
24
+ declare function outlineLeafText(leaf: OutlineLeaf): string;
25
+ declare function leafContentHash(leaf: OutlineLeaf): string;
26
+ //#endregion
27
+ export { OutlineChild, OutlineLeaf, OutlineNode, OutlineNodeSchema, buildOutline, effectivePackage, flattenOutline, isOutlineChild, isOutlineLeaf, isOutlineNode, leafContentHash, outlineLeafText };
package/dist/index.js ADDED
@@ -0,0 +1,500 @@
1
+ import { applyParagraphStyleProperties, applyRunStyleProperties, isHeadingGroupNode, isListGroupNode, isPackageLeaf, isShapeGroupNode, resolveStyleChain } from "document-schema.js";
2
+ import { z } from "zod";
3
+ //#region src/outline/build.ts
4
+ function buildOutline(pkg) {
5
+ switch (pkg.kind) {
6
+ case "wordprocessing": return wordprocessingOutline(pkg.children);
7
+ case "presentation": return presentationOutline(pkg.children);
8
+ case "spreadsheet": return spreadsheetOutline(pkg.children);
9
+ case "drawing": return drawingOutline(pkg.children);
10
+ case "formula": return formulaOutline(pkg.children[0]);
11
+ }
12
+ }
13
+ function wordprocessingOutline(sections) {
14
+ const root = [];
15
+ const headingStack = [];
16
+ const listStack = [];
17
+ const headingScope = () => headingStack.at(-1)?.children ?? root;
18
+ const walk = (children) => {
19
+ for (const child of children) if (isHeadingGroupNode(child)) {
20
+ listStack.length = 0;
21
+ const level = child.node.headingLevel;
22
+ for (let top = headingStack.at(-1); top !== void 0 && top.level >= level; top = headingStack.at(-1)) headingStack.pop();
23
+ const node = {
24
+ text: paragraphText(child.node),
25
+ level,
26
+ children: []
27
+ };
28
+ const parent = headingStack.at(-1);
29
+ (parent !== void 0 ? parent.children : root).push(node);
30
+ headingStack.push(node);
31
+ walk(child.children);
32
+ } else if (isListGroupNode(child)) {
33
+ openListGroup(listStack, headingScope(), paragraphText(child.node), child.node.list.level);
34
+ walk(child.children);
35
+ } else if (child.kind === "paragraph") {
36
+ listStack.length = 0;
37
+ headingScope().push(child);
38
+ } else {
39
+ const parent = listStack.at(-1);
40
+ (parent !== void 0 ? parent.children : headingScope()).push(child);
41
+ }
42
+ };
43
+ for (const section of sections) walk(section.children);
44
+ return root;
45
+ }
46
+ function presentationOutline(slides) {
47
+ return slides.map((slide, index) => {
48
+ const group = {
49
+ text: `Slide ${String(index + 1)}`,
50
+ level: 1,
51
+ children: []
52
+ };
53
+ const listStack = [];
54
+ const walk = (children) => {
55
+ for (const child of children) if (isListGroupNode(child)) {
56
+ openListGroup(listStack, group.children, paragraphText(child.node), child.node.list.level);
57
+ walk(child.children);
58
+ } else if (child.kind === "paragraph") {
59
+ listStack.length = 0;
60
+ group.children.push(child);
61
+ } else {
62
+ const parent = listStack.at(-1);
63
+ (parent !== void 0 ? parent.children : group.children).push(child);
64
+ }
65
+ };
66
+ for (const shape of slide.children) walk(shape.children);
67
+ return group;
68
+ });
69
+ }
70
+ function spreadsheetOutline(sheets) {
71
+ return sheets.map((sheet) => ({
72
+ text: sheet.node.name,
73
+ level: 1,
74
+ children: [...sheet.children]
75
+ }));
76
+ }
77
+ function drawingOutline(pages) {
78
+ return pages.map((page, index) => ({
79
+ text: `Page ${String(index + 1)}`,
80
+ level: 1,
81
+ children: page.children.flatMap((child) => isShapeGroupNode(child) ? flattenShapeChildren(child) : [child])
82
+ }));
83
+ }
84
+ function flattenShapeChildren(group) {
85
+ return flattenListFlow(group.children);
86
+ }
87
+ function flattenListFlow(children) {
88
+ const leaves = [];
89
+ for (const child of children) if (isListGroupNode(child)) leaves.push(child.node, ...flattenListFlow(child.children));
90
+ else leaves.push(child);
91
+ return leaves;
92
+ }
93
+ function formulaOutline(formula) {
94
+ return [{
95
+ text: formula.presentation?.latex ?? "",
96
+ level: 1,
97
+ children: [formula]
98
+ }];
99
+ }
100
+ function openListGroup(listStack, scopeChildren, text, level) {
101
+ for (let top = listStack.at(-1); top !== void 0 && top.level >= level; top = listStack.at(-1)) listStack.pop();
102
+ const node = {
103
+ text,
104
+ level,
105
+ children: []
106
+ };
107
+ const parent = listStack.at(-1);
108
+ (parent !== void 0 ? parent.children : scopeChildren).push(node);
109
+ listStack.push(node);
110
+ }
111
+ function paragraphText(paragraph) {
112
+ return paragraph.runs.map((run) => run.text).join("");
113
+ }
114
+ //#endregion
115
+ //#region src/outline/effective.ts
116
+ function effectivePackage(pkg) {
117
+ const styles = pkg.styles;
118
+ if (styles === void 0) return pkg;
119
+ switch (pkg.kind) {
120
+ case "wordprocessing": return withoutStyles({
121
+ ...pkg,
122
+ children: pkg.children.map((group) => resolveSectionGroup(styles, [], group))
123
+ });
124
+ case "presentation": return withoutStyles({
125
+ ...pkg,
126
+ children: pkg.children.map((group) => resolveSlideGroup(styles, [], group))
127
+ });
128
+ case "spreadsheet": return withoutStyles({
129
+ ...pkg,
130
+ children: pkg.children.map(resolveSheetGroup)
131
+ });
132
+ case "drawing": return withoutStyles({
133
+ ...pkg,
134
+ children: pkg.children.map((group) => resolveDrawPageGroup(styles, [], group))
135
+ });
136
+ case "formula": return withoutStyles({ ...pkg });
137
+ }
138
+ }
139
+ function withoutStyles(pkg) {
140
+ const copy = { ...pkg };
141
+ delete copy.styles;
142
+ return copy;
143
+ }
144
+ function chainWithRef(chain, group) {
145
+ return group.style === void 0 ? chain : [...chain, group.style];
146
+ }
147
+ function resolveSectionGroup(styles, chain, group) {
148
+ const children = resolveSectionChildren(styles, chainWithRef(chain, group), group.children);
149
+ if (group.style === void 0 && children === group.children) return group;
150
+ return {
151
+ node: group.node,
152
+ children
153
+ };
154
+ }
155
+ function resolveSlideGroup(styles, chain, group) {
156
+ const own = chainWithRef(chain, group);
157
+ let changed = group.style !== void 0;
158
+ const children = [];
159
+ for (const shape of group.children) {
160
+ const resolved = resolveShapeGroup(styles, own, shape);
161
+ changed ||= resolved !== shape;
162
+ children.push(resolved);
163
+ }
164
+ if (!changed) return group;
165
+ return {
166
+ node: group.node,
167
+ children
168
+ };
169
+ }
170
+ function resolveSheetGroup(group) {
171
+ return group.style === void 0 ? group : {
172
+ node: group.node,
173
+ children: group.children
174
+ };
175
+ }
176
+ function resolveDrawPageGroup(styles, chain, group) {
177
+ const own = chainWithRef(chain, group);
178
+ let changed = group.style !== void 0;
179
+ const children = [];
180
+ for (const child of group.children) {
181
+ const resolved = isShapeGroup(child) ? resolveShapeGroup(styles, own, child) : child;
182
+ changed ||= resolved !== child;
183
+ children.push(resolved);
184
+ }
185
+ if (!changed) return group;
186
+ return {
187
+ node: group.node,
188
+ children
189
+ };
190
+ }
191
+ function resolveShapeGroup(styles, chain, group) {
192
+ const children = resolveListChildren(styles, chainWithRef(chain, group), group.children);
193
+ if (group.style === void 0 && children === group.children) return group;
194
+ return {
195
+ node: group.node,
196
+ children
197
+ };
198
+ }
199
+ function resolveHeadingGroup(styles, chain, group) {
200
+ const own = chainWithRef(chain, group);
201
+ const entry = own.length > 0 ? resolveStyleChain(styles, own) : void 0;
202
+ let anchor = group.node;
203
+ if (entry !== void 0) {
204
+ const applied = applyEntry(entry, group.node);
205
+ assertResolvedHeadingAnchor(applied);
206
+ anchor = applied;
207
+ }
208
+ const children = resolveSectionChildren(styles, own, group.children);
209
+ if (group.style === void 0 && anchor === group.node && children === group.children) return group;
210
+ return {
211
+ node: anchor,
212
+ children
213
+ };
214
+ }
215
+ function resolveListGroup(styles, chain, group) {
216
+ const own = chainWithRef(chain, group);
217
+ const entry = own.length > 0 ? resolveStyleChain(styles, own) : void 0;
218
+ let anchor = group.node;
219
+ if (entry !== void 0) {
220
+ const applied = applyEntry(entry, group.node);
221
+ assertResolvedListAnchor(applied);
222
+ anchor = applied;
223
+ }
224
+ const children = resolveListChildren(styles, own, group.children);
225
+ if (group.style === void 0 && anchor === group.node && children === group.children) return group;
226
+ return {
227
+ node: anchor,
228
+ children
229
+ };
230
+ }
231
+ function assertResolvedHeadingAnchor(paragraph) {
232
+ if (paragraph.headingLevel === void 0) throw new Error("effectivePackage: resolution dropped a heading anchor's headingLevel");
233
+ }
234
+ function assertResolvedListAnchor(paragraph) {
235
+ if (paragraph.list === void 0) throw new Error("effectivePackage: resolution dropped a list anchor's list membership");
236
+ }
237
+ function resolveSectionChildren(styles, chain, children) {
238
+ let changed = false;
239
+ const out = [];
240
+ for (const child of children) {
241
+ let resolved;
242
+ if (isHeadingGroupNode(child)) resolved = resolveHeadingGroup(styles, chain, child);
243
+ else if (isListGroupNode(child)) resolved = resolveListGroup(styles, chain, child);
244
+ else if (child.kind === "paragraph") resolved = resolveParagraphLeaf(styles, chain, child);
245
+ else resolved = child;
246
+ changed ||= resolved !== child;
247
+ out.push(resolved);
248
+ }
249
+ return changed ? out : children;
250
+ }
251
+ function resolveListChildren(styles, chain, children) {
252
+ let changed = false;
253
+ const out = [];
254
+ for (const child of children) {
255
+ let resolved;
256
+ if (isListGroupNode(child)) resolved = resolveListGroup(styles, chain, child);
257
+ else if (child.kind === "paragraph") resolved = resolveParagraphLeaf(styles, chain, child);
258
+ else resolved = child;
259
+ changed ||= resolved !== child;
260
+ out.push(resolved);
261
+ }
262
+ return changed ? out : children;
263
+ }
264
+ function resolveParagraphLeaf(styles, chain, leaf) {
265
+ if (chain.length === 0) return leaf;
266
+ return applyEntry(resolveStyleChain(styles, chain), leaf);
267
+ }
268
+ function applyEntry(entry, paragraph) {
269
+ const withParagraph = applyParagraphStyleProperties(entry.paragraph, paragraph);
270
+ const runProperties = entry.run;
271
+ if (runProperties === void 0) return withParagraph;
272
+ return {
273
+ ...withParagraph,
274
+ runs: withParagraph.runs.map((run) => applyRunStyleProperties(runProperties, run))
275
+ };
276
+ }
277
+ function isShapeGroup(child) {
278
+ return "node" in child;
279
+ }
280
+ //#endregion
281
+ //#region src/outline/node.ts
282
+ function isOutlineLeaf(value) {
283
+ return isPackageLeaf(value);
284
+ }
285
+ function isOutlineChild(value) {
286
+ return isOutlineNode(value) || isOutlineLeaf(value);
287
+ }
288
+ function isOutlineNode(value) {
289
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
290
+ if (!("text" in value) || typeof value.text !== "string") return false;
291
+ if (!("level" in value) || typeof value.level !== "number" || !Number.isFinite(value.level)) return false;
292
+ if (!("children" in value) || !Array.isArray(value.children)) return false;
293
+ return value.children.every(isOutlineChild);
294
+ }
295
+ const OutlineNodeSchema = z.custom(isOutlineNode);
296
+ //#endregion
297
+ //#region src/outline/hash.ts
298
+ function stableContentHash(value) {
299
+ return sha256Hex(new TextEncoder().encode(JSON.stringify(canonicalise(stripSchemaKeys(value)))));
300
+ }
301
+ function isRecord(value) {
302
+ return typeof value === "object" && value !== null && !Array.isArray(value);
303
+ }
304
+ function stripSchemaKeys(value) {
305
+ if (Array.isArray(value)) return value.map(stripSchemaKeys);
306
+ if (isRecord(value)) {
307
+ const stripped = {};
308
+ for (const [key, entry] of Object.entries(value)) {
309
+ if (key === "$schema") continue;
310
+ stripped[key] = stripSchemaKeys(entry);
311
+ }
312
+ return stripped;
313
+ }
314
+ return value;
315
+ }
316
+ function canonicalise(value) {
317
+ if (Array.isArray(value)) return value.map(canonicalise);
318
+ if (isRecord(value)) {
319
+ const sorted = {};
320
+ for (const key of Object.keys(value).sort()) sorted[key] = canonicalise(value[key]);
321
+ return sorted;
322
+ }
323
+ return value;
324
+ }
325
+ function sha256Hex(bytes) {
326
+ const digest = sha256(bytes);
327
+ let hex = "";
328
+ for (const byte of digest) hex += byte.toString(16).padStart(2, "0");
329
+ return hex;
330
+ }
331
+ const K = [
332
+ 1116352408,
333
+ 1899447441,
334
+ 3049323471,
335
+ 3921009573,
336
+ 961987163,
337
+ 1508970993,
338
+ 2453635748,
339
+ 2870763221,
340
+ 3624381080,
341
+ 310598401,
342
+ 607225278,
343
+ 1426881987,
344
+ 1925078388,
345
+ 2162078206,
346
+ 2614888103,
347
+ 3248222580,
348
+ 3835390401,
349
+ 4022224774,
350
+ 264347078,
351
+ 604807628,
352
+ 770255983,
353
+ 1249150122,
354
+ 1555081692,
355
+ 1996064986,
356
+ 2554220882,
357
+ 2821834349,
358
+ 2952996808,
359
+ 3210313671,
360
+ 3336571891,
361
+ 3584528711,
362
+ 113926993,
363
+ 338241895,
364
+ 666307205,
365
+ 773529912,
366
+ 1294757372,
367
+ 1396182291,
368
+ 1695183700,
369
+ 1986661051,
370
+ 2177026350,
371
+ 2456956037,
372
+ 2730485921,
373
+ 2820302411,
374
+ 3259730800,
375
+ 3345764771,
376
+ 3516065817,
377
+ 3600352804,
378
+ 4094571909,
379
+ 275423344,
380
+ 430227734,
381
+ 506948616,
382
+ 659060556,
383
+ 883997877,
384
+ 958139571,
385
+ 1322822218,
386
+ 1537002063,
387
+ 1747873779,
388
+ 1955562222,
389
+ 2024104815,
390
+ 2227730452,
391
+ 2361852424,
392
+ 2428436474,
393
+ 2756734187,
394
+ 3204031479,
395
+ 3329325298
396
+ ];
397
+ function rotr(x, n) {
398
+ return (x >>> n | x << 32 - n) >>> 0;
399
+ }
400
+ function sha256(bytes) {
401
+ const bitLength = bytes.length * 8;
402
+ const paddedLength = (bytes.length + 8 >> 6) + 1 << 6;
403
+ const padded = new Uint8Array(paddedLength);
404
+ padded.set(bytes);
405
+ padded[bytes.length] = 128;
406
+ const view = new DataView(padded.buffer);
407
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296));
408
+ view.setUint32(paddedLength - 4, bitLength >>> 0);
409
+ let h0 = 1779033703;
410
+ let h1 = 3144134277;
411
+ let h2 = 1013904242;
412
+ let h3 = 2773480762;
413
+ let h4 = 1359893119;
414
+ let h5 = 2600822924;
415
+ let h6 = 528734635;
416
+ let h7 = 1541459225;
417
+ const w = /* @__PURE__ */ new Uint32Array(64);
418
+ for (let offset = 0; offset < paddedLength; offset += 64) {
419
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
420
+ for (let i = 16; i < 64; i++) {
421
+ const w15 = w[i - 15];
422
+ const w2 = w[i - 2];
423
+ const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ w15 >>> 3;
424
+ const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ w2 >>> 10;
425
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
426
+ }
427
+ let a = h0;
428
+ let b = h1;
429
+ let c = h2;
430
+ let d = h3;
431
+ let e = h4;
432
+ let f = h5;
433
+ let g = h6;
434
+ let h = h7;
435
+ for (let i = 0; i < 64; i++) {
436
+ const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
437
+ const ch = e & f ^ ~e & g;
438
+ const temp1 = h + s1 + ch + K[i] + w[i] >>> 0;
439
+ const temp2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
440
+ h = g;
441
+ g = f;
442
+ f = e;
443
+ e = d + temp1 >>> 0;
444
+ d = c;
445
+ c = b;
446
+ b = a;
447
+ a = temp1 + temp2 >>> 0;
448
+ }
449
+ h0 = h0 + a >>> 0;
450
+ h1 = h1 + b >>> 0;
451
+ h2 = h2 + c >>> 0;
452
+ h3 = h3 + d >>> 0;
453
+ h4 = h4 + e >>> 0;
454
+ h5 = h5 + f >>> 0;
455
+ h6 = h6 + g >>> 0;
456
+ h7 = h7 + h >>> 0;
457
+ }
458
+ const digest = /* @__PURE__ */ new Uint8Array(32);
459
+ const out = new DataView(digest.buffer);
460
+ out.setUint32(0, h0);
461
+ out.setUint32(4, h1);
462
+ out.setUint32(8, h2);
463
+ out.setUint32(12, h3);
464
+ out.setUint32(16, h4);
465
+ out.setUint32(20, h5);
466
+ out.setUint32(24, h6);
467
+ out.setUint32(28, h7);
468
+ return digest;
469
+ }
470
+ //#endregion
471
+ //#region src/outline/helpers.ts
472
+ function flattenOutline(children) {
473
+ const leaves = [];
474
+ const walk = (subtree) => {
475
+ for (const child of subtree) if (isOutlineNode(child)) walk(child.children);
476
+ else leaves.push(child);
477
+ };
478
+ walk(children);
479
+ return leaves;
480
+ }
481
+ function outlineLeafText(leaf) {
482
+ if ("runs" in leaf) return leaf.runs.map((run) => run.text).join("");
483
+ if ("rows" in leaf) return leaf.rows.map((row) => row.cells.map((cell) => blockTexts(cell.blocks).join(" ")).join(" ")).join("\n");
484
+ if ("base64" in leaf) return leaf.altText ?? "";
485
+ if ("mathml" in leaf) return leaf.presentation?.latex ?? "";
486
+ return "";
487
+ }
488
+ function blockTexts(blocks) {
489
+ const parts = [];
490
+ for (const block of blocks) {
491
+ if (block.kind === "paragraph") parts.push(block.runs.map((run) => run.text).join(""));
492
+ if (block.kind === "table") parts.push(...block.rows.flatMap((row) => row.cells.map((cell) => blockTexts(cell.blocks).join(" "))));
493
+ }
494
+ return parts;
495
+ }
496
+ function leafContentHash(leaf) {
497
+ return stableContentHash(leaf);
498
+ }
499
+ //#endregion
500
+ export { OutlineNodeSchema, buildOutline, effectivePackage, flattenOutline, isOutlineChild, isOutlineLeaf, isOutlineNode, leafContentHash, outlineLeafText };
package/package.json CHANGED
@@ -1,5 +1,93 @@
1
1
  {
2
2
  "name": "document-outline.js",
3
- "version": "0.0.0",
4
- "private": false
3
+ "version": "1.0.0",
4
+ "description": "Utilities for consumers holding a tree-form DocumentPackage - the TOC outline projection, effective-property resolution, and flatten/leaf-text/stable-hash helpers, the outline package for the documents.js family.",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ExaDev/document-outline.js.git"
9
+ },
10
+ "homepage": "https://github.com/ExaDev/document-outline.js#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/ExaDev/document-outline.js/issues"
13
+ },
14
+ "license": "MIT",
15
+ "exports": {
16
+ ".": {
17
+ "types": {
18
+ "import": "./dist/index.d.ts",
19
+ "require": "./dist/index.d.cts"
20
+ },
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs"
23
+ },
24
+ "./*": {
25
+ "types": {
26
+ "import": "./dist/*.d.ts",
27
+ "require": "./dist/*.d.cts"
28
+ },
29
+ "import": "./dist/*.js",
30
+ "require": "./dist/*.cjs"
31
+ }
32
+ },
33
+ "main": "./dist/index.cjs",
34
+ "module": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "provenance": true,
42
+ "registry": "https://registry.npmjs.org/"
43
+ },
44
+ "sideEffects": false,
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "scripts": {
49
+ "build": "turbo run _build",
50
+ "_build": "tsdown",
51
+ "prepublishOnly": "pnpm run lint && pnpm run typecheck && tsdown && publint && attw --pack",
52
+ "lint": "turbo run _lint",
53
+ "_lint": "eslint . --fix --cache --max-warnings 0",
54
+ "lint:check": "eslint . --max-warnings 0",
55
+ "typecheck": "turbo run _typecheck",
56
+ "_typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.node.json",
57
+ "test": "turbo run _test",
58
+ "_test": "vitest run",
59
+ "test:watch": "vitest",
60
+ "test:workers": "turbo run _test:workers",
61
+ "_test:workers": "vitest run --config vitest.workers.config.ts",
62
+ "prepare": "husky"
63
+ },
64
+ "packageManager": "pnpm@11.6.0",
65
+ "dependencies": {
66
+ "document-schema.js": "^4.0.0",
67
+ "zod": "^4.4.3"
68
+ },
69
+ "devDependencies": {
70
+ "@arethetypeswrong/cli": "^0.18.5",
71
+ "@cloudflare/vitest-pool-workers": "^0.20.1",
72
+ "@commitlint/cli": "^21.2.1",
73
+ "@commitlint/config-conventional": "^21.2.0",
74
+ "@eslint/js": "^10.0.1",
75
+ "@semantic-release/changelog": "^7.0.0",
76
+ "@semantic-release/git": "^11.0.1",
77
+ "@types/node": "^26.1.2",
78
+ "eslint": "^10.8.0",
79
+ "globals": "^17.8.0",
80
+ "husky": "^9.1.7",
81
+ "lint-staged": "^17.2.0",
82
+ "publint": "^0.3.21",
83
+ "semantic-release": "^25.0.8",
84
+ "tsdown": "^0.22.13",
85
+ "turbo": "^2.10.8",
86
+ "typescript": "^6.0.3",
87
+ "typescript-eslint": "^8.65.0",
88
+ "vitest": "^4.1.10"
89
+ },
90
+ "lint-staged": {
91
+ "*.ts": "eslint --fix"
92
+ }
5
93
  }