@beyondwork/docx-react-component 1.0.58 → 1.0.60
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 +2 -2
- package/package.json +2 -1
- package/src/api/awareness-identity-types.ts +4 -2
- package/src/api/comment-negotiation-types.ts +4 -1
- package/src/api/external-custody-types.ts +16 -0
- package/src/api/internal/build-ref-projections.ts +108 -0
- package/src/api/package-version.ts +1 -1
- package/src/api/participants-types.ts +11 -1
- package/src/api/public-types.ts +980 -10
- package/src/api/scope-metadata-resolver-types.ts +6 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +225 -16
- package/src/core/commands/legacy-form-field-commands.ts +181 -0
- package/src/core/commands/table-structure-commands.ts +149 -31
- package/src/core/selection/mapping.ts +20 -0
- package/src/core/state/editor-state.ts +4 -1
- package/src/index.ts +28 -0
- package/src/io/docx-session.ts +22 -3
- package/src/io/export/export-session.ts +11 -7
- package/src/io/export/ooxml-namespaces.ts +47 -0
- package/src/io/export/reattach-preserved-parts.ts +4 -16
- package/src/io/export/serialize-comments.ts +3 -131
- package/src/io/export/serialize-ffdata.ts +89 -0
- package/src/io/export/serialize-headers-footers.ts +5 -0
- package/src/io/export/serialize-main-document.ts +224 -34
- package/src/io/export/serialize-numbering.ts +22 -2
- package/src/io/export/serialize-revisions.ts +99 -0
- package/src/io/export/serialize-tables.ts +9 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/export/table-properties-xml.ts +14 -0
- package/src/io/load-scheduler.ts +70 -28
- package/src/io/normalize/normalize-text.ts +13 -0
- package/src/io/ooxml/_mini-xml.ts +198 -0
- package/src/io/ooxml/canonicalize-payload.ts +1 -4
- package/src/io/ooxml/chart/chart-style-table.ts +4 -3
- package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
- package/src/io/ooxml/chart/parse-series.ts +2 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +6 -434
- package/src/io/ooxml/comment-presentation-payload.ts +6 -5
- package/src/io/ooxml/highlight-colors.ts +8 -5
- package/src/io/ooxml/parse-anchor.ts +68 -53
- package/src/io/ooxml/parse-comments.ts +14 -142
- package/src/io/ooxml/parse-complex-content.ts +3 -106
- package/src/io/ooxml/parse-drawing.ts +100 -195
- package/src/io/ooxml/parse-ffdata.ts +93 -0
- package/src/io/ooxml/parse-fields.ts +7 -146
- package/src/io/ooxml/parse-fill.ts +88 -8
- package/src/io/ooxml/parse-font-table.ts +5 -105
- package/src/io/ooxml/parse-footnotes.ts +28 -152
- package/src/io/ooxml/parse-headers-footers.ts +106 -212
- package/src/io/ooxml/parse-inline-media.ts +3 -200
- package/src/io/ooxml/parse-main-document.ts +180 -217
- package/src/io/ooxml/parse-numbering.ts +154 -335
- package/src/io/ooxml/parse-object.ts +147 -0
- package/src/io/ooxml/parse-ole-relationship.ts +82 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
- package/src/io/ooxml/parse-picture-sdt.ts +85 -0
- package/src/io/ooxml/parse-picture.ts +72 -42
- package/src/io/ooxml/parse-revisions.ts +285 -51
- package/src/io/ooxml/parse-settings.ts +6 -99
- package/src/io/ooxml/parse-shapes.ts +25 -140
- package/src/io/ooxml/parse-styles.ts +3 -218
- package/src/io/ooxml/parse-tables.ts +76 -256
- package/src/io/ooxml/parse-theme.ts +1 -4
- package/src/io/ooxml/property-grab-bag.ts +5 -47
- package/src/io/ooxml/workflow-payload.ts +6 -1
- package/src/io/ooxml/xml-element-serialize.ts +32 -0
- package/src/io/ooxml/xml-parser.ts +183 -0
- package/src/legal/bookmarks.ts +1 -1
- package/src/legal/cross-references.ts +1 -1
- package/src/legal/defined-terms.ts +1 -1
- package/src/legal/{_document-root.ts → document-root.ts} +8 -0
- package/src/legal/signature-blocks.ts +1 -1
- package/src/model/canonical-document.ts +159 -6
- package/src/model/chart-types.ts +439 -0
- package/src/model/snapshot.ts +5 -1
- package/src/review/store/comment-remapping.ts +24 -11
- package/src/review/store/revision-actions.ts +482 -2
- package/src/review/store/revision-store.ts +15 -0
- package/src/review/store/revision-types.ts +76 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
- package/src/runtime/collab/runtime-collab-sync.ts +33 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +153 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +230 -0
- package/src/runtime/document-runtime.ts +821 -54
- package/src/runtime/document-search.ts +115 -0
- package/src/runtime/edit-ops/index.ts +18 -2
- package/src/runtime/footnote-resolver.ts +130 -0
- package/src/runtime/layout/layout-engine-instance.ts +31 -4
- package/src/runtime/layout/layout-engine-version.ts +37 -1
- package/src/runtime/layout/page-graph.ts +14 -1
- package/src/runtime/layout/resolved-formatting-state.ts +21 -0
- package/src/runtime/numbering-prefix.ts +17 -0
- package/src/runtime/query-scopes.ts +108 -10
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- package/src/runtime/selection/post-edit-validator.ts +60 -6
- package/src/runtime/structure-ops/index.ts +20 -4
- package/src/runtime/surface-projection.ts +290 -21
- package/src/runtime/table-schema.ts +6 -0
- package/src/runtime/theme-color-resolver.ts +2 -2
- package/src/runtime/units.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +4 -0
- package/src/ui/WordReviewEditor.tsx +187 -43
- package/src/ui/editor-runtime-boundary.ts +10 -0
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui/headless/chrome-registry.ts +53 -0
- package/src/ui/headless/selection-tool-resolver.ts +11 -1
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
- package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
- package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +0 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +1 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +87 -25
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +9 -0
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
- package/src/ui-tailwind/index.ts +9 -0
- package/src/ui-tailwind/page-chrome-model.ts +77 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
- package/src/ui-tailwind/theme/tokens.ts +14 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +29 -87
- package/src/validation/diagnostics.ts +1 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { LegacyFormFieldNode } from "../../model/canonical-document.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Phase V.a — Regenerate `<w:ffData>…</w:ffData>` XML from a typed
|
|
5
|
+
* `LegacyFormFieldNode`.
|
|
6
|
+
*
|
|
7
|
+
* Used when typed-field mutations (via `setLegacyFormFieldValue`) need to be
|
|
8
|
+
* reflected in the serialized output. When no mutation has occurred, the
|
|
9
|
+
* preserved `legacyFormField.rawXml` is emitted verbatim for lossless
|
|
10
|
+
* round-trip — this helper is only invoked on the mutation path.
|
|
11
|
+
*
|
|
12
|
+
* Output is intentionally minimal: only the fields present on the typed
|
|
13
|
+
* node are emitted. Unknown-shape preservation (e.g. w:format, w:helpText,
|
|
14
|
+
* w:statusText) is handled by the rawXml passthrough path; once a consumer
|
|
15
|
+
* mutates the field, those unknown fields are lost unless the host
|
|
16
|
+
* re-populates them. This is the documented trade-off.
|
|
17
|
+
*/
|
|
18
|
+
export function regenerateFFDataRawXml(node: LegacyFormFieldNode): string {
|
|
19
|
+
const parts: string[] = ["<w:ffData>"];
|
|
20
|
+
|
|
21
|
+
if (node.name !== undefined) {
|
|
22
|
+
parts.push(`<w:name w:val="${escapeXmlAttribute(node.name)}"/>`);
|
|
23
|
+
}
|
|
24
|
+
if (node.enabled !== undefined) {
|
|
25
|
+
parts.push(node.enabled ? "<w:enabled/>" : `<w:enabled w:val="0"/>`);
|
|
26
|
+
}
|
|
27
|
+
if (node.calcOnExit !== undefined) {
|
|
28
|
+
parts.push(node.calcOnExit ? "<w:calcOnExit/>" : `<w:calcOnExit w:val="0"/>`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
switch (node.kind) {
|
|
32
|
+
case "textInput": {
|
|
33
|
+
const ti = node.textInput;
|
|
34
|
+
parts.push("<w:textInput>");
|
|
35
|
+
if (ti?.default !== undefined) {
|
|
36
|
+
parts.push(`<w:default w:val="${escapeXmlAttribute(ti.default)}"/>`);
|
|
37
|
+
}
|
|
38
|
+
if (ti?.maxLength !== undefined) {
|
|
39
|
+
parts.push(`<w:maxLength w:val="${ti.maxLength}"/>`);
|
|
40
|
+
}
|
|
41
|
+
if (ti?.format !== undefined) {
|
|
42
|
+
parts.push(`<w:format w:val="${escapeXmlAttribute(ti.format)}"/>`);
|
|
43
|
+
}
|
|
44
|
+
parts.push("</w:textInput>");
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case "checkBox": {
|
|
48
|
+
const cb = node.checkBox;
|
|
49
|
+
parts.push("<w:checkBox>");
|
|
50
|
+
if (cb?.size !== undefined) {
|
|
51
|
+
parts.push(`<w:size w:val="${cb.size}"/>`);
|
|
52
|
+
}
|
|
53
|
+
if (cb?.default !== undefined) {
|
|
54
|
+
parts.push(`<w:default w:val="${cb.default ? "1" : "0"}"/>`);
|
|
55
|
+
}
|
|
56
|
+
if (cb?.checked !== undefined) {
|
|
57
|
+
parts.push(`<w:checked w:val="${cb.checked ? "1" : "0"}"/>`);
|
|
58
|
+
}
|
|
59
|
+
parts.push("</w:checkBox>");
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case "ddList": {
|
|
63
|
+
const dd = node.ddList;
|
|
64
|
+
parts.push("<w:ddList>");
|
|
65
|
+
if (dd?.default !== undefined) {
|
|
66
|
+
parts.push(`<w:default w:val="${dd.default}"/>`);
|
|
67
|
+
}
|
|
68
|
+
if (dd?.listEntry) {
|
|
69
|
+
for (const entry of dd.listEntry) {
|
|
70
|
+
parts.push(`<w:listEntry w:val="${escapeXmlAttribute(entry)}"/>`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
parts.push("</w:ddList>");
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
parts.push("</w:ffData>");
|
|
79
|
+
return parts.join("");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function escapeXmlAttribute(value: string): string {
|
|
83
|
+
return value
|
|
84
|
+
.replace(/&/g, "&")
|
|
85
|
+
.replace(/</g, "<")
|
|
86
|
+
.replace(/>/g, ">")
|
|
87
|
+
.replace(/"/g, """)
|
|
88
|
+
.replace(/'/g, "'");
|
|
89
|
+
}
|
|
@@ -263,6 +263,11 @@ function serializeInlineNode(node: InlineNode): string {
|
|
|
263
263
|
case "wordart":
|
|
264
264
|
case "vml_shape":
|
|
265
265
|
return wrapInlineRawXml(node.rawXml);
|
|
266
|
+
case "ole_embed":
|
|
267
|
+
// OLE in header/footer is rare but legal in OOXML. Relationship
|
|
268
|
+
// tracking for sub-parts is not wired; emit rawXml verbatim
|
|
269
|
+
// (preserve-only contract — same fallback as vml_shape above).
|
|
270
|
+
return wrapInlineRawXml(node.rawXml);
|
|
266
271
|
case "opaque_inline":
|
|
267
272
|
throw new Error(`Cannot safely serialize ${node.type} content in header/footer sub-parts.`);
|
|
268
273
|
case "image":
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
DocumentRootNode,
|
|
6
6
|
FootnoteProperties,
|
|
7
7
|
InlineNode,
|
|
8
|
+
LegacyFormFieldNode,
|
|
8
9
|
MediaCatalog,
|
|
9
10
|
ParagraphNode,
|
|
10
11
|
PreservationStore,
|
|
@@ -14,6 +15,7 @@ import type {
|
|
|
14
15
|
TableCellNode,
|
|
15
16
|
TextMark,
|
|
16
17
|
} from "../../model/canonical-document.ts";
|
|
18
|
+
import { regenerateFFDataRawXml } from "./serialize-ffdata.ts";
|
|
17
19
|
import type { OpcRelationship } from "../ooxml/part-manifest.ts";
|
|
18
20
|
import type { RevisionParagraphBoundary } from "../ooxml/revision-boundaries.ts";
|
|
19
21
|
import { SCOPE_MARKER_BOOKMARK_PREFIX } from "../ooxml/parse-scope-markers.ts";
|
|
@@ -28,10 +30,27 @@ import {
|
|
|
28
30
|
import { twip } from "./twip.ts";
|
|
29
31
|
import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
|
|
30
32
|
import { emitPropertyGrabBag } from "../ooxml/property-grab-bag.ts";
|
|
33
|
+
import { type NamespaceFlavor, nsUris } from "./ooxml-namespaces.ts";
|
|
31
34
|
|
|
32
35
|
const HYPERLINK_RELATIONSHIP_TYPE =
|
|
33
36
|
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
|
|
34
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Phase V.a — decide which ffData XML to emit for a FieldNode's legacyFormField.
|
|
40
|
+
*
|
|
41
|
+
* - Un-mutated (or missing): emit preserved `rawXml` verbatim for lossless
|
|
42
|
+
* round-trip (Phase O contract).
|
|
43
|
+
* - Mutated (after `setLegacyFormFieldValue`): regenerate fresh XML from
|
|
44
|
+
* the typed fields via `regenerateFFDataRawXml`. Unknown-shape fields
|
|
45
|
+
* (helpText/statusText/format) that the typed model doesn't cover are
|
|
46
|
+
* dropped on the mutation path — documented tradeoff.
|
|
47
|
+
*/
|
|
48
|
+
function resolveFFDataXml(legacyFormField: LegacyFormFieldNode | undefined): string {
|
|
49
|
+
if (!legacyFormField) return "";
|
|
50
|
+
if (legacyFormField.mutated) return regenerateFFDataRawXml(legacyFormField);
|
|
51
|
+
return legacyFormField.rawXml ?? "";
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
export interface SerializedMainDocument {
|
|
36
55
|
documentXml: string;
|
|
37
56
|
relationships: OpcRelationship[];
|
|
@@ -42,6 +61,8 @@ export interface SerializeMainDocumentOptions {
|
|
|
42
61
|
documentAttributes?: Record<string, string>;
|
|
43
62
|
media?: MediaCatalog;
|
|
44
63
|
finalSectionProperties?: SectionProperties;
|
|
64
|
+
/** Defaults to "transitional". Pass "strict" to emit ISO 29500 Strict namespace URIs. */
|
|
65
|
+
namespaceFlavor?: NamespaceFlavor;
|
|
45
66
|
}
|
|
46
67
|
|
|
47
68
|
interface SerializationState {
|
|
@@ -51,6 +72,13 @@ interface SerializationState {
|
|
|
51
72
|
retainedRelationshipIds: Set<string>;
|
|
52
73
|
media: MediaCatalog;
|
|
53
74
|
preservation: PreservationStore;
|
|
75
|
+
namespaceFlavor: NamespaceFlavor;
|
|
76
|
+
/**
|
|
77
|
+
* Phase 3.1 B2 — monotonic counter for minted `wp:docPr` ids on reconstructed
|
|
78
|
+
* DrawingFrameNode pictures that lack `content.rawXml`. Starts at 10000 to
|
|
79
|
+
* avoid collision with Word-authored ids (which are typically small ints).
|
|
80
|
+
*/
|
|
81
|
+
nextDrawingFrameReconstructId: number;
|
|
54
82
|
/**
|
|
55
83
|
* A.7: per-body-context dedupe sets for w14:paraId / w14:textId.
|
|
56
84
|
* Every paragraph emitted into the same document part must carry a
|
|
@@ -100,6 +128,8 @@ export function serializeMainDocument(
|
|
|
100
128
|
),
|
|
101
129
|
media: options.media ?? { items: {} },
|
|
102
130
|
preservation,
|
|
131
|
+
namespaceFlavor: options.namespaceFlavor ?? "transitional",
|
|
132
|
+
nextDrawingFrameReconstructId: 10000,
|
|
103
133
|
usedParaIds: new Set<string>(),
|
|
104
134
|
usedTextIds: new Set<string>(),
|
|
105
135
|
mintedParaIdCounter: 0,
|
|
@@ -214,6 +244,7 @@ export function serializeMainDocument(
|
|
|
214
244
|
options.documentAttributes,
|
|
215
245
|
content,
|
|
216
246
|
extensionAliases,
|
|
247
|
+
options.namespaceFlavor ?? "transitional",
|
|
217
248
|
)}>`;
|
|
218
249
|
const prefix = [
|
|
219
250
|
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
|
|
@@ -532,7 +563,17 @@ function serializeTableInlineNode(
|
|
|
532
563
|
case "vml_shape":
|
|
533
564
|
return wrapInlineRawXml(node.rawXml);
|
|
534
565
|
case "drawing_frame":
|
|
535
|
-
return serializeDrawingFrameNode(node);
|
|
566
|
+
return serializeDrawingFrameNode(node, state);
|
|
567
|
+
case "ole_embed":
|
|
568
|
+
// Lane 7c Slice 7c.1 — emit rawXml verbatim and retain the
|
|
569
|
+
// embedding relationship. Defensive: the serialize-state
|
|
570
|
+
// constructor pre-seeds retainedRelationshipIds with every
|
|
571
|
+
// pre-existing non-hyperlink relationship, so the embedding
|
|
572
|
+
// binary part is already preserved via collectPreservedPackageParts.
|
|
573
|
+
// This .add() guards against a future constructor tightening
|
|
574
|
+
// that narrows the pre-seed set.
|
|
575
|
+
state.retainedRelationshipIds.add(node.relationshipId);
|
|
576
|
+
return wrapInlineRawXml(node.rawXml);
|
|
536
577
|
case "hyperlink": {
|
|
537
578
|
const hyperlinkOpen = node.href.startsWith("#")
|
|
538
579
|
? `<w:hyperlink w:anchor="${escapeXmlAttribute(node.href.slice(1))}">`
|
|
@@ -557,8 +598,17 @@ function serializeTableInlineNode(
|
|
|
557
598
|
.map((child) => serializeTableInlineNode(child, state))
|
|
558
599
|
.join("");
|
|
559
600
|
if (node.fieldType === "complex") {
|
|
601
|
+
// Invariant (Phase T): legacyFormField.rawXml is the authoritative
|
|
602
|
+
// form-field markup. Mutating legacyFormField.{textInput.default |
|
|
603
|
+
// checkBox.checked | ddList.selected} without regenerating rawXml
|
|
604
|
+
// will desync the serialized output. Phase V will add a regeneration
|
|
605
|
+
// path via `setLegacyFormFieldValue` + `regenerateFFDataRawXml`.
|
|
606
|
+
const ffDataXml = resolveFFDataXml(node.legacyFormField);
|
|
607
|
+
const beginRun = ffDataXml
|
|
608
|
+
? `<w:r><w:fldChar w:fldCharType="begin">${ffDataXml}</w:fldChar></w:r>`
|
|
609
|
+
: `<w:r><w:fldChar w:fldCharType="begin"/></w:r>`;
|
|
560
610
|
return (
|
|
561
|
-
|
|
611
|
+
beginRun +
|
|
562
612
|
`<w:r><w:instrText xml:space="preserve"> ${escapeXml(node.instruction)} </w:instrText></w:r>` +
|
|
563
613
|
`<w:r><w:fldChar w:fldCharType="separate"/></w:r>` +
|
|
564
614
|
childrenXml +
|
|
@@ -567,6 +617,18 @@ function serializeTableInlineNode(
|
|
|
567
617
|
}
|
|
568
618
|
return `<w:fldSimple w:instr="${escapeXmlAttribute(node.instruction)}">${childrenXml}</w:fldSimple>`;
|
|
569
619
|
}
|
|
620
|
+
if (node.fieldType === "complex") {
|
|
621
|
+
const ffDataXml = resolveFFDataXml(node.legacyFormField);
|
|
622
|
+
const beginRun = ffDataXml
|
|
623
|
+
? `<w:r><w:fldChar w:fldCharType="begin">${ffDataXml}</w:fldChar></w:r>`
|
|
624
|
+
: `<w:r><w:fldChar w:fldCharType="begin"/></w:r>`;
|
|
625
|
+
return (
|
|
626
|
+
beginRun +
|
|
627
|
+
`<w:r><w:instrText xml:space="preserve"> ${escapeXml(node.instruction)} </w:instrText></w:r>` +
|
|
628
|
+
`<w:r><w:fldChar w:fldCharType="separate"/></w:r>` +
|
|
629
|
+
`<w:r><w:fldChar w:fldCharType="end"/></w:r>`
|
|
630
|
+
);
|
|
631
|
+
}
|
|
570
632
|
return `<w:fldSimple w:instr="${escapeXmlAttribute(node.instruction)}"/>`;
|
|
571
633
|
}
|
|
572
634
|
case "bookmark_start":
|
|
@@ -970,7 +1032,22 @@ function serializeInlineNode(
|
|
|
970
1032
|
// variant retains the original w:drawing slice on its content or is
|
|
971
1033
|
// a picture that also keeps the slice on its parent). Matches the
|
|
972
1034
|
// same "rawXml preservation" contract as shape/chart/smartart above.
|
|
973
|
-
const xml = serializeDrawingFrameNode(node);
|
|
1035
|
+
const xml = serializeDrawingFrameNode(node, state);
|
|
1036
|
+
const boundaries = new Map<number, number>();
|
|
1037
|
+
boundaries.set(cursor, xmlOffset);
|
|
1038
|
+
boundaries.set(cursor + 1, xmlOffset + xml.length);
|
|
1039
|
+
return {
|
|
1040
|
+
xml,
|
|
1041
|
+
cursor: cursor + 1,
|
|
1042
|
+
boundaries,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
case "ole_embed": {
|
|
1046
|
+
// Lane 7c Slice 7c.1 — emit rawXml verbatim and retain the
|
|
1047
|
+
// embedding relationship. Defensive — see the matching branch
|
|
1048
|
+
// in the run dispatcher above for the pre-seed rationale.
|
|
1049
|
+
state.retainedRelationshipIds.add(node.relationshipId);
|
|
1050
|
+
const xml = wrapInlineRawXml(node.rawXml);
|
|
974
1051
|
const boundaries = new Map<number, number>();
|
|
975
1052
|
boundaries.set(cursor, xmlOffset);
|
|
976
1053
|
boundaries.set(cursor + 1, xmlOffset + xml.length);
|
|
@@ -1027,8 +1104,12 @@ function serializeInlineNode(
|
|
|
1027
1104
|
let nextOffset = xmlOffset;
|
|
1028
1105
|
|
|
1029
1106
|
if (node.fieldType === "complex") {
|
|
1107
|
+
const ffDataXml = resolveFFDataXml(node.legacyFormField);
|
|
1108
|
+
const beginRun = ffDataXml
|
|
1109
|
+
? `<w:r><w:fldChar w:fldCharType="begin">${ffDataXml}</w:fldChar></w:r>`
|
|
1110
|
+
: `<w:r><w:fldChar w:fldCharType="begin"/></w:r>`;
|
|
1030
1111
|
const beginXml =
|
|
1031
|
-
|
|
1112
|
+
beginRun +
|
|
1032
1113
|
`<w:r><w:instrText xml:space="preserve"> ${escapeXml(node.instruction)} </w:instrText></w:r>` +
|
|
1033
1114
|
`<w:r><w:fldChar w:fldCharType="separate"/></w:r>`;
|
|
1034
1115
|
nextOffset += beginXml.length;
|
|
@@ -1069,6 +1150,22 @@ function serializeInlineNode(
|
|
|
1069
1150
|
return { xml: children.join(""), cursor: nextCursor, boundaries };
|
|
1070
1151
|
}
|
|
1071
1152
|
|
|
1153
|
+
if (node.fieldType === "complex") {
|
|
1154
|
+
const ffDataXml = resolveFFDataXml(node.legacyFormField);
|
|
1155
|
+
const beginRun = ffDataXml
|
|
1156
|
+
? `<w:r><w:fldChar w:fldCharType="begin">${ffDataXml}</w:fldChar></w:r>`
|
|
1157
|
+
: `<w:r><w:fldChar w:fldCharType="begin"/></w:r>`;
|
|
1158
|
+
const xml =
|
|
1159
|
+
beginRun +
|
|
1160
|
+
`<w:r><w:instrText xml:space="preserve"> ${escapeXml(node.instruction)} </w:instrText></w:r>` +
|
|
1161
|
+
`<w:r><w:fldChar w:fldCharType="separate"/></w:r>` +
|
|
1162
|
+
`<w:r><w:fldChar w:fldCharType="end"/></w:r>`;
|
|
1163
|
+
const boundaries = new Map<number, number>();
|
|
1164
|
+
boundaries.set(cursor, xmlOffset);
|
|
1165
|
+
boundaries.set(cursor + 1, xmlOffset + xml.length);
|
|
1166
|
+
return { xml, cursor: cursor + 1, boundaries };
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1072
1169
|
const xml = `<w:fldSimple w:instr="${escapeXmlAttribute(node.instruction)}"/>`;
|
|
1073
1170
|
const boundaries = new Map<number, number>();
|
|
1074
1171
|
boundaries.set(cursor, xmlOffset);
|
|
@@ -1144,41 +1241,128 @@ function serializeInlineNode(
|
|
|
1144
1241
|
}
|
|
1145
1242
|
|
|
1146
1243
|
/**
|
|
1147
|
-
*
|
|
1244
|
+
* Serialize a DrawingFrameNode.
|
|
1245
|
+
*
|
|
1246
|
+
* Primary path: emit preserved `content.rawXml` unchanged — byte-stable
|
|
1247
|
+
* round-trip. Every typed content variant (picture, shape, chart_preview,
|
|
1248
|
+
* smartart_preview, opaque) carries rawXml when the parser produced it.
|
|
1148
1249
|
*
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
*
|
|
1152
|
-
*
|
|
1153
|
-
* blipRef if needed. In practice picture content also originates from a
|
|
1154
|
-
* larger drawingXml slice — the parent `w:drawing` substring — which we
|
|
1155
|
-
* don't retain explicitly yet. When `content.rawXml` is absent we emit a
|
|
1156
|
-
* minimal wp:inline/wp:anchor envelope preserving extent + relationship id.
|
|
1250
|
+
* Reconstruction fallback (picture only): when a DrawingFrameNode lacks
|
|
1251
|
+
* rawXml (produced programmatically in memory — not yet a supported author
|
|
1252
|
+
* path, but possible), synthesise a schema-valid wp:inline or wp:anchor
|
|
1253
|
+
* envelope from the parsed AnchorGeometry + PictureContent fields.
|
|
1157
1254
|
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
* a
|
|
1255
|
+
* Schema-validity rules for reconstruction (Phase 3.1 B2+B3):
|
|
1256
|
+
* - Every `wp:docPr` gets a unique id — either the parsed `anchor.docPr.id`
|
|
1257
|
+
* or a fresh id minted from `state.nextDrawingFrameReconstructId`.
|
|
1258
|
+
* - Floating reconstruction (wp:anchor) emits the required children:
|
|
1259
|
+
* `wp:simplePos`, `wp:positionH`, `wp:positionV`, `wp:extent`,
|
|
1260
|
+
* `wp:effectExtent`, `wp:wrap*` — refuses reconstruction if positionH
|
|
1261
|
+
* or positionV is missing (returns empty string = defensive fail-loud).
|
|
1262
|
+
* - Inline reconstruction (wp:inline) needs only `wp:extent`, `wp:docPr`,
|
|
1263
|
+
* `wp:cNvGraphicFramePr`, `a:graphic`.
|
|
1264
|
+
*
|
|
1265
|
+
* Non-picture content (shape/chart/smartart/opaque) without rawXml returns
|
|
1266
|
+
* empty string — no reconstructor for those variants. In practice the parser
|
|
1267
|
+
* always sets rawXml for those; this is a defensive fail-loud path.
|
|
1161
1268
|
*/
|
|
1162
1269
|
function serializeDrawingFrameNode(
|
|
1163
1270
|
node: Extract<InlineNode, { type: "drawing_frame" }>,
|
|
1271
|
+
state: SerializationState,
|
|
1164
1272
|
): string {
|
|
1165
1273
|
const content = node.content;
|
|
1166
1274
|
if ("rawXml" in content && typeof content.rawXml === "string" && content.rawXml.length > 0) {
|
|
1167
1275
|
return wrapInlineRawXml(content.rawXml);
|
|
1168
1276
|
}
|
|
1169
|
-
// Picture content with no rawXml — reconstruct a minimal inline image so the
|
|
1170
|
-
// round-trip doesn't silently drop the image. Use the blipRef + anchor extent.
|
|
1171
1277
|
if (content.type === "picture") {
|
|
1172
|
-
|
|
1173
|
-
const embed = escapeXmlAttribute(content.blipRef);
|
|
1174
|
-
const envelope = node.anchor.display === "floating" ? "wp:anchor" : "wp:inline";
|
|
1175
|
-
return `<w:r><w:drawing><${envelope} xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="${widthEmu}" cy="${heightEmu}"/><wp:docPr id="1" name=""/><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic><pic:nvPicPr><pic:cNvPr id="0" name=""/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${embed}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${widthEmu}" cy="${heightEmu}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></${envelope}></w:drawing></w:r>`;
|
|
1278
|
+
return reconstructPictureFrame(node, content, state);
|
|
1176
1279
|
}
|
|
1177
|
-
// Shape/chart/smartart/opaque without rawXml —
|
|
1178
|
-
// happen in practice since the parser always sets rawXml.
|
|
1280
|
+
// Shape/chart/smartart/opaque without rawXml — no reconstructor; emit nothing.
|
|
1179
1281
|
return "";
|
|
1180
1282
|
}
|
|
1181
1283
|
|
|
1284
|
+
function reconstructPictureFrame(
|
|
1285
|
+
node: Extract<InlineNode, { type: "drawing_frame" }>,
|
|
1286
|
+
content: Extract<AnchorGeometryContent, { type: "picture" }>,
|
|
1287
|
+
state: SerializationState,
|
|
1288
|
+
): string {
|
|
1289
|
+
const { widthEmu, heightEmu } = node.anchor.extent;
|
|
1290
|
+
const embed = escapeXmlAttribute(content.blipRef);
|
|
1291
|
+
|
|
1292
|
+
// docPr uniqueness (B2): use parsed id; otherwise mint a fresh one.
|
|
1293
|
+
const parsedDocPr = node.anchor.docPr;
|
|
1294
|
+
const docPrId = parsedDocPr?.id ?? String(state.nextDrawingFrameReconstructId++);
|
|
1295
|
+
const docPrNameAttr = parsedDocPr?.name
|
|
1296
|
+
? ` name="${escapeXmlAttribute(parsedDocPr.name)}"`
|
|
1297
|
+
: ` name=""`;
|
|
1298
|
+
const docPrDescrAttr = parsedDocPr?.descr
|
|
1299
|
+
? ` descr="${escapeXmlAttribute(parsedDocPr.descr)}"`
|
|
1300
|
+
: "";
|
|
1301
|
+
const docPrXml = `<wp:docPr id="${escapeXmlAttribute(docPrId)}"${docPrNameAttr}${docPrDescrAttr}/>`;
|
|
1302
|
+
|
|
1303
|
+
const ns = nsUris(state.namespaceFlavor);
|
|
1304
|
+
const graphicXml = `<a:graphic><a:graphicData uri="${ns.pic}"><pic:pic><pic:nvPicPr><pic:cNvPr id="0" name=""/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${embed}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${widthEmu}" cy="${heightEmu}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic>`;
|
|
1305
|
+
|
|
1306
|
+
const drawingNs = `xmlns:a="${ns.a}" xmlns:pic="${ns.pic}" xmlns:wp="${ns.wp}"`;
|
|
1307
|
+
|
|
1308
|
+
if (node.anchor.display === "inline") {
|
|
1309
|
+
const extent = `<wp:extent cx="${widthEmu}" cy="${heightEmu}"/>`;
|
|
1310
|
+
return `<w:r><w:drawing><wp:inline ${drawingNs}>${extent}${docPrXml}<wp:cNvGraphicFramePr/>${graphicXml}</wp:inline></w:drawing></w:r>`;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Floating (wp:anchor): refuse reconstruction if required children missing.
|
|
1314
|
+
// Fail-loud: return empty string so consumer sees the picture was dropped
|
|
1315
|
+
// rather than Word seeing schema-invalid XML and rejecting / renumbering.
|
|
1316
|
+
if (!node.anchor.positionH || !node.anchor.positionV) return "";
|
|
1317
|
+
|
|
1318
|
+
const simplePos = `<wp:simplePos x="0" y="0"/>`;
|
|
1319
|
+
const positionH = buildPositionAxisXml("positionH", node.anchor.positionH);
|
|
1320
|
+
const positionV = buildPositionAxisXml("positionV", node.anchor.positionV);
|
|
1321
|
+
const extent = `<wp:extent cx="${widthEmu}" cy="${heightEmu}"/>`;
|
|
1322
|
+
const distMargins = node.anchor.distMargins ?? { top: 0, bottom: 0, left: 0, right: 0 };
|
|
1323
|
+
const effectExtent = `<wp:effectExtent l="${distMargins.left ?? 0}" t="${distMargins.top ?? 0}" r="${distMargins.right ?? 0}" b="${distMargins.bottom ?? 0}"/>`;
|
|
1324
|
+
const wrap = buildWrapElementXml(node.anchor.wrapMode);
|
|
1325
|
+
|
|
1326
|
+
const anchorAttrs = [
|
|
1327
|
+
`distT="${distMargins.top ?? 0}"`,
|
|
1328
|
+
`distB="${distMargins.bottom ?? 0}"`,
|
|
1329
|
+
`distL="${distMargins.left ?? 0}"`,
|
|
1330
|
+
`distR="${distMargins.right ?? 0}"`,
|
|
1331
|
+
`simplePos="${node.anchor.simplePos ? 1 : 0}"`,
|
|
1332
|
+
`relativeHeight="${node.anchor.relativeHeight ?? 0}"`,
|
|
1333
|
+
`behindDoc="${node.anchor.behindDoc ? 1 : 0}"`,
|
|
1334
|
+
`locked="0"`,
|
|
1335
|
+
`layoutInCell="${(node.anchor.layoutInCell ?? true) ? 1 : 0}"`,
|
|
1336
|
+
`allowOverlap="${(node.anchor.allowOverlap ?? true) ? 1 : 0}"`,
|
|
1337
|
+
].join(" ");
|
|
1338
|
+
|
|
1339
|
+
return `<w:r><w:drawing><wp:anchor ${drawingNs} ${anchorAttrs}>${simplePos}${positionH}${positionV}${extent}${effectExtent}${wrap}${docPrXml}<wp:cNvGraphicFramePr/>${graphicXml}</wp:anchor></w:drawing></w:r>`;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function buildPositionAxisXml(
|
|
1343
|
+
kind: "positionH" | "positionV",
|
|
1344
|
+
axis: { relativeFrom: string; align?: string; offset?: number },
|
|
1345
|
+
): string {
|
|
1346
|
+
const body = axis.align
|
|
1347
|
+
? `<wp:align>${escapeXmlAttribute(axis.align)}</wp:align>`
|
|
1348
|
+
: `<wp:posOffset>${axis.offset ?? 0}</wp:posOffset>`;
|
|
1349
|
+
return `<wp:${kind} relativeFrom="${escapeXmlAttribute(axis.relativeFrom || "column")}">${body}</wp:${kind}>`;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function buildWrapElementXml(mode: "none" | "square" | "tight" | "through" | "topAndBottom"): string {
|
|
1353
|
+
switch (mode) {
|
|
1354
|
+
case "none": return `<wp:wrapNone/>`;
|
|
1355
|
+
case "square": return `<wp:wrapSquare wrapText="bothSides"/>`;
|
|
1356
|
+
case "tight": return `<wp:wrapTight wrapText="bothSides"/>`;
|
|
1357
|
+
case "through": return `<wp:wrapThrough wrapText="bothSides"/>`;
|
|
1358
|
+
case "topAndBottom": return `<wp:wrapTopAndBottom/>`;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Narrow helper type used only in the reconstruction signature.
|
|
1363
|
+
type AnchorGeometryContent =
|
|
1364
|
+
Extract<InlineNode, { type: "drawing_frame" }>["content"];
|
|
1365
|
+
|
|
1182
1366
|
function serializeImageNode(
|
|
1183
1367
|
node: Extract<InlineNode, { type: "image" }>,
|
|
1184
1368
|
state: SerializationState,
|
|
@@ -1191,7 +1375,8 @@ function serializeImageNode(
|
|
|
1191
1375
|
const mediaItem = state.media.items[node.mediaId];
|
|
1192
1376
|
if (mediaItem?.relationshipId && state.existingRelationshipMap.has(mediaItem.relationshipId)) {
|
|
1193
1377
|
const altText = node.altText ?? mediaItem.altText ?? mediaItem.filename;
|
|
1194
|
-
|
|
1378
|
+
const ins = nsUris(state.namespaceFlavor);
|
|
1379
|
+
return `<w:r><w:drawing><wp:inline xmlns:a="${ins.a}" xmlns:pic="${ins.pic}" xmlns:wp="${ins.wp}"><wp:extent cx="9525" cy="9525"/><wp:docPr id="1" name="${escapeXmlAttribute(mediaItem.filename)}" descr="${escapeXmlAttribute(altText ?? "")}"/><wp:cNvGraphicFramePr/><a:graphic><a:graphicData uri="${ins.pic}"><pic:pic><pic:nvPicPr><pic:cNvPr id="0" name="${escapeXmlAttribute(mediaItem.filename)}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${escapeXmlAttribute(mediaItem.relationshipId)}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="9525" cy="9525"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
|
|
1195
1380
|
}
|
|
1196
1381
|
|
|
1197
1382
|
return serializeRun({
|
|
@@ -1458,26 +1643,31 @@ function serializeDocumentAttributes(
|
|
|
1458
1643
|
attributes: Record<string, string> | undefined,
|
|
1459
1644
|
content?: DocumentRootNode,
|
|
1460
1645
|
extensionAliases: readonly string[] = [],
|
|
1646
|
+
flavor: NamespaceFlavor = "transitional",
|
|
1461
1647
|
): string {
|
|
1648
|
+
const ns = nsUris(flavor);
|
|
1649
|
+
// Extension namespaces are Transitional-only (Microsoft-specific w14/w15/w16*).
|
|
1650
|
+
// In Strict mode they are omitted — any w14/w15 content in preserved XML
|
|
1651
|
+
// is technically non-conformant to ISO 29500 Strict anyway.
|
|
1462
1652
|
const extensionXmlns: Record<string, string> = {};
|
|
1463
|
-
|
|
1464
|
-
const
|
|
1465
|
-
|
|
1653
|
+
if (flavor !== "strict") {
|
|
1654
|
+
for (const alias of extensionAliases) {
|
|
1655
|
+
const uri = EXTENSION_NAMESPACE_URIS[alias];
|
|
1656
|
+
if (uri) extensionXmlns[`xmlns:${alias}`] = uri;
|
|
1657
|
+
}
|
|
1466
1658
|
}
|
|
1467
1659
|
|
|
1468
1660
|
const merged: Record<string, string> = {
|
|
1469
|
-
"xmlns:r":
|
|
1470
|
-
"xmlns:w":
|
|
1661
|
+
"xmlns:r": ns.r,
|
|
1662
|
+
"xmlns:w": ns.w,
|
|
1471
1663
|
...extensionXmlns,
|
|
1472
1664
|
...(attributes ?? {}),
|
|
1473
1665
|
};
|
|
1474
1666
|
|
|
1475
1667
|
// mc:Ignorable declares extension-namespace aliases that downstream readers
|
|
1476
1668
|
// may skip without dropping the document. Only emit when aliases are in play.
|
|
1477
|
-
if (extensionAliases.length > 0) {
|
|
1478
|
-
merged["xmlns:mc"] =
|
|
1479
|
-
merged["xmlns:mc"] ??
|
|
1480
|
-
"http://schemas.openxmlformats.org/markup-compatibility/2006";
|
|
1669
|
+
if (flavor !== "strict" && extensionAliases.length > 0) {
|
|
1670
|
+
merged["xmlns:mc"] = merged["xmlns:mc"] ?? ns.mc;
|
|
1481
1671
|
merged["mc:Ignorable"] = extensionAliases.join(" ");
|
|
1482
1672
|
}
|
|
1483
1673
|
|
|
@@ -11,6 +11,16 @@ export const WORD_NUMBERING_CONTENT_TYPE =
|
|
|
11
11
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
|
|
12
12
|
|
|
13
13
|
export function serializeNumberingXml(catalog: NumberingCatalog): string {
|
|
14
|
+
// ECMA-376 §17.9.19 — <w:numPicBullet> entries precede <w:abstractNum> and
|
|
15
|
+
// <w:num> in the numbering part. Emit verbatim rawXml so picture bullet data
|
|
16
|
+
// round-trips byte-for-byte regardless of VML vs DrawingML variant.
|
|
17
|
+
const picBullets = catalog.numPicBullets
|
|
18
|
+
? Object.values(catalog.numPicBullets)
|
|
19
|
+
.sort((a, b) => a.numPicBulletId.localeCompare(b.numPicBulletId, "en", { numeric: true }))
|
|
20
|
+
.map((b) => b.rawXml)
|
|
21
|
+
.join("")
|
|
22
|
+
: "";
|
|
23
|
+
|
|
14
24
|
const abstractDefinitions = Object.values(catalog.abstractDefinitions)
|
|
15
25
|
.filter((definition) => !isSyntheticDocxNullAbstractDefinition(definition))
|
|
16
26
|
.sort((left, right) =>
|
|
@@ -23,6 +33,7 @@ export function serializeNumberingXml(catalog: NumberingCatalog): string {
|
|
|
23
33
|
);
|
|
24
34
|
|
|
25
35
|
const body = [
|
|
36
|
+
picBullets,
|
|
26
37
|
...abstractDefinitions.map((definition) => serializeAbstractDefinition(definition)),
|
|
27
38
|
...instances.map((instance) => serializeInstance(instance)),
|
|
28
39
|
].join("");
|
|
@@ -35,6 +46,7 @@ export function serializeNumberingXml(catalog: NumberingCatalog): string {
|
|
|
35
46
|
|
|
36
47
|
export function hasSerializableNumberingEntries(catalog: NumberingCatalog): boolean {
|
|
37
48
|
return (
|
|
49
|
+
(catalog.numPicBullets !== undefined && Object.keys(catalog.numPicBullets).length > 0) ||
|
|
38
50
|
Object.values(catalog.abstractDefinitions).some(
|
|
39
51
|
(definition) => !isSyntheticDocxNullAbstractDefinition(definition),
|
|
40
52
|
) ||
|
|
@@ -112,13 +124,17 @@ function serializeLevel(
|
|
|
112
124
|
const isLegal = level.isLegalNumbering ? "<w:isLgl/>" : "";
|
|
113
125
|
const suffix = level.suffix ? `<w:suff w:val="${escapeXmlAttribute(level.suffix)}"/>` : "";
|
|
114
126
|
const lvlText = `<w:lvlText w:val="${escapeXmlAttribute(level.text)}"/>`;
|
|
127
|
+
// ECMA-376 CT_Lvl: lvlPicBulletId follows lvlText, precedes lvlJc.
|
|
128
|
+
const picBulletId = level.picBulletId
|
|
129
|
+
? `<w:lvlPicBulletId w:val="${escapeXmlAttribute(level.picBulletId)}"/>`
|
|
130
|
+
: "";
|
|
115
131
|
const justification = level.paragraphGeometry?.justification
|
|
116
132
|
? `<w:lvlJc w:val="${escapeXmlAttribute(level.paragraphGeometry.justification)}"/>`
|
|
117
133
|
: "";
|
|
118
134
|
const paragraphProperties = serializeLevelParagraphGeometry(level.paragraphGeometry);
|
|
119
135
|
const runProperties = buildRunPropertiesXml(level.runProperties);
|
|
120
136
|
|
|
121
|
-
return `<w:lvl w:ilvl="${clampIlvl(serializedLevel)}">${start}${numFmt}${lvlRestart}${paragraphStyle}${isLegal}${suffix}${lvlText}${justification}${paragraphProperties}${runProperties}</w:lvl>`;
|
|
137
|
+
return `<w:lvl w:ilvl="${clampIlvl(serializedLevel)}">${start}${numFmt}${lvlRestart}${paragraphStyle}${isLegal}${suffix}${lvlText}${picBulletId}${justification}${paragraphProperties}${runProperties}</w:lvl>`;
|
|
122
138
|
}
|
|
123
139
|
|
|
124
140
|
function serializeLevelOverride(
|
|
@@ -158,12 +174,16 @@ function serializeLevelOverride(
|
|
|
158
174
|
const text = level.text !== undefined
|
|
159
175
|
? `<w:lvlText w:val="${escapeXmlAttribute(level.text)}"/>`
|
|
160
176
|
: "";
|
|
177
|
+
// ECMA-376 CT_Lvl: lvlPicBulletId follows lvlText, precedes lvlJc.
|
|
178
|
+
const picBulletId = level.picBulletId
|
|
179
|
+
? `<w:lvlPicBulletId w:val="${escapeXmlAttribute(level.picBulletId)}"/>`
|
|
180
|
+
: "";
|
|
161
181
|
const justification = level.paragraphGeometry?.justification
|
|
162
182
|
? `<w:lvlJc w:val="${escapeXmlAttribute(level.paragraphGeometry.justification)}"/>`
|
|
163
183
|
: "";
|
|
164
184
|
const paragraphProperties = serializeLevelParagraphGeometry(level.paragraphGeometry);
|
|
165
185
|
const runProperties = buildRunPropertiesXml(level.runProperties);
|
|
166
|
-
const body = `${start}${format}${lvlRestart}${paragraphStyle}${isLegal}${suffix}${text}${justification}${paragraphProperties}${runProperties}`;
|
|
186
|
+
const body = `${start}${format}${lvlRestart}${paragraphStyle}${isLegal}${suffix}${text}${picBulletId}${justification}${paragraphProperties}${runProperties}`;
|
|
167
187
|
|
|
168
188
|
return body.length > 0
|
|
169
189
|
? `<w:lvl w:ilvl="${clampIlvl(serializedLevel)}">${body}</w:lvl>`
|