@beyondwork/docx-react-component 1.0.42 → 1.0.43
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 +30 -41
- package/src/api/editor-state-types.ts +110 -0
- package/src/api/public-types.ts +194 -1
- package/src/core/commands/index.ts +33 -8
- package/src/core/search/search-text.ts +15 -2
- package/src/index.ts +13 -0
- package/src/io/docx-session.ts +672 -2
- package/src/io/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +83 -0
- package/src/io/ooxml/workflow-payload-validator.ts +97 -1
- package/src/io/ooxml/workflow-payload.ts +172 -1
- package/src/runtime/collab-session.ts +1 -1
- package/src/runtime/document-runtime.ts +364 -36
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +2 -0
- package/src/runtime/layout/layout-engine-instance.ts +17 -2
- package/src/runtime/layout/paginated-layout-engine.ts +211 -14
- package/src/runtime/layout/public-facet.ts +400 -1
- package/src/runtime/perf-counters.ts +28 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/surface-projection.ts +10 -5
- package/src/runtime/workflow-markup.ts +71 -16
- package/src/ui/WordReviewEditor.tsx +67 -45
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +110 -11
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +48 -0
- package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
- package/src/ui-tailwind/editor-surface/pm-schema.ts +152 -4
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +35 -7
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +8 -255
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -0
- package/src/ui-tailwind/index.ts +5 -1
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- package/src/ui-tailwind/tw-review-workspace.tsx +172 -94
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract a sequence of callback operations from a plain-text clipboard
|
|
3
|
+
* payload. Used by the `pm-command-bridge` handlePaste / handleDrop
|
|
4
|
+
* hooks to turn a pasted string into calls to `onInsertText` /
|
|
5
|
+
* `onSplitParagraph` / `onInsertHardBreak` — the same runtime-owned
|
|
6
|
+
* callbacks typing already uses.
|
|
7
|
+
*
|
|
8
|
+
* Separator convention:
|
|
9
|
+
* - LF (`\n`), CRLF (`\r\n`), CR (`\r`) → paragraph split
|
|
10
|
+
* ({ kind: "split" }).
|
|
11
|
+
* - U+000B (vertical tab) → hard break ({ kind: "hard_break" }).
|
|
12
|
+
* Word's plain-text clipboard represents shift-enter line breaks
|
|
13
|
+
* as vertical-tab; preserving them keeps paragraph-internal line
|
|
14
|
+
* structure.
|
|
15
|
+
* - Tab (U+0009) stays inside the current text segment. The
|
|
16
|
+
* runtime's `onInsertText` decides whether it is rendered as a
|
|
17
|
+
* tab-stop or swallowed.
|
|
18
|
+
*
|
|
19
|
+
* Consecutive separators produce multiple splits in sequence (empty
|
|
20
|
+
* paragraphs). Leading / trailing separators are emitted verbatim so
|
|
21
|
+
* the caller can decide whether to coalesce into a trailing blank.
|
|
22
|
+
*
|
|
23
|
+
* Pure function: no DOM, no React, no ProseMirror state.
|
|
24
|
+
*
|
|
25
|
+
* Source plan: `docs/plans/editor-paste-drop.md` §Phase 1.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export type PastePlainSegment =
|
|
29
|
+
| { kind: "text"; value: string }
|
|
30
|
+
| { kind: "split" }
|
|
31
|
+
| { kind: "hard_break" };
|
|
32
|
+
|
|
33
|
+
export function extractPlainTextSegments(input: string): PastePlainSegment[] {
|
|
34
|
+
if (input.length === 0) return [];
|
|
35
|
+
|
|
36
|
+
const segments: PastePlainSegment[] = [];
|
|
37
|
+
let buffer = "";
|
|
38
|
+
|
|
39
|
+
const flush = (): void => {
|
|
40
|
+
if (buffer.length > 0) {
|
|
41
|
+
segments.push({ kind: "text", value: buffer });
|
|
42
|
+
buffer = "";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
47
|
+
const ch = input[i];
|
|
48
|
+
|
|
49
|
+
if (ch === "\r") {
|
|
50
|
+
flush();
|
|
51
|
+
segments.push({ kind: "split" });
|
|
52
|
+
// Collapse CRLF into one split — advance past the LF.
|
|
53
|
+
if (input[i + 1] === "\n") i += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (ch === "\n") {
|
|
57
|
+
flush();
|
|
58
|
+
segments.push({ kind: "split" });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch === "\u000B") {
|
|
62
|
+
flush();
|
|
63
|
+
segments.push({ kind: "hard_break" });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
buffer += ch;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
flush();
|
|
71
|
+
return segments;
|
|
72
|
+
}
|
|
@@ -7,8 +7,62 @@ import {
|
|
|
7
7
|
createSelectionSnapshot,
|
|
8
8
|
} from "../../ui/headless/selection-helpers";
|
|
9
9
|
import { resolveSurfaceShortcut } from "../../ui/runtime-shortcut-dispatch";
|
|
10
|
+
import {
|
|
11
|
+
extractPlainTextSegments,
|
|
12
|
+
type PastePlainSegment,
|
|
13
|
+
} from "./paste-plain-text";
|
|
10
14
|
import type { PositionMap } from "./pm-position-map";
|
|
11
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Callback subset used by paste / drop dispatch. Exported so tests can
|
|
18
|
+
* record dispatch order without constructing the full
|
|
19
|
+
* `CommandBridgeCallbacks` surface.
|
|
20
|
+
*/
|
|
21
|
+
export interface PasteDispatchCallbacks {
|
|
22
|
+
onInsertText: (text: string) => void;
|
|
23
|
+
onSplitParagraph: () => void;
|
|
24
|
+
onInsertHardBreak: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Dispatch an ordered list of plain-text segments to the runtime-owned
|
|
29
|
+
* callbacks. Empty text segments are skipped. Pure with respect to the
|
|
30
|
+
* callbacks — no global state, no PM mutation.
|
|
31
|
+
*/
|
|
32
|
+
export function applyPasteSegmentsToCallbacks(
|
|
33
|
+
segments: readonly PastePlainSegment[],
|
|
34
|
+
callbacks: PasteDispatchCallbacks,
|
|
35
|
+
): void {
|
|
36
|
+
for (const seg of segments) {
|
|
37
|
+
switch (seg.kind) {
|
|
38
|
+
case "text":
|
|
39
|
+
if (seg.value.length > 0) callbacks.onInsertText(seg.value);
|
|
40
|
+
break;
|
|
41
|
+
case "split":
|
|
42
|
+
callbacks.onSplitParagraph();
|
|
43
|
+
break;
|
|
44
|
+
case "hard_break":
|
|
45
|
+
callbacks.onInsertHardBreak();
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Sum the character length across every `text` segment. Used by the
|
|
53
|
+
* paste / drop handlers to populate the `charCount` field of the
|
|
54
|
+
* public `paste_applied` event.
|
|
55
|
+
*/
|
|
56
|
+
export function totalTextCharCount(
|
|
57
|
+
segments: readonly PastePlainSegment[],
|
|
58
|
+
): number {
|
|
59
|
+
let total = 0;
|
|
60
|
+
for (const seg of segments) {
|
|
61
|
+
if (seg.kind === "text") total += seg.value.length;
|
|
62
|
+
}
|
|
63
|
+
return total;
|
|
64
|
+
}
|
|
65
|
+
|
|
12
66
|
export interface SelectionSyncCallbacks {
|
|
13
67
|
onSelectionChange: (selection: SelectionSnapshot) => void;
|
|
14
68
|
getPositionMap: () => PositionMap | null;
|
|
@@ -28,6 +82,17 @@ export interface CommandBridgeCallbacks extends SelectionSyncCallbacks {
|
|
|
28
82
|
onUndo: () => void;
|
|
29
83
|
onRedo: () => void;
|
|
30
84
|
onBlockedInput?: (command: "paste" | "drop", message: string) => void;
|
|
85
|
+
/**
|
|
86
|
+
* Optional. Fires after a plain-text paste or drop successfully
|
|
87
|
+
* dispatches through the runtime callbacks. `source` distinguishes
|
|
88
|
+
* paste from drop. Rich paste (HTML, Office clipboard) still fires
|
|
89
|
+
* `onBlockedInput`, not this callback.
|
|
90
|
+
*/
|
|
91
|
+
onPasteApplied?: (meta: {
|
|
92
|
+
segmentCount: number;
|
|
93
|
+
charCount: number;
|
|
94
|
+
source: "paste" | "drop";
|
|
95
|
+
}) => void;
|
|
31
96
|
/**
|
|
32
97
|
* Optional. Fires on `compositionstart` (true) and `compositionend`
|
|
33
98
|
* (false). The surface forwards this to the predicted lane's session
|
|
@@ -130,16 +195,61 @@ export function createCommandBridgePlugins(
|
|
|
130
195
|
return true; // Block PM from processing
|
|
131
196
|
},
|
|
132
197
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
198
|
+
// Plain-text paste: extract text/plain from the clipboard and
|
|
199
|
+
// dispatch through the runtime-owned callbacks that typing uses.
|
|
200
|
+
// Rich paste (HTML, Office clipboard) stays blocked — hosts that
|
|
201
|
+
// listen for onBlockedInput still get notified when a non-plain-
|
|
202
|
+
// text payload arrives. See docs/plans/editor-paste-drop.md.
|
|
203
|
+
handlePaste(_view, event) {
|
|
204
|
+
if (isComposing) return true;
|
|
205
|
+
const clipboard = event.clipboardData;
|
|
206
|
+
if (!clipboard) {
|
|
207
|
+
callbacks.onBlockedInput?.("paste", "Clipboard data was not available.");
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
const plain = clipboard.getData("text/plain");
|
|
211
|
+
if (!plain) {
|
|
212
|
+
callbacks.onBlockedInput?.(
|
|
213
|
+
"paste",
|
|
214
|
+
"Non-plain-text paste is not supported yet.",
|
|
215
|
+
);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
const segments = extractPlainTextSegments(plain);
|
|
219
|
+
applyPasteSegmentsToCallbacks(segments, callbacks);
|
|
220
|
+
callbacks.onPasteApplied?.({
|
|
221
|
+
segmentCount: segments.length,
|
|
222
|
+
charCount: totalTextCharCount(segments),
|
|
223
|
+
source: "paste",
|
|
224
|
+
});
|
|
225
|
+
return true;
|
|
137
226
|
},
|
|
138
227
|
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return true;
|
|
228
|
+
// Plain-text drop: symmetric path — extract text/plain from the
|
|
229
|
+
// DataTransfer and dispatch through the same callbacks paste uses.
|
|
230
|
+
handleDrop(_view, event) {
|
|
231
|
+
if (isComposing) return true;
|
|
232
|
+
const dt = (event as DragEvent).dataTransfer;
|
|
233
|
+
if (!dt) {
|
|
234
|
+
callbacks.onBlockedInput?.("drop", "Drop data was not available.");
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
const plain = dt.getData("text/plain");
|
|
238
|
+
if (!plain) {
|
|
239
|
+
callbacks.onBlockedInput?.(
|
|
240
|
+
"drop",
|
|
241
|
+
"Non-plain-text drop is not supported yet.",
|
|
242
|
+
);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
const segments = extractPlainTextSegments(plain);
|
|
246
|
+
applyPasteSegmentsToCallbacks(segments, callbacks);
|
|
247
|
+
callbacks.onPasteApplied?.({
|
|
248
|
+
segmentCount: segments.length,
|
|
249
|
+
charCount: totalTextCharCount(segments),
|
|
250
|
+
source: "drop",
|
|
251
|
+
});
|
|
252
|
+
return true;
|
|
143
253
|
},
|
|
144
254
|
},
|
|
145
255
|
});
|
|
@@ -634,16 +634,51 @@ export const editorSchema = new Schema({
|
|
|
634
634
|
selectable: false,
|
|
635
635
|
attrs: {
|
|
636
636
|
previewMediaId: { default: null },
|
|
637
|
+
previewSrc: { default: null },
|
|
637
638
|
detail: { default: null },
|
|
638
639
|
},
|
|
639
640
|
toDOM(node) {
|
|
641
|
+
const previewSrc = node.attrs.previewSrc as string | null;
|
|
642
|
+
const detail = (node.attrs.detail as string) ?? "Chart";
|
|
643
|
+
if (previewSrc) {
|
|
644
|
+
// Bitmap-backed: render the fallback image Word cached in mc:Fallback.
|
|
645
|
+
// The corner chip preserves the typed identity so agents and humans
|
|
646
|
+
// still see "this is a chart" at a glance.
|
|
647
|
+
return [
|
|
648
|
+
"span",
|
|
649
|
+
{
|
|
650
|
+
class: "relative inline-block align-baseline mx-0.5 max-w-full",
|
|
651
|
+
"data-node-type": "chart_atom",
|
|
652
|
+
"data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
|
|
653
|
+
contenteditable: "false",
|
|
654
|
+
title: detail,
|
|
655
|
+
},
|
|
656
|
+
[
|
|
657
|
+
"img",
|
|
658
|
+
{
|
|
659
|
+
src: previewSrc,
|
|
660
|
+
alt: detail,
|
|
661
|
+
class: "block max-w-full h-auto",
|
|
662
|
+
draggable: "false",
|
|
663
|
+
},
|
|
664
|
+
],
|
|
665
|
+
[
|
|
666
|
+
"span",
|
|
667
|
+
{
|
|
668
|
+
class: "absolute top-1 right-1 inline-flex items-center gap-0.5 rounded border border-blue-200 bg-blue-50/90 px-1 py-0.5 text-[10px] text-blue-700",
|
|
669
|
+
"aria-hidden": "true",
|
|
670
|
+
},
|
|
671
|
+
"\uD83D\uDCC8 Chart",
|
|
672
|
+
],
|
|
673
|
+
];
|
|
674
|
+
}
|
|
640
675
|
return [
|
|
641
676
|
"span",
|
|
642
677
|
{
|
|
643
678
|
class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-blue-700 bg-blue-50 border border-blue-200",
|
|
644
679
|
"data-node-type": "chart_atom",
|
|
645
680
|
contenteditable: "false",
|
|
646
|
-
title:
|
|
681
|
+
title: detail,
|
|
647
682
|
},
|
|
648
683
|
"\uD83D\uDCC8 Chart",
|
|
649
684
|
];
|
|
@@ -657,16 +692,48 @@ export const editorSchema = new Schema({
|
|
|
657
692
|
selectable: false,
|
|
658
693
|
attrs: {
|
|
659
694
|
previewMediaId: { default: null },
|
|
695
|
+
previewSrc: { default: null },
|
|
660
696
|
detail: { default: null },
|
|
661
697
|
},
|
|
662
698
|
toDOM(node) {
|
|
699
|
+
const previewSrc = node.attrs.previewSrc as string | null;
|
|
700
|
+
const detail = (node.attrs.detail as string) ?? "SmartArt";
|
|
701
|
+
if (previewSrc) {
|
|
702
|
+
return [
|
|
703
|
+
"span",
|
|
704
|
+
{
|
|
705
|
+
class: "relative inline-block align-baseline mx-0.5 max-w-full",
|
|
706
|
+
"data-node-type": "smartart_atom",
|
|
707
|
+
"data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
|
|
708
|
+
contenteditable: "false",
|
|
709
|
+
title: detail,
|
|
710
|
+
},
|
|
711
|
+
[
|
|
712
|
+
"img",
|
|
713
|
+
{
|
|
714
|
+
src: previewSrc,
|
|
715
|
+
alt: detail,
|
|
716
|
+
class: "block max-w-full h-auto",
|
|
717
|
+
draggable: "false",
|
|
718
|
+
},
|
|
719
|
+
],
|
|
720
|
+
[
|
|
721
|
+
"span",
|
|
722
|
+
{
|
|
723
|
+
class: "absolute top-1 right-1 inline-flex items-center gap-0.5 rounded border border-purple-200 bg-purple-50/90 px-1 py-0.5 text-[10px] text-purple-700",
|
|
724
|
+
"aria-hidden": "true",
|
|
725
|
+
},
|
|
726
|
+
"\uD83D\uDDFA SmartArt",
|
|
727
|
+
],
|
|
728
|
+
];
|
|
729
|
+
}
|
|
663
730
|
return [
|
|
664
731
|
"span",
|
|
665
732
|
{
|
|
666
733
|
class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-purple-700 bg-purple-50 border border-purple-200",
|
|
667
734
|
"data-node-type": "smartart_atom",
|
|
668
735
|
contenteditable: "false",
|
|
669
|
-
title:
|
|
736
|
+
title: detail,
|
|
670
737
|
},
|
|
671
738
|
"\uD83D\uDDFA SmartArt",
|
|
672
739
|
];
|
|
@@ -707,17 +774,50 @@ export const editorSchema = new Schema({
|
|
|
707
774
|
attrs: {
|
|
708
775
|
text: { default: "" },
|
|
709
776
|
geometry: { default: null },
|
|
777
|
+
previewMediaId: { default: null },
|
|
778
|
+
previewSrc: { default: null },
|
|
710
779
|
detail: { default: null },
|
|
711
780
|
},
|
|
712
781
|
toDOM(node) {
|
|
713
782
|
const text = node.attrs.text as string;
|
|
783
|
+
const previewSrc = node.attrs.previewSrc as string | null;
|
|
784
|
+
const detail = (node.attrs.detail as string) ?? (text ? `WordArt: ${text}` : "WordArt");
|
|
785
|
+
if (previewSrc) {
|
|
786
|
+
return [
|
|
787
|
+
"span",
|
|
788
|
+
{
|
|
789
|
+
class: "relative inline-block align-baseline mx-0.5 max-w-full",
|
|
790
|
+
"data-node-type": "wordart_atom",
|
|
791
|
+
"data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
|
|
792
|
+
contenteditable: "false",
|
|
793
|
+
title: detail,
|
|
794
|
+
},
|
|
795
|
+
[
|
|
796
|
+
"img",
|
|
797
|
+
{
|
|
798
|
+
src: previewSrc,
|
|
799
|
+
alt: detail,
|
|
800
|
+
class: "block max-w-full h-auto",
|
|
801
|
+
draggable: "false",
|
|
802
|
+
},
|
|
803
|
+
],
|
|
804
|
+
[
|
|
805
|
+
"span",
|
|
806
|
+
{
|
|
807
|
+
class: "absolute top-1 right-1 inline-flex items-center gap-0.5 rounded border border-orange-200 bg-orange-50/90 px-1 py-0.5 text-[10px] text-orange-700",
|
|
808
|
+
"aria-hidden": "true",
|
|
809
|
+
},
|
|
810
|
+
"\u2728 WordArt",
|
|
811
|
+
],
|
|
812
|
+
];
|
|
813
|
+
}
|
|
714
814
|
return [
|
|
715
815
|
"span",
|
|
716
816
|
{
|
|
717
817
|
class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-orange-700 bg-orange-50 border border-orange-200 font-medium italic",
|
|
718
818
|
"data-node-type": "wordart_atom",
|
|
719
819
|
contenteditable: "false",
|
|
720
|
-
title:
|
|
820
|
+
title: detail,
|
|
721
821
|
},
|
|
722
822
|
"\u2728 " + (text || "WordArt"),
|
|
723
823
|
];
|
|
@@ -732,18 +832,51 @@ export const editorSchema = new Schema({
|
|
|
732
832
|
attrs: {
|
|
733
833
|
text: { default: null },
|
|
734
834
|
shapeType: { default: null },
|
|
835
|
+
previewMediaId: { default: null },
|
|
836
|
+
previewSrc: { default: null },
|
|
735
837
|
detail: { default: null },
|
|
736
838
|
},
|
|
737
839
|
toDOM(node) {
|
|
738
840
|
const text = node.attrs.text as string | null;
|
|
739
841
|
const label = text ? `VML: ${text}` : "VML shape";
|
|
842
|
+
const previewSrc = node.attrs.previewSrc as string | null;
|
|
843
|
+
const detail = (node.attrs.detail as string) ?? label;
|
|
844
|
+
if (previewSrc) {
|
|
845
|
+
return [
|
|
846
|
+
"span",
|
|
847
|
+
{
|
|
848
|
+
class: "relative inline-block align-baseline mx-0.5 max-w-full",
|
|
849
|
+
"data-node-type": "vml_atom",
|
|
850
|
+
"data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
|
|
851
|
+
contenteditable: "false",
|
|
852
|
+
title: detail,
|
|
853
|
+
},
|
|
854
|
+
[
|
|
855
|
+
"img",
|
|
856
|
+
{
|
|
857
|
+
src: previewSrc,
|
|
858
|
+
alt: detail,
|
|
859
|
+
class: "block max-w-full h-auto",
|
|
860
|
+
draggable: "false",
|
|
861
|
+
},
|
|
862
|
+
],
|
|
863
|
+
[
|
|
864
|
+
"span",
|
|
865
|
+
{
|
|
866
|
+
class: "absolute top-1 right-1 inline-flex items-center gap-0.5 rounded border border-gray-300 bg-gray-100/90 px-1 py-0.5 text-[10px] text-gray-600",
|
|
867
|
+
"aria-hidden": "true",
|
|
868
|
+
},
|
|
869
|
+
"\u25A6 VML",
|
|
870
|
+
],
|
|
871
|
+
];
|
|
872
|
+
}
|
|
740
873
|
return [
|
|
741
874
|
"span",
|
|
742
875
|
{
|
|
743
876
|
class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-gray-600 bg-gray-100 border border-gray-300",
|
|
744
877
|
"data-node-type": "vml_atom",
|
|
745
878
|
contenteditable: "false",
|
|
746
|
-
title:
|
|
879
|
+
title: detail,
|
|
747
880
|
},
|
|
748
881
|
"\u25A6 " + label,
|
|
749
882
|
];
|
|
@@ -759,10 +892,25 @@ export const editorSchema = new Schema({
|
|
|
759
892
|
warningId: { default: "" },
|
|
760
893
|
label: { default: "Locked" },
|
|
761
894
|
detail: { default: "" },
|
|
895
|
+
presentation: { default: "callout" },
|
|
762
896
|
},
|
|
763
897
|
toDOM(node) {
|
|
764
898
|
const fragmentId = node.attrs.fragmentId as string;
|
|
765
899
|
const isPreview = fragmentId.startsWith("preview:");
|
|
900
|
+
const presentation = node.attrs.presentation as string;
|
|
901
|
+
if (presentation === "quiet-marker") {
|
|
902
|
+
return [
|
|
903
|
+
"div",
|
|
904
|
+
{
|
|
905
|
+
class: "block h-0 w-0 overflow-hidden",
|
|
906
|
+
contenteditable: "false",
|
|
907
|
+
"data-node-type": "opaque_block",
|
|
908
|
+
"data-block-presentation": "quiet-marker",
|
|
909
|
+
title: node.attrs.detail as string,
|
|
910
|
+
"aria-label": node.attrs.label as string,
|
|
911
|
+
},
|
|
912
|
+
];
|
|
913
|
+
}
|
|
766
914
|
return [
|
|
767
915
|
"div",
|
|
768
916
|
{
|
|
@@ -268,7 +268,7 @@ function buildPMBlocks(
|
|
|
268
268
|
} else if (block.kind === "sdt_block") {
|
|
269
269
|
nodes.push(buildSdtBlock(block, mediaPreviews, showUnsupportedObjectPreviews));
|
|
270
270
|
} else {
|
|
271
|
-
nodes.push(buildOpaqueBlock(block));
|
|
271
|
+
nodes.push(buildOpaqueBlock(block, showUnsupportedObjectPreviews));
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
274
|
|
|
@@ -366,7 +366,13 @@ function buildParagraph(
|
|
|
366
366
|
indentRight: paragraphLayout.indentation?.right ?? null,
|
|
367
367
|
indentFirstLine: paragraphLayout.indentation?.firstLine ?? null,
|
|
368
368
|
indentHanging: paragraphLayout.indentation?.hanging ?? null,
|
|
369
|
-
numberingMarkerWidth:
|
|
369
|
+
numberingMarkerWidth:
|
|
370
|
+
paragraphLayout.markerLane?.width ??
|
|
371
|
+
paragraphLayout.indentation?.hanging ??
|
|
372
|
+
(paragraphLayout.indentation?.firstLine !== undefined &&
|
|
373
|
+
paragraphLayout.indentation.firstLine < 0
|
|
374
|
+
? Math.abs(paragraphLayout.indentation.firstLine)
|
|
375
|
+
: null),
|
|
370
376
|
numberingMarkerJustification: paragraphLayout.markerJustification ?? null,
|
|
371
377
|
numberingMarkerRunProperties: block.resolvedNumbering?.markerRunProperties ?? null,
|
|
372
378
|
shadingFill: block.shading?.fill ?? cascade?.shading?.fill ?? null,
|
|
@@ -453,7 +459,7 @@ function buildInlineContent(
|
|
|
453
459
|
}
|
|
454
460
|
|
|
455
461
|
case "opaque_inline":
|
|
456
|
-
return [buildOpaqueInlineOrComplexAtom(segment, showUnsupportedObjectPreviews)];
|
|
462
|
+
return [buildOpaqueInlineOrComplexAtom(segment, mediaPreviews, showUnsupportedObjectPreviews)];
|
|
457
463
|
|
|
458
464
|
case "note_ref": {
|
|
459
465
|
const text = editorSchema.text(
|
|
@@ -602,6 +608,7 @@ const UNSUPPORTED_COMPLEX_PREVIEW_LABELS = new Set<string>([
|
|
|
602
608
|
*/
|
|
603
609
|
function buildOpaqueInlineOrComplexAtom(
|
|
604
610
|
segment: Extract<import("../../api/public-types").SurfaceInlineSegment, { kind: "opaque_inline" }>,
|
|
611
|
+
mediaPreviews: Record<string, MediaPreviewDescriptor>,
|
|
605
612
|
showUnsupportedObjectPreviews: boolean,
|
|
606
613
|
): PMNode {
|
|
607
614
|
const label = segment.label;
|
|
@@ -618,11 +625,30 @@ function buildOpaqueInlineOrComplexAtom(
|
|
|
618
625
|
});
|
|
619
626
|
}
|
|
620
627
|
|
|
621
|
-
|
|
622
|
-
|
|
628
|
+
// Bitmap-backed complex objects always upgrade to the typed atom so the
|
|
629
|
+
// reviewer sees Word's own cached rendering regardless of the debug-preview
|
|
630
|
+
// flag. The flag still gates the badge-only path for shape/wordart/vml
|
|
631
|
+
// families (below) which are decoration-weight. Chart and SmartArt are
|
|
632
|
+
// *always* rendered as typed atoms regardless of flag — a silent quiet
|
|
633
|
+
// marker over a chart leaves the reviewer with no signal that data is
|
|
634
|
+
// missing, which is worse than the small cost of an always-visible chip.
|
|
635
|
+
const previewSrc = segment.previewMediaId
|
|
636
|
+
? mediaPreviews[segment.previewMediaId]?.src ?? null
|
|
637
|
+
: null;
|
|
638
|
+
|
|
639
|
+
if (label === "Embedded chart") {
|
|
640
|
+
return editorSchema.nodes.chart_atom.create({
|
|
641
|
+
previewMediaId: segment.previewMediaId ?? null,
|
|
642
|
+
previewSrc,
|
|
643
|
+
detail,
|
|
644
|
+
});
|
|
623
645
|
}
|
|
624
|
-
if (
|
|
625
|
-
return editorSchema.nodes.smartart_atom.create({
|
|
646
|
+
if (label === "SmartArt diagram") {
|
|
647
|
+
return editorSchema.nodes.smartart_atom.create({
|
|
648
|
+
previewMediaId: segment.previewMediaId ?? null,
|
|
649
|
+
previewSrc,
|
|
650
|
+
detail,
|
|
651
|
+
});
|
|
626
652
|
}
|
|
627
653
|
if (showUnsupportedObjectPreviews && (label === "Drawing shape" || label === "Text box")) {
|
|
628
654
|
const textMatch = /(?:Text content|Content): "([^"]+)"/.exec(detail);
|
|
@@ -672,12 +698,14 @@ function buildOpaqueInlineOrComplexAtom(
|
|
|
672
698
|
|
|
673
699
|
function buildOpaqueBlock(
|
|
674
700
|
block: Extract<SurfaceBlockSnapshot, { kind: "opaque_block" }>,
|
|
701
|
+
showUnsupportedObjectPreviews: boolean,
|
|
675
702
|
): PMNode {
|
|
676
703
|
return editorSchema.nodes.opaque_block.create({
|
|
677
704
|
fragmentId: block.fragmentId,
|
|
678
705
|
warningId: block.warningId,
|
|
679
706
|
label: block.label,
|
|
680
707
|
detail: block.detail,
|
|
708
|
+
presentation: showUnsupportedObjectPreviews ? "callout" : "quiet-marker",
|
|
681
709
|
});
|
|
682
710
|
}
|
|
683
711
|
|