@stll/folio-core 0.33.0 → 0.33.2
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/dist/ai-edits/apply.d.ts +15 -0
- package/dist/ai-edits/apply.js +135 -24
- package/dist/ai-edits/headless.d.ts +47 -3
- package/dist/ai-edits/headless.js +53 -3
- package/dist/ai-edits/index.d.ts +1 -1
- package/dist/ai-edits/snapshot.d.ts +19 -1
- package/dist/ai-edits/snapshot.js +25 -3
- package/dist/ai-edits/table-geometry.d.ts +56 -0
- package/dist/ai-edits/table-geometry.js +217 -0
- package/dist/ai-edits/table-row-column-mutations.d.ts +34 -16
- package/dist/ai-edits/table-row-column-mutations.js +12 -4
- package/dist/ai-edits/table-template.d.ts +60 -0
- package/dist/ai-edits/table-template.js +176 -0
- package/dist/ai-edits/types.d.ts +7 -0
- package/dist/compare/__fixtures__/body-sequence.d.ts +86 -4
- package/dist/compare/__fixtures__/body-sequence.js +169 -12
- package/dist/compare/compare.d.ts +15 -8
- package/dist/compare/compare.js +138 -34
- package/dist/compare/plan.d.ts +31 -1
- package/dist/compare/plan.js +277 -28
- package/dist/compare/reproducible-package.d.ts +7 -3
- package/dist/compare/reproducible-package.js +20 -3
- package/dist/compare/types.d.ts +20 -3
- package/dist/compare/types.js +13 -1
- package/dist/compare/verification.d.ts +49 -2
- package/dist/compare/verification.js +115 -1
- package/dist/compat/eigenpal.d.ts +5 -5
- package/dist/compat/eigenpal.js +3 -3
- package/dist/document-operations.d.ts +10 -1
- package/dist/document-operations.js +3 -2
- package/dist/docx/appVersionNormalization.d.ts +49 -0
- package/dist/docx/appVersionNormalization.js +74 -0
- package/dist/docx/blockContentParser.js +3 -2
- package/dist/docx/drawingUtils.js +4 -3
- package/dist/docx/hyperlinkParser.d.ts +9 -1
- package/dist/docx/hyperlinkParser.js +19 -13
- package/dist/docx/paraIdRangeNormalization.d.ts +40 -0
- package/dist/docx/paraIdRangeNormalization.js +64 -0
- package/dist/docx/paragraphParser.js +82 -6
- package/dist/docx/revisionIdNormalization.d.ts +15 -1
- package/dist/docx/revisionIdNormalization.js +22 -4
- package/dist/docx/rezip.d.ts +13 -1
- package/dist/docx/rezip.js +80 -21
- package/dist/docx/runParser.js +9 -8
- package/dist/docx/sdtProperties.js +4 -3
- package/dist/docx/selectiveSave.js +7 -6
- package/dist/docx/serializer/commentSerializer.d.ts +1 -1
- package/dist/docx/serializer/commentSerializer.js +43 -19
- package/dist/docx/serializer/documentSerializer.d.ts +3 -1
- package/dist/docx/serializer/documentSerializer.js +42 -78
- package/dist/docx/serializer/fontTableSerializer.js +8 -5
- package/dist/docx/serializer/headerFooterSerializer.d.ts +4 -1
- package/dist/docx/serializer/headerFooterSerializer.js +34 -29
- package/dist/docx/serializer/noteSerializer.js +34 -30
- package/dist/docx/serializer/numberingSerializer.js +8 -4
- package/dist/docx/serializer/paragraphSerializer.js +45 -21
- package/dist/docx/serializer/partNamespaces.d.ts +78 -0
- package/dist/docx/serializer/partNamespaces.js +324 -0
- package/dist/docx/serializer/runSerializer.js +1 -1
- package/dist/docx/serializer/settingsSerializer.js +8 -2
- package/dist/docx/serializer/stylesSerializer.js +8 -5
- package/dist/docx/serializer/tableSerializer.js +107 -27
- package/dist/docx/serializer/themeSerializer.js +10 -2
- package/dist/docx/server/build.d.ts +1 -1
- package/dist/docx/strictValueEncodings.gen.d.ts +15 -0
- package/dist/docx/strictValueEncodings.gen.js +275 -0
- package/dist/docx/tableParser.d.ts +0 -6
- package/dist/docx/tableParser.js +52 -7
- package/dist/docx/transitionalSpelling.d.ts +23 -0
- package/dist/docx/transitionalSpelling.js +36 -0
- package/dist/docx/universalMeasure.d.ts +20 -0
- package/dist/docx/universalMeasure.js +32 -0
- package/dist/docx/verbatimCapture.d.ts +20 -0
- package/dist/docx/verbatimCapture.js +131 -0
- package/dist/docx/vmlImageParser.js +4 -3
- package/dist/docx/watermarkParser.js +4 -3
- package/dist/docx/xmlParser.d.ts +23 -2
- package/dist/docx/xmlParser.js +43 -11
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/model.d.ts +3 -3
- package/dist/model.js +2 -2
- package/dist/prosemirror/commands/comments.js +8 -2
- package/dist/prosemirror/containerFinalParagraph.d.ts +47 -0
- package/dist/prosemirror/containerFinalParagraph.js +92 -0
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +15 -4
- package/dist/prosemirror/conversion/toProseDoc.js +12 -3
- package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -0
- package/dist/prosemirror/schema/marks.d.ts +1 -1
- package/dist/prosemirror/schema/nodes.d.ts +22 -0
- package/dist/server.d.ts +1 -1
- package/dist/types/block-id.d.ts +12 -1
- package/dist/types/block-id.js +17 -1
- package/dist/utils/canonicalJson.d.ts +14 -0
- package/dist/utils/canonicalJson.js +19 -0
- package/dist/utils/formatToStyle.d.ts +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { OoxmlPrefix } from "@stll/docx-utils";
|
|
2
|
+
//#region src/docx/serializer/partNamespaces.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A namespace a rebuilt WordprocessingML part may declare.
|
|
5
|
+
*
|
|
6
|
+
* `ignorable` marks an extension that decorates baseline elements in place
|
|
7
|
+
* (`w14:paraId`, `w16du:dateUtc`, `wp14:sizeRelH`): a consumer that predates it
|
|
8
|
+
* has to be told through `mc:Ignorable` to skip it, or it rejects the part.
|
|
9
|
+
* Extensions reachable only inside an `mc:AlternateContent` choice or an
|
|
10
|
+
* `a:graphicData` payload (`wps`, `wpg`, `wne`, `cx*`) are already gated by
|
|
11
|
+
* that wrapper, so they stay out of the attribute, as mainstream producers
|
|
12
|
+
* leave them.
|
|
13
|
+
*/
|
|
14
|
+
type OoxmlNamespace = {
|
|
15
|
+
readonly uri: string;
|
|
16
|
+
readonly ignorable: boolean;
|
|
17
|
+
};
|
|
18
|
+
/** Packaging prefixes; they never appear inside a WordprocessingML part. */
|
|
19
|
+
type PackagingPrefix = "ct" | "pr";
|
|
20
|
+
/**
|
|
21
|
+
* A prefix folio can bind on a rebuilt part without consulting the source.
|
|
22
|
+
*
|
|
23
|
+
* Every prefix the parser resolves is a member, so a prefix the parser
|
|
24
|
+
* understands can always be written back, and {@link OOXML_NAMESPACES} is total
|
|
25
|
+
* over the union: a member with no entry, or an entry with no member, does not
|
|
26
|
+
* compile.
|
|
27
|
+
*/
|
|
28
|
+
type OoxmlNamespacePrefix = Exclude<OoxmlPrefix, PackagingPrefix> | "cx" | "cx1" | "cx2" | "cx3" | "cx4" | "cx5" | "cx6" | "cx7" | "cx8" | "aink" | "am3d" | "oel" | "w10" | "w16" | "w16cex" | "w16cid" | "w16du" | "w16sdtdh" | "w16se" | "wpi" | "wne";
|
|
29
|
+
/**
|
|
30
|
+
* Every namespace a rebuilt part can declare, in the order producers write them.
|
|
31
|
+
*
|
|
32
|
+
* URIs shared with the parser are taken from its table rather than retyped, so
|
|
33
|
+
* the two cannot drift.
|
|
34
|
+
*/
|
|
35
|
+
declare const OOXML_NAMESPACES: Readonly<Record<OoxmlNamespacePrefix, OoxmlNamespace>>;
|
|
36
|
+
declare const UnboundNamespacePrefixError_base: import("better-result").TaggedErrorClass<"UnboundNamespacePrefixError">;
|
|
37
|
+
/** A rebuilt part uses a prefix that resolves to no namespace URI. */
|
|
38
|
+
declare class UnboundNamespacePrefixError extends UnboundNamespacePrefixError_base<{
|
|
39
|
+
message: string;
|
|
40
|
+
partPath: string;
|
|
41
|
+
prefixes: readonly string[];
|
|
42
|
+
}> {}
|
|
43
|
+
/**
|
|
44
|
+
* The `xmlns:*` bindings a source part declared on its root element.
|
|
45
|
+
*
|
|
46
|
+
* Reused when the part is rebuilt so a producer-specific prefix that
|
|
47
|
+
* {@link OOXML_NAMESPACES} does not know keeps the URI the document gave it.
|
|
48
|
+
*/
|
|
49
|
+
declare const readRootNamespaceBindings: (partXml: string) => ReadonlyMap<string, string>;
|
|
50
|
+
/** The part a rebuild replaces: where it lives, and what its root element bound. */
|
|
51
|
+
type SourcePart = {
|
|
52
|
+
path: string;
|
|
53
|
+
bindings: ReadonlyMap<string, string>;
|
|
54
|
+
};
|
|
55
|
+
type PartElementOptions = {
|
|
56
|
+
/** Package-relative path of the part, reported when a prefix is unbound. */
|
|
57
|
+
partPath: string;
|
|
58
|
+
/** Qualified name of the root element, for example `w:document`. */
|
|
59
|
+
rootName: string;
|
|
60
|
+
/** Root-element attributes other than the namespace declarations. */
|
|
61
|
+
rootAttributes?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Prefixes declared whether or not the body uses them, so the part keeps a
|
|
64
|
+
* stable shape across saves and unmodeled extensions have somewhere to land.
|
|
65
|
+
*/
|
|
66
|
+
baselinePrefixes: readonly OoxmlNamespacePrefix[];
|
|
67
|
+
/** Root bindings of the part being replaced, when the save has the source. */
|
|
68
|
+
sourceBindings: ReadonlyMap<string, string> | undefined;
|
|
69
|
+
/** Assembled part content, everything between the root tags. */
|
|
70
|
+
body: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Wrap an assembled part body in a root element that declares every prefix the
|
|
74
|
+
* part uses and lists the ignorable ones in `mc:Ignorable`.
|
|
75
|
+
*/
|
|
76
|
+
declare const serializePartElement: ({ partPath, rootName, rootAttributes, baselinePrefixes, sourceBindings, body }: PartElementOptions) => string;
|
|
77
|
+
//#endregion
|
|
78
|
+
export { OOXML_NAMESPACES, OoxmlNamespace, OoxmlNamespacePrefix, PartElementOptions, SourcePart, UnboundNamespacePrefixError, readRootNamespaceBindings, serializePartElement };
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { toTransitionalNamespaceUri } from "../transitionalSpelling.js";
|
|
2
|
+
import { escapeXml } from "./xmlUtils.js";
|
|
3
|
+
import { TaggedError } from "better-result";
|
|
4
|
+
import { OOXML_NS } from "@stll/docx-utils";
|
|
5
|
+
//#region src/docx/serializer/partNamespaces.ts
|
|
6
|
+
/**
|
|
7
|
+
* Namespace declarations for parts folio rebuilds from the model.
|
|
8
|
+
*
|
|
9
|
+
* A rebuilt part carries content the parser preserved verbatim: raw property
|
|
10
|
+
* XML, text-box bodies, drawing extensions, unmodeled attributes. That content
|
|
11
|
+
* can use any prefix its producer bound on the source part's root, so a
|
|
12
|
+
* hand-maintained declaration list on the serializer silently drops bindings
|
|
13
|
+
* and emits an unbound prefix, which makes the part malformed and the package
|
|
14
|
+
* unopenable. The declarations are therefore derived: every prefix the
|
|
15
|
+
* assembled part actually uses is resolved through {@link OOXML_NAMESPACES},
|
|
16
|
+
* falling back to what the source part bound, and a prefix that resolves to
|
|
17
|
+
* nothing fails the save instead of being written unbound.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Every namespace a rebuilt part can declare, in the order producers write them.
|
|
21
|
+
*
|
|
22
|
+
* URIs shared with the parser are taken from its table rather than retyped, so
|
|
23
|
+
* the two cannot drift.
|
|
24
|
+
*/
|
|
25
|
+
const OOXML_NAMESPACES = {
|
|
26
|
+
wpc: {
|
|
27
|
+
uri: OOXML_NS.wpc,
|
|
28
|
+
ignorable: false
|
|
29
|
+
},
|
|
30
|
+
cx: {
|
|
31
|
+
uri: "http://schemas.microsoft.com/office/drawing/2014/chartex",
|
|
32
|
+
ignorable: false
|
|
33
|
+
},
|
|
34
|
+
cx1: {
|
|
35
|
+
uri: "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex",
|
|
36
|
+
ignorable: false
|
|
37
|
+
},
|
|
38
|
+
cx2: {
|
|
39
|
+
uri: "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex",
|
|
40
|
+
ignorable: false
|
|
41
|
+
},
|
|
42
|
+
cx3: {
|
|
43
|
+
uri: "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex",
|
|
44
|
+
ignorable: false
|
|
45
|
+
},
|
|
46
|
+
cx4: {
|
|
47
|
+
uri: "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex",
|
|
48
|
+
ignorable: false
|
|
49
|
+
},
|
|
50
|
+
cx5: {
|
|
51
|
+
uri: "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex",
|
|
52
|
+
ignorable: false
|
|
53
|
+
},
|
|
54
|
+
cx6: {
|
|
55
|
+
uri: "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex",
|
|
56
|
+
ignorable: false
|
|
57
|
+
},
|
|
58
|
+
cx7: {
|
|
59
|
+
uri: "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex",
|
|
60
|
+
ignorable: false
|
|
61
|
+
},
|
|
62
|
+
cx8: {
|
|
63
|
+
uri: "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex",
|
|
64
|
+
ignorable: false
|
|
65
|
+
},
|
|
66
|
+
mc: {
|
|
67
|
+
uri: OOXML_NS.mc,
|
|
68
|
+
ignorable: false
|
|
69
|
+
},
|
|
70
|
+
aink: {
|
|
71
|
+
uri: "http://schemas.microsoft.com/office/drawing/2016/ink",
|
|
72
|
+
ignorable: false
|
|
73
|
+
},
|
|
74
|
+
am3d: {
|
|
75
|
+
uri: "http://schemas.microsoft.com/office/drawing/2017/model3d",
|
|
76
|
+
ignorable: false
|
|
77
|
+
},
|
|
78
|
+
o: {
|
|
79
|
+
uri: OOXML_NS.o,
|
|
80
|
+
ignorable: false
|
|
81
|
+
},
|
|
82
|
+
oel: {
|
|
83
|
+
uri: "http://schemas.microsoft.com/office/2019/extlst",
|
|
84
|
+
ignorable: false
|
|
85
|
+
},
|
|
86
|
+
r: {
|
|
87
|
+
uri: OOXML_NS.r,
|
|
88
|
+
ignorable: false
|
|
89
|
+
},
|
|
90
|
+
m: {
|
|
91
|
+
uri: OOXML_NS.m,
|
|
92
|
+
ignorable: false
|
|
93
|
+
},
|
|
94
|
+
v: {
|
|
95
|
+
uri: OOXML_NS.v,
|
|
96
|
+
ignorable: false
|
|
97
|
+
},
|
|
98
|
+
wp14: {
|
|
99
|
+
uri: OOXML_NS.wp14,
|
|
100
|
+
ignorable: true
|
|
101
|
+
},
|
|
102
|
+
wp: {
|
|
103
|
+
uri: OOXML_NS.wp,
|
|
104
|
+
ignorable: false
|
|
105
|
+
},
|
|
106
|
+
w10: {
|
|
107
|
+
uri: "urn:schemas-microsoft-com:office:word",
|
|
108
|
+
ignorable: false
|
|
109
|
+
},
|
|
110
|
+
w: {
|
|
111
|
+
uri: OOXML_NS.w,
|
|
112
|
+
ignorable: false
|
|
113
|
+
},
|
|
114
|
+
w14: {
|
|
115
|
+
uri: OOXML_NS.w14,
|
|
116
|
+
ignorable: true
|
|
117
|
+
},
|
|
118
|
+
w15: {
|
|
119
|
+
uri: OOXML_NS.w15,
|
|
120
|
+
ignorable: true
|
|
121
|
+
},
|
|
122
|
+
w16cex: {
|
|
123
|
+
uri: "http://schemas.microsoft.com/office/word/2018/wordml/cex",
|
|
124
|
+
ignorable: true
|
|
125
|
+
},
|
|
126
|
+
w16cid: {
|
|
127
|
+
uri: "http://schemas.microsoft.com/office/word/2016/wordml/cid",
|
|
128
|
+
ignorable: true
|
|
129
|
+
},
|
|
130
|
+
w16: {
|
|
131
|
+
uri: "http://schemas.microsoft.com/office/word/2018/wordml",
|
|
132
|
+
ignorable: true
|
|
133
|
+
},
|
|
134
|
+
w16du: {
|
|
135
|
+
uri: "http://schemas.microsoft.com/office/word/2023/wordml/word16du",
|
|
136
|
+
ignorable: true
|
|
137
|
+
},
|
|
138
|
+
w16sdtdh: {
|
|
139
|
+
uri: "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash",
|
|
140
|
+
ignorable: true
|
|
141
|
+
},
|
|
142
|
+
w16se: {
|
|
143
|
+
uri: "http://schemas.microsoft.com/office/word/2015/wordml/symex",
|
|
144
|
+
ignorable: true
|
|
145
|
+
},
|
|
146
|
+
wpg: {
|
|
147
|
+
uri: OOXML_NS.wpg,
|
|
148
|
+
ignorable: false
|
|
149
|
+
},
|
|
150
|
+
wpi: {
|
|
151
|
+
uri: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
|
|
152
|
+
ignorable: false
|
|
153
|
+
},
|
|
154
|
+
wne: {
|
|
155
|
+
uri: "http://schemas.microsoft.com/office/word/2006/wordml",
|
|
156
|
+
ignorable: false
|
|
157
|
+
},
|
|
158
|
+
wps: {
|
|
159
|
+
uri: OOXML_NS.wps,
|
|
160
|
+
ignorable: false
|
|
161
|
+
},
|
|
162
|
+
a: {
|
|
163
|
+
uri: OOXML_NS.a,
|
|
164
|
+
ignorable: false
|
|
165
|
+
},
|
|
166
|
+
pic: {
|
|
167
|
+
uri: OOXML_NS.pic,
|
|
168
|
+
ignorable: false
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
/** {@link OOXML_NAMESPACES} keyed for lookup by a prefix read out of markup. */
|
|
172
|
+
const NAMESPACE_TABLE = new Map(Object.entries(OOXML_NAMESPACES));
|
|
173
|
+
/** A rebuilt part uses a prefix that resolves to no namespace URI. */
|
|
174
|
+
var UnboundNamespacePrefixError = class extends TaggedError("UnboundNamespacePrefixError") {};
|
|
175
|
+
const XMLNS_ATTRIBUTE_PREFIX = "xmlns:";
|
|
176
|
+
/** Bound by the XML specification itself; never declared. */
|
|
177
|
+
const RESERVED_PREFIX = "xml";
|
|
178
|
+
const isWhitespace = (character) => character === " " || character === " " || character === "\n" || character === "\r";
|
|
179
|
+
const isNameBoundary = (character) => isWhitespace(character) || character === ">" || character === "/" || character === "=";
|
|
180
|
+
const namePrefix = (qualifiedName) => {
|
|
181
|
+
const colon = qualifiedName.indexOf(":");
|
|
182
|
+
if (colon <= 0) return;
|
|
183
|
+
const prefix = qualifiedName.slice(0, colon);
|
|
184
|
+
return prefix === RESERVED_PREFIX ? void 0 : prefix;
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Collect prefixes from element and attribute names in one quote-aware pass.
|
|
188
|
+
*
|
|
189
|
+
* Attribute values are skipped rather than matched, so a URI or a text value
|
|
190
|
+
* containing a colon cannot be mistaken for a namespace prefix.
|
|
191
|
+
*/
|
|
192
|
+
const scanXmlNames = (xml, rootTagOnly) => {
|
|
193
|
+
const used = /* @__PURE__ */ new Set();
|
|
194
|
+
const declared = /* @__PURE__ */ new Map();
|
|
195
|
+
const length = xml.length;
|
|
196
|
+
let index = 0;
|
|
197
|
+
while (index < length) {
|
|
198
|
+
const tagStart = xml.indexOf("<", index);
|
|
199
|
+
if (tagStart === -1) break;
|
|
200
|
+
index = tagStart + 1;
|
|
201
|
+
if (xml.startsWith("?", index)) {
|
|
202
|
+
const end = xml.indexOf("?>", index);
|
|
203
|
+
if (end === -1) break;
|
|
204
|
+
index = end + 2;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (xml.startsWith("!--", index)) {
|
|
208
|
+
const end = xml.indexOf("-->", index);
|
|
209
|
+
if (end === -1) break;
|
|
210
|
+
index = end + 3;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (xml.startsWith("![CDATA[", index)) {
|
|
214
|
+
const end = xml.indexOf("]]>", index);
|
|
215
|
+
if (end === -1) break;
|
|
216
|
+
index = end + 3;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (xml.startsWith("!", index)) {
|
|
220
|
+
const end = xml.indexOf(">", index);
|
|
221
|
+
if (end === -1) break;
|
|
222
|
+
index = end + 1;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (xml.startsWith("/", index)) index += 1;
|
|
226
|
+
const nameStart = index;
|
|
227
|
+
while (index < length && !isNameBoundary(xml[index] ?? ">")) index += 1;
|
|
228
|
+
const elementPrefix = namePrefix(xml.slice(nameStart, index));
|
|
229
|
+
if (elementPrefix) used.add(elementPrefix);
|
|
230
|
+
while (index < length) {
|
|
231
|
+
const character = xml[index] ?? ">";
|
|
232
|
+
if (isWhitespace(character) || character === "/") {
|
|
233
|
+
index += 1;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (character === ">") {
|
|
237
|
+
index += 1;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
const attributeStart = index;
|
|
241
|
+
while (index < length && !isNameBoundary(xml[index] ?? ">")) index += 1;
|
|
242
|
+
const attributeName = xml.slice(attributeStart, index);
|
|
243
|
+
while (index < length && isWhitespace(xml[index] ?? ">")) index += 1;
|
|
244
|
+
let value = "";
|
|
245
|
+
if (xml[index] === "=") {
|
|
246
|
+
index += 1;
|
|
247
|
+
while (index < length && isWhitespace(xml[index] ?? ">")) index += 1;
|
|
248
|
+
const quote = xml[index];
|
|
249
|
+
if (quote === "\"" || quote === "'") {
|
|
250
|
+
const valueEnd = xml.indexOf(quote, index + 1);
|
|
251
|
+
if (valueEnd === -1) {
|
|
252
|
+
index = length;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
value = xml.slice(index + 1, valueEnd);
|
|
256
|
+
index = valueEnd + 1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (attributeName.startsWith(XMLNS_ATTRIBUTE_PREFIX)) {
|
|
260
|
+
declared.set(attributeName.slice(6), value);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const attributePrefix = namePrefix(attributeName);
|
|
264
|
+
if (attributePrefix) used.add(attributePrefix);
|
|
265
|
+
}
|
|
266
|
+
if (rootTagOnly) break;
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
used,
|
|
270
|
+
declared
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* The `xmlns:*` bindings a source part declared on its root element.
|
|
275
|
+
*
|
|
276
|
+
* Reused when the part is rebuilt so a producer-specific prefix that
|
|
277
|
+
* {@link OOXML_NAMESPACES} does not know keeps the URI the document gave it.
|
|
278
|
+
*/
|
|
279
|
+
const readRootNamespaceBindings = (partXml) => scanXmlNames(partXml, true).declared;
|
|
280
|
+
/**
|
|
281
|
+
* Wrap an assembled part body in a root element that declares every prefix the
|
|
282
|
+
* part uses and lists the ignorable ones in `mc:Ignorable`.
|
|
283
|
+
*/
|
|
284
|
+
const serializePartElement = ({ partPath, rootName, rootAttributes, baselinePrefixes, sourceBindings, body }) => {
|
|
285
|
+
const openingTag = `<${rootName}${rootAttributes ? ` ${rootAttributes}` : ""}`;
|
|
286
|
+
const fromRoot = scanXmlNames(`${openingTag}/>`, true);
|
|
287
|
+
const { used, declared } = scanXmlNames(body, false);
|
|
288
|
+
const required = /* @__PURE__ */ new Set([...fromRoot.used, ...used]);
|
|
289
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
290
|
+
for (const prefix of baselinePrefixes) bindings.set(prefix, OOXML_NAMESPACES[prefix].uri);
|
|
291
|
+
const unbound = [];
|
|
292
|
+
for (const prefix of required) {
|
|
293
|
+
if (bindings.has(prefix)) continue;
|
|
294
|
+
const sourceBinding = sourceBindings?.get(prefix);
|
|
295
|
+
const uri = NAMESPACE_TABLE.get(prefix)?.uri ?? (sourceBinding === void 0 ? void 0 : toTransitionalNamespaceUri(sourceBinding));
|
|
296
|
+
if (uri !== void 0) {
|
|
297
|
+
bindings.set(prefix, uri);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (!declared.has(prefix)) unbound.push(prefix);
|
|
301
|
+
}
|
|
302
|
+
if (unbound.length > 0) {
|
|
303
|
+
const prefixes = unbound.sort();
|
|
304
|
+
throw new UnboundNamespacePrefixError({
|
|
305
|
+
message: `${partPath} uses namespace prefixes bound to no URI: ${prefixes.join(", ")}`,
|
|
306
|
+
partPath,
|
|
307
|
+
prefixes
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if ([...bindings.keys()].some((prefix) => NAMESPACE_TABLE.get(prefix)?.ignorable === true)) bindings.set("mc", OOXML_NAMESPACES.mc.uri);
|
|
311
|
+
const baseline = new Set(baselinePrefixes);
|
|
312
|
+
const extra = new Set([...bindings.keys()].filter((prefix) => !baseline.has(prefix)));
|
|
313
|
+
const orderedPrefixes = [
|
|
314
|
+
...baselinePrefixes,
|
|
315
|
+
...[...NAMESPACE_TABLE.keys()].filter((prefix) => extra.has(prefix)),
|
|
316
|
+
...[...extra].filter((prefix) => !NAMESPACE_TABLE.has(prefix)).sort()
|
|
317
|
+
];
|
|
318
|
+
const attributes = orderedPrefixes.map((prefix) => `xmlns:${prefix}="${escapeXml(bindings.get(prefix))}"`);
|
|
319
|
+
const ignorable = orderedPrefixes.filter((prefix) => NAMESPACE_TABLE.get(prefix)?.ignorable === true);
|
|
320
|
+
if (ignorable.length > 0) attributes.push(`mc:Ignorable="${ignorable.join(" ")}"`);
|
|
321
|
+
return `${openingTag} ${attributes.join(" ")}>${body}</${rootName}>`;
|
|
322
|
+
};
|
|
323
|
+
//#endregion
|
|
324
|
+
export { OOXML_NAMESPACES, UnboundNamespacePrefixError, readRootNamespaceBindings, serializePartElement };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { serializePartElement } from "./partNamespaces.js";
|
|
1
2
|
import { escapeXml, intAttr } from "./xmlUtils.js";
|
|
2
3
|
//#region src/docx/serializer/settingsSerializer.ts
|
|
3
|
-
const W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
4
4
|
const serializeSettingsXml = (settings) => {
|
|
5
5
|
const parts = [`<w:defaultTabStop w:val="${intAttr(settings.defaultTabStop)}"/>`];
|
|
6
6
|
if (settings.evenAndOddHeaders) parts.push("<w:evenAndOddHeaders/>");
|
|
@@ -11,7 +11,13 @@ const serializeSettingsXml = (settings) => {
|
|
|
11
11
|
if (settings.themeFontLang.bidi) attrs.push(`w:bidi="${escapeXml(settings.themeFontLang.bidi)}"`);
|
|
12
12
|
if (attrs.length > 0) parts.push(`<w:themeFontLang ${attrs.join(" ")}/>`);
|
|
13
13
|
}
|
|
14
|
-
return
|
|
14
|
+
return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + serializePartElement({
|
|
15
|
+
partPath: "word/settings.xml",
|
|
16
|
+
rootName: "w:settings",
|
|
17
|
+
baselinePrefixes: ["w"],
|
|
18
|
+
sourceBindings: void 0,
|
|
19
|
+
body: parts.join("")
|
|
20
|
+
});
|
|
15
21
|
};
|
|
16
22
|
//#endregion
|
|
17
23
|
export { serializeSettingsXml };
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { serializeParagraphFormatting } from "./paragraphSerializer.js";
|
|
2
|
+
import { serializePartElement } from "./partNamespaces.js";
|
|
2
3
|
import { serializeTextFormatting } from "./runSerializer.js";
|
|
3
4
|
import { serializeTableCellFormatting, serializeTableFormatting, serializeTableRowFormatting } from "./tableSerializer.js";
|
|
4
5
|
import { escapeXml } from "./xmlUtils.js";
|
|
5
6
|
//#region src/docx/serializer/stylesSerializer.ts
|
|
6
|
-
const W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
7
7
|
const serializeStylesXml = (definitions) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + serializePartElement({
|
|
9
|
+
partPath: "word/styles.xml",
|
|
10
|
+
rootName: "w:styles",
|
|
11
|
+
baselinePrefixes: ["w"],
|
|
12
|
+
sourceBindings: void 0,
|
|
13
|
+
body: `${serializeDocumentDefaults(definitions)}${serializeLatentStyles(definitions)}${definitions.styles.map(serializeStyle).join("")}`
|
|
14
|
+
});
|
|
12
15
|
};
|
|
13
16
|
const serializeDocumentDefaults = (definitions) => {
|
|
14
17
|
return `<w:docDefaults><w:rPrDefault>${serializeTextFormatting(definitions.docDefaults?.rPr) || "<w:rPr/>"}</w:rPrDefault><w:pPrDefault>${serializeParagraphFormatting(definitions.docDefaults?.pPr) || "<w:pPr/>"}</w:pPrDefault></w:docDefaults>`;
|
|
@@ -1,8 +1,74 @@
|
|
|
1
|
+
import { canonicalJson } from "../../utils/canonicalJson.js";
|
|
1
2
|
import { isValidHexColor } from "../../utils/colorResolver.js";
|
|
3
|
+
import { parseTableCellProperties, parseTableGrid, parseTableProperties, parseTableRowProperties } from "../tableParser.js";
|
|
4
|
+
import { OOXML_NAMESPACE_SCOPE, parseXml } from "../xmlParser.js";
|
|
2
5
|
import { serializeBorder } from "./borderSerializer.js";
|
|
3
6
|
import { escapeXml, intAttr } from "./xmlUtils.js";
|
|
4
7
|
import { normalizeRevisionId } from "@stll/docx-core/model";
|
|
5
8
|
//#region src/docx/serializer/tableSerializer.ts
|
|
9
|
+
/**
|
|
10
|
+
* The captured element with the revision children written back into it.
|
|
11
|
+
*
|
|
12
|
+
* The capture holds properties only, so a revision the model carries — a row
|
|
13
|
+
* marked inserted, a `w:tcPrChange` — is appended here rather than forcing the
|
|
14
|
+
* whole element to be rebuilt and losing everything the model does not cover.
|
|
15
|
+
*/
|
|
16
|
+
const withRevisionChildren = (sourceXml, revisions) => {
|
|
17
|
+
const written = revisions.join("");
|
|
18
|
+
if (written.length === 0) return sourceXml;
|
|
19
|
+
if (sourceXml.endsWith("/>")) {
|
|
20
|
+
const name = sourceXml.slice(1, sourceXml.length - 2).split(/[\s/]/u)[0] ?? "";
|
|
21
|
+
return `${sourceXml.slice(0, -2)}>${written}</${name}>`;
|
|
22
|
+
}
|
|
23
|
+
const close = sourceXml.lastIndexOf("</");
|
|
24
|
+
return close === -1 ? sourceXml : `${sourceXml.slice(0, close)}${written}${sourceXml.slice(close)}`;
|
|
25
|
+
};
|
|
26
|
+
/** `w:trPr/w:ins` | `w:trPr/w:del`, the row's own structural revision. */
|
|
27
|
+
const rowStructuralChangeXml = (change) => {
|
|
28
|
+
if (change?.type === "tableRowInsertion") return [`<w:ins ${serializeTrackedChangeAttributes(change.info)}/>`];
|
|
29
|
+
if (change?.type === "tableRowDeletion") return [`<w:del ${serializeTrackedChangeAttributes(change.info)}/>`];
|
|
30
|
+
return [];
|
|
31
|
+
};
|
|
32
|
+
/** `w:tcPr/w:cellIns` | `w:cellDel` | `w:cellMerge`, the cell's own. */
|
|
33
|
+
const cellStructuralChangeXml = (change) => {
|
|
34
|
+
if (change?.type === "tableCellInsertion") return [`<w:cellIns ${serializeTrackedChangeAttributes(change.info)}/>`];
|
|
35
|
+
if (change?.type === "tableCellDeletion") return [`<w:cellDel ${serializeTrackedChangeAttributes(change.info)}/>`];
|
|
36
|
+
if (change?.type !== "tableCellMerge") return [];
|
|
37
|
+
const attrs = [serializeTrackedChangeAttributes(change.info)];
|
|
38
|
+
if (change.verticalMerge) attrs.push(`w:vMerge="${change.verticalMerge === "continue" ? "cont" : "rest"}"`);
|
|
39
|
+
if (change.verticalMergeOriginal) attrs.push(`w:vMergeOrig="${change.verticalMergeOriginal === "continue" ? "cont" : "rest"}"`);
|
|
40
|
+
return [`<w:cellMerge ${attrs.join(" ")}/>`];
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* A property set without the elements kept beside it. Only the typed values
|
|
44
|
+
* decide whether a capture still describes the node; the captures themselves
|
|
45
|
+
* are what is being decided about.
|
|
46
|
+
*/
|
|
47
|
+
const withoutCaptures = (formatting) => {
|
|
48
|
+
const { sourceXml: _source, gridSourceXml: _grid, ...rest } = formatting ?? {};
|
|
49
|
+
return rest;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The element a formatting object was parsed from, when it still describes it.
|
|
53
|
+
*
|
|
54
|
+
* `sourceXml` is only usable while the typed values around it are the ones it
|
|
55
|
+
* was parsed into. Anything may edit a `Document` in place — a host building
|
|
56
|
+
* one by hand, a migration, a test — and a capture written back over a changed
|
|
57
|
+
* model would silently discard the change. So the capture is re-parsed and
|
|
58
|
+
* checked rather than trusted: it is written only when parsing it reproduces
|
|
59
|
+
* the formatting it sits on.
|
|
60
|
+
*
|
|
61
|
+
* The re-parse runs under {@link OOXML_NAMESPACE_SCOPE} because a captured
|
|
62
|
+
* fragment carries no `xmlns` of its own. A document that binds the
|
|
63
|
+
* WordprocessingML prefix differently fails the check and is rebuilt from the
|
|
64
|
+
* model, which is what happened to every document before the capture existed.
|
|
65
|
+
*/
|
|
66
|
+
const verifiedSourceXml = (formatting, parse) => {
|
|
67
|
+
if (formatting === void 0 || formatting.sourceXml === void 0) return null;
|
|
68
|
+
const { sourceXml } = formatting;
|
|
69
|
+
const reparsed = parse(parseXml(sourceXml, OOXML_NAMESPACE_SCOPE).elements?.[0] ?? null);
|
|
70
|
+
return canonicalJson(withoutCaptures(reparsed)) === canonicalJson(withoutCaptures(formatting)) ? sourceXml : null;
|
|
71
|
+
};
|
|
6
72
|
function normalizeTrackedChangeInfo(info) {
|
|
7
73
|
const normalizedId = normalizeRevisionId(info.id);
|
|
8
74
|
const authorCandidate = typeof info.author === "string" ? info.author.trim() : "";
|
|
@@ -18,6 +84,7 @@ function serializeTrackedChangeAttributes(info, rsid) {
|
|
|
18
84
|
const normalized = normalizeTrackedChangeInfo(info);
|
|
19
85
|
const attrs = [`w:id="${normalized.id}"`, `w:author="${escapeXml(normalized.author)}"`];
|
|
20
86
|
if (normalized.date) attrs.push(`w:date="${escapeXml(normalized.date)}"`);
|
|
87
|
+
if (info.utcDate) attrs.push(`${info.utcDate.attribute}="${escapeXml(info.utcDate.value)}"`);
|
|
21
88
|
if (rsid && rsid.trim().length > 0) attrs.push(`w:rsid="${escapeXml(rsid.trim())}"`);
|
|
22
89
|
return attrs.join(" ");
|
|
23
90
|
}
|
|
@@ -129,11 +196,14 @@ function serializeFloatingTableProperties(floating) {
|
|
|
129
196
|
* Serialize table formatting properties (w:tblPr)
|
|
130
197
|
*/
|
|
131
198
|
function serializeTableFormatting(formatting, propertyChanges) {
|
|
199
|
+
const tableSource = verifiedSourceXml(formatting, parseTableProperties);
|
|
200
|
+
if (tableSource !== null) return withRevisionChildren(tableSource, (propertyChanges ?? []).map((change) => serializeTablePropertyChange(change)));
|
|
132
201
|
const parts = [];
|
|
133
202
|
if (formatting) {
|
|
134
203
|
if (formatting.styleId) parts.push(`<w:tblStyle w:val="${escapeXml(formatting.styleId)}"/>`);
|
|
135
204
|
const floatingXml = serializeFloatingTableProperties(formatting.floating);
|
|
136
205
|
if (floatingXml) parts.push(floatingXml);
|
|
206
|
+
if (formatting.overlap) parts.push(`<w:tblOverlap w:val="${formatting.overlap}"/>`);
|
|
137
207
|
if (formatting.bidi !== void 0) parts.push(formatting.bidi ? "<w:bidiVisual/>" : "<w:bidiVisual w:val=\"0\"/>");
|
|
138
208
|
const widthXml = serializeMeasurement(formatting.width, "tblW");
|
|
139
209
|
if (widthXml) parts.push(widthXml);
|
|
@@ -144,14 +214,13 @@ function serializeTableFormatting(formatting, propertyChanges) {
|
|
|
144
214
|
if (indentXml) parts.push(indentXml);
|
|
145
215
|
const bordersXml = serializeTableBorders(formatting.borders, "tblBorders");
|
|
146
216
|
if (bordersXml) parts.push(bordersXml);
|
|
147
|
-
const marginsXml = serializeCellMargins(formatting.cellMargins, "tblCellMar");
|
|
148
|
-
if (marginsXml) parts.push(marginsXml);
|
|
149
|
-
if (formatting.layout) parts.push(`<w:tblLayout w:type="${formatting.layout}"/>`);
|
|
150
217
|
const shadingXml = serializeShading(formatting.shading);
|
|
151
218
|
if (shadingXml) parts.push(shadingXml);
|
|
219
|
+
if (formatting.layout) parts.push(`<w:tblLayout w:type="${formatting.layout}"/>`);
|
|
220
|
+
const marginsXml = serializeCellMargins(formatting.cellMargins, "tblCellMar");
|
|
221
|
+
if (marginsXml) parts.push(marginsXml);
|
|
152
222
|
const lookXml = serializeTableLook(formatting.look);
|
|
153
223
|
if (lookXml) parts.push(lookXml);
|
|
154
|
-
if (formatting.overlap) parts.push(`<w:tblOverlap w:val="${formatting.overlap}"/>`);
|
|
155
224
|
}
|
|
156
225
|
if (propertyChanges && propertyChanges.length > 0) parts.push(...propertyChanges.map((change) => serializeTablePropertyChange(change)));
|
|
157
226
|
if (parts.length === 0) return "";
|
|
@@ -170,6 +239,8 @@ function serializeTablePropertyChange(change) {
|
|
|
170
239
|
* Serialize table row formatting properties (w:trPr)
|
|
171
240
|
*/
|
|
172
241
|
function serializeTableRowFormatting(formatting, propertyChanges, structuralChange) {
|
|
242
|
+
const rowSource = verifiedSourceXml(formatting, parseTableRowProperties);
|
|
243
|
+
if (rowSource !== null) return withRevisionChildren(rowSource, [...rowStructuralChangeXml(structuralChange), ...(propertyChanges ?? []).map((change) => serializeTableRowPropertyChange(change))]);
|
|
173
244
|
const parts = [];
|
|
174
245
|
if (formatting) {
|
|
175
246
|
const cnfStyleXml = serializeConditionalFormatStyle(formatting.conditionalFormat);
|
|
@@ -188,10 +259,7 @@ function serializeTableRowFormatting(formatting, propertyChanges, structuralChan
|
|
|
188
259
|
if (formatting.justification) parts.push(`<w:jc w:val="${formatting.justification}"/>`);
|
|
189
260
|
if (formatting.hidden) parts.push("<w:hidden/>");
|
|
190
261
|
}
|
|
191
|
-
|
|
192
|
-
if (structuralChange.type === "tableRowInsertion") parts.push(`<w:ins ${serializeTrackedChangeAttributes(structuralChange.info)}/>`);
|
|
193
|
-
else if (structuralChange.type === "tableRowDeletion") parts.push(`<w:del ${serializeTrackedChangeAttributes(structuralChange.info)}/>`);
|
|
194
|
-
}
|
|
262
|
+
parts.push(...rowStructuralChangeXml(structuralChange));
|
|
195
263
|
if (propertyChanges && propertyChanges.length > 0) parts.push(...propertyChanges.map((change) => serializeTableRowPropertyChange(change)));
|
|
196
264
|
if (parts.length === 0) return "";
|
|
197
265
|
return `<w:trPr>${parts.join("")}</w:trPr>`;
|
|
@@ -231,6 +299,8 @@ function serializeConditionalFormatStyle(style) {
|
|
|
231
299
|
* Serialize table cell formatting properties (w:tcPr)
|
|
232
300
|
*/
|
|
233
301
|
function serializeTableCellFormatting(formatting, propertyChanges, structuralChange) {
|
|
302
|
+
const cellSource = verifiedSourceXml(formatting, parseTableCellProperties);
|
|
303
|
+
if (cellSource !== null) return withRevisionChildren(cellSource, [...cellStructuralChangeXml(structuralChange), ...(propertyChanges ?? []).map((change) => serializeTableCellPropertyChange(change))]);
|
|
234
304
|
const parts = [];
|
|
235
305
|
if (formatting) {
|
|
236
306
|
const cnfStyleXml = serializeConditionalFormatStyle(formatting.conditionalFormat);
|
|
@@ -253,16 +323,7 @@ function serializeTableCellFormatting(formatting, propertyChanges, structuralCha
|
|
|
253
323
|
if (formatting.hideMark === true) parts.push("<w:hideMark/>");
|
|
254
324
|
else if (formatting.hideMark === false) parts.push("<w:hideMark w:val=\"off\"/>");
|
|
255
325
|
}
|
|
256
|
-
|
|
257
|
-
if (structuralChange.type === "tableCellInsertion") parts.push(`<w:cellIns ${serializeTrackedChangeAttributes(structuralChange.info)}/>`);
|
|
258
|
-
else if (structuralChange.type === "tableCellDeletion") parts.push(`<w:cellDel ${serializeTrackedChangeAttributes(structuralChange.info)}/>`);
|
|
259
|
-
else if (structuralChange.type === "tableCellMerge") {
|
|
260
|
-
const attrs = [serializeTrackedChangeAttributes(structuralChange.info)];
|
|
261
|
-
if (structuralChange.verticalMerge) attrs.push(`w:vMerge="${structuralChange.verticalMerge === "continue" ? "cont" : "rest"}"`);
|
|
262
|
-
if (structuralChange.verticalMergeOriginal) attrs.push(`w:vMergeOrig="${structuralChange.verticalMergeOriginal === "continue" ? "cont" : "rest"}"`);
|
|
263
|
-
parts.push(`<w:cellMerge ${attrs.join(" ")}/>`);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
326
|
+
parts.push(...cellStructuralChangeXml(structuralChange));
|
|
266
327
|
if (propertyChanges && propertyChanges.length > 0) parts.push(...propertyChanges.map((change) => serializeTableCellPropertyChange(change)));
|
|
267
328
|
if (parts.length === 0) return "";
|
|
268
329
|
return `<w:tcPr>${parts.join("")}</w:tcPr>`;
|
|
@@ -277,11 +338,34 @@ function serializeTableCellPropertyChange(change) {
|
|
|
277
338
|
return `<w:tcPrChange ${attrs}>${previousTcPrInner.length > 0 ? `<w:tcPr>${previousTcPrInner}</w:tcPr>` : "<w:tcPr/>"}</w:tcPrChange>`;
|
|
278
339
|
}
|
|
279
340
|
/**
|
|
280
|
-
*
|
|
341
|
+
* Columns the grid has to declare: the widest row's span total, because a grid
|
|
342
|
+
* narrower than a row leaves cells with no column to sit in.
|
|
281
343
|
*/
|
|
282
|
-
function
|
|
283
|
-
|
|
284
|
-
|
|
344
|
+
function gridColumnCount(table) {
|
|
345
|
+
let widest = 0;
|
|
346
|
+
for (const row of table.rows) {
|
|
347
|
+
let columns = 0;
|
|
348
|
+
for (const cell of row.cells) columns += cell.formatting?.gridSpan ?? 1;
|
|
349
|
+
widest = Math.max(widest, columns);
|
|
350
|
+
}
|
|
351
|
+
return widest;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Serialize table grid (w:tblGrid).
|
|
355
|
+
*
|
|
356
|
+
* `w:tblGrid` is required on every `w:tbl` and `w:w` is optional on a
|
|
357
|
+
* `w:gridCol`, so a table whose column widths were never measured — one the
|
|
358
|
+
* comparison creates, say — still declares a grid, just without widths.
|
|
359
|
+
*/
|
|
360
|
+
function serializeTableGrid(table) {
|
|
361
|
+
const columnWidths = table.columnWidths;
|
|
362
|
+
const gridSourceXml = table.formatting?.gridSourceXml;
|
|
363
|
+
if (gridSourceXml !== void 0) {
|
|
364
|
+
if (canonicalJson(parseTableGrid(parseXml(gridSourceXml, OOXML_NAMESPACE_SCOPE).elements?.[0] ?? null)) === canonicalJson(columnWidths)) return gridSourceXml;
|
|
365
|
+
}
|
|
366
|
+
if (columnWidths && columnWidths.length > 0) return `<w:tblGrid>${columnWidths.map((w) => `<w:gridCol w:w="${intAttr(w)}"/>`).join("")}</w:tblGrid>`;
|
|
367
|
+
const columns = gridColumnCount(table);
|
|
368
|
+
return columns === 0 ? "<w:tblGrid/>" : `<w:tblGrid>${"<w:gridCol/>".repeat(columns)}</w:tblGrid>`;
|
|
285
369
|
}
|
|
286
370
|
/**
|
|
287
371
|
* Serialize cell content (paragraphs, nested tables)
|
|
@@ -320,11 +404,7 @@ function serializeTableRow(row, serializeParagraph) {
|
|
|
320
404
|
* @returns XML string for the table
|
|
321
405
|
*/
|
|
322
406
|
function serializeTable(table, serializeParagraph) {
|
|
323
|
-
const parts = [];
|
|
324
|
-
const tblPrXml = serializeTableFormatting(table.formatting, table.propertyChanges);
|
|
325
|
-
if (tblPrXml) parts.push(tblPrXml);
|
|
326
|
-
const tblGridXml = serializeTableGrid(table.columnWidths);
|
|
327
|
-
if (tblGridXml) parts.push(tblGridXml);
|
|
407
|
+
const parts = [serializeTableFormatting(table.formatting, table.propertyChanges) || "<w:tblPr/>", serializeTableGrid(table)];
|
|
328
408
|
for (const row of table.rows) parts.push(serializeTableRow(row, serializeParagraph));
|
|
329
409
|
return `<w:tbl>${parts.join("")}</w:tbl>`;
|
|
330
410
|
}
|