@beyondwork/docx-react-component 1.0.2 → 1.0.4
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 +11 -7
- package/package.json +21 -29
- package/src/compare/export-redlines.ts +0 -2
- package/src/core/commands/index.ts +116 -8
- package/src/core/state/text-transaction.ts +226 -0
- package/src/formats/xlsx/io/xlsx-session.ts +1 -1
- package/src/io/docx-session.ts +1 -1
- package/src/io/export/serialize-tables.ts +27 -0
- package/src/io/ooxml/parse-tables.ts +50 -0
- package/src/io/opc/package-reader.ts +32 -27
- package/src/runtime/table-commands.ts +10 -3
- package/src/runtime/table-schema.ts +120 -44
- package/src/ui/WordReviewEditor.tsx +2 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +249 -16
- package/src/ui-tailwind/theme/editor-theme.css +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/bwllaming/React-OOXML-Office/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@beyondwork/docx-react-component` is now configured as an npm-ready package for the shipped product in this repository: `WordReviewEditor`, a fidelity-first React editor for legal-review-safe `docx` workflows.
|
|
6
6
|
|
|
7
7
|
The broader repository is still evolving toward a layered `react-ooxml-office` platform, but the source reality is unchanged:
|
|
8
8
|
|
|
@@ -13,7 +13,7 @@ The broader repository is still evolving toward a layered `react-ooxml-office` p
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
pnpm add docx-react-component react react-dom \
|
|
16
|
+
pnpm add @beyondwork/docx-react-component react react-dom tailwindcss \
|
|
17
17
|
prosemirror-commands prosemirror-keymap prosemirror-model \
|
|
18
18
|
prosemirror-state prosemirror-tables prosemirror-transform prosemirror-view
|
|
19
19
|
```
|
|
@@ -23,6 +23,7 @@ Current packaging truth:
|
|
|
23
23
|
- the package is ESM-only
|
|
24
24
|
- exports point at shipped TypeScript source entry points
|
|
25
25
|
- consumers need a bundler or runtime that can resolve `.ts` and `.tsx` ESM imports
|
|
26
|
+
- consumers should import `@beyondwork/docx-react-component/ui-tailwind/theme/editor-theme.css` once and provide a Tailwind v4 CSS pipeline for it
|
|
26
27
|
- package and source identifiers remain docx-first until a deliberate rename lands
|
|
27
28
|
|
|
28
29
|
## Shipped Product
|
|
@@ -30,7 +31,7 @@ Current packaging truth:
|
|
|
30
31
|
The primary shipped surface is:
|
|
31
32
|
|
|
32
33
|
```tsx
|
|
33
|
-
import { WordReviewEditor } from "docx-react-component";
|
|
34
|
+
import { WordReviewEditor } from "@beyondwork/docx-react-component";
|
|
34
35
|
|
|
35
36
|
<WordReviewEditor />
|
|
36
37
|
```
|
|
@@ -43,9 +44,10 @@ import { WordReviewEditor } from "docx-react-component";
|
|
|
43
44
|
|
|
44
45
|
The current public ESM exports are:
|
|
45
46
|
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
47
|
+
- `@beyondwork/docx-react-component` -> `WordReviewEditor`
|
|
48
|
+
- `@beyondwork/docx-react-component/public-types` -> public TypeScript contracts
|
|
49
|
+
- `@beyondwork/docx-react-component/ui-tailwind` -> current Tailwind UI primitives
|
|
50
|
+
- `@beyondwork/docx-react-component/ui-tailwind/theme/editor-theme.css` -> shipped theme variables and Tailwind theme import
|
|
49
51
|
|
|
50
52
|
## Product Contract
|
|
51
53
|
|
|
@@ -84,6 +86,7 @@ Start here:
|
|
|
84
86
|
Current shipped docx contracts:
|
|
85
87
|
|
|
86
88
|
- `docs/reference/public-api.md`
|
|
89
|
+
This doc separates the shipped Wave 21 surface from the future Waves 25 through 27 ref expansion.
|
|
87
90
|
- `docs/reference/ooxml-compliance.md`
|
|
88
91
|
- `docs/reference/word-review-editor-frontend-architecture.md`
|
|
89
92
|
- `docs/reference/word-review-editor-ux-guide.md`
|
|
@@ -99,9 +102,10 @@ Shared platform and planned xlsx docs:
|
|
|
99
102
|
|
|
100
103
|
## Packaging And Release
|
|
101
104
|
|
|
102
|
-
- `.github/workflows/publish.yml` publishes on `v*` tags
|
|
105
|
+
- `.github/workflows/publish.yml` publishes on `v*` tags after verifying the tag matches `package.json`
|
|
103
106
|
- `pnpm pack --dry-run` is the baseline package proof for this wave
|
|
104
107
|
- npm provenance is enabled in `publishConfig`
|
|
108
|
+
- the published package currently ships source ESM entry points plus TypeScript source-backed `types` exports
|
|
105
109
|
- the Microsoft Open XML SDK remains CI/internal-service only, never part of the shipped browser runtime
|
|
106
110
|
|
|
107
111
|
## Contribution Rules
|
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.4",
|
|
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
|
"files": [
|
|
9
8
|
"README.md",
|
|
@@ -30,26 +29,10 @@
|
|
|
30
29
|
"./io/docx-session": "./src/io/docx-session.ts",
|
|
31
30
|
"./runtime/document-runtime": "./src/runtime/document-runtime.ts",
|
|
32
31
|
"./api/public-types": "./src/api/public-types.ts",
|
|
32
|
+
"./ui-tailwind/theme/editor-theme.css": "./src/ui-tailwind/theme/editor-theme.css",
|
|
33
33
|
"./package.json": "./package.json"
|
|
34
34
|
},
|
|
35
35
|
"types": "./src/index.ts",
|
|
36
|
-
"scripts": {
|
|
37
|
-
"build": "tsup",
|
|
38
|
-
"test": "bash scripts/run-workspace-tests.sh",
|
|
39
|
-
"test:repo": "pnpm exec tsx --test $(find test -type f \\( -name '*.test.ts' -o -name '*.test.tsx' \\) | sort)",
|
|
40
|
-
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
41
|
-
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
42
|
-
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
43
|
-
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
44
|
-
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
45
|
-
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
46
|
-
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
47
|
-
"wave:status": "bash scripts/wave-status.sh",
|
|
48
|
-
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
49
|
-
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
50
|
-
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
51
|
-
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
52
|
-
},
|
|
53
36
|
"keywords": [
|
|
54
37
|
"docx",
|
|
55
38
|
"word",
|
|
@@ -71,6 +54,7 @@
|
|
|
71
54
|
"@radix-ui/react-toggle": "^1.1.10",
|
|
72
55
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
|
73
56
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
57
|
+
"fflate": "^0.8.2",
|
|
74
58
|
"lucide-react": "^1.7.0"
|
|
75
59
|
},
|
|
76
60
|
"peerDependencies": {
|
|
@@ -82,7 +66,8 @@
|
|
|
82
66
|
"prosemirror-transform": "^1.11.0",
|
|
83
67
|
"prosemirror-view": "^1.41.7",
|
|
84
68
|
"react": "^19.2.0",
|
|
85
|
-
"react-dom": "^19.2.0"
|
|
69
|
+
"react-dom": "^19.2.0",
|
|
70
|
+
"tailwindcss": "^4.2.2"
|
|
86
71
|
},
|
|
87
72
|
"devDependencies": {
|
|
88
73
|
"@chllming/wave-orchestration": "^0.9.8",
|
|
@@ -101,14 +86,21 @@
|
|
|
101
86
|
"tsup": "^8.3.0",
|
|
102
87
|
"tsx": "^4.21.0"
|
|
103
88
|
},
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
89
|
+
"scripts": {
|
|
90
|
+
"build": "tsup",
|
|
91
|
+
"test": "bash scripts/run-workspace-tests.sh",
|
|
92
|
+
"test:repo": "pnpm exec tsx --test $(find test -type f \\( -name '*.test.ts' -o -name '*.test.tsx' \\) | sort)",
|
|
93
|
+
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
94
|
+
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
95
|
+
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
96
|
+
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
97
|
+
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
98
|
+
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
99
|
+
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
100
|
+
"wave:status": "bash scripts/wave-status.sh",
|
|
101
|
+
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
102
|
+
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
103
|
+
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
104
|
+
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
113
105
|
}
|
|
114
106
|
}
|
|
@@ -9,8 +9,13 @@ import {
|
|
|
9
9
|
} from "../state/editor-state.ts";
|
|
10
10
|
import {
|
|
11
11
|
areAnchorsEqual,
|
|
12
|
+
createDetachedAnchor,
|
|
12
13
|
createEmptyMapping,
|
|
14
|
+
createNodeAnchor,
|
|
15
|
+
createRangeAnchor,
|
|
16
|
+
DEFAULT_BOUNDARY_ASSOC,
|
|
13
17
|
mapAnchor,
|
|
18
|
+
type BoundaryAssoc,
|
|
14
19
|
type EditorAnchorProjection,
|
|
15
20
|
type MappingStep,
|
|
16
21
|
type TransactionMapping,
|
|
@@ -655,20 +660,123 @@ function createTransaction(
|
|
|
655
660
|
}
|
|
656
661
|
|
|
657
662
|
function normalizeSelection(selection: SelectionSnapshot): SelectionSnapshot {
|
|
658
|
-
|
|
663
|
+
const activeRange = normalizeSelectionAnchor(
|
|
664
|
+
selection.activeRange,
|
|
665
|
+
selection.anchor,
|
|
666
|
+
selection.head,
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
if (activeRange.kind === "range") {
|
|
659
670
|
return {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
671
|
+
anchor: activeRange.range.from,
|
|
672
|
+
head: activeRange.range.to,
|
|
673
|
+
isCollapsed: activeRange.range.from === activeRange.range.to,
|
|
674
|
+
activeRange,
|
|
664
675
|
};
|
|
665
676
|
}
|
|
666
677
|
|
|
667
|
-
if (
|
|
668
|
-
return createSelectionSnapshot(
|
|
678
|
+
if (activeRange.kind === "node") {
|
|
679
|
+
return createSelectionSnapshot(activeRange.at, activeRange.at);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
anchor: activeRange.lastKnownRange.from,
|
|
684
|
+
head: activeRange.lastKnownRange.to,
|
|
685
|
+
isCollapsed: activeRange.lastKnownRange.from === activeRange.lastKnownRange.to,
|
|
686
|
+
activeRange,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function normalizeSelectionAnchor(
|
|
691
|
+
value: SelectionSnapshot["activeRange"],
|
|
692
|
+
anchor: number,
|
|
693
|
+
head: number,
|
|
694
|
+
): EditorAnchorProjection {
|
|
695
|
+
if (!value || typeof value !== "object") {
|
|
696
|
+
return createRangeAnchor(anchor, head, DEFAULT_BOUNDARY_ASSOC);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const record = value as unknown as Record<string, unknown>;
|
|
700
|
+
switch (record.kind) {
|
|
701
|
+
case "range": {
|
|
702
|
+
const assoc = normalizeBoundaryAssoc(record.assoc);
|
|
703
|
+
const internalRange = record.range;
|
|
704
|
+
if (
|
|
705
|
+
internalRange &&
|
|
706
|
+
typeof internalRange === "object" &&
|
|
707
|
+
typeof (internalRange as { from?: unknown }).from === "number" &&
|
|
708
|
+
typeof (internalRange as { to?: unknown }).to === "number"
|
|
709
|
+
) {
|
|
710
|
+
return createRangeAnchor(
|
|
711
|
+
(internalRange as { from: number }).from,
|
|
712
|
+
(internalRange as { to: number }).to,
|
|
713
|
+
assoc,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (typeof record.from === "number" && typeof record.to === "number") {
|
|
718
|
+
return createRangeAnchor(record.from, record.to, assoc);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return createRangeAnchor(anchor, head, assoc);
|
|
722
|
+
}
|
|
723
|
+
case "node":
|
|
724
|
+
return createNodeAnchor(
|
|
725
|
+
typeof record.at === "number" ? record.at : anchor,
|
|
726
|
+
normalizeAssoc(record.assoc),
|
|
727
|
+
);
|
|
728
|
+
case "detached": {
|
|
729
|
+
const lastKnownRange = record.lastKnownRange;
|
|
730
|
+
if (
|
|
731
|
+
lastKnownRange &&
|
|
732
|
+
typeof lastKnownRange === "object" &&
|
|
733
|
+
typeof (lastKnownRange as { from?: unknown }).from === "number" &&
|
|
734
|
+
typeof (lastKnownRange as { to?: unknown }).to === "number"
|
|
735
|
+
) {
|
|
736
|
+
return createDetachedAnchor(
|
|
737
|
+
{
|
|
738
|
+
from: (lastKnownRange as { from: number }).from,
|
|
739
|
+
to: (lastKnownRange as { to: number }).to,
|
|
740
|
+
},
|
|
741
|
+
normalizeDetachedReason(record.reason),
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return createDetachedAnchor({ from: anchor, to: head }, normalizeDetachedReason(record.reason));
|
|
746
|
+
}
|
|
747
|
+
default:
|
|
748
|
+
return createRangeAnchor(anchor, head, DEFAULT_BOUNDARY_ASSOC);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function normalizeBoundaryAssoc(value: unknown): BoundaryAssoc {
|
|
753
|
+
if (value && typeof value === "object") {
|
|
754
|
+
const record = value as unknown as Record<string, unknown>;
|
|
755
|
+
return {
|
|
756
|
+
start: normalizeAssoc(record.start),
|
|
757
|
+
end: normalizeAssoc(record.end),
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return DEFAULT_BOUNDARY_ASSOC;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function normalizeAssoc(value: unknown): -1 | 1 {
|
|
765
|
+
return value === -1 ? -1 : 1;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function normalizeDetachedReason(
|
|
769
|
+
value: unknown,
|
|
770
|
+
): "deleted" | "invalidatedByStructureChange" | "importAmbiguity" {
|
|
771
|
+
if (
|
|
772
|
+
value === "deleted" ||
|
|
773
|
+
value === "invalidatedByStructureChange" ||
|
|
774
|
+
value === "importAmbiguity"
|
|
775
|
+
) {
|
|
776
|
+
return value;
|
|
669
777
|
}
|
|
670
778
|
|
|
671
|
-
return
|
|
779
|
+
return "importAmbiguity";
|
|
672
780
|
}
|
|
673
781
|
|
|
674
782
|
function applyTextCommand(
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
type StoryUnit,
|
|
11
11
|
type TextStory,
|
|
12
12
|
} from "../schema/text-schema.ts";
|
|
13
|
+
import { createEditorSurfaceSnapshot } from "../../runtime/surface-projection.ts";
|
|
14
|
+
import type { DocumentRootNode, ParagraphNode, TableNode } from "../../model/canonical-document.ts";
|
|
13
15
|
|
|
14
16
|
export type TextInsertion =
|
|
15
17
|
| {
|
|
@@ -69,6 +71,27 @@ export function applyTextTransaction(
|
|
|
69
71
|
options: {
|
|
70
72
|
timestamp: string;
|
|
71
73
|
},
|
|
74
|
+
): TextTransactionResult {
|
|
75
|
+
const tableScopedResult = tryApplyTableParagraphTransaction(
|
|
76
|
+
document,
|
|
77
|
+
selection,
|
|
78
|
+
intent,
|
|
79
|
+
options,
|
|
80
|
+
);
|
|
81
|
+
if (tableScopedResult) {
|
|
82
|
+
return tableScopedResult;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return applyLinearTextTransaction(document, selection, intent, options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function applyLinearTextTransaction(
|
|
89
|
+
document: CanonicalDocumentEnvelope,
|
|
90
|
+
selection: SelectionSnapshot,
|
|
91
|
+
intent: TextTransactionIntent,
|
|
92
|
+
options: {
|
|
93
|
+
timestamp: string;
|
|
94
|
+
},
|
|
72
95
|
): TextTransactionResult {
|
|
73
96
|
const story = parseTextStory(document.content);
|
|
74
97
|
const normalizedRange = resolveRange(selection, story.size, intent);
|
|
@@ -118,6 +141,209 @@ export function applyTextTransaction(
|
|
|
118
141
|
};
|
|
119
142
|
}
|
|
120
143
|
|
|
144
|
+
function tryApplyTableParagraphTransaction(
|
|
145
|
+
document: CanonicalDocumentEnvelope,
|
|
146
|
+
selection: SelectionSnapshot,
|
|
147
|
+
intent: TextTransactionIntent,
|
|
148
|
+
options: {
|
|
149
|
+
timestamp: string;
|
|
150
|
+
},
|
|
151
|
+
): TextTransactionResult | null {
|
|
152
|
+
const scope = resolveTableParagraphScope(document, selection);
|
|
153
|
+
if (!scope) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
if (scope === "unsupported") {
|
|
157
|
+
throw new TextTransactionError(
|
|
158
|
+
"unsupported_content",
|
|
159
|
+
"Text transactions inside table structures are only supported when the selection stays within one paragraph-backed table cell.",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const localDocument: CanonicalDocumentEnvelope = {
|
|
164
|
+
...document,
|
|
165
|
+
content: {
|
|
166
|
+
type: "doc",
|
|
167
|
+
children: [scope.paragraph],
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
const localSelection = createSelectionSnapshot(
|
|
171
|
+
selection.anchor - scope.paragraphStart,
|
|
172
|
+
selection.head - scope.paragraphStart,
|
|
173
|
+
);
|
|
174
|
+
const localResult = applyLinearTextTransaction(localDocument, localSelection, intent, options);
|
|
175
|
+
const nextParagraphBlocks = (localResult.document.content as DocumentRootNode).children;
|
|
176
|
+
const nextRoot: DocumentRootNode = {
|
|
177
|
+
...scope.root,
|
|
178
|
+
children: scope.root.children.map((child, blockIndex) => {
|
|
179
|
+
if (blockIndex !== scope.tableBlockIndex) {
|
|
180
|
+
return child;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const table = child as TableNode;
|
|
184
|
+
return {
|
|
185
|
+
...table,
|
|
186
|
+
rows: table.rows.map((row, rowIndex) => {
|
|
187
|
+
if (rowIndex !== scope.rowIndex) {
|
|
188
|
+
return row;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
...row,
|
|
193
|
+
cells: row.cells.map((cell, cellIndex) => {
|
|
194
|
+
if (cellIndex !== scope.cellIndex) {
|
|
195
|
+
return cell;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
...cell,
|
|
200
|
+
children: [
|
|
201
|
+
...cell.children.slice(0, scope.childIndex),
|
|
202
|
+
...nextParagraphBlocks,
|
|
203
|
+
...cell.children.slice(scope.childIndex + 1),
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
}),
|
|
207
|
+
};
|
|
208
|
+
}),
|
|
209
|
+
};
|
|
210
|
+
}),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
document: {
|
|
215
|
+
...document,
|
|
216
|
+
updatedAt: options.timestamp,
|
|
217
|
+
content: nextRoot,
|
|
218
|
+
},
|
|
219
|
+
selection: createSelectionSnapshot(
|
|
220
|
+
localResult.selection.anchor + scope.paragraphStart,
|
|
221
|
+
localResult.selection.head + scope.paragraphStart,
|
|
222
|
+
),
|
|
223
|
+
mapping: {
|
|
224
|
+
...localResult.mapping,
|
|
225
|
+
steps: localResult.mapping.steps.map((step) => ({
|
|
226
|
+
...step,
|
|
227
|
+
from: step.from + scope.paragraphStart,
|
|
228
|
+
to: step.to + scope.paragraphStart,
|
|
229
|
+
})),
|
|
230
|
+
},
|
|
231
|
+
storyText: localResult.storyText,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolveTableParagraphScope(
|
|
236
|
+
document: CanonicalDocumentEnvelope,
|
|
237
|
+
selection: SelectionSnapshot,
|
|
238
|
+
):
|
|
239
|
+
| {
|
|
240
|
+
root: DocumentRootNode;
|
|
241
|
+
tableBlockIndex: number;
|
|
242
|
+
rowIndex: number;
|
|
243
|
+
cellIndex: number;
|
|
244
|
+
childIndex: number;
|
|
245
|
+
paragraph: ParagraphNode;
|
|
246
|
+
paragraphStart: number;
|
|
247
|
+
}
|
|
248
|
+
| "unsupported"
|
|
249
|
+
| null {
|
|
250
|
+
const root = document.content as DocumentRootNode;
|
|
251
|
+
if (!root || root.type !== "doc" || !Array.isArray(root.children)) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const surface = createEditorSurfaceSnapshot(document, selection);
|
|
256
|
+
const selectionFrom = Math.min(selection.anchor, selection.head);
|
|
257
|
+
const selectionTo = Math.max(selection.anchor, selection.head);
|
|
258
|
+
|
|
259
|
+
for (let blockIndex = 0; blockIndex < root.children.length; blockIndex += 1) {
|
|
260
|
+
const block = root.children[blockIndex];
|
|
261
|
+
const surfaceBlock = surface.blocks[blockIndex];
|
|
262
|
+
if (block?.type !== "table" || surfaceBlock?.kind !== "table") {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const insideTable = selectionFallsWithinSurfaceRange(
|
|
267
|
+
selectionFrom,
|
|
268
|
+
selectionTo,
|
|
269
|
+
surfaceBlock.from,
|
|
270
|
+
surfaceBlock.to,
|
|
271
|
+
);
|
|
272
|
+
if (!insideTable) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (let rowIndex = 0; rowIndex < block.rows.length; rowIndex += 1) {
|
|
277
|
+
const row = block.rows[rowIndex];
|
|
278
|
+
const surfaceRow = surfaceBlock.rows[rowIndex];
|
|
279
|
+
if (!row || !surfaceRow) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (let cellIndex = 0; cellIndex < row.cells.length; cellIndex += 1) {
|
|
284
|
+
const cell = row.cells[cellIndex];
|
|
285
|
+
const surfaceCell = surfaceRow.cells[cellIndex];
|
|
286
|
+
if (!cell || !surfaceCell) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
for (
|
|
291
|
+
let childIndex = 0;
|
|
292
|
+
childIndex < Math.min(cell.children.length, surfaceCell.content.length);
|
|
293
|
+
childIndex += 1
|
|
294
|
+
) {
|
|
295
|
+
const child = cell.children[childIndex];
|
|
296
|
+
const surfaceChild = surfaceCell.content[childIndex];
|
|
297
|
+
if (child?.type !== "paragraph" || surfaceChild?.kind !== "paragraph") {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const insideParagraph = selectionFallsWithinSurfaceRange(
|
|
302
|
+
selectionFrom,
|
|
303
|
+
selectionTo,
|
|
304
|
+
surfaceChild.from,
|
|
305
|
+
surfaceChild.to,
|
|
306
|
+
);
|
|
307
|
+
if (!insideParagraph) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
root,
|
|
313
|
+
tableBlockIndex: blockIndex,
|
|
314
|
+
rowIndex,
|
|
315
|
+
cellIndex,
|
|
316
|
+
childIndex,
|
|
317
|
+
paragraph: child,
|
|
318
|
+
paragraphStart: surfaceChild.from,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return "unsupported";
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function selectionFallsWithinSurfaceRange(
|
|
331
|
+
selectionFrom: number,
|
|
332
|
+
selectionTo: number,
|
|
333
|
+
rangeFrom: number,
|
|
334
|
+
rangeTo: number,
|
|
335
|
+
): boolean {
|
|
336
|
+
if (rangeFrom === rangeTo) {
|
|
337
|
+
return selectionFrom === rangeFrom && selectionTo === rangeTo;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
selectionFrom >= rangeFrom &&
|
|
342
|
+
selectionTo <= rangeTo &&
|
|
343
|
+
selectionFrom < rangeTo
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
121
347
|
function resolveRange(
|
|
122
348
|
selection: SelectionSnapshot,
|
|
123
349
|
storySize: number,
|
|
@@ -302,5 +302,5 @@ function convertCachedFormulaValue(
|
|
|
302
302
|
// ---------------------------------------------------------------------------
|
|
303
303
|
|
|
304
304
|
function decodePartBytes(bytes: Uint8Array): string {
|
|
305
|
-
return
|
|
305
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
306
306
|
}
|
package/src/io/docx-session.ts
CHANGED
|
@@ -1733,7 +1733,7 @@ function decodeUtf8(bytes: Uint8Array | undefined): string {
|
|
|
1733
1733
|
return "";
|
|
1734
1734
|
}
|
|
1735
1735
|
|
|
1736
|
-
return
|
|
1736
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
1737
1737
|
}
|
|
1738
1738
|
|
|
1739
1739
|
function toUint8Array(bytes: Uint8Array | ArrayBuffer): Uint8Array {
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ParsedTableBorders,
|
|
7
7
|
ParsedTableCell,
|
|
8
8
|
ParsedTableCellBorders,
|
|
9
|
+
ParsedTableLook,
|
|
9
10
|
ParsedTableRow,
|
|
10
11
|
ParsedTableWidth,
|
|
11
12
|
} from "../ooxml/parse-tables.ts";
|
|
@@ -36,6 +37,9 @@ function serializeCell(cell: ParsedTableCell): string {
|
|
|
36
37
|
|
|
37
38
|
function buildTablePropertiesXml(table: ParsedTable): string {
|
|
38
39
|
const children: string[] = [];
|
|
40
|
+
if (table.styleId) {
|
|
41
|
+
children.push(`<w:tblStyle w:val="${table.styleId}"/>`);
|
|
42
|
+
}
|
|
39
43
|
if (table.width) {
|
|
40
44
|
children.push(serializeWidth("tblW", table.width));
|
|
41
45
|
}
|
|
@@ -50,6 +54,10 @@ function buildTablePropertiesXml(table: ParsedTable): string {
|
|
|
50
54
|
const marginsXml = serializeTableCellMargins(table.cellMargins);
|
|
51
55
|
if (marginsXml) children.push(`<w:tblCellMar>${marginsXml}</w:tblCellMar>`);
|
|
52
56
|
}
|
|
57
|
+
if (table.tblLook) {
|
|
58
|
+
const tblLookXml = serializeTableLook(table.tblLook);
|
|
59
|
+
if (tblLookXml) children.push(tblLookXml);
|
|
60
|
+
}
|
|
53
61
|
return children.length > 0 ? `<w:tblPr>${children.join("")}</w:tblPr>` : "";
|
|
54
62
|
}
|
|
55
63
|
|
|
@@ -128,6 +136,25 @@ function serializeCellBorders(borders: ParsedTableCellBorders): string {
|
|
|
128
136
|
.join("");
|
|
129
137
|
}
|
|
130
138
|
|
|
139
|
+
function serializeTableLook(tblLook: ParsedTableLook): string {
|
|
140
|
+
const attrs: string[] = [];
|
|
141
|
+
if (tblLook.val) attrs.push(`w:val="${tblLook.val}"`);
|
|
142
|
+
for (const [key, attr] of [
|
|
143
|
+
["firstRow", "w:firstRow"],
|
|
144
|
+
["lastRow", "w:lastRow"],
|
|
145
|
+
["firstColumn", "w:firstColumn"],
|
|
146
|
+
["lastColumn", "w:lastColumn"],
|
|
147
|
+
["noHBand", "w:noHBand"],
|
|
148
|
+
["noVBand", "w:noVBand"],
|
|
149
|
+
] as const) {
|
|
150
|
+
const value = tblLook[key];
|
|
151
|
+
if (value !== undefined) {
|
|
152
|
+
attrs.push(`${attr}="${value ? "1" : "0"}"`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return attrs.length > 0 ? `<w:tblLook ${attrs.join(" ")}/>` : "";
|
|
156
|
+
}
|
|
157
|
+
|
|
131
158
|
function serializeCellShading(shading: ParsedCellShading): string {
|
|
132
159
|
const attrs: string[] = [];
|
|
133
160
|
if (shading.val) attrs.push(`w:val="${shading.val}"`);
|