@stll/folio-core 0.33.1 → 0.33.2

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 (91) hide show
  1. package/dist/ai-edits/apply.d.ts +15 -0
  2. package/dist/ai-edits/apply.js +134 -25
  3. package/dist/ai-edits/headless.d.ts +47 -3
  4. package/dist/ai-edits/headless.js +53 -3
  5. package/dist/ai-edits/index.d.ts +1 -1
  6. package/dist/ai-edits/snapshot.d.ts +19 -1
  7. package/dist/ai-edits/snapshot.js +25 -3
  8. package/dist/ai-edits/table-geometry.d.ts +56 -0
  9. package/dist/ai-edits/table-geometry.js +217 -0
  10. package/dist/ai-edits/table-row-column-mutations.d.ts +34 -16
  11. package/dist/ai-edits/table-row-column-mutations.js +12 -4
  12. package/dist/ai-edits/table-template.d.ts +60 -0
  13. package/dist/ai-edits/table-template.js +176 -0
  14. package/dist/ai-edits/types.d.ts +7 -0
  15. package/dist/compare/__fixtures__/body-sequence.d.ts +61 -2
  16. package/dist/compare/__fixtures__/body-sequence.js +94 -6
  17. package/dist/compare/compare.d.ts +14 -7
  18. package/dist/compare/compare.js +136 -38
  19. package/dist/compare/plan.d.ts +31 -1
  20. package/dist/compare/plan.js +233 -66
  21. package/dist/compare/types.d.ts +9 -7
  22. package/dist/compare/types.js +7 -5
  23. package/dist/compare/verification.d.ts +31 -12
  24. package/dist/compare/verification.js +88 -17
  25. package/dist/compat/eigenpal.d.ts +4 -4
  26. package/dist/compat/eigenpal.js +2 -2
  27. package/dist/display-list/primitives.d.ts +1 -1
  28. package/dist/document-operations.d.ts +10 -1
  29. package/dist/document-operations.js +3 -2
  30. package/dist/docx/appVersionNormalization.d.ts +49 -0
  31. package/dist/docx/appVersionNormalization.js +74 -0
  32. package/dist/docx/blockContentParser.js +3 -2
  33. package/dist/docx/drawingUtils.js +4 -3
  34. package/dist/docx/paragraphParser.js +3 -2
  35. package/dist/docx/rezip.d.ts +10 -4
  36. package/dist/docx/rezip.js +54 -18
  37. package/dist/docx/runParser.js +9 -8
  38. package/dist/docx/sdtProperties.js +4 -3
  39. package/dist/docx/selectiveSave.js +7 -6
  40. package/dist/docx/serializer/commentSerializer.d.ts +1 -1
  41. package/dist/docx/serializer/commentSerializer.js +43 -19
  42. package/dist/docx/serializer/documentSerializer.d.ts +3 -1
  43. package/dist/docx/serializer/documentSerializer.js +42 -78
  44. package/dist/docx/serializer/fontTableSerializer.js +8 -5
  45. package/dist/docx/serializer/headerFooterSerializer.d.ts +4 -1
  46. package/dist/docx/serializer/headerFooterSerializer.js +34 -29
  47. package/dist/docx/serializer/noteSerializer.js +34 -30
  48. package/dist/docx/serializer/numberingSerializer.js +8 -4
  49. package/dist/docx/serializer/paragraphSerializer.js +3 -3
  50. package/dist/docx/serializer/partNamespaces.d.ts +78 -0
  51. package/dist/docx/serializer/partNamespaces.js +324 -0
  52. package/dist/docx/serializer/runSerializer.js +1 -1
  53. package/dist/docx/serializer/settingsSerializer.js +8 -2
  54. package/dist/docx/serializer/stylesSerializer.js +8 -5
  55. package/dist/docx/serializer/tableSerializer.js +83 -18
  56. package/dist/docx/serializer/themeSerializer.js +10 -2
  57. package/dist/docx/server/build.d.ts +1 -1
  58. package/dist/docx/strictValueEncodings.gen.d.ts +15 -0
  59. package/dist/docx/strictValueEncodings.gen.js +275 -0
  60. package/dist/docx/tableParser.d.ts +0 -6
  61. package/dist/docx/tableParser.js +52 -7
  62. package/dist/docx/transitionalSpelling.d.ts +23 -0
  63. package/dist/docx/transitionalSpelling.js +36 -0
  64. package/dist/docx/universalMeasure.d.ts +20 -0
  65. package/dist/docx/universalMeasure.js +32 -0
  66. package/dist/docx/verbatimCapture.d.ts +20 -0
  67. package/dist/docx/verbatimCapture.js +131 -0
  68. package/dist/docx/vmlImageParser.js +4 -3
  69. package/dist/docx/watermarkParser.js +4 -3
  70. package/dist/docx/xmlParser.d.ts +23 -2
  71. package/dist/docx/xmlParser.js +43 -11
  72. package/dist/index.d.ts +4 -4
  73. package/dist/index.js +2 -2
  74. package/dist/model.d.ts +3 -3
  75. package/dist/model.js +2 -2
  76. package/dist/prosemirror/commands/comments.js +2 -1
  77. package/dist/prosemirror/containerFinalParagraph.d.ts +47 -0
  78. package/dist/prosemirror/containerFinalParagraph.js +92 -0
  79. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
  80. package/dist/prosemirror/conversion/fromProseDoc.js +15 -4
  81. package/dist/prosemirror/conversion/toProseDoc.js +12 -3
  82. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -0
  83. package/dist/prosemirror/schema/marks.d.ts +1 -1
  84. package/dist/prosemirror/schema/nodes.d.ts +22 -0
  85. package/dist/server.d.ts +1 -1
  86. package/dist/types/block-id.d.ts +12 -1
  87. package/dist/types/block-id.js +17 -1
  88. package/dist/utils/canonicalJson.d.ts +14 -0
  89. package/dist/utils/canonicalJson.js +19 -0
  90. package/dist/utils/formatToStyle.d.ts +1 -1
  91. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import { isAllowedExternalWatermarkImageUrl } from "../watermark/index.js";
2
+ import { normalizeAppVersionInExtendedProperties } from "./appVersionNormalization.js";
2
3
  import { withoutOrphanCommentRanges } from "./commentRangeIntegrity.js";
3
4
  import { applyReplyThreadMarkers } from "./commentReplyMarkers.js";
4
5
  import { parseEndnotes, parseFootnotes } from "./footnoteParser.js";
@@ -16,6 +17,7 @@ import { serializeFontTableXml } from "./serializer/fontTableSerializer.js";
16
17
  import { serializeHeaderFooter } from "./serializer/headerFooterSerializer.js";
17
18
  import { serializeEndnotes, serializeFootnotes, serializeNewEndnotesPart, serializeNewFootnotesPart } from "./serializer/noteSerializer.js";
18
19
  import { serializeNumberingXml } from "./serializer/numberingSerializer.js";
20
+ import { readRootNamespaceBindings } from "./serializer/partNamespaces.js";
19
21
  import { serializeSettingsXml } from "./serializer/settingsSerializer.js";
20
22
  import { serializeStyle, serializeStylesXml } from "./serializer/stylesSerializer.js";
21
23
  import { serializeThemeXml } from "./serializer/themeSerializer.js";
@@ -122,13 +124,12 @@ function assertDocumentPackageFidelity(originalDocumentXml, serializedDocumentXm
122
124
  async function serializeCommentsToZip(doc, zip, compressionLevel) {
123
125
  const comments = doc.package.document.comments ?? [];
124
126
  const sourceCommentsFile = findZipEntryCaseInsensitive(zip, "word/comments.xml");
127
+ const sourceCommentsXml = sourceCommentsFile ? await sourceCommentsFile.async("text") : void 0;
125
128
  if (comments.length === 0) {
126
- if (!sourceCommentsFile) return;
127
- const sourceCommentsXml = await sourceCommentsFile.async("text");
128
- if (!hasCommentEntries(sourceCommentsXml)) return;
129
+ if (sourceCommentsXml === void 0 || !hasCommentEntries(sourceCommentsXml)) return;
129
130
  }
130
131
  ensureThreadedCommentParaIds(comments);
131
- const commentsXml = serializeComments(comments);
132
+ const commentsXml = serializeComments(comments, sourceCommentsXml === void 0 ? void 0 : readRootNamespaceBindings(sourceCommentsXml));
132
133
  zip.file(sourceCommentsFile?.name ?? "word/comments.xml", commentsXml, {
133
134
  compression: "DEFLATE",
134
135
  compressionOptions: { level: compressionLevel }
@@ -428,6 +429,30 @@ const normalizePackageIdsInZip = async (zip, compressionLevel) => {
428
429
  compressionOptions: { level: compressionLevel }
429
430
  });
430
431
  };
432
+ const EXTENDED_PROPERTIES_PATH = "docProps/app.xml";
433
+ /**
434
+ * Bring the application version the package states about itself into the form
435
+ * the schema gives it. A package that holds no extended properties keeps
436
+ * holding none.
437
+ */
438
+ const normalizeAppVersionInZip = async (zip, compressionLevel) => {
439
+ const extendedProperties = zip.file(EXTENDED_PROPERTIES_PATH);
440
+ if (!extendedProperties) return;
441
+ const xml = await extendedProperties.async("text");
442
+ const normalized = normalizeAppVersionInExtendedProperties(xml);
443
+ if (normalized !== xml) zip.file(EXTENDED_PROPERTIES_PATH, normalized, {
444
+ compression: "DEFLATE",
445
+ compressionOptions: { level: compressionLevel }
446
+ });
447
+ };
448
+ /**
449
+ * Every pass a save owes the package as a whole, in one function so that the
450
+ * full repack and the selective save cannot drift into running different ones.
451
+ */
452
+ const normalizePackageOnSave = async (zip, compressionLevel) => {
453
+ await normalizePackageIdsInZip(zip, compressionLevel);
454
+ await normalizeAppVersionInZip(zip, compressionLevel);
455
+ };
431
456
  /**
432
457
  * The single exit for a repacked package. Reconciliation runs here rather than
433
458
  * at each caller so no save path can emit a package whose relationships or
@@ -435,7 +460,7 @@ const normalizePackageIdsInZip = async (zip, compressionLevel) => {
435
460
  */
436
461
  const generateDocxZip = async (zip, compressionLevel) => {
437
462
  await reconcilePackageReferences(zip, compressionLevel);
438
- await normalizePackageIdsInZip(zip, compressionLevel);
463
+ await normalizePackageOnSave(zip, compressionLevel);
439
464
  return zip.generateAsync({
440
465
  type: "arraybuffer",
441
466
  compression: "DEFLATE",
@@ -470,14 +495,14 @@ const finishRepack = async ({ document, originalZip, outputZip, originalDocument
470
495
  await processNewHyperlinks(parts, outputZip, compressionLevel);
471
496
  assertValidFolioDocumentModel(document, "Cannot repack invalid DOCX document model");
472
497
  applyReplyThreadMarkers(document);
473
- const documentXml = serializeDocument(document);
498
+ const documentXml = serializeDocument(document, originalDocumentXml === void 0 ? void 0 : readRootNamespaceBindings(originalDocumentXml));
474
499
  if (originalDocumentXml) assertDocumentPackageFidelity(originalDocumentXml, documentXml, document);
475
500
  outputZip.file("word/document.xml", documentXml, {
476
501
  compression: "DEFLATE",
477
502
  compressionOptions: { level: compressionLevel }
478
503
  });
479
504
  await rebindWatermarkRelIds(document, outputZip, compressionLevel);
480
- serializeHeadersFootersToZip(document, outputZip, compressionLevel);
505
+ await serializeHeadersFootersToZip(document, outputZip, compressionLevel);
481
506
  await serializeNotesToZip({
482
507
  doc: document,
483
508
  originalZip,
@@ -559,14 +584,14 @@ async function repackDocxFromRaw(doc, rawContent, options = {}) {
559
584
  await processNewHyperlinks(parts, newZip, compressionLevel);
560
585
  assertValidFolioDocumentModel(exportDocument, "Cannot repack invalid DOCX document model");
561
586
  applyReplyThreadMarkers(exportDocument);
562
- const documentXml = serializeDocument(exportDocument);
587
+ const documentXml = serializeDocument(exportDocument, rawContent.documentXml ? readRootNamespaceBindings(rawContent.documentXml) : void 0);
563
588
  if (rawContent.documentXml) assertDocumentPackageFidelity(rawContent.documentXml, documentXml, exportDocument);
564
589
  newZip.file("word/document.xml", documentXml, {
565
590
  compression: "DEFLATE",
566
591
  compressionOptions: { level: compressionLevel }
567
592
  });
568
593
  await rebindWatermarkRelIds(exportDocument, newZip, compressionLevel);
569
- serializeHeadersFootersToZip(exportDocument, newZip, compressionLevel);
594
+ await serializeHeadersFootersToZip(exportDocument, newZip, compressionLevel);
570
595
  await serializeNotesToZip({
571
596
  doc: exportDocument,
572
597
  originalZip: rawContent.originalZip,
@@ -775,11 +800,11 @@ async function updateMultipleFiles(originalBuffer, updates, options = {}) {
775
800
  * Apply file updates to an already-loaded JSZip instance and generate the output.
776
801
  * Use this when the zip is already loaded to avoid a redundant decompression pass.
777
802
  *
778
- * This is the selective save's exit, so it owes the package the same id passes
803
+ * This is the selective save's exit, so it owes the package the same passes
779
804
  * {@link generateDocxZip} runs: a save that rewrites only the changed
780
805
  * paragraphs still has to see the parts it left alone, both to know which
781
- * revision ids are free and because an out-of-range paragraph id can sit in a
782
- * part it never touched.
806
+ * revision ids are free and because an out-of-range paragraph id, or a
807
+ * malformed application version, can sit in a part it never touched.
783
808
  */
784
809
  async function applyUpdatesToZip(zip, updates, options = {}) {
785
810
  const { compressionLevel = 6 } = options;
@@ -787,7 +812,7 @@ async function applyUpdatesToZip(zip, updates, options = {}) {
787
812
  compression: "DEFLATE",
788
813
  compressionOptions: { level: compressionLevel }
789
814
  });
790
- await normalizePackageIdsInZip(zip, compressionLevel);
815
+ await normalizePackageOnSave(zip, compressionLevel);
791
816
  return await zip.generateAsync({
792
817
  type: "arraybuffer",
793
818
  compression: "DEFLATE",
@@ -1090,7 +1115,13 @@ async function rebindWatermarkRelIds(doc, zip, compressionLevel) {
1090
1115
  });
1091
1116
  }
1092
1117
  }
1093
- function collectHeaderFooterUpdates(doc) {
1118
+ /**
1119
+ * Re-serialize every header/footer the model still owns, keyed by part path.
1120
+ *
1121
+ * `sourceZip` supplies each part as it stands before the save so the rebuilt
1122
+ * root can keep any prefix binding only the source document declared.
1123
+ */
1124
+ async function collectHeaderFooterUpdates(doc, sourceZip) {
1094
1125
  const updates = /* @__PURE__ */ new Map();
1095
1126
  const rels = doc.package.relationships;
1096
1127
  if (!rels) return updates;
@@ -1107,8 +1138,13 @@ function collectHeaderFooterUpdates(doc) {
1107
1138
  for (const [rId, headerFooter] of map.entries()) {
1108
1139
  const rel = rels.get(rId);
1109
1140
  if (rel && rel.type === type && rel.target) {
1110
- const filename = resolveRelativePath(documentRelsPath, rel.target);
1111
- updates.set(filename, serializeHeaderFooter(headerFooter));
1141
+ const path = resolveRelativePath(documentRelsPath, rel.target);
1142
+ const sourceFile = findZipEntryCaseInsensitive(sourceZip, path.toLowerCase());
1143
+ const bindings = sourceFile ? readRootNamespaceBindings(await sourceFile.async("text")) : /* @__PURE__ */ new Map();
1144
+ updates.set(path, serializeHeaderFooter(headerFooter, {
1145
+ path,
1146
+ bindings
1147
+ }));
1112
1148
  }
1113
1149
  }
1114
1150
  }
@@ -1117,9 +1153,9 @@ function collectHeaderFooterUpdates(doc) {
1117
1153
  /**
1118
1154
  * Serialize modified headers and footers into the ZIP
1119
1155
  */
1120
- function serializeHeadersFootersToZip(doc, zip, compressionLevel) {
1156
+ async function serializeHeadersFootersToZip(doc, zip, compressionLevel) {
1121
1157
  const compressionOptions = { level: compressionLevel };
1122
- for (const [filename, xml] of collectHeaderFooterUpdates(doc)) zip.file(filename, xml, {
1158
+ for (const [filename, xml] of await collectHeaderFooterUpdates(doc, zip)) zip.file(filename, xml, {
1123
1159
  compression: "DEFLATE",
1124
1160
  compressionOptions
1125
1161
  });
@@ -7,8 +7,9 @@ import { parseShapeFromDrawing, shouldPreserveRawShapeDrawing } from "./shapePar
7
7
  import { isTextBoxDrawing } from "./textBoxParser.js";
8
8
  import { requiresXmlSpacePreserve } from "./textWhitespace.js";
9
9
  import { resolveThemeFontRef } from "./themeParser.js";
10
+ import { captureVerbatimXml } from "./verbatimCapture.js";
10
11
  import { parseVmlImageContent } from "./vmlImageParser.js";
11
- import { cloneWithXmlnsDeclarations, elementToXml, findAllDeep, findChild, findChildren, getAttribute, getChildElements, getLocalName, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js";
12
+ import { cloneWithXmlnsDeclarations, findAllDeep, findChild, findChildren, getAttribute, getChildElements, getLocalName, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js";
12
13
  import { DRAWING_RAW_XML_MODES, normalizeRevisionId } from "@stll/docx-core/model";
13
14
  //#region src/docx/runParser.ts
14
15
  /**
@@ -489,7 +490,7 @@ function parseDrawingContent(element, rels, media) {
489
490
  if (groupImage) return {
490
491
  type: "drawing",
491
492
  image: groupImage,
492
- rawXml: elementToXml(element)
493
+ rawXml: captureVerbatimXml(element)
493
494
  };
494
495
  if (shouldPreserveRawShapeDrawing(element)) return {
495
496
  type: "drawing",
@@ -502,7 +503,7 @@ function parseDrawingContent(element, rels, media) {
502
503
  },
503
504
  wrap: { type: "inline" }
504
505
  },
505
- rawXml: elementToXml(element),
506
+ rawXml: captureVerbatimXml(element),
506
507
  rawXmlMode: DRAWING_RAW_XML_MODES.PRESERVE_ONLY
507
508
  };
508
509
  const shape = parseShapeFromDrawing(element);
@@ -516,7 +517,7 @@ function parseDrawingContent(element, rels, media) {
516
517
  type: "drawing",
517
518
  image
518
519
  };
519
- if (!image.src) drawing.rawXml = elementToXml(element);
520
+ if (!image.src) drawing.rawXml = captureVerbatimXml(element);
520
521
  return drawing;
521
522
  }
522
523
  /**
@@ -593,7 +594,7 @@ function parseRunContents(runElement, rels, media, rootXmlns = {}) {
593
594
  if (groupedChoiceDrawing) {
594
595
  const groupedDrawing = parseDrawingContent(groupedChoiceDrawing, rels, media);
595
596
  if (groupedDrawing?.type === "drawing" && groupedDrawing.image.src) {
596
- groupedDrawing.rawXml = elementToXml(child);
597
+ groupedDrawing.rawXml = captureVerbatimXml(child);
597
598
  contents.push(groupedDrawing);
598
599
  break;
599
600
  }
@@ -601,7 +602,7 @@ function parseRunContents(runElement, rels, media, rootXmlns = {}) {
601
602
  const fallbackPict = fallbackEl ? getChildElements(fallbackEl).find((el) => getLocalName(el.name) === "pict") : void 0;
602
603
  const fallbackVml = fallbackPict && !choiceTextBoxDrawing ? parseVmlImageContent(fallbackPict, rels, media, rootXmlns) : null;
603
604
  if (fallbackVml?.image.src) {
604
- fallbackVml.rawXml = elementToXml(cloneWithXmlnsDeclarations(child, rootXmlns));
605
+ fallbackVml.rawXml = captureVerbatimXml(cloneWithXmlnsDeclarations(child, rootXmlns));
605
606
  contents.push(fallbackVml);
606
607
  break;
607
608
  }
@@ -611,13 +612,13 @@ function parseRunContents(runElement, rels, media, rootXmlns = {}) {
611
612
  if (innerName === "drawing") {
612
613
  const innerDrawing = parseDrawingContent(innerChild, rels, media);
613
614
  if (innerDrawing) {
614
- if (innerDrawing.type === "drawing" && (innerDrawing.rawXml !== void 0 || !innerDrawing.image.src)) innerDrawing.rawXml = elementToXml(child);
615
+ if (innerDrawing.type === "drawing" && (innerDrawing.rawXml !== void 0 || !innerDrawing.image.src)) innerDrawing.rawXml = captureVerbatimXml(child);
615
616
  contents.push(innerDrawing);
616
617
  }
617
618
  } else if (innerName === "pict") {
618
619
  const innerVml = parseVmlImageContent(innerChild, rels, media, rootXmlns);
619
620
  if (innerVml) {
620
- innerVml.rawXml = elementToXml(cloneWithXmlnsDeclarations(child, rootXmlns));
621
+ innerVml.rawXml = captureVerbatimXml(cloneWithXmlnsDeclarations(child, rootXmlns));
621
622
  contents.push(innerVml);
622
623
  }
623
624
  }
@@ -1,5 +1,6 @@
1
1
  import { SdtLockSchema, narrowEnum } from "./parserEnums.js";
2
- import { elementToXml, findChild, getAttributeAnyPrefix, getLocalName, parseBooleanElement } from "./xmlParser.js";
2
+ import { captureVerbatimXml } from "./verbatimCapture.js";
3
+ import { findChild, getAttributeAnyPrefix, getLocalName, parseBooleanElement } from "./xmlParser.js";
3
4
  //#region src/docx/sdtProperties.ts
4
5
  function parseListItems(el) {
5
6
  const items = [];
@@ -201,7 +202,7 @@ function rewriteOpenTagAttrPrefixes(raw, canonical, local) {
201
202
  function parseSdtProperties(sdtPr, sdtEndPr) {
202
203
  const props = { sdtType: "richText" };
203
204
  if (sdtPr) {
204
- props.rawPropertiesXml = normalizeWordPrefix(elementToXml(sdtPr), sdtPr);
205
+ props.rawPropertiesXml = normalizeWordPrefix(captureVerbatimXml(sdtPr), sdtPr);
205
206
  for (const el of sdtPr.elements ?? []) {
206
207
  if (el.type !== "element") continue;
207
208
  const name = el.name ? getLocalName(el.name) : "";
@@ -283,7 +284,7 @@ function parseSdtProperties(sdtPr, sdtEndPr) {
283
284
  }
284
285
  }
285
286
  }
286
- if (sdtEndPr) props.rawEndPropertiesXml = normalizeWordPrefix(elementToXml(sdtEndPr), sdtEndPr);
287
+ if (sdtEndPr) props.rawEndPropertiesXml = normalizeWordPrefix(captureVerbatimXml(sdtEndPr), sdtEndPr);
287
288
  return props;
288
289
  }
289
290
  //#endregion
@@ -13,6 +13,7 @@ import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExten
13
13
  import { serializeDocument } from "./serializer/documentSerializer.js";
14
14
  import { serializeEndnotes, serializeFootnotes } from "./serializer/noteSerializer.js";
15
15
  import { serializeNumberingXml } from "./serializer/numberingSerializer.js";
16
+ import { readRootNamespaceBindings } from "./serializer/partNamespaces.js";
16
17
  //#region src/docx/selectiveSave.ts
17
18
  /**
18
19
  * Check if document content has new images (data: URL without rId) or
@@ -209,7 +210,6 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
209
210
  if (!validateFolioDocumentModel(doc).valid) return null;
210
211
  const comments = doc.package.document.comments ?? [];
211
212
  const hasComments = comments.length > 0;
212
- const headerFooterUpdates = collectHeaderFooterUpdates(doc);
213
213
  try {
214
214
  const zip = await (await import("jszip")).default.loadAsync(originalBuffer);
215
215
  for (const [path, file] of Object.entries(zip.files)) if (!file.dir && isUnsafePackagePath(path)) return null;
@@ -228,15 +228,16 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
228
228
  if (unrouted === null || unrouted.size > 0) return null;
229
229
  }
230
230
  if (bodyChangedIds.size > 0) {
231
- const patchedDocXml = buildPatchedDocumentXml(originalDocXml, serializeDocument(doc), bodyChangedIds);
231
+ const patchedDocXml = buildPatchedDocumentXml(originalDocXml, serializeDocument(doc, readRootNamespaceBindings(originalDocXml)), bodyChangedIds);
232
232
  if (!patchedDocXml) return null;
233
233
  updates.set("word/document.xml", patchedDocXml);
234
234
  }
235
235
  }
236
- const hadCommentsFile = zip.file("word/comments.xml") !== null;
237
- if (hasComments || hadCommentsFile) {
236
+ const sourceCommentsFile = zip.file("word/comments.xml");
237
+ if (hasComments || sourceCommentsFile) {
238
238
  ensureThreadedCommentParaIds(comments);
239
- updates.set("word/comments.xml", serializeComments(comments));
239
+ const sourceBindings = sourceCommentsFile ? readRootNamespaceBindings(await sourceCommentsFile.async("text")) : void 0;
240
+ updates.set("word/comments.xml", serializeComments(comments, sourceBindings));
240
241
  }
241
242
  if (hasComments) {
242
243
  const ctFile = zip.file("[Content_Types].xml");
@@ -256,7 +257,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
256
257
  }
257
258
  if (!await patchCommentsExtended(zip, comments, updates)) return null;
258
259
  await patchNumberingPart(zip, doc, updates);
259
- for (const [path, xml] of headerFooterUpdates) updates.set(path, xml);
260
+ for (const [path, xml] of await collectHeaderFooterUpdates(doc, zip)) updates.set(path, xml);
260
261
  await queueSettingsUpdates(zip, updates);
261
262
  const corePropsFile = zip.file("docProps/core.xml");
262
263
  if (corePropsFile) {
@@ -7,7 +7,7 @@ import { Comment } from "../../types/content.js";
7
7
  * comment — leaving the previous file in place would otherwise re-emit
8
8
  * the orphaned comment threads on every save.
9
9
  */
10
- declare function serializeComments(comments: Comment[]): string;
10
+ declare function serializeComments(comments: Comment[], sourceBindings?: ReadonlyMap<string, string>): string;
11
11
  /**
12
12
  * Assign a deterministic `w14:paraId` to the LAST paragraph of every comment
13
13
  * that needs a commentsExtended entry (a reply, a reply's parent, or a resolved
@@ -1,5 +1,6 @@
1
1
  import { deterministicHexId } from "../../utils/hexId.js";
2
2
  import { serializeParagraph } from "./paragraphSerializer.js";
3
+ import { serializePartElement } from "./partNamespaces.js";
3
4
  import { serializeTextFormatting } from "./runSerializer.js";
4
5
  import { escapeXml } from "./xmlUtils.js";
5
6
  //#region src/docx/serializer/commentSerializer.ts
@@ -36,12 +37,29 @@ function serializeComment(comment) {
36
37
  xml += "</w:comment>";
37
38
  return xml;
38
39
  }
39
- const COMMENT_EXTENSION_NAMESPACES = {
40
- w14: "http://schemas.microsoft.com/office/word/2010/wordml",
41
- wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
42
- };
43
- const COMMENTS_HEADER = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
44
- <w:comments xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ${Object.entries(COMMENT_EXTENSION_NAMESPACES).map(([prefix, namespace]) => `xmlns:${prefix}="${namespace}"`).join(" ")} xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="${Object.keys(COMMENT_EXTENSION_NAMESPACES).join(" ")}">`;
40
+ const COMMENTS_BASELINE_PREFIXES = [
41
+ "wpc",
42
+ "mc",
43
+ "o",
44
+ "r",
45
+ "m",
46
+ "v",
47
+ "wp",
48
+ "w10",
49
+ "w",
50
+ "w14",
51
+ "wp14",
52
+ "wpg",
53
+ "wpi",
54
+ "wne",
55
+ "wps"
56
+ ];
57
+ const COMMENTS_EXTENDED_BASELINE_PREFIXES = [
58
+ "mc",
59
+ "w",
60
+ "w15"
61
+ ];
62
+ const XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n";
45
63
  /**
46
64
  * Serialize comments array to comments.xml content. Returns a valid empty
47
65
  * `<w:comments/>` document for an empty array so callers can overwrite an
@@ -49,18 +67,21 @@ const COMMENTS_HEADER = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
49
67
  * comment — leaving the previous file in place would otherwise re-emit
50
68
  * the orphaned comment threads on every save.
51
69
  */
52
- function serializeComments(comments) {
70
+ function serializeComments(comments, sourceBindings) {
53
71
  const topLevel = [];
54
72
  const replies = [];
55
73
  for (const c of comments) {
56
74
  const { parentId } = c;
57
75
  (parentId === null || parentId === void 0 ? topLevel : replies).push(c);
58
76
  }
59
- let xml = COMMENTS_HEADER;
60
- for (const comment of topLevel) xml += serializeComment(comment);
61
- for (const reply of replies) xml += serializeComment(reply);
62
- xml += "</w:comments>";
63
- return xml;
77
+ const body = topLevel.map((comment) => serializeComment(comment)).join("") + replies.map((reply) => serializeComment(reply)).join("");
78
+ return XML_DECLARATION + serializePartElement({
79
+ partPath: "word/comments.xml",
80
+ rootName: "w:comments",
81
+ baselinePrefixes: COMMENTS_BASELINE_PREFIXES,
82
+ sourceBindings,
83
+ body
84
+ });
64
85
  }
65
86
  /** The `w14:paraId` Word threads a comment by: its LAST paragraph's paraId. */
66
87
  function commentThreadParaId(comment) {
@@ -145,7 +166,6 @@ function buildCommentExtendedEntries(comments) {
145
166
  }
146
167
  return entries.length > 0 ? entries : null;
147
168
  }
148
- const COMMENTS_EXTENDED_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<w15:commentsEx xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\" xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\" xmlns:w15=\"http://schemas.microsoft.com/office/word/2012/wordml\" mc:Ignorable=\"w15\">";
149
169
  /**
150
170
  * Serialize `commentsExtended.xml` (`w15:commentsEx`) for reply threading and
151
171
  * resolved state, or `null` when no comment needs an entry (see
@@ -155,13 +175,17 @@ const COMMENTS_EXTENDED_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\" stand
155
175
  function serializeCommentsExtended(comments) {
156
176
  const entries = buildCommentExtendedEntries(comments);
157
177
  if (!entries) return null;
158
- let xml = COMMENTS_EXTENDED_HEADER;
159
- for (const entry of entries) {
178
+ const body = entries.map((entry) => {
160
179
  const parentAttr = entry.paraIdParent !== void 0 ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
161
- xml += `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parentAttr} w15:done="${entry.done ? "1" : "0"}"/>`;
162
- }
163
- xml += "</w15:commentsEx>";
164
- return xml;
180
+ return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parentAttr} w15:done="${entry.done ? "1" : "0"}"/>`;
181
+ }).join("");
182
+ return XML_DECLARATION + serializePartElement({
183
+ partPath: "word/commentsExtended.xml",
184
+ rootName: "w15:commentsEx",
185
+ baselinePrefixes: COMMENTS_EXTENDED_BASELINE_PREFIXES,
186
+ sourceBindings: void 0,
187
+ body
188
+ });
165
189
  }
166
190
  //#endregion
167
191
  export { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExtended };
@@ -11,9 +11,11 @@ declare function serializeDocumentBody(body: document_d_exports.DocumentBody): s
11
11
  * Serialize a complete Document to valid document.xml
12
12
  *
13
13
  * @param doc - The document to serialize
14
+ * @param sourceBindings - Root `xmlns:*` of the part being replaced, so a
15
+ * prefix only the source document bound keeps its URI
14
16
  * @returns Complete XML string for document.xml
15
17
  */
16
- declare function serializeDocument(doc: document_d_exports.Document): string;
18
+ declare function serializeDocument(doc: document_d_exports.Document, sourceBindings?: ReadonlyMap<string, string>): string;
17
19
  /**
18
20
  * Serialize just the document body (useful for partial updates)
19
21
  *
@@ -1,78 +1,41 @@
1
1
  import { serializeBlockSdt } from "./blockSdtSerializer.js";
2
2
  import { serializeParagraph } from "./paragraphSerializer.js";
3
+ import { serializePartElement } from "./partNamespaces.js";
3
4
  import { resetAutoIdCounter } from "./runSerializer.js";
4
5
  import { serializeSectionProperties } from "./sectionPropertiesSerializer.js";
5
6
  import { serializeTable } from "./tableSerializer.js";
6
7
  //#region src/docx/serializer/documentSerializer.ts
7
8
  /**
8
- * Standard OOXML namespaces for document.xml
9
- */
10
- const NAMESPACES = {
11
- a: "http://schemas.openxmlformats.org/drawingml/2006/main",
12
- wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas",
13
- cx: "http://schemas.microsoft.com/office/drawing/2014/chartex",
14
- cx1: "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex",
15
- cx2: "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex",
16
- cx3: "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex",
17
- cx4: "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex",
18
- cx5: "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex",
19
- cx6: "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex",
20
- cx7: "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex",
21
- cx8: "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex",
22
- mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
23
- aink: "http://schemas.microsoft.com/office/drawing/2016/ink",
24
- am3d: "http://schemas.microsoft.com/office/drawing/2017/model3d",
25
- o: "urn:schemas-microsoft-com:office:office",
26
- oel: "http://schemas.microsoft.com/office/2019/extlst",
27
- pic: "http://schemas.openxmlformats.org/drawingml/2006/picture",
28
- r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
29
- m: "http://schemas.openxmlformats.org/officeDocument/2006/math",
30
- v: "urn:schemas-microsoft-com:vml",
31
- wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
32
- wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
33
- w10: "urn:schemas-microsoft-com:office:word",
34
- w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
35
- w14: "http://schemas.microsoft.com/office/word/2010/wordml",
36
- w15: "http://schemas.microsoft.com/office/word/2012/wordml",
37
- w16cex: "http://schemas.microsoft.com/office/word/2018/wordml/cex",
38
- w16cid: "http://schemas.microsoft.com/office/word/2016/wordml/cid",
39
- w16: "http://schemas.microsoft.com/office/word/2018/wordml",
40
- w16sdtdh: "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash",
41
- w16se: "http://schemas.microsoft.com/office/word/2015/wordml/symex",
42
- wpg: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
43
- wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
44
- wne: "http://schemas.microsoft.com/office/word/2006/wordml",
45
- wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
46
- };
47
- /**
48
- * Build namespace declaration string for document element
49
- */
50
- function buildNamespaceDeclarations() {
51
- const declared = {
52
- a: NAMESPACES.a,
53
- wpc: NAMESPACES.wpc,
54
- mc: NAMESPACES.mc,
55
- o: NAMESPACES.o,
56
- pic: NAMESPACES.pic,
57
- r: NAMESPACES.r,
58
- m: NAMESPACES.m,
59
- v: NAMESPACES.v,
60
- wp14: NAMESPACES.wp14,
61
- wp: NAMESPACES.wp,
62
- w10: NAMESPACES.w10,
63
- w: NAMESPACES.w,
64
- w14: NAMESPACES.w14,
65
- w15: NAMESPACES.w15,
66
- w16: NAMESPACES.w16,
67
- w16cex: NAMESPACES.w16cex,
68
- w16cid: NAMESPACES.w16cid,
69
- w16sdtdh: NAMESPACES.w16sdtdh,
70
- w16se: NAMESPACES.w16se,
71
- wpg: NAMESPACES.wpg,
72
- wps: NAMESPACES.wps
73
- };
74
- return Object.entries(declared).map(([prefix, uri]) => `xmlns:${prefix}="${uri}"`).join(" ");
75
- }
9
+ * Prefixes document.xml declares whether or not the body uses them.
10
+ *
11
+ * A canonical `<w:sdtPr>` legitimately carries `<w16sdtdh:dataHash>` /
12
+ * `<w16cex:*>` / `<w16cid:*>` children the parser stores opaquely, and the
13
+ * drawing prefixes host raw-replayed shapes, so the part keeps the shape Word
14
+ * writes even when this particular save emits none of them.
15
+ */
16
+ const DOCUMENT_BASELINE_PREFIXES = [
17
+ "a",
18
+ "wpc",
19
+ "mc",
20
+ "o",
21
+ "pic",
22
+ "r",
23
+ "m",
24
+ "v",
25
+ "wp14",
26
+ "wp",
27
+ "w10",
28
+ "w",
29
+ "w14",
30
+ "w15",
31
+ "w16",
32
+ "w16cex",
33
+ "w16cid",
34
+ "w16sdtdh",
35
+ "w16se",
36
+ "wpg",
37
+ "wps"
38
+ ];
76
39
  /**
77
40
  * Serialize a single block content item (paragraph, table, or block-level SDT).
78
41
  */
@@ -103,19 +66,20 @@ function serializeDocumentBody(body) {
103
66
  * Serialize a complete Document to valid document.xml
104
67
  *
105
68
  * @param doc - The document to serialize
69
+ * @param sourceBindings - Root `xmlns:*` of the part being replaced, so a
70
+ * prefix only the source document bound keeps its URI
106
71
  * @returns Complete XML string for document.xml
107
72
  */
108
- function serializeDocument(doc) {
73
+ function serializeDocument(doc, sourceBindings) {
109
74
  resetAutoIdCounter();
110
- const parts = [];
111
- parts.push("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
112
- const nsDecl = buildNamespaceDeclarations();
113
- parts.push(`<w:document ${nsDecl} mc:Ignorable="w14 w15 wp14">`);
114
- parts.push("<w:body>");
115
- parts.push(serializeDocumentBody(doc.package.document));
116
- parts.push("</w:body>");
117
- parts.push("</w:document>");
118
- return parts.join("");
75
+ const body = `<w:body>${serializeDocumentBody(doc.package.document)}</w:body>`;
76
+ return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" + serializePartElement({
77
+ partPath: "word/document.xml",
78
+ rootName: "w:document",
79
+ baselinePrefixes: DOCUMENT_BASELINE_PREFIXES,
80
+ sourceBindings,
81
+ body
82
+ });
119
83
  }
120
84
  /**
121
85
  * Serialize just the document body (useful for partial updates)
@@ -1,10 +1,13 @@
1
+ import { serializePartElement } from "./partNamespaces.js";
1
2
  import { escapeXml } from "./xmlUtils.js";
2
3
  //#region src/docx/serializer/fontTableSerializer.ts
3
- const W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
4
- const serializeFontTableXml = (fontTable) => {
5
- const fonts = fontTable.fonts.map(serializeFont).join("");
6
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:fonts xmlns:w="${W_NS}">${fonts}</w:fonts>`;
7
- };
4
+ const serializeFontTableXml = (fontTable) => "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + serializePartElement({
5
+ partPath: "word/fontTable.xml",
6
+ rootName: "w:fonts",
7
+ baselinePrefixes: ["w"],
8
+ sourceBindings: void 0,
9
+ body: fontTable.fonts.map(serializeFont).join("")
10
+ });
8
11
  const serializeFont = (font) => {
9
12
  const parts = [];
10
13
  if (font.altName) parts.push(`<w:altName w:val="${escapeXml(font.altName)}"/>`);
@@ -1,11 +1,14 @@
1
1
  import { document_d_exports } from "../../types/document.js";
2
+ import { SourcePart } from "./partNamespaces.js";
2
3
  //#region src/docx/serializer/headerFooterSerializer.d.ts
3
4
  /**
4
5
  * Serialize a HeaderFooter object to valid OOXML XML
5
6
  *
6
7
  * @param hf - HeaderFooter object to serialize
8
+ * @param source - The part being replaced, so a prefix only the source
9
+ * document bound keeps its URI
7
10
  * @returns Complete XML string for header*.xml or footer*.xml
8
11
  */
9
- declare function serializeHeaderFooter(hf: document_d_exports.HeaderFooter): string;
12
+ declare function serializeHeaderFooter(hf: document_d_exports.HeaderFooter, source?: SourcePart): string;
10
13
  //#endregion
11
14
  export { serializeHeaderFooter };