@stll/folio-core 0.12.0 → 0.13.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 (131) hide show
  1. package/dist/ai-edits/apply.d.ts +2 -1
  2. package/dist/ai-edits/apply.js +118 -40
  3. package/dist/ai-edits/blockRange.js +1 -1
  4. package/dist/ai-edits/clean-text.js +2 -1
  5. package/dist/ai-edits/headless.js +3 -3
  6. package/dist/ai-edits/snapshot.js +21 -3
  7. package/dist/ai-edits/table-cell-mutations.js +2 -1
  8. package/dist/ai-edits/table-row-column-mutations.d.ts +9 -1
  9. package/dist/ai-edits/table-row-column-mutations.js +1 -1
  10. package/dist/ai-edits/types.d.ts +28 -1
  11. package/dist/ai-edits/word-diff.js +21 -0
  12. package/dist/compat/eigenpal.d.ts +3 -3
  13. package/dist/compat/eigenpal.js +3 -3
  14. package/dist/controller/headerFooterEditorManager.js +1 -1
  15. package/dist/controller/hiddenEditorManager.js +12 -1
  16. package/dist/controller/layoutPipeline.js +1 -1
  17. package/dist/controller/noteEditorManager.js +1 -1
  18. package/dist/document-operations.d.ts +13 -13
  19. package/dist/document-operations.js +31 -21
  20. package/dist/docx/blockContentParser.js +19 -5
  21. package/dist/docx/commentRangeIntegrity.d.ts +6 -0
  22. package/dist/docx/commentRangeIntegrity.js +186 -0
  23. package/dist/docx/conformance.js +57 -3
  24. package/dist/docx/encryption/agileDecryption.js +12 -2
  25. package/dist/docx/encryption/compoundFile.js +6 -1
  26. package/dist/docx/encryption/encryptionInfo.d.ts +9 -1
  27. package/dist/docx/encryption/encryptionInfo.js +18 -2
  28. package/dist/docx/encryption/openEncryptedDocx.js +1 -1
  29. package/dist/docx/ensureParaIds.js +7 -6
  30. package/dist/docx/groupDrawingParser.js +10 -5
  31. package/dist/docx/hyperlinkParser.js +1 -1
  32. package/dist/docx/imageParser.js +10 -6
  33. package/dist/docx/metadataPrivacy.js +17 -3
  34. package/dist/docx/paragraphParser.js +7 -3
  35. package/dist/docx/rezip.js +19 -8
  36. package/dist/docx/runParser.js +13 -4
  37. package/dist/docx/sectionParser.js +17 -4
  38. package/dist/docx/selectiveSave.js +4 -0
  39. package/dist/docx/selectiveXmlPatch.d.ts +22 -1
  40. package/dist/docx/selectiveXmlPatch.js +61 -1
  41. package/dist/docx/serializer/blockSdtSerializer.js +3 -2
  42. package/dist/docx/serializer/commentSerializer.js +4 -4
  43. package/dist/docx/serializer/paragraphSerializer.js +16 -14
  44. package/dist/docx/serializer/runSerializer.js +21 -19
  45. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -1
  46. package/dist/docx/serializer/tableSerializer.js +9 -7
  47. package/dist/docx/serializer/xmlUtils.d.ts +13 -1
  48. package/dist/docx/serializer/xmlUtils.js +27 -1
  49. package/dist/docx/server/extractDocxText.js +55 -8
  50. package/dist/docx/settingsParser.js +15 -2
  51. package/dist/docx/tableParser.d.ts +8 -1
  52. package/dist/docx/tableParser.js +13 -3
  53. package/dist/docx/unzip.js +1 -0
  54. package/dist/docx/vmlImageParser.js +11 -3
  55. package/dist/docx/xmlParser.js +11 -1
  56. package/dist/fonts/embeddedFonts.d.ts +33 -5
  57. package/dist/fonts/embeddedFonts.js +37 -8
  58. package/dist/index.d.ts +3 -3
  59. package/dist/index.js +3 -3
  60. package/dist/layout-bridge/convert/toFlowBlocks.js +15 -0
  61. package/dist/layout-engine/index.js +16 -3
  62. package/dist/layout-engine/measure/cache.js +2 -0
  63. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
  64. package/dist/layout-engine/measure/lineBreakProvider.d.ts +2 -2
  65. package/dist/layout-engine/measure/lineBreakProvider.js +3 -3
  66. package/dist/layout-engine/measure/measureBlocks.js +9 -1
  67. package/dist/layout-engine/measure/measureHelpers.d.ts +10 -1
  68. package/dist/layout-engine/measure/measureHelpers.js +12 -1
  69. package/dist/layout-engine/measure/measureParagraph.js +16 -0
  70. package/dist/layout-engine/types.d.ts +22 -2
  71. package/dist/layout-painter/renderImage.d.ts +12 -1
  72. package/dist/layout-painter/renderImage.js +21 -4
  73. package/dist/layout-painter/renderPage.js +3 -2
  74. package/dist/layout-painter/renderParagraph.js +33 -9
  75. package/dist/layout-painter/renderTable.js +2 -1
  76. package/dist/layout-painter/renderWatermark.d.ts +1 -1
  77. package/dist/layout-painter/renderWatermark.js +2 -1
  78. package/dist/markdown/fromMarkdown.js +1 -1
  79. package/dist/prosemirror/attrs/index.d.ts +1 -1
  80. package/dist/prosemirror/attrs/index.js +21 -0
  81. package/dist/prosemirror/commands/comments.d.ts +76 -1
  82. package/dist/prosemirror/commands/comments.js +401 -28
  83. package/dist/prosemirror/commands/hyperlink.js +15 -1
  84. package/dist/prosemirror/commands/image.js +7 -1
  85. package/dist/prosemirror/commands/index.d.ts +2 -2
  86. package/dist/prosemirror/commands/index.js +2 -2
  87. package/dist/prosemirror/commands/tableCellMergeResolution.js +1 -1
  88. package/dist/prosemirror/commentIdAllocator.js +13 -3
  89. package/dist/prosemirror/conversion/fromProseDoc.js +122 -5
  90. package/dist/prosemirror/conversion/index.js +1 -1
  91. package/dist/prosemirror/conversion/toProseDoc.js +20 -8
  92. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -1
  93. package/dist/prosemirror/extensions/features/ImagePasteExtension.js +10 -2
  94. package/dist/prosemirror/extensions/features/pasteCleanup.js +15 -2
  95. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +30 -4
  96. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +49 -13
  97. package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
  98. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -3
  99. package/dist/prosemirror/index.d.ts +1 -1
  100. package/dist/prosemirror/index.js +1 -1
  101. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +2 -2
  102. package/dist/prosemirror/plugins/contentControlWidgets.js +4 -4
  103. package/dist/prosemirror/plugins/revisionIds.d.ts +23 -0
  104. package/dist/prosemirror/plugins/revisionIds.js +57 -0
  105. package/dist/prosemirror/plugins/suggestionMode.js +4 -3
  106. package/dist/prosemirror/runFormattingMarkNames.d.ts +14 -0
  107. package/dist/prosemirror/runFormattingMarkNames.js +40 -0
  108. package/dist/prosemirror/schema/index.d.ts +1 -1
  109. package/dist/prosemirror/schema/marks.d.ts +27 -4
  110. package/dist/prosemirror/schema/marks.js +15 -0
  111. package/dist/prosemirror/schema/nodes.d.ts +50 -2
  112. package/dist/render-dom/RemoteSelectionOverlay.js +2 -2
  113. package/dist/server.d.ts +2 -2
  114. package/dist/server.js +3 -3
  115. package/dist/utils/clipboard.d.ts +10 -1
  116. package/dist/utils/clipboard.js +33 -6
  117. package/dist/utils/colorResolver.d.ts +8 -1
  118. package/dist/utils/colorResolver.js +35 -5
  119. package/dist/utils/fontResolver.d.ts +2 -1
  120. package/dist/utils/fontResolver.js +9 -3
  121. package/dist/utils/hexId.d.ts +11 -1
  122. package/dist/utils/hexId.js +11 -1
  123. package/dist/utils/sanitizeImageSrc.d.ts +21 -0
  124. package/dist/utils/sanitizeImageSrc.js +30 -0
  125. package/dist/utils/stripXmlDeclarations.d.ts +11 -0
  126. package/dist/utils/stripXmlDeclarations.js +35 -0
  127. package/dist/version-comparison.d.ts +22 -2
  128. package/dist/version-comparison.js +41 -12
  129. package/dist/watermark/index.d.ts +11 -1
  130. package/dist/watermark/index.js +22 -1
  131. package/package.json +3 -2
@@ -1,9 +1,11 @@
1
1
  import { numPrEqual } from "../numberingParser.js";
2
- import { escapeXml, intAttr } from "./xmlUtils.js";
2
+ import { isValidHexColor } from "../../utils/colorResolver.js";
3
+ import { escapeXml, intAttr, isSingleWellFormedElement } from "./xmlUtils.js";
3
4
  import { reconcileRawSdtPr } from "../sdtPropertiesPatch.js";
4
5
  import { serializeBorder } from "./borderSerializer.js";
5
6
  import { serializeRun, serializeTextFormatting } from "./runSerializer.js";
6
7
  import { serializeSectionProperties } from "./sectionPropertiesSerializer.js";
8
+ import { normalizeRevisionId } from "@stll/docx-core/model";
7
9
  //#region src/docx/serializer/paragraphSerializer.ts
8
10
  /**
9
11
  * Serialize paragraph borders (w:pBdr)
@@ -44,15 +46,15 @@ function serializeParagraphBorders(borders) {
44
46
  function serializeShading(shading) {
45
47
  if (!shading) return "";
46
48
  const attrs = [];
47
- if (shading.pattern) attrs.push(`w:val="${shading.pattern}"`);
49
+ if (shading.pattern) attrs.push(`w:val="${escapeXml(shading.pattern)}"`);
48
50
  else attrs.push("w:val=\"clear\"");
49
- if (shading.color?.rgb) attrs.push(`w:color="${shading.color.rgb}"`);
51
+ if (shading.color?.rgb && isValidHexColor(shading.color.rgb)) attrs.push(`w:color="${escapeXml(shading.color.rgb)}"`);
50
52
  else if (shading.color?.auto) attrs.push("w:color=\"auto\"");
51
- if (shading.fill?.rgb) attrs.push(`w:fill="${shading.fill.rgb}"`);
53
+ if (shading.fill?.rgb && isValidHexColor(shading.fill.rgb)) attrs.push(`w:fill="${escapeXml(shading.fill.rgb)}"`);
52
54
  else if (shading.fill?.auto) attrs.push("w:fill=\"auto\"");
53
- if (shading.fill?.themeColor) attrs.push(`w:themeFill="${shading.fill.themeColor}"`);
54
- if (shading.fill?.themeTint) attrs.push(`w:themeFillTint="${shading.fill.themeTint}"`);
55
- if (shading.fill?.themeShade) attrs.push(`w:themeFillShade="${shading.fill.themeShade}"`);
55
+ if (shading.fill?.themeColor) attrs.push(`w:themeFill="${escapeXml(shading.fill.themeColor)}"`);
56
+ if (shading.fill?.themeTint) attrs.push(`w:themeFillTint="${escapeXml(shading.fill.themeTint)}"`);
57
+ if (shading.fill?.themeShade) attrs.push(`w:themeFillShade="${escapeXml(shading.fill.themeShade)}"`);
56
58
  if (attrs.length === 0) return "";
57
59
  return `<w:shd ${attrs.join(" ")}/>`;
58
60
  }
@@ -132,7 +134,7 @@ function serializeFrameProperties(frame) {
132
134
  * Serialize paragraph formatting properties to w:pPr XML
133
135
  */
134
136
  function serializeTrackedChangeAttrs(info) {
135
- const parts = [`w:id="${info.id}"`, `w:author="${escapeXml(info.author)}"`];
137
+ const parts = [`w:id="${normalizeRevisionId(info.id)}"`, `w:author="${escapeXml(info.author)}"`];
136
138
  if (info.date !== void 0) parts.push(`w:date="${escapeXml(info.date)}"`);
137
139
  return parts.join(" ");
138
140
  }
@@ -198,7 +200,7 @@ function extractRPrInner(rPrXml) {
198
200
  return rPrXml.slice(7, -8);
199
201
  }
200
202
  function serializeParagraphPropertyChange(change) {
201
- const normalizedId = Number.isInteger(change.info.id) && change.info.id >= 0 ? change.info.id : 0;
203
+ const normalizedId = normalizeRevisionId(change.info.id);
202
204
  const authorCandidate = typeof change.info.author === "string" ? change.info.author.trim() : "";
203
205
  const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
204
206
  const normalizedDate = typeof change.info.date === "string" ? change.info.date.trim() : void 0;
@@ -361,13 +363,13 @@ function serializeInlineSdt(sdt) {
361
363
  default: return item;
362
364
  }
363
365
  }).join("");
364
- const baseSdtPr = props.rawPropertiesXml ?? synthesizeInlineSdtPr(props);
366
+ const baseSdtPr = props.rawPropertiesXml && isSingleWellFormedElement(props.rawPropertiesXml, "sdtPr") ? props.rawPropertiesXml : synthesizeInlineSdtPr(props);
365
367
  const dateFullDate = props.sdtType === "date" && props.dateValueISO ? props.dateValueISO : void 0;
366
368
  const dropdownLastValue = (props.sdtType === "dropdown" || props.sdtType === "comboBox") && typeof props.dropdownLastValue === "string" ? props.dropdownLastValue : void 0;
367
369
  return `<w:sdt>${reconcileRawSdtPr(baseSdtPr, props, {
368
370
  ...dateFullDate !== void 0 ? { dateFullDate } : {},
369
371
  ...dropdownLastValue !== void 0 ? { dropdownLastValue } : {}
370
- })}${props.rawEndPropertiesXml ?? ""}<w:sdtContent>${contentXml}</w:sdtContent></w:sdt>`;
372
+ })}${props.rawEndPropertiesXml && isSingleWellFormedElement(props.rawEndPropertiesXml, "sdtEndPr") ? props.rawEndPropertiesXml : ""}<w:sdtContent>${contentXml}</w:sdtContent></w:sdt>`;
371
373
  }
372
374
  function serializeMoveRangeStart(tag, marker) {
373
375
  return `<w:${tag} ${[`w:id="${marker.id}"`, `w:name="${escapeXml(marker.name)}"`].join(" ")}/>`;
@@ -380,7 +382,7 @@ function rewriteRunTextAsDeleted(xml) {
380
382
  }
381
383
  function serializeTrackedChange(tag, change) {
382
384
  const info = change.info;
383
- const normalizedId = Number.isInteger(info.id) && info.id >= 0 ? info.id : 0;
385
+ const normalizedId = normalizeRevisionId(info.id);
384
386
  const authorCandidate = typeof info.author === "string" ? info.author.trim() : "";
385
387
  const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
386
388
  const normalizedDate = typeof info.date === "string" ? info.date.trim() : void 0;
@@ -455,8 +457,8 @@ function serializeParagraphContent(content, explicitCommentReferenceIds) {
455
457
  function serializeParagraph(paragraph) {
456
458
  const parts = [];
457
459
  const attrs = [];
458
- if (paragraph.paraId) attrs.push(`w14:paraId="${paragraph.paraId}"`);
459
- if (paragraph.textId) attrs.push(`w14:textId="${paragraph.textId}"`);
460
+ if (paragraph.paraId) attrs.push(`w14:paraId="${escapeXml(paragraph.paraId)}"`);
461
+ if (paragraph.textId) attrs.push(`w14:textId="${escapeXml(paragraph.textId)}"`);
460
462
  const attrsStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
461
463
  const pPrXml = serializeParagraphFormatting(paragraph.formatting, paragraph.propertyChanges, paragraph.pPrMark);
462
464
  const sectionPropertiesXml = serializeSectionProperties(paragraph.sectionProperties);
@@ -1,8 +1,10 @@
1
1
  import { HIGHLIGHT_COLOR_VALUES } from "../../types/documentEnumValues.js";
2
+ import { isValidHexColor } from "../../utils/colorResolver.js";
2
3
  import { escapeXml, intAttr } from "./xmlUtils.js";
3
4
  import { serializeTable } from "./tableSerializer.js";
4
5
  import { serializeParagraph } from "./paragraphSerializer.js";
5
6
  import { panic } from "better-result";
7
+ import { normalizeRevisionId } from "@stll/docx-core/model";
6
8
  //#region src/docx/serializer/runSerializer.ts
7
9
  /**
8
10
  * Run Serializer - Serialize runs to OOXML XML
@@ -42,10 +44,10 @@ function serializeColorElement(color) {
42
44
  if (!color) return "";
43
45
  const attrs = [];
44
46
  if (color.auto) attrs.push("w:val=\"auto\"");
45
- else if (color.rgb) attrs.push(`w:val="${color.rgb}"`);
46
- if (color.themeColor) attrs.push(`w:themeColor="${color.themeColor}"`);
47
- if (color.themeTint) attrs.push(`w:themeTint="${color.themeTint}"`);
48
- if (color.themeShade) attrs.push(`w:themeShade="${color.themeShade}"`);
47
+ else if (color.rgb && isValidHexColor(color.rgb)) attrs.push(`w:val="${escapeXml(color.rgb)}"`);
48
+ if (color.themeColor) attrs.push(`w:themeColor="${escapeXml(color.themeColor)}"`);
49
+ if (color.themeTint) attrs.push(`w:themeTint="${escapeXml(color.themeTint)}"`);
50
+ if (color.themeShade) attrs.push(`w:themeShade="${escapeXml(color.themeShade)}"`);
49
51
  if (attrs.length === 0) return "";
50
52
  return `<w:color ${attrs.join(" ")}/>`;
51
53
  }
@@ -55,15 +57,15 @@ function serializeColorElement(color) {
55
57
  function serializeShading(shading) {
56
58
  if (!shading) return "";
57
59
  const attrs = [];
58
- if (shading.pattern) attrs.push(`w:val="${shading.pattern}"`);
60
+ if (shading.pattern) attrs.push(`w:val="${escapeXml(shading.pattern)}"`);
59
61
  else attrs.push("w:val=\"clear\"");
60
- if (shading.color?.rgb) attrs.push(`w:color="${shading.color.rgb}"`);
62
+ if (shading.color?.rgb && isValidHexColor(shading.color.rgb)) attrs.push(`w:color="${escapeXml(shading.color.rgb)}"`);
61
63
  else if (shading.color?.auto) attrs.push("w:color=\"auto\"");
62
- if (shading.fill?.rgb) attrs.push(`w:fill="${shading.fill.rgb}"`);
64
+ if (shading.fill?.rgb && isValidHexColor(shading.fill.rgb)) attrs.push(`w:fill="${escapeXml(shading.fill.rgb)}"`);
63
65
  else if (shading.fill?.auto) attrs.push("w:fill=\"auto\"");
64
- if (shading.fill?.themeColor) attrs.push(`w:themeFill="${shading.fill.themeColor}"`);
65
- if (shading.fill?.themeTint) attrs.push(`w:themeFillTint="${shading.fill.themeTint}"`);
66
- if (shading.fill?.themeShade) attrs.push(`w:themeFillShade="${shading.fill.themeShade}"`);
66
+ if (shading.fill?.themeColor) attrs.push(`w:themeFill="${escapeXml(shading.fill.themeColor)}"`);
67
+ if (shading.fill?.themeTint) attrs.push(`w:themeFillTint="${escapeXml(shading.fill.themeTint)}"`);
68
+ if (shading.fill?.themeShade) attrs.push(`w:themeFillShade="${escapeXml(shading.fill.themeShade)}"`);
67
69
  if (attrs.length === 0) return "";
68
70
  return `<w:shd ${attrs.join(" ")}/>`;
69
71
  }
@@ -137,10 +139,10 @@ function serializeTextFormatting(formatting) {
137
139
  if (formatting.underline) {
138
140
  const uAttrs = [`w:val="${formatting.underline.style}"`];
139
141
  if (formatting.underline.color) {
140
- if (formatting.underline.color.rgb) uAttrs.push(`w:color="${formatting.underline.color.rgb}"`);
141
- if (formatting.underline.color.themeColor) uAttrs.push(`w:themeColor="${formatting.underline.color.themeColor}"`);
142
- if (formatting.underline.color.themeTint) uAttrs.push(`w:themeTint="${formatting.underline.color.themeTint}"`);
143
- if (formatting.underline.color.themeShade) uAttrs.push(`w:themeShade="${formatting.underline.color.themeShade}"`);
142
+ if (formatting.underline.color.rgb && isValidHexColor(formatting.underline.color.rgb)) uAttrs.push(`w:color="${escapeXml(formatting.underline.color.rgb)}"`);
143
+ if (formatting.underline.color.themeColor) uAttrs.push(`w:themeColor="${escapeXml(formatting.underline.color.themeColor)}"`);
144
+ if (formatting.underline.color.themeTint) uAttrs.push(`w:themeTint="${escapeXml(formatting.underline.color.themeTint)}"`);
145
+ if (formatting.underline.color.themeShade) uAttrs.push(`w:themeShade="${escapeXml(formatting.underline.color.themeShade)}"`);
144
146
  }
145
147
  parts.push(`<w:u ${uAttrs.join(" ")}/>`);
146
148
  }
@@ -160,7 +162,7 @@ function extractRPrInner(rPrXml) {
160
162
  return rPrXml.slice(7, -8);
161
163
  }
162
164
  function serializeRunPropertyChange(change) {
163
- const normalizedId = Number.isInteger(change.info.id) && change.info.id >= 0 ? change.info.id : 0;
165
+ const normalizedId = normalizeRevisionId(change.info.id);
164
166
  const authorCandidate = typeof change.info.author === "string" ? change.info.author.trim() : "";
165
167
  const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
166
168
  const normalizedDate = typeof change.info.date === "string" ? change.info.date.trim() : void 0;
@@ -246,11 +248,11 @@ function serializeNoBreakHyphen(_content) {
246
248
  /** Serialize a color value to DrawingML a:srgbClr or a:schemeClr */
247
249
  function serializeDrawingColor(color) {
248
250
  if (!color) return "";
249
- if (color.rgb) return `<a:srgbClr val="${color.rgb.replace("#", "")}"/>`;
251
+ if (color.rgb && isValidHexColor(color.rgb)) return `<a:srgbClr val="${escapeXml(color.rgb.replace("#", ""))}"/>`;
250
252
  if (color.themeColor) {
251
- let clr = `<a:schemeClr val="${color.themeColor}"`;
252
- if (color.themeTint) clr += `><a:tint val="${color.themeTint}"/></a:schemeClr>`;
253
- else if (color.themeShade) clr += `><a:shade val="${color.themeShade}"/></a:schemeClr>`;
253
+ let clr = `<a:schemeClr val="${escapeXml(color.themeColor)}"`;
254
+ if (color.themeTint) clr += `><a:tint val="${escapeXml(color.themeTint)}"/></a:schemeClr>`;
255
+ else if (color.themeShade) clr += `><a:shade val="${escapeXml(color.themeShade)}"/></a:schemeClr>`;
254
256
  else clr += `/>`;
255
257
  return clr;
256
258
  }
@@ -1,6 +1,7 @@
1
1
  import { getUnserializedSectionPropertyChildNames } from "../sectionParser.js";
2
2
  import { escapeXml, intAttr } from "./xmlUtils.js";
3
3
  import { serializeBorder } from "./borderSerializer.js";
4
+ import { normalizeRevisionId } from "@stll/docx-core/model";
4
5
  //#region src/docx/serializer/sectionPropertiesSerializer.ts
5
6
  const serializeHeaderReference = (ref) => `<w:headerReference w:type="${ref.type}" r:id="${ref.rId}"/>`;
6
7
  const serializeFooterReference = (ref) => `<w:footerReference w:type="${ref.type}" r:id="${ref.rId}"/>`;
@@ -132,7 +133,7 @@ function serializeOnOffElement(value, name) {
132
133
  return value ? `<w:${name}/>` : `<w:${name} w:val="0"/>`;
133
134
  }
134
135
  function serializeSectionPropertyChange(change) {
135
- const normalizedId = Number.isInteger(change.info.id) && change.info.id >= 0 ? change.info.id : 0;
136
+ const normalizedId = normalizeRevisionId(change.info.id);
136
137
  const authorCandidate = typeof change.info.author === "string" ? change.info.author.trim() : "";
137
138
  const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
138
139
  const normalizedDate = typeof change.info.date === "string" ? change.info.date.trim() : void 0;
@@ -1,8 +1,10 @@
1
+ import { isValidHexColor } from "../../utils/colorResolver.js";
1
2
  import { escapeXml, intAttr } from "./xmlUtils.js";
2
3
  import { serializeBorder } from "./borderSerializer.js";
4
+ import { normalizeRevisionId } from "@stll/docx-core/model";
3
5
  //#region src/docx/serializer/tableSerializer.ts
4
6
  function normalizeTrackedChangeInfo(info) {
5
- const normalizedId = Number.isInteger(info.id) && info.id >= 0 ? info.id : 0;
7
+ const normalizedId = normalizeRevisionId(info.id);
6
8
  const authorCandidate = typeof info.author === "string" ? info.author.trim() : "";
7
9
  const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
8
10
  const normalizedDate = typeof info.date === "string" ? info.date.trim() : void 0;
@@ -77,15 +79,15 @@ function serializeCellMargins(margins, elementName) {
77
79
  function serializeShading(shading) {
78
80
  if (!shading) return "";
79
81
  const attrs = [];
80
- if (shading.pattern) attrs.push(`w:val="${shading.pattern}"`);
82
+ if (shading.pattern) attrs.push(`w:val="${escapeXml(shading.pattern)}"`);
81
83
  else attrs.push("w:val=\"clear\"");
82
- if (shading.color?.rgb) attrs.push(`w:color="${shading.color.rgb}"`);
84
+ if (shading.color?.rgb && isValidHexColor(shading.color.rgb)) attrs.push(`w:color="${escapeXml(shading.color.rgb)}"`);
83
85
  else if (shading.color?.auto) attrs.push("w:color=\"auto\"");
84
- if (shading.fill?.rgb) attrs.push(`w:fill="${shading.fill.rgb}"`);
86
+ if (shading.fill?.rgb && isValidHexColor(shading.fill.rgb)) attrs.push(`w:fill="${escapeXml(shading.fill.rgb)}"`);
85
87
  else if (shading.fill?.auto) attrs.push("w:fill=\"auto\"");
86
- if (shading.fill?.themeColor) attrs.push(`w:themeFill="${shading.fill.themeColor}"`);
87
- if (shading.fill?.themeTint) attrs.push(`w:themeFillTint="${shading.fill.themeTint}"`);
88
- if (shading.fill?.themeShade) attrs.push(`w:themeFillShade="${shading.fill.themeShade}"`);
88
+ if (shading.fill?.themeColor) attrs.push(`w:themeFill="${escapeXml(shading.fill.themeColor)}"`);
89
+ if (shading.fill?.themeTint) attrs.push(`w:themeFillTint="${escapeXml(shading.fill.themeTint)}"`);
90
+ if (shading.fill?.themeShade) attrs.push(`w:themeFillShade="${escapeXml(shading.fill.themeShade)}"`);
89
91
  if (attrs.length === 0) return "";
90
92
  return `<w:shd ${attrs.join(" ")}/>`;
91
93
  }
@@ -16,5 +16,17 @@ declare function escapeXml(text: string): string;
16
16
  * literal `"NaN"` or `"Infinity"` into the XML.
17
17
  */
18
18
  declare function intAttr(value: number | undefined | null): string;
19
+ /**
20
+ * Verify that `xml` parses to exactly one root element named `expectedLocalName`
21
+ * (namespace prefix ignored). Used to gate raw XML snapshots — e.g. an SDT's
22
+ * `rawPropertiesXml`/`rawEndPropertiesXml` — before splicing them verbatim
23
+ * into a serialized document. Those snapshots are normally produced by our
24
+ * own parser, but they can also arrive from an untrusted surface (a
25
+ * programmatically constructed node, a collaboration payload); a value that
26
+ * is not a single well-formed `<w:sdtPr>`/`<w:sdtEndPr>` element could inject
27
+ * sibling markup or close the enclosing `<w:sdt>` early. Callers should fall
28
+ * back to a synthesized properties block when this returns `false`.
29
+ */
30
+ declare function isSingleWellFormedElement(xml: string, expectedLocalName: string): boolean;
19
31
  //#endregion
20
- export { escapeXml, intAttr };
32
+ export { escapeXml, intAttr, isSingleWellFormedElement };
@@ -1,3 +1,4 @@
1
+ import { getLocalName, parseXml } from "../xmlParser.js";
1
2
  //#region src/docx/serializer/xmlUtils.ts
2
3
  /**
3
4
  * Shared XML utility functions for serializers.
@@ -21,5 +22,30 @@ function intAttr(value) {
21
22
  if (value == null || !Number.isFinite(value)) return "0";
22
23
  return String(Math.round(value));
23
24
  }
25
+ /**
26
+ * Verify that `xml` parses to exactly one root element named `expectedLocalName`
27
+ * (namespace prefix ignored). Used to gate raw XML snapshots — e.g. an SDT's
28
+ * `rawPropertiesXml`/`rawEndPropertiesXml` — before splicing them verbatim
29
+ * into a serialized document. Those snapshots are normally produced by our
30
+ * own parser, but they can also arrive from an untrusted surface (a
31
+ * programmatically constructed node, a collaboration payload); a value that
32
+ * is not a single well-formed `<w:sdtPr>`/`<w:sdtEndPr>` element could inject
33
+ * sibling markup or close the enclosing `<w:sdt>` early. Callers should fall
34
+ * back to a synthesized properties block when this returns `false`.
35
+ */
36
+ function isSingleWellFormedElement(xml, expectedLocalName) {
37
+ const trimmed = xml.trim();
38
+ if (!trimmed) return false;
39
+ let parsed;
40
+ try {
41
+ parsed = parseXml(trimmed);
42
+ } catch {
43
+ return false;
44
+ }
45
+ const roots = (parsed.elements ?? []).filter((element) => element.type === "element");
46
+ if (roots.length !== 1) return false;
47
+ const root = roots[0];
48
+ return root?.name !== void 0 && getLocalName(root.name) === expectedLocalName;
49
+ }
24
50
  //#endregion
25
- export { escapeXml, intAttr };
51
+ export { escapeXml, intAttr, isSingleWellFormedElement };
@@ -1,7 +1,8 @@
1
- import { findAllDeep, findChild, findDeep, getAttributeAnyPrefix, getLocalName, getTextContent, parseXml } from "../xmlParser.js";
1
+ import { findAllDeep, findChild, findDeep, getAttribute, getAttributeAnyPrefix, getLocalName, getTextContent, parseXml } from "../xmlParser.js";
2
+ import { RELATIONSHIP_TYPES, parseRelationships } from "../relsParser.js";
2
3
  import { loadDocxArchive } from "./boundedArchive.js";
3
4
  //#region src/docx/server/extractDocxText.ts
4
- const HEADER_FOOTER_PATH = /^word\/(?:header|footer)\d+\.xml$/u;
5
+ const DOCUMENT_RELS_PATH = "word/_rels/document.xml.rels";
5
6
  const childElements = (element) => element.elements?.filter((child) => child.type === "element") ?? [];
6
7
  const collectText = (element) => {
7
8
  let text = "";
@@ -86,12 +87,10 @@ const extractContainer = ({ container, source, startIndex }) => {
86
87
  charCount
87
88
  };
88
89
  };
89
- const extractParts = async ({ archive, source, rootName, startIndex }) => {
90
+ const extractParts = async ({ archive, source, rootName, startIndex, paths }) => {
90
91
  const paragraphs = [];
91
92
  let charCount = 0;
92
93
  let nextIndex = startIndex;
93
- const prefix = `word/${source}`;
94
- const paths = archive.entries.filter((path) => HEADER_FOOTER_PATH.test(path) && path.startsWith(prefix)).toSorted();
95
94
  for (const path of paths) {
96
95
  const xml = await archive.readEntryString(path);
97
96
  if (xml === null) continue;
@@ -111,6 +110,50 @@ const extractParts = async ({ archive, source, rootName, startIndex }) => {
111
110
  charCount
112
111
  };
113
112
  };
113
+ /** A `word/_rels/document.xml.rels` `Target` is relative to `word/`; resolve it to a full archive-entry path. */
114
+ const resolveWordPartPath = (target) => target.startsWith("/") ? target.slice(1) : `word/${target}`;
115
+ /**
116
+ * Resolve the header/footer parts actually wired into the document via
117
+ * `word/_rels/document.xml.rels` + each section's `w:headerReference` /
118
+ * `w:footerReference`, instead of extracting every `word/header*.xml` /
119
+ * `word/footer*.xml` entry by filename. A DOCX can carry an orphaned
120
+ * header/footer part (stale, or planted by an attacker) that no section
121
+ * references — reading it unconditionally would surface prompt-injection or
122
+ * stale content that Word itself never renders.
123
+ */
124
+ const resolveReferencedHeaderFooterParts = async (archive, documentRoot) => {
125
+ const relsXml = await archive.readEntryString(DOCUMENT_RELS_PATH);
126
+ if (relsXml === null) return {
127
+ headers: [],
128
+ footers: []
129
+ };
130
+ const relationships = parseRelationships(relsXml);
131
+ const headerRIds = /* @__PURE__ */ new Set();
132
+ const footerRIds = /* @__PURE__ */ new Set();
133
+ for (const sectPr of findAllDeep(documentRoot, "w", "sectPr")) {
134
+ for (const ref of findAllDeep(sectPr, "w", "headerReference")) {
135
+ const rId = getAttribute(ref, "r", "id");
136
+ if (rId !== null) headerRIds.add(rId);
137
+ }
138
+ for (const ref of findAllDeep(sectPr, "w", "footerReference")) {
139
+ const rId = getAttribute(ref, "r", "id");
140
+ if (rId !== null) footerRIds.add(rId);
141
+ }
142
+ }
143
+ const resolvePaths = (rIds, relationshipType) => {
144
+ const paths = /* @__PURE__ */ new Set();
145
+ for (const rId of rIds) {
146
+ const relationship = relationships.get(rId);
147
+ if (!relationship || relationship.type !== relationshipType || relationship.targetMode === "External") continue;
148
+ paths.add(resolveWordPartPath(relationship.target));
149
+ }
150
+ return [...paths].toSorted();
151
+ };
152
+ return {
153
+ headers: resolvePaths(headerRIds, RELATIONSHIP_TYPES.header),
154
+ footers: resolvePaths(footerRIds, RELATIONSHIP_TYPES.footer)
155
+ };
156
+ };
114
157
  const createEmptyResult = () => ({
115
158
  paragraphs: [],
116
159
  charCount: 0,
@@ -121,13 +164,16 @@ const extractDocxText = async (bytes) => {
121
164
  const archive = await loadDocxArchive(bytes);
122
165
  const documentXml = await archive.readEntryString("word/document.xml");
123
166
  if (documentXml === null) return createEmptyResult();
124
- const body = findDeep(parseXml(documentXml), "w", "body");
167
+ const root = parseXml(documentXml);
168
+ const body = findDeep(root, "w", "body");
125
169
  if (!body) return createEmptyResult();
170
+ const referencedParts = await resolveReferencedHeaderFooterParts(archive, root);
126
171
  const headers = await extractParts({
127
172
  archive,
128
173
  source: "header",
129
174
  rootName: "hdr",
130
- startIndex: 0
175
+ startIndex: 0,
176
+ paths: referencedParts.headers
131
177
  });
132
178
  const bodyResult = extractContainer({
133
179
  container: body,
@@ -138,7 +184,8 @@ const extractDocxText = async (bytes) => {
138
184
  archive,
139
185
  source: "footer",
140
186
  rootName: "ftr",
141
- startIndex: headers.paragraphs.length + bodyResult.paragraphs.length
187
+ startIndex: headers.paragraphs.length + bodyResult.paragraphs.length,
188
+ paths: referencedParts.footers
142
189
  });
143
190
  return {
144
191
  paragraphs: [
@@ -10,6 +10,19 @@ const DEFAULT_TAB_STOP_TWIPS = 720;
10
10
  const MAX_TAB_STOP_TWIPS = 31680;
11
11
  const MAX_HYPHENATION_ZONE_TWIPS = 31680;
12
12
  const MAX_CONSECUTIVE_HYPHEN_LIMIT = 255;
13
+ /**
14
+ * Floor on `w:defaultTabStop`, in twips (1/12 inch). A near-zero value would
15
+ * force the layout engine to generate thousands of tab stops across a
16
+ * single line; below this floor we substitute the OOXML default instead.
17
+ */
18
+ const MIN_TAB_STOP_TWIPS = 120;
19
+ /**
20
+ * Sanity cap on parsed `w:noLineBreaksBefore`/`w:noLineBreaksAfter`
21
+ * character lists. Typical kinsoku overrides list a handful of prohibited
22
+ * characters; anything past this is corruption or a hostile input crafted
23
+ * to inflate the per-character membership set built at measure time.
24
+ */
25
+ const MAX_KINSOKU_CHARACTERS_LENGTH = 128;
13
26
  function parseSettings(xml) {
14
27
  const root = xml ? parseXmlDocument(xml) : null;
15
28
  const settings = { defaultTabStop: parseDefaultTabStop(root) };
@@ -76,7 +89,7 @@ function parseKinsokuOverride(root, name) {
76
89
  if (!characters) return;
77
90
  const language = getAttribute(element, "w", "lang") || void 0;
78
91
  return {
79
- characters,
92
+ characters: Array.from(characters).slice(0, MAX_KINSOKU_CHARACTERS_LENGTH).join(""),
80
93
  ...language ? { language } : {}
81
94
  };
82
95
  }
@@ -87,7 +100,7 @@ function parseDefaultTabStop(root) {
87
100
  const raw = getAttribute(el, "w", "val");
88
101
  if (raw === null) return 720;
89
102
  const parsed = Number.parseInt(raw, 10);
90
- if (!Number.isFinite(parsed) || parsed <= 0 || parsed > MAX_TAB_STOP_TWIPS) return 720;
103
+ if (!Number.isFinite(parsed) || parsed < MIN_TAB_STOP_TWIPS || parsed > MAX_TAB_STOP_TWIPS) return 720;
91
104
  return parsed;
92
105
  }
93
106
  //#endregion
@@ -4,6 +4,13 @@ import { StyleMap } from "./styleParser.js";
4
4
  import { XmlElement } from "./xmlParser.js";
5
5
 
6
6
  //#region src/docx/tableParser.d.ts
7
+ /**
8
+ * Sanity cap on `w:gridSpan` (and the derived table column count). Word's
9
+ * practical column limit is 63; a hostile/corrupt value here would blow up
10
+ * every downstream structure sized by column count (TableMap, border grids,
11
+ * cell-grid arrays).
12
+ */
13
+ declare const MAX_TABLE_COLUMNS = 63;
7
14
  /**
8
15
  * Parse a table measurement (width, height, etc.)
9
16
  *
@@ -199,4 +206,4 @@ declare function getHeaderRows(table: document_d_exports.Table): document_d_expo
199
206
  */
200
207
  declare function isFloatingTable(table: document_d_exports.Table): boolean;
201
208
  //#endregion
202
- export { getHeaderRows, getTableColumnCount, getTableRowCount, getTableText, hasHeaderRow, isCellHorizontallyMerged, isCellMergeContinuation, isCellMergeStart, isFloatingTable, parseBorderSpec, parseCellMargins, parseConditionalFormatStyle, parseFloatingTableProperties, parseShading, parseTable, parseTableBorders, parseTableCell, parseTableCellProperties, parseTableGrid, parseTableLook, parseTableMeasurement, parseTableProperties, parseTableRow, parseTableRowProperties };
209
+ export { MAX_TABLE_COLUMNS, getHeaderRows, getTableColumnCount, getTableRowCount, getTableText, hasHeaderRow, isCellHorizontallyMerged, isCellMergeContinuation, isCellMergeStart, isFloatingTable, parseBorderSpec, parseCellMargins, parseConditionalFormatStyle, parseFloatingTableProperties, parseShading, parseTable, parseTableBorders, parseTableCell, parseTableCellProperties, parseTableGrid, parseTableLook, parseTableMeasurement, parseTableProperties, parseTableRow, parseTableRowProperties };
@@ -4,8 +4,16 @@ import { parseBookmarkEnd, parseBookmarkStart } from "./bookmarkParser.js";
4
4
  import { parseParagraph } from "./paragraphParser.js";
5
5
  import { appendBookmarkMarkerToLastParagraphInBlocks, appendBookmarkMarkerToLastParagraphInCells, prependBookmarkMarkersToFirstParagraphInBlocks, prependBookmarkMarkersToFirstParagraphInCell } from "./bookmarkPlacement.js";
6
6
  import { enrichParagraphTextBoxes } from "./paragraphTextBoxEnrichment.js";
7
+ import { normalizeRevisionId } from "@stll/docx-core/model";
7
8
  //#region src/docx/tableParser.ts
8
9
  /**
10
+ * Sanity cap on `w:gridSpan` (and the derived table column count). Word's
11
+ * practical column limit is 63; a hostile/corrupt value here would blow up
12
+ * every downstream structure sized by column count (TableMap, border grids,
13
+ * cell-grid arrays).
14
+ */
15
+ const MAX_TABLE_COLUMNS = 63;
16
+ /**
9
17
  * Parse a table measurement (width, height, etc.)
10
18
  *
11
19
  * @param element - Element with w:w and w:type attributes
@@ -32,11 +40,13 @@ function parseTrackedChangeInfo(node) {
32
40
  const parsedId = rawId ? Number.parseInt(rawId, 10) : 0;
33
41
  const author = (getAttribute(node, "w", "author") ?? "").trim();
34
42
  const date = (getAttribute(node, "w", "date") ?? "").trim();
43
+ const initials = (getAttribute(node, "w", "initials") ?? "").trim();
35
44
  const info = {
36
- id: Number.isInteger(parsedId) && parsedId >= 0 ? parsedId : 0,
45
+ id: normalizeRevisionId(parsedId),
37
46
  author: author.length > 0 ? author : "Unknown"
38
47
  };
39
48
  if (date.length > 0) info.date = date;
49
+ if (initials.length > 0) info.initials = initials;
40
50
  return info;
41
51
  }
42
52
  function parsePropertyChangeInfo(node) {
@@ -485,7 +495,7 @@ function parseTableCellProperties(tcPrElement) {
485
495
  const gridSpanElement = findChild(tcPrElement, "w", "gridSpan");
486
496
  if (gridSpanElement) {
487
497
  const gridSpan = parseNumericAttribute(gridSpanElement, "w", "val");
488
- if (gridSpan !== void 0 && gridSpan > 1) formatting.gridSpan = gridSpan;
498
+ if (gridSpan !== void 0 && gridSpan > 1) formatting.gridSpan = Math.min(gridSpan, 63);
489
499
  }
490
500
  const vMergeElement = findChild(tcPrElement, "w", "vMerge");
491
501
  if (vMergeElement) if (getAttribute(vMergeElement, "w", "val") === "restart") formatting.vMerge = "restart";
@@ -838,4 +848,4 @@ function isFloatingTable(table) {
838
848
  return table.formatting?.floating !== void 0;
839
849
  }
840
850
  //#endregion
841
- export { getHeaderRows, getTableColumnCount, getTableRowCount, getTableText, hasHeaderRow, isCellHorizontallyMerged, isCellMergeContinuation, isCellMergeStart, isFloatingTable, parseBorderSpec, parseCellMargins, parseConditionalFormatStyle, parseFloatingTableProperties, parseShading, parseTable, parseTableBorders, parseTableCell, parseTableCellProperties, parseTableGrid, parseTableLook, parseTableMeasurement, parseTableProperties, parseTableRow, parseTableRowProperties };
851
+ export { MAX_TABLE_COLUMNS, getHeaderRows, getTableColumnCount, getTableRowCount, getTableText, hasHeaderRow, isCellHorizontallyMerged, isCellMergeContinuation, isCellMergeStart, isFloatingTable, parseBorderSpec, parseCellMargins, parseConditionalFormatStyle, parseFloatingTableProperties, parseShading, parseTable, parseTableBorders, parseTableCell, parseTableCellProperties, parseTableGrid, parseTableLook, parseTableMeasurement, parseTableProperties, parseTableRow, parseTableRowProperties };
@@ -261,6 +261,7 @@ function isPreservableDocxEntry(path) {
261
261
  if (!isSafeDocxPath(path)) return false;
262
262
  const lowerPath = path.toLowerCase();
263
263
  if (lowerPath.startsWith("word/media/")) return PRESERVABLE_MEDIA_MIME_TYPES.has(getMediaMimeType(path));
264
+ if (lowerPath.startsWith("docprops/thumbnail.")) return PRESERVABLE_MEDIA_MIME_TYPES.has(getMediaMimeType(path));
264
265
  if (lowerPath.startsWith("word/fonts/")) return true;
265
266
  if (!(lowerPath.endsWith(".xml") || lowerPath.endsWith(".rels"))) return false;
266
267
  return lowerPath === "[content_types].xml" || lowerPath.startsWith("_rels/") || lowerPath.startsWith("docprops/") || lowerPath.startsWith("word/") || lowerPath.startsWith("customxml/");
@@ -1,5 +1,6 @@
1
1
  import { cloneWithXmlnsDeclarations, elementToXml, findAllDeep, findChild, getAttribute, getChildElements, getLocalName } from "./xmlParser.js";
2
2
  import { pixelsToEmu } from "../utils/units.js";
3
+ import { sanitizeImageSrc } from "../utils/sanitizeImageSrc.js";
3
4
  import { resolveImageData } from "./imageParser.js";
4
5
  import { isWatermarkShape } from "./watermarkParser.js";
5
6
  //#region src/docx/vmlImageParser.ts
@@ -49,16 +50,22 @@ function cssLengthToPx(raw) {
49
50
  default: return;
50
51
  }
51
52
  }
53
+ const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
54
+ "__proto__",
55
+ "constructor",
56
+ "prototype"
57
+ ]);
52
58
  /** Read a `style="k1:v1;k2:v2"` attribute into a lowercased key/value record. */
53
59
  function parseStyleAttr(style) {
54
- const out = {};
60
+ const out = Object.create(null);
55
61
  if (!style) return out;
56
62
  for (const decl of style.split(";")) {
57
63
  const colon = decl.indexOf(":");
58
64
  if (colon < 0) continue;
59
65
  const key = decl.slice(0, colon).trim().toLowerCase();
60
66
  const value = decl.slice(colon + 1).trim();
61
- if (key) out[key] = value;
67
+ if (!key || PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
68
+ out[key] = value;
62
69
  }
63
70
  return out;
64
71
  }
@@ -216,7 +223,8 @@ function parseVmlImageContent(pictElement, rels, media, rootXmlns = {}) {
216
223
  },
217
224
  wrap: { type: "inline" }
218
225
  };
219
- if (src) image.src = src;
226
+ const safeSrc = sanitizeImageSrc(src);
227
+ if (safeSrc) image.src = safeSrc;
220
228
  if (mimeType) image.mimeType = mimeType;
221
229
  if (filename) image.filename = filename;
222
230
  const title = getAttribute(imagedata, "o", "title");
@@ -500,6 +500,13 @@ function findAllDeep(root, namespace, localName) {
500
500
  return results;
501
501
  }
502
502
  /**
503
+ * Sanity cap on distinct `xmlns:*` declarations collected per element. Real
504
+ * documents declare a handful; a hostile element with thousands of unique
505
+ * prefixes would otherwise get replayed onto every captured `w:pict` rawXml
506
+ * subtree that inherits from it.
507
+ */
508
+ const MAX_XMLNS_DECLARATIONS_PER_ELEMENT = 64;
509
+ /**
503
510
  * Collect every `xmlns` / `xmlns:*` declaration from an element's attributes.
504
511
  *
505
512
  * The serializer's hard-coded root namespaces only cover canonical prefixes
@@ -512,7 +519,10 @@ function collectXmlnsDeclarations(element) {
512
519
  const out = {};
513
520
  const attrs = element.attributes;
514
521
  if (!attrs) return out;
515
- for (const [key, value] of Object.entries(attrs)) if ((key === "xmlns" || key.startsWith("xmlns:")) && value !== void 0) out[key] = String(value);
522
+ for (const [key, value] of Object.entries(attrs)) {
523
+ if (Object.keys(out).length >= MAX_XMLNS_DECLARATIONS_PER_ELEMENT) break;
524
+ if ((key === "xmlns" || key.startsWith("xmlns:")) && value !== void 0) out[key] = String(value);
525
+ }
516
526
  return out;
517
527
  }
518
528
  /**
@@ -15,9 +15,24 @@
15
15
  *
16
16
  * Ported from eigenpal/docx-editor (see NOTICE.md).
17
17
  */
18
+ /**
19
+ * Build the per-document scoped family name an embedded face registers
20
+ * under. `docNonce` is fresh per document load (see {@link getEmbeddedFontFaces}),
21
+ * so the same original name in two different documents (or two loads of the
22
+ * same document) never collides.
23
+ */
24
+ declare function scopeEmbeddedFontFamily(docNonce: string, originalFamily: string): string;
18
25
  /** A single de-obfuscated embedded font face, ready to register as `@font-face`. */
19
26
  type EmbeddedFont = {
20
- /** Word font name to register the face under (`w:font w:name`). */family: string; /** CSS `font-style` the face maps to (`embed*Italic` → `italic`). */
27
+ /**
28
+ * Family name to register the face under (`@font-face`/`FontFace`).
29
+ * Always a per-document SCOPED name (see {@link scopeEmbeddedFontFamily}) —
30
+ * never the raw `w:font w:name` from the DOCX. Use `originalFamily` (and
31
+ * {@link buildEmbeddedFontFamilyMap}) to resolve document runs, which
32
+ * still reference the font by its original DOCX name, to this family.
33
+ */
34
+ family: string; /** Original Word font name (`w:font w:name`), before scoping. */
35
+ originalFamily: string; /** CSS `font-style` the face maps to (`embed*Italic` → `italic`). */
21
36
  style: "normal" | "italic"; /** CSS `font-weight` the face maps to (`embedBold*` → `700`). */
22
37
  weight: 400 | 700; /** De-obfuscated OpenType/TrueType bytes (backed by a fresh, non-shared buffer). */
23
38
  bytes: Uint8Array<ArrayBuffer>; /** Whether the source face was subsetted (`w:subsetted`). */
@@ -33,14 +48,27 @@ type EmbeddedFontParts = {
33
48
  * Resolve and de-obfuscate every embedded font face declared in a font table.
34
49
  * Pure and DOM-free. Faces whose relationship or binary is missing are skipped,
35
50
  * so the result only contains faces that produced usable bytes.
51
+ *
52
+ * `docNonce` scopes every face's registered family to one document load (see
53
+ * {@link scopeEmbeddedFontFamily}); defaults to a fresh random id so callers
54
+ * that don't care about a specific value still get per-call isolation. Pass
55
+ * an explicit value for deterministic tests.
56
+ */
57
+ declare function getEmbeddedFontFaces(parts: EmbeddedFontParts, docNonce?: string): EmbeddedFont[];
58
+ /**
59
+ * Build the original DOCX font name → scoped registered family map for a set
60
+ * of resolved embedded faces. Callers thread this into font resolution (see
61
+ * `utils/fontResolver.ts`'s `setEmbeddedFontFamilyMap`) so the document's own
62
+ * runs — which still carry the original name — keep resolving to the scoped
63
+ * face that was actually registered on `document.fonts`.
36
64
  */
37
- declare function getEmbeddedFontFaces(parts: EmbeddedFontParts): EmbeddedFont[];
65
+ declare function buildEmbeddedFontFamilyMap(faces: readonly EmbeddedFont[]): Map<string, string>;
38
66
  /**
39
67
  * Extract every embedded font face from a raw `.docx` buffer. Unzips the
40
68
  * package, then resolves + de-obfuscates the faces via
41
69
  * {@link getEmbeddedFontFaces}. Returns an empty list for documents with no
42
- * embedded fonts.
70
+ * embedded fonts. See {@link getEmbeddedFontFaces} for `docNonce`.
43
71
  */
44
- declare function extractEmbeddedFonts(buffer: ArrayBuffer): Promise<EmbeddedFont[]>;
72
+ declare function extractEmbeddedFonts(buffer: ArrayBuffer, docNonce?: string): Promise<EmbeddedFont[]>;
45
73
  //#endregion
46
- export { EmbeddedFont, EmbeddedFontParts, extractEmbeddedFonts, getEmbeddedFontFaces };
74
+ export { EmbeddedFont, EmbeddedFontParts, buildEmbeddedFontFamilyMap, extractEmbeddedFonts, getEmbeddedFontFaces, scopeEmbeddedFontFamily };