@stll/folio-core 0.43.0 → 0.45.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 (237) hide show
  1. package/dist/ai-edits/__fixtures__/paragraphs.js +2 -2
  2. package/dist/ai-edits/headless.js +7 -5
  3. package/dist/ai-edits/index.d.ts +2 -2
  4. package/dist/ai-edits/index.js +2 -2
  5. package/dist/ai-edits/snapshot.js +13 -9
  6. package/dist/compare/content-alignment.js +94 -54
  7. package/dist/compare/inline-atoms.js +34 -20
  8. package/dist/compare/style-resources.js +6 -0
  9. package/dist/content-controls/mutateContentControls.js +4 -2
  10. package/dist/display-list/dom/renderDisplayListToDom.js +8 -8
  11. package/dist/document-operations.js +14 -3
  12. package/dist/docx/appVersionNormalization.d.ts +0 -18
  13. package/dist/docx/blockContentParser.js +8 -0
  14. package/dist/docx/blockRangeMarkers.d.ts +36 -0
  15. package/dist/docx/blockRangeMarkers.js +59 -0
  16. package/dist/docx/bookmarkParser.d.ts +2 -20
  17. package/dist/docx/bookmarkParser.js +6 -30
  18. package/dist/docx/borderParser.d.ts +13 -0
  19. package/dist/docx/borderParser.js +71 -0
  20. package/dist/docx/builtInStyles.d.ts +165 -0
  21. package/dist/docx/builtInStyles.js +239 -0
  22. package/dist/docx/commentIdNormalization.d.ts +3 -1
  23. package/dist/docx/commentIdNormalization.js +18 -1
  24. package/dist/docx/commentParser.d.ts +2 -1
  25. package/dist/docx/commentParser.js +80 -42
  26. package/dist/docx/commentReferenceNormalization.d.ts +4 -1
  27. package/dist/docx/commentReferenceNormalization.js +23 -14
  28. package/dist/docx/commentThreadKey.d.ts +18 -0
  29. package/dist/docx/commentThreadKey.js +22 -0
  30. package/dist/docx/danglingRelationshipReferences.d.ts +15 -0
  31. package/dist/docx/danglingRelationshipReferences.js +30 -0
  32. package/dist/docx/defaultParagraphStyle.d.ts +18 -1
  33. package/dist/docx/defaultParagraphStyle.js +23 -1
  34. package/dist/docx/diagramPreview.js +87 -27
  35. package/dist/docx/documentParser.d.ts +2 -1
  36. package/dist/docx/documentParser.js +2 -2
  37. package/dist/docx/drawingUtils.d.ts +8 -1
  38. package/dist/docx/drawingUtils.js +12 -3
  39. package/dist/docx/fieldParser.js +3 -5
  40. package/dist/docx/footnoteParser.d.ts +3 -2
  41. package/dist/docx/footnoteParser.js +19 -4
  42. package/dist/docx/groupDrawingParser.js +4 -4
  43. package/dist/docx/headerFooterRefParser.d.ts +4 -3
  44. package/dist/docx/headerFooterRefParser.js +42 -12
  45. package/dist/docx/headerFooterReferenceNormalization.d.ts +4 -1
  46. package/dist/docx/headerFooterReferenceNormalization.js +5 -1
  47. package/dist/docx/hyperlinkParser.js +13 -17
  48. package/dist/docx/imageParser.d.ts +10 -2
  49. package/dist/docx/imageParser.js +80 -30
  50. package/dist/docx/imageRawXml.d.ts +14 -1
  51. package/dist/docx/imageRawXml.js +35 -11
  52. package/dist/docx/markupRangeMarker.d.ts +15 -0
  53. package/dist/docx/markupRangeMarker.js +44 -0
  54. package/dist/docx/mathToMathml.js +12 -14
  55. package/dist/docx/nonVisualDrawingProps.d.ts +34 -0
  56. package/dist/docx/nonVisualDrawingProps.js +46 -0
  57. package/dist/docx/noteReferenceStyles.d.ts +29 -0
  58. package/dist/docx/noteReferenceStyles.js +70 -0
  59. package/dist/docx/numberingReferenceNormalization.d.ts +4 -1
  60. package/dist/docx/numberingReferenceNormalization.js +20 -1
  61. package/dist/docx/paraIdRangeNormalization.d.ts +0 -19
  62. package/dist/docx/paragraphParser.js +66 -99
  63. package/dist/docx/paragraphPropertySource.js +1 -0
  64. package/dist/docx/paragraphTextBoxEnrichment.js +3 -0
  65. package/dist/docx/paragraphTraversal.d.ts +37 -1
  66. package/dist/docx/paragraphTraversal.js +84 -1
  67. package/dist/docx/parseContext.d.ts +37 -0
  68. package/dist/docx/parseContext.js +67 -0
  69. package/dist/docx/parseWarningMessage.d.ts +6 -0
  70. package/dist/docx/parseWarningMessage.js +44 -0
  71. package/dist/docx/parser.js +83 -29
  72. package/dist/docx/previewBudget.d.ts +64 -0
  73. package/dist/docx/previewBudget.js +88 -0
  74. package/dist/docx/relsParser.d.ts +28 -11
  75. package/dist/docx/relsParser.js +26 -13
  76. package/dist/docx/revisionIdNormalization.js +96 -10
  77. package/dist/docx/rezip.js +80 -40
  78. package/dist/docx/runConsolidator.js +1 -2
  79. package/dist/docx/runParser.d.ts +8 -1
  80. package/dist/docx/runParser.js +30 -48
  81. package/dist/docx/sdtPropertiesPatch.js +24 -18
  82. package/dist/docx/sectionParser.d.ts +2 -1
  83. package/dist/docx/sectionParser.js +21 -65
  84. package/dist/docx/sectionReferenceHistory.js +2 -2
  85. package/dist/docx/selectiveSave.js +6 -6
  86. package/dist/docx/serializer/blockSdtSerializer.js +38 -26
  87. package/dist/docx/serializer/borderSerializer.d.ts +2 -3
  88. package/dist/docx/serializer/borderSerializer.js +13 -12
  89. package/dist/docx/serializer/commentSerializer.d.ts +41 -16
  90. package/dist/docx/serializer/commentSerializer.js +82 -72
  91. package/dist/docx/serializer/documentSerializer.d.ts +1 -5
  92. package/dist/docx/serializer/documentSerializer.js +6 -16
  93. package/dist/docx/serializer/fontTableSerializer.js +6 -6
  94. package/dist/docx/serializer/headerFooterSerializer.js +10 -5
  95. package/dist/docx/serializer/markupRangeAttributes.d.ts +8 -0
  96. package/dist/docx/serializer/markupRangeAttributes.js +24 -0
  97. package/dist/docx/serializer/noteSerializer.js +5 -0
  98. package/dist/docx/serializer/numberingSerializer.js +7 -6
  99. package/dist/docx/serializer/paragraphSerializer.d.ts +1 -5
  100. package/dist/docx/serializer/paragraphSerializer.js +47 -52
  101. package/dist/docx/serializer/partNamespaces.js +2 -2
  102. package/dist/docx/serializer/runSerializer.js +57 -31
  103. package/dist/docx/serializer/sectionPropertiesSerializer.js +11 -10
  104. package/dist/docx/serializer/settingsSerializer.js +4 -3
  105. package/dist/docx/serializer/stylesSerializer.js +6 -6
  106. package/dist/docx/serializer/tableSerializer.js +37 -21
  107. package/dist/docx/serializer/textFormattingSerializer.d.ts +2 -3
  108. package/dist/docx/serializer/textFormattingSerializer.js +29 -28
  109. package/dist/docx/serializer/themeSerializer.js +6 -6
  110. package/dist/docx/serializer/trackedChangeAttributes.js +2 -2
  111. package/dist/docx/serializer/xmlUtils.d.ts +1 -2
  112. package/dist/docx/serializer/xmlUtils.js +1 -13
  113. package/dist/docx/server/boundedArchive.d.ts +12 -0
  114. package/dist/docx/server/boundedArchive.js +20 -1
  115. package/dist/docx/server/build.js +8 -1
  116. package/dist/docx/server/createBilingualDocument.js +10 -18
  117. package/dist/docx/server/extractDocxText.js +3 -4
  118. package/dist/docx/server/validateDocxConformance.js +22 -1
  119. package/dist/docx/shadingParser.d.ts +6 -0
  120. package/dist/docx/shadingParser.js +32 -0
  121. package/dist/docx/shapeParser.js +10 -8
  122. package/dist/docx/styleParser.js +13 -87
  123. package/dist/docx/styleReferenceResolution.d.ts +36 -0
  124. package/dist/docx/styleReferenceResolution.js +51 -0
  125. package/dist/docx/tableLook.d.ts +57 -0
  126. package/dist/docx/tableLook.js +63 -0
  127. package/dist/docx/tableParser.d.ts +7 -9
  128. package/dist/docx/tableParser.js +64 -110
  129. package/dist/docx/textBoxParser.js +11 -6
  130. package/dist/docx/trackedMoveRangeNormalization.d.ts +3 -1
  131. package/dist/docx/trackedMoveRangeNormalization.js +11 -21
  132. package/dist/docx/transitionalSpelling.d.ts +13 -2
  133. package/dist/docx/transitionalSpelling.js +23 -1
  134. package/dist/docx/unzip.d.ts +23 -0
  135. package/dist/docx/unzip.js +32 -22
  136. package/dist/docx/verbatimCapture.js +5 -12
  137. package/dist/docx/vmlImageParser.js +5 -4
  138. package/dist/docx/vmlPreview.d.ts +1 -3
  139. package/dist/docx/vmlPreview.js +2 -30
  140. package/dist/docx/watermarkParser.js +2 -2
  141. package/dist/docx/xmlParser.d.ts +38 -33
  142. package/dist/docx/xmlParser.js +92 -47
  143. package/dist/docx/xmlResourceLimits.d.ts +89 -9
  144. package/dist/docx/xmlResourceLimits.js +105 -24
  145. package/dist/internal/pageBreakRunSourceDescendantIndex.js +2 -1
  146. package/dist/internal/paragraphFormattingSerialization.d.ts +2 -3
  147. package/dist/internal/paragraphFormattingSerialization.js +29 -8
  148. package/dist/layout-bridge/convert/footnoteLayout.js +2 -7
  149. package/dist/layout-engine/index.d.ts +2 -2
  150. package/dist/layout-engine/index.js +2 -2
  151. package/dist/layout-engine/measure/measureBlocks.js +1 -6
  152. package/dist/layout-engine/types.d.ts +8 -2
  153. package/dist/layout-engine/types.js +35 -2
  154. package/dist/layout-painter/renderImage.js +4 -3
  155. package/dist/layout-painter/renderParagraph.js +4 -3
  156. package/dist/managers/autoSaveCodec.js +2 -8
  157. package/dist/markdown/images.js +1 -4
  158. package/dist/markdown/index.js +1 -1
  159. package/dist/markdown/internals.d.ts +6 -1
  160. package/dist/markdown/internals.js +14 -1
  161. package/dist/markdown/renderBlock.js +35 -21
  162. package/dist/markdown/renderParagraph.js +14 -5
  163. package/dist/markdown/renderRuns.js +4 -3
  164. package/dist/markdown/renderTable.js +4 -3
  165. package/dist/markdown/trailers.js +41 -7
  166. package/dist/markdown/types.d.ts +3 -7
  167. package/dist/prosemirror/attrs/index.js +71 -5
  168. package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +11 -1
  169. package/dist/prosemirror/bookmarkBoundaryAttrs.js +18 -3
  170. package/dist/prosemirror/commands/image.js +1 -0
  171. package/dist/prosemirror/commands/index.d.ts +3 -3
  172. package/dist/prosemirror/commands/index.js +2 -2
  173. package/dist/prosemirror/commands/paragraph.d.ts +3 -3
  174. package/dist/prosemirror/commands/paragraph.js +2 -2
  175. package/dist/prosemirror/commentIdAllocator.js +2 -7
  176. package/dist/prosemirror/conversion/fromProseDoc.js +197 -68
  177. package/dist/prosemirror/conversion/toProseDoc.d.ts +1 -14
  178. package/dist/prosemirror/conversion/toProseDoc.js +458 -335
  179. package/dist/prosemirror/extensions/core/ParagraphExtension.d.ts +14 -1
  180. package/dist/prosemirror/extensions/core/ParagraphExtension.js +11 -6
  181. package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.js +3 -3
  182. package/dist/prosemirror/extensions/features/PasteCleanupExtension.d.ts +4 -1
  183. package/dist/prosemirror/extensions/features/PasteCleanupExtension.js +6 -2
  184. package/dist/prosemirror/extensions/features/pastedHeadingStyles.d.ts +7 -0
  185. package/dist/prosemirror/extensions/features/pastedHeadingStyles.js +74 -0
  186. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +2 -3
  187. package/dist/prosemirror/extensions/marks/markUtils.d.ts +11 -3
  188. package/dist/prosemirror/extensions/marks/markUtils.js +98 -19
  189. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +7 -3
  190. package/dist/prosemirror/extensions/nodes/ImageExtension.js +6 -1
  191. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +8 -2
  192. package/dist/prosemirror/extensions/nodes/TableExtension.js +15 -1
  193. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +8 -4
  194. package/dist/prosemirror/extensions/types.d.ts +2 -2
  195. package/dist/prosemirror/index.d.ts +3 -3
  196. package/dist/prosemirror/index.js +3 -3
  197. package/dist/prosemirror/insertOperations.d.ts +9 -2
  198. package/dist/prosemirror/insertOperations.js +9 -4
  199. package/dist/prosemirror/paragraphFormattingProvenance.d.ts +162 -0
  200. package/dist/prosemirror/paragraphFormattingProvenance.js +115 -0
  201. package/dist/prosemirror/plugins/documentStyles.d.ts +9 -1
  202. package/dist/prosemirror/plugins/documentStyles.js +11 -1
  203. package/dist/prosemirror/plugins/index.d.ts +2 -2
  204. package/dist/prosemirror/plugins/index.js +2 -2
  205. package/dist/prosemirror/plugins/revisionIds.d.ts +11 -2
  206. package/dist/prosemirror/plugins/revisionIds.js +21 -6
  207. package/dist/prosemirror/runFormattingReconciliation.js +3 -2
  208. package/dist/prosemirror/runStyleFormatting.d.ts +1 -1
  209. package/dist/prosemirror/schema/nodes.d.ts +81 -1
  210. package/dist/prosemirror/styles/resolvedStyleAttrs.js +2 -0
  211. package/dist/prosemirror/styles/styleResolver.d.ts +9 -0
  212. package/dist/prosemirror/styles/styleResolver.js +12 -0
  213. package/dist/style-engine/styleEngine.d.ts +3 -0
  214. package/dist/style-engine/styleEngine.js +3 -0
  215. package/dist/style-sets/extract.js +1 -23
  216. package/dist/style-sets/stellaStyle.js +46 -39
  217. package/dist/style-sets/styleSetNormalization.d.ts +19 -0
  218. package/dist/style-sets/styleSetNormalization.js +99 -0
  219. package/dist/types/content.d.ts +2 -2
  220. package/dist/utils/base64.d.ts +36 -0
  221. package/dist/utils/base64.js +40 -0
  222. package/dist/utils/clipboard.js +2 -1
  223. package/dist/utils/createDocument.js +145 -20
  224. package/dist/utils/headingCollector.d.ts +8 -5
  225. package/dist/utils/headingCollector.js +23 -25
  226. package/dist/utils/tableOfContentsStyle.js +9 -2
  227. package/dist/utils/units.d.ts +10 -1
  228. package/dist/utils/units.js +12 -1
  229. package/dist/utils/urlSecurity.d.ts +8 -2
  230. package/dist/utils/urlSecurity.js +21 -3
  231. package/package.json +2 -2
  232. package/dist/docx/textWhitespace.d.ts +0 -4
  233. package/dist/docx/textWhitespace.js +0 -4
  234. package/dist/layout-bridge/engine/tableWidthUtils.d.ts +0 -6
  235. package/dist/layout-bridge/engine/tableWidthUtils.js +0 -25
  236. package/dist/markdown/headings.d.ts +0 -13
  237. package/dist/markdown/headings.js +0 -20
@@ -31,16 +31,79 @@ const REVISION_ELEMENT_NAMES = /* @__PURE__ */ new Set([
31
31
  "trPrChange"
32
32
  ]);
33
33
  const REVISION_ELEMENT_CANDIDATE = new RegExp(`<(?:[^\\s<>/:]+:)?(?:${[...REVISION_ELEMENT_NAMES].join("|")})(?:[\\s/>])`, "u");
34
- const revisionAttribute = (element) => {
35
- if (!element.name || !WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "") || !REVISION_ELEMENT_NAMES.has(getLocalName(element.name))) return null;
34
+ /**
35
+ * The rest of the annotation id space.
36
+ *
37
+ * A comment, a bookmark, a protected range and a tracked change all draw their
38
+ * `w:id` from one space: Word allocates from a single counter, which is why a
39
+ * package carrying several kinds almost never repeats a value across them. So
40
+ * an id this pass mints must avoid these as well, or a renumbered `w:ins`
41
+ * lands on a live comment.
42
+ *
43
+ * They are only ever reserved, never claimed. A comment id legitimately
44
+ * appears four times (`w:comment`, both range markers and the reference) and a
45
+ * bookmark id twice, so feeding them to the uniqueness machinery would reject
46
+ * a package Word wrote. Their pairing is also why they are not revision
47
+ * elements: renumbering one end of a range would unpair it.
48
+ */
49
+ const ANNOTATION_ELEMENT_NAMES = /* @__PURE__ */ new Set([
50
+ "bookmarkEnd",
51
+ "bookmarkStart",
52
+ "comment",
53
+ "commentRangeEnd",
54
+ "commentRangeStart",
55
+ "commentReference",
56
+ "customXmlDelRangeEnd",
57
+ "customXmlDelRangeStart",
58
+ "customXmlInsRangeEnd",
59
+ "customXmlInsRangeStart",
60
+ "customXmlMoveFromRangeEnd",
61
+ "customXmlMoveFromRangeStart",
62
+ "customXmlMoveToRangeEnd",
63
+ "customXmlMoveToRangeStart",
64
+ "moveFromRangeEnd",
65
+ "moveFromRangeStart",
66
+ "moveToRangeEnd",
67
+ "moveToRangeStart",
68
+ "permEnd",
69
+ "permStart"
70
+ ]);
71
+ const ANNOTATION_ELEMENT_CANDIDATE = new RegExp(`<(?:[^\\s<>/:]+:)?(?:${[...ANNOTATION_ELEMENT_NAMES].join("|")})(?:[\\s/>])`, "u");
72
+ const ID_KINDS = {
73
+ revision: "revision",
74
+ annotation: "annotation"
75
+ };
76
+ /** One lookup for both halves of the space, so an element is classified once. */
77
+ const ID_KIND_BY_ELEMENT_NAME = new Map([...[...REVISION_ELEMENT_NAMES].map((name) => [name, ID_KINDS.revision]), ...[...ANNOTATION_ELEMENT_NAMES].map((name) => [name, ID_KINDS.annotation])]);
78
+ /**
79
+ * An element's `w:id` and which half of the annotation space it belongs to.
80
+ *
81
+ * One classifier rather than two, because it runs on every element of every
82
+ * scanned part: resolving the namespace and the local name twice to ask two
83
+ * questions measured 18% on a 17.6 MiB package.
84
+ *
85
+ * `w:permStart` types its id as a string, so a protected range named
86
+ * `everyone` yields nothing. That is correct: a value the allocator can never
87
+ * mint is not one it has to avoid.
88
+ */
89
+ const identifiedElement = (element) => {
90
+ if (!element.name || !WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "")) return null;
91
+ const localName = getLocalName(element.name);
92
+ const kind = ID_KIND_BY_ELEMENT_NAME.get(localName);
93
+ if (kind === void 0) return null;
36
94
  const attribute = findAttributeByNamespaceUri(element, WORDPROCESSINGML_NAMESPACE_URIS, "id");
37
95
  if (!attribute) return null;
38
96
  const id = Number(attribute.value);
39
97
  return Number.isSafeInteger(id) && id >= 0 ? {
98
+ kind,
40
99
  name: attribute.name,
41
100
  id
42
101
  } : null;
43
102
  };
103
+ const revisionAttribute = (element) => {
104
+ const identified = identifiedElement(element);
105
+ return identified?.kind === ID_KINDS.revision ? identified : null;
106
+ };
44
107
  /**
45
108
  * Keep physical tracked-change element ids unique across a package.
46
109
  *
@@ -54,18 +117,22 @@ const normalizeRevisionIdsInXmlParts = (parts) => {
54
117
  const occurrencesByPath = /* @__PURE__ */ new Map();
55
118
  const reserved = /* @__PURE__ */ new Set();
56
119
  for (const [path, xml] of candidates) {
57
- assertXmlResourceLimits(xml);
120
+ assertXmlResourceLimits({
121
+ xml,
122
+ partPath: path
123
+ });
58
124
  const ids = [];
59
125
  if (rewriteStreamingXmlDecimalAttributes(xml, (element) => {
60
- const attribute = revisionAttribute(element);
61
- if (attribute) {
62
- ids.push(attribute.id);
63
- reserved.add(attribute.id);
64
- }
126
+ const identified = identifiedElement(element);
127
+ if (identified === null) return null;
128
+ if (identified.kind === ID_KINDS.revision) ids.push(identified.id);
129
+ reserved.add(identified.id);
65
130
  return null;
66
131
  }).status === "unsupported") throw new XmlResourceLimitError({
67
132
  message: `Revision-id normalization could not safely scan ${path}`,
68
- limit: "syntax"
133
+ limit: "syntax",
134
+ observed: 0,
135
+ allowed: 0
69
136
  });
70
137
  occurrencesByPath.set(path, ids);
71
138
  }
@@ -73,6 +140,23 @@ const normalizeRevisionIdsInXmlParts = (parts) => {
73
140
  const firstSeen = /* @__PURE__ */ new Set();
74
141
  for (const [path, ids] of occurrencesByPath) for (const id of ids) if (firstSeen.has(id)) repeatedPaths.add(path);
75
142
  else firstSeen.add(id);
143
+ if (repeatedPaths.size > 0) for (const [path, xml] of parts) {
144
+ if (occurrencesByPath.has(path) || !ANNOTATION_ELEMENT_CANDIDATE.test(xml)) continue;
145
+ assertXmlResourceLimits({
146
+ xml,
147
+ partPath: path
148
+ });
149
+ if (rewriteStreamingXmlDecimalAttributes(xml, (element) => {
150
+ const identified = identifiedElement(element);
151
+ if (identified !== null) reserved.add(identified.id);
152
+ return null;
153
+ }).status === "unsupported") throw new XmlResourceLimitError({
154
+ message: `Revision-id normalization could not safely scan ${path}`,
155
+ limit: "syntax",
156
+ observed: 0,
157
+ allowed: 0
158
+ });
159
+ }
76
160
  let nextId = 0;
77
161
  const allocate = () => {
78
162
  while (reserved.has(nextId)) nextId += 1;
@@ -111,7 +195,9 @@ const normalizeRevisionIdsInXmlParts = (parts) => {
111
195
  });
112
196
  if (rewritten.status === "unsupported") throw new XmlResourceLimitError({
113
197
  message: `Revision-id normalization could not safely rewrite ${path}`,
114
- limit: "syntax"
198
+ limit: "syntax",
199
+ observed: 0,
200
+ allowed: 0
115
201
  });
116
202
  normalized.set(path, rewritten.value);
117
203
  }
@@ -10,6 +10,7 @@ import { parseEndnotes, parseFootnotes } from "./footnoteParser.js";
10
10
  import { parseHeaderFooterType } from "./headerFooterRefParser.js";
11
11
  import { assertValidFolioDocumentModel } from "./modelValidation.js";
12
12
  import { isNewDataUrlDrawing } from "./newImage.js";
13
+ import { missingNoteReferenceStyles, noteReferenceNeeds } from "./noteReferenceStyles.js";
13
14
  import { parseNumbering } from "./numberingParser.js";
14
15
  import { isNumberingReference } from "./numberingReference.js";
15
16
  import { isUnsafePackagePath, reconcilePackageReferences, removeUnsafeEntries } from "./packageParts.js";
@@ -18,7 +19,7 @@ import { RELATIONSHIP_TYPES, parseRelationships, resolveRelativePath } from "./r
18
19
  import { removeResolvedHeaderFooterParts } from "./removeHeaderFooterParts.js";
19
20
  import { normalizeRevisionIdsInXmlParts } from "./revisionIdNormalization.js";
20
21
  import { buildPatchedNotePartXml, collectChangedNoteParaIds, collectParaIds, patchNumberingDefinitions } from "./selectiveXmlPatch.js";
21
- import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
22
+ import { planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
22
23
  import { serializeDocument } from "./serializer/documentSerializer.js";
23
24
  import { serializeFontTableXml } from "./serializer/fontTableSerializer.js";
24
25
  import { serializeHeaderFooter } from "./serializer/headerFooterSerializer.js";
@@ -28,11 +29,10 @@ import { readRootNamespaceBindings } from "./serializer/partNamespaces.js";
28
29
  import { serializeSettingsXml } from "./serializer/settingsSerializer.js";
29
30
  import { serializeStyle, serializeStylesXml } from "./serializer/stylesSerializer.js";
30
31
  import { serializeThemeXml } from "./serializer/themeSerializer.js";
31
- import { escapeXml } from "./serializer/xmlUtils.js";
32
32
  import { OFFICE_RELATIONSHIP_NAMESPACE_URIS, WORDPROCESSINGML_NAMESPACE_URIS, findChild, getAttribute, getAttributeByNamespaceUri, getChildElements, getLocalName, getNamespaceUri, matchesName, parseXml, parseXmlDocument } from "./xmlParser.js";
33
33
  import { assertXmlResourceLimits } from "./xmlResourceLimits.js";
34
34
  import { panic } from "better-result";
35
- import { validateDocxPackage } from "@stll/docx-core";
35
+ import { escapeXmlAttribute, escapeXmlText, validateDocxPackage } from "@stll/docx-core";
36
36
  import JSZip from "jszip";
37
37
  //#region src/docx/rezip.ts
38
38
  /**
@@ -84,7 +84,7 @@ function findMaxRId(relsXml) {
84
84
  }
85
85
  const isWordprocessingElement = (element, localName) => getLocalName(element.name) === localName && WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "");
86
86
  const countDocumentSections = (xml) => {
87
- assertXmlResourceLimits(xml);
87
+ assertXmlResourceLimits({ xml });
88
88
  let count = 0;
89
89
  const pending = [{
90
90
  element: parseXml(xml),
@@ -105,7 +105,7 @@ const countDocumentSections = (xml) => {
105
105
  };
106
106
  const extractHeaderFooterReferences = (xml) => {
107
107
  const references = [];
108
- assertXmlResourceLimits(xml);
108
+ assertXmlResourceLimits({ xml });
109
109
  const pending = [parseXml(xml)];
110
110
  while (pending.length > 0) {
111
111
  const node = pending.pop();
@@ -165,15 +165,15 @@ async function serializeCommentsToZip(doc, zip, compressionLevel) {
165
165
  if (comments.length === 0) {
166
166
  if (sourceCommentsXml === void 0 || !hasCommentEntries(sourceCommentsXml)) return;
167
167
  }
168
- ensureThreadedCommentParaIds(comments);
169
- const commentsXml = serializeComments(comments, sourceCommentsXml === void 0 ? void 0 : readRootNamespaceBindings(sourceCommentsXml));
168
+ const plan = planCommentParts(comments);
169
+ const commentsXml = serializeComments(plan, sourceCommentsXml === void 0 ? void 0 : readRootNamespaceBindings(sourceCommentsXml));
170
170
  zip.file(sourceCommentsFile?.name ?? "word/comments.xml", commentsXml, {
171
171
  compression: "DEFLATE",
172
172
  compressionOptions: { level: compressionLevel }
173
173
  });
174
174
  await ensureCommentsContentType(zip, compressionLevel);
175
175
  await ensureCommentsRelationship(zip, compressionLevel);
176
- await syncCommentsExtendedPart(comments, zip, compressionLevel);
176
+ await syncCommentsExtendedPart(plan, zip, compressionLevel);
177
177
  }
178
178
  const hasCommentEntries = (xml) => {
179
179
  const root = parseXml(xml);
@@ -188,8 +188,8 @@ const hasCommentEntries = (xml) => {
188
188
  * removed part triggers Word's repair prompt). The reply-thread markers in
189
189
  * `document.xml` are synthesized separately (see {@link applyReplyThreadMarkers}).
190
190
  */
191
- async function syncCommentsExtendedPart(comments, zip, compressionLevel) {
192
- const xml = serializeCommentsExtended(comments);
191
+ async function syncCommentsExtendedPart(plan, zip, compressionLevel) {
192
+ const xml = serializeCommentsExtended(plan);
193
193
  const existing = findZipEntryCaseInsensitive(zip, COMMENTS_EXTENDED_PART_LOWER);
194
194
  if (!xml) {
195
195
  if (!existing) return;
@@ -395,7 +395,7 @@ async function processNewImages(parts, zip, compressionLevel) {
395
395
  compression: "DEFLATE",
396
396
  compressionOptions: { level: compressionLevel }
397
397
  });
398
- relEntries.push(`<Relationship Id="${newRId}" Type="${RELATIONSHIP_TYPES.image}" Target="${escapeXml(relativeTargetForPart(partPath, mediaPath))}"/>`);
398
+ relEntries.push(`<Relationship Id="${newRId}" Type="${RELATIONSHIP_TYPES.image}" Target="${escapeXmlAttribute(relativeTargetForPart(partPath, mediaPath))}"/>`);
399
399
  extensionsAdded.add(extension);
400
400
  if (drawing.rawXml) drawing.rawXml = rebindDrawingImageRelationship({
401
401
  xml: drawing.rawXml,
@@ -475,7 +475,7 @@ async function processNewHyperlinks(parts, zip, compressionLevel) {
475
475
  if (!hyperlink.href) continue;
476
476
  maxId++;
477
477
  const newRId = `rId${maxId}`;
478
- relEntries.push(`<Relationship Id="${newRId}" Type="${RELATIONSHIP_TYPES.hyperlink}" Target="${escapeXml(hyperlink.href)}" TargetMode="External"/>`);
478
+ relEntries.push(`<Relationship Id="${newRId}" Type="${RELATIONSHIP_TYPES.hyperlink}" Target="${escapeXmlAttribute(hyperlink.href)}" TargetMode="External"/>`);
479
479
  hyperlink.rId = newRId;
480
480
  }
481
481
  zip.file(relsPath, relsXml.replace("</Relationships>", `${relEntries.join("")}</Relationships>`), {
@@ -951,7 +951,7 @@ async function addRelationship(originalBuffer, relationship) {
951
951
  const relsXml = await relsFile.async("text");
952
952
  const newRId = `rId${findMaxRId(relsXml) + 1}`;
953
953
  const targetModeAttr = relationship.targetMode === "External" ? " TargetMode=\"External\"" : "";
954
- const newRelElement = `<Relationship Id="${newRId}" Type="${relationship.type}" Target="${escapeXml(relationship.target)}"${targetModeAttr}/>`;
954
+ const newRelElement = `<Relationship Id="${newRId}" Type="${relationship.type}" Target="${escapeXmlAttribute(relationship.target)}"${targetModeAttr}/>`;
955
955
  const updatedRelsXml = relsXml.replace("</Relationships>", `${newRelElement}</Relationships>`);
956
956
  zip.file(relsPath, updatedRelsXml);
957
957
  return {
@@ -1100,7 +1100,7 @@ async function materializeNewHeaderFooterParts(doc, zip, compressionLevel) {
1100
1100
  type: relType,
1101
1101
  target: filename
1102
1102
  });
1103
- relEntries.push(`<Relationship Id="${escapeXml(effectiveRId)}" Type="${relType}" Target="${filename}"/>`);
1103
+ relEntries.push(`<Relationship Id="${escapeXmlAttribute(effectiveRId)}" Type="${relType}" Target="${filename}"/>`);
1104
1104
  overrides.push(`<Override PartName="/word/${filename}" ContentType="${contentType}"/>`);
1105
1105
  }
1106
1106
  };
@@ -1217,7 +1217,7 @@ async function rebindWatermarkRelIds(doc, zip, compressionLevel) {
1217
1217
  }
1218
1218
  if (!resolvedRId) {
1219
1219
  resolvedRId = `rId${findMaxRId(relsXml) + 1}`;
1220
- const relXml = canonical.mode === "external" ? `<Relationship Id="${resolvedRId}" Type="${RELATIONSHIP_TYPES.image}" Target="${escapeXml(canonical.url)}" TargetMode="External"/>` : `<Relationship Id="${resolvedRId}" Type="${RELATIONSHIP_TYPES.image}" Target="${escapeXml(relativeTargetForPart(partPath, canonical.absolute))}"/>`;
1220
+ const relXml = canonical.mode === "external" ? `<Relationship Id="${resolvedRId}" Type="${RELATIONSHIP_TYPES.image}" Target="${escapeXmlAttribute(canonical.url)}" TargetMode="External"/>` : `<Relationship Id="${resolvedRId}" Type="${RELATIONSHIP_TYPES.image}" Target="${escapeXmlAttribute(relativeTargetForPart(partPath, canonical.absolute))}"/>`;
1221
1221
  relsXmlByPath.set(relsPath, relsXml.replace("</Relationships>", `${relXml}</Relationships>`));
1222
1222
  changedPaths.add(relsPath);
1223
1223
  }
@@ -1389,6 +1389,42 @@ async function serializeNumberingIntoZip(doc, originalZip, newZip, compressionLe
1389
1389
  }
1390
1390
  const STYLES_PART_PATH = "word/styles.xml";
1391
1391
  const STYLES_CLOSE_ROOT = "</w:styles>";
1392
+ /**
1393
+ * The style table a package folio creates starts from: `docDefaults` and
1394
+ * `Normal`, and nothing else. A document that carries its own style table
1395
+ * replaces this part wholesale.
1396
+ */
1397
+ const SEED_STYLES_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1398
+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1399
+ <w:docDefaults>
1400
+ <w:rPrDefault>
1401
+ <w:rPr>
1402
+ <w:rFonts w:ascii="Calibri" w:hAnsi="Calibri"/>
1403
+ <w:sz w:val="22"/>
1404
+ </w:rPr>
1405
+ </w:rPrDefault>
1406
+ <w:pPrDefault>
1407
+ <w:pPr>
1408
+ <w:spacing w:after="200" w:line="276" w:lineRule="auto"/>
1409
+ </w:pPr>
1410
+ </w:pPrDefault>
1411
+ </w:docDefaults>
1412
+ <w:style w:type="paragraph" w:default="1" w:styleId="Normal">
1413
+ <w:name w:val="Normal"/>
1414
+ </w:style>
1415
+ </w:styles>`;
1416
+ /**
1417
+ * The seed part plus the reference styles this package's comments and notes
1418
+ * need. A document with no style table of its own never reaches
1419
+ * {@link styleDefinitionsToSerialize}, so without this its reference marks
1420
+ * would carry a `w:rStyle` naming nothing — the defect
1421
+ * `noteReferenceStyles.ts` exists to remove, on the one path that skips it.
1422
+ */
1423
+ const seedStylesXmlWith = (missing) => {
1424
+ if (missing.length === 0) return SEED_STYLES_XML;
1425
+ const rootClose = SEED_STYLES_XML.lastIndexOf(STYLES_CLOSE_ROOT);
1426
+ return SEED_STYLES_XML.slice(0, rootClose) + missing.map(serializeStyle).join("") + SEED_STYLES_XML.slice(rootClose);
1427
+ };
1392
1428
  const STYLE_ID_PATTERN = /<w:style\b[^>]*?\bw:styleId="(?<id>[^"]+)"/gu;
1393
1429
  /**
1394
1430
  * Append styles the model defines but the original `word/styles.xml` lacks.
@@ -1397,6 +1433,27 @@ const STYLE_ID_PATTERN = /<w:style\b[^>]*?\bw:styleId="(?<id>[^"]+)"/gu;
1397
1433
  * per-language clones a bilingual transform adds, are emitted before the root
1398
1434
  * close so paragraphs referencing them resolve on reopen.
1399
1435
  */
1436
+ /**
1437
+ * The styles to write into a package folio is authoring: the model's, plus any
1438
+ * reference character style the serializers are about to emit for this
1439
+ * package's comments and notes but the style table does not define. See
1440
+ * `noteReferenceStyles.ts` — the reference mark would otherwise carry a
1441
+ * `w:rStyle` pointing at nothing.
1442
+ *
1443
+ * Only for a package folio writes from scratch. Repacking a document someone
1444
+ * else authored preserves `word/styles.xml` byte for byte, and adding a
1445
+ * definition there would rewrite a part the user never edited: their document,
1446
+ * their style table, missing reference style included.
1447
+ */
1448
+ const styleDefinitionsToSerialize = (doc) => {
1449
+ const styles = doc.package.styles;
1450
+ if (!styles) return;
1451
+ const missing = missingNoteReferenceStyles(styles, noteReferenceNeeds(doc.package));
1452
+ return missing.length === 0 ? styles : {
1453
+ ...styles,
1454
+ styles: [...styles.styles, ...missing]
1455
+ };
1456
+ };
1400
1457
  async function serializeAddedStylesIntoZip(doc, originalZip, newZip, compressionLevel) {
1401
1458
  const styles = doc.package.styles;
1402
1459
  if (!styles || styles.styles.length === 0) return;
@@ -1473,7 +1530,7 @@ function updateCoreProperties(corePropsXml, { updateModifiedDate, modifiedBy })
1473
1530
  if (result.includes("<dcterms:modified")) result = result.replace(/<dcterms:modified[^<>]*>[^<]*<\/dcterms:modified>/u, `<dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified>`);
1474
1531
  }
1475
1532
  if (modifiedBy) {
1476
- if (result.includes("<cp:lastModifiedBy")) result = result.replace(/<cp:lastModifiedBy>[^<]*<\/cp:lastModifiedBy>/u, `<cp:lastModifiedBy>${escapeXml(modifiedBy)}</cp:lastModifiedBy>`);
1533
+ if (result.includes("<cp:lastModifiedBy")) result = result.replace(/<cp:lastModifiedBy>[^<]*<\/cp:lastModifiedBy>/u, `<cp:lastModifiedBy>${escapeXmlText(modifiedBy)}</cp:lastModifiedBy>`);
1477
1534
  }
1478
1535
  return result;
1479
1536
  }
@@ -1578,33 +1635,15 @@ const createEmptyDocxZip = ({ creator, application }) => {
1578
1635
  </w:sectPr>
1579
1636
  </w:body>
1580
1637
  </w:document>`);
1581
- zip.file("word/styles.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1582
- <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1583
- <w:docDefaults>
1584
- <w:rPrDefault>
1585
- <w:rPr>
1586
- <w:rFonts w:ascii="Calibri" w:hAnsi="Calibri"/>
1587
- <w:sz w:val="22"/>
1588
- </w:rPr>
1589
- </w:rPrDefault>
1590
- <w:pPrDefault>
1591
- <w:pPr>
1592
- <w:spacing w:after="200" w:line="276" w:lineRule="auto"/>
1593
- </w:pPr>
1594
- </w:pPrDefault>
1595
- </w:docDefaults>
1596
- <w:style w:type="paragraph" w:default="1" w:styleId="Normal">
1597
- <w:name w:val="Normal"/>
1598
- </w:style>
1599
- </w:styles>`);
1638
+ zip.file(STYLES_PART_PATH, SEED_STYLES_XML);
1600
1639
  const now = (/* @__PURE__ */ new Date()).toISOString();
1601
- const creatorElement = creator === void 0 ? "" : `\n <dc:creator>${escapeXml(creator)}</dc:creator>`;
1640
+ const creatorElement = creator === void 0 ? "" : `\n <dc:creator>${escapeXmlText(creator)}</dc:creator>`;
1602
1641
  zip.file("docProps/core.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1603
1642
  <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">${creatorElement}
1604
1643
  <dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created>
1605
1644
  <dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified>
1606
1645
  </cp:coreProperties>`);
1607
- const applicationElements = application === void 0 ? "" : `\n <Application>${escapeXml(application)}</Application>\n <AppVersion>${CREATED_APP_VERSION}</AppVersion>`;
1646
+ const applicationElements = application === void 0 ? "" : `\n <Application>${escapeXmlText(application)}</Application>\n <AppVersion>${CREATED_APP_VERSION}</AppVersion>`;
1608
1647
  zip.file("docProps/app.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1609
1648
  <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">${applicationElements}
1610
1649
  </Properties>`);
@@ -1647,10 +1686,11 @@ const createDocumentSeedZip = async (doc, properties) => {
1647
1686
  const relationships = ["<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/>"];
1648
1687
  const overrides = [];
1649
1688
  let nextRelationshipId = 2;
1650
- if (doc.package.styles) {
1689
+ const styleDefinitions = styleDefinitionsToSerialize(doc);
1690
+ if (styleDefinitions) {
1651
1691
  assertStyleNumberingReferences(doc);
1652
- zip.file("word/styles.xml", serializeStylesXml(doc.package.styles));
1653
- }
1692
+ zip.file(STYLES_PART_PATH, serializeStylesXml(styleDefinitions));
1693
+ } else zip.file(STYLES_PART_PATH, seedStylesXmlWith(missingNoteReferenceStyles(void 0, noteReferenceNeeds(doc.package))));
1654
1694
  const numbering = doc.package.numbering;
1655
1695
  if (numbering && (numbering.abstractNums.length > 0 || numbering.nums.length > 0)) {
1656
1696
  zip.file("word/numbering.xml", serializeNumberingXml(numbering));
@@ -114,8 +114,7 @@ function mergeRunContent(content1, content2) {
114
114
  if (lastText?.type === "text" && firstText?.type === "text") {
115
115
  result[result.length - 1] = {
116
116
  type: "text",
117
- text: lastText.text + firstText.text,
118
- ...lastText.preserveSpace || firstText.preserveSpace ? { preserveSpace: true } : {}
117
+ text: lastText.text + firstText.text
119
118
  };
120
119
  for (let i = 1; i < content2.length; i++) result.push(content2[i]);
121
120
  } else for (const c of content2) result.push(c);
@@ -2,6 +2,13 @@ import { document_d_exports } from "../types/document.js";
2
2
  import { StyleMap } from "./styleParser.js";
3
3
  import { XmlElement } from "./xmlParser.js";
4
4
  //#region src/docx/runParser.d.ts
5
+ /**
6
+ * `w:vertAlign` `baseline` is the reserved value that means "no vertical
7
+ * offset", not a third offset beside `superscript` and `subscript`. It is
8
+ * answered here because this module reads the slot, so a consumer deciding
9
+ * whether a run sits on the baseline cannot drift from how it was parsed.
10
+ */
11
+ declare const isBaselineVertAlign: (vertAlign: document_d_exports.TextFormatting["vertAlign"]) => boolean;
5
12
  /**
6
13
  * Parse run formatting properties (w:rPr)
7
14
  *
@@ -74,4 +81,4 @@ declare function hasFieldChar(run: document_d_exports.Run): boolean;
74
81
  */
75
82
  declare function getFieldCharType(run: document_d_exports.Run): "begin" | "separate" | "end" | null;
76
83
  //#endregion
77
- export { getFieldCharType, getImages, getRunText, hasContent, hasFieldChar, hasImage, parseRun, parseRunProperties };
84
+ export { getFieldCharType, getImages, getRunText, hasContent, hasFieldChar, hasImage, isBaselineVertAlign, parseRun, parseRunProperties };
@@ -1,18 +1,17 @@
1
- import { isValidHexColor } from "../utils/colorResolver.js";
2
1
  import { parseHorizontalScalePercent } from "../utils/horizontalScale.js";
3
2
  import { parseDiagramPreview } from "./diagramPreview.js";
4
3
  import { isGroupDrawing, parseGroupDrawing } from "./groupDrawingParser.js";
5
4
  import { parseImage } from "./imageParser.js";
6
5
  import { imageRawXmlFingerprint } from "./imageRawXml.js";
7
- import { EmphasisMarkSchema, FontHintSchema, FontThemeSchema, HighlightColorSchema, PositionalTabAlignmentSchema, PositionalTabLeaderSchema, PositionalTabRelativeToSchema, ShadingPatternSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
6
+ import { EmphasisMarkSchema, FontHintSchema, FontThemeSchema, HighlightColorSchema, PositionalTabAlignmentSchema, PositionalTabLeaderSchema, PositionalTabRelativeToSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
7
+ import { parseShading } from "./shadingParser.js";
8
8
  import { parseShapeFromDrawing, shouldPreserveRawShapeDrawing } from "./shapeParser.js";
9
9
  import { isTextBoxDrawing } from "./textBoxParser.js";
10
- import { requiresXmlSpacePreserve } from "./textWhitespace.js";
11
10
  import { resolveThemeFontRef } from "./themeParser.js";
12
11
  import { parsePropertyChangeInfo } from "./trackedChangeInfo.js";
13
12
  import { captureVerbatimXml } from "./verbatimCapture.js";
14
13
  import { parseVmlImageContent, shouldPreserveRawVmlPict } from "./vmlImageParser.js";
15
- import { cloneWithXmlnsDeclarations, findAllDeep, findChild, findChildren, getAttribute, getChildElements, getLocalName, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, selectAlternateContentBranch } from "./xmlParser.js";
14
+ import { cloneWithXmlnsDeclarations, findAllDeep, findChild, findChildren, getAttribute, getChildElements, getLocalName, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, parseOnOffAttribute, selectAlternateContentBranch } from "./xmlParser.js";
16
15
  import { DRAWING_RAW_XML_MODES } from "@stll/docx-core/model";
17
16
  //#region src/docx/runParser.ts
18
17
  /**
@@ -36,31 +35,6 @@ function parseColorValue(rgb, themeColor, themeTint, themeShade) {
36
35
  if (themeShade) color.themeShade = themeShade;
37
36
  return color;
38
37
  }
39
- /**
40
- * Parse shading properties (w:shd)
41
- */
42
- function parseShadingProperties(shd) {
43
- if (!shd) return;
44
- const props = {};
45
- const color = getAttribute(shd, "w", "color");
46
- if (color === "auto") props.color = { auto: true };
47
- else if (color && isValidHexColor(color)) props.color = { rgb: color };
48
- const fill = getAttribute(shd, "w", "fill");
49
- if (fill === "auto") props.fill = { auto: true };
50
- else if (fill && isValidHexColor(fill)) props.fill = { rgb: fill };
51
- const validatedThemeFill = narrowEnum(getAttribute(shd, "w", "themeFill"), ThemeColorSlotSchema);
52
- if (validatedThemeFill) {
53
- if (!props.fill) props.fill = {};
54
- props.fill.themeColor = validatedThemeFill;
55
- }
56
- const themeFillTint = getAttribute(shd, "w", "themeFillTint");
57
- if (themeFillTint && props.fill) props.fill.themeTint = themeFillTint;
58
- const themeFillShade = getAttribute(shd, "w", "themeFillShade");
59
- if (themeFillShade && props.fill) props.fill.themeShade = themeFillShade;
60
- const pattern = narrowEnum(getAttribute(shd, "w", "val"), ShadingPatternSchema);
61
- if (pattern) props.pattern = pattern;
62
- return Object.keys(props).length > 0 ? props : void 0;
63
- }
64
38
  function collectFirstRunPropertyChildren(rPr) {
65
39
  const children = {};
66
40
  for (const child of rPr.elements ?? []) {
@@ -167,6 +141,13 @@ function collectFirstRunPropertyChildren(rPr) {
167
141
  return children;
168
142
  }
169
143
  /**
144
+ * `w:vertAlign` `baseline` is the reserved value that means "no vertical
145
+ * offset", not a third offset beside `superscript` and `subscript`. It is
146
+ * answered here because this module reads the slot, so a consumer deciding
147
+ * whether a run sits on the baseline cannot drift from how it was parsed.
148
+ */
149
+ const isBaselineVertAlign = (vertAlign) => vertAlign === "baseline";
150
+ /**
170
151
  * Parse run formatting properties (w:rPr)
171
152
  *
172
153
  * Handles ALL rPr properties:
@@ -231,7 +212,7 @@ function parseRunProperties(rPr, theme, _styles) {
231
212
  }
232
213
  const shd = propertyChildren.shd;
233
214
  if (shd) {
234
- const shadingResult = parseShadingProperties(shd);
215
+ const shadingResult = parseShading(shd);
235
216
  if (shadingResult) formatting.shading = shadingResult;
236
217
  }
237
218
  const sz = propertyChildren.sz;
@@ -370,14 +351,10 @@ function parseRunPropertyChanges(rPr, theme, styles, currentFormatting) {
370
351
  * Parse text content (w:t)
371
352
  */
372
353
  function parseTextContent(element) {
373
- const text = getTextContent(element);
374
- const preserveSpace = getAttribute(element, "xml", "space") === "preserve" || requiresXmlSpacePreserve(text);
375
- const content = {
354
+ return {
376
355
  type: "text",
377
- text
356
+ text: getTextContent(element)
378
357
  };
379
- if (preserveSpace) content.preserveSpace = true;
380
- return content;
381
358
  }
382
359
  /**
383
360
  * Parse tab element (w:tab)
@@ -442,8 +419,8 @@ function parseEndnoteReference(element) {
442
419
  */
443
420
  function parseFieldChar(element) {
444
421
  const fldCharType = getAttribute(element, "w", "fldCharType");
445
- const fldLock = getAttribute(element, "w", "fldLock") === "true" || getAttribute(element, "w", "fldLock") === "1";
446
- const dirty = getAttribute(element, "w", "dirty") === "true" || getAttribute(element, "w", "dirty") === "1";
422
+ const fldLock = parseOnOffAttribute(element, "w", "fldLock") === true;
423
+ const dirty = parseOnOffAttribute(element, "w", "dirty") === true;
447
424
  let charType = "begin";
448
425
  if (fldCharType === "separate") charType = "separate";
449
426
  else if (fldCharType === "end") charType = "end";
@@ -473,15 +450,14 @@ function parseInstrText(element) {
473
450
  * Wrap raw XML the model cannot project at all.
474
451
  *
475
452
  * `DrawingContent` always carries an `Image`, so preservation-only content
476
- * gets a placeholder one: the empty `rId` marks it as backed by no
477
- * relationship, which keeps `classifyDrawingSafety` and the serializer on the
478
- * replay path instead of regenerating DrawingML from the placeholder.
453
+ * gets a placeholder one. It names no relationship, which keeps
454
+ * `classifyDrawingSafety` and the serializer on the replay path instead of
455
+ * regenerating DrawingML from the placeholder.
479
456
  */
480
457
  const preserveOnlyDrawing = (rawXml) => ({
481
458
  type: "drawing",
482
459
  image: {
483
460
  type: "image",
484
- rId: "",
485
461
  size: {
486
462
  width: 0,
487
463
  height: 0
@@ -530,13 +506,19 @@ function parseDrawingContent(element, rels, media) {
530
506
  };
531
507
  const image = parseImage(element, rels ?? void 0, media ?? void 0);
532
508
  if (!image) return null;
533
- const drawing = {
509
+ const rawXml = captureVerbatimXml(element);
510
+ if (image.rId === void 0) return {
511
+ type: "drawing",
512
+ image,
513
+ rawXml,
514
+ rawXmlMode: DRAWING_RAW_XML_MODES.PRESERVE_ONLY
515
+ };
516
+ return {
534
517
  type: "drawing",
535
- image
518
+ image,
519
+ rawXml,
520
+ rawImageFingerprint: imageRawXmlFingerprint(image)
536
521
  };
537
- drawing.rawXml = captureVerbatimXml(element);
538
- drawing.rawImageFingerprint = imageRawXmlFingerprint(image);
539
- return drawing;
540
522
  }
541
523
  /**
542
524
  * Parse all content within a run element
@@ -750,4 +732,4 @@ function getFieldCharType(run) {
750
732
  return run.content.find((c) => c.type === "fieldChar")?.charType ?? null;
751
733
  }
752
734
  //#endregion
753
- export { getFieldCharType, getImages, getRunText, hasContent, hasFieldChar, hasImage, parseRun, parseRunProperties };
735
+ export { getFieldCharType, getImages, getRunText, hasContent, hasFieldChar, hasImage, isBaselineVertAlign, parseRun, parseRunProperties };
@@ -1,21 +1,27 @@
1
+ import { escapeXmlAttribute } from "@stll/docx-core";
1
2
  //#region src/docx/sdtPropertiesPatch.ts
2
- const XML_ATTR_ESCAPES = {
3
- "&": "&amp;",
4
- "<": "&lt;",
5
- ">": "&gt;",
6
- "\"": "&quot;",
7
- "'": "&apos;"
8
- };
9
3
  /**
10
- * Escape a value for embedding in an XML attribute. Single-pass replace
11
- * over the five XML metacharacters (incl. apostrophe so the function
12
- * stays correct for `'`-delimited attributes even though this file always
13
- * emits double-quoted ones). Output is XML written to a DOCX zip — no
14
- * HTML / browser interpretation downstream.
4
+ * Surgical patches for the captured `<w:sdtPr>` XML string.
5
+ *
6
+ * Background. The block-SDT serializer (commit 3) replays
7
+ * `properties.rawPropertiesXml` verbatim so unmodeled OOXML markers
8
+ * (`w:dataBinding`, `w15:repeatingSection`, custom XML mappings) survive
9
+ * a round trip. That replay is correct for unchanged controls but goes
10
+ * stale the moment the editor mutates a modeled property: a user
11
+ * toggling a checkbox, picking a date, or choosing a dropdown value
12
+ * updates `properties.checked` / `dateFormat` / etc., but the raw
13
+ * `w14:checked w14:val="0"` already encoded by the source DOCX stays
14
+ * in `rawPropertiesXml` and gets written back on save — Word reopens
15
+ * the document with the user's interactive change discarded.
16
+ *
17
+ * `reconcileRawSdtPr` walks every modeled field that has an OOXML
18
+ * representation inside `<w:sdtPr>` and, if the field is set on the
19
+ * model, rewrites the matching element in the raw string. Unmodeled
20
+ * markers are untouched, so dataBinding / repeatingSection round-trip
21
+ * stays lossless even after the user has mutated the control.
22
+ *
23
+ * Picked up from upstream eigenpal/docx-editor#661.
15
24
  */
16
- function escapeXmlAttr(value) {
17
- return value.replace(/[&<>"']/gu, (ch) => XML_ATTR_ESCAPES[ch] ?? ch);
18
- }
19
25
  /**
20
26
  * Drop any `*:lastValue="…"` attribute (any namespace prefix, or
21
27
  * unprefixed) from an attribute-list string. We avoid a single greedy
@@ -116,8 +122,8 @@ function reconcileRawSdtPr(raw, props, options = {}) {
116
122
  if (fullDate !== void 0 || dateFormat !== void 0) {
117
123
  const wDate = /<(?<prefix>\w+):date\b(?<attrs>[^>]*)>(?<inner>[\s\S]*?)<\/\w+:date>/iu;
118
124
  const wDateSelf = /<(?<prefix>\w+):date\b(?<attrs>[^/>]*)\/>/iu;
119
- const fullDateAttr = fullDate !== void 0 ? ` w:fullDate="${escapeXmlAttr(fullDate)}"` : "";
120
- const formatChild = dateFormat !== void 0 ? `<w:dateFormat w:val="${escapeXmlAttr(dateFormat)}"/>` : "";
125
+ const fullDateAttr = fullDate !== void 0 ? ` w:fullDate="${escapeXmlAttribute(fullDate)}"` : "";
126
+ const formatChild = dateFormat !== void 0 ? `<w:dateFormat w:val="${escapeXmlAttribute(dateFormat)}"/>` : "";
121
127
  if (wDate.test(next)) next = next.replace(wDate, (_match, prefix, matchedAttrs, inner) => {
122
128
  let body = inner.replaceAll(/<\w+:dateFormat\b[^>]*(?:\/>|>[\s\S]*?<\/\w+:dateFormat>)/giu, "");
123
129
  if (formatChild) body = `${formatChild}${body}`;
@@ -128,7 +134,7 @@ function reconcileRawSdtPr(raw, props, options = {}) {
128
134
  }
129
135
  }
130
136
  if ((props.sdtType === "dropdown" || props.sdtType === "comboBox") && options.dropdownLastValue !== void 0) {
131
- const escapedValue = escapeXmlAttr(options.dropdownLastValue);
137
+ const escapedValue = escapeXmlAttribute(options.dropdownLastValue);
132
138
  const opened = /<(?<prefix>\w+):(?<name>dropDownList|comboBox)\b(?<attrs>[^>]*)>(?<inner>[\s\S]*?)<\/\w+:(?:dropDownList|comboBox)>/iu;
133
139
  const selfClosing = /<(?<prefix>\w+):(?<name>dropDownList|comboBox)\b(?<attrs>[^/>]*)\/>/iu;
134
140
  if (opened.test(next)) next = next.replace(opened, (_match, prefix, name, attrs, inner) => {