@beyondwork/docx-react-component 1.0.48 → 1.0.49
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/README.md +16 -11
- package/package.json +30 -41
- package/src/api/public-types.ts +84 -12
- package/src/core/commands/index.ts +9 -1
- package/src/core/commands/text-commands.ts +3 -1
- package/src/core/selection/anchor-conversion.ts +112 -0
- package/src/core/selection/review-anchors.ts +108 -3
- package/src/core/state/text-transaction.ts +86 -2
- package/src/internal/harness-debug-ports.ts +168 -0
- package/src/io/chart-preview-resolver.ts +32 -1
- package/src/io/export/serialize-main-document.ts +9 -0
- package/src/io/export/serialize-paragraph-formatting.ts +8 -0
- package/src/io/export/serialize-run-formatting.ts +10 -1
- package/src/io/ooxml/chart/chart-style-table.ts +543 -0
- package/src/io/ooxml/chart/color-palette.ts +101 -0
- package/src/io/ooxml/chart/compose-series-color.ts +147 -0
- package/src/io/ooxml/chart/parse-chart-space.ts +118 -46
- package/src/io/ooxml/chart/parse-series.ts +76 -11
- package/src/io/ooxml/chart/resolve-color.ts +16 -6
- package/src/io/ooxml/chart/types.ts +30 -11
- package/src/io/ooxml/parse-complex-content.ts +6 -3
- package/src/io/ooxml/parse-main-document.ts +41 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +46 -0
- package/src/io/ooxml/parse-run-formatting.ts +49 -0
- package/src/io/ooxml/property-grab-bag.ts +211 -0
- package/src/model/canonical-document.ts +69 -3
- package/src/runtime/collab/index.ts +7 -0
- package/src/runtime/collab/runtime-collab-sync.ts +51 -0
- package/src/runtime/collab/workflow-shared.ts +247 -0
- package/src/runtime/document-locations.ts +1 -9
- package/src/runtime/document-outline.ts +1 -9
- package/src/runtime/document-runtime.ts +74 -49
- package/src/runtime/hyperlink-color-resolver.ts +119 -0
- package/src/runtime/surface-projection.ts +94 -36
- package/src/runtime/theme-color-resolver.ts +188 -0
- package/src/runtime/workflow-markup.ts +7 -18
- package/src/ui/WordReviewEditor.tsx +18 -2
- package/src/ui/editor-runtime-boundary.ts +36 -0
- package/src/ui/headless/selection-helpers.ts +10 -23
- package/src/ui/unsupported-previews-policy.ts +23 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +10 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +47 -0
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +88 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +16 -1
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
resolveEffectiveRunFormatting,
|
|
52
52
|
resolveNumberingMarkerRunFormatting,
|
|
53
53
|
} from "./paragraph-style-resolver.ts";
|
|
54
|
+
import { resolveHyperlinkRunFormatting } from "./hyperlink-color-resolver.ts";
|
|
54
55
|
import type { CanonicalParagraphFormatting, CanonicalRunFormatting } from "../model/canonical-document.ts";
|
|
55
56
|
|
|
56
57
|
interface ParagraphAccumulator {
|
|
@@ -97,6 +98,17 @@ export function createEditorSurfaceSnapshot(
|
|
|
97
98
|
};
|
|
98
99
|
|
|
99
100
|
for (let index = 0; index < root.children.length; index += 1) {
|
|
101
|
+
const isInViewport =
|
|
102
|
+
viewportBlockRange === null ||
|
|
103
|
+
(index >= viewportBlockRange.start && index < viewportBlockRange.end);
|
|
104
|
+
// L7 Phase 2.9 — viewport bail on the style-cascade work. When the
|
|
105
|
+
// block is outside the viewport, the surface block produced below is
|
|
106
|
+
// immediately discarded in favor of a `placeholder-culled` entry (we
|
|
107
|
+
// only consume `nextCursor`). Pass `cullBuild: true` so the paragraph
|
|
108
|
+
// + inline walkers skip `resolveEffectiveParagraphFormatting`,
|
|
109
|
+
// `resolveNumberingMarkerRunFormatting`, and per-segment
|
|
110
|
+
// `resolveEffectiveRunFormatting` — the expensive style-catalog walks
|
|
111
|
+
// that dominate surface-projection cost on large docs.
|
|
100
112
|
const surfaceBlock = createSurfaceBlock(
|
|
101
113
|
root.children[index],
|
|
102
114
|
document,
|
|
@@ -104,10 +116,8 @@ export function createEditorSurfaceSnapshot(
|
|
|
104
116
|
counters,
|
|
105
117
|
numberingPrefixResolver,
|
|
106
118
|
activeStory.kind !== "main",
|
|
119
|
+
!isInViewport,
|
|
107
120
|
);
|
|
108
|
-
const isInViewport =
|
|
109
|
-
viewportBlockRange === null ||
|
|
110
|
-
(index >= viewportBlockRange.start && index < viewportBlockRange.end);
|
|
111
121
|
|
|
112
122
|
if (isInViewport) {
|
|
113
123
|
blocks.push(surfaceBlock.block);
|
|
@@ -165,6 +175,7 @@ function createSurfaceBlock(
|
|
|
165
175
|
},
|
|
166
176
|
numberingPrefixResolver: NumberingPrefixResolver,
|
|
167
177
|
promoteSecondaryStoryTextBoxes: boolean,
|
|
178
|
+
cullBuild: boolean = false,
|
|
168
179
|
): { block: SurfaceBlockSnapshot; lockedFragmentIds: string[]; nextCursor: number } {
|
|
169
180
|
if (block.type === "opaque_block") {
|
|
170
181
|
const fragment = getOpaqueFragment(document.preservation as never, block.fragmentId);
|
|
@@ -205,6 +216,7 @@ function createSurfaceBlock(
|
|
|
205
216
|
counters,
|
|
206
217
|
numberingPrefixResolver,
|
|
207
218
|
promoteSecondaryStoryTextBoxes,
|
|
219
|
+
cullBuild,
|
|
208
220
|
);
|
|
209
221
|
}
|
|
210
222
|
|
|
@@ -244,6 +256,7 @@ function createSurfaceBlock(
|
|
|
244
256
|
counters,
|
|
245
257
|
numberingPrefixResolver,
|
|
246
258
|
promoteSecondaryStoryTextBoxes,
|
|
259
|
+
cullBuild,
|
|
247
260
|
);
|
|
248
261
|
}
|
|
249
262
|
|
|
@@ -336,6 +349,7 @@ function createSurfaceBlock(
|
|
|
336
349
|
cursor,
|
|
337
350
|
numberingPrefixResolver,
|
|
338
351
|
promoteSecondaryStoryTextBoxes,
|
|
352
|
+
cullBuild,
|
|
339
353
|
);
|
|
340
354
|
}
|
|
341
355
|
|
|
@@ -354,6 +368,7 @@ function createTableBlock(
|
|
|
354
368
|
},
|
|
355
369
|
numberingPrefixResolver: NumberingPrefixResolver,
|
|
356
370
|
promoteSecondaryStoryTextBoxes: boolean,
|
|
371
|
+
cullBuild: boolean = false,
|
|
357
372
|
): { block: SurfaceBlockSnapshot; lockedFragmentIds: string[]; nextCursor: number } {
|
|
358
373
|
const lockedFragmentIds: string[] = [];
|
|
359
374
|
let innerCursor = cursor;
|
|
@@ -374,6 +389,7 @@ function createTableBlock(
|
|
|
374
389
|
counters,
|
|
375
390
|
numberingPrefixResolver,
|
|
376
391
|
promoteSecondaryStoryTextBoxes,
|
|
392
|
+
cullBuild,
|
|
377
393
|
);
|
|
378
394
|
cellContent.push(result.block);
|
|
379
395
|
lockedFragmentIds.push(...result.lockedFragmentIds);
|
|
@@ -518,6 +534,7 @@ function createSdtBlock(
|
|
|
518
534
|
},
|
|
519
535
|
numberingPrefixResolver: NumberingPrefixResolver,
|
|
520
536
|
promoteSecondaryStoryTextBoxes: boolean,
|
|
537
|
+
cullBuild: boolean = false,
|
|
521
538
|
): { block: SurfaceBlockSnapshot; lockedFragmentIds: string[]; nextCursor: number } {
|
|
522
539
|
const children: SurfaceBlockSnapshot[] = [];
|
|
523
540
|
const lockedFragmentIds: string[] = [];
|
|
@@ -531,6 +548,7 @@ function createSdtBlock(
|
|
|
531
548
|
counters,
|
|
532
549
|
numberingPrefixResolver,
|
|
533
550
|
promoteSecondaryStoryTextBoxes,
|
|
551
|
+
cullBuild,
|
|
534
552
|
);
|
|
535
553
|
children.push(result.block);
|
|
536
554
|
lockedFragmentIds.push(...result.lockedFragmentIds);
|
|
@@ -566,34 +584,52 @@ function createParagraphBlock(
|
|
|
566
584
|
start: number,
|
|
567
585
|
numberingPrefixResolver: NumberingPrefixResolver,
|
|
568
586
|
promoteSecondaryStoryTextBoxes: boolean,
|
|
587
|
+
cullBuild: boolean = false,
|
|
569
588
|
): {
|
|
570
589
|
block: SurfaceBlockSnapshot;
|
|
571
590
|
nextCursor: number;
|
|
572
591
|
lockedFragmentIds: string[];
|
|
573
592
|
} {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
//
|
|
593
|
+
// L7 Phase 2.9 — viewport bail. When the paragraph is outside the
|
|
594
|
+
// viewport, the returned block is discarded (the outer caller in
|
|
595
|
+
// `createEditorSurfaceSnapshot` replaces it with a placeholder-culled
|
|
596
|
+
// entry and only consumes `nextCursor`). Skip the two style-catalog
|
|
597
|
+
// walks (`resolveEffectiveParagraphFormatting`,
|
|
598
|
+
// `resolveNumberingMarkerRunFormatting`) — their results are not read
|
|
599
|
+
// by the placeholder path. Segment-level work inside
|
|
600
|
+
// `appendInlineSegments` is suppressed symmetrically via the same
|
|
601
|
+
// `cullBuild` flag, preserving cursor arithmetic.
|
|
602
|
+
const effectiveNumbering = cullBuild
|
|
603
|
+
? undefined
|
|
604
|
+
: resolveEffectiveParagraphNumbering(document, paragraph);
|
|
605
|
+
const resolvedNumbering =
|
|
606
|
+
!cullBuild && effectiveNumbering
|
|
607
|
+
? numberingPrefixResolver.resolveDetailed(effectiveNumbering, paragraph)
|
|
608
|
+
: null;
|
|
609
|
+
|
|
610
|
+
// Task 11: compute cascaded paragraph formatting (expensive — styles-catalog walk).
|
|
580
611
|
const stylesCatalog = document.styles;
|
|
581
|
-
const directParagraphFormatting =
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const markerRunProperties = effectiveNumbering
|
|
589
|
-
? resolveNumberingMarkerRunFormatting(
|
|
590
|
-
{
|
|
591
|
-
paragraphStyleId: paragraph.styleId,
|
|
592
|
-
levelRunProperties: resolvedNumbering?.markerRunProperties,
|
|
593
|
-
},
|
|
612
|
+
const directParagraphFormatting = cullBuild
|
|
613
|
+
? undefined
|
|
614
|
+
: buildDirectParagraphFormattingFromNode(paragraph);
|
|
615
|
+
const resolvedParagraphFormatting = cullBuild
|
|
616
|
+
? undefined
|
|
617
|
+
: resolveEffectiveParagraphFormatting(
|
|
618
|
+
{ styleId: paragraph.styleId, direct: directParagraphFormatting },
|
|
594
619
|
stylesCatalog,
|
|
595
|
-
)
|
|
596
|
-
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
// Task 11: compute cascaded marker run formatting (expensive).
|
|
623
|
+
const markerRunProperties =
|
|
624
|
+
!cullBuild && effectiveNumbering
|
|
625
|
+
? resolveNumberingMarkerRunFormatting(
|
|
626
|
+
{
|
|
627
|
+
paragraphStyleId: paragraph.styleId,
|
|
628
|
+
levelRunProperties: resolvedNumbering?.markerRunProperties,
|
|
629
|
+
},
|
|
630
|
+
stylesCatalog,
|
|
631
|
+
)
|
|
632
|
+
: undefined;
|
|
597
633
|
|
|
598
634
|
const accumulator: ParagraphAccumulator = {
|
|
599
635
|
blockId: `paragraph-${paragraphIndex}`,
|
|
@@ -650,6 +686,8 @@ function createParagraphBlock(
|
|
|
650
686
|
document,
|
|
651
687
|
cursor,
|
|
652
688
|
promoteSecondaryStoryTextBoxes,
|
|
689
|
+
undefined,
|
|
690
|
+
cullBuild,
|
|
653
691
|
);
|
|
654
692
|
cursor = result.nextCursor;
|
|
655
693
|
lockedFragmentIds.push(...result.lockedFragmentIds);
|
|
@@ -812,22 +850,39 @@ function appendInlineSegments(
|
|
|
812
850
|
start: number,
|
|
813
851
|
promoteSecondaryStoryTextBoxes: boolean,
|
|
814
852
|
hyperlinkHref?: string,
|
|
853
|
+
cullBuild: boolean = false,
|
|
815
854
|
): { nextCursor: number; lockedFragmentIds: string[] } {
|
|
816
855
|
switch (node.type) {
|
|
817
856
|
case "text": {
|
|
818
857
|
const cloned = node.marks ? cloneMarks(node.marks) : { marks: [] as SurfaceTextMark[] };
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
858
|
+
// L7 Phase 2.9 — skip the styles-catalog run-cascade walk when
|
|
859
|
+
// the block will be culled. `resolveEffectiveRunFormatting`
|
|
860
|
+
// dominates per-text-segment cost on style-heavy docs; the
|
|
861
|
+
// placeholder path does not read `resolvedRunFormatting`.
|
|
862
|
+
const directRunFormatting = cullBuild
|
|
863
|
+
? undefined
|
|
864
|
+
: buildDirectRunFormattingFromMarks(
|
|
865
|
+
cloned.marks.length > 0 ? cloned.marks : undefined,
|
|
866
|
+
cloned.markAttrs,
|
|
867
|
+
);
|
|
868
|
+
// V7 — runs inside <w:hyperlink> go through the hyperlink color
|
|
869
|
+
// cascade (auto-applies the Hyperlink character style + resolves
|
|
870
|
+
// theme hlink slot with Word-default fallback). Non-hyperlink
|
|
871
|
+
// runs take the unchanged cascade path.
|
|
872
|
+
const runResolveInput = {
|
|
873
|
+
paragraphStyleId: paragraph.styleId,
|
|
874
|
+
characterStyleId: undefined,
|
|
875
|
+
direct: directRunFormatting,
|
|
876
|
+
};
|
|
877
|
+
const resolvedRunFormatting = cullBuild
|
|
878
|
+
? {}
|
|
879
|
+
: hyperlinkHref
|
|
880
|
+
? resolveHyperlinkRunFormatting(
|
|
881
|
+
runResolveInput,
|
|
882
|
+
document.styles,
|
|
883
|
+
document.subParts?.resolvedTheme,
|
|
884
|
+
)
|
|
885
|
+
: resolveEffectiveRunFormatting(runResolveInput, document.styles);
|
|
831
886
|
paragraph.segments.push({
|
|
832
887
|
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
833
888
|
kind: "text",
|
|
@@ -869,6 +924,7 @@ function appendInlineSegments(
|
|
|
869
924
|
cursor,
|
|
870
925
|
promoteSecondaryStoryTextBoxes,
|
|
871
926
|
node.href,
|
|
927
|
+
cullBuild,
|
|
872
928
|
);
|
|
873
929
|
cursor = result.nextCursor;
|
|
874
930
|
}
|
|
@@ -1003,6 +1059,8 @@ function appendInlineSegments(
|
|
|
1003
1059
|
document,
|
|
1004
1060
|
cursor,
|
|
1005
1061
|
promoteSecondaryStoryTextBoxes,
|
|
1062
|
+
undefined,
|
|
1063
|
+
cullBuild,
|
|
1006
1064
|
);
|
|
1007
1065
|
cursor = result.nextCursor;
|
|
1008
1066
|
lockedIds.push(...result.lockedFragmentIds);
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a (`<w:color>`-shaped) theme-color reference to a concrete hex
|
|
3
|
+
* string, applying the `w:themeTint` / `w:themeShade` HSL-luminance
|
|
4
|
+
* modulation per ECMA-376 §17.18.85 / §17.18.83.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* - The IO layer (`parse-run-formatting.ts`) captures
|
|
8
|
+
* `colorThemeSlot` / `colorThemeTint` / `colorThemeShade` on
|
|
9
|
+
* `CanonicalRunFormatting` as raw strings (preserved for
|
|
10
|
+
* byte-stable round-trip).
|
|
11
|
+
* - The render/cascade layer calls `resolveThemeColorHex(rPr, theme)`
|
|
12
|
+
* to collapse the reference to the actual hex colour the browser
|
|
13
|
+
* should paint. The original fields stay intact so export still
|
|
14
|
+
* round-trips the theme reference (not the computed hex).
|
|
15
|
+
*
|
|
16
|
+
* Lane 3 L2.c. Mirrors LibreOffice's `Color::ApplyTintOrShade` at
|
|
17
|
+
* `vendor/libreoffice/include/tools/color.hxx:200-240` (shape only; no
|
|
18
|
+
* code copied — our implementation is pure TypeScript against the
|
|
19
|
+
* ECMA-376 formulae).
|
|
20
|
+
*
|
|
21
|
+
* **Scope boundary:** this resolver handles the standard tint/shade
|
|
22
|
+
* pair. Theme-color-mapping (clrSchemeMapping — how "accent1" maps to a
|
|
23
|
+
* different `<w:clrScheme>` slot in older Word versions) is NOT applied
|
|
24
|
+
* here — `resolveThemeColor` from `src/io/ooxml/parse-theme.ts` is the
|
|
25
|
+
* authoritative slot→hex lookup and future work will layer scheme
|
|
26
|
+
* mapping into it.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { CanonicalRunFormatting, ResolvedTheme } from "../model/canonical-document.ts";
|
|
30
|
+
import { resolveThemeColor } from "../io/ooxml/parse-theme.ts";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Collapse `<w:color>`-style theme-slot + tint + shade references into a
|
|
34
|
+
* single resolved hex colour. Returns:
|
|
35
|
+
* - the raw `colorHex` as-is when it's a direct hex (the theme fields
|
|
36
|
+
* are ignored because direct colour wins per ECMA-376 cascade).
|
|
37
|
+
* - `"auto"` when the source declared `w:color w:val="auto"` (sentinel
|
|
38
|
+
* kept verbatim per our long-standing A.9 round-trip rule).
|
|
39
|
+
* - the theme slot's hex — tint/shade applied — when the source
|
|
40
|
+
* declared `w:themeColor="accent1"` etc. and the theme is available.
|
|
41
|
+
* - `undefined` otherwise (no colour declared, or theme slot absent).
|
|
42
|
+
*/
|
|
43
|
+
export function resolveThemeColorHex(
|
|
44
|
+
rPr: Pick<
|
|
45
|
+
CanonicalRunFormatting,
|
|
46
|
+
"colorHex" | "colorThemeSlot" | "colorThemeTint" | "colorThemeShade"
|
|
47
|
+
>,
|
|
48
|
+
theme: ResolvedTheme | undefined,
|
|
49
|
+
): string | undefined {
|
|
50
|
+
if (rPr.colorHex === "auto") return "auto";
|
|
51
|
+
if (rPr.colorHex) return rPr.colorHex;
|
|
52
|
+
if (!rPr.colorThemeSlot) return undefined;
|
|
53
|
+
|
|
54
|
+
const baseHex = resolveThemeColor(theme, rPr.colorThemeSlot);
|
|
55
|
+
if (!baseHex) return undefined;
|
|
56
|
+
|
|
57
|
+
return applyThemeTintShade(baseHex, rPr.colorThemeTint, rPr.colorThemeShade);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Apply `w:themeTint` (shift toward white) and/or `w:themeShade` (shift
|
|
62
|
+
* toward black) to a base hex colour. Pure function.
|
|
63
|
+
*
|
|
64
|
+
* Tint/shade are each a 0x00–0xFF hex byte representing a fraction of
|
|
65
|
+
* 255 (ECMA-376 §17.18.85 / §17.18.83). Only one may be present per
|
|
66
|
+
* `<w:color>`; when both are omitted, the base hex is returned.
|
|
67
|
+
*
|
|
68
|
+
* Formulae (per Microsoft's Word/OOXML guidance mirrored in LibreOffice):
|
|
69
|
+
* - tint byte T → frac = T / 255; newL = frac * L + (1 - frac) * 1.0
|
|
70
|
+
* (at T=0 the colour becomes pure white; at T=255 it stays unchanged)
|
|
71
|
+
* - shade byte S → frac = S / 255; newL = frac * L
|
|
72
|
+
* (at S=0 the colour becomes pure black; at S=255 it stays unchanged)
|
|
73
|
+
*
|
|
74
|
+
* These are LUMINANCE modulations in the HSL colour space (hue + saturation
|
|
75
|
+
* preserved). The conversion hex → HSL → hex is the standard 0–1 / 0–360
|
|
76
|
+
* form.
|
|
77
|
+
*/
|
|
78
|
+
export function applyThemeTintShade(
|
|
79
|
+
baseHex: string,
|
|
80
|
+
tint: string | undefined,
|
|
81
|
+
shade: string | undefined,
|
|
82
|
+
): string {
|
|
83
|
+
const tintByte = parseHexByte(tint);
|
|
84
|
+
const shadeByte = parseHexByte(shade);
|
|
85
|
+
if (tintByte === undefined && shadeByte === undefined) {
|
|
86
|
+
return baseHex;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const rgb = parseHexColor(baseHex);
|
|
90
|
+
if (!rgb) return baseHex;
|
|
91
|
+
|
|
92
|
+
const hsl = rgbToHsl(rgb);
|
|
93
|
+
let newL = hsl.l;
|
|
94
|
+
if (tintByte !== undefined) {
|
|
95
|
+
const frac = tintByte / 255;
|
|
96
|
+
newL = frac * hsl.l + (1 - frac);
|
|
97
|
+
} else if (shadeByte !== undefined) {
|
|
98
|
+
const frac = shadeByte / 255;
|
|
99
|
+
newL = frac * hsl.l;
|
|
100
|
+
}
|
|
101
|
+
newL = Math.max(0, Math.min(1, newL));
|
|
102
|
+
|
|
103
|
+
const out = hslToRgb({ h: hsl.h, s: hsl.s, l: newL });
|
|
104
|
+
return formatHexColor(out);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseHexByte(value: string | undefined): number | undefined {
|
|
108
|
+
if (!value) return undefined;
|
|
109
|
+
const n = Number.parseInt(value, 16);
|
|
110
|
+
if (!Number.isFinite(n)) return undefined;
|
|
111
|
+
return Math.max(0, Math.min(255, n));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseHexColor(hex: string): { r: number; g: number; b: number } | undefined {
|
|
115
|
+
const normalized = hex.replace(/^#/u, "").trim();
|
|
116
|
+
if (normalized.length !== 6) return undefined;
|
|
117
|
+
const n = Number.parseInt(normalized, 16);
|
|
118
|
+
if (!Number.isFinite(n)) return undefined;
|
|
119
|
+
return {
|
|
120
|
+
r: (n >> 16) & 0xff,
|
|
121
|
+
g: (n >> 8) & 0xff,
|
|
122
|
+
b: n & 0xff,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatHexColor(rgb: { r: number; g: number; b: number }): string {
|
|
127
|
+
const to2 = (n: number): string => Math.round(n).toString(16).padStart(2, "0").toUpperCase();
|
|
128
|
+
return `${to2(rgb.r)}${to2(rgb.g)}${to2(rgb.b)}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function rgbToHsl(rgb: { r: number; g: number; b: number }): {
|
|
132
|
+
h: number;
|
|
133
|
+
s: number;
|
|
134
|
+
l: number;
|
|
135
|
+
} {
|
|
136
|
+
const r = rgb.r / 255;
|
|
137
|
+
const g = rgb.g / 255;
|
|
138
|
+
const b = rgb.b / 255;
|
|
139
|
+
const max = Math.max(r, g, b);
|
|
140
|
+
const min = Math.min(r, g, b);
|
|
141
|
+
const l = (max + min) / 2;
|
|
142
|
+
let h = 0;
|
|
143
|
+
let s = 0;
|
|
144
|
+
if (max !== min) {
|
|
145
|
+
const d = max - min;
|
|
146
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
147
|
+
switch (max) {
|
|
148
|
+
case r:
|
|
149
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
|
|
150
|
+
break;
|
|
151
|
+
case g:
|
|
152
|
+
h = ((b - r) / d + 2) * 60;
|
|
153
|
+
break;
|
|
154
|
+
default:
|
|
155
|
+
h = ((r - g) / d + 4) * 60;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { h, s, l };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function hslToRgb(hsl: { h: number; s: number; l: number }): {
|
|
162
|
+
r: number;
|
|
163
|
+
g: number;
|
|
164
|
+
b: number;
|
|
165
|
+
} {
|
|
166
|
+
const { h, s, l } = hsl;
|
|
167
|
+
if (s === 0) {
|
|
168
|
+
const gray = l * 255;
|
|
169
|
+
return { r: gray, g: gray, b: gray };
|
|
170
|
+
}
|
|
171
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
172
|
+
const p = 2 * l - q;
|
|
173
|
+
const hueToRgb = (t: number): number => {
|
|
174
|
+
let tt = t;
|
|
175
|
+
if (tt < 0) tt += 1;
|
|
176
|
+
if (tt > 1) tt -= 1;
|
|
177
|
+
if (tt < 1 / 6) return p + (q - p) * 6 * tt;
|
|
178
|
+
if (tt < 1 / 2) return q;
|
|
179
|
+
if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
|
|
180
|
+
return p;
|
|
181
|
+
};
|
|
182
|
+
const hFrac = h / 360;
|
|
183
|
+
return {
|
|
184
|
+
r: hueToRgb(hFrac + 1 / 3) * 255,
|
|
185
|
+
g: hueToRgb(hFrac) * 255,
|
|
186
|
+
b: hueToRgb(hFrac - 1 / 3) * 255,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
WorkflowCommentMarkup,
|
|
23
23
|
} from "../api/public-types";
|
|
24
24
|
import { MAIN_STORY_TARGET } from "../core/selection/mapping.ts";
|
|
25
|
+
import { createPublicRangeAnchor } from "../core/selection/anchor-conversion.ts";
|
|
25
26
|
import {
|
|
26
27
|
projectSurfaceText,
|
|
27
28
|
searchProjectedSurfaceText,
|
|
@@ -145,7 +146,7 @@ export function collectWorkflowMarkupSnapshot(input: {
|
|
|
145
146
|
markupId: `protected-range:${range.rangeId}`,
|
|
146
147
|
kind: "protected_range",
|
|
147
148
|
rangeId: range.rangeId,
|
|
148
|
-
anchor:
|
|
149
|
+
anchor: createPublicRangeAnchor(range.start, range.end),
|
|
149
150
|
storyTarget: MAIN_STORY_TARGET,
|
|
150
151
|
label: `Protected range ${range.rangeId}`,
|
|
151
152
|
excerpt: range.enforcementReason,
|
|
@@ -283,7 +284,7 @@ function collectSurfaceMarkup(
|
|
|
283
284
|
kind: "opaque_fragment",
|
|
284
285
|
fragmentId: block.fragmentId,
|
|
285
286
|
warningId: block.warningId,
|
|
286
|
-
anchor:
|
|
287
|
+
anchor: createPublicRangeAnchor(block.from, block.to),
|
|
287
288
|
storyTarget,
|
|
288
289
|
label: block.label,
|
|
289
290
|
excerpt: block.detail,
|
|
@@ -308,7 +309,7 @@ function collectSegmentMarkup(
|
|
|
308
309
|
highlights.push({
|
|
309
310
|
markupId: `highlight:${storyTargetKey(storyTarget)}:${segment.from}:${segment.to}:${segment.markAttrs.backgroundColor}`,
|
|
310
311
|
kind: "highlight",
|
|
311
|
-
anchor:
|
|
312
|
+
anchor: createPublicRangeAnchor(segment.from, segment.to),
|
|
312
313
|
storyTarget,
|
|
313
314
|
label: `Highlight ${segment.markAttrs.backgroundColor}`,
|
|
314
315
|
excerpt: segment.text,
|
|
@@ -326,7 +327,7 @@ function collectSegmentMarkup(
|
|
|
326
327
|
kind: "opaque_fragment",
|
|
327
328
|
fragmentId: segment.fragmentId,
|
|
328
329
|
warningId: segment.warningId,
|
|
329
|
-
anchor:
|
|
330
|
+
anchor: createPublicRangeAnchor(segment.from, segment.to),
|
|
330
331
|
storyTarget,
|
|
331
332
|
label: segment.label,
|
|
332
333
|
excerpt: segment.detail,
|
|
@@ -380,7 +381,7 @@ function collectFieldMarkup(
|
|
|
380
381
|
{
|
|
381
382
|
markupId: `field:${field.index}`,
|
|
382
383
|
kind: "field",
|
|
383
|
-
anchor:
|
|
384
|
+
anchor: createPublicRangeAnchor(match.from, match.to),
|
|
384
385
|
storyTarget: story.storyTarget,
|
|
385
386
|
label: displayText,
|
|
386
387
|
excerpt: field.instruction,
|
|
@@ -417,7 +418,7 @@ function collectOpaqueFragmentMarkup(
|
|
|
417
418
|
kind: "opaque_fragment",
|
|
418
419
|
fragmentId: fragment.fragmentId,
|
|
419
420
|
warningId: fragment.warningId,
|
|
420
|
-
anchor:
|
|
421
|
+
anchor: createPublicRangeAnchor(fragment.lastKnownRange.from, fragment.lastKnownRange.to),
|
|
421
422
|
storyTarget: MAIN_STORY_TARGET,
|
|
422
423
|
label: descriptor.label,
|
|
423
424
|
excerpt: descriptor.detail,
|
|
@@ -427,18 +428,6 @@ function collectOpaqueFragmentMarkup(
|
|
|
427
428
|
});
|
|
428
429
|
}
|
|
429
430
|
|
|
430
|
-
function createRangeAnchor(from: number, to: number): EditorAnchorProjection {
|
|
431
|
-
return {
|
|
432
|
-
kind: "range",
|
|
433
|
-
from,
|
|
434
|
-
to,
|
|
435
|
-
assoc: {
|
|
436
|
-
start: -1,
|
|
437
|
-
end: 1,
|
|
438
|
-
},
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
|
|
442
431
|
function storyTargetKey(storyTarget: EditorStoryTarget): string {
|
|
443
432
|
switch (storyTarget.kind) {
|
|
444
433
|
case "main":
|
|
@@ -75,6 +75,7 @@ import type {
|
|
|
75
75
|
ZoomLevel,
|
|
76
76
|
} from "../api/public-types";
|
|
77
77
|
import { MetadataResolverMissingError } from "../api/public-types";
|
|
78
|
+
import { readHarnessDebugPortsFlag } from "../internal/harness-debug-ports.ts";
|
|
78
79
|
import type { ScopeMetadataResolver } from "../api/scope-metadata-resolver-types.ts";
|
|
79
80
|
import {
|
|
80
81
|
editorSessionStateFromPersistedSnapshot,
|
|
@@ -178,6 +179,7 @@ import {
|
|
|
178
179
|
useRuntimeSnapshotSlice,
|
|
179
180
|
useRuntimeValue,
|
|
180
181
|
} from "./runtime-snapshot-selectors.ts";
|
|
182
|
+
import { computeEffectiveShowUnsupportedPreviews } from "./unsupported-previews-policy.ts";
|
|
181
183
|
import type { MarkupDisplay } from "./headless/comment-decoration-model";
|
|
182
184
|
import { resolveScopedChromePolicy } from "./headless/scoped-chrome-policy";
|
|
183
185
|
import type {
|
|
@@ -779,6 +781,9 @@ export function __createWordReviewEditorRefBridge(
|
|
|
779
781
|
clearWorkflowOverlay: () => {
|
|
780
782
|
runtime.clearWorkflowOverlay();
|
|
781
783
|
},
|
|
784
|
+
setSharedWorkflowState: (state) => {
|
|
785
|
+
runtime.setSharedWorkflowState(state === null ? null : clonePublicValue(state));
|
|
786
|
+
},
|
|
782
787
|
getWorkflowScopeSnapshot: () => {
|
|
783
788
|
return clonePublicValue(runtime.getWorkflowScopeSnapshot());
|
|
784
789
|
},
|
|
@@ -1036,7 +1041,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1036
1041
|
chromePreset,
|
|
1037
1042
|
chromeOptions,
|
|
1038
1043
|
markupDisplay,
|
|
1039
|
-
|
|
1044
|
+
__harnessDebugPorts,
|
|
1045
|
+
unsupportedPreviewsPolicy = "never",
|
|
1040
1046
|
onError,
|
|
1041
1047
|
onEvent,
|
|
1042
1048
|
onWarning,
|
|
@@ -1825,6 +1831,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1825
1831
|
clearWorkflowOverlay: () => {
|
|
1826
1832
|
activeRuntime.clearWorkflowOverlay();
|
|
1827
1833
|
},
|
|
1834
|
+
setSharedWorkflowState: (state) => {
|
|
1835
|
+
activeRuntime.setSharedWorkflowState(state === null ? null : clonePublicValue(state));
|
|
1836
|
+
},
|
|
1828
1837
|
getWorkflowScopeSnapshot: () => {
|
|
1829
1838
|
return clonePublicValue(activeRuntime.getWorkflowScopeSnapshot());
|
|
1830
1839
|
},
|
|
@@ -2938,6 +2947,13 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2938
2947
|
},
|
|
2939
2948
|
});
|
|
2940
2949
|
|
|
2950
|
+
const harnessShowUnsupportedPreviews = readHarnessDebugPortsFlag(__harnessDebugPorts, "unsupportedObjectPreviews");
|
|
2951
|
+
const effectiveShowUnsupportedPreviews = computeEffectiveShowUnsupportedPreviews({
|
|
2952
|
+
harnessShowUnsupportedPreviews,
|
|
2953
|
+
unsupportedPreviewsPolicy,
|
|
2954
|
+
reviewMode,
|
|
2955
|
+
});
|
|
2956
|
+
|
|
2941
2957
|
const documentElement = (
|
|
2942
2958
|
<EditorSurfaceController
|
|
2943
2959
|
ref={surfaceRef}
|
|
@@ -2948,7 +2964,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2948
2964
|
documentNavigation={documentNavigation}
|
|
2949
2965
|
reviewMode={reviewMode}
|
|
2950
2966
|
markupDisplay={liveMarkupDisplay}
|
|
2951
|
-
showUnsupportedObjectPreviews={
|
|
2967
|
+
showUnsupportedObjectPreviews={effectiveShowUnsupportedPreviews}
|
|
2952
2968
|
activeRevisionId={activeRevisionId}
|
|
2953
2969
|
activeSelectionToolKind={activeSelectionTool?.kind ?? null}
|
|
2954
2970
|
showTrackedChanges={showTrackedChanges}
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
loadDocxEditorSession,
|
|
45
45
|
loadDocxEditorSessionAsync,
|
|
46
46
|
} from "../io/docx-session.ts";
|
|
47
|
+
import { tryReadLaycacheEnvelope } from "../runtime/prerender/customxml-probe.ts";
|
|
47
48
|
import {
|
|
48
49
|
createLoadScheduler,
|
|
49
50
|
type LoadScheduler,
|
|
@@ -79,6 +80,14 @@ export interface ResolvedSource {
|
|
|
79
80
|
* does the classic synchronous load.
|
|
80
81
|
*/
|
|
81
82
|
preloadedDocxSession?: ReturnType<typeof loadDocxEditorSession>;
|
|
83
|
+
/**
|
|
84
|
+
* L7 Phase 2.7 — when the editor-runtime-boundary probe finds a
|
|
85
|
+
* laycache envelope in `/customXml/item1.xml`, its `graph` is stashed
|
|
86
|
+
* here and forwarded to `createDocumentRuntime` as `seedLayoutCache`.
|
|
87
|
+
* This is the Plan A (graph-seed) win layered on top of the Plan B
|
|
88
|
+
* (loader short-circuit) win. Undefined when no envelope is available.
|
|
89
|
+
*/
|
|
90
|
+
preloadedLaycacheGraph?: import("../runtime/layout/page-graph.ts").RuntimePageGraph;
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
export interface CreateRuntimeArgs {
|
|
@@ -404,6 +413,18 @@ export function useEditorRuntimeBoundary(
|
|
|
404
413
|
// can paint the skeleton while the rest of the parse finishes.
|
|
405
414
|
// SSR / Node tests fall through to the synchronous load inside
|
|
406
415
|
// `createRuntime`.
|
|
416
|
+
//
|
|
417
|
+
// L7 Phase 2.7 — Plan B read-path wiring. Before invoking the
|
|
418
|
+
// async loader, probe the docx for a laycache envelope stashed
|
|
419
|
+
// in `/customXml/item1.xml` (shipped by Plan B Slice 2). When the
|
|
420
|
+
// probe returns a validated envelope, pass it to the loader so
|
|
421
|
+
// the five expensive parse stages short-circuit against the
|
|
422
|
+
// cached `canonicalDocument`. Measured win on 138-pp extra-large
|
|
423
|
+
// CCEP: ~947 ms cold → ~590 ms warm (Δ 376 ms), same delta the
|
|
424
|
+
// Node-side `prerender-cache.bench.ts` records. Probe is ~20–50 ms;
|
|
425
|
+
// `tryReadLaycacheEnvelope` returns null on any rejection path
|
|
426
|
+
// (missing part, stale structural hash, schema mismatch) and the
|
|
427
|
+
// loader falls through to the full parse.
|
|
407
428
|
if (
|
|
408
429
|
source.initialDocx !== undefined &&
|
|
409
430
|
source.preloadedDocxSession === undefined &&
|
|
@@ -411,12 +432,20 @@ export function useEditorRuntimeBoundary(
|
|
|
411
432
|
) {
|
|
412
433
|
const scheduler = createLoadScheduler();
|
|
413
434
|
loadSchedulerRef.current = scheduler;
|
|
435
|
+
const probeResult = await tryReadLaycacheEnvelope(source.initialDocx);
|
|
436
|
+
if (cancelled) {
|
|
437
|
+
scheduler.dispose();
|
|
438
|
+
loadSchedulerRef.current = null;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
recordPerfSample("loadSession.laycacheProbe");
|
|
414
442
|
const preloaded = await loadDocxEditorSessionAsync({
|
|
415
443
|
documentId,
|
|
416
444
|
sourceLabel: source.sourceLabel,
|
|
417
445
|
bytes: source.initialDocx,
|
|
418
446
|
editorBuild: "dev",
|
|
419
447
|
scheduler,
|
|
448
|
+
...(probeResult ? { laycacheEnvelope: probeResult.envelope } : {}),
|
|
420
449
|
});
|
|
421
450
|
if (cancelled) {
|
|
422
451
|
scheduler.dispose();
|
|
@@ -424,6 +453,9 @@ export function useEditorRuntimeBoundary(
|
|
|
424
453
|
return;
|
|
425
454
|
}
|
|
426
455
|
source.preloadedDocxSession = preloaded;
|
|
456
|
+
if (probeResult) {
|
|
457
|
+
source.preloadedLaycacheGraph = probeResult.envelope.graph;
|
|
458
|
+
}
|
|
427
459
|
}
|
|
428
460
|
|
|
429
461
|
const nextRuntime = createRuntime(
|
|
@@ -662,6 +694,9 @@ function createRuntime(
|
|
|
662
694
|
editorBuild: runtimeSessionState.editorBuild,
|
|
663
695
|
fatalError: docxSession?.fatalError,
|
|
664
696
|
protectionSnapshot: docxSession?.protectionSnapshot,
|
|
697
|
+
...(args.source.preloadedLaycacheGraph
|
|
698
|
+
? { seedLayoutCache: args.source.preloadedLaycacheGraph }
|
|
699
|
+
: {}),
|
|
665
700
|
exportDocx: async (sessionState, options) => {
|
|
666
701
|
if (docxSession) {
|
|
667
702
|
return docxSession.exportDocx(sessionState, options);
|
|
@@ -965,6 +1000,7 @@ function createLoadingRuntimeBridge(input: {
|
|
|
965
1000
|
Promise.reject(createLoadingBoundaryError(input.snapshot.documentId, "export")),
|
|
966
1001
|
setWorkflowOverlay: () => undefined,
|
|
967
1002
|
clearWorkflowOverlay: () => undefined,
|
|
1003
|
+
setSharedWorkflowState: () => undefined,
|
|
968
1004
|
getWorkflowOverlay: () => null,
|
|
969
1005
|
getWorkflowScopeSnapshot: () => null,
|
|
970
1006
|
getInteractionGuardSnapshot: () => ({ effectiveMode: "edit", blockedReasons: [] }),
|