@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
@@ -1,4 +1,6 @@
1
1
  import { modelParagraphFormattingEmission } from "../../internal/paragraphFormattingSerialization.js";
2
+ import { serializePreservedAttributes } from "../attributeRemainder.js";
3
+ import { fieldStateAttributes } from "../fieldState.js";
2
4
  import { getParagraphPropertySource, paragraphPropertySourceMatchesEmission } from "../paragraphPropertySource.js";
3
5
  import { reconcileRawSdtPr } from "../sdtPropertiesPatch.js";
4
6
  import { DATE_UTC_ATTRIBUTE } from "../trackedChangeInfo.js";
@@ -10,8 +12,9 @@ import { serializeRun } from "./runSerializer.js";
10
12
  import { serializeSectionProperties } from "./sectionPropertiesSerializer.js";
11
13
  import { serializeTextFormatting } from "./textFormattingSerializer.js";
12
14
  import { serializeTrackedChangeAttributes, trackedChangeAttributeRecord } from "./trackedChangeAttributes.js";
13
- import { escapeXml, isSingleWellFormedElement } from "./xmlUtils.js";
15
+ import { isSingleWellFormedElement } from "./xmlUtils.js";
14
16
  import { panic } from "better-result";
17
+ import { escapeXmlAttribute, escapeXmlText } from "@stll/docx-core";
15
18
  import { PARAGRAPH_MARK_CHANGE_KINDS } from "@stll/docx-core/model";
16
19
  //#region src/docx/serializer/paragraphSerializer.ts
17
20
  /**
@@ -331,25 +334,26 @@ function serializeParagraphPropertyChange(change) {
331
334
  /** The attribute list of a `w:hyperlink`, without its children. */
332
335
  function hyperlinkAttributes(hyperlink) {
333
336
  const attrs = [];
334
- if (hyperlink.rId) attrs.push(`r:id="${escapeXml(hyperlink.rId)}"`);
335
- if (hyperlink.anchor) attrs.push(`w:anchor="${escapeXml(hyperlink.anchor)}"`);
336
- if (hyperlink.tooltip) attrs.push(`w:tooltip="${escapeXml(hyperlink.tooltip)}"`);
337
- if (hyperlink.target) attrs.push(`w:tgtFrame="${escapeXml(hyperlink.target)}"`);
337
+ if (hyperlink.rId) attrs.push(`r:id="${escapeXmlAttribute(hyperlink.rId)}"`);
338
+ if (hyperlink.anchor) attrs.push(`w:anchor="${escapeXmlAttribute(hyperlink.anchor)}"`);
339
+ if (hyperlink.tooltip) attrs.push(`w:tooltip="${escapeXmlAttribute(hyperlink.tooltip)}"`);
340
+ if (hyperlink.target) attrs.push(`w:tgtFrame="${escapeXmlAttribute(hyperlink.target)}"`);
338
341
  if (hyperlink.history === true) attrs.push("w:history=\"1\"");
339
342
  else if (hyperlink.history === false) attrs.push("w:history=\"0\"");
340
- if (hyperlink.docLocation) attrs.push(`w:docLocation="${escapeXml(hyperlink.docLocation)}"`);
343
+ if (hyperlink.docLocation) attrs.push(`w:docLocation="${escapeXmlAttribute(hyperlink.docLocation)}"`);
341
344
  return attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
342
345
  }
343
346
  /** One `w:hyperlink` child, with the caller deciding how a run is written. */
344
347
  function serializeHyperlinkChild(child, serializeChildRun) {
345
348
  if (child.type === "run") return serializeChildRun(child);
346
- return child.type === "bookmarkStart" ? serializeBookmarkStart(child) : serializeBookmarkEnd(child);
349
+ if (child.type === "bookmarkStart") return serializeBookmarkStart(child);
350
+ return child.type === "bookmarkEnd" ? serializeBookmarkEnd(child) : child.xml;
347
351
  }
348
352
  /**
349
353
  * Serialize a hyperlink (w:hyperlink)
350
354
  */
351
- function serializeHyperlink(hyperlink) {
352
- const childrenXml = hyperlink.children.map((child) => serializeHyperlinkChild(child, serializeRun)).join("");
355
+ function serializeHyperlink(hyperlink, disposition = "kept") {
356
+ const childrenXml = hyperlink.children.map((child) => serializeHyperlinkChild(child, (run) => serializeInlineRun(run, disposition))).join("");
353
357
  return `<w:hyperlink${hyperlinkAttributes(hyperlink)}>${childrenXml}</w:hyperlink>`;
354
358
  }
355
359
  /**
@@ -366,10 +370,11 @@ function serializeBookmarkEnd(bookmark) {
366
370
  }
367
371
  /** Serialize a simple field without changing its authored OOXML field form. */
368
372
  function serializeSimpleField(field) {
369
- const attrs = [`w:instr="${escapeXml(field.instruction)}"`];
370
- if (field.fldLock) attrs.push("w:fldLock=\"true\"");
371
- if (field.dirty) attrs.push("w:dirty=\"true\"");
372
- const contentXml = field.content.map((item) => item.type === "run" ? serializeRun(item) : serializeHyperlink(item)).join("");
373
+ const attrs = [`w:instr="${escapeXmlAttribute(field.instruction)}"`, ...fieldStateAttributes(field)];
374
+ const contentXml = field.content.map((item) => {
375
+ if (item.type === "run") return serializeRun(item);
376
+ return item.type === "hyperlink" ? serializeHyperlink(item) : item.xml;
377
+ }).join("");
373
378
  return `<w:fldSimple ${attrs.join(" ")}>${contentXml}</w:fldSimple>`;
374
379
  }
375
380
  /**
@@ -381,14 +386,12 @@ function serializeComplexField(field) {
381
386
  const parts = [];
382
387
  const structuralFormatting = field.formatting ?? field.fieldResult[0]?.formatting;
383
388
  const rPrXml = structuralFormatting ? serializeTextFormatting(structuralFormatting) : "";
384
- const beginAttrs = ["w:fldCharType=\"begin\""];
385
- if (field.fldLock) beginAttrs.push("w:fldLock=\"true\"");
386
- if (field.dirty) beginAttrs.push("w:dirty=\"true\"");
389
+ const beginAttrs = ["w:fldCharType=\"begin\"", ...fieldStateAttributes(field)];
387
390
  parts.push(`<w:r>${rPrXml}<w:fldChar ${beginAttrs.join(" ")}/></w:r>`);
388
391
  if (field.fieldCode.length > 0) parts.push(...field.fieldCode.map((run) => serializeRun(run)));
389
392
  else if (field.instruction.length > 0) {
390
393
  const spaceAttr = field.instruction.startsWith(" ") || field.instruction.endsWith(" ") || field.instruction.includes(" ") ? " xml:space=\"preserve\"" : "";
391
- parts.push(`<w:r>${rPrXml}<w:instrText${spaceAttr}>${escapeXml(field.instruction)}</w:instrText></w:r>`);
394
+ parts.push(`<w:r>${rPrXml}<w:instrText${spaceAttr}>${escapeXmlText(field.instruction)}</w:instrText></w:r>`);
392
395
  }
393
396
  parts.push(`<w:r>${rPrXml}<w:fldChar w:fldCharType="separate"/></w:r>`);
394
397
  parts.push(...field.fieldResult.map((run) => serializeRun(run)));
@@ -406,29 +409,29 @@ function serializeComplexField(field) {
406
409
  function synthesizeInlineSdtPr(props) {
407
410
  const prParts = [];
408
411
  if (typeof props.id === "number") prParts.push(`<w:id w:val="${props.id}"/>`);
409
- if (props.alias) prParts.push(`<w:alias w:val="${escapeXml(props.alias)}"/>`);
410
- if (props.tag) prParts.push(`<w:tag w:val="${escapeXml(props.tag)}"/>`);
412
+ if (props.alias) prParts.push(`<w:alias w:val="${escapeXmlAttribute(props.alias)}"/>`);
413
+ if (props.tag) prParts.push(`<w:tag w:val="${escapeXmlAttribute(props.tag)}"/>`);
411
414
  if (props.lock && props.lock !== "unlocked") prParts.push(`<w:lock w:val="${props.lock}"/>`);
412
- if (props.placeholder) prParts.push(`<w:placeholder><w:docPart w:val="${escapeXml(props.placeholder)}"/></w:placeholder>`);
415
+ if (props.placeholder) prParts.push(`<w:placeholder><w:docPart w:val="${escapeXmlAttribute(props.placeholder)}"/></w:placeholder>`);
413
416
  if (props.showingPlaceholder) prParts.push("<w:showingPlcHdr/>");
414
417
  switch (props.sdtType) {
415
418
  case "plainText":
416
419
  prParts.push("<w:text/>");
417
420
  break;
418
421
  case "date": {
419
- const fullDateAttr = props.dateValueISO ? ` w:fullDate="${escapeXml(props.dateValueISO)}"` : "";
420
- const formatChild = props.dateFormat ? `<w:dateFormat w:val="${escapeXml(props.dateFormat)}"/>` : "";
422
+ const fullDateAttr = props.dateValueISO ? ` w:fullDate="${escapeXmlAttribute(props.dateValueISO)}"` : "";
423
+ const formatChild = props.dateFormat ? `<w:dateFormat w:val="${escapeXmlAttribute(props.dateFormat)}"/>` : "";
421
424
  if (fullDateAttr || formatChild) prParts.push(`<w:date${fullDateAttr}>${formatChild}</w:date>`);
422
425
  else prParts.push("<w:date/>");
423
426
  break;
424
427
  }
425
428
  case "dropdown": {
426
- const items = (props.listItems ?? []).map((i) => `<w:listItem w:displayText="${escapeXml(i.displayText)}" w:value="${escapeXml(i.value)}"/>`).join("");
429
+ const items = (props.listItems ?? []).map((i) => `<w:listItem w:displayText="${escapeXmlAttribute(i.displayText)}" w:value="${escapeXmlAttribute(i.value)}"/>`).join("");
427
430
  prParts.push(`<w:dropDownList>${items}</w:dropDownList>`);
428
431
  break;
429
432
  }
430
433
  case "comboBox": {
431
- const items = (props.listItems ?? []).map((i) => `<w:listItem w:displayText="${escapeXml(i.displayText)}" w:value="${escapeXml(i.value)}"/>`).join("");
434
+ const items = (props.listItems ?? []).map((i) => `<w:listItem w:displayText="${escapeXmlAttribute(i.displayText)}" w:value="${escapeXmlAttribute(i.value)}"/>`).join("");
432
435
  prParts.push(`<w:comboBox>${items}</w:comboBox>`);
433
436
  break;
434
437
  }
@@ -454,20 +457,22 @@ function synthesizeInlineSdtPr(props) {
454
457
  * the properties block was re-synthesized from the modeled projection alone,
455
458
  * silently dropping every unmodeled feature (and `w:sdtEndPr`) on save.
456
459
  */
457
- function serializeInlineSdt(sdt) {
460
+ function serializeInlineSdt(sdt, disposition = "kept") {
458
461
  const props = sdt.properties;
459
462
  const contentXml = sdt.content.map((item) => {
460
463
  switch (item.type) {
461
- case "run": return serializeRun(item);
462
- case "hyperlink": return serializeHyperlink(item);
464
+ case "run": return serializeInlineRun(item, disposition);
465
+ case "hyperlink": return serializeHyperlink(item, disposition);
463
466
  case "simpleField": return serializeSimpleField(item);
464
467
  case "complexField": return serializeComplexField(item);
465
- case "inlineSdt": return serializeInlineSdt(item);
468
+ case "inlineSdt": return serializeInlineSdt(item, disposition);
469
+ case "inlineWrapper": return serializeParagraphContent(item, disposition);
466
470
  case "insertion": return serializeTrackedChange("ins", item);
467
471
  case "deletion": return serializeTrackedChange("del", item);
468
472
  case "moveFrom": return serializeTrackedChange("moveFrom", item);
469
473
  case "moveTo": return serializeTrackedChange("moveTo", item);
470
474
  case "mathEquation": return item.ommlXml || "";
475
+ case "preservedInline": return item.xml;
471
476
  default: return item;
472
477
  }
473
478
  }).join("");
@@ -488,6 +493,22 @@ function serializeMoveRangeStart(tag, marker) {
488
493
  function rewriteRunTextAsDeleted(xml) {
489
494
  return xml.replace(/<w:t\b/gu, "<w:delText").replace(/<\/w:t>/gu, "</w:delText>").replace(/<w:instrText\b/gu, "<w:delInstrText").replace(/<\/w:instrText>/gu, "</w:delInstrText>");
490
495
  }
496
+ function serializeDeletedRun(run) {
497
+ const xml = serializeRun(run);
498
+ if (!run.content.some((c) => c.type === "drawing" || c.type === "shape")) return rewriteRunTextAsDeleted(xml);
499
+ if (!run.content.some((c) => c.type !== "drawing" && c.type !== "shape")) return xml;
500
+ return run.content.map((content) => {
501
+ const contentXml = serializeRun({
502
+ ...run,
503
+ content: [content]
504
+ });
505
+ if (content.type === "drawing" || content.type === "shape") return contentXml;
506
+ return rewriteRunTextAsDeleted(contentXml);
507
+ }).join("");
508
+ }
509
+ function serializeInlineRun(run, disposition) {
510
+ return disposition === "removed" ? serializeDeletedRun(run) : serializeRun(run);
511
+ }
491
512
  function trackedChangeTag(change) {
492
513
  switch (change.type) {
493
514
  case "insertion": return "ins";
@@ -498,29 +519,28 @@ function trackedChangeTag(change) {
498
519
  }
499
520
  function serializeTrackedChange(tag, change) {
500
521
  const attrs = serializeTrackedChangeAttributes(change.info);
501
- const serializeDeletedRun = (run) => {
502
- const xml = serializeRun(run);
503
- if (!run.content.some((c) => c.type === "drawing" || c.type === "shape")) return rewriteRunTextAsDeleted(xml);
504
- if (!run.content.some((c) => c.type !== "drawing" && c.type !== "shape")) return xml;
505
- return run.content.map((content) => {
506
- const contentXml = serializeRun({
507
- ...run,
508
- content: [content]
509
- });
510
- if (content.type === "drawing" || content.type === "shape") return contentXml;
511
- return rewriteRunTextAsDeleted(contentXml);
512
- }).join("");
513
- };
514
- const serializeContentRun = (run) => tag === "del" || tag === "moveFrom" ? serializeDeletedRun(run) : serializeRun(run);
522
+ const disposition = tag === "del" || tag === "moveFrom" ? "removed" : "kept";
523
+ const serializeContentRun = (run) => serializeInlineRun(run, disposition);
515
524
  const serializeWrappedItem = (item) => {
516
- if (item.type === "run") return serializeContentRun(item);
517
- if (item.type === "simpleField" || item.type === "complexField") {
518
- const xml = item.type === "simpleField" ? serializeSimpleField(item) : serializeComplexField(item);
519
- return tag === "del" || tag === "moveFrom" ? rewriteRunTextAsDeleted(xml) : xml;
525
+ switch (item.type) {
526
+ case "run": return serializeContentRun(item);
527
+ case "simpleField":
528
+ case "complexField": {
529
+ const xml = item.type === "simpleField" ? serializeSimpleField(item) : serializeComplexField(item);
530
+ return disposition === "removed" ? rewriteRunTextAsDeleted(xml) : xml;
531
+ }
532
+ case "mathEquation": return item.ommlXml;
533
+ case "insertion":
534
+ case "deletion":
535
+ case "moveFrom":
536
+ case "moveTo": return serializeTrackedChange(trackedChangeTag(item), item);
537
+ case "bookmarkStart": return serializeBookmarkStart(item);
538
+ case "bookmarkEnd": return serializeBookmarkEnd(item);
539
+ case "preservedInline": return item.xml;
540
+ case "inlineWrapper":
541
+ case "inlineSdt": return serializeParagraphContent(item, disposition);
542
+ default: return item;
520
543
  }
521
- if (item.type === "mathEquation") return item.ommlXml;
522
- if (item.type === "insertion" || item.type === "deletion" || item.type === "moveFrom" || item.type === "moveTo") return serializeTrackedChange(trackedChangeTag(item), item);
523
- return item.type === "bookmarkStart" ? serializeBookmarkStart(item) : serializeBookmarkEnd(item);
524
544
  };
525
545
  const open = `<w:${tag} ${attrs}>`;
526
546
  const close = `</w:${tag}>`;
@@ -553,27 +573,22 @@ function serializeCommentReferenceRun(id) {
553
573
  /**
554
574
  * Serialize a single paragraph content item.
555
575
  *
556
- * `explicitCommentReferenceIds` holds the comment ids that already have their
557
- * own `commentReference` node in this paragraph (the parsed-from-Word shape).
558
- * For those, the `commentRangeEnd` marker must NOT also synthesize a reference
559
- * run, or a save→parse round-trip doubles the `<w:commentReference>`. The
560
- * editor (fromProseDoc) path emits range markers with no reference node, so the
561
- * synthetic run is still written when the id is absent from the set.
576
+ * A `commentRangeEnd` writes an end and nothing else. Where the comment's
577
+ * reference run goes is authored data the model carries as its own item, and
578
+ * `completeCommentReferences` fills it in for a model that arrived without
579
+ * one; a guess made here from one paragraph cannot see the rest of the story.
562
580
  */
563
- function serializeParagraphContent(content, explicitCommentReferenceIds) {
581
+ function serializeParagraphContent(content, disposition = "kept") {
564
582
  switch (content.type) {
565
- case "run": return serializeRun(content);
566
- case "hyperlink": return serializeHyperlink(content);
583
+ case "run": return serializeInlineRun(content, disposition);
584
+ case "hyperlink": return serializeHyperlink(content, disposition);
567
585
  case "bookmarkStart": return serializeBookmarkStart(content);
568
586
  case "bookmarkEnd": return serializeBookmarkEnd(content);
569
587
  case "simpleField": return serializeSimpleField(content);
570
588
  case "complexField": return serializeComplexField(content);
571
- case "inlineSdt": return serializeInlineSdt(content);
589
+ case "inlineSdt": return serializeInlineSdt(content, disposition);
572
590
  case "commentRangeStart": return `<w:commentRangeStart ${markupRangeAttributes(content).join(" ")}/>`;
573
- case "commentRangeEnd": {
574
- const end = `<w:commentRangeEnd ${markupRangeAttributes(content).join(" ")}/>`;
575
- return explicitCommentReferenceIds.has(content.id) ? end : `${end}${serializeCommentReferenceRun(content.id)}`;
576
- }
591
+ case "commentRangeEnd": return `<w:commentRangeEnd ${markupRangeAttributes(content).join(" ")}/>`;
577
592
  case "commentReference": return serializeCommentReferenceRun(content.id);
578
593
  case "insertion": return serializeTrackedChange("ins", content);
579
594
  case "deletion": return serializeTrackedChange("del", content);
@@ -583,11 +598,18 @@ function serializeParagraphContent(content, explicitCommentReferenceIds) {
583
598
  case "moveFromRangeEnd": return `<w:moveFromRangeEnd ${markupRangeAttributes(content).join(" ")}/>`;
584
599
  case "moveToRangeStart": return serializeMoveRangeStart("moveToRangeStart", content);
585
600
  case "moveToRangeEnd": return `<w:moveToRangeEnd ${markupRangeAttributes(content).join(" ")}/>`;
586
- case "bidiWrapper": {
587
- const tag = content.control === "override" ? "bdo" : "dir";
588
- return `<w:${tag}${content.direction === void 0 ? "" : ` w:val="${escapeXml(content.direction)}"`}>${content.content.map((child) => serializeParagraphContent(child, explicitCommentReferenceIds)).join("")}</w:${tag}>`;
601
+ case "inlineWrapper": {
602
+ const inner = content.content.map((child) => serializeParagraphContent(child, disposition)).join("");
603
+ switch (content.kind) {
604
+ case "bidi": {
605
+ const tag = content.control === "override" ? "bdo" : "dir";
606
+ return `<w:${tag}${content.direction === void 0 ? "" : ` w:val="${escapeXmlAttribute(content.direction)}"`}>${inner}</w:${tag}>`;
607
+ }
608
+ default: return content.kind;
609
+ }
589
610
  }
590
611
  case "mathEquation": return content.ommlXml || "";
612
+ case "preservedInline": return content.xml;
591
613
  default: return "";
592
614
  }
593
615
  }
@@ -600,21 +622,20 @@ function serializeParagraphContent(content, explicitCommentReferenceIds) {
600
622
  function serializeParagraph(paragraph) {
601
623
  const parts = [];
602
624
  const attrs = [];
603
- if (paragraph.paraId) attrs.push(`w14:paraId="${escapeXml(paragraph.paraId)}"`);
604
- if (paragraph.textId) attrs.push(`w14:textId="${escapeXml(paragraph.textId)}"`);
625
+ if (paragraph.paraId) attrs.push(`w14:paraId="${escapeXmlAttribute(paragraph.paraId)}"`);
626
+ if (paragraph.textId) attrs.push(`w14:textId="${escapeXmlAttribute(paragraph.textId)}"`);
605
627
  if (paragraph.reviewCarrier) attrs.push(`folio:reviewCarrier="${paragraph.reviewCarrier}"`);
606
- const attrsStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
628
+ const written = serializePreservedAttributes(attrs, paragraph.preservedAttributes);
629
+ const attrsStr = written.length > 0 ? ` ${written.join(" ")}` : "";
607
630
  parts.push(serializeParagraphFormattingWithOptions(paragraph.formatting, {
608
631
  propertyChanges: paragraph.propertyChanges,
609
632
  paragraphMarkChange: paragraph.pPrMark,
610
633
  propertySource: getParagraphPropertySource(paragraph),
611
634
  sectionProperties: paragraph.sectionProperties
612
635
  }));
613
- const explicitCommentReferenceIds = /* @__PURE__ */ new Set();
614
- for (const content of paragraph.content) if (content.type === "commentReference") explicitCommentReferenceIds.add(content.id);
615
636
  let pendingRenderedPageBreak = paragraph.renderedPageBreakBefore === true;
616
637
  for (const content of paragraph.content) {
617
- let contentXml = serializeParagraphContent(content, explicitCommentReferenceIds);
638
+ let contentXml = serializeParagraphContent(content);
618
639
  if (contentXml) {
619
640
  if (pendingRenderedPageBreak) {
620
641
  const next = injectRenderedPageBreakIntoFirstRun(contentXml);
@@ -1,6 +1,6 @@
1
1
  import { toTransitionalNamespaceUri } from "../transitionalSpelling.js";
2
- import { escapeXml } from "./xmlUtils.js";
3
2
  import { TaggedError } from "better-result";
3
+ import { escapeXmlAttribute } from "@stll/docx-core";
4
4
  import { OOXML_NS } from "@stll/docx-utils";
5
5
  //#region src/docx/serializer/partNamespaces.ts
6
6
  /**
@@ -319,7 +319,7 @@ const serializePartElement = ({ partPath, rootName, rootAttributes, baselinePref
319
319
  ...[...NAMESPACE_TABLE.keys()].filter((prefix) => extra.has(prefix)),
320
320
  ...[...extra].filter((prefix) => !NAMESPACE_TABLE.has(prefix)).sort()
321
321
  ];
322
- const attributes = orderedPrefixes.map((prefix) => `xmlns:${prefix}="${escapeXml(bindings.get(prefix))}"`);
322
+ const attributes = orderedPrefixes.map((prefix) => `xmlns:${prefix}="${escapeXmlAttribute(bindings.get(prefix))}"`);
323
323
  const ignorable = orderedPrefixes.filter((prefix) => NAMESPACE_TABLE.get(prefix)?.ignorable === true);
324
324
  if (ignorable.length > 0) attributes.push(`mc:Ignorable="${ignorable.join(" ")}"`);
325
325
  return `${openingTag} ${attributes.join(" ")}>${body}</${rootName}>`;
@@ -1,14 +1,19 @@
1
1
  import { isValidHexColor } from "../../utils/colorResolver.js";
2
+ import { normalizeImageLuminancePercent } from "../../utils/imageLuminance.js";
3
+ import { serializePreservedAttributes } from "../attributeRemainder.js";
2
4
  import { THEME_COLOR_TO_DRAWING_SCHEME } from "../drawingUtils.js";
5
+ import { fieldStateAttributes } from "../fieldState.js";
3
6
  import { serializeGraphicFrameLocks } from "../graphicFrameLocks.js";
7
+ import { DECORATIVE_EXTENSION_URI, DECORATIVE_NAMESPACE } from "../imageParser.js";
4
8
  import { canReplayEditableImageRawXml } from "../imageRawXml.js";
9
+ import { serializeNonVisualDrawingNames } from "../nonVisualDrawingProps.js";
5
10
  import { serializeParagraph } from "./paragraphSerializer.js";
6
11
  import { serializeTable } from "./tableSerializer.js";
7
12
  import { serializeTextFormatting } from "./textFormattingSerializer.js";
8
13
  import { getSingularRunPropertyChange, serializeTrackedChangeAttributes } from "./trackedChangeAttributes.js";
9
- import { escapeXml, intAttr } from "./xmlUtils.js";
14
+ import { intAttr } from "./xmlUtils.js";
10
15
  import { panic } from "better-result";
11
- import { requiresXmlSpacePreserve } from "@stll/docx-core";
16
+ import { escapeXmlAttribute, escapeXmlText, requiresXmlSpacePreserve } from "@stll/docx-core";
12
17
  //#region src/docx/serializer/runSerializer.ts
13
18
  /**
14
19
  * Run Serializer - Serialize runs to OOXML XML
@@ -59,7 +64,7 @@ function serializeRunProperties(formatting, propertyChanges) {
59
64
  * Serialize text content (w:t)
60
65
  */
61
66
  function serializeTextContent(content) {
62
- return `<w:t${requiresXmlSpacePreserve(content.text) ? " xml:space=\"preserve\"" : ""}>${escapeXml(content.text)}</w:t>`;
67
+ return `<w:t${requiresXmlSpacePreserve(content.text) ? " xml:space=\"preserve\"" : ""}>${escapeXmlText(content.text)}</w:t>`;
63
68
  }
64
69
  /**
65
70
  * Serialize tab content (w:tab)
@@ -67,9 +72,9 @@ function serializeTextContent(content) {
67
72
  function serializeTabContent(content) {
68
73
  if (!content.positional) return "<w:tab/>";
69
74
  return `<w:ptab${[
70
- content.positional.relativeTo ? ` w:relativeTo="${escapeXml(content.positional.relativeTo)}"` : "",
71
- content.positional.alignment ? ` w:alignment="${escapeXml(content.positional.alignment)}"` : "",
72
- content.positional.leader ? ` w:leader="${escapeXml(content.positional.leader)}"` : ""
75
+ content.positional.relativeTo ? ` w:relativeTo="${escapeXmlAttribute(content.positional.relativeTo)}"` : "",
76
+ content.positional.alignment ? ` w:alignment="${escapeXmlAttribute(content.positional.alignment)}"` : "",
77
+ content.positional.leader ? ` w:leader="${escapeXmlAttribute(content.positional.leader)}"` : ""
73
78
  ].join("")}/>`;
74
79
  }
75
80
  /**
@@ -88,7 +93,7 @@ function serializeBreakContent(content) {
88
93
  * Serialize symbol content (w:sym)
89
94
  */
90
95
  function serializeSymbolContent(content) {
91
- return `<w:sym${content.font === "" ? "" : ` w:font="${escapeXml(content.font)}"`}${content.char === "" ? "" : ` w:char="${escapeXml(content.char)}"`}/>`;
96
+ return `<w:sym${content.font === "" ? "" : ` w:font="${escapeXmlAttribute(content.font)}"`}${content.char === "" ? "" : ` w:char="${escapeXmlAttribute(content.char)}"`}/>`;
92
97
  }
93
98
  /**
94
99
  * Serialize footnote/endnote reference
@@ -101,16 +106,13 @@ function serializeNoteReference(content) {
101
106
  * Serialize field character (w:fldChar)
102
107
  */
103
108
  function serializeFieldChar(content) {
104
- const attrs = [`w:fldCharType="${content.charType}"`];
105
- if (content.fldLock) attrs.push("w:fldLock=\"true\"");
106
- if (content.dirty) attrs.push("w:dirty=\"true\"");
107
- return `<w:fldChar ${attrs.join(" ")}/>`;
109
+ return `<w:fldChar ${[`w:fldCharType="${content.charType}"`, ...fieldStateAttributes(content)].join(" ")}/>`;
108
110
  }
109
111
  /**
110
112
  * Serialize field instruction text (w:instrText)
111
113
  */
112
114
  function serializeInstrText(content) {
113
- return `<w:instrText${content.text.startsWith(" ") || content.text.endsWith(" ") || content.text.includes(" ") ? " xml:space=\"preserve\"" : ""}>${escapeXml(content.text)}</w:instrText>`;
115
+ return `<w:instrText${content.text.startsWith(" ") || content.text.endsWith(" ") || content.text.includes(" ") ? " xml:space=\"preserve\"" : ""}>${escapeXmlText(content.text)}</w:instrText>`;
114
116
  }
115
117
  /**
116
118
  * Serialize soft hyphen (w:softHyphen)
@@ -127,11 +129,11 @@ function serializeNoBreakHyphen(_content) {
127
129
  /** Serialize a color value to DrawingML a:srgbClr or a:schemeClr */
128
130
  function serializeDrawingColor(color) {
129
131
  if (!color) return "";
130
- if (color.rgb && isValidHexColor(color.rgb)) return `<a:srgbClr val="${escapeXml(color.rgb.replace("#", ""))}"/>`;
132
+ if (color.rgb && isValidHexColor(color.rgb)) return `<a:srgbClr val="${escapeXmlAttribute(color.rgb.replace("#", ""))}"/>`;
131
133
  if (color.themeColor) {
132
134
  let clr = `<a:schemeClr val="${THEME_COLOR_TO_DRAWING_SCHEME[color.themeColor]}"`;
133
- if (color.themeTint) clr += `><a:tint val="${escapeXml(color.themeTint)}"/></a:schemeClr>`;
134
- else if (color.themeShade) clr += `><a:shade val="${escapeXml(color.themeShade)}"/></a:schemeClr>`;
135
+ if (color.themeTint) clr += `><a:tint val="${escapeXmlAttribute(color.themeTint)}"/></a:schemeClr>`;
136
+ else if (color.themeShade) clr += `><a:shade val="${escapeXmlAttribute(color.themeShade)}"/></a:schemeClr>`;
135
137
  else clr += `/>`;
136
138
  return clr;
137
139
  }
@@ -207,6 +209,17 @@ function serializeWrap(wrap) {
207
209
  }
208
210
  }
209
211
  /**
212
+ * `a:xfrm` attributes for an authored transform.
213
+ *
214
+ * Written iff authored: `rot="0"` and `flipH="0"` are OOXML's defaults, so a
215
+ * truthiness guard cannot tell "the author said none" from "the author said
216
+ * nothing", and a save erased an authored zero.
217
+ */
218
+ const serializeTransformAttrs = (transform) => {
219
+ if (transform === void 0) return "";
220
+ return `${transform.rotation === void 0 ? "" : ` rot="${Math.round(transform.rotation * 6e4)}"`}${transform.flipH === void 0 ? "" : ` flipH="${transform.flipH ? 1 : 0}"`}${transform.flipV === void 0 ? "" : ` flipV="${transform.flipV ? 1 : 0}"`}`;
221
+ };
222
+ /**
210
223
  * Build the common a:graphic > pic:pic element for images.
211
224
  *
212
225
  * Takes the relationship id as a separate argument so the caller has to have
@@ -216,27 +229,27 @@ function serializeWrap(wrap) {
216
229
  function serializePicGraphic(image, imageRId, sharedId) {
217
230
  const cx = image.size.width;
218
231
  const cy = image.size.height;
219
- const rId = escapeXml(imageRId);
232
+ const rId = escapeXmlAttribute(imageRId);
220
233
  const id = sharedId;
221
234
  const name = image.filename || `image${id}`;
222
- let xfrmAttrs = "";
223
- if (image.transform?.rotation) xfrmAttrs += ` rot="${Math.round(image.transform.rotation * 6e4)}"`;
224
- if (image.transform?.flipH) xfrmAttrs += " flipH=\"1\"";
225
- if (image.transform?.flipV) xfrmAttrs += " flipV=\"1\"";
235
+ const xfrmAttrs = serializeTransformAttrs(image.transform);
226
236
  const cropAttrs = [];
227
237
  if (image.crop?.left) cropAttrs.push(`l="${Math.round(image.crop.left * 1e5)}"`);
228
238
  if (image.crop?.top) cropAttrs.push(`t="${Math.round(image.crop.top * 1e5)}"`);
229
239
  if (image.crop?.right) cropAttrs.push(`r="${Math.round(image.crop.right * 1e5)}"`);
230
240
  if (image.crop?.bottom) cropAttrs.push(`b="${Math.round(image.crop.bottom * 1e5)}"`);
231
241
  const srcRectEl = cropAttrs.length > 0 ? `<a:srcRect ${cropAttrs.join(" ")}/>` : "";
232
- const alphaChild = image.opacity !== void 0 && image.opacity < 1 ? `<a:alphaModFix amt="${Math.round(Math.max(0, image.opacity) * 1e5)}"/>` : "";
233
- const blipEl = alphaChild ? `<a:blip r:embed="${rId}">${alphaChild}</a:blip>` : `<a:blip r:embed="${rId}"/>`;
242
+ const luminanceAttrs = [];
243
+ if (image.effects?.brightness !== void 0 && Number.isFinite(image.effects.brightness)) luminanceAttrs.push(`bright="${Math.round(normalizeImageLuminancePercent(image.effects.brightness) * 1e3)}"`);
244
+ if (image.effects?.contrast !== void 0 && Number.isFinite(image.effects.contrast)) luminanceAttrs.push(`contrast="${Math.round(normalizeImageLuminancePercent(image.effects.contrast) * 1e3)}"`);
245
+ const blipChildren = `${luminanceAttrs.length === 0 ? "" : `<a:lum ${luminanceAttrs.join(" ")}/>`}${image.opacity !== void 0 && image.opacity < 1 ? `<a:alphaModFix amt="${Math.round(Math.max(0, image.opacity) * 1e5)}"/>` : ""}`;
246
+ const blipEl = blipChildren ? `<a:blip r:embed="${rId}">${blipChildren}</a:blip>` : `<a:blip r:embed="${rId}"/>`;
234
247
  return [
235
248
  "<a:graphic xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\">",
236
249
  "<a:graphicData uri=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">",
237
250
  "<pic:pic xmlns:pic=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">",
238
251
  "<pic:nvPicPr>",
239
- `<pic:cNvPr id="${id}" name="${escapeXml(name)}"${image.alt ? ` descr="${escapeXml(image.alt)}"` : ""}/>`,
252
+ `<pic:cNvPr id="${id}" name="${escapeXmlAttribute(name)}"${image.alt ? ` descr="${escapeXmlAttribute(image.alt)}"` : ""}/>`,
240
253
  "<pic:cNvPicPr/>",
241
254
  "</pic:nvPicPr>",
242
255
  "<pic:blipFill>",
@@ -270,6 +283,19 @@ function serializeWrapDistanceAttrs(wrap) {
270
283
  return attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
271
284
  }
272
285
  /**
286
+ * Serialize the `wp:docPr` extension list.
287
+ *
288
+ * The decorative flag is the one extension folio models; every other `a:ext`
289
+ * the source carried is replayed as it was captured, in its original order,
290
+ * because an extension dropped on save is a fact the document had and lost.
291
+ * The decorative extension goes first: it is the one folio may have added.
292
+ */
293
+ function serializeDocPrExtensions(image) {
294
+ const preserved = image.docPrExtensions ?? [];
295
+ const entries = `${image.decorative === void 0 ? "" : `<a:ext uri="${DECORATIVE_EXTENSION_URI}"><adec:decorative xmlns:adec="${DECORATIVE_NAMESPACE}" val="${image.decorative ? "1" : "0"}"/></a:ext>`}${preserved.join("")}`;
296
+ return entries ? `<a:extLst>${entries}</a:extLst>` : "";
297
+ }
298
+ /**
273
299
  * Serialize drawing/image content (w:drawing) to full DrawingML XML
274
300
  */
275
301
  function serializeDrawingContent(content) {
@@ -284,14 +310,15 @@ function serializeDrawingContent(content) {
284
310
  const effB = image.padding?.bottom ?? 0;
285
311
  const effectExtentEl = `<wp:effectExtent l="${intAttr(effL)}" t="${intAttr(effT)}" r="${intAttr(effR)}" b="${intAttr(effB)}"/>`;
286
312
  const docPrId = getUniqueId(image.id);
287
- const docPrName = image.docPrName ?? image.filename ?? `Picture ${docPrId}`;
288
- const docPrDescription = image.alt !== void 0 ? ` descr="${escapeXml(image.alt)}"` : "";
289
- const docPrTitle = image.title !== void 0 ? ` title="${escapeXml(image.title)}"` : "";
290
- const hlinkClick = image.hlinkRId ? `<a:hlinkClick r:id="${escapeXml(image.hlinkRId)}"/>` : "";
291
- const inlineDocPrAttrs = `id="${docPrId}" name="${escapeXml(docPrName)}"${docPrDescription}${docPrTitle}${image.decorative ? " hidden=\"1\"" : ""}`;
292
- const inlineDocPr = hlinkClick ? `<wp:docPr ${inlineDocPrAttrs}>${hlinkClick}</wp:docPr>` : `<wp:docPr ${inlineDocPrAttrs}/>`;
293
- const anchorDocPrAttrs = `id="${docPrId}" name="${escapeXml(docPrName)}"${docPrDescription}${docPrTitle}`;
294
- const anchorDocPr = hlinkClick ? `<wp:docPr ${anchorDocPrAttrs}>${hlinkClick}</wp:docPr>` : `<wp:docPr ${anchorDocPrAttrs}/>`;
313
+ const docPrNames = serializeNonVisualDrawingNames({
314
+ ...image.docPrName !== void 0 ? { name: image.docPrName } : {},
315
+ ...image.alt !== void 0 ? { alt: image.alt } : {},
316
+ ...image.title !== void 0 ? { title: image.title } : {}
317
+ });
318
+ const hlinkClick = image.hlinkRId ? `<a:hlinkClick r:id="${escapeXmlAttribute(image.hlinkRId)}"/>` : "";
319
+ const docPrAttrs = `id="${docPrId}"${docPrNames}${image.hidden === void 0 ? "" : ` hidden="${image.hidden ? "1" : "0"}"`}`;
320
+ const docPrChildren = `${hlinkClick}${serializeDocPrExtensions(image)}`;
321
+ const docPr = docPrChildren ? `<wp:docPr ${docPrAttrs}>${docPrChildren}</wp:docPr>` : `<wp:docPr ${docPrAttrs}/>`;
295
322
  const graphicFramePr = serializeGraphicFrameLocks(image.frameLocks);
296
323
  const graphic = image.rId === void 0 ? "" : serializePicGraphic(image, image.rId, docPrId);
297
324
  if (!isFloating) return [
@@ -299,7 +326,7 @@ function serializeDrawingContent(content) {
299
326
  `<wp:inline${wrapDistanceAttrs}>`,
300
327
  `<wp:extent cx="${intAttr(cx)}" cy="${intAttr(cy)}"/>`,
301
328
  effectExtentEl,
302
- inlineDocPr,
329
+ docPr,
303
330
  graphicFramePr,
304
331
  graphic,
305
332
  "</wp:inline>",
@@ -316,7 +343,7 @@ function serializeDrawingContent(content) {
316
343
  `<wp:extent cx="${intAttr(cx)}" cy="${intAttr(cy)}"/>`,
317
344
  effectExtentEl,
318
345
  wrap,
319
- anchorDocPr,
346
+ docPr,
320
347
  graphicFramePr,
321
348
  graphic,
322
349
  "</wp:anchor>",
@@ -339,7 +366,7 @@ const serializeTextBodyAnchor = (anchor) => {
339
366
  };
340
367
  function serializeGeometryAdjustments(shape) {
341
368
  if (!shape.geometryAdjustments || shape.geometryAdjustments.length === 0) return "<a:avLst/>";
342
- return `<a:avLst>${shape.geometryAdjustments.map(({ name, formula }) => `<a:gd name="${escapeXml(name)}" fmla="${escapeXml(formula)}"/>`).join("")}</a:avLst>`;
369
+ return `<a:avLst>${shape.geometryAdjustments.map(({ name, formula }) => `<a:gd name="${escapeXmlAttribute(name)}" fmla="${escapeXmlAttribute(formula)}"/>`).join("")}</a:avLst>`;
343
370
  }
344
371
  /**
345
372
  * Serialize shape content to full DrawingML XML (wps:wsp inside w:drawing)
@@ -352,14 +379,14 @@ function serializeShapeContent(content) {
352
379
  const isFloating = shape.wrap && shape.wrap.type !== "inline";
353
380
  const wrapDistances = serializeWrapDistanceAttrs(shape.wrap);
354
381
  const docPrId = getUniqueId(shape.id);
355
- const docPrName = shape.name ?? (isTextBox ? `TextBox ${docPrId}` : `Shape ${docPrId}`);
356
- let xfrmAttrs = "";
357
- if (shape.transform?.rotation) xfrmAttrs += ` rot="${Math.round(shape.transform.rotation * 6e4)}"`;
358
- if (shape.transform?.flipH) xfrmAttrs += " flipH=\"1\"";
359
- if (shape.transform?.flipV) xfrmAttrs += " flipV=\"1\"";
382
+ const docPrNames = serializeNonVisualDrawingNames({
383
+ ...shape.name !== void 0 ? { name: shape.name } : {},
384
+ ...shape.alt !== void 0 ? { alt: shape.alt } : {},
385
+ ...shape.title !== void 0 ? { title: shape.title } : {}
386
+ });
360
387
  const spPr = [
361
388
  "<wps:spPr>",
362
- `<a:xfrm${xfrmAttrs}>`,
389
+ `<a:xfrm${serializeTransformAttrs(shape.transform)}>`,
363
390
  "<a:off x=\"0\" y=\"0\"/>",
364
391
  `<a:ext cx="${intAttr(cx)}" cy="${intAttr(cy)}"/>`,
365
392
  "</a:xfrm>",
@@ -386,7 +413,7 @@ function serializeShapeContent(content) {
386
413
  if (tb.autoFit === "shape") autoFitXml = "<a:spAutoFit/>";
387
414
  else if (tb.autoFit === "normal") autoFitXml = "<a:normAutofit/>";
388
415
  else if (tb.autoFit === "none") autoFitXml = "<a:noAutofit/>";
389
- const bodyPrChildren = `${tb.wordArt?.preset ? `<a:prstTxWarp prst="${escapeXml(tb.wordArt.preset)}">${tb.wordArt.adjustments && tb.wordArt.adjustments.length > 0 ? `<a:avLst>${tb.wordArt.adjustments.map(({ name, formula }) => `<a:gd name="${escapeXml(name)}" fmla="${escapeXml(formula)}"/>`).join("")}</a:avLst>` : "<a:avLst/>"}</a:prstTxWarp>` : ""}${autoFitXml}`;
416
+ const bodyPrChildren = `${tb.wordArt?.preset ? `<a:prstTxWarp prst="${escapeXmlAttribute(tb.wordArt.preset)}">${tb.wordArt.adjustments && tb.wordArt.adjustments.length > 0 ? `<a:avLst>${tb.wordArt.adjustments.map(({ name, formula }) => `<a:gd name="${escapeXmlAttribute(name)}" fmla="${escapeXmlAttribute(formula)}"/>`).join("")}</a:avLst>` : "<a:avLst/>"}</a:prstTxWarp>` : ""}${autoFitXml}`;
390
417
  const bodyPrXml = bodyPrChildren ? `<wps:bodyPr ${bpAttrs.join(" ")}>${bodyPrChildren}</wps:bodyPr>` : `<wps:bodyPr ${bpAttrs.join(" ")}/>`;
391
418
  if (isTextBox) textBody = [
392
419
  "<wps:txbx><w:txbxContent>",
@@ -414,7 +441,7 @@ function serializeShapeContent(content) {
414
441
  `<wp:inline${wrapDistances}>`,
415
442
  `<wp:extent cx="${intAttr(cx)}" cy="${intAttr(cy)}"/>`,
416
443
  "<wp:effectExtent l=\"0\" t=\"0\" r=\"0\" b=\"0\"/>",
417
- `<wp:docPr id="${docPrId}" name="${escapeXml(docPrName)}"/>`,
444
+ `<wp:docPr id="${docPrId}"${docPrNames}/>`,
418
445
  "<wp:cNvGraphicFramePr/>",
419
446
  graphic,
420
447
  "</wp:inline>",
@@ -432,7 +459,7 @@ function serializeShapeContent(content) {
432
459
  `<wp:extent cx="${intAttr(cx)}" cy="${intAttr(cy)}"/>`,
433
460
  "<wp:effectExtent l=\"0\" t=\"0\" r=\"0\" b=\"0\"/>",
434
461
  wrap,
435
- `<wp:docPr id="${docPrId}" name="${escapeXml(docPrName)}"/>`,
462
+ `<wp:docPr id="${docPrId}"${docPrNames}/>`,
436
463
  "<wp:cNvGraphicFramePr/>",
437
464
  graphic,
438
465
  "</wp:anchor>",
@@ -455,6 +482,7 @@ function serializeRunContent(content) {
455
482
  case "softHyphen": return serializeSoftHyphen(content);
456
483
  case "noBreakHyphen": return serializeNoBreakHyphen(content);
457
484
  case "renderedPageBreak": return "<w:lastRenderedPageBreak/>";
485
+ case "preservedXml": return content.xml;
458
486
  case "drawing":
459
487
  if (content.rawXml && canReplayEditableImageRawXml(content)) return content.rawXml;
460
488
  return serializeDrawingContent(content);
@@ -476,7 +504,8 @@ function serializeRun(run) {
476
504
  const contentXml = serializeRunContent(content);
477
505
  if (contentXml) parts.push(contentXml);
478
506
  }
479
- return `<w:r>${parts.join("")}</w:r>`;
507
+ const attrs = serializePreservedAttributes([], run.preservedAttributes);
508
+ return `<w:r${attrs.length > 0 ? ` ${attrs.join(" ")}` : ""}>${parts.join("")}</w:r>`;
480
509
  }
481
510
  /**
482
511
  * Serialize multiple runs to OOXML XML