@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
package/README.md
CHANGED
|
@@ -224,17 +224,22 @@ The CCEP corpus is kept on `main` as a maintainer-safe smoke-doc source set for
|
|
|
224
224
|
|
|
225
225
|
Engineering work is organized into 9 lanes (5 active now + 2 later polish + 2 final) — full plan index, status, and worktree map at [`docs/plans/README.md`](docs/plans/README.md). Each lane has a single self-contained doc at `docs/plans/lane-N-<name>.md`:
|
|
226
226
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
|
230
|
-
|
|
231
|
-
|
|
|
232
|
-
|
|
|
233
|
-
|
|
|
234
|
-
|
|
|
235
|
-
|
|
|
236
|
-
|
|
|
237
|
-
|
|
|
227
|
+
**Progress snapshot — 2026-04-19 (post PR #189 + #190 + #191 + #192 merged to `main`; Lane 3 split into 3a + 3b on 2026-04-19 deep evening):**
|
|
228
|
+
|
|
229
|
+
| # | Lane | % to goal | Status summary |
|
|
230
|
+
|---|---|---|---|
|
|
231
|
+
| 1 | [**Editing Foundation**](docs/plans/lane-1-editing-foundation.md) | **65%** | I1–I6 + I3 widening + I4 + S1 + X3 + I8 shipped; I2B / I7 / R.1–R.5 backlog |
|
|
232
|
+
| 2 | [**Render Performance**](docs/plans/lane-2-render-performance.md) | **85%** | All Phases 1.x + 2.2 + 2.4b + 2.5 (A+B) + 2.9 shipped; Tasks 2.1 / 2.3 left |
|
|
233
|
+
| 3a | [**Layout & Render Engine**](docs/plans/lane-3a-layout-render-engine.md) | **45%** | P1–P6 + P8 + P14.a/b/e shipped; P9 (now unblocked) + P10 + P11 + P12 + P14.c/d backlog |
|
|
234
|
+
| 3b | [**OOXML Fidelity & Round-Trip**](docs/plans/lane-3b-ooxml-fidelity.md) | **55%** | V1 + O3 + O4 + O2 (all 4 slices) + V7 closed; 🚨 O8 + L2.c + V6 + V2a/b/c + R3–R5 backlog |
|
|
235
|
+
| 4 | [**Collab + CLM/Vallor**](docs/plans/lane-4-collab-clm-vallor.md) | **80%** | P1–P14 + all P11 sub-bullets + P12 + perf-parity + P13 A/B/C shipped; P15 / P16 / P17 left |
|
|
236
|
+
| 5 | [**Charts (independent)**](docs/plans/lane-5-charts.md) | **30%** | Stages 0–2 shipped (parsers + theme); Stages 3–7 (SVG renderers + pixel-diff) left |
|
|
237
|
+
| 6 | [**Visual Chrome / Layout Polish**](docs/plans/lane-6-visual-chrome-layout-polish.md) | **0%** | LATER — activates after Lane 3b V2c + Lane 2 Phase 2.2 ship; discrete paper cards, native chrome, float-wrap, validation bar |
|
|
238
|
+
| 7 | [**Bugs / Gaps / Cross-cutting**](docs/plans/lane-7-bugs-gaps-cross-cutting.md) | **0%** | LATER — drain V#/O#/X# register, trigger-gated work, infrastructure hardening |
|
|
239
|
+
| 8 | [**API Ergonomics / Errors / BC**](docs/plans/lane-8-api-ergonomics.md) | **40%** | LATER — Tracks A+C shipped (error catalog + ergonomics fixes); Tracks B+D+E + public-api.md end-to-end refactor remain |
|
|
240
|
+
| 9 | [**Shipping (v2.0.0)**](docs/plans/lane-9-shipping.md) | **0%** | FINAL — API freeze, semver discipline, changelog, telemetry, customer migration guides, doc completeness audit |
|
|
241
|
+
|
|
242
|
+
**Aggregate v2.0.0 readiness:** ~70% — long poles are Lane 3 O8 + P9/P10, Lane 4 P15/P16/P17, Lane 1 R.1–R.5, Lane 5 Stage 4 SVG renderers (Lane 5 ships on independent v2.x cadence). Detailed task-completeness factor + per-lane backlog inventory in [`docs/plans/coordination-prompt.md`](docs/plans/coordination-prompt.md).
|
|
238
243
|
|
|
239
244
|
### Technical Wiki
|
|
240
245
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.49",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
|
-
"packageManager": "pnpm@10.30.3",
|
|
7
6
|
"type": "module",
|
|
8
7
|
"sideEffects": [
|
|
9
8
|
"**/*.css"
|
|
@@ -93,35 +92,6 @@
|
|
|
93
92
|
"./ui-tailwind/theme/editor-theme.css": "./src/ui-tailwind/theme/editor-theme.css"
|
|
94
93
|
},
|
|
95
94
|
"types": "./src/index.ts",
|
|
96
|
-
"scripts": {
|
|
97
|
-
"build": "tsup",
|
|
98
|
-
"test": "bash scripts/run-workspace-tests.sh",
|
|
99
|
-
"test:repo": "node scripts/ci-check-layout-engine-version.mjs && node scripts/run-repo-tests.mjs core",
|
|
100
|
-
"test:repo:all": "node scripts/run-repo-tests.mjs all",
|
|
101
|
-
"test:repo:optional": "node scripts/run-repo-tests.mjs optional",
|
|
102
|
-
"test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
|
|
103
|
-
"test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
|
|
104
|
-
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
105
|
-
"test:visual": "VISUAL_SMOKE_PROFILE=bare pnpm exec playwright test --project=chromium",
|
|
106
|
-
"test:visual:chrome": "VISUAL_SMOKE_PROFILE=chrome-cycle pnpm exec playwright test --project=chromium",
|
|
107
|
-
"visual:list-runs": "node scripts/visual-smoke-list-runs.mjs",
|
|
108
|
-
"mcp:visual-smoke": "node scripts/visual-smoke-mcp.mjs",
|
|
109
|
-
"lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
110
|
-
"lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
|
|
111
|
-
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
112
|
-
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
113
|
-
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
114
|
-
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
115
|
-
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
116
|
-
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
117
|
-
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
118
|
-
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
119
|
-
"wave:status": "bash scripts/wave-status.sh",
|
|
120
|
-
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
121
|
-
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
122
|
-
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
123
|
-
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
124
|
-
},
|
|
125
95
|
"keywords": [
|
|
126
96
|
"docx",
|
|
127
97
|
"word",
|
|
@@ -205,14 +175,33 @@
|
|
|
205
175
|
"y-protocols": "^1.0.7",
|
|
206
176
|
"yjs": "^13.6.30"
|
|
207
177
|
},
|
|
208
|
-
"
|
|
209
|
-
"
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
"
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
178
|
+
"scripts": {
|
|
179
|
+
"build": "tsup",
|
|
180
|
+
"test": "bash scripts/run-workspace-tests.sh",
|
|
181
|
+
"test:repo": "node scripts/ci-check-layout-engine-version.mjs && node scripts/run-repo-tests.mjs core",
|
|
182
|
+
"test:repo:all": "node scripts/run-repo-tests.mjs all",
|
|
183
|
+
"test:repo:optional": "node scripts/run-repo-tests.mjs optional",
|
|
184
|
+
"test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
|
|
185
|
+
"test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
|
|
186
|
+
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
187
|
+
"test:visual": "VISUAL_SMOKE_PROFILE=bare pnpm exec playwright test --project=chromium",
|
|
188
|
+
"test:visual:chrome": "VISUAL_SMOKE_PROFILE=chrome-cycle pnpm exec playwright test --project=chromium",
|
|
189
|
+
"visual:list-runs": "node scripts/visual-smoke-list-runs.mjs",
|
|
190
|
+
"mcp:visual-smoke": "node scripts/visual-smoke-mcp.mjs",
|
|
191
|
+
"lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
192
|
+
"lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
|
|
193
|
+
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
194
|
+
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
195
|
+
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
196
|
+
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
197
|
+
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
198
|
+
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
199
|
+
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
200
|
+
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
201
|
+
"wave:status": "bash scripts/wave-status.sh",
|
|
202
|
+
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
203
|
+
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
204
|
+
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
205
|
+
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
217
206
|
}
|
|
218
|
-
}
|
|
207
|
+
}
|
package/src/api/public-types.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { PersistedEditorSnapshot as RuntimePersistedEditorSnapshot } from "../core/state/editor-state.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type { HarnessDebugPorts } from "../internal/harness-debug-ports.ts";
|
|
3
|
+
import type { CanonicalParagraphFormatting, CanonicalRunFormatting, TextMark } from "../model/canonical-document.ts";
|
|
3
4
|
import type { WordReviewEditorLayoutFacet } from "../runtime/layout/public-facet.ts";
|
|
4
5
|
import type { RenderFrameRect } from "../runtime/render/index.ts";
|
|
5
6
|
import type { ScopeRailPosture } from "../runtime/workflow-rail-segments.ts";
|
|
7
|
+
import type { SharedWorkflowState } from "../runtime/collab/workflow-shared.ts";
|
|
6
8
|
import type {
|
|
7
9
|
MetadataPersistenceMode,
|
|
8
10
|
ScopeMetadataPersistence,
|
|
@@ -46,6 +48,7 @@ export type {
|
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
export type { CanonicalParagraphFormatting, CanonicalRunFormatting };
|
|
51
|
+
export type { SharedWorkflowState };
|
|
49
52
|
|
|
50
53
|
export type {
|
|
51
54
|
WordReviewEditorLayoutFacet,
|
|
@@ -750,6 +753,25 @@ export interface InsertImageOptions {
|
|
|
750
753
|
altText?: string;
|
|
751
754
|
}
|
|
752
755
|
|
|
756
|
+
/** Re-export canonical `TextMark` so hosts can construct explicit-mode directives for I7 `replaceText`. */
|
|
757
|
+
export type { TextMark };
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* I7 — `replaceText` / `text.insert` formatting directive.
|
|
761
|
+
*
|
|
762
|
+
* - `paragraph-default` (default): inserted text carries no run marks; the paragraph-level
|
|
763
|
+
* cascade applies. Pre-I7 behavior.
|
|
764
|
+
* - `match-replaced-range`: walk text units within the replaced range; if all share the
|
|
765
|
+
* same marks, apply them to the inserted text. Mixed ranges fall back to
|
|
766
|
+
* `paragraph-default`. For collapsed (empty) ranges the marks of the run immediately
|
|
767
|
+
* left of the caret are used (Word-matching behavior).
|
|
768
|
+
* - `explicit`: apply the caller-supplied `marks` verbatim.
|
|
769
|
+
*/
|
|
770
|
+
export type TextFormattingDirective =
|
|
771
|
+
| { mode: "paragraph-default" }
|
|
772
|
+
| { mode: "match-replaced-range" }
|
|
773
|
+
| { mode: "explicit"; marks: TextMark[] };
|
|
774
|
+
|
|
753
775
|
export type SurfaceTextMark =
|
|
754
776
|
| "bold"
|
|
755
777
|
| "italic"
|
|
@@ -1685,6 +1707,16 @@ export interface AddCommentParams {
|
|
|
1685
1707
|
anchor?: EditorAnchorProjection;
|
|
1686
1708
|
body?: string;
|
|
1687
1709
|
authorId?: string;
|
|
1710
|
+
/**
|
|
1711
|
+
* I8 — When `true`, anchors that would be rejected with reason
|
|
1712
|
+
* `comment_anchor_table_adjacent` are auto-snapped to the enclosing
|
|
1713
|
+
* paragraph boundaries before the comment is created. Defaults to
|
|
1714
|
+
* `false` for back-compat. Removed once Lane 3 §O8 lands the deep
|
|
1715
|
+
* serializer fix and mid-run-near-table anchors no longer need
|
|
1716
|
+
* mitigation — the option stays useful for hosts that prefer
|
|
1717
|
+
* boundary-aligned comments regardless.
|
|
1718
|
+
*/
|
|
1719
|
+
snapToSafeBoundary?: boolean;
|
|
1688
1720
|
}
|
|
1689
1721
|
|
|
1690
1722
|
export interface AddCommentResult {
|
|
@@ -2007,6 +2039,7 @@ export interface WorkflowBlockedCommandReason {
|
|
|
2007
2039
|
code:
|
|
2008
2040
|
| "outside_workflow_scope"
|
|
2009
2041
|
| "workflow_comment_only"
|
|
2042
|
+
| "workflow_round_locked"
|
|
2010
2043
|
| "workflow_view_only"
|
|
2011
2044
|
| "workflow_preserve_only"
|
|
2012
2045
|
| "workflow_blocked_import"
|
|
@@ -2881,7 +2914,11 @@ export interface WordReviewEditorRef {
|
|
|
2881
2914
|
isDirty(): boolean;
|
|
2882
2915
|
getFormattingState(): FormattingStateSnapshot;
|
|
2883
2916
|
getStyleCatalog(): StyleCatalogSnapshot;
|
|
2884
|
-
replaceText(
|
|
2917
|
+
replaceText(
|
|
2918
|
+
text: string,
|
|
2919
|
+
target?: EditorAnchorProjection,
|
|
2920
|
+
formatting?: TextFormattingDirective,
|
|
2921
|
+
): void;
|
|
2885
2922
|
toggleBulletedList(): void;
|
|
2886
2923
|
toggleNumberedList(): void;
|
|
2887
2924
|
toggleBold(): void;
|
|
@@ -2969,6 +3006,7 @@ export interface WordReviewEditorRef {
|
|
|
2969
3006
|
setImageFrame(mediaId: string, offsets: { horizontalOffsetEmu?: number; verticalOffsetEmu?: number }): void;
|
|
2970
3007
|
setWorkflowOverlay(overlay: WorkflowOverlay): void;
|
|
2971
3008
|
clearWorkflowOverlay(): void;
|
|
3009
|
+
setSharedWorkflowState(state: SharedWorkflowState | null): void;
|
|
2972
3010
|
getWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null;
|
|
2973
3011
|
getInteractionGuardSnapshot(): InteractionGuardSnapshot;
|
|
2974
3012
|
getWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot;
|
|
@@ -3208,20 +3246,54 @@ export interface WordReviewEditorProps {
|
|
|
3208
3246
|
chromeOptions?: Partial<WordReviewEditorChromeOptions>;
|
|
3209
3247
|
markupDisplay?: "clean" | "simple" | "all";
|
|
3210
3248
|
/**
|
|
3211
|
-
*
|
|
3212
|
-
* (charts, SmartArt, shapes, WordArt, VML rendered as typed atoms;
|
|
3213
|
-
* the "N preserve-only features detected" banner; lock-callouts).
|
|
3249
|
+
* @internal HARNESS-ONLY debug-ports token.
|
|
3214
3250
|
*
|
|
3215
|
-
*
|
|
3216
|
-
*
|
|
3217
|
-
*
|
|
3218
|
-
*
|
|
3219
|
-
*
|
|
3251
|
+
* **Do not set this from downstream consumer code.** This prop
|
|
3252
|
+
* replaces the former `showUnsupportedObjectPreviews?: boolean`
|
|
3253
|
+
* toggle, which regressed to `true` three times through merges
|
|
3254
|
+
* (PRs #124, #131, #160) and leaked preserve-only preview chrome
|
|
3255
|
+
* (charts, SmartArt, shapes, WordArt, VML, "N preserve-only
|
|
3256
|
+
* features detected" banner, lock-callouts) into consumer apps.
|
|
3257
|
+
*
|
|
3258
|
+
* A valid value is only obtainable from the internal module
|
|
3259
|
+
* `src/internal/harness-debug-ports.ts` via
|
|
3260
|
+
* `__createHarnessDebugPorts()`. That module is deliberately **not
|
|
3261
|
+
* listed in `package.json#exports`**, so downstream consumers
|
|
3262
|
+
* cannot import it through any public subpath. The token type is
|
|
3263
|
+
* branded with a module-local `unique symbol`, so downstream
|
|
3264
|
+
* callers cannot construct a value structurally either — any
|
|
3265
|
+
* attempt to set this prop from outside the in-repo harness will
|
|
3266
|
+
* produce a TypeScript error.
|
|
3267
|
+
*
|
|
3268
|
+
* At runtime, even a successfully-constructed token is
|
|
3269
|
+
* gated by `globalThis.__DOCX_REACT_COMPONENT_HARNESS__ === true`,
|
|
3270
|
+
* which only the harness sets via `__markHarnessEnvironment()`.
|
|
3271
|
+
* The permissions field (`unsupportedObjectPreviews`) is forced
|
|
3272
|
+
* to `false` outside the harness environment regardless of input.
|
|
3273
|
+
*
|
|
3274
|
+
* Invariant locked at
|
|
3220
3275
|
* `test/ui/unsupported-previews-invariant.test.ts`.
|
|
3276
|
+
*/
|
|
3277
|
+
__harnessDebugPorts?: HarnessDebugPorts;
|
|
3278
|
+
/**
|
|
3279
|
+
* Declarative policy for showing preserve-only previews
|
|
3280
|
+
* (charts, SmartArt, shapes, WordArt, VML) and the
|
|
3281
|
+
* "N preserve-only features detected" banner. Orthogonal to the
|
|
3282
|
+
* harness-only `__harnessDebugPorts` token: hosts can opt into
|
|
3283
|
+
* previews without a harness environment.
|
|
3284
|
+
*
|
|
3285
|
+
* - `"never"` — previews are hidden (default).
|
|
3286
|
+
* - `"review-only"` — previews show when `reviewMode === "review"`
|
|
3287
|
+
* and hide when `reviewMode === "editing"`.
|
|
3288
|
+
* - `"always"` — previews show in every review mode.
|
|
3289
|
+
*
|
|
3290
|
+
* Effective visibility rule:
|
|
3291
|
+
* `policy === "always"` OR
|
|
3292
|
+
* (`policy === "review-only"` AND `reviewMode === "review"`)
|
|
3221
3293
|
*
|
|
3222
|
-
* @default
|
|
3294
|
+
* @default "never"
|
|
3223
3295
|
*/
|
|
3224
|
-
|
|
3296
|
+
unsupportedPreviewsPolicy?: "never" | "review-only" | "always";
|
|
3225
3297
|
showReviewPanel?: boolean;
|
|
3226
3298
|
chromeVisibility?: Partial<WordReviewEditorChromeVisibility>;
|
|
3227
3299
|
hostAdapter?: EditorHostAdapter;
|
|
@@ -50,6 +50,7 @@ import type {
|
|
|
50
50
|
SectionBreakType,
|
|
51
51
|
SectionLayoutPatch,
|
|
52
52
|
SectionPageNumberingPatch,
|
|
53
|
+
TextFormattingDirective,
|
|
53
54
|
WorkflowMetadataDefinition,
|
|
54
55
|
WorkflowMetadataEntry,
|
|
55
56
|
WorkflowOverlay,
|
|
@@ -136,6 +137,13 @@ export type EditorCommand =
|
|
|
136
137
|
| {
|
|
137
138
|
type: "text.insert";
|
|
138
139
|
text: string;
|
|
140
|
+
/**
|
|
141
|
+
* I7 — optional directive controlling which character-level marks the inserted
|
|
142
|
+
* text carries. Defaults to `{ mode: "paragraph-default" }` (today's behavior:
|
|
143
|
+
* no inherited run marks). See `src/api/public-types.ts` `TextFormattingDirective`
|
|
144
|
+
* for semantics.
|
|
145
|
+
*/
|
|
146
|
+
formatting?: TextFormattingDirective;
|
|
139
147
|
origin?: CommandOrigin;
|
|
140
148
|
}
|
|
141
149
|
| {
|
|
@@ -556,7 +564,7 @@ export function executeEditorCommand(
|
|
|
556
564
|
: undefined;
|
|
557
565
|
if (suggestingResult) return suggestingResult;
|
|
558
566
|
return applyTextCommand(state, context.timestamp, (document, selection) =>
|
|
559
|
-
insertText(document, selection, command.text, context),
|
|
567
|
+
insertText(document, selection, command.text, context, command.formatting),
|
|
560
568
|
);
|
|
561
569
|
}
|
|
562
570
|
case "text.delete-backward": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InsertTableOptions } from "../../api/public-types";
|
|
1
|
+
import type { InsertTableOptions, TextFormattingDirective } from "../../api/public-types";
|
|
2
2
|
import type {
|
|
3
3
|
DocumentRootNode,
|
|
4
4
|
ParagraphNode,
|
|
@@ -126,6 +126,7 @@ export function insertText(
|
|
|
126
126
|
selection: SelectionSnapshot,
|
|
127
127
|
text: string,
|
|
128
128
|
context: TextCommandContext,
|
|
129
|
+
formatting?: TextFormattingDirective,
|
|
129
130
|
): TextTransactionResult {
|
|
130
131
|
return applyTextTransaction(
|
|
131
132
|
document,
|
|
@@ -138,6 +139,7 @@ export function insertText(
|
|
|
138
139
|
text,
|
|
139
140
|
},
|
|
140
141
|
],
|
|
142
|
+
formatting,
|
|
141
143
|
},
|
|
142
144
|
context,
|
|
143
145
|
);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical boundary between the internal `EditorAnchorProjection` shape
|
|
3
|
+
* (`src/core/selection/mapping.ts` — `{ kind: "range", range: DocRange,
|
|
4
|
+
* assoc }`) and the public `EditorAnchorProjection` shape
|
|
5
|
+
* (`src/api/public-types.ts` — `{ kind: "range", from, to, assoc }`).
|
|
6
|
+
*
|
|
7
|
+
* Two shapes stay split intentionally: the internal shape uses `DocRange`
|
|
8
|
+
* so mapping helpers compose ranges independently of assoc semantics;
|
|
9
|
+
* the public shape is flat so host consumers can destructure without
|
|
10
|
+
* reaching through a nested `range` member.
|
|
11
|
+
*
|
|
12
|
+
* Every conversion MUST go through this module. Every creation of a
|
|
13
|
+
* public-shape anchor MUST go through this module. A regression test
|
|
14
|
+
* in `test/api/anchor-boundary-invariants.test.ts` enforces the "no
|
|
15
|
+
* ad-hoc helpers outside this file" rule.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { EditorAnchorProjection as PublicEditorAnchorProjection } from "../../api/public-types";
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_BOUNDARY_ASSOC,
|
|
21
|
+
createDetachedAnchor,
|
|
22
|
+
createNodeAnchor,
|
|
23
|
+
createRangeAnchor,
|
|
24
|
+
normalizeRange,
|
|
25
|
+
type DocRange,
|
|
26
|
+
type EditorAnchorProjection as InternalEditorAnchorProjection,
|
|
27
|
+
} from "./mapping.ts";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default boundary-associativity used by public-shape range anchors
|
|
31
|
+
* when the caller does not provide one. Matches the inline default the
|
|
32
|
+
* three ad-hoc `createPublicRangeAnchor` helpers all used before this
|
|
33
|
+
* module existed (`{ start: -1, end: 1 }` — the "outward-biased"
|
|
34
|
+
* selection).
|
|
35
|
+
*/
|
|
36
|
+
export const DEFAULT_PUBLIC_ASSOC: { start: -1 | 1; end: -1 | 1 } = {
|
|
37
|
+
start: -1,
|
|
38
|
+
end: 1,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type PublicRangeAssoc = { start: -1 | 1; end: -1 | 1 };
|
|
42
|
+
|
|
43
|
+
export function createPublicRangeAnchor(
|
|
44
|
+
from: number,
|
|
45
|
+
to: number,
|
|
46
|
+
assoc: PublicRangeAssoc = DEFAULT_PUBLIC_ASSOC,
|
|
47
|
+
): PublicEditorAnchorProjection {
|
|
48
|
+
const range = normalizeRange({ from, to });
|
|
49
|
+
return {
|
|
50
|
+
kind: "range",
|
|
51
|
+
from: range.from,
|
|
52
|
+
to: range.to,
|
|
53
|
+
assoc,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createPublicNodeAnchor(
|
|
58
|
+
at: number,
|
|
59
|
+
assoc: -1 | 1 = 1,
|
|
60
|
+
): PublicEditorAnchorProjection {
|
|
61
|
+
return { kind: "node", at, assoc };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createPublicDetachedAnchor(
|
|
65
|
+
lastKnownRange: DocRange,
|
|
66
|
+
reason: "deleted" | "invalidatedByStructureChange" | "importAmbiguity",
|
|
67
|
+
): PublicEditorAnchorProjection {
|
|
68
|
+
return {
|
|
69
|
+
kind: "detached",
|
|
70
|
+
lastKnownRange: normalizeRange(lastKnownRange),
|
|
71
|
+
reason,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function toPublicAnchorProjection(
|
|
76
|
+
anchor: InternalEditorAnchorProjection,
|
|
77
|
+
): PublicEditorAnchorProjection {
|
|
78
|
+
switch (anchor.kind) {
|
|
79
|
+
case "range":
|
|
80
|
+
return {
|
|
81
|
+
kind: "range",
|
|
82
|
+
from: anchor.range.from,
|
|
83
|
+
to: anchor.range.to,
|
|
84
|
+
assoc: anchor.assoc,
|
|
85
|
+
};
|
|
86
|
+
case "node":
|
|
87
|
+
return { kind: "node", at: anchor.at, assoc: anchor.assoc };
|
|
88
|
+
case "detached":
|
|
89
|
+
return {
|
|
90
|
+
kind: "detached",
|
|
91
|
+
lastKnownRange: anchor.lastKnownRange,
|
|
92
|
+
reason: anchor.reason,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function toInternalAnchorProjection(
|
|
98
|
+
anchor: PublicEditorAnchorProjection,
|
|
99
|
+
): InternalEditorAnchorProjection {
|
|
100
|
+
switch (anchor.kind) {
|
|
101
|
+
case "range":
|
|
102
|
+
return createRangeAnchor(
|
|
103
|
+
anchor.from,
|
|
104
|
+
anchor.to,
|
|
105
|
+
anchor.assoc ?? DEFAULT_BOUNDARY_ASSOC,
|
|
106
|
+
);
|
|
107
|
+
case "node":
|
|
108
|
+
return createNodeAnchor(anchor.at, anchor.assoc);
|
|
109
|
+
case "detached":
|
|
110
|
+
return createDetachedAnchor(anchor.lastKnownRange, anchor.reason);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -103,20 +103,125 @@ export function rangeStaysWithinSingleParagraph(
|
|
|
103
103
|
return true;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* I8 — "mid-run-near-table" guard. Comment anchors whose endpoints
|
|
108
|
+
* land strictly inside a paragraph that sits adjacent to a table
|
|
109
|
+
* block are rejected: the serializer's per-paragraph offset walker
|
|
110
|
+
* (Lane 3 §O8) produces invalid OOXML for these anchors. Removed
|
|
111
|
+
* once O8 ships.
|
|
112
|
+
*/
|
|
113
|
+
export const TABLE_ADJACENT_WINDOW = 1;
|
|
114
|
+
|
|
115
|
+
export type CommentAnchorRejectionReason =
|
|
116
|
+
| "invalid_comment_anchor"
|
|
117
|
+
| "comment_anchor_table_adjacent";
|
|
118
|
+
|
|
106
119
|
export function canCreateDocxCommentAnchor(
|
|
107
120
|
content: unknown,
|
|
108
121
|
anchor: ReviewAnchor,
|
|
109
122
|
): boolean {
|
|
123
|
+
return commentAnchorRejectionReason(content, anchor) === null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function commentAnchorRejectionReason(
|
|
127
|
+
content: unknown,
|
|
128
|
+
anchor: ReviewAnchor,
|
|
129
|
+
): CommentAnchorRejectionReason | null {
|
|
110
130
|
if (anchor.kind !== "range") {
|
|
111
|
-
return
|
|
131
|
+
return "invalid_comment_anchor";
|
|
112
132
|
}
|
|
113
133
|
|
|
114
134
|
const normalized = normalizeRange(anchor.range);
|
|
115
135
|
if (normalized.from === normalized.to) {
|
|
116
|
-
return
|
|
136
|
+
return "invalid_comment_anchor";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!rangeStaysWithinCommentableStory(content, normalized)) {
|
|
140
|
+
return "invalid_comment_anchor";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (rangeLandsMidRunNearTableBoundary(content, normalized)) {
|
|
144
|
+
return "comment_anchor_table_adjacent";
|
|
117
145
|
}
|
|
118
146
|
|
|
119
|
-
return
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* I8.3 — Snap a rejected mid-run-near-table anchor to paragraph
|
|
152
|
+
* boundaries so downstream serialization stays safe. Returns `null`
|
|
153
|
+
* if the anchor cannot be rescued (e.g. crosses an opaque block).
|
|
154
|
+
*/
|
|
155
|
+
export function snapCommentAnchorAwayFromTable(
|
|
156
|
+
content: unknown,
|
|
157
|
+
anchor: ReviewAnchor,
|
|
158
|
+
): ReviewAnchor | null {
|
|
159
|
+
if (anchor.kind !== "range") return null;
|
|
160
|
+
|
|
161
|
+
const normalized = normalizeRange(anchor.range);
|
|
162
|
+
if (normalized.from === normalized.to) return null;
|
|
163
|
+
|
|
164
|
+
const reason = commentAnchorRejectionReason(content, anchor);
|
|
165
|
+
if (reason === null) return anchor;
|
|
166
|
+
if (reason !== "comment_anchor_table_adjacent") return null;
|
|
167
|
+
|
|
168
|
+
const surfaceBlocks = readSurfaceBlocks(content);
|
|
169
|
+
if (!surfaceBlocks) return null;
|
|
170
|
+
|
|
171
|
+
const fromOwner = findContainingParagraphForEndpoint(surfaceBlocks, normalized.from, "start");
|
|
172
|
+
const toOwner = findContainingParagraphForEndpoint(surfaceBlocks, normalized.to, "end");
|
|
173
|
+
if (!fromOwner || !toOwner) return null;
|
|
174
|
+
|
|
175
|
+
const snappedFrom = normalized.from > fromOwner.from && normalized.from < fromOwner.to
|
|
176
|
+
? fromOwner.from
|
|
177
|
+
: normalized.from;
|
|
178
|
+
const snappedTo = normalized.to > toOwner.from && normalized.to < toOwner.to
|
|
179
|
+
? toOwner.to
|
|
180
|
+
: normalized.to;
|
|
181
|
+
|
|
182
|
+
if (snappedFrom === snappedTo) return null;
|
|
183
|
+
|
|
184
|
+
const snapped = createRangeAnchor(snappedFrom, snappedTo);
|
|
185
|
+
return canCreateDocxCommentAnchor(content, snapped) ? snapped : null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function rangeLandsMidRunNearTableBoundary(
|
|
189
|
+
content: unknown,
|
|
190
|
+
range: DocRange,
|
|
191
|
+
): boolean {
|
|
192
|
+
const surfaceBlocks = readSurfaceBlocks(content);
|
|
193
|
+
if (!surfaceBlocks) return false;
|
|
194
|
+
|
|
195
|
+
const tableBlocks = surfaceBlocks.filter((block) => block.kind === "table");
|
|
196
|
+
if (tableBlocks.length === 0) return false;
|
|
197
|
+
|
|
198
|
+
const fromOwner = findContainingParagraphForEndpoint(surfaceBlocks, range.from, "start");
|
|
199
|
+
const toOwner = findContainingParagraphForEndpoint(surfaceBlocks, range.to, "end");
|
|
200
|
+
if (!fromOwner || !toOwner) return false;
|
|
201
|
+
|
|
202
|
+
const fromMidRun = range.from > fromOwner.from && range.from < fromOwner.to;
|
|
203
|
+
const toMidRun = range.to > toOwner.from && range.to < toOwner.to;
|
|
204
|
+
if (!fromMidRun && !toMidRun) return false;
|
|
205
|
+
|
|
206
|
+
const fromAdjacent = fromMidRun && paragraphIsTableAdjacent(fromOwner, tableBlocks);
|
|
207
|
+
const toAdjacent = toMidRun && paragraphIsTableAdjacent(toOwner, tableBlocks);
|
|
208
|
+
return fromAdjacent || toAdjacent;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function paragraphIsTableAdjacent(
|
|
212
|
+
paragraph: FlattenedSurfaceBlock,
|
|
213
|
+
tableBlocks: readonly FlattenedSurfaceBlock[],
|
|
214
|
+
): boolean {
|
|
215
|
+
return tableBlocks.some((table) => {
|
|
216
|
+
if (table.tableId !== null && table.tableId === paragraph.tableId) {
|
|
217
|
+
// Skip: a table block at the same cell scope would be a
|
|
218
|
+
// descendant of the paragraph's containing cell, not a sibling.
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
const leftGap = Math.abs(paragraph.from - table.to);
|
|
222
|
+
const rightGap = Math.abs(paragraph.to - table.from);
|
|
223
|
+
return leftGap <= TABLE_ADJACENT_WINDOW || rightGap <= TABLE_ADJACENT_WINDOW;
|
|
224
|
+
});
|
|
120
225
|
}
|
|
121
226
|
|
|
122
227
|
export function rangeStaysWithinCommentableStory(
|