smartrte-core 0.2.1 → 1.0.0-beta.2

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 +24 -0
  19. package/dist/foundation/atom/formats.js +105 -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 +21 -0
  127. package/dist/foundation/list/formats.js +813 -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 +27 -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 +28 -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,813 @@
1
+ import { parseFragment, serializeOuter } from "parse5";
2
+ import { unified } from "unified";
3
+ import remarkGfm from "remark-gfm";
4
+ import remarkParse from "remark-parse";
5
+ import { createNodeId, isTextNode } from "../identity.js";
6
+ import { canonicalMarkAttrs, canonicalMarkOrder } from "../marks/canonical.js";
7
+ import { occupancyGridFor } from "../table/grid.js";
8
+ import { atomToHtml, atomToMarkdown } from "../atom/formats.js";
9
+ import { sanitizeAtomSource } from "../atom/security.js";
10
+ import { foundationListStyleForPresetDepth, isFoundationSmartListPreset } from "./presets.js";
11
+ const escapeHtml = (value) => String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
12
+ const attr = (node, name) => node.attrs?.find((candidate) => candidate.name === name)?.value;
13
+ const generatedId = (node, prefix) => attr(node, "data-smart-id") || `${prefix}-${createNodeId()}`;
14
+ const isEditorUiNode = (node) => attr(node, "data-smart-ui") !== undefined || attr(node, "data-srte-check") !== undefined;
15
+ /**
16
+ * Real-world exports (Google Docs, Word, ChatGPT, Sootr) routinely wrap
17
+ * meaningful block content - most often a <table> - in one or more <div>s
18
+ * (table-wrapper divs, layout divs) that carry no semantic meaning of
19
+ * their own. parseBlock has no case for any of these tags; see
20
+ * parseBlockList below for how they're unwrapped instead of swallowed.
21
+ */
22
+ const TRANSPARENT_CONTAINER_TAGS = ["div", "section", "article", "figure"];
23
+ /** See serializeCanonicalListHtml's own doc comment for why this is a module-scoped flag rather than a threaded parameter. */
24
+ let renderFormulaHtmlMode = false;
25
+ const serializeInline = (node) => {
26
+ if (!isTextNode(node)) {
27
+ if (node.type === "hard_break")
28
+ return `<br data-smart-id="${escapeHtml(node.id)}" data-smart-type="hard_break">`;
29
+ if (["image", "formula"].includes(node.type))
30
+ return atomToHtml(node, { renderFormulaHtml: renderFormulaHtmlMode });
31
+ const raw = node.attrs?.raw;
32
+ if (typeof raw?.html === "string")
33
+ return raw.html;
34
+ return `<span data-smart-id="${escapeHtml(node.id)}" data-smart-atomic="true"></span>`;
35
+ }
36
+ let html = escapeHtml(node.text);
37
+ [...canonicalMarkOrder(node.marks)].reverse().forEach((mark) => {
38
+ if (mark.type === "bold")
39
+ html = `<strong>${html}</strong>`;
40
+ else if (mark.type === "italic")
41
+ html = `<em>${html}</em>`;
42
+ else if (mark.type === "underline")
43
+ html = `<u>${html}</u>`;
44
+ else if (mark.type === "strike")
45
+ html = `<s>${html}</s>`;
46
+ else if (mark.type === "code")
47
+ html = `<code>${html}</code>`;
48
+ else if (mark.type === "superscript")
49
+ html = `<sup>${html}</sup>`;
50
+ else if (mark.type === "subscript")
51
+ html = `<sub>${html}</sub>`;
52
+ else if (mark.type === "textColor")
53
+ html = `<span style="color:${escapeHtml(mark.attrs?.value || "")}">${html}</span>`;
54
+ else if (mark.type === "backgroundColor")
55
+ html = `<span style="background-color:${escapeHtml(mark.attrs?.value || "")}">${html}</span>`;
56
+ else if (mark.type === "fontSize")
57
+ html = `<span style="font-size:${escapeHtml(mark.attrs?.valuePx || "")}px">${html}</span>`;
58
+ else if (mark.type === "fontFamily")
59
+ html = `<span style="font-family:${escapeHtml(mark.attrs?.value || "")}">${html}</span>`;
60
+ else if (mark.type === "link")
61
+ html = `<a href="${escapeHtml(mark.attrs?.href || "")}"${mark.attrs?.target ? ` target="${escapeHtml(mark.attrs.target)}"` : ""}>${html}</a>`;
62
+ });
63
+ return html;
64
+ };
65
+ const orderedList = (node) => {
66
+ const marker = String(node.attrs?.style || node.attrs?.preset || "");
67
+ return /^(?:decimal|lower-|upper-|ordered)/.test(marker) || node.attrs?.start !== undefined;
68
+ };
69
+ const effectiveStyle = (node, depth) => {
70
+ if (typeof node.attrs?.style === "string")
71
+ return node.attrs.style;
72
+ return isFoundationSmartListPreset(node.attrs?.preset)
73
+ ? foundationListStyleForPresetDepth(node.attrs.preset, depth)
74
+ : undefined;
75
+ };
76
+ const blockAttributes = (node) => {
77
+ const declarations = [
78
+ typeof node.attrs?.htmlStyle === "string" ? node.attrs.htmlStyle.replace(/;\s*$/, "") : "",
79
+ typeof node.attrs?.align === "string" ? `text-align:${node.attrs.align}` : "",
80
+ Number(node.attrs?.indentLevel) > 0 ? `margin-inline-start:${Number(node.attrs?.indentLevel) * 2}em` : "",
81
+ ].filter(Boolean);
82
+ return [
83
+ node.attrs?.align ? ` data-smart-align="${escapeHtml(node.attrs.align)}"` : "",
84
+ Number(node.attrs?.indentLevel) > 0 ? ` data-smart-indent="${escapeHtml(node.attrs?.indentLevel)}"` : "",
85
+ declarations.length ? ` style="${escapeHtml(declarations.join(";"))}"` : "",
86
+ ].join("");
87
+ };
88
+ const serializeBlock = (node, includeIds, listDepth = 0, tableCellA11y) => {
89
+ if (node.type === "unknown") {
90
+ const raw = node.attrs?.raw;
91
+ if (typeof raw?.html === "string")
92
+ return raw.html;
93
+ }
94
+ const id = includeIds ? ` data-smart-id="${escapeHtml(node.id)}"` : "";
95
+ if (["block_image", "block_formula", "video", "audio", "divider"].includes(node.type))
96
+ return atomToHtml(node, { renderFormulaHtml: renderFormulaHtmlMode });
97
+ if (node.type === "paragraph" || node.type === "heading") {
98
+ const tag = node.type === "heading" ? `h${String(node.attrs?.level || 1)}` : "p";
99
+ return `<${tag}${id}${blockAttributes(node)}>${(node.children || []).map(serializeInline).join("")}</${tag}>`;
100
+ }
101
+ if (node.type === "blockquote") {
102
+ return `<blockquote${id}${blockAttributes(node)}>${(node.children || []).map((child) => isTextNode(child) ? serializeInline(child) : serializeBlock(child, includeIds, listDepth)).join("")}</blockquote>`;
103
+ }
104
+ if (node.type === "code_block") {
105
+ const language = typeof node.attrs?.language === "string" && node.attrs.language ? node.attrs.language : undefined;
106
+ const languageAttrs = language
107
+ ? ` data-smart-language="${escapeHtml(language)}"><code class="language-${escapeHtml(language)}"`
108
+ : "><code";
109
+ const text = (node.children || []).map((child) => isTextNode(child) ? child.text : child.type === "hard_break" ? "\n" : "").join("");
110
+ return `<pre${id}${blockAttributes(node)}${languageAttrs}>${escapeHtml(text)}</code></pre>`;
111
+ }
112
+ if (node.type === "table") {
113
+ const grid = occupancyGridFor(node);
114
+ let headerRows = 0;
115
+ while (headerRows < grid.rows && Array.from({ length: grid.columns }, (_, column) => grid.at(headerRows, column)?.node.attrs?.header === true).every(Boolean))
116
+ headerRows += 1;
117
+ let headerColumns = 0;
118
+ while (headerColumns < grid.columns && Array.from({ length: grid.rows }, (_, row) => grid.at(row, headerColumns)?.node.attrs?.header === true).every(Boolean))
119
+ headerColumns += 1;
120
+ const headerIds = new Map(grid.anchors.filter((cell) => cell.node.attrs?.header === true).map((cell) => [cell.cellId, `smart-header-${node.id}-${cell.cellId}`]));
121
+ const cellA11y = (cell) => {
122
+ const anchor = grid.anchors.find((candidate) => candidate.cellId === cell.id);
123
+ if (!anchor)
124
+ return {};
125
+ if (cell.attrs?.header === true)
126
+ return { id: headerIds.get(cell.id), scope: anchor.left < headerColumns && anchor.top >= headerRows ? "row" : "col" };
127
+ const headers = grid.anchors.filter((header) => header.node.attrs?.header === true && (header.top < headerRows && header.left <= anchor.left && header.right > anchor.left
128
+ || header.left < headerColumns && header.top <= anchor.top && header.bottom > anchor.top)).map((header) => headerIds.get(header.cellId)).filter((value) => Boolean(value));
129
+ return headers.length ? { headers: headers.join(" ") } : {};
130
+ };
131
+ const widths = Array.isArray(node.attrs?.columnWidths) ? node.attrs.columnWidths : [];
132
+ const colgroup = widths.length ? `<colgroup>${widths.map((width) => `<col style="width:${escapeHtml(width)}px">`).join("")}</colgroup>` : "";
133
+ const caption = typeof node.attrs?.caption === "string" && node.attrs.caption ? `<caption>${escapeHtml(node.attrs.caption)}</caption>` : "";
134
+ const layout = node.attrs?.layout ? ` data-smart-layout="${escapeHtml(node.attrs.layout)}" style="table-layout:${escapeHtml(node.attrs.layout)}"` : "";
135
+ const rows = (node.children || []).map((row) => isTextNode(row) ? "" : `<tr${includeIds ? ` data-smart-id="${escapeHtml(row.id)}"` : ""}${Number(row.attrs?.height) > 0 ? ` style="height:${Number(row.attrs?.height)}px"` : ""}>${(row.children || []).map((cell) => isTextNode(cell) ? "" : serializeBlock(cell, includeIds, listDepth, cellA11y(cell))).join("")}</tr>`).join("");
136
+ return `<table${id}${layout}>${caption}${colgroup}<tbody>${rows}</tbody></table>`;
137
+ }
138
+ if (node.type === "table_row") {
139
+ const height = Number(node.attrs?.height);
140
+ return `<tr${id}${Number.isFinite(height) && height > 0 ? ` style="height:${height}px"` : ""}>${(node.children || []).map((child) => isTextNode(child) ? "" : serializeBlock(child, includeIds, listDepth)).join("")}</tr>`;
141
+ }
142
+ if (node.type === "table_cell") {
143
+ const tag = node.attrs?.header === true ? "th" : "td";
144
+ const rowspan = Math.max(1, Number(node.attrs?.rowspan) || 1);
145
+ const colspan = Math.max(1, Number(node.attrs?.colspan) || 1);
146
+ const styles = [
147
+ node.attrs?.background ? `background:${String(node.attrs.background)}` : "",
148
+ node.attrs?.textColor ? `color:${String(node.attrs.textColor)}` : "",
149
+ node.attrs?.borders ? `border:${String(node.attrs.borders)}` : "",
150
+ // Per-side overrides emit after the uniform shorthand, same
151
+ // last-write-wins order the renderer applies them in.
152
+ node.attrs?.borderTop ? `border-top:${String(node.attrs.borderTop)}` : "",
153
+ node.attrs?.borderRight ? `border-right:${String(node.attrs.borderRight)}` : "",
154
+ node.attrs?.borderBottom ? `border-bottom:${String(node.attrs.borderBottom)}` : "",
155
+ node.attrs?.borderLeft ? `border-left:${String(node.attrs.borderLeft)}` : "",
156
+ node.attrs?.verticalAlign ? `vertical-align:${String(node.attrs.verticalAlign)}` : "",
157
+ ].filter(Boolean).join(";");
158
+ return `<${tag}${id}${tableCellA11y?.id ? ` id="${escapeHtml(tableCellA11y.id)}"` : ""}${tableCellA11y?.scope ? ` scope="${tableCellA11y.scope}"` : ""}${tableCellA11y?.headers ? ` headers="${escapeHtml(tableCellA11y.headers)}"` : ""}${rowspan > 1 ? ` rowspan="${rowspan}"` : ""}${colspan > 1 ? ` colspan="${colspan}"` : ""}${styles ? ` style="${escapeHtml(styles)}"` : ""}>${(node.children || []).map((child) => isTextNode(child) ? serializeInline(child) : serializeBlock(child, includeIds, listDepth)).join("")}</${tag}>`;
159
+ }
160
+ if (node.type === "list") {
161
+ const tag = orderedList(node) ? "ol" : "ul";
162
+ const style = effectiveStyle(node, listDepth);
163
+ const attrs = [
164
+ id,
165
+ node.attrs?.preset ? ` data-smart-list-preset="${escapeHtml(node.attrs.preset)}" data-srte-list-preset="${escapeHtml(node.attrs.preset)}" data-srte-list-depth="${listDepth}"` : "",
166
+ node.attrs?.style ? ` data-smart-list-style="${escapeHtml(node.attrs.style)}"` : "",
167
+ style ? ` style="list-style-type:${escapeHtml(style)}"` : "",
168
+ node.attrs?.start !== undefined ? ` start="${escapeHtml(node.attrs.start)}"` : "",
169
+ node.attrs?.checkable === true ? ` data-smart-checkable="true" data-srte-checklist="true"` : "",
170
+ ].join("");
171
+ return `<${tag}${attrs}>${(node.children || []).map((child) => isTextNode(child) ? "" : serializeBlock(child, includeIds, listDepth)).join("")}</${tag}>`;
172
+ }
173
+ if (node.type === "list_item") {
174
+ const attrs = [
175
+ id,
176
+ node.attrs?.checked !== undefined ? ` data-smart-checked="${node.attrs.checked === true ? "true" : "false"}" data-checked="${node.attrs.checked === true ? "true" : "false"}"` : "",
177
+ typeof node.attrs?.htmlStyle === "string" ? ` style="${escapeHtml(node.attrs.htmlStyle)}"` : "",
178
+ node.attrs?.numberOverride !== undefined ? ` value="${escapeHtml(node.attrs.numberOverride)}"` : "",
179
+ ].join("");
180
+ return `<li${attrs}>${(node.children || []).map((child) => isTextNode(child) ? serializeInline(child) : serializeBlock(child, includeIds, child.type === "list" ? listDepth + 1 : listDepth)).join("")}</li>`;
181
+ }
182
+ return `<div${id}>${(node.children || []).map((child) => isTextNode(child) ? serializeInline(child) : serializeBlock(child, includeIds)).join("")}</div>`;
183
+ };
184
+ export const serializeCanonicalListHtml = (document, options = {}) => {
185
+ // A module-scoped flag, not a threaded parameter: serializeInline/
186
+ // serializeBlock recurse through dozens of call sites (lists, tables,
187
+ // blockquotes) - threading a new parameter through every one of them is
188
+ // exactly the kind of change that's easy to miss at one nested call site
189
+ // and silently break there. Safe here because serialization is
190
+ // synchronous, single-threaded, non-reentrant tree recursion - the flag
191
+ // is always restored in `finally` before this function ever returns.
192
+ const previous = renderFormulaHtmlMode;
193
+ renderFormulaHtmlMode = options.renderFormulaHtml === true;
194
+ try {
195
+ return options.clean === true
196
+ ? document.children.map((node) => isTextNode(node) ? serializeInline(node) : serializeBlock(node, false)).join("")
197
+ .replace(/\sdata-smart-id=(?:"[^"]*"|'[^']*')/g, "")
198
+ : options.fragment === true
199
+ ? document.children.map((node) => isTextNode(node) ? serializeInline(node) : serializeBlock(node, true)).join("")
200
+ : `<div data-smart-document="true" data-smart-id="${escapeHtml(document.id)}">${document.children.map((node) => isTextNode(node) ? serializeInline(node) : serializeBlock(node, true)).join("")}</div>`;
201
+ }
202
+ finally {
203
+ renderFormulaHtmlMode = previous;
204
+ }
205
+ };
206
+ const textWithMarks = (node, inherited = []) => {
207
+ if (isEditorUiNode(node))
208
+ return [];
209
+ if (node.nodeName === "#text")
210
+ return node.value ? [{ type: "text", text: node.value, ...(inherited.length ? { marks: [...inherited] } : {}) }] : [];
211
+ const tag = node.tagName || "";
212
+ if (tag === "br" || attr(node, "data-smart-type") === "hard_break")
213
+ return [{
214
+ type: "hard_break", id: generatedId(node, "break"),
215
+ }];
216
+ const declaredAtom = attr(node, "data-smart-type");
217
+ if (tag === "img" || declaredAtom === "image" || declaredAtom === "formula") {
218
+ if (declaredAtom === "formula")
219
+ return [{ type: "formula", id: generatedId(node, "formula"), attrs: { source: attr(node, "data-smart-formula") || rawText(node), notation: attr(node, "data-smart-notation") === "mathml" ? "mathml" : "latex" } }];
220
+ const src = sanitizeAtomSource(attr(node, "src"), { kind: "image", allowBlobPreview: attr(node, "data-smart-status") === "pending" });
221
+ if (src) {
222
+ // Real-world sources (Google Docs, most web pages, this app's own
223
+ // exported markup) far more commonly size an <img> via inline
224
+ // `style="width:...;height:...;"` than the legacy HTML width/height
225
+ // attributes - style must win when both are present. naturalWidth/
226
+ // naturalHeight are deliberately not consulted: paste parsing is
227
+ // synchronous and the image hasn't loaded yet at this point.
228
+ const width = parsePixelWidth(styleValue(node, "width")) ?? parsePixelWidth(attr(node, "width"));
229
+ const height = parsePixelWidth(styleValue(node, "height")) ?? parsePixelWidth(attr(node, "height"));
230
+ return [{ type: "image", id: generatedId(node, "image"), attrs: {
231
+ src, alt: attr(node, "alt") || "", status: attr(node, "data-smart-status") || "ready",
232
+ ...(attr(node, "data-smart-decorative") === "true" ? { decorative: true } : {}),
233
+ ...(attr(node, "title") ? { title: attr(node, "title") } : {}),
234
+ ...(attr(node, "data-smart-align") ? { align: attr(node, "data-smart-align") } : {}),
235
+ ...(width !== null ? { width } : {}), ...(height !== null ? { height } : {}),
236
+ } }];
237
+ }
238
+ }
239
+ if (attr(node, "data-smart-atomic") === "true")
240
+ return [{
241
+ type: "unknown", id: generatedId(node, "atom"),
242
+ attrs: { originalType: attr(node, "data-smart-unknown-type") || tag || "inline-atom", originalGroup: "inline", raw: { html: serializeOuter(node) }, editable: false },
243
+ }];
244
+ let marks = [...inherited];
245
+ const push = (mark) => { if (mark)
246
+ marks.push(mark); };
247
+ if (tag === "strong" || tag === "b")
248
+ push({ type: "bold" });
249
+ if (tag === "em" || tag === "i")
250
+ push({ type: "italic" });
251
+ if (tag === "u")
252
+ push({ type: "underline" });
253
+ if (tag === "s" || tag === "strike" || tag === "del")
254
+ push({ type: "strike" });
255
+ if (tag === "code")
256
+ push({ type: "code" });
257
+ if (tag === "sup")
258
+ push({ type: "superscript" });
259
+ if (tag === "sub")
260
+ push({ type: "subscript" });
261
+ if (tag === "a")
262
+ push({ type: "link", attrs: {
263
+ href: attr(node, "href") || "",
264
+ ...(attr(node, "target") ? { target: attr(node, "target") } : {}),
265
+ } });
266
+ // Real-world exports (this includes our own DOCX import - styledImport.ts's
267
+ // runStyle already emits font-weight/font-style/text-decoration for
268
+ // <w:b>/<w:i>/<w:u> runs, on the assumption these get parsed back into
269
+ // marks) commonly signal bold/italic/underline via inline style rather
270
+ // than <strong>/<em>/<u>. "bolder" is technically relative to the
271
+ // inherited weight, but without full cascade resolution here, treating it
272
+ // (and >=700, matching what styledImport.ts itself generates) as bold is
273
+ // the same approximation color/background-color/font-size/font-family
274
+ // below already make from raw style values.
275
+ const weight = styleValue(node, "font-weight")?.toLowerCase();
276
+ if (weight && !marks.some((mark) => mark.type === "bold") && (weight === "bold" || weight === "bolder" || Number(weight) >= 700)) {
277
+ push({ type: "bold" });
278
+ }
279
+ const style = styleValue(node, "font-style")?.toLowerCase();
280
+ if (style && !marks.some((mark) => mark.type === "italic") && (style === "italic" || style === "oblique")) {
281
+ push({ type: "italic" });
282
+ }
283
+ const decoration = styleValue(node, "text-decoration")?.toLowerCase();
284
+ if (decoration?.includes("underline") && !marks.some((mark) => mark.type === "underline"))
285
+ push({ type: "underline" });
286
+ if (decoration?.includes("line-through") && !marks.some((mark) => mark.type === "strike"))
287
+ push({ type: "strike" });
288
+ const declared = attr(node, "data-smart-mark");
289
+ if (declared && !marks.some((mark) => mark.type === declared)) {
290
+ const raw = attr(node, "data-smart-mark-attrs");
291
+ try {
292
+ push({ type: declared, ...(raw ? { attrs: JSON.parse(raw) } : {}) });
293
+ }
294
+ catch { /* invalid metadata is ignored */ }
295
+ }
296
+ const color = styleValue(node, "color");
297
+ const background = styleValue(node, "background-color");
298
+ const size = styleValue(node, "font-size");
299
+ const family = styleValue(node, "font-family");
300
+ const attributed = [
301
+ ...(color ? [["textColor", { value: color }]] : []),
302
+ ...(background ? [["backgroundColor", { value: background }]] : []),
303
+ ...(size ? [["fontSize", { valuePx: size }]] : []),
304
+ ...(family ? [["fontFamily", { value: family }]] : []),
305
+ ];
306
+ attributed.forEach(([type, attrs]) => {
307
+ const canonical = canonicalMarkAttrs(type, attrs);
308
+ if (canonical && !marks.some((mark) => mark.type === type))
309
+ push({ type, attrs: canonical });
310
+ });
311
+ marks = canonicalMarkOrder(marks);
312
+ return (node.childNodes || []).flatMap((child) => textWithMarks(child, marks));
313
+ };
314
+ const elementChildren = (node) => (node.childNodes || []).filter((child) => Boolean(child.tagName) && !isEditorUiNode(child));
315
+ const styleValue = (node, property) => attr(node, "style")?.split(";").map((part) => part.split(":"))
316
+ .find(([name]) => name?.trim().toLowerCase() === property)?.[1]?.trim();
317
+ /**
318
+ * A bare number (the legacy HTML `width` attribute's convention) or an
319
+ * explicit "Npx" CSS value - anything else (a percentage, "auto", em,
320
+ * missing) is rejected rather than guessed at. `Number.parseFloat("50%")`
321
+ * would silently read as "50 real pixels", a drastic understatement once
322
+ * a table's own rendered width is pinned to the literal sum of its column
323
+ * widths (surface/renderer.ts) - this is what previously made a pasted
324
+ * table with percentage or unspecified column widths shrink far below its
325
+ * real size instead of being left to render at its natural default.
326
+ */
327
+ const parsePixelWidth = (raw) => {
328
+ const match = raw ? /^\s*([\d.]+)\s*(?:px)?\s*$/i.exec(raw) : null;
329
+ if (!match)
330
+ return null;
331
+ const width = Number.parseFloat(match[1]);
332
+ return Number.isFinite(width) && width > 0 ? width : null;
333
+ };
334
+ const withoutListMarkerStyle = (style) => {
335
+ if (!style)
336
+ return undefined;
337
+ const retained = style.split(";")
338
+ .map((declaration) => declaration.trim())
339
+ .filter((declaration) => declaration && !/^list-style(?:-type)?\s*:/i.test(declaration));
340
+ return retained.length ? retained.join(";") : undefined;
341
+ };
342
+ const parsedBlockAttrs = (node) => {
343
+ const attrs = {};
344
+ const align = attr(node, "data-smart-align") || styleValue(node, "text-align");
345
+ const explicitIndent = Number(attr(node, "data-smart-indent"));
346
+ const legacyIndent = styleValue(node, "margin-left");
347
+ const logicalIndent = styleValue(node, "margin-inline-start");
348
+ const cssIndent = legacyIndent || logicalIndent;
349
+ const computedIndent = cssIndent?.endsWith("px") ? Number.parseFloat(cssIndent) / 24
350
+ : cssIndent?.endsWith("em") ? Number.parseFloat(cssIndent) / 2 : 0;
351
+ const indent = Number.isFinite(explicitIndent) && explicitIndent > 0 ? explicitIndent : computedIndent;
352
+ if (align && ["left", "center", "right", "justify"].includes(align))
353
+ attrs.align = align;
354
+ if (Number.isInteger(indent) && indent > 0)
355
+ attrs.indentLevel = indent;
356
+ return attrs;
357
+ };
358
+ const rawText = (node) => node.nodeName === "#text" ? node.value || ""
359
+ : node.tagName === "br" ? "\n"
360
+ : (node.childNodes || []).filter((child) => !isEditorUiNode(child)).map(rawText).join("");
361
+ const parseBlock = (node) => {
362
+ const tag = node.tagName || "";
363
+ const declaredAtom = attr(node, "data-smart-type");
364
+ if (declaredAtom === "block_image") {
365
+ const src = sanitizeAtomSource(attr(node, "src"), { kind: "image" });
366
+ if (!src)
367
+ return null;
368
+ // Mirrors the inline `image` atom's width/height parsing just above -
369
+ // style wins over the legacy HTML attributes, and this is also what
370
+ // this app's own copy output writes them as (atom/formats.ts's
371
+ // atomToHtml), so a self-copy/self-paste round-trip depends on this.
372
+ const width = parsePixelWidth(styleValue(node, "width")) ?? parsePixelWidth(attr(node, "width"));
373
+ const height = parsePixelWidth(styleValue(node, "height")) ?? parsePixelWidth(attr(node, "height"));
374
+ return { type: "block_image", id: generatedId(node, "image"), attrs: {
375
+ src, alt: attr(node, "alt") || "", status: attr(node, "data-smart-status") || "ready",
376
+ ...(attr(node, "data-smart-decorative") === "true" ? { decorative: true } : {}),
377
+ ...(width !== null ? { width } : {}), ...(height !== null ? { height } : {}),
378
+ } };
379
+ }
380
+ if (declaredAtom === "block_formula")
381
+ return { type: "block_formula", id: generatedId(node, "formula"), attrs: { source: attr(node, "data-smart-formula") || rawText(node), notation: attr(node, "data-smart-notation") === "mathml" ? "mathml" : "latex" } };
382
+ if (tag === "hr")
383
+ return { type: "divider", id: generatedId(node, "divider") };
384
+ // A third-party <img> sitting directly at block level (not wrapped in a
385
+ // <p>) - the ordinary shape for a standalone content photo on most real
386
+ // websites, e.g. a bare <img> between paragraphs or inside a <figure>
387
+ // (now a TRANSPARENT_CONTAINER_TAGS entry, so its <img> reaches here
388
+ // directly). Previously had no case at all - every recognized block tag
389
+ // above and below is checked first, and a bare "img" tag matches none of
390
+ // them, so it fell all the way to the generic "unrecognized tag"
391
+ // fallback at the bottom of this function, producing "[Unsupported:
392
+ // img]"/"[Unsupported: figure]" instead of an actual image. Mirrors the
393
+ // inline `image` atom's own src/width/height parsing just above in
394
+ // textWithMarks (declaredAtom === "block_image" above only fires for
395
+ // this app's own round-tripped export, which explicitly marks the tag -
396
+ // third-party HTML never does).
397
+ if (tag === "img") {
398
+ const src = sanitizeAtomSource(attr(node, "src"), { kind: "image" });
399
+ if (src) {
400
+ const width = parsePixelWidth(styleValue(node, "width")) ?? parsePixelWidth(attr(node, "width"));
401
+ const height = parsePixelWidth(styleValue(node, "height")) ?? parsePixelWidth(attr(node, "height"));
402
+ return { type: "block_image", id: generatedId(node, "image"), attrs: {
403
+ src, alt: attr(node, "alt") || "", status: "ready",
404
+ ...(width !== null ? { width } : {}), ...(height !== null ? { height } : {}),
405
+ } };
406
+ }
407
+ }
408
+ // Real, captured evidence (an actual browser "select the image element,
409
+ // copy" gesture - the closest scriptable equivalent of a native
410
+ // right-click "Copy image" - against a live Wikipedia photo): the
411
+ // clipboard payload was a bare <a href="..."><img ...></a>, no <p>/<td>
412
+ // wrapper at all. A link wrapping nothing but a single image, sitting at
413
+ // block level, unwraps to that image the same way the bare-<img> case
414
+ // above does - the link target (usually the source page/file, not
415
+ // meaningful to preserve as a clickable wrapper around an atomic block
416
+ // image in this schema) is intentionally dropped rather than attempted
417
+ // to be represented some other way.
418
+ if (tag === "a") {
419
+ const onlyChild = elementChildren(node);
420
+ if (onlyChild.length === 1 && onlyChild[0].tagName === "img") {
421
+ const image = parseBlock(onlyChild[0]);
422
+ if (image)
423
+ return image;
424
+ }
425
+ }
426
+ if (tag === "video" || tag === "audio") {
427
+ const src = sanitizeAtomSource(attr(node, "src"), { kind: tag });
428
+ return src ? { type: tag, id: generatedId(node, tag), attrs: { src, status: "ready", ...(tag === "video" && attr(node, "poster") ? { poster: attr(node, "poster") } : {}) } } : null;
429
+ }
430
+ if (tag === "p" || /^h[1-6]$/.test(tag))
431
+ return {
432
+ type: tag === "p" ? "paragraph" : "heading",
433
+ id: generatedId(node, tag),
434
+ ...((tag !== "p" || Object.keys(parsedBlockAttrs(node)).length) ? { attrs: {
435
+ ...(tag !== "p" ? { level: Number(tag.slice(1)) } : {}),
436
+ ...parsedBlockAttrs(node),
437
+ } } : {}),
438
+ children: (node.childNodes || []).flatMap((child) => textWithMarks(child)),
439
+ };
440
+ if (tag === "blockquote") {
441
+ // Real-world exports (Sootr among them) put inline content - <span>
442
+ // wrapper runs, bare text, <br> - directly inside <blockquote> with no
443
+ // wrapping <p>. Without this split, elementChildren+parseBlock alone
444
+ // sent every such child through the generic "unrecognized tag" fallback
445
+ // at the bottom of this function, producing an unknown block node per
446
+ // span (rendered as "[Unsupported: span]") instead of parsed text/marks
447
+ // - the same directInline pattern td/li already use below.
448
+ const blockTags = ["p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "blockquote", "pre", "table"];
449
+ const isBlockLike = (child) => blockTags.includes(child.tagName || "") || TRANSPARENT_CONTAINER_TAGS.includes(child.tagName || "");
450
+ const children = parseBlockList(elementChildren(node).filter(isBlockLike));
451
+ const directInline = (node.childNodes || []).filter((child) => !child.tagName || !isBlockLike(child)).flatMap((child) => textWithMarks(child));
452
+ if (directInline.length)
453
+ children.unshift({ type: "paragraph", id: createNodeId(), children: directInline });
454
+ return {
455
+ type: "blockquote", id: generatedId(node, "quote"),
456
+ ...(Object.keys(parsedBlockAttrs(node)).length ? { attrs: parsedBlockAttrs(node) } : {}),
457
+ children: children.length ? children : [{ type: "paragraph", id: createNodeId(), children: [] }],
458
+ };
459
+ }
460
+ if (tag === "pre") {
461
+ const code = elementChildren(node).find((child) => child.tagName === "code");
462
+ const language = attr(node, "data-smart-language")
463
+ || attr(code || node, "data-smart-language")
464
+ || attr(code || node, "class")?.split(/\s+/).find((value) => value.startsWith("language-"))?.slice(9);
465
+ const attrs = { ...parsedBlockAttrs(node), ...(language ? { language } : {}) };
466
+ const text = rawText(code || node);
467
+ return {
468
+ type: "code_block", id: generatedId(node, "code"),
469
+ ...(Object.keys(attrs).length ? { attrs } : {}),
470
+ children: text ? [{ type: "text", text }] : [],
471
+ };
472
+ }
473
+ if (tag === "table") {
474
+ const rowNodes = elementChildren(node).flatMap((child) => child.tagName === "tr" ? [child]
475
+ : ["thead", "tbody", "tfoot"].includes(child.tagName || "") ? elementChildren(child).filter((candidate) => candidate.tagName === "tr") : []);
476
+ const columns = elementChildren(node).find((child) => child.tagName === "colgroup");
477
+ // All-or-nothing: a table where only some (or none) of its <col>s
478
+ // specify a real pixel width is rendered at its natural/stretched
479
+ // default (no columnWidths at all) rather than mixing real values
480
+ // with a fabricated fallback for the rest - see parsePixelWidth's
481
+ // comment for why a fallback here caused pasted tables to shrink.
482
+ const parsedColumnWidths = columns
483
+ ? elementChildren(columns).filter((child) => child.tagName === "col")
484
+ .map((col) => parsePixelWidth(styleValue(col, "width")) ?? parsePixelWidth(attr(col, "width")))
485
+ : [];
486
+ const columnWidths = parsedColumnWidths.length && parsedColumnWidths.every((width) => width !== null)
487
+ ? parsedColumnWidths
488
+ : [];
489
+ const captionNode = elementChildren(node).find((child) => child.tagName === "caption");
490
+ const tableAttrs = {};
491
+ if (columnWidths.length)
492
+ tableAttrs.columnWidths = columnWidths;
493
+ const caption = captionNode ? rawText(captionNode).trim() : "";
494
+ if (caption)
495
+ tableAttrs.caption = caption;
496
+ const layout = attr(node, "data-smart-layout") || styleValue(node, "table-layout");
497
+ if (layout)
498
+ tableAttrs.layout = layout;
499
+ const rows = rowNodes.map((rowNode) => parseBlock(rowNode)).filter((row) => Boolean(row));
500
+ return { type: "table", id: generatedId(node, "table"), ...(Object.keys(tableAttrs).length ? { attrs: tableAttrs } : {}), children: rows };
501
+ }
502
+ if (tag === "tr") {
503
+ const height = Number.parseFloat(styleValue(node, "height") || attr(node, "height") || "");
504
+ const cells = elementChildren(node).filter((child) => child.tagName === "td" || child.tagName === "th")
505
+ .map((cell) => parseBlock(cell)).filter((cell) => Boolean(cell));
506
+ return { type: "table_row", id: generatedId(node, "row"), ...(Number.isFinite(height) && height > 0 ? { attrs: { height } } : {}), children: cells };
507
+ }
508
+ if (tag === "td" || tag === "th") {
509
+ const cellAttrs = {
510
+ rowspan: Math.max(1, Number(attr(node, "rowspan")) || 1),
511
+ colspan: Math.max(1, Number(attr(node, "colspan")) || 1),
512
+ header: tag === "th",
513
+ };
514
+ const background = styleValue(node, "background") || styleValue(node, "background-color");
515
+ const borders = styleValue(node, "border");
516
+ const textColor = styleValue(node, "color");
517
+ const verticalAlign = styleValue(node, "vertical-align");
518
+ if (background)
519
+ cellAttrs.background = background;
520
+ if (borders)
521
+ cellAttrs.borders = borders;
522
+ // Per-side declarations (e.g. from a source that styled only one edge)
523
+ // parse independently of the uniform shorthand above - both can be
524
+ // present at once, matching how the renderer layers them.
525
+ const borderTop = styleValue(node, "border-top");
526
+ const borderRight = styleValue(node, "border-right");
527
+ const borderBottom = styleValue(node, "border-bottom");
528
+ const borderLeft = styleValue(node, "border-left");
529
+ if (borderTop)
530
+ cellAttrs.borderTop = borderTop;
531
+ if (borderRight)
532
+ cellAttrs.borderRight = borderRight;
533
+ if (borderBottom)
534
+ cellAttrs.borderBottom = borderBottom;
535
+ if (borderLeft)
536
+ cellAttrs.borderLeft = borderLeft;
537
+ if (textColor)
538
+ cellAttrs.textColor = textColor;
539
+ if (verticalAlign)
540
+ cellAttrs.verticalAlign = verticalAlign;
541
+ const blockTags = ["p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "blockquote", "pre", "table"];
542
+ const isBlockLike = (child) => blockTags.includes(child.tagName || "") || TRANSPARENT_CONTAINER_TAGS.includes(child.tagName || "");
543
+ const children = parseBlockList(elementChildren(node).filter(isBlockLike));
544
+ const directInline = (node.childNodes || []).filter((child) => !child.tagName || !isBlockLike(child)).flatMap((child) => textWithMarks(child));
545
+ if (directInline.length)
546
+ children.unshift({ type: "paragraph", id: createNodeId(), children: directInline });
547
+ if (!children.length)
548
+ children.push({ type: "paragraph", id: createNodeId(), children: [] });
549
+ return { type: "table_cell", id: generatedId(node, "cell"), attrs: cellAttrs, children };
550
+ }
551
+ if (tag === "ul" || tag === "ol") {
552
+ const listAttrs = {};
553
+ const requestedPreset = attr(node, "data-smart-list-preset") || attr(node, "data-srte-list-preset");
554
+ const preset = isFoundationSmartListPreset(requestedPreset) ? requestedPreset : undefined;
555
+ const explicitStyle = attr(node, "data-smart-list-style");
556
+ const style = explicitStyle || (!preset ? styleValue(node, "list-style-type") : undefined) || (!preset ? (tag === "ol" ? "decimal" : "disc") : undefined);
557
+ const start = Number(attr(node, "start"));
558
+ if (preset)
559
+ listAttrs.preset = preset;
560
+ if (style)
561
+ listAttrs.style = style;
562
+ if (Number.isInteger(start) && start >= 1)
563
+ listAttrs.start = start;
564
+ if (attr(node, "data-smart-checkable") === "true" || attr(node, "data-srte-checklist") === "true")
565
+ listAttrs.checkable = true;
566
+ const items = [];
567
+ elementChildren(node).forEach((child) => {
568
+ if (child.tagName === "li") {
569
+ const parsed = parseBlock(child);
570
+ if (parsed)
571
+ items.push(parsed);
572
+ }
573
+ else if ((child.tagName === "ul" || child.tagName === "ol") && items.length) {
574
+ const nested = parseBlock(child);
575
+ if (nested) {
576
+ const last = items[items.length - 1];
577
+ items[items.length - 1] = { ...last, children: [...(last.children || []), nested] };
578
+ }
579
+ }
580
+ });
581
+ return { type: "list", id: generatedId(node, "list"), attrs: listAttrs, children: items.length ? items : [{ type: "list_item", id: createNodeId(), children: [{ type: "paragraph", id: createNodeId(), children: [] }] }] };
582
+ }
583
+ if (tag === "li") {
584
+ const attrs = {};
585
+ const checked = attr(node, "data-smart-checked") || attr(node, "data-srte-checked") || attr(node, "data-checked");
586
+ const value = Number(attr(node, "value"));
587
+ if (checked !== undefined)
588
+ attrs.checked = checked === "true";
589
+ const htmlStyle = withoutListMarkerStyle(attr(node, "style"));
590
+ if (htmlStyle)
591
+ attrs.htmlStyle = htmlStyle;
592
+ if (Number.isInteger(value) && value >= 1)
593
+ attrs.numberOverride = value;
594
+ const children = [];
595
+ const blockTags = ["p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "blockquote", "pre", "div", "table", "figure"];
596
+ const inlineNodes = (node.childNodes || []).filter((child) => !child.tagName || !blockTags.includes(child.tagName));
597
+ const directText = inlineNodes.flatMap((child) => textWithMarks(child));
598
+ if (directText.length)
599
+ children.push({ type: "paragraph", id: createNodeId(), children: directText });
600
+ // blockTags already lists "div"/"figure" as block-worthy, but parseBlock
601
+ // itself has no case for either - parseBlockList is what actually
602
+ // unwraps them (rather than swallowing their content into one opaque
603
+ // `unknown` node) instead of parsing them directly.
604
+ children.push(...parseBlockList(elementChildren(node).filter((child) => blockTags.includes(child.tagName || ""))));
605
+ if (!children.length)
606
+ children.push({ type: "paragraph", id: createNodeId(), children: [] });
607
+ return { type: "list_item", id: generatedId(node, "item"), ...(Object.keys(attrs).length ? { attrs } : {}), children };
608
+ }
609
+ if (tag)
610
+ return {
611
+ type: "unknown", id: generatedId(node, "unknown"),
612
+ attrs: { originalType: attr(node, "data-smart-unknown-type") || tag, originalGroup: "block", raw: { html: serializeOuter(node) }, editable: false },
613
+ };
614
+ return null;
615
+ };
616
+ /**
617
+ * A thin wrapper around parseBlock for "list of block-level children"
618
+ * call sites: a transparent container (TRANSPARENT_CONTAINER_TAGS) is
619
+ * recursed into and its own children spliced in flat, rather than parsed
620
+ * as one opaque node - a div wrapping N real blocks (a table, N
621
+ * paragraphs, or nothing at all) produces exactly those N blocks instead
622
+ * of a single `unknown` placeholder that hides all of them.
623
+ */
624
+ const parseBlockList = (nodes) => nodes.flatMap((node) => {
625
+ if (TRANSPARENT_CONTAINER_TAGS.includes(node.tagName || ""))
626
+ return parseBlockList(elementChildren(node));
627
+ const parsed = parseBlock(node);
628
+ return parsed ? [parsed] : [];
629
+ });
630
+ export const parseCanonicalListHtml = (html) => {
631
+ const fragment = parseFragment(html);
632
+ const wrapper = elementChildren(fragment).find((node) => attr(node, "data-smart-document") === "true");
633
+ const source = wrapper || fragment;
634
+ const children = parseBlockList(elementChildren(source));
635
+ return { type: "doc", id: attr(source, "data-smart-id") || createNodeId(), children: children.length ? children : [{ type: "paragraph", id: createNodeId(), children: [] }] };
636
+ };
637
+ const plainText = (node) => (node.children || []).map((child) => isTextNode(child) ? child.text : child.type === "hard_break" ? "\n" : "").join("");
638
+ const markdownInlineText = (node) => (node.children || []).map((child) => {
639
+ if (!isTextNode(child)) {
640
+ if (child.type === "hard_break")
641
+ return " \n";
642
+ if (child.type === "image" || child.type === "formula")
643
+ return atomToMarkdown(child);
644
+ return "";
645
+ }
646
+ let value = child.text;
647
+ [...canonicalMarkOrder(child.marks)].reverse().forEach((mark) => {
648
+ if (mark.type === "code")
649
+ value = `\`${value}\``;
650
+ else if (mark.type === "bold")
651
+ value = `**${value}**`;
652
+ else if (mark.type === "italic")
653
+ value = `*${value}*`;
654
+ else if (mark.type === "strike")
655
+ value = `~~${value}~~`;
656
+ else if (mark.type === "link")
657
+ value = `[${value}](${String(mark.attrs?.href || "")})`;
658
+ });
659
+ return value;
660
+ }).join("");
661
+ const markdownList = (list, depth) => (list.children || []).flatMap((child, index) => {
662
+ if (isTextNode(child))
663
+ return [];
664
+ const content = (child.children || []).filter((node) => !isTextNode(node) && node.type !== "list");
665
+ const marker = orderedList(list) ? `${Number(list.attrs?.start || 1) + index}.` : "-";
666
+ const task = list.attrs?.checkable === true ? `[${child.attrs?.checked === true ? "x" : " "}] ` : "";
667
+ const indentation = " ".repeat(depth);
668
+ const line = `${indentation}${marker} ${task}${content[0] ? markdownInlineText(content[0]) : ""}`;
669
+ const continuation = content.slice(1).flatMap((block) => ["", `${indentation} ${markdownInlineText(block)}`]);
670
+ const nested = (child.children || []).filter((node) => !isTextNode(node) && node.type === "list")
671
+ .flatMap((node) => markdownList(node, depth + 1));
672
+ return [line, ...continuation, ...nested];
673
+ });
674
+ const markdownBlock = (node) => {
675
+ if (["block_image", "block_formula", "video", "audio", "divider"].includes(node.type))
676
+ return [atomToMarkdown(node)];
677
+ if (node.type === "list")
678
+ return markdownList(node, 0);
679
+ if (node.type === "heading")
680
+ return [`${"#".repeat(Math.max(1, Math.min(6, Number(node.attrs?.level) || 1)))} ${markdownInlineText(node)}`];
681
+ if (node.type === "code_block") {
682
+ const language = typeof node.attrs?.language === "string" ? node.attrs.language : "";
683
+ return [`\`\`\`${language}\n${plainText(node)}\n\`\`\``];
684
+ }
685
+ if (node.type === "blockquote") {
686
+ const content = (node.children || []).filter((child) => !isTextNode(child))
687
+ .flatMap(markdownBlock).join("\n");
688
+ return [content.split("\n").map((line) => `> ${line}`).join("\n")];
689
+ }
690
+ if (node.type === "table") {
691
+ const rows = (node.children || []).filter((child) => !isTextNode(child) && child.type === "table_row");
692
+ const logical = rows.map((row) => (row.children || []).filter((child) => !isTextNode(child) && child.type === "table_cell")
693
+ .flatMap((cell) => [markdownInlineText((cell.children || []).find((child) => !isTextNode(child)) || cell), ...Array(Math.max(0, Number(cell.attrs?.colspan) || 1) - 1).fill("")]));
694
+ const width = logical.reduce((max, row) => Math.max(max, row.length), 0);
695
+ if (!width)
696
+ return [""];
697
+ const padded = logical.map((row) => [...row, ...Array(width - row.length).fill("")]);
698
+ const first = padded[0] || Array(width).fill("");
699
+ return [[`| ${first.join(" | ")} |`, `| ${Array(width).fill("---").join(" | ")} |`, ...padded.slice(1).map((row) => `| ${row.join(" | ")} |`)].join("\n")];
700
+ }
701
+ return [markdownInlineText(node)];
702
+ };
703
+ /** Alignment and indent are unsupported in Markdown; block content survives semantically. */
704
+ export const serializeCanonicalListMarkdown = (document) => document.children.flatMap((node) => isTextNode(node) ? [node.text] : markdownBlock(node)).join("\n");
705
+ const markdownText = (node) => node.value || (node.children || []).map(markdownText).join("");
706
+ /**
707
+ * remark's core parser has no math extension, so `$x^2$` survives parsing
708
+ * as literal text on a plain text node - split any dollar-delimited
709
+ * formula regions out of a text run into formula atoms, post-parse. Only
710
+ * applied to unmarked (mark-free) text, matching how the serializer only
711
+ * emits `$source$` at the top level, never inside bold/italic/etc.
712
+ */
713
+ const splitInlineFormulas = (text, marks) => {
714
+ if (marks.length)
715
+ return [{ type: "text", text, marks: canonicalMarkOrder(marks) }];
716
+ // Block math ($$\n...\n$$, atomToMarkdown's block_formula form) first,
717
+ // since it would otherwise be swallowed as three separate single-$ matches;
718
+ // then inline math ($...$) over what's left.
719
+ const pattern = /\$\$\n([^]*?)\n\$\$|\$([^$\n]+)\$/g;
720
+ const output = [];
721
+ let cursor = 0;
722
+ let match;
723
+ while ((match = pattern.exec(text))) {
724
+ if (match.index > cursor)
725
+ output.push({ type: "text", text: text.slice(cursor, match.index) });
726
+ output.push({ type: "formula", id: createNodeId(), attrs: { source: match[1] ?? match[2] ?? "", notation: "latex" } });
727
+ cursor = pattern.lastIndex;
728
+ }
729
+ if (!output.length)
730
+ return [{ type: "text", text }];
731
+ if (cursor < text.length)
732
+ output.push({ type: "text", text: text.slice(cursor) });
733
+ return output;
734
+ };
735
+ const markdownInline = (node, inherited = []) => {
736
+ if (node.type === "text")
737
+ return node.value ? splitInlineFormulas(node.value, inherited) : [];
738
+ if (node.type === "inlineCode")
739
+ return node.value ? [{ type: "text", text: node.value, marks: canonicalMarkOrder([...inherited, { type: "code" }]) }] : [];
740
+ if (node.type === "break")
741
+ return [{ type: "hard_break", id: createNodeId() }];
742
+ if (node.type === "image")
743
+ return node.url ? [{ type: "image", id: createNodeId(), attrs: { src: node.url, alt: node.alt || "" } }] : [];
744
+ const marks = [...inherited];
745
+ if (node.type === "strong")
746
+ marks.push({ type: "bold" });
747
+ if (node.type === "emphasis")
748
+ marks.push({ type: "italic" });
749
+ if (node.type === "delete")
750
+ marks.push({ type: "strike" });
751
+ if (node.type === "link")
752
+ marks.push({ type: "link", attrs: { href: node.url || "" } });
753
+ return (node.children || []).flatMap((child) => markdownInline(child, marks));
754
+ };
755
+ const listFromMarkdown = (node) => {
756
+ const checkable = (node.children || []).some((item) => item.checked !== null && item.checked !== undefined);
757
+ return {
758
+ type: "list", id: createNodeId(), attrs: {
759
+ style: node.ordered ? "decimal" : "disc",
760
+ ...(node.ordered && node.start && node.start !== 1 ? { start: node.start } : {}),
761
+ ...(checkable ? { checkable: true } : {}),
762
+ }, children: (node.children || []).map((itemNode) => {
763
+ const blocks = [];
764
+ (itemNode.children || []).forEach((child) => {
765
+ blocks.push(...blocksFromMarkdown(child));
766
+ });
767
+ return { type: "list_item", id: createNodeId(), ...(checkable ? { attrs: { checked: itemNode.checked === true } } : {}), children: blocks.length ? blocks : [{ type: "paragraph", id: createNodeId(), children: [] }] };
768
+ }),
769
+ };
770
+ };
771
+ const blocksFromMarkdown = (node) => {
772
+ if (node.type === "list")
773
+ return [listFromMarkdown(node)];
774
+ if (node.type === "heading")
775
+ return [{ type: "heading", id: createNodeId(), attrs: { level: Math.max(1, Math.min(6, node.depth || 1)) }, children: (node.children || []).flatMap((inline) => markdownInline(inline)) }];
776
+ if (node.type === "code")
777
+ return [{ type: "code_block", id: createNodeId(), ...(node.lang ? { attrs: { language: node.lang } } : {}), children: node.value ? [{ type: "text", text: node.value }] : [] }];
778
+ if (node.type === "blockquote") {
779
+ const children = (node.children || []).flatMap(blocksFromMarkdown);
780
+ return [{ type: "blockquote", id: createNodeId(), children: children.length ? children : [{ type: "paragraph", id: createNodeId(), children: [] }] }];
781
+ }
782
+ return [{ type: "paragraph", id: createNodeId(), children: (node.children || []).flatMap((inline) => markdownInline(inline)) }];
783
+ };
784
+ export const parseCanonicalListMarkdown = (markdown) => {
785
+ const tree = unified().use(remarkParse).use(remarkGfm).parse(markdown);
786
+ const children = (tree.children || []).flatMap(blocksFromMarkdown);
787
+ return { type: "doc", id: createNodeId(), children: children.length ? children : [{ type: "paragraph", id: createNodeId(), children: [] }] };
788
+ };
789
+ /** DOCX has numId/ilvl but no Smart preset identity; presets fall back semantically. */
790
+ export const canonicalListToDocxNumbering = (document) => {
791
+ const entries = [];
792
+ let numId = 1;
793
+ const visit = (list, depth, currentNumId) => {
794
+ for (const child of list.children || []) {
795
+ if (isTextNode(child))
796
+ continue;
797
+ const content = (child.children || []).find((node) => !isTextNode(node) && node.type !== "list");
798
+ entries.push({
799
+ itemId: child.id, numId: currentNumId, ilvl: depth,
800
+ marker: orderedList(list) ? "decimal" : "bullet",
801
+ text: content ? plainText(content) : "",
802
+ ...(list.attrs?.checkable === true ? { checked: child.attrs?.checked === true } : {}),
803
+ });
804
+ (child.children || []).forEach((node) => { if (!isTextNode(node) && node.type === "list")
805
+ visit(node, depth + 1, currentNumId); });
806
+ }
807
+ };
808
+ document.children.forEach((node) => { if (!isTextNode(node) && node.type === "list")
809
+ visit(node, 0, numId++); });
810
+ return entries;
811
+ };
812
+ export const canonicalListPdfText = (document) => canonicalListToDocxNumbering(document)
813
+ .map((entry) => `${" ".repeat(entry.ilvl)}${entry.marker === "decimal" ? "1." : "•"} ${entry.text}`).join("\n");