@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,8 +1,10 @@
1
1
  import { deterministicHexId } from "../../utils/hexId.js";
2
+ import { commentThreadParaId } from "../commentThreadKey.js";
3
+ import { serializeWithPreservedChildren } from "../containerChildren.js";
2
4
  import { serializeParagraph } from "./paragraphSerializer.js";
3
5
  import { serializePartElement } from "./partNamespaces.js";
4
6
  import { serializeTextFormatting } from "./textFormattingSerializer.js";
5
- import { escapeXml } from "./xmlUtils.js";
7
+ import { escapeXmlAttribute } from "@stll/docx-core";
6
8
  //#region src/docx/serializer/commentSerializer.ts
7
9
  /**
8
10
  * Comment Serializer
@@ -26,16 +28,13 @@ function serializeParagraphWithAnnotationRef(paragraph, formatting) {
26
28
  return xml.replace(/^<w:p(?=[\s>])[^>]*>/u, (openingTag) => `${openingTag}${annotationReference}`);
27
29
  }
28
30
  function serializeComment(comment) {
29
- const attrs = [`w:id="${comment.id}"`, `w:author="${escapeXml(comment.author)}"`];
30
- if (comment.initials !== void 0) attrs.push(`w:initials="${escapeXml(comment.initials)}"`);
31
- if (comment.date) attrs.push(`w:date="${escapeXml(comment.date)}"`);
32
- let xml = `<w:comment ${attrs.join(" ")}>`;
33
- if (comment.content.length > 0) {
34
- xml += serializeParagraphWithAnnotationRef(comment.content[0], comment.annotationReferenceFormatting);
35
- for (let i = 1; i < comment.content.length; i++) xml += serializeParagraph(comment.content[i]);
36
- } else xml += `<w:p>${serializeAnnotationReference(comment.annotationReferenceFormatting)}</w:p>`;
37
- xml += "</w:comment>";
38
- return xml;
31
+ const attrs = [`w:id="${comment.id}"`, `w:author="${escapeXmlAttribute(comment.author)}"`];
32
+ if (comment.initials !== void 0) attrs.push(`w:initials="${escapeXmlAttribute(comment.initials)}"`);
33
+ if (comment.date) attrs.push(`w:date="${escapeXmlAttribute(comment.date)}"`);
34
+ const paragraphs = comment.content.map((paragraph, index) => index === 0 ? serializeParagraphWithAnnotationRef(paragraph, comment.annotationReferenceFormatting) : serializeParagraph(paragraph));
35
+ if (paragraphs.length === 0 && comment.preserved === void 0) paragraphs.push(`<w:p>${serializeAnnotationReference(comment.annotationReferenceFormatting)}</w:p>`);
36
+ const body = serializeWithPreservedChildren(paragraphs, comment.preserved);
37
+ return `<w:comment ${attrs.join(" ")}>${body}</w:comment>`;
39
38
  }
40
39
  const COMMENTS_BASELINE_PREFIXES = [
41
40
  "wpc",
@@ -61,46 +60,6 @@ const COMMENTS_EXTENDED_BASELINE_PREFIXES = [
61
60
  ];
62
61
  const XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n";
63
62
  /**
64
- * The order every comment part is written in: top-level comments, then replies.
65
- *
66
- * `word/comments.xml` has always been written this way, and the order matters
67
- * beyond that one part: the save rewrites comments.xml, so the next parse hands
68
- * the model back in exactly this order. Any other part built by walking the
69
- * same array — `word/commentsExtended.xml`, and the paraId minting both parts
70
- * key on — has to walk it in the same order, or the first save writes a part
71
- * whose order the second save cannot reproduce and saving is not a fixed point.
72
- */
73
- const commentPartOrder = (comments) => {
74
- const topLevel = [];
75
- const replies = [];
76
- for (const comment of comments) {
77
- const { parentId } = comment;
78
- (parentId === null || parentId === void 0 ? topLevel : replies).push(comment);
79
- }
80
- return [...topLevel, ...replies];
81
- };
82
- /**
83
- * Serialize comments array to comments.xml content. Returns a valid empty
84
- * `<w:comments/>` document for an empty array so callers can overwrite an
85
- * existing `word/comments.xml` part when the editor has removed the last
86
- * comment — leaving the previous file in place would otherwise re-emit
87
- * the orphaned comment threads on every save.
88
- */
89
- function serializeComments(comments, sourceBindings) {
90
- const body = commentPartOrder(comments).map((comment) => serializeComment(comment)).join("");
91
- return XML_DECLARATION + serializePartElement({
92
- partPath: "word/comments.xml",
93
- rootName: "w:comments",
94
- baselinePrefixes: COMMENTS_BASELINE_PREFIXES,
95
- sourceBindings,
96
- body
97
- });
98
- }
99
- /** The `w14:paraId` Word threads a comment by: its LAST paragraph's paraId. */
100
- function commentThreadParaId(comment) {
101
- return comment.content?.at(-1)?.paraId;
102
- }
103
- /**
104
63
  * The ids of comments that need a `commentsExtended.xml` entry: a reply, a
105
64
  * reply's parent (the thread root), or any comment carrying a resolved state.
106
65
  * A plain top-level comment set yields an empty set, so no part is written.
@@ -121,29 +80,71 @@ const commentPlainText = (comment) => {
121
80
  return text;
122
81
  };
123
82
  /**
124
- * Assign a deterministic `w14:paraId` to the LAST paragraph of every comment
125
- * that needs a commentsExtended entry (a reply, a reply's parent, or a resolved
126
- * comment) but has none. A document authored or loaded without comment paraIds
127
- * would otherwise have no stable key to thread through commentsExtended.xml, and
128
- * the thread link would be silently dropped. Word-authored ids are preserved;
129
- * only threaded, id-less paragraphs are filled. Deterministic (content- and
130
- * id-derived) so repeated saves mint the SAME id. MUST run before serializing
131
- * BOTH comments.xml and commentsExtended.xml so the two reference the same id.
83
+ * Plan both comment parts from the model, in the model's own order.
84
+ *
85
+ * The order is the document's: the order `word/comments.xml` listed the
86
+ * comments in is the order it is written back in, so a save neither reshuffles
87
+ * a reviewer's threads nor hands the next parse a different `comments[]` than
88
+ * the one it read.
89
+ *
90
+ * Planning also mints the `w14:paraId` a thread needs when the model has none —
91
+ * a comment written in the editor has no Word-authored id, and without one the
92
+ * `commentsExtended.xml` link would be dropped. Minting is deterministic
93
+ * (comment id and text derived) so repeated saves mint the same id, and it
94
+ * happens here rather than at each call site because both parts must see it.
132
95
  */
133
- function ensureThreadedCommentParaIds(comments) {
134
- const threaded = threadedCommentIds(comments);
135
- if (threaded.size === 0) return;
96
+ const planCommentParts = (comments) => {
97
+ const byId = /* @__PURE__ */ new Map();
98
+ const order = [];
99
+ for (const comment of comments) {
100
+ if (byId.has(comment.id)) continue;
101
+ byId.set(comment.id, comment);
102
+ order.push(comment.id);
103
+ }
104
+ const planned = [...byId.values()];
105
+ const threadedIds = threadedCommentIds(planned);
136
106
  const used = /* @__PURE__ */ new Set();
137
- for (const comment of comments) for (const paragraph of comment.content ?? []) if (paragraph.paraId) used.add(paragraph.paraId.toUpperCase());
138
- for (const comment of commentPartOrder(comments)) {
139
- if (!threaded.has(comment.id)) continue;
107
+ for (const comment of planned) for (const paragraph of comment.content ?? []) if (paragraph.paraId) used.add(paragraph.paraId.toUpperCase());
108
+ const threadParaIdById = /* @__PURE__ */ new Map();
109
+ for (const id of order) {
110
+ const comment = byId.get(id);
111
+ const existing = commentThreadParaId((comment.content ?? []).map(({ paraId }) => paraId));
112
+ if (existing) {
113
+ threadParaIdById.set(id, existing);
114
+ continue;
115
+ }
140
116
  const last = (comment.content ?? []).at(-1);
141
- if (!last || last.paraId) continue;
142
- let paraId = deterministicHexId(`comment:${comment.id}:${commentPlainText(comment)}`);
143
- for (let salt = 1; used.has(paraId.toUpperCase()); salt++) paraId = deterministicHexId(`comment:${comment.id}:${salt}`);
144
- used.add(paraId.toUpperCase());
145
- last.paraId = paraId;
117
+ if (!threadedIds.has(id) || !last) continue;
118
+ let minted = deterministicHexId(`comment:${id}:${commentPlainText(comment)}`);
119
+ for (let salt = 1; used.has(minted.toUpperCase()); salt++) minted = deterministicHexId(`comment:${id}:${salt}`);
120
+ used.add(minted.toUpperCase());
121
+ last.paraId = minted;
122
+ threadParaIdById.set(id, minted);
146
123
  }
124
+ return {
125
+ order,
126
+ byId,
127
+ threadParaIdById,
128
+ threadedIds
129
+ };
130
+ };
131
+ /**
132
+ * Serialize a comment plan to comments.xml content. Returns a valid empty
133
+ * `<w:comments/>` document for an empty plan so callers can overwrite an
134
+ * existing `word/comments.xml` part when the editor has removed the last
135
+ * comment — leaving the previous file in place would otherwise re-emit
136
+ * the orphaned comment threads on every save.
137
+ */
138
+ function serializeComments({ order, byId }, sourceBindings) {
139
+ let body = "";
140
+ for (const id of order) body += serializeComment(byId.get(id));
141
+ return XML_DECLARATION + serializePartElement({
142
+ partPath: "word/comments.xml",
143
+ rootName: "w:comments",
144
+ baselinePrefixes: COMMENTS_BASELINE_PREFIXES,
145
+ sourceBindings,
146
+ body
147
+ });
147
148
  }
148
149
  /**
149
150
  * Build the `commentsExtended.xml` entries for the comments that participate in
@@ -152,29 +153,23 @@ function ensureThreadedCommentParaIds(comments) {
152
153
  * leave any existing part untouched — a plain top-level comment set gets no
153
154
  * `commentsExtended.xml`, matching how Word omits it.
154
155
  *
155
- * A `w15:commentEx` keys on the comment's LAST paragraph paraId; a reply's
156
- * `w15:paraIdParent` points at its parent comment's last-paragraph paraId. Ids
157
- * are guaranteed present by {@link ensureThreadedCommentParaIds}; the `!paraId`
158
- * skip is a last-ditch safety for a malformed model.
156
+ * A `w15:commentEx` keys on the comment's thread paraId; a reply's
157
+ * `w15:paraIdParent` on its parent's. Both come from the plan, so a reply and
158
+ * its parent name each other by `w:id` right up to the attribute.
159
159
  */
160
- function buildCommentExtendedEntries(comments) {
161
- const threaded = threadedCommentIds(comments);
162
- if (threaded.size === 0) return null;
163
- const paraIdByCommentId = /* @__PURE__ */ new Map();
164
- for (const comment of comments) {
165
- const paraId = commentThreadParaId(comment);
166
- if (paraId) paraIdByCommentId.set(comment.id, paraId);
167
- }
160
+ function buildCommentExtendedEntries({ order, byId, threadParaIdById, threadedIds }) {
161
+ if (threadedIds.size === 0) return null;
168
162
  const entries = [];
169
- for (const comment of commentPartOrder(comments)) {
170
- if (!threaded.has(comment.id)) continue;
171
- const paraId = paraIdByCommentId.get(comment.id);
163
+ for (const id of order) {
164
+ if (!threadedIds.has(id)) continue;
165
+ const paraId = threadParaIdById.get(id);
172
166
  if (!paraId) continue;
173
- const parentParaId = comment.parentId !== void 0 ? paraIdByCommentId.get(comment.parentId) : void 0;
167
+ const { parentId, done } = byId.get(id);
168
+ const parentParaId = parentId !== void 0 ? threadParaIdById.get(parentId) : void 0;
174
169
  entries.push({
175
170
  paraId,
176
171
  ...parentParaId !== void 0 ? { paraIdParent: parentParaId } : {},
177
- done: comment.done ?? false
172
+ done: done ?? false
178
173
  });
179
174
  }
180
175
  return entries.length > 0 ? entries : null;
@@ -185,12 +180,12 @@ function buildCommentExtendedEntries(comments) {
185
180
  * {@link buildCommentExtendedEntries}). This is the part that makes Word render
186
181
  * a comment as a REPLY rather than a separate top-level thread.
187
182
  */
188
- function serializeCommentsExtended(comments) {
189
- const entries = buildCommentExtendedEntries(comments);
183
+ function serializeCommentsExtended(plan) {
184
+ const entries = buildCommentExtendedEntries(plan);
190
185
  if (!entries) return null;
191
186
  const body = entries.map((entry) => {
192
- const parentAttr = entry.paraIdParent !== void 0 ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
193
- return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parentAttr} w15:done="${entry.done ? "1" : "0"}"/>`;
187
+ const parentAttr = entry.paraIdParent !== void 0 ? ` w15:paraIdParent="${escapeXmlAttribute(entry.paraIdParent)}"` : "";
188
+ return `<w15:commentEx w15:paraId="${escapeXmlAttribute(entry.paraId)}"${parentAttr} w15:done="${entry.done ? "1" : "0"}"/>`;
194
189
  }).join("");
195
190
  return XML_DECLARATION + serializePartElement({
196
191
  partPath: "word/commentsExtended.xml",
@@ -201,4 +196,4 @@ function serializeCommentsExtended(comments) {
201
196
  });
202
197
  }
203
198
  //#endregion
204
- export { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExtended };
199
+ export { planCommentParts, serializeComments, serializeCommentsExtended };
@@ -1,4 +1,3 @@
1
- import { withBlockRangeMarkers } from "../blockRangeMarkers.js";
2
1
  import { serializeBlockSdt } from "./blockSdtSerializer.js";
3
2
  import { serializeParagraph } from "./paragraphSerializer.js";
4
3
  import { serializePartElement } from "./partNamespaces.js";
@@ -37,17 +36,15 @@ const DOCUMENT_BASELINE_PREFIXES = [
37
36
  "wpg",
38
37
  "wps"
39
38
  ];
40
- /**
41
- * Serialize a single block content item (paragraph, table, or block-level SDT).
42
- */
39
+ /** Serialize a single block, whether or not folio models what is in it. */
43
40
  function serializeBlockContent(block) {
44
- return withBlockRangeMarkers(block, serializeBlockBody(block));
45
- }
46
- /** The block itself, without the range markers that stood around it. */
47
- function serializeBlockBody(block) {
48
- if (block.type === "paragraph") return serializeParagraph(block);
49
- if (block.type === "table") return serializeTable(block, serializeParagraph);
50
- return serializeBlockSdt(block, serializeBlockContent);
41
+ switch (block.type) {
42
+ case "paragraph": return serializeParagraph(block);
43
+ case "table": return serializeTable(block, serializeParagraph);
44
+ case "blockSdt": return serializeBlockSdt(block, serializeBlockContent);
45
+ case "preservedBlock": return block.xml;
46
+ default: return block;
47
+ }
51
48
  }
52
49
  /**
53
50
  * Serialize document body content
@@ -1,5 +1,5 @@
1
1
  import { serializePartElement } from "./partNamespaces.js";
2
- import { escapeXml } from "./xmlUtils.js";
2
+ import { escapeXmlAttribute } from "@stll/docx-core";
3
3
  //#region src/docx/serializer/fontTableSerializer.ts
4
4
  const serializeFontTableXml = (fontTable) => "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + serializePartElement({
5
5
  partPath: "word/fontTable.xml",
@@ -10,20 +10,20 @@ const serializeFontTableXml = (fontTable) => "<?xml version=\"1.0\" encoding=\"U
10
10
  });
11
11
  const serializeFont = (font) => {
12
12
  const parts = [];
13
- if (font.altName) parts.push(`<w:altName w:val="${escapeXml(font.altName)}"/>`);
14
- if (font.panose1) parts.push(`<w:panose1 w:val="${escapeXml(font.panose1)}"/>`);
15
- if (font.charset) parts.push(`<w:charset w:val="${escapeXml(font.charset)}"/>`);
13
+ if (font.altName) parts.push(`<w:altName w:val="${escapeXmlAttribute(font.altName)}"/>`);
14
+ if (font.panose1) parts.push(`<w:panose1 w:val="${escapeXmlAttribute(font.panose1)}"/>`);
15
+ if (font.charset) parts.push(`<w:charset w:val="${escapeXmlAttribute(font.charset)}"/>`);
16
16
  if (font.family) parts.push(`<w:family w:val="${font.family}"/>`);
17
17
  if (font.pitch) parts.push(`<w:pitch w:val="${font.pitch}"/>`);
18
18
  if (font.sig) {
19
19
  const attrs = [];
20
20
  for (const [key, value] of Object.entries(font.sig)) {
21
21
  if (value === void 0) continue;
22
- attrs.push(`w:${key}="${escapeXml(value)}"`);
22
+ attrs.push(`w:${key}="${escapeXmlAttribute(value)}"`);
23
23
  }
24
24
  if (attrs.length > 0) parts.push(`<w:sig ${attrs.join(" ")}/>`);
25
25
  }
26
- return `<w:font w:name="${escapeXml(font.name)}">${parts.join("")}</w:font>`;
26
+ return `<w:font w:name="${escapeXmlAttribute(font.name)}">${parts.join("")}</w:font>`;
27
27
  };
28
28
  //#endregion
29
29
  export { serializeFontTableXml };
@@ -1,4 +1,3 @@
1
- import { withBlockRangeMarkers } from "../blockRangeMarkers.js";
2
1
  import { canReplayHeaderFooterVerbatim, getHeaderFooterVerbatimXml } from "../headerFooterVerbatim.js";
3
2
  import { isEmptyParagraph } from "../paragraphParser.js";
4
3
  import { captureVerbatimXml } from "../verbatimCapture.js";
@@ -7,7 +6,7 @@ import { serializeBlockSdt } from "./blockSdtSerializer.js";
7
6
  import { serializeParagraph } from "./paragraphSerializer.js";
8
7
  import { serializePartElement } from "./partNamespaces.js";
9
8
  import { serializeTable } from "./tableSerializer.js";
10
- import { escapeXml } from "./xmlUtils.js";
9
+ import { escapeXmlAttribute } from "@stll/docx-core";
11
10
  //#region src/docx/serializer/headerFooterSerializer.ts
12
11
  const HEADER_FOOTER_BASELINE_PREFIXES = [
13
12
  "wpc",
@@ -37,13 +36,13 @@ const HEADER_FOOTER_BASELINE_PREFIXES = [
37
36
  * header/footer.
38
37
  */
39
38
  function serializeBlock(block) {
40
- return withBlockRangeMarkers(block, serializeBlockBody(block));
41
- }
42
- /** The block itself, without the range markers that stood around it. */
43
- function serializeBlockBody(block) {
44
- if (block.type === "paragraph") return serializeParagraph(block);
45
- if (block.type === "table") return serializeTable(block, serializeParagraph);
46
- return serializeBlockSdt(block, serializeBlock);
39
+ switch (block.type) {
40
+ case "paragraph": return serializeParagraph(block);
41
+ case "table": return serializeTable(block, serializeParagraph);
42
+ case "blockSdt": return serializeBlockSdt(block, serializeBlock);
43
+ case "preservedBlock": return block.xml;
44
+ default: return block;
45
+ }
47
46
  }
48
47
  /**
49
48
  * Serialize a HeaderFooter object to valid OOXML XML
@@ -71,7 +70,6 @@ function serializeHeaderFooter(hf, source) {
71
70
  });
72
71
  contentXml = blocksXml.slice(0, watermarkInsertIndex).join("") + (mergesIntoRetainedHost ? "" : watermarkXml) + blocksXml.slice(watermarkInsertIndex).join("");
73
72
  } else contentXml = blocksXml.join("");
74
- if (!contentXml) contentXml = "<w:p><w:pPr/></w:p>";
75
73
  return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + serializePartElement({
76
74
  partPath: source?.path ?? `word/${hf.type}.xml`,
77
75
  rootName: rootTag,
@@ -85,7 +83,7 @@ function serializeRawWatermarkIntoHost({ hostXml, rawWatermarkXml }) {
85
83
  if (!rawHost || getLocalName(rawHost.name ?? "") !== "p") return rawWatermarkXml;
86
84
  const watermarkContent = (rawHost.elements ?? []).filter((child) => child.type === "element" && getLocalName(child.name ?? "") !== "pPr").map((child) => captureVerbatimXml(child)).join("");
87
85
  if (!watermarkContent) return rawWatermarkXml;
88
- const namespaceAttributes = Object.entries(rawHost.attributes ?? {}).filter(([name, value]) => value !== void 0 && (name === "xmlns" || name.startsWith("xmlns:"))).map(([name, value]) => ` ${name}="${escapeXml(String(value))}"`).join("");
86
+ const namespaceAttributes = Object.entries(rawHost.attributes ?? {}).filter(([name, value]) => value !== void 0 && (name === "xmlns" || name.startsWith("xmlns:"))).map(([name, value]) => ` ${name}="${escapeXmlAttribute(String(value))}"`).join("");
89
87
  return hostXml.replace(/^<w:p(?=[\s>])/u, `<w:p${namespaceAttributes}`).replace("</w:p>", `${watermarkContent}</w:p>`);
90
88
  }
91
89
  function serializeWatermarkParagraph(hf) {
@@ -101,15 +99,15 @@ function synthesizeTextWatermark(watermark) {
101
99
  const rotation = watermark.diagonal === false ? 0 : 315;
102
100
  const fillcolor = watermark.color && watermark.color !== "auto" ? `#${watermark.color}` : "#C0C0C0";
103
101
  const fontFamily = watermark.font ?? "Calibri";
104
- const text = escapeXml(watermark.text);
105
- return `<w:p><w:r><w:pict><v:shape id="PowerPlusWaterMarkObject1" type="#_x0000_t136" style="position:absolute;margin-left:0;margin-top:0;width:415pt;height:207pt;rotation:${rotation};z-index:-251658240;mso-position-horizontal:center;mso-position-horizontal-relative:margin;mso-position-vertical:center;mso-position-vertical-relative:margin" fillcolor="${fillcolor}" stroked="f">${watermark.opacity !== void 0 ? `<v:fill opacity="${watermark.opacity}"/>` : ""}<v:textpath style="font-family:&quot;${escapeXml(fontFamily)}&quot;;font-size:1pt" string="${text}"/></v:shape></w:pict></w:r></w:p>`;
102
+ const text = escapeXmlAttribute(watermark.text);
103
+ return `<w:p><w:r><w:pict><v:shape id="PowerPlusWaterMarkObject1" type="#_x0000_t136" style="position:absolute;margin-left:0;margin-top:0;width:415pt;height:207pt;rotation:${rotation};z-index:-251658240;mso-position-horizontal:center;mso-position-horizontal-relative:margin;mso-position-vertical:center;mso-position-vertical-relative:margin" fillcolor="${fillcolor}" stroked="f">${watermark.opacity !== void 0 ? `<v:fill opacity="${watermark.opacity}"/>` : ""}<v:textpath style="font-family:&quot;${escapeXmlAttribute(fontFamily)}&quot;;font-size:1pt" string="${text}"/></v:shape></w:pict></w:r></w:p>`;
106
104
  }
107
105
  const PICTURE_WATERMARK_DEFAULT_WIDTH_PT = 415;
108
106
  const PICTURE_WATERMARK_DEFAULT_HEIGHT_PT = 207;
109
107
  const PICTURE_WATERMARK_WASHOUT_GAIN = "19661f";
110
108
  const PICTURE_WATERMARK_WASHOUT_BLACKLEVEL = "22938f";
111
109
  function synthesizePictureWatermark(watermark) {
112
- const rId = escapeXml(watermark.imageRId);
110
+ const rId = escapeXmlAttribute(watermark.imageRId);
113
111
  return `<w:p><w:r><w:pict><v:shape id="WordPictureWatermark1" type="#_x0000_t75" style="position:absolute;margin-left:0;margin-top:0;width:${watermark.scale === void 0 ? watermark.widthPt ?? PICTURE_WATERMARK_DEFAULT_WIDTH_PT : PICTURE_WATERMARK_DEFAULT_WIDTH_PT * watermark.scale}pt;height:${watermark.scale === void 0 ? watermark.heightPt ?? PICTURE_WATERMARK_DEFAULT_HEIGHT_PT : PICTURE_WATERMARK_DEFAULT_HEIGHT_PT * watermark.scale}pt;z-index:-251658240;mso-position-horizontal:center;mso-position-horizontal-relative:margin;mso-position-vertical:center;mso-position-vertical-relative:margin"><v:imagedata r:id="${rId}" o:title=""${watermark.washout === false ? "" : ` gain="${PICTURE_WATERMARK_WASHOUT_GAIN}" blacklevel="${PICTURE_WATERMARK_WASHOUT_BLACKLEVEL}"`}/></v:shape></w:pict></w:r></w:p>`;
114
112
  }
115
113
  //#endregion
@@ -1,6 +1,6 @@
1
- import { escapeXml } from "./xmlUtils.js";
1
+ import { escapeXmlAttribute } from "@stll/docx-core";
2
2
  //#region src/docx/serializer/markupRangeAttributes.ts
3
- const attribute = (name, value) => `${name}="${escapeXml(String(value))}"`;
3
+ const attribute = (name, value) => `${name}="${escapeXmlAttribute(String(value))}"`;
4
4
  const markupRangeAttributes = (marker) => {
5
5
  const attributes = [attribute("w:id", marker.id)];
6
6
  if (marker.displacedByCustomXml !== void 0) attributes.push(attribute("w:displacedByCustomXml", marker.displacedByCustomXml));
@@ -1,4 +1,3 @@
1
- import { withBlockRangeMarkers } from "../blockRangeMarkers.js";
2
1
  import { serializeBlockSdt } from "./blockSdtSerializer.js";
3
2
  import { serializeParagraph } from "./paragraphSerializer.js";
4
3
  import { serializePartElement } from "./partNamespaces.js";
@@ -39,13 +38,13 @@ const serializeNotePart = (elementName, body) => "<?xml version=\"1.0\" encoding
39
38
  * note body.
40
39
  */
41
40
  function serializeBlock(block) {
42
- return withBlockRangeMarkers(block, serializeBlockBody(block));
43
- }
44
- /** The block itself, without the range markers that stood around it. */
45
- function serializeBlockBody(block) {
46
- if (block.type === "paragraph") return serializeParagraph(block);
47
- if (block.type === "table") return serializeTable(block, serializeParagraph);
48
- return serializeBlockSdt(block, serializeBlock);
41
+ switch (block.type) {
42
+ case "paragraph": return serializeParagraph(block);
43
+ case "table": return serializeTable(block, serializeParagraph);
44
+ case "blockSdt": return serializeBlockSdt(block, serializeBlock);
45
+ case "preservedBlock": return block.xml;
46
+ default: return block;
47
+ }
49
48
  }
50
49
  /**
51
50
  * Serialize a single note element. Footnotes and endnotes share the same
@@ -63,6 +62,7 @@ function serializeRequiredNoteSeparators(elementName) {
63
62
  return `<w:${elementName} w:type="separator" w:id="-1">${NOTE_SEPARATOR_BODY}<w:r><w:separator/></w:r></w:p></w:${elementName}><w:${elementName} w:type="continuationSeparator" w:id="0">${NOTE_SEPARATOR_BODY}<w:r><w:continuationSeparator/></w:r></w:p></w:${elementName}>`;
64
63
  }
65
64
  function insertNoteReferenceMark(xml, elementName) {
65
+ if (xml.includes(`<w:${elementName}Ref`)) return xml;
66
66
  const paragraphOpen = /<w:p(?=[\s>])[^>]*>/u.exec(xml);
67
67
  if (!paragraphOpen) return `${`<w:p><w:r><w:rPr><w:rStyle w:val="${elementName === "footnote" ? "FootnoteReference" : "EndnoteReference"}"/></w:rPr><w:${elementName}Ref/></w:r></w:p>`}${xml}`;
68
68
  const paragraphStart = paragraphOpen.index + paragraphOpen[0].length;
@@ -1,6 +1,7 @@
1
1
  import { serializePartElement } from "./partNamespaces.js";
2
2
  import { serializeTextFormatting } from "./textFormattingSerializer.js";
3
- import { escapeXml, intAttr } from "./xmlUtils.js";
3
+ import { intAttr } from "./xmlUtils.js";
4
+ import { escapeXmlAttribute } from "@stll/docx-core";
4
5
  //#region src/docx/serializer/numberingSerializer.ts
5
6
  /**
6
7
  * Serialize a level's paragraph properties — the modeled subset is indentation
@@ -36,11 +37,11 @@ function serializeLevelParagraphProps(pPr) {
36
37
  function serializeLevel(level) {
37
38
  const parts = [];
38
39
  if (level.start !== void 0) parts.push(`<w:start w:val="${intAttr(level.start)}"/>`);
39
- parts.push(`<w:numFmt w:val="${escapeXml(level.numFmt)}"/>`);
40
+ parts.push(`<w:numFmt w:val="${escapeXmlAttribute(level.numFmt)}"/>`);
40
41
  if (level.lvlRestart !== void 0) parts.push(`<w:lvlRestart w:val="${intAttr(level.lvlRestart)}"/>`);
41
42
  if (level.isLgl) parts.push("<w:isLgl/>");
42
43
  if (level.suffix) parts.push(`<w:suff w:val="${level.suffix}"/>`);
43
- parts.push(`<w:lvlText w:val="${escapeXml(level.lvlText)}"/>`);
44
+ parts.push(`<w:lvlText w:val="${escapeXmlAttribute(level.lvlText)}"/>`);
44
45
  if (level.legacy) {
45
46
  const legacyAttrs = [`w:legacy="${level.legacy.legacy ? 1 : 0}"`];
46
47
  if (level.legacy.legacySpace !== void 0) legacyAttrs.push(`w:legacySpace="${intAttr(level.legacy.legacySpace)}"`);
@@ -60,9 +61,9 @@ function serializeLevel(level) {
60
61
  function serializeAbstractNum(abstractNum) {
61
62
  const parts = [];
62
63
  if (abstractNum.multiLevelType) parts.push(`<w:multiLevelType w:val="${abstractNum.multiLevelType}"/>`);
63
- if (abstractNum.name !== void 0) parts.push(`<w:name w:val="${escapeXml(abstractNum.name)}"/>`);
64
- if (abstractNum.styleLink !== void 0) parts.push(`<w:styleLink w:val="${escapeXml(abstractNum.styleLink)}"/>`);
65
- if (abstractNum.numStyleLink !== void 0) parts.push(`<w:numStyleLink w:val="${escapeXml(abstractNum.numStyleLink)}"/>`);
64
+ if (abstractNum.name !== void 0) parts.push(`<w:name w:val="${escapeXmlAttribute(abstractNum.name)}"/>`);
65
+ if (abstractNum.styleLink !== void 0) parts.push(`<w:styleLink w:val="${escapeXmlAttribute(abstractNum.styleLink)}"/>`);
66
+ if (abstractNum.numStyleLink !== void 0) parts.push(`<w:numStyleLink w:val="${escapeXmlAttribute(abstractNum.numStyleLink)}"/>`);
66
67
  const levels = [...abstractNum.levels].sort((a, b) => a.ilvl - b.ilvl);
67
68
  for (const level of levels) parts.push(serializeLevel(level));
68
69
  return `<w:abstractNum w:abstractNumId="${intAttr(abstractNum.abstractNumId)}">${parts.join("")}</w:abstractNum>`;