edytor 2.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 (222) hide show
  1. package/README.md +19 -4
  2. package/dist/block/block.svelte.js +2 -4
  3. package/dist/clipboard/htmlFlow.d.ts +20 -7
  4. package/dist/clipboard/htmlFlow.js +23 -15
  5. package/dist/clipboard/serializeClipboardFragment.d.ts +37 -4
  6. package/dist/clipboard/serializeClipboardFragment.js +6 -4
  7. package/dist/clipboard/textBlocks.d.ts +8 -0
  8. package/dist/clipboard/textBlocks.js +6 -2
  9. package/dist/cloudflare/room/admission.js +3 -3
  10. package/dist/cloudflare/room/context.d.ts +1 -1
  11. package/dist/cloudflare/routeDocumentSocket.d.ts +17 -0
  12. package/dist/cloudflare/routeDocumentSocket.js +53 -0
  13. package/dist/collaboration/comments/client.d.ts +1 -1
  14. package/dist/collaboration/comments/client.js +1 -1
  15. package/dist/collaboration/remoteSelection.js +3 -3
  16. package/dist/components/Edytor.svelte +2 -10
  17. package/dist/components/Edytor.svelte.d.ts +1 -3
  18. package/dist/crdt/admission.d.ts +5 -10
  19. package/dist/crdt/admission.js +5 -11
  20. package/dist/crdt/doc/funnel.d.ts +7 -6
  21. package/dist/crdt/doc/funnel.js +10 -9
  22. package/dist/crdt/doc/gate.d.ts +0 -17
  23. package/dist/crdt/doc/gate.js +2 -41
  24. package/dist/crdt/doc/history.js +2 -2
  25. package/dist/crdt/document.d.ts +5 -14
  26. package/dist/crdt/document.js +5 -19
  27. package/dist/crdt/edytor-doc.d.ts +4 -4
  28. package/dist/crdt/edytor-doc.js +3 -7
  29. package/dist/crdt/index.d.ts +3 -3
  30. package/dist/crdt/index.js +2 -2
  31. package/dist/crdt/protocol.d.ts +2 -2
  32. package/dist/crdt/protocol.js +2 -2
  33. package/dist/crdt/providers/index.d.ts +1 -1
  34. package/dist/crdt/providers/room.d.ts +10 -9
  35. package/dist/crdt/providers/room.js +13 -10
  36. package/dist/crdt/providers/websocket.d.ts +1 -1
  37. package/dist/crdt/providers/websocket.js +28 -29
  38. package/dist/crdt/semantics.js +3 -1
  39. package/dist/dnd/autoScroll.d.ts +15 -7
  40. package/dist/dnd/autoScroll.js +10 -15
  41. package/dist/dnd/dropIndicator.js +2 -5
  42. package/dist/dnd/external.d.ts +6 -0
  43. package/dist/dnd/external.js +17 -0
  44. package/dist/dnd/geometry.d.ts +2 -0
  45. package/dist/dnd/geometry.js +3 -4
  46. package/dist/dnd/placement.d.ts +1 -1
  47. package/dist/dnd/pragmatic.d.ts +3 -1
  48. package/dist/dnd/pragmatic.js +12 -2
  49. package/dist/dnd/vendor/pragmatic/UPSTREAM.md +51 -18
  50. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/drop-target-for-external.d.ts +4 -0
  51. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/drop-target-for-external.js +6 -0
  52. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/element-adapter.d.ts +0 -2
  53. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/element-adapter.js +4 -22
  54. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/external-adapter-types.d.ts +26 -0
  55. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/external-adapter.d.ts +30 -0
  56. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/external-adapter.js +203 -0
  57. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/get-available-items.js +13 -0
  58. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/get-available-types.js +9 -0
  59. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/is-an-available-type.js +19 -0
  60. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/monitor-for-external.d.ts +4 -0
  61. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/monitor-for-external.js +6 -0
  62. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/honey-pot-fix/make-honey-pot-fix.js +4 -16
  63. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/internal-types.d.ts +1 -8
  64. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/make-adapter/make-drop-target.js +5 -6
  65. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/make-adapter/make-monitor.js +2 -3
  66. 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
  67. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/element/custom-native-drag-preview/set-custom-native-drag-preview.js +6 -24
  68. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/contains-files.d.ts +2 -0
  69. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/contains-files.js +5 -0
  70. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/get-files.d.ts +3 -0
  71. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/get-files.js +12 -0
  72. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/native-types.d.ts +12 -0
  73. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/changing-window/is-entering-window-in-safari.js +10 -0
  74. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/changing-window/is-entering-window.js +55 -0
  75. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/entry-point/external.d.ts +4 -0
  76. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/entry-point/external.js +8 -0
  77. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/internal-types.d.ts +0 -4
  78. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/over-element/get-scroll-by.js +16 -72
  79. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/over-element/try-scroll.js +1 -9
  80. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/shared/configuration.js +4 -29
  81. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/shared/get-scroll-change.js +2 -3
  82. package/dist/edytor.svelte.d.ts +2 -5
  83. package/dist/edytor.svelte.js +17 -31
  84. package/dist/events/onBeforeInput.js +1 -1
  85. package/dist/events/onDrop.d.ts +0 -6
  86. package/dist/events/onDrop.js +10 -23
  87. package/dist/events/onInput.d.ts +0 -12
  88. package/dist/events/onInput.js +1 -67
  89. package/dist/export/forms.d.ts +627 -0
  90. package/dist/export/forms.js +749 -0
  91. package/dist/export/index.d.ts +170 -0
  92. package/dist/export/index.js +161 -0
  93. package/dist/export/markdown.d.ts +41 -0
  94. package/dist/export/markdown.js +585 -0
  95. package/dist/export/markdownWriter.d.ts +9 -0
  96. package/dist/export/markdownWriter.js +133 -0
  97. package/dist/export/mdx.d.ts +15 -0
  98. package/dist/export/mdx.js +33 -0
  99. package/dist/index.d.ts +3 -3
  100. package/dist/index.js +1 -1
  101. package/dist/kinds.js +1 -1
  102. package/dist/plugins/anchored.svelte.d.ts +21 -25
  103. package/dist/plugins/anchored.svelte.js +23 -39
  104. package/dist/plugins/blockHandles/BlockHandleController.svelte.js +24 -32
  105. package/dist/plugins/blockHandles/dragPreview.d.ts +26 -7
  106. package/dist/plugins/blockHandles/dragPreview.js +33 -0
  107. package/dist/plugins/blockMenu/BlockMenuController.svelte.d.ts +5 -0
  108. package/dist/plugins/blockMenu/BlockMenuController.svelte.js +7 -2
  109. package/dist/plugins/blockMenu/blockMenuPlugin.js +5 -50
  110. package/dist/plugins/chrome.d.ts +27 -5
  111. package/dist/plugins/chrome.js +24 -9
  112. package/dist/plugins/code/CodePlugin.svelte +12 -9
  113. package/dist/plugins/code/header.svelte.js +7 -11
  114. package/dist/plugins/code/languageMenu.svelte.d.ts +5 -2
  115. package/dist/plugins/code/languageMenu.svelte.js +13 -20
  116. package/dist/plugins/columns/ColumnsPlugin.svelte +7 -20
  117. package/dist/plugins/columns/resize.svelte.d.ts +2 -3
  118. package/dist/plugins/columns/resize.svelte.js +2 -5
  119. package/dist/plugins/comments/CommentsController.svelte.d.ts +6 -0
  120. package/dist/plugins/comments/CommentsController.svelte.js +20 -0
  121. package/dist/plugins/equation/EquationPlugin.svelte +12 -22
  122. package/dist/plugins/equation/equation.svelte.d.ts +7 -15
  123. package/dist/plugins/equation/equation.svelte.js +13 -50
  124. package/dist/plugins/equation/tex.d.ts +17 -0
  125. package/dist/plugins/equation/tex.js +42 -0
  126. package/dist/plugins/fileDrop.d.ts +38 -23
  127. package/dist/plugins/fileDrop.js +82 -76
  128. package/dist/plugins/find/FindController.svelte.d.ts +5 -0
  129. package/dist/plugins/find/FindController.svelte.js +14 -14
  130. package/dist/plugins/find/findPlugin.js +1 -15
  131. package/dist/plugins/image/ImageChrome.svelte +8 -2
  132. package/dist/plugins/image/ImagePlugin.svelte +15 -52
  133. package/dist/plugins/image/controls.svelte.js +16 -2
  134. package/dist/plugins/image/toolbar.svelte.d.ts +5 -4
  135. package/dist/plugins/image/toolbar.svelte.js +21 -31
  136. package/dist/plugins/index.d.ts +1 -1
  137. package/dist/plugins/list.svelte.d.ts +9 -0
  138. package/dist/plugins/list.svelte.js +18 -0
  139. package/dist/plugins/marquee/marqueePlugin.js +0 -4
  140. package/dist/plugins/media/AudioPlugin.svelte +1 -1
  141. package/dist/plugins/media/BookmarkPlugin.svelte +6 -97
  142. package/dist/plugins/media/EmbedPlugin.svelte +5 -90
  143. package/dist/plugins/media/EmptySlot.svelte +36 -20
  144. package/dist/plugins/media/EmptySlot.svelte.d.ts +6 -15
  145. package/dist/plugins/media/FilePlugin.svelte +1 -19
  146. package/dist/plugins/media/MediaEmpty.svelte +2 -55
  147. package/dist/plugins/media/MediaEmpty.svelte.d.ts +18 -14
  148. package/dist/plugins/media/SourceBlock.svelte +142 -52
  149. package/dist/plugins/media/SourceBlock.svelte.d.ts +10 -4
  150. package/dist/plugins/media/VideoPlugin.svelte +1 -1
  151. package/dist/plugins/media/empty.svelte.js +2 -7
  152. package/dist/plugins/media/media.d.ts +2 -63
  153. package/dist/plugins/media/media.js +1 -194
  154. package/dist/plugins/media/sources.d.ts +60 -0
  155. package/dist/plugins/media/sources.js +165 -0
  156. package/dist/plugins/media/urlPaste.svelte.d.ts +2 -1
  157. package/dist/plugins/media/urlPaste.svelte.js +12 -21
  158. package/dist/plugins/mention/MentionPlugin.svelte +8 -35
  159. package/dist/plugins/page/PagePlugin.svelte +2 -9
  160. package/dist/plugins/page/page.js +1 -1
  161. package/dist/plugins/pageLink/PageLinkPlugin.svelte +6 -13
  162. package/dist/plugins/richtext/RichTextPlugin.svelte +85 -197
  163. package/dist/plugins/richtext/body.js +1 -1
  164. package/dist/plugins/richtext/calloutIcons.svelte.d.ts +2 -2
  165. package/dist/plugins/richtext/calloutIcons.svelte.js +5 -7
  166. package/dist/plugins/richtext/richTextOperations.d.ts +1 -19
  167. package/dist/plugins/richtext/richTextOperations.js +3 -49
  168. package/dist/plugins/slashMenu/SlashMenuController.svelte.d.ts +2 -0
  169. package/dist/plugins/slashMenu/SlashMenuController.svelte.js +7 -0
  170. package/dist/plugins/slashMenu/slashMenuPlugin.js +5 -14
  171. package/dist/plugins/table/TableChrome.svelte +2 -3
  172. package/dist/plugins/table/TablePlugin.svelte +20 -54
  173. package/dist/plugins/table/chrome.svelte.d.ts +5 -2
  174. package/dist/plugins/table/chrome.svelte.js +30 -54
  175. package/dist/plugins/table/table.js +3 -3
  176. package/dist/plugins/toc/TocPlugin.svelte +2 -4
  177. package/dist/plugins/toolbar/ToolbarController.svelte.d.ts +7 -0
  178. package/dist/plugins/toolbar/ToolbarController.svelte.js +20 -13
  179. package/dist/plugins/toolbar/toolbarPlugin.js +19 -53
  180. package/dist/plugins/triggers/TriggerController.svelte.d.ts +7 -2
  181. package/dist/plugins/triggers/TriggerController.svelte.js +11 -4
  182. package/dist/plugins/triggers/place.js +1 -2
  183. package/dist/plugins/triggers/triggers.d.ts +6 -1
  184. package/dist/plugins/triggers/triggers.js +12 -13
  185. package/dist/plugins/uploads.svelte.d.ts +5 -2
  186. package/dist/plugins/uploads.svelte.js +25 -15
  187. package/dist/plugins.d.ts +26 -0
  188. package/dist/plugins.js +0 -24
  189. package/dist/selection/replaceSelection.js +2 -2
  190. package/dist/selection/selection.svelte.d.ts +4 -10
  191. package/dist/selection/selection.svelte.js +7 -11
  192. package/dist/session/bindings.js +1 -1
  193. package/dist/session/commands.d.ts +7 -11
  194. package/dist/session/commands.js +13 -19
  195. package/dist/session/inputRules.js +1 -1
  196. package/dist/session/navigation.js +1 -1
  197. package/dist/session/suggestions.svelte.d.ts +0 -11
  198. package/dist/session/suggestions.svelte.js +15 -3
  199. package/dist/session/uploads.d.ts +8 -8
  200. package/dist/surface/lines.js +1 -3
  201. package/dist/surface/observer.svelte.d.ts +17 -1
  202. package/dist/surface/observer.svelte.js +72 -17
  203. package/dist/surface/overlay.d.ts +0 -14
  204. package/dist/surface/overlay.js +0 -25
  205. package/dist/surface/projector.svelte.js +1 -1
  206. package/dist/text/text.svelte.js +3 -13
  207. package/dist/utils/sanitize.d.ts +38 -0
  208. package/dist/utils/sanitize.js +82 -0
  209. package/dist/utils.d.ts +2 -0
  210. package/dist/utils.js +5 -0
  211. package/package.json +18 -1
  212. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/max-z-index.js +0 -3
  213. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/supports-popover.js +0 -16
  214. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/shared/is-axis-allowed.js +0 -3
  215. package/dist/plugins/code/LanguageMenu.svelte +0 -25
  216. package/dist/plugins/code/LanguageMenu.svelte.d.ts +0 -9
  217. package/dist/plugins/equation/EquationEditor.svelte +0 -27
  218. package/dist/plugins/equation/EquationEditor.svelte.d.ts +0 -9
  219. package/dist/plugins/image/ImageToolbarHost.svelte +0 -37
  220. package/dist/plugins/image/ImageToolbarHost.svelte.d.ts +0 -9
  221. package/dist/plugins/richtext/CalloutIconPicker.svelte +0 -26
  222. package/dist/plugins/richtext/CalloutIconPicker.svelte.d.ts +0 -9
@@ -38,7 +38,9 @@ import { diffText } from '../utils/diffText.js';
38
38
  import { jsonEquals } from '../utils/json.js';
39
39
  import { activeMarks, marksForInsertion } from '../session/editing/text.js';
40
40
  import { INTENTS } from '../session/attempt.js';
41
- import { getCollapsedDomTextSelection, handleNativeLineBreakTextValue, readDomText } from '../events/onInput.js';
41
+ import { runOccurrence } from '../events/onBeforeInput.js';
42
+ import { getDomSelectionSnapshot } from '../selection/domSelection.js';
43
+ import { domOffsetIn } from '../selection/selection.utils.js';
42
44
  import { diverges, heal, ownedOf } from './attributes.js';
43
45
  import { getTextContentOffsetAtPoint } from '../events/domTextOffset.js';
44
46
  import { rendersMark } from './cells.js';
@@ -67,22 +69,68 @@ const textOf = (cell) => cell.runs
67
69
  .replace(ZWSP, '');
68
70
  /** Svelte's anchors: comments and empty text nodes are never input and never removed. */
69
71
  const isAnchor = (node) => node.nodeType === Node.COMMENT_NODE || (node.nodeType === Node.TEXT_NODE && !node.nodeValue);
70
- /** Non-empty text nodes under `node`. */
71
- const leaves = (node) => {
72
+ /** The `nth` non-empty text node under `node`. */
73
+ const textLeaf = (node, nth = 0) => {
72
74
  const walker = (node.ownerDocument ?? document).createTreeWalker(node, NodeFilter.SHOW_TEXT);
73
- let count = 0;
74
75
  while (walker.nextNode())
75
- if (walker.currentNode.data)
76
- count++;
77
- return count;
78
- };
79
- const firstLeaf = (node) => {
80
- const walker = (node.ownerDocument ?? document).createTreeWalker(node, NodeFilter.SHOW_TEXT);
81
- while (walker.nextNode())
82
- if (walker.currentNode.data)
76
+ if (walker.currentNode.data && nth-- === 0)
83
77
  return walker.currentNode;
84
78
  return null;
85
79
  };
80
+ const ZERO_WIDTH_SPACE = '\u200B';
81
+ /**
82
+ * What the browser shows for `text`: its element's text nodes in order (WebKit's
83
+ * converted-space wrappers read as plain spaces), the empty filler and the
84
+ * trailing-newline marker stripped.
85
+ */
86
+ export const readDomText = (text) => {
87
+ let value = '';
88
+ const node = text.node;
89
+ if (node) {
90
+ const walker = (node.ownerDocument ?? document).createTreeWalker(node, NodeFilter.SHOW_TEXT);
91
+ while (walker.nextNode()) {
92
+ const leaf = walker.currentNode;
93
+ value += leaf.parentElement?.closest('span.Apple-converted-space')
94
+ ? leaf.data.replaceAll('\u00A0', ' ')
95
+ : leaf.data;
96
+ }
97
+ }
98
+ // An empty text's filler shares its node with what the browser typed, on either side.
99
+ if (text.isEmpty)
100
+ return value.replaceAll(ZERO_WIDTH_SPACE, '');
101
+ if (text.endsWithNewline && value.endsWith(ZERO_WIDTH_SPACE)) {
102
+ value = value.slice(0, -ZERO_WIDTH_SPACE.length);
103
+ }
104
+ return value;
105
+ };
106
+ export const getCollapsedDomTextSelection = (edytor) => {
107
+ const selection = getDomSelectionSnapshot(edytor.node);
108
+ if (!selection?.anchorNode || !selection.isCollapsed) {
109
+ return null;
110
+ }
111
+ if (!edytor.node?.contains(selection.anchorNode)) {
112
+ return null;
113
+ }
114
+ const text = edytor.selection.getTextOfNode(selection.anchorNode);
115
+ if (!text?.node) {
116
+ return null;
117
+ }
118
+ // No model rule here: the DOM caret is read BEFORE the reconcile writes
119
+ // the model, so a native insertion legitimately sits past `text.length`
120
+ // (`selectText` clamps the final write).
121
+ return { text, offset: domOffsetIn(text, selection.anchorNode, selection.anchorOffset) };
122
+ };
123
+ /** A native line break (a diff inserting only newlines at `at`) runs as the key's occurrence. */
124
+ export const lineBreak = async (edytor, text, value, at) => {
125
+ if (!text.isInDocument)
126
+ return;
127
+ const inputType = edytor.attempts.confirm()?.inputType === 'insertLineBreak' || value.includes('\n')
128
+ ? 'insertLineBreak'
129
+ : 'insertParagraph';
130
+ text.refreshFromModel();
131
+ edytor.selection.selectText(text, at);
132
+ await runOccurrence(edytor, { inputType, cancelable: false });
133
+ };
86
134
  const settle = (promises) => Promise.all(promises).then(() => undefined);
87
135
  /** The collapsed DOM caret's text offset inside `nodes` (a replacement's), if it is there. */
88
136
  const caretIn = (nodes) => {
@@ -167,9 +215,16 @@ export class SurfaceObserver {
167
215
  clearTimeout(this.#window ?? undefined);
168
216
  };
169
217
  };
170
- /** Register an element the core rendered (an attachment); returns its release. */
218
+ /**
219
+ * Register an element the core rendered (an attachment); returns its
220
+ * release. A text element takes the table's attributes now (its id, its
221
+ * editing host inside a void).
222
+ */
171
223
  register = (node, kind, block, name, owned) => {
172
- this.#registry.set(node, { kind, block, name, owned });
224
+ const entry = { kind, block, name, owned };
225
+ this.#registry.set(node, entry);
226
+ if (kind === 'text')
227
+ heal(node, ownedOf(this.edytor, node, entry));
173
228
  if (kind === 'block')
174
229
  this.#blocks.set(block, node);
175
230
  else if (kind !== 'anchor' && kind !== 'preview') {
@@ -579,7 +634,7 @@ export class SurfaceObserver {
579
634
  const shown = deltas.length ? deltas : [{ text: '\u200B' }];
580
635
  for (const [i, delta] of shown.entries()) {
581
636
  const node = nodes[i];
582
- const leaf = node?.nodeType === Node.TEXT_NODE ? node : node && firstLeaf(node);
637
+ const leaf = node?.nodeType === Node.TEXT_NODE ? node : node && textLeaf(node);
583
638
  if (!leaf)
584
639
  return false;
585
640
  if (leaf.data !== delta.text)
@@ -614,7 +669,7 @@ export class SurfaceObserver {
614
669
  const at = replaced ? replaced.caret : caret?.text === text ? caret.offset : undefined;
615
670
  const line = diffText(text.stringContent, dom);
616
671
  if (line && !line.remove && /^[\r\n]+$/.test(line.insert)) {
617
- work.push(handleNativeLineBreakTextValue(edytor, text, dom));
672
+ work.push(lineBreak(edytor, text, dom, line.at));
618
673
  continue;
619
674
  }
620
675
  work.push(adopt(edytor, text, dom, at));
@@ -698,7 +753,7 @@ export class SurfaceObserver {
698
753
  if (marks.join() !== chain.join())
699
754
  return 'mark-chain';
700
755
  // The snippet's markup is its own, but the delta renders one text node.
701
- if (leaves(node) !== 1)
756
+ if (!textLeaf(node) || textLeaf(node, 1))
702
757
  return 'mark-text';
703
758
  }
704
759
  }
@@ -50,20 +50,6 @@ export declare const pin: (host: HTMLElement) => CSSStyleDeclaration & {
50
50
  left: string;
51
51
  top: string;
52
52
  };
53
- /**
54
- * A popup's place near `rect` (client px), 8px inside the viewport: `gap`
55
- * below it, above it when there is no room below (`above`: above first,
56
- * below when there is no room above), its left edge at `rect`'s or
57
- * centered on it (`center`). Measured now from `box` (the host's first
58
- * element by default; `fallback` before it has a size), written by the
59
- * answer (the overlay's read-then-write pass).
60
- */
61
- export declare const placeNear: (host: HTMLElement, rect: DOMRect, fallback: readonly [width: number, height: number], { gap, above, center, box }?: {
62
- gap?: number;
63
- above?: boolean;
64
- center?: boolean;
65
- box?: Element | null;
66
- }) => (() => void) | void;
67
53
  export declare class Overlay {
68
54
  #private;
69
55
  /** The layer: absolutely positioned at its static place right after the host. */
@@ -68,31 +68,6 @@ export const sameValues = (a, b) => a === b ||
68
68
  Object.keys(a).every((key) => a[key] === b[key]));
69
69
  /** A mounted host whose chrome places itself layer-relative: pinned at the layer's origin. */
70
70
  export const pin = (host) => Object.assign(host.style, { position: 'absolute', left: '0px', top: '0px' });
71
- /**
72
- * A popup's place near `rect` (client px), 8px inside the viewport: `gap`
73
- * below it, above it when there is no room below (`above`: above first,
74
- * below when there is no room above), its left edge at `rect`'s or
75
- * centered on it (`center`). Measured now from `box` (the host's first
76
- * element by default; `fallback` before it has a size), written by the
77
- * answer (the overlay's read-then-write pass).
78
- */
79
- export const placeNear = (host, rect, fallback, { gap = 8, above = false, center = false, box = host.firstElementChild } = {}) => {
80
- const view = host.ownerDocument.defaultView;
81
- if (!view)
82
- return;
83
- const size = box?.getBoundingClientRect();
84
- const width = size?.width || fallback[0];
85
- const height = size?.height || fallback[1];
86
- const x = center ? rect.left + rect.width / 2 - width / 2 : rect.left;
87
- const [below, up] = [rect.bottom + gap, rect.top - gap - height];
88
- const y = above
89
- ? up >= 8
90
- ? up
91
- : below
92
- : Math.max(8, below + height < view.innerHeight ? below : up);
93
- const left = `${Math.max(8, Math.min(x, view.innerWidth - width - 8))}px`;
94
- return () => Object.assign(host.style, { left, top: `${y}px` });
95
- };
96
71
  /** The band around the viewport (`near`) whose blocks' resizes are watched, in screens above and below. */
97
72
  const NEAR_SCREENS = 1;
98
73
  export class Overlay {
@@ -467,7 +467,7 @@ export class Projector {
467
467
  };
468
468
  #write = (dom, { anchor, focus }, collapsed) => {
469
469
  this.#parking = false;
470
- if (!collapsed && typeof dom.setBaseAndExtent === 'function') {
470
+ if (!collapsed) {
471
471
  try {
472
472
  dom.setBaseAndExtent(anchor[0], anchor[1], focus[0], focus[1]);
473
473
  return;
@@ -1,3 +1,4 @@
1
+ import { cleanups } from '../utils.js';
1
2
  import {} from '../utils/json.js';
2
3
  import { segmentJSON } from '../session/handles.js';
3
4
  /**
@@ -101,20 +102,9 @@ export class Text {
101
102
  attach = (node) => {
102
103
  this.node = node;
103
104
  this.edytor.nodeToText.set(node, this);
105
+ // Registered, it takes the attribute table's attributes (`surface/attributes.ts`).
104
106
  const release = this.edytor.surface.register(node, 'text', this.parent.id);
105
- node.setAttribute('data-edytor-id', `${this.id}`);
106
- node.setAttribute('data-edytor-text', `true`);
107
- const pluginDestroy = this.edytor.plugins.flatMap((plugin) => {
108
- const action = plugin.onTextAttached?.({ node, text: this });
109
- return typeof action === 'function' ? [action] : [];
110
- });
111
- let insideVoid = false;
112
- for (let block = this.parent; block && !insideVoid; block = block.parent)
113
- insideVoid = !!block.definition.void;
114
- if (insideVoid) {
115
- node.setAttribute('contenteditable', 'true');
116
- node.style.outline = 'none';
117
- }
107
+ const pluginDestroy = cleanups(this.edytor.plugins, (plugin) => plugin.onTextAttached?.({ node, text: this }));
118
108
  // A display that waited for a mounted destination runs in the next pass.
119
109
  this.edytor.projector?.mounted(this);
120
110
  return {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The sanitizers every reader of untrusted values shares: a link's href and
3
+ * a CSS colour (marks), read the same by render, the clipboard, HTML and
4
+ * markdown import and export. Pure: no DOM, no Svelte, Worker-safe.
5
+ */
6
+ /**
7
+ * CSS-color sanitizer for `color`/`highlight` marks. The value lands in
8
+ * `style="color: {value}"` — a `;`/`{`/`}` payload escapes the property
9
+ * and injects arbitrary declarations (`url()` exfil, `position:fixed`
10
+ * overlays). Metacharacters, escape/obfuscation sequences, and
11
+ * resource-loading functions are rejected; real `<color>` values
12
+ * (`#hex`, `rgb()/hsl()/oklch()/color-mix()/var()`, named colors) pass.
13
+ */
14
+ export declare const sanitizeCssColorValue: (value: unknown) => string | null;
15
+ /**
16
+ * Scheme allowlist for link hrefs. `javascript:`/`data:`/`vbscript:`
17
+ * payloads can arrive through native `insertLink`, pasted HTML, or a
18
+ * malicious collaborator — and Svelte renders `href` verbatim. The href is
19
+ * read as the browser's URL parser reads it: it strips C0 controls and
20
+ * spaces at both ends and tab/newline/CR anywhere (`\u0001javascript:` is
21
+ * `javascript:` to a browser), then resolves it against a base; only an
22
+ * allowed scheme, or a relative href that stays relative, passes.
23
+ */
24
+ export declare const sanitizeLinkHref: (href: unknown) => string | null;
25
+ /**
26
+ * A web page's URL a media block may store or render: the link sanitizer's
27
+ * reading of it ({@link sanitizeLinkHref}: ASCII control characters and spaces stripped,
28
+ * tab/newline anywhere), then only an absolute `http:`/`https:` URL with a
29
+ * host. `null` for anything else: a script or `data:` URL, a `mailto:`, a
30
+ * relative path, a bare domain.
31
+ */
32
+ export declare const safeWebUrl: (value: unknown) => string | null;
33
+ /**
34
+ * A media file's source (a video, an audio track, an attachment): a
35
+ * {@link safeWebUrl}, or a `blob:` URL an upload answered. Never `data:`:
36
+ * an inline file would be stored in the document and sent in every update.
37
+ */
38
+ export declare const safeMediaSrc: (value: unknown) => string | null;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The sanitizers every reader of untrusted values shares: a link's href and
3
+ * a CSS colour (marks), read the same by render, the clipboard, HTML and
4
+ * markdown import and export. Pure: no DOM, no Svelte, Worker-safe.
5
+ */
6
+ const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
7
+ /**
8
+ * CSS-color sanitizer for `color`/`highlight` marks. The value lands in
9
+ * `style="color: {value}"` — a `;`/`{`/`}` payload escapes the property
10
+ * and injects arbitrary declarations (`url()` exfil, `position:fixed`
11
+ * overlays). Metacharacters, escape/obfuscation sequences, and
12
+ * resource-loading functions are rejected; real `<color>` values
13
+ * (`#hex`, `rgb()/hsl()/oklch()/color-mix()/var()`, named colors) pass.
14
+ */
15
+ export const sanitizeCssColorValue = (value) => {
16
+ if (typeof value !== 'string')
17
+ return null;
18
+ const trimmed = value.trim();
19
+ if (!trimmed || trimmed.length > 128)
20
+ return null;
21
+ if (/[;{}<>\\'"`]/.test(trimmed) || /url\s*\(|expression\s*\(|\/\*|\*\//i.test(trimmed)) {
22
+ return null;
23
+ }
24
+ return trimmed;
25
+ };
26
+ /**
27
+ * Scheme allowlist for link hrefs. `javascript:`/`data:`/`vbscript:`
28
+ * payloads can arrive through native `insertLink`, pasted HTML, or a
29
+ * malicious collaborator — and Svelte renders `href` verbatim. The href is
30
+ * read as the browser's URL parser reads it: it strips C0 controls and
31
+ * spaces at both ends and tab/newline/CR anywhere (`\u0001javascript:` is
32
+ * `javascript:` to a browser), then resolves it against a base; only an
33
+ * allowed scheme, or a relative href that stays relative, passes.
34
+ */
35
+ export const sanitizeLinkHref = (href) => {
36
+ const trimmed = hrefOf(href);
37
+ // A relative href resolves to the base's `https:`: it names no scheme of its own.
38
+ return trimmed &&
39
+ SAFE_LINK_PROTOCOLS.has(urlOf(trimmed, 'https://relative.invalid/')?.protocol ?? '')
40
+ ? trimmed
41
+ : null;
42
+ };
43
+ /**
44
+ * A href as the browser's URL parser reads it: C0 controls and spaces
45
+ * stripped at both ends, tab/newline/CR anywhere; `''` for a non-string
46
+ * (marks arrive from untrusted sources too: synced peers, paste).
47
+ */
48
+ const hrefOf = (href) => typeof href === 'string'
49
+ ? // eslint-disable-next-line no-control-regex
50
+ href.replace(/[\t\n\r]/g, '').replace(/^[\u0000-\u0020]+|[\u0000-\u0020]+$/g, '')
51
+ : '';
52
+ /** `href` parsed (against `base`), or `undefined` when it is no URL. */
53
+ const urlOf = (href, base) => {
54
+ try {
55
+ return new URL(href, base);
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ };
61
+ /**
62
+ * A web page's URL a media block may store or render: the link sanitizer's
63
+ * reading of it ({@link sanitizeLinkHref}: ASCII control characters and spaces stripped,
64
+ * tab/newline anywhere), then only an absolute `http:`/`https:` URL with a
65
+ * host. `null` for anything else: a script or `data:` URL, a `mailto:`, a
66
+ * relative path, a bare domain.
67
+ */
68
+ export const safeWebUrl = (value) => {
69
+ const href = hrefOf(value);
70
+ const url = href ? urlOf(href) : undefined;
71
+ return url && /^https?:$/.test(url.protocol) && url.host ? href : null;
72
+ };
73
+ /**
74
+ * A media file's source (a video, an audio track, an attachment): a
75
+ * {@link safeWebUrl}, or a `blob:` URL an upload answered. Never `data:`:
76
+ * an inline file would be stored in the document and sent in every update.
77
+ */
78
+ export const safeMediaSrc = (value) => {
79
+ const blob = typeof value === 'string' ? /^blob:(.*)$/is.exec(value.trim()) : null;
80
+ const origin = blob && safeWebUrl(blob[1]);
81
+ return origin ? `blob:${origin}` : safeWebUrl(value);
82
+ };
package/dist/utils.d.ts CHANGED
@@ -15,3 +15,5 @@ export declare class PreventionError extends Error {
15
15
  export declare const isPrevention: (error: unknown) => error is PreventionError;
16
16
  /** A hook's `prevent`: veto the gesture, and optionally run `cb` in its place. */
17
17
  export type Prevent = (cb?: () => void) => void;
18
+ /** The cleanups `call` answers for each of `items` (a hook answers a function or nothing). */
19
+ export declare const cleanups: <T>(items: readonly T[], call: (item: T) => unknown) => (() => void)[];
package/dist/utils.js CHANGED
@@ -39,3 +39,8 @@ export const vetoable = (call) => {
39
39
  throw veto;
40
40
  return out;
41
41
  };
42
+ /** The cleanups `call` answers for each of `items` (a hook answers a function or nothing). */
43
+ export const cleanups = (items, call) => items.flatMap((item) => {
44
+ const action = call(item);
45
+ return typeof action === 'function' ? [action] : [];
46
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "edytor",
3
3
  "license": "MIT",
4
- "version": "2.0.0",
4
+ "version": "3.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/beynar/edytor.git"
@@ -50,6 +50,9 @@
50
50
  "bench:browser": "node bench/browser.js",
51
51
  "bench:runs": "BENCH_RUNS=1 vitest --config ./vitest.crdt.config.ts --run src/tests/crdt/runs/runs.bench.test.ts",
52
52
  "size": "node scripts/bundle-size.mjs --by dir",
53
+ "registry:build": "shadcn-svelte registry build ./registry.json --output site/public/r",
54
+ "check:registry": "node scripts/check-registry.mjs",
55
+ "test:registry-install": "sh tests/registry-install/run.sh",
53
56
  "census": "node scripts/census.mjs"
54
57
  },
55
58
  "exports": {
@@ -66,6 +69,10 @@
66
69
  "types": "./dist/crdt/index.d.ts",
67
70
  "default": "./dist/crdt/index.js"
68
71
  },
72
+ "./export": {
73
+ "types": "./dist/export/index.d.ts",
74
+ "default": "./dist/export/index.js"
75
+ },
69
76
  "./protocol": {
70
77
  "types": "./dist/crdt/protocol.d.ts",
71
78
  "default": "./dist/crdt/protocol.js"
@@ -99,6 +106,9 @@
99
106
  "@cloudflare/vitest-plugin": "1.3.0",
100
107
  "@cloudflare/workers-types": "^5.20260927.1",
101
108
  "@eslint/js": "^10.0.1",
109
+ "@floating-ui/dom": "1.8.0",
110
+ "@internationalized/date": "^3.12.4",
111
+ "@lucide/svelte": "^1.56.0",
102
112
  "@playwright/test": "^1.59.1",
103
113
  "@sveltejs/adapter-static": "^3.0.10",
104
114
  "@sveltejs/kit": "^2.55.0",
@@ -110,6 +120,8 @@
110
120
  "@typescript-eslint/parser": "^8.58.0",
111
121
  "@vitest/ui": "^4.1.2",
112
122
  "@y/protocols": "1.0.6-rc.1",
123
+ "bits-ui": "^2.19.5",
124
+ "cn": "^0.4.0",
113
125
  "eslint": "^10.1.0",
114
126
  "eslint-config-prettier": "^10.1.8",
115
127
  "eslint-plugin-svelte": "^3.17.0",
@@ -117,12 +129,16 @@
117
129
  "globals": "^17.6.0",
118
130
  "jsdom": "^29.0.1",
119
131
  "katex": "0.18.7",
132
+ "linkedom": "^0.18.12",
120
133
  "prettier": "^3.8.1",
121
134
  "prettier-plugin-svelte": "^3.5.1",
122
135
  "publint": "^0.3.18",
136
+ "shadcn-svelte": "^1.7.0",
123
137
  "svelte": "5.55.1",
124
138
  "svelte-check": "^4.4.6",
139
+ "tailwind-variants": "^3.3.1",
125
140
  "tailwindcss": "^4.2.2",
141
+ "tw-animate-css": "^1.4.0",
126
142
  "typescript": "^5.9.3",
127
143
  "vite": "^8.0.3",
128
144
  "vitest": "^4.1.2",
@@ -148,6 +164,7 @@
148
164
  },
149
165
  "dependencies": {
150
166
  "@tanstack/highlight": "^0.1.0",
167
+ "@tanstack/markdown": "1.0.0",
151
168
  "esm-env": "^1.2.2",
152
169
  "lib0-v14": "npm:lib0@1.0.0-rc.32"
153
170
  }
@@ -1,3 +0,0 @@
1
- // Maximum possible z-index
2
- // https://stackoverflow.com/questions/491052/minimum-and-maximum-value-of-z-index
3
- export const maxZIndex = 2147483647;
@@ -1,16 +0,0 @@
1
- import { once } from '../public-utils/once.js';
2
-
3
- // Using `once` as the value won't change in a browser
4
-
5
- /**
6
- * Returns `true` if the browser supports the Popover API (top-layer).
7
- *
8
- * When supported, elements with `popover="manual"` can be promoted to the
9
- * browser's top layer via `element.showPopover()`, which is more reliable
10
- * than `z-index` for ensuring an element renders above everything else.
11
- *
12
- * @see https://developer.mozilla.org/en-US/docs/Web/API/Popover_API
13
- */
14
- export const supportsPopover = once(function supportsPopover() {
15
- return typeof HTMLElement !== 'undefined' && typeof HTMLElement.prototype.showPopover === 'function';
16
- });
@@ -1,3 +0,0 @@
1
- export function isAxisAllowed(axis, allowedAxis) {
2
- return allowedAxis === 'all' || axis === allowedAxis;
3
- }
@@ -1,25 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import type { LanguageMenu } from './languageMenu.svelte.js';
4
-
5
- /**
6
- * The code language list (`LanguageMenu`), in the overlay under the
7
- * language button of its block: the plugin's `menu` snippet, placed. Its
8
- * keys and ARIA are the controller's attachments (`keys`, `popup`,
9
- * `option(index)`).
10
- */
11
- let { menu, snippet }: { menu: LanguageMenu; snippet: Snippet<[LanguageMenu]> } = $props();
12
-
13
- const place = $derived(menu.open && menu.panel.place(4));
14
- </script>
15
-
16
- {#if place}
17
- <div
18
- data-edytor-code-language-panel
19
- style:position="absolute"
20
- style:left="{place.left}px"
21
- style:top="{place.top}px"
22
- >
23
- {@render snippet(menu)}
24
- </div>
25
- {/if}
@@ -1,9 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- import type { LanguageMenu } from './languageMenu.svelte.js';
3
- type $$ComponentProps = {
4
- menu: LanguageMenu;
5
- snippet: Snippet<[LanguageMenu]>;
6
- };
7
- declare const LanguageMenu: import("svelte").Component<$$ComponentProps, {}, "">;
8
- type LanguageMenu = ReturnType<typeof LanguageMenu>;
9
- export default LanguageMenu;
@@ -1,27 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import type { EquationEditor } from './equation.svelte.js';
4
-
5
- /**
6
- * The equation editor (`EquationEditor`), in the overlay under the edited
7
- * equation (Notion's), centered under a block one: the plugin's `panel`
8
- * snippet, placed. It takes the field's focus and keys through the
9
- * controller's attachment (`field`).
10
- */
11
- let { editor, panel }: { editor: EquationEditor; panel: Snippet<[EquationEditor]> } = $props();
12
-
13
- const place = $derived(editor.place);
14
- </script>
15
-
16
- {#if place}
17
- <div
18
- data-edytor-equation-panel
19
- data-display={editor.display ? 'block' : 'inline'}
20
- style:position="absolute"
21
- style:left="{place.left}px"
22
- style:top="{place.top}px"
23
- style:transform={editor.display ? 'translateX(-50%)' : undefined}
24
- >
25
- {@render panel(editor)}
26
- </div>
27
- {/if}
@@ -1,9 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- import type { EquationEditor } from './equation.svelte.js';
3
- type $$ComponentProps = {
4
- editor: EquationEditor;
5
- panel: Snippet<[EquationEditor]>;
6
- };
7
- declare const EquationEditor: import("svelte").Component<$$ComponentProps, {}, "">;
8
- type EquationEditor = ReturnType<typeof EquationEditor>;
9
- export default EquationEditor;
@@ -1,37 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import type { ImageToolbar } from './toolbar.svelte.js';
4
-
5
- /**
6
- * The image toolbar's place: the app's `toolbar` at the top right of the
7
- * image the chrome is on, while it shows and no handle drags.
8
- */
9
- let { toolbar, snippet }: { toolbar: ImageToolbar; snippet: Snippet<[ImageToolbar]> } = $props();
10
- const controls = $derived(toolbar.controls);
11
- </script>
12
-
13
- {#if controls.shown && controls.box && !controls.drag}
14
- {@const box = controls.box}
15
- <div
16
- data-edytor-image-toolbar-slot
17
- style:left="{box.x + box.width - 6}px"
18
- style:top="{box.y + 6}px"
19
- data-selecting={controls.selecting ? 'true' : undefined}
20
- >
21
- {@render snippet(toolbar)}
22
- </div>
23
- {/if}
24
-
25
- <style>
26
- /* The toolbar's place: the image's top right, sized to its markup. */
27
- [data-edytor-image-toolbar-slot] {
28
- position: absolute;
29
- width: max-content;
30
- transform: translateX(-100%);
31
- }
32
-
33
- /* A text selection in progress: the chrome takes no pointer. */
34
- [data-selecting='true'] {
35
- pointer-events: none;
36
- }
37
- </style>
@@ -1,9 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- import type { ImageToolbar } from './toolbar.svelte.js';
3
- type $$ComponentProps = {
4
- toolbar: ImageToolbar;
5
- snippet: Snippet<[ImageToolbar]>;
6
- };
7
- declare const ImageToolbarHost: import("svelte").Component<$$ComponentProps, {}, "">;
8
- type ImageToolbarHost = ReturnType<typeof ImageToolbarHost>;
9
- export default ImageToolbarHost;
@@ -1,26 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import type { CalloutIconPicker } from './calloutIcons.svelte.js';
4
-
5
- /**
6
- * A callout's icon picker (`callout.icon`), in the overlay under the icon:
7
- * the app's `picker` snippet, hidden until its first placement (never
8
- * `display: none`: it takes the focus as it mounts). Its keys and popup
9
- * are the controller's attachments (`keys`, `popup`) and rows (`option`).
10
- */
11
- let { picker, menu }: { picker: CalloutIconPicker; menu: Snippet<[CalloutIconPicker]> } =
12
- $props();
13
- const place = $derived(picker.isOpen && picker.panel.place(4));
14
- </script>
15
-
16
- {#if picker.isOpen}
17
- <div
18
- data-edytor-callout-icons
19
- style:position="absolute"
20
- style:left="{place ? place.left : 0}px"
21
- style:top="{place ? place.top : 0}px"
22
- style:opacity={place ? undefined : 0}
23
- >
24
- {@render menu(picker)}
25
- </div>
26
- {/if}
@@ -1,9 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- import type { CalloutIconPicker } from './calloutIcons.svelte.js';
3
- type $$ComponentProps = {
4
- picker: CalloutIconPicker;
5
- menu: Snippet<[CalloutIconPicker]>;
6
- };
7
- declare const CalloutIconPicker: import("svelte").Component<$$ComponentProps, {}, "">;
8
- type CalloutIconPicker = ReturnType<typeof CalloutIconPicker>;
9
- export default CalloutIconPicker;