@beyondwork/docx-react-component 1.0.18 → 1.0.19

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 (74) hide show
  1. package/README.md +8 -2
  2. package/package.json +24 -34
  3. package/src/api/README.md +5 -1
  4. package/src/api/public-types.ts +374 -4
  5. package/src/api/session-state.ts +58 -0
  6. package/src/core/commands/formatting-commands.ts +1 -0
  7. package/src/core/commands/image-commands.ts +147 -0
  8. package/src/core/commands/index.ts +5 -1
  9. package/src/core/commands/list-commands.ts +231 -36
  10. package/src/core/commands/paragraph-layout-commands.ts +339 -0
  11. package/src/core/commands/section-layout-commands.ts +680 -0
  12. package/src/core/commands/style-commands.ts +262 -0
  13. package/src/core/search/search-text.ts +329 -0
  14. package/src/core/selection/mapping.ts +41 -0
  15. package/src/core/state/editor-state.ts +1 -1
  16. package/src/index.ts +30 -0
  17. package/src/io/docx-session.ts +260 -39
  18. package/src/io/export/serialize-main-document.ts +202 -5
  19. package/src/io/export/serialize-numbering.ts +28 -7
  20. package/src/io/normalize/normalize-text.ts +63 -25
  21. package/src/io/ooxml/numbering-sentinels.ts +44 -0
  22. package/src/io/ooxml/parse-footnotes.ts +212 -20
  23. package/src/io/ooxml/parse-headers-footers.ts +229 -25
  24. package/src/io/ooxml/parse-inline-media.ts +16 -0
  25. package/src/io/ooxml/parse-main-document.ts +411 -6
  26. package/src/io/ooxml/parse-numbering.ts +7 -0
  27. package/src/io/ooxml/parse-settings.ts +184 -0
  28. package/src/io/ooxml/parse-shapes.ts +25 -0
  29. package/src/io/ooxml/parse-styles.ts +463 -0
  30. package/src/io/ooxml/parse-theme.ts +32 -0
  31. package/src/model/canonical-document.ts +133 -3
  32. package/src/model/cds-1.0.0.ts +13 -0
  33. package/src/model/snapshot.ts +2 -1
  34. package/src/runtime/document-layout.ts +332 -0
  35. package/src/runtime/document-navigation.ts +564 -0
  36. package/src/runtime/document-runtime.ts +265 -35
  37. package/src/runtime/document-search.ts +145 -0
  38. package/src/runtime/numbering-prefix.ts +47 -26
  39. package/src/runtime/page-layout-estimation.ts +212 -0
  40. package/src/runtime/read-only-diagnostics-runtime.ts +1 -0
  41. package/src/runtime/session-capabilities.ts +2 -0
  42. package/src/runtime/story-context.ts +164 -0
  43. package/src/runtime/story-targeting.ts +162 -0
  44. package/src/runtime/surface-projection.ts +239 -12
  45. package/src/runtime/table-schema.ts +87 -5
  46. package/src/runtime/view-state.ts +459 -0
  47. package/src/ui/WordReviewEditor.tsx +1902 -312
  48. package/src/ui/browser-export.ts +52 -0
  49. package/src/ui/headless/preserve-editor-selection.ts +5 -0
  50. package/src/ui/headless/selection-helpers.ts +20 -0
  51. package/src/ui/headless/selection-toolbar-model.ts +22 -0
  52. package/src/ui/headless/use-editor-keyboard.ts +6 -1
  53. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
  54. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +125 -14
  55. package/src/ui-tailwind/editor-surface/perf-probe.ts +107 -0
  56. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +45 -6
  57. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
  58. package/src/ui-tailwind/editor-surface/pm-position-map.ts +2 -2
  59. package/src/ui-tailwind/editor-surface/pm-schema.ts +47 -5
  60. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +95 -22
  61. package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
  62. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
  63. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +394 -77
  64. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
  65. package/src/ui-tailwind/index.ts +2 -1
  66. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
  67. package/src/ui-tailwind/review/tw-review-rail.tsx +6 -6
  68. package/src/ui-tailwind/theme/editor-theme.css +123 -0
  69. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
  70. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +291 -12
  71. package/src/ui-tailwind/tw-review-workspace.tsx +926 -27
  72. package/src/validation/compatibility-engine.ts +92 -20
  73. package/src/validation/diagnostics.ts +1 -0
  74. package/src/validation/docx-comment-proof.ts +487 -0
@@ -9,8 +9,14 @@ interface NumberingSequenceState {
9
9
  lastLevel: number | null;
10
10
  }
11
11
 
12
+ export interface NumberingPrefixResult {
13
+ text: string;
14
+ suffix?: "tab" | "space" | "nothing";
15
+ }
16
+
12
17
  export interface NumberingPrefixResolver {
13
18
  resolve(numbering: ParagraphNode["numbering"] | undefined): string | null;
19
+ resolveDetailed(numbering: ParagraphNode["numbering"] | undefined): NumberingPrefixResult | null;
14
20
  }
15
21
 
16
22
  const DEFAULT_START_AT = 1;
@@ -20,34 +26,49 @@ export function createNumberingPrefixResolver(
20
26
  ): NumberingPrefixResolver {
21
27
  const sequenceStates = new Map<string, NumberingSequenceState>();
22
28
 
29
+ function resolveInternal(numbering: ParagraphNode["numbering"] | undefined): NumberingPrefixResult | null {
30
+ if (!numbering) {
31
+ return null;
32
+ }
33
+
34
+ const instance = catalog.instances[numbering.numberingInstanceId];
35
+ if (!instance) {
36
+ return null;
37
+ }
38
+
39
+ const definition = catalog.abstractDefinitions[instance.abstractNumberingId];
40
+ if (!definition) {
41
+ return null;
42
+ }
43
+
44
+ const levelDefinitions = new Map(
45
+ definition.levels.map((level) => [level.level, level] as const),
46
+ );
47
+ const levelDefinition = levelDefinitions.get(numbering.level);
48
+ if (!levelDefinition || levelDefinition.format === "none") {
49
+ return null;
50
+ }
51
+
52
+ const sequenceState = getSequenceState(sequenceStates, numbering.numberingInstanceId);
53
+ advanceSequence(sequenceState, numbering.level, levelDefinitions, instance.overrides);
54
+
55
+ // When isLegalNumbering is true, use decimal format for all referenced levels
56
+ const effectiveLevelDefs = levelDefinition.isLegalNumbering
57
+ ? new Map(Array.from(levelDefinitions.entries()).map(([k, v]) => [k, { ...v, format: "decimal" }]))
58
+ : levelDefinitions;
59
+
60
+ const text = renderLevelText(levelDefinition.text, sequenceState.counters, effectiveLevelDefs);
61
+ if (text === null) return null;
62
+ return { text, suffix: levelDefinition.suffix };
63
+ }
64
+
23
65
  return {
24
66
  resolve(numbering) {
25
- if (!numbering) {
26
- return null;
27
- }
28
-
29
- const instance = catalog.instances[numbering.numberingInstanceId];
30
- if (!instance) {
31
- return null;
32
- }
33
-
34
- const definition = catalog.abstractDefinitions[instance.abstractNumberingId];
35
- if (!definition) {
36
- return null;
37
- }
38
-
39
- const levelDefinitions = new Map(
40
- definition.levels.map((level) => [level.level, level] as const),
41
- );
42
- const levelDefinition = levelDefinitions.get(numbering.level);
43
- if (!levelDefinition || levelDefinition.format === "none") {
44
- return null;
45
- }
46
-
47
- const sequenceState = getSequenceState(sequenceStates, numbering.numberingInstanceId);
48
- advanceSequence(sequenceState, numbering.level, levelDefinitions, instance.overrides);
49
-
50
- return renderLevelText(levelDefinition.text, sequenceState.counters, levelDefinitions);
67
+ const result = resolveInternal(numbering);
68
+ return result?.text ?? null;
69
+ },
70
+ resolveDetailed(numbering) {
71
+ return resolveInternal(numbering);
51
72
  },
52
73
  };
53
74
  }
@@ -0,0 +1,212 @@
1
+ import type {
2
+ PageLayoutSnapshot,
3
+ SurfaceBlockSnapshot,
4
+ SurfaceInlineSegment,
5
+ } from "../api/public-types";
6
+
7
+ const DEFAULT_FONT_SIZE_POINTS = 12;
8
+ const DEFAULT_LINE_HEIGHT_TWIPS = 280;
9
+ const MIN_BLOCK_HEIGHT_TWIPS = 240;
10
+ const TABLE_ROW_PADDING_TWIPS = 120;
11
+
12
+ export const DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP = 1 / 15;
13
+
14
+ export function estimateBlockHeight(
15
+ block: SurfaceBlockSnapshot | undefined,
16
+ columnWidth: number,
17
+ ): number {
18
+ if (!block) {
19
+ return 0;
20
+ }
21
+
22
+ switch (block.kind) {
23
+ case "paragraph":
24
+ return estimateParagraphHeight(block, columnWidth);
25
+ case "table":
26
+ return estimateTableHeight(block, columnWidth);
27
+ case "sdt_block":
28
+ return Math.max(
29
+ MIN_BLOCK_HEIGHT_TWIPS,
30
+ block.children.reduce((total, child) => total + estimateBlockHeight(child, columnWidth), 0),
31
+ );
32
+ case "opaque_block":
33
+ return block.label === "Section break" ? 0 : MIN_BLOCK_HEIGHT_TWIPS;
34
+ }
35
+ }
36
+
37
+ export function estimateParagraphHeight(
38
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
39
+ columnWidth: number,
40
+ ): number {
41
+ const lineHeight = estimateParagraphLineHeight(block);
42
+ const lineCount = estimateParagraphLineCount(block, columnWidth);
43
+ const spacingBefore = block.spacing?.before ?? 0;
44
+ const spacingAfter = block.spacing?.after ?? 0;
45
+ return Math.max(
46
+ MIN_BLOCK_HEIGHT_TWIPS,
47
+ lineHeight * lineCount + spacingBefore + spacingAfter,
48
+ );
49
+ }
50
+
51
+ export function estimateParagraphLineHeight(
52
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
53
+ ): number {
54
+ const explicitLine = block.spacing?.line;
55
+ if (typeof explicitLine === "number" && explicitLine > 0) {
56
+ return explicitLine;
57
+ }
58
+
59
+ const fontSizeHalfPoints = block.segments.find(
60
+ (segment): segment is Extract<SurfaceInlineSegment, { kind: "text" }> =>
61
+ segment.kind === "text" && typeof segment.markAttrs?.fontSize === "number",
62
+ )?.markAttrs?.fontSize;
63
+ const fontSizePoints =
64
+ typeof fontSizeHalfPoints === "number"
65
+ ? fontSizeHalfPoints / 2
66
+ : DEFAULT_FONT_SIZE_POINTS;
67
+ return Math.max(
68
+ DEFAULT_LINE_HEIGHT_TWIPS,
69
+ Math.round(fontSizePoints * 20 * 1.35),
70
+ );
71
+ }
72
+
73
+ export function estimateParagraphLineCount(
74
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
75
+ columnWidth: number,
76
+ ): number {
77
+ const averageCharWidth = estimateAverageCharWidth(block);
78
+ const charsPerLine = Math.max(12, Math.floor(columnWidth / averageCharWidth));
79
+ let lineCount = 1;
80
+ let currentLineChars = estimatedPrefixLength(block);
81
+
82
+ for (const segment of block.segments) {
83
+ switch (segment.kind) {
84
+ case "text":
85
+ currentLineChars += Array.from(segment.text).length;
86
+ while (currentLineChars > charsPerLine) {
87
+ lineCount += 1;
88
+ currentLineChars -= charsPerLine;
89
+ }
90
+ break;
91
+ case "tab":
92
+ currentLineChars += 4;
93
+ break;
94
+ case "hard_break":
95
+ lineCount += 1;
96
+ currentLineChars = 0;
97
+ break;
98
+ case "image":
99
+ lineCount += Math.max(1, Math.round(segment.display === "floating" ? 2 : 1));
100
+ currentLineChars = 0;
101
+ break;
102
+ case "note_ref":
103
+ currentLineChars += 1;
104
+ break;
105
+ case "opaque_inline":
106
+ if (segment.presentation !== "quiet-marker") {
107
+ currentLineChars += segment.label.length > 0 ? 1 : 0;
108
+ }
109
+ break;
110
+ }
111
+ }
112
+
113
+ return Math.max(1, lineCount);
114
+ }
115
+
116
+ export function getUsablePageHeight(layout: PageLayoutSnapshot): number {
117
+ return Math.max(
118
+ 1440,
119
+ layout.pageHeight - layout.marginTop - layout.marginBottom,
120
+ );
121
+ }
122
+
123
+ export interface PageColumnMetric {
124
+ width: number;
125
+ space: number;
126
+ }
127
+
128
+ export function getUsableColumnMetrics(
129
+ layout: PageLayoutSnapshot,
130
+ ): PageColumnMetric[] {
131
+ const usableWidth = Math.max(
132
+ 1440,
133
+ layout.pageWidth - layout.marginLeft - layout.marginRight - layout.gutter,
134
+ );
135
+ const columnCount = Math.max(1, layout.columns);
136
+ if (
137
+ !layout.equalWidthColumns &&
138
+ layout.columnDefinitions.length > 0
139
+ ) {
140
+ return layout.columnDefinitions.map((column, index) => ({
141
+ width: Math.max(720, column.width),
142
+ space:
143
+ index < layout.columnDefinitions.length - 1
144
+ ? Math.max(0, column.space ?? 0)
145
+ : 0,
146
+ }));
147
+ }
148
+
149
+ const width = columnCount <= 1
150
+ ? usableWidth
151
+ : Math.max(720, Math.floor(usableWidth / columnCount));
152
+ return Array.from({ length: columnCount }, () => ({
153
+ width,
154
+ space: 0,
155
+ }));
156
+ }
157
+
158
+ export function getUsableColumnWidth(layout: PageLayoutSnapshot): number {
159
+ return getUsableColumnMetrics(layout)[0]?.width ?? 1440;
160
+ }
161
+
162
+ function estimateTableHeight(
163
+ block: Extract<SurfaceBlockSnapshot, { kind: "table" }>,
164
+ columnWidth: number,
165
+ ): number {
166
+ let totalHeight = 0;
167
+ for (const row of block.rows) {
168
+ const explicitHeight = row.height ?? 0;
169
+ if (explicitHeight > 0) {
170
+ totalHeight += explicitHeight;
171
+ continue;
172
+ }
173
+
174
+ let rowHeight = MIN_BLOCK_HEIGHT_TWIPS;
175
+ for (const cell of row.cells) {
176
+ const cellHeight = cell.content.reduce(
177
+ (total, child) => total + estimateBlockHeight(child, columnWidth),
178
+ 0,
179
+ );
180
+ rowHeight = Math.max(rowHeight, cellHeight + TABLE_ROW_PADDING_TWIPS);
181
+ }
182
+ totalHeight += rowHeight;
183
+ }
184
+ return Math.max(MIN_BLOCK_HEIGHT_TWIPS, totalHeight);
185
+ }
186
+
187
+ function estimateAverageCharWidth(
188
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
189
+ ): number {
190
+ const fontSizeHalfPoints = block.segments.find(
191
+ (segment): segment is Extract<SurfaceInlineSegment, { kind: "text" }> =>
192
+ segment.kind === "text" && typeof segment.markAttrs?.fontSize === "number",
193
+ )?.markAttrs?.fontSize;
194
+ const fontSizePoints =
195
+ typeof fontSizeHalfPoints === "number"
196
+ ? fontSizeHalfPoints / 2
197
+ : DEFAULT_FONT_SIZE_POINTS;
198
+ return Math.max(96, Math.round(fontSizePoints * 12));
199
+ }
200
+
201
+ function estimatedPrefixLength(
202
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
203
+ ): number {
204
+ const prefix = block.numberingPrefix ?? "";
205
+ const suffix =
206
+ block.numberingSuffix === "space"
207
+ ? 1
208
+ : block.numberingSuffix === "tab"
209
+ ? 4
210
+ : 0;
211
+ return prefix.length + suffix;
212
+ }
@@ -155,6 +155,7 @@ function createDiagnosticsRenderSnapshot(
155
155
  isDirty: false,
156
156
  readOnly: true,
157
157
  selection: collapsedSelection(),
158
+ activeStory: { kind: "main" },
158
159
  documentStats: {
159
160
  storyLength: 0,
160
161
  commentCount: 0,
@@ -68,6 +68,7 @@ export function deriveCapabilities(
68
68
  const isReady = snapshot.isReady;
69
69
  const isReadOnly = snapshot.readOnly;
70
70
  const exportBlocked = snapshot.compatibility.blockExport;
71
+ const activeStory = snapshot.activeStory ?? { kind: "main" as const };
71
72
 
72
73
  // Phase derivation
73
74
  const phase: SessionCapabilities["phase"] = !isReady
@@ -88,6 +89,7 @@ export function deriveCapabilities(
88
89
  const canRedo = snapshot.commandState.canRedo && canEdit;
89
90
  const canAddComment =
90
91
  canEdit &&
92
+ activeStory.kind === "main" &&
91
93
  !snapshot.selection.isCollapsed &&
92
94
  Boolean(snapshot.surface) &&
93
95
  canCreateDocxCommentAnchor(snapshot.surface, toRuntimeAnchor(snapshot.selection.activeRange));
@@ -0,0 +1,164 @@
1
+ import type { EditorStoryTarget } from "../api/public-types";
2
+ import type { CanonicalDocumentEnvelope } from "../core/state/editor-state.ts";
3
+ import type {
4
+ FooterDocument,
5
+ HeaderDocument,
6
+ SectionProperties,
7
+ } from "../model/canonical-document.ts";
8
+
9
+ export interface DocumentSectionContext {
10
+ index: number;
11
+ properties?: SectionProperties;
12
+ }
13
+
14
+ export type HeaderFooterStoryTarget =
15
+ | Extract<EditorStoryTarget, { kind: "header" }>
16
+ | Extract<EditorStoryTarget, { kind: "footer" }>;
17
+
18
+ export function collectSectionContexts(
19
+ document: CanonicalDocumentEnvelope,
20
+ ): DocumentSectionContext[] {
21
+ const sections: DocumentSectionContext[] = [];
22
+ let sectionIndex = 0;
23
+
24
+ for (const block of document.content.children) {
25
+ if (block.type !== "section_break") {
26
+ continue;
27
+ }
28
+ sections.push({
29
+ index: sectionIndex,
30
+ properties: block.sectionProperties,
31
+ });
32
+ sectionIndex += 1;
33
+ }
34
+
35
+ sections.push({
36
+ index: sectionIndex,
37
+ properties: document.subParts?.finalSectionProperties,
38
+ });
39
+
40
+ return sections;
41
+ }
42
+
43
+ export function resolveSectionVariants(
44
+ kind: "header" | "footer",
45
+ sectionIndex: number,
46
+ explicitReferences:
47
+ | SectionProperties["headerReferences"]
48
+ | SectionProperties["footerReferences"]
49
+ | undefined,
50
+ documents: HeaderDocument[] | FooterDocument[],
51
+ ): Array<{ variant: "default" | "first" | "even"; relationshipId: string }> {
52
+ if (explicitReferences && explicitReferences.length > 0) {
53
+ return explicitReferences.map((ref) => ({
54
+ variant: ref.variant,
55
+ relationshipId: ref.relationshipId,
56
+ }));
57
+ }
58
+
59
+ return documents
60
+ .filter(
61
+ (entry) =>
62
+ entry.sectionIndex === undefined || entry.sectionIndex === sectionIndex,
63
+ )
64
+ .map((entry) => ({
65
+ variant: entry.variant,
66
+ relationshipId: entry.relationshipId,
67
+ }));
68
+ }
69
+
70
+ export function findHeaderFooterDocumentEntry(
71
+ document: CanonicalDocumentEnvelope,
72
+ target: HeaderFooterStoryTarget,
73
+ ): HeaderDocument | FooterDocument | undefined {
74
+ const documents =
75
+ target.kind === "header"
76
+ ? document.subParts?.headers ?? []
77
+ : document.subParts?.footers ?? [];
78
+ const matches = documents.filter(
79
+ (entry) =>
80
+ entry.relationshipId === target.relationshipId &&
81
+ entry.variant === target.variant,
82
+ );
83
+ if (matches.length === 0) {
84
+ return undefined;
85
+ }
86
+ if (target.sectionIndex !== undefined) {
87
+ return (
88
+ matches.find((entry) => entry.sectionIndex === target.sectionIndex) ??
89
+ matches.find((entry) => entry.sectionIndex === undefined) ??
90
+ matches[0]
91
+ );
92
+ }
93
+ return matches[0];
94
+ }
95
+
96
+ export function sectionSupportsStoryTarget(
97
+ document: CanonicalDocumentEnvelope,
98
+ sectionIndex: number,
99
+ target: HeaderFooterStoryTarget,
100
+ ): boolean {
101
+ if (!findHeaderFooterDocumentEntry(document, target)) {
102
+ return false;
103
+ }
104
+
105
+ const section = collectSectionContexts(document).find(
106
+ (candidate) => candidate.index === sectionIndex,
107
+ );
108
+ if (!section) {
109
+ return false;
110
+ }
111
+
112
+ const variants = resolveSectionVariants(
113
+ target.kind,
114
+ sectionIndex,
115
+ target.kind === "header"
116
+ ? section.properties?.headerReferences
117
+ : section.properties?.footerReferences,
118
+ target.kind === "header"
119
+ ? document.subParts?.headers ?? []
120
+ : document.subParts?.footers ?? [],
121
+ );
122
+
123
+ return variants.some(
124
+ (variant) =>
125
+ variant.relationshipId === target.relationshipId &&
126
+ variant.variant === target.variant,
127
+ );
128
+ }
129
+
130
+ export function normalizeHeaderFooterTarget(
131
+ document: CanonicalDocumentEnvelope,
132
+ target: HeaderFooterStoryTarget,
133
+ preferredSectionIndex?: number,
134
+ ): HeaderFooterStoryTarget | undefined {
135
+ if (!findHeaderFooterDocumentEntry(document, target)) {
136
+ return undefined;
137
+ }
138
+
139
+ if (target.sectionIndex !== undefined) {
140
+ return sectionSupportsStoryTarget(document, target.sectionIndex, target)
141
+ ? target
142
+ : undefined;
143
+ }
144
+
145
+ const candidateIndexes = collectSectionContexts(document)
146
+ .map((section) => section.index)
147
+ .filter((sectionIndex) =>
148
+ sectionSupportsStoryTarget(document, sectionIndex, target),
149
+ );
150
+ if (candidateIndexes.length === 0) {
151
+ return undefined;
152
+ }
153
+
154
+ const resolvedSectionIndex =
155
+ preferredSectionIndex !== undefined &&
156
+ candidateIndexes.includes(preferredSectionIndex)
157
+ ? preferredSectionIndex
158
+ : candidateIndexes[0];
159
+
160
+ return {
161
+ ...target,
162
+ sectionIndex: resolvedSectionIndex,
163
+ };
164
+ }
@@ -0,0 +1,162 @@
1
+ import type { EditorStoryTarget } from "../api/public-types";
2
+ import type { CanonicalDocumentEnvelope } from "../core/state/editor-state.ts";
3
+ import type { BlockNode } from "../model/canonical-document.ts";
4
+ import {
5
+ findHeaderFooterDocumentEntry,
6
+ normalizeHeaderFooterTarget,
7
+ } from "./story-context.ts";
8
+
9
+ export function storyTargetKey(target: EditorStoryTarget): string {
10
+ switch (target.kind) {
11
+ case "main":
12
+ return "main";
13
+ case "header":
14
+ return `header:${target.relationshipId}:${target.variant}:${target.sectionIndex ?? "*"}`;
15
+ case "footer":
16
+ return `footer:${target.relationshipId}:${target.variant}:${target.sectionIndex ?? "*"}`;
17
+ case "footnote":
18
+ return `footnote:${target.noteId}`;
19
+ case "endnote":
20
+ return `endnote:${target.noteId}`;
21
+ }
22
+ }
23
+
24
+ export function getStoryBlocks(
25
+ document: CanonicalDocumentEnvelope,
26
+ target: EditorStoryTarget,
27
+ ): readonly BlockNode[] {
28
+ if (target.kind === "main") {
29
+ return document.content.children;
30
+ }
31
+
32
+ const subParts = document.subParts;
33
+ if (!subParts) {
34
+ return [];
35
+ }
36
+
37
+ switch (target.kind) {
38
+ case "header": {
39
+ const resolvedTarget = normalizeHeaderFooterTarget(document, target);
40
+ return (
41
+ resolvedTarget &&
42
+ findHeaderFooterDocumentEntry(document, resolvedTarget)?.blocks
43
+ ) ?? [];
44
+ }
45
+ case "footer": {
46
+ const resolvedTarget = normalizeHeaderFooterTarget(document, target);
47
+ return (
48
+ resolvedTarget &&
49
+ findHeaderFooterDocumentEntry(document, resolvedTarget)?.blocks
50
+ ) ?? [];
51
+ }
52
+ case "footnote":
53
+ return subParts.footnoteCollection?.footnotes[target.noteId]?.blocks ?? [];
54
+ case "endnote":
55
+ return subParts.footnoteCollection?.endnotes[target.noteId]?.blocks ?? [];
56
+ }
57
+ }
58
+
59
+ export function replaceStoryBlocks(
60
+ document: CanonicalDocumentEnvelope,
61
+ target: EditorStoryTarget,
62
+ blocks: readonly BlockNode[],
63
+ ): CanonicalDocumentEnvelope {
64
+ if (target.kind === "main") {
65
+ return {
66
+ ...document,
67
+ content: {
68
+ ...document.content,
69
+ children: [...blocks],
70
+ },
71
+ };
72
+ }
73
+
74
+ if (!document.subParts) {
75
+ return document;
76
+ }
77
+
78
+ switch (target.kind) {
79
+ case "header": {
80
+ const resolvedTarget = normalizeHeaderFooterTarget(document, target);
81
+ const matchedEntry = resolvedTarget
82
+ ? findHeaderFooterDocumentEntry(document, resolvedTarget)
83
+ : undefined;
84
+ if (!matchedEntry) {
85
+ return document;
86
+ }
87
+ return {
88
+ ...document,
89
+ subParts: {
90
+ ...document.subParts,
91
+ headers: document.subParts.headers.map((header) =>
92
+ header.relationshipId === matchedEntry.relationshipId &&
93
+ header.variant === matchedEntry.variant &&
94
+ header.partPath === matchedEntry.partPath
95
+ ? { ...header, blocks: [...blocks] }
96
+ : header,
97
+ ),
98
+ },
99
+ };
100
+ }
101
+ case "footer": {
102
+ const resolvedTarget = normalizeHeaderFooterTarget(document, target);
103
+ const matchedEntry = resolvedTarget
104
+ ? findHeaderFooterDocumentEntry(document, resolvedTarget)
105
+ : undefined;
106
+ if (!matchedEntry) {
107
+ return document;
108
+ }
109
+ return {
110
+ ...document,
111
+ subParts: {
112
+ ...document.subParts,
113
+ footers: document.subParts.footers.map((footer) =>
114
+ footer.relationshipId === matchedEntry.relationshipId &&
115
+ footer.variant === matchedEntry.variant &&
116
+ footer.partPath === matchedEntry.partPath
117
+ ? { ...footer, blocks: [...blocks] }
118
+ : footer,
119
+ ),
120
+ },
121
+ };
122
+ }
123
+ case "footnote":
124
+ return !document.subParts.footnoteCollection?.footnotes[target.noteId]
125
+ ? document
126
+ : {
127
+ ...document,
128
+ subParts: {
129
+ ...document.subParts,
130
+ footnoteCollection: {
131
+ ...document.subParts.footnoteCollection,
132
+ footnotes: {
133
+ ...document.subParts.footnoteCollection.footnotes,
134
+ [target.noteId]: {
135
+ ...document.subParts.footnoteCollection.footnotes[target.noteId],
136
+ blocks: [...blocks],
137
+ },
138
+ },
139
+ },
140
+ },
141
+ };
142
+ case "endnote":
143
+ return !document.subParts.footnoteCollection?.endnotes[target.noteId]
144
+ ? document
145
+ : {
146
+ ...document,
147
+ subParts: {
148
+ ...document.subParts,
149
+ footnoteCollection: {
150
+ ...document.subParts.footnoteCollection,
151
+ endnotes: {
152
+ ...document.subParts.footnoteCollection.endnotes,
153
+ [target.noteId]: {
154
+ ...document.subParts.footnoteCollection.endnotes[target.noteId],
155
+ blocks: [...blocks],
156
+ },
157
+ },
158
+ },
159
+ },
160
+ };
161
+ }
162
+ }