@stll/folio-core 0.12.0 → 0.14.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 (138) 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/docxToMarkdown.d.ts +15 -0
  50. package/dist/docx/server/docxToMarkdown.js +17 -0
  51. package/dist/docx/server/extractDocxText.js +55 -8
  52. package/dist/docx/settingsParser.js +15 -2
  53. package/dist/docx/tableParser.d.ts +8 -1
  54. package/dist/docx/tableParser.js +29 -8
  55. package/dist/docx/unzip.js +1 -0
  56. package/dist/docx/vmlImageParser.js +11 -3
  57. package/dist/docx/xmlParser.js +11 -1
  58. package/dist/fonts/embeddedFonts.d.ts +33 -5
  59. package/dist/fonts/embeddedFonts.js +37 -8
  60. package/dist/index.d.ts +3 -3
  61. package/dist/index.js +3 -3
  62. package/dist/layout-bridge/convert/toFlowBlocks.js +15 -0
  63. package/dist/layout-engine/index.js +19 -12
  64. package/dist/layout-engine/measure/cache.js +2 -0
  65. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
  66. package/dist/layout-engine/measure/floatingTablePosition.d.ts +9 -0
  67. package/dist/layout-engine/measure/floatingTablePosition.js +17 -0
  68. package/dist/layout-engine/measure/index.d.ts +2 -1
  69. package/dist/layout-engine/measure/index.js +2 -1
  70. package/dist/layout-engine/measure/lineBreakProvider.d.ts +2 -2
  71. package/dist/layout-engine/measure/lineBreakProvider.js +3 -3
  72. package/dist/layout-engine/measure/measureBlocks.js +11 -8
  73. package/dist/layout-engine/measure/measureHelpers.d.ts +10 -1
  74. package/dist/layout-engine/measure/measureHelpers.js +12 -1
  75. package/dist/layout-engine/measure/measureParagraph.js +16 -0
  76. package/dist/layout-engine/types.d.ts +22 -2
  77. package/dist/layout-painter/renderImage.d.ts +12 -1
  78. package/dist/layout-painter/renderImage.js +21 -4
  79. package/dist/layout-painter/renderPage.js +5 -8
  80. package/dist/layout-painter/renderParagraph.js +33 -9
  81. package/dist/layout-painter/renderTable.js +2 -1
  82. package/dist/layout-painter/renderWatermark.d.ts +1 -1
  83. package/dist/layout-painter/renderWatermark.js +2 -1
  84. package/dist/markdown/fromMarkdown.js +1 -1
  85. package/dist/prosemirror/attrs/index.d.ts +1 -1
  86. package/dist/prosemirror/attrs/index.js +21 -0
  87. package/dist/prosemirror/commands/comments.d.ts +76 -1
  88. package/dist/prosemirror/commands/comments.js +401 -28
  89. package/dist/prosemirror/commands/hyperlink.js +15 -1
  90. package/dist/prosemirror/commands/image.js +7 -1
  91. package/dist/prosemirror/commands/index.d.ts +2 -2
  92. package/dist/prosemirror/commands/index.js +2 -2
  93. package/dist/prosemirror/commands/tableCellMergeResolution.js +1 -1
  94. package/dist/prosemirror/commentIdAllocator.js +13 -3
  95. package/dist/prosemirror/conversion/fromProseDoc.js +122 -5
  96. package/dist/prosemirror/conversion/index.js +1 -1
  97. package/dist/prosemirror/conversion/toProseDoc.js +20 -8
  98. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -1
  99. package/dist/prosemirror/extensions/features/ImagePasteExtension.js +10 -2
  100. package/dist/prosemirror/extensions/features/pasteCleanup.js +15 -2
  101. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +30 -4
  102. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +49 -13
  103. package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
  104. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -3
  105. package/dist/prosemirror/index.d.ts +1 -1
  106. package/dist/prosemirror/index.js +1 -1
  107. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +2 -2
  108. package/dist/prosemirror/plugins/contentControlWidgets.js +4 -4
  109. package/dist/prosemirror/plugins/revisionIds.d.ts +23 -0
  110. package/dist/prosemirror/plugins/revisionIds.js +57 -0
  111. package/dist/prosemirror/plugins/suggestionMode.js +4 -3
  112. package/dist/prosemirror/runFormattingMarkNames.d.ts +14 -0
  113. package/dist/prosemirror/runFormattingMarkNames.js +40 -0
  114. package/dist/prosemirror/schema/index.d.ts +1 -1
  115. package/dist/prosemirror/schema/marks.d.ts +27 -4
  116. package/dist/prosemirror/schema/marks.js +15 -0
  117. package/dist/prosemirror/schema/nodes.d.ts +50 -2
  118. package/dist/prosemirror/utils/tabCalculator.js +3 -3
  119. package/dist/render-dom/RemoteSelectionOverlay.js +2 -2
  120. package/dist/server.d.ts +3 -2
  121. package/dist/server.js +4 -3
  122. package/dist/utils/clipboard.d.ts +10 -1
  123. package/dist/utils/clipboard.js +33 -6
  124. package/dist/utils/colorResolver.d.ts +8 -1
  125. package/dist/utils/colorResolver.js +35 -5
  126. package/dist/utils/fontResolver.d.ts +2 -1
  127. package/dist/utils/fontResolver.js +9 -3
  128. package/dist/utils/hexId.d.ts +11 -1
  129. package/dist/utils/hexId.js +11 -1
  130. package/dist/utils/sanitizeImageSrc.d.ts +21 -0
  131. package/dist/utils/sanitizeImageSrc.js +30 -0
  132. package/dist/utils/stripXmlDeclarations.d.ts +11 -0
  133. package/dist/utils/stripXmlDeclarations.js +35 -0
  134. package/dist/version-comparison.d.ts +22 -2
  135. package/dist/version-comparison.js +41 -12
  136. package/dist/watermark/index.d.ts +11 -1
  137. package/dist/watermark/index.js +22 -1
  138. 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 };
@@ -0,0 +1,15 @@
1
+ import { MarkdownOptions } from "../../markdown/types.js";
2
+
3
+ //#region src/docx/server/docxToMarkdown.d.ts
4
+ /**
5
+ * Convert a DOCX file to markdown in a single server-safe call.
6
+ *
7
+ * Composes {@link parseDocx} (bytes -> Document, with font preloading disabled
8
+ * so it never touches the DOM) and {@link toMarkdown} (Document -> markdown).
9
+ * This gives non-browser callers folio's full DOCX fidelity — headings,
10
+ * tables, lists, and content controls (`w:sdt`) at every position — without
11
+ * deep-importing internal subpaths or re-implementing an OOXML walker.
12
+ */
13
+ declare function docxToMarkdown(input: ArrayBuffer | Uint8Array, opts?: MarkdownOptions): Promise<string>;
14
+ //#endregion
15
+ export { docxToMarkdown };
@@ -0,0 +1,17 @@
1
+ import { parseDocx } from "../parser.js";
2
+ import { toMarkdown } from "../../markdown/index.js";
3
+ //#region src/docx/server/docxToMarkdown.ts
4
+ /**
5
+ * Convert a DOCX file to markdown in a single server-safe call.
6
+ *
7
+ * Composes {@link parseDocx} (bytes -> Document, with font preloading disabled
8
+ * so it never touches the DOM) and {@link toMarkdown} (Document -> markdown).
9
+ * This gives non-browser callers folio's full DOCX fidelity — headings,
10
+ * tables, lists, and content controls (`w:sdt`) at every position — without
11
+ * deep-importing internal subpaths or re-implementing an OOXML walker.
12
+ */
13
+ async function docxToMarkdown(input, opts) {
14
+ return toMarkdown(await parseDocx(input, { preloadFonts: false }), opts);
15
+ }
16
+ //#endregion
17
+ export { docxToMarkdown };
@@ -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";
@@ -514,23 +524,34 @@ function parseCellContent(tcElement, styles, theme, numbering, rels, media, opti
514
524
  const content = [];
515
525
  const pendingBookmarkMarkers = [];
516
526
  const elements = getChildElements(tcElement);
517
- for (const child of elements) {
518
- if (!child.name) continue;
527
+ const parseCellChild = (child) => {
528
+ if (!child.name) return;
519
529
  const localName = getLocalName(child.name);
520
530
  if (localName === "p") {
521
531
  const para = parseParagraph(child, styles, theme, numbering, rels, media, options);
522
532
  enrichParagraphTextBoxes(para, child, styles, theme, numbering, rels, media, parseTable);
523
533
  prependPendingBookmarkMarkers(para, pendingBookmarkMarkers);
524
534
  content.push(para);
525
- } else if (localName === "tbl") {
535
+ return;
536
+ }
537
+ if (localName === "tbl") {
526
538
  const table = parseTable(child, styles, theme, numbering, rels, media, options);
527
539
  if (prependBookmarkMarkersToFirstParagraphInBlocks([table], pendingBookmarkMarkers)) pendingBookmarkMarkers.length = 0;
528
540
  content.push(table);
529
- } else if (localName === "bookmarkStart" || localName === "bookmarkEnd") {
541
+ return;
542
+ }
543
+ if (localName === "sdt") {
544
+ const sdtContent = findChildByLocalName(child, "sdtContent");
545
+ if (!sdtContent) return;
546
+ for (const sdtChild of getChildElements(sdtContent)) parseCellChild(sdtChild);
547
+ return;
548
+ }
549
+ if (localName === "bookmarkStart" || localName === "bookmarkEnd") {
530
550
  const marker = parseBookmarkMarker(child, localName);
531
551
  if (!appendBookmarkMarkerToLastParagraphInBlocks(content, marker)) pendingBookmarkMarkers.push(marker);
532
552
  }
533
- }
553
+ };
554
+ for (const child of elements) parseCellChild(child);
534
555
  if (content.length === 0) content.push({
535
556
  type: "paragraph",
536
557
  content: [...pendingBookmarkMarkers]
@@ -838,4 +859,4 @@ function isFloatingTable(table) {
838
859
  return table.formatting?.floating !== void 0;
839
860
  }
840
861
  //#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 };
862
+ 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");