@stll/folio-core 0.33.1 → 0.34.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 (121) hide show
  1. package/dist/ai-edits/apply.d.ts +15 -0
  2. package/dist/ai-edits/apply.js +271 -48
  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 +49 -5
  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 +13 -6
  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/formatting.d.ts +1 -1
  20. package/dist/compare/formatting.js +38 -9
  21. package/dist/compare/plan.d.ts +31 -1
  22. package/dist/compare/plan.js +233 -66
  23. package/dist/compare/types.d.ts +9 -7
  24. package/dist/compare/types.js +7 -5
  25. package/dist/compare/verification.d.ts +31 -12
  26. package/dist/compare/verification.js +106 -18
  27. package/dist/compat/eigenpal.d.ts +4 -4
  28. package/dist/compat/eigenpal.js +2 -2
  29. package/dist/controller/headerFooterEditorManager.js +11 -9
  30. package/dist/controller/layoutPipeline.js +24 -3
  31. package/dist/display-list/build/watermarkPrimitives.js +15 -3
  32. package/dist/display-list/primitives.d.ts +1 -1
  33. package/dist/document-operations.d.ts +10 -1
  34. package/dist/document-operations.js +33 -5
  35. package/dist/docx/appVersionNormalization.d.ts +49 -0
  36. package/dist/docx/appVersionNormalization.js +74 -0
  37. package/dist/docx/blockContentParser.js +3 -2
  38. package/dist/docx/drawingUtils.js +4 -3
  39. package/dist/docx/headerFooterParser.js +8 -14
  40. package/dist/docx/paragraphParser.js +48 -2
  41. package/dist/docx/rezip.d.ts +10 -4
  42. package/dist/docx/rezip.js +54 -18
  43. package/dist/docx/runParser.js +9 -8
  44. package/dist/docx/sdtProperties.js +4 -3
  45. package/dist/docx/selectiveSave.js +7 -6
  46. package/dist/docx/serializer/commentSerializer.d.ts +1 -1
  47. package/dist/docx/serializer/commentSerializer.js +43 -19
  48. package/dist/docx/serializer/documentSerializer.d.ts +3 -1
  49. package/dist/docx/serializer/documentSerializer.js +42 -78
  50. package/dist/docx/serializer/fontTableSerializer.js +8 -5
  51. package/dist/docx/serializer/headerFooterSerializer.d.ts +4 -1
  52. package/dist/docx/serializer/headerFooterSerializer.js +55 -31
  53. package/dist/docx/serializer/noteSerializer.js +34 -30
  54. package/dist/docx/serializer/numberingSerializer.js +8 -4
  55. package/dist/docx/serializer/paragraphSerializer.d.ts +1 -1
  56. package/dist/docx/serializer/paragraphSerializer.js +22 -12
  57. package/dist/docx/serializer/partNamespaces.d.ts +78 -0
  58. package/dist/docx/serializer/partNamespaces.js +324 -0
  59. package/dist/docx/serializer/runSerializer.js +1 -1
  60. package/dist/docx/serializer/settingsSerializer.js +8 -2
  61. package/dist/docx/serializer/stylesSerializer.js +8 -5
  62. package/dist/docx/serializer/tableSerializer.js +83 -18
  63. package/dist/docx/serializer/themeSerializer.js +10 -2
  64. package/dist/docx/server/build.d.ts +1 -1
  65. package/dist/docx/settingsParser.js +3 -0
  66. package/dist/docx/strictValueEncodings.gen.d.ts +15 -0
  67. package/dist/docx/strictValueEncodings.gen.js +275 -0
  68. package/dist/docx/tableParser.d.ts +0 -6
  69. package/dist/docx/tableParser.js +52 -7
  70. package/dist/docx/transitionalSpelling.d.ts +23 -0
  71. package/dist/docx/transitionalSpelling.js +36 -0
  72. package/dist/docx/universalMeasure.d.ts +20 -0
  73. package/dist/docx/universalMeasure.js +32 -0
  74. package/dist/docx/verbatimCapture.d.ts +20 -0
  75. package/dist/docx/verbatimCapture.js +131 -0
  76. package/dist/docx/vmlImageParser.js +4 -3
  77. package/dist/docx/watermarkParser.d.ts +2 -4
  78. package/dist/docx/watermarkParser.js +8 -9
  79. package/dist/docx/xmlParser.d.ts +23 -2
  80. package/dist/docx/xmlParser.js +43 -11
  81. package/dist/headless-layout.js +14 -2
  82. package/dist/index.d.ts +4 -4
  83. package/dist/index.js +2 -2
  84. package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -2
  85. package/dist/layout-bridge/convert/footnoteLayout.js +23 -10
  86. package/dist/layout-bridge/convert/headerFooterLayout.js +13 -2
  87. package/dist/layout-bridge/convert/toFlowBlocks.js +96 -13
  88. package/dist/layout-engine/index.js +11 -4
  89. package/dist/layout-engine/justifiedLineFit.d.ts +4 -4
  90. package/dist/layout-engine/justifiedLineFit.js +4 -4
  91. package/dist/layout-engine/measure/lineBreakProvider.js +1 -0
  92. package/dist/layout-engine/measure/measureBlocks.js +1 -1
  93. package/dist/layout-engine/measure/measureParagraph.js +29 -29
  94. package/dist/layout-painter/renderPage.js +6 -1
  95. package/dist/layout-painter/renderParagraph.js +18 -8
  96. package/dist/layout-painter/renderWatermark.js +11 -4
  97. package/dist/model.d.ts +3 -3
  98. package/dist/model.js +2 -2
  99. package/dist/prosemirror/attrs/index.js +11 -0
  100. package/dist/prosemirror/commands/comments.js +24 -3
  101. package/dist/prosemirror/containerFinalParagraph.d.ts +47 -0
  102. package/dist/prosemirror/containerFinalParagraph.js +92 -0
  103. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
  104. package/dist/prosemirror/conversion/fromProseDoc.js +47 -9
  105. package/dist/prosemirror/conversion/toProseDoc.js +61 -14
  106. package/dist/prosemirror/extensions/core/DocExtension.js +7 -1
  107. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  108. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +1 -0
  109. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -0
  110. package/dist/prosemirror/schema/marks.d.ts +3 -9
  111. package/dist/prosemirror/schema/nodes.d.ts +22 -0
  112. package/dist/server.d.ts +1 -1
  113. package/dist/types/block-id.d.ts +12 -1
  114. package/dist/types/block-id.js +17 -1
  115. package/dist/utils/canonicalJson.d.ts +14 -0
  116. package/dist/utils/canonicalJson.js +19 -0
  117. package/dist/utils/fontResolver.js +51 -0
  118. package/dist/utils/formatToStyle.d.ts +1 -1
  119. package/dist/utils/formatToStyle.js +41 -1
  120. package/dist/watermark/index.js +7 -0
  121. package/package.json +2 -2
@@ -0,0 +1,74 @@
1
+ import { TaggedError } from "better-result";
2
+ //#region src/docx/appVersionNormalization.ts
3
+ /**
4
+ * Keep the application version a package states about itself in the form the
5
+ * schema gives it.
6
+ *
7
+ * `AppVersion` in the extended-properties part is `XX.YYYY`: a one- or
8
+ * two-digit integer, a dot, and four digits. Producers exist that write a
9
+ * three-part version there instead, and folio copies `docProps/app.xml`
10
+ * through verbatim when it saves a document it did not create — so a package
11
+ * can carry a value with two dots in, and a save that copies it out hands a
12
+ * consumer a package it refuses to open at all.
13
+ *
14
+ * {@link appVersionInSchemaForm} is the one mapping, and it is a pure function
15
+ * of the value alone: it keeps the leading integer where the value opens with
16
+ * one the form allows, and writes the build digits the form requires. Nothing
17
+ * else in the part is touched, and a package that carries no extended
18
+ * properties keeps carrying none — synthesizing metadata a document never
19
+ * stated is a different decision.
20
+ */
21
+ /** `XX.YYYY`: the only form the extended-properties `AppVersion` may take. */
22
+ const SCHEMA_FORM = /^\d{1,2}\.\d{4}$/u;
23
+ /** The leading integer, when the value opens with one the form allows. */
24
+ const LEADING_MAJOR = /^\d{1,2}(?!\d)/u;
25
+ /** Major version for a value that does not open with one. */
26
+ const DEFAULT_MAJOR = "1";
27
+ /**
28
+ * The build digits a rewritten value gets. A value the form rejects states no
29
+ * build this pass could carry over, so every rewrite lands on the same one.
30
+ */
31
+ const CANONICAL_BUILD = "0000";
32
+ /**
33
+ * A value reached the package that the schema form does not accept.
34
+ *
35
+ * The normalization hands every value it writes to one choke point, which
36
+ * throws this rather than letting the package go out carrying a version a
37
+ * consumer refuses.
38
+ */
39
+ var AppVersionSchemaError = class extends TaggedError("AppVersionSchemaError") {};
40
+ /**
41
+ * `value` when it already fits, and a value derived from it when it does not.
42
+ *
43
+ * The derivation reads the value and nothing else — not the package, not the
44
+ * clock, not folio's own version — so two saves of one document state the same
45
+ * application version.
46
+ */
47
+ const appVersionInSchemaForm = (value) => {
48
+ if (SCHEMA_FORM.test(value)) return value;
49
+ const normalized = `${LEADING_MAJOR.exec(value.trim())?.[0] ?? DEFAULT_MAJOR}.${CANONICAL_BUILD}`;
50
+ if (!SCHEMA_FORM.test(normalized)) throw new AppVersionSchemaError({
51
+ message: `Derived application version ${normalized} is not of the form XX.YYYY`,
52
+ appVersion: value
53
+ });
54
+ return normalized;
55
+ };
56
+ /**
57
+ * The element and its content, with the prefix and attributes it was written
58
+ * with preserved: the replacement rewrites the value between the tags and
59
+ * leaves the rest of the part byte-identical.
60
+ */
61
+ const APP_VERSION_ELEMENT = /(<(?:[^\s<>/:]+:)?AppVersion(?:\s[^<>]*)?>)([^<]*)(<\/(?:[^\s<>/:]+:)?AppVersion>)/u;
62
+ /**
63
+ * Rewrite the application version an extended-properties part states.
64
+ *
65
+ * Returns the part unchanged when the value already fits and when the part
66
+ * states no version at all, so a save of a document that never carried a
67
+ * malformed one is byte-identical.
68
+ */
69
+ const normalizeAppVersionInExtendedProperties = (xml) => xml.replace(APP_VERSION_ELEMENT, (whole, open, value, close) => {
70
+ const replacement = appVersionInSchemaForm(value);
71
+ return replacement === value ? whole : `${open}${replacement}${close}`;
72
+ });
73
+ //#endregion
74
+ export { AppVersionSchemaError, appVersionInSchemaForm, normalizeAppVersionInExtendedProperties };
@@ -6,7 +6,8 @@ import { parseParagraph } from "./paragraphParser.js";
6
6
  import { enrichParagraphTextBoxes } from "./paragraphTextBoxEnrichment.js";
7
7
  import { parseSdtProperties } from "./sdtProperties.js";
8
8
  import { parseTable } from "./tableParser.js";
9
- import { elementToXml, findChild, getChildElements, getLocalName, mergeXmlnsDeclarations } from "./xmlParser.js";
9
+ import { captureVerbatimXml } from "./verbatimCapture.js";
10
+ import { findChild, getChildElements, getLocalName, mergeXmlnsDeclarations } from "./xmlParser.js";
10
11
  //#region src/docx/blockContentParser.ts
11
12
  const computeListMarker = (paragraph, { numbering, listCounters, abstractCounters, restartedNumIds, previousList, siblingNumIdsByAbstractNumId }) => {
12
13
  const listRendering = paragraph.listRendering;
@@ -192,7 +193,7 @@ const captureSdtSiblingMarkers = (sdt) => {
192
193
  sawContent = true;
193
194
  continue;
194
195
  }
195
- const xml = elementToXml(ch);
196
+ const xml = captureVerbatimXml(ch);
196
197
  if (sawContent) afterParts.push(xml);
197
198
  else beforeParts.push(xml);
198
199
  }
@@ -1,5 +1,6 @@
1
1
  import { ImageHorizontalAlignmentSchema, ImageHorizontalRelativeToSchema, ImageVerticalAlignmentSchema, ImageVerticalRelativeToSchema, ImageWrapTextSchema, ShapeOutlineStyleSchema, narrowEnum } from "./parserEnums.js";
2
- import { elementToXml, findByFullName, findChildByLocalName, findChildrenByLocalName, getAttribute, getChildElements, getTextContent, parseNumericAttribute } from "./xmlParser.js";
2
+ import { captureVerbatimXml } from "./verbatimCapture.js";
3
+ import { findByFullName, findChildByLocalName, findChildrenByLocalName, getAttribute, getChildElements, getTextContent, parseNumericAttribute } from "./xmlParser.js";
3
4
  //#region src/docx/drawingUtils.ts
4
5
  /**
5
6
  * Map OOXML scheme names to standard theme color slots.
@@ -162,7 +163,7 @@ function parseGradientFill(gradientFill) {
162
163
  }
163
164
  return {
164
165
  type: "gradient",
165
- rawXml: elementToXml(gradientFill),
166
+ rawXml: captureVerbatimXml(gradientFill),
166
167
  gradient: {
167
168
  type,
168
169
  ...angle !== void 0 ? { angle } : {},
@@ -177,7 +178,7 @@ function parseOutline(spPr) {
177
178
  const ln = spPr ? findChildByLocalName(spPr, "ln") : null;
178
179
  if (!ln) return;
179
180
  if (findChildByLocalName(ln, "noFill")) return;
180
- const outline = { rawXml: elementToXml(ln) };
181
+ const outline = { rawXml: captureVerbatimXml(ln) };
181
182
  const w = getAttribute(ln, null, "w");
182
183
  if (w) {
183
184
  const parsed = Number.parseInt(w, 10);
@@ -31,27 +31,21 @@ function parseHeader(headerXml, hdrFtrType = "default", styles = null, theme = n
31
31
  result.rawWatermarkXml = watermarkResult.rawParagraphXml;
32
32
  result.watermarkBlockIndex = watermarkResult.blockIndex;
33
33
  }
34
- result.content = parseBlockContent(watermarkResult ? withoutChild(rootElement, watermarkResult.hostingParagraph) : rootElement, styles, theme, numbering, rels, media, {
34
+ result.content = parseBlockContent(rootElement, styles, theme, numbering, rels, media, {
35
35
  inHeaderFooter: true,
36
36
  rootXmlns: collectXmlnsDeclarations(rootElement)
37
37
  });
38
+ if (watermarkResult) {
39
+ const host = result.content.at(watermarkResult.blockIndex);
40
+ if (host?.type === "paragraph") result.content[watermarkResult.blockIndex] = {
41
+ ...host,
42
+ content: []
43
+ };
44
+ }
38
45
  assignHeaderFooterVerbatimXml(result, headerXml);
39
46
  return result;
40
47
  }
41
48
  /**
42
- * Return a shallow copy of `parent` whose `elements` array omits the
43
- * single child reference `child`. Used to skip the watermark paragraph
44
- * when feeding the header into `parseBlockContent` — without this the
45
- * body parser would emit an empty placeholder paragraph where the
46
- * watermark sits in the source.
47
- */
48
- function withoutChild(parent, child) {
49
- return {
50
- ...parent,
51
- elements: (parent.elements ?? []).filter((el) => el !== child)
52
- };
53
- }
54
- /**
55
49
  * Parse a footer XML file (word/footer*.xml)
56
50
  *
57
51
  * @param footerXml - The raw XML content of the footer file
@@ -10,7 +10,8 @@ import { consolidateParagraphContent } from "./runConsolidator.js";
10
10
  import { parseRun, parseRunProperties } from "./runParser.js";
11
11
  import { parseSdtProperties } from "./sdtProperties.js";
12
12
  import { parseSectionProperties } from "./sectionParser.js";
13
- import { WORDPROCESSINGML_NAMESPACE_URIS, elementToXml, findChild, findChildByNamespaceUri, findChildren, getAttribute, getChildElements, getLocalName, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseNumberingLevelAttribute, parseNumericAttribute } from "./xmlParser.js";
13
+ import { captureVerbatimXml } from "./verbatimCapture.js";
14
+ import { WORDPROCESSINGML_NAMESPACE_URIS, findChild, findChildByNamespaceUri, findChildren, getAttribute, getChildElements, getLocalName, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseNumberingLevelAttribute, parseNumericAttribute } from "./xmlParser.js";
14
15
  import { panic } from "better-result";
15
16
  import { PARAGRAPH_MARK_CHANGE_KINDS, normalizeRevisionId } from "@stll/docx-core/model";
16
17
  //#region src/docx/paragraphParser.ts
@@ -762,6 +763,47 @@ function parseSimpleField(node, styles, theme, rels, media, rootXmlns = {}) {
762
763
  function hasRunPayloadElement(runElement) {
763
764
  return getChildElements(runElement).some((child) => !matchesName(child, "w", "rPr"));
764
765
  }
766
+ const LEGACY_FORM_CHECKBOX_GLYPHS = {
767
+ checked: "☒",
768
+ unchecked: "☐"
769
+ };
770
+ const LEGACY_FORM_CHECKBOX_INSTRUCTION = "FORMCHECKBOX";
771
+ function getLegacyFormCheckboxDisplay(runElement) {
772
+ const fieldChar = findChild(runElement, "w", "fldChar");
773
+ const fieldData = fieldChar ? findChild(fieldChar, "w", "ffData") : null;
774
+ const checkBox = fieldData ? findChild(fieldData, "w", "checkBox") : null;
775
+ if (!checkBox) return;
776
+ const checked = findChild(checkBox, "w", "checked");
777
+ const defaultChecked = findChild(checkBox, "w", "default");
778
+ let isChecked = defaultChecked ? parseBooleanElement(defaultChecked) : false;
779
+ if (checked) isChecked = parseBooleanElement(checked);
780
+ const explicitSize = parseNumericAttribute(findChild(checkBox, "w", "size"), "w", "val");
781
+ const text = isChecked ? LEGACY_FORM_CHECKBOX_GLYPHS.checked : LEGACY_FORM_CHECKBOX_GLYPHS.unchecked;
782
+ if (explicitSize === void 0) return { text };
783
+ return {
784
+ text,
785
+ fontSize: explicitSize
786
+ };
787
+ }
788
+ function createLegacyFormCheckboxResultRun(display, inheritedFormatting) {
789
+ const run = {
790
+ type: "run",
791
+ content: [{
792
+ type: "text",
793
+ text: display.text
794
+ }]
795
+ };
796
+ const hasInheritedFormatting = inheritedFormatting !== void 0 && Object.keys(inheritedFormatting).length > 0;
797
+ if (display.fontSize !== void 0) run.formatting = hasInheritedFormatting ? {
798
+ ...inheritedFormatting,
799
+ fontSize: display.fontSize
800
+ } : { fontSize: display.fontSize };
801
+ else if (hasInheritedFormatting) run.formatting = inheritedFormatting;
802
+ return run;
803
+ }
804
+ function isLegacyFormCheckboxInstruction(instruction) {
805
+ return instruction.trim().split(/\s+/u).at(0)?.toUpperCase() === LEGACY_FORM_CHECKBOX_INSTRUCTION;
806
+ }
765
807
  /**
766
808
  * Parse all content within a paragraph
767
809
  *
@@ -778,6 +820,7 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
778
820
  let afterSeparator = false;
779
821
  let complexFieldLock = false;
780
822
  let complexFieldDirty = false;
823
+ let complexFieldFallbackDisplay;
781
824
  let complexFieldFormatting;
782
825
  for (const child of children) {
783
826
  const localName = getLocalName(child.name);
@@ -789,6 +832,7 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
789
832
  let hasFieldBegin = false;
790
833
  let beginFldLock = false;
791
834
  let beginDirty = false;
835
+ const beginFallbackDisplay = getLegacyFormCheckboxDisplay(runElement);
792
836
  let hasFieldSeparate = false;
793
837
  let hasFieldEnd = false;
794
838
  let endOriginalValue;
@@ -815,6 +859,7 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
815
859
  complexFieldResultRuns = [];
816
860
  complexFieldLock = beginFldLock;
817
861
  complexFieldDirty = beginDirty;
862
+ complexFieldFallbackDisplay = beginFallbackDisplay;
818
863
  complexFieldFormatting = run.formatting;
819
864
  }
820
865
  if (inComplexField) {
@@ -839,6 +884,7 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
839
884
  }
840
885
  if (hasFieldEnd) {
841
886
  let resultRuns = complexFieldResultRuns;
887
+ if (resultRuns.length === 0 && complexFieldFallbackDisplay !== void 0 && isLegacyFormCheckboxInstruction(complexFieldInstr)) resultRuns = [createLegacyFormCheckboxResultRun(complexFieldFallbackDisplay, complexFieldFormatting)];
842
888
  if (resultRuns.length === 0 && !afterSeparator && endOriginalValue !== void 0) resultRuns = [{
843
889
  type: "run",
844
890
  content: [{
@@ -995,7 +1041,7 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
995
1041
  case "oMath":
996
1042
  case "oMathPara": {
997
1043
  const isBlock = localName === "oMathPara";
998
- const ommlXml = elementToXml(child);
1044
+ const ommlXml = captureVerbatimXml(child);
999
1045
  const plainText = extractMathText(child);
1000
1046
  const mathEq = {
1001
1047
  type: "mathEquation",
@@ -106,11 +106,11 @@ declare function updateMultipleFiles(originalBuffer: ArrayBuffer, updates: Map<s
106
106
  * Apply file updates to an already-loaded JSZip instance and generate the output.
107
107
  * Use this when the zip is already loaded to avoid a redundant decompression pass.
108
108
  *
109
- * This is the selective save's exit, so it owes the package the same id passes
109
+ * This is the selective save's exit, so it owes the package the same passes
110
110
  * {@link generateDocxZip} runs: a save that rewrites only the changed
111
111
  * paragraphs still has to see the parts it left alone, both to know which
112
- * revision ids are free and because an out-of-range paragraph id can sit in a
113
- * part it never touched.
112
+ * revision ids are free and because an out-of-range paragraph id, or a
113
+ * malformed application version, can sit in a part it never touched.
114
114
  */
115
115
  declare function applyUpdatesToZip(zip: JSZip, updates: Map<string, string | ArrayBuffer>, options?: RepackOptions): Promise<ArrayBuffer>;
116
116
  /**
@@ -159,7 +159,13 @@ declare function hasUnmaterializedHeaderFooter(doc: document_d_exports.Document)
159
159
  * have been set without yet resolving in that header's rels).
160
160
  */
161
161
  declare function hasModelDrivenPictureWatermark(doc: document_d_exports.Document): boolean;
162
- declare function collectHeaderFooterUpdates(doc: document_d_exports.Document): Map<string, string>;
162
+ /**
163
+ * Re-serialize every header/footer the model still owns, keyed by part path.
164
+ *
165
+ * `sourceZip` supplies each part as it stands before the save so the rebuilt
166
+ * root can keep any prefix binding only the source document declared.
167
+ */
168
+ declare function collectHeaderFooterUpdates(doc: document_d_exports.Document, sourceZip: JSZip): Promise<Map<string, string>>;
163
169
  /** `word/Footnotes.xml` -> `word/_rels/Footnotes.xml.rels` (casing preserved). */
164
170
  declare function notePartRelsPath(partPath: string): string;
165
171
  /** Update existing core-property values without synthesizing absent metadata. */
@@ -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