edytor 1.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/README.md +26 -69
  2. package/dist/block/block.svelte.d.ts +6 -4
  3. package/dist/block/block.svelte.js +12 -8
  4. package/dist/block/inlineBlock.svelte.d.ts +6 -2
  5. package/dist/block/inlineBlock.svelte.js +7 -2
  6. package/dist/clipboard/htmlFlow.d.ts +20 -7
  7. package/dist/clipboard/htmlFlow.js +23 -15
  8. package/dist/clipboard/serializeClipboardFragment.d.ts +37 -4
  9. package/dist/clipboard/serializeClipboardFragment.js +6 -4
  10. package/dist/clipboard/textBlocks.d.ts +8 -0
  11. package/dist/clipboard/textBlocks.js +6 -2
  12. package/dist/cloudflare/room/admission.js +3 -3
  13. package/dist/cloudflare/room/context.d.ts +7 -1
  14. package/dist/cloudflare/routeDocumentSocket.d.ts +17 -0
  15. package/dist/cloudflare/routeDocumentSocket.js +53 -0
  16. package/dist/collaboration/awarenessSelection.d.ts +22 -1
  17. package/dist/collaboration/awarenessSelection.js +42 -0
  18. package/dist/collaboration/comments/client.d.ts +1 -1
  19. package/dist/collaboration/comments/client.js +1 -1
  20. package/dist/collaboration/remoteSelection.js +3 -3
  21. package/dist/components/Block.svelte +3 -1
  22. package/dist/components/Edytor.svelte +38 -20
  23. package/dist/components/Edytor.svelte.d.ts +9 -4
  24. package/dist/components/InlineBlock.svelte +1 -1
  25. package/dist/crdt/admission.d.ts +5 -10
  26. package/dist/crdt/admission.js +5 -11
  27. package/dist/crdt/data.d.ts +10 -0
  28. package/dist/crdt/data.js +20 -2
  29. package/dist/crdt/doc/funnel.d.ts +7 -6
  30. package/dist/crdt/doc/funnel.js +10 -9
  31. package/dist/crdt/doc/gate.d.ts +0 -17
  32. package/dist/crdt/doc/gate.js +2 -41
  33. package/dist/crdt/doc/history.js +2 -2
  34. package/dist/crdt/doc/reads.d.ts +7 -0
  35. package/dist/crdt/doc/reads.js +11 -1
  36. package/dist/crdt/document.d.ts +13 -14
  37. package/dist/crdt/document.js +17 -19
  38. package/dist/crdt/edytor-doc.d.ts +22 -4
  39. package/dist/crdt/edytor-doc.js +5 -8
  40. package/dist/crdt/index.d.ts +9 -3
  41. package/dist/crdt/index.js +2 -2
  42. package/dist/crdt/operations.d.ts +1 -1
  43. package/dist/crdt/operations.js +1 -0
  44. package/dist/crdt/props.d.ts +32 -0
  45. package/dist/crdt/props.js +172 -0
  46. package/dist/crdt/protocol.d.ts +2 -2
  47. package/dist/crdt/protocol.js +2 -2
  48. package/dist/crdt/providers/index.d.ts +1 -1
  49. package/dist/crdt/providers/room.d.ts +10 -9
  50. package/dist/crdt/providers/room.js +13 -10
  51. package/dist/crdt/providers/websocket.d.ts +1 -1
  52. package/dist/crdt/providers/websocket.js +28 -29
  53. package/dist/crdt/semantics.js +3 -1
  54. package/dist/dnd/autoScroll.d.ts +15 -7
  55. package/dist/dnd/autoScroll.js +10 -15
  56. package/dist/{plugins/blockHandles → dnd}/dropIndicator.d.ts +2 -2
  57. package/dist/{plugins/blockHandles → dnd}/dropIndicator.js +2 -5
  58. package/dist/dnd/external.d.ts +6 -0
  59. package/dist/dnd/external.js +17 -0
  60. package/dist/{plugins/blockHandles → dnd}/geometry.d.ts +4 -2
  61. package/dist/{plugins/blockHandles → dnd}/geometry.js +3 -4
  62. package/dist/dnd/placement.d.ts +200 -0
  63. package/dist/dnd/placement.js +426 -0
  64. package/dist/dnd/pragmatic.d.ts +3 -1
  65. package/dist/dnd/pragmatic.js +12 -2
  66. package/dist/dnd/vendor/pragmatic/UPSTREAM.md +51 -18
  67. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/drop-target-for-external.d.ts +4 -0
  68. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/drop-target-for-external.js +6 -0
  69. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/element-adapter.d.ts +0 -2
  70. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/element-adapter.js +4 -22
  71. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/external-adapter-types.d.ts +26 -0
  72. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/external-adapter.d.ts +30 -0
  73. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/external-adapter.js +203 -0
  74. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/get-available-items.js +13 -0
  75. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/get-available-types.js +9 -0
  76. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/is-an-available-type.js +19 -0
  77. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/monitor-for-external.d.ts +4 -0
  78. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/monitor-for-external.js +6 -0
  79. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/honey-pot-fix/make-honey-pot-fix.js +4 -16
  80. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/internal-types.d.ts +1 -8
  81. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/make-adapter/make-drop-target.js +5 -6
  82. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/make-adapter/make-monitor.js +2 -3
  83. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/element/custom-native-drag-preview/set-custom-native-drag-preview.d.ts +1 -1
  84. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/element/custom-native-drag-preview/set-custom-native-drag-preview.js +6 -24
  85. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/contains-files.d.ts +2 -0
  86. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/contains-files.js +5 -0
  87. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/get-files.d.ts +3 -0
  88. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/get-files.js +12 -0
  89. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/native-types.d.ts +12 -0
  90. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/changing-window/is-entering-window-in-safari.js +10 -0
  91. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/changing-window/is-entering-window.js +55 -0
  92. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/entry-point/external.d.ts +4 -0
  93. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/entry-point/external.js +8 -0
  94. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/internal-types.d.ts +0 -4
  95. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/over-element/get-scroll-by.js +16 -72
  96. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/over-element/try-scroll.js +1 -9
  97. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/shared/configuration.js +4 -29
  98. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/shared/get-scroll-change.js +2 -3
  99. package/dist/edytor.svelte.d.ts +19 -9
  100. package/dist/edytor.svelte.js +28 -33
  101. package/dist/events/beforeInputCommands.d.ts +2 -2
  102. package/dist/events/beforeInputCommands.js +8 -4
  103. package/dist/events/onBeforeInput.js +1 -1
  104. package/dist/events/onDrop.js +25 -10
  105. package/dist/events/onInput.d.ts +0 -12
  106. package/dist/events/onInput.js +1 -67
  107. package/dist/events/onPaste.d.ts +2 -1
  108. package/dist/events/onPaste.js +8 -3
  109. package/dist/export/forms.d.ts +627 -0
  110. package/dist/export/forms.js +749 -0
  111. package/dist/export/index.d.ts +170 -0
  112. package/dist/export/index.js +161 -0
  113. package/dist/export/markdown.d.ts +41 -0
  114. package/dist/export/markdown.js +585 -0
  115. package/dist/export/markdownWriter.d.ts +9 -0
  116. package/dist/export/markdownWriter.js +133 -0
  117. package/dist/export/mdx.d.ts +15 -0
  118. package/dist/export/mdx.js +33 -0
  119. package/dist/index.d.ts +4 -4
  120. package/dist/index.js +2 -2
  121. package/dist/kinds.js +1 -1
  122. package/dist/labels.d.ts +2 -0
  123. package/dist/labels.js +2 -1
  124. package/dist/plugins/anchored.svelte.d.ts +21 -25
  125. package/dist/plugins/anchored.svelte.js +23 -39
  126. package/dist/plugins/arrowMove/arrowMove.d.ts +1 -1
  127. package/dist/plugins/arrowMove/arrowMove.js +1 -1
  128. package/dist/plugins/blockHandles/BlockHandleController.svelte.d.ts +2 -122
  129. package/dist/plugins/blockHandles/BlockHandleController.svelte.js +48 -414
  130. package/dist/plugins/blockHandles/BlockHandles.svelte +14 -32
  131. package/dist/plugins/blockHandles/blockHandlesPlugin.js +2 -1
  132. package/dist/plugins/blockHandles/dragPreview.d.ts +26 -7
  133. package/dist/plugins/blockHandles/dragPreview.js +33 -0
  134. package/dist/plugins/blockMenu/BlockMenuController.svelte.d.ts +5 -0
  135. package/dist/plugins/blockMenu/BlockMenuController.svelte.js +7 -2
  136. package/dist/plugins/blockMenu/blockMenuPlugin.js +5 -50
  137. package/dist/plugins/chrome.d.ts +27 -5
  138. package/dist/plugins/chrome.js +24 -9
  139. package/dist/plugins/code/CodeHeader.svelte +5 -3
  140. package/dist/plugins/code/CodePlugin.svelte +15 -12
  141. package/dist/plugins/code/header.svelte.js +7 -11
  142. package/dist/plugins/code/languageMenu.svelte.d.ts +5 -2
  143. package/dist/plugins/code/languageMenu.svelte.js +13 -20
  144. package/dist/plugins/code/languages.d.ts +0 -3
  145. package/dist/plugins/code/languages.js +0 -2
  146. package/dist/plugins/columns/ColumnResize.svelte +3 -2
  147. package/dist/plugins/columns/ColumnsPlugin.svelte +8 -21
  148. package/dist/plugins/columns/gaps.d.ts +7 -5
  149. package/dist/plugins/columns/gaps.js +11 -6
  150. package/dist/plugins/columns/resize.svelte.d.ts +5 -6
  151. package/dist/plugins/columns/resize.svelte.js +12 -14
  152. package/dist/plugins/comments/CommentsController.svelte.d.ts +6 -0
  153. package/dist/plugins/comments/CommentsController.svelte.js +20 -0
  154. package/dist/plugins/equation/EquationPlugin.svelte +13 -30
  155. package/dist/plugins/equation/EquationPlugin.svelte.d.ts +1 -0
  156. package/dist/plugins/equation/equation.css +7 -0
  157. package/dist/plugins/equation/equation.svelte.d.ts +7 -15
  158. package/dist/plugins/equation/equation.svelte.js +13 -50
  159. package/dist/plugins/equation/tex.d.ts +17 -0
  160. package/dist/plugins/equation/tex.js +42 -0
  161. package/dist/plugins/fileDrop.d.ts +43 -0
  162. package/dist/plugins/fileDrop.js +122 -0
  163. package/dist/plugins/find/FindController.svelte.d.ts +5 -0
  164. package/dist/plugins/find/FindController.svelte.js +14 -14
  165. package/dist/plugins/find/findPlugin.js +1 -15
  166. package/dist/plugins/image/ImageChrome.svelte +8 -2
  167. package/dist/plugins/image/ImagePlugin.svelte +56 -84
  168. package/dist/plugins/image/ImagePlugin.svelte.d.ts +13 -1
  169. package/dist/plugins/image/controls.svelte.js +16 -2
  170. package/dist/plugins/image/image.css +17 -0
  171. package/dist/plugins/image/toolbar.svelte.d.ts +5 -4
  172. package/dist/plugins/image/toolbar.svelte.js +21 -31
  173. package/dist/plugins/index.d.ts +4 -5
  174. package/dist/plugins/index.js +0 -1
  175. package/dist/plugins/list.svelte.d.ts +9 -0
  176. package/dist/plugins/list.svelte.js +18 -0
  177. package/dist/plugins/markdownShortcuts.js +5 -5
  178. package/dist/plugins/marquee/marqueePlugin.js +0 -4
  179. package/dist/plugins/media/AudioPlugin.svelte +13 -1
  180. package/dist/plugins/media/AudioPlugin.svelte.d.ts +11 -1
  181. package/dist/plugins/media/BookmarkPlugin.svelte +6 -97
  182. package/dist/plugins/media/EmbedPlugin.svelte +5 -90
  183. package/dist/plugins/media/EmptySlot.svelte +36 -20
  184. package/dist/plugins/media/EmptySlot.svelte.d.ts +6 -15
  185. package/dist/plugins/media/FilePlugin.svelte +12 -20
  186. package/dist/plugins/media/FilePlugin.svelte.d.ts +9 -0
  187. package/dist/plugins/media/MediaEmpty.svelte +2 -55
  188. package/dist/plugins/media/MediaEmpty.svelte.d.ts +18 -14
  189. package/dist/plugins/media/SourceBlock.svelte +178 -52
  190. package/dist/plugins/media/SourceBlock.svelte.d.ts +13 -6
  191. package/dist/plugins/media/UploadPreview.svelte +32 -0
  192. package/dist/plugins/media/UploadPreview.svelte.d.ts +11 -0
  193. package/dist/plugins/media/VideoPlugin.svelte +13 -1
  194. package/dist/plugins/media/VideoPlugin.svelte.d.ts +11 -1
  195. package/dist/plugins/media/empty.svelte.d.ts +13 -4
  196. package/dist/plugins/media/empty.svelte.js +32 -26
  197. package/dist/plugins/media/media.d.ts +9 -63
  198. package/dist/plugins/media/media.js +1 -194
  199. package/dist/plugins/media/sources.d.ts +60 -0
  200. package/dist/plugins/media/sources.js +165 -0
  201. package/dist/plugins/media/upload.css +23 -0
  202. package/dist/plugins/media/urlPaste.svelte.d.ts +2 -1
  203. package/dist/plugins/media/urlPaste.svelte.js +12 -21
  204. package/dist/plugins/mention/MentionPlugin.svelte +50 -38
  205. package/dist/plugins/mention/MentionPlugin.svelte.d.ts +11 -2
  206. package/dist/plugins/mention/mention.css +6 -0
  207. package/dist/plugins/page/PagePlugin.svelte +2 -9
  208. package/dist/plugins/page/page.js +1 -1
  209. package/dist/plugins/pageLink/PageLinkPlugin.svelte +126 -28
  210. package/dist/plugins/pageLink/PageLinkPlugin.svelte.d.ts +13 -1
  211. package/dist/plugins/pageLink/pageLink.css +6 -0
  212. package/dist/plugins/richtext/RichTextPlugin.svelte +99 -204
  213. package/dist/plugins/richtext/RichTextPlugin.svelte.d.ts +1 -0
  214. package/dist/plugins/richtext/body.js +1 -1
  215. package/dist/plugins/richtext/calloutIcons.svelte.d.ts +2 -2
  216. package/dist/plugins/richtext/calloutIcons.svelte.js +5 -7
  217. package/dist/plugins/richtext/richTextOperations.d.ts +1 -19
  218. package/dist/plugins/richtext/richTextOperations.js +3 -49
  219. package/dist/plugins/richtext/richtext.css +6 -0
  220. package/dist/plugins/slashMenu/SlashMenuController.svelte.d.ts +2 -0
  221. package/dist/plugins/slashMenu/SlashMenuController.svelte.js +7 -0
  222. package/dist/plugins/slashMenu/slashMenuPlugin.js +5 -14
  223. package/dist/plugins/table/TableChrome.svelte +2 -3
  224. package/dist/plugins/table/TablePlugin.svelte +20 -54
  225. package/dist/plugins/table/chrome.svelte.d.ts +5 -2
  226. package/dist/plugins/table/chrome.svelte.js +33 -56
  227. package/dist/plugins/table/table.js +3 -3
  228. package/dist/plugins/toc/TocPlugin.svelte +2 -4
  229. package/dist/plugins/toolbar/ToolbarController.svelte.d.ts +7 -0
  230. package/dist/plugins/toolbar/ToolbarController.svelte.js +20 -13
  231. package/dist/plugins/toolbar/toolbarPlugin.js +19 -53
  232. package/dist/plugins/triggers/TriggerController.svelte.d.ts +7 -2
  233. package/dist/plugins/triggers/TriggerController.svelte.js +11 -4
  234. package/dist/plugins/triggers/place.js +1 -2
  235. package/dist/plugins/triggers/triggers.d.ts +6 -1
  236. package/dist/plugins/triggers/triggers.js +12 -13
  237. package/dist/plugins/uploads.svelte.d.ts +48 -32
  238. package/dist/plugins/uploads.svelte.js +119 -43
  239. package/dist/plugins.d.ts +66 -14
  240. package/dist/plugins.js +0 -24
  241. package/dist/selection/replaceSelection.js +2 -2
  242. package/dist/selection/selection.svelte.d.ts +4 -10
  243. package/dist/selection/selection.svelte.js +7 -11
  244. package/dist/session/attempt.d.ts +16 -6
  245. package/dist/session/attempt.js +1 -0
  246. package/dist/session/bindings.js +35 -32
  247. package/dist/session/commands.d.ts +7 -11
  248. package/dist/session/commands.js +15 -19
  249. package/dist/session/history.js +1 -0
  250. package/dist/session/inputRules.d.ts +6 -3
  251. package/dist/session/inputRules.js +10 -6
  252. package/dist/session/moves.d.ts +33 -0
  253. package/dist/session/moves.js +71 -1
  254. package/dist/session/navigation.d.ts +7 -0
  255. package/dist/session/navigation.js +20 -11
  256. package/dist/session/props.d.ts +28 -0
  257. package/dist/session/props.js +65 -0
  258. package/dist/session/suggestions.svelte.d.ts +0 -11
  259. package/dist/session/suggestions.svelte.js +15 -3
  260. package/dist/session/uploads.d.ts +29 -0
  261. package/dist/session/uploads.js +10 -0
  262. package/dist/surface/lines.js +1 -3
  263. package/dist/surface/observer.svelte.d.ts +17 -1
  264. package/dist/surface/observer.svelte.js +72 -17
  265. package/dist/surface/overlay.d.ts +0 -14
  266. package/dist/surface/overlay.js +0 -25
  267. package/dist/surface/projector.svelte.js +1 -1
  268. package/dist/text/text.svelte.js +3 -13
  269. package/dist/themes/notion.css +9 -0
  270. package/dist/utils/sanitize.d.ts +38 -0
  271. package/dist/utils/sanitize.js +82 -0
  272. package/dist/utils.d.ts +2 -0
  273. package/dist/utils.js +5 -0
  274. package/package.json +18 -1
  275. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/max-z-index.js +0 -3
  276. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/supports-popover.js +0 -16
  277. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/shared/is-axis-allowed.js +0 -3
  278. package/dist/plugins/code/LanguageMenu.svelte +0 -25
  279. package/dist/plugins/code/LanguageMenu.svelte.d.ts +0 -9
  280. package/dist/plugins/equation/EquationEditor.svelte +0 -27
  281. package/dist/plugins/equation/EquationEditor.svelte.d.ts +0 -9
  282. package/dist/plugins/image/ImageToolbarHost.svelte +0 -37
  283. package/dist/plugins/image/ImageToolbarHost.svelte.d.ts +0 -9
  284. package/dist/plugins/richtext/CalloutIconPicker.svelte +0 -26
  285. package/dist/plugins/richtext/CalloutIconPicker.svelte.d.ts +0 -9
  286. /package/dist/{plugins/columns → dnd}/stacking.d.ts +0 -0
  287. /package/dist/{plugins/columns → dnd}/stacking.js +0 -0
@@ -28,6 +28,50 @@ import { CLOSE, validRoomId } from '../crdt/providers/room.js';
28
28
  import { COMMENTS_HEADER, HISTORY_HEADER, HISTORY_KEY_HEADER, IDENTITY_HEADERS, PROBE_HEADER, closedSocket } from './room/shared.js';
29
29
  import { parseReplica } from './room/access.js';
30
30
  import { MAX_COMMENT_REQUEST_BYTES } from './room/comments.js';
31
+ /** The CORS headers `options.cors` gives an answer to `request` (none: `null`). */
32
+ const corsHeaders = (request, options) => {
33
+ const cors = options?.cors;
34
+ const origin = request.headers.get('Origin');
35
+ if (!cors || origin === null || !originAllowed(request, options))
36
+ return null;
37
+ const headers = new Headers();
38
+ if (options?.allowedOrigins === undefined) {
39
+ headers.set('Access-Control-Allow-Origin', '*');
40
+ return headers;
41
+ }
42
+ headers.set('Access-Control-Allow-Origin', origin);
43
+ headers.set('Vary', 'Origin');
44
+ if (typeof cors === 'object' && cors.credentials === true)
45
+ headers.set('Access-Control-Allow-Credentials', 'true');
46
+ return headers;
47
+ };
48
+ /**
49
+ * One HTTP route with `options.cors`: a preflight answered here, every other
50
+ * answer given its CORS headers (a `101` upgrade passes as it is).
51
+ */
52
+ const withCors = async (request, options, route) => {
53
+ const cors = options?.cors;
54
+ if (cors && request.method === 'OPTIONS' && request.headers.has('Origin')) {
55
+ const headers = corsHeaders(request, options);
56
+ if (headers === null)
57
+ return new Response('origin not allowed', { status: 403 });
58
+ headers.set('Access-Control-Allow-Methods', 'GET, POST');
59
+ headers.set('Access-Control-Allow-Headers', request.headers.get('Access-Control-Request-Headers') ?? 'Content-Type');
60
+ headers.set('Access-Control-Max-Age', String((typeof cors === 'object' ? cors.maxAge : undefined) ?? 600));
61
+ return new Response(null, { status: 204, headers });
62
+ }
63
+ const response = await route();
64
+ const headers = corsHeaders(request, options);
65
+ if (headers === null || response.status === 101)
66
+ return response;
67
+ const merged = new Headers(response.headers);
68
+ headers.forEach((value, name) => merged.set(name, value));
69
+ return new Response(response.body, {
70
+ status: response.status,
71
+ statusText: response.statusText,
72
+ headers: merged
73
+ });
74
+ };
31
75
  /** Is `request`'s `Origin` one `options` allows (no `Origin`, or no list: yes)? */
32
76
  const originAllowed = (request, options) => {
33
77
  const allowed = options?.allowedOrigins;
@@ -109,6 +153,9 @@ const identityHeaders = (identity) => {
109
153
  * `404` (the room keeps no history), `405` (another method).
110
154
  */
111
155
  export async function routeDocumentHistory(request, rooms, documentId, authorize, options) {
156
+ return withCors(request, options, () => routeHistory(request, rooms, documentId, authorize, options));
157
+ }
158
+ async function routeHistory(request, rooms, documentId, authorize, options) {
112
159
  const query = new URL(request.url).searchParams;
113
160
  const op = request.method === 'GET'
114
161
  ? query.has('key')
@@ -191,6 +238,9 @@ const bodyWithin = async (request, max) => {
191
238
  * comment bytes full), `429` (the user past the room's comment rate).
192
239
  */
193
240
  export async function routeDocumentComments(request, rooms, documentId, authorize, options) {
241
+ return withCors(request, options, () => routeComments(request, rooms, documentId, authorize, options));
242
+ }
243
+ async function routeComments(request, rooms, documentId, authorize, options) {
194
244
  const op = request.method === 'GET' ? 'list' : request.method === 'POST' ? 'post' : null;
195
245
  if (op === null)
196
246
  return new Response('method not allowed', { status: 405 });
@@ -215,6 +265,9 @@ export async function routeDocumentComments(request, rooms, documentId, authoriz
215
265
  .fetch(new Request(request.url, { method: op === 'post' ? 'POST' : 'GET', headers, body }));
216
266
  }
217
267
  export async function routeDocumentSocket(request, rooms, documentId, authorize, options) {
268
+ return withCors(request, options, () => routeSocket(request, rooms, documentId, authorize, options));
269
+ }
270
+ async function routeSocket(request, rooms, documentId, authorize, options) {
218
271
  // The probes: a plain GET, authorized like a dial, answered with JSON or
219
272
  // an HTTP status — `lastUpdated` and `snapshot` (the document).
220
273
  const query = new URL(request.url).searchParams;
@@ -56,6 +56,20 @@ export type AwarenessLike = {
56
56
  getLocalState: () => Record<string, unknown> | null;
57
57
  setLocalState: (state: Record<string, unknown> | null) => void;
58
58
  };
59
+ /**
60
+ * The awareness a view's presence writer reads and writes: its own state,
61
+ * every client's state, and the `change` event (the real `Awareness`).
62
+ */
63
+ export type PresenceSource = AwarenessLike & {
64
+ clientID: number;
65
+ getStates: () => Map<number, Record<string, unknown>>;
66
+ on: (name: 'change', listener: () => void) => void;
67
+ off: (name: 'change', listener: () => void) => void;
68
+ };
69
+ /** Another person online: their client id and the fields of their `user` state (`name`, `color`, …), the document's actor name and colour when `user` has none. */
70
+ export type PresencePeer = EdytorAwarenessUser & Record<string, unknown> & {
71
+ clientId: number;
72
+ };
59
73
  /** A client-local presence key, minted once by the view that owns the entry. */
60
74
  export declare const mintPresenceKey: () => string;
61
75
  /**
@@ -90,9 +104,16 @@ export declare class PresenceWriter {
90
104
  private readonly awareness;
91
105
  private readonly key;
92
106
  throttle: number;
93
- constructor(awareness: AwarenessLike, key: string, { share, throttle }?: PresenceOptions);
107
+ constructor(awareness: PresenceSource, key: string, { share, throttle }?: PresenceOptions);
94
108
  get share(): PresenceShare;
95
109
  set share(share: PresenceShare);
110
+ /**
111
+ * The other people online: every client but this one, by client id,
112
+ * each `{ clientId, name?, color?, …user }`. Reactive: read in markup or
113
+ * an effect, it updates when someone joins, leaves or changes their
114
+ * state; the awareness listener exists only while something reads it.
115
+ */
116
+ get peers(): PresencePeer[];
96
117
  /** Publish `payload` (the full selection; `focus`: the block holding its focus) as this view shares it, within the throttle. */
97
118
  write(payload: PresenceSelection | null, focus?: string | null): void;
98
119
  /** Remove this view's entry and drop a held write (the view's teardown). */
@@ -1,4 +1,19 @@
1
1
  import { isRecord, jsonEquals } from '../utils/json.js';
2
+ import { createSubscriber } from 'svelte/reactivity';
3
+ /** Every client but this one, by client id. */
4
+ const readPeers = (awareness) => [...awareness.getStates()]
5
+ .filter(([clientId]) => clientId !== awareness.clientID)
6
+ .sort(([a], [b]) => a - b)
7
+ .map(([clientId, state]) => {
8
+ const actor = isRecord(state.actor) ? state.actor : {};
9
+ const user = isRecord(state.user) ? state.user : {};
10
+ const peer = { ...user, clientId };
11
+ if (peer.name === undefined && typeof actor.name === 'string')
12
+ peer.name = actor.name;
13
+ if (peer.color === undefined && typeof actor.color === 'string')
14
+ peer.color = actor.color;
15
+ return peer;
16
+ });
2
17
  let presenceKeys = 0;
3
18
  /** A client-local presence key, minted once by the view that owns the entry. */
4
19
  export const mintPresenceKey = () => `view-${++presenceKeys}`;
@@ -50,6 +65,21 @@ export class PresenceWriter {
50
65
  #held;
51
66
  /** The last payload and focused block handed in, kept so a share change republishes at once. */
52
67
  #latest = [null, null];
68
+ /** The peers while someone reads them reactively (`undefined`: recompute). */
69
+ #peers;
70
+ #watched = false;
71
+ #subscribe = createSubscriber((update) => {
72
+ const changed = () => {
73
+ this.#peers = undefined;
74
+ update();
75
+ };
76
+ this.#watched = true;
77
+ this.awareness.on('change', changed);
78
+ return () => {
79
+ this.awareness.off('change', changed);
80
+ [this.#watched, this.#peers] = [false, undefined];
81
+ };
82
+ });
53
83
  constructor(awareness, key, { share = 'caret', throttle = DEFAULT_PRESENCE_THROTTLE } = {}) {
54
84
  this.awareness = awareness;
55
85
  this.key = key;
@@ -65,6 +95,18 @@ export class PresenceWriter {
65
95
  this.#share = share;
66
96
  this.write(...this.#latest);
67
97
  }
98
+ /**
99
+ * The other people online: every client but this one, by client id,
100
+ * each `{ clientId, name?, color?, …user }`. Reactive: read in markup or
101
+ * an effect, it updates when someone joins, leaves or changes their
102
+ * state; the awareness listener exists only while something reads it.
103
+ */
104
+ get peers() {
105
+ this.#subscribe();
106
+ if (!this.#watched)
107
+ return readPeers(this.awareness);
108
+ return (this.#peers ??= readPeers(this.awareness));
109
+ }
68
110
  /** Publish `payload` (the full selection; `focus`: the block holding its focus) as this view shares it, within the throttle. */
69
111
  write(payload, focus = null) {
70
112
  this.#latest = [payload, focus];
@@ -48,7 +48,7 @@ export type CommentsClientOptions = {
48
48
  /**
49
49
  * The websocket sync's server URL (the `server` of `<Edytor>` or
50
50
  * `createWebsocketSync`), whose socket hears the room's changes. Default:
51
- * `server` read as `ws:`/`wss:`.
51
+ * every websocket sync of the same room on the page, whatever its URL.
52
52
  */
53
53
  socket?: string;
54
54
  /** Query parameters sent with each request (a token): an object, or a function read at every request. */
@@ -34,7 +34,7 @@ export const createCommentsClient = (options) => {
34
34
  assertRoomId(options.room);
35
35
  const server = options.server.replace(/\/+$/, '');
36
36
  const base = `${server.replace(/^ws/, 'http')}/${encodeURIComponent(options.room)}`;
37
- const socket = (options.socket ?? server.replace(/^http/, 'ws')).replace(/\/+$/, '');
37
+ const socket = options.socket?.replace(/\/+$/, '') ?? null;
38
38
  const request = async (method, body) => {
39
39
  const url = new URL(base);
40
40
  const params = typeof options.params === 'function' ? options.params() : options.params;
@@ -42,8 +42,8 @@ const getCaretRect = (point, origin, editor) => {
42
42
  const range = (point.node.ownerDocument ?? editor.ownerDocument).createRange();
43
43
  range.setStart(point.node, point.offset);
44
44
  range.collapse(true);
45
- const rect = typeof range.getBoundingClientRect === 'function' ? range.getBoundingClientRect() : null;
46
- if (rect && (rect.width || rect.height)) {
45
+ const rect = range.getBoundingClientRect();
46
+ if (rect.width || rect.height) {
47
47
  return toRemoteRect(rect, origin);
48
48
  }
49
49
  return getFallbackRect(point, origin, editor);
@@ -71,7 +71,7 @@ const getSelectionRects = (start, end, origin, editor) => {
71
71
  (end.text.node && renderSkipped(end.text.node))) {
72
72
  return [getFallbackRect(start, origin, editor)];
73
73
  }
74
- const clientRects = typeof range.getClientRects === 'function' ? Array.from(range.getClientRects()) : [];
74
+ const clientRects = Array.from(range.getClientRects());
75
75
  const seen = new Set();
76
76
  const rects = clientRects
77
77
  .filter((rect) => rect.width || rect.height)
@@ -35,7 +35,9 @@
35
35
  const previewViewOf = (cell: PreviewCell): BlockView => ({
36
36
  id: cell.id,
37
37
  type: cell.type,
38
- data: cell.data ?? {},
38
+ get data() {
39
+ return structuredClone(cell.data ?? {});
40
+ },
39
41
  selected: false,
40
42
  focused: false,
41
43
  handle: undefined,
@@ -5,11 +5,9 @@
5
5
  import { Edytor as EdytorClass, useEdytor } from '../edytor.svelte.js';
6
6
  import {
7
7
  SyncRefusedError,
8
- type Awareness,
9
8
  type DocChange,
10
9
  type DocumentActor,
11
- type EdytorDocument,
12
- type YDoc
10
+ type EdytorDocument
13
11
  } from '../crdt/index.js';
14
12
  import { CLOSE, validRoomId } from '../crdt/providers/room.js';
15
13
  import type { EdytorSync, WebsocketSyncOptions } from '../collaboration/index.js';
@@ -23,21 +21,15 @@
23
21
  import { brandOf } from '../plugins/brand.js';
24
22
  import type { PartialLabels } from '../labels.js';
25
23
  import { createRichTextPlugin } from '../plugins/richtext/RichTextPlugin.svelte';
26
- import { createArrowMovePlugin } from '../plugins/arrowMove/arrowMove.js';
27
- import { createImagePlugin } from '../plugins/image/ImagePlugin.svelte';
28
24
  import { createSuggestionsPlugin } from '../plugins/suggestions/suggestionsPlugin.js';
29
25
 
30
26
  /**
31
27
  * The plugins every view has unless it lists one of their kind (or sets
32
28
  * `defaultPlugins={false}`), after the app's so its kinds and keys win:
33
- * block moves, the suggestion UI, the image kind, then rich text.
29
+ * the suggestion UI, then rich text (an app lists `createArrowMovePlugin()`
30
+ * for block moves and `createImagePlugin({ upload })` for images itself).
34
31
  */
35
- const DEFAULTS = [
36
- createArrowMovePlugin(),
37
- createSuggestionsPlugin(),
38
- createImagePlugin(),
39
- createRichTextPlugin()
40
- ];
32
+ const DEFAULTS = [createSuggestionsPlugin(), createRichTextPlugin()];
41
33
  const withDefaults = (plugins: Plugin[] = []): Plugin[] => {
42
34
  const listed = new Set(plugins.map(brandOf));
43
35
  return [...plugins, ...DEFAULTS.filter((plugin) => !listed.has(brandOf(plugin)))];
@@ -67,7 +59,7 @@
67
59
 
68
60
  export type EdytorProps = {
69
61
  plugins?: Plugin[];
70
- /** Add rich text, arrow moves, the suggestion UI and images unless `plugins` lists them (default `true`). */
62
+ /** Add rich text and the suggestion UI unless `plugins` lists them (default `true`). */
71
63
  defaultPlugins?: boolean;
72
64
  class?: string;
73
65
  /** The root textbox's accessible name (or name it with `aria-labelledby`). */
@@ -81,8 +73,6 @@
81
73
  edytor?: EdytorClass;
82
74
  /** The assembled document this view renders (see `crdt/document.ts`). */
83
75
  document?: EdytorDocument;
84
- doc?: YDoc;
85
- awareness?: Awareness;
86
76
  /** The local author (id, name, color) of the document this view owns: attribution and presence. */
87
77
  actor?: DocumentActor;
88
78
  /**
@@ -96,6 +86,13 @@
96
86
  * the connection.
97
87
  */
98
88
  readonly?: boolean;
89
+ /**
90
+ * Let users nest blocks (default `false`): Tab and Shift+Tab nest and
91
+ * unnest, drops and handle keys may place a block inside another. Off,
92
+ * Tab is left to the browser and no gesture nests; containers keep their
93
+ * bodies. Read once.
94
+ */
95
+ allowNesting?: boolean;
99
96
  /** Chords (`mod+s`, `shift+alt+enter`) the view binds before plugins and built-ins. Read once. */
100
97
  hotkeys?: Partial<Record<HotKeyCombination, HotKey>>;
101
98
  /** After every commit that changed the visible document: a `JSONDoc`, the type `value` takes. */
@@ -186,8 +183,8 @@
186
183
  edytor = $bindable(),
187
184
  // Aliased so the DOM `document` global keeps working in this scope.
188
185
  document: edytorDocument,
189
- doc,
190
186
  readonly = false,
187
+ allowNesting = false,
191
188
  value = defaultValue,
192
189
  hotkeys,
193
190
  sync,
@@ -198,7 +195,6 @@
198
195
  onSyncRefused,
199
196
  requireHydration,
200
197
  snapshot,
201
- awareness,
202
198
  actor,
203
199
  presence,
204
200
  onChange,
@@ -216,10 +212,9 @@
216
212
 
217
213
  const initialEdytorOptions = untrack(() => ({
218
214
  readonly,
215
+ allowNesting,
219
216
  plugins: defaultPlugins ? withDefaults(userPlugins) : userPlugins,
220
217
  document: edytorDocument,
221
- doc,
222
- awareness,
223
218
  actor,
224
219
  requireHydration,
225
220
  hotkeys,
@@ -291,7 +286,7 @@
291
286
  }
292
287
  });
293
288
 
294
- // The component owns the Edytor — release its doc/awareness/facade/
289
+ // The component owns the Edytor — release its facade/
295
290
  // undo-manager listeners so a shared doc doesn't retain dead mounts.
296
291
  // `onDestroy` also runs after a server render: a view-owned document
297
292
  // (and its awareness timer) must not outlive the request.
@@ -408,6 +403,29 @@
408
403
  {/if}
409
404
 
410
405
  <style>
406
+ /* Where a drag lands, a block handle's or files' from outside (`DropIndicator`):
407
+ * the bar (4px) and the nest backdrop, placed in the overlay and shown
408
+ * (`data-shown`) by the drag; their look is a theme's (`edytor/themes/notion.css`). */
409
+ :global([data-edytor-drop-indicator]),
410
+ :global([data-edytor-drop-backdrop]) {
411
+ position: absolute;
412
+ pointer-events: none;
413
+ }
414
+
415
+ :global([data-edytor-drop-indicator]) {
416
+ z-index: 100;
417
+ box-sizing: border-box;
418
+ height: 4px;
419
+ }
420
+
421
+ :global([data-edytor-drop-backdrop]) {
422
+ z-index: 99;
423
+ }
424
+
425
+ :global([data-edytor-drop-backdrop]:not([data-shown='true'])) {
426
+ visibility: hidden;
427
+ }
428
+
411
429
  /*
412
430
  * Room below the last block, where a press puts the caret in a trailing
413
431
  * paragraph (`nav.trailing.press`): `--edytor-trailing-space`. No
@@ -1,5 +1,5 @@
1
1
  import { Edytor as EdytorClass, useEdytor } from '../edytor.svelte.js';
2
- import { SyncRefusedError, type Awareness, type DocChange, type DocumentActor, type EdytorDocument, type YDoc } from '../crdt/index.js';
2
+ import { SyncRefusedError, type DocChange, type DocumentActor, type EdytorDocument } from '../crdt/index.js';
3
3
  import type { EdytorSync, WebsocketSyncOptions } from '../collaboration/index.js';
4
4
  import { type PresenceOptions } from '../collaboration/awarenessSelection.js';
5
5
  export { EdytorClass as EdytorContext, useEdytor };
@@ -7,7 +7,7 @@ import type { Plugin } from '../plugins.js';
7
7
  import type { PartialLabels } from '../labels.js';
8
8
  export type EdytorProps = {
9
9
  plugins?: Plugin[];
10
- /** Add rich text, arrow moves, the suggestion UI and images unless `plugins` lists them (default `true`). */
10
+ /** Add rich text and the suggestion UI unless `plugins` lists them (default `true`). */
11
11
  defaultPlugins?: boolean;
12
12
  class?: string;
13
13
  /** The root textbox's accessible name (or name it with `aria-labelledby`). */
@@ -21,8 +21,6 @@ export type EdytorProps = {
21
21
  edytor?: EdytorClass;
22
22
  /** The assembled document this view renders (see `crdt/document.ts`). */
23
23
  document?: EdytorDocument;
24
- doc?: YDoc;
25
- awareness?: Awareness;
26
24
  /** The local author (id, name, color) of the document this view owns: attribution and presence. */
27
25
  actor?: DocumentActor;
28
26
  /**
@@ -36,6 +34,13 @@ export type EdytorProps = {
36
34
  * the connection.
37
35
  */
38
36
  readonly?: boolean;
37
+ /**
38
+ * Let users nest blocks (default `false`): Tab and Shift+Tab nest and
39
+ * unnest, drops and handle keys may place a block inside another. Off,
40
+ * Tab is left to the browser and no gesture nests; containers keep their
41
+ * bodies. Read once.
42
+ */
43
+ allowNesting?: boolean;
39
44
  /** Chords (`mod+s`, `shift+alt+enter`) the view binds before plugins and built-ins. Read once. */
40
45
  hotkeys?: Partial<Record<HotKeyCombination, HotKey>>;
41
46
  /** After every commit that changed the visible document: a `JSONDoc`, the type `value` takes. */
@@ -28,7 +28,7 @@
28
28
  return part.type;
29
29
  },
30
30
  get data() {
31
- return block ? block.data : (part.data ?? {});
31
+ return block ? block.data : structuredClone(part.data ?? {});
32
32
  },
33
33
  get selected() {
34
34
  return block?.selected ?? false;
@@ -4,14 +4,12 @@
4
4
  * Every way content enters an {@link EdytorDocument} crosses the same
5
5
  * gate here, in the same order, with the same refusal semantics:
6
6
  *
7
- * 1. **`assertUsableDoc`** — is this a v14 engine doc at all
8
- * ({@link UnsupportedDocError} `'foreign'`)?
9
- * 2. **`assertSchema`** — does the committed schema manifest allow
7
+ * 1. **`assertSchema`** — does the committed schema manifest allow
10
8
  * this build to own the doc ({@link SchemaMismatchError}:
11
9
  * `'unversioned'` content with no version stamp, `'unsupported'`
12
10
  * version this build cannot speak, `'foreign'` manifest naming
13
11
  * another schema)?
14
- * 3. **Verdict** — `'fresh'` (registry empty — seeding is legal) or
12
+ * 2. **Verdict** — `'fresh'` (registry empty — seeding is legal) or
15
13
  * `'initialized'` (supported schema content present — assert and
16
14
  * adopt, never seed).
17
15
  *
@@ -59,10 +57,10 @@
59
57
  * readiness transition, and a pending document never writes or
60
58
  * broadcasts a bootstrap block.
61
59
  */
62
- import { assertSchema, assertUsableDoc, checkSchema, isInitialized, registryEmpty, schemaVersion, SchemaMismatchError, UnsupportedDocError } from './edytor-doc.js';
60
+ import { assertSchema, checkSchema, isInitialized, registryEmpty, schemaVersion, SchemaMismatchError } from './edytor-doc.js';
63
61
  import type { SchemaProblem } from './edytor-doc.js';
64
62
  import type { EngineApi, EngineDoc, YDoc } from './engine-api.js';
65
- export { assertSchema, assertUsableDoc, checkSchema, isInitialized, registryEmpty, schemaVersion, SchemaMismatchError, UnsupportedDocError };
63
+ export { assertSchema, checkSchema, isInitialized, registryEmpty, schemaVersion, SchemaMismatchError };
66
64
  export type { SchemaProblem };
67
65
  /**
68
66
  * What an admitted doc carries — the `sync()` readiness decision:
@@ -83,7 +81,7 @@ export type AdmissionResult = {
83
81
  verdict: AdmissionVerdict;
84
82
  } | {
85
83
  admitted: false;
86
- error: UnsupportedDocError | SchemaMismatchError;
84
+ error: SchemaMismatchError;
87
85
  };
88
86
  /**
89
87
  * Raised when a candidate update cannot even be decoded or integrated —
@@ -102,9 +100,6 @@ export declare class UndecodableUpdateError extends Error {
102
100
  * The document-level admission gate — runs the ordered checks and throws
103
101
  * the typed refusal:
104
102
  *
105
- * - {@link UnsupportedDocError} `'foreign'` — not a v14 engine doc
106
- * (foreign roots/engines may still attach *content* later; this is
107
- * about doc shape);
108
103
  * - {@link SchemaMismatchError} `'unversioned' | 'unsupported' |
109
104
  * 'foreign'` — a schema claim this build cannot own.
110
105
  *
@@ -4,14 +4,12 @@
4
4
  * Every way content enters an {@link EdytorDocument} crosses the same
5
5
  * gate here, in the same order, with the same refusal semantics:
6
6
  *
7
- * 1. **`assertUsableDoc`** — is this a v14 engine doc at all
8
- * ({@link UnsupportedDocError} `'foreign'`)?
9
- * 2. **`assertSchema`** — does the committed schema manifest allow
7
+ * 1. **`assertSchema`** — does the committed schema manifest allow
10
8
  * this build to own the doc ({@link SchemaMismatchError}:
11
9
  * `'unversioned'` content with no version stamp, `'unsupported'`
12
10
  * version this build cannot speak, `'foreign'` manifest naming
13
11
  * another schema)?
14
- * 3. **Verdict** — `'fresh'` (registry empty — seeding is legal) or
12
+ * 2. **Verdict** — `'fresh'` (registry empty — seeding is legal) or
15
13
  * `'initialized'` (supported schema content present — assert and
16
14
  * adopt, never seed).
17
15
  *
@@ -59,11 +57,11 @@
59
57
  * readiness transition, and a pending document never writes or
60
58
  * broadcasts a bootstrap block.
61
59
  */
62
- import { assertSchema, assertUsableDoc, checkSchema, isInitialized, registryEmpty, schemaVersion, SchemaMismatchError, UnsupportedDocError } from './edytor-doc.js';
60
+ import { assertSchema, checkSchema, isInitialized, registryEmpty, schemaVersion, SchemaMismatchError } from './edytor-doc.js';
63
61
  import { asEngineDoc } from './structs.js';
64
62
  import { keepingReplaced } from './incarnations.js';
65
63
  // ── shared gate vocabulary (one doorway for document + transport layers) ──
66
- export { assertSchema, assertUsableDoc, checkSchema, isInitialized, registryEmpty, schemaVersion, SchemaMismatchError, UnsupportedDocError };
64
+ export { assertSchema, checkSchema, isInitialized, registryEmpty, schemaVersion, SchemaMismatchError };
67
65
  /**
68
66
  * Raised when a candidate update cannot even be decoded or integrated —
69
67
  * refused before the schema question is ever asked. Distinct from the
@@ -86,9 +84,6 @@ export class UndecodableUpdateError extends Error {
86
84
  * The document-level admission gate — runs the ordered checks and throws
87
85
  * the typed refusal:
88
86
  *
89
- * - {@link UnsupportedDocError} `'foreign'` — not a v14 engine doc
90
- * (foreign roots/engines may still attach *content* later; this is
91
- * about doc shape);
92
87
  * - {@link SchemaMismatchError} `'unversioned' | 'unsupported' |
93
88
  * 'foreign'` — a schema claim this build cannot own.
94
89
  *
@@ -98,7 +93,6 @@ export class UndecodableUpdateError extends Error {
98
93
  * readiness decision.
99
94
  */
100
95
  export const assertAdmission = (doc, docName = 'document') => {
101
- assertUsableDoc(doc);
102
96
  assertSchema(doc, docName);
103
97
  return isInitialized(doc) ? 'initialized' : 'fresh';
104
98
  };
@@ -112,7 +106,7 @@ export const inspectAdmission = (doc, docName = 'document') => {
112
106
  return { admitted: true, verdict: assertAdmission(doc, docName) };
113
107
  }
114
108
  catch (error) {
115
- if (error instanceof UnsupportedDocError || error instanceof SchemaMismatchError) {
109
+ if (error instanceof SchemaMismatchError) {
116
110
  return { admitted: false, error };
117
111
  }
118
112
  throw error;
@@ -73,6 +73,16 @@ export declare const readData: (node: EngineNode) => JsonObj | undefined;
73
73
  * in `node`'s data, in order; `[]` where no array is.
74
74
  */
75
75
  export declare const itemIds: (node: EngineNode, path: readonly string[]) => string[];
76
+ /**
77
+ * `node`'s data as it reads once `patches` are applied (nothing written),
78
+ * with the ids of the array items at a path (`itemIds`): an item a patch
79
+ * makes has no id yet (`''`), so a later patch names it by index.
80
+ * `null` when a patch fits no value.
81
+ */
82
+ export declare const previewData: (node: EngineNode | undefined, patches: readonly DataPatch[]) => {
83
+ data: JsonObj;
84
+ ids: (path: readonly string[]) => string[];
85
+ } | null;
76
86
  /** `data` with `patches` applied in order, as a fresh value (a root patch replaces it); `null` when one fits no value. */
77
87
  export declare const applyPatch: (data: JsonObj, patches: readonly DataPatch[]) => JsonObj | null;
78
88
  /**
package/dist/crdt/data.js CHANGED
@@ -354,8 +354,9 @@ export const readData = (node) => valueOf(treeOf(leavesOf(node)));
354
354
  * The ids of the items of the array at `path` (keys, item indexes or ids)
355
355
  * in `node`'s data, in order; `[]` where no array is.
356
356
  */
357
- export const itemIds = (node, path) => {
358
- let t = treeOf(leavesOf(node));
357
+ export const itemIds = (node, path) => idsAt(treeOf(leavesOf(node)), path);
358
+ const idsAt = (root, path) => {
359
+ let t = root;
359
360
  for (const k of path)
360
361
  if (t && isArr(open(t))) {
361
362
  const item = itemAt(itemsOf(t), k);
@@ -365,6 +366,23 @@ export const itemIds = (node, path) => {
365
366
  t = itemish(k) ? undefined : t?.k.get(enc(segmentKey(k)));
366
367
  return t ? itemsOf(open(t)).map(({ id }) => id) : [];
367
368
  };
369
+ /**
370
+ * `node`'s data as it reads once `patches` are applied (nothing written),
371
+ * with the ids of the array items at a path (`itemIds`): an item a patch
372
+ * makes has no id yet (`''`), so a later patch names it by index.
373
+ * `null` when a patch fits no value.
374
+ */
375
+ export const previewData = (node, patches) => {
376
+ const base = node ? leavesOf(node) : new Map();
377
+ const root = treeOf(base);
378
+ if (!patches.every((p) => edits().apply(root, p)))
379
+ return null;
380
+ const known = new Set([...base.keys()].flatMap((key) => key.split('/')));
381
+ return {
382
+ data: valueOf(root) ?? {},
383
+ ids: (path) => idsAt(root, path).map((id) => (known.has(`${id}#`) ? id : ''))
384
+ };
385
+ };
368
386
  /** `data` with `patches` applied in order, as a fresh value (a root patch replaces it); `null` when one fits no value. */
369
387
  export const applyPatch = (data, patches) => {
370
388
  const root = treeOf(dataLeaves(data));
@@ -2,13 +2,14 @@ import type { BlockAttributionApi } from '../attribution/block.js';
2
2
  import type { EdytorDocConfig, OpResult, Prepared } from './types.js';
3
3
  import type { DocBase, DocReads } from './reads.js';
4
4
  /**
5
- * Raised when a mutating facade op runs after `dispose()` — disposal is
6
- * terminal for writes (reads stay dead-safe: a stale handle may still be
7
- * inspected while its document outlives the facade). The facade-level
8
- * counterpart of `DocumentDestroyedError` — kept in this module because a
9
- * bare `bindEdytorDoc` facade has no document layer above it.
5
+ * Raised when an operation runs against a torn-down document: every attach
6
+ * reference released, or its facade disposed (terminal for writes; reads
7
+ * stay dead-safe: a stale handle may still be inspected). Distinct from
8
+ * `DocumentNotReadyError`: destroyed is terminal, pending may still resolve.
9
+ * Kept in this module because a bare `bindEdytorDoc` facade has no document
10
+ * layer above it.
10
11
  */
11
- export declare class EdytorDocDisposedError extends Error {
12
+ export declare class DocumentDestroyedError extends Error {
12
13
  constructor(service?: string);
13
14
  }
14
15
  /** What the write funnel writes with: the reads, the attribution writer and the facade's configuration. */
@@ -11,16 +11,17 @@ import { writeLeaves } from '../data.js';
11
11
  import { NOOP } from './plan.js';
12
12
  import { claimInto, deleteRange, formatRangeIn, insertIntoText } from '../text/model.js';
13
13
  /**
14
- * Raised when a mutating facade op runs after `dispose()` — disposal is
15
- * terminal for writes (reads stay dead-safe: a stale handle may still be
16
- * inspected while its document outlives the facade). The facade-level
17
- * counterpart of `DocumentDestroyedError` — kept in this module because a
18
- * bare `bindEdytorDoc` facade has no document layer above it.
14
+ * Raised when an operation runs against a torn-down document: every attach
15
+ * reference released, or its facade disposed (terminal for writes; reads
16
+ * stay dead-safe: a stale handle may still be inspected). Distinct from
17
+ * `DocumentNotReadyError`: destroyed is terminal, pending may still resolve.
18
+ * Kept in this module because a bare `bindEdytorDoc` facade has no document
19
+ * layer above it.
19
20
  */
20
- export class EdytorDocDisposedError extends Error {
21
+ export class DocumentDestroyedError extends Error {
21
22
  constructor(service) {
22
- super(`EdytorDoc${service ? `.${service}` : ''}: facade is disposed.`);
23
- this.name = 'EdytorDocDisposedError';
23
+ super(`${service ?? 'document'}: the document is destroyed`);
24
+ this.name = 'DocumentDestroyedError';
24
25
  }
25
26
  }
26
27
  /** The write funnel of one facade. */
@@ -37,7 +38,7 @@ export const writeFunnel = (c) => {
37
38
  /** Mutation boundary — every facade write funnels through `write`. */
38
39
  const write = (fn) => {
39
40
  if (disposed) {
40
- throw new EdytorDocDisposedError();
41
+ throw new DocumentDestroyedError();
41
42
  }
42
43
  config.assertWritable?.();
43
44
  return fn();