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