@beyondwork/docx-react-component 1.0.106 → 1.0.109

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.
Files changed (190) hide show
  1. package/package.json +19 -5
  2. package/src/api/geometry-overlay-rects.ts +5 -0
  3. package/src/api/package-version.ts +1 -1
  4. package/src/api/page-anchor-id.ts +5 -0
  5. package/src/api/public-types.ts +16 -9
  6. package/src/api/table-node-specs.ts +6 -0
  7. package/src/api/v3/_create.ts +2 -1
  8. package/src/api/v3/_page-anchor-id.ts +52 -0
  9. package/src/api/v3/_runtime-handle.ts +92 -1
  10. package/src/api/v3/ai/_audit-time.ts +5 -0
  11. package/src/api/v3/ai/_pe2-evidence.ts +38 -0
  12. package/src/api/v3/ai/attach.ts +7 -2
  13. package/src/api/v3/ai/replacement.ts +101 -18
  14. package/src/api/v3/ai/resolve.ts +2 -2
  15. package/src/api/v3/ai/review.ts +177 -3
  16. package/src/api/v3/index.ts +1 -0
  17. package/src/api/v3/runtime/collab.ts +462 -0
  18. package/src/api/v3/runtime/document.ts +503 -20
  19. package/src/api/v3/runtime/geometry.ts +97 -0
  20. package/src/api/v3/runtime/layout.ts +744 -0
  21. package/src/api/v3/runtime/perf-probe.ts +14 -0
  22. package/src/api/v3/runtime/viewport.ts +9 -8
  23. package/src/api/v3/ui/_types.ts +149 -55
  24. package/src/api/v3/ui/chrome-preset-model.ts +5 -5
  25. package/src/api/v3/ui/debug.ts +115 -2
  26. package/src/api/v3/ui/index.ts +13 -0
  27. package/src/api/v3/ui/overlays.ts +0 -8
  28. package/src/api/v3/ui/surface.ts +56 -0
  29. package/src/api/v3/ui/viewport.ts +22 -9
  30. package/src/core/commands/image-commands.ts +1 -0
  31. package/src/core/commands/index.ts +6 -0
  32. package/src/core/schema/text-schema.ts +43 -5
  33. package/src/core/selection/mapping.ts +8 -1
  34. package/src/core/selection/review-anchors.ts +5 -1
  35. package/src/core/state/text-transaction.ts +8 -2
  36. package/src/io/export/serialize-revisions.ts +149 -1
  37. package/src/io/normalize/normalize-text.ts +6 -0
  38. package/src/io/ooxml/parse-bookmark-references.ts +55 -0
  39. package/src/io/ooxml/parse-fields.ts +24 -2
  40. package/src/io/ooxml/parse-headers-footers.ts +38 -5
  41. package/src/io/ooxml/parse-main-document.ts +153 -9
  42. package/src/io/ooxml/parse-numbering.ts +20 -0
  43. package/src/io/ooxml/parse-revisions.ts +19 -8
  44. package/src/io/opc/package-reader.ts +98 -8
  45. package/src/model/anchor.ts +4 -3
  46. package/src/model/canonical-document.ts +220 -2
  47. package/src/model/canonical-hash.ts +221 -0
  48. package/src/model/canonical-layout-inputs.ts +245 -6
  49. package/src/model/layout/index.ts +1 -0
  50. package/src/model/layout/page-graph-types.ts +118 -1
  51. package/src/model/review/revision-types.ts +14 -3
  52. package/src/preservation/store.ts +20 -4
  53. package/src/review/README.md +1 -1
  54. package/src/review/store/revision-actions.ts +14 -2
  55. package/src/runtime/collab/event-types.ts +67 -1
  56. package/src/runtime/collab/runtime-collab-sync.ts +177 -5
  57. package/src/runtime/diagnostics/layout-guard-warning.ts +18 -0
  58. package/src/runtime/document-heading-outline.ts +147 -0
  59. package/src/runtime/document-navigation.ts +8 -243
  60. package/src/runtime/document-runtime.ts +240 -97
  61. package/src/runtime/edit-dispatch/dispatch-text-command.ts +11 -0
  62. package/src/runtime/formatting/layout-inputs.ts +38 -5
  63. package/src/runtime/formatting/numbering/geometry.ts +28 -2
  64. package/src/runtime/geometry/adjacent-geometry-intake.ts +835 -0
  65. package/src/runtime/geometry/caret-geometry.ts +5 -6
  66. package/src/runtime/geometry/geometry-facet.ts +60 -10
  67. package/src/runtime/geometry/geometry-index.ts +591 -20
  68. package/src/runtime/geometry/geometry-types.ts +59 -0
  69. package/src/runtime/geometry/hit-test.ts +11 -1
  70. package/src/runtime/geometry/overlay-rects.ts +5 -3
  71. package/src/runtime/geometry/project-anchors.ts +1 -1
  72. package/src/runtime/geometry/word-layout-v2-line-intake.ts +323 -0
  73. package/src/runtime/layout/index.ts +6 -0
  74. package/src/runtime/layout/layout-engine-instance.ts +6 -1
  75. package/src/runtime/layout/layout-engine-version.ts +181 -16
  76. package/src/runtime/layout/layout-facet-types.ts +6 -0
  77. package/src/runtime/layout/page-graph.ts +21 -4
  78. package/src/runtime/layout/paginated-layout-engine.ts +139 -15
  79. package/src/runtime/layout/project-block-fragments.ts +265 -7
  80. package/src/runtime/layout/public-facet.ts +78 -24
  81. package/src/runtime/layout/table-row-continuation-contract.ts +107 -0
  82. package/src/runtime/layout/table-row-split.ts +92 -35
  83. package/src/runtime/prerender/cache-envelope.ts +2 -2
  84. package/src/runtime/prerender/cache-key.ts +5 -4
  85. package/src/runtime/prerender/customxml-cache.ts +0 -1
  86. package/src/runtime/render/render-kernel.ts +1 -1
  87. package/src/runtime/revision-runtime.ts +112 -10
  88. package/src/runtime/scopes/_scope-dependencies.ts +1 -0
  89. package/src/runtime/scopes/action-validation.ts +22 -2
  90. package/src/runtime/scopes/capabilities.ts +316 -0
  91. package/src/runtime/scopes/compile-scope-bundle.ts +14 -0
  92. package/src/runtime/scopes/compiler-service.ts +108 -4
  93. package/src/runtime/scopes/content-control-evidence.ts +79 -0
  94. package/src/runtime/scopes/create-issue.ts +5 -5
  95. package/src/runtime/scopes/evidence.ts +91 -0
  96. package/src/runtime/scopes/formatting/apply.ts +2 -0
  97. package/src/runtime/scopes/geometry-evidence.ts +130 -0
  98. package/src/runtime/scopes/index.ts +54 -0
  99. package/src/runtime/scopes/issue-lifecycle.ts +224 -0
  100. package/src/runtime/scopes/layout-evidence.ts +374 -0
  101. package/src/runtime/scopes/multi-paragraph-refusal.ts +37 -0
  102. package/src/runtime/scopes/preservation-boundary.ts +7 -1
  103. package/src/runtime/scopes/replacement/apply.ts +97 -34
  104. package/src/runtime/scopes/scope-kinds/paragraph.ts +108 -12
  105. package/src/runtime/scopes/semantic-scope-types.ts +242 -3
  106. package/src/runtime/scopes/visualization.ts +28 -0
  107. package/src/runtime/surface-projection.ts +44 -5
  108. package/src/runtime/telemetry/perf-probe.ts +216 -0
  109. package/src/runtime/virtualized-rendering.ts +36 -1
  110. package/src/runtime/workflow/ai-issue-lifecycle.ts +253 -0
  111. package/src/runtime/workflow/coordinator.ts +39 -11
  112. package/src/runtime/workflow/derived-scope-resolver.ts +63 -9
  113. package/src/runtime/workflow/index.ts +3 -0
  114. package/src/runtime/workflow/overlay-lane-types.ts +58 -0
  115. package/src/runtime/workflow/overlay-lanes.ts +168 -10
  116. package/src/runtime/workflow/overlay-store.ts +2 -2
  117. package/src/runtime/workflow/redline-posture-calibration.ts +257 -0
  118. package/src/runtime/workflow/word-field-matrix-calibration.ts +231 -0
  119. package/src/session/_sync-legacy.ts +17 -27
  120. package/src/session/import/loader.ts +6 -4
  121. package/src/session/import/source-package-evidence.ts +186 -2
  122. package/src/session/index.ts +5 -6
  123. package/src/session/session.ts +30 -56
  124. package/src/session/types.ts +8 -13
  125. package/src/shell/session-bootstrap.ts +155 -81
  126. package/src/ui/WordReviewEditor.tsx +520 -12
  127. package/src/ui/editor-shell-view.tsx +14 -4
  128. package/src/ui/editor-surface-controller.tsx +5 -3
  129. package/src/ui/headless/selection-tool-resolver.ts +1 -2
  130. package/src/ui/presence-overlay-lane.ts +0 -1
  131. package/src/ui/ui-controller-factory.ts +7 -0
  132. package/src/ui-tailwind/chrome/build-context-menu-entries.ts +5 -1
  133. package/src/ui-tailwind/chrome/editor-action-registry.ts +105 -5
  134. package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +7 -0
  135. package/src/ui-tailwind/chrome/layer-debug-contracts.ts +208 -0
  136. package/src/ui-tailwind/chrome/resolve-target-kind.ts +13 -0
  137. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +11 -3
  138. package/src/ui-tailwind/chrome/tw-command-palette.tsx +36 -6
  139. package/src/ui-tailwind/chrome/tw-context-menu.tsx +6 -1
  140. package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +42 -109
  141. package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +26 -6
  142. package/src/ui-tailwind/chrome/tw-navigation-command-bar.tsx +328 -0
  143. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +8 -4
  144. package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +129 -1
  145. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +19 -5
  146. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
  147. package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +28 -12
  148. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +30 -3
  149. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +116 -10
  150. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +223 -94
  151. package/src/ui-tailwind/chrome-overlay/tw-presence-overlay-lane.tsx +157 -0
  152. package/src/ui-tailwind/chrome-overlay/tw-review-overlay-lane-markers.tsx +259 -0
  153. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +5 -2
  154. package/src/ui-tailwind/chrome-overlay/tw-substrate-overlay-lanes.tsx +314 -0
  155. package/src/ui-tailwind/debug/README.md +4 -1
  156. package/src/ui-tailwind/debug/layer11-consumer-readiness.ts +272 -0
  157. package/src/ui-tailwind/debug/layer11-word-field-matrix-evidence.ts +160 -0
  158. package/src/ui-tailwind/editor-surface/perf-probe.ts +14 -215
  159. package/src/ui-tailwind/editor-surface/pm-decorations.ts +42 -0
  160. package/src/ui-tailwind/editor-surface/pm-position-map.ts +38 -2
  161. package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -4
  162. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +34 -5
  163. package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +9 -19
  164. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -2
  165. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +145 -0
  166. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +16 -11
  167. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +8 -10
  168. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +3 -0
  169. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +4 -2
  170. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +60 -20
  171. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +16 -11
  172. package/src/ui-tailwind/review/tw-health-panel.tsx +36 -17
  173. package/src/ui-tailwind/review/tw-review-rail.tsx +7 -4
  174. package/src/ui-tailwind/review-workspace/diagnostics-visibility.ts +44 -0
  175. package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +11 -0
  176. package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +16 -1
  177. package/src/ui-tailwind/review-workspace/types.ts +26 -12
  178. package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +40 -11
  179. package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +2 -1
  180. package/src/ui-tailwind/review-workspace/use-page-markers.ts +15 -26
  181. package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +35 -18
  182. package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +41 -32
  183. package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +2 -1
  184. package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +2 -1
  185. package/src/ui-tailwind/status/tw-status-bar.tsx +6 -5
  186. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +52 -80
  187. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +12 -48
  188. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +9 -4
  189. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +328 -361
  190. package/src/ui-tailwind/tw-review-workspace.tsx +152 -286
@@ -0,0 +1,147 @@
1
+ import type {
2
+ DocumentHeadingSnapshot,
3
+ DocumentPageSnapshot,
4
+ EditorSurfaceSnapshot,
5
+ SurfaceBlockSnapshot,
6
+ } from "../api/public-types";
7
+ import type { CanonicalDocumentEnvelope } from "../core/state/editor-state.ts";
8
+ import {
9
+ findSectionForPosition,
10
+ type ResolvedDocumentSection,
11
+ } from "./document-layout.ts";
12
+
13
+ /** Style IDs that map to heading levels by convention. */
14
+ const HEADING_STYLE_PATTERN = /^heading\s*(\d+)$/i;
15
+
16
+ function headingLevelFromStyleId(styleId?: string): number | null {
17
+ if (!styleId) return null;
18
+ const match = HEADING_STYLE_PATTERN.exec(styleId);
19
+ if (match) {
20
+ const level = parseInt(match[1]!, 10);
21
+ return level >= 1 && level <= 9 ? level : null;
22
+ }
23
+ return null;
24
+ }
25
+
26
+ export function buildHeadingOutline(
27
+ document: CanonicalDocumentEnvelope,
28
+ mainSurface: EditorSurfaceSnapshot,
29
+ sections: ResolvedDocumentSection[],
30
+ pages: DocumentPageSnapshot[],
31
+ ): DocumentHeadingSnapshot[] {
32
+ const headings: DocumentHeadingSnapshot[] = [];
33
+
34
+ for (const block of mainSurface.blocks) {
35
+ if (block.kind !== "paragraph") continue;
36
+
37
+ const outlineLevel = block.outlineLevel;
38
+ const styleLevel = resolveHeadingLevelFromStyle(document, block.styleId);
39
+ const level =
40
+ outlineLevel !== undefined && outlineLevel >= 0
41
+ ? outlineLevel + 1
42
+ : styleLevel;
43
+
44
+ if (level === null) continue;
45
+
46
+ const text = extractParagraphText(block);
47
+ if (!text.trim()) continue;
48
+
49
+ const offset = block.from;
50
+ const pageIndex = findPageForOffset(pages, offset);
51
+ const sectionIndex = findSectionForOffset(sections, offset);
52
+
53
+ headings.push({
54
+ headingId: `heading-${block.blockId}-${offset}`,
55
+ level,
56
+ text: text.trim(),
57
+ offset,
58
+ pageIndex,
59
+ sectionIndex,
60
+ });
61
+ }
62
+
63
+ return headings;
64
+ }
65
+
66
+ function findPageForOffset(
67
+ pages: readonly DocumentPageSnapshot[],
68
+ offset: number,
69
+ ): number {
70
+ for (let i = 0; i < pages.length; i += 1) {
71
+ if (offset < pages[i]!.endOffset) {
72
+ return i;
73
+ }
74
+ }
75
+ return Math.max(0, pages.length - 1);
76
+ }
77
+
78
+ function findSectionForOffset(
79
+ sections: readonly ResolvedDocumentSection[],
80
+ offset: number,
81
+ ): number {
82
+ return sections.length > 0 ? findSectionForPosition(sections, offset).index : 0;
83
+ }
84
+
85
+ function resolveHeadingLevelFromStyle(
86
+ document: CanonicalDocumentEnvelope,
87
+ styleId?: string,
88
+ ): number | null {
89
+ if (!styleId) {
90
+ return null;
91
+ }
92
+
93
+ const visited = new Set<string>();
94
+ let currentStyleId: string | undefined = styleId;
95
+ while (currentStyleId && !visited.has(currentStyleId)) {
96
+ visited.add(currentStyleId);
97
+ const style:
98
+ | { outlineLevel?: number; basedOn?: string }
99
+ | undefined = document.styles.paragraphs[currentStyleId];
100
+ if (!style) {
101
+ break;
102
+ }
103
+ if (
104
+ typeof style.outlineLevel === "number" &&
105
+ style.outlineLevel >= 0 &&
106
+ style.outlineLevel <= 8
107
+ ) {
108
+ return style.outlineLevel + 1;
109
+ }
110
+ currentStyleId = style.basedOn;
111
+ }
112
+
113
+ return headingLevelFromStyleId(styleId);
114
+ }
115
+
116
+ function extractParagraphText(
117
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
118
+ ): string {
119
+ let text = "";
120
+ for (const segment of block.segments) {
121
+ switch (segment.kind) {
122
+ case "text":
123
+ text += segment.text;
124
+ break;
125
+ case "tab":
126
+ text += "\t";
127
+ break;
128
+ case "hard_break":
129
+ text += "\n";
130
+ break;
131
+ }
132
+ }
133
+ if (!block.numberingPrefix) {
134
+ return text;
135
+ }
136
+ const trimmedText = text.trimStart();
137
+ if (
138
+ trimmedText === block.numberingPrefix ||
139
+ trimmedText.startsWith(`${block.numberingPrefix} `) ||
140
+ trimmedText.startsWith(`${block.numberingPrefix}\t`) ||
141
+ (block.numberingSuffix === "nothing" && trimmedText.startsWith(block.numberingPrefix))
142
+ ) {
143
+ return text;
144
+ }
145
+ const separator = block.numberingSuffix === "nothing" ? "" : " ";
146
+ return `${block.numberingPrefix}${separator}${text}`;
147
+ }
@@ -11,40 +11,22 @@
11
11
  */
12
12
 
13
13
  import type {
14
- DocumentHeadingSnapshot,
15
14
  DocumentNavigationSnapshot,
16
15
  DocumentPageSnapshot,
17
16
  EditorStoryTarget,
18
- EditorSurfaceSnapshot,
19
- SurfaceBlockSnapshot,
20
17
  } from "../api/public-types";
21
18
  import type { CanonicalDocumentEnvelope } from "../core/state/editor-state.ts";
22
19
  import { createSelectionSnapshot } from "../core/state/editor-state.ts";
23
- import { MAIN_STORY_TARGET } from "../core/selection/mapping.ts";
24
- import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
25
20
  import {
26
- buildResolvedSections,
27
21
  findSectionForPosition as resolveSectionForPosition,
28
- resolveSectionForStoryTarget,
29
22
  type ResolvedDocumentSection,
30
23
  } from "./document-layout.ts";
31
- import { buildPageStack } from "./layout/paginated-layout-engine.ts";
32
- import { findNoteReferencePosition } from "./view-state.ts";
24
+ import { createLayoutEngine } from "./layout/layout-engine-instance.ts";
25
+ export { buildHeadingOutline } from "./document-heading-outline.ts";
33
26
 
34
- interface NavigationBaseSnapshot {
35
- mainSurface: EditorSurfaceSnapshot;
36
- sections: ResolvedDocumentSection[];
37
- pages: DocumentPageSnapshot[];
38
- headings: DocumentHeadingSnapshot[];
39
- }
40
-
41
- const navigationBaseCache = new WeakMap<
42
- CanonicalDocumentEnvelope["content"],
43
- NavigationBaseSnapshot & {
44
- styles: CanonicalDocumentEnvelope["styles"];
45
- subParts: CanonicalDocumentEnvelope["subParts"];
46
- }
47
- >();
27
+ const navigationLayoutEngine = createLayoutEngine({
28
+ autoUpgradeToCanvasBackend: false,
29
+ });
48
30
 
49
31
  // ---------------------------------------------------------------------------
50
32
  // Public API
@@ -62,55 +44,11 @@ export function createDocumentNavigationSnapshot(
62
44
  selectionHead: number,
63
45
  activeStory: EditorStoryTarget,
64
46
  ): DocumentNavigationSnapshot {
65
- const base = getNavigationBaseSnapshot(document);
66
- const navigationContext = resolveActiveNavigationContext(
67
- document,
68
- base.pages,
69
- base.sections,
70
- base.mainSurface,
71
- selectionHead,
47
+ return navigationLayoutEngine.getNavigationSnapshot(
48
+ { document },
49
+ createSelectionSnapshot(selectionHead, selectionHead),
72
50
  activeStory,
73
51
  );
74
-
75
- return {
76
- pageCount: base.pages.length,
77
- pages: base.pages,
78
- headings: base.headings,
79
- activePageIndex: navigationContext.activePageIndex,
80
- activeSectionIndex: navigationContext.activeSectionIndex,
81
- };
82
- }
83
-
84
- function getNavigationBaseSnapshot(
85
- document: CanonicalDocumentEnvelope,
86
- ): NavigationBaseSnapshot {
87
- const cached = navigationBaseCache.get(document.content);
88
- if (
89
- cached &&
90
- cached.styles === document.styles &&
91
- cached.subParts === document.subParts
92
- ) {
93
- return cached;
94
- }
95
-
96
- const mainSurface = createEditorSurfaceSnapshot(
97
- document,
98
- createSelectionSnapshot(0, 0),
99
- MAIN_STORY_TARGET,
100
- );
101
- const sections = buildResolvedSections(document);
102
- const pages = buildPageStack(document, sections, mainSurface);
103
- const headings = buildHeadingOutline(document, mainSurface, sections, pages);
104
- const next = {
105
- mainSurface,
106
- sections,
107
- pages,
108
- headings,
109
- styles: document.styles,
110
- subParts: document.subParts,
111
- };
112
- navigationBaseCache.set(document.content, next);
113
- return next;
114
52
  }
115
53
 
116
54
  /**
@@ -137,176 +75,3 @@ export function findSectionForOffset(
137
75
  ): number {
138
76
  return sections.length > 0 ? resolveSectionForPosition(sections, offset).index : 0;
139
77
  }
140
-
141
- function resolveActiveNavigationContext(
142
- document: CanonicalDocumentEnvelope,
143
- pages: readonly DocumentPageSnapshot[],
144
- sections: readonly ResolvedDocumentSection[],
145
- mainSurface: EditorSurfaceSnapshot,
146
- selectionHead: number,
147
- activeStory: EditorStoryTarget,
148
- ): { activePageIndex: number; activeSectionIndex: number } {
149
- if (activeStory.kind === "main") {
150
- const activePageIndex = findPageForOffset(pages, selectionHead);
151
- return {
152
- activePageIndex,
153
- activeSectionIndex: pages[activePageIndex]?.sectionIndex ?? 0,
154
- };
155
- }
156
-
157
- if (activeStory.kind === "header" || activeStory.kind === "footer") {
158
- const section = resolveSectionForStoryTarget(document, sections, activeStory);
159
- if (section) {
160
- return {
161
- activePageIndex: findFirstPageIndexForSection(pages, section.index),
162
- activeSectionIndex: section.index,
163
- };
164
- }
165
- }
166
-
167
- if (activeStory.kind === "footnote" || activeStory.kind === "endnote") {
168
- const referencePosition = findNoteReferencePosition(mainSurface, activeStory);
169
- const activePageIndex = findPageForOffset(pages, referencePosition);
170
- return {
171
- activePageIndex,
172
- activeSectionIndex:
173
- pages[activePageIndex]?.sectionIndex ??
174
- findSectionForOffset(sections, referencePosition),
175
- };
176
- }
177
-
178
- return {
179
- activePageIndex: 0,
180
- activeSectionIndex: 0,
181
- };
182
- }
183
-
184
- function findFirstPageIndexForSection(
185
- pages: readonly DocumentPageSnapshot[],
186
- sectionIndex: number,
187
- ): number {
188
- const match = pages.findIndex((page) => page.sectionIndex === sectionIndex);
189
- return match >= 0 ? match : 0;
190
- }
191
-
192
- // ---------------------------------------------------------------------------
193
- // Heading outline
194
- // ---------------------------------------------------------------------------
195
-
196
- /** Style IDs that map to heading levels by convention. */
197
- const HEADING_STYLE_PATTERN = /^heading\s*(\d+)$/i;
198
-
199
- function headingLevelFromStyleId(styleId?: string): number | null {
200
- if (!styleId) return null;
201
- const match = HEADING_STYLE_PATTERN.exec(styleId);
202
- if (match) {
203
- const level = parseInt(match[1]!, 10);
204
- return level >= 1 && level <= 9 ? level : null;
205
- }
206
- return null;
207
- }
208
-
209
- export function buildHeadingOutline(
210
- document: CanonicalDocumentEnvelope,
211
- mainSurface: EditorSurfaceSnapshot,
212
- sections: ResolvedDocumentSection[],
213
- pages: DocumentPageSnapshot[],
214
- ): DocumentHeadingSnapshot[] {
215
- const headings: DocumentHeadingSnapshot[] = [];
216
-
217
- for (const block of mainSurface.blocks) {
218
- if (block.kind !== "paragraph") continue;
219
-
220
- // Check explicit outlineLevel first, then style-based heading detection
221
- const outlineLevel = block.outlineLevel;
222
- const styleLevel = resolveHeadingLevelFromStyle(document, block.styleId);
223
- const level =
224
- outlineLevel !== undefined && outlineLevel >= 0
225
- ? outlineLevel + 1
226
- : styleLevel;
227
-
228
- if (level === null) continue;
229
-
230
- const text = extractParagraphText(block);
231
- if (!text.trim()) continue;
232
-
233
- const offset = block.from;
234
- const pageIndex = findPageForOffset(pages, offset);
235
- const sectionIndex = findSectionForOffset(sections, offset);
236
-
237
- headings.push({
238
- headingId: `heading-${block.blockId}-${offset}`,
239
- level,
240
- text: text.trim(),
241
- offset,
242
- pageIndex,
243
- sectionIndex,
244
- });
245
- }
246
-
247
- return headings;
248
- }
249
-
250
- function resolveHeadingLevelFromStyle(
251
- document: CanonicalDocumentEnvelope,
252
- styleId?: string,
253
- ): number | null {
254
- if (!styleId) {
255
- return null;
256
- }
257
-
258
- const visited = new Set<string>();
259
- let currentStyleId: string | undefined = styleId;
260
- while (currentStyleId && !visited.has(currentStyleId)) {
261
- visited.add(currentStyleId);
262
- const style:
263
- | { outlineLevel?: number; basedOn?: string }
264
- | undefined = document.styles.paragraphs[currentStyleId];
265
- if (!style) {
266
- break;
267
- }
268
- if (
269
- typeof style.outlineLevel === "number" &&
270
- style.outlineLevel >= 0 &&
271
- style.outlineLevel <= 8
272
- ) {
273
- return style.outlineLevel + 1;
274
- }
275
- currentStyleId = style.basedOn;
276
- }
277
-
278
- return headingLevelFromStyleId(styleId);
279
- }
280
-
281
- function extractParagraphText(
282
- block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
283
- ): string {
284
- let text = "";
285
- for (const segment of block.segments) {
286
- switch (segment.kind) {
287
- case "text":
288
- text += segment.text;
289
- break;
290
- case "tab":
291
- text += "\t";
292
- break;
293
- case "hard_break":
294
- text += "\n";
295
- break;
296
- }
297
- }
298
- if (!block.numberingPrefix) {
299
- return text;
300
- }
301
- const trimmedText = text.trimStart();
302
- if (
303
- trimmedText === block.numberingPrefix ||
304
- trimmedText.startsWith(`${block.numberingPrefix} `) ||
305
- trimmedText.startsWith(`${block.numberingPrefix}\t`) ||
306
- (block.numberingSuffix === "nothing" && trimmedText.startsWith(block.numberingPrefix))
307
- ) {
308
- return text;
309
- }
310
- const separator = block.numberingSuffix === "nothing" ? "" : " ";
311
- return `${block.numberingPrefix}${separator}${text}`;
312
- }