@beyondwork/docx-react-component 1.0.48 → 1.0.50
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 +19 -11
- package/package.json +30 -41
- package/src/api/public-types.ts +103 -12
- package/src/core/commands/index.ts +30 -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-comments.ts +50 -5
- 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/io/paste/word-clipboard.ts +114 -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 +98 -50
- package/src/runtime/hyperlink-color-resolver.ts +119 -0
- package/src/runtime/layout/layout-engine-version.ts +11 -1
- package/src/runtime/layout/public-facet.ts +5 -12
- package/src/runtime/render/render-frame-types.ts +14 -0
- package/src/runtime/render/render-kernel.ts +40 -2
- package/src/runtime/structure-ops/fragment-insert.ts +134 -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 +22 -4
- package/src/ui/editor-runtime-boundary.ts +37 -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/editor-surface/pm-command-bridge.ts +60 -5
- 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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane 3 V7 — hyperlink color cascade.
|
|
3
|
+
*
|
|
4
|
+
* OOXML convention: runs inside `<w:hyperlink>` inherit the `Hyperlink`
|
|
5
|
+
* character style implicitly, EVEN WHEN source XML does not declare an
|
|
6
|
+
* explicit `<w:rStyle w:val="Hyperlink"/>` on each run. Word applies the
|
|
7
|
+
* style based on the containing hyperlink element's context alone.
|
|
8
|
+
*
|
|
9
|
+
* Our existing cascade (`resolveEffectiveRunFormatting`) only applies the
|
|
10
|
+
* character-style chain when `input.characterStyleId` is populated — so
|
|
11
|
+
* runs inside hyperlinks that lacked explicit rStyle were inheriting
|
|
12
|
+
* whatever the paragraph style said (usually black body text).
|
|
13
|
+
*
|
|
14
|
+
* This module closes that gap by resolving hyperlink color via a
|
|
15
|
+
* four-tier fallback chain:
|
|
16
|
+
*
|
|
17
|
+
* 1. Direct color on the run (`colorHex !== "auto"`) — wins outright.
|
|
18
|
+
* 2. Character-style cascade — forces Hyperlink style participation.
|
|
19
|
+
* 3. Theme hlink slot (`ResolvedTheme.colors.hlink`).
|
|
20
|
+
* 4. Hardcoded Word default `#0563C1`.
|
|
21
|
+
*
|
|
22
|
+
* The resolver also honors `colorThemeSlot` + `colorThemeTint`/`colorThemeShade`
|
|
23
|
+
* from L2.c by delegating to `resolveThemeColorHex`.
|
|
24
|
+
*
|
|
25
|
+
* Contract: the returned `CanonicalRunFormatting` is the effective cascade
|
|
26
|
+
* result with `colorHex` concretized to a non-theme hex (or `"auto"`). The
|
|
27
|
+
* original `colorThemeSlot` / `colorThemeTint` / `colorThemeShade` fields
|
|
28
|
+
* are preserved on the returned object so downstream code (or re-export
|
|
29
|
+
* via the canonical document) still sees the theme reference.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type {
|
|
33
|
+
CanonicalRunFormatting,
|
|
34
|
+
ResolvedTheme,
|
|
35
|
+
StylesCatalog,
|
|
36
|
+
} from "../model/canonical-document.ts";
|
|
37
|
+
import { resolveThemeColor } from "../io/ooxml/parse-theme.ts";
|
|
38
|
+
import { resolveThemeColorHex } from "./theme-color-resolver.ts";
|
|
39
|
+
import {
|
|
40
|
+
resolveEffectiveRunFormatting,
|
|
41
|
+
type RunResolveInput,
|
|
42
|
+
} from "./paragraph-style-resolver.ts";
|
|
43
|
+
|
|
44
|
+
export const HYPERLINK_CHARACTER_STYLE_ID = "Hyperlink";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Microsoft Word's default hyperlink color (applied when neither the
|
|
48
|
+
* Hyperlink character style nor the theme's `hlink` slot supplies one).
|
|
49
|
+
* Matches Word 2013+ fresh-document rendering.
|
|
50
|
+
*/
|
|
51
|
+
export const DEFAULT_HYPERLINK_COLOR_HEX = "0563C1";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve effective run formatting for a hyperlink-inner run. Honors the
|
|
55
|
+
* Hyperlink character style implicitly, resolves any theme-slot color
|
|
56
|
+
* references, and applies the Word-default fallback when upstream data
|
|
57
|
+
* is absent.
|
|
58
|
+
*
|
|
59
|
+
* `input.characterStyleId` is respected when the caller already passed
|
|
60
|
+
* one (e.g., source XML had an explicit `<w:rStyle>` overriding the
|
|
61
|
+
* implicit Hyperlink). Only when it is absent does the resolver inject
|
|
62
|
+
* `"Hyperlink"` itself.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveHyperlinkRunFormatting(
|
|
65
|
+
input: RunResolveInput,
|
|
66
|
+
catalog: StylesCatalog | undefined,
|
|
67
|
+
theme: ResolvedTheme | undefined,
|
|
68
|
+
): CanonicalRunFormatting {
|
|
69
|
+
// V7a — auto-apply the Hyperlink character style when the caller did
|
|
70
|
+
// not supply one (runs inside <w:hyperlink> typically lack explicit
|
|
71
|
+
// rStyle; Word applies the style by context).
|
|
72
|
+
const augmentedInput: RunResolveInput =
|
|
73
|
+
input.characterStyleId === undefined
|
|
74
|
+
? { ...input, characterStyleId: HYPERLINK_CHARACTER_STYLE_ID }
|
|
75
|
+
: input;
|
|
76
|
+
|
|
77
|
+
const cascade = resolveEffectiveRunFormatting(augmentedInput, catalog);
|
|
78
|
+
|
|
79
|
+
// V7b — concretize the color through the theme resolver + Word default.
|
|
80
|
+
const resolvedColor = resolveHyperlinkColorHex(cascade, theme);
|
|
81
|
+
if (resolvedColor && resolvedColor !== cascade.colorHex) {
|
|
82
|
+
return { ...cascade, colorHex: resolvedColor };
|
|
83
|
+
}
|
|
84
|
+
return cascade;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Four-tier hyperlink color fallback. Exported for targeted testing; use
|
|
89
|
+
* `resolveHyperlinkRunFormatting` as the primary entry point.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveHyperlinkColorHex(
|
|
92
|
+
cascade: Pick<
|
|
93
|
+
CanonicalRunFormatting,
|
|
94
|
+
"colorHex" | "colorThemeSlot" | "colorThemeTint" | "colorThemeShade"
|
|
95
|
+
>,
|
|
96
|
+
theme: ResolvedTheme | undefined,
|
|
97
|
+
): string | undefined {
|
|
98
|
+
// Tier 1 — direct non-auto hex wins.
|
|
99
|
+
if (cascade.colorHex && cascade.colorHex !== "auto") {
|
|
100
|
+
return cascade.colorHex;
|
|
101
|
+
}
|
|
102
|
+
// Tier 2 — theme-slot reference from the cascade (which now includes the
|
|
103
|
+
// Hyperlink style's rPr — typically `<w:color w:themeColor="hlink"/>`).
|
|
104
|
+
if (cascade.colorThemeSlot) {
|
|
105
|
+
const viaTheme = resolveThemeColorHex(cascade, theme);
|
|
106
|
+
if (viaTheme && viaTheme !== "auto") {
|
|
107
|
+
return viaTheme;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Tier 3 — theme hlink slot even when the cascade never wrote a slot
|
|
111
|
+
// reference. This catches docs whose Hyperlink style lacks a color
|
|
112
|
+
// declaration entirely but whose theme defines hlink.
|
|
113
|
+
const themeHlink = resolveThemeColor(theme, "hlink");
|
|
114
|
+
if (themeHlink) {
|
|
115
|
+
return themeHlink;
|
|
116
|
+
}
|
|
117
|
+
// Tier 4 — Word's hardcoded default.
|
|
118
|
+
return DEFAULT_HYPERLINK_COLOR_HEX;
|
|
119
|
+
}
|
|
@@ -42,8 +42,18 @@
|
|
|
42
42
|
* file under `src/runtime/layout/**` changed. Safe to treat
|
|
43
43
|
* versions 3, 4, and 5 as cache-compatible if a migration ever
|
|
44
44
|
* needs to collapse them.
|
|
45
|
+
* 6 — Lane 3a P9 Phase A. `RenderAnchorIndex` gains three chrome-kind
|
|
46
|
+
* resolvers (`byScopeId`, `byCommentId`, `byRevisionId`) sourced
|
|
47
|
+
* from the resolved `DecorationIndex`, and `buildAnchorIndex`
|
|
48
|
+
* runs in two phases inside the render kernel so the final index
|
|
49
|
+
* carries decoration-aware lookups. No cached-geometry change;
|
|
50
|
+
* consumers (Lane 1 R.1 SelectionLayer, Lane 6 P11 chrome rails)
|
|
51
|
+
* can now query one unified API instead of reaching into
|
|
52
|
+
* `frame.decorationIndex`. Cache envelopes from version 5 are
|
|
53
|
+
* invalidated on load because the anchor-index public shape
|
|
54
|
+
* changed even though pixel geometry did not.
|
|
45
55
|
*/
|
|
46
|
-
export const LAYOUT_ENGINE_VERSION =
|
|
56
|
+
export const LAYOUT_ENGINE_VERSION = 6 as const;
|
|
47
57
|
|
|
48
58
|
/**
|
|
49
59
|
* Serialization schema version for the LayCache payload (the cache envelope
|
|
@@ -1824,22 +1824,15 @@ function resolveAnchorRects(
|
|
|
1824
1824
|
return rect ? [rect] : [];
|
|
1825
1825
|
}
|
|
1826
1826
|
case "scope-id": {
|
|
1827
|
-
|
|
1828
|
-
return frame.decorationIndex.workflow
|
|
1829
|
-
.filter((decoration) => decoration.refId === id)
|
|
1830
|
-
.map((decoration) => decoration.frame);
|
|
1827
|
+
return frame.anchorIndex.byScopeId(String(query.value));
|
|
1831
1828
|
}
|
|
1832
1829
|
case "comment-id": {
|
|
1833
|
-
const
|
|
1834
|
-
return
|
|
1835
|
-
.filter((decoration) => decoration.refId === id)
|
|
1836
|
-
.map((decoration) => decoration.frame);
|
|
1830
|
+
const rect = frame.anchorIndex.byCommentId(String(query.value));
|
|
1831
|
+
return rect ? [rect] : [];
|
|
1837
1832
|
}
|
|
1838
1833
|
case "revision-id": {
|
|
1839
|
-
const
|
|
1840
|
-
return
|
|
1841
|
-
.filter((decoration) => decoration.refId === id)
|
|
1842
|
-
.map((decoration) => decoration.frame);
|
|
1834
|
+
const rect = frame.anchorIndex.byRevisionId(String(query.value));
|
|
1835
|
+
return rect ? [rect] : [];
|
|
1843
1836
|
}
|
|
1844
1837
|
default: {
|
|
1845
1838
|
const exhaustive: never = query.kind;
|
|
@@ -230,6 +230,20 @@ export interface RenderAnchorIndex {
|
|
|
230
230
|
tableBlockId: string,
|
|
231
231
|
rowIndex: number,
|
|
232
232
|
): RenderFrameRect | null;
|
|
233
|
+
/**
|
|
234
|
+
* Chrome-kind resolvers (P9 Phase A). Read against the frame's
|
|
235
|
+
* `decorationIndex` so chrome surfaces (scope rails, comment balloons,
|
|
236
|
+
* revision margin bars, Lane 1 R.1 SelectionLayer) query one unified
|
|
237
|
+
* API instead of reaching into `frame.decorationIndex` directly.
|
|
238
|
+
*
|
|
239
|
+
* `byScopeId` returns every rect because a single workflow scope may
|
|
240
|
+
* cover multiple pages (one `RenderBlockDecoration` per page); chrome
|
|
241
|
+
* rails read the list. `byCommentId` and `byRevisionId` return a single
|
|
242
|
+
* rect — `resolveDecorationIndex` emits one entry per thread/revision.
|
|
243
|
+
*/
|
|
244
|
+
byScopeId(scopeId: string): readonly RenderFrameRect[];
|
|
245
|
+
byCommentId(commentId: string): RenderFrameRect | null;
|
|
246
|
+
byRevisionId(revisionId: string): RenderFrameRect | null;
|
|
233
247
|
}
|
|
234
248
|
|
|
235
249
|
// ---------------------------------------------------------------------------
|
|
@@ -192,7 +192,16 @@ export function createRenderKernel(input: CreateRenderKernelInput): RenderKernel
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
const pendingDeltas = input.getPendingOpDeltas?.() ?? [];
|
|
195
|
-
|
|
195
|
+
// P9 Phase A — two-phase anchor-index build. The decoration resolver
|
|
196
|
+
// reads the anchor index to map runtime ranges to frame rects; the
|
|
197
|
+
// final anchor index then exposes chrome-kind resolvers that read
|
|
198
|
+
// back from the resolved decoration index. Rebuilding the index with
|
|
199
|
+
// the resolved decoration data avoids a post-hoc mutation seam.
|
|
200
|
+
const baseAnchorIndex = buildAnchorIndex(
|
|
201
|
+
renderPages,
|
|
202
|
+
pendingDeltas,
|
|
203
|
+
zoom.pxPerTwip,
|
|
204
|
+
);
|
|
196
205
|
const includeDecorations = options?.includeDecorations ?? true;
|
|
197
206
|
const sources = input.getDecorationSources?.();
|
|
198
207
|
const hasSources =
|
|
@@ -205,8 +214,14 @@ export function createRenderKernel(input: CreateRenderKernelInput): RenderKernel
|
|
|
205
214
|
const decorationIndex: DecorationIndex = !includeDecorations
|
|
206
215
|
? EMPTY_DECORATION_INDEX
|
|
207
216
|
: hasSources
|
|
208
|
-
? resolveDecorationIndex({ anchorIndex, ...sources })
|
|
217
|
+
? resolveDecorationIndex({ anchorIndex: baseAnchorIndex, ...sources })
|
|
209
218
|
: buildDecorationIndex(renderPages);
|
|
219
|
+
const anchorIndex = buildAnchorIndex(
|
|
220
|
+
renderPages,
|
|
221
|
+
pendingDeltas,
|
|
222
|
+
zoom.pxPerTwip,
|
|
223
|
+
decorationIndex,
|
|
224
|
+
);
|
|
210
225
|
|
|
211
226
|
// Revision: keyed off the engine's current page graph so repeated reads
|
|
212
227
|
// at the same revision return the same cached frame. We derive it
|
|
@@ -634,6 +649,7 @@ function buildAnchorIndex(
|
|
|
634
649
|
pages: readonly RenderPage[],
|
|
635
650
|
pendingDeltas: readonly PendingOpDelta[] = [],
|
|
636
651
|
pxPerTwip = 1,
|
|
652
|
+
decorationIndex: DecorationIndex = EMPTY_DECORATION_INDEX,
|
|
637
653
|
): RenderAnchorIndex {
|
|
638
654
|
const byRuntimeOffset = new Map<number, RenderFrameRect>();
|
|
639
655
|
const byFragmentId = new Map<string, RenderFrameRect>();
|
|
@@ -739,6 +755,28 @@ function buildAnchorIndex(
|
|
|
739
755
|
byTableRowEdge(tableBlockId, rowIndex) {
|
|
740
756
|
return tableRowEdges.get(`${tableBlockId}:${rowIndex}`) ?? null;
|
|
741
757
|
},
|
|
758
|
+
// P9 Phase A — chrome-kind resolvers sourced from the resolved
|
|
759
|
+
// decoration index. Empty by default (the initial frame build passes
|
|
760
|
+
// `EMPTY_DECORATION_INDEX` until decoration resolution runs); the
|
|
761
|
+
// kernel re-invokes `buildAnchorIndex` with the resolved index so the
|
|
762
|
+
// final anchor index carries chrome-aware lookups.
|
|
763
|
+
byScopeId(scopeId) {
|
|
764
|
+
return decorationIndex.workflow
|
|
765
|
+
.filter((decoration) => decoration.refId === scopeId)
|
|
766
|
+
.map((decoration) => decoration.frame);
|
|
767
|
+
},
|
|
768
|
+
byCommentId(commentId) {
|
|
769
|
+
const match = decorationIndex.comments.find(
|
|
770
|
+
(decoration) => decoration.refId === commentId,
|
|
771
|
+
);
|
|
772
|
+
return match?.frame ?? null;
|
|
773
|
+
},
|
|
774
|
+
byRevisionId(revisionId) {
|
|
775
|
+
const match = decorationIndex.revisions.find(
|
|
776
|
+
(decoration) => decoration.refId === revisionId,
|
|
777
|
+
);
|
|
778
|
+
return match?.frame ?? null;
|
|
779
|
+
},
|
|
742
780
|
};
|
|
743
781
|
}
|
|
744
782
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 1 of the I2 Tier B rich-paste sub-plan (`docs/plans/lane-1-i2-tier-b-rich-paste.md`).
|
|
3
|
+
*
|
|
4
|
+
* `applyFragmentInsert` is the canonical splicer for `CanonicalDocumentFragment` —
|
|
5
|
+
* the block-level payload that the HTML and Word-clipboard paste parsers (Slices 2+3)
|
|
6
|
+
* will produce. Slice 1 ships the shape and a baseline "split-and-splice" semantic
|
|
7
|
+
* without any parser in front of it, so the public `insertFragment` method can be
|
|
8
|
+
* driven directly from tests + future hosts.
|
|
9
|
+
*
|
|
10
|
+
* Baseline semantics:
|
|
11
|
+
* 1. Empty fragment → no-op (no revisionToken bump).
|
|
12
|
+
* 2. Range selection → the range is deleted first via `applyTextTransaction`, then
|
|
13
|
+
* the caret is the range start.
|
|
14
|
+
* 3. Caret paragraph is split via `splitParagraph`; fragment blocks are spliced
|
|
15
|
+
* between the two halves. Empty halves at document boundaries are preserved —
|
|
16
|
+
* callers can trim them if desired.
|
|
17
|
+
*
|
|
18
|
+
* What Slice 1 deliberately does NOT do:
|
|
19
|
+
* - Merge-intent (pre-Slice-1 drafts had `firstParagraphMergeIntent` on the type).
|
|
20
|
+
* Merge semantics will land in a follow-up slice once we have a paste fixture
|
|
21
|
+
* that demonstrates the need.
|
|
22
|
+
* - Fragment insertion inside table cells beyond the trivial case. Table-cell
|
|
23
|
+
* splicing is currently best-effort: the target paragraph within the cell is
|
|
24
|
+
* split, but cross-cell fragments are rejected as a no-op.
|
|
25
|
+
* - Comment/revision remapping across the fragment boundary — the splicer returns
|
|
26
|
+
* an empty mapping; follow-up slices will produce richer mappings.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { CanonicalDocumentFragment } from "../../api/public-types.ts";
|
|
30
|
+
import {
|
|
31
|
+
type CanonicalDocumentEnvelope,
|
|
32
|
+
type SelectionSnapshot,
|
|
33
|
+
createSelectionSnapshot,
|
|
34
|
+
} from "../../core/state/editor-state.ts";
|
|
35
|
+
import { applyTextTransaction } from "../../core/state/text-transaction.ts";
|
|
36
|
+
import { splitParagraph, type TextCommandContext } from "../../core/commands/text-commands.ts";
|
|
37
|
+
import { resolveParagraphScope, type StructuralMutationResult } from "../../core/commands/structural-helpers.ts";
|
|
38
|
+
import type { BlockNode, DocumentRootNode, ParagraphNode } from "../../model/canonical-document.ts";
|
|
39
|
+
import { createEmptyMapping } from "../../core/selection/mapping.ts";
|
|
40
|
+
|
|
41
|
+
export function applyFragmentInsert(
|
|
42
|
+
document: CanonicalDocumentEnvelope,
|
|
43
|
+
selection: SelectionSnapshot,
|
|
44
|
+
fragment: CanonicalDocumentFragment,
|
|
45
|
+
context: TextCommandContext,
|
|
46
|
+
): StructuralMutationResult {
|
|
47
|
+
if (fragment.blocks.length === 0) {
|
|
48
|
+
return {
|
|
49
|
+
changed: false,
|
|
50
|
+
document,
|
|
51
|
+
selection,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Collapse any range selection by first deleting the selected content. The
|
|
56
|
+
// resulting caret is at `min(anchor, head)`.
|
|
57
|
+
let workingDocument = document;
|
|
58
|
+
let workingSelection = selection;
|
|
59
|
+
if (selection.anchor !== selection.head) {
|
|
60
|
+
const collapseResult = applyTextTransaction(
|
|
61
|
+
workingDocument,
|
|
62
|
+
workingSelection,
|
|
63
|
+
{ type: "replace", insertion: [] },
|
|
64
|
+
context,
|
|
65
|
+
);
|
|
66
|
+
workingDocument = collapseResult.document;
|
|
67
|
+
workingSelection = collapseResult.selection;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Split the caret paragraph; the fragment blocks go between the two halves.
|
|
71
|
+
const splitResult = splitParagraph(workingDocument, workingSelection, context);
|
|
72
|
+
const splitRoot = splitResult.document.content;
|
|
73
|
+
if (!splitRoot || splitRoot.type !== "doc") {
|
|
74
|
+
return {
|
|
75
|
+
changed: false,
|
|
76
|
+
document,
|
|
77
|
+
selection,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Locate the split boundary by re-resolving the paragraph scope against the
|
|
82
|
+
// pre-split snapshot. The right-half index = scope.blockIndex + 1.
|
|
83
|
+
const scope = resolveParagraphScope(workingDocument, workingSelection);
|
|
84
|
+
if (!scope || scope.kind !== "top-level") {
|
|
85
|
+
// Table-cell fragment insert is out of scope for Slice 1.
|
|
86
|
+
return {
|
|
87
|
+
changed: false,
|
|
88
|
+
document,
|
|
89
|
+
selection,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const rightHalfIndex = scope.blockIndex + 1;
|
|
94
|
+
const splicedChildren: BlockNode[] = [
|
|
95
|
+
...splitRoot.children.slice(0, rightHalfIndex),
|
|
96
|
+
...fragment.blocks.map((block) => cloneBlock(block)),
|
|
97
|
+
...splitRoot.children.slice(rightHalfIndex),
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const nextRoot: DocumentRootNode = {
|
|
101
|
+
...splitRoot,
|
|
102
|
+
children: splicedChildren,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const nextDocument: CanonicalDocumentEnvelope = {
|
|
106
|
+
...splitResult.document,
|
|
107
|
+
updatedAt: context.timestamp,
|
|
108
|
+
content: nextRoot,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Caret lands at the end of the last fragment block. For Slice 1 we approximate
|
|
112
|
+
// this with a collapsed selection at position 0 of the new document — richer
|
|
113
|
+
// caret placement follows once parsers drive this.
|
|
114
|
+
const nextSelection = createSelectionSnapshot(0, 0);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
changed: true,
|
|
118
|
+
document: nextDocument,
|
|
119
|
+
selection: nextSelection,
|
|
120
|
+
mapping: createEmptyMapping(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function cloneBlock(block: BlockNode): BlockNode {
|
|
125
|
+
// Slice 1 uses a structural clone; no need to deep-clone formatting attrs since
|
|
126
|
+
// fragment blocks are presumed freshly minted by the caller.
|
|
127
|
+
if (block.type === "paragraph") {
|
|
128
|
+
return {
|
|
129
|
+
...block,
|
|
130
|
+
children: block.children.map((child) => ({ ...child })),
|
|
131
|
+
} as ParagraphNode;
|
|
132
|
+
}
|
|
133
|
+
return JSON.parse(JSON.stringify(block)) as BlockNode;
|
|
134
|
+
}
|
|
@@ -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);
|