@beyondwork/docx-react-component 1.0.85 → 1.0.87
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/package.json +1 -1
- package/src/api/public-types.ts +49 -0
- package/src/api/v3/ui/chrome-composition.ts +2 -11
- package/src/api/v3/ui/chrome.ts +6 -8
- package/src/index.ts +5 -0
- package/src/io/export/serialize-main-document.ts +215 -6
- package/src/io/ooxml/parse-drawing.ts +15 -1
- package/src/io/ooxml/parse-fields.ts +410 -12
- package/src/model/canonical-document.ts +177 -2
- package/src/model/layout/page-layout-snapshot.ts +2 -0
- package/src/model/layout/runtime-page-graph-types.ts +6 -0
- package/src/preservation/store.ts +4 -5
- package/src/runtime/document-outline.ts +80 -0
- package/src/runtime/document-runtime.ts +338 -13
- package/src/runtime/formatting/field/page-number-format.ts +49 -0
- package/src/runtime/formatting/field/resolver.ts +61 -40
- package/src/runtime/layout/layout-engine-instance.ts +18 -1
- package/src/runtime/layout/layout-engine-version.ts +19 -1
- package/src/runtime/layout/measurement-backend-canvas.ts +21 -9
- package/src/runtime/layout/measurement-backend-empirical.ts +18 -4
- package/src/runtime/layout/page-graph.ts +13 -2
- package/src/runtime/layout/paginated-layout-engine.ts +440 -117
- package/src/runtime/layout/project-block-fragments.ts +87 -4
- package/src/runtime/layout/resolve-page-fields.ts +8 -5
- package/src/runtime/layout/table-row-split.ts +97 -23
- package/src/runtime/surface-projection.ts +227 -27
- package/src/shell/session-bootstrap.ts +6 -1
- package/src/ui/WordReviewEditor.tsx +112 -33
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-shell-view.tsx +1 -0
- package/src/ui/editor-surface-controller.tsx +1 -0
- package/src/ui/headless/revision-decoration-model.ts +11 -13
- package/src/ui-tailwind/chrome/editor-action-registry.ts +7 -26
- package/src/ui-tailwind/chrome/responsive-chrome.ts +2 -2
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +27 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +6 -2
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +57 -6
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +17 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +5 -0
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +34 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +146 -20
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +8 -2
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +41 -44
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +1 -1
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +8 -3
- package/src/ui-tailwind/review/tw-review-rail.tsx +2 -0
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +75 -31
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +159 -8
- package/src/ui-tailwind/review-workspace/types.ts +4 -0
- package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +1 -1
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +8 -10
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -17
- package/src/ui-tailwind/tw-review-workspace.tsx +27 -2
|
@@ -25,8 +25,10 @@ import type {
|
|
|
25
25
|
SurfaceBlockSnapshot,
|
|
26
26
|
} from "../../api/public-types";
|
|
27
27
|
import type { RuntimeBlockFragment } from "./page-graph.ts";
|
|
28
|
+
import type { RuntimeLineBox } from "../../model/layout/page-graph-types.ts";
|
|
28
29
|
import type {
|
|
29
30
|
BlockSplits,
|
|
31
|
+
FragmentMeasurement,
|
|
30
32
|
ParagraphLineSlice,
|
|
31
33
|
TableRowSlice,
|
|
32
34
|
} from "./paginated-layout-engine.ts";
|
|
@@ -38,6 +40,7 @@ export function projectSurfaceBlocksToPageFragments(
|
|
|
38
40
|
pages: readonly DocumentPageSnapshot[],
|
|
39
41
|
splits?: BlockSplits,
|
|
40
42
|
columnByBlockIdByPageIndex?: ReadonlyMap<number, ReadonlyMap<string, number>>,
|
|
43
|
+
fragmentMeasurementsByPageIndex?: ReadonlyMap<number, ReadonlyMap<string, FragmentMeasurement>>,
|
|
41
44
|
): Map<number, FragmentWithoutPageId[]> {
|
|
42
45
|
const byPage = new Map<number, FragmentWithoutPageId[]>();
|
|
43
46
|
const perPageCounter = new Map<number, number>();
|
|
@@ -63,6 +66,13 @@ export function projectSurfaceBlocksToPageFragments(
|
|
|
63
66
|
// Refactor/04 Slice 4 — per-block column placement lookup.
|
|
64
67
|
const columnIndexFor = (pageIndex: number, blockId: string): number | undefined =>
|
|
65
68
|
columnByBlockIdByPageIndex?.get(pageIndex)?.get(blockId);
|
|
69
|
+
const measuredHeightFor = (
|
|
70
|
+
pageIndex: number,
|
|
71
|
+
blockId: string,
|
|
72
|
+
fallback: number,
|
|
73
|
+
): number =>
|
|
74
|
+
fragmentMeasurementsByPageIndex?.get(pageIndex)?.get(blockId)?.heightTwips ??
|
|
75
|
+
fallback;
|
|
66
76
|
|
|
67
77
|
for (const block of surface.blocks) {
|
|
68
78
|
// R3: table split across pages — emit one fragment per row slice.
|
|
@@ -78,7 +88,12 @@ export function projectSurfaceBlocksToPageFragments(
|
|
|
78
88
|
pushFragment(pageIndex, {
|
|
79
89
|
...fragment,
|
|
80
90
|
orderInRegion: nextOrder(pageIndex),
|
|
81
|
-
|
|
91
|
+
heightTwips: fragment.heightTwips,
|
|
92
|
+
...(fragment.columnIndex !== undefined
|
|
93
|
+
? { columnIndex: fragment.columnIndex }
|
|
94
|
+
: columnIndex !== undefined
|
|
95
|
+
? { columnIndex }
|
|
96
|
+
: {}),
|
|
82
97
|
});
|
|
83
98
|
},
|
|
84
99
|
);
|
|
@@ -118,7 +133,11 @@ export function projectSurfaceBlocksToPageFragments(
|
|
|
118
133
|
regionKind: "body",
|
|
119
134
|
from: block.from,
|
|
120
135
|
to: block.to,
|
|
121
|
-
heightTwips:
|
|
136
|
+
heightTwips: measuredHeightFor(
|
|
137
|
+
pageIndex,
|
|
138
|
+
block.blockId,
|
|
139
|
+
estimateBlockHeightFromSpan(block),
|
|
140
|
+
),
|
|
122
141
|
...deriveStyleMetadata(block),
|
|
123
142
|
kind: "whole",
|
|
124
143
|
...(columnIndex !== undefined ? { columnIndex } : {}),
|
|
@@ -130,6 +149,69 @@ export function projectSurfaceBlocksToPageFragments(
|
|
|
130
149
|
return byPage;
|
|
131
150
|
}
|
|
132
151
|
|
|
152
|
+
export function projectLineBoxesForPageFragments(
|
|
153
|
+
pages: readonly DocumentPageSnapshot[],
|
|
154
|
+
fragmentsByPageIndex: ReadonlyMap<
|
|
155
|
+
number,
|
|
156
|
+
ReadonlyArray<Omit<RuntimeBlockFragment, "pageId">>
|
|
157
|
+
>,
|
|
158
|
+
fragmentMeasurementsByPageIndex?: ReadonlyMap<number, ReadonlyMap<string, FragmentMeasurement>>,
|
|
159
|
+
): Map<number, RuntimeLineBox[]> {
|
|
160
|
+
const byPage = new Map<number, RuntimeLineBox[]>();
|
|
161
|
+
for (const page of pages) {
|
|
162
|
+
const fragments = [...(fragmentsByPageIndex.get(page.pageIndex) ?? [])]
|
|
163
|
+
.filter((fragment) => fragment.regionKind === "body")
|
|
164
|
+
.sort((a, b) => a.orderInRegion - b.orderInRegion);
|
|
165
|
+
const lines: RuntimeLineBox[] = [];
|
|
166
|
+
let cursorTwips = 0;
|
|
167
|
+
for (const fragment of fragments) {
|
|
168
|
+
const measurement = fragmentMeasurementsByPageIndex
|
|
169
|
+
?.get(page.pageIndex)
|
|
170
|
+
?.get(fragment.blockId);
|
|
171
|
+
const explicitLines =
|
|
172
|
+
fragment.kind === "paragraph-slice" && fragment.paragraphLineRange
|
|
173
|
+
? fragment.paragraphLineRange.to - fragment.paragraphLineRange.from
|
|
174
|
+
: measurement?.lineCount;
|
|
175
|
+
const lineCount = Math.max(
|
|
176
|
+
0,
|
|
177
|
+
explicitLines ?? (fragment.heightTwips > 0 ? 1 : 0),
|
|
178
|
+
);
|
|
179
|
+
if (lineCount === 0) {
|
|
180
|
+
cursorTwips += Math.max(0, fragment.heightTwips);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const lineHeight =
|
|
184
|
+
fragment.kind === "paragraph-slice" && fragment.paragraphLineRange
|
|
185
|
+
? Math.max(1, Math.floor(fragment.heightTwips / lineCount))
|
|
186
|
+
: measurement?.lineHeightTwips ??
|
|
187
|
+
Math.max(1, Math.floor(fragment.heightTwips / lineCount));
|
|
188
|
+
const widthTwips =
|
|
189
|
+
measurement?.widthTwips ??
|
|
190
|
+
Math.max(
|
|
191
|
+
0,
|
|
192
|
+
page.layout.pageWidth -
|
|
193
|
+
page.layout.marginLeft -
|
|
194
|
+
page.layout.marginRight -
|
|
195
|
+
page.layout.gutter,
|
|
196
|
+
);
|
|
197
|
+
for (let lineIndex = 0; lineIndex < lineCount; lineIndex += 1) {
|
|
198
|
+
lines.push({
|
|
199
|
+
fragmentId: fragment.fragmentId,
|
|
200
|
+
lineIndex,
|
|
201
|
+
baselineTwips: cursorTwips + lineIndex * lineHeight,
|
|
202
|
+
heightTwips: lineHeight,
|
|
203
|
+
widthTwips,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
cursorTwips += Math.max(0, fragment.heightTwips);
|
|
207
|
+
}
|
|
208
|
+
if (lines.length > 0) {
|
|
209
|
+
byPage.set(page.pageIndex, lines);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return byPage;
|
|
213
|
+
}
|
|
214
|
+
|
|
133
215
|
/**
|
|
134
216
|
* Emit one fragment per slice for a paragraph that pagination split across
|
|
135
217
|
* pages. The source `block` offset range stays intact on every slice; the
|
|
@@ -149,7 +231,7 @@ function emitSlicedParagraph(
|
|
|
149
231
|
regionKind: "body",
|
|
150
232
|
from: block.from,
|
|
151
233
|
to: block.to,
|
|
152
|
-
heightTwips: estimateSliceHeightFromLines(slice.lineRange),
|
|
234
|
+
heightTwips: slice.heightTwips ?? estimateSliceHeightFromLines(slice.lineRange),
|
|
153
235
|
...deriveStyleMetadata(block),
|
|
154
236
|
kind: "paragraph-slice",
|
|
155
237
|
paragraphLineRange: slice.lineRange,
|
|
@@ -190,10 +272,11 @@ function emitSlicedTable(
|
|
|
190
272
|
regionKind: "body",
|
|
191
273
|
from: block.from,
|
|
192
274
|
to: block.to,
|
|
193
|
-
heightTwips: estimateSliceHeightFromRows(slice.rowRange),
|
|
275
|
+
heightTwips: slice.heightTwips ?? estimateSliceHeightFromRows(slice.rowRange),
|
|
194
276
|
...deriveStyleMetadata(block),
|
|
195
277
|
kind: "table-slice",
|
|
196
278
|
tableRowRange: slice.rowRange,
|
|
279
|
+
...(slice.columnIndex !== undefined ? { columnIndex: slice.columnIndex } : {}),
|
|
197
280
|
};
|
|
198
281
|
emit(slice.pageIndex, fragment);
|
|
199
282
|
}
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { SupportedFieldFamily } from "../../model/canonical-document.ts";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
formatPageNumber,
|
|
19
|
+
formatPageNumberWithChapter,
|
|
20
|
+
} from "../formatting/field/page-number-format.ts";
|
|
18
21
|
import type { PublicPageNode } from "./public-facet.ts";
|
|
19
22
|
import type { RuntimePageGraph, RuntimePageNode } from "./page-graph.ts";
|
|
20
23
|
|
|
@@ -46,9 +49,9 @@ export function resolvePageFieldDisplayText(
|
|
|
46
49
|
): string {
|
|
47
50
|
switch (family) {
|
|
48
51
|
case "PAGE":
|
|
49
|
-
return
|
|
52
|
+
return formatPageNumberWithChapter(
|
|
50
53
|
context.page.stories.displayPageNumber,
|
|
51
|
-
context.page.layout?.pageNumbering
|
|
54
|
+
context.page.layout?.pageNumbering,
|
|
52
55
|
);
|
|
53
56
|
case "NUMPAGES":
|
|
54
57
|
// Blank fillers (evenPage/oddPage section breaks) don't count toward
|
|
@@ -78,9 +81,9 @@ export function resolvePublicPageFieldDisplayText(
|
|
|
78
81
|
): string {
|
|
79
82
|
switch (family) {
|
|
80
83
|
case "PAGE":
|
|
81
|
-
return
|
|
84
|
+
return formatPageNumberWithChapter(
|
|
82
85
|
context.page.displayPageNumber,
|
|
83
|
-
context.page.layout.pageNumbering
|
|
86
|
+
context.page.layout.pageNumbering,
|
|
84
87
|
);
|
|
85
88
|
case "NUMPAGES":
|
|
86
89
|
return formatPageNumber(
|
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
|
|
38
38
|
import type { LayoutMeasurementProvider } from "./layout-measurement-provider.ts";
|
|
39
39
|
import type { SurfaceBlockSnapshot } from "../../api/public-types";
|
|
40
|
+
import {
|
|
41
|
+
buildRunFormattingMap,
|
|
42
|
+
calculateParagraphHeight,
|
|
43
|
+
resolveBlockFormatting,
|
|
44
|
+
type LayoutThemeFonts,
|
|
45
|
+
} from "./resolved-formatting-state.ts";
|
|
40
46
|
|
|
41
47
|
// Re-export the resolveCellWidth helper from paginated-layout-engine so the
|
|
42
48
|
// math stays single-sourced. The engine module exposes it via the
|
|
@@ -50,6 +56,9 @@ export interface MeasureTableRowHeightsInput {
|
|
|
50
56
|
block: Extract<SurfaceBlockSnapshot, { kind: "table" }>;
|
|
51
57
|
columnWidth: number;
|
|
52
58
|
measurementProvider?: LayoutMeasurementProvider;
|
|
59
|
+
defaultTabInterval?: number;
|
|
60
|
+
themeFonts?: LayoutThemeFonts;
|
|
61
|
+
contentMode?: "legacy-min" | "resolved";
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
/**
|
|
@@ -65,7 +74,14 @@ export interface MeasureTableRowHeightsInput {
|
|
|
65
74
|
export function measureTableRowHeights(
|
|
66
75
|
input: MeasureTableRowHeightsInput,
|
|
67
76
|
): number[] {
|
|
68
|
-
const {
|
|
77
|
+
const {
|
|
78
|
+
block,
|
|
79
|
+
columnWidth,
|
|
80
|
+
measurementProvider,
|
|
81
|
+
defaultTabInterval = 720,
|
|
82
|
+
themeFonts,
|
|
83
|
+
contentMode = "legacy-min",
|
|
84
|
+
} = input;
|
|
69
85
|
const heights: number[] = [];
|
|
70
86
|
|
|
71
87
|
const totalGridTwips = block.gridColumns.reduce((sum, w) => sum + w, 0);
|
|
@@ -96,11 +112,15 @@ export function measureTableRowHeights(
|
|
|
96
112
|
let cellContentHeight = 0;
|
|
97
113
|
for (const child of cell.content) {
|
|
98
114
|
if (child.kind === "paragraph") {
|
|
99
|
-
cellContentHeight +=
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
115
|
+
cellContentHeight += contentMode === "resolved"
|
|
116
|
+
? measureParagraphStandaloneHeight(
|
|
117
|
+
child,
|
|
118
|
+
cellWidth,
|
|
119
|
+
measurementProvider,
|
|
120
|
+
defaultTabInterval,
|
|
121
|
+
themeFonts,
|
|
122
|
+
)
|
|
123
|
+
: MIN_ROW_HEIGHT_TWIPS;
|
|
104
124
|
} else {
|
|
105
125
|
cellContentHeight += MIN_ROW_HEIGHT_TWIPS;
|
|
106
126
|
}
|
|
@@ -126,26 +146,80 @@ export function measureTableRowHeights(
|
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* `measureBlockHeight`, which would require threading the
|
|
133
|
-
* per-invocation cache through. Since cell content already re-runs
|
|
134
|
-
* through `measureBlockHeight` on the main pagination path, this
|
|
135
|
-
* helper is only used by `measureTableRowHeights` for the split-math
|
|
136
|
-
* preflight and does not affect canonical pagination.
|
|
149
|
+
* Paragraph height used when walking table cell content. This mirrors the
|
|
150
|
+
* paginator's paragraph measurement path closely enough for row-boundary
|
|
151
|
+
* decisions without importing `measureBlockHeight` back from the paginator.
|
|
137
152
|
*/
|
|
138
153
|
function measureParagraphStandaloneHeight(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
155
|
+
columnWidth: number,
|
|
156
|
+
provider: LayoutMeasurementProvider | undefined,
|
|
157
|
+
defaultTabInterval: number,
|
|
158
|
+
themeFonts: LayoutThemeFonts | undefined,
|
|
142
159
|
): number {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
160
|
+
const formatting = resolveBlockFormatting(block, defaultTabInterval, themeFonts);
|
|
161
|
+
if (!formatting) return MIN_ROW_HEIGHT_TWIPS;
|
|
162
|
+
if (provider) {
|
|
163
|
+
const measured = provider.measureLineFragments({
|
|
164
|
+
block,
|
|
165
|
+
formatting,
|
|
166
|
+
runs: buildRunFormattingMap(block),
|
|
167
|
+
columnWidth,
|
|
168
|
+
});
|
|
169
|
+
const contentHeight = measured.lineHeights.reduce(
|
|
170
|
+
(total, lineHeight) => total + lineHeight,
|
|
171
|
+
0,
|
|
172
|
+
);
|
|
173
|
+
return Math.max(
|
|
174
|
+
MIN_ROW_HEIGHT_TWIPS,
|
|
175
|
+
contentHeight + formatting.spacingBefore + formatting.spacingAfter,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const firstLineWidth = Math.max(
|
|
179
|
+
1,
|
|
180
|
+
columnWidth -
|
|
181
|
+
formatting.indentLeft -
|
|
182
|
+
formatting.indentRight -
|
|
183
|
+
Math.max(0, formatting.firstLineIndent),
|
|
184
|
+
);
|
|
185
|
+
const subsequentLineWidth = Math.max(
|
|
186
|
+
1,
|
|
187
|
+
columnWidth - formatting.indentLeft - formatting.indentRight,
|
|
188
|
+
);
|
|
189
|
+
const firstCapacity = Math.max(
|
|
190
|
+
1,
|
|
191
|
+
Math.floor(firstLineWidth / formatting.averageCharWidthTwips),
|
|
192
|
+
);
|
|
193
|
+
const subsequentCapacity = Math.max(
|
|
194
|
+
1,
|
|
195
|
+
Math.floor(subsequentLineWidth / formatting.averageCharWidthTwips),
|
|
196
|
+
);
|
|
197
|
+
let lineCount = 1;
|
|
198
|
+
let current = 0;
|
|
199
|
+
let capacity = firstCapacity;
|
|
200
|
+
for (const segment of block.segments) {
|
|
201
|
+
if (segment.kind === "hard_break") {
|
|
202
|
+
lineCount += 1;
|
|
203
|
+
current = 0;
|
|
204
|
+
capacity = subsequentCapacity;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const advance =
|
|
208
|
+
segment.kind === "text"
|
|
209
|
+
? Array.from(segment.text).length
|
|
210
|
+
: segment.kind === "tab"
|
|
211
|
+
? 4
|
|
212
|
+
: segment.kind === "opaque_inline" && segment.presentation === "quiet-marker"
|
|
213
|
+
? 0
|
|
214
|
+
: 1;
|
|
215
|
+
current += advance;
|
|
216
|
+
while (current > capacity) {
|
|
217
|
+
lineCount += 1;
|
|
218
|
+
current -= capacity;
|
|
219
|
+
capacity = subsequentCapacity;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return calculateParagraphHeight(formatting, lineCount);
|
|
149
223
|
}
|
|
150
224
|
|
|
151
225
|
export interface FindTableRowSplitInput {
|
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
SdtNode,
|
|
30
30
|
ShapeNode,
|
|
31
31
|
SmartArtPreviewNode,
|
|
32
|
+
LegacyFormFieldNode,
|
|
32
33
|
BorderSpec,
|
|
33
34
|
TableBorders,
|
|
34
35
|
TableCellBorders,
|
|
@@ -1186,7 +1187,11 @@ function createParagraphBlock(
|
|
|
1186
1187
|
let cursor = start;
|
|
1187
1188
|
const children = Array.isArray(paragraph.children) ? paragraph.children : [];
|
|
1188
1189
|
|
|
1189
|
-
for (
|
|
1190
|
+
for (let childIndex = 0; childIndex < children.length; childIndex += 1) {
|
|
1191
|
+
const child = children[childIndex];
|
|
1192
|
+
if (isEmptyTocFieldWithCachedResult(child, children, childIndex)) {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1190
1195
|
const result = appendInlineSegments(
|
|
1191
1196
|
accumulator,
|
|
1192
1197
|
child,
|
|
@@ -1210,6 +1215,54 @@ function createParagraphBlock(
|
|
|
1210
1215
|
};
|
|
1211
1216
|
}
|
|
1212
1217
|
|
|
1218
|
+
function isEmptyTocFieldWithCachedResult(
|
|
1219
|
+
child: InlineNode,
|
|
1220
|
+
siblings: readonly InlineNode[],
|
|
1221
|
+
childIndex: number,
|
|
1222
|
+
): boolean {
|
|
1223
|
+
if (child.type !== "field") return false;
|
|
1224
|
+
if (child.fieldFamily !== "TOC") return false;
|
|
1225
|
+
if (child.children.length > 0) return false;
|
|
1226
|
+
|
|
1227
|
+
for (let index = childIndex + 1; index < siblings.length; index += 1) {
|
|
1228
|
+
if (isVisibleTocResultInline(siblings[index])) {
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return false;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function isVisibleTocResultInline(node: InlineNode): boolean {
|
|
1236
|
+
switch (node.type) {
|
|
1237
|
+
case "text":
|
|
1238
|
+
return node.text.length > 0;
|
|
1239
|
+
case "hyperlink":
|
|
1240
|
+
return node.children.some(isVisibleTocResultInline);
|
|
1241
|
+
case "tab":
|
|
1242
|
+
case "hard_break":
|
|
1243
|
+
case "symbol":
|
|
1244
|
+
case "image":
|
|
1245
|
+
case "opaque_inline":
|
|
1246
|
+
case "footnote_ref":
|
|
1247
|
+
case "chart_preview":
|
|
1248
|
+
case "smartart_preview":
|
|
1249
|
+
case "shape":
|
|
1250
|
+
case "wordart":
|
|
1251
|
+
case "vml_shape":
|
|
1252
|
+
case "drawing_frame":
|
|
1253
|
+
case "ole_embed":
|
|
1254
|
+
return true;
|
|
1255
|
+
case "bookmark_start":
|
|
1256
|
+
case "bookmark_end":
|
|
1257
|
+
case "scope_marker_start":
|
|
1258
|
+
case "scope_marker_end":
|
|
1259
|
+
case "field":
|
|
1260
|
+
return false;
|
|
1261
|
+
default:
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1213
1266
|
// `advanceNumberingCounterOnly`, `resolveEffectiveParagraphNumbering`,
|
|
1214
1267
|
// `collectParagraphStyleChain`, `resolveStyleLinkedNumberingLevel`, and
|
|
1215
1268
|
// `buildDirectRunFormattingFromMarks` were removed from surface-projection
|
|
@@ -1374,22 +1427,7 @@ function appendInlineSegments(
|
|
|
1374
1427
|
return { nextCursor: start + 1, lockedFragmentIds: [node.fragmentId] };
|
|
1375
1428
|
}
|
|
1376
1429
|
case "chart_preview": {
|
|
1377
|
-
|
|
1378
|
-
if (node.parsedData) {
|
|
1379
|
-
parsedChartId = stableChartId(node.rawXml);
|
|
1380
|
-
// Always call `set` (even when the entry exists) so the active
|
|
1381
|
-
// `chartModelStore` build pass records the id in its seen-set —
|
|
1382
|
-
// the pass uses the seen-set to evict stale entries from earlier
|
|
1383
|
-
// builds. The entry object is a tiny reference so the set cost
|
|
1384
|
-
// is negligible.
|
|
1385
|
-
const { widthPx, heightPx } = extractChartDimensions(node.rawXml);
|
|
1386
|
-
chartModelStore.set(parsedChartId, {
|
|
1387
|
-
model: node.parsedData,
|
|
1388
|
-
widthPx,
|
|
1389
|
-
heightPx,
|
|
1390
|
-
theme: undefined,
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1430
|
+
const parsedChartId = registerParsedChartPreview(node);
|
|
1393
1431
|
return appendComplexPreviewSegment(paragraph, node, start, "Embedded chart", createChartDetail(node), {
|
|
1394
1432
|
previewMediaId: node.previewMediaId,
|
|
1395
1433
|
parsedChartId,
|
|
@@ -1467,14 +1505,37 @@ function appendInlineSegments(
|
|
|
1467
1505
|
});
|
|
1468
1506
|
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
1469
1507
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1508
|
+
if (c.type === "chart_preview") {
|
|
1509
|
+
const parsedChartId = registerParsedChartPreview(c);
|
|
1510
|
+
return appendComplexPreviewSegment(
|
|
1511
|
+
paragraph,
|
|
1512
|
+
c,
|
|
1513
|
+
start,
|
|
1514
|
+
"Embedded chart",
|
|
1515
|
+
`DrawingFrame chart_preview (${node.anchor.wrapMode}).`,
|
|
1516
|
+
{
|
|
1517
|
+
previewMediaId: c.previewMediaId,
|
|
1518
|
+
parsedChartId,
|
|
1519
|
+
},
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
if (c.type === "smartart_preview") {
|
|
1523
|
+
return appendComplexPreviewSegment(
|
|
1524
|
+
paragraph,
|
|
1525
|
+
c,
|
|
1526
|
+
start,
|
|
1527
|
+
"SmartArt diagram",
|
|
1528
|
+
`DrawingFrame smartart_preview (${node.anchor.wrapMode}).`,
|
|
1529
|
+
{ previewMediaId: c.previewMediaId },
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
return appendComplexPreviewSegment(
|
|
1533
|
+
paragraph,
|
|
1534
|
+
c,
|
|
1535
|
+
start,
|
|
1536
|
+
"Drawing frame",
|
|
1537
|
+
`DrawingFrame ${c.type} (${node.anchor.wrapMode}).`,
|
|
1538
|
+
);
|
|
1478
1539
|
}
|
|
1479
1540
|
case "symbol":
|
|
1480
1541
|
paragraph.segments.push({
|
|
@@ -1530,6 +1591,51 @@ function appendInlineSegments(
|
|
|
1530
1591
|
});
|
|
1531
1592
|
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
1532
1593
|
case "field": {
|
|
1594
|
+
const legacyFormDisplay = legacyFormFieldDisplay(node.legacyFormField);
|
|
1595
|
+
if (legacyFormDisplay && !fieldHasVisibleChildren(node)) {
|
|
1596
|
+
if (legacyFormDisplay.presentation === "checkbox") {
|
|
1597
|
+
paragraph.segments.push({
|
|
1598
|
+
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
1599
|
+
kind: "opaque_inline",
|
|
1600
|
+
from: start,
|
|
1601
|
+
to: start + 1,
|
|
1602
|
+
fragmentId: `legacy-form:${legacyFormDisplay.kind}:${start}`,
|
|
1603
|
+
warningId: `warning:legacy-form:${start}`,
|
|
1604
|
+
label: legacyFormDisplay.label,
|
|
1605
|
+
detail: legacyFormDisplay.detail,
|
|
1606
|
+
presentation: "checkbox",
|
|
1607
|
+
displayText: legacyFormDisplay.text,
|
|
1608
|
+
state: "locked-preserve-only",
|
|
1609
|
+
});
|
|
1610
|
+
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
if (legacyFormDisplay.text.length > 0) {
|
|
1614
|
+
paragraph.segments.push({
|
|
1615
|
+
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
1616
|
+
kind: "text",
|
|
1617
|
+
from: start,
|
|
1618
|
+
to: start + legacyFormDisplay.text.length,
|
|
1619
|
+
text: legacyFormDisplay.text,
|
|
1620
|
+
});
|
|
1621
|
+
return { nextCursor: start + legacyFormDisplay.text.length, lockedFragmentIds: [] };
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
paragraph.segments.push({
|
|
1625
|
+
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
1626
|
+
kind: "opaque_inline",
|
|
1627
|
+
from: start,
|
|
1628
|
+
to: start + 1,
|
|
1629
|
+
fragmentId: `legacy-form:${legacyFormDisplay.kind}:${start}`,
|
|
1630
|
+
warningId: `warning:legacy-form:${start}`,
|
|
1631
|
+
label: legacyFormDisplay.label,
|
|
1632
|
+
detail: legacyFormDisplay.detail,
|
|
1633
|
+
presentation: "quiet-marker",
|
|
1634
|
+
state: "locked-preserve-only",
|
|
1635
|
+
});
|
|
1636
|
+
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1533
1639
|
const isSupportedField =
|
|
1534
1640
|
node.fieldFamily === "REF" ||
|
|
1535
1641
|
node.fieldFamily === "PAGEREF" ||
|
|
@@ -1565,9 +1671,12 @@ function appendInlineSegments(
|
|
|
1565
1671
|
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
1566
1672
|
}
|
|
1567
1673
|
if (node.children && node.children.length > 0) {
|
|
1568
|
-
// For REF \h, pass the bookmark as a hyperlink
|
|
1674
|
+
// For REF/PAGEREF/NOTEREF \h, pass the bookmark as a hyperlink
|
|
1675
|
+
// href so cached result text gets hyperlink styling.
|
|
1569
1676
|
const refHyperlinkHref =
|
|
1570
|
-
node.fieldFamily === "REF"
|
|
1677
|
+
(node.fieldFamily === "REF" ||
|
|
1678
|
+
node.fieldFamily === "PAGEREF" ||
|
|
1679
|
+
node.fieldFamily === "NOTEREF") &&
|
|
1571
1680
|
node.switches?.hyperlink === true &&
|
|
1572
1681
|
node.fieldTarget
|
|
1573
1682
|
? `#${node.fieldTarget}`
|
|
@@ -1643,6 +1752,97 @@ function appendInlineSegments(
|
|
|
1643
1752
|
}
|
|
1644
1753
|
}
|
|
1645
1754
|
|
|
1755
|
+
function registerParsedChartPreview(node: ChartPreviewNode): string | undefined {
|
|
1756
|
+
if (!node.parsedData) return undefined;
|
|
1757
|
+
const parsedChartId = stableChartId(node.rawXml);
|
|
1758
|
+
// Always call `set` (even when the entry exists) so the active
|
|
1759
|
+
// `chartModelStore` build pass records the id in its seen-set; the
|
|
1760
|
+
// pass uses that seen-set to evict stale entries from earlier builds.
|
|
1761
|
+
const { widthPx, heightPx } = extractChartDimensions(node.rawXml);
|
|
1762
|
+
chartModelStore.set(parsedChartId, {
|
|
1763
|
+
model: node.parsedData,
|
|
1764
|
+
widthPx,
|
|
1765
|
+
heightPx,
|
|
1766
|
+
theme: undefined,
|
|
1767
|
+
});
|
|
1768
|
+
return parsedChartId;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function fieldHasVisibleChildren(node: Extract<InlineNode, { type: "field" }>): boolean {
|
|
1772
|
+
return (node.children ?? []).some((child) => inlineNodeHasVisibleText(child));
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function inlineNodeHasVisibleText(node: InlineNode): boolean {
|
|
1776
|
+
switch (node.type) {
|
|
1777
|
+
case "text":
|
|
1778
|
+
return node.text.trim().length > 0;
|
|
1779
|
+
case "tab":
|
|
1780
|
+
case "hard_break":
|
|
1781
|
+
case "image":
|
|
1782
|
+
case "footnote_ref":
|
|
1783
|
+
case "symbol":
|
|
1784
|
+
case "chart_preview":
|
|
1785
|
+
case "smartart_preview":
|
|
1786
|
+
case "shape":
|
|
1787
|
+
case "wordart":
|
|
1788
|
+
case "vml_shape":
|
|
1789
|
+
case "drawing_frame":
|
|
1790
|
+
case "ole_embed":
|
|
1791
|
+
return true;
|
|
1792
|
+
case "hyperlink":
|
|
1793
|
+
case "field":
|
|
1794
|
+
return node.children.some((child) => inlineNodeHasVisibleText(child));
|
|
1795
|
+
default:
|
|
1796
|
+
return false;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
function legacyFormFieldDisplay(
|
|
1801
|
+
legacyFormField: LegacyFormFieldNode | undefined,
|
|
1802
|
+
): {
|
|
1803
|
+
kind: LegacyFormFieldNode["kind"];
|
|
1804
|
+
text: string;
|
|
1805
|
+
label: string;
|
|
1806
|
+
detail: string;
|
|
1807
|
+
presentation?: "checkbox";
|
|
1808
|
+
} | undefined {
|
|
1809
|
+
if (!legacyFormField) return undefined;
|
|
1810
|
+
switch (legacyFormField.kind) {
|
|
1811
|
+
case "textInput": {
|
|
1812
|
+
const value = legacyFormField.textInput?.default;
|
|
1813
|
+
return {
|
|
1814
|
+
kind: legacyFormField.kind,
|
|
1815
|
+
text: value && value.length > 0 ? value : "",
|
|
1816
|
+
label: "Legacy text form field",
|
|
1817
|
+
detail: "Word legacy text form field preserved from ffData.",
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
case "checkBox": {
|
|
1821
|
+
const checked =
|
|
1822
|
+
legacyFormField.checkBox?.checked ??
|
|
1823
|
+
legacyFormField.checkBox?.default ??
|
|
1824
|
+
false;
|
|
1825
|
+
return {
|
|
1826
|
+
kind: legacyFormField.kind,
|
|
1827
|
+
text: checked ? "☒" : "☐",
|
|
1828
|
+
label: "Legacy checkbox form field",
|
|
1829
|
+
detail: "Word legacy checkbox form field preserved from ffData.",
|
|
1830
|
+
presentation: "checkbox",
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
case "ddList": {
|
|
1834
|
+
const entries = legacyFormField.ddList?.listEntry ?? [];
|
|
1835
|
+
const index = legacyFormField.ddList?.default ?? 0;
|
|
1836
|
+
return {
|
|
1837
|
+
kind: legacyFormField.kind,
|
|
1838
|
+
text: entries[index] ?? entries[0] ?? "",
|
|
1839
|
+
label: "Legacy dropdown form field",
|
|
1840
|
+
detail: "Word legacy dropdown form field preserved from ffData.",
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1646
1846
|
/**
|
|
1647
1847
|
* V2c.4 — Map a canonical `AnchorGeometry` onto the surface anchor type.
|
|
1648
1848
|
* Returns `undefined` when the anchor carries only the trivial inline
|
|
@@ -1486,7 +1486,12 @@ export async function persistAndExport(input: {
|
|
|
1486
1486
|
const saveExport = input.hostAdapter?.saveExport ?? input.datastore?.saveExport;
|
|
1487
1487
|
const saveExportSource = input.hostAdapter?.saveExport ? "host" : "datastore";
|
|
1488
1488
|
if (!saveExport) {
|
|
1489
|
-
result =
|
|
1489
|
+
result =
|
|
1490
|
+
input.options?.download === false
|
|
1491
|
+
? withExportDelivery(result, {
|
|
1492
|
+
mode: "exported-bytes-only",
|
|
1493
|
+
})
|
|
1494
|
+
: downloadExportResult(result);
|
|
1490
1495
|
emitEditorEvent({
|
|
1491
1496
|
hostAdapter: input.hostAdapter,
|
|
1492
1497
|
datastore: input.datastore,
|