@beyondwork/docx-react-component 1.0.35 → 1.0.37
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 +103 -13
- package/package.json +1 -1
- package/src/api/package-version.ts +13 -0
- package/src/api/public-types.ts +84 -1
- package/src/core/commands/index.ts +19 -2
- package/src/core/selection/mapping.ts +6 -0
- package/src/io/docx-session.ts +24 -9
- package/src/io/export/build-app-properties-xml.ts +88 -0
- package/src/io/export/serialize-comments.ts +6 -1
- package/src/io/export/serialize-footnotes.ts +10 -9
- package/src/io/export/serialize-headers-footers.ts +11 -10
- package/src/io/export/serialize-main-document.ts +337 -50
- package/src/io/export/serialize-numbering.ts +115 -24
- package/src/io/export/serialize-tables.ts +13 -11
- package/src/io/export/table-properties-xml.ts +35 -16
- package/src/io/export/twip.ts +66 -0
- package/src/io/normalize/normalize-text.ts +5 -0
- package/src/io/ooxml/parse-footnotes.ts +2 -1
- package/src/io/ooxml/parse-headers-footers.ts +2 -1
- package/src/io/ooxml/parse-main-document.ts +21 -1
- package/src/legal/bookmarks.ts +78 -0
- package/src/model/canonical-document.ts +11 -0
- package/src/review/store/scope-tag-diff.ts +130 -0
- package/src/runtime/document-navigation.ts +1 -305
- package/src/runtime/document-runtime.ts +178 -16
- package/src/runtime/layout/docx-font-loader.ts +143 -0
- package/src/runtime/layout/index.ts +188 -0
- package/src/runtime/layout/inert-layout-facet.ts +45 -0
- package/src/runtime/layout/layout-engine-instance.ts +618 -0
- package/src/runtime/layout/layout-invalidation.ts +257 -0
- package/src/runtime/layout/layout-measurement-provider.ts +175 -0
- package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
- package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
- package/src/runtime/layout/page-fragment-mapper.ts +179 -0
- package/src/runtime/layout/page-graph.ts +433 -0
- package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
- package/src/runtime/layout/page-story-resolver.ts +195 -0
- package/src/runtime/layout/paginated-layout-engine.ts +788 -0
- package/src/runtime/layout/public-facet.ts +705 -0
- package/src/runtime/layout/resolved-formatting-document.ts +317 -0
- package/src/runtime/layout/resolved-formatting-state.ts +430 -0
- package/src/runtime/scope-tag-registry.ts +95 -0
- package/src/runtime/session-capabilities.ts +7 -4
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/text-ack-range.ts +49 -0
- package/src/ui/WordReviewEditor.tsx +15 -0
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-surface-controller.tsx +3 -0
- package/src/ui/headless/chrome-registry.ts +235 -0
- package/src/ui/headless/scoped-chrome-policy.ts +164 -0
- package/src/ui/headless/selection-tool-context.ts +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +36 -17
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +333 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +89 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +21 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +8 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +73 -13
- package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
- package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
- package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +173 -6
- package/src/ui-tailwind/theme/editor-theme.css +40 -14
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +235 -166
- package/src/ui-tailwind/tw-review-workspace.tsx +27 -1
package/README.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
3
|
-
summary:
|
|
2
|
+
title: Beyond Work Components
|
|
3
|
+
summary: Backoffice work components landing page and primary router into consumer, wrapper, agent, and maintainer documentation.
|
|
4
4
|
audience: consumer, wrapper, agent, maintainer
|
|
5
5
|
stability: main-path
|
|
6
6
|
docRole: main-path
|
|
7
7
|
canonical: true
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
#
|
|
10
|
+
# Beyond Work Components
|
|
11
11
|
|
|
12
12
|
[](https://github.com/bwllaming/React-OOXML-Office/actions/workflows/ci.yml)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
This repository builds **backoffice work components** for the [Beyond Work](https://beyondwork.ai) runtime — embeddable, composable modules that handle document editing, review workflows, data processing, and task automation inside enterprise workspaces.
|
|
15
|
+
|
|
16
|
+
The design principle across every component: **advanced tasks should be equally easy for AI agents and human users.** Every capability exposed through the UI is also available through a structured API. Every API is designed so an agent can inspect state, reason about it, and act on it without special accommodation. Humans and agents share the same runtime, the same commands, the same read models, and the same fidelity guarantees.
|
|
15
17
|
|
|
16
18
|
## Use This README For
|
|
17
19
|
|
|
@@ -19,11 +21,70 @@ canonical: true
|
|
|
19
21
|
- installing the package
|
|
20
22
|
- finding the right documentation lane quickly
|
|
21
23
|
|
|
22
|
-
Do not use this README as the full package contract. The canonical consumer path starts in `docs/README.md`, `docs/reference/public-api.md
|
|
24
|
+
Do not use this README as the full package contract. The canonical consumer path starts in `docs/README.md`, [`docs/reference/public-api.md`](docs/reference/public-api.md), and [`docs/reference/integration-guide.md`](docs/reference/integration-guide.md).
|
|
25
|
+
|
|
26
|
+
## Components
|
|
27
|
+
|
|
28
|
+
### Document Editor (`@beyondwork/docx-react-component`)
|
|
29
|
+
|
|
30
|
+
A fidelity-first React `.docx` editor centered on `WordReviewEditor`. Built for legal review, contract negotiation, and any workflow where documents need to survive a round-trip through Microsoft Word without damage.
|
|
31
|
+
|
|
32
|
+
**Capabilities:**
|
|
33
|
+
- Full tracked-change and comment support (add, resolve, accept, reject)
|
|
34
|
+
- Suggesting mode where every edit automatically becomes a tracked change
|
|
35
|
+
- Round-trip OOXML preservation — open, edit, export, reopen in Word without repair prompts
|
|
36
|
+
- Workflow overlays with scope-based editing constraints
|
|
37
|
+
- Runtime-backed read models for document structure, review state, selection, compatibility, and fields
|
|
38
|
+
- Document comparison with LCS-based diffing and redline export
|
|
39
|
+
- Legal document analysis (bookmarks, cross-references, defined terms, signature blocks)
|
|
40
|
+
- Real-time collaboration via Yjs
|
|
41
|
+
|
|
42
|
+
**Human-AI parity in practice:**
|
|
43
|
+
- The human clicks "Add Comment" on selected text; the agent calls `addComment({ anchor, body })` on the same selection
|
|
44
|
+
- The human reviews tracked changes in the sidebar; the agent reads `getTrackedChanges()` and makes the same accept/reject decisions
|
|
45
|
+
- The human exports via toolbar; the agent calls `exportDocx()` after the same `getCompatibilityReport()` check
|
|
46
|
+
- Workflow overlays constrain both humans and agents identically — `getInteractionGuardSnapshot()` is the single source of posture truth
|
|
47
|
+
- The AI action policy module gates 37 discrete agent operations with risk classification and context validation
|
|
48
|
+
|
|
49
|
+
### Workblocks
|
|
50
|
+
|
|
51
|
+
Composable task modules that automate business processes. Each workblock is a self-contained package of Python backend logic, React UI components, and a declarative task graph defined in `manifest.yaml`. They execute on the Beyond Work platform via Temporal workflows.
|
|
52
|
+
|
|
53
|
+
**Categories:**
|
|
54
|
+
- **Document processing** — invoice extraction, PDF template OCR, bulk processing, email-to-action
|
|
55
|
+
- **AI agents** — explore, flow debugging, data querying, task launching, authoring
|
|
56
|
+
- **Analytics** — execution history, company insights, timeline queries
|
|
57
|
+
- **Learning** — human correction capture, learning set curation, embedding management
|
|
58
|
+
- **Infrastructure** — registry, health checks, credential management (9 types)
|
|
59
|
+
|
|
60
|
+
**Human-AI parity in practice:**
|
|
61
|
+
- Users create workblocks through VS Code Studio with prompts and visual editing; authoring agents create them through the same manifest schema and file tools
|
|
62
|
+
- Users validate invoice fields in a React form; agents validate the same fields through typed task inputs/outputs
|
|
63
|
+
- Both humans and agents operate within the same workblock execution engine — same task graph, same data flow, same error handling
|
|
64
|
+
|
|
65
|
+
## The Beyond Work Runtime
|
|
66
|
+
|
|
67
|
+
These components plug into a four-layer platform:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
User-Facing (Layer 4)
|
|
71
|
+
neui (Next.js 15), bport (BFF), MCP server, VS Code extension
|
|
72
|
+
|
|
|
73
|
+
Workblocks (Layer 3)
|
|
74
|
+
20+ composable task modules: agents, data processing, credentials
|
|
75
|
+
|
|
|
76
|
+
Platform Services (Layer 2)
|
|
77
|
+
bworker (Go), modelproxy (LiteLLM), registry, pythonworker, Temporal
|
|
78
|
+
|
|
|
79
|
+
AWS Infrastructure (Layer 1)
|
|
80
|
+
EKS, Crossplane, Terraform, PostgreSQL, Redis, Qdrant, S3, SQS
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The document editor lives at Layer 4 as an embeddable surface. Workblocks live at Layer 3 and are orchestrated by the platform. Both expose their capabilities symmetrically to human UIs and agent APIs.
|
|
23
84
|
|
|
24
85
|
## Current Package Reality
|
|
25
86
|
|
|
26
|
-
The
|
|
87
|
+
The shipped contract today is docx-first:
|
|
27
88
|
|
|
28
89
|
- `docx` is the only implemented and shipped runtime contract
|
|
29
90
|
- `xlsx` is planned work, not part of the current package contract
|
|
@@ -93,11 +154,11 @@ The current shipped ESM exports include:
|
|
|
93
154
|
- `@beyondwork/docx-react-component/tailwind`
|
|
94
155
|
- `@beyondwork/docx-react-component/api/public-types`
|
|
95
156
|
|
|
96
|
-
Use `docs/reference/public-api.md` and `docs/reference/public-api.manifest.json` for the current contract inventory and stability guidance.
|
|
157
|
+
Use [`docs/reference/public-api.md`](docs/reference/public-api.md) and `docs/reference/public-api.manifest.json` for the current contract inventory and stability guidance.
|
|
97
158
|
|
|
98
159
|
## Product Contract
|
|
99
160
|
|
|
100
|
-
For every
|
|
161
|
+
For every component this repo ships, the standard is:
|
|
101
162
|
|
|
102
163
|
> Open -> edit -> save -> reopen in the host application without damage.
|
|
103
164
|
|
|
@@ -158,7 +219,38 @@ Current integration honesty:
|
|
|
158
219
|
|
|
159
220
|
The CCEP corpus is kept on `main` as a maintainer-safe smoke-doc source set for agreement-heavy validation and wrapper or agent benchmarking.
|
|
160
221
|
|
|
161
|
-
|
|
222
|
+
### Technical Wiki
|
|
223
|
+
|
|
224
|
+
- [`docs/wiki/`](docs/wiki/) — Feature-by-feature technical documentation (25+ topics covering OOXML, ProseMirror, runtime, and platform)
|
|
225
|
+
|
|
226
|
+
## Agent Integration
|
|
227
|
+
|
|
228
|
+
Agents consume the editor through a consistent pipeline:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
inspect --> locate --> mutate --> validate --> export
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Beyond Work platform agents extend this with propose/approve gates:
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
inspect --> locate --> propose --> approve --> mutate --> validate --> export
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The package exposes purpose-built read models — not a monolithic state dump:
|
|
241
|
+
|
|
242
|
+
| Read model | Getter | Purpose |
|
|
243
|
+
|---|---|---|
|
|
244
|
+
| Document surface | `getRenderSnapshot()` | Structure, blocks, text, selection |
|
|
245
|
+
| Comments | `getComments()` | Thread state, resolution, anchors |
|
|
246
|
+
| Tracked changes | `getTrackedChanges()` | Revision inventory, actionability |
|
|
247
|
+
| Workflow scope | `getWorkflowScopeSnapshot()` | Scope boundaries, work items, mode |
|
|
248
|
+
| Interaction guard | `getInteractionGuardSnapshot()` | Effective mode, blocked reasons |
|
|
249
|
+
| Compatibility | `getCompatibilityReport()` | Export risk assessment |
|
|
250
|
+
| Warnings | `getWarnings()` | Active fidelity or mutation warnings |
|
|
251
|
+
| Fields | `getFieldSnapshot()` | Field posture and refresh state |
|
|
252
|
+
|
|
253
|
+
Agents should read the narrow snapshot that answers the next question, not pull the full render snapshot for every decision.
|
|
162
254
|
|
|
163
255
|
## Packaging And Release
|
|
164
256
|
|
|
@@ -181,9 +273,7 @@ Maintainer prompts, operator runbooks, and proof/closure material remain importa
|
|
|
181
273
|
|
|
182
274
|
## Guiding Principle
|
|
183
275
|
|
|
184
|
-
This repo is not
|
|
185
|
-
|
|
186
|
-
It is building fidelity-first office-document runtimes with explicit preservation and calm, reviewable UI.
|
|
276
|
+
This repo builds backoffice work components where humans and AI agents are first-class peers. Every capability is designed for both audiences from the start — not bolted on for one after the other. The result is tools that are powerful enough for complex enterprise workflows and structured enough that an agent can operate them safely and predictably.
|
|
187
277
|
|
|
188
278
|
## Using the package
|
|
189
279
|
|
|
@@ -663,7 +753,7 @@ Fired when the document transitions between clean and dirty (unsaved) states.
|
|
|
663
753
|
|
|
664
754
|
#### `story_changed`
|
|
665
755
|
|
|
666
|
-
Fired when the user navigates between document stories (e.g. main body
|
|
756
|
+
Fired when the user navigates between document stories (e.g. main body -> header/footer).
|
|
667
757
|
|
|
668
758
|
```ts
|
|
669
759
|
{
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.37",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package version string — pinned to package.json `version`.
|
|
3
|
+
*
|
|
4
|
+
* Consumed by export paths that embed a producer identity (e.g.
|
|
5
|
+
* `/docProps/app.xml` `<Application>` tag) so exported packages can be
|
|
6
|
+
* traced back to the exact runtime release.
|
|
7
|
+
*
|
|
8
|
+
* Keep this in lockstep with `package.json#version`. A future enhancement
|
|
9
|
+
* would be to generate this file from package.json at build time; for now
|
|
10
|
+
* the string is committed directly so the runtime works without a build
|
|
11
|
+
* step for tests.
|
|
12
|
+
*/
|
|
13
|
+
export const PACKAGE_VERSION = "1.0.36";
|
package/src/api/public-types.ts
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
import type { PersistedEditorSnapshot as RuntimePersistedEditorSnapshot } from "../core/state/editor-state.ts";
|
|
2
|
+
import type { WordReviewEditorLayoutFacet } from "../runtime/layout/public-facet.ts";
|
|
3
|
+
|
|
4
|
+
export type {
|
|
5
|
+
WordReviewEditorLayoutFacet,
|
|
6
|
+
LayoutFacetEvent,
|
|
7
|
+
LayoutFacetInvalidationReason,
|
|
8
|
+
PublicBlockFragment,
|
|
9
|
+
PublicBlockMeasurement,
|
|
10
|
+
PublicFieldDirtinessReport,
|
|
11
|
+
PublicLineBox,
|
|
12
|
+
PublicMeasurementFidelity,
|
|
13
|
+
PublicNoteAllocation,
|
|
14
|
+
PublicPageAnchor,
|
|
15
|
+
PublicPageNode,
|
|
16
|
+
PublicPageRegion,
|
|
17
|
+
PublicPageRegions,
|
|
18
|
+
PublicPageSpan,
|
|
19
|
+
PublicResolvedPageStories,
|
|
20
|
+
PublicResolvedParagraphFormatting,
|
|
21
|
+
PublicResolvedRunFormatting,
|
|
22
|
+
PublicSectionNode,
|
|
23
|
+
} from "../runtime/layout/public-facet.ts";
|
|
2
24
|
|
|
3
25
|
export type FieldFamily = import("../model/canonical-document.ts").FieldFamily;
|
|
4
26
|
export type FieldRefreshStatus = import("../model/canonical-document.ts").FieldRefreshStatus;
|
|
@@ -767,6 +789,7 @@ export type SurfaceBlockSnapshot =
|
|
|
767
789
|
keepNext?: boolean;
|
|
768
790
|
keepLines?: boolean;
|
|
769
791
|
pageBreakBefore?: boolean;
|
|
792
|
+
widowControl?: boolean;
|
|
770
793
|
outlineLevel?: number;
|
|
771
794
|
bidi?: boolean;
|
|
772
795
|
suppressLineNumbers?: boolean;
|
|
@@ -920,7 +943,7 @@ export type ViewMode = "editing" | "review" | "view";
|
|
|
920
943
|
* Distinct from `ViewMode` (editor posture) and `WorkspaceMode` (shell layout).
|
|
921
944
|
* `DocumentMode` reflects document-level editing authority, not presentation.
|
|
922
945
|
*/
|
|
923
|
-
export type DocumentMode = "editing" | "suggesting" | "viewing";
|
|
946
|
+
export type DocumentMode = "editing" | "suggesting" | "viewing" | "commenting";
|
|
924
947
|
|
|
925
948
|
/**
|
|
926
949
|
* A single permission range parsed from `w:permStart` / `w:permEnd` in the
|
|
@@ -1916,6 +1939,19 @@ export interface WordReviewEditorRef {
|
|
|
1916
1939
|
getRuntimeContextAnalytics(
|
|
1917
1940
|
query?: RuntimeContextAnalyticsQuery,
|
|
1918
1941
|
): RuntimeContextAnalyticsSnapshot | null;
|
|
1942
|
+
|
|
1943
|
+
/**
|
|
1944
|
+
* Runtime-owned layout facet.
|
|
1945
|
+
*
|
|
1946
|
+
* The ergonomic surface for walking the page graph, resolving offsets to
|
|
1947
|
+
* fragments, inspecting formatting, and observing layout events. Prefer
|
|
1948
|
+
* this over `getPageLayoutSnapshot()` / `getDocumentNavigationSnapshot()`
|
|
1949
|
+
* for new code — those remain as thin adapters for compatibility.
|
|
1950
|
+
*
|
|
1951
|
+
* Returns `null` only when the runtime is still loading; once the `ready`
|
|
1952
|
+
* event has fired, the facet is always available.
|
|
1953
|
+
*/
|
|
1954
|
+
readonly layout: WordReviewEditorLayoutFacet;
|
|
1919
1955
|
}
|
|
1920
1956
|
|
|
1921
1957
|
export type WordReviewEditorChromePreset =
|
|
@@ -1970,3 +2006,50 @@ export interface WordReviewEditorChromeVisibility {
|
|
|
1970
2006
|
statusBar: boolean;
|
|
1971
2007
|
reviewRail: boolean;
|
|
1972
2008
|
}
|
|
2009
|
+
|
|
2010
|
+
// ---------------------------------------------------------------------------
|
|
2011
|
+
// Bounded local-first predicted-text lane contract
|
|
2012
|
+
// ---------------------------------------------------------------------------
|
|
2013
|
+
|
|
2014
|
+
export type TextCommandAckKind =
|
|
2015
|
+
| "equivalent"
|
|
2016
|
+
| "adjusted"
|
|
2017
|
+
| "rejected"
|
|
2018
|
+
| "structural-divergence";
|
|
2019
|
+
|
|
2020
|
+
export interface ScopeTagTouch {
|
|
2021
|
+
/** Tag family: "comment" | "revision" | "field" | "bookmark" | "sdt" | "opaque" | custom string. */
|
|
2022
|
+
tagType: string;
|
|
2023
|
+
tagId: string;
|
|
2024
|
+
behavior: "extended" | "trimmed" | "split" | "detached" | "unchanged";
|
|
2025
|
+
range: { from: number; to: number };
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
/**
|
|
2029
|
+
* Sync reconciliation result returned by `DocumentRuntime.applyActiveStoryTextCommand`.
|
|
2030
|
+
*
|
|
2031
|
+
* The predicted-text lane in the mounted editor dispatches a local PM transaction,
|
|
2032
|
+
* then calls the runtime synchronously. The ack tells the lane how to reconcile:
|
|
2033
|
+
*
|
|
2034
|
+
* - `equivalent`: runtime accepted the edit and produced the same text shape;
|
|
2035
|
+
* the lane can skip the Lane A PM rebuild entirely.
|
|
2036
|
+
* - `adjusted`: runtime accepted but normalized or added review marks; the lane
|
|
2037
|
+
* patches the affected range from the canonical snapshot.
|
|
2038
|
+
* - `rejected`: runtime blocked the edit (workflow, protection, read-only);
|
|
2039
|
+
* the lane rolls back the predicted PM range from its pre-image.
|
|
2040
|
+
* - `structural-divergence`: the command affected structure beyond the
|
|
2041
|
+
* predicted range; the lane falls back to a full PM rebuild.
|
|
2042
|
+
*/
|
|
2043
|
+
export interface TextCommandAck {
|
|
2044
|
+
kind: TextCommandAckKind;
|
|
2045
|
+
/** Opaque id echoed back from the predicted op. Undefined for canonical callers. */
|
|
2046
|
+
opId?: string;
|
|
2047
|
+
/** Revision token of the document AFTER commit. Empty string when `kind === "rejected"`. */
|
|
2048
|
+
newRevisionToken: string;
|
|
2049
|
+
/** For `adjusted`: the canonical range that differs from the prediction. */
|
|
2050
|
+
adjustedRange?: { fromRuntime: number; toRuntime: number };
|
|
2051
|
+
/** For `rejected`: the blocked reasons. */
|
|
2052
|
+
blockedReasons?: readonly { code: string; message: string }[];
|
|
2053
|
+
/** Tag touches the runtime applied so the lane can redraw decorations without a PM rebuild. */
|
|
2054
|
+
scopeTagTouches?: readonly ScopeTagTouch[];
|
|
2055
|
+
}
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
} from "./text-commands.ts";
|
|
36
36
|
import type { RevisionRecord as CanonicalRevisionRecord } from "../../model/canonical-document.ts";
|
|
37
37
|
import { remapCommentThreads } from "../../review/store/comment-remapping.ts";
|
|
38
|
+
import { collectScopeTagTouches } from "../../review/store/scope-tag-diff.ts";
|
|
38
39
|
import { applyRevisionRuntimeCommand } from "../../runtime/revision-runtime.ts";
|
|
39
40
|
import type { RevisionStore } from "../../review/store/revision-store.ts";
|
|
40
41
|
|
|
@@ -195,7 +196,7 @@ export interface EditorTransaction {
|
|
|
195
196
|
|
|
196
197
|
export interface CommandExecutionContext {
|
|
197
198
|
timestamp: string;
|
|
198
|
-
documentMode?: "editing" | "suggesting" | "viewing";
|
|
199
|
+
documentMode?: "editing" | "suggesting" | "viewing" | "commenting";
|
|
199
200
|
defaultAuthorId?: string;
|
|
200
201
|
}
|
|
201
202
|
|
|
@@ -878,6 +879,22 @@ function applyTextCommand(
|
|
|
878
879
|
result.mapping,
|
|
879
880
|
);
|
|
880
881
|
|
|
882
|
+
const scopeTagTouches = collectScopeTagTouches(
|
|
883
|
+
state.document.review.comments,
|
|
884
|
+
reviewState.document.review.comments,
|
|
885
|
+
state.document.review.revisions,
|
|
886
|
+
reviewState.document.review.revisions,
|
|
887
|
+
);
|
|
888
|
+
const mappingWithTouches: TransactionMapping = scopeTagTouches.length > 0
|
|
889
|
+
? {
|
|
890
|
+
...result.mapping,
|
|
891
|
+
metadata: {
|
|
892
|
+
...(result.mapping.metadata ?? {}),
|
|
893
|
+
scopeTagTouches,
|
|
894
|
+
},
|
|
895
|
+
}
|
|
896
|
+
: result.mapping;
|
|
897
|
+
|
|
881
898
|
return createTransaction(
|
|
882
899
|
{
|
|
883
900
|
...state,
|
|
@@ -892,7 +909,7 @@ function applyTextCommand(
|
|
|
892
909
|
{
|
|
893
910
|
historyBoundary: "push",
|
|
894
911
|
markDirty: true,
|
|
895
|
-
mapping:
|
|
912
|
+
mapping: mappingWithTouches,
|
|
896
913
|
effects: reviewState.effects,
|
|
897
914
|
},
|
|
898
915
|
);
|
|
@@ -51,6 +51,12 @@ export interface TransactionMapping {
|
|
|
51
51
|
affectsComments?: boolean;
|
|
52
52
|
affectsRevisions?: boolean;
|
|
53
53
|
affectsOpaqueFragments?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Review-tag touches performed during this transaction. Used by the
|
|
56
|
+
* predicted-text lane's reconciler to classify acks as `adjusted` vs
|
|
57
|
+
* `structural-divergence` without re-walking the document.
|
|
58
|
+
*/
|
|
59
|
+
scopeTagTouches?: readonly import("../../api/public-types.ts").ScopeTagTouch[];
|
|
54
60
|
[key: string]: unknown;
|
|
55
61
|
};
|
|
56
62
|
}
|
package/src/io/docx-session.ts
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
createBrokenRelationshipIssue,
|
|
68
68
|
createMissingPartIssue,
|
|
69
69
|
} from "./opc/corrupt-package.ts";
|
|
70
|
+
import { buildAppPropertiesXml } from "./export/build-app-properties-xml.ts";
|
|
70
71
|
import { createExportSession } from "./export/export-session.ts";
|
|
71
72
|
import { serializeMainDocument } from "./export/serialize-main-document.ts";
|
|
72
73
|
import {
|
|
@@ -2909,10 +2910,31 @@ function serializeCanonicalDocumentForExport(document: CanonicalDocumentEnvelope
|
|
|
2909
2910
|
});
|
|
2910
2911
|
}
|
|
2911
2912
|
|
|
2913
|
+
/**
|
|
2914
|
+
* Read a Node-style environment variable without referencing the Node-only
|
|
2915
|
+
* `process` global in the production build (which excludes @types/node).
|
|
2916
|
+
* Returns `undefined` in browser environments or when the var is unset.
|
|
2917
|
+
*/
|
|
2918
|
+
function readNodeEnvVar(name: string): string | undefined {
|
|
2919
|
+
const proc = (globalThis as unknown as {
|
|
2920
|
+
process?: { env?: Record<string, string | undefined> };
|
|
2921
|
+
}).process;
|
|
2922
|
+
return proc?.env?.[name];
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2912
2925
|
function canReuseSourceBytesForCurrentDocument(
|
|
2913
2926
|
state: ImportedDocxState,
|
|
2914
2927
|
document: CanonicalDocumentEnvelope,
|
|
2915
2928
|
): boolean {
|
|
2929
|
+
// The validator-loop CI harness (§1 / §7) needs to exercise the full
|
|
2930
|
+
// serializer pipeline — including A.1 / A.2 / A.3 / A.6 / A.7 / A.8 /
|
|
2931
|
+
// A.9 fixes — so it sets DOCX_VALIDATOR_FORCE_REGEN=1 to disable the
|
|
2932
|
+
// fast-path byte-reuse. Never set this flag in production; it forces a
|
|
2933
|
+
// full XML regeneration on every export and is only used by the CCEP
|
|
2934
|
+
// validator harness to prove our serializer's correctness.
|
|
2935
|
+
if (readNodeEnvVar("DOCX_VALIDATOR_FORCE_REGEN") === "1") {
|
|
2936
|
+
return false;
|
|
2937
|
+
}
|
|
2916
2938
|
if (requiresHostMetadataNormalization(state.sourcePackage, state.sourceDocumentPartPath)) {
|
|
2917
2939
|
return false;
|
|
2918
2940
|
}
|
|
@@ -3108,15 +3130,8 @@ function buildCorePropertiesXml(document: CanonicalDocumentEnvelope): string {
|
|
|
3108
3130
|
].join("\n");
|
|
3109
3131
|
}
|
|
3110
3132
|
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
|
|
3114
|
-
`<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">`,
|
|
3115
|
-
` <Application>React OOXML Office</Application>`,
|
|
3116
|
-
` <AppVersion>1.0</AppVersion>`,
|
|
3117
|
-
`</Properties>`,
|
|
3118
|
-
].join("\n");
|
|
3119
|
-
}
|
|
3133
|
+
// buildAppPropertiesXml moved to src/io/export/build-app-properties-xml.ts
|
|
3134
|
+
// per close-render-fidelity §2 A.5. Re-import as `buildAppPropertiesXmlFn`.
|
|
3120
3135
|
|
|
3121
3136
|
function xmlNode(tagName: string, value: string | undefined): string | undefined {
|
|
3122
3137
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build-app-properties-xml — synthesises `/docProps/app.xml` for the export.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `buildCorePropertiesXml` (in `docx-session.ts`) but targets the
|
|
5
|
+
* `extended-properties` namespace. Emitted once per export when the source
|
|
6
|
+
* package does not already carry a valid app.xml part with the correct
|
|
7
|
+
* content type.
|
|
8
|
+
*
|
|
9
|
+
* Required by:
|
|
10
|
+
* docs/plans/close-render-fidelity.md §2 A.5
|
|
11
|
+
*
|
|
12
|
+
* Schema shape (ECMA-376 Part 1, Office Extended properties):
|
|
13
|
+
* <Properties xmlns="…/extended-properties" xmlns:vt="…/docPropsVTypes">
|
|
14
|
+
* <Application>@beyondwork/docx-react-component/<pkg.version></Application>
|
|
15
|
+
* <AppVersion>1.0</AppVersion>
|
|
16
|
+
* <Pages>0</Pages>
|
|
17
|
+
* <Words>0</Words>
|
|
18
|
+
* <Characters>0</Characters>
|
|
19
|
+
* <Lines>0</Lines>
|
|
20
|
+
* <Paragraphs>0</Paragraphs>
|
|
21
|
+
* </Properties>
|
|
22
|
+
*
|
|
23
|
+
* Zero counts are acceptable per the ECMA schema — Word-authoritative counts
|
|
24
|
+
* are recomputed at open time by the hosting application. A future §4 pass
|
|
25
|
+
* can replace these with canonical-derived counts without changing the XML
|
|
26
|
+
* shape.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { PACKAGE_VERSION } from "../../api/package-version.ts";
|
|
30
|
+
|
|
31
|
+
export const APP_PROPERTIES_NAMESPACE =
|
|
32
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties";
|
|
33
|
+
export const APP_PROPERTIES_VT_NAMESPACE =
|
|
34
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes";
|
|
35
|
+
|
|
36
|
+
export interface AppPropertiesStats {
|
|
37
|
+
/** Rendered page count; zero is a valid placeholder. */
|
|
38
|
+
pages?: number;
|
|
39
|
+
/** Word count. */
|
|
40
|
+
words?: number;
|
|
41
|
+
/** Character count. */
|
|
42
|
+
characters?: number;
|
|
43
|
+
/** Line count. */
|
|
44
|
+
lines?: number;
|
|
45
|
+
/** Paragraph count. */
|
|
46
|
+
paragraphs?: number;
|
|
47
|
+
/** App-version string override; defaults to the package version. */
|
|
48
|
+
appVersion?: string;
|
|
49
|
+
/** Application identifier override; defaults to package.json name@version. */
|
|
50
|
+
application?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildAppPropertiesXml(
|
|
54
|
+
stats: AppPropertiesStats = {},
|
|
55
|
+
): string {
|
|
56
|
+
const application = stats.application ?? defaultApplicationLabel();
|
|
57
|
+
const appVersion = stats.appVersion ?? PACKAGE_VERSION;
|
|
58
|
+
const pages = stats.pages ?? 0;
|
|
59
|
+
const words = stats.words ?? 0;
|
|
60
|
+
const characters = stats.characters ?? 0;
|
|
61
|
+
const lines = stats.lines ?? 0;
|
|
62
|
+
const paragraphs = stats.paragraphs ?? 0;
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
|
|
66
|
+
`<Properties xmlns="${APP_PROPERTIES_NAMESPACE}" xmlns:vt="${APP_PROPERTIES_VT_NAMESPACE}">`,
|
|
67
|
+
` <Application>${escapeXml(application)}</Application>`,
|
|
68
|
+
` <AppVersion>${escapeXml(appVersion)}</AppVersion>`,
|
|
69
|
+
` <Pages>${Math.max(0, Math.round(pages))}</Pages>`,
|
|
70
|
+
` <Words>${Math.max(0, Math.round(words))}</Words>`,
|
|
71
|
+
` <Characters>${Math.max(0, Math.round(characters))}</Characters>`,
|
|
72
|
+
` <Lines>${Math.max(0, Math.round(lines))}</Lines>`,
|
|
73
|
+
` <Paragraphs>${Math.max(0, Math.round(paragraphs))}</Paragraphs>`,
|
|
74
|
+
`</Properties>`,
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function defaultApplicationLabel(): string {
|
|
79
|
+
return `@beyondwork/docx-react-component/${PACKAGE_VERSION}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function escapeXml(value: string): string {
|
|
83
|
+
return value
|
|
84
|
+
.replace(/&/gu, "&")
|
|
85
|
+
.replace(/</gu, "<")
|
|
86
|
+
.replace(/>/gu, ">")
|
|
87
|
+
.replace(/"/gu, """);
|
|
88
|
+
}
|
|
@@ -64,8 +64,13 @@ export interface SerializedCommentDocumentResult {
|
|
|
64
64
|
skippedCommentIds: string[];
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// A.8: emit root attributes in deterministic order (xmlns:* alphabetised,
|
|
68
|
+
// then mc:Ignorable, then other attrs). The sort above happens to produce
|
|
69
|
+
// the same token order `mc < w < w14` + `mc:Ignorable` — keep the literal
|
|
70
|
+
// in sync with renderDeterministicRootAttributes so regenerated docs and
|
|
71
|
+
// comments.xml samples match byte-for-byte when snapshots compare them.
|
|
67
72
|
const DEFAULT_COMMENTS_ROOT_TAG =
|
|
68
|
-
`<w:comments xmlns:
|
|
73
|
+
`<w:comments xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" mc:Ignorable="w14">`;
|
|
69
74
|
const DEFAULT_COMMENTS_EXTENDED_ROOT_TAG =
|
|
70
75
|
`<w15:commentsEx xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml">`;
|
|
71
76
|
const DEFAULT_COMMENTS_IDS_ROOT_TAG =
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
serializeTablePropertiesXml,
|
|
17
17
|
serializeTableRowPropertiesXml,
|
|
18
18
|
} from "./table-properties-xml.ts";
|
|
19
|
+
import { twip } from "./twip.ts";
|
|
19
20
|
|
|
20
21
|
export const WORD_FOOTNOTES_CONTENT_TYPE =
|
|
21
22
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml";
|
|
@@ -142,7 +143,7 @@ function serializeTable(table: TableNode): string {
|
|
|
142
143
|
if (table.gridColumns.length > 0) {
|
|
143
144
|
xml += "<w:tblGrid>";
|
|
144
145
|
for (const width of table.gridColumns) {
|
|
145
|
-
xml += `<w:gridCol w:w="${width}"/>`;
|
|
146
|
+
xml += `<w:gridCol w:w="${twip(width)}"/>`;
|
|
146
147
|
}
|
|
147
148
|
xml += "</w:tblGrid>";
|
|
148
149
|
}
|
|
@@ -194,9 +195,9 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
|
|
|
194
195
|
}
|
|
195
196
|
if (paragraph.spacing) {
|
|
196
197
|
const attrs: string[] = [];
|
|
197
|
-
if (paragraph.spacing.before !== undefined) attrs.push(`w:before="${paragraph.spacing.before}"`);
|
|
198
|
-
if (paragraph.spacing.after !== undefined) attrs.push(`w:after="${paragraph.spacing.after}"`);
|
|
199
|
-
if (paragraph.spacing.line !== undefined) attrs.push(`w:line="${paragraph.spacing.line}"`);
|
|
198
|
+
if (paragraph.spacing.before !== undefined) attrs.push(`w:before="${twip(paragraph.spacing.before)}"`);
|
|
199
|
+
if (paragraph.spacing.after !== undefined) attrs.push(`w:after="${twip(paragraph.spacing.after)}"`);
|
|
200
|
+
if (paragraph.spacing.line !== undefined) attrs.push(`w:line="${twip(paragraph.spacing.line)}"`);
|
|
200
201
|
if (paragraph.spacing.lineRule) attrs.push(`w:lineRule="${escapeAttribute(paragraph.spacing.lineRule)}"`);
|
|
201
202
|
if (attrs.length > 0) {
|
|
202
203
|
parts.push(`<w:spacing ${attrs.join(" ")}/>`);
|
|
@@ -204,10 +205,10 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
|
|
|
204
205
|
}
|
|
205
206
|
if (paragraph.indentation) {
|
|
206
207
|
const attrs: string[] = [];
|
|
207
|
-
if (paragraph.indentation.left !== undefined) attrs.push(`w:left="${paragraph.indentation.left}"`);
|
|
208
|
-
if (paragraph.indentation.right !== undefined) attrs.push(`w:right="${paragraph.indentation.right}"`);
|
|
209
|
-
if (paragraph.indentation.firstLine !== undefined) attrs.push(`w:firstLine="${paragraph.indentation.firstLine}"`);
|
|
210
|
-
if (paragraph.indentation.hanging !== undefined) attrs.push(`w:hanging="${paragraph.indentation.hanging}"`);
|
|
208
|
+
if (paragraph.indentation.left !== undefined) attrs.push(`w:left="${twip(paragraph.indentation.left)}"`);
|
|
209
|
+
if (paragraph.indentation.right !== undefined) attrs.push(`w:right="${twip(paragraph.indentation.right)}"`);
|
|
210
|
+
if (paragraph.indentation.firstLine !== undefined) attrs.push(`w:firstLine="${twip(paragraph.indentation.firstLine)}"`);
|
|
211
|
+
if (paragraph.indentation.hanging !== undefined) attrs.push(`w:hanging="${twip(paragraph.indentation.hanging)}"`);
|
|
211
212
|
if (attrs.length > 0) {
|
|
212
213
|
parts.push(`<w:ind ${attrs.join(" ")}/>`);
|
|
213
214
|
}
|
|
@@ -330,7 +331,7 @@ function buildRunPropertiesXml(marks: TextMark[] | undefined): string {
|
|
|
330
331
|
);
|
|
331
332
|
break;
|
|
332
333
|
case "fontSize":
|
|
333
|
-
parts.push(`<w:sz w:val="${mark.val}"/>`);
|
|
334
|
+
parts.push(`<w:sz w:val="${twip(mark.val)}"/>`);
|
|
334
335
|
break;
|
|
335
336
|
case "textColor":
|
|
336
337
|
parts.push(`<w:color w:val="${escapeAttribute(mark.color)}"/>`);
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
serializeTablePropertiesXml,
|
|
17
17
|
serializeTableRowPropertiesXml,
|
|
18
18
|
} from "./table-properties-xml.ts";
|
|
19
|
+
import { twip } from "./twip.ts";
|
|
19
20
|
|
|
20
21
|
export const WORD_HEADER_CONTENT_TYPE =
|
|
21
22
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml";
|
|
@@ -132,7 +133,7 @@ function serializeTable(table: TableNode): string {
|
|
|
132
133
|
if (table.gridColumns.length > 0) {
|
|
133
134
|
xml += "<w:tblGrid>";
|
|
134
135
|
for (const width of table.gridColumns) {
|
|
135
|
-
xml += `<w:gridCol w:w="${width}"/>`;
|
|
136
|
+
xml += `<w:gridCol w:w="${twip(width)}"/>`;
|
|
136
137
|
}
|
|
137
138
|
xml += "</w:tblGrid>";
|
|
138
139
|
}
|
|
@@ -182,19 +183,19 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
|
|
|
182
183
|
if (paragraph.spacing) {
|
|
183
184
|
const s = paragraph.spacing;
|
|
184
185
|
const attrs: string[] = [];
|
|
185
|
-
if (s.before !== undefined) attrs.push(`w:before="${s.before}"`);
|
|
186
|
-
if (s.after !== undefined) attrs.push(`w:after="${s.after}"`);
|
|
187
|
-
if (s.line !== undefined) attrs.push(`w:line="${s.line}"`);
|
|
186
|
+
if (s.before !== undefined) attrs.push(`w:before="${twip(s.before)}"`);
|
|
187
|
+
if (s.after !== undefined) attrs.push(`w:after="${twip(s.after)}"`);
|
|
188
|
+
if (s.line !== undefined) attrs.push(`w:line="${twip(s.line)}"`);
|
|
188
189
|
if (s.lineRule) attrs.push(`w:lineRule="${escapeAttribute(s.lineRule)}"`);
|
|
189
190
|
if (attrs.length > 0) parts.push(`<w:spacing ${attrs.join(" ")}/>`);
|
|
190
191
|
}
|
|
191
192
|
if (paragraph.indentation) {
|
|
192
193
|
const ind = paragraph.indentation;
|
|
193
194
|
const attrs: string[] = [];
|
|
194
|
-
if (ind.left !== undefined) attrs.push(`w:left="${ind.left}"`);
|
|
195
|
-
if (ind.right !== undefined) attrs.push(`w:right="${ind.right}"`);
|
|
196
|
-
if (ind.firstLine !== undefined) attrs.push(`w:firstLine="${ind.firstLine}"`);
|
|
197
|
-
if (ind.hanging !== undefined) attrs.push(`w:hanging="${ind.hanging}"`);
|
|
195
|
+
if (ind.left !== undefined) attrs.push(`w:left="${twip(ind.left)}"`);
|
|
196
|
+
if (ind.right !== undefined) attrs.push(`w:right="${twip(ind.right)}"`);
|
|
197
|
+
if (ind.firstLine !== undefined) attrs.push(`w:firstLine="${twip(ind.firstLine)}"`);
|
|
198
|
+
if (ind.hanging !== undefined) attrs.push(`w:hanging="${twip(ind.hanging)}"`);
|
|
198
199
|
if (attrs.length > 0) parts.push(`<w:ind ${attrs.join(" ")}/>`);
|
|
199
200
|
}
|
|
200
201
|
if (paragraph.alignment) {
|
|
@@ -203,7 +204,7 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
|
|
|
203
204
|
if (paragraph.tabStops && paragraph.tabStops.length > 0) {
|
|
204
205
|
const tabsXml = paragraph.tabStops.map((tab) => {
|
|
205
206
|
const leaderAttr = tab.leader ? ` w:leader="${escapeAttribute(tab.leader)}"` : "";
|
|
206
|
-
return `<w:tab w:val="${tab.align}" w:pos="${tab.position}"${leaderAttr}/>`;
|
|
207
|
+
return `<w:tab w:val="${tab.align}" w:pos="${twip(tab.position)}"${leaderAttr}/>`;
|
|
207
208
|
}).join("");
|
|
208
209
|
parts.push(`<w:tabs>${tabsXml}</w:tabs>`);
|
|
209
210
|
}
|
|
@@ -303,7 +304,7 @@ function buildRunPropertiesXml(marks: TextMark[] | undefined): string {
|
|
|
303
304
|
parts.push(`<w:rFonts w:ascii="${escapeAttribute(mark.val)}" w:hAnsi="${escapeAttribute(mark.val)}"/>`);
|
|
304
305
|
break;
|
|
305
306
|
case "fontSize":
|
|
306
|
-
parts.push(`<w:sz w:val="${mark.val}"/>`);
|
|
307
|
+
parts.push(`<w:sz w:val="${twip(mark.val)}"/>`);
|
|
307
308
|
break;
|
|
308
309
|
case "textColor":
|
|
309
310
|
parts.push(`<w:color w:val="${escapeAttribute(mark.color)}"/>`);
|