smartrte-core 0.2.0 → 1.0.0-beta.1

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 (329) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +23 -0
  3. package/dist/command.d.ts +8 -8
  4. package/dist/editor.d.ts +32 -0
  5. package/dist/editor.js +121 -0
  6. package/dist/foundation/annotations/index.d.ts +2 -0
  7. package/dist/foundation/annotations/index.js +2 -0
  8. package/dist/foundation/annotations/range.d.ts +54 -0
  9. package/dist/foundation/annotations/range.js +158 -0
  10. package/dist/foundation/annotations/types.d.ts +33 -0
  11. package/dist/foundation/annotations/types.js +1 -0
  12. package/dist/foundation/atom/commands.d.ts +11 -0
  13. package/dist/foundation/atom/commands.js +118 -0
  14. package/dist/foundation/atom/composition.d.ts +8 -0
  15. package/dist/foundation/atom/composition.js +22 -0
  16. package/dist/foundation/atom/declarations.d.ts +2 -0
  17. package/dist/foundation/atom/declarations.js +17 -0
  18. package/dist/foundation/atom/formats.d.ts +21 -0
  19. package/dist/foundation/atom/formats.js +77 -0
  20. package/dist/foundation/atom/index.d.ts +10 -0
  21. package/dist/foundation/atom/index.js +10 -0
  22. package/dist/foundation/atom/lifecycle.d.ts +12 -0
  23. package/dist/foundation/atom/lifecycle.js +41 -0
  24. package/dist/foundation/atom/persistence.d.ts +3 -0
  25. package/dist/foundation/atom/persistence.js +20 -0
  26. package/dist/foundation/atom/plugin.d.ts +20 -0
  27. package/dist/foundation/atom/plugin.js +55 -0
  28. package/dist/foundation/atom/schema.d.ts +3 -0
  29. package/dist/foundation/atom/schema.js +29 -0
  30. package/dist/foundation/atom/security.d.ts +10 -0
  31. package/dist/foundation/atom/security.js +20 -0
  32. package/dist/foundation/atom/selection.d.ts +3 -0
  33. package/dist/foundation/atom/selection.js +4 -0
  34. package/dist/foundation/atom/types.d.ts +47 -0
  35. package/dist/foundation/atom/types.js +1 -0
  36. package/dist/foundation/block/commands.d.ts +17 -0
  37. package/dist/foundation/block/commands.js +197 -0
  38. package/dist/foundation/block/formats.d.ts +33 -0
  39. package/dist/foundation/block/formats.js +57 -0
  40. package/dist/foundation/block/index.d.ts +5 -0
  41. package/dist/foundation/block/index.js +5 -0
  42. package/dist/foundation/block/input.d.ts +19 -0
  43. package/dist/foundation/block/input.js +67 -0
  44. package/dist/foundation/block/plugin.d.ts +26 -0
  45. package/dist/foundation/block/plugin.js +79 -0
  46. package/dist/foundation/block/schema.d.ts +49 -0
  47. package/dist/foundation/block/schema.js +27 -0
  48. package/dist/foundation/block/types.d.ts +40 -0
  49. package/dist/foundation/block/types.js +1 -0
  50. package/dist/foundation/boundaries.d.ts +11 -0
  51. package/dist/foundation/boundaries.js +117 -0
  52. package/dist/foundation/clipboard/detection.d.ts +3 -0
  53. package/dist/foundation/clipboard/detection.js +29 -0
  54. package/dist/foundation/clipboard/diagnostics.d.ts +24 -0
  55. package/dist/foundation/clipboard/diagnostics.js +59 -0
  56. package/dist/foundation/clipboard/index.d.ts +8 -0
  57. package/dist/foundation/clipboard/index.js +8 -0
  58. package/dist/foundation/clipboard/insertion.d.ts +22 -0
  59. package/dist/foundation/clipboard/insertion.js +210 -0
  60. package/dist/foundation/clipboard/normalizers.d.ts +8 -0
  61. package/dist/foundation/clipboard/normalizers.js +229 -0
  62. package/dist/foundation/clipboard/pipeline.d.ts +13 -0
  63. package/dist/foundation/clipboard/pipeline.js +58 -0
  64. package/dist/foundation/clipboard/sanitizer.d.ts +3 -0
  65. package/dist/foundation/clipboard/sanitizer.js +58 -0
  66. package/dist/foundation/clipboard/serialization.d.ts +5 -0
  67. package/dist/foundation/clipboard/serialization.js +33 -0
  68. package/dist/foundation/clipboard/transfer.d.ts +11 -0
  69. package/dist/foundation/clipboard/transfer.js +108 -0
  70. package/dist/foundation/clipboard/types.d.ts +65 -0
  71. package/dist/foundation/clipboard/types.js +1 -0
  72. package/dist/foundation/collab/index.d.ts +3 -0
  73. package/dist/foundation/collab/index.js +3 -0
  74. package/dist/foundation/collab/rebase.d.ts +28 -0
  75. package/dist/foundation/collab/rebase.js +82 -0
  76. package/dist/foundation/collab/transform.d.ts +73 -0
  77. package/dist/foundation/collab/transform.js +251 -0
  78. package/dist/foundation/collab/types.d.ts +52 -0
  79. package/dist/foundation/collab/types.js +1 -0
  80. package/dist/foundation/comments/fromSelection.d.ts +16 -0
  81. package/dist/foundation/comments/fromSelection.js +24 -0
  82. package/dist/foundation/comments/index.d.ts +3 -0
  83. package/dist/foundation/comments/index.js +3 -0
  84. package/dist/foundation/comments/rebase.d.ts +31 -0
  85. package/dist/foundation/comments/rebase.js +43 -0
  86. package/dist/foundation/comments/types.d.ts +37 -0
  87. package/dist/foundation/comments/types.js +1 -0
  88. package/dist/foundation/diff/documentDiff.d.ts +12 -0
  89. package/dist/foundation/diff/documentDiff.js +148 -0
  90. package/dist/foundation/diff/index.d.ts +3 -0
  91. package/dist/foundation/diff/index.js +3 -0
  92. package/dist/foundation/diff/textDiff.d.ts +12 -0
  93. package/dist/foundation/diff/textDiff.js +114 -0
  94. package/dist/foundation/diff/types.d.ts +74 -0
  95. package/dist/foundation/diff/types.js +1 -0
  96. package/dist/foundation/editor.d.ts +138 -0
  97. package/dist/foundation/editor.js +398 -0
  98. package/dist/foundation/formats/codec.d.ts +45 -0
  99. package/dist/foundation/formats/codec.js +1 -0
  100. package/dist/foundation/formats/docx/export.d.ts +4 -0
  101. package/dist/foundation/formats/docx/export.js +285 -0
  102. package/dist/foundation/formats/docx/import.d.ts +9 -0
  103. package/dist/foundation/formats/docx/import.js +37 -0
  104. package/dist/foundation/formats/docx/nestingGuard.d.ts +40 -0
  105. package/dist/foundation/formats/docx/nestingGuard.js +66 -0
  106. package/dist/foundation/formats/docx/portableAtoms.d.ts +4 -0
  107. package/dist/foundation/formats/docx/portableAtoms.js +66 -0
  108. package/dist/foundation/formats/docx/styledImport.d.ts +10 -0
  109. package/dist/foundation/formats/docx/styledImport.js +205 -0
  110. package/dist/foundation/formats/featureCodecs.d.ts +37 -0
  111. package/dist/foundation/formats/featureCodecs.js +69 -0
  112. package/dist/foundation/formats/fidelity.d.ts +31 -0
  113. package/dist/foundation/formats/fidelity.js +123 -0
  114. package/dist/foundation/formats/index.d.ts +7 -0
  115. package/dist/foundation/formats/index.js +7 -0
  116. package/dist/foundation/formats/pdf/format.d.ts +29 -0
  117. package/dist/foundation/formats/pdf/format.js +190 -0
  118. package/dist/foundation/history.d.ts +20 -0
  119. package/dist/foundation/history.js +121 -0
  120. package/dist/foundation/identity.d.ts +8 -0
  121. package/dist/foundation/identity.js +30 -0
  122. package/dist/foundation/index.d.ts +30 -0
  123. package/dist/foundation/index.js +30 -0
  124. package/dist/foundation/list/commands.d.ts +28 -0
  125. package/dist/foundation/list/commands.js +422 -0
  126. package/dist/foundation/list/formats.d.ts +20 -0
  127. package/dist/foundation/list/formats.js +795 -0
  128. package/dist/foundation/list/index.d.ts +8 -0
  129. package/dist/foundation/list/index.js +8 -0
  130. package/dist/foundation/list/input.d.ts +35 -0
  131. package/dist/foundation/list/input.js +320 -0
  132. package/dist/foundation/list/plugin.d.ts +21 -0
  133. package/dist/foundation/list/plugin.js +71 -0
  134. package/dist/foundation/list/presets.d.ts +16 -0
  135. package/dist/foundation/list/presets.js +21 -0
  136. package/dist/foundation/list/schema.d.ts +35 -0
  137. package/dist/foundation/list/schema.js +29 -0
  138. package/dist/foundation/list/shadow.d.ts +54 -0
  139. package/dist/foundation/list/shadow.js +75 -0
  140. package/dist/foundation/list/shadowCorpus.d.ts +10 -0
  141. package/dist/foundation/list/shadowCorpus.js +41 -0
  142. package/dist/foundation/list/types.d.ts +50 -0
  143. package/dist/foundation/list/types.js +1 -0
  144. package/dist/foundation/mapping.d.ts +11 -0
  145. package/dist/foundation/mapping.js +40 -0
  146. package/dist/foundation/marks/canonical.d.ts +9 -0
  147. package/dist/foundation/marks/canonical.js +72 -0
  148. package/dist/foundation/marks/commands.d.ts +21 -0
  149. package/dist/foundation/marks/commands.js +136 -0
  150. package/dist/foundation/marks/dom.d.ts +2 -0
  151. package/dist/foundation/marks/dom.js +46 -0
  152. package/dist/foundation/marks/editor.d.ts +8 -0
  153. package/dist/foundation/marks/editor.js +50 -0
  154. package/dist/foundation/marks/formats.d.ts +37 -0
  155. package/dist/foundation/marks/formats.js +49 -0
  156. package/dist/foundation/marks/hardBreak.d.ts +8 -0
  157. package/dist/foundation/marks/hardBreak.js +80 -0
  158. package/dist/foundation/marks/index.d.ts +11 -0
  159. package/dist/foundation/marks/index.js +11 -0
  160. package/dist/foundation/marks/normalization.d.ts +2 -0
  161. package/dist/foundation/marks/normalization.js +49 -0
  162. package/dist/foundation/marks/plugin.d.ts +14 -0
  163. package/dist/foundation/marks/plugin.js +63 -0
  164. package/dist/foundation/marks/resolveMarkRun.d.ts +8 -0
  165. package/dist/foundation/marks/resolveMarkRun.js +47 -0
  166. package/dist/foundation/marks/schema.d.ts +167 -0
  167. package/dist/foundation/marks/schema.js +50 -0
  168. package/dist/foundation/marks/stored.d.ts +2 -0
  169. package/dist/foundation/marks/stored.js +31 -0
  170. package/dist/foundation/marks/types.d.ts +40 -0
  171. package/dist/foundation/marks/types.js +1 -0
  172. package/dist/foundation/modelDom.d.ts +38 -0
  173. package/dist/foundation/modelDom.js +327 -0
  174. package/dist/foundation/normalization.d.ts +22 -0
  175. package/dist/foundation/normalization.js +132 -0
  176. package/dist/foundation/operations.d.ts +28 -0
  177. package/dist/foundation/operations.js +568 -0
  178. package/dist/foundation/plugin/builtins.d.ts +10 -0
  179. package/dist/foundation/plugin/builtins.js +27 -0
  180. package/dist/foundation/plugin/dispatch.d.ts +33 -0
  181. package/dist/foundation/plugin/dispatch.js +34 -0
  182. package/dist/foundation/plugin/index.d.ts +4 -0
  183. package/dist/foundation/plugin/index.js +4 -0
  184. package/dist/foundation/plugin/registry.d.ts +36 -0
  185. package/dist/foundation/plugin/registry.js +115 -0
  186. package/dist/foundation/plugin/types.d.ts +141 -0
  187. package/dist/foundation/plugin/types.js +13 -0
  188. package/dist/foundation/positions.d.ts +20 -0
  189. package/dist/foundation/positions.js +121 -0
  190. package/dist/foundation/schema.d.ts +57 -0
  191. package/dist/foundation/schema.js +371 -0
  192. package/dist/foundation/schemaBuilder.d.ts +41 -0
  193. package/dist/foundation/schemaBuilder.js +96 -0
  194. package/dist/foundation/scope/index.d.ts +2 -0
  195. package/dist/foundation/scope/index.js +2 -0
  196. package/dist/foundation/scope/resolveScope.d.ts +13 -0
  197. package/dist/foundation/scope/resolveScope.js +860 -0
  198. package/dist/foundation/scope/types.d.ts +122 -0
  199. package/dist/foundation/scope/types.js +1 -0
  200. package/dist/foundation/security/urlPolicy.d.ts +21 -0
  201. package/dist/foundation/security/urlPolicy.js +75 -0
  202. package/dist/foundation/structural/contentTarget.d.ts +21 -0
  203. package/dist/foundation/structural/contentTarget.js +51 -0
  204. package/dist/foundation/structural/index.d.ts +2 -0
  205. package/dist/foundation/structural/index.js +2 -0
  206. package/dist/foundation/structural/move.d.ts +7 -0
  207. package/dist/foundation/structural/move.js +35 -0
  208. package/dist/foundation/suggestions/index.d.ts +3 -0
  209. package/dist/foundation/suggestions/index.js +3 -0
  210. package/dist/foundation/suggestions/inline.d.ts +64 -0
  211. package/dist/foundation/suggestions/inline.js +116 -0
  212. package/dist/foundation/suggestions/structural.d.ts +40 -0
  213. package/dist/foundation/suggestions/structural.js +47 -0
  214. package/dist/foundation/suggestions/types.d.ts +19 -0
  215. package/dist/foundation/suggestions/types.js +1 -0
  216. package/dist/foundation/surface/index.d.ts +3 -0
  217. package/dist/foundation/surface/index.js +3 -0
  218. package/dist/foundation/surface/input.d.ts +127 -0
  219. package/dist/foundation/surface/input.js +1761 -0
  220. package/dist/foundation/surface/renderer.d.ts +84 -0
  221. package/dist/foundation/surface/renderer.js +974 -0
  222. package/dist/foundation/surface/types.d.ts +48 -0
  223. package/dist/foundation/surface/types.js +1 -0
  224. package/dist/foundation/table/commands.d.ts +31 -0
  225. package/dist/foundation/table/commands.js +470 -0
  226. package/dist/foundation/table/formats.d.ts +26 -0
  227. package/dist/foundation/table/formats.js +40 -0
  228. package/dist/foundation/table/grid.d.ts +8 -0
  229. package/dist/foundation/table/grid.js +178 -0
  230. package/dist/foundation/table/index.d.ts +6 -0
  231. package/dist/foundation/table/index.js +6 -0
  232. package/dist/foundation/table/plugin.d.ts +23 -0
  233. package/dist/foundation/table/plugin.js +119 -0
  234. package/dist/foundation/table/schema.d.ts +2 -0
  235. package/dist/foundation/table/schema.js +38 -0
  236. package/dist/foundation/table/selection.d.ts +7 -0
  237. package/dist/foundation/table/selection.js +35 -0
  238. package/dist/foundation/table/types.d.ts +109 -0
  239. package/dist/foundation/table/types.js +1 -0
  240. package/dist/foundation/transactions.d.ts +41 -0
  241. package/dist/foundation/transactions.js +90 -0
  242. package/dist/foundation/types.d.ts +261 -0
  243. package/dist/foundation/types.js +1 -0
  244. package/dist/foundation/versioning/diffVersions.d.ts +5 -0
  245. package/dist/foundation/versioning/diffVersions.js +3 -0
  246. package/dist/foundation/versioning/index.d.ts +2 -0
  247. package/dist/foundation/versioning/index.js +2 -0
  248. package/dist/foundation/versioning/types.d.ts +17 -0
  249. package/dist/foundation/versioning/types.js +1 -0
  250. package/dist/history.d.ts +20 -0
  251. package/dist/history.js +165 -0
  252. package/dist/html/compatibility.js +7 -1
  253. package/dist/index.d.ts +1 -14
  254. package/dist/index.js +9 -14
  255. package/dist/legacy/index.d.ts +45 -0
  256. package/dist/legacy/index.js +45 -0
  257. package/dist/legacyCommands/alignment.d.ts +20 -0
  258. package/dist/{commands → legacyCommands}/alignment.js +17 -0
  259. package/dist/legacyCommands/blocks.d.ts +14 -0
  260. package/dist/legacyCommands/blocks.js +105 -0
  261. package/dist/legacyCommands/checklist.d.ts +11 -0
  262. package/dist/legacyCommands/checklist.js +45 -0
  263. package/dist/legacyCommands/code.d.ts +15 -0
  264. package/dist/{commands → legacyCommands}/code.js +25 -1
  265. package/dist/legacyCommands/formula.d.ts +7 -0
  266. package/dist/legacyCommands/formula.js +67 -0
  267. package/dist/legacyCommands/inlineAtoms.d.ts +7 -0
  268. package/dist/legacyCommands/inlineAtoms.js +58 -0
  269. package/dist/legacyCommands/list.d.ts +38 -0
  270. package/dist/legacyCommands/list.js +477 -0
  271. package/dist/{commands → legacyCommands}/marks.d.ts +3 -0
  272. package/dist/{commands → legacyCommands}/marks.js +45 -11
  273. package/dist/legacyCommands/media.d.ts +31 -0
  274. package/dist/legacyCommands/media.js +205 -0
  275. package/dist/legacyCommands/move.d.ts +14 -0
  276. package/dist/legacyCommands/move.js +91 -0
  277. package/dist/legacyCommands/table.d.ts +91 -0
  278. package/dist/legacyCommands/table.js +547 -0
  279. package/dist/listPresets.d.ts +12 -0
  280. package/dist/listPresets.js +6 -0
  281. package/dist/listScope.d.ts +27 -0
  282. package/dist/listScope.js +104 -0
  283. package/dist/markdown/compatibility.d.ts +2 -0
  284. package/dist/markdown/compatibility.js +94 -2
  285. package/dist/marks.d.ts +5 -5
  286. package/dist/model.d.ts +65 -9
  287. package/dist/plugin.d.ts +15 -0
  288. package/dist/plugin.js +31 -0
  289. package/dist/plugins/alignment.d.ts +3 -0
  290. package/dist/plugins/alignment.js +6 -0
  291. package/dist/plugins/basicFormatting.d.ts +9 -0
  292. package/dist/plugins/basicFormatting.js +24 -0
  293. package/dist/plugins/blockType.d.ts +3 -0
  294. package/dist/plugins/blockType.js +6 -0
  295. package/dist/plugins/blockquote.d.ts +3 -0
  296. package/dist/plugins/blockquote.js +6 -0
  297. package/dist/plugins/checklist.d.ts +3 -0
  298. package/dist/plugins/checklist.js +10 -0
  299. package/dist/plugins/codeBlock.d.ts +3 -0
  300. package/dist/plugins/codeBlock.js +6 -0
  301. package/dist/plugins/formula.d.ts +3 -0
  302. package/dist/plugins/formula.js +9 -0
  303. package/dist/plugins/list.d.ts +3 -0
  304. package/dist/plugins/list.js +10 -0
  305. package/dist/plugins/media.d.ts +3 -0
  306. package/dist/plugins/media.js +12 -0
  307. package/dist/plugins/move.d.ts +3 -0
  308. package/dist/plugins/move.js +9 -0
  309. package/dist/plugins/table.d.ts +3 -0
  310. package/dist/plugins/table.js +22 -0
  311. package/dist/preset.d.ts +12 -0
  312. package/dist/preset.js +40 -0
  313. package/dist/schema.d.ts +22 -0
  314. package/dist/schema.js +333 -0
  315. package/dist/security/urlPolicy.d.ts +1 -17
  316. package/dist/security/urlPolicy.js +3 -69
  317. package/dist/selection.d.ts +1 -1
  318. package/dist/selectionMapping.d.ts +4 -0
  319. package/dist/selectionMapping.js +222 -0
  320. package/dist/table.d.ts +3 -3
  321. package/dist/transaction.d.ts +35 -12
  322. package/dist/transaction.js +174 -28
  323. package/dist/tree.d.ts +8 -2
  324. package/dist/tree.js +44 -0
  325. package/package.json +27 -8
  326. package/dist/commands/alignment.d.ts +0 -18
  327. package/dist/commands/code.d.ts +0 -13
  328. package/dist/commands/list.d.ts +0 -20
  329. package/dist/commands/list.js +0 -52
@@ -0,0 +1,285 @@
1
+ import JSZip from "jszip";
2
+ import { isTextNode } from "../../identity.js";
3
+ import { occupancyGridFor } from "../../table/grid.js";
4
+ import { foundationListStyleForPresetDepth, isFoundationSmartListPreset } from "../../list/presets.js";
5
+ import { portableFormulaMarker, portableImageMarker } from "./portableAtoms.js";
6
+ export const DOCX_MEDIA_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
7
+ const xmlEscape = (value) => String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
8
+ const colorValue = (value) => {
9
+ const hex = /^#([0-9a-f]{6})$/i.exec(value.trim());
10
+ if (hex)
11
+ return hex[1].toUpperCase();
12
+ const rgb = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i.exec(value.trim());
13
+ return rgb
14
+ ? rgb.slice(1, 4).map((part) => Math.max(0, Math.min(255, Number(part))).toString(16).padStart(2, "0")).join("").toUpperCase()
15
+ : "";
16
+ };
17
+ /**
18
+ * table_cell's border attrs are free-form CSS border shorthand strings - the
19
+ * editor's own UI only ever writes "{width}px {style} {hex|rgb}" (style one
20
+ * of solid/dashed/dotted; see CanonicalAuthorityEditor.tsx's
21
+ * composeBorderShorthand), so that's the one shape parsed into real OOXML
22
+ * here; anything else (arbitrary pasted CSS) falls through to no override,
23
+ * same as before per-side support existed - not a regression, since nothing
24
+ * but the "none" sentinel was ever handled here originally. w:sz is eighths
25
+ * of a point; this codebase's own px->pt conversion elsewhere
26
+ * (columnWidths/row height * 15 for twips, i.e. 1px = 0.75pt) gives
27
+ * eighths = pt * 8 = px * 6.
28
+ */
29
+ const DOCX_BORDER_STYLE = { solid: "single", dashed: "dashed", dotted: "dotted", double: "double" };
30
+ const parseCellBorderValue = (value) => {
31
+ if (typeof value !== "string")
32
+ return null;
33
+ const width = /(\d+(?:\.\d+)?)\s*px/.exec(value);
34
+ const color = /#[0-9a-f]{3,6}\b|rgba?\([^)]+\)/i.exec(value);
35
+ if (!width || !color)
36
+ return null;
37
+ const hex = colorValue(color[0]);
38
+ if (!hex)
39
+ return null;
40
+ const styleMatch = /\b(solid|dashed|dotted|double)\b/i.exec(value);
41
+ return { sz: Math.max(2, Math.round(Number(width[1]) * 6)), val: DOCX_BORDER_STYLE[styleMatch?.[1].toLowerCase() ?? "solid"] ?? "single", color: hex };
42
+ };
43
+ /** borderTop/Right/Bottom/Left (the border-options UI's per-side control) win over the uniform `borders` fallback for that one side, same precedence order the live renderer applies them in. */
44
+ const cellSideBorderXml = (name, sideValue, uniformValue) => {
45
+ const parsed = parseCellBorderValue(sideValue) ?? parseCellBorderValue(uniformValue);
46
+ return parsed ? `<w:${name} w:val="${parsed.val}" w:sz="${parsed.sz}" w:color="${parsed.color}"/>` : "";
47
+ };
48
+ const cellBorderXml = (attrs, borderNone) => {
49
+ if (attrs?.borders === "none")
50
+ return borderNone;
51
+ const xml = [
52
+ cellSideBorderXml("top", attrs?.borderTop, attrs?.borders),
53
+ cellSideBorderXml("left", attrs?.borderLeft, attrs?.borders),
54
+ cellSideBorderXml("bottom", attrs?.borderBottom, attrs?.borders),
55
+ cellSideBorderXml("right", attrs?.borderRight, attrs?.borders),
56
+ ].join("");
57
+ return xml ? `<w:tcBorders>${xml}</w:tcBorders>` : "";
58
+ };
59
+ const runProperties = (marks = []) => marks.map((mark) => {
60
+ if (mark.type === "bold")
61
+ return "<w:b/>";
62
+ if (mark.type === "italic")
63
+ return "<w:i/>";
64
+ if (mark.type === "underline")
65
+ return '<w:u w:val="single"/>';
66
+ if (mark.type === "strike")
67
+ return "<w:strike/>";
68
+ if (mark.type === "superscript")
69
+ return '<w:vertAlign w:val="superscript"/>';
70
+ if (mark.type === "subscript")
71
+ return '<w:vertAlign w:val="subscript"/>';
72
+ if (mark.type === "textColor") {
73
+ const color = colorValue(String(mark.attrs?.value || ""));
74
+ return color ? `<w:color w:val="${color}"/>` : "";
75
+ }
76
+ if (mark.type === "backgroundColor") {
77
+ const color = colorValue(String(mark.attrs?.value || ""));
78
+ return color ? `<w:shd w:val="clear" w:color="auto" w:fill="${color}"/>` : "";
79
+ }
80
+ if (mark.type === "fontSize")
81
+ return `<w:sz w:val="${Math.max(2, Math.round(Number(mark.attrs?.valuePx || 0) * 1.5))}"/>`;
82
+ return "";
83
+ }).join("");
84
+ const textRun = (text, properties = "") => `<w:r>${properties ? `<w:rPr>${properties}</w:rPr>` : ""}<w:t xml:space="preserve">${xmlEscape(text)}</w:t></w:r>`;
85
+ const createSerializationContext = () => ({
86
+ relationships: [], hyperlinkIds: new Map(), media: [], drawingId: 1,
87
+ });
88
+ const hyperlinkRelationshipId = (context, href) => {
89
+ const existing = context.hyperlinkIds.get(href);
90
+ if (existing)
91
+ return existing;
92
+ const id = `rId${context.relationships.length + 2}`;
93
+ context.hyperlinkIds.set(href, id);
94
+ context.relationships.push({ id, target: href, kind: "hyperlink" });
95
+ return id;
96
+ };
97
+ const embeddableImage = (src) => {
98
+ const match = /^data:(image\/(?:png|jpeg|gif));base64,([a-z0-9+/=\s]+)$/i.exec(src);
99
+ if (!match)
100
+ return;
101
+ return { mimeType: match[1].toLowerCase(), base64: match[2].replace(/\s/g, "") };
102
+ };
103
+ const imageRun = (node, context) => {
104
+ const src = String(node.attrs?.src || "");
105
+ const alt = node.attrs?.alt !== undefined ? String(node.attrs.alt) : undefined;
106
+ const title = node.attrs?.title !== undefined ? String(node.attrs.title) : undefined;
107
+ const image = embeddableImage(src);
108
+ if (!image)
109
+ return textRun(portableImageMarker(src, alt, title));
110
+ const extension = image.mimeType === "image/jpeg" ? "jpg" : image.mimeType.split("/")[1];
111
+ const filename = `image${context.media.length + 1}.${extension}`;
112
+ const relationshipId = `rId${context.relationships.length + 2}`;
113
+ const drawingId = context.drawingId++;
114
+ context.media.push({ filename, ...image });
115
+ context.relationships.push({ id: relationshipId, target: `media/${filename}`, kind: "image" });
116
+ const widthEmu = Math.max(1, Math.round(Number(node.attrs?.width || 96) * 9525));
117
+ const heightEmu = Math.max(1, Math.round(Number(node.attrs?.height || 96) * 9525));
118
+ const name = xmlEscape(title || alt || filename);
119
+ const description = xmlEscape(alt || "");
120
+ return `<w:r><w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" distT="0" distB="0" distL="0" distR="0"><wp:extent cx="${widthEmu}" cy="${heightEmu}"/><wp:docPr id="${drawingId}" name="${name}" descr="${description}"/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:nvPicPr><pic:cNvPr id="0" name="${name}" descr="${description}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${relationshipId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${widthEmu}" cy="${heightEmu}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
121
+ };
122
+ const formulaRun = (node) => {
123
+ const source = xmlEscape(String(node.attrs?.source || ""));
124
+ const displayText = node.attrs?.displayText !== undefined ? String(node.attrs.displayText) : undefined;
125
+ const marker = xmlEscape(portableFormulaMarker(String(node.attrs?.source || ""), displayText));
126
+ return `<m:oMath><m:r><m:t>${source}</m:t></m:r></m:oMath><w:r><w:rPr><w:vanish/></w:rPr><w:t>${marker}</w:t></w:r>`;
127
+ };
128
+ /** Fidelity: media type "file" (a legacy concept) has no canonical atom equivalent and is skipped. */
129
+ const inlineRun = (node, context) => {
130
+ if (isTextNode(node)) {
131
+ const properties = runProperties(node.marks);
132
+ const runs = node.text.split("\n").map((part, index) => `${index ? "<w:r><w:br/></w:r>" : ""}${part ? textRun(part, properties) : ""}`).join("");
133
+ const link = node.marks?.find((mark) => mark.type === "link");
134
+ if (!link || !runs)
135
+ return runs;
136
+ return `<w:hyperlink r:id="${hyperlinkRelationshipId(context, String(link.attrs?.href || ""))}"${link.attrs?.target ? ' w:history="1"' : ""}>${runs}</w:hyperlink>`;
137
+ }
138
+ if (node.type === "hard_break")
139
+ return "<w:r><w:br/></w:r>";
140
+ if (node.type === "formula")
141
+ return formulaRun(node);
142
+ if (node.type === "image")
143
+ return imageRun(node, context);
144
+ return "";
145
+ };
146
+ const alignmentOf = (node) => typeof node.attrs?.align === "string" ? node.attrs.align : undefined;
147
+ const indentLevelOf = (node) => Number(node.attrs?.indentLevel) || 0;
148
+ const paragraphProperties = (block, extra = "") => [
149
+ alignmentOf(block) ? `<w:jc w:val="${xmlEscape(alignmentOf(block))}"/>` : "",
150
+ indentLevelOf(block) ? `<w:ind w:left="${indentLevelOf(block) * 480}"/>` : "",
151
+ extra,
152
+ ].join("");
153
+ const paragraphXml = (children, block, context, extraProperties = "") => {
154
+ const properties = paragraphProperties(block, extraProperties);
155
+ const content = children.map((node) => inlineRun(node, context)).join("") || textRun("");
156
+ return `<w:p>${properties ? `<w:pPr>${properties}</w:pPr>` : ""}${content}</w:p>`;
157
+ };
158
+ const listStyleNumId = {
159
+ disc: 1, circle: 2, square: 3, decimal: 4,
160
+ "decimal-leading-zero": 4,
161
+ "lower-alpha": 5, "upper-alpha": 6, "lower-roman": 7, "upper-roman": 8,
162
+ };
163
+ const orderedList = (node) => {
164
+ const marker = String(node.attrs?.style || node.attrs?.preset || "");
165
+ return /^(?:decimal|lower-|upper-|ordered)/.test(marker) || node.attrs?.start !== undefined;
166
+ };
167
+ const effectiveStyle = (node, depth) => {
168
+ if (typeof node.attrs?.style === "string")
169
+ return node.attrs.style;
170
+ if (isFoundationSmartListPreset(node.attrs?.preset))
171
+ return foundationListStyleForPresetDepth(node.attrs.preset, depth);
172
+ return "disc";
173
+ };
174
+ const blockChildren = (node) => (node.children || []);
175
+ const elementChildren = (node) => blockChildren(node).filter((child) => !isTextNode(child));
176
+ const tableXml = (block, context) => {
177
+ const grid = occupancyGridFor(block);
178
+ const widths = Array.from({ length: grid.columns }, (_, index) => Number(block.attrs?.columnWidths?.[index]) || 96);
179
+ const gridXml = `<w:tblGrid>${widths.map((widthPx) => `<w:gridCol w:w="${Math.max(1, Math.round(widthPx * 15))}"/>`).join("")}</w:tblGrid>`;
180
+ const borderNone = '<w:tcBorders><w:top w:val="nil"/><w:left w:val="nil"/><w:bottom w:val="nil"/><w:right w:val="nil"/></w:tcBorders>';
181
+ const rows = elementChildren(block).map((row, rowIndex) => {
182
+ const header = Array.from({ length: grid.columns }, (_, column) => grid.at(rowIndex, column)?.node.attrs?.header === true).every(Boolean);
183
+ const rowProperties = [
184
+ header ? "<w:tblHeader/>" : "",
185
+ Number(row.attrs?.height) > 0 ? `<w:trHeight w:val="${Math.round(Number(row.attrs?.height) * 15)}" w:hRule="atLeast"/>` : "",
186
+ ].join("");
187
+ let cells = "";
188
+ for (let column = 0; column < grid.columns;) {
189
+ const cell = grid.at(rowIndex, column);
190
+ if (!cell || cell.left !== column) {
191
+ column += 1;
192
+ continue;
193
+ }
194
+ const rowspan = cell.bottom - cell.top;
195
+ const colspan = cell.right - cell.left;
196
+ const continuation = cell.top < rowIndex;
197
+ const shade = colorValue(String(cell.node.attrs?.background || ""));
198
+ const properties = [
199
+ colspan > 1 ? `<w:gridSpan w:val="${colspan}"/>` : "",
200
+ rowspan > 1 ? (continuation ? "<w:vMerge/>" : '<w:vMerge w:val="restart"/>') : "",
201
+ shade ? `<w:shd w:val="clear" w:color="auto" w:fill="${shade}"/>` : "",
202
+ cellBorderXml(cell.node.attrs, borderNone),
203
+ ].join("");
204
+ const content = continuation ? "<w:p/>" : elementChildren(cell.node).map((child) => blockXml(child, context)).join("") || "<w:p/>";
205
+ cells += `<w:tc><w:tcPr>${properties}</w:tcPr>${content}</w:tc>`;
206
+ column += colspan;
207
+ }
208
+ return `<w:tr>${rowProperties ? `<w:trPr>${rowProperties}</w:trPr>` : ""}${cells}</w:tr>`;
209
+ }).join("");
210
+ return `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/><w:tblBorders><w:top w:val="single" w:sz="4" w:color="D1D5DB"/><w:left w:val="single" w:sz="4" w:color="D1D5DB"/><w:bottom w:val="single" w:sz="4" w:color="D1D5DB"/><w:right w:val="single" w:sz="4" w:color="D1D5DB"/><w:insideH w:val="single" w:sz="4" w:color="D1D5DB"/><w:insideV w:val="single" w:sz="4" w:color="D1D5DB"/></w:tblBorders></w:tblPr>${gridXml}${rows}</w:tbl>`;
211
+ };
212
+ /** Fidelity: DOCX export is `semantic` for structure; formulas fall back to a portable text marker (`lossy`), matching Phase 7's explicit decision not to build partial OMML. */
213
+ const blockXml = (block, context, listLevel = 0) => {
214
+ if (block.type === "paragraph")
215
+ return paragraphXml(blockChildren(block), block, context);
216
+ if (block.type === "heading")
217
+ return paragraphXml(blockChildren(block), block, context, `<w:pStyle w:val="Heading${Number(block.attrs?.level) || 1}"/>`);
218
+ if (block.type === "code_block") {
219
+ const text = blockChildren(block).map((child) => isTextNode(child) ? child.text : child.type === "hard_break" ? "\n" : "").join("");
220
+ return paragraphXml([{ type: "text", text, marks: [{ type: "code" }] }], block, context);
221
+ }
222
+ if (block.type === "blockquote") {
223
+ return elementChildren(block).map((child) => child.type === "paragraph"
224
+ ? paragraphXml(blockChildren(child), child, context, '<w:ind w:left="720"/>')
225
+ : blockXml(child, context, listLevel)).join("");
226
+ }
227
+ if (block.type === "list") {
228
+ const style = effectiveStyle(block, listLevel);
229
+ return elementChildren(block).flatMap((item) => elementChildren(item).map((child) => child.type === "paragraph"
230
+ ? paragraphXml(blockChildren(child), child, context, `<w:numPr><w:ilvl w:val="${Math.min(listLevel, 8)}"/><w:numId w:val="${listStyleNumId[style]}"/></w:numPr>`)
231
+ : blockXml(child, context, listLevel + 1))).join("");
232
+ }
233
+ if (block.type === "table")
234
+ return tableXml(block, context);
235
+ if (block.type === "block_image")
236
+ return paragraphXml([{ ...block, type: "image" }], block, context);
237
+ if (block.type === "block_formula")
238
+ return paragraphXml([{ ...block, type: "formula" }], block, context);
239
+ return paragraphXml([{ type: "text", text: String(block.attrs?.title || block.attrs?.src || "") }], block, context);
240
+ };
241
+ const numberingLevelXml = (level, format, text, font) => `<w:lvl w:ilvl="${level}"><w:start w:val="1"/><w:numFmt w:val="${format}"/><w:lvlText w:val="${text}"/><w:lvlJc w:val="left"/><w:pPr><w:tabs><w:tab w:val="num" w:pos="${720 + level * 360}"/></w:tabs><w:ind w:left="${720 + level * 360}" w:hanging="360"/></w:pPr>${font ? `<w:rPr><w:rFonts w:ascii="${font}" w:hAnsi="${font}"/></w:rPr>` : ""}</w:lvl>`;
242
+ const numberingXml = () => {
243
+ const definitions = [
244
+ { id: 1, format: "bullet", text: "•", font: "Symbol" },
245
+ { id: 2, format: "bullet", text: "○", font: "Arial" },
246
+ { id: 3, format: "bullet", text: "▪", font: "Arial" },
247
+ { id: 4, format: "decimal", text: "%1." },
248
+ { id: 5, format: "lowerLetter", text: "%1." },
249
+ { id: 6, format: "upperLetter", text: "%1." },
250
+ { id: 7, format: "lowerRoman", text: "%1." },
251
+ { id: 8, format: "upperRoman", text: "%1." },
252
+ ];
253
+ const abstracts = definitions.map(({ id, format, text, font }) => `<w:abstractNum w:abstractNumId="${id}"><w:multiLevelType w:val="multilevel"/>${Array.from({ length: 9 }, (_, level) => numberingLevelXml(level, format, text.replace("%1", `%${level + 1}`), font)).join("")}</w:abstractNum>`).join("");
254
+ const instances = definitions.map(({ id }) => `<w:num w:numId="${id}"><w:abstractNumId w:val="${id}"/></w:num>`).join("");
255
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">${abstracts}${instances}</w:numbering>`;
256
+ };
257
+ const serializeDocxDocument = (document) => {
258
+ const context = createSerializationContext();
259
+ const body = elementChildren(document).map((block) => blockXml(block, context)).join("");
260
+ return {
261
+ documentXml: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><w:body>${body}<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="720" w:right="720" w:bottom="720" w:left="720"/></w:sectPr></w:body></w:document>`,
262
+ relationships: context.relationships,
263
+ media: context.media,
264
+ };
265
+ };
266
+ export const smartDocumentToDocxXml = (document) => serializeDocxDocument(document).documentXml;
267
+ export const exportDocxDocument = async (document) => {
268
+ const zip = new JSZip();
269
+ const serialized = serializeDocxDocument(document);
270
+ const imageTypes = [...new Set(serialized.media.map(({ filename, mimeType }) => JSON.stringify({
271
+ extension: filename.split(".").pop(), mimeType,
272
+ })))].map((entry) => JSON.parse(entry));
273
+ zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${imageTypes.map(({ extension, mimeType }) => `<Default Extension="${extension}" ContentType="${mimeType}"/>`).join("")}<Override PartName="/word/document.xml" ContentType="${DOCX_MEDIA_TYPE}.main+xml"/><Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/></Types>`);
274
+ zip.folder("_rels")?.file(".rels", `<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`);
275
+ zip.folder("word")?.file("document.xml", serialized.documentXml);
276
+ zip.folder("word")?.file("numbering.xml", numberingXml());
277
+ serialized.media.forEach(({ filename, base64 }) => {
278
+ zip.folder("word")?.folder("media")?.file(filename, base64, { base64: true });
279
+ });
280
+ const relationships = serialized.relationships.map((relationship) => relationship.kind === "hyperlink"
281
+ ? `<Relationship Id="${relationship.id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="${xmlEscape(relationship.target)}" TargetMode="External"/>`
282
+ : `<Relationship Id="${relationship.id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="${relationship.target}"/>`).join("");
283
+ zip.folder("word")?.folder("_rels")?.file("document.xml.rels", `<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>${relationships}</Relationships>`);
284
+ return zip.generateAsync({ type: "blob", mimeType: DOCX_MEDIA_TYPE });
285
+ };
@@ -0,0 +1,9 @@
1
+ import type { SmartDocument } from "../../types.js";
2
+ /**
3
+ * Fidelity: `semantic` for structure. Matches the established pattern for
4
+ * plain HTML import (CanonicalAuthorityEditor's runImport) - mammoth already
5
+ * produces clean semantic HTML, so no compatibility/normalize pass is
6
+ * inserted here; parseCanonicalListHtml's own fallback ("unknown" node with
7
+ * raw HTML) handles anything it doesn't recognize.
8
+ */
9
+ export declare const importDocxDocumentWithMammoth: (arrayBuffer: ArrayBuffer) => Promise<SmartDocument>;
@@ -0,0 +1,37 @@
1
+ import JSZip from "jszip";
2
+ import mammoth from "mammoth";
3
+ import { parseCanonicalListHtml } from "../../list/formats.js";
4
+ import { restorePortableDocxAtoms } from "./portableAtoms.js";
5
+ import { assertDocxXmlWithinDepthLimit } from "./nestingGuard.js";
6
+ /**
7
+ * Fidelity: `semantic` for structure. Matches the established pattern for
8
+ * plain HTML import (CanonicalAuthorityEditor's runImport) - mammoth already
9
+ * produces clean semantic HTML, so no compatibility/normalize pass is
10
+ * inserted here; parseCanonicalListHtml's own fallback ("unknown" node with
11
+ * raw HTML) handles anything it doesn't recognize.
12
+ */
13
+ export const importDocxDocumentWithMammoth = async (arrayBuffer) => {
14
+ // Defense-in-depth against pathologically deep XML - see nestingGuard.ts's
15
+ // own doc comment for why this exists alongside (not instead of) the real
16
+ // fixes in mammoth's own patched code. Checked against document.xml
17
+ // specifically: the primary, always-present part every .docx has, and the
18
+ // one the disclosed attack targets. A package that fails to unzip or has
19
+ // no document.xml is left for mammoth's own error handling below, not
20
+ // rejected here - this guard only rejects on a confirmed-deep document.
21
+ try {
22
+ const zip = await JSZip.loadAsync(arrayBuffer);
23
+ const documentXml = await zip.file("word/document.xml")?.async("text");
24
+ if (documentXml)
25
+ assertDocxXmlWithinDepthLimit(documentXml);
26
+ }
27
+ catch (error) {
28
+ if (error instanceof Error && error.name === "DocxNestingTooDeepError")
29
+ throw error;
30
+ // Any other failure here (corrupt zip, etc.) is mammoth's to report.
31
+ }
32
+ // Mammoth's browser entry reads `arrayBuffer`, while its Node entry reads
33
+ // `buffer`. Supplying both keeps the adapter portable across bundlers/tests.
34
+ const result = await mammoth.convertToHtml({ arrayBuffer, buffer: arrayBuffer });
35
+ const html = restorePortableDocxAtoms(result.value);
36
+ return parseCanonicalListHtml(html);
37
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Defense-in-depth against pathologically deep XML inside a `.docx` package.
3
+ *
4
+ * Not the real fix for any specific recursion bug - see
5
+ * docs/bugs/mammoth-own-reader-unguarded-recursion-dos.md and
6
+ * docs/bugs/mammoth-bundles-vulnerable-transitive-xmldom.md for those (an
7
+ * iterative rewrite of mammoth's own XML-to-node conversion, and a pinned/
8
+ * patched @xmldom/xmldom). This exists because verifying those two fixes
9
+ * against the real attack surfaced a *third*, independent unguarded-recursion
10
+ * function elsewhere in mammoth's own code (`office-xml-reader.js`'s
11
+ * `collapseAlternateContent`, out of scope to patch here) - a pattern of
12
+ * multiple such functions, not one isolated bug. A cheap, iterative (never
13
+ * recursive - this guard must not become another instance of the same class
14
+ * of bug) upfront depth check protects against that specific function and
15
+ * any other not-yet-discovered one, without waiting on an exhaustive audit
16
+ * of mammoth's entire codebase.
17
+ *
18
+ * Deliberately coarse, not a real XML parser: a linear scan counting
19
+ * tag-open/tag-close balance. Does not correctly account for `<`/`>`
20
+ * characters inside comments, CDATA sections, or attribute values - for a
21
+ * generously-set threshold, that only risks *under*-counting depth (a
22
+ * false negative, i.e. missing a genuinely deep document), never rejecting
23
+ * a shallow legitimate one, so it stays a safe, one-directional guard.
24
+ */
25
+ /** Real Word documents essentially never nest structurally beyond a few
26
+ * dozen levels even in pathological cases (tables nested in tables nested
27
+ * in tracked-change wrappers nested in content controls). The disclosed
28
+ * @xmldom/xmldom recursion advisory measured crash thresholds starting
29
+ * around 5,000 levels for the lightest-per-frame operations tested. 1,000
30
+ * sits an order of magnitude above any plausible real document and a
31
+ * comfortable margin below the lowest crash threshold observed for any
32
+ * function measured so far, leaving room for an undiscovered function with
33
+ * heavier per-frame stack usage than anything tested to date. */
34
+ export declare const MAX_DOCX_XML_NESTING_DEPTH = 1000;
35
+ export declare class DocxNestingTooDeepError extends Error {
36
+ constructor(depth: number, max: number);
37
+ }
38
+ /** Returns the deepest tag-nesting level found, without recursing. */
39
+ export declare const maxXmlNestingDepth: (xml: string) => number;
40
+ export declare const assertDocxXmlWithinDepthLimit: (xml: string, max?: number) => void;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Defense-in-depth against pathologically deep XML inside a `.docx` package.
3
+ *
4
+ * Not the real fix for any specific recursion bug - see
5
+ * docs/bugs/mammoth-own-reader-unguarded-recursion-dos.md and
6
+ * docs/bugs/mammoth-bundles-vulnerable-transitive-xmldom.md for those (an
7
+ * iterative rewrite of mammoth's own XML-to-node conversion, and a pinned/
8
+ * patched @xmldom/xmldom). This exists because verifying those two fixes
9
+ * against the real attack surfaced a *third*, independent unguarded-recursion
10
+ * function elsewhere in mammoth's own code (`office-xml-reader.js`'s
11
+ * `collapseAlternateContent`, out of scope to patch here) - a pattern of
12
+ * multiple such functions, not one isolated bug. A cheap, iterative (never
13
+ * recursive - this guard must not become another instance of the same class
14
+ * of bug) upfront depth check protects against that specific function and
15
+ * any other not-yet-discovered one, without waiting on an exhaustive audit
16
+ * of mammoth's entire codebase.
17
+ *
18
+ * Deliberately coarse, not a real XML parser: a linear scan counting
19
+ * tag-open/tag-close balance. Does not correctly account for `<`/`>`
20
+ * characters inside comments, CDATA sections, or attribute values - for a
21
+ * generously-set threshold, that only risks *under*-counting depth (a
22
+ * false negative, i.e. missing a genuinely deep document), never rejecting
23
+ * a shallow legitimate one, so it stays a safe, one-directional guard.
24
+ */
25
+ /** Real Word documents essentially never nest structurally beyond a few
26
+ * dozen levels even in pathological cases (tables nested in tables nested
27
+ * in tracked-change wrappers nested in content controls). The disclosed
28
+ * @xmldom/xmldom recursion advisory measured crash thresholds starting
29
+ * around 5,000 levels for the lightest-per-frame operations tested. 1,000
30
+ * sits an order of magnitude above any plausible real document and a
31
+ * comfortable margin below the lowest crash threshold observed for any
32
+ * function measured so far, leaving room for an undiscovered function with
33
+ * heavier per-frame stack usage than anything tested to date. */
34
+ export const MAX_DOCX_XML_NESTING_DEPTH = 1000;
35
+ export class DocxNestingTooDeepError extends Error {
36
+ constructor(depth, max) {
37
+ super(`DOCX document.xml nests ${depth} levels deep, exceeding the ${max}-level safety limit.`);
38
+ this.name = "DocxNestingTooDeepError";
39
+ }
40
+ }
41
+ const TAG_PATTERN = /<(\/?)([^\s/>!?][^>]*?)(\/?)>/g;
42
+ /** Returns the deepest tag-nesting level found, without recursing. */
43
+ export const maxXmlNestingDepth = (xml) => {
44
+ let depth = 0;
45
+ let peak = 0;
46
+ TAG_PATTERN.lastIndex = 0;
47
+ let match;
48
+ while ((match = TAG_PATTERN.exec(xml))) {
49
+ const isClosing = match[1] === "/";
50
+ const isSelfClosing = match[3] === "/";
51
+ if (isClosing) {
52
+ depth = Math.max(0, depth - 1);
53
+ }
54
+ else if (!isSelfClosing) {
55
+ depth += 1;
56
+ if (depth > peak)
57
+ peak = depth;
58
+ }
59
+ }
60
+ return peak;
61
+ };
62
+ export const assertDocxXmlWithinDepthLimit = (xml, max = MAX_DOCX_XML_NESTING_DEPTH) => {
63
+ const depth = maxXmlNestingDepth(xml);
64
+ if (depth > max)
65
+ throw new DocxNestingTooDeepError(depth, max);
66
+ };
@@ -0,0 +1,4 @@
1
+ export declare const portableFormulaMarker: (value: string, displayText?: string) => string;
2
+ export declare const portableImageMarker: (src: string, alt?: string, title?: string) => string;
3
+ /** Restores Smart RTE atom fallback markers retained by DOCX text runs. */
4
+ export declare const restorePortableDocxAtoms: (html: string) => string;
@@ -0,0 +1,66 @@
1
+ import { parseFragment, serialize } from "parse5";
2
+ const marker = /⟦SRTE_(FORMULA|IMAGE):([^⟧]+)⟧/g;
3
+ export const portableFormulaMarker = (value, displayText) => `⟦SRTE_FORMULA:${encodeURIComponent(JSON.stringify({ value, displayText }))}⟧`;
4
+ export const portableImageMarker = (src, alt, title) => `⟦SRTE_IMAGE:${encodeURIComponent(JSON.stringify({ src, alt, title }))}⟧`;
5
+ const XHTML_NS = "http://www.w3.org/1999/xhtml";
6
+ const textNode = (value) => ({ nodeName: "#text", value });
7
+ const elementNode = (tagName, attrs, childNodes = []) => ({
8
+ nodeName: tagName, tagName, namespaceURI: XHTML_NS, attrs, childNodes,
9
+ });
10
+ const splitPortableAtoms = (text) => {
11
+ marker.lastIndex = 0;
12
+ if (!marker.test(text))
13
+ return [textNode(text)];
14
+ marker.lastIndex = 0;
15
+ const output = [];
16
+ let cursor = 0;
17
+ let match;
18
+ while ((match = marker.exec(text))) {
19
+ if (match.index > cursor)
20
+ output.push(textNode(text.slice(cursor, match.index)));
21
+ try {
22
+ const payload = JSON.parse(decodeURIComponent(match[2]));
23
+ if (match[1] === "FORMULA" && payload.value) {
24
+ // Fidelity note: canonical's own native HTML round-trip never
25
+ // preserves formula displayText either (atomToHtml doesn't write
26
+ // it, the parser doesn't read it back) - only source/notation
27
+ // survive. Not a DOCX-specific gap.
28
+ output.push(elementNode("span", [
29
+ { name: "data-smart-type", value: "formula" },
30
+ { name: "data-smart-formula", value: payload.value },
31
+ ], [textNode(payload.displayText || `$${payload.value}$`)]));
32
+ }
33
+ else if (match[1] === "IMAGE" && payload.src) {
34
+ const attrs = [
35
+ { name: "src", value: payload.src },
36
+ { name: "alt", value: payload.alt || "" },
37
+ { name: "data-srte-inline", value: "true" },
38
+ ];
39
+ if (payload.title)
40
+ attrs.push({ name: "title", value: payload.title });
41
+ output.push(elementNode("img", attrs));
42
+ }
43
+ else {
44
+ output.push(textNode(match[0]));
45
+ }
46
+ }
47
+ catch {
48
+ output.push(textNode(match[0]));
49
+ }
50
+ cursor = marker.lastIndex;
51
+ }
52
+ if (cursor < text.length)
53
+ output.push(textNode(text.slice(cursor)));
54
+ return output;
55
+ };
56
+ const visit = (node) => {
57
+ if (!node.childNodes)
58
+ return node;
59
+ const childNodes = node.childNodes.flatMap((child) => child.nodeName === "#text" && child.value ? splitPortableAtoms(child.value) : [visit(child)]);
60
+ return { ...node, childNodes };
61
+ };
62
+ /** Restores Smart RTE atom fallback markers retained by DOCX text runs. */
63
+ export const restorePortableDocxAtoms = (html) => {
64
+ const fragment = visit(parseFragment(html));
65
+ return serialize(fragment);
66
+ };
@@ -0,0 +1,10 @@
1
+ import type { SmartDocument } from "../../types.js";
2
+ export interface StyledDocxImportResult {
3
+ document: SmartDocument;
4
+ layoutHtml: string;
5
+ source: "wordprocessingml" | "mammoth";
6
+ }
7
+ /** Defensive baseline styling for any table missing explicit inline styles (e.g. the mammoth fallback path, which produces plain semantic HTML with no inline CSS at all). */
8
+ export declare const enhanceDocxTables: (html: string) => string;
9
+ /** Fidelity: `semantic` for structure, richer visual reproduction than plain mammoth import via inline CSS reconstructed from raw wordprocessingml formatting XML, with graceful fallback to mammoth if that XML is missing or malformed. */
10
+ export declare const importStyledDocxDocument: (arrayBuffer: ArrayBuffer) => Promise<StyledDocxImportResult>;