@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
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
type TextStory,
|
|
14
14
|
} from "../schema/text-schema.ts";
|
|
15
15
|
import { createEditorSurfaceSnapshot } from "../../runtime/surface-projection.ts";
|
|
16
|
-
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";
|
|
17
18
|
|
|
18
19
|
export type TextInsertion =
|
|
19
20
|
| {
|
|
@@ -38,6 +39,13 @@ export type TextTransactionIntent =
|
|
|
38
39
|
to: number;
|
|
39
40
|
};
|
|
40
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;
|
|
41
49
|
}
|
|
42
50
|
| {
|
|
43
51
|
type: "delete_backward";
|
|
@@ -117,7 +125,8 @@ function applyLinearTextTransaction(
|
|
|
117
125
|
): TextTransactionResult {
|
|
118
126
|
const story = parseTextStory(document.content);
|
|
119
127
|
const normalizedRange = resolveRange(selection, story.size, intent);
|
|
120
|
-
const
|
|
128
|
+
const resolvedMarks = resolveMarksForInsertion(intent, story, normalizedRange);
|
|
129
|
+
const insertionUnits = createInsertionUnits(intent, story, normalizedRange.from, resolvedMarks);
|
|
121
130
|
|
|
122
131
|
// `normalizedRange.{from,to}` are logical positions (scope markers = 0 width,
|
|
123
132
|
// matching surface-projection). Translate to unit-array indices so scope
|
|
@@ -779,6 +788,7 @@ function createInsertionUnits(
|
|
|
779
788
|
intent: TextTransactionIntent,
|
|
780
789
|
story: TextStory,
|
|
781
790
|
position: number,
|
|
791
|
+
marks: TextMark[] | undefined,
|
|
782
792
|
): StoryUnit[] {
|
|
783
793
|
if (intent.type !== "replace") {
|
|
784
794
|
return [];
|
|
@@ -792,6 +802,7 @@ function createInsertionUnits(
|
|
|
792
802
|
return Array.from(entry.text).map<StoryUnit>((character) => ({
|
|
793
803
|
kind: "text",
|
|
794
804
|
value: character,
|
|
805
|
+
...(marks && marks.length > 0 ? { marks: marks.map((mark) => ({ ...mark })) } : {}),
|
|
795
806
|
}));
|
|
796
807
|
case "tab":
|
|
797
808
|
return [{ kind: "tab" }];
|
|
@@ -808,6 +819,79 @@ function createInsertionUnits(
|
|
|
808
819
|
});
|
|
809
820
|
}
|
|
810
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
|
+
|
|
811
895
|
function resolveParagraphPropertiesAtPosition(
|
|
812
896
|
story: TextStory,
|
|
813
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) => {
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from "./table-properties-xml.ts";
|
|
27
27
|
import { twip } from "./twip.ts";
|
|
28
28
|
import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
|
|
29
|
+
import { emitPropertyGrabBag } from "../ooxml/property-grab-bag.ts";
|
|
29
30
|
|
|
30
31
|
const HYPERLINK_RELATIONSHIP_TYPE =
|
|
31
32
|
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
|
|
@@ -1607,6 +1608,14 @@ export function serializeSectionPropertiesXml(props: SectionProperties): string
|
|
|
1607
1608
|
}
|
|
1608
1609
|
}
|
|
1609
1610
|
|
|
1611
|
+
// O2 Slice 4 grab-bag: unmodelled sectPr children preserved verbatim
|
|
1612
|
+
// from import. Emitted last so the modelled body stays ECMA-376 canonical
|
|
1613
|
+
// and extension-namespace properties land at the tail of <w:sectPr>.
|
|
1614
|
+
const grabBagXml = emitPropertyGrabBag(props.unknownPropertyChildren);
|
|
1615
|
+
if (grabBagXml.length > 0) {
|
|
1616
|
+
children.push(grabBagXml);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1610
1619
|
if (children.length === 0) {
|
|
1611
1620
|
return "<w:sectPr/>";
|
|
1612
1621
|
}
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ParagraphSpacing,
|
|
13
13
|
TabStop,
|
|
14
14
|
} from "../../model/canonical-document.ts";
|
|
15
|
+
import { emitPropertyGrabBag } from "../ooxml/property-grab-bag.ts";
|
|
15
16
|
import { buildRunPropertiesXml } from "./serialize-run-formatting.ts";
|
|
16
17
|
|
|
17
18
|
function escXml(value: string): string {
|
|
@@ -147,6 +148,13 @@ export function buildParagraphPropertiesXml(
|
|
|
147
148
|
if (markXml) parts.push(markXml);
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
// 16. Grab-bag: unmodelled pPr children preserved verbatim from import.
|
|
152
|
+
// Emitted last so the typed emit order above stays ECMA-376 canonical and
|
|
153
|
+
// any extension-namespace properties land after the modelled set. Word
|
|
154
|
+
// tolerates extra trailing children inside <w:pPr> better than it
|
|
155
|
+
// tolerates interleaving them with the typed set.
|
|
156
|
+
parts.push(emitPropertyGrabBag(pPr.unknownPropertyChildren));
|
|
157
|
+
|
|
150
158
|
const body = parts.filter(Boolean).join("");
|
|
151
159
|
return body.length > 0 ? `<w:pPr>${body}</w:pPr>` : "";
|
|
152
160
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { CanonicalRunFormatting } from "../../model/canonical-document.ts";
|
|
11
|
+
import { emitPropertyGrabBag } from "../ooxml/property-grab-bag.ts";
|
|
11
12
|
|
|
12
13
|
function escXml(value: string): string {
|
|
13
14
|
return value
|
|
@@ -57,10 +58,12 @@ export function buildRunPropertiesXml(rPr: CanonicalRunFormatting | undefined):
|
|
|
57
58
|
parts.push(toggleEl("vanish", rPr.vanish));
|
|
58
59
|
|
|
59
60
|
// 8. color
|
|
60
|
-
if (rPr.colorHex || rPr.colorThemeSlot) {
|
|
61
|
+
if (rPr.colorHex || rPr.colorThemeSlot || rPr.colorThemeTint || rPr.colorThemeShade) {
|
|
61
62
|
const attrs: string[] = [];
|
|
62
63
|
if (rPr.colorHex) attrs.push(`w:val="${escXml(rPr.colorHex)}"`);
|
|
63
64
|
if (rPr.colorThemeSlot) attrs.push(`w:themeColor="${escXml(rPr.colorThemeSlot)}"`);
|
|
65
|
+
if (rPr.colorThemeTint) attrs.push(`w:themeTint="${escXml(rPr.colorThemeTint)}"`);
|
|
66
|
+
if (rPr.colorThemeShade) attrs.push(`w:themeShade="${escXml(rPr.colorThemeShade)}"`);
|
|
64
67
|
parts.push(`<w:color ${attrs.join(" ")}/>`);
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -85,6 +88,12 @@ export function buildRunPropertiesXml(rPr: CanonicalRunFormatting | undefined):
|
|
|
85
88
|
if (rPr.fontSizeHalfPoints !== undefined) parts.push(`<w:sz w:val="${rPr.fontSizeHalfPoints}"/>`);
|
|
86
89
|
if (rPr.fontSizeCsHalfPoints !== undefined) parts.push(`<w:szCs w:val="${rPr.fontSizeCsHalfPoints}"/>`);
|
|
87
90
|
|
|
91
|
+
// 15. Grab-bag: unmodelled rPr children preserved verbatim from import
|
|
92
|
+
// (extension-namespace properties like <w14:textOutline>, Word-internal
|
|
93
|
+
// knobs like <w:em>, <w:kern>). Emitted last so the typed body stays
|
|
94
|
+
// ECMA-376 canonical and Word tolerates extras at the tail of <w:rPr>.
|
|
95
|
+
parts.push(emitPropertyGrabBag(rPr.unknownPropertyChildren));
|
|
96
|
+
|
|
88
97
|
const body = parts.filter(Boolean).join("");
|
|
89
98
|
return body.length > 0 ? `<w:rPr>${body}</w:rPr>` : "";
|
|
90
99
|
}
|