@beyondwork/docx-react-component 1.0.53 → 1.0.55
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 +125 -7
- package/src/index.ts +5 -0
- package/src/io/docx-session.ts +27 -3
- package/src/io/normalize/normalize-text.ts +1 -0
- package/src/io/ooxml/parse-field-switches.ts +134 -0
- package/src/io/ooxml/parse-fields.ts +28 -2
- package/src/model/canonical-document.ts +13 -2
- package/src/runtime/chart/chart-model-store.ts +88 -0
- package/src/runtime/chart/chart-snapshot.ts +239 -0
- package/src/runtime/collab/checkpoint-store.ts +1 -1
- package/src/runtime/collab/event-types.ts +4 -0
- package/src/runtime/collab/runtime-collab-sync.ts +1 -2
- package/src/runtime/document-runtime.ts +115 -13
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/layout-engine-version.ts +58 -1
- package/src/runtime/layout/layout-invalidation.ts +150 -30
- package/src/runtime/layout/page-graph.ts +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +128 -19
- package/src/runtime/layout/project-block-fragments.ts +27 -0
- package/src/runtime/layout/public-facet.ts +27 -0
- package/src/runtime/page-number-format.ts +207 -0
- package/src/runtime/render/render-frame-diff.ts +38 -2
- package/src/runtime/surface-projection.ts +32 -3
- package/src/ui/WordReviewEditor.tsx +57 -3
- package/src/ui/headless/comment-decoration-model.ts +60 -5
- package/src/ui/headless/revision-decoration-model.ts +94 -6
- package/src/ui/shared/revision-filters.ts +16 -6
- package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
- package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
- package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
- package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
- package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
- package/src/ui-tailwind/chart/render/area.tsx +277 -0
- package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
- package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
- package/src/ui-tailwind/chart/render/combo.tsx +85 -0
- package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
- package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
- package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
- package/src/ui-tailwind/chart/render/line.tsx +363 -0
- package/src/ui-tailwind/chart/render/number-format.ts +120 -16
- package/src/ui-tailwind/chart/render/pie.tsx +275 -0
- package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
- package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
- package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
- package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
- package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
- package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
- package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
- package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
- package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
- package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
- package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
- package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +90 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +4 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +14 -0
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +2 -1
- package/src/ui-tailwind/index.ts +11 -0
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +52 -2
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +13 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +13 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
- package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
- package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
- package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
- package/src/ui-tailwind/theme/editor-theme.css +249 -22
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +0 -85
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public read-model for chart data. Projected from a `ChartModel` parsed during
|
|
3
|
+
* DOCX import. Agents consume this without re-parsing `rawXml`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ChartModel,
|
|
8
|
+
BarChartModel,
|
|
9
|
+
LineChartModel,
|
|
10
|
+
PieChartModel,
|
|
11
|
+
AreaChartModel,
|
|
12
|
+
ScatterChartModel,
|
|
13
|
+
BubbleChartModel,
|
|
14
|
+
ComboChartModel,
|
|
15
|
+
} from "../../io/ooxml/chart/types.ts";
|
|
16
|
+
|
|
17
|
+
export interface ChartSnapshot {
|
|
18
|
+
chartId: string;
|
|
19
|
+
kind: ChartModel["kind"];
|
|
20
|
+
title?: string;
|
|
21
|
+
seriesCount: number;
|
|
22
|
+
categoryCount: number;
|
|
23
|
+
data: ChartSnapshotData;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ChartSnapshotData =
|
|
27
|
+
| {
|
|
28
|
+
kind: "bar";
|
|
29
|
+
direction: "bar" | "column";
|
|
30
|
+
grouping: "clustered" | "stacked" | "percentStacked" | "standard";
|
|
31
|
+
series: ChartSnapshotSeries[];
|
|
32
|
+
categories: string[];
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
kind: "line";
|
|
36
|
+
grouping: "standard" | "stacked" | "percentStacked";
|
|
37
|
+
series: ChartSnapshotSeries[];
|
|
38
|
+
categories: string[];
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
kind: "pie";
|
|
42
|
+
doughnut: boolean;
|
|
43
|
+
series: ChartSnapshotSeries[];
|
|
44
|
+
categories: string[];
|
|
45
|
+
}
|
|
46
|
+
| {
|
|
47
|
+
kind: "area";
|
|
48
|
+
grouping: "standard" | "stacked" | "percentStacked";
|
|
49
|
+
series: ChartSnapshotSeries[];
|
|
50
|
+
categories: string[];
|
|
51
|
+
}
|
|
52
|
+
| { kind: "scatter"; series: ChartSnapshotScatterSeries[] }
|
|
53
|
+
| { kind: "bubble"; series: ChartSnapshotBubbleSeries[] }
|
|
54
|
+
| {
|
|
55
|
+
kind: "combo";
|
|
56
|
+
groups: Array<{ kind: "bar" | "line" | "area"; series: ChartSnapshotSeries[] }>;
|
|
57
|
+
categories: string[];
|
|
58
|
+
}
|
|
59
|
+
| { kind: "unsupported"; reason: string };
|
|
60
|
+
|
|
61
|
+
export interface ChartSnapshotSeries {
|
|
62
|
+
name?: string;
|
|
63
|
+
values: Array<number | null>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ChartSnapshotScatterSeries {
|
|
67
|
+
name?: string;
|
|
68
|
+
xValues: Array<number | null>;
|
|
69
|
+
yValues: Array<number | null>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ChartSnapshotBubbleSeries {
|
|
73
|
+
name?: string;
|
|
74
|
+
xValues: Array<number | null>;
|
|
75
|
+
yValues: Array<number | null>;
|
|
76
|
+
sizes: Array<number | null>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function projectChartSnapshot(chartId: string, model: ChartModel): ChartSnapshot {
|
|
80
|
+
switch (model.kind) {
|
|
81
|
+
case "bar": {
|
|
82
|
+
const m = model as BarChartModel;
|
|
83
|
+
const categories = m.categoryAxis.kind === "category"
|
|
84
|
+
? m.categoryAxis.categoryLabels
|
|
85
|
+
: (m.series[0]?.categories ?? []);
|
|
86
|
+
return {
|
|
87
|
+
chartId,
|
|
88
|
+
kind: "bar",
|
|
89
|
+
title: m.title?.text,
|
|
90
|
+
seriesCount: m.series.length,
|
|
91
|
+
categoryCount: categories.length,
|
|
92
|
+
data: {
|
|
93
|
+
kind: "bar",
|
|
94
|
+
direction: m.direction,
|
|
95
|
+
grouping: m.grouping,
|
|
96
|
+
series: m.series.map((s) => ({ name: s.name, values: s.values })),
|
|
97
|
+
categories,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
case "line": {
|
|
102
|
+
const m = model as LineChartModel;
|
|
103
|
+
const categories = m.categoryAxis.kind === "category"
|
|
104
|
+
? m.categoryAxis.categoryLabels
|
|
105
|
+
: (m.series[0]?.categories ?? []);
|
|
106
|
+
return {
|
|
107
|
+
chartId,
|
|
108
|
+
kind: "line",
|
|
109
|
+
title: m.title?.text,
|
|
110
|
+
seriesCount: m.series.length,
|
|
111
|
+
categoryCount: categories.length,
|
|
112
|
+
data: {
|
|
113
|
+
kind: "line",
|
|
114
|
+
grouping: m.grouping,
|
|
115
|
+
series: m.series.map((s) => ({ name: s.name, values: s.values })),
|
|
116
|
+
categories,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
case "pie": {
|
|
121
|
+
const m = model as PieChartModel;
|
|
122
|
+
return {
|
|
123
|
+
chartId,
|
|
124
|
+
kind: "pie",
|
|
125
|
+
title: m.title?.text,
|
|
126
|
+
seriesCount: m.series.length,
|
|
127
|
+
categoryCount: m.categoryLabels.length,
|
|
128
|
+
data: {
|
|
129
|
+
kind: "pie",
|
|
130
|
+
doughnut: m.doughnut,
|
|
131
|
+
series: m.series.map((s) => ({ name: s.name, values: s.values })),
|
|
132
|
+
categories: m.categoryLabels,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
case "area": {
|
|
137
|
+
const m = model as AreaChartModel;
|
|
138
|
+
const categories = m.categoryAxis.kind === "category"
|
|
139
|
+
? m.categoryAxis.categoryLabels
|
|
140
|
+
: (m.series[0]?.categories ?? []);
|
|
141
|
+
return {
|
|
142
|
+
chartId,
|
|
143
|
+
kind: "area",
|
|
144
|
+
title: m.title?.text,
|
|
145
|
+
seriesCount: m.series.length,
|
|
146
|
+
categoryCount: categories.length,
|
|
147
|
+
data: {
|
|
148
|
+
kind: "area",
|
|
149
|
+
grouping: m.grouping,
|
|
150
|
+
series: m.series.map((s) => ({ name: s.name, values: s.values })),
|
|
151
|
+
categories,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
case "scatter": {
|
|
156
|
+
const m = model as ScatterChartModel;
|
|
157
|
+
return {
|
|
158
|
+
chartId,
|
|
159
|
+
kind: "scatter",
|
|
160
|
+
seriesCount: m.series.length,
|
|
161
|
+
categoryCount: 0,
|
|
162
|
+
data: {
|
|
163
|
+
kind: "scatter",
|
|
164
|
+
series: m.series.map((s) => ({
|
|
165
|
+
name: s.name,
|
|
166
|
+
xValues: s.xValues,
|
|
167
|
+
yValues: s.yValues,
|
|
168
|
+
})),
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
case "bubble": {
|
|
173
|
+
const m = model as BubbleChartModel;
|
|
174
|
+
return {
|
|
175
|
+
chartId,
|
|
176
|
+
kind: "bubble",
|
|
177
|
+
seriesCount: m.series.length,
|
|
178
|
+
categoryCount: 0,
|
|
179
|
+
data: {
|
|
180
|
+
kind: "bubble",
|
|
181
|
+
series: m.series.map((s) => ({
|
|
182
|
+
name: s.name,
|
|
183
|
+
xValues: s.xValues,
|
|
184
|
+
yValues: s.yValues,
|
|
185
|
+
sizes: s.sizes,
|
|
186
|
+
})),
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
case "combo": {
|
|
191
|
+
const m = model as ComboChartModel;
|
|
192
|
+
const firstGroup = m.groups[0];
|
|
193
|
+
const categories =
|
|
194
|
+
firstGroup && "categoryAxis" in firstGroup &&
|
|
195
|
+
firstGroup.categoryAxis?.kind === "category"
|
|
196
|
+
? firstGroup.categoryAxis.categoryLabels
|
|
197
|
+
: [];
|
|
198
|
+
return {
|
|
199
|
+
chartId,
|
|
200
|
+
kind: "combo",
|
|
201
|
+
title: m.title?.text,
|
|
202
|
+
seriesCount: m.groups.reduce((acc, g) => acc + g.series.length, 0),
|
|
203
|
+
categoryCount: categories.length,
|
|
204
|
+
data: {
|
|
205
|
+
kind: "combo",
|
|
206
|
+
groups: m.groups.map((g) => ({
|
|
207
|
+
kind: g.kind as "bar" | "line" | "area",
|
|
208
|
+
series: "series" in g
|
|
209
|
+
? g.series.map((s) => ({
|
|
210
|
+
name: s.name,
|
|
211
|
+
values: "values" in s ? s.values : [],
|
|
212
|
+
}))
|
|
213
|
+
: [],
|
|
214
|
+
})),
|
|
215
|
+
categories,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
case "unsupported": {
|
|
220
|
+
return {
|
|
221
|
+
chartId,
|
|
222
|
+
kind: "unsupported",
|
|
223
|
+
seriesCount: 0,
|
|
224
|
+
categoryCount: 0,
|
|
225
|
+
data: { kind: "unsupported", reason: model.detail },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
default: {
|
|
229
|
+
const _exhaustive: never = model;
|
|
230
|
+
return {
|
|
231
|
+
chartId,
|
|
232
|
+
kind: (_exhaustive as ChartModel).kind,
|
|
233
|
+
seriesCount: 0,
|
|
234
|
+
categoryCount: 0,
|
|
235
|
+
data: { kind: "unsupported", reason: "unknown chart kind" },
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -141,6 +141,10 @@ export const BROADCAST_COMMAND_TYPES: ReadonlySet<EditorCommand["type"]> = new S
|
|
|
141
141
|
"section.set-header-footer-link",
|
|
142
142
|
"content.insert-page-break",
|
|
143
143
|
"content.insert-table",
|
|
144
|
+
// C1: Shift+Tab list/paragraph de-indent — produces a document mutation, must broadcast
|
|
145
|
+
"text.outdent-tab",
|
|
146
|
+
// C2: host insertFragment() API — routes through executeEditorCommand same as other mutations
|
|
147
|
+
"fragment.insert",
|
|
144
148
|
]);
|
|
145
149
|
|
|
146
150
|
/**
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type CommandEvent,
|
|
23
23
|
} from "./event-types.ts";
|
|
24
24
|
import { computeBaseDocFingerprint } from "./base-doc-fingerprint.ts";
|
|
25
|
-
import type
|
|
25
|
+
import { CHECKPOINTS_KEY, type Checkpoint } from "./checkpoint-store.ts";
|
|
26
26
|
import { createWorkflowShared, type WorkflowSharedHandle } from "./workflow-shared.ts";
|
|
27
27
|
|
|
28
28
|
/** Shared Y.Map key — {@link SHARED_META_MAP_KEY}. */
|
|
@@ -30,7 +30,6 @@ const SHARED_META_MAP_KEY = "meta";
|
|
|
30
30
|
const META_BASE_DOC_HASH_KEY = "baseDocHash";
|
|
31
31
|
const META_SCHEMA_VERSION_KEY = "schemaVersion";
|
|
32
32
|
const META_CREATED_AT_KEY = "createdAt";
|
|
33
|
-
const CHECKPOINTS_KEY = "checkpoints";
|
|
34
33
|
|
|
35
34
|
/**
|
|
36
35
|
* Lifecycle + correctness events surfaced by a
|
|
@@ -242,6 +242,7 @@ import type {
|
|
|
242
242
|
import { collectEditorStateForSerialize } from "./editor-state-integration.ts";
|
|
243
243
|
import type { EditorStatePayload } from "../io/ooxml/workflow-payload.ts";
|
|
244
244
|
import type { SharedWorkflowState } from "./collab/workflow-shared.ts";
|
|
245
|
+
import { formatPageNumber } from "./page-number-format.ts";
|
|
245
246
|
|
|
246
247
|
/** Internal extension of ExportDocxOptions that threads the collected
|
|
247
248
|
* editorState payload from the runtime to the docx serializer. */
|
|
@@ -5196,8 +5197,34 @@ function refreshDocumentTableOfContents(
|
|
|
5196
5197
|
protectionSelection?: import("../core/state/editor-state.ts").SelectionSnapshot;
|
|
5197
5198
|
} {
|
|
5198
5199
|
const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
|
|
5200
|
+
// Build a single O(N) map from paragraph offset → bookmark name so the
|
|
5201
|
+
// per-heading lookup below is O(1) instead of O(N) per heading.
|
|
5202
|
+
const bookmarkNameByOffset = new Map<number, string>();
|
|
5203
|
+
{
|
|
5204
|
+
let runningOffset = 0;
|
|
5205
|
+
for (const block of document.content.children) {
|
|
5206
|
+
if (block.type !== "paragraph") {
|
|
5207
|
+
continue;
|
|
5208
|
+
}
|
|
5209
|
+
const paragraphStart = runningOffset;
|
|
5210
|
+
for (const child of block.children) {
|
|
5211
|
+
if (child.type === "text") {
|
|
5212
|
+
runningOffset += child.text.length;
|
|
5213
|
+
} else if (child.type === "tab" || child.type === "hard_break") {
|
|
5214
|
+
runningOffset += 1;
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
const bookmarkStart = block.children.find(
|
|
5218
|
+
(child): child is Extract<typeof child, { type: "bookmark_start" }> =>
|
|
5219
|
+
child.type === "bookmark_start" && Boolean(child.name),
|
|
5220
|
+
);
|
|
5221
|
+
if (bookmarkStart?.name) {
|
|
5222
|
+
bookmarkNameByOffset.set(paragraphStart, bookmarkStart.name);
|
|
5223
|
+
}
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
5199
5226
|
let changed = false;
|
|
5200
|
-
let resultEntries: Array<{ level: number; text: string; pageIndex: number }> = [];
|
|
5227
|
+
let resultEntries: Array<{ level: number; text: string; pageIndex: number; bookmarkName?: string }> = [];
|
|
5201
5228
|
let changedFrom: number | undefined;
|
|
5202
5229
|
let changedTo: number | undefined;
|
|
5203
5230
|
const nextChildren = refreshBlocksWithCursor(document.content.children, (field, range) => {
|
|
@@ -5209,11 +5236,15 @@ function refreshDocumentTableOfContents(
|
|
|
5209
5236
|
: parseTocLevelRange(field.instruction);
|
|
5210
5237
|
const entries = navigation.headings
|
|
5211
5238
|
.filter((heading) => heading.level >= levelRange.from && heading.level <= levelRange.to)
|
|
5212
|
-
.map((heading) =>
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5239
|
+
.map((heading) => {
|
|
5240
|
+
const bookmarkName = bookmarkNameByOffset.get(heading.offset);
|
|
5241
|
+
return {
|
|
5242
|
+
level: heading.level,
|
|
5243
|
+
text: heading.text,
|
|
5244
|
+
pageIndex: heading.pageIndex,
|
|
5245
|
+
...(bookmarkName ? { bookmarkName } : {}),
|
|
5246
|
+
};
|
|
5247
|
+
});
|
|
5217
5248
|
if (resultEntries.length === 0) {
|
|
5218
5249
|
resultEntries = entries;
|
|
5219
5250
|
}
|
|
@@ -5410,12 +5441,20 @@ function buildInlineNodesFromDisplayText(text: string): InlineNode[] {
|
|
|
5410
5441
|
* resolver, falls back to the raw `pageIndex + 1` (pre-P5 behavior).
|
|
5411
5442
|
*/
|
|
5412
5443
|
function buildTocInlineNodes(
|
|
5413
|
-
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
|
|
5444
|
+
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number; bookmarkName?: string }>,
|
|
5414
5445
|
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
5415
5446
|
): InlineNode[] {
|
|
5416
5447
|
const children: InlineNode[] = [];
|
|
5417
5448
|
entries.forEach((entry, index) => {
|
|
5418
|
-
|
|
5449
|
+
if (entry.bookmarkName) {
|
|
5450
|
+
children.push({
|
|
5451
|
+
type: "hyperlink",
|
|
5452
|
+
href: `#${entry.bookmarkName}`,
|
|
5453
|
+
children: [{ type: "text", text: entry.text }],
|
|
5454
|
+
});
|
|
5455
|
+
} else {
|
|
5456
|
+
children.push({ type: "text", text: entry.text });
|
|
5457
|
+
}
|
|
5419
5458
|
children.push({ type: "tab" });
|
|
5420
5459
|
const displayed = resolveDisplayPageNumber?.(entry.pageIndex);
|
|
5421
5460
|
children.push({
|
|
@@ -5431,7 +5470,7 @@ function buildTocInlineNodes(
|
|
|
5431
5470
|
|
|
5432
5471
|
/** Test-only export of `buildTocInlineNodes` (P5 unit tests). */
|
|
5433
5472
|
export function __buildTocInlineNodes(
|
|
5434
|
-
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
|
|
5473
|
+
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number; bookmarkName?: string }>,
|
|
5435
5474
|
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
5436
5475
|
): InlineNode[] {
|
|
5437
5476
|
return buildTocInlineNodes(entries, resolveDisplayPageNumber);
|
|
@@ -5471,6 +5510,38 @@ function collectFieldsFromSubParts(
|
|
|
5471
5510
|
return nextIndex;
|
|
5472
5511
|
}
|
|
5473
5512
|
|
|
5513
|
+
function resolveStyleRefFieldText(
|
|
5514
|
+
styleQuery: string,
|
|
5515
|
+
paragraphs: readonly ParagraphContext[],
|
|
5516
|
+
styles: CanonicalDocumentEnvelope["styles"],
|
|
5517
|
+
): { text: string; refreshStatus: FieldRefreshStatus } {
|
|
5518
|
+
const normalized = styleQuery.trim().toLowerCase();
|
|
5519
|
+
|
|
5520
|
+
// Look up styleId: first by direct id match, then by displayName
|
|
5521
|
+
const styleId = (() => {
|
|
5522
|
+
const byId = Object.keys(styles.paragraphs).find(
|
|
5523
|
+
(id) => id.toLowerCase() === normalized,
|
|
5524
|
+
);
|
|
5525
|
+
if (byId) return byId;
|
|
5526
|
+
const byName = Object.values(styles.paragraphs).find(
|
|
5527
|
+
(s) => s.displayName?.toLowerCase() === normalized,
|
|
5528
|
+
);
|
|
5529
|
+
return byName?.styleId;
|
|
5530
|
+
})();
|
|
5531
|
+
|
|
5532
|
+
if (!styleId) return { text: "", refreshStatus: "unresolvable" };
|
|
5533
|
+
|
|
5534
|
+
// Walk paragraphs top-down for the first matching styleId
|
|
5535
|
+
for (const ctx of paragraphs) {
|
|
5536
|
+
if (ctx.paragraph.styleId === styleId) {
|
|
5537
|
+
const text = flattenInlineDisplayText(ctx.paragraph.children);
|
|
5538
|
+
if (text.trim()) return { text: text.trim(), refreshStatus: "current" };
|
|
5539
|
+
}
|
|
5540
|
+
}
|
|
5541
|
+
|
|
5542
|
+
return { text: "", refreshStatus: "unresolvable" };
|
|
5543
|
+
}
|
|
5544
|
+
|
|
5474
5545
|
function resolveSupportedFieldDisplay(
|
|
5475
5546
|
field: FieldNode,
|
|
5476
5547
|
document: CanonicalDocumentEnvelope,
|
|
@@ -5485,13 +5556,30 @@ function resolveSupportedFieldDisplay(
|
|
|
5485
5556
|
if (field.fieldFamily === "TOC") {
|
|
5486
5557
|
return undefined;
|
|
5487
5558
|
}
|
|
5559
|
+
if (field.fieldFamily === "STYLEREF") {
|
|
5560
|
+
if (!field.fieldTarget) return { displayText: "", refreshStatus: "unresolvable" };
|
|
5561
|
+
const result = resolveStyleRefFieldText(field.fieldTarget, paragraphs, document.styles);
|
|
5562
|
+
return { displayText: result.text, refreshStatus: result.refreshStatus };
|
|
5563
|
+
}
|
|
5564
|
+
if (field.fieldFamily === "SECTIONPAGES") {
|
|
5565
|
+
const sectionIndex = "sectionIndex" in storyTarget && typeof storyTarget.sectionIndex === "number"
|
|
5566
|
+
? storyTarget.sectionIndex
|
|
5567
|
+
: navigation.activeSectionIndex;
|
|
5568
|
+
const sectionPages = navigation.pages.filter((p) => p.sectionIndex === sectionIndex);
|
|
5569
|
+
if (sectionPages.length === 0) return { displayText: "", refreshStatus: "unresolvable" };
|
|
5570
|
+
const fmt = sectionPages[0]?.layout.pageNumbering?.format;
|
|
5571
|
+
return {
|
|
5572
|
+
displayText: formatPageNumber(sectionPages.length, fmt),
|
|
5573
|
+
refreshStatus: "current",
|
|
5574
|
+
};
|
|
5575
|
+
}
|
|
5488
5576
|
if (field.fieldFamily === "PAGE") {
|
|
5489
5577
|
const page = resolveRepresentativePageForStory(navigation, storyTarget);
|
|
5490
5578
|
if (!page) {
|
|
5491
5579
|
return { displayText: "", refreshStatus: "unresolvable" };
|
|
5492
5580
|
}
|
|
5493
5581
|
return {
|
|
5494
|
-
displayText:
|
|
5582
|
+
displayText: resolveDisplayedPageNumber(page),
|
|
5495
5583
|
refreshStatus: "current",
|
|
5496
5584
|
};
|
|
5497
5585
|
}
|
|
@@ -5522,10 +5610,23 @@ function resolveSupportedFieldDisplay(
|
|
|
5522
5610
|
if (!paragraph) {
|
|
5523
5611
|
return { displayText: "", refreshStatus: "unresolvable" };
|
|
5524
5612
|
}
|
|
5613
|
+
|
|
5614
|
+
// \p switch: emit relative position text ("above" / "below" / "on this page")
|
|
5615
|
+
if (field.switches?.relativePosition === true) {
|
|
5616
|
+
const fieldPage = resolveRepresentativePageForStory(navigation, storyTarget);
|
|
5617
|
+
const targetPageIndex = findPageForOffset(navigation.pages, paragraph.startOffset);
|
|
5618
|
+
const fieldPageIndex = fieldPage
|
|
5619
|
+
? navigation.pages.indexOf(fieldPage)
|
|
5620
|
+
: navigation.activePageIndex;
|
|
5621
|
+
if (targetPageIndex < fieldPageIndex) return { displayText: "above", refreshStatus: "current" };
|
|
5622
|
+
if (targetPageIndex > fieldPageIndex) return { displayText: "below", refreshStatus: "current" };
|
|
5623
|
+
return { displayText: "on this page", refreshStatus: "current" };
|
|
5624
|
+
}
|
|
5625
|
+
|
|
5525
5626
|
const pageIndex = findPageForOffset(navigation.pages, paragraph.startOffset);
|
|
5526
5627
|
const page = navigation.pages[pageIndex] ?? navigation.pages[0];
|
|
5527
5628
|
return page
|
|
5528
|
-
? { displayText:
|
|
5629
|
+
? { displayText: resolveDisplayedPageNumber(page), refreshStatus: "current" }
|
|
5529
5630
|
: { displayText: "", refreshStatus: "unresolvable" };
|
|
5530
5631
|
}
|
|
5531
5632
|
if (field.fieldFamily === "NOTEREF") {
|
|
@@ -5584,8 +5685,9 @@ function isDefaultHeaderFooterPage(
|
|
|
5584
5685
|
|
|
5585
5686
|
function resolveDisplayedPageNumber(
|
|
5586
5687
|
page: DocumentNavigationSnapshot["pages"][number],
|
|
5587
|
-
):
|
|
5588
|
-
|
|
5688
|
+
): string {
|
|
5689
|
+
const n = (page.layout.pageNumbering?.start ?? 1) + page.pageInSection;
|
|
5690
|
+
return formatPageNumber(n, page.layout.pageNumbering?.format);
|
|
5589
5691
|
}
|
|
5590
5692
|
|
|
5591
5693
|
interface ParagraphContext {
|
|
@@ -59,6 +59,7 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
|
|
|
59
59
|
swapMeasurementProvider: () => undefined,
|
|
60
60
|
invalidateMeasurementCache: () => undefined,
|
|
61
61
|
getTableRenderPlan: () => null,
|
|
62
|
+
getTableBodyYOffsetOnPage: () => null,
|
|
62
63
|
getDirtyFieldFamilies: () => [],
|
|
63
64
|
getFieldDirtinessReport: () => emptyReport,
|
|
64
65
|
setVisibleBlockRange: () => undefined,
|
|
@@ -113,6 +113,13 @@
|
|
|
113
113
|
* pages so chrome can prepend header rows visually. No
|
|
114
114
|
* pixel-geometry change; cache envelopes from v11 invalidate
|
|
115
115
|
* because the table-render-plan contract changed.
|
|
116
|
+
* 13 — Lane 6d.U2 canvas-seam pill polish: the canvas-posture page-break
|
|
117
|
+
* widget's "N / M" badge is promoted from transparent text over the
|
|
118
|
+
* dotted seam to a true pill with `--radius-pill` geometry, hairline
|
|
119
|
+
* `--color-border-default` border, and `--shadow-soft`. Widget DOM
|
|
120
|
+
* shape changed (new `data-variant="pill"` attribute; additional
|
|
121
|
+
* inline style declarations on the badge). Cache envelopes from v12
|
|
122
|
+
* invalidate because the decoration's cacheable DOM shape changed.
|
|
116
123
|
* 13 — Lane 3a P14.c: render-kernel gains a single-slot `DecorationIndex`
|
|
117
124
|
* cache keyed on (revision, activeStory.kind, zoom.pxPerTwip, and
|
|
118
125
|
* reference equality on each decoration source). When layout
|
|
@@ -122,8 +129,58 @@
|
|
|
122
129
|
* rebuild path (on every keystroke that triggers a layout event).
|
|
123
130
|
* No pixel-geometry change; cache envelopes from v12 invalidate
|
|
124
131
|
* because the render-kernel source changed.
|
|
132
|
+
* 14 — Lane 3a Slice 5: `RuntimeBlockFragment` gains `resolvedStyleChainRef`
|
|
133
|
+
* (block's styleId) and `numberingInstanceId` (block's list-instance id).
|
|
134
|
+
* `analyzeInvalidation` for `styles-change` (when `dirtyStyleIds` is
|
|
135
|
+
* supplied) and `numbering-change` (when `numberingInstanceId` is
|
|
136
|
+
* supplied) now return `scope: "bounded"` starting from the first page
|
|
137
|
+
* whose fragments reference the dirty style / instance. Fallback to
|
|
138
|
+
* `scope: "full"` when payload is absent or no match found. No
|
|
139
|
+
* pixel-geometry change; cache envelopes from v13 invalidate because
|
|
140
|
+
* the fragment shape and invalidation-scope contract changed.
|
|
141
|
+
* 15 — Bug fixes: `pageNodesStructurallyEqual` now compares
|
|
142
|
+
* `lineBoxes.length` and `noteAllocations.length` as structural
|
|
143
|
+
* proxies to prevent stale-node reuse when line geometry changes
|
|
144
|
+
* with stable fragment IDs (L1). `analyzeSectionChange` normalizes
|
|
145
|
+
* `dirtySectionRange` to guarantee from ≤ to for all graph states
|
|
146
|
+
* including empty-sections fallback (L2).
|
|
147
|
+
* 16 — Bug fixes: `diffRenderFrames` now flags pages whose physical frame
|
|
148
|
+
* changed (but block regions are stable) with `pageFrameChanged: true`
|
|
149
|
+
* in `changedPages` so consumers can re-project without a block-region
|
|
150
|
+
* signal (R1). Chrome reservation changes (`railLaneTwips`,
|
|
151
|
+
* `balloonLaneTwips`, `footnoteAreaTwips`, `pageFrameWidthPx`,
|
|
152
|
+
* `pageFrameHeightPx`) now trigger `changedPages` so overlay
|
|
153
|
+
* re-projection is not silently skipped (R2).
|
|
154
|
+
* 17 — Lane 3a Slice 2 + R4: `WordReviewEditorLayoutFacet` gains
|
|
155
|
+
* `getTableBodyYOffsetOnPage(blockId, pageIndex)` which returns the
|
|
156
|
+
* Y offset (in twips from body top) of the table's first fragment on
|
|
157
|
+
* a given page by summing prior body-fragment heights. Used by the
|
|
158
|
+
* new `TwTableContinuationHeader` chrome overlay to position repeated
|
|
159
|
+
* header rows on continuation pages of multi-page tables — no DOM
|
|
160
|
+
* measurement, layout-engine fragment heights only. No cached-geometry
|
|
161
|
+
* change; cache envelopes from v16 invalidate because the facet
|
|
162
|
+
* interface changed.
|
|
163
|
+
* 18 — Lane 3a Slice 6: `buildPageStackFromWithSplits` no longer discards
|
|
164
|
+
* `resumeAt.startOffset`. When `startOffset > 0` and no block
|
|
165
|
+
* straddles the dirty section boundary, only sections at and after
|
|
166
|
+
* the first dirty section are paginated; the resulting page indices
|
|
167
|
+
* are shifted by `startPageIndex` so they align with the global graph.
|
|
168
|
+
* Full-paginate + tail-slice fallback used when a block straddles the
|
|
169
|
+
* section boundary (safety guard). This eliminates re-paginating
|
|
170
|
+
* settled head sections on every bounded-invalidation relayout.
|
|
171
|
+
* No pixel-geometry change; cache envelopes from v17 invalidate
|
|
172
|
+
* because `buildPageStackFromWithSplits` output contract changed.
|
|
173
|
+
* 19 — Slice 5 bug-fix: `analyzeNumberingChange` now honors its own
|
|
174
|
+
* "Fallback to full rebuild when absent or no match" contract. When
|
|
175
|
+
* `numberingInstanceId` is supplied but no materialized fragment
|
|
176
|
+
* matches it, the analyzer returns `scope: "full"` +
|
|
177
|
+
* `requiresFullRecompute: true` instead of the prior "bounded over
|
|
178
|
+
* full range" shortcut, which bypassed the safety guard and could
|
|
179
|
+
* leak stale field-family projections. No pixel-geometry change;
|
|
180
|
+
* cache envelopes from v18 invalidate because the invalidation
|
|
181
|
+
* classifier's contract corrected.
|
|
125
182
|
*/
|
|
126
|
-
export const LAYOUT_ENGINE_VERSION =
|
|
183
|
+
export const LAYOUT_ENGINE_VERSION = 19 as const;
|
|
127
184
|
|
|
128
185
|
/**
|
|
129
186
|
* Serialization schema version for the LayCache payload (the cache envelope
|