@stll/folio-core 0.44.0 → 0.46.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 (263) hide show
  1. package/dist/ai-edits/__fixtures__/paragraphs.js +2 -2
  2. package/dist/ai-edits/apply.js +59 -19
  3. package/dist/ai-edits/headless.js +2 -1
  4. package/dist/compare/compare.js +2 -5
  5. package/dist/compare/content-alignment.js +78 -53
  6. package/dist/compare/inline-atoms.js +34 -20
  7. package/dist/compare/scenario.js +2 -5
  8. package/dist/compare/types.d.ts +9 -1
  9. package/dist/compare/types.js +12 -1
  10. package/dist/compat/eigenpal.d.ts +2 -1
  11. package/dist/compat/eigenpal.js +2 -1
  12. package/dist/content-controls/checkboxDisplay.d.ts +11 -0
  13. package/dist/content-controls/checkboxDisplay.js +40 -0
  14. package/dist/content-controls/findContentControls.js +3 -1
  15. package/dist/content-controls/mutateContentControls.js +21 -4
  16. package/dist/controller/contentControlWidgetController.d.ts +1 -0
  17. package/dist/controller/contentControlWidgetController.js +3 -0
  18. package/dist/controller/hiddenEditorApi.js +3 -1
  19. package/dist/controller/hiddenEditorManager.d.ts +1 -1
  20. package/dist/controller/hiddenEditorManager.js +6 -3
  21. package/dist/display-list/build/buildDisplayList.js +20 -1
  22. package/dist/display-list/build/imagePrimitives.d.ts +1 -1
  23. package/dist/display-list/build/imagePrimitives.js +3 -1
  24. package/dist/display-list/build/paragraphPrimitives.js +21 -2
  25. package/dist/display-list/dom/renderDisplayListToDom.js +18 -10
  26. package/dist/display-list/types.d.ts +17 -0
  27. package/dist/document-operations.js +14 -3
  28. package/dist/docx/attributeRemainder.d.ts +43 -0
  29. package/dist/docx/attributeRemainder.js +65 -0
  30. package/dist/docx/blockContentParser.d.ts +1 -1
  31. package/dist/docx/blockContentParser.js +87 -63
  32. package/dist/docx/blockPlainText.d.ts +3 -4
  33. package/dist/docx/blockPlainText.js +1 -0
  34. package/dist/docx/bookmarkPlacement.js +2 -0
  35. package/dist/docx/borderParser.js +5 -5
  36. package/dist/docx/commentAnchorIndex.d.ts +30 -0
  37. package/dist/docx/commentAnchorIndex.js +50 -0
  38. package/dist/docx/commentParser.d.ts +1 -1
  39. package/dist/docx/commentParser.js +78 -50
  40. package/dist/docx/commentRangeIntegrity.d.ts +16 -1
  41. package/dist/docx/commentRangeIntegrity.js +42 -1
  42. package/dist/docx/commentRangeJoin.d.ts +9 -0
  43. package/dist/docx/commentRangeJoin.js +33 -0
  44. package/dist/docx/commentReferenceCompletion.d.ts +9 -0
  45. package/dist/docx/commentReferenceCompletion.js +47 -0
  46. package/dist/docx/commentReferenceNormalization.js +1 -0
  47. package/dist/docx/commentReplyMarkers.js +11 -19
  48. package/dist/docx/commentThreadKey.d.ts +18 -0
  49. package/dist/docx/commentThreadKey.js +22 -0
  50. package/dist/docx/compatibility.js +1 -0
  51. package/dist/docx/containerChildren.d.ts +111 -0
  52. package/dist/docx/containerChildren.gen.d.ts +28 -0
  53. package/dist/docx/containerChildren.gen.js +246 -0
  54. package/dist/docx/containerChildren.js +103 -0
  55. package/dist/docx/diagramPreview.js +87 -27
  56. package/dist/docx/documentParser.d.ts +1 -1
  57. package/dist/docx/documentParser.js +3 -3
  58. package/dist/docx/ensureParaIds.js +17 -7
  59. package/dist/docx/fieldParser.d.ts +1 -1
  60. package/dist/docx/fieldParser.js +4 -4
  61. package/dist/docx/fieldState.d.ts +26 -0
  62. package/dist/docx/fieldState.js +53 -0
  63. package/dist/docx/footnoteParser.d.ts +1 -1
  64. package/dist/docx/graphicFrameLocks.d.ts +16 -4
  65. package/dist/docx/graphicFrameLocks.js +19 -6
  66. package/dist/docx/groupDrawingParser.js +3 -3
  67. package/dist/docx/headerFooterParser.js +1 -1
  68. package/dist/docx/headerFooterReferenceNormalization.js +1 -0
  69. package/dist/docx/hyperlinkParser.d.ts +27 -8
  70. package/dist/docx/hyperlinkParser.js +78 -20
  71. package/dist/docx/imageParser.d.ts +9 -1
  72. package/dist/docx/imageParser.js +91 -20
  73. package/dist/docx/imageRawXml.d.ts +14 -1
  74. package/dist/docx/imageRawXml.js +30 -6
  75. package/dist/docx/inlineWrapperContent.d.ts +85 -0
  76. package/dist/docx/inlineWrapperContent.js +84 -0
  77. package/dist/docx/mathToMathml.js +12 -14
  78. package/dist/docx/nonVisualDrawingProps.d.ts +34 -0
  79. package/dist/docx/nonVisualDrawingProps.js +46 -0
  80. package/dist/docx/normalizeBaseDirection.js +10 -1
  81. package/dist/docx/paraIdAttribute.d.ts +21 -0
  82. package/dist/docx/paraIdAttribute.js +64 -0
  83. package/dist/docx/paragraphParser.d.ts +1 -1
  84. package/dist/docx/paragraphParser.js +277 -168
  85. package/dist/docx/paragraphPropertySource.js +5 -1
  86. package/dist/docx/paragraphTextBoxEnrichment.d.ts +1 -1
  87. package/dist/docx/paragraphTextBoxEnrichment.js +9 -52
  88. package/dist/docx/paragraphTraversal.js +7 -4
  89. package/dist/docx/parseWarningMessage.js +2 -1
  90. package/dist/docx/parser.js +2 -2
  91. package/dist/docx/preservedRunContent.d.ts +32 -0
  92. package/dist/docx/preservedRunContent.js +86 -0
  93. package/dist/docx/previewBudget.d.ts +64 -0
  94. package/dist/docx/previewBudget.js +88 -0
  95. package/dist/docx/renderedPageBreakNormalization.js +2 -4
  96. package/dist/docx/revisionIdNormalization.js +17 -5
  97. package/dist/docx/rezip.js +91 -36
  98. package/dist/docx/runParser.d.ts +1 -1
  99. package/dist/docx/runParser.js +16 -12
  100. package/dist/docx/sdtPropertiesPatch.js +24 -18
  101. package/dist/docx/sectionParser.js +8 -1
  102. package/dist/docx/sectionReferenceHistory.js +2 -2
  103. package/dist/docx/selectiveSave.js +6 -6
  104. package/dist/docx/selectiveXmlPatch.d.ts +46 -2
  105. package/dist/docx/selectiveXmlPatch.js +86 -39
  106. package/dist/docx/serializer/blockSdtSerializer.js +38 -26
  107. package/dist/docx/serializer/borderSerializer.d.ts +1 -1
  108. package/dist/docx/serializer/borderSerializer.js +15 -14
  109. package/dist/docx/serializer/commentSerializer.d.ts +41 -16
  110. package/dist/docx/serializer/commentSerializer.js +87 -92
  111. package/dist/docx/serializer/documentSerializer.js +8 -11
  112. package/dist/docx/serializer/fontTableSerializer.js +6 -6
  113. package/dist/docx/serializer/headerFooterSerializer.js +12 -14
  114. package/dist/docx/serializer/markupRangeAttributes.js +2 -2
  115. package/dist/docx/serializer/noteSerializer.js +8 -8
  116. package/dist/docx/serializer/numberingSerializer.js +7 -6
  117. package/dist/docx/serializer/paragraphSerializer.js +93 -72
  118. package/dist/docx/serializer/partNamespaces.js +2 -2
  119. package/dist/docx/serializer/runSerializer.js +73 -44
  120. package/dist/docx/serializer/sectionPropertiesSerializer.js +19 -14
  121. package/dist/docx/serializer/settingsSerializer.js +4 -3
  122. package/dist/docx/serializer/stylesSerializer.js +6 -6
  123. package/dist/docx/serializer/tableSerializer.js +28 -17
  124. package/dist/docx/serializer/textFormattingSerializer.js +29 -28
  125. package/dist/docx/serializer/themeSerializer.js +6 -6
  126. package/dist/docx/serializer/trackedChangeAttributes.js +2 -2
  127. package/dist/docx/serializer/xmlUtils.d.ts +1 -2
  128. package/dist/docx/serializer/xmlUtils.js +1 -13
  129. package/dist/docx/server/boundedArchive.d.ts +12 -0
  130. package/dist/docx/server/boundedArchive.js +20 -1
  131. package/dist/docx/server/createBilingualDocument.js +3 -1
  132. package/dist/docx/server/materializeYjsDocx.d.ts +1 -1
  133. package/dist/docx/server/materializeYjsDocx.js +9 -1
  134. package/dist/docx/server/migrateYjsAttrSchema.d.ts +55 -0
  135. package/dist/docx/server/migrateYjsAttrSchema.js +95 -0
  136. package/dist/docx/server/validateDocxConformance.js +22 -1
  137. package/dist/docx/shapeParser.js +12 -10
  138. package/dist/docx/tableParser.d.ts +1 -1
  139. package/dist/docx/tableParser.js +206 -85
  140. package/dist/docx/textBoxParser.d.ts +25 -2
  141. package/dist/docx/textBoxParser.js +59 -4
  142. package/dist/docx/unzip.d.ts +23 -0
  143. package/dist/docx/unzip.js +37 -26
  144. package/dist/docx/verbatimCapture.js +1 -1
  145. package/dist/docx/vmlImageParser.d.ts +17 -1
  146. package/dist/docx/vmlImageParser.js +20 -3
  147. package/dist/docx/vmlPreview.d.ts +1 -3
  148. package/dist/docx/vmlPreview.js +2 -30
  149. package/dist/docx/xmlEncoding.d.ts +5 -0
  150. package/dist/docx/xmlEncoding.js +12 -0
  151. package/dist/docx/xmlParser.d.ts +26 -2
  152. package/dist/docx/xmlParser.js +70 -27
  153. package/dist/docx/xmlResourceLimits.d.ts +89 -9
  154. package/dist/docx/xmlResourceLimits.js +105 -24
  155. package/dist/headless-layout.js +10 -1
  156. package/dist/index.d.ts +2 -1
  157. package/dist/index.js +2 -1
  158. package/dist/internal/pageBreakRunSourceDescendantIndex.js +5 -2
  159. package/dist/internal/paragraphFormattingSerialization.js +3 -2
  160. package/dist/layout-bridge/convert/toFlowBlocks.js +102 -12
  161. package/dist/layout-engine/measure/tableCellFloating.d.ts +2 -0
  162. package/dist/layout-engine/measure/tableCellFloating.js +2 -0
  163. package/dist/layout-engine/types.d.ts +23 -1
  164. package/dist/layout-painter/renderImage.d.ts +5 -1
  165. package/dist/layout-painter/renderImage.js +14 -6
  166. package/dist/layout-painter/renderPage.d.ts +2 -0
  167. package/dist/layout-painter/renderPage.js +4 -0
  168. package/dist/layout-painter/renderParagraph.d.ts +13 -1
  169. package/dist/layout-painter/renderParagraph.js +26 -6
  170. package/dist/managers/autoSaveCodec.d.ts +9 -1
  171. package/dist/managers/autoSaveCodec.js +14 -12
  172. package/dist/markdown/images.js +1 -4
  173. package/dist/markdown/renderBlock.js +1 -0
  174. package/dist/markdown/renderRuns.d.ts +1 -1
  175. package/dist/markdown/renderRuns.js +28 -5
  176. package/dist/markdown/renderTable.js +22 -5
  177. package/dist/markdown/trailers.js +4 -1
  178. package/dist/pdf/images.js +142 -6
  179. package/dist/prosemirror/attrs/index.d.ts +14 -3
  180. package/dist/prosemirror/attrs/index.js +224 -2
  181. package/dist/prosemirror/authoredTransformAttrs.d.ts +28 -0
  182. package/dist/prosemirror/authoredTransformAttrs.js +64 -0
  183. package/dist/prosemirror/commands/contentControls.js +10 -2
  184. package/dist/prosemirror/commands/image.d.ts +9 -2
  185. package/dist/prosemirror/commands/image.js +43 -28
  186. package/dist/prosemirror/commands/pageBreak.js +1 -1
  187. package/dist/prosemirror/commentReferenceAttrs.d.ts +8 -0
  188. package/dist/prosemirror/commentReferenceAttrs.js +33 -0
  189. package/dist/prosemirror/commentReferenceIntegrity.d.ts +47 -0
  190. package/dist/prosemirror/commentReferenceIntegrity.js +43 -0
  191. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -13
  192. package/dist/prosemirror/conversion/fromProseDoc.js +338 -99
  193. package/dist/prosemirror/conversion/index.d.ts +2 -2
  194. package/dist/prosemirror/conversion/toProseDoc.d.ts +7 -9
  195. package/dist/prosemirror/conversion/toProseDoc.js +551 -327
  196. package/dist/prosemirror/extensions/StarterKit.js +8 -58
  197. package/dist/prosemirror/extensions/core/DocExtension.js +1 -1
  198. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -1
  199. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +45 -27
  200. package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.js +3 -1
  201. package/dist/prosemirror/extensions/features/ImagePasteExtension.js +5 -3
  202. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +1 -0
  203. package/dist/prosemirror/extensions/markRegistry.d.ts +41 -0
  204. package/dist/prosemirror/extensions/markRegistry.js +75 -0
  205. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +2 -3
  206. package/dist/prosemirror/extensions/marks/InlineWrapperExtension.d.ts +18 -0
  207. package/dist/prosemirror/extensions/marks/InlineWrapperExtension.js +63 -0
  208. package/dist/prosemirror/extensions/marks/markUtils.d.ts +2 -1
  209. package/dist/prosemirror/extensions/marks/markUtils.js +47 -4
  210. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.d.ts +11 -0
  211. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +87 -0
  212. package/dist/prosemirror/extensions/nodes/FieldExtension.js +9 -7
  213. package/dist/prosemirror/extensions/nodes/ImageExtension.js +20 -0
  214. package/dist/prosemirror/extensions/nodes/PreservedBlockExtension.d.ts +32 -0
  215. package/dist/prosemirror/extensions/nodes/PreservedBlockExtension.js +67 -0
  216. package/dist/prosemirror/extensions/nodes/PreservedXmlExtension.d.ts +16 -0
  217. package/dist/prosemirror/extensions/nodes/PreservedXmlExtension.js +60 -0
  218. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +10 -2
  219. package/dist/prosemirror/extensions/nodes/TableExtension.js +3 -2
  220. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +13 -5
  221. package/dist/prosemirror/inlineWrapperStack.d.ts +39 -0
  222. package/dist/prosemirror/inlineWrapperStack.js +75 -0
  223. package/dist/prosemirror/pageBreakRunProjection.d.ts +17 -7
  224. package/dist/prosemirror/pageBreakRunProjection.js +19 -9
  225. package/dist/prosemirror/paragraphFormattingProvenance.d.ts +6 -3
  226. package/dist/prosemirror/paragraphFormattingProvenance.js +12 -3
  227. package/dist/prosemirror/replacedAnnotations.d.ts +59 -0
  228. package/dist/prosemirror/replacedAnnotations.js +165 -0
  229. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +3 -1
  230. package/dist/prosemirror/runFormattingInlineCarriers.js +5 -1
  231. package/dist/prosemirror/schema/index.d.ts +3 -3
  232. package/dist/prosemirror/schema/marks.d.ts +25 -1
  233. package/dist/prosemirror/schema/nodes.d.ts +139 -3
  234. package/dist/prosemirror/schema/nodes.js +16 -1
  235. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +2 -0
  236. package/dist/prosemirror/trackedRunInlineAtoms.js +2 -0
  237. package/dist/prosemirror/validation.js +16 -2
  238. package/dist/prosemirror/yjsDocumentMetadata.d.ts +73 -0
  239. package/dist/prosemirror/yjsDocumentMetadata.js +171 -0
  240. package/dist/prosemirror/zeroWidthAnchors.js +2 -0
  241. package/dist/render-dom/commentAnchorAttributes.d.ts +37 -0
  242. package/dist/render-dom/commentAnchorAttributes.js +54 -0
  243. package/dist/server.d.ts +3 -1
  244. package/dist/server.js +3 -1
  245. package/dist/types/content.d.ts +2 -2
  246. package/dist/utils/base64.d.ts +36 -0
  247. package/dist/utils/base64.js +40 -0
  248. package/dist/utils/clipboard.d.ts +9 -1
  249. package/dist/utils/clipboard.js +29 -2
  250. package/dist/utils/findReplace.js +1 -1
  251. package/dist/utils/imageLuminance.d.ts +15 -0
  252. package/dist/utils/imageLuminance.js +32 -0
  253. package/dist/utils/mergeDocumentContent.js +7 -17
  254. package/dist/utils/replaceText.js +1 -0
  255. package/dist/utils/units.d.ts +10 -1
  256. package/dist/utils/units.js +12 -1
  257. package/dist/utils/urlSecurity.d.ts +8 -2
  258. package/dist/utils/urlSecurity.js +21 -3
  259. package/package.json +2 -2
  260. package/dist/docx/blockRangeMarkers.d.ts +0 -36
  261. package/dist/docx/blockRangeMarkers.js +0 -59
  262. package/dist/prosemirror/yjsParagraphSourceContract.d.ts +0 -9
  263. package/dist/prosemirror/yjsParagraphSourceContract.js +0 -26
@@ -19,7 +19,7 @@ import { RELATIONSHIP_TYPES, parseRelationships, resolveRelativePath } from "./r
19
19
  import { removeResolvedHeaderFooterParts } from "./removeHeaderFooterParts.js";
20
20
  import { normalizeRevisionIdsInXmlParts } from "./revisionIdNormalization.js";
21
21
  import { buildPatchedNotePartXml, collectChangedNoteParaIds, collectParaIds, patchNumberingDefinitions } from "./selectiveXmlPatch.js";
22
- import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
22
+ import { planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
23
23
  import { serializeDocument } from "./serializer/documentSerializer.js";
24
24
  import { serializeFontTableXml } from "./serializer/fontTableSerializer.js";
25
25
  import { serializeHeaderFooter } from "./serializer/headerFooterSerializer.js";
@@ -29,11 +29,10 @@ import { readRootNamespaceBindings } from "./serializer/partNamespaces.js";
29
29
  import { serializeSettingsXml } from "./serializer/settingsSerializer.js";
30
30
  import { serializeStyle, serializeStylesXml } from "./serializer/stylesSerializer.js";
31
31
  import { serializeThemeXml } from "./serializer/themeSerializer.js";
32
- import { escapeXml } from "./serializer/xmlUtils.js";
33
- import { OFFICE_RELATIONSHIP_NAMESPACE_URIS, WORDPROCESSINGML_NAMESPACE_URIS, findChild, getAttribute, getAttributeByNamespaceUri, getChildElements, getLocalName, getNamespaceUri, matchesName, parseXml, parseXmlDocument } from "./xmlParser.js";
32
+ import { OFFICE_RELATIONSHIP_NAMESPACE_URIS, WORDPROCESSINGML_NAMESPACE_URIS, findChild, findChildByNamespaceUri, findChildrenByNamespaceUri, getAttribute, getAttributeByNamespaceUri, getChildElements, getLocalName, getNamespaceUri, matchesName, parseXml, parseXmlDocument } from "./xmlParser.js";
34
33
  import { assertXmlResourceLimits } from "./xmlResourceLimits.js";
35
34
  import { panic } from "better-result";
36
- import { validateDocxPackage } from "@stll/docx-core";
35
+ import { escapeXmlAttribute, escapeXmlText, validateDocxPackage } from "@stll/docx-core";
37
36
  import JSZip from "jszip";
38
37
  //#region src/docx/rezip.ts
39
38
  /**
@@ -85,7 +84,7 @@ function findMaxRId(relsXml) {
85
84
  }
86
85
  const isWordprocessingElement = (element, localName) => getLocalName(element.name) === localName && WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "");
87
86
  const countDocumentSections = (xml) => {
88
- assertXmlResourceLimits(xml);
87
+ assertXmlResourceLimits({ xml });
89
88
  let count = 0;
90
89
  const pending = [{
91
90
  element: parseXml(xml),
@@ -106,7 +105,7 @@ const countDocumentSections = (xml) => {
106
105
  };
107
106
  const extractHeaderFooterReferences = (xml) => {
108
107
  const references = [];
109
- assertXmlResourceLimits(xml);
108
+ assertXmlResourceLimits({ xml });
110
109
  const pending = [parseXml(xml)];
111
110
  while (pending.length > 0) {
112
111
  const node = pending.pop();
@@ -166,15 +165,15 @@ async function serializeCommentsToZip(doc, zip, compressionLevel) {
166
165
  if (comments.length === 0) {
167
166
  if (sourceCommentsXml === void 0 || !hasCommentEntries(sourceCommentsXml)) return;
168
167
  }
169
- ensureThreadedCommentParaIds(comments);
170
- 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));
171
170
  zip.file(sourceCommentsFile?.name ?? "word/comments.xml", commentsXml, {
172
171
  compression: "DEFLATE",
173
172
  compressionOptions: { level: compressionLevel }
174
173
  });
175
174
  await ensureCommentsContentType(zip, compressionLevel);
176
175
  await ensureCommentsRelationship(zip, compressionLevel);
177
- await syncCommentsExtendedPart(comments, zip, compressionLevel);
176
+ await syncCommentsExtendedPart(plan, zip, compressionLevel);
178
177
  }
179
178
  const hasCommentEntries = (xml) => {
180
179
  const root = parseXml(xml);
@@ -189,8 +188,8 @@ const hasCommentEntries = (xml) => {
189
188
  * removed part triggers Word's repair prompt). The reply-thread markers in
190
189
  * `document.xml` are synthesized separately (see {@link applyReplyThreadMarkers}).
191
190
  */
192
- async function syncCommentsExtendedPart(comments, zip, compressionLevel) {
193
- const xml = serializeCommentsExtended(comments);
191
+ async function syncCommentsExtendedPart(plan, zip, compressionLevel) {
192
+ const xml = serializeCommentsExtended(plan);
194
193
  const existing = findZipEntryCaseInsensitive(zip, COMMENTS_EXTENDED_PART_LOWER);
195
194
  if (!xml) {
196
195
  if (!existing) return;
@@ -396,7 +395,7 @@ async function processNewImages(parts, zip, compressionLevel) {
396
395
  compression: "DEFLATE",
397
396
  compressionOptions: { level: compressionLevel }
398
397
  });
399
- 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))}"/>`);
400
399
  extensionsAdded.add(extension);
401
400
  if (drawing.rawXml) drawing.rawXml = rebindDrawingImageRelationship({
402
401
  xml: drawing.rawXml,
@@ -444,6 +443,7 @@ function collectHyperlinksWithoutRId(blocks) {
444
443
  break;
445
444
  case "simpleField":
446
445
  case "inlineSdt":
446
+ case "inlineWrapper":
447
447
  case "insertion":
448
448
  case "deletion":
449
449
  case "moveFrom":
@@ -455,7 +455,7 @@ function collectHyperlinksWithoutRId(blocks) {
455
455
  };
456
456
  for (const block of blocks) if (block.type === "paragraph") collectInlineHyperlinks(block.content);
457
457
  else if (block.type === "table") for (const row of block.rows) for (const cell of row.cells) hyperlinks.push(...collectHyperlinksWithoutRId(cell.content));
458
- else hyperlinks.push(...collectHyperlinksWithoutRId(block.content));
458
+ else if (block.type === "blockSdt") hyperlinks.push(...collectHyperlinksWithoutRId(block.content));
459
459
  return hyperlinks;
460
460
  }
461
461
  /** The selective save boundary must use the same resource census as full repack. */
@@ -476,7 +476,7 @@ async function processNewHyperlinks(parts, zip, compressionLevel) {
476
476
  if (!hyperlink.href) continue;
477
477
  maxId++;
478
478
  const newRId = `rId${maxId}`;
479
- relEntries.push(`<Relationship Id="${newRId}" Type="${RELATIONSHIP_TYPES.hyperlink}" Target="${escapeXml(hyperlink.href)}" TargetMode="External"/>`);
479
+ relEntries.push(`<Relationship Id="${newRId}" Type="${RELATIONSHIP_TYPES.hyperlink}" Target="${escapeXmlAttribute(hyperlink.href)}" TargetMode="External"/>`);
480
480
  hyperlink.rId = newRId;
481
481
  }
482
482
  zip.file(relsPath, relsXml.replace("</Relationships>", `${relEntries.join("")}</Relationships>`), {
@@ -952,7 +952,7 @@ async function addRelationship(originalBuffer, relationship) {
952
952
  const relsXml = await relsFile.async("text");
953
953
  const newRId = `rId${findMaxRId(relsXml) + 1}`;
954
954
  const targetModeAttr = relationship.targetMode === "External" ? " TargetMode=\"External\"" : "";
955
- const newRelElement = `<Relationship Id="${newRId}" Type="${relationship.type}" Target="${escapeXml(relationship.target)}"${targetModeAttr}/>`;
955
+ const newRelElement = `<Relationship Id="${newRId}" Type="${relationship.type}" Target="${escapeXmlAttribute(relationship.target)}"${targetModeAttr}/>`;
956
956
  const updatedRelsXml = relsXml.replace("</Relationships>", `${newRelElement}</Relationships>`);
957
957
  zip.file(relsPath, updatedRelsXml);
958
958
  return {
@@ -1101,7 +1101,7 @@ async function materializeNewHeaderFooterParts(doc, zip, compressionLevel) {
1101
1101
  type: relType,
1102
1102
  target: filename
1103
1103
  });
1104
- relEntries.push(`<Relationship Id="${escapeXml(effectiveRId)}" Type="${relType}" Target="${filename}"/>`);
1104
+ relEntries.push(`<Relationship Id="${escapeXmlAttribute(effectiveRId)}" Type="${relType}" Target="${filename}"/>`);
1105
1105
  overrides.push(`<Override PartName="/word/${filename}" ContentType="${contentType}"/>`);
1106
1106
  }
1107
1107
  };
@@ -1218,7 +1218,7 @@ async function rebindWatermarkRelIds(doc, zip, compressionLevel) {
1218
1218
  }
1219
1219
  if (!resolvedRId) {
1220
1220
  resolvedRId = `rId${findMaxRId(relsXml) + 1}`;
1221
- 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))}"/>`;
1221
+ 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))}"/>`;
1222
1222
  relsXmlByPath.set(relsPath, relsXml.replace("</Relationships>", `${relXml}</Relationships>`));
1223
1223
  changedPaths.add(relsPath);
1224
1224
  }
@@ -1426,7 +1426,26 @@ const seedStylesXmlWith = (missing) => {
1426
1426
  const rootClose = SEED_STYLES_XML.lastIndexOf(STYLES_CLOSE_ROOT);
1427
1427
  return SEED_STYLES_XML.slice(0, rootClose) + missing.map(serializeStyle).join("") + SEED_STYLES_XML.slice(rootClose);
1428
1428
  };
1429
- const STYLE_ID_PATTERN = /<w:style\b[^>]*?\bw:styleId="(?<id>[^"]+)"/gu;
1429
+ /**
1430
+ * The style ids `word/styles.xml` already defines.
1431
+ *
1432
+ * Read by parsing the part, because the model holds the *decoded* id and the
1433
+ * part holds its XML spelling: `w:styleId='Body A'` is single-quoted,
1434
+ * `w:styleId="Header &amp; Footer"` is escaped, and either may sit behind an
1435
+ * attribute value containing `>`. A reader that misses one of those reports a
1436
+ * style the part already defines as missing, and
1437
+ * {@link serializeAddedStylesIntoZip} appends it — on every save, for ever.
1438
+ */
1439
+ const definedStyleIds = (stylesXml) => {
1440
+ const root = parseXml(stylesXml);
1441
+ const styles = findChildByNamespaceUri(root, WORDPROCESSINGML_NAMESPACE_URIS, "styles") ?? root;
1442
+ const ids = /* @__PURE__ */ new Set();
1443
+ for (const style of findChildrenByNamespaceUri(styles, WORDPROCESSINGML_NAMESPACE_URIS, "style")) {
1444
+ const id = getAttributeByNamespaceUri(style, WORDPROCESSINGML_NAMESPACE_URIS, "styleId") ?? style.attributes?.["w:styleId"];
1445
+ if (id !== void 0 && id !== null && id !== "") ids.add(String(id));
1446
+ }
1447
+ return ids;
1448
+ };
1430
1449
  /**
1431
1450
  * Append styles the model defines but the original `word/styles.xml` lacks.
1432
1451
  * Existing styles stay byte-exact (edits to them are not written here, the
@@ -1472,9 +1491,13 @@ async function serializeAddedStylesIntoZip(doc, originalZip, newZip, compression
1472
1491
  });
1473
1492
  return;
1474
1493
  }
1475
- const existing = /* @__PURE__ */ new Set();
1476
- for (const match of originalXml.matchAll(STYLE_ID_PATTERN)) existing.add(match.groups["id"]);
1477
- const added = styles.styles.filter((style) => !existing.has(style.styleId));
1494
+ const existing = new Set(definedStyleIds(originalXml));
1495
+ const added = [];
1496
+ for (const style of styles.styles) {
1497
+ if (existing.has(style.styleId)) continue;
1498
+ existing.add(style.styleId);
1499
+ added.push(style);
1500
+ }
1478
1501
  if (added.length === 0) return;
1479
1502
  const patched = originalXml.slice(0, rootClose) + added.map(serializeStyle).join("") + originalXml.slice(rootClose);
1480
1503
  newZip.file(file.name, patched, {
@@ -1482,28 +1505,60 @@ async function serializeAddedStylesIntoZip(doc, originalZip, newZip, compression
1482
1505
  compressionOptions: { level: compressionLevel }
1483
1506
  });
1484
1507
  }
1508
+ /**
1509
+ * The XML to write for a patched note part, or null to keep the original.
1510
+ *
1511
+ * A refusal over comment-range balance is not a failure to serialize: the
1512
+ * splice was too narrow to keep a comment's range whole, so the part is
1513
+ * written from the model, which is balanced with itself and with the
1514
+ * `document.xml` this repack rewrites beside it. Word's required separator
1515
+ * notes come back synthesized rather than byte-exact, the price of keeping the
1516
+ * comment anchored to the text it was written about. Any other refusal means a
1517
+ * changed paragraph could not be located in the part at all, which no wider
1518
+ * rewrite fixes.
1519
+ */
1520
+ const notePartXmlFor = ({ patch, originalXml, replacementXml, hasDirtyParagraph, elementName, partName }) => {
1521
+ const keepOriginal = () => {
1522
+ if (hasDirtyParagraph) throw new DocxPackageFidelityError(`Cannot serialize changed ${elementName} paragraphs into ${partName}`);
1523
+ return null;
1524
+ };
1525
+ switch (patch.type) {
1526
+ case "patched": return patch.xml === originalXml ? keepOriginal() : patch.xml;
1527
+ case "refused": switch (patch.reason) {
1528
+ case "comment-range-balance": return replacementXml;
1529
+ case "unroutable-paragraph": return keepOriginal();
1530
+ default: {
1531
+ const unreachable = patch.reason;
1532
+ return panic("Unhandled note-part patch refusal", { reason: unreachable });
1533
+ }
1534
+ }
1535
+ default: return panic("Unhandled note-part patch", { patch });
1536
+ }
1537
+ };
1485
1538
  async function patchNotePartIntoZip({ conventionalLowerPath, currentXml, replacementXml, baselineFrom, elementName, changedNoteParaIds, originalZip, newZip, compressionLevel }) {
1486
1539
  const file = findNotePartEntry(originalZip, conventionalLowerPath);
1487
1540
  if (!file) return;
1488
1541
  const originalXml = await file.async("text");
1489
1542
  const baselineXml = baselineFrom(originalXml);
1490
1543
  const effectiveChangedNoteParaIds = changedNoteParaIds ?? collectChangedNoteParaIds(baselineXml, currentXml);
1491
- const patched = buildPatchedNotePartXml({
1544
+ const currentParaIds = collectParaIds(currentXml);
1545
+ const patchedXml = notePartXmlFor({
1546
+ patch: buildPatchedNotePartXml({
1547
+ originalXml,
1548
+ baselineXml,
1549
+ serializedXml: currentXml,
1550
+ replacementXml,
1551
+ elementName,
1552
+ changedParaIds: effectiveChangedNoteParaIds
1553
+ }),
1492
1554
  originalXml,
1493
- baselineXml,
1494
- serializedXml: currentXml,
1495
1555
  replacementXml,
1556
+ hasDirtyParagraph: [...effectiveChangedNoteParaIds].some((paraId) => currentParaIds.has(paraId)),
1496
1557
  elementName,
1497
- changedParaIds: effectiveChangedNoteParaIds
1558
+ partName: file.name
1498
1559
  });
1499
- const currentParaIds = collectParaIds(currentXml);
1500
- const hasDirtyParagraph = [...effectiveChangedNoteParaIds].some((paraId) => currentParaIds.has(paraId));
1501
- if (patched === null || hasDirtyParagraph && patched === originalXml) {
1502
- if (hasDirtyParagraph) throw new DocxPackageFidelityError(`Cannot serialize changed ${elementName} paragraphs into ${file.name}`);
1503
- return;
1504
- }
1505
- if (patched === originalXml) return;
1506
- newZip.file(file.name, patched, {
1560
+ if (patchedXml === null || patchedXml === originalXml) return;
1561
+ newZip.file(file.name, patchedXml, {
1507
1562
  compression: "DEFLATE",
1508
1563
  compressionOptions: { level: compressionLevel }
1509
1564
  });
@@ -1531,7 +1586,7 @@ function updateCoreProperties(corePropsXml, { updateModifiedDate, modifiedBy })
1531
1586
  if (result.includes("<dcterms:modified")) result = result.replace(/<dcterms:modified[^<>]*>[^<]*<\/dcterms:modified>/u, `<dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified>`);
1532
1587
  }
1533
1588
  if (modifiedBy) {
1534
- if (result.includes("<cp:lastModifiedBy")) result = result.replace(/<cp:lastModifiedBy>[^<]*<\/cp:lastModifiedBy>/u, `<cp:lastModifiedBy>${escapeXml(modifiedBy)}</cp:lastModifiedBy>`);
1589
+ if (result.includes("<cp:lastModifiedBy")) result = result.replace(/<cp:lastModifiedBy>[^<]*<\/cp:lastModifiedBy>/u, `<cp:lastModifiedBy>${escapeXmlText(modifiedBy)}</cp:lastModifiedBy>`);
1535
1590
  }
1536
1591
  return result;
1537
1592
  }
@@ -1638,13 +1693,13 @@ const createEmptyDocxZip = ({ creator, application }) => {
1638
1693
  </w:document>`);
1639
1694
  zip.file(STYLES_PART_PATH, SEED_STYLES_XML);
1640
1695
  const now = (/* @__PURE__ */ new Date()).toISOString();
1641
- const creatorElement = creator === void 0 ? "" : `\n <dc:creator>${escapeXml(creator)}</dc:creator>`;
1696
+ const creatorElement = creator === void 0 ? "" : `\n <dc:creator>${escapeXmlText(creator)}</dc:creator>`;
1642
1697
  zip.file("docProps/core.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1643
1698
  <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}
1644
1699
  <dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created>
1645
1700
  <dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified>
1646
1701
  </cp:coreProperties>`);
1647
- const applicationElements = application === void 0 ? "" : `\n <Application>${escapeXml(application)}</Application>\n <AppVersion>${CREATED_APP_VERSION}</AppVersion>`;
1702
+ const applicationElements = application === void 0 ? "" : `\n <Application>${escapeXmlText(application)}</Application>\n <AppVersion>${CREATED_APP_VERSION}</AppVersion>`;
1648
1703
  zip.file("docProps/app.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1649
1704
  <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">${applicationElements}
1650
1705
  </Properties>`);
@@ -1,6 +1,6 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
- import { StyleMap } from "./styleParser.js";
3
2
  import { XmlElement } from "./xmlParser.js";
3
+ import { StyleMap } from "./styleParser.js";
4
4
  //#region src/docx/runParser.d.ts
5
5
  /**
6
6
  * `w:vertAlign` `baseline` is the reserved value that means "no vertical
@@ -1,9 +1,12 @@
1
1
  import { parseHorizontalScalePercent } from "../utils/horizontalScale.js";
2
+ import { NO_MODELLED_ATTRIBUTES, attributeRemainder } from "./attributeRemainder.js";
2
3
  import { parseDiagramPreview } from "./diagramPreview.js";
4
+ import { parseFieldState } from "./fieldState.js";
3
5
  import { isGroupDrawing, parseGroupDrawing } from "./groupDrawingParser.js";
4
6
  import { parseImage } from "./imageParser.js";
5
7
  import { imageRawXmlFingerprint } from "./imageRawXml.js";
6
8
  import { EmphasisMarkSchema, FontHintSchema, FontThemeSchema, HighlightColorSchema, PositionalTabAlignmentSchema, PositionalTabLeaderSchema, PositionalTabRelativeToSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
9
+ import { preserveRunChild } from "./preservedRunContent.js";
7
10
  import { parseShading } from "./shadingParser.js";
8
11
  import { parseShapeFromDrawing, shouldPreserveRawShapeDrawing } from "./shapeParser.js";
9
12
  import { isTextBoxDrawing } from "./textBoxParser.js";
@@ -11,7 +14,7 @@ import { resolveThemeFontRef } from "./themeParser.js";
11
14
  import { parsePropertyChangeInfo } from "./trackedChangeInfo.js";
12
15
  import { captureVerbatimXml } from "./verbatimCapture.js";
13
16
  import { parseVmlImageContent, shouldPreserveRawVmlPict } from "./vmlImageParser.js";
14
- import { cloneWithXmlnsDeclarations, findAllDeep, findChild, findChildren, getAttribute, getChildElements, getLocalName, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, parseOnOffAttribute, selectAlternateContentBranch } from "./xmlParser.js";
17
+ import { cloneWithXmlnsDeclarations, findAllDeep, findChild, findChildren, getAttribute, getChildElements, getLocalName, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, selectAlternateContentBranch } from "./xmlParser.js";
15
18
  import { DRAWING_RAW_XML_MODES } from "@stll/docx-core/model";
16
19
  //#region src/docx/runParser.ts
17
20
  /**
@@ -419,17 +422,14 @@ function parseEndnoteReference(element) {
419
422
  */
420
423
  function parseFieldChar(element) {
421
424
  const fldCharType = getAttribute(element, "w", "fldCharType");
422
- const fldLock = parseOnOffAttribute(element, "w", "fldLock") === true;
423
- const dirty = parseOnOffAttribute(element, "w", "dirty") === true;
424
425
  let charType = "begin";
425
426
  if (fldCharType === "separate") charType = "separate";
426
427
  else if (fldCharType === "end") charType = "end";
427
428
  const content = {
428
429
  type: "fieldChar",
429
- charType
430
+ charType,
431
+ ...parseFieldState(element)
430
432
  };
431
- if (fldLock) content.fldLock = true;
432
- if (dirty) content.dirty = true;
433
433
  const numberingChange = findChild(element, "w", "numberingChange");
434
434
  if (numberingChange) {
435
435
  const original = getAttribute(numberingChange, "w", "original");
@@ -576,10 +576,11 @@ function parseRunContents(runElement, rels, media, rootXmlns = {}) {
576
576
  }
577
577
  case "object": {
578
578
  const objectPreview = parseVmlImageContent(child, rels, media, rootXmlns);
579
- if (objectPreview) contents.push(objectPreview);
579
+ contents.push(objectPreview ?? preserveRunChild(child));
580
580
  break;
581
581
  }
582
582
  case "rPr": break;
583
+ case "commentReference": break;
583
584
  case "lastRenderedPageBreak":
584
585
  contents.push({ type: "renderedPageBreak" });
585
586
  break;
@@ -637,11 +638,9 @@ function parseRunContents(runElement, rels, media, rootXmlns = {}) {
637
638
  if (contents.length === contentsBeforeAlternate && !hasTextBoxBranch) contents.push(preserveOnlyDrawing(captureVerbatimXml(cloneWithXmlnsDeclarations(child, rootXmlns))));
638
639
  break;
639
640
  }
640
- case "footnoteRef":
641
- case "endnoteRef": break;
642
- case "separator":
643
- case "continuationSeparator": break;
644
- default: break;
641
+ default:
642
+ contents.push(preserveRunChild(child));
643
+ break;
645
644
  }
646
645
  return contents;
647
646
  }
@@ -668,6 +667,11 @@ function parseRun(node, styles, theme, rels = null, media = null, rootXmlns = {}
668
667
  if (propertyChangesResult) run.propertyChanges = propertyChangesResult;
669
668
  }
670
669
  run.content = parseRunContents(node, rels, media, mergeXmlnsDeclarations(rootXmlns, node));
670
+ const remainder = attributeRemainder({
671
+ element: node,
672
+ modelled: NO_MODELLED_ATTRIBUTES
673
+ });
674
+ if (remainder) run.preservedAttributes = remainder;
671
675
  return run;
672
676
  }
673
677
  /**
@@ -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) => {
@@ -1,3 +1,4 @@
1
+ import { NO_MODELLED_ATTRIBUTES, attributeRemainder } from "./attributeRemainder.js";
1
2
  import { parseBorderSpec } from "./borderParser.js";
2
3
  import { parseFooterReference, parseHeaderReference } from "./headerFooterRefParser.js";
3
4
  import { parseEndnoteProperties, parseFootnoteProperties } from "./notePropertiesParser.js";
@@ -159,7 +160,8 @@ function parseSectionProperties(sectPr, context) {
159
160
  if (space !== void 0) props.columnSpace = space;
160
161
  const equalWidth = parseOnOffAttribute(cols, "w", "equalWidth");
161
162
  if (equalWidth !== void 0) props.equalWidth = equalWidth;
162
- if (parseOnOffAttribute(cols, "w", "sep") === true) props.separator = true;
163
+ const separator = parseOnOffAttribute(cols, "w", "sep");
164
+ if (separator !== void 0) props.separator = separator;
163
165
  const colElements = findChildren(cols, "w", "col");
164
166
  if (colElements.length > 0) {
165
167
  props.columns = [];
@@ -310,6 +312,11 @@ function parseSectionProperties(sectPr, context) {
310
312
  return change;
311
313
  });
312
314
  if (propertyChanges.length > 0) props.propertyChanges = propertyChanges;
315
+ const remainder = attributeRemainder({
316
+ element: sectPr,
317
+ modelled: NO_MODELLED_ATTRIBUTES
318
+ });
319
+ if (remainder) props.preservedAttributes = remainder;
313
320
  Object.defineProperty(props, unserializedSectionPropertyChildNames, {
314
321
  enumerable: true,
315
322
  value: unhandledChildNames
@@ -1,7 +1,7 @@
1
1
  import { canonicalJson } from "../utils/canonicalJson.js";
2
- import { escapeXml } from "./serializer/xmlUtils.js";
3
2
  import { OFFICE_RELATIONSHIP_NAMESPACE_URIS, WORDPROCESSINGML_NAMESPACE_URIS, findChildrenByNamespaceUri, getAttributeByNamespaceUri, getChildElements, getLocalName, getNamespaceUri } from "./xmlParser.js";
4
3
  import { TaggedError } from "better-result";
4
+ import { escapeXmlAttribute } from "@stll/docx-core";
5
5
  //#region src/docx/sectionReferenceHistory.ts
6
6
  const SECTION_REFERENCE_HISTORY_NAMESPACE = "urn:stella:folio:section-reference-history:1";
7
7
  const HISTORY_NAMESPACES = /* @__PURE__ */ new Set([SECTION_REFERENCE_HISTORY_NAMESPACE]);
@@ -41,7 +41,7 @@ const serializeSectionReferenceHistory = (references) => {
41
41
  if (new Set(values.map(({ type }) => type)).size !== values.length) return invalidHistory();
42
42
  return values.map(({ type, rId }) => {
43
43
  if (!rId || rId.trim() !== rId) return invalidHistory();
44
- return `<w:${kind}Reference w:type="${type}" r:id="${escapeXml(rId)}"/>`;
44
+ return `<w:${kind}Reference w:type="${type}" r:id="${escapeXmlAttribute(rId)}"/>`;
45
45
  }).join("");
46
46
  };
47
47
  const content = serializeReferences("header", references.headerReferences ?? []) + serializeReferences("footer", references.footerReferences ?? []);
@@ -8,7 +8,7 @@ import { RELATIONSHIP_TYPES } from "./relsParser.js";
8
8
  import { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_PART_LOWER, addCommentsExtendedOverride, addCommentsExtendedRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, hasUnmaterializedInlineResources, updateCoreProperties, withoutAttachedTemplate } from "./rezip.js";
9
9
  import "./selectiveSaveFlags.js";
10
10
  import { buildPatchedDocumentXml, buildPatchedNoteXml, collectParaIds, patchNumberingDefinitions } from "./selectiveXmlPatch.js";
11
- import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
11
+ import { planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
12
12
  import { serializeDocument } from "./serializer/documentSerializer.js";
13
13
  import { serializeEndnotes, serializeFootnotes } from "./serializer/noteSerializer.js";
14
14
  import { serializeNumberingXml } from "./serializer/numberingSerializer.js";
@@ -84,8 +84,8 @@ const commentsExtendedInfoEqual = (a, b) => {
84
84
  * - Threading changed: write the part and ensure its override + relationship,
85
85
  * bailing if the packaging files needed to wire it are absent.
86
86
  */
87
- async function patchCommentsExtended(zip, comments, updates) {
88
- const desiredXml = serializeCommentsExtended(comments);
87
+ async function patchCommentsExtended(zip, plan, updates) {
88
+ const desiredXml = serializeCommentsExtended(plan);
89
89
  const existing = findZipEntryCaseInsensitive(zip, COMMENTS_EXTENDED_PART_LOWER);
90
90
  if (!desiredXml) return existing === null;
91
91
  const baselineInfo = existing ? parseCommentsExtended(await existing.async("text")) : /* @__PURE__ */ new Map();
@@ -190,6 +190,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
190
190
  if (!validateFolioDocumentModel(doc).valid) return null;
191
191
  const comments = doc.package.document.comments ?? [];
192
192
  const hasComments = comments.length > 0;
193
+ const commentPlan = planCommentParts(comments);
193
194
  try {
194
195
  const zip = await (await import("jszip")).default.loadAsync(originalBuffer);
195
196
  for (const [path, file] of Object.entries(zip.files)) if (!file.dir && isUnsafePackagePath(path)) return null;
@@ -215,9 +216,8 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
215
216
  }
216
217
  const sourceCommentsFile = zip.file("word/comments.xml");
217
218
  if (hasComments || sourceCommentsFile) {
218
- ensureThreadedCommentParaIds(comments);
219
219
  const sourceBindings = sourceCommentsFile ? readRootNamespaceBindings(await sourceCommentsFile.async("text")) : void 0;
220
- updates.set("word/comments.xml", serializeComments(comments, sourceBindings));
220
+ updates.set("word/comments.xml", serializeComments(commentPlan, sourceBindings));
221
221
  }
222
222
  if (hasComments) {
223
223
  const ctFile = zip.file("[Content_Types].xml");
@@ -235,7 +235,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
235
235
  }
236
236
  }
237
237
  }
238
- if (!await patchCommentsExtended(zip, comments, updates)) return null;
238
+ if (!await patchCommentsExtended(zip, commentPlan, updates)) return null;
239
239
  await patchNumberingPart(zip, doc, updates);
240
240
  for (const [path, xml] of await collectHeaderFooterUpdates(doc, zip)) updates.set(path, xml);
241
241
  await queueSettingsUpdates(zip, updates);
@@ -109,6 +109,24 @@ declare function buildPatchedDocumentXml(originalXml: string, serializedXml: str
109
109
  * the caller can fall back to preserving the original part verbatim.
110
110
  */
111
111
  declare function buildPatchedNoteXml(originalXml: string, serializedXml: string, changedIds: Set<string>): string | null;
112
+ /** One region of a part replaced by re-serialized XML. */
113
+ type XmlSplice = {
114
+ start: number;
115
+ end: number;
116
+ newXml: string;
117
+ };
118
+ /**
119
+ * Apply `splices` to `xml`, end-to-start so earlier offsets stay valid.
120
+ *
121
+ * Every selective patch is a splice, and every splice goes through here, so
122
+ * the refusal below is a property of the operation rather than a check each
123
+ * site has to remember. A patch rewrites the regions an edit touched and keeps
124
+ * the rest of the part byte-for-byte, so it can write half a comment range:
125
+ * invalid OOXML that anchors the comment to nothing. Answers null when it
126
+ * would, leaving the caller to rewrite a wider region — ultimately the whole
127
+ * part from the model, which is balanced with itself.
128
+ */
129
+ declare const spliceXml: (xml: string, splices: readonly XmlSplice[]) => string | null;
112
130
  type NoteElementName = "footnote" | "endnote";
113
131
  declare const collectChangedNoteParaIds: (baselineXml: string, currentXml: string) => Set<string>;
114
132
  type BuildPatchedNotePartXmlOptions = {
@@ -119,6 +137,25 @@ type BuildPatchedNotePartXmlOptions = {
119
137
  elementName: NoteElementName;
120
138
  changedParaIds?: ReadonlySet<string>;
121
139
  };
140
+ /** Why a note part could not be patched; see {@link NotePartPatch}. */
141
+ type NotePartPatchRefusal =
142
+ /** A note element, or a changed paragraph, was missing or ambiguous. */
143
+ "unroutable-paragraph" |
144
+ /** Even rewriting whole notes would leave a comment range with one half. */
145
+ "comment-range-balance";
146
+ /**
147
+ * What patching a note part produced. `refused` is not a failure to serialize:
148
+ * the caller writes the part from the model instead, the note-part reading of
149
+ * the fall back to a full repack the document story takes when its own splice
150
+ * is refused.
151
+ */
152
+ type NotePartPatch = {
153
+ type: "patched";
154
+ xml: string;
155
+ } | {
156
+ type: "refused";
157
+ reason: NotePartPatchRefusal;
158
+ };
122
159
  /**
123
160
  * Patch an existing note part from its model serialization.
124
161
  *
@@ -130,8 +167,15 @@ type BuildPatchedNotePartXmlOptions = {
130
167
  * notes, and unaffected equal-shape paragraphs remain byte-exact.
131
168
  * `replacementXml` also supplies synthesized automatic note-reference marks,
132
169
  * which the parsed model intentionally omits.
170
+ *
171
+ * A comment can be anchored on a note's own text, so its range spans that
172
+ * note's paragraphs and an edit inside the span moves a range half from one
173
+ * paragraph to another. Replacing only the dirty paragraph then drops the half
174
+ * it held and leaves the other standing, so {@link spliceXml} refuses the
175
+ * result and the changed notes are rewritten whole instead — model content on
176
+ * both sides of the range, with every other note still byte-exact.
133
177
  */
134
- declare function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, replacementXml, elementName, changedParaIds }: BuildPatchedNotePartXmlOptions): string | null;
178
+ declare function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, replacementXml, elementName, changedParaIds }: BuildPatchedNotePartXmlOptions): NotePartPatch;
135
179
  type ChangedNumberingDefs = {
136
180
  abstractNums: Set<string>;
137
181
  nums: Set<string>;
@@ -178,4 +222,4 @@ declare const patchNumberingDefinitions: ({ originalXml, baselineXml, currentXml
178
222
  */
179
223
  declare function appendNumberingDefs(xml: string, currentXml: string, added: ChangedNumberingDefs): string | null;
180
224
  //#endregion
181
- export { ChangedNumberingDefs, ParagraphOffsets, PatchSafetyOptions, PatchValidationResult, appendNumberingDefs, buildParagraphOffsetIndex, buildPatchedDocumentXml, buildPatchedNotePartXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, patchNumberingDefinitions, validatePatchSafety };
225
+ export { ChangedNumberingDefs, NotePartPatch, NotePartPatchRefusal, ParagraphOffsets, PatchSafetyOptions, PatchValidationResult, XmlSplice, appendNumberingDefs, buildParagraphOffsetIndex, buildPatchedDocumentXml, buildPatchedNotePartXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, patchNumberingDefinitions, spliceXml, validatePatchSafety };