@beyondwork/docx-react-component 1.0.95 → 1.0.97
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 +33 -19
- package/src/api/v3/ui/_types.ts +11 -21
- package/src/api/v3/ui/chrome.ts +8 -9
- package/src/api/v3/ui/debug.ts +15 -77
- package/src/api/v3/ui/overlays-visibility.ts +9 -10
- package/src/api/v3/ui/overlays.ts +8 -75
- package/src/io/ooxml/parse-main-document.ts +30 -0
- package/src/io/ooxml/parse-picture.ts +14 -0
- package/src/io/ooxml/parse-shapes.ts +41 -1
- package/src/model/canonical-document.ts +17 -0
- package/src/runtime/document-runtime.ts +46 -1
- package/src/runtime/layout/layout-engine-version.ts +8 -1
- package/src/runtime/layout/page-story-resolver.ts +1 -0
- package/src/runtime/layout/paginated-layout-engine.ts +26 -10
- package/src/runtime/surface-projection.ts +114 -12
- package/src/runtime/workflow/rail/compose.ts +5 -0
- package/src/ui/WordReviewEditor.tsx +6 -10
- package/src/ui/editor-command-bag.ts +2 -0
- package/src/ui/ui-controller-factory.ts +2 -2
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +12 -41
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +3 -7
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +22 -228
- package/src/ui-tailwind/debug/README.md +12 -50
- package/src/ui-tailwind/debug/tw-debug-overlay.tsx +6 -6
- package/src/ui-tailwind/debug/tw-debug-presentation.tsx +9 -20
- package/src/ui-tailwind/debug/tw-debug-top-bar.tsx +5 -6
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +1 -4
- package/src/ui-tailwind/editor-surface/picture-effects.ts +96 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +89 -62
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +205 -0
- package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +190 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +53 -53
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +83 -20
- package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +114 -4
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +5 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +3 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +3 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +26 -0
- package/src/ui-tailwind/theme/editor-theme.css +82 -84
- package/src/ui-tailwind/tw-review-workspace.tsx +15 -0
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
* preserved in the canonical node's rawXml field for lossless round-trip export.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
BlockNode,
|
|
15
|
+
ShapeContent,
|
|
16
|
+
TextBoxBodyProperties,
|
|
17
|
+
} from "../../model/canonical-document.ts";
|
|
14
18
|
import { parseFill } from "./parse-fill.ts";
|
|
15
19
|
import {
|
|
16
20
|
type XmlElementNode,
|
|
@@ -32,6 +36,8 @@ export interface ParsedWpsShape {
|
|
|
32
36
|
text?: string;
|
|
33
37
|
/** Raw txbxContent XML for structured re-rendering. */
|
|
34
38
|
txbxContentXml?: string;
|
|
39
|
+
/** Text-box body layout from wps:bodyPr / a:bodyPr. */
|
|
40
|
+
textBoxBody?: TextBoxBodyProperties;
|
|
35
41
|
/**
|
|
36
42
|
* Parsed block-level structure from `w:txbxContent`, populated when a
|
|
37
43
|
* `blockParser` callback is supplied (coord-02 §14 / coord-11 §22 —
|
|
@@ -97,6 +103,7 @@ export function parseShapeXml(
|
|
|
97
103
|
const txbx = findFirstChild(wsp, "txbx");
|
|
98
104
|
const txbxContent = txbx ? findFirstDescendant(txbx, "txbxContent") : null;
|
|
99
105
|
const text = txbxContent ? extractAllText(txbxContent).trim() || undefined : undefined;
|
|
106
|
+
const textBoxBody = readTextBoxBody(wsp);
|
|
100
107
|
|
|
101
108
|
// WordArt detection: geometry preset names that start with "text"
|
|
102
109
|
if (prst && /^text/i.test(prst)) {
|
|
@@ -145,6 +152,7 @@ export function parseShapeXml(
|
|
|
145
152
|
type: "shape",
|
|
146
153
|
...(isTextBox ? { isTextBox: true } : {}),
|
|
147
154
|
...(text ? { text } : {}),
|
|
155
|
+
...(textBoxBody ? { textBoxBody } : {}),
|
|
148
156
|
...(txbxContentXml ? { txbxContentXml } : {}),
|
|
149
157
|
...(txbxBlocks && txbxBlocks.length > 0 ? { txbxBlocks } : {}),
|
|
150
158
|
...(prst ? { geometry: prst } : {}),
|
|
@@ -254,12 +262,14 @@ export function parseShapeContent(
|
|
|
254
262
|
|
|
255
263
|
const fill = spPr ? readFill(spPr) : undefined;
|
|
256
264
|
const line = spPr ? readLine(spPr) : undefined;
|
|
265
|
+
const textBoxBody = readTextBoxBody(wsp);
|
|
257
266
|
|
|
258
267
|
// Text-box content — preserve raw XML for serialization + recurse via the
|
|
259
268
|
// optional blockParser callback (CO4 F3.3) to populate txbxBlocks.
|
|
260
269
|
const txbx = findFirstChild(wsp, "txbx");
|
|
261
270
|
const txbxContent = txbx ? findFirstDescendant(txbx, "txbxContent") : undefined;
|
|
262
271
|
const txbxContentXml = txbxContent ? extractRawXml(txbxContent) : undefined;
|
|
272
|
+
const text = txbxContent ? extractAllText(txbxContent).trim() || undefined : undefined;
|
|
263
273
|
|
|
264
274
|
let txbxBlocks: ReadonlyArray<BlockNode> | undefined;
|
|
265
275
|
if (txbxContentXml && blockParser) {
|
|
@@ -286,14 +296,37 @@ export function parseShapeContent(
|
|
|
286
296
|
|
|
287
297
|
const result: ShapeContent = { type: "shape", rawXml: drawingRawXml };
|
|
288
298
|
if (geometry) result.geometry = geometry;
|
|
299
|
+
if (text) result.text = text;
|
|
289
300
|
if (fill) result.fill = fill;
|
|
290
301
|
if (line) result.line = line;
|
|
291
302
|
if (isTextBox) result.isTextBox = true;
|
|
303
|
+
if (textBoxBody) result.textBoxBody = textBoxBody;
|
|
292
304
|
if (txbxContentXml) result.txbxContentXml = txbxContentXml;
|
|
293
305
|
if (txbxBlocks && txbxBlocks.length > 0) result.txbxBlocks = txbxBlocks;
|
|
294
306
|
return result;
|
|
295
307
|
}
|
|
296
308
|
|
|
309
|
+
function readTextBoxBody(wsp: XmlElementNode): TextBoxBodyProperties | undefined {
|
|
310
|
+
const bodyPr = findFirstChild(wsp, "bodyPr");
|
|
311
|
+
if (!bodyPr) return undefined;
|
|
312
|
+
|
|
313
|
+
const result: TextBoxBodyProperties = {};
|
|
314
|
+
const anchor = bodyPr.attributes.anchor;
|
|
315
|
+
if (anchor === "t" || anchor === "ctr" || anchor === "b") {
|
|
316
|
+
result.anchor = anchor;
|
|
317
|
+
}
|
|
318
|
+
const left = readIntAttr(bodyPr, "lIns");
|
|
319
|
+
const top = readIntAttr(bodyPr, "tIns");
|
|
320
|
+
const right = readIntAttr(bodyPr, "rIns");
|
|
321
|
+
const bottom = readIntAttr(bodyPr, "bIns");
|
|
322
|
+
if (left !== undefined) result.insetLeftEmu = left;
|
|
323
|
+
if (top !== undefined) result.insetTopEmu = top;
|
|
324
|
+
if (right !== undefined) result.insetRightEmu = right;
|
|
325
|
+
if (bottom !== undefined) result.insetBottomEmu = bottom;
|
|
326
|
+
|
|
327
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
328
|
+
}
|
|
329
|
+
|
|
297
330
|
// F3.4 + P5 — readFill delegates to the shared `parseFill` primitive covering
|
|
298
331
|
// solid / none / gradient / pattern. Lane 5 chart-style cascade can consume
|
|
299
332
|
// the same parser via `src/io/ooxml/parse-fill.ts`.
|
|
@@ -320,3 +353,10 @@ function readLine(
|
|
|
320
353
|
}
|
|
321
354
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
322
355
|
}
|
|
356
|
+
|
|
357
|
+
function readIntAttr(node: XmlElementNode, attr: string): number | undefined {
|
|
358
|
+
const raw = node.attributes[attr];
|
|
359
|
+
if (raw === undefined) return undefined;
|
|
360
|
+
const parsed = parseInt(raw, 10);
|
|
361
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
362
|
+
}
|
|
@@ -1876,6 +1876,8 @@ export interface ShapeNode {
|
|
|
1876
1876
|
text?: string;
|
|
1877
1877
|
geometry?: string;
|
|
1878
1878
|
isTextBox?: boolean;
|
|
1879
|
+
/** Text box body layout from `wps:bodyPr` / `a:bodyPr`. */
|
|
1880
|
+
textBoxBody?: TextBoxBodyProperties;
|
|
1879
1881
|
/** Raw `<w:txbxContent>` XML, preserved for serialization + round-trip. */
|
|
1880
1882
|
txbxContentXml?: string;
|
|
1881
1883
|
/**
|
|
@@ -2001,6 +2003,8 @@ export interface PictureContent {
|
|
|
2001
2003
|
packagePartName?: string;
|
|
2002
2004
|
/** MIME resolved from the OPC media part, when known. */
|
|
2003
2005
|
contentType?: string;
|
|
2006
|
+
/** DrawingML a:lum brightness/contrast adjustments on the blip. Values are OOXML fixed percentages. */
|
|
2007
|
+
lum?: { bright?: number; contrast?: number };
|
|
2004
2008
|
srcRect?: { top: number; bottom: number; left: number; right: number };
|
|
2005
2009
|
stretch?: boolean;
|
|
2006
2010
|
/**
|
|
@@ -2032,6 +2036,8 @@ export interface PictureContent {
|
|
|
2032
2036
|
|
|
2033
2037
|
export interface ShapeContent {
|
|
2034
2038
|
type: "shape";
|
|
2039
|
+
/** Plain-text fallback extracted from `w:txbxContent` when present. */
|
|
2040
|
+
text?: string;
|
|
2035
2041
|
geometry?: string;
|
|
2036
2042
|
/**
|
|
2037
2043
|
* Shape fill — solid, gradient, pattern, or none. srgbClr values on solid +
|
|
@@ -2061,6 +2067,8 @@ export interface ShapeContent {
|
|
|
2061
2067
|
line?: { color?: string; widthEmu?: number; noLine?: boolean };
|
|
2062
2068
|
/** True when the shape's geometry + txbxContent presence make it a text box. */
|
|
2063
2069
|
isTextBox?: boolean;
|
|
2070
|
+
/** Text box body layout from `wps:bodyPr` / `a:bodyPr`. */
|
|
2071
|
+
textBoxBody?: TextBoxBodyProperties;
|
|
2064
2072
|
/** Raw w:txbxContent XML, preserved for serialization + lossless round-trip. */
|
|
2065
2073
|
txbxContentXml?: string;
|
|
2066
2074
|
/**
|
|
@@ -2080,6 +2088,15 @@ export interface ShapeContent {
|
|
|
2080
2088
|
rawXml: string;
|
|
2081
2089
|
}
|
|
2082
2090
|
|
|
2091
|
+
export interface TextBoxBodyProperties {
|
|
2092
|
+
/** OOXML bodyPr anchor: top, center, or bottom. */
|
|
2093
|
+
anchor?: "t" | "ctr" | "b";
|
|
2094
|
+
insetLeftEmu?: number;
|
|
2095
|
+
insetTopEmu?: number;
|
|
2096
|
+
insetRightEmu?: number;
|
|
2097
|
+
insetBottomEmu?: number;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2083
2100
|
export interface DrawingFrameNode {
|
|
2084
2101
|
type: "drawing_frame";
|
|
2085
2102
|
anchor: AnchorGeometry;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type EditorWarning as InternalEditorWarning,
|
|
14
14
|
} from "../core/state/editor-state.ts";
|
|
15
15
|
import {
|
|
16
|
+
createPlainText,
|
|
16
17
|
logicalPositionToUnitIndex,
|
|
17
18
|
parseTextStory,
|
|
18
19
|
serializeTextStory,
|
|
@@ -3195,6 +3196,13 @@ export function createDocumentRuntime(
|
|
|
3195
3196
|
replaceText(text, target, formatting) {
|
|
3196
3197
|
try {
|
|
3197
3198
|
const timestamp = clock();
|
|
3199
|
+
const selection = target ? createSelectionFromPublicAnchor(target) : state.selection;
|
|
3200
|
+
if (
|
|
3201
|
+
shouldPreserveEquivalentReplacement(formatting) &&
|
|
3202
|
+
replacementTextMatchesCurrentRange(state.document, activeStory, selection, text)
|
|
3203
|
+
) {
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3198
3206
|
applyTextCommandInActiveStory(
|
|
3199
3207
|
{
|
|
3200
3208
|
type: "text.insert",
|
|
@@ -3203,7 +3211,7 @@ export function createDocumentRuntime(
|
|
|
3203
3211
|
origin: createOrigin("api", timestamp),
|
|
3204
3212
|
},
|
|
3205
3213
|
{
|
|
3206
|
-
selection
|
|
3214
|
+
selection,
|
|
3207
3215
|
blockedCommandName: "replaceText",
|
|
3208
3216
|
},
|
|
3209
3217
|
);
|
|
@@ -6448,6 +6456,43 @@ function createSelectionFromPublicAnchor(
|
|
|
6448
6456
|
}
|
|
6449
6457
|
}
|
|
6450
6458
|
|
|
6459
|
+
function shouldPreserveEquivalentReplacement(formatting: TextFormattingDirective | undefined): boolean {
|
|
6460
|
+
return !formatting || formatting.mode === "match-replaced-range";
|
|
6461
|
+
}
|
|
6462
|
+
|
|
6463
|
+
function replacementTextMatchesCurrentRange(
|
|
6464
|
+
document: CanonicalDocumentEnvelope,
|
|
6465
|
+
activeStory: EditorStoryTarget,
|
|
6466
|
+
selection: import("../core/state/editor-state.ts").SelectionSnapshot,
|
|
6467
|
+
replacement: string,
|
|
6468
|
+
): boolean {
|
|
6469
|
+
const from = Math.max(0, Math.min(selection.anchor, selection.head));
|
|
6470
|
+
const to = Math.max(0, Math.max(selection.anchor, selection.head));
|
|
6471
|
+
if (from === to) {
|
|
6472
|
+
return replacement.length === 0;
|
|
6473
|
+
}
|
|
6474
|
+
|
|
6475
|
+
const content = activeStory.kind === "main"
|
|
6476
|
+
? document.content
|
|
6477
|
+
: {
|
|
6478
|
+
type: "doc" as const,
|
|
6479
|
+
children: [...getStoryBlocks(document, activeStory)],
|
|
6480
|
+
};
|
|
6481
|
+
const story = parseTextStory(content);
|
|
6482
|
+
if (from > story.size || to > story.size) {
|
|
6483
|
+
return false;
|
|
6484
|
+
}
|
|
6485
|
+
|
|
6486
|
+
const unitFrom = logicalPositionToUnitIndex(story.units, from, "after");
|
|
6487
|
+
const unitTo = logicalPositionToUnitIndex(story.units, to, "before");
|
|
6488
|
+
const selectedText = createPlainText({
|
|
6489
|
+
firstParagraph: story.firstParagraph,
|
|
6490
|
+
units: story.units.slice(unitFrom, unitTo),
|
|
6491
|
+
size: to - from,
|
|
6492
|
+
});
|
|
6493
|
+
return selectedText === replacement;
|
|
6494
|
+
}
|
|
6495
|
+
|
|
6451
6496
|
/**
|
|
6452
6497
|
* I2 Tier B Slice 4b — extract the selection range from a document as a
|
|
6453
6498
|
* `CanonicalDocumentFragment`. The fragment preserves text + marks +
|
|
@@ -1018,8 +1018,15 @@
|
|
|
1018
1018
|
* of reconstructing chapter-prefixed numbering in caller-local formulas.
|
|
1019
1019
|
* Cache envelopes from v59 invalidate because page graph line-box,
|
|
1020
1020
|
* fragment geometry, and page-numbering payloads changed.
|
|
1021
|
+
*
|
|
1022
|
+
* 61 — Page-story resolver + paginator refresh landed on react-refactor
|
|
1023
|
+
* alongside the floating-drawings-in-page-view fix (`70acfd9ae`). Both
|
|
1024
|
+
* `page-story-resolver.ts` and `paginated-layout-engine.ts` changed
|
|
1025
|
+
* without an upstream bump. Bump here so persisted cache envelopes
|
|
1026
|
+
* re-derive; no algorithm change beyond the floating-drawing clip
|
|
1027
|
+
* widening already covered by the resurrected tests.
|
|
1021
1028
|
*/
|
|
1022
|
-
export const LAYOUT_ENGINE_VERSION =
|
|
1029
|
+
export const LAYOUT_ENGINE_VERSION = 61 as const;
|
|
1023
1030
|
|
|
1024
1031
|
/**
|
|
1025
1032
|
* Serialization schema version for the LayCache payload (the cache envelope
|
|
@@ -2432,17 +2432,33 @@ function isOutOfFlowFrame(
|
|
|
2432
2432
|
}
|
|
2433
2433
|
|
|
2434
2434
|
function hasColumnBreak(block: SurfaceBlockSnapshot): boolean {
|
|
2435
|
-
|
|
2436
|
-
(
|
|
2437
|
-
segment
|
|
2438
|
-
|
|
2439
|
-
|
|
2435
|
+
if (block.kind === "paragraph") {
|
|
2436
|
+
return block.segments.some(
|
|
2437
|
+
(segment) =>
|
|
2438
|
+
segment.kind === "opaque_inline" &&
|
|
2439
|
+
segment.label === "Column break",
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
return nestedBlocks(block).some(hasColumnBreak);
|
|
2440
2443
|
}
|
|
2441
2444
|
|
|
2442
2445
|
function hasPageBreak(block: SurfaceBlockSnapshot): boolean {
|
|
2443
|
-
|
|
2444
|
-
(
|
|
2445
|
-
segment
|
|
2446
|
-
|
|
2447
|
-
|
|
2446
|
+
if (block.kind === "paragraph") {
|
|
2447
|
+
return block.segments.some(
|
|
2448
|
+
(segment) =>
|
|
2449
|
+
segment.kind === "opaque_inline" &&
|
|
2450
|
+
segment.label === "Page break",
|
|
2451
|
+
);
|
|
2452
|
+
}
|
|
2453
|
+
return nestedBlocks(block).some(hasPageBreak);
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
function nestedBlocks(block: SurfaceBlockSnapshot): readonly SurfaceBlockSnapshot[] {
|
|
2457
|
+
if (block.kind === "sdt_block") {
|
|
2458
|
+
return block.children;
|
|
2459
|
+
}
|
|
2460
|
+
if (block.kind === "table") {
|
|
2461
|
+
return block.rows.flatMap((row) => row.cells.flatMap((cell) => cell.content));
|
|
2462
|
+
}
|
|
2463
|
+
return [];
|
|
2448
2464
|
}
|
|
@@ -49,6 +49,34 @@ import {
|
|
|
49
49
|
isBlockedImportFeatureKey,
|
|
50
50
|
} from "../preservation/store.ts";
|
|
51
51
|
import { getStoryBlocks } from "./story-targeting.ts";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Refactor/11b Slice A — internal seam for the L1 PM Node identity cache.
|
|
55
|
+
*
|
|
56
|
+
* Surface-projection attaches a parallel `(BlockNode | null)[]` array
|
|
57
|
+
* alongside `EditorSurfaceSnapshot.blocks` via a globally-registered
|
|
58
|
+
* Symbol. `pm-state-from-snapshot` reads it through an inline mirror of
|
|
59
|
+
* the same Symbol — `Symbol.for` guarantees both sides resolve to the
|
|
60
|
+
* same symbol even though they live in different layers and never
|
|
61
|
+
* import each other. This avoids a cross-layer import that would blow
|
|
62
|
+
* the Layer 11 boundary register.
|
|
63
|
+
*
|
|
64
|
+
* The property is non-enumerable, so JSON.stringify and public
|
|
65
|
+
* consumers of `EditorSurfaceSnapshot` see no change in shape.
|
|
66
|
+
*/
|
|
67
|
+
const CANONICAL_BLOCK_REFS_SYMBOL = Symbol.for("wre.canonical-block-refs");
|
|
68
|
+
|
|
69
|
+
function attachCanonicalBlockRefs(
|
|
70
|
+
snapshot: EditorSurfaceSnapshot,
|
|
71
|
+
refs: readonly (BlockNode | null)[],
|
|
72
|
+
): void {
|
|
73
|
+
Object.defineProperty(snapshot, CANONICAL_BLOCK_REFS_SYMBOL, {
|
|
74
|
+
value: refs,
|
|
75
|
+
enumerable: false,
|
|
76
|
+
configurable: true,
|
|
77
|
+
writable: false,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
52
80
|
import {
|
|
53
81
|
collectSectionContexts,
|
|
54
82
|
findHeaderFooterDocumentEntry,
|
|
@@ -182,6 +210,12 @@ export function createEditorSurfaceSnapshot(
|
|
|
182
210
|
children: [...getStoryBlocks(document, activeStory)],
|
|
183
211
|
});
|
|
184
212
|
const blocks: SurfaceBlockSnapshot[] = [];
|
|
213
|
+
// Refactor/11b Slice A — canonical BlockNode ref parallel to `blocks`.
|
|
214
|
+
// Feeds the L1 identity cache in `pm-state-from-snapshot` so unchanged
|
|
215
|
+
// canonical blocks reuse their PM Node instance across commits, which
|
|
216
|
+
// lets PM's `node.eq()` short-circuit `ViewDesc.update()`. `null` for
|
|
217
|
+
// placeholder-culled blocks — those are synthetic, not canonical.
|
|
218
|
+
const blockRefs: (BlockNode | null)[] = [];
|
|
185
219
|
const lockedFragmentIds: string[] = [];
|
|
186
220
|
// L03 boundary: construct one formatting context per projection pass.
|
|
187
221
|
// The context owns the theme resolver + numbering prefix counter +
|
|
@@ -251,6 +285,7 @@ export function createEditorSurfaceSnapshot(
|
|
|
251
285
|
|
|
252
286
|
if (isInViewport) {
|
|
253
287
|
blocks.push(surfaceBlock.block);
|
|
288
|
+
blockRefs.push(root.children[index]);
|
|
254
289
|
lockedFragmentIds.push(...surfaceBlock.lockedFragmentIds);
|
|
255
290
|
} else {
|
|
256
291
|
// Replace with size-preserving placeholder. from/to track the SAME
|
|
@@ -270,6 +305,8 @@ export function createEditorSurfaceSnapshot(
|
|
|
270
305
|
placeholderSize,
|
|
271
306
|
state: "placeholder-culled",
|
|
272
307
|
} as SurfaceBlockSnapshot);
|
|
308
|
+
// Placeholder has no canonical ref — L1 cache will skip it.
|
|
309
|
+
blockRefs.push(null);
|
|
273
310
|
// Do NOT push lockedFragmentIds — placeholder has no real fragment.
|
|
274
311
|
}
|
|
275
312
|
|
|
@@ -317,7 +354,7 @@ export function createEditorSurfaceSnapshot(
|
|
|
317
354
|
}
|
|
318
355
|
}
|
|
319
356
|
|
|
320
|
-
|
|
357
|
+
const snapshot: EditorSurfaceSnapshot = {
|
|
321
358
|
storySize: cursor,
|
|
322
359
|
plainText: createPlainText(blocks),
|
|
323
360
|
blocks,
|
|
@@ -325,6 +362,8 @@ export function createEditorSurfaceSnapshot(
|
|
|
325
362
|
secondaryStories,
|
|
326
363
|
viewportBlockRanges,
|
|
327
364
|
};
|
|
365
|
+
attachCanonicalBlockRefs(snapshot, blockRefs);
|
|
366
|
+
return snapshot;
|
|
328
367
|
}
|
|
329
368
|
|
|
330
369
|
/**
|
|
@@ -1427,7 +1466,7 @@ function appendInlineSegments(
|
|
|
1427
1466
|
return { nextCursor: start + 1, lockedFragmentIds: [node.fragmentId] };
|
|
1428
1467
|
}
|
|
1429
1468
|
case "chart_preview": {
|
|
1430
|
-
const parsedChartId = registerParsedChartPreview(node);
|
|
1469
|
+
const parsedChartId = registerParsedChartPreview(node, document);
|
|
1431
1470
|
return appendComplexPreviewSegment(paragraph, node, start, "Embedded chart", createChartDetail(node), {
|
|
1432
1471
|
previewMediaId: node.previewMediaId,
|
|
1433
1472
|
parsedChartId,
|
|
@@ -1438,6 +1477,9 @@ function appendInlineSegments(
|
|
|
1438
1477
|
previewMediaId: node.previewMediaId,
|
|
1439
1478
|
});
|
|
1440
1479
|
case "shape":
|
|
1480
|
+
if (isMicrosoftSensitivityLabelShape(node)) {
|
|
1481
|
+
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
1482
|
+
}
|
|
1441
1483
|
if (promoteSecondaryStoryTextBoxes && node.isTextBox && node.text) {
|
|
1442
1484
|
return appendTextBoxSegment(
|
|
1443
1485
|
paragraph,
|
|
@@ -1452,6 +1494,9 @@ function appendInlineSegments(
|
|
|
1452
1494
|
case "wordart":
|
|
1453
1495
|
return appendComplexPreviewSegment(paragraph, node, start, "WordArt", createWordArtDetail(node));
|
|
1454
1496
|
case "vml_shape":
|
|
1497
|
+
if (isMicrosoftSensitivityLabelShape(node)) {
|
|
1498
|
+
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
1499
|
+
}
|
|
1455
1500
|
if (promoteSecondaryStoryTextBoxes && shouldRenderSecondaryStoryVmlTextBox(node)) {
|
|
1456
1501
|
return appendTextBoxSegment(
|
|
1457
1502
|
paragraph,
|
|
@@ -1487,7 +1532,8 @@ function appendInlineSegments(
|
|
|
1487
1532
|
const label = c.isTextBox ? "Text box" : "Drawing shape";
|
|
1488
1533
|
const detail = `DrawingFrame shape (${node.anchor.wrapMode}).`;
|
|
1489
1534
|
const anchor = surfaceAnchorFromGeometry(node.anchor);
|
|
1490
|
-
const
|
|
1535
|
+
const txbxTextSegment = c.isTextBox ? extractTxbxFirstTextSegment(c.txbxBlocks) : undefined;
|
|
1536
|
+
const txbxText = txbxTextSegment?.text ?? (c.isTextBox ? c.text : undefined);
|
|
1491
1537
|
const surfaceFill = c.fill;
|
|
1492
1538
|
paragraph.segments.push({
|
|
1493
1539
|
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
@@ -1501,12 +1547,17 @@ function appendInlineSegments(
|
|
|
1501
1547
|
...(surfaceFill ? { fill: surfaceFill } : {}),
|
|
1502
1548
|
...(c.line ? { line: c.line } : {}),
|
|
1503
1549
|
...(c.isTextBox ? { isTextBox: true } : {}),
|
|
1550
|
+
...(c.textBoxBody ? { textBoxBody: c.textBoxBody } : {}),
|
|
1504
1551
|
...(txbxText ? { txbxText } : {}),
|
|
1552
|
+
...(txbxTextSegment?.marks && txbxTextSegment.marks.length > 0
|
|
1553
|
+
? { txbxMarks: txbxTextSegment.marks }
|
|
1554
|
+
: {}),
|
|
1555
|
+
...(txbxTextSegment?.markAttrs ? { txbxMarkAttrs: txbxTextSegment.markAttrs } : {}),
|
|
1505
1556
|
});
|
|
1506
1557
|
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
1507
1558
|
}
|
|
1508
1559
|
if (c.type === "chart_preview") {
|
|
1509
|
-
const parsedChartId = registerParsedChartPreview(c);
|
|
1560
|
+
const parsedChartId = registerParsedChartPreview(c, document);
|
|
1510
1561
|
return appendComplexPreviewSegment(
|
|
1511
1562
|
paragraph,
|
|
1512
1563
|
c,
|
|
@@ -1752,7 +1803,10 @@ function appendInlineSegments(
|
|
|
1752
1803
|
}
|
|
1753
1804
|
}
|
|
1754
1805
|
|
|
1755
|
-
function registerParsedChartPreview(
|
|
1806
|
+
function registerParsedChartPreview(
|
|
1807
|
+
node: ChartPreviewNode,
|
|
1808
|
+
document: CanonicalDocumentEnvelope,
|
|
1809
|
+
): string | undefined {
|
|
1756
1810
|
if (!node.parsedData) return undefined;
|
|
1757
1811
|
const parsedChartId = stableChartId(node.rawXml);
|
|
1758
1812
|
// Always call `set` (even when the entry exists) so the active
|
|
@@ -1763,7 +1817,7 @@ function registerParsedChartPreview(node: ChartPreviewNode): string | undefined
|
|
|
1763
1817
|
model: node.parsedData,
|
|
1764
1818
|
widthPx,
|
|
1765
1819
|
heightPx,
|
|
1766
|
-
theme:
|
|
1820
|
+
theme: document.subParts?.resolvedTheme,
|
|
1767
1821
|
});
|
|
1768
1822
|
return parsedChartId;
|
|
1769
1823
|
}
|
|
@@ -1898,6 +1952,7 @@ function surfacePictureEffectsFromContent(
|
|
|
1898
1952
|
const outerShadow = resolveSurfacePictureShadow(content.outerShadow, themeResolver);
|
|
1899
1953
|
const glow = resolveSurfacePictureGlow(content.glow, themeResolver);
|
|
1900
1954
|
const has =
|
|
1955
|
+
content.lum !== undefined ||
|
|
1901
1956
|
content.srcRect !== undefined ||
|
|
1902
1957
|
content.rotation !== undefined ||
|
|
1903
1958
|
content.flipH !== undefined ||
|
|
@@ -1909,6 +1964,7 @@ function surfacePictureEffectsFromContent(
|
|
|
1909
1964
|
glow !== undefined;
|
|
1910
1965
|
if (!has) return undefined;
|
|
1911
1966
|
return {
|
|
1967
|
+
...(content.lum ? { lum: { ...content.lum } } : {}),
|
|
1912
1968
|
...(content.srcRect ? { srcRect: { ...content.srcRect } } : {}),
|
|
1913
1969
|
...(content.rotation !== undefined ? { rotation: content.rotation } : {}),
|
|
1914
1970
|
...(content.flipH !== undefined ? { flipH: content.flipH } : {}),
|
|
@@ -2007,16 +2063,51 @@ function flattenSurfaceFieldDisplayText(
|
|
|
2007
2063
|
* and read the immediate run content. Returns `undefined` when no text
|
|
2008
2064
|
* is present.
|
|
2009
2065
|
*/
|
|
2010
|
-
function
|
|
2066
|
+
function extractTxbxFirstTextSegment(
|
|
2011
2067
|
blocks: ShapeContent["txbxBlocks"],
|
|
2012
|
-
):
|
|
2068
|
+
): {
|
|
2069
|
+
text: string;
|
|
2070
|
+
marks?: SurfaceTextMark[];
|
|
2071
|
+
markAttrs?: {
|
|
2072
|
+
backgroundColor?: string;
|
|
2073
|
+
charSpacing?: number;
|
|
2074
|
+
kerning?: number;
|
|
2075
|
+
textFill?: string;
|
|
2076
|
+
fontFamily?: string;
|
|
2077
|
+
fontSize?: number;
|
|
2078
|
+
textColor?: string;
|
|
2079
|
+
};
|
|
2080
|
+
} | undefined {
|
|
2013
2081
|
if (!blocks || blocks.length === 0) return undefined;
|
|
2014
2082
|
for (const block of blocks) {
|
|
2015
2083
|
if (block.type !== "paragraph") continue;
|
|
2016
|
-
const
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2084
|
+
const children = (block as { children?: ReadonlyArray<{ type?: string; text?: string; marks?: TextMark[] }> }).children;
|
|
2085
|
+
const legacyRuns = (block as { runs?: ReadonlyArray<{ text?: string; marks?: TextMark[] }> }).runs;
|
|
2086
|
+
if (!children && !legacyRuns) continue;
|
|
2087
|
+
let firstMarks: TextMark[] | undefined;
|
|
2088
|
+
const text = (children
|
|
2089
|
+
? children
|
|
2090
|
+
.map((child) => {
|
|
2091
|
+
if (child.type !== "text") return "";
|
|
2092
|
+
if (!firstMarks && child.marks) firstMarks = child.marks;
|
|
2093
|
+
return child.text ?? "";
|
|
2094
|
+
})
|
|
2095
|
+
.join("")
|
|
2096
|
+
: legacyRuns
|
|
2097
|
+
?.map((run) => {
|
|
2098
|
+
if (!firstMarks && run.marks) firstMarks = run.marks;
|
|
2099
|
+
return run.text ?? "";
|
|
2100
|
+
})
|
|
2101
|
+
.join("") ?? ""
|
|
2102
|
+
).trim();
|
|
2103
|
+
if (text) {
|
|
2104
|
+
const cloned = firstMarks ? cloneMarks(firstMarks) : undefined;
|
|
2105
|
+
return {
|
|
2106
|
+
text,
|
|
2107
|
+
...(cloned?.marks && cloned.marks.length > 0 ? { marks: cloned.marks } : {}),
|
|
2108
|
+
...(cloned?.markAttrs ? { markAttrs: cloned.markAttrs } : {}),
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2020
2111
|
}
|
|
2021
2112
|
return undefined;
|
|
2022
2113
|
}
|
|
@@ -2072,6 +2163,17 @@ function shouldRenderSecondaryStoryVmlTextBox(node: VmlShapeNode): boolean {
|
|
|
2072
2163
|
return Boolean(node.text) && (!node.shapeType || /_x0000_t202$/iu.test(node.shapeType));
|
|
2073
2164
|
}
|
|
2074
2165
|
|
|
2166
|
+
function isMicrosoftSensitivityLabelShape(
|
|
2167
|
+
node: ShapeNode | VmlShapeNode,
|
|
2168
|
+
): boolean {
|
|
2169
|
+
if (!/classification/i.test(node.text ?? "")) {
|
|
2170
|
+
return false;
|
|
2171
|
+
}
|
|
2172
|
+
return /\b(?:id|name)="MSIPCM/iu.test(node.rawXml) ||
|
|
2173
|
+
/\b(?:alt|descr)="[^"]*"Placement":"Footer"/iu.test(node.rawXml) ||
|
|
2174
|
+
/"Placement"\s*:\s*"Footer"/iu.test(node.rawXml);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2075
2177
|
function createChartDetail(node: ChartPreviewNode): string {
|
|
2076
2178
|
const parts = ["Embedded chart."];
|
|
2077
2179
|
if (node.previewMediaId) {
|
|
@@ -94,6 +94,11 @@ export function collectScopeRailSegments(
|
|
|
94
94
|
const activeIds = new Set(input.activeWorkItemScopeIds ?? []);
|
|
95
95
|
|
|
96
96
|
for (const scope of input.scopes ?? []) {
|
|
97
|
+
// Invisible scopes are runtime/agent context only. They may still
|
|
98
|
+
// participate in guard decisions, but they must not surface as rail,
|
|
99
|
+
// card, or body-tint chrome.
|
|
100
|
+
if (scope.visibility === "invisible") continue;
|
|
101
|
+
|
|
97
102
|
const range = anchorToRuntimeRange(scope.anchor);
|
|
98
103
|
if (!range) continue;
|
|
99
104
|
const storyTarget = scope.storyTarget ?? MAIN_STORY_TARGET;
|
|
@@ -187,7 +187,6 @@ import {
|
|
|
187
187
|
withExportDelivery,
|
|
188
188
|
} from "./browser-export";
|
|
189
189
|
import { EditorShellView } from "./editor-shell-view.tsx";
|
|
190
|
-
import { TwDebugPresentation } from "../ui-tailwind/debug/index.ts";
|
|
191
190
|
import { shellPasteFragmentParser as SHELL_PASTE_FRAGMENT_PARSER } from "../shell/paste-adapter.ts";
|
|
192
191
|
import { EditorSurfaceController } from "./editor-surface-controller.tsx";
|
|
193
192
|
import type { EditorActionHostCallbacks } from "../ui-tailwind/chrome/editor-action-registry";
|
|
@@ -1061,7 +1060,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1061
1060
|
chromeControllerRef,
|
|
1062
1061
|
commandPaletteDisabled,
|
|
1063
1062
|
customSelectionTools,
|
|
1064
|
-
debugMode = "off",
|
|
1065
1063
|
} = props;
|
|
1066
1064
|
|
|
1067
1065
|
const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
|
|
@@ -1140,7 +1138,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1140
1138
|
hostPosture: {
|
|
1141
1139
|
reviewMode: reviewMode === "review" ? "reviewer" : "author",
|
|
1142
1140
|
markupDisplay: normalizeHostMarkupDisplay(markupDisplay),
|
|
1143
|
-
debugMode,
|
|
1144
1141
|
chromePreset,
|
|
1145
1142
|
},
|
|
1146
1143
|
chromePresetInput: {
|
|
@@ -1261,7 +1258,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1261
1258
|
api,
|
|
1262
1259
|
reviewMode,
|
|
1263
1260
|
markupDisplay,
|
|
1264
|
-
debugMode,
|
|
1265
1261
|
chromePreset,
|
|
1266
1262
|
readOnly,
|
|
1267
1263
|
]);
|
|
@@ -3395,6 +3391,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3395
3391
|
activeRuntime.rejectAllChanges();
|
|
3396
3392
|
setActiveRailTab("changes");
|
|
3397
3393
|
},
|
|
3394
|
+
onAcceptSuggestionGroup: (groupId: string) =>
|
|
3395
|
+
applySuggestionGroupAction(activeRuntime, groupId, "accept"),
|
|
3396
|
+
onRejectSuggestionGroup: (groupId: string) =>
|
|
3397
|
+
applySuggestionGroupAction(activeRuntime, groupId, "reject"),
|
|
3398
3398
|
onCloseStory: () => {
|
|
3399
3399
|
activeRuntime.closeStory();
|
|
3400
3400
|
},
|
|
@@ -3907,10 +3907,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3907
3907
|
});
|
|
3908
3908
|
}}
|
|
3909
3909
|
onScopeAcceptSuggestionGroup={(payload) => {
|
|
3910
|
-
|
|
3910
|
+
commands.onAcceptSuggestionGroup?.(payload.groupId);
|
|
3911
3911
|
}}
|
|
3912
3912
|
onScopeRejectSuggestionGroup={(payload) => {
|
|
3913
|
-
|
|
3913
|
+
commands.onRejectSuggestionGroup?.(payload.groupId);
|
|
3914
3914
|
}}
|
|
3915
3915
|
mediaPreviews={mediaPreviews}
|
|
3916
3916
|
onActivateFloatingImage={(payload) => {
|
|
@@ -3959,10 +3959,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3959
3959
|
}}
|
|
3960
3960
|
/>
|
|
3961
3961
|
<TwRuntimeReplDialog runtime={activeRuntime} editorRef={editorRefForRepl} />
|
|
3962
|
-
<TwDebugPresentation
|
|
3963
|
-
mode={debugMode}
|
|
3964
|
-
sessionId={documentId}
|
|
3965
|
-
/>
|
|
3966
3962
|
</>
|
|
3967
3963
|
</OverlayAnchorBridgeProvider>
|
|
3968
3964
|
</UiShellChannelsProvider>
|
|
@@ -104,6 +104,8 @@ export interface EditorCommandBag {
|
|
|
104
104
|
onRejectRevision(revisionId: string): void;
|
|
105
105
|
onAcceptAllChanges(): void;
|
|
106
106
|
onRejectAllChanges(): void;
|
|
107
|
+
onAcceptSuggestionGroup?(groupId: string): void;
|
|
108
|
+
onRejectSuggestionGroup?(groupId: string): void;
|
|
107
109
|
onCloseStory?(): void;
|
|
108
110
|
/**
|
|
109
111
|
* @deprecated P8.11 — see the matching prop on `TwReviewWorkspaceProps`.
|
|
@@ -79,7 +79,7 @@ let nextControllerId = 0;
|
|
|
79
79
|
export interface ShellUiControllerDeps {
|
|
80
80
|
/**
|
|
81
81
|
* Current host-provided posture slice — reviewMode / markupDisplay /
|
|
82
|
-
*
|
|
82
|
+
* chromePreset. MUST read through a live ref so the factory
|
|
83
83
|
* closure returns current render state, not stale construction-time state.
|
|
84
84
|
*/
|
|
85
85
|
readonly getHostPosture?: () => ChromeHostPosture | undefined;
|
|
@@ -95,7 +95,7 @@ export interface ShellUiControllerDeps {
|
|
|
95
95
|
readonly getOverlayAnchor?: (query: OverlayAnchorQuery) => GeometryRect | null;
|
|
96
96
|
/**
|
|
97
97
|
* Posture-change stream. Fires when any of the inputs to `ChromePosture`
|
|
98
|
-
* change — reviewMode / markupDisplay /
|
|
98
|
+
* change — reviewMode / markupDisplay / chromePreset (host),
|
|
99
99
|
* effectiveMode / blockedReasons / documentMode / readOnly (runtime).
|
|
100
100
|
* Returns an unsubscribe function.
|
|
101
101
|
*
|