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,117 @@
1
+ import { isTextNode } from "./identity.js";
2
+ import { nodeAtPath } from "./positions.js";
3
+ /**
4
+ * Structural blocks such as quotes, tables, and atoms do not expose a native
5
+ * caret at their outside edge. Keep an ordinary editable block beside them
6
+ * so the start/end of every block container remains addressable. This is a
7
+ * model invariant, not a renderer workaround: DOM whitespace is not a
8
+ * position in the canonical document.
9
+ */
10
+ const isBoundaryBlock = (node, schema) => {
11
+ if (isTextNode(node))
12
+ return false;
13
+ const spec = schema.nodes[node.type];
14
+ return spec?.group === "block" && (node.type === "blockquote" ||
15
+ node.type === "table" ||
16
+ spec.atomic === true ||
17
+ spec.isolating === true);
18
+ };
19
+ const acceptsBlockContent = (node, schema) => {
20
+ if (node.type === "doc")
21
+ return true;
22
+ const expression = schema.nodes[node.type]?.content || "";
23
+ return /(?:^|[|(\s])block(?:[+*?]|$)/.test(expression);
24
+ };
25
+ const isEditableBlock = (node, schema) => {
26
+ if (isTextNode(node) || schema.nodes[node.type]?.group !== "block")
27
+ return false;
28
+ if (["paragraph", "heading", "code_block"].includes(node.type))
29
+ return true;
30
+ return /(?:^|[|(\s])inline(?:[+*?]|$)/.test(schema.nodes[node.type]?.content || "");
31
+ };
32
+ const collectIds = (node, ids) => {
33
+ if (isTextNode(node))
34
+ return;
35
+ ids.add(node.id);
36
+ node.children?.forEach((child) => collectIds(child, ids));
37
+ };
38
+ const uniqueBoundaryId = (base, ids) => {
39
+ let candidate = base;
40
+ let suffix = 2;
41
+ while (ids.has(candidate))
42
+ candidate = `${base}-${suffix++}`;
43
+ ids.add(candidate);
44
+ return candidate;
45
+ };
46
+ const comparePathsDescending = (left, right) => {
47
+ if (left.length !== right.length)
48
+ return right.length - left.length;
49
+ for (let index = 0; index < Math.min(left.length, right.length); index += 1) {
50
+ if (left[index] !== right[index])
51
+ return right[index] - left[index];
52
+ }
53
+ return 0;
54
+ };
55
+ /**
56
+ * Returns deterministic insert operations for missing outside-edge caret
57
+ * blocks. The traversal is scoped by the supplied root path, allowing the
58
+ * normalizer to remain local for ordinary typing.
59
+ */
60
+ export const editableBoundaryOperations = (document, schema, rootPath = []) => {
61
+ const root = nodeAtPath(document, rootPath);
62
+ if (!root || isTextNode(root))
63
+ return [];
64
+ const ids = new Set();
65
+ let idsCollected = false;
66
+ collectIds(document, ids);
67
+ const insertions = [];
68
+ const seen = new Set();
69
+ const add = (path, offset, base) => {
70
+ const key = `${path.join(".")}:${offset}`;
71
+ if (seen.has(key))
72
+ return;
73
+ seen.add(key);
74
+ // Ordinary text/mark transactions normally need no boundary repair. Do
75
+ // not pay a document-wide ID scan unless this traversal has actually
76
+ // found a missing boundary paragraph to insert.
77
+ if (!idsCollected) {
78
+ collectIds(document, ids);
79
+ idsCollected = true;
80
+ }
81
+ insertions.push({ path: [...path], offset, id: uniqueBoundaryId(base, ids) });
82
+ };
83
+ const visit = (node, path) => {
84
+ if (isTextNode(node))
85
+ return;
86
+ const children = node.children || [];
87
+ if (acceptsBlockContent(node, schema)) {
88
+ children.forEach((child, index) => {
89
+ if (!isBoundaryBlock(child, schema))
90
+ return;
91
+ const previous = children[index - 1];
92
+ const next = children[index + 1];
93
+ if (index === 0 || !isEditableBlock(previous, schema)) {
94
+ add(path, index, `smart-boundary-${node.id}-before-${child.id}`);
95
+ }
96
+ if (index === children.length - 1 || !isEditableBlock(next, schema)) {
97
+ add(path, index + 1, `smart-boundary-${node.id}-after-${child.id}`);
98
+ }
99
+ });
100
+ }
101
+ children.forEach((child, index) => visit(child, [...path, index]));
102
+ };
103
+ visit(root, rootPath);
104
+ insertions.sort((left, right) => comparePathsDescending(left.path, right.path) || right.offset - left.offset);
105
+ return insertions.map(({ path, offset, id }) => ({
106
+ type: "insertNode",
107
+ pos: { path, offset },
108
+ node: { type: "paragraph", id, children: [] },
109
+ }));
110
+ };
111
+ export const createEditableBoundaryNormalizer = (options = {}) => ({
112
+ id: "foundation.editable-boundaries",
113
+ priority: options.priority ?? 20,
114
+ normalize: (document, context) => ({
115
+ operations: editableBoundaryOperations(document, context.schema, context.affectedPath),
116
+ }),
117
+ });
@@ -0,0 +1,3 @@
1
+ import { type ClipboardDetection, type RawClipboardPayload } from "./types.js";
2
+ /** Detection selects an optimization/normalizer only; generic parsing remains the fallback. */
3
+ export declare const detectClipboardSource: (payload: RawClipboardPayload) => ClipboardDetection;
@@ -0,0 +1,29 @@
1
+ import { NATIVE_CLIPBOARD_MIME } from "./types.js";
2
+ const markdownPattern = /^(?:#{1,6}\s|[-*+]\s|\d+\.\s|```|\|.+\|)/m;
3
+ /** Detection selects an optimization/normalizer only; generic parsing remains the fallback. */
4
+ export const detectClipboardSource = (payload) => {
5
+ const html = payload.html || "";
6
+ const types = new Set(payload.types || []);
7
+ if (payload.native || types.has(NATIVE_CLIPBOARD_MIME))
8
+ return { source: "native", signals: [NATIVE_CLIPBOARD_MIME] };
9
+ if (/<table\b/i.test(html) && /(?:google-sheets-html-origin|data-sheets-root|mso-number-format|Microsoft Excel)/i.test(html)) {
10
+ return { source: "spreadsheet", signals: ["table-with-spreadsheet-marker"] };
11
+ }
12
+ const wordSignals = [
13
+ /\bmso-[\w-]+\s*:/i.test(html) && "mso-style",
14
+ /<!--\[if\s+gte\s+mso/i.test(html) && "mso-conditional-comment",
15
+ /xmlns:(?:o|w)\s*=/i.test(html) && "office-namespace",
16
+ /\b(?:data-listid|data-ccp-props|class=["'][^"']*\bSCXW)/i.test(html) && "office-web-marker",
17
+ ].filter((value) => Boolean(value));
18
+ if (wordSignals.length)
19
+ return { source: "word", signals: wordSignals };
20
+ if (/\bid=["']docs-internal-guid-/i.test(html))
21
+ return { source: "google-docs", signals: ["docs-internal-guid"] };
22
+ if (types.has("vscode-editor-data") && markdownPattern.test(payload.plainText || ""))
23
+ return { source: "markdown", signals: ["vscode-plain-text-structure"] };
24
+ if (html)
25
+ return { source: "html", signals: ["text/html"] };
26
+ if (markdownPattern.test(payload.plainText || ""))
27
+ return { source: "markdown", signals: ["plain-text-structure"] };
28
+ return { source: "plain-text", signals: ["text/plain"] };
29
+ };
@@ -0,0 +1,24 @@
1
+ import type { ClipboardFragment, ClipboardSource, RawClipboardPayload } from "./types.js";
2
+ export interface ClipboardStructuralShape {
3
+ readonly nodeCounts: Readonly<Record<string, number>>;
4
+ readonly maxDepth: number;
5
+ readonly textCodeUnits: number;
6
+ }
7
+ export interface ClipboardDiagnosticReport {
8
+ readonly version: 1;
9
+ /** Stable correlation key over the payload bytes; raw content is never included. */
10
+ readonly fixtureHash: string;
11
+ readonly detectedSource: ClipboardSource;
12
+ readonly detectionSignals: readonly string[];
13
+ readonly mimeTypes: readonly string[];
14
+ readonly inputBytes: number;
15
+ readonly status: "parsed" | "rejected";
16
+ readonly structuralShape: ClipboardStructuralShape | null;
17
+ /** Normalizer IDs and schema repair codes only; never messages/before/after values. */
18
+ readonly repairs: readonly string[];
19
+ readonly failureCode?: "payload-too-large" | "parse-failed";
20
+ }
21
+ /** Produces a telemetry-safe shape report containing no clipboard text, HTML, IDs, or attributes. */
22
+ export declare const reportParsedClipboard: (payload: RawClipboardPayload, fragment: ClipboardFragment) => ClipboardDiagnosticReport;
23
+ /** Produces a telemetry-safe rejection report without exposing an exception message. */
24
+ export declare const reportRejectedClipboard: (payload: RawClipboardPayload, error: unknown) => ClipboardDiagnosticReport;
@@ -0,0 +1,59 @@
1
+ import { isTextNode } from "../identity.js";
2
+ import { detectClipboardSource } from "./detection.js";
3
+ import { estimateClipboardPayloadBytes } from "./pipeline.js";
4
+ const hashPayload = (payload) => {
5
+ const entries = payload.representations
6
+ ? Object.entries(payload.representations)
7
+ : [["text/html", payload.html || ""], ["text/plain", payload.plainText || ""], ["native", payload.native || ""]];
8
+ let hash = 2166136261;
9
+ entries.sort(([left], [right]) => left.localeCompare(right)).forEach(([type, value]) => {
10
+ const input = `${type}\0${value.length}\0${value}`;
11
+ for (let index = 0; index < input.length; index += 1)
12
+ hash = Math.imul(hash ^ input.charCodeAt(index), 16777619);
13
+ });
14
+ return (hash >>> 0).toString(16).padStart(8, "0");
15
+ };
16
+ const shapeOf = (root) => {
17
+ const nodeCounts = {};
18
+ let maxDepth = 0;
19
+ let textCodeUnits = 0;
20
+ const visit = (node, depth) => {
21
+ nodeCounts[node.type] = (nodeCounts[node.type] || 0) + 1;
22
+ maxDepth = Math.max(maxDepth, depth);
23
+ if (isTextNode(node))
24
+ textCodeUnits += node.text.length;
25
+ else
26
+ (node.children || []).forEach((child) => visit(child, depth + 1));
27
+ };
28
+ visit(root, 0);
29
+ return { nodeCounts, maxDepth, textCodeUnits };
30
+ };
31
+ const repairCodes = (fragment) => fragment.repairs.map((entry) => typeof entry === "string" ? entry : entry.code);
32
+ const baseReport = (payload) => {
33
+ const detection = detectClipboardSource(payload);
34
+ return {
35
+ version: 1,
36
+ fixtureHash: hashPayload(payload),
37
+ detectedSource: detection.source,
38
+ detectionSignals: detection.signals,
39
+ mimeTypes: [...new Set(payload.types || Object.keys(payload.representations || {}))].sort(),
40
+ inputBytes: estimateClipboardPayloadBytes(payload),
41
+ };
42
+ };
43
+ /** Produces a telemetry-safe shape report containing no clipboard text, HTML, IDs, or attributes. */
44
+ export const reportParsedClipboard = (payload, fragment) => ({
45
+ ...baseReport(payload),
46
+ status: "parsed",
47
+ structuralShape: shapeOf(fragment.document),
48
+ repairs: repairCodes(fragment),
49
+ });
50
+ /** Produces a telemetry-safe rejection report without exposing an exception message. */
51
+ export const reportRejectedClipboard = (payload, error) => ({
52
+ ...baseReport(payload),
53
+ status: "rejected",
54
+ structuralShape: null,
55
+ repairs: [],
56
+ failureCode: error instanceof Error && error.name === "ClipboardPayloadTooLargeError"
57
+ ? "payload-too-large"
58
+ : "parse-failed",
59
+ });
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./detection.js";
3
+ export * from "./pipeline.js";
4
+ export * from "./serialization.js";
5
+ export * from "./normalizers.js";
6
+ export * from "./insertion.js";
7
+ export * from "./transfer.js";
8
+ export * from "./diagnostics.js";
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./detection.js";
3
+ export * from "./pipeline.js";
4
+ export * from "./serialization.js";
5
+ export * from "./normalizers.js";
6
+ export * from "./insertion.js";
7
+ export * from "./transfer.js";
8
+ export * from "./diagnostics.js";
@@ -0,0 +1,22 @@
1
+ import type { SmartDocument, SmartOperation, SmartSchema, SmartSelection } from "../types.js";
2
+ import type { PositionLookup } from "../scope/types.js";
3
+ export interface ClipboardInsertionContext {
4
+ readonly schema: SmartSchema;
5
+ readonly positions: PositionLookup;
6
+ readonly idFactory: () => string;
7
+ }
8
+ export interface ClipboardInsertionResult {
9
+ readonly operations: readonly SmartOperation[];
10
+ readonly selectionTarget: {
11
+ readonly ownerId: string;
12
+ readonly offset: number;
13
+ };
14
+ readonly definingAncestorId: string | null;
15
+ }
16
+ /** External paste is a copy: remint every non-text ID before insertion. */
17
+ export declare const remintClipboardFragmentIds: (fragment: SmartDocument, idFactory: () => string) => SmartDocument;
18
+ /**
19
+ * Pure canonical-fragment insertion. The caller owns the transaction and maps
20
+ * `selectionTarget` after applying the returned operations.
21
+ */
22
+ export declare const insertClipboardFragment: (document: SmartDocument, selection: SmartSelection, fragment: SmartDocument, context: ClipboardInsertionContext) => ClipboardInsertionResult;
@@ -0,0 +1,210 @@
1
+ import { isTextNode } from "../identity.js";
2
+ import { comparePos, nodeAtPath } from "../positions.js";
3
+ /** External paste is a copy: remint every non-text ID before insertion. */
4
+ export const remintClipboardFragmentIds = (fragment, idFactory) => {
5
+ const visit = (node) => isTextNode(node) ? structuredClone(node) : {
6
+ ...structuredClone(node), id: idFactory(), children: node.children?.map(visit),
7
+ };
8
+ return visit(fragment);
9
+ };
10
+ const inlineSize = (node) => isTextNode(node) ? node.text.length : 1;
11
+ const inlineChildren = (node) => node.children || [];
12
+ const inlineLength = (node) => inlineChildren(node).reduce((total, child) => total + inlineSize(child), 0);
13
+ const splitInline = (children, offset) => {
14
+ const before = [];
15
+ const after = [];
16
+ let cursor = 0;
17
+ children.forEach((node) => {
18
+ const end = cursor + inlineSize(node);
19
+ if (end <= offset)
20
+ before.push(structuredClone(node));
21
+ else if (cursor >= offset)
22
+ after.push(structuredClone(node));
23
+ else if (isTextNode(node)) {
24
+ const local = offset - cursor;
25
+ if (local)
26
+ before.push({ ...node, text: node.text.slice(0, local) });
27
+ if (local < node.text.length)
28
+ after.push({ ...node, text: node.text.slice(local) });
29
+ }
30
+ else
31
+ throw new Error("Clipboard insertion cannot split an atomic node.");
32
+ cursor = end;
33
+ });
34
+ if (offset < 0 || offset > cursor)
35
+ throw new Error("Clipboard insertion offset is out of bounds.");
36
+ return [before, after];
37
+ };
38
+ const textOf = (node) => isTextNode(node) ? node.text
39
+ : node.type === "hard_break" ? "\n" : (node.children || []).map(textOf).join(node.type === "paragraph" ? "" : "\n");
40
+ const elementAt = (document, pos) => {
41
+ const node = nodeAtPath(document, pos.path);
42
+ if (!node || isTextNode(node))
43
+ throw new Error("Clipboard insertion requires an element owner.");
44
+ return node;
45
+ };
46
+ const definingAncestor = (document, path, schema) => {
47
+ let result = null;
48
+ for (let depth = 0; depth <= path.length; depth += 1) {
49
+ const node = nodeAtPath(document, path.slice(0, depth));
50
+ if (node && !isTextNode(node) && schema.nodes[node.type]?.defining === true)
51
+ result = node.id;
52
+ }
53
+ return result;
54
+ };
55
+ const replacementRange = (selection) => {
56
+ const compare = comparePos(selection.anchor, selection.head);
57
+ return compare <= 0 ? { from: selection.anchor, to: selection.head } : { from: selection.head, to: selection.anchor };
58
+ };
59
+ const samePath = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
60
+ const listItemPath = (document, path) => {
61
+ for (let depth = path.length; depth > 0; depth -= 1) {
62
+ const candidate = nodeAtPath(document, path.slice(0, depth));
63
+ if (candidate && !isTextNode(candidate) && candidate.type === "list_item")
64
+ return path.slice(0, depth);
65
+ }
66
+ return null;
67
+ };
68
+ /**
69
+ * Pure canonical-fragment insertion. The caller owns the transaction and maps
70
+ * `selectionTarget` after applying the returned operations.
71
+ */
72
+ export const insertClipboardFragment = (document, selection, fragment, context) => {
73
+ const range = replacementRange(selection);
74
+ const definingAncestorId = definingAncestor(document, range.from.path, context.schema);
75
+ if (selection.type === "node") {
76
+ const parent = nodeAtPath(document, range.from.path);
77
+ if (!parent || isTextNode(parent) || !parent.children)
78
+ throw new Error("Node selection parent is invalid.");
79
+ const selected = parent.children[range.from.offset];
80
+ if (!selected)
81
+ throw new Error("Node selection is empty.");
82
+ const nodes = fragment.children.map((node) => structuredClone(node));
83
+ const operations = [{ type: "removeNode", pos: range.from, node: selected }];
84
+ nodes.forEach((node, index) => operations.push({ type: "insertNode", pos: { path: [...range.from.path], offset: range.from.offset + index }, node }));
85
+ const target = [...nodes].reverse().find((node) => !isTextNode(node));
86
+ return {
87
+ operations,
88
+ selectionTarget: { ownerId: target?.id || parent.id, offset: target ? inlineLength(target) : range.from.offset },
89
+ definingAncestorId,
90
+ };
91
+ }
92
+ const owner = elementAt(document, range.from);
93
+ if (owner.type === "code_block") {
94
+ const [before] = splitInline(inlineChildren(owner), range.from.offset);
95
+ const [, after] = splitInline(inlineChildren(owner), range.to.path.join("/") === range.from.path.join("/") ? range.to.offset : range.from.offset);
96
+ const insertedText = fragment.children.map(textOf).join("\n");
97
+ const afterNode = {
98
+ ...owner,
99
+ children: [...before, ...(insertedText ? [{ type: "text", text: insertedText }] : []), ...after]
100
+ .map((node) => isTextNode(node) ? { type: "text", text: node.text } : node),
101
+ };
102
+ const position = context.positions.positionOf(owner.id);
103
+ if (!position)
104
+ throw new Error("Code block position is unavailable.");
105
+ return {
106
+ operations: [{ type: "replaceNode", pos: position.pos, before: owner, after: afterNode }],
107
+ selectionTarget: { ownerId: owner.id, offset: range.from.offset + insertedText.length },
108
+ definingAncestorId: owner.id,
109
+ };
110
+ }
111
+ const itemPath = listItemPath(document, range.from.path);
112
+ const fragmentBlocks = fragment.children.filter((node) => !isTextNode(node));
113
+ if (samePath(range.from.path, range.to.path) && range.from.offset === range.to.offset
114
+ && itemPath && (fragmentBlocks.length > 1 || fragmentBlocks.some((node) => node.type === "list"))) {
115
+ const item = nodeAtPath(document, itemPath);
116
+ const list = nodeAtPath(document, itemPath.slice(0, -1));
117
+ if (!item || isTextNode(item) || !list || isTextNode(list) || list.type !== "list")
118
+ throw new Error("List paste target is invalid.");
119
+ const insertedItems = fragmentBlocks.flatMap((block) => block.type === "list"
120
+ ? (block.children || []).filter((node) => !isTextNode(node) && node.type === "list_item").map((node) => structuredClone(node))
121
+ : [{ type: "list_item", id: context.idFactory(), children: [structuredClone(block)] }]);
122
+ const index = itemPath[itemPath.length - 1];
123
+ const children = [...(list.children || [])];
124
+ children.splice(index + 1, 0, ...insertedItems);
125
+ const listPosition = context.positions.positionOf(list.id);
126
+ if (!listPosition)
127
+ throw new Error("List position is unavailable.");
128
+ const target = insertedItems[insertedItems.length - 1];
129
+ const targetBlock = [...(target.children || [])].reverse().find((node) => !isTextNode(node));
130
+ return {
131
+ operations: [{ type: "replaceNode", pos: listPosition.pos, before: list, after: { ...list, children } }],
132
+ selectionTarget: { ownerId: targetBlock?.id || target.id, offset: targetBlock ? inlineLength(targetBlock) : 0 },
133
+ definingAncestorId,
134
+ };
135
+ }
136
+ if (!samePath(range.from.path, range.to.path)) {
137
+ const parentPath = range.from.path.slice(0, -1);
138
+ if (!samePath(parentPath, range.to.path.slice(0, -1)))
139
+ throw new Error("Cross-parent clipboard replacement is clamped by scope before insertion.");
140
+ const parent = nodeAtPath(document, parentPath);
141
+ const toOwner = elementAt(document, range.to);
142
+ if (!parent || isTextNode(parent) || !parent.children)
143
+ throw new Error("Cross-owner parent is invalid.");
144
+ const fromIndex = range.from.path[range.from.path.length - 1];
145
+ const toIndex = range.to.path[range.to.path.length - 1];
146
+ if (fromIndex >= toIndex)
147
+ throw new Error("Cross-owner clipboard range is invalid.");
148
+ const [prefix] = splitInline(inlineChildren(owner), range.from.offset);
149
+ const [, suffix] = splitInline(inlineChildren(toOwner), range.to.offset);
150
+ const ownerPosition = context.positions.positionOf(owner.id);
151
+ if (!ownerPosition)
152
+ throw new Error("Clipboard owner position is unavailable.");
153
+ const operations = [];
154
+ for (let index = toIndex; index > fromIndex; index -= 1)
155
+ operations.push({
156
+ type: "removeNode", pos: { path: [...parentPath], offset: index }, node: parent.children[index],
157
+ });
158
+ const singleInline = fragmentBlocks.length === 1 && ["paragraph", "heading"].includes(fragmentBlocks[0].type);
159
+ if (singleInline) {
160
+ const inserted = inlineChildren(fragmentBlocks[0]).map((node) => structuredClone(node));
161
+ operations.push({ type: "replaceNode", pos: ownerPosition.pos, before: owner, after: { ...owner, children: [...prefix, ...inserted, ...suffix] } });
162
+ return {
163
+ operations,
164
+ selectionTarget: { ownerId: owner.id, offset: prefix.reduce((total, node) => total + inlineSize(node), 0) + inserted.reduce((total, node) => total + inlineSize(node), 0) },
165
+ definingAncestorId,
166
+ };
167
+ }
168
+ operations.push({ type: "replaceNode", pos: ownerPosition.pos, before: owner, after: { ...owner, children: prefix } });
169
+ fragmentBlocks.forEach((node, index) => operations.push({ type: "insertNode", pos: { path: [...parentPath], offset: fromIndex + 1 + index }, node: structuredClone(node) }));
170
+ let target = fragmentBlocks[fragmentBlocks.length - 1];
171
+ if (suffix.length) {
172
+ target = { type: "paragraph", id: context.idFactory(), children: suffix };
173
+ operations.push({ type: "insertNode", pos: { path: [...parentPath], offset: fromIndex + 1 + fragmentBlocks.length }, node: target });
174
+ }
175
+ return { operations, selectionTarget: { ownerId: target?.id || owner.id, offset: target ? inlineLength(target) : 0 }, definingAncestorId };
176
+ }
177
+ const [prefix] = splitInline(inlineChildren(owner), range.from.offset);
178
+ const [, suffix] = splitInline(inlineChildren(owner), range.to.offset);
179
+ const ownerPosition = context.positions.positionOf(owner.id);
180
+ if (!ownerPosition)
181
+ throw new Error("Clipboard owner position is unavailable.");
182
+ const blocks = fragmentBlocks.map((node) => structuredClone(node));
183
+ const singleInline = blocks.length === 1 && ["paragraph", "heading"].includes(blocks[0].type);
184
+ if (singleInline) {
185
+ const inserted = inlineChildren(blocks[0]).map((node) => structuredClone(node));
186
+ const after = { ...owner, children: [...prefix, ...inserted, ...suffix] };
187
+ return {
188
+ operations: [{ type: "replaceNode", pos: ownerPosition.pos, before: owner, after }],
189
+ selectionTarget: { ownerId: owner.id, offset: prefix.reduce((total, node) => total + inlineSize(node), 0) + inserted.reduce((total, node) => total + inlineSize(node), 0) },
190
+ definingAncestorId,
191
+ };
192
+ }
193
+ const operations = [{
194
+ type: "replaceNode", pos: ownerPosition.pos, before: owner, after: { ...owner, children: prefix },
195
+ }];
196
+ blocks.forEach((node, index) => operations.push({
197
+ type: "insertNode", pos: { path: [...ownerPosition.pos.path], offset: ownerPosition.pos.offset + 1 + index }, node,
198
+ }));
199
+ let target = blocks[blocks.length - 1];
200
+ if (suffix.length) {
201
+ const tail = { type: "paragraph", id: context.idFactory(), children: suffix };
202
+ operations.push({ type: "insertNode", pos: { path: [...ownerPosition.pos.path], offset: ownerPosition.pos.offset + 1 + blocks.length }, node: tail });
203
+ target = tail;
204
+ }
205
+ return {
206
+ operations,
207
+ selectionTarget: { ownerId: target?.id || owner.id, offset: target ? inlineLength(target) : 0 },
208
+ definingAncestorId,
209
+ };
210
+ };
@@ -0,0 +1,8 @@
1
+ import type { SourceNormalizer } from "./types.js";
2
+ export declare const wordClipboardNormalizer: SourceNormalizer;
3
+ export declare const googleDocsClipboardNormalizer: SourceNormalizer;
4
+ export declare const spreadsheetClipboardNormalizer: SourceNormalizer;
5
+ export declare const htmlClipboardNormalizer: SourceNormalizer;
6
+ export declare const markdownClipboardNormalizer: SourceNormalizer;
7
+ export declare const plainTextClipboardNormalizer: SourceNormalizer;
8
+ export declare const capturedSourceNormalizers: readonly SourceNormalizer[];