@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.
- package/README.md +8 -2
- package/package.json +24 -34
- package/src/api/README.md +5 -1
- package/src/api/public-types.ts +374 -4
- package/src/api/session-state.ts +58 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/image-commands.ts +147 -0
- package/src/core/commands/index.ts +5 -1
- package/src/core/commands/list-commands.ts +231 -36
- package/src/core/commands/paragraph-layout-commands.ts +339 -0
- package/src/core/commands/section-layout-commands.ts +680 -0
- package/src/core/commands/style-commands.ts +262 -0
- package/src/core/search/search-text.ts +329 -0
- package/src/core/selection/mapping.ts +41 -0
- package/src/core/state/editor-state.ts +1 -1
- package/src/index.ts +30 -0
- package/src/io/docx-session.ts +260 -39
- package/src/io/export/serialize-main-document.ts +202 -5
- package/src/io/export/serialize-numbering.ts +28 -7
- package/src/io/normalize/normalize-text.ts +63 -25
- package/src/io/ooxml/numbering-sentinels.ts +44 -0
- package/src/io/ooxml/parse-footnotes.ts +212 -20
- package/src/io/ooxml/parse-headers-footers.ts +229 -25
- package/src/io/ooxml/parse-inline-media.ts +16 -0
- package/src/io/ooxml/parse-main-document.ts +411 -6
- package/src/io/ooxml/parse-numbering.ts +7 -0
- package/src/io/ooxml/parse-settings.ts +184 -0
- package/src/io/ooxml/parse-shapes.ts +25 -0
- package/src/io/ooxml/parse-styles.ts +463 -0
- package/src/io/ooxml/parse-theme.ts +32 -0
- package/src/model/canonical-document.ts +133 -3
- package/src/model/cds-1.0.0.ts +13 -0
- package/src/model/snapshot.ts +2 -1
- package/src/runtime/document-layout.ts +332 -0
- package/src/runtime/document-navigation.ts +564 -0
- package/src/runtime/document-runtime.ts +265 -35
- package/src/runtime/document-search.ts +145 -0
- package/src/runtime/numbering-prefix.ts +47 -26
- package/src/runtime/page-layout-estimation.ts +212 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +1 -0
- package/src/runtime/session-capabilities.ts +2 -0
- package/src/runtime/story-context.ts +164 -0
- package/src/runtime/story-targeting.ts +162 -0
- package/src/runtime/surface-projection.ts +239 -12
- package/src/runtime/table-schema.ts +87 -5
- package/src/runtime/view-state.ts +459 -0
- package/src/ui/WordReviewEditor.tsx +1902 -312
- package/src/ui/browser-export.ts +52 -0
- package/src/ui/headless/preserve-editor-selection.ts +5 -0
- package/src/ui/headless/selection-helpers.ts +20 -0
- package/src/ui/headless/selection-toolbar-model.ts +22 -0
- package/src/ui/headless/use-editor-keyboard.ts +6 -1
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +125 -14
- package/src/ui-tailwind/editor-surface/perf-probe.ts +107 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +45 -6
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-schema.ts +47 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +95 -22
- package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +394 -77
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
- package/src/ui-tailwind/index.ts +2 -1
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
- package/src/ui-tailwind/review/tw-review-rail.tsx +6 -6
- package/src/ui-tailwind/theme/editor-theme.css +123 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +291 -12
- package/src/ui-tailwind/tw-review-workspace.tsx +926 -27
- package/src/validation/compatibility-engine.ts +92 -20
- package/src/validation/diagnostics.ts +1 -0
- 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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|