@stll/folio-core 0.45.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 (207) hide show
  1. package/dist/ai-edits/apply.js +59 -19
  2. package/dist/ai-edits/headless.js +1 -1
  3. package/dist/compare/compare.js +2 -5
  4. package/dist/compare/scenario.js +2 -5
  5. package/dist/compare/types.d.ts +9 -1
  6. package/dist/compare/types.js +12 -1
  7. package/dist/compat/eigenpal.d.ts +2 -1
  8. package/dist/compat/eigenpal.js +2 -1
  9. package/dist/content-controls/checkboxDisplay.d.ts +11 -0
  10. package/dist/content-controls/checkboxDisplay.js +40 -0
  11. package/dist/content-controls/findContentControls.js +3 -1
  12. package/dist/content-controls/mutateContentControls.js +17 -2
  13. package/dist/controller/contentControlWidgetController.d.ts +1 -0
  14. package/dist/controller/contentControlWidgetController.js +3 -0
  15. package/dist/controller/hiddenEditorApi.js +3 -1
  16. package/dist/controller/hiddenEditorManager.d.ts +1 -1
  17. package/dist/controller/hiddenEditorManager.js +6 -3
  18. package/dist/display-list/build/buildDisplayList.js +20 -1
  19. package/dist/display-list/build/imagePrimitives.d.ts +1 -1
  20. package/dist/display-list/build/imagePrimitives.js +3 -1
  21. package/dist/display-list/build/paragraphPrimitives.js +21 -2
  22. package/dist/display-list/dom/renderDisplayListToDom.js +10 -2
  23. package/dist/display-list/types.d.ts +17 -0
  24. package/dist/docx/attributeRemainder.d.ts +43 -0
  25. package/dist/docx/attributeRemainder.js +65 -0
  26. package/dist/docx/blockContentParser.d.ts +1 -1
  27. package/dist/docx/blockContentParser.js +87 -63
  28. package/dist/docx/blockPlainText.d.ts +3 -4
  29. package/dist/docx/blockPlainText.js +1 -0
  30. package/dist/docx/bookmarkPlacement.js +2 -0
  31. package/dist/docx/commentAnchorIndex.d.ts +30 -0
  32. package/dist/docx/commentAnchorIndex.js +50 -0
  33. package/dist/docx/commentParser.d.ts +1 -1
  34. package/dist/docx/commentParser.js +54 -37
  35. package/dist/docx/commentRangeIntegrity.d.ts +16 -1
  36. package/dist/docx/commentRangeIntegrity.js +42 -1
  37. package/dist/docx/commentRangeJoin.d.ts +9 -0
  38. package/dist/docx/commentRangeJoin.js +33 -0
  39. package/dist/docx/commentReferenceCompletion.d.ts +9 -0
  40. package/dist/docx/commentReferenceCompletion.js +47 -0
  41. package/dist/docx/commentReferenceNormalization.js +1 -0
  42. package/dist/docx/commentReplyMarkers.js +11 -19
  43. package/dist/docx/compatibility.js +1 -0
  44. package/dist/docx/containerChildren.d.ts +111 -0
  45. package/dist/docx/containerChildren.gen.d.ts +28 -0
  46. package/dist/docx/containerChildren.gen.js +246 -0
  47. package/dist/docx/containerChildren.js +103 -0
  48. package/dist/docx/documentParser.d.ts +1 -1
  49. package/dist/docx/documentParser.js +3 -3
  50. package/dist/docx/ensureParaIds.js +17 -7
  51. package/dist/docx/fieldParser.d.ts +1 -1
  52. package/dist/docx/fieldParser.js +4 -4
  53. package/dist/docx/fieldState.d.ts +26 -0
  54. package/dist/docx/fieldState.js +53 -0
  55. package/dist/docx/footnoteParser.d.ts +1 -1
  56. package/dist/docx/graphicFrameLocks.d.ts +16 -4
  57. package/dist/docx/graphicFrameLocks.js +19 -6
  58. package/dist/docx/headerFooterParser.js +1 -1
  59. package/dist/docx/headerFooterReferenceNormalization.js +1 -0
  60. package/dist/docx/hyperlinkParser.d.ts +27 -8
  61. package/dist/docx/hyperlinkParser.js +76 -18
  62. package/dist/docx/imageParser.js +33 -8
  63. package/dist/docx/inlineWrapperContent.d.ts +85 -0
  64. package/dist/docx/inlineWrapperContent.js +84 -0
  65. package/dist/docx/normalizeBaseDirection.js +10 -1
  66. package/dist/docx/paraIdAttribute.d.ts +21 -0
  67. package/dist/docx/paraIdAttribute.js +64 -0
  68. package/dist/docx/paragraphParser.d.ts +1 -1
  69. package/dist/docx/paragraphParser.js +277 -168
  70. package/dist/docx/paragraphPropertySource.js +5 -1
  71. package/dist/docx/paragraphTextBoxEnrichment.d.ts +1 -1
  72. package/dist/docx/paragraphTextBoxEnrichment.js +6 -52
  73. package/dist/docx/paragraphTraversal.js +7 -4
  74. package/dist/docx/parseWarningMessage.js +2 -1
  75. package/dist/docx/preservedRunContent.d.ts +32 -0
  76. package/dist/docx/preservedRunContent.js +86 -0
  77. package/dist/docx/renderedPageBreakNormalization.js +2 -4
  78. package/dist/docx/rezip.js +74 -18
  79. package/dist/docx/runParser.d.ts +1 -1
  80. package/dist/docx/runParser.js +16 -12
  81. package/dist/docx/sectionParser.js +8 -1
  82. package/dist/docx/selectiveXmlPatch.d.ts +46 -2
  83. package/dist/docx/selectiveXmlPatch.js +86 -39
  84. package/dist/docx/serializer/borderSerializer.js +2 -2
  85. package/dist/docx/serializer/commentSerializer.js +5 -7
  86. package/dist/docx/serializer/documentSerializer.js +8 -11
  87. package/dist/docx/serializer/headerFooterSerializer.js +7 -9
  88. package/dist/docx/serializer/noteSerializer.js +8 -8
  89. package/dist/docx/serializer/paragraphSerializer.js +76 -56
  90. package/dist/docx/serializer/runSerializer.js +25 -16
  91. package/dist/docx/serializer/sectionPropertiesSerializer.js +8 -4
  92. package/dist/docx/serializer/tableSerializer.js +18 -8
  93. package/dist/docx/server/createBilingualDocument.js +3 -1
  94. package/dist/docx/server/materializeYjsDocx.d.ts +1 -1
  95. package/dist/docx/server/materializeYjsDocx.js +9 -1
  96. package/dist/docx/server/migrateYjsAttrSchema.d.ts +55 -0
  97. package/dist/docx/server/migrateYjsAttrSchema.js +95 -0
  98. package/dist/docx/shapeParser.js +5 -5
  99. package/dist/docx/tableParser.d.ts +1 -1
  100. package/dist/docx/tableParser.js +206 -85
  101. package/dist/docx/textBoxParser.d.ts +25 -2
  102. package/dist/docx/textBoxParser.js +52 -2
  103. package/dist/docx/unzip.js +5 -4
  104. package/dist/docx/vmlImageParser.d.ts +17 -1
  105. package/dist/docx/vmlImageParser.js +17 -1
  106. package/dist/docx/xmlEncoding.d.ts +5 -0
  107. package/dist/docx/xmlEncoding.js +12 -0
  108. package/dist/docx/xmlParser.d.ts +10 -1
  109. package/dist/docx/xmlParser.js +14 -1
  110. package/dist/headless-layout.js +10 -1
  111. package/dist/index.d.ts +2 -1
  112. package/dist/index.js +2 -1
  113. package/dist/internal/pageBreakRunSourceDescendantIndex.js +5 -2
  114. package/dist/layout-bridge/convert/toFlowBlocks.js +102 -12
  115. package/dist/layout-engine/measure/tableCellFloating.d.ts +2 -0
  116. package/dist/layout-engine/measure/tableCellFloating.js +2 -0
  117. package/dist/layout-engine/types.d.ts +23 -1
  118. package/dist/layout-painter/renderImage.d.ts +5 -1
  119. package/dist/layout-painter/renderImage.js +10 -3
  120. package/dist/layout-painter/renderPage.d.ts +2 -0
  121. package/dist/layout-painter/renderPage.js +4 -0
  122. package/dist/layout-painter/renderParagraph.d.ts +13 -1
  123. package/dist/layout-painter/renderParagraph.js +22 -3
  124. package/dist/managers/autoSaveCodec.d.ts +9 -1
  125. package/dist/managers/autoSaveCodec.js +12 -4
  126. package/dist/markdown/renderBlock.js +1 -0
  127. package/dist/markdown/renderRuns.d.ts +1 -1
  128. package/dist/markdown/renderRuns.js +28 -5
  129. package/dist/markdown/renderTable.js +22 -5
  130. package/dist/markdown/trailers.js +4 -1
  131. package/dist/pdf/images.js +142 -6
  132. package/dist/prosemirror/attrs/index.d.ts +14 -3
  133. package/dist/prosemirror/attrs/index.js +155 -2
  134. package/dist/prosemirror/authoredTransformAttrs.d.ts +28 -0
  135. package/dist/prosemirror/authoredTransformAttrs.js +64 -0
  136. package/dist/prosemirror/commands/contentControls.js +10 -2
  137. package/dist/prosemirror/commands/image.d.ts +9 -2
  138. package/dist/prosemirror/commands/image.js +42 -28
  139. package/dist/prosemirror/commands/pageBreak.js +1 -1
  140. package/dist/prosemirror/commentReferenceAttrs.d.ts +8 -0
  141. package/dist/prosemirror/commentReferenceAttrs.js +33 -0
  142. package/dist/prosemirror/commentReferenceIntegrity.d.ts +47 -0
  143. package/dist/prosemirror/commentReferenceIntegrity.js +43 -0
  144. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -13
  145. package/dist/prosemirror/conversion/fromProseDoc.js +269 -70
  146. package/dist/prosemirror/conversion/index.d.ts +2 -2
  147. package/dist/prosemirror/conversion/toProseDoc.d.ts +7 -9
  148. package/dist/prosemirror/conversion/toProseDoc.js +468 -308
  149. package/dist/prosemirror/extensions/StarterKit.js +8 -58
  150. package/dist/prosemirror/extensions/core/DocExtension.js +1 -1
  151. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -1
  152. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +45 -27
  153. package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.js +3 -1
  154. package/dist/prosemirror/extensions/features/ImagePasteExtension.js +5 -3
  155. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +1 -0
  156. package/dist/prosemirror/extensions/markRegistry.d.ts +41 -0
  157. package/dist/prosemirror/extensions/markRegistry.js +75 -0
  158. package/dist/prosemirror/extensions/marks/InlineWrapperExtension.d.ts +18 -0
  159. package/dist/prosemirror/extensions/marks/InlineWrapperExtension.js +63 -0
  160. package/dist/prosemirror/extensions/marks/markUtils.d.ts +2 -1
  161. package/dist/prosemirror/extensions/marks/markUtils.js +47 -4
  162. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.d.ts +11 -0
  163. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +87 -0
  164. package/dist/prosemirror/extensions/nodes/FieldExtension.js +9 -7
  165. package/dist/prosemirror/extensions/nodes/ImageExtension.js +16 -0
  166. package/dist/prosemirror/extensions/nodes/PreservedBlockExtension.d.ts +32 -0
  167. package/dist/prosemirror/extensions/nodes/PreservedBlockExtension.js +67 -0
  168. package/dist/prosemirror/extensions/nodes/PreservedXmlExtension.d.ts +16 -0
  169. package/dist/prosemirror/extensions/nodes/PreservedXmlExtension.js +60 -0
  170. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +3 -0
  171. package/dist/prosemirror/extensions/nodes/TableExtension.js +3 -2
  172. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +5 -1
  173. package/dist/prosemirror/inlineWrapperStack.d.ts +39 -0
  174. package/dist/prosemirror/inlineWrapperStack.js +75 -0
  175. package/dist/prosemirror/pageBreakRunProjection.d.ts +17 -7
  176. package/dist/prosemirror/pageBreakRunProjection.js +19 -9
  177. package/dist/prosemirror/replacedAnnotations.d.ts +59 -0
  178. package/dist/prosemirror/replacedAnnotations.js +165 -0
  179. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +3 -1
  180. package/dist/prosemirror/runFormattingInlineCarriers.js +5 -1
  181. package/dist/prosemirror/schema/index.d.ts +3 -3
  182. package/dist/prosemirror/schema/marks.d.ts +25 -1
  183. package/dist/prosemirror/schema/nodes.d.ts +90 -3
  184. package/dist/prosemirror/schema/nodes.js +16 -1
  185. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +2 -0
  186. package/dist/prosemirror/trackedRunInlineAtoms.js +2 -0
  187. package/dist/prosemirror/validation.js +16 -2
  188. package/dist/prosemirror/yjsDocumentMetadata.d.ts +73 -0
  189. package/dist/prosemirror/yjsDocumentMetadata.js +171 -0
  190. package/dist/prosemirror/zeroWidthAnchors.js +2 -0
  191. package/dist/render-dom/commentAnchorAttributes.d.ts +37 -0
  192. package/dist/render-dom/commentAnchorAttributes.js +54 -0
  193. package/dist/server.d.ts +3 -1
  194. package/dist/server.js +3 -1
  195. package/dist/types/content.d.ts +2 -2
  196. package/dist/utils/clipboard.d.ts +9 -1
  197. package/dist/utils/clipboard.js +27 -1
  198. package/dist/utils/findReplace.js +1 -1
  199. package/dist/utils/imageLuminance.d.ts +15 -0
  200. package/dist/utils/imageLuminance.js +32 -0
  201. package/dist/utils/mergeDocumentContent.js +7 -17
  202. package/dist/utils/replaceText.js +1 -0
  203. package/package.json +2 -2
  204. package/dist/docx/blockRangeMarkers.d.ts +0 -36
  205. package/dist/docx/blockRangeMarkers.js +0 -59
  206. package/dist/prosemirror/yjsParagraphSourceContract.d.ts +0 -9
  207. package/dist/prosemirror/yjsParagraphSourceContract.js +0 -26
@@ -1,3 +1,4 @@
1
+ import { patchBreaksCommentRangeBalance } from "./commentRangeIntegrity.js";
1
2
  import { captureVerbatimXml } from "./verbatimCapture.js";
2
3
  import { WORDPROCESSINGML_NAMESPACE_URIS, cloneElement, findAttributeByNamespaceUri, getAttributeByNamespaceUri, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, parseXmlDocument } from "./xmlParser.js";
3
4
  //#region src/docx/selectiveXmlPatch.ts
@@ -237,10 +238,27 @@ function buildPatchedNoteXml(originalXml, serializedXml, changedIds) {
237
238
  return spliceChangedParagraphs(originalXml, serializedXml, changedIds);
238
239
  }
239
240
  /**
241
+ * Apply `splices` to `xml`, end-to-start so earlier offsets stay valid.
242
+ *
243
+ * Every selective patch is a splice, and every splice goes through here, so
244
+ * the refusal below is a property of the operation rather than a check each
245
+ * site has to remember. A patch rewrites the regions an edit touched and keeps
246
+ * the rest of the part byte-for-byte, so it can write half a comment range:
247
+ * invalid OOXML that anchors the comment to nothing. Answers null when it
248
+ * would, leaving the caller to rewrite a wider region — ultimately the whole
249
+ * part from the model, which is balanced with itself.
250
+ */
251
+ const spliceXml = (xml, splices) => {
252
+ let result = xml;
253
+ for (const { start, end, newXml } of [...splices].toSorted((a, b) => b.start - a.start)) result = result.slice(0, start) + newXml + result.slice(end);
254
+ return patchBreaksCommentRangeBalance(xml, result) ? null : result;
255
+ };
256
+ /**
240
257
  * Replace each changed paragraph in `originalXml` with its re-serialized form
241
- * extracted from `serializedXml`, splicing end-to-start so earlier offsets stay
242
- * valid. Assumes safety has already been validated. Returns null if an offset
243
- * or extraction unexpectedly fails.
258
+ * extracted from `serializedXml`. Assumes safety has already been validated.
259
+ * Returns null if an offset or extraction unexpectedly fails, or if
260
+ * {@link spliceXml} refuses the result; the caller then falls back to a full
261
+ * repack, whose parts are all re-serialized from one model.
244
262
  */
245
263
  function spliceChangedParagraphs(originalXml, serializedXml, changedIds) {
246
264
  const replacements = [];
@@ -255,10 +273,7 @@ function spliceChangedParagraphs(originalXml, serializedXml, changedIds) {
255
273
  newXml
256
274
  });
257
275
  }
258
- replacements.sort((a, b) => b.start - a.start);
259
- let result = originalXml;
260
- for (const { start, end, newXml } of replacements) result = result.slice(0, start) + newXml + result.slice(end);
261
- return result;
276
+ return spliceXml(originalXml, replacements);
262
277
  }
263
278
  /**
264
279
  * Depth-count the end of the element that opens at `start` (an `<openLiteral…>`
@@ -424,11 +439,6 @@ const collectChangedNoteParaIds = (baselineXml, currentXml) => {
424
439
  }
425
440
  return changed;
426
441
  };
427
- const replaceRanges = (xml, replacements) => {
428
- let result = xml;
429
- for (const { start, end, newXml } of [...replacements].toSorted((a, b) => b.start - a.start)) result = result.slice(0, start) + newXml + result.slice(end);
430
- return result;
431
- };
432
442
  /**
433
443
  * Patch an existing note part from its model serialization.
434
444
  *
@@ -440,56 +450,79 @@ const replaceRanges = (xml, replacements) => {
440
450
  * notes, and unaffected equal-shape paragraphs remain byte-exact.
441
451
  * `replacementXml` also supplies synthesized automatic note-reference marks,
442
452
  * which the parsed model intentionally omits.
453
+ *
454
+ * A comment can be anchored on a note's own text, so its range spans that
455
+ * note's paragraphs and an edit inside the span moves a range half from one
456
+ * paragraph to another. Replacing only the dirty paragraph then drops the half
457
+ * it held and leaves the other standing, so {@link spliceXml} refuses the
458
+ * result and the changed notes are rewritten whole instead — model content on
459
+ * both sides of the range, with every other note still byte-exact.
443
460
  */
444
461
  function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, replacementXml, elementName, changedParaIds }) {
445
462
  const currentElements = collectNoteElementSyntax(serializedXml, elementName);
446
463
  const originalElements = collectNoteElementSyntax(originalXml, elementName);
447
464
  const replacementElements = collectNoteElementSyntax(replacementXml, elementName);
448
465
  const replacementXmlnsDeclarations = collectXmlnsFromOpeningTag(replacementXml);
449
- const ordinalReplacements = [];
466
+ const paragraphSplices = [];
467
+ /** The same edits at note granularity, should the paragraph splices be refused. */
468
+ const noteSplices = [];
450
469
  const serializedParaIds = collectParaIds(serializedXml);
451
470
  const effectiveChangedParaIds = changedParaIds ?? collectChangedNoteParaIds(baselineXml, serializedXml);
452
471
  const unroutedChangedParaIds = new Set([...effectiveChangedParaIds].filter((paraId) => serializedParaIds.has(paraId)));
453
472
  for (const [id, currentSyntaxEntries] of currentElements) {
454
473
  const originalSyntaxEntries = originalElements.get(id);
455
474
  const replacementSyntaxEntries = replacementElements.get(id);
456
- if (currentSyntaxEntries.length !== 1 || originalSyntaxEntries?.length !== 1 || replacementSyntaxEntries?.length !== 1) return null;
475
+ if (currentSyntaxEntries.length !== 1 || originalSyntaxEntries?.length !== 1 || replacementSyntaxEntries?.length !== 1) return {
476
+ type: "refused",
477
+ reason: "unroutable-paragraph"
478
+ };
457
479
  const currentSyntax = currentSyntaxEntries[0];
458
480
  const originalSyntax = originalSyntaxEntries[0];
459
481
  const replacementSyntax = replacementSyntaxEntries[0];
460
- if (!currentSyntax || !originalSyntax || !replacementSyntax) return null;
482
+ if (!currentSyntax || !originalSyntax || !replacementSyntax) return {
483
+ type: "refused",
484
+ reason: "unroutable-paragraph"
485
+ };
461
486
  const currentNote = extractNoteElement(serializedXml, currentSyntax, id);
462
487
  const originalOffsets = findNoteElement(originalXml, originalSyntax, id);
463
488
  const replacementNote = extractNoteElement(replacementXml, replacementSyntax, id);
464
- if (!currentNote || !originalOffsets || !replacementNote) return null;
489
+ if (!currentNote || !originalOffsets || !replacementNote) return {
490
+ type: "refused",
491
+ reason: "unroutable-paragraph"
492
+ };
465
493
  const originalNote = originalXml.slice(originalOffsets.start, originalOffsets.end);
466
494
  const currentParagraphs = paragraphRanges(currentNote, currentSyntax.elementPrefix);
467
495
  const originalParagraphs = paragraphRanges(originalNote, originalSyntax.elementPrefix);
468
496
  const replacementParagraphs = paragraphRanges(replacementNote, replacementSyntax.elementPrefix);
469
497
  const noteChangedParaIds = [...collectParaIds(currentNote).keys()].filter((paraId) => unroutedChangedParaIds.has(paraId));
470
498
  if (noteChangedParaIds.length === 0) continue;
499
+ const wholeNoteSplice = {
500
+ start: originalOffsets.start,
501
+ end: originalOffsets.end,
502
+ newXml: rewriteWordprocessingPrefixes(replacementNote, {
503
+ source: replacementSyntax,
504
+ target: originalSyntax,
505
+ sourceXmlnsDeclarations: replacementXmlnsDeclarations
506
+ })
507
+ };
508
+ noteSplices.push(wholeNoteSplice);
471
509
  if (currentParagraphs.length !== originalParagraphs.length || currentParagraphs.length !== replacementParagraphs.length) {
472
510
  for (const paraId of noteChangedParaIds) unroutedChangedParaIds.delete(paraId);
473
- ordinalReplacements.push({
474
- start: originalOffsets.start,
475
- end: originalOffsets.end,
476
- newXml: rewriteWordprocessingPrefixes(replacementNote, {
477
- source: replacementSyntax,
478
- target: originalSyntax,
479
- sourceXmlnsDeclarations: replacementXmlnsDeclarations
480
- })
481
- });
511
+ paragraphSplices.push(wholeNoteSplice);
482
512
  continue;
483
513
  }
484
514
  for (let index = 0; index < currentParagraphs.length; index++) {
485
515
  const currentRange = currentParagraphs[index];
486
516
  const originalRange = originalParagraphs[index];
487
517
  const replacementRange = replacementParagraphs[index];
488
- if (!currentRange || !originalRange || !replacementRange) return null;
518
+ if (!currentRange || !originalRange || !replacementRange) return {
519
+ type: "refused",
520
+ reason: "unroutable-paragraph"
521
+ };
489
522
  const routedIds = [...collectParaIds(currentNote.slice(currentRange.start, currentRange.end)).keys()].filter((paraId) => unroutedChangedParaIds.has(paraId));
490
523
  if (routedIds.length === 0) continue;
491
524
  for (const paraId of routedIds) unroutedChangedParaIds.delete(paraId);
492
- ordinalReplacements.push({
525
+ paragraphSplices.push({
493
526
  start: originalOffsets.start + originalRange.start,
494
527
  end: originalOffsets.start + originalRange.end,
495
528
  newXml: rewriteWordprocessingPrefixes(replacementNote.slice(replacementRange.start, replacementRange.end), {
@@ -500,8 +533,23 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
500
533
  });
501
534
  }
502
535
  }
503
- if (unroutedChangedParaIds.size > 0) return null;
504
- return replaceRanges(originalXml, ordinalReplacements);
536
+ if (unroutedChangedParaIds.size > 0) return {
537
+ type: "refused",
538
+ reason: "unroutable-paragraph"
539
+ };
540
+ const patched = spliceXml(originalXml, paragraphSplices);
541
+ if (patched !== null) return {
542
+ type: "patched",
543
+ xml: patched
544
+ };
545
+ const wholeNotes = spliceXml(originalXml, noteSplices);
546
+ return wholeNotes === null ? {
547
+ type: "refused",
548
+ reason: "comment-range-balance"
549
+ } : {
550
+ type: "patched",
551
+ xml: wholeNotes
552
+ };
505
553
  }
506
554
  /**
507
555
  * The full range of the first `<openLiteral …>…</closeTag>` element, or null.
@@ -695,17 +743,19 @@ function restoreLevelNumFmts(originalDefXml, currentDefXml) {
695
743
  ...collectXmlnsFromOpeningTag(origLevel)
696
744
  });
697
745
  else replacement = reconstructCustomNumFmt(currentWidth);
698
- const restoredLevel = curLevel.slice(0, synthetic.index) + replacement + curLevel.slice(synthetic.index + synthetic[0].length);
746
+ const restoredLevel = spliceXml(curLevel, [{
747
+ start: synthetic.index,
748
+ end: synthetic.index + synthetic[0].length,
749
+ newXml: replacement
750
+ }]);
751
+ if (restoredLevel === null) continue;
699
752
  replacements.push({
700
753
  start: curOffsets.start,
701
754
  end: curOffsets.end,
702
755
  newXml: restoredLevel
703
756
  });
704
757
  }
705
- replacements.sort((a, b) => b.start - a.start);
706
- let result = currentDefXml;
707
- for (const { start, end, newXml } of replacements) result = result.slice(0, start) + newXml + result.slice(end);
708
- return result;
758
+ return spliceXml(currentDefXml, replacements) ?? currentDefXml;
709
759
  }
710
760
  /**
711
761
  * Build a patched `word/numbering.xml` by splicing the changed `w:abstractNum` /
@@ -733,10 +783,7 @@ function buildPatchedNumberingXml(originalXml, currentXml, changed) {
733
783
  return true;
734
784
  };
735
785
  if (!collect("abstractNum", changed.abstractNums) || !collect("num", changed.nums)) return null;
736
- replacements.sort((a, b) => b.start - a.start);
737
- let result = originalXml;
738
- for (const { start, end, newXml } of replacements) result = result.slice(0, start) + newXml + result.slice(end);
739
- return result;
786
+ return spliceXml(originalXml, replacements);
740
787
  }
741
788
  /**
742
789
  * Numbering definitions present in `currentXml` (the model's serialization)
@@ -872,4 +919,4 @@ function escapeRegExp(str) {
872
919
  return str.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
873
920
  }
874
921
  //#endregion
875
- export { appendNumberingDefs, buildParagraphOffsetIndex, buildPatchedDocumentXml, buildPatchedNotePartXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, patchNumberingDefinitions, validatePatchSafety };
922
+ export { appendNumberingDefs, buildParagraphOffsetIndex, buildPatchedDocumentXml, buildPatchedNotePartXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, patchNumberingDefinitions, spliceXml, validatePatchSafety };
@@ -34,8 +34,8 @@ function serializeBorder(input, elementName) {
34
34
  if (themeTint) attrs.push(`w:themeTint="${escapeXmlAttribute(themeTint)}"`);
35
35
  if (themeShade) attrs.push(`w:themeShade="${escapeXmlAttribute(themeShade)}"`);
36
36
  }
37
- if (shadow) attrs.push("w:shadow=\"true\"");
38
- if (frame) attrs.push("w:frame=\"true\"");
37
+ if (shadow !== void 0) attrs.push(`w:shadow="${shadow ? "1" : "0"}"`);
38
+ if (frame !== void 0) attrs.push(`w:frame="${frame ? "1" : "0"}"`);
39
39
  if (artRelationshipId) attrs.push(`r:id="${escapeXmlAttribute(artRelationshipId)}"`);
40
40
  if (topLeftArtRelationshipId) attrs.push(`r:topLeft="${escapeXmlAttribute(topLeftArtRelationshipId)}"`);
41
41
  if (topRightArtRelationshipId) attrs.push(`r:topRight="${escapeXmlAttribute(topRightArtRelationshipId)}"`);
@@ -1,5 +1,6 @@
1
1
  import { deterministicHexId } from "../../utils/hexId.js";
2
2
  import { commentThreadParaId } from "../commentThreadKey.js";
3
+ import { serializeWithPreservedChildren } from "../containerChildren.js";
3
4
  import { serializeParagraph } from "./paragraphSerializer.js";
4
5
  import { serializePartElement } from "./partNamespaces.js";
5
6
  import { serializeTextFormatting } from "./textFormattingSerializer.js";
@@ -30,13 +31,10 @@ function serializeComment(comment) {
30
31
  const attrs = [`w:id="${comment.id}"`, `w:author="${escapeXmlAttribute(comment.author)}"`];
31
32
  if (comment.initials !== void 0) attrs.push(`w:initials="${escapeXmlAttribute(comment.initials)}"`);
32
33
  if (comment.date) attrs.push(`w:date="${escapeXmlAttribute(comment.date)}"`);
33
- let xml = `<w:comment ${attrs.join(" ")}>`;
34
- if (comment.content.length > 0) {
35
- xml += serializeParagraphWithAnnotationRef(comment.content[0], comment.annotationReferenceFormatting);
36
- for (let i = 1; i < comment.content.length; i++) xml += serializeParagraph(comment.content[i]);
37
- } else xml += `<w:p>${serializeAnnotationReference(comment.annotationReferenceFormatting)}</w:p>`;
38
- xml += "</w:comment>";
39
- return xml;
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>`;
40
38
  }
41
39
  const COMMENTS_BASELINE_PREFIXES = [
42
40
  "wpc",
@@ -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,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";
@@ -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,
@@ -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,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";
@@ -344,13 +346,14 @@ function hyperlinkAttributes(hyperlink) {
344
346
  /** One `w:hyperlink` child, with the caller deciding how a run is written. */
345
347
  function serializeHyperlinkChild(child, serializeChildRun) {
346
348
  if (child.type === "run") return serializeChildRun(child);
347
- 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;
348
351
  }
349
352
  /**
350
353
  * Serialize a hyperlink (w:hyperlink)
351
354
  */
352
- function serializeHyperlink(hyperlink) {
353
- 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("");
354
357
  return `<w:hyperlink${hyperlinkAttributes(hyperlink)}>${childrenXml}</w:hyperlink>`;
355
358
  }
356
359
  /**
@@ -367,10 +370,11 @@ function serializeBookmarkEnd(bookmark) {
367
370
  }
368
371
  /** Serialize a simple field without changing its authored OOXML field form. */
369
372
  function serializeSimpleField(field) {
370
- const attrs = [`w:instr="${escapeXmlAttribute(field.instruction)}"`];
371
- if (field.fldLock) attrs.push("w:fldLock=\"true\"");
372
- if (field.dirty) attrs.push("w:dirty=\"true\"");
373
- 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("");
374
378
  return `<w:fldSimple ${attrs.join(" ")}>${contentXml}</w:fldSimple>`;
375
379
  }
376
380
  /**
@@ -382,9 +386,7 @@ function serializeComplexField(field) {
382
386
  const parts = [];
383
387
  const structuralFormatting = field.formatting ?? field.fieldResult[0]?.formatting;
384
388
  const rPrXml = structuralFormatting ? serializeTextFormatting(structuralFormatting) : "";
385
- const beginAttrs = ["w:fldCharType=\"begin\""];
386
- if (field.fldLock) beginAttrs.push("w:fldLock=\"true\"");
387
- if (field.dirty) beginAttrs.push("w:dirty=\"true\"");
389
+ const beginAttrs = ["w:fldCharType=\"begin\"", ...fieldStateAttributes(field)];
388
390
  parts.push(`<w:r>${rPrXml}<w:fldChar ${beginAttrs.join(" ")}/></w:r>`);
389
391
  if (field.fieldCode.length > 0) parts.push(...field.fieldCode.map((run) => serializeRun(run)));
390
392
  else if (field.instruction.length > 0) {
@@ -455,20 +457,22 @@ function synthesizeInlineSdtPr(props) {
455
457
  * the properties block was re-synthesized from the modeled projection alone,
456
458
  * silently dropping every unmodeled feature (and `w:sdtEndPr`) on save.
457
459
  */
458
- function serializeInlineSdt(sdt) {
460
+ function serializeInlineSdt(sdt, disposition = "kept") {
459
461
  const props = sdt.properties;
460
462
  const contentXml = sdt.content.map((item) => {
461
463
  switch (item.type) {
462
- case "run": return serializeRun(item);
463
- case "hyperlink": return serializeHyperlink(item);
464
+ case "run": return serializeInlineRun(item, disposition);
465
+ case "hyperlink": return serializeHyperlink(item, disposition);
464
466
  case "simpleField": return serializeSimpleField(item);
465
467
  case "complexField": return serializeComplexField(item);
466
- case "inlineSdt": return serializeInlineSdt(item);
468
+ case "inlineSdt": return serializeInlineSdt(item, disposition);
469
+ case "inlineWrapper": return serializeParagraphContent(item, disposition);
467
470
  case "insertion": return serializeTrackedChange("ins", item);
468
471
  case "deletion": return serializeTrackedChange("del", item);
469
472
  case "moveFrom": return serializeTrackedChange("moveFrom", item);
470
473
  case "moveTo": return serializeTrackedChange("moveTo", item);
471
474
  case "mathEquation": return item.ommlXml || "";
475
+ case "preservedInline": return item.xml;
472
476
  default: return item;
473
477
  }
474
478
  }).join("");
@@ -489,6 +493,22 @@ function serializeMoveRangeStart(tag, marker) {
489
493
  function rewriteRunTextAsDeleted(xml) {
490
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>");
491
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
+ }
492
512
  function trackedChangeTag(change) {
493
513
  switch (change.type) {
494
514
  case "insertion": return "ins";
@@ -499,29 +519,28 @@ function trackedChangeTag(change) {
499
519
  }
500
520
  function serializeTrackedChange(tag, change) {
501
521
  const attrs = serializeTrackedChangeAttributes(change.info);
502
- const serializeDeletedRun = (run) => {
503
- const xml = serializeRun(run);
504
- if (!run.content.some((c) => c.type === "drawing" || c.type === "shape")) return rewriteRunTextAsDeleted(xml);
505
- if (!run.content.some((c) => c.type !== "drawing" && c.type !== "shape")) return xml;
506
- return run.content.map((content) => {
507
- const contentXml = serializeRun({
508
- ...run,
509
- content: [content]
510
- });
511
- if (content.type === "drawing" || content.type === "shape") return contentXml;
512
- return rewriteRunTextAsDeleted(contentXml);
513
- }).join("");
514
- };
515
- 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);
516
524
  const serializeWrappedItem = (item) => {
517
- if (item.type === "run") return serializeContentRun(item);
518
- if (item.type === "simpleField" || item.type === "complexField") {
519
- const xml = item.type === "simpleField" ? serializeSimpleField(item) : serializeComplexField(item);
520
- 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;
521
543
  }
522
- if (item.type === "mathEquation") return item.ommlXml;
523
- if (item.type === "insertion" || item.type === "deletion" || item.type === "moveFrom" || item.type === "moveTo") return serializeTrackedChange(trackedChangeTag(item), item);
524
- return item.type === "bookmarkStart" ? serializeBookmarkStart(item) : serializeBookmarkEnd(item);
525
544
  };
526
545
  const open = `<w:${tag} ${attrs}>`;
527
546
  const close = `</w:${tag}>`;
@@ -554,27 +573,22 @@ function serializeCommentReferenceRun(id) {
554
573
  /**
555
574
  * Serialize a single paragraph content item.
556
575
  *
557
- * `explicitCommentReferenceIds` holds the comment ids that already have their
558
- * own `commentReference` node in this paragraph (the parsed-from-Word shape).
559
- * For those, the `commentRangeEnd` marker must NOT also synthesize a reference
560
- * run, or a save→parse round-trip doubles the `<w:commentReference>`. The
561
- * editor (fromProseDoc) path emits range markers with no reference node, so the
562
- * 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.
563
580
  */
564
- function serializeParagraphContent(content, explicitCommentReferenceIds) {
581
+ function serializeParagraphContent(content, disposition = "kept") {
565
582
  switch (content.type) {
566
- case "run": return serializeRun(content);
567
- case "hyperlink": return serializeHyperlink(content);
583
+ case "run": return serializeInlineRun(content, disposition);
584
+ case "hyperlink": return serializeHyperlink(content, disposition);
568
585
  case "bookmarkStart": return serializeBookmarkStart(content);
569
586
  case "bookmarkEnd": return serializeBookmarkEnd(content);
570
587
  case "simpleField": return serializeSimpleField(content);
571
588
  case "complexField": return serializeComplexField(content);
572
- case "inlineSdt": return serializeInlineSdt(content);
589
+ case "inlineSdt": return serializeInlineSdt(content, disposition);
573
590
  case "commentRangeStart": return `<w:commentRangeStart ${markupRangeAttributes(content).join(" ")}/>`;
574
- case "commentRangeEnd": {
575
- const end = `<w:commentRangeEnd ${markupRangeAttributes(content).join(" ")}/>`;
576
- return explicitCommentReferenceIds.has(content.id) ? end : `${end}${serializeCommentReferenceRun(content.id)}`;
577
- }
591
+ case "commentRangeEnd": return `<w:commentRangeEnd ${markupRangeAttributes(content).join(" ")}/>`;
578
592
  case "commentReference": return serializeCommentReferenceRun(content.id);
579
593
  case "insertion": return serializeTrackedChange("ins", content);
580
594
  case "deletion": return serializeTrackedChange("del", content);
@@ -584,11 +598,18 @@ function serializeParagraphContent(content, explicitCommentReferenceIds) {
584
598
  case "moveFromRangeEnd": return `<w:moveFromRangeEnd ${markupRangeAttributes(content).join(" ")}/>`;
585
599
  case "moveToRangeStart": return serializeMoveRangeStart("moveToRangeStart", content);
586
600
  case "moveToRangeEnd": return `<w:moveToRangeEnd ${markupRangeAttributes(content).join(" ")}/>`;
587
- case "bidiWrapper": {
588
- const tag = content.control === "override" ? "bdo" : "dir";
589
- return `<w:${tag}${content.direction === void 0 ? "" : ` w:val="${escapeXmlAttribute(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
+ }
590
610
  }
591
611
  case "mathEquation": return content.ommlXml || "";
612
+ case "preservedInline": return content.xml;
592
613
  default: return "";
593
614
  }
594
615
  }
@@ -604,18 +625,17 @@ function serializeParagraph(paragraph) {
604
625
  if (paragraph.paraId) attrs.push(`w14:paraId="${escapeXmlAttribute(paragraph.paraId)}"`);
605
626
  if (paragraph.textId) attrs.push(`w14:textId="${escapeXmlAttribute(paragraph.textId)}"`);
606
627
  if (paragraph.reviewCarrier) attrs.push(`folio:reviewCarrier="${paragraph.reviewCarrier}"`);
607
- const attrsStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
628
+ const written = serializePreservedAttributes(attrs, paragraph.preservedAttributes);
629
+ const attrsStr = written.length > 0 ? ` ${written.join(" ")}` : "";
608
630
  parts.push(serializeParagraphFormattingWithOptions(paragraph.formatting, {
609
631
  propertyChanges: paragraph.propertyChanges,
610
632
  paragraphMarkChange: paragraph.pPrMark,
611
633
  propertySource: getParagraphPropertySource(paragraph),
612
634
  sectionProperties: paragraph.sectionProperties
613
635
  }));
614
- const explicitCommentReferenceIds = /* @__PURE__ */ new Set();
615
- for (const content of paragraph.content) if (content.type === "commentReference") explicitCommentReferenceIds.add(content.id);
616
636
  let pendingRenderedPageBreak = paragraph.renderedPageBreakBefore === true;
617
637
  for (const content of paragraph.content) {
618
- let contentXml = serializeParagraphContent(content, explicitCommentReferenceIds);
638
+ let contentXml = serializeParagraphContent(content);
619
639
  if (contentXml) {
620
640
  if (pendingRenderedPageBreak) {
621
641
  const next = injectRenderedPageBreakIntoFirstRun(contentXml);