@beyondwork/docx-react-component 1.0.47 → 1.0.48
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/package.json +1 -1
- package/src/api/public-types.ts +115 -1
- 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/schema/text-schema.ts +95 -1
- package/src/core/state/text-transaction.ts +17 -5
- package/src/io/chart-preview-resolver.ts +27 -0
- package/src/io/docx-session.ts +226 -38
- package/src/io/export/serialize-main-document.ts +37 -0
- 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/parse-axis.ts +277 -0
- package/src/io/ooxml/chart/parse-chart-space.ts +813 -0
- package/src/io/ooxml/chart/parse-series.ts +570 -0
- package/src/io/ooxml/chart/resolve-color.ts +251 -0
- package/src/io/ooxml/chart/types.ts +420 -0
- package/src/io/ooxml/parse-block-structure.ts +99 -0
- package/src/io/ooxml/parse-complex-content.ts +87 -2
- package/src/io/ooxml/parse-main-document.ts +115 -1
- 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/xml-attr-helpers.ts +59 -1
- package/src/io/ooxml/xml-parser.ts +142 -0
- package/src/model/canonical-document.ts +94 -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 +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +167 -0
- package/src/runtime/collab/runtime-collab-sync.ts +279 -0
- package/src/runtime/document-runtime.ts +214 -16
- package/src/runtime/editor-surface/capabilities.ts +63 -50
- 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 +8 -1
- package/src/ui/WordReviewEditor.tsx +30 -0
- package/src/ui/editor-runtime-boundary.ts +6 -1
- package/src/ui/runtime-shortcut-dispatch.ts +12 -7
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a `DocumentSettings` to an OOXML `<w:settings>` document.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Synthesis** (`serializeSettingsXml(settings)`): emit a complete
|
|
7
|
+
* `<w:settings>` document from canonical fields only. Drops every
|
|
8
|
+
* unmodelled top-level child (`<w:defaultTabStop>`, etc.) — so this
|
|
9
|
+
* mode is safe ONLY for synthetic / green-field documents that never
|
|
10
|
+
* had those fields to begin with.
|
|
11
|
+
*
|
|
12
|
+
* 2. **Graft** (`serializeSettingsXml(settings, sourceXml)`): walk the
|
|
13
|
+
* source's top-level children and decide per child:
|
|
14
|
+
* - **Modelled child whose source-derived canonical projection
|
|
15
|
+
* equals the current canonical projection** → keep source bytes
|
|
16
|
+
* verbatim (byte-preservation). This preserves multi-line
|
|
17
|
+
* whitespace inside `<w:compat>`, source child ordering inside
|
|
18
|
+
* `<w:compat>`, unknown attributes on modelled elements (e.g.
|
|
19
|
+
* `<w:zoom w:percent="120%" w:newAttr="..."/>`), and empty
|
|
20
|
+
* self-closing forms like `<w:compat/>`.
|
|
21
|
+
* - **Modelled child that canonical mutated** → rebuild from
|
|
22
|
+
* synthesis. This is where unknown attributes on the mutated
|
|
23
|
+
* element are dropped (documented limitation: the canonical
|
|
24
|
+
* model only carries the attributes the parser captured).
|
|
25
|
+
* - **Modelled child that canonical removed** → drop element AND
|
|
26
|
+
* its leading interstitial whitespace.
|
|
27
|
+
* - **Unmodelled child** → preserve source bytes verbatim.
|
|
28
|
+
* Modelled fields canonical has but source lacks are appended at
|
|
29
|
+
* the end of the body.
|
|
30
|
+
*
|
|
31
|
+
* Critical invariant proven by the CCEP fixture sweep in
|
|
32
|
+
* `test/io/parse-settings-ccep.test.ts`: when canonical equals
|
|
33
|
+
* `parseSettingsXml(sourceXml)`, the grafted output is byte-identical
|
|
34
|
+
* to `sourceXml`. The sweep dynamically discovers every fixture under
|
|
35
|
+
* `test/fixtures/docx/F*.docx` that ships a `/word/settings.xml` part
|
|
36
|
+
* and asserts the no-op round-trip plus an `unmodelledSettingsChildren`
|
|
37
|
+
* symmetry check.
|
|
38
|
+
*
|
|
39
|
+
* Known limitation: a canonical mutation on a modelled element rebuilds
|
|
40
|
+
* the element from the typed fields, dropping any unknown attributes the
|
|
41
|
+
* source carried. The trade-off is acceptable because the alternative
|
|
42
|
+
* (preserving unknown attrs across mutations) would require the parser
|
|
43
|
+
* to track raw attribute bags. For the no-mutation case, byte-
|
|
44
|
+
* preservation kicks in and unknown attrs survive.
|
|
45
|
+
*
|
|
46
|
+
* **WIRED INTO THE EXPORT PIPELINE** as of Lane 3 O4 Phase 3c. The
|
|
47
|
+
* `exportDocxEditorSession` flow in `src/io/docx-session.ts` adds
|
|
48
|
+
* `/word/settings.xml` to `ownedOutputPaths` whenever the source
|
|
49
|
+
* package carried one (or canonical surfaces `subParts.settings`)
|
|
50
|
+
* and routes the bytes through `serializeSettingsXml(settings,
|
|
51
|
+
* sourceXml)`. The `sourceXml` argument comes from
|
|
52
|
+
* `state.sourceSettingsXml`, captured at import time. The CCEP
|
|
53
|
+
* integration test at `test/io/ccep-roundtrip-settings.test.ts`
|
|
54
|
+
* proves both the byte-reuse short-circuit AND the forced-reserialize
|
|
55
|
+
* path (`DOCX_VALIDATOR_FORCE_REGEN=1`) keep settings.xml byte-clean
|
|
56
|
+
* across the full settings-bearing CCEP fixture set.
|
|
57
|
+
*
|
|
58
|
+
* Authority for emit shape: ECMA-376 §17.15 (settings.xml schema) plus
|
|
59
|
+
* the LibreOffice settings serializer at
|
|
60
|
+
* vendor/libreoffice/sw/source/writerfilter/dmapper/SettingsTable.cxx for
|
|
61
|
+
* compatSetting triple emission.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
import type {
|
|
65
|
+
CompatSetting,
|
|
66
|
+
DocumentSettings,
|
|
67
|
+
} from "../../model/canonical-document.ts";
|
|
68
|
+
import {
|
|
69
|
+
parseSettingsXml,
|
|
70
|
+
ROOT_COMPAT_FLAG_NAMES,
|
|
71
|
+
} from "../ooxml/parse-settings.ts";
|
|
72
|
+
import {
|
|
73
|
+
parseSettingsBlueprint,
|
|
74
|
+
type SettingsBlueprintChild,
|
|
75
|
+
} from "../ooxml/parse-settings-blueprint.ts";
|
|
76
|
+
import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
|
|
77
|
+
|
|
78
|
+
export const WORD_SETTINGS_CONTENT_TYPE =
|
|
79
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml";
|
|
80
|
+
|
|
81
|
+
const WORDPROCESSINGML_2006_MAIN_NS =
|
|
82
|
+
"http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Render a complete `<w:settings>` XML document from canonical
|
|
86
|
+
* `DocumentSettings`. The output is the standard XML declaration plus a
|
|
87
|
+
* `<w:settings>` root carrying every modelled field.
|
|
88
|
+
*
|
|
89
|
+
* Emit order is OOXML-schema-friendly to maximize Word's tolerance:
|
|
90
|
+
* 1. <w:evenAndOddHeaders>
|
|
91
|
+
* 2. <w:zoom>
|
|
92
|
+
* 3. root-level compat-adjacent flags (e.g. <w:doNotEmbedSmartTags/>)
|
|
93
|
+
* 4. <w:compat> wrapping flags then compatSetting triples
|
|
94
|
+
* 5. <w:themeFontLang>
|
|
95
|
+
*
|
|
96
|
+
* Insertion order of `compatSettings` array entries and `compatFlags` /
|
|
97
|
+
* `rootCompatFlags` / `themeFontLang` keys is preserved so a byte-stable
|
|
98
|
+
* diff is achievable on round-trip when caller-supplied data is itself
|
|
99
|
+
* stable.
|
|
100
|
+
*/
|
|
101
|
+
export function serializeSettingsXml(
|
|
102
|
+
settings: DocumentSettings,
|
|
103
|
+
sourceXml?: string,
|
|
104
|
+
): string {
|
|
105
|
+
if (sourceXml !== undefined) {
|
|
106
|
+
return graftSettingsXml(settings, sourceXml);
|
|
107
|
+
}
|
|
108
|
+
return synthesizeSettingsXml(settings);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function synthesizeSettingsXml(settings: DocumentSettings): string {
|
|
112
|
+
const parts: string[] = [];
|
|
113
|
+
parts.push(emitEvenAndOddHeaders(settings));
|
|
114
|
+
parts.push(emitZoom(settings));
|
|
115
|
+
parts.push(emitRootCompatFlags(settings));
|
|
116
|
+
parts.push(emitCompatBlock(settings));
|
|
117
|
+
parts.push(emitThemeFontLang(settings));
|
|
118
|
+
|
|
119
|
+
const body = parts.filter((p) => p.length > 0).join("");
|
|
120
|
+
return [
|
|
121
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
|
|
122
|
+
`<w:settings xmlns:w="${WORDPROCESSINGML_2006_MAIN_NS}">${body}</w:settings>`,
|
|
123
|
+
].join("");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Graft canonical fields onto a source settings.xml. Walks the source's
|
|
128
|
+
* top-level children: replaces modelled names with canonical re-emission
|
|
129
|
+
* (or removes them when canonical drops the field), keeps unmodelled
|
|
130
|
+
* children byte-identical to the source. Modelled fields the canonical
|
|
131
|
+
* has but the source lacks are appended at the end of the body.
|
|
132
|
+
*
|
|
133
|
+
* Critical invariant: when canonical equals `parseSettingsXml(sourceXml)`,
|
|
134
|
+
* the output is byte-identical to `sourceXml`. This is the no-edit
|
|
135
|
+
* round-trip guarantee that lets the export pipeline take ownership of
|
|
136
|
+
* `/word/settings.xml` without introducing validator regressions.
|
|
137
|
+
*/
|
|
138
|
+
function graftSettingsXml(
|
|
139
|
+
settings: DocumentSettings,
|
|
140
|
+
sourceXml: string,
|
|
141
|
+
): string {
|
|
142
|
+
const blueprint = parseSettingsBlueprint(sourceXml);
|
|
143
|
+
|
|
144
|
+
// Track which top-level modelled names we already emitted in place so we
|
|
145
|
+
// don't double-emit them in the append-at-end pass.
|
|
146
|
+
const emittedTopLevel = new Set<string>();
|
|
147
|
+
|
|
148
|
+
// Mutable copy of canonical rootCompatFlags so we can "consume" entries as
|
|
149
|
+
// we replace them in place; what's left at the end gets appended.
|
|
150
|
+
const pendingRootFlags = new Map<string, boolean>(
|
|
151
|
+
Object.entries(settings.rootCompatFlags ?? {}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const childParts: string[] = [];
|
|
155
|
+
for (const child of blueprint.topLevelChildren) {
|
|
156
|
+
const replacement = computeChildReplacement(
|
|
157
|
+
settings,
|
|
158
|
+
child,
|
|
159
|
+
pendingRootFlags,
|
|
160
|
+
);
|
|
161
|
+
// Mark modelled names as encountered in source so the append-at-end pass
|
|
162
|
+
// does not duplicate them. Root flags are tracked via pendingRootFlags
|
|
163
|
+
// (computeChildReplacement deletes them as it handles each one), so we
|
|
164
|
+
// only need to track the four named modelled top-level elements here.
|
|
165
|
+
if (
|
|
166
|
+
replacement.kind !== "drop" &&
|
|
167
|
+
MODELLED_TOP_LEVEL_NAMES.has(child.localName)
|
|
168
|
+
) {
|
|
169
|
+
emittedTopLevel.add(child.localName);
|
|
170
|
+
}
|
|
171
|
+
if (replacement.kind === "keep") {
|
|
172
|
+
childParts.push(child.interstitialBefore + child.rawXml);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (replacement.kind === "drop") {
|
|
176
|
+
// Skip both raw and interstitial — leaving interstitial would create
|
|
177
|
+
// dangling whitespace.
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// replacement.kind === "replace"
|
|
181
|
+
childParts.push(child.interstitialBefore + replacement.xml);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Append modelled elements canonical has that source did NOT.
|
|
185
|
+
const appendedParts: string[] = [];
|
|
186
|
+
if (
|
|
187
|
+
!emittedTopLevel.has("evenAndOddHeaders") &&
|
|
188
|
+
settings.evenAndOddHeaders !== undefined
|
|
189
|
+
) {
|
|
190
|
+
appendedParts.push(emitEvenAndOddHeaders(settings));
|
|
191
|
+
}
|
|
192
|
+
if (
|
|
193
|
+
!emittedTopLevel.has("zoom") &&
|
|
194
|
+
settings.zoomLevel !== undefined
|
|
195
|
+
) {
|
|
196
|
+
appendedParts.push(emitZoom(settings));
|
|
197
|
+
}
|
|
198
|
+
// Any rootCompatFlags entries that didn't have a source counterpart.
|
|
199
|
+
for (const [name, value] of pendingRootFlags) {
|
|
200
|
+
appendedParts.push(emitOnOffElement(name, value));
|
|
201
|
+
}
|
|
202
|
+
if (!emittedTopLevel.has("compat")) {
|
|
203
|
+
const compatXml = emitCompatBlock(settings);
|
|
204
|
+
if (compatXml.length > 0) appendedParts.push(compatXml);
|
|
205
|
+
}
|
|
206
|
+
if (
|
|
207
|
+
!emittedTopLevel.has("themeFontLang") &&
|
|
208
|
+
settings.themeFontLang !== undefined
|
|
209
|
+
) {
|
|
210
|
+
appendedParts.push(emitThemeFontLang(settings));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
blueprint.prelude +
|
|
215
|
+
blueprint.settingsOpenTag +
|
|
216
|
+
childParts.join("") +
|
|
217
|
+
appendedParts.join("") +
|
|
218
|
+
blueprint.trailingWhitespace +
|
|
219
|
+
blueprint.settingsCloseTag
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Modelled top-level <w:settings> children that the graft loop tracks via
|
|
225
|
+
* `emittedTopLevel`. Root compat flags (`<w:doNotEmbedSmartTags/>`, etc.)
|
|
226
|
+
* are NOT in this set — they're tracked via `pendingRootFlags` because
|
|
227
|
+
* canonical can carry an arbitrary set of them.
|
|
228
|
+
*/
|
|
229
|
+
const MODELLED_TOP_LEVEL_NAMES: ReadonlySet<string> = new Set([
|
|
230
|
+
"evenAndOddHeaders",
|
|
231
|
+
"zoom",
|
|
232
|
+
"compat",
|
|
233
|
+
"themeFontLang",
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
type ChildReplacement =
|
|
237
|
+
| { kind: "keep" }
|
|
238
|
+
| { kind: "drop" }
|
|
239
|
+
| { kind: "replace"; xml: string };
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Decide what graft does with a single top-level source child.
|
|
243
|
+
*
|
|
244
|
+
* Byte-preservation precedence: for any modelled child whose source-derived
|
|
245
|
+
* canonical projection equals the current canonical projection, return
|
|
246
|
+
* `{ kind: "keep" }` so the source rawXml survives untouched. This preserves
|
|
247
|
+
*
|
|
248
|
+
* - multi-line whitespace inside `<w:compat>` (reviewer C2(a))
|
|
249
|
+
* - source child ordering inside `<w:compat>` (reviewer C2(b))
|
|
250
|
+
* - unknown attributes on modelled elements like
|
|
251
|
+
* `<w:compatSetting w:name=… w:uri=… w:val=… w:futureAttr=…/>` or
|
|
252
|
+
* `<w:zoom w:percent="120%" w:newAttr=…/>` (reviewer C2(c))
|
|
253
|
+
* - empty self-closing `<w:compat/>` source (reviewer Risk #6)
|
|
254
|
+
*
|
|
255
|
+
* Only when canonical genuinely mutates a modelled field does the byte
|
|
256
|
+
* form get rebuilt from synthesis. The trade-off is documented: a
|
|
257
|
+
* mutation drops unknown attributes on the mutated element, because the
|
|
258
|
+
* canonical model only carries the attributes the parser captured.
|
|
259
|
+
*/
|
|
260
|
+
function computeChildReplacement(
|
|
261
|
+
settings: DocumentSettings,
|
|
262
|
+
child: SettingsBlueprintChild,
|
|
263
|
+
pendingRootFlags: Map<string, boolean>,
|
|
264
|
+
): ChildReplacement {
|
|
265
|
+
switch (child.localName) {
|
|
266
|
+
case "evenAndOddHeaders": {
|
|
267
|
+
if (
|
|
268
|
+
emitEvenAndOddHeaders(parseModelledChild(child.rawXml)) ===
|
|
269
|
+
emitEvenAndOddHeaders(settings)
|
|
270
|
+
) {
|
|
271
|
+
return { kind: "keep" };
|
|
272
|
+
}
|
|
273
|
+
const xml = emitEvenAndOddHeaders(settings);
|
|
274
|
+
return xml.length > 0 ? { kind: "replace", xml } : { kind: "drop" };
|
|
275
|
+
}
|
|
276
|
+
case "zoom": {
|
|
277
|
+
if (
|
|
278
|
+
emitZoom(parseModelledChild(child.rawXml)) === emitZoom(settings)
|
|
279
|
+
) {
|
|
280
|
+
return { kind: "keep" };
|
|
281
|
+
}
|
|
282
|
+
const xml = emitZoom(settings);
|
|
283
|
+
return xml.length > 0 ? { kind: "replace", xml } : { kind: "drop" };
|
|
284
|
+
}
|
|
285
|
+
case "compat": {
|
|
286
|
+
if (
|
|
287
|
+
emitCompatBlock(parseModelledChild(child.rawXml)) ===
|
|
288
|
+
emitCompatBlock(settings)
|
|
289
|
+
) {
|
|
290
|
+
return { kind: "keep" };
|
|
291
|
+
}
|
|
292
|
+
const xml = emitCompatBlock(settings);
|
|
293
|
+
return xml.length > 0 ? { kind: "replace", xml } : { kind: "drop" };
|
|
294
|
+
}
|
|
295
|
+
case "themeFontLang": {
|
|
296
|
+
if (
|
|
297
|
+
emitThemeFontLang(parseModelledChild(child.rawXml)) ===
|
|
298
|
+
emitThemeFontLang(settings)
|
|
299
|
+
) {
|
|
300
|
+
return { kind: "keep" };
|
|
301
|
+
}
|
|
302
|
+
const xml = emitThemeFontLang(settings);
|
|
303
|
+
return xml.length > 0 ? { kind: "replace", xml } : { kind: "drop" };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Root compat flag?
|
|
307
|
+
if (ROOT_COMPAT_FLAG_NAMES.has(child.localName)) {
|
|
308
|
+
if (pendingRootFlags.has(child.localName)) {
|
|
309
|
+
const value = pendingRootFlags.get(child.localName)!;
|
|
310
|
+
pendingRootFlags.delete(child.localName);
|
|
311
|
+
// Byte-preserve when canonical value matches the source-encoded value.
|
|
312
|
+
const sourceParsed = parseModelledChild(child.rawXml);
|
|
313
|
+
if (sourceParsed.rootCompatFlags?.[child.localName] === value) {
|
|
314
|
+
return { kind: "keep" };
|
|
315
|
+
}
|
|
316
|
+
return { kind: "replace", xml: emitOnOffElement(child.localName, value) };
|
|
317
|
+
}
|
|
318
|
+
// Canonical dropped it.
|
|
319
|
+
return { kind: "drop" };
|
|
320
|
+
}
|
|
321
|
+
// Unmodelled — keep verbatim.
|
|
322
|
+
return { kind: "keep" };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Re-parse a single top-level source child element by wrapping it in a
|
|
327
|
+
* synthetic `<w:settings>` envelope and feeding it through the canonical
|
|
328
|
+
* parser. Used to compare source-derived canonical projection against the
|
|
329
|
+
* caller-supplied canonical for byte-preservation eligibility.
|
|
330
|
+
*
|
|
331
|
+
* Tiny string allocation per child; fine for settings.xml-sized inputs.
|
|
332
|
+
*/
|
|
333
|
+
function parseModelledChild(rawXml: string): DocumentSettings {
|
|
334
|
+
const wrapped =
|
|
335
|
+
`<w:settings xmlns:w="${WORDPROCESSINGML_2006_MAIN_NS}">` +
|
|
336
|
+
rawXml +
|
|
337
|
+
`</w:settings>`;
|
|
338
|
+
return parseSettingsXml(wrapped);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function emitEvenAndOddHeaders(settings: DocumentSettings): string {
|
|
342
|
+
if (settings.evenAndOddHeaders === undefined) return "";
|
|
343
|
+
if (settings.evenAndOddHeaders) {
|
|
344
|
+
return `<w:evenAndOddHeaders/>`;
|
|
345
|
+
}
|
|
346
|
+
return `<w:evenAndOddHeaders w:val="false"/>`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function emitZoom(settings: DocumentSettings): string {
|
|
350
|
+
const { zoomLevel } = settings;
|
|
351
|
+
if (zoomLevel === undefined) return "";
|
|
352
|
+
if (zoomLevel === "pageWidth") {
|
|
353
|
+
return `<w:zoom w:val="bestFit"/>`;
|
|
354
|
+
}
|
|
355
|
+
if (zoomLevel === "onePage") {
|
|
356
|
+
return `<w:zoom w:val="fullPage"/>`;
|
|
357
|
+
}
|
|
358
|
+
if (typeof zoomLevel === "number" && Number.isFinite(zoomLevel)) {
|
|
359
|
+
return `<w:zoom w:percent="${Math.round(zoomLevel)}"/>`;
|
|
360
|
+
}
|
|
361
|
+
return "";
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function emitRootCompatFlags(settings: DocumentSettings): string {
|
|
365
|
+
const flags = settings.rootCompatFlags;
|
|
366
|
+
if (!flags) return "";
|
|
367
|
+
return Object.entries(flags)
|
|
368
|
+
.map(([name, value]) => emitOnOffElement(name, value))
|
|
369
|
+
.join("");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function emitCompatBlock(settings: DocumentSettings): string {
|
|
373
|
+
const flags = settings.compatFlags;
|
|
374
|
+
const triples = settings.compatSettings;
|
|
375
|
+
const hasFlags = flags && Object.keys(flags).length > 0;
|
|
376
|
+
const hasTriples = triples && triples.length > 0;
|
|
377
|
+
if (!hasFlags && !hasTriples) return "";
|
|
378
|
+
|
|
379
|
+
const flagXml = hasFlags
|
|
380
|
+
? Object.entries(flags!)
|
|
381
|
+
.map(([name, value]) => emitOnOffElement(name, value))
|
|
382
|
+
.join("")
|
|
383
|
+
: "";
|
|
384
|
+
const triplesXml = hasTriples
|
|
385
|
+
? triples!.map((cs) => emitCompatSetting(cs)).join("")
|
|
386
|
+
: "";
|
|
387
|
+
|
|
388
|
+
return `<w:compat>${flagXml}${triplesXml}</w:compat>`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function emitCompatSetting(cs: CompatSetting): string {
|
|
392
|
+
return (
|
|
393
|
+
`<w:compatSetting` +
|
|
394
|
+
` w:name="${escapeXmlAttribute(cs.name)}"` +
|
|
395
|
+
` w:uri="${escapeXmlAttribute(cs.uri)}"` +
|
|
396
|
+
` w:val="${escapeXmlAttribute(cs.value)}"` +
|
|
397
|
+
`/>`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function emitThemeFontLang(settings: DocumentSettings): string {
|
|
402
|
+
const lang = settings.themeFontLang;
|
|
403
|
+
if (!lang) return "";
|
|
404
|
+
const attrs = Object.entries(lang)
|
|
405
|
+
.map(([name, value]) => ` ${name}="${escapeXmlAttribute(value)}"`)
|
|
406
|
+
.join("");
|
|
407
|
+
if (attrs.length === 0) {
|
|
408
|
+
return `<w:themeFontLang/>`;
|
|
409
|
+
}
|
|
410
|
+
return `<w:themeFontLang${attrs}/>`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Emit a ST_OnOff element. true → bare self-closing tag; false → explicit
|
|
415
|
+
* `w:val="false"` so the parser doesn't infer the default-true. Symmetric
|
|
416
|
+
* with `readOnOffValue` in `parse-settings.ts`.
|
|
417
|
+
*/
|
|
418
|
+
function emitOnOffElement(localName: string, value: boolean): string {
|
|
419
|
+
if (value) return `<w:${localName}/>`;
|
|
420
|
+
return `<w:${localName} w:val="false"/>`;
|
|
421
|
+
}
|
|
@@ -73,6 +73,10 @@ function buildParagraphStyleXml(
|
|
|
73
73
|
const nextEl = style.nextStyle
|
|
74
74
|
? `<w:next w:val="${escXml(style.nextStyle)}"/>`
|
|
75
75
|
: "";
|
|
76
|
+
// ECMA-376 §17.7 emit order: name → basedOn → next → link → ...
|
|
77
|
+
const linkEl = style.linkedStyleId
|
|
78
|
+
? `<w:link w:val="${escXml(style.linkedStyleId)}"/>`
|
|
79
|
+
: "";
|
|
76
80
|
|
|
77
81
|
// Build pPr: may contain numPr (from numbering) and any canonical formatting.
|
|
78
82
|
// We reconstruct the pPr children in canonical order:
|
|
@@ -95,6 +99,7 @@ function buildParagraphStyleXml(
|
|
|
95
99
|
nameEl +
|
|
96
100
|
basedOnEl +
|
|
97
101
|
nextEl +
|
|
102
|
+
linkEl +
|
|
98
103
|
pPrBodyXml +
|
|
99
104
|
rPrXml +
|
|
100
105
|
`</w:style>`
|
|
@@ -175,12 +180,17 @@ function buildCharacterStyleXml(
|
|
|
175
180
|
const basedOnEl = style.basedOn
|
|
176
181
|
? `<w:basedOn w:val="${escXml(style.basedOn)}"/>`
|
|
177
182
|
: "";
|
|
183
|
+
// ECMA-376 §17.7 emit order: name → basedOn → link → ... → rPr
|
|
184
|
+
const linkEl = style.linkedStyleId
|
|
185
|
+
? `<w:link w:val="${escXml(style.linkedStyleId)}"/>`
|
|
186
|
+
: "";
|
|
178
187
|
const rPrXml = buildRunPropertiesXml(style.runProperties);
|
|
179
188
|
|
|
180
189
|
return (
|
|
181
190
|
`<w:style w:type="character" w:styleId="${escXml(style.styleId)}"${defaultAttr}>` +
|
|
182
191
|
nameEl +
|
|
183
192
|
basedOnEl +
|
|
193
|
+
linkEl +
|
|
184
194
|
rPrXml +
|
|
185
195
|
`</w:style>`
|
|
186
196
|
);
|
|
@@ -482,6 +482,7 @@ function normalizeInlineChildren(
|
|
|
482
482
|
normalized.push({
|
|
483
483
|
type: "chart_preview",
|
|
484
484
|
...(node.previewMediaId ? { previewMediaId: node.previewMediaId } : {}),
|
|
485
|
+
...(node.parsedData ? { parsedData: node.parsedData } : {}),
|
|
485
486
|
rawXml: node.rawXml,
|
|
486
487
|
});
|
|
487
488
|
state.cursor += 1;
|