@stll/folio-core 0.55.0 → 0.56.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 (119) hide show
  1. package/dist/ai-edits/apply.js +169 -38
  2. package/dist/ai-edits/headless.js +2 -2
  3. package/dist/ai-edits/types.d.ts +7 -1
  4. package/dist/compare/compare.js +1 -0
  5. package/dist/compare/reproducible-package.d.ts +0 -17
  6. package/dist/compare/reproducible-package.js +12 -18
  7. package/dist/controller/canonicalClipboard.d.ts +23 -0
  8. package/dist/controller/canonicalClipboard.js +476 -0
  9. package/dist/controller/canonicalClipboardResources.d.ts +21 -0
  10. package/dist/controller/canonicalClipboardResources.js +382 -0
  11. package/dist/controller/canonicalComposition.d.ts +20 -3
  12. package/dist/controller/canonicalComposition.js +166 -38
  13. package/dist/controller/canonicalInlineProjection.d.ts +113 -0
  14. package/dist/controller/canonicalInlineProjection.js +393 -0
  15. package/dist/controller/canonicalInput.d.ts +13 -4
  16. package/dist/controller/canonicalInput.js +52 -13
  17. package/dist/controller/canonicalInputTimer.d.ts +7 -0
  18. package/dist/controller/canonicalInputTimer.js +11 -0
  19. package/dist/controller/canonicalPublicOperations.d.ts +48 -0
  20. package/dist/controller/canonicalPublicOperations.js +556 -0
  21. package/dist/controller/canonicalSession.d.ts +24 -6
  22. package/dist/controller/canonicalSession.js +259 -111
  23. package/dist/controller/canonicalStoryEditor.d.ts +5 -3
  24. package/dist/controller/canonicalStoryEditor.js +7 -2
  25. package/dist/controller/canonicalStructure.js +57 -4
  26. package/dist/controller/canonicalTocSelection.d.ts +11 -0
  27. package/dist/controller/canonicalTocSelection.js +43 -0
  28. package/dist/controller/folioEditor.d.ts +2 -9
  29. package/dist/controller/folioEditor.js +5 -4
  30. package/dist/controller/headerFooterEditorManager.d.ts +2 -1
  31. package/dist/controller/headerFooterEditorManager.js +8 -7
  32. package/dist/controller/hiddenEditorApi.d.ts +12 -2
  33. package/dist/controller/hiddenEditorApi.js +4 -0
  34. package/dist/controller/hiddenEditorManager.d.ts +3 -1
  35. package/dist/controller/hiddenEditorManager.js +216 -34
  36. package/dist/controller/noteEditorManager.d.ts +2 -1
  37. package/dist/controller/noteEditorManager.js +8 -7
  38. package/dist/document-operations.d.ts +8 -1
  39. package/dist/document-operations.js +6 -4
  40. package/dist/docx/blockPlainText.d.ts +1 -1
  41. package/dist/docx/blockPlainText.js +11 -3
  42. package/dist/docx/canonicalResourceSave.d.ts +16 -0
  43. package/dist/docx/canonicalResourceSave.js +27 -0
  44. package/dist/docx/canonicalSave.d.ts +29 -0
  45. package/dist/docx/canonicalSave.js +101 -0
  46. package/dist/docx/canonicalStoryRepack.d.ts +1 -1
  47. package/dist/docx/canonicalStoryRepack.js +38 -5
  48. package/dist/docx/documentParser.d.ts +1 -13
  49. package/dist/docx/documentParser.js +1 -47
  50. package/dist/docx/ensureParaIds.js +10 -4
  51. package/dist/docx/footnoteParser.d.ts +1 -11
  52. package/dist/docx/footnoteParser.js +1 -16
  53. package/dist/docx/headerFooterParser.d.ts +1 -9
  54. package/dist/docx/headerFooterParser.js +1 -12
  55. package/dist/docx/headerFooterVerbatim.d.ts +26 -1
  56. package/dist/docx/headerFooterVerbatim.js +81 -3
  57. package/dist/docx/imageParser.d.ts +13 -1
  58. package/dist/docx/imageParser.js +20 -11
  59. package/dist/docx/metadataPrivacy.js +6 -1
  60. package/dist/docx/packageParts.js +9 -3
  61. package/dist/docx/paragraphPropertySource.js +2 -1
  62. package/dist/docx/parser.js +2 -1
  63. package/dist/docx/removeHeaderFooterParts.js +17 -6
  64. package/dist/docx/rezip.d.ts +49 -5
  65. package/dist/docx/rezip.js +745 -229
  66. package/dist/docx/saveDiagnostics.d.ts +11 -0
  67. package/dist/docx/selectiveSave.d.ts +2 -1
  68. package/dist/docx/selectiveSave.js +32 -3
  69. package/dist/docx/selectiveXmlPatch.js +7 -5
  70. package/dist/docx/serializer/documentSerializer.d.ts +9 -2
  71. package/dist/docx/serializer/documentSerializer.js +29 -10
  72. package/dist/docx/server/applyDocxXmlPatchProposal.js +14 -9
  73. package/dist/docx/storyBlockReplay.d.ts +21 -1
  74. package/dist/docx/storyBlockReplay.js +113 -9
  75. package/dist/docx/storyPlainText.d.ts +20 -0
  76. package/dist/docx/storyPlainText.js +23 -0
  77. package/dist/docx/structuralXmlPatch.js +10 -22
  78. package/dist/docx/styleParser.d.ts +5 -1
  79. package/dist/docx/styleParser.js +1 -1
  80. package/dist/docx/tableParser.d.ts +1 -8
  81. package/dist/docx/tableParser.js +1 -28
  82. package/dist/docx/textBoxParser.d.ts +1 -5
  83. package/dist/docx/textBoxParser.js +1 -20
  84. package/dist/docx/unzip.js +6 -1
  85. package/dist/internal/acceptedBlockProjection.d.ts +12 -0
  86. package/dist/internal/acceptedBlockProjection.js +45 -0
  87. package/dist/managers/DocumentLoaderManager.js +2 -1
  88. package/dist/markdown/escape.d.ts +3 -1
  89. package/dist/markdown/escape.js +9 -2
  90. package/dist/markdown/renderBlock.js +5 -120
  91. package/dist/markdown/renderParagraph.js +4 -2
  92. package/dist/markdown/renderRuns.d.ts +7 -1
  93. package/dist/markdown/renderRuns.js +32 -13
  94. package/dist/prosemirror/canonicalCommands.d.ts +28 -4
  95. package/dist/prosemirror/canonicalCommands.js +11 -4
  96. package/dist/prosemirror/canonicalSelectionRange.d.ts +9 -0
  97. package/dist/prosemirror/canonicalSelectionRange.js +15 -0
  98. package/dist/prosemirror/clipboardIntent.d.ts +7 -0
  99. package/dist/prosemirror/clipboardIntent.js +9 -0
  100. package/dist/prosemirror/commands/pageBreak.js +2 -2
  101. package/dist/prosemirror/commands/pastePlainText.js +2 -1
  102. package/dist/prosemirror/conversion/hyphenTextCarriers.d.ts +8 -0
  103. package/dist/prosemirror/conversion/hyphenTextCarriers.js +8 -0
  104. package/dist/prosemirror/conversion/toProseDoc.d.ts +7 -1
  105. package/dist/prosemirror/conversion/toProseDoc.js +5 -4
  106. package/dist/prosemirror/extensions/core/ParagraphExtension.js +39 -30
  107. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +56 -28
  108. package/dist/prosemirror/extensions/marks/markUtils.js +3 -1
  109. package/dist/prosemirror/insertOperations.js +2 -1
  110. package/dist/prosemirror/paragraphPropertyCarry.d.ts +7 -1
  111. package/dist/prosemirror/paragraphPropertyCarry.js +8 -3
  112. package/dist/prosemirror/plugins/suggestionMode.js +7 -5
  113. package/dist/types/canonicalCapabilities.d.ts +215 -0
  114. package/dist/types/canonicalCapabilities.js +212 -0
  115. package/dist/types/canonicalSave.d.ts +11 -0
  116. package/dist/types/canonicalSave.js +0 -0
  117. package/dist/types/docxSerialization.d.ts +14 -0
  118. package/dist/types/docxSerialization.js +7 -0
  119. package/package.json +4 -4
@@ -1,5 +1,7 @@
1
1
  import { consumeTrackedSectionEndpointRemoval } from "../internal/sectionEndpointResolution.js";
2
2
  import { consumeSectionReferenceResolution } from "../internal/sectionReferenceResolution.js";
3
+ import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
4
+ import { canonicalJson } from "../utils/canonicalJson.js";
3
5
  import { isAllowedExternalWatermarkImageUrl } from "../watermark/index.js";
4
6
  import { appVersionInSchemaForm, normalizeAppVersionInExtendedProperties } from "./appVersionNormalization.js";
5
7
  import { withoutOrphanCommentRanges } from "./commentRangeIntegrity.js";
@@ -9,6 +11,7 @@ import { readDocumentSectionFacts } from "./documentSectionFacts.js";
9
11
  import { normalizeDrawingIds } from "./drawingIdNormalization.js";
10
12
  import { rebindDrawingImageRelationship } from "./drawingRelationships.js";
11
13
  import { parseEndnotes, parseFootnotes } from "./footnoteParser.js";
14
+ import { getDocumentSourceBaseline } from "./headerFooterVerbatim.js";
12
15
  import { assertValidFolioDocumentModel } from "./modelValidation.js";
13
16
  import { isNewDataUrlDrawing } from "./newImage.js";
14
17
  import { missingNoteReferenceStyles, noteReferenceNeeds } from "./noteReferenceStyles.js";
@@ -16,12 +19,12 @@ import { parseNumbering } from "./numberingParser.js";
16
19
  import { paragraphNumberingReferenceId as paragraphNumberingReferenceId$1 } from "./numberingReference.js";
17
20
  import { isUnsafePackagePath, reconcilePackageReferences, removeUnsafeEntries } from "./packageParts.js";
18
21
  import { normalizeParaIdRangeInXmlParts } from "./paraIdRangeNormalization.js";
19
- import { cloneParagraphWithPropertySource } from "./paragraphPropertySource.js";
20
- import { BLOCK_TREE_DESCENT, visitBlockTreeRecords } from "./paragraphTraversal.js";
22
+ import { cloneDocumentWithParagraphPropertySources, cloneParagraphWithPropertySource, copyDocumentParagraphPropertySourceContract } from "./paragraphPropertySource.js";
23
+ import { BLOCK_TREE_DESCENT, visitBlockTreeRecords, visitInlineContentSlots, visitParagraphRuns } from "./paragraphTraversal.js";
21
24
  import { RELATIONSHIP_TYPES, parseRelationships, resolveRelativePath } from "./relsParser.js";
22
25
  import { removeResolvedHeaderFooterParts } from "./removeHeaderFooterParts.js";
23
26
  import { normalizeRevisionIdsInXmlParts } from "./revisionIdNormalization.js";
24
- import { buildPatchedNotePartXml, collectChangedNoteParaIds, collectParaIds, patchNumberingDefinitions } from "./selectiveXmlPatch.js";
27
+ import { buildPatchedNotePartXml, collectChangedNoteParaIds, collectParaIds, patchNumberingDefinitions, spliceXml } from "./selectiveXmlPatch.js";
25
28
  import { planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
26
29
  import { serializeDocument } from "./serializer/documentSerializer.js";
27
30
  import { serializeFontTableXml } from "./serializer/fontTableSerializer.js";
@@ -34,11 +37,14 @@ import { serializeStyle, serializeStylesXml } from "./serializer/stylesSerialize
34
37
  import { serializeThemeXml } from "./serializer/themeSerializer.js";
35
38
  import { updateEvenAndOddHeaders } from "./settingsHeaderFooterUpdate.js";
36
39
  import { parseSettings } from "./settingsParser.js";
40
+ import { getXmlSourceRange, parseStreamingXmlWithSourceRanges } from "./streamingXmlParser.js";
41
+ import { captureVerbatimXml } from "./verbatimCapture.js";
37
42
  import { hasCanonicalWordprocessingPrefixes } from "./wordprocessingPrefixes.js";
38
- import { WORDPROCESSINGML_NAMESPACE_URIS, findChild, findChildByNamespaceUri, findChildrenByNamespaceUri, getAttribute, getAttributeByNamespaceUri, getChildElements, getLocalName, getNamespaceUri, matchesName, parseXml, parseXmlDocument } from "./xmlParser.js";
39
- import { TaggedError, panic } from "better-result";
43
+ import { OFFICE_RELATIONSHIP_NAMESPACE_URIS, OOXML_NAMESPACE_SCOPE, WORDPROCESSINGML_NAMESPACE_URIS, findAttributeByNamespaceUri, findChild, findChildByNamespaceUri, findChildrenByNamespaceUri, getAttribute, getAttributeByNamespaceUri, getChildElements, getLocalName, getNamespaceUri, matchesName, parseXml, parseXmlDocument } from "./xmlParser.js";
44
+ import { Result, TaggedError, panic } from "better-result";
40
45
  import { assertValidOoxmlNumericIds, escapeXmlAttribute, escapeXmlText, validateDocxPackage } from "@stll/docx-core";
41
46
  import { mintRelationshipId } from "@stll/docx-core/model";
47
+ import { writeZipPart } from "@stll/docx-core/zip";
42
48
  import JSZip from "jszip";
43
49
  //#region src/docx/rezip.ts
44
50
  /**
@@ -200,9 +206,14 @@ async function serializeCommentsToZip(doc, zip, compressionLevel) {
200
206
  }
201
207
  const plan = planCommentParts(comments);
202
208
  const commentsXml = serializeComments(plan, sourceCommentsXml === void 0 ? void 0 : readRootNamespaceBindings(sourceCommentsXml));
203
- zip.file(sourceCommentsFile?.name ?? "word/comments.xml", commentsXml, {
204
- compression: "DEFLATE",
205
- compressionOptions: { level: compressionLevel }
209
+ writeZipPart({
210
+ zip,
211
+ path: sourceCommentsFile?.name ?? "word/comments.xml",
212
+ data: commentsXml,
213
+ options: {
214
+ compression: "DEFLATE",
215
+ compressionOptions: { level: compressionLevel }
216
+ }
206
217
  });
207
218
  await ensureCommentsContentType(zip, compressionLevel);
208
219
  await ensureCommentsRelationship(zip, compressionLevel);
@@ -231,9 +242,14 @@ async function syncCommentsExtendedPart(plan, zip, compressionLevel) {
231
242
  await transformPackagingFile(zip, "word/_rels/document.xml.rels", removeCommentsExtendedRelationship, compressionLevel);
232
243
  return;
233
244
  }
234
- zip.file(existing?.name ?? "word/commentsExtended.xml", xml, {
235
- compression: "DEFLATE",
236
- compressionOptions: { level: compressionLevel }
245
+ writeZipPart({
246
+ zip,
247
+ path: existing?.name ?? "word/commentsExtended.xml",
248
+ data: xml,
249
+ options: {
250
+ compression: "DEFLATE",
251
+ compressionOptions: { level: compressionLevel }
252
+ }
237
253
  });
238
254
  await transformPackagingFile(zip, "[Content_Types].xml", addCommentsExtendedOverride, compressionLevel);
239
255
  await transformPackagingFile(zip, "word/_rels/document.xml.rels", addCommentsExtendedRelationship, compressionLevel);
@@ -244,9 +260,14 @@ async function transformPackagingFile(zip, path, transform, compressionLevel) {
244
260
  if (!file) return;
245
261
  const xml = await file.async("text");
246
262
  const next = transform(xml);
247
- if (next !== xml) zip.file(path, next, {
248
- compression: "DEFLATE",
249
- compressionOptions: { level: compressionLevel }
263
+ if (next !== xml) writeZipPart({
264
+ zip,
265
+ path,
266
+ data: next,
267
+ options: {
268
+ compression: "DEFLATE",
269
+ compressionOptions: { level: compressionLevel }
270
+ }
250
271
  });
251
272
  }
252
273
  function findZipEntryCaseInsensitive(zip, lowerPath) {
@@ -255,7 +276,9 @@ function findZipEntryCaseInsensitive(zip, lowerPath) {
255
276
  for (const [path, file] of Object.entries(zip.files)) if (!file.dir && path.toLowerCase() === lowerPath) return file;
256
277
  return null;
257
278
  }
258
- const EMPTY_RELS_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"></Relationships>";
279
+ const PACKAGE_RELATIONSHIPS_NAMESPACE_URI = "http://schemas.openxmlformats.org/package/2006/relationships";
280
+ const EMPTY_RELS_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
281
+ <Relationships xmlns="${PACKAGE_RELATIONSHIPS_NAMESPACE_URI}"></Relationships>`;
259
282
  function headerFooterFilename(target) {
260
283
  return target.startsWith("/") ? target.slice(1) : `word/${target}`;
261
284
  }
@@ -352,9 +375,14 @@ async function registerImageExtensions(zip, extensions, compressionLevel) {
352
375
  changed = true;
353
376
  }
354
377
  if (!changed) return;
355
- zip.file("[Content_Types].xml", ctXml, {
356
- compression: "DEFLATE",
357
- compressionOptions: { level: compressionLevel }
378
+ writeZipPart({
379
+ zip,
380
+ path: "[Content_Types].xml",
381
+ data: ctXml,
382
+ options: {
383
+ compression: "DEFLATE",
384
+ compressionOptions: { level: compressionLevel }
385
+ }
358
386
  });
359
387
  }
360
388
  function collectNewImages(blocks) {
@@ -425,9 +453,14 @@ async function processNewImages(parts, zip, compressionLevel) {
425
453
  maxId++;
426
454
  const mediaPath = `word/media/${`image${maxImageNum}.${extension}`}`;
427
455
  const newRId = mintRelationshipId(maxId);
428
- zip.file(mediaPath, data, {
429
- compression: "DEFLATE",
430
- compressionOptions: { level: compressionLevel }
456
+ writeZipPart({
457
+ zip,
458
+ path: mediaPath,
459
+ data,
460
+ options: {
461
+ compression: "DEFLATE",
462
+ compressionOptions: { level: compressionLevel }
463
+ }
431
464
  });
432
465
  relEntries.push(`<Relationship Id="${newRId}" Type="${RELATIONSHIP_TYPES.image}" Target="${escapeXmlAttribute(relativeTargetForPart(partPath, mediaPath))}"/>`);
433
466
  extensionsAdded.add(extension);
@@ -438,13 +471,15 @@ async function processNewImages(parts, zip, compressionLevel) {
438
471
  }) ?? panic("A detached drawing lost its embedded image relationship");
439
472
  image.rId = newRId;
440
473
  }
441
- if (relEntries.length > 0) {
442
- const updatedRelsXml = relsXml.replace("</Relationships>", `${relEntries.join("")}</Relationships>`);
443
- zip.file(relsPath, updatedRelsXml, {
474
+ if (relEntries.length > 0) writeZipPart({
475
+ zip,
476
+ path: relsPath,
477
+ data: relsXml.replace("</Relationships>", `${relEntries.join("")}</Relationships>`),
478
+ options: {
444
479
  compression: "DEFLATE",
445
480
  compressionOptions: { level: compressionLevel }
446
- });
447
- }
481
+ }
482
+ });
448
483
  }
449
484
  await registerImageExtensions(zip, extensionsAdded, compressionLevel);
450
485
  }
@@ -456,67 +491,259 @@ const materializeEmbeddedMedia = async ({ document, zip, compressionLevel }) =>
456
491
  if (isUnsafePackagePath(media.path) || !media.path.startsWith("word/media/")) panic("Detached media has an invalid package path");
457
492
  const extension = media.path.split(".").at(-1)?.toLowerCase();
458
493
  if (!extension || !/^[a-z0-9]+$/u.test(extension)) panic("Embedded media has an invalid extension");
459
- zip.file(media.path, media.data, {
460
- compression: "DEFLATE",
461
- compressionOptions: { level: compressionLevel }
494
+ writeZipPart({
495
+ zip,
496
+ path: media.path,
497
+ data: media.data,
498
+ options: {
499
+ compression: "DEFLATE",
500
+ compressionOptions: { level: compressionLevel }
501
+ }
462
502
  });
463
503
  extensions.add(extension);
464
504
  }
465
505
  await registerImageExtensions(zip, extensions, compressionLevel);
466
506
  };
467
- /**
468
- * Collect all hyperlinks that have an href but no rId from block content.
469
- * These are newly created hyperlinks that need relationship entries.
470
- */
471
- function collectHyperlinksWithoutRId(blocks) {
472
- const hyperlinks = [];
473
- const collectInlineHyperlinks = (content) => {
474
- for (const item of content) switch (item.type) {
475
- case "hyperlink":
476
- if (item.href && !item.rId && !item.anchor) hyperlinks.push(item);
477
- break;
478
- case "simpleField":
479
- case "inlineSdt":
480
- case "inlineWrapper":
481
- case "insertion":
482
- case "deletion":
483
- case "moveFrom":
484
- case "moveTo":
485
- collectInlineHyperlinks(item.content);
486
- break;
487
- default: break;
507
+ var CanonicalImageRelationshipRefusalError = class extends TaggedError("CanonicalImageRelationshipRefusalError") {};
508
+ /** Publish allocated pictures into each owning story without rebinding canonical ids. */
509
+ const publishCanonicalImageResources = async ({ document, zip, compressionLevel }) => {
510
+ const baseline = getDocumentSourceBaseline(document);
511
+ for (const { relsPath, blocks } of collectDocxParts(document, zip)) {
512
+ const images = [];
513
+ visitBlockTreeRecords(blocks, (record) => {
514
+ if (record.type !== "paragraph") return BLOCK_TREE_DESCENT.descend;
515
+ visitParagraphRuns(record, (run) => {
516
+ for (const content of run.content) if (content.type === "drawing" && content.image.rId !== void 0) images.push(content.image);
517
+ });
518
+ return BLOCK_TREE_DESCENT.descend;
519
+ });
520
+ if (images.length === 0) continue;
521
+ let xml = await readRelsOrStub(zip, relsPath);
522
+ const local = parseRelationships(xml);
523
+ const partPath = relsPath.replace("/_rels/", "/").replace(/\.rels$/u, "");
524
+ let changed = false;
525
+ for (const image of images) {
526
+ const id = image.rId;
527
+ if (id === void 0) continue;
528
+ const refuse = (reason, message) => {
529
+ throw new CanonicalImageRelationshipRefusalError({
530
+ message,
531
+ gap: CANONICAL_GAP.save,
532
+ part: relsPath,
533
+ relationshipId: id,
534
+ reason
535
+ });
536
+ };
537
+ const source = local.get(id);
538
+ const allocated = document.package.relationships?.get(id);
539
+ const allocatedMediaPath = allocated?.targetMode === "External" || allocated?.type !== RELATIONSHIP_TYPES.image ? void 0 : resolveRelativePath("word/_rels/document.xml.rels", allocated.target);
540
+ const allocatedMedia = allocatedMediaPath === void 0 ? void 0 : document.package.media?.get(allocatedMediaPath);
541
+ const newlyAllocated = allocated?.type === RELATIONSHIP_TYPES.image && (baseline.type !== "captured" || canonicalJson(allocated) !== canonicalJson(baseline.resourceRelationships?.get(id)));
542
+ const desired = relsPath === "word/_rels/document.xml.rels" || newlyAllocated || source === void 0 && image.src !== void 0 && (allocated?.targetMode === "External" ? image.src === allocated.target : image.src === allocatedMedia?.dataUrl) ? allocated : source;
543
+ if (desired?.type !== RELATIONSHIP_TYPES.image) return refuse("unresolved", "A canonical inline picture has no owning image relationship.");
544
+ const desiredRelsPath = desired === allocated ? "word/_rels/document.xml.rels" : relsPath;
545
+ const absolute = desired.targetMode === "External" ? void 0 : resolveRelativePath(desiredRelsPath, desired.target);
546
+ if (desired.targetMode === "External" && !isAllowedExternalWatermarkImageUrl(desired.target)) return refuse("unsafeTarget", "A canonical inline picture has an unsafe external target.");
547
+ if (absolute !== void 0 && isUnsafePackagePath(absolute)) return refuse("unsafeTarget", "A canonical inline picture has an unsafe media target.");
548
+ if (source) {
549
+ const sourceAbsolute = source.targetMode === "External" ? void 0 : resolveRelativePath(relsPath, source.target);
550
+ if (source.type !== RELATIONSHIP_TYPES.image || source.targetMode === "External" !== (desired.targetMode === "External") || (absolute === void 0 ? source.target !== desired.target : sourceAbsolute !== absolute)) return refuse("conflict", "A canonical inline picture relationship conflicts with its owning source binding.");
551
+ }
552
+ if (absolute !== void 0 && !findZipEntryCaseInsensitive(zip, absolute.toLowerCase())) {
553
+ const media = document.package.media?.get(absolute);
554
+ if (!media) return refuse("missingMedia", "A canonical inline picture is missing its media bytes.");
555
+ if (isUnsafePackagePath(media.path) || media.path !== absolute) return refuse("unsafeTarget", "A canonical inline picture media path does not match its relationship.");
556
+ writeZipPart({
557
+ zip,
558
+ path: absolute,
559
+ data: media.data,
560
+ options: {
561
+ compression: "DEFLATE",
562
+ compressionOptions: { level: compressionLevel }
563
+ }
564
+ });
565
+ const extension = absolute.split(".").at(-1)?.toLowerCase();
566
+ if (!extension || !/^[a-z0-9]+$/u.test(extension)) return refuse("unsafeTarget", "A canonical inline picture media extension is invalid.");
567
+ await registerImageExtensions(zip, /* @__PURE__ */ new Set([extension]), compressionLevel);
568
+ }
569
+ if (source) continue;
570
+ const root = parseXmlDocument(xml);
571
+ const type = root !== null && getChildElements(root).some((element) => getAttribute(element, null, "Type")?.startsWith("http://purl.oclc.org/ooxml/officeDocument/relationships/")) ? "http://purl.oclc.org/ooxml/officeDocument/relationships/image" : RELATIONSHIP_TYPES.image;
572
+ const target = absolute === void 0 ? desired.target : relativeTargetForPart(partPath, absolute);
573
+ xml = appendCanonicalRelationship({
574
+ xml,
575
+ partPath: relsPath,
576
+ relationshipXml: `<Relationship Id="${escapeXmlAttribute(id)}" Type="${type}" Target="${escapeXmlAttribute(target)}"${desired.targetMode === "External" ? " TargetMode=\"External\"" : ""}/>`
577
+ });
578
+ local.set(id, {
579
+ id,
580
+ type: RELATIONSHIP_TYPES.image,
581
+ target,
582
+ ...desired.targetMode === "External" ? { targetMode: "External" } : {}
583
+ });
584
+ changed = true;
488
585
  }
489
- };
490
- for (const block of blocks) if (block.type === "paragraph") collectInlineHyperlinks(block.content);
491
- else if (block.type === "table") for (const row of block.rows) for (const cell of row.cells) hyperlinks.push(...collectHyperlinksWithoutRId(cell.content));
492
- else if (block.type === "blockSdt" || block.type === "blockCustomXml") hyperlinks.push(...collectHyperlinksWithoutRId(block.content));
493
- return hyperlinks;
586
+ if (changed) writeZipPart({
587
+ zip,
588
+ path: relsPath,
589
+ data: xml,
590
+ options: {
591
+ compression: "DEFLATE",
592
+ compressionOptions: { level: compressionLevel }
593
+ }
594
+ });
595
+ }
596
+ };
597
+ const collectExternalHyperlinkResources = (blocks) => {
598
+ const resources = [];
599
+ visitBlockTreeRecords(blocks, (record) => {
600
+ if (record.type !== "paragraph") return BLOCK_TREE_DESCENT.descend;
601
+ visitInlineContentSlots(record, ({ item }) => {
602
+ if (item.type === "hyperlink" && item.href && !item.anchor) resources.push({
603
+ type: "text",
604
+ hyperlink: item
605
+ });
606
+ });
607
+ visitParagraphRuns(record, (run) => {
608
+ for (const drawing of run.content) if (drawing.type === "drawing" && drawing.rawXmlMode === void 0 && drawing.image.hlinkHref) resources.push({
609
+ type: "drawing",
610
+ image: drawing.image,
611
+ drawing
612
+ });
613
+ });
614
+ return BLOCK_TREE_DESCENT.descend;
615
+ });
616
+ return resources;
617
+ };
618
+ const externalHyperlinkTarget = (resource) => {
619
+ switch (resource.type) {
620
+ case "text": return {
621
+ id: resource.hyperlink.rId,
622
+ href: resource.hyperlink.href
623
+ };
624
+ case "drawing": return {
625
+ id: resource.image.hlinkRId,
626
+ href: resource.image.hlinkHref
627
+ };
628
+ default: return resource;
629
+ }
630
+ };
631
+ /** Newly authored text links; the census shares the complete owning-part traversal. */
632
+ function collectHyperlinksWithoutRId(blocks) {
633
+ return collectExternalHyperlinkResources(blocks).flatMap((resource) => resource.type === "text" && !resource.hyperlink.rId ? [resource.hyperlink] : []);
494
634
  }
495
635
  /** The selective save boundary must use the same resource census as full repack. */
496
- const hasUnmaterializedInlineResources = (blocks) => collectNewImages(blocks).length > 0 || collectHyperlinksWithoutRId(blocks).length > 0;
497
- /**
498
- * Process newly created hyperlinks in every part: assign rIds and add
499
- * relationship entries to the owning part's rels. Mutates the hyperlinks' rId
500
- * fields in-place.
501
- */
636
+ const hasUnmaterializedInlineResources = (blocks) => collectNewImages(blocks).length > 0 || collectExternalHyperlinkResources(blocks).some((resource) => externalHyperlinkTarget(resource).id === void 0);
637
+ const matchesExternalHyperlink = (relation, href) => relation?.type === RELATIONSHIP_TYPES.hyperlink && relation.targetMode === "External" && relation.target === href;
638
+ /** Compare against the already loaded source ZIP, without mutating model or package. */
639
+ const hasUnmaterializedHyperlinkBindings = async (document, zip) => {
640
+ for (const { blocks, relsPath } of collectDocxParts(document, zip)) {
641
+ const resources = collectExternalHyperlinkResources(blocks);
642
+ if (resources.length === 0) continue;
643
+ const relationships = parseRelationships(await readRelsOrStub(zip, relsPath));
644
+ for (const resource of resources) {
645
+ const { id, href } = externalHyperlinkTarget(resource);
646
+ if (href && (id === void 0 || !matchesExternalHyperlink(relationships.get(id), href))) return true;
647
+ }
648
+ }
649
+ return false;
650
+ };
651
+ const rebindCapturedDrawingLink = ({ xml, previousId, nextId }) => {
652
+ const parsed = Result.try({
653
+ try: () => parseXml(xml, OOXML_NAMESPACE_SCOPE),
654
+ catch: () => new DocxPackageFidelityError("Cannot rebind invalid captured drawing hyperlink XML.")
655
+ });
656
+ if (parsed.isErr()) throw parsed.error;
657
+ let rebound = 0;
658
+ const visit = (element) => {
659
+ const attribute = findAttributeByNamespaceUri(element, OFFICE_RELATIONSHIP_NAMESPACE_URIS, "id");
660
+ if (attribute?.value === previousId && element.attributes !== void 0) {
661
+ element.attributes[attribute.name] = nextId;
662
+ rebound += 1;
663
+ }
664
+ for (const child of element.elements ?? []) visit(child);
665
+ };
666
+ visit(parsed.value);
667
+ if (rebound !== 1) throw new DocxPackageFidelityError("Captured drawing hyperlink must own exactly one click relationship.");
668
+ return (parsed.value.elements ?? []).map(captureVerbatimXml).join("");
669
+ };
670
+ /** Materialize each modeled hyperlink against the actual owning ZIP relationship table. */
502
671
  async function processNewHyperlinks(parts, zip, compressionLevel) {
503
672
  for (const { relsPath, blocks } of parts) {
504
- const newHyperlinks = collectHyperlinksWithoutRId(blocks);
505
- if (newHyperlinks.length === 0) continue;
673
+ const resources = collectExternalHyperlinkResources(blocks);
674
+ if (resources.length === 0) continue;
506
675
  const relsXml = await readRelsOrStub(zip, relsPath);
676
+ const relationships = parseRelationships(relsXml);
677
+ const root = parseXmlDocument(relsXml);
678
+ if (root?.name === void 0 || getLocalName(root.name) !== "Relationships" || getNamespaceUri(root) !== PACKAGE_RELATIONSHIPS_NAMESPACE_URI) throw new DocxPackageFidelityError("Cannot materialize hyperlinks into an invalid relationship part.");
679
+ const strictRelationshipNamespace = "http://purl.oclc.org/ooxml/officeDocument/relationships";
680
+ const relationshipType = getChildElements(root).some((child) => getAttribute(child, null, "Type")?.startsWith(`${strictRelationshipNamespace}/`)) ? `${strictRelationshipNamespace}/hyperlink` : RELATIONSHIP_TYPES.hyperlink;
681
+ const byTarget = /* @__PURE__ */ new Map();
682
+ for (const relation of relationships.values()) if (relation.type === RELATIONSHIP_TYPES.hyperlink && relation.targetMode === "External" && !byTarget.has(relation.target)) byTarget.set(relation.target, relation.id);
507
683
  let maxId = findMaxRId(relsXml);
508
684
  const relEntries = [];
509
- for (const hyperlink of newHyperlinks) {
510
- if (!hyperlink.href) continue;
511
- maxId++;
512
- const newRId = `rId${maxId}`;
513
- relEntries.push(`<Relationship Id="${newRId}" Type="${RELATIONSHIP_TYPES.hyperlink}" Target="${escapeXmlAttribute(hyperlink.href)}" TargetMode="External"/>`);
514
- hyperlink.rId = newRId;
685
+ for (const resource of resources) {
686
+ const { id, href } = externalHyperlinkTarget(resource);
687
+ if (!href || id !== void 0 && matchesExternalHyperlink(relationships.get(id), href)) continue;
688
+ let nextId = byTarget.get(href);
689
+ if (nextId === void 0) {
690
+ if (id !== void 0 && !relationships.has(id)) nextId = id;
691
+ else do {
692
+ maxId += 1;
693
+ nextId = `rId${maxId}`;
694
+ } while (relationships.has(nextId));
695
+ relationships.set(nextId, {
696
+ id: nextId,
697
+ type: RELATIONSHIP_TYPES.hyperlink,
698
+ target: href,
699
+ targetMode: "External"
700
+ });
701
+ byTarget.set(href, nextId);
702
+ relEntries.push({
703
+ type: "element",
704
+ name: `${root.name.slice(0, -13)}Relationship`,
705
+ attributes: {
706
+ Id: nextId,
707
+ Type: relationshipType,
708
+ Target: href,
709
+ TargetMode: "External"
710
+ }
711
+ });
712
+ }
713
+ switch (resource.type) {
714
+ case "text":
715
+ resource.hyperlink.rId = nextId;
716
+ break;
717
+ case "drawing": {
718
+ const source = resource.image.hlinkClickSource;
719
+ if (source !== void 0 && source.rId === id && id !== void 0) resource.image.hlinkClickSource = {
720
+ xml: rebindCapturedDrawingLink({
721
+ xml: source.xml,
722
+ previousId: id,
723
+ nextId
724
+ }),
725
+ rId: nextId
726
+ };
727
+ resource.image.hlinkRId = nextId;
728
+ delete resource.drawing.rawXml;
729
+ delete resource.drawing.rawImageFingerprint;
730
+ break;
731
+ }
732
+ default: panic(`Unsupported hyperlink resource ${JSON.stringify(resource)}`);
733
+ }
734
+ }
735
+ if (relEntries.length > 0) {
736
+ (root.elements ??= []).push(...relEntries);
737
+ writeZipPart({
738
+ zip,
739
+ path: relsPath,
740
+ data: captureVerbatimXml(root),
741
+ options: {
742
+ compression: "DEFLATE",
743
+ compressionOptions: { level: compressionLevel }
744
+ }
745
+ });
515
746
  }
516
- zip.file(relsPath, relsXml.replace("</Relationships>", `${relEntries.join("")}</Relationships>`), {
517
- compression: "DEFLATE",
518
- compressionOptions: { level: compressionLevel }
519
- });
520
747
  }
521
748
  }
522
749
  /**
@@ -541,9 +768,14 @@ const normalizePackageIdsInZip = async (zip, compressionLevel) => {
541
768
  const normalizedParts = normalizeParaIdRangeInXmlParts(normalizeRevisionIdsInXmlParts(xmlParts));
542
769
  for (const [path, xml] of normalizedParts) if (xml !== xmlParts.get(path)) {
543
770
  assertValidOoxmlNumericIds(xml, path);
544
- zip.file(path, xml, {
545
- compression: "DEFLATE",
546
- compressionOptions: { level: compressionLevel }
771
+ writeZipPart({
772
+ zip,
773
+ path,
774
+ data: xml,
775
+ options: {
776
+ compression: "DEFLATE",
777
+ compressionOptions: { level: compressionLevel }
778
+ }
547
779
  });
548
780
  }
549
781
  };
@@ -558,9 +790,14 @@ const normalizeAppVersionInZip = async (zip, compressionLevel) => {
558
790
  if (!extendedProperties) return;
559
791
  const xml = await extendedProperties.async("text");
560
792
  const normalized = normalizeAppVersionInExtendedProperties(xml);
561
- if (normalized !== xml) zip.file(EXTENDED_PROPERTIES_PATH, normalized, {
562
- compression: "DEFLATE",
563
- compressionOptions: { level: compressionLevel }
793
+ if (normalized !== xml) writeZipPart({
794
+ zip,
795
+ path: EXTENDED_PROPERTIES_PATH,
796
+ data: normalized,
797
+ options: {
798
+ compression: "DEFLATE",
799
+ compressionOptions: { level: compressionLevel }
800
+ }
564
801
  });
565
802
  };
566
803
  /**
@@ -639,20 +876,37 @@ const normalizeExportDrawingIds = ({ package: docxPackage }) => {
639
876
  ...docxPackage.endnotes !== void 0 ? { endnotes: docxPackage.endnotes } : {}
640
877
  });
641
878
  };
642
- const finishRepack = async ({ document, originalZip, outputZip, originalDocument, originalCorePropertiesXml, compressionLevel, updateModifiedDate, modifiedBy, changedNoteParaIds, onDiagnostic, sectionEndpointRemoval, sectionReferenceRemovals }) => {
643
- document = await materializeNewHeaderFooterParts(document, outputZip, compressionLevel);
879
+ /** Keep export mutations off authority while retaining the source-buffer cache key. */
880
+ const cloneExportDocument = (sourceDocument) => {
881
+ const { originalBuffer, ...graph } = sourceDocument;
882
+ const document = cloneDocumentWithParagraphPropertySources(graph);
883
+ if (originalBuffer !== void 0) document.originalBuffer = originalBuffer;
884
+ copyDocumentParagraphPropertySourceContract(document, sourceDocument);
885
+ return document;
886
+ };
887
+ const finishRepack = async ({ document: sourceDocument, originalZip, outputZip, originalDocument, originalCorePropertiesXml, compressionLevel, updateModifiedDate, modifiedBy, changedNoteParaIds, onDiagnostic, bodyAuthority, sectionEndpointRemoval, sectionReferenceRemovals }) => {
888
+ const document = await materializeNewHeaderFooterParts(cloneExportDocument(sourceDocument), outputZip, compressionLevel);
644
889
  await materializeEmbeddedMedia({
645
890
  document,
646
891
  zip: outputZip,
647
892
  compressionLevel
648
893
  });
894
+ if (bodyAuthority === "canonical") await publishCanonicalImageResources({
895
+ document,
896
+ zip: outputZip,
897
+ compressionLevel
898
+ });
649
899
  const parts = collectDocxParts(document, outputZip);
650
900
  normalizeExportDrawingIds(document);
651
901
  await processNewImages(parts, outputZip, compressionLevel);
652
902
  await processNewHyperlinks(parts, outputZip, compressionLevel);
653
903
  assertValidFolioDocumentModel(document, "Cannot repack invalid DOCX document model");
654
904
  applyReplyThreadMarkers(document);
655
- const documentXml = serializeDocument(document, originalDocument === void 0 ? void 0 : readRootNamespaceBindings(originalDocument.xml));
905
+ const documentXml = serializeDocument(document, originalDocument === void 0 ? void 0 : readRootNamespaceBindings(originalDocument.xml), {
906
+ xml: originalDocument?.xml,
907
+ onDiagnostic,
908
+ bodyAuthority
909
+ });
656
910
  if (originalDocument?.xml) assertDocumentPackageFidelity({
657
911
  originalDocumentFacts: originalDocument.sectionFacts(),
658
912
  serializedDocumentXml: documentXml,
@@ -666,11 +920,21 @@ const finishRepack = async ({ document, originalZip, outputZip, originalDocument
666
920
  removedReferences: [...sectionReferenceRemovals ?? [], ...sectionEndpointRemoval?.removedReferences ?? []],
667
921
  compressionLevel
668
922
  });
669
- outputZip.file("word/document.xml", documentXml, {
670
- compression: "DEFLATE",
671
- compressionOptions: { level: compressionLevel }
923
+ writeZipPart({
924
+ zip: outputZip,
925
+ path: "word/document.xml",
926
+ data: documentXml,
927
+ options: {
928
+ compression: "DEFLATE",
929
+ compressionOptions: { level: compressionLevel }
930
+ }
931
+ });
932
+ await rebindWatermarkRelIds({
933
+ document,
934
+ zip: outputZip,
935
+ compressionLevel,
936
+ bodyAuthority
672
937
  });
673
- await rebindWatermarkRelIds(document, outputZip, compressionLevel);
674
938
  await serializeHeadersFootersToZip({
675
939
  doc: document,
676
940
  zip: outputZip,
@@ -689,16 +953,18 @@ const finishRepack = async ({ document, originalZip, outputZip, originalDocument
689
953
  await serializeHeaderFooterSettingsIntoZip(document, outputZip, compressionLevel);
690
954
  await serializeCommentsToZip(document, outputZip, compressionLevel);
691
955
  await dropAttachedTemplateReference(outputZip, compressionLevel);
692
- if (updateModifiedDate && originalCorePropertiesXml) {
693
- const updatedCoreProperties = updateCoreProperties(originalCorePropertiesXml, {
956
+ if (updateModifiedDate && originalCorePropertiesXml) writeZipPart({
957
+ zip: outputZip,
958
+ path: "docProps/core.xml",
959
+ data: updateCoreProperties(originalCorePropertiesXml, {
694
960
  updateModifiedDate,
695
961
  ...modifiedBy !== void 0 ? { modifiedBy } : {}
696
- });
697
- outputZip.file("docProps/core.xml", updatedCoreProperties, {
962
+ }),
963
+ options: {
698
964
  compression: "DEFLATE",
699
965
  compressionOptions: { level: compressionLevel }
700
- });
701
- }
966
+ }
967
+ });
702
968
  return generateDocxZip(outputZip, compressionLevel);
703
969
  };
704
970
  async function repackDocxWithSectionEndpointRemoval({ document: doc, options, sectionEndpointRemoval, sectionReferenceRemovals }) {
@@ -718,6 +984,7 @@ async function repackDocxWithSectionEndpointRemoval({ document: doc, options, se
718
984
  compressionLevel,
719
985
  updateModifiedDate,
720
986
  onDiagnostic: options.onDiagnostic,
987
+ bodyAuthority: options.bodyAuthority,
721
988
  ...modifiedBy !== void 0 ? { modifiedBy } : {},
722
989
  ...changedNoteParaIds !== void 0 ? { changedNoteParaIds } : {},
723
990
  ...sectionEndpointRemoval !== void 0 ? { sectionEndpointRemoval } : {},
@@ -744,18 +1011,30 @@ function repackDocx(doc, options = {}) {
744
1011
  */
745
1012
  async function repackDocxFromRaw(doc, rawContent, options = {}) {
746
1013
  const { compressionLevel = 6, updateModifiedDate = true, modifiedBy, changedNoteParaIds } = options;
747
- let exportDocument = withoutOrphanCommentRanges(doc);
1014
+ let exportDocument = withoutOrphanCommentRanges(cloneExportDocument(doc));
748
1015
  const newZip = new JSZip();
749
1016
  for (const [path, file] of Object.entries(rawContent.originalZip.files)) {
750
1017
  if (file.dir) {
751
- newZip.folder(path.replace(/\/$/u, ""));
1018
+ writeZipPart({
1019
+ zip: newZip,
1020
+ path,
1021
+ data: "",
1022
+ options: {
1023
+ dir: true,
1024
+ date: file.date
1025
+ }
1026
+ });
752
1027
  continue;
753
1028
  }
754
1029
  if (isUnsafePackagePath(path)) continue;
755
- const content = await file.async("arraybuffer");
756
- newZip.file(path, content, {
757
- compression: "DEFLATE",
758
- compressionOptions: { level: compressionLevel }
1030
+ writeZipPart({
1031
+ zip: newZip,
1032
+ path,
1033
+ data: await file.async("arraybuffer"),
1034
+ options: {
1035
+ compression: "DEFLATE",
1036
+ compressionOptions: { level: compressionLevel }
1037
+ }
759
1038
  });
760
1039
  }
761
1040
  exportDocument = await materializeNewHeaderFooterParts(exportDocument, newZip, compressionLevel);
@@ -764,23 +1043,42 @@ async function repackDocxFromRaw(doc, rawContent, options = {}) {
764
1043
  zip: newZip,
765
1044
  compressionLevel
766
1045
  });
1046
+ if (options.bodyAuthority === "canonical") await publishCanonicalImageResources({
1047
+ document: exportDocument,
1048
+ zip: newZip,
1049
+ compressionLevel
1050
+ });
767
1051
  const parts = collectDocxParts(exportDocument, newZip);
768
1052
  normalizeExportDrawingIds(exportDocument);
769
1053
  await processNewImages(parts, newZip, compressionLevel);
770
1054
  await processNewHyperlinks(parts, newZip, compressionLevel);
771
1055
  assertValidFolioDocumentModel(exportDocument, "Cannot repack invalid DOCX document model");
772
1056
  applyReplyThreadMarkers(exportDocument);
773
- const documentXml = serializeDocument(exportDocument, rawContent.documentXml ? readRootNamespaceBindings(rawContent.documentXml) : void 0);
1057
+ const documentXml = serializeDocument(exportDocument, rawContent.documentXml ? readRootNamespaceBindings(rawContent.documentXml) : void 0, {
1058
+ xml: rawContent.documentXml ?? void 0,
1059
+ onDiagnostic: options.onDiagnostic,
1060
+ bodyAuthority: options.bodyAuthority
1061
+ });
774
1062
  if (rawContent.documentXml) assertDocumentPackageFidelity({
775
1063
  originalDocumentFacts: readDocumentSectionFacts(rawContent.documentXml),
776
1064
  serializedDocumentXml: documentXml,
777
1065
  doc: exportDocument
778
1066
  });
779
- newZip.file("word/document.xml", documentXml, {
780
- compression: "DEFLATE",
781
- compressionOptions: { level: compressionLevel }
1067
+ writeZipPart({
1068
+ zip: newZip,
1069
+ path: "word/document.xml",
1070
+ data: documentXml,
1071
+ options: {
1072
+ compression: "DEFLATE",
1073
+ compressionOptions: { level: compressionLevel }
1074
+ }
1075
+ });
1076
+ await rebindWatermarkRelIds({
1077
+ document: exportDocument,
1078
+ zip: newZip,
1079
+ compressionLevel,
1080
+ bodyAuthority: options.bodyAuthority
782
1081
  });
783
- await rebindWatermarkRelIds(exportDocument, newZip, compressionLevel);
784
1082
  await serializeHeadersFootersToZip({
785
1083
  doc: exportDocument,
786
1084
  zip: newZip,
@@ -799,16 +1097,18 @@ async function repackDocxFromRaw(doc, rawContent, options = {}) {
799
1097
  await serializeAddedStylesIntoZip(exportDocument, rawContent.originalZip, newZip, compressionLevel);
800
1098
  await serializeCommentsToZip(exportDocument, newZip, compressionLevel);
801
1099
  await dropAttachedTemplateReference(newZip, compressionLevel);
802
- if (updateModifiedDate && rawContent.corePropsXml) {
803
- const updatedCoreProps = updateCoreProperties(rawContent.corePropsXml, {
1100
+ if (updateModifiedDate && rawContent.corePropsXml) writeZipPart({
1101
+ zip: newZip,
1102
+ path: "docProps/core.xml",
1103
+ data: updateCoreProperties(rawContent.corePropsXml, {
804
1104
  updateModifiedDate,
805
1105
  ...modifiedBy !== void 0 ? { modifiedBy } : {}
806
- });
807
- newZip.file("docProps/core.xml", updatedCoreProps, {
1106
+ }),
1107
+ options: {
808
1108
  compression: "DEFLATE",
809
1109
  compressionOptions: { level: compressionLevel }
810
- });
811
- }
1110
+ }
1111
+ });
812
1112
  return generateDocxZip(newZip, compressionLevel);
813
1113
  }
814
1114
  const COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
@@ -897,13 +1197,23 @@ async function dropAttachedTemplateReference(zip, compressionLevel) {
897
1197
  if (!settingsFile) return;
898
1198
  const relsFile = zip.file(SETTINGS_RELS_PART);
899
1199
  const filtered = withoutAttachedTemplate(await settingsFile.async("text"), await relsFile?.async("text"));
900
- if (filtered.settingsXml !== void 0) zip.file(SETTINGS_PART, filtered.settingsXml, {
901
- compression: "DEFLATE",
902
- compressionOptions: { level: compressionLevel }
1200
+ if (filtered.settingsXml !== void 0) writeZipPart({
1201
+ zip,
1202
+ path: SETTINGS_PART,
1203
+ data: filtered.settingsXml,
1204
+ options: {
1205
+ compression: "DEFLATE",
1206
+ compressionOptions: { level: compressionLevel }
1207
+ }
903
1208
  });
904
- if (filtered.relsXml !== void 0) zip.file(SETTINGS_RELS_PART, filtered.relsXml, {
905
- compression: "DEFLATE",
906
- compressionOptions: { level: compressionLevel }
1209
+ if (filtered.relsXml !== void 0) writeZipPart({
1210
+ zip,
1211
+ path: SETTINGS_RELS_PART,
1212
+ data: filtered.relsXml,
1213
+ options: {
1214
+ compression: "DEFLATE",
1215
+ compressionOptions: { level: compressionLevel }
1216
+ }
907
1217
  });
908
1218
  }
909
1219
  /**
@@ -916,9 +1226,14 @@ async function ensureCommentsContentType(zip, compressionLevel) {
916
1226
  let ctXml = await ctFile.async("text");
917
1227
  if (ctXml.includes("/word/comments.xml")) return;
918
1228
  ctXml = ctXml.replace("</Types>", `<Override PartName="/word/comments.xml" ContentType="${COMMENTS_CONTENT_TYPE}"/></Types>`);
919
- zip.file("[Content_Types].xml", ctXml, {
920
- compression: "DEFLATE",
921
- compressionOptions: { level: compressionLevel }
1229
+ writeZipPart({
1230
+ zip,
1231
+ path: "[Content_Types].xml",
1232
+ data: ctXml,
1233
+ options: {
1234
+ compression: "DEFLATE",
1235
+ compressionOptions: { level: compressionLevel }
1236
+ }
922
1237
  });
923
1238
  }
924
1239
  /**
@@ -933,9 +1248,14 @@ async function ensureCommentsRelationship(zip, compressionLevel) {
933
1248
  if (relsXml.includes("comments.xml")) return;
934
1249
  const newRId = `rId${findMaxRId(relsXml) + 1}`;
935
1250
  relsXml = relsXml.replace("</Relationships>", `<Relationship Id="${newRId}" Type="${RELATIONSHIP_TYPES.comments}" Target="comments.xml"/></Relationships>`);
936
- zip.file(relsPath, relsXml, {
937
- compression: "DEFLATE",
938
- compressionOptions: { level: compressionLevel }
1251
+ writeZipPart({
1252
+ zip,
1253
+ path: relsPath,
1254
+ data: relsXml,
1255
+ options: {
1256
+ compression: "DEFLATE",
1257
+ compressionOptions: { level: compressionLevel }
1258
+ }
939
1259
  });
940
1260
  }
941
1261
  /**
@@ -949,9 +1269,14 @@ async function ensureCommentsRelationship(zip, compressionLevel) {
949
1269
  async function updateDocumentXml(originalBuffer, newDocumentXml, options = {}) {
950
1270
  const { compressionLevel = 6 } = options;
951
1271
  const zip = await JSZip.loadAsync(originalBuffer);
952
- zip.file("word/document.xml", newDocumentXml, {
953
- compression: "DEFLATE",
954
- compressionOptions: { level: compressionLevel }
1272
+ writeZipPart({
1273
+ zip,
1274
+ path: "word/document.xml",
1275
+ data: newDocumentXml,
1276
+ options: {
1277
+ compression: "DEFLATE",
1278
+ compressionOptions: { level: compressionLevel }
1279
+ }
955
1280
  });
956
1281
  return zip.generateAsync({
957
1282
  type: "arraybuffer",
@@ -971,9 +1296,14 @@ async function updateDocumentXml(originalBuffer, newDocumentXml, options = {}) {
971
1296
  async function updateXmlFile(originalBuffer, path, content, options = {}) {
972
1297
  const { compressionLevel = 6 } = options;
973
1298
  const zip = await JSZip.loadAsync(originalBuffer);
974
- zip.file(path, content, {
975
- compression: "DEFLATE",
976
- compressionOptions: { level: compressionLevel }
1299
+ writeZipPart({
1300
+ zip,
1301
+ path,
1302
+ data: content,
1303
+ options: {
1304
+ compression: "DEFLATE",
1305
+ compressionOptions: { level: compressionLevel }
1306
+ }
977
1307
  });
978
1308
  return zip.generateAsync({
979
1309
  type: "arraybuffer",
@@ -1004,9 +1334,14 @@ async function updateMultipleFiles(originalBuffer, updates, options = {}) {
1004
1334
  */
1005
1335
  async function applyUpdatesToZip(zip, updates, options = {}) {
1006
1336
  const { compressionLevel = 6 } = options;
1007
- for (const [path, content] of updates) zip.file(path, content, {
1008
- compression: "DEFLATE",
1009
- compressionOptions: { level: compressionLevel }
1337
+ for (const [path, content] of updates) writeZipPart({
1338
+ zip,
1339
+ path,
1340
+ data: content,
1341
+ options: {
1342
+ compression: "DEFLATE",
1343
+ compressionOptions: { level: compressionLevel }
1344
+ }
1010
1345
  });
1011
1346
  await normalizePackageOnSave(zip, compressionLevel);
1012
1347
  return await zip.generateAsync({
@@ -1031,8 +1366,11 @@ async function addRelationship(originalBuffer, relationship) {
1031
1366
  const newRId = `rId${findMaxRId(relsXml) + 1}`;
1032
1367
  const targetModeAttr = relationship.targetMode === "External" ? " TargetMode=\"External\"" : "";
1033
1368
  const newRelElement = `<Relationship Id="${newRId}" Type="${relationship.type}" Target="${escapeXmlAttribute(relationship.target)}"${targetModeAttr}/>`;
1034
- const updatedRelsXml = relsXml.replace("</Relationships>", `${newRelElement}</Relationships>`);
1035
- zip.file(relsPath, updatedRelsXml);
1369
+ writeZipPart({
1370
+ zip,
1371
+ path: relsPath,
1372
+ data: relsXml.replace("</Relationships>", `${newRelElement}</Relationships>`)
1373
+ });
1036
1374
  return {
1037
1375
  buffer: await zip.generateAsync({
1038
1376
  type: "arraybuffer",
@@ -1054,7 +1392,11 @@ async function addRelationship(originalBuffer, relationship) {
1054
1392
  async function addMedia(originalBuffer, filename, data, mimeType) {
1055
1393
  const zip = await JSZip.loadAsync(originalBuffer);
1056
1394
  const mediaPath = `word/media/${filename}`;
1057
- zip.file(mediaPath, data);
1395
+ writeZipPart({
1396
+ zip,
1397
+ path: mediaPath,
1398
+ data
1399
+ });
1058
1400
  const relResult = await addRelationship(await zip.generateAsync({ type: "arraybuffer" }), {
1059
1401
  type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
1060
1402
  target: `media/${filename}`
@@ -1067,7 +1409,11 @@ async function addMedia(originalBuffer, filename, data, mimeType) {
1067
1409
  const extensionElement = `<Default Extension="${extension}" ContentType="${getContentTypeForExtension(extension, mimeType)}"/>`;
1068
1410
  const updatedContentTypes = contentTypesXml.replace("</Types>", `${extensionElement}</Types>`);
1069
1411
  const finalZip = await JSZip.loadAsync(relResult.buffer);
1070
- finalZip.file("[Content_Types].xml", updatedContentTypes);
1412
+ writeZipPart({
1413
+ zip: finalZip,
1414
+ path: "[Content_Types].xml",
1415
+ data: updatedContentTypes
1416
+ });
1071
1417
  return {
1072
1418
  buffer: await finalZip.generateAsync({
1073
1419
  type: "arraybuffer",
@@ -1213,17 +1559,27 @@ async function materializeNewHeaderFooterParts(doc, zip, compressionLevel) {
1213
1559
  materialize(doc.package.headers, RELATIONSHIP_TYPES.header, "header", HEADER_CONTENT_TYPE, true);
1214
1560
  materialize(doc.package.footers, RELATIONSHIP_TYPES.footer, "footer", FOOTER_CONTENT_TYPE, false);
1215
1561
  const compressionOptions = { level: compressionLevel };
1216
- if (relEntries.length > 0) zip.file(relsPath, relsXml.replace("</Relationships>", `${relEntries.join("")}</Relationships>`), {
1217
- compression: "DEFLATE",
1218
- compressionOptions
1562
+ if (relEntries.length > 0) writeZipPart({
1563
+ zip,
1564
+ path: relsPath,
1565
+ data: relsXml.replace("</Relationships>", `${relEntries.join("")}</Relationships>`),
1566
+ options: {
1567
+ compression: "DEFLATE",
1568
+ compressionOptions
1569
+ }
1219
1570
  });
1220
1571
  const ctFile = zip.file("[Content_Types].xml");
1221
1572
  if (ctFile) {
1222
1573
  const original = await ctFile.async("text");
1223
1574
  const updated = registerContentTypeParts(original, contentTypeParts);
1224
- if (updated !== original) zip.file("[Content_Types].xml", updated, {
1225
- compression: "DEFLATE",
1226
- compressionOptions
1575
+ if (updated !== original) writeZipPart({
1576
+ zip,
1577
+ path: "[Content_Types].xml",
1578
+ data: updated,
1579
+ options: {
1580
+ compression: "DEFLATE",
1581
+ compressionOptions
1582
+ }
1227
1583
  });
1228
1584
  }
1229
1585
  return doc;
@@ -1242,25 +1598,46 @@ function hasModelDrivenPictureWatermark(doc) {
1242
1598
  for (const hf of headers.values()) if (hf.watermark?.kind === "picture" && !hf.rawWatermarkXml) return true;
1243
1599
  return false;
1244
1600
  }
1245
- /**
1246
- * Rebind each picture watermark's `imageRId` so it resolves in its own header
1247
- * part's rels. A watermark propagated across headers (or onto a header created
1248
- * by coverage) carries the source header's rId, which is meaningless in a
1249
- * sibling header's `word/_rels/header*.xml.rels`. Per header: keep the rId if
1250
- * it already resolves; otherwise reuse an existing relationship to the same
1251
- * media target, or mint a new one. Media bytes are materialized before this step. Raw watermark XML
1252
- * changes only when its image relationship must be rebound.
1253
- */
1254
- async function rebindWatermarkRelIds(doc, zip, compressionLevel) {
1601
+ const appendCanonicalRelationship = ({ xml, relationshipXml, partPath }) => {
1602
+ const parsed = parseStreamingXmlWithSourceRanges(xml);
1603
+ if (parsed.status !== "parsed") throw new DocxPackageFidelityError(`Cannot read canonical relationships in ${partPath}`);
1604
+ const root = getChildElements(parsed.value).find((element) => getLocalName(element.name ?? "") === "Relationships" && getNamespaceUri(element) === PACKAGE_RELATIONSHIPS_NAMESPACE_URI);
1605
+ const range = root ? getXmlSourceRange(root) : void 0;
1606
+ if (!root?.name || !range) throw new DocxPackageFidelityError(`Cannot find canonical relationship root in ${partPath}`);
1607
+ const sourceRoot = xml.slice(range.start, range.end);
1608
+ const bound = relationshipXml.replace("<Relationship ", `<Relationship xmlns="${PACKAGE_RELATIONSHIPS_NAMESPACE_URI}" `);
1609
+ const selfClosing = sourceRoot.endsWith("/>");
1610
+ const close = xml.lastIndexOf("</", range.end);
1611
+ if (!selfClosing && close < range.start) throw new DocxPackageFidelityError(`Cannot extend canonical relationships in ${partPath}`);
1612
+ const result = spliceXml(xml, [selfClosing ? {
1613
+ start: range.end - 2,
1614
+ end: range.end,
1615
+ newXml: ">" + bound + `</${root.name}>`
1616
+ } : {
1617
+ start: close,
1618
+ end: close,
1619
+ newXml: bound
1620
+ }]);
1621
+ if (result === null) throw new DocxPackageFidelityError(`Cannot splice canonical relationships in ${partPath}`);
1622
+ return result;
1623
+ };
1624
+ async function rebindWatermarkRelIds({ document: doc, zip, compressionLevel, bodyAuthority }) {
1255
1625
  const rels = doc.package.relationships;
1256
1626
  const headers = doc.package.headers;
1257
- if (!rels || !headers) return;
1627
+ if (!headers) return;
1628
+ if (!rels) {
1629
+ if (bodyAuthority === "canonical" && [...headers.values()].some(({ watermark }) => watermark?.kind === "picture")) throw new DocxPackageFidelityError("A canonical picture watermark has no header relationship map");
1630
+ return;
1631
+ }
1258
1632
  const pending = [];
1259
1633
  for (const [rId, hf] of headers) {
1260
1634
  const watermark = hf.watermark;
1261
1635
  if (!watermark || watermark.kind !== "picture") continue;
1262
1636
  const rel = rels.get(rId);
1263
- if (!rel?.target) continue;
1637
+ if (!rel?.target) {
1638
+ if (bodyAuthority === "canonical") throw new DocxPackageFidelityError(`A canonical picture watermark has no header target for ${rId}`);
1639
+ continue;
1640
+ }
1264
1641
  pending.push({
1265
1642
  watermark,
1266
1643
  header: hf,
@@ -1293,20 +1670,37 @@ async function rebindWatermarkRelIds(doc, zip, compressionLevel) {
1293
1670
  const changedPaths = /* @__PURE__ */ new Set();
1294
1671
  for (const { watermark, header, relsPath, partPath } of pending) {
1295
1672
  let canonical;
1296
- if (watermark.imageTarget !== void 0) if (watermark.imageTargetExternal) canonical = isAllowedExternalWatermarkImageUrl(watermark.imageTarget) ? {
1297
- mode: "external",
1298
- url: watermark.imageTarget
1299
- } : resolveCanonical(watermark.imageRId);
1300
- else canonical = {
1673
+ if (watermark.imageTarget !== void 0) if (watermark.imageTargetExternal) {
1674
+ if (bodyAuthority === "canonical" && !isAllowedExternalWatermarkImageUrl(watermark.imageTarget)) throw new DocxPackageFidelityError("A canonical picture watermark has an unsafe external target");
1675
+ canonical = isAllowedExternalWatermarkImageUrl(watermark.imageTarget) ? {
1676
+ mode: "external",
1677
+ url: watermark.imageTarget
1678
+ } : resolveCanonical(watermark.imageRId);
1679
+ } else canonical = {
1301
1680
  mode: "internal",
1302
1681
  absolute: watermark.imageTarget
1303
1682
  };
1304
1683
  else canonical = resolveCanonical(watermark.imageRId);
1305
- if (!canonical) continue;
1684
+ if (!canonical) {
1685
+ if (bodyAuthority === "canonical") throw new DocxPackageFidelityError("A canonical picture watermark image cannot be resolved");
1686
+ continue;
1687
+ }
1688
+ if (bodyAuthority === "canonical" && canonical.mode === "internal" && !zip.file(canonical.absolute)) throw new DocxPackageFidelityError("A canonical picture watermark image part is missing");
1306
1689
  const relsXml = relsXmlByPath.get(relsPath) ?? EMPTY_RELS_XML;
1307
1690
  const localRels = parseRelationships(relsXml);
1308
1691
  const local = relImage(localRels.get(watermark.imageRId), relsPath);
1309
1692
  if (local && sameImage(local, canonical)) continue;
1693
+ if (bodyAuthority === "canonical") {
1694
+ if (localRels.has(watermark.imageRId)) throw new DocxPackageFidelityError(`A canonical picture watermark relationship ${watermark.imageRId} has a conflicting target`);
1695
+ const relXml = canonical.mode === "external" ? `<Relationship Id="${escapeXmlAttribute(watermark.imageRId)}" Type="${RELATIONSHIP_TYPES.image}" Target="${escapeXmlAttribute(canonical.url)}" TargetMode="External"/>` : `<Relationship Id="${escapeXmlAttribute(watermark.imageRId)}" Type="${RELATIONSHIP_TYPES.image}" Target="${escapeXmlAttribute(relativeTargetForPart(partPath, canonical.absolute))}"/>`;
1696
+ relsXmlByPath.set(relsPath, appendCanonicalRelationship({
1697
+ xml: relsXml,
1698
+ relationshipXml: relXml,
1699
+ partPath: relsPath
1700
+ }));
1701
+ changedPaths.add(relsPath);
1702
+ continue;
1703
+ }
1310
1704
  let resolvedRId;
1311
1705
  for (const [id, rel] of localRels) {
1312
1706
  const image = relImage(rel, relsPath);
@@ -1331,9 +1725,14 @@ async function rebindWatermarkRelIds(doc, zip, compressionLevel) {
1331
1725
  const compressionOptions = { level: compressionLevel };
1332
1726
  for (const path of changedPaths) {
1333
1727
  const xml = relsXmlByPath.get(path);
1334
- if (xml) zip.file(path, xml, {
1335
- compression: "DEFLATE",
1336
- compressionOptions
1728
+ if (xml) writeZipPart({
1729
+ zip,
1730
+ path,
1731
+ data: xml,
1732
+ options: {
1733
+ compression: "DEFLATE",
1734
+ compressionOptions
1735
+ }
1337
1736
  });
1338
1737
  }
1339
1738
  }
@@ -1373,9 +1772,14 @@ async function serializeHeadersFootersToZip({ doc, zip, compressionLevel, onDiag
1373
1772
  for (const [filename, xml] of await collectHeaderFooterUpdates(doc, {
1374
1773
  sourceZip: zip,
1375
1774
  onDiagnostic
1376
- })) zip.file(filename, xml, {
1377
- compression: "DEFLATE",
1378
- compressionOptions
1775
+ })) writeZipPart({
1776
+ zip,
1777
+ path: filename,
1778
+ data: xml,
1779
+ options: {
1780
+ compression: "DEFLATE",
1781
+ compressionOptions
1782
+ }
1379
1783
  });
1380
1784
  }
1381
1785
  async function serializeNotesToZip({ doc, originalZip, newZip, compressionLevel, changedNoteParaIds }) {
@@ -1429,22 +1833,37 @@ async function materializeNewNotePart({ contentType, newZip, partPath, relations
1429
1833
  const compressionOptions = { level: compressionLevel };
1430
1834
  const contentTypesXml = await contentTypesFile.async("text");
1431
1835
  const partName = `/${partPath}`;
1432
- if (!contentTypesXml.toLowerCase().includes(partName.toLowerCase())) newZip.file(contentTypesFile.name, contentTypesXml.replace("</Types>", () => `<Override PartName="${partName}" ContentType="${contentType}"/></Types>`), {
1433
- compression: "DEFLATE",
1434
- compressionOptions
1836
+ if (!contentTypesXml.toLowerCase().includes(partName.toLowerCase())) writeZipPart({
1837
+ zip: newZip,
1838
+ path: contentTypesFile.name,
1839
+ data: contentTypesXml.replace("</Types>", () => `<Override PartName="${partName}" ContentType="${contentType}"/></Types>`),
1840
+ options: {
1841
+ compression: "DEFLATE",
1842
+ compressionOptions
1843
+ }
1435
1844
  });
1436
1845
  const relationshipsXml = await relationshipsFile.async("text");
1437
1846
  if (!relationshipsXml.includes(relationshipType)) {
1438
1847
  const relationshipId = `rId${findMaxRId(relationshipsXml) + 1}`;
1439
1848
  const target = partPath.slice(5);
1440
- newZip.file(relationshipsFile.name, relationshipsXml.replace("</Relationships>", () => `<Relationship Id="${relationshipId}" Type="${relationshipType}" Target="${target}"/></Relationships>`), {
1441
- compression: "DEFLATE",
1442
- compressionOptions
1849
+ writeZipPart({
1850
+ zip: newZip,
1851
+ path: relationshipsFile.name,
1852
+ data: relationshipsXml.replace("</Relationships>", () => `<Relationship Id="${relationshipId}" Type="${relationshipType}" Target="${target}"/></Relationships>`),
1853
+ options: {
1854
+ compression: "DEFLATE",
1855
+ compressionOptions
1856
+ }
1443
1857
  });
1444
1858
  }
1445
- newZip.file(partPath, serializedPart, {
1446
- compression: "DEFLATE",
1447
- compressionOptions
1859
+ writeZipPart({
1860
+ zip: newZip,
1861
+ path: partPath,
1862
+ data: serializedPart,
1863
+ options: {
1864
+ compression: "DEFLATE",
1865
+ compressionOptions
1866
+ }
1448
1867
  });
1449
1868
  }
1450
1869
  /** Only this modeled switch changes on story lifecycle edits; preserve all other settings. */
@@ -1468,9 +1887,14 @@ async function serializeHeaderFooterSettingsIntoZip(doc, zip, compressionLevel)
1468
1887
  if (parseSettings(xml).evenAndOddHeaders === enabled) return;
1469
1888
  const patched = updateEvenAndOddHeaders(xml, enabled);
1470
1889
  if (patched === null) throw new DocxPackageFidelityError("Cannot update malformed header/footer settings");
1471
- zip.file(file.name, patched, {
1472
- compression: "DEFLATE",
1473
- compressionOptions: { level: compressionLevel }
1890
+ writeZipPart({
1891
+ zip,
1892
+ path: file.name,
1893
+ data: patched,
1894
+ options: {
1895
+ compression: "DEFLATE",
1896
+ compressionOptions: { level: compressionLevel }
1897
+ }
1474
1898
  });
1475
1899
  }
1476
1900
  const numberingBaselines = /* @__PURE__ */ new WeakMap();
@@ -1506,9 +1930,14 @@ async function serializeNumberingIntoZip(doc, originalZip, newZip, compressionLe
1506
1930
  currentXml
1507
1931
  });
1508
1932
  if (patched === null || patched === baseline.originalXml) return;
1509
- newZip.file(file.name, patched, {
1510
- compression: "DEFLATE",
1511
- compressionOptions: { level: compressionLevel }
1933
+ writeZipPart({
1934
+ zip: newZip,
1935
+ path: file.name,
1936
+ data: patched,
1937
+ options: {
1938
+ compression: "DEFLATE",
1939
+ compressionOptions: { level: compressionLevel }
1940
+ }
1512
1941
  });
1513
1942
  }
1514
1943
  const STYLES_PART_PATH = "word/styles.xml";
@@ -1616,24 +2045,18 @@ const styleDefinitionsToSerialize = (doc) => {
1616
2045
  styles: [...styles.styles, ...missing]
1617
2046
  };
1618
2047
  };
1619
- async function serializeAddedStylesIntoZip(doc, originalZip, newZip, compressionLevel) {
2048
+ /** One append-only style owner for full repack and selective overlays. */
2049
+ async function planAddedStyles(doc, originalZip) {
1620
2050
  const styles = doc.package.styles;
1621
- if (!styles || styles.styles.length === 0) return;
2051
+ if (!styles || styles.styles.length === 0) return { type: "unchanged" };
1622
2052
  const file = findNotePartEntry(originalZip, STYLES_PART_PATH);
1623
2053
  const originalXml = file ? await file.async("text") : null;
1624
2054
  const rootName = originalXml === null ? void 0 : /<(?![?!])(?<name>[^\s/>]+)/u.exec(originalXml)?.groups?.["name"];
1625
2055
  const rootClose = originalXml !== null && rootName !== void 0 && getLocalName(rootName) === "styles" ? originalXml.lastIndexOf(`</${rootName}>`) : -1;
1626
- if (file === null || originalXml === null || rootClose < 0) {
1627
- await materializeNewNotePart({
1628
- contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
1629
- newZip,
1630
- partPath: STYLES_PART_PATH,
1631
- relationshipType: RELATIONSHIP_TYPES.styles,
1632
- serializedPart: serializeStylesXml(styles),
1633
- compressionLevel
1634
- });
1635
- return;
1636
- }
2056
+ if (file === null || originalXml === null || rootClose < 0) return {
2057
+ type: "materialize",
2058
+ styles
2059
+ };
1637
2060
  const existing = new Set(definedStyleIds(originalXml));
1638
2061
  const added = [];
1639
2062
  for (const style of styles.styles) {
@@ -1641,13 +2064,42 @@ async function serializeAddedStylesIntoZip(doc, originalZip, newZip, compression
1641
2064
  existing.add(style.styleId);
1642
2065
  added.push(style);
1643
2066
  }
1644
- if (added.length === 0) return;
2067
+ if (added.length === 0) return { type: "unchanged" };
1645
2068
  const canonical = hasCanonicalWordprocessingPrefixes(originalXml);
1646
2069
  const patched = originalXml.slice(0, rootClose) + added.map((style) => canonical ? serializeStyle(style) : selfBoundFragment(serializeStyle(style), file.name)).join("") + originalXml.slice(rootClose);
1647
- newZip.file(file.name, patched, {
1648
- compression: "DEFLATE",
1649
- compressionOptions: { level: compressionLevel }
1650
- });
2070
+ return {
2071
+ type: "patch",
2072
+ path: file.name,
2073
+ xml: patched
2074
+ };
2075
+ }
2076
+ async function serializeAddedStylesIntoZip(doc, originalZip, newZip, compressionLevel) {
2077
+ const plan = await planAddedStyles(doc, originalZip);
2078
+ switch (plan.type) {
2079
+ case "unchanged": return;
2080
+ case "materialize":
2081
+ await materializeNewNotePart({
2082
+ contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
2083
+ newZip,
2084
+ partPath: STYLES_PART_PATH,
2085
+ relationshipType: RELATIONSHIP_TYPES.styles,
2086
+ serializedPart: serializeStylesXml(plan.styles),
2087
+ compressionLevel
2088
+ });
2089
+ return;
2090
+ case "patch":
2091
+ writeZipPart({
2092
+ zip: newZip,
2093
+ path: plan.path,
2094
+ data: plan.xml,
2095
+ options: {
2096
+ compression: "DEFLATE",
2097
+ compressionOptions: { level: compressionLevel }
2098
+ }
2099
+ });
2100
+ return;
2101
+ default: return plan;
2102
+ }
1651
2103
  }
1652
2104
  /**
1653
2105
  * The XML to write for a patched note part, or null to keep the original.
@@ -1702,9 +2154,14 @@ async function patchNotePartIntoZip({ conventionalLowerPath, currentXml, replace
1702
2154
  partName: file.name
1703
2155
  });
1704
2156
  if (patchedXml === null || patchedXml === originalXml) return;
1705
- newZip.file(file.name, patchedXml, {
1706
- compression: "DEFLATE",
1707
- compressionOptions: { level: compressionLevel }
2157
+ writeZipPart({
2158
+ zip: newZip,
2159
+ path: file.name,
2160
+ data: patchedXml,
2161
+ options: {
2162
+ compression: "DEFLATE",
2163
+ compressionOptions: { level: compressionLevel }
2164
+ }
1708
2165
  });
1709
2166
  }
1710
2167
  /** `word/Footnotes.xml` -> `word/_rels/Footnotes.xml.rels` (casing preserved). */
@@ -1802,7 +2259,10 @@ function createEmptyDocx(properties = {}) {
1802
2259
  }
1803
2260
  const createEmptyDocxZip = ({ creator, application }) => {
1804
2261
  const zip = new JSZip();
1805
- zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2262
+ writeZipPart({
2263
+ zip,
2264
+ path: "[Content_Types].xml",
2265
+ data: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1806
2266
  <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
1807
2267
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
1808
2268
  <Default Extension="xml" ContentType="application/xml"/>
@@ -1810,18 +2270,30 @@ const createEmptyDocxZip = ({ creator, application }) => {
1810
2270
  <Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
1811
2271
  <Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
1812
2272
  <Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
1813
- </Types>`);
1814
- zip.file("_rels/.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2273
+ </Types>`
2274
+ });
2275
+ writeZipPart({
2276
+ zip,
2277
+ path: "_rels/.rels",
2278
+ data: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1815
2279
  <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
1816
2280
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
1817
2281
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
1818
2282
  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
1819
- </Relationships>`);
1820
- zip.file("word/_rels/document.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2283
+ </Relationships>`
2284
+ });
2285
+ writeZipPart({
2286
+ zip,
2287
+ path: "word/_rels/document.xml.rels",
2288
+ data: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1821
2289
  <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
1822
2290
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
1823
- </Relationships>`);
1824
- zip.file("word/document.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2291
+ </Relationships>`
2292
+ });
2293
+ writeZipPart({
2294
+ zip,
2295
+ path: "word/document.xml",
2296
+ data: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1825
2297
  <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
1826
2298
  <w:body>
1827
2299
  <w:p>
@@ -1834,19 +2306,30 @@ const createEmptyDocxZip = ({ creator, application }) => {
1834
2306
  <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="720" w:footer="720" w:gutter="0"/>
1835
2307
  </w:sectPr>
1836
2308
  </w:body>
1837
- </w:document>`);
1838
- zip.file(STYLES_PART_PATH, SEED_STYLES_XML);
2309
+ </w:document>`
2310
+ });
2311
+ writeZipPart({
2312
+ zip,
2313
+ path: STYLES_PART_PATH,
2314
+ data: SEED_STYLES_XML
2315
+ });
1839
2316
  const now = (/* @__PURE__ */ new Date()).toISOString();
1840
- const creatorElement = creator === void 0 ? "" : `\n <dc:creator>${escapeXmlText(creator)}</dc:creator>`;
1841
- zip.file("docProps/core.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1842
- <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">${creatorElement}
2317
+ writeZipPart({
2318
+ zip,
2319
+ path: "docProps/core.xml",
2320
+ data: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2321
+ <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">${creator === void 0 ? "" : `\n <dc:creator>${escapeXmlText(creator)}</dc:creator>`}
1843
2322
  <dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created>
1844
2323
  <dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified>
1845
- </cp:coreProperties>`);
1846
- const applicationElements = application === void 0 ? "" : `\n <Application>${escapeXmlText(application)}</Application>\n <AppVersion>${CREATED_APP_VERSION}</AppVersion>`;
1847
- zip.file("docProps/app.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1848
- <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">${applicationElements}
1849
- </Properties>`);
2324
+ </cp:coreProperties>`
2325
+ });
2326
+ writeZipPart({
2327
+ zip,
2328
+ path: "docProps/app.xml",
2329
+ data: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2330
+ <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">${application === void 0 ? "" : `\n <Application>${escapeXmlText(application)}</Application>\n <AppVersion>${CREATED_APP_VERSION}</AppVersion>`}
2331
+ </Properties>`
2332
+ });
1850
2333
  return zip;
1851
2334
  };
1852
2335
  /**
@@ -1868,7 +2351,8 @@ async function createDocx(doc, properties = {}) {
1868
2351
  originalCorePropertiesXml: source.corePropertiesXml,
1869
2352
  compressionLevel: 6,
1870
2353
  updateModifiedDate: true,
1871
- onDiagnostic: properties.onDiagnostic
2354
+ onDiagnostic: properties.onDiagnostic,
2355
+ bodyAuthority: properties.bodyAuthority
1872
2356
  });
1873
2357
  }
1874
2358
  const zip = await createDocumentSeedZip(doc, properties);
@@ -1880,7 +2364,8 @@ async function createDocx(doc, properties = {}) {
1880
2364
  originalCorePropertiesXml: await zip.file("docProps/core.xml")?.async("text"),
1881
2365
  compressionLevel: 6,
1882
2366
  updateModifiedDate: true,
1883
- onDiagnostic: properties.onDiagnostic
2367
+ onDiagnostic: properties.onDiagnostic,
2368
+ bodyAuthority: properties.bodyAuthority
1884
2369
  });
1885
2370
  }
1886
2371
  const createDocumentSeedZip = async (doc, properties) => {
@@ -1891,38 +2376,69 @@ const createDocumentSeedZip = async (doc, properties) => {
1891
2376
  const styleDefinitions = styleDefinitionsToSerialize(doc);
1892
2377
  if (styleDefinitions) {
1893
2378
  assertStyleNumberingReferences(doc);
1894
- zip.file(STYLES_PART_PATH, serializeStylesXml(styleDefinitions));
1895
- } else zip.file(STYLES_PART_PATH, seedStylesXmlWith(missingNoteReferenceStyles(void 0, noteReferenceNeeds(doc.package))));
2379
+ writeZipPart({
2380
+ zip,
2381
+ path: STYLES_PART_PATH,
2382
+ data: serializeStylesXml(styleDefinitions)
2383
+ });
2384
+ } else writeZipPart({
2385
+ zip,
2386
+ path: STYLES_PART_PATH,
2387
+ data: seedStylesXmlWith(missingNoteReferenceStyles(void 0, noteReferenceNeeds(doc.package)))
2388
+ });
1896
2389
  const numbering = doc.package.numbering;
1897
2390
  if (numbering && (numbering.abstractNums.length > 0 || numbering.nums.length > 0)) {
1898
- zip.file("word/numbering.xml", serializeNumberingXml(numbering));
2391
+ writeZipPart({
2392
+ zip,
2393
+ path: "word/numbering.xml",
2394
+ data: serializeNumberingXml(numbering)
2395
+ });
1899
2396
  relationships.push(relationshipXml(nextRelationshipId, "numbering", "numbering.xml"));
1900
2397
  overrides.push(overrideXml("/word/numbering.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"));
1901
2398
  nextRelationshipId += 1;
1902
2399
  }
1903
2400
  if (doc.package.theme) {
1904
- zip.file("word/theme/theme1.xml", serializeThemeXml(doc.package.theme));
2401
+ writeZipPart({
2402
+ zip,
2403
+ path: "word/theme/theme1.xml",
2404
+ data: serializeThemeXml(doc.package.theme)
2405
+ });
1905
2406
  relationships.push(relationshipXml(nextRelationshipId, "theme", "theme/theme1.xml"));
1906
2407
  overrides.push(overrideXml("/word/theme/theme1.xml", "application/vnd.openxmlformats-officedocument.theme+xml"));
1907
2408
  nextRelationshipId += 1;
1908
2409
  }
1909
2410
  if (doc.package.fontTable && doc.package.fontTable.fonts.length > 0) {
1910
- zip.file("word/fontTable.xml", serializeFontTableXml(doc.package.fontTable));
2411
+ writeZipPart({
2412
+ zip,
2413
+ path: "word/fontTable.xml",
2414
+ data: serializeFontTableXml(doc.package.fontTable)
2415
+ });
1911
2416
  relationships.push(relationshipXml(nextRelationshipId, "fontTable", "fontTable.xml"));
1912
2417
  overrides.push(overrideXml("/word/fontTable.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"));
1913
2418
  nextRelationshipId += 1;
1914
2419
  }
1915
2420
  if (doc.package.settings) {
1916
- zip.file("word/settings.xml", serializeSettingsXml(doc.package.settings));
2421
+ writeZipPart({
2422
+ zip,
2423
+ path: "word/settings.xml",
2424
+ data: serializeSettingsXml(doc.package.settings)
2425
+ });
1917
2426
  relationships.push(relationshipXml(nextRelationshipId, "settings", "settings.xml"));
1918
2427
  overrides.push(overrideXml("/word/settings.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"));
1919
2428
  }
1920
- zip.file("word/_rels/document.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">${relationships.join("")}</Relationships>`);
2429
+ writeZipPart({
2430
+ zip,
2431
+ path: "word/_rels/document.xml.rels",
2432
+ data: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">${relationships.join("")}</Relationships>`
2433
+ });
1921
2434
  if (overrides.length > 0) {
1922
2435
  const contentTypesFile = zip.file("[Content_Types].xml");
1923
2436
  if (!contentTypesFile) panic("Fresh DOCX seed is missing [Content_Types].xml");
1924
- const contentTypes = await contentTypesFile.async("string");
1925
- zip.file("[Content_Types].xml", contentTypes.replace("</Types>", `${overrides.join("")}\n</Types>`));
2437
+ writeZipPart({
2438
+ zip,
2439
+ path: "[Content_Types].xml",
2440
+ data: (await contentTypesFile.async("string")).replace("</Types>", `${overrides.join("")}\n</Types>`)
2441
+ });
1926
2442
  }
1927
2443
  return zip;
1928
2444
  };
@@ -1956,4 +2472,4 @@ const assertStyleNumberingReferences = (doc) => {
1956
2472
  }
1957
2473
  };
1958
2474
  //#endregion
1959
- export { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_CONTENT_TYPE, COMMENTS_EXTENDED_PART, COMMENTS_EXTENDED_PART_LOWER, DocxDuplicateSectionCarrierError, DocxPackageFidelityError, addCommentsExtendedOverride, addCommentsExtendedRelationship, addMedia, addRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, collectHyperlinksWithoutRId, createDocx, createEmptyDocx, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, hasUnmaterializedInlineResources, isDocxBuffer, notePartRelsPath, removeCommentsExtendedOverride, removeCommentsExtendedRelationship, repackDocx, repackDocxFromRaw, updateCoreProperties, updateDocumentXml, updateMultipleFiles, updateXmlFile, validateDocx, withoutAttachedTemplate };
2475
+ export { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_CONTENT_TYPE, COMMENTS_EXTENDED_PART, COMMENTS_EXTENDED_PART_LOWER, CanonicalImageRelationshipRefusalError, DocxDuplicateSectionCarrierError, DocxPackageFidelityError, addCommentsExtendedOverride, addCommentsExtendedRelationship, addMedia, addRelationship, applyUpdatesToZip, assertSectionCarriersMatchModel, collectHeaderFooterUpdates, collectHyperlinksWithoutRId, createDocx, createEmptyDocx, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, hasUnmaterializedHyperlinkBindings, hasUnmaterializedInlineResources, isDocxBuffer, notePartRelsPath, planAddedStyles, publishCanonicalImageResources, removeCommentsExtendedOverride, removeCommentsExtendedRelationship, repackDocx, repackDocxFromRaw, updateCoreProperties, updateDocumentXml, updateMultipleFiles, updateXmlFile, validateDocx, withoutAttachedTemplate };