@beyondwork/docx-react-component 1.0.47 → 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 +199 -13
- package/src/compare/diff-engine.ts +4 -0
- package/src/core/commands/add-scope.ts +257 -0
- package/src/core/commands/formatting-commands.ts +2 -0
- package/src/core/commands/index.ts +9 -1
- package/src/core/commands/text-commands.ts +3 -1
- package/src/core/schema/text-schema.ts +95 -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 +103 -7
- package/src/internal/harness-debug-ports.ts +168 -0
- package/src/io/chart-preview-resolver.ts +59 -1
- package/src/io/docx-session.ts +226 -38
- package/src/io/export/serialize-main-document.ts +46 -0
- package/src/io/export/serialize-paragraph-formatting.ts +8 -0
- package/src/io/export/serialize-run-formatting.ts +10 -1
- package/src/io/export/serialize-settings.ts +421 -0
- package/src/io/export/serialize-styles.ts +10 -0
- package/src/io/normalize/normalize-text.ts +1 -0
- 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-axis.ts +277 -0
- package/src/io/ooxml/chart/parse-chart-space.ts +885 -0
- package/src/io/ooxml/chart/parse-series.ts +635 -0
- package/src/io/ooxml/chart/resolve-color.ts +261 -0
- package/src/io/ooxml/chart/types.ts +439 -0
- package/src/io/ooxml/parse-block-structure.ts +99 -0
- package/src/io/ooxml/parse-complex-content.ts +90 -2
- package/src/io/ooxml/parse-main-document.ts +156 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +46 -0
- package/src/io/ooxml/parse-run-formatting.ts +49 -0
- package/src/io/ooxml/parse-scope-markers.ts +184 -0
- package/src/io/ooxml/parse-settings-blueprint.ts +349 -0
- package/src/io/ooxml/parse-settings.ts +97 -1
- package/src/io/ooxml/parse-styles.ts +65 -0
- package/src/io/ooxml/parse-theme.ts +2 -127
- package/src/io/ooxml/property-grab-bag.ts +211 -0
- package/src/io/ooxml/xml-attr-helpers.ts +59 -1
- package/src/io/ooxml/xml-parser.ts +142 -0
- package/src/model/canonical-document.ts +160 -0
- package/src/model/scope-markers.ts +144 -0
- package/src/runtime/collab/base-doc-fingerprint.ts +99 -0
- package/src/runtime/collab/checkpoint-election.ts +75 -0
- package/src/runtime/collab/checkpoint-scheduler.ts +204 -0
- package/src/runtime/collab/checkpoint-store.ts +115 -0
- package/src/runtime/collab/event-types.ts +27 -0
- package/src/runtime/collab/index.ts +29 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +167 -0
- package/src/runtime/collab/runtime-collab-sync.ts +330 -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 +288 -65
- package/src/runtime/editor-surface/capabilities.ts +63 -50
- package/src/runtime/hyperlink-color-resolver.ts +119 -0
- package/src/runtime/layout/layout-engine-version.ts +8 -1
- package/src/runtime/prerender/cache-envelope.ts +19 -7
- package/src/runtime/prerender/cache-key.ts +25 -14
- package/src/runtime/prerender/canonical-document-hash.ts +63 -0
- package/src/runtime/prerender/customxml-cache.ts +211 -0
- package/src/runtime/prerender/customxml-probe.ts +78 -0
- package/src/runtime/prerender/prerender-document.ts +74 -7
- package/src/runtime/scope-resolver.ts +148 -0
- package/src/runtime/scope-tag-registry.ts +10 -0
- package/src/runtime/surface-projection.ts +102 -37
- package/src/runtime/theme-color-resolver.ts +188 -0
- package/src/runtime/workflow-markup.ts +7 -18
- package/src/ui/WordReviewEditor.tsx +48 -2
- package/src/ui/editor-runtime-boundary.ts +42 -1
- package/src/ui/headless/selection-helpers.ts +10 -23
- package/src/ui/runtime-shortcut-dispatch.ts +12 -7
- 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
|
@@ -3,7 +3,9 @@ import type { TransactionMapping } from "../selection/mapping.ts";
|
|
|
3
3
|
import {
|
|
4
4
|
cloneParagraphProperties,
|
|
5
5
|
cloneStoryUnit,
|
|
6
|
+
countLogicalPositions,
|
|
6
7
|
createPlainText,
|
|
8
|
+
logicalPositionToUnitIndex,
|
|
7
9
|
parseTextStory,
|
|
8
10
|
serializeTextStory,
|
|
9
11
|
type ParagraphProperties,
|
|
@@ -11,7 +13,8 @@ import {
|
|
|
11
13
|
type TextStory,
|
|
12
14
|
} from "../schema/text-schema.ts";
|
|
13
15
|
import { createEditorSurfaceSnapshot } from "../../runtime/surface-projection.ts";
|
|
14
|
-
import type { DocumentRootNode, ParagraphNode, SdtNode, TableNode } from "../../model/canonical-document.ts";
|
|
16
|
+
import type { DocumentRootNode, ParagraphNode, SdtNode, TableNode, TextMark } from "../../model/canonical-document.ts";
|
|
17
|
+
import type { TextFormattingDirective } from "../../api/public-types.ts";
|
|
15
18
|
|
|
16
19
|
export type TextInsertion =
|
|
17
20
|
| {
|
|
@@ -36,6 +39,13 @@ export type TextTransactionIntent =
|
|
|
36
39
|
to: number;
|
|
37
40
|
};
|
|
38
41
|
insertion: TextInsertion[];
|
|
42
|
+
/**
|
|
43
|
+
* I7 — optional formatting directive governing which `marks` the inserted text
|
|
44
|
+
* units carry. Resolved against `story` + `normalizedRange` inside
|
|
45
|
+
* `applyLinearTextTransaction`. `undefined` (or `{ mode: "paragraph-default" }`)
|
|
46
|
+
* preserves pre-I7 behavior (no inherited run marks).
|
|
47
|
+
*/
|
|
48
|
+
formatting?: TextFormattingDirective;
|
|
39
49
|
}
|
|
40
50
|
| {
|
|
41
51
|
type: "delete_backward";
|
|
@@ -115,14 +125,21 @@ function applyLinearTextTransaction(
|
|
|
115
125
|
): TextTransactionResult {
|
|
116
126
|
const story = parseTextStory(document.content);
|
|
117
127
|
const normalizedRange = resolveRange(selection, story.size, intent);
|
|
118
|
-
const
|
|
128
|
+
const resolvedMarks = resolveMarksForInsertion(intent, story, normalizedRange);
|
|
129
|
+
const insertionUnits = createInsertionUnits(intent, story, normalizedRange.from, resolvedMarks);
|
|
119
130
|
|
|
120
|
-
|
|
131
|
+
// `normalizedRange.{from,to}` are logical positions (scope markers = 0 width,
|
|
132
|
+
// matching surface-projection). Translate to unit-array indices so scope
|
|
133
|
+
// marker units preserved at the boundary stay intact on either side.
|
|
134
|
+
const unitFrom = logicalPositionToUnitIndex(story.units, normalizedRange.from, "before");
|
|
135
|
+
const unitTo = logicalPositionToUnitIndex(story.units, normalizedRange.to, "after");
|
|
136
|
+
|
|
137
|
+
ensureEditableRange(story.units.slice(unitFrom, unitTo));
|
|
121
138
|
|
|
122
139
|
const nextUnits = [
|
|
123
|
-
...story.units.slice(0,
|
|
140
|
+
...story.units.slice(0, unitFrom).map(cloneStoryUnit),
|
|
124
141
|
...insertionUnits.map(cloneStoryUnit),
|
|
125
|
-
...story.units.slice(
|
|
142
|
+
...story.units.slice(unitTo).map(cloneStoryUnit),
|
|
126
143
|
];
|
|
127
144
|
|
|
128
145
|
const nextStory: TextStory = {
|
|
@@ -130,9 +147,13 @@ function applyLinearTextTransaction(
|
|
|
130
147
|
units: normalizeStoryUnits(nextUnits),
|
|
131
148
|
size: 0,
|
|
132
149
|
};
|
|
133
|
-
nextStory.size = nextStory.units
|
|
150
|
+
nextStory.size = countLogicalPositions(nextStory.units);
|
|
134
151
|
|
|
135
|
-
|
|
152
|
+
// `normalizedRange.from` is the logical insertion point; count the logical
|
|
153
|
+
// positions added by `insertionUnits` (skipping any scope markers) to derive
|
|
154
|
+
// the post-insert caret.
|
|
155
|
+
const logicalInsertionSize = countLogicalPositions(insertionUnits);
|
|
156
|
+
const caret = normalizedRange.from + logicalInsertionSize;
|
|
136
157
|
|
|
137
158
|
return {
|
|
138
159
|
document: {
|
|
@@ -767,6 +788,7 @@ function createInsertionUnits(
|
|
|
767
788
|
intent: TextTransactionIntent,
|
|
768
789
|
story: TextStory,
|
|
769
790
|
position: number,
|
|
791
|
+
marks: TextMark[] | undefined,
|
|
770
792
|
): StoryUnit[] {
|
|
771
793
|
if (intent.type !== "replace") {
|
|
772
794
|
return [];
|
|
@@ -780,6 +802,7 @@ function createInsertionUnits(
|
|
|
780
802
|
return Array.from(entry.text).map<StoryUnit>((character) => ({
|
|
781
803
|
kind: "text",
|
|
782
804
|
value: character,
|
|
805
|
+
...(marks && marks.length > 0 ? { marks: marks.map((mark) => ({ ...mark })) } : {}),
|
|
783
806
|
}));
|
|
784
807
|
case "tab":
|
|
785
808
|
return [{ kind: "tab" }];
|
|
@@ -796,6 +819,79 @@ function createInsertionUnits(
|
|
|
796
819
|
});
|
|
797
820
|
}
|
|
798
821
|
|
|
822
|
+
/**
|
|
823
|
+
* I7 — given a formatting directive, produce the `marks` array that inserted text
|
|
824
|
+
* units should carry. Returns `undefined` for the paragraph-default path (no marks).
|
|
825
|
+
*
|
|
826
|
+
* - `paragraph-default` / no directive → `undefined`.
|
|
827
|
+
* - `explicit` → caller-supplied marks verbatim.
|
|
828
|
+
* - `match-replaced-range`:
|
|
829
|
+
* - If the range is collapsed, use the marks of the text unit immediately left of
|
|
830
|
+
* the caret (Word-matching behavior for empty-range inserts).
|
|
831
|
+
* - Otherwise walk text units in `[from, to)`. If every text unit shares the same
|
|
832
|
+
* marks (by type), use them. Mixed → fall back to paragraph-default (`undefined`).
|
|
833
|
+
*/
|
|
834
|
+
function resolveMarksForInsertion(
|
|
835
|
+
intent: TextTransactionIntent,
|
|
836
|
+
story: TextStory,
|
|
837
|
+
range: { from: number; to: number },
|
|
838
|
+
): TextMark[] | undefined {
|
|
839
|
+
if (intent.type !== "replace") {
|
|
840
|
+
return undefined;
|
|
841
|
+
}
|
|
842
|
+
const directive = intent.formatting;
|
|
843
|
+
if (!directive || directive.mode === "paragraph-default") {
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
if (directive.mode === "explicit") {
|
|
847
|
+
return directive.marks.map((mark) => ({ ...mark }));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// match-replaced-range
|
|
851
|
+
const unitFrom = logicalPositionToUnitIndex(story.units, range.from, "before");
|
|
852
|
+
const unitTo = logicalPositionToUnitIndex(story.units, range.to, "after");
|
|
853
|
+
|
|
854
|
+
if (range.from === range.to) {
|
|
855
|
+
// Empty range — inherit from the text unit immediately left of the caret.
|
|
856
|
+
for (let i = unitFrom - 1; i >= 0; i -= 1) {
|
|
857
|
+
const unit = story.units[i];
|
|
858
|
+
if (unit?.kind === "text") {
|
|
859
|
+
return unit.marks ? unit.marks.map((mark) => ({ ...mark })) : undefined;
|
|
860
|
+
}
|
|
861
|
+
if (unit?.kind === "paragraph_break") {
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return undefined;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const textUnits: Array<{ marks?: TextMark[] }> = [];
|
|
869
|
+
for (let i = unitFrom; i < unitTo; i += 1) {
|
|
870
|
+
const unit = story.units[i];
|
|
871
|
+
if (unit?.kind === "text") {
|
|
872
|
+
textUnits.push(unit);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (textUnits.length === 0) {
|
|
876
|
+
return undefined;
|
|
877
|
+
}
|
|
878
|
+
const firstMarks = textUnits[0].marks;
|
|
879
|
+
const uniform = textUnits.every((unit) => marksAreEqual(firstMarks, unit.marks));
|
|
880
|
+
if (!uniform) {
|
|
881
|
+
return undefined;
|
|
882
|
+
}
|
|
883
|
+
return firstMarks ? firstMarks.map((mark) => ({ ...mark })) : undefined;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function marksAreEqual(left: TextMark[] | undefined, right: TextMark[] | undefined): boolean {
|
|
887
|
+
if (!left && !right) return true;
|
|
888
|
+
if (!left || !right) return false;
|
|
889
|
+
if (left.length !== right.length) return false;
|
|
890
|
+
const sortedLeft = [...left].map((mark) => mark.type).sort();
|
|
891
|
+
const sortedRight = [...right].map((mark) => mark.type).sort();
|
|
892
|
+
return sortedLeft.every((type, index) => type === sortedRight[index]);
|
|
893
|
+
}
|
|
894
|
+
|
|
799
895
|
function resolveParagraphPropertiesAtPosition(
|
|
800
896
|
story: TextStory,
|
|
801
897
|
position: number,
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal HARNESS-ONLY debug-ports token module.
|
|
3
|
+
*
|
|
4
|
+
* This module is **not listed in `package.json#exports`**, so
|
|
5
|
+
* downstream consumers of `@beyondwork/docx-react-component` cannot
|
|
6
|
+
* import it via any public entry point. The in-repo harness at
|
|
7
|
+
* `services/react-word-editor/` reaches it via a relative path
|
|
8
|
+
* (`../../../src/internal/harness-debug-ports`); that is the only
|
|
9
|
+
* supported caller.
|
|
10
|
+
*
|
|
11
|
+
* ## Why this exists
|
|
12
|
+
*
|
|
13
|
+
* It replaces the former public `showUnsupportedObjectPreviews?: boolean`
|
|
14
|
+
* prop on `WordReviewEditorProps`, which regressed to `true` three
|
|
15
|
+
* times through merges (PRs #124, #131, #160). Each regression leaked
|
|
16
|
+
* preserve-only preview chrome (charts, SmartArt, shapes, WordArt,
|
|
17
|
+
* VML, "N preserve-only features detected" banner, lock-callouts)
|
|
18
|
+
* into consumer applications.
|
|
19
|
+
*
|
|
20
|
+
* ## What the new shape buys us
|
|
21
|
+
*
|
|
22
|
+
* 1. **Type-level**: the `HarnessDebugPorts` brand uses a `unique
|
|
23
|
+
* symbol` key that cannot be obtained outside this file, so
|
|
24
|
+
* downstream TypeScript consumers cannot structurally construct a
|
|
25
|
+
* valid token.
|
|
26
|
+
* 2. **Module-level**: this file is not in the package's export map.
|
|
27
|
+
* External consumers would have to reach into the installed
|
|
28
|
+
* package's internals via a non-guaranteed path to find it.
|
|
29
|
+
* 3. **Runtime gate**: even if a caller does manage to invoke
|
|
30
|
+
* `__createHarnessDebugPorts`, the factory checks for a harness
|
|
31
|
+
* environment marker (`globalThis.__DOCX_REACT_COMPONENT_HARNESS__`)
|
|
32
|
+
* that only the harness sets via `__markHarnessEnvironment()`.
|
|
33
|
+
* In a consumer app the flag is false; the returned token's
|
|
34
|
+
* permissions are all `false` regardless of input.
|
|
35
|
+
* 4. **Intent assertion**: the factory input requires a literal
|
|
36
|
+
* `confirmHarnessAuthor: "I-am-the-harness-dev-drawer"` string as
|
|
37
|
+
* a self-documenting "I know what I'm doing" gate.
|
|
38
|
+
*
|
|
39
|
+
* Flipping any of (1)–(4) is the regression this module is designed
|
|
40
|
+
* to prevent. The invariant test at
|
|
41
|
+
* `test/ui/unsupported-previews-invariant.test.ts` locks them all.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
// NOT exported. The symbol's identity is what makes the brand
|
|
45
|
+
// unobtainable from outside this module: `typeof` on a const symbol
|
|
46
|
+
// gives TypeScript a `unique symbol` type that's only satisfiable
|
|
47
|
+
// by the same `const` binding. Consumers that don't import this
|
|
48
|
+
// module cannot satisfy the `[harnessDebugPortsBrand]: true` field.
|
|
49
|
+
const harnessDebugPortsBrand = Symbol("harnessDebugPortsBrand");
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Opaque, brand-typed token passed through the internal
|
|
53
|
+
* `__harnessDebugPorts` editor prop. Cannot be constructed
|
|
54
|
+
* structurally — the brand key is a module-local `unique symbol`.
|
|
55
|
+
*
|
|
56
|
+
* @internal HARNESS-ONLY.
|
|
57
|
+
*/
|
|
58
|
+
export interface HarnessDebugPorts {
|
|
59
|
+
readonly [harnessDebugPortsBrand]: true;
|
|
60
|
+
/**
|
|
61
|
+
* Sanitized. Only `true` when the token factory confirmed
|
|
62
|
+
* harness-env + debug-mode + literal author assertion.
|
|
63
|
+
*/
|
|
64
|
+
readonly unsupportedObjectPreviews: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const HARNESS_GLOBAL_KEY = "__DOCX_REACT_COMPONENT_HARNESS__" as const;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Input to the harness-debug-ports token factory.
|
|
71
|
+
*
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
export interface HarnessDebugPortsInput {
|
|
75
|
+
/**
|
|
76
|
+
* Self-documenting author gate. Must be the literal string
|
|
77
|
+
* `"I-am-the-harness-dev-drawer"`. Any other value fails the
|
|
78
|
+
* runtime check and the token's flags are all `false`.
|
|
79
|
+
*/
|
|
80
|
+
readonly confirmHarnessAuthor: "I-am-the-harness-dev-drawer";
|
|
81
|
+
/**
|
|
82
|
+
* Harness dev-drawer debug-mode toggle. Even if `true`, the factory
|
|
83
|
+
* refuses to honor the request unless the harness environment is
|
|
84
|
+
* active (`__markHarnessEnvironment()` was called).
|
|
85
|
+
*/
|
|
86
|
+
readonly debugMode: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Requested preview-rendering permission. Honored only when all
|
|
89
|
+
* runtime checks pass.
|
|
90
|
+
*/
|
|
91
|
+
readonly unsupportedObjectPreviews: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build a harness-debug-ports token.
|
|
96
|
+
*
|
|
97
|
+
* Runtime gate: the returned token's `unsupportedObjectPreviews`
|
|
98
|
+
* field is `true` **only** when all of the following hold:
|
|
99
|
+
*
|
|
100
|
+
* - `__markHarnessEnvironment()` was called previously (sets the
|
|
101
|
+
* `__DOCX_REACT_COMPONENT_HARNESS__` global to `true`);
|
|
102
|
+
* - `input.confirmHarnessAuthor === "I-am-the-harness-dev-drawer"`;
|
|
103
|
+
* - `input.debugMode === true`;
|
|
104
|
+
* - `input.unsupportedObjectPreviews === true`.
|
|
105
|
+
*
|
|
106
|
+
* Any check failure produces a fully-disabled token (same brand, all
|
|
107
|
+
* flags `false`). The editor component reads the token's flag
|
|
108
|
+
* through `readHarnessDebugPortsFlag()` so downstream rendering code
|
|
109
|
+
* sees a plain `boolean`.
|
|
110
|
+
*
|
|
111
|
+
* @internal HARNESS-ONLY.
|
|
112
|
+
*/
|
|
113
|
+
export function __createHarnessDebugPorts(
|
|
114
|
+
input: HarnessDebugPortsInput,
|
|
115
|
+
): HarnessDebugPorts {
|
|
116
|
+
const harnessActive =
|
|
117
|
+
typeof globalThis !== "undefined" &&
|
|
118
|
+
(globalThis as Record<string, unknown>)[HARNESS_GLOBAL_KEY] === true;
|
|
119
|
+
const authorConfirmed =
|
|
120
|
+
input.confirmHarnessAuthor === "I-am-the-harness-dev-drawer";
|
|
121
|
+
const allow =
|
|
122
|
+
harnessActive && authorConfirmed && input.debugMode === true;
|
|
123
|
+
return Object.freeze({
|
|
124
|
+
[harnessDebugPortsBrand]: true as const,
|
|
125
|
+
unsupportedObjectPreviews:
|
|
126
|
+
allow && input.unsupportedObjectPreviews === true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Mark the current JavaScript realm as a harness. MUST be called
|
|
132
|
+
* once, synchronously, before the first editor render so the token
|
|
133
|
+
* factory can confirm the environment.
|
|
134
|
+
*
|
|
135
|
+
* @internal HARNESS-ONLY.
|
|
136
|
+
*/
|
|
137
|
+
export function __markHarnessEnvironment(): void {
|
|
138
|
+
if (typeof globalThis !== "undefined") {
|
|
139
|
+
(globalThis as Record<string, unknown>)[HARNESS_GLOBAL_KEY] = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Read a sanitized flag from a token. Always returns `false` for
|
|
145
|
+
* `undefined` tokens (consumer code paths).
|
|
146
|
+
*
|
|
147
|
+
* @internal
|
|
148
|
+
*/
|
|
149
|
+
export function readHarnessDebugPortsFlag(
|
|
150
|
+
token: HarnessDebugPorts | undefined,
|
|
151
|
+
flag: "unsupportedObjectPreviews",
|
|
152
|
+
): boolean {
|
|
153
|
+
if (!token) return false;
|
|
154
|
+
return token[flag] === true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Test-only: reset the harness environment flag. Exported so the
|
|
159
|
+
* invariant/factory tests can assert the "consumer app" path
|
|
160
|
+
* (factory called without `__markHarnessEnvironment`).
|
|
161
|
+
*
|
|
162
|
+
* @internal TEST-ONLY.
|
|
163
|
+
*/
|
|
164
|
+
export function __resetHarnessEnvironmentForTests(): void {
|
|
165
|
+
if (typeof globalThis !== "undefined") {
|
|
166
|
+
delete (globalThis as Record<string, unknown>)[HARNESS_GLOBAL_KEY];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
InlineNode,
|
|
25
25
|
MediaItem,
|
|
26
26
|
ParagraphNode,
|
|
27
|
+
ResolvedTheme,
|
|
27
28
|
} from "../model/canonical-document.ts";
|
|
28
29
|
import type {
|
|
29
30
|
ChartPreviewResolveParams,
|
|
@@ -31,11 +32,34 @@ import type {
|
|
|
31
32
|
} from "../api/public-types.ts";
|
|
32
33
|
import type { OpcPackage } from "./opc/package-reader.ts";
|
|
33
34
|
import { normalizePartPath, resolveRelationshipTarget } from "./ooxml/part-manifest.ts";
|
|
35
|
+
import { parseThemeXml, resolveTheme } from "./ooxml/parse-theme.ts";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse and resolve the workbook theme once, tolerating malformed XML.
|
|
39
|
+
* An unparseable theme must NOT abort the import — fall back to
|
|
40
|
+
* undefined so downstream renderers use the fallback palette.
|
|
41
|
+
*/
|
|
42
|
+
function tryParseTheme(themeXml: string): ResolvedTheme | undefined {
|
|
43
|
+
try {
|
|
44
|
+
return resolveTheme(parseThemeXml(themeXml));
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
34
49
|
|
|
35
50
|
interface ResolveContext {
|
|
36
51
|
readonly package: OpcPackage;
|
|
37
52
|
readonly adapter: EditorHostAdapter;
|
|
38
53
|
readonly themeXml: string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Once-per-import cache of the workbook theme. Stage 2 wiring:
|
|
56
|
+
* in-tree renderers consume `parsedTheme` via
|
|
57
|
+
* `composeSeriesColor(model, parsedTheme, seriesIdx)` to resolve a
|
|
58
|
+
* series palette entry into a concrete sRGB string without re-parsing
|
|
59
|
+
* theme XML on every render. Undefined when the document has no
|
|
60
|
+
* theme1.xml or when parseThemeXml threw.
|
|
61
|
+
*/
|
|
62
|
+
readonly parsedTheme: ResolvedTheme | undefined;
|
|
39
63
|
/** Monotonic counter so we can generate unique media ids across one run. */
|
|
40
64
|
seq: number;
|
|
41
65
|
}
|
|
@@ -63,8 +87,15 @@ export async function resolveChartPreviewsForDocument(
|
|
|
63
87
|
if (pending.length === 0) return doc;
|
|
64
88
|
|
|
65
89
|
const themeXml = extractPartTextFromPackage(pkg, "/word/theme/theme1.xml");
|
|
90
|
+
const parsedTheme = themeXml ? tryParseTheme(themeXml) : undefined;
|
|
66
91
|
const renderer = adapter.renderChartPreview;
|
|
67
|
-
const ctx: ResolveContext = {
|
|
92
|
+
const ctx: ResolveContext = {
|
|
93
|
+
package: pkg,
|
|
94
|
+
adapter,
|
|
95
|
+
themeXml,
|
|
96
|
+
parsedTheme,
|
|
97
|
+
seq: 0,
|
|
98
|
+
};
|
|
68
99
|
|
|
69
100
|
const resolutions = await Promise.all(
|
|
70
101
|
pending.map(async (entry) => {
|
|
@@ -214,6 +245,33 @@ function extractPartTextFromPackage(pkg: OpcPackage, path: string): string | und
|
|
|
214
245
|
}
|
|
215
246
|
}
|
|
216
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Build a chart-part lookup callback suitable for
|
|
250
|
+
* `parseMainDocumentXml(..., chartPartLookup)`.
|
|
251
|
+
*
|
|
252
|
+
* The callback is called synchronously during parsing with a chart
|
|
253
|
+
* relationship id (the `r:id` on a `<c:chart>` reference). It resolves
|
|
254
|
+
* the id to a chart-part target path via the document's relationship
|
|
255
|
+
* table, then decodes the matching package part's bytes as UTF-8. Unknown
|
|
256
|
+
* ids and missing parts return undefined, in which case the parser
|
|
257
|
+
* proceeds without a typed `ChartModel` (the drawing still produces a
|
|
258
|
+
* `ChartPreviewNode` with `rawXml`).
|
|
259
|
+
*/
|
|
260
|
+
export function createChartPartLookup(
|
|
261
|
+
pkg: OpcPackage,
|
|
262
|
+
documentPartPath: string,
|
|
263
|
+
documentRelationships: readonly import("./ooxml/part-manifest.ts").OpcRelationship[],
|
|
264
|
+
): (rId: string) => string | undefined {
|
|
265
|
+
const relById = new Map(documentRelationships.map((r) => [r.id, r]));
|
|
266
|
+
return (rId: string): string | undefined => {
|
|
267
|
+
const rel = relById.get(rId);
|
|
268
|
+
if (!rel) return undefined;
|
|
269
|
+
const target = resolveRelationshipTarget(documentPartPath, rel);
|
|
270
|
+
if (!target) return undefined;
|
|
271
|
+
return extractPartTextFromPackage(pkg, normalizePartPath(target));
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
217
275
|
/**
|
|
218
276
|
* Produce a new CanonicalDocument with the resolved chart_preview
|
|
219
277
|
* nodes carrying previewMediaId + corresponding MediaCatalog entries.
|