@stll/folio-core 0.33.1 → 0.34.0

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.
Files changed (121) hide show
  1. package/dist/ai-edits/apply.d.ts +15 -0
  2. package/dist/ai-edits/apply.js +271 -48
  3. package/dist/ai-edits/headless.d.ts +47 -3
  4. package/dist/ai-edits/headless.js +53 -3
  5. package/dist/ai-edits/index.d.ts +1 -1
  6. package/dist/ai-edits/snapshot.d.ts +19 -1
  7. package/dist/ai-edits/snapshot.js +49 -5
  8. package/dist/ai-edits/table-geometry.d.ts +56 -0
  9. package/dist/ai-edits/table-geometry.js +217 -0
  10. package/dist/ai-edits/table-row-column-mutations.d.ts +34 -16
  11. package/dist/ai-edits/table-row-column-mutations.js +12 -4
  12. package/dist/ai-edits/table-template.d.ts +60 -0
  13. package/dist/ai-edits/table-template.js +176 -0
  14. package/dist/ai-edits/types.d.ts +13 -6
  15. package/dist/compare/__fixtures__/body-sequence.d.ts +61 -2
  16. package/dist/compare/__fixtures__/body-sequence.js +94 -6
  17. package/dist/compare/compare.d.ts +14 -7
  18. package/dist/compare/compare.js +136 -38
  19. package/dist/compare/formatting.d.ts +1 -1
  20. package/dist/compare/formatting.js +38 -9
  21. package/dist/compare/plan.d.ts +31 -1
  22. package/dist/compare/plan.js +233 -66
  23. package/dist/compare/types.d.ts +9 -7
  24. package/dist/compare/types.js +7 -5
  25. package/dist/compare/verification.d.ts +31 -12
  26. package/dist/compare/verification.js +106 -18
  27. package/dist/compat/eigenpal.d.ts +4 -4
  28. package/dist/compat/eigenpal.js +2 -2
  29. package/dist/controller/headerFooterEditorManager.js +11 -9
  30. package/dist/controller/layoutPipeline.js +24 -3
  31. package/dist/display-list/build/watermarkPrimitives.js +15 -3
  32. package/dist/display-list/primitives.d.ts +1 -1
  33. package/dist/document-operations.d.ts +10 -1
  34. package/dist/document-operations.js +33 -5
  35. package/dist/docx/appVersionNormalization.d.ts +49 -0
  36. package/dist/docx/appVersionNormalization.js +74 -0
  37. package/dist/docx/blockContentParser.js +3 -2
  38. package/dist/docx/drawingUtils.js +4 -3
  39. package/dist/docx/headerFooterParser.js +8 -14
  40. package/dist/docx/paragraphParser.js +48 -2
  41. package/dist/docx/rezip.d.ts +10 -4
  42. package/dist/docx/rezip.js +54 -18
  43. package/dist/docx/runParser.js +9 -8
  44. package/dist/docx/sdtProperties.js +4 -3
  45. package/dist/docx/selectiveSave.js +7 -6
  46. package/dist/docx/serializer/commentSerializer.d.ts +1 -1
  47. package/dist/docx/serializer/commentSerializer.js +43 -19
  48. package/dist/docx/serializer/documentSerializer.d.ts +3 -1
  49. package/dist/docx/serializer/documentSerializer.js +42 -78
  50. package/dist/docx/serializer/fontTableSerializer.js +8 -5
  51. package/dist/docx/serializer/headerFooterSerializer.d.ts +4 -1
  52. package/dist/docx/serializer/headerFooterSerializer.js +55 -31
  53. package/dist/docx/serializer/noteSerializer.js +34 -30
  54. package/dist/docx/serializer/numberingSerializer.js +8 -4
  55. package/dist/docx/serializer/paragraphSerializer.d.ts +1 -1
  56. package/dist/docx/serializer/paragraphSerializer.js +22 -12
  57. package/dist/docx/serializer/partNamespaces.d.ts +78 -0
  58. package/dist/docx/serializer/partNamespaces.js +324 -0
  59. package/dist/docx/serializer/runSerializer.js +1 -1
  60. package/dist/docx/serializer/settingsSerializer.js +8 -2
  61. package/dist/docx/serializer/stylesSerializer.js +8 -5
  62. package/dist/docx/serializer/tableSerializer.js +83 -18
  63. package/dist/docx/serializer/themeSerializer.js +10 -2
  64. package/dist/docx/server/build.d.ts +1 -1
  65. package/dist/docx/settingsParser.js +3 -0
  66. package/dist/docx/strictValueEncodings.gen.d.ts +15 -0
  67. package/dist/docx/strictValueEncodings.gen.js +275 -0
  68. package/dist/docx/tableParser.d.ts +0 -6
  69. package/dist/docx/tableParser.js +52 -7
  70. package/dist/docx/transitionalSpelling.d.ts +23 -0
  71. package/dist/docx/transitionalSpelling.js +36 -0
  72. package/dist/docx/universalMeasure.d.ts +20 -0
  73. package/dist/docx/universalMeasure.js +32 -0
  74. package/dist/docx/verbatimCapture.d.ts +20 -0
  75. package/dist/docx/verbatimCapture.js +131 -0
  76. package/dist/docx/vmlImageParser.js +4 -3
  77. package/dist/docx/watermarkParser.d.ts +2 -4
  78. package/dist/docx/watermarkParser.js +8 -9
  79. package/dist/docx/xmlParser.d.ts +23 -2
  80. package/dist/docx/xmlParser.js +43 -11
  81. package/dist/headless-layout.js +14 -2
  82. package/dist/index.d.ts +4 -4
  83. package/dist/index.js +2 -2
  84. package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -2
  85. package/dist/layout-bridge/convert/footnoteLayout.js +23 -10
  86. package/dist/layout-bridge/convert/headerFooterLayout.js +13 -2
  87. package/dist/layout-bridge/convert/toFlowBlocks.js +96 -13
  88. package/dist/layout-engine/index.js +11 -4
  89. package/dist/layout-engine/justifiedLineFit.d.ts +4 -4
  90. package/dist/layout-engine/justifiedLineFit.js +4 -4
  91. package/dist/layout-engine/measure/lineBreakProvider.js +1 -0
  92. package/dist/layout-engine/measure/measureBlocks.js +1 -1
  93. package/dist/layout-engine/measure/measureParagraph.js +29 -29
  94. package/dist/layout-painter/renderPage.js +6 -1
  95. package/dist/layout-painter/renderParagraph.js +18 -8
  96. package/dist/layout-painter/renderWatermark.js +11 -4
  97. package/dist/model.d.ts +3 -3
  98. package/dist/model.js +2 -2
  99. package/dist/prosemirror/attrs/index.js +11 -0
  100. package/dist/prosemirror/commands/comments.js +24 -3
  101. package/dist/prosemirror/containerFinalParagraph.d.ts +47 -0
  102. package/dist/prosemirror/containerFinalParagraph.js +92 -0
  103. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
  104. package/dist/prosemirror/conversion/fromProseDoc.js +47 -9
  105. package/dist/prosemirror/conversion/toProseDoc.js +61 -14
  106. package/dist/prosemirror/extensions/core/DocExtension.js +7 -1
  107. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  108. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +1 -0
  109. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -0
  110. package/dist/prosemirror/schema/marks.d.ts +3 -9
  111. package/dist/prosemirror/schema/nodes.d.ts +22 -0
  112. package/dist/server.d.ts +1 -1
  113. package/dist/types/block-id.d.ts +12 -1
  114. package/dist/types/block-id.js +17 -1
  115. package/dist/utils/canonicalJson.d.ts +14 -0
  116. package/dist/utils/canonicalJson.js +19 -0
  117. package/dist/utils/fontResolver.js +51 -0
  118. package/dist/utils/formatToStyle.d.ts +1 -1
  119. package/dist/utils/formatToStyle.js +41 -1
  120. package/dist/watermark/index.js +7 -0
  121. 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 };
@@ -525,7 +525,7 @@ function serializeShapeContent(content) {
525
525
  "<wps:wsp>",
526
526
  `<wps:cNvSpPr${isTextBox ? " txBox=\"1\"" : ""}/>`,
527
527
  spPr,
528
- textBody,
528
+ textBody === "" ? "<wps:bodyPr/>" : textBody,
529
529
  "</wps:wsp>"
530
530
  ].join(""),
531
531
  "</a:graphicData>",
@@ -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 `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:settings xmlns:w="${W_NS}">${parts.join("")}</w:settings>`;
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
- const docDefaults = serializeDocumentDefaults(definitions);
9
- const latentStyles = serializeLatentStyles(definitions);
10
- const styles = definitions.styles.map(serializeStyle).join("");
11
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:styles xmlns:w="${W_NS}">${docDefaults}${latentStyles}${styles}</w:styles>`;
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
- if (structuralChange) {
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
- if (structuralChange) {
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>`;
@@ -298,6 +359,10 @@ function gridColumnCount(table) {
298
359
  */
299
360
  function serializeTableGrid(table) {
300
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
+ }
301
366
  if (columnWidths && columnWidths.length > 0) return `<w:tblGrid>${columnWidths.map((w) => `<w:gridCol w:w="${intAttr(w)}"/>`).join("")}</w:tblGrid>`;
302
367
  const columns = gridColumnCount(table);
303
368
  return columns === 0 ? "<w:tblGrid/>" : `<w:tblGrid>${"<w:gridCol/>".repeat(columns)}</w:tblGrid>`;
@@ -1,9 +1,17 @@
1
+ import { serializePartElement } from "./partNamespaces.js";
1
2
  import { escapeXml } from "./xmlUtils.js";
2
3
  //#region src/docx/serializer/themeSerializer.ts
3
- const A_NS = "http://schemas.openxmlformats.org/drawingml/2006/main";
4
4
  const serializeThemeXml = (theme) => {
5
5
  const name = escapeXml(theme.name ?? "Folio Theme");
6
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<a:theme xmlns:a="${A_NS}" name="${name}"><a:themeElements>${serializeColorScheme(theme.colorScheme)}${serializeFontScheme(theme)}${serializeFormatScheme(theme)}</a:themeElements></a:theme>`;
6
+ const elements = `${serializeColorScheme(theme.colorScheme)}${serializeFontScheme(theme)}${serializeFormatScheme(theme)}`;
7
+ return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + serializePartElement({
8
+ partPath: "word/theme/theme1.xml",
9
+ rootName: "a:theme",
10
+ rootAttributes: `name="${name}"`,
11
+ baselinePrefixes: ["a"],
12
+ sourceBindings: void 0,
13
+ body: `<a:themeElements>${elements}</a:themeElements>`
14
+ });
7
15
  };
8
16
  const serializeColorScheme = (colors) => {
9
17
  const values = {
@@ -1,5 +1,5 @@
1
- import { document_d_exports } from "../../types/document.js";
2
1
  import { ShadingProperties } from "../../types/colors.js";
2
+ import { document_d_exports } from "../../types/document.js";
3
3
  import { Hyperlink, Paragraph, ParagraphContent, Run, Table } from "../../types/content.js";
4
4
  import { ParagraphFormatting, TextFormatting } from "../../types/formatting.js";
5
5
  //#region src/docx/server/build.d.ts
@@ -55,6 +55,9 @@ function parseSettings(xml) {
55
55
  if (compatibilityMode !== void 0) settings.compatibilityMode = compatibilityMode;
56
56
  const splitPageBreakAndParagraphMark = compat ? findChild(compat, "w", "splitPgBreakAndParaMark") : null;
57
57
  if (splitPageBreakAndParagraphMark && parseBooleanElement(splitPageBreakAndParagraphMark)) settings.splitPageBreakAndParagraphMark = true;
58
+ const wordprocessingCompat = root ? findChildByNamespaceUri(root, WORDPROCESSINGML_NAMESPACE_URIS, "compat") : null;
59
+ const adjustLineHeightInTable = wordprocessingCompat ? findChildByNamespaceUri(wordprocessingCompat, WORDPROCESSINGML_NAMESPACE_URIS, "adjustLineHeightInTable") : null;
60
+ if (adjustLineHeightInTable && parseBooleanElement(adjustLineHeightInTable)) settings.adjustLineHeightInTable = true;
58
61
  const applyBreakingRules = compat ? findChild(compat, "w", "applyBreakingRules") : null;
59
62
  const useLegacyEthiopicAmharicRules = applyBreakingRules !== null && parseBooleanElement(applyBreakingRules);
60
63
  if (noLineBreaksBefore || noLineBreaksAfter || useLegacyEthiopicAmharicRules) settings.lineBreakRules = {