@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,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OOXML page number format codes (ECMA-376 §17.18.59).
|
|
3
|
+
*
|
|
4
|
+
* This module is intentionally dependency-free so it can be unit-tested
|
|
5
|
+
* without any runtime or model imports.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const ROMAN_PAIRS: [number, string][] = [
|
|
9
|
+
[1000, "M"],
|
|
10
|
+
[900, "CM"],
|
|
11
|
+
[500, "D"],
|
|
12
|
+
[400, "CD"],
|
|
13
|
+
[100, "C"],
|
|
14
|
+
[90, "XC"],
|
|
15
|
+
[50, "L"],
|
|
16
|
+
[40, "XL"],
|
|
17
|
+
[10, "X"],
|
|
18
|
+
[9, "IX"],
|
|
19
|
+
[5, "V"],
|
|
20
|
+
[4, "IV"],
|
|
21
|
+
[1, "I"],
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function toRoman(n: number): string {
|
|
25
|
+
if (n <= 0 || n >= 4000) {
|
|
26
|
+
return String(n);
|
|
27
|
+
}
|
|
28
|
+
let result = "";
|
|
29
|
+
let remainder = n;
|
|
30
|
+
for (const [value, numeral] of ROMAN_PAIRS) {
|
|
31
|
+
while (remainder >= value) {
|
|
32
|
+
result += numeral;
|
|
33
|
+
remainder -= value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toAlphabetic(n: number): string {
|
|
40
|
+
if (n <= 0) {
|
|
41
|
+
return String(n);
|
|
42
|
+
}
|
|
43
|
+
let result = "";
|
|
44
|
+
let remainder = n;
|
|
45
|
+
while (remainder > 0) {
|
|
46
|
+
remainder -= 1;
|
|
47
|
+
result = String.fromCharCode(65 + (remainder % 26)) + result;
|
|
48
|
+
remainder = Math.floor(remainder / 26);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toOrdinal(n: number): string {
|
|
54
|
+
const abs = Math.abs(n);
|
|
55
|
+
const mod100 = abs % 100;
|
|
56
|
+
const mod10 = abs % 10;
|
|
57
|
+
if (mod100 >= 11 && mod100 <= 13) {
|
|
58
|
+
return `${n}th`;
|
|
59
|
+
}
|
|
60
|
+
if (mod10 === 1) return `${n}st`;
|
|
61
|
+
if (mod10 === 2) return `${n}nd`;
|
|
62
|
+
if (mod10 === 3) return `${n}rd`;
|
|
63
|
+
return `${n}th`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ONES = [
|
|
67
|
+
"",
|
|
68
|
+
"one",
|
|
69
|
+
"two",
|
|
70
|
+
"three",
|
|
71
|
+
"four",
|
|
72
|
+
"five",
|
|
73
|
+
"six",
|
|
74
|
+
"seven",
|
|
75
|
+
"eight",
|
|
76
|
+
"nine",
|
|
77
|
+
"ten",
|
|
78
|
+
"eleven",
|
|
79
|
+
"twelve",
|
|
80
|
+
"thirteen",
|
|
81
|
+
"fourteen",
|
|
82
|
+
"fifteen",
|
|
83
|
+
"sixteen",
|
|
84
|
+
"seventeen",
|
|
85
|
+
"eighteen",
|
|
86
|
+
"nineteen",
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const TENS = [
|
|
90
|
+
"",
|
|
91
|
+
"",
|
|
92
|
+
"twenty",
|
|
93
|
+
"thirty",
|
|
94
|
+
"forty",
|
|
95
|
+
"fifty",
|
|
96
|
+
"sixty",
|
|
97
|
+
"seventy",
|
|
98
|
+
"eighty",
|
|
99
|
+
"ninety",
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
function toCardinalText(n: number): string {
|
|
103
|
+
if (n <= 0 || n >= 100) {
|
|
104
|
+
return String(n);
|
|
105
|
+
}
|
|
106
|
+
if (n < 20) {
|
|
107
|
+
return ONES[n]!;
|
|
108
|
+
}
|
|
109
|
+
const tensWord = TENS[Math.floor(n / 10)]!;
|
|
110
|
+
const onesDigit = n % 10;
|
|
111
|
+
if (onesDigit === 0) {
|
|
112
|
+
return tensWord;
|
|
113
|
+
}
|
|
114
|
+
return `${tensWord}-${ONES[onesDigit]}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const ORDINAL_IRREGULARS: Record<string, string> = {
|
|
118
|
+
one: "first",
|
|
119
|
+
two: "second",
|
|
120
|
+
three: "third",
|
|
121
|
+
four: "fourth",
|
|
122
|
+
five: "fifth",
|
|
123
|
+
six: "sixth",
|
|
124
|
+
seven: "seventh",
|
|
125
|
+
eight: "eighth",
|
|
126
|
+
nine: "ninth",
|
|
127
|
+
ten: "tenth",
|
|
128
|
+
eleven: "eleventh",
|
|
129
|
+
twelve: "twelfth",
|
|
130
|
+
thirteen: "thirteenth",
|
|
131
|
+
fifteen: "fifteenth",
|
|
132
|
+
twenty: "twentieth",
|
|
133
|
+
thirty: "thirtieth",
|
|
134
|
+
forty: "fortieth",
|
|
135
|
+
fifty: "fiftieth",
|
|
136
|
+
sixty: "sixtieth",
|
|
137
|
+
seventy: "seventieth",
|
|
138
|
+
eighty: "eightieth",
|
|
139
|
+
ninety: "ninetieth",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
function toOrdinalText(n: number): string {
|
|
143
|
+
if (n <= 0 || n >= 100) {
|
|
144
|
+
return `${n}th`;
|
|
145
|
+
}
|
|
146
|
+
const cardinal = toCardinalText(n);
|
|
147
|
+
// Check if it's a hyphenated compound like "twenty-one"
|
|
148
|
+
const hyphenIdx = cardinal.lastIndexOf("-");
|
|
149
|
+
if (hyphenIdx !== -1) {
|
|
150
|
+
const prefix = cardinal.slice(0, hyphenIdx + 1);
|
|
151
|
+
const suffix = cardinal.slice(hyphenIdx + 1);
|
|
152
|
+
const ordinalSuffix = ORDINAL_IRREGULARS[suffix] ?? `${suffix}th`;
|
|
153
|
+
return `${prefix}${ordinalSuffix}`;
|
|
154
|
+
}
|
|
155
|
+
return ORDINAL_IRREGULARS[cardinal] ?? `${cardinal}th`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const CHICAGO_SYMBOLS = ["*", "†", "‡", "§", "‖", "¶"];
|
|
159
|
+
|
|
160
|
+
function toChicago(n: number): string {
|
|
161
|
+
if (n <= 0) {
|
|
162
|
+
return String(n);
|
|
163
|
+
}
|
|
164
|
+
const symbolCount = CHICAGO_SYMBOLS.length;
|
|
165
|
+
// Which cycle: 1-6 → repeat 1×, 7-12 → repeat 2×, etc.
|
|
166
|
+
const cycleIndex = Math.ceil(n / symbolCount) - 1; // 0-based repeat count
|
|
167
|
+
const symbolIndex = ((n - 1) % symbolCount);
|
|
168
|
+
const symbol = CHICAGO_SYMBOLS[symbolIndex]!;
|
|
169
|
+
return symbol.repeat(cycleIndex + 1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Format a page number integer using an OOXML ST_NumberFormat code.
|
|
174
|
+
*
|
|
175
|
+
* @param n - The raw page number (1-based within section).
|
|
176
|
+
* @param format - The OOXML format code from `<w:pgNumType w:fmt="…"/>`.
|
|
177
|
+
* @returns Formatted string for display.
|
|
178
|
+
*/
|
|
179
|
+
export function formatPageNumber(n: number, format: string | undefined): string {
|
|
180
|
+
switch (format) {
|
|
181
|
+
case undefined:
|
|
182
|
+
case "decimal":
|
|
183
|
+
return String(n);
|
|
184
|
+
case "upperRoman":
|
|
185
|
+
return toRoman(n);
|
|
186
|
+
case "lowerRoman":
|
|
187
|
+
return toRoman(n).toLowerCase();
|
|
188
|
+
case "upperLetter":
|
|
189
|
+
return toAlphabetic(n);
|
|
190
|
+
case "lowerLetter":
|
|
191
|
+
return toAlphabetic(n).toLowerCase();
|
|
192
|
+
case "none":
|
|
193
|
+
return "";
|
|
194
|
+
case "ordinal":
|
|
195
|
+
return toOrdinal(n);
|
|
196
|
+
case "cardinalText":
|
|
197
|
+
return toCardinalText(n);
|
|
198
|
+
case "ordinalText":
|
|
199
|
+
return toOrdinalText(n);
|
|
200
|
+
case "hex":
|
|
201
|
+
return n.toString(16).toUpperCase();
|
|
202
|
+
case "chicago":
|
|
203
|
+
return toChicago(n);
|
|
204
|
+
default:
|
|
205
|
+
return String(n);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
RenderPage,
|
|
23
23
|
RenderStoryRegion,
|
|
24
24
|
DecorationIndex,
|
|
25
|
+
PageChromeReservations,
|
|
25
26
|
} from "./render-frame-types.ts";
|
|
26
27
|
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
@@ -46,6 +47,14 @@ export interface ChangedPageEntry {
|
|
|
46
47
|
*/
|
|
47
48
|
changedBlockIds: readonly string[];
|
|
48
49
|
}[];
|
|
50
|
+
/**
|
|
51
|
+
* R1: Set when the page's physical frame rect changed but no individual
|
|
52
|
+
* block regions changed. Consumers that iterate `regions` for targeted
|
|
53
|
+
* re-render targets must also honour this flag to avoid silently skipping
|
|
54
|
+
* pages whose geometry shifted without block-level changes (e.g. zoom
|
|
55
|
+
* change or page margin edit that shifts every page frame uniformly).
|
|
56
|
+
*/
|
|
57
|
+
pageFrameChanged?: boolean;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
export interface RenderFrameDiff {
|
|
@@ -110,10 +119,21 @@ export function diffRenderFrames(
|
|
|
110
119
|
continue;
|
|
111
120
|
}
|
|
112
121
|
const regions = diffPage(prevPage, nextPage, prev.decorationIndex, next.decorationIndex);
|
|
113
|
-
|
|
122
|
+
// R1: track page-frame geometry changes independently of block regions.
|
|
123
|
+
const frameChanged = !rectEquals(prevPage.frame, nextPage.frame);
|
|
124
|
+
// R2: track chrome-reservation changes so chrome re-projection isn't skipped.
|
|
125
|
+
const reservationsChanged = !reservationsEqual(
|
|
126
|
+
prevPage.chromeReservations,
|
|
127
|
+
nextPage.chromeReservations,
|
|
128
|
+
);
|
|
129
|
+
if (regions.length === 0 && !frameChanged && !reservationsChanged) {
|
|
114
130
|
unchangedPages.push(pageIndex);
|
|
115
131
|
} else {
|
|
116
|
-
changedPages.push({
|
|
132
|
+
changedPages.push({
|
|
133
|
+
pageIndex,
|
|
134
|
+
regions,
|
|
135
|
+
...(frameChanged ? { pageFrameChanged: true } : {}),
|
|
136
|
+
});
|
|
117
137
|
}
|
|
118
138
|
}
|
|
119
139
|
|
|
@@ -268,6 +288,22 @@ function decorationHashForBlock(
|
|
|
268
288
|
return tokens.join("|");
|
|
269
289
|
}
|
|
270
290
|
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Chrome reservations compare
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
// R2: Compare all five reservation fields so changes to rail/balloon lanes
|
|
296
|
+
// or footnote area trigger changedPages, not unchangedPages.
|
|
297
|
+
function reservationsEqual(a: PageChromeReservations, b: PageChromeReservations): boolean {
|
|
298
|
+
return (
|
|
299
|
+
a.railLaneTwips === b.railLaneTwips &&
|
|
300
|
+
a.balloonLaneTwips === b.balloonLaneTwips &&
|
|
301
|
+
a.footnoteAreaTwips === b.footnoteAreaTwips &&
|
|
302
|
+
a.pageFrameWidthPx === b.pageFrameWidthPx &&
|
|
303
|
+
a.pageFrameHeightPx === b.pageFrameHeightPx
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
271
307
|
// ---------------------------------------------------------------------------
|
|
272
308
|
// Geometry compare
|
|
273
309
|
// ---------------------------------------------------------------------------
|
|
@@ -8,6 +8,11 @@ import type {
|
|
|
8
8
|
SurfaceTableRowSnapshot,
|
|
9
9
|
SurfaceTextMark,
|
|
10
10
|
} from "../api/public-types";
|
|
11
|
+
import {
|
|
12
|
+
chartModelStore,
|
|
13
|
+
extractChartDimensions,
|
|
14
|
+
stableChartId,
|
|
15
|
+
} from "./chart/chart-model-store.ts";
|
|
11
16
|
import type {
|
|
12
17
|
CanonicalDocumentEnvelope,
|
|
13
18
|
SelectionSnapshot,
|
|
@@ -1011,10 +1016,25 @@ function appendInlineSegments(
|
|
|
1011
1016
|
});
|
|
1012
1017
|
return { nextCursor: start + 1, lockedFragmentIds: [node.fragmentId] };
|
|
1013
1018
|
}
|
|
1014
|
-
case "chart_preview":
|
|
1019
|
+
case "chart_preview": {
|
|
1020
|
+
let parsedChartId: string | undefined;
|
|
1021
|
+
if (node.parsedData) {
|
|
1022
|
+
parsedChartId = stableChartId(node.rawXml);
|
|
1023
|
+
if (!chartModelStore.has(parsedChartId)) {
|
|
1024
|
+
const { widthPx, heightPx } = extractChartDimensions(node.rawXml);
|
|
1025
|
+
chartModelStore.set(parsedChartId, {
|
|
1026
|
+
model: node.parsedData,
|
|
1027
|
+
widthPx,
|
|
1028
|
+
heightPx,
|
|
1029
|
+
theme: undefined,
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1015
1033
|
return appendComplexPreviewSegment(paragraph, node, start, "Embedded chart", createChartDetail(node), {
|
|
1016
1034
|
previewMediaId: node.previewMediaId,
|
|
1035
|
+
parsedChartId,
|
|
1017
1036
|
});
|
|
1037
|
+
}
|
|
1018
1038
|
case "smartart_preview":
|
|
1019
1039
|
return appendComplexPreviewSegment(paragraph, node, start, "SmartArt diagram", createSmartArtDetail(node), {
|
|
1020
1040
|
previewMediaId: node.previewMediaId,
|
|
@@ -1087,6 +1107,14 @@ function appendInlineSegments(
|
|
|
1087
1107
|
node.fieldFamily === "PAGE" ||
|
|
1088
1108
|
node.fieldFamily === "NUMPAGES";
|
|
1089
1109
|
if (node.children && node.children.length > 0) {
|
|
1110
|
+
// For REF \h, pass the bookmark as a hyperlink href so child text gets hyperlink styling
|
|
1111
|
+
const refHyperlinkHref =
|
|
1112
|
+
node.fieldFamily === "REF" &&
|
|
1113
|
+
node.switches?.hyperlink === true &&
|
|
1114
|
+
node.fieldTarget
|
|
1115
|
+
? `#${node.fieldTarget}`
|
|
1116
|
+
: undefined;
|
|
1117
|
+
|
|
1090
1118
|
let cursor = start;
|
|
1091
1119
|
const lockedIds: string[] = [];
|
|
1092
1120
|
for (const child of node.children) {
|
|
@@ -1096,7 +1124,7 @@ function appendInlineSegments(
|
|
|
1096
1124
|
document,
|
|
1097
1125
|
cursor,
|
|
1098
1126
|
promoteSecondaryStoryTextBoxes,
|
|
1099
|
-
|
|
1127
|
+
refHyperlinkHref ?? hyperlinkHref,
|
|
1100
1128
|
cullBuild,
|
|
1101
1129
|
);
|
|
1102
1130
|
cursor = result.nextCursor;
|
|
@@ -1159,7 +1187,7 @@ function appendComplexPreviewSegment(
|
|
|
1159
1187
|
start: number,
|
|
1160
1188
|
label: string,
|
|
1161
1189
|
detail: string,
|
|
1162
|
-
extras: { previewMediaId?: string } = {},
|
|
1190
|
+
extras: { previewMediaId?: string; parsedChartId?: string } = {},
|
|
1163
1191
|
): { nextCursor: number; lockedFragmentIds: string[] } {
|
|
1164
1192
|
paragraph.segments.push({
|
|
1165
1193
|
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
@@ -1171,6 +1199,7 @@ function appendComplexPreviewSegment(
|
|
|
1171
1199
|
label,
|
|
1172
1200
|
detail,
|
|
1173
1201
|
...(extras.previewMediaId ? { previewMediaId: extras.previewMediaId } : {}),
|
|
1202
|
+
...(extras.parsedChartId ? { parsedChartId: extras.parsedChartId } : {}),
|
|
1174
1203
|
state: "locked-preserve-only",
|
|
1175
1204
|
});
|
|
1176
1205
|
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
@@ -220,6 +220,12 @@ import {
|
|
|
220
220
|
getCursorColorForUser,
|
|
221
221
|
setLocalCursorState,
|
|
222
222
|
} from "../runtime/collab/remote-cursor-awareness.ts";
|
|
223
|
+
import {
|
|
224
|
+
stableChartId,
|
|
225
|
+
} from "../runtime/chart/chart-model-store.ts";
|
|
226
|
+
import {
|
|
227
|
+
projectChartSnapshot,
|
|
228
|
+
} from "../runtime/chart/chart-snapshot.ts";
|
|
223
229
|
|
|
224
230
|
export {
|
|
225
231
|
__createFallbackRuntime,
|
|
@@ -466,6 +472,40 @@ async function runConvertScopesToExternal(args: {
|
|
|
466
472
|
|
|
467
473
|
// ---------------------------------------------------------------------------
|
|
468
474
|
|
|
475
|
+
type CanonicalDocType = ReturnType<WordReviewEditorRuntime["getCanonicalDocument"]>;
|
|
476
|
+
|
|
477
|
+
function collectChartSnapshots(doc: CanonicalDocType): import("../api/public-types").ChartSnapshot[] {
|
|
478
|
+
const results: import("../api/public-types").ChartSnapshot[] = [];
|
|
479
|
+
collectChartSnapshotsFromBlocks(doc.content.children, results);
|
|
480
|
+
return results;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function collectChartSnapshotsFromBlocks(
|
|
484
|
+
blocks: CanonicalDocType["content"]["children"],
|
|
485
|
+
results: import("../api/public-types").ChartSnapshot[],
|
|
486
|
+
): void {
|
|
487
|
+
for (const block of blocks) {
|
|
488
|
+
if (block.type === "paragraph") {
|
|
489
|
+
for (const inline of block.children) {
|
|
490
|
+
if (inline.type === "chart_preview" && inline.parsedData) {
|
|
491
|
+
const chartId = stableChartId(inline.rawXml);
|
|
492
|
+
results.push(projectChartSnapshot(chartId, inline.parsedData));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
} else if (block.type === "table") {
|
|
496
|
+
for (const row of block.rows) {
|
|
497
|
+
for (const cell of row.cells) {
|
|
498
|
+
collectChartSnapshotsFromBlocks(cell.children, results);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} else if (block.type === "sdt" || block.type === "custom_xml") {
|
|
502
|
+
collectChartSnapshotsFromBlocks(block.children, results);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
|
|
469
509
|
export function __createWordReviewEditorRefBridge(
|
|
470
510
|
runtime: WordReviewEditorRuntime,
|
|
471
511
|
mountedSurface?: TwProseMirrorSurfaceRef | null,
|
|
@@ -514,6 +554,12 @@ export function __createWordReviewEditorRefBridge(
|
|
|
514
554
|
getRenderSnapshot: () => clonePublicValue(runtime.getRenderSnapshot()),
|
|
515
555
|
getCompatibilityReport: () => runtime.getCompatibilityReport(),
|
|
516
556
|
getWarnings: () => runtime.getWarnings(),
|
|
557
|
+
getChartSnapshot: (chartId) => {
|
|
558
|
+
return collectChartSnapshots(runtime.getCanonicalDocument()).find(
|
|
559
|
+
(s) => s.chartId === chartId,
|
|
560
|
+
) ?? null;
|
|
561
|
+
},
|
|
562
|
+
getChartSnapshots: () => collectChartSnapshots(runtime.getCanonicalDocument()),
|
|
517
563
|
getCommentSidebarSnapshot: () =>
|
|
518
564
|
clonePublicValue(runtime.getRenderSnapshot().comments),
|
|
519
565
|
getTrackedChangesSnapshot: () =>
|
|
@@ -782,7 +828,9 @@ export function __createWordReviewEditorRefBridge(
|
|
|
782
828
|
clearWorkflowOverlay: () => {
|
|
783
829
|
runtime.clearWorkflowOverlay();
|
|
784
830
|
},
|
|
785
|
-
getWorkflowOverlay: () =>
|
|
831
|
+
getWorkflowOverlay: () => {
|
|
832
|
+
return clonePublicValue(runtime.getWorkflowOverlay());
|
|
833
|
+
},
|
|
786
834
|
setSharedWorkflowState: (state) => {
|
|
787
835
|
runtime.setSharedWorkflowState(state === null ? null : clonePublicValue(state));
|
|
788
836
|
},
|
|
@@ -1535,6 +1583,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1535
1583
|
getRenderSnapshot: () => clonePublicValue(activeRuntime.getRenderSnapshot()),
|
|
1536
1584
|
getCompatibilityReport: () => activeRuntime.getCompatibilityReport(),
|
|
1537
1585
|
getWarnings: () => activeRuntime.getWarnings(),
|
|
1586
|
+
getChartSnapshot: (chartId) =>
|
|
1587
|
+
collectChartSnapshots(activeRuntime.getCanonicalDocument()).find(
|
|
1588
|
+
(s) => s.chartId === chartId,
|
|
1589
|
+
) ?? null,
|
|
1590
|
+
getChartSnapshots: () => collectChartSnapshots(activeRuntime.getCanonicalDocument()),
|
|
1538
1591
|
getCommentSidebarSnapshot: () =>
|
|
1539
1592
|
clonePublicValue(activeRuntime.getRenderSnapshot().comments),
|
|
1540
1593
|
getTrackedChangesSnapshot: () =>
|
|
@@ -1834,8 +1887,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1834
1887
|
clearWorkflowOverlay: () => {
|
|
1835
1888
|
activeRuntime.clearWorkflowOverlay();
|
|
1836
1889
|
},
|
|
1837
|
-
getWorkflowOverlay: () =>
|
|
1838
|
-
clonePublicValue(activeRuntime.getWorkflowOverlay())
|
|
1890
|
+
getWorkflowOverlay: () => {
|
|
1891
|
+
return clonePublicValue(activeRuntime.getWorkflowOverlay());
|
|
1892
|
+
},
|
|
1839
1893
|
setSharedWorkflowState: (state) => {
|
|
1840
1894
|
activeRuntime.setSharedWorkflowState(state === null ? null : clonePublicValue(state));
|
|
1841
1895
|
},
|
|
@@ -77,7 +77,61 @@ export function getCommentRangeState(
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Markup display mode — controls how tracked changes and comments render.
|
|
82
|
+
*
|
|
83
|
+
* L6d.N2 widens the union to Word's 4-mode grammar (`"all-markup"`,
|
|
84
|
+
* `"simple-markup"`, `"no-markup"`, `"original"`) while keeping the
|
|
85
|
+
* legacy aliases `"clean" | "simple" | "all"` for backward compat with
|
|
86
|
+
* hosts that already pass the old values. Callers that switch on this
|
|
87
|
+
* value should either:
|
|
88
|
+
* - handle both sets of names (the decoration models do), or
|
|
89
|
+
* - normalize via `normalizeMarkupDisplay()` before matching.
|
|
90
|
+
*
|
|
91
|
+
* New in 6d.N2:
|
|
92
|
+
* - `"original"` — hide insertions entirely, render deletions as
|
|
93
|
+
* plain body text. Shows what the document looked like before the
|
|
94
|
+
* current batch of tracked changes.
|
|
95
|
+
*/
|
|
96
|
+
export type MarkupDisplay =
|
|
97
|
+
| "all-markup"
|
|
98
|
+
| "simple-markup"
|
|
99
|
+
| "no-markup"
|
|
100
|
+
| "original"
|
|
101
|
+
// Legacy aliases — accepted on the public API; prefer the canonical names above.
|
|
102
|
+
| "clean"
|
|
103
|
+
| "simple"
|
|
104
|
+
| "all";
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Collapse legacy `MarkupDisplay` aliases to their canonical Word-grammar
|
|
108
|
+
* name:
|
|
109
|
+
* - `"all"` → `"all-markup"`
|
|
110
|
+
* - `"simple"` → `"simple-markup"`
|
|
111
|
+
* - `"clean"` → `"no-markup"`
|
|
112
|
+
* - `"original"` → `"original"` (identity)
|
|
113
|
+
*
|
|
114
|
+
* The two sets remain interchangeable inside the decoration models, but
|
|
115
|
+
* external surfaces (selector labels, telemetry, host events) should
|
|
116
|
+
* normalize first so Word's grammar is the single source of truth.
|
|
117
|
+
*/
|
|
118
|
+
export function normalizeMarkupDisplay(
|
|
119
|
+
value: MarkupDisplay,
|
|
120
|
+
): "all-markup" | "simple-markup" | "no-markup" | "original" {
|
|
121
|
+
switch (value) {
|
|
122
|
+
case "all":
|
|
123
|
+
case "all-markup":
|
|
124
|
+
return "all-markup";
|
|
125
|
+
case "simple":
|
|
126
|
+
case "simple-markup":
|
|
127
|
+
return "simple-markup";
|
|
128
|
+
case "clean":
|
|
129
|
+
case "no-markup":
|
|
130
|
+
return "no-markup";
|
|
131
|
+
case "original":
|
|
132
|
+
return "original";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
81
135
|
|
|
82
136
|
export function getCommentHighlightClass(
|
|
83
137
|
model: CommentDecorationModel | undefined,
|
|
@@ -90,10 +144,11 @@ export function getCommentHighlightClass(
|
|
|
90
144
|
return "";
|
|
91
145
|
}
|
|
92
146
|
|
|
93
|
-
switch (markupDisplay) {
|
|
94
|
-
case "
|
|
147
|
+
switch (normalizeMarkupDisplay(markupDisplay)) {
|
|
148
|
+
case "no-markup":
|
|
149
|
+
case "original":
|
|
95
150
|
return state.hasActive ? "bg-comment-soft" : "";
|
|
96
|
-
case "simple":
|
|
151
|
+
case "simple-markup":
|
|
97
152
|
if (state.hasActive) {
|
|
98
153
|
return "underline decoration-comment decoration-2 underline-offset-4";
|
|
99
154
|
}
|
|
@@ -101,7 +156,7 @@ export function getCommentHighlightClass(
|
|
|
101
156
|
return "underline decoration-comment/60 decoration-1 underline-offset-4";
|
|
102
157
|
}
|
|
103
158
|
return "underline decoration-comment/40 decoration-1 underline-offset-4";
|
|
104
|
-
case "all":
|
|
159
|
+
case "all-markup":
|
|
105
160
|
if (state.hasActive) {
|
|
106
161
|
return "bg-comment-strong";
|
|
107
162
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { TrackedChangesSnapshot, TrackedChangeEntrySnapshot } from "../../api/public-types";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
normalizeMarkupDisplay,
|
|
4
|
+
rangesOverlap,
|
|
5
|
+
type MarkupDisplay,
|
|
6
|
+
} from "./comment-decoration-model";
|
|
3
7
|
|
|
4
8
|
export interface RevisionDecorationModel {
|
|
5
9
|
revisions: RevisionDecorationEntry[];
|
|
@@ -13,6 +17,65 @@ export interface RevisionDecorationEntry {
|
|
|
13
17
|
status: TrackedChangeEntrySnapshot["status"];
|
|
14
18
|
actionability: TrackedChangeEntrySnapshot["actionability"];
|
|
15
19
|
isActive: boolean;
|
|
20
|
+
/** L6d.N3 — source author, used to pick the decoration color. */
|
|
21
|
+
authorId?: string;
|
|
22
|
+
/** L6d.N3 — palette slot for `authorId` (0..7, stable via FNV-1a hash). */
|
|
23
|
+
authorPaletteIndex?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* L6d.N3 — author-color assignment.
|
|
28
|
+
*
|
|
29
|
+
* Map `authorId` → one of the 8 `--color-chart-categorical-*` vars
|
|
30
|
+
* using FNV-1a 32-bit hashing. FNV-1a is deterministic, allocation-
|
|
31
|
+
* free, and has better avalanche on short ASCII keys than djb2, so
|
|
32
|
+
* typical author-id alphabets (emails, UUIDs, Slack IDs) distribute
|
|
33
|
+
* evenly across the 8 slots.
|
|
34
|
+
*
|
|
35
|
+
* The palette lives as CSS vars so theme swaps / reduced-contrast
|
|
36
|
+
* overrides in `tokens.css` cascade automatically — we emit `var(...)`
|
|
37
|
+
* strings, never hex literals.
|
|
38
|
+
*/
|
|
39
|
+
export const AUTHOR_PALETTE: readonly string[] = [
|
|
40
|
+
"var(--color-chart-categorical-1)",
|
|
41
|
+
"var(--color-chart-categorical-2)",
|
|
42
|
+
"var(--color-chart-categorical-3)",
|
|
43
|
+
"var(--color-chart-categorical-4)",
|
|
44
|
+
"var(--color-chart-categorical-5)",
|
|
45
|
+
"var(--color-chart-categorical-6)",
|
|
46
|
+
"var(--color-chart-categorical-7)",
|
|
47
|
+
"var(--color-chart-categorical-8)",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* FNV-1a 32-bit hash. Constants from the Fowler–Noll–Vo reference:
|
|
52
|
+
* offset basis = 0x811c9dc5
|
|
53
|
+
* prime = 0x01000193
|
|
54
|
+
* The `>>> 0` keeps the value in unsigned 32-bit territory after each
|
|
55
|
+
* multiplication so two identical inputs always produce the same hash.
|
|
56
|
+
*/
|
|
57
|
+
export function hashAuthorId(authorId: string): number {
|
|
58
|
+
let hash = 0x811c9dc5;
|
|
59
|
+
for (let i = 0; i < authorId.length; i += 1) {
|
|
60
|
+
hash ^= authorId.charCodeAt(i);
|
|
61
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
62
|
+
}
|
|
63
|
+
return hash;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Reduce a hash to a palette slot in `[0, AUTHOR_PALETTE.length)`. */
|
|
67
|
+
export function authorPaletteIndex(authorId: string): number {
|
|
68
|
+
return hashAuthorId(authorId) % AUTHOR_PALETTE.length;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a CSS color string for an author. Returns `undefined` when
|
|
73
|
+
* `authorId` is missing so callers can fall back to the default
|
|
74
|
+
* per-kind class (e.g. `bg-insert-soft`).
|
|
75
|
+
*/
|
|
76
|
+
export function getAuthorColor(authorId: string | undefined): string | undefined {
|
|
77
|
+
if (!authorId) return undefined;
|
|
78
|
+
return AUTHOR_PALETTE[authorPaletteIndex(authorId)];
|
|
16
79
|
}
|
|
17
80
|
|
|
18
81
|
export interface RevisionRangeState {
|
|
@@ -47,6 +110,8 @@ export function createRevisionDecorationModel(
|
|
|
47
110
|
status: rev.status,
|
|
48
111
|
actionability: rev.actionability,
|
|
49
112
|
isActive: rev.revisionId === activeRevisionId,
|
|
113
|
+
authorId: rev.authorId,
|
|
114
|
+
authorPaletteIndex: rev.authorId ? authorPaletteIndex(rev.authorId) : undefined,
|
|
50
115
|
};
|
|
51
116
|
}),
|
|
52
117
|
};
|
|
@@ -94,12 +159,17 @@ export function getRevisionHighlightClass(
|
|
|
94
159
|
|
|
95
160
|
const activeRing = state.hasActive ? " ring-1 ring-accent/30" : "";
|
|
96
161
|
|
|
97
|
-
switch (markupDisplay) {
|
|
98
|
-
case "
|
|
99
|
-
// In
|
|
162
|
+
switch (normalizeMarkupDisplay(markupDisplay)) {
|
|
163
|
+
case "no-markup":
|
|
164
|
+
// In no-markup mode, deletions are hidden entirely (caller should not render).
|
|
100
165
|
// Insertions render as normal text with no decoration.
|
|
101
166
|
return "";
|
|
102
|
-
case "
|
|
167
|
+
case "original":
|
|
168
|
+
// L6d.N2 — "original" shows what the doc looked like BEFORE the
|
|
169
|
+
// pending tracked changes: insertions are hidden (caller checks
|
|
170
|
+
// `shouldHideInOriginalMode`), deletions render as plain body text.
|
|
171
|
+
return "";
|
|
172
|
+
case "simple-markup":
|
|
103
173
|
if (state.hasInsertions) {
|
|
104
174
|
return `underline decoration-insert/60 decoration-1 underline-offset-2 text-primary${activeRing}`;
|
|
105
175
|
}
|
|
@@ -107,7 +177,7 @@ export function getRevisionHighlightClass(
|
|
|
107
177
|
return `text-secondary line-through decoration-danger/70 decoration-1${activeRing}`;
|
|
108
178
|
}
|
|
109
179
|
return activeRing;
|
|
110
|
-
case "all":
|
|
180
|
+
case "all-markup":
|
|
111
181
|
if (state.hasInsertions) {
|
|
112
182
|
return `text-primary bg-insert-soft/80 ring-1 ring-insert/20${activeRing}`;
|
|
113
183
|
}
|
|
@@ -118,6 +188,10 @@ export function getRevisionHighlightClass(
|
|
|
118
188
|
}
|
|
119
189
|
}
|
|
120
190
|
|
|
191
|
+
/**
|
|
192
|
+
* `no-markup` hides deletions (pretending the doc already accepted all
|
|
193
|
+
* pending changes). The caller skips rendering affected spans entirely.
|
|
194
|
+
*/
|
|
121
195
|
export function shouldHideInCleanMode(
|
|
122
196
|
model: RevisionDecorationModel | undefined,
|
|
123
197
|
from: number,
|
|
@@ -126,3 +200,17 @@ export function shouldHideInCleanMode(
|
|
|
126
200
|
const state = getRevisionRangeState(model, from, to);
|
|
127
201
|
return state.hasDeletions;
|
|
128
202
|
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* L6d.N2 — `original` mode hides insertions (pretending the pending
|
|
206
|
+
* batch of tracked changes was rejected). Caller skips rendering
|
|
207
|
+
* affected spans entirely.
|
|
208
|
+
*/
|
|
209
|
+
export function shouldHideInOriginalMode(
|
|
210
|
+
model: RevisionDecorationModel | undefined,
|
|
211
|
+
from: number,
|
|
212
|
+
to: number,
|
|
213
|
+
): boolean {
|
|
214
|
+
const state = getRevisionRangeState(model, from, to);
|
|
215
|
+
return state.hasInsertions;
|
|
216
|
+
}
|