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
@@ -1,6 +1,7 @@
1
1
  <svelte:options runes />
2
2
 
3
3
  <script module lang="ts">
4
+ import { pageLinkForms } from '../../export/forms.js';
4
5
  import './pageLink.css';
5
6
  import type { Snippet } from 'svelte';
6
7
  import type {
@@ -12,7 +13,7 @@
12
13
  import type { Edytor } from '../../edytor.svelte.js';
13
14
  import type { TextAnchor } from '../../selection/selection.svelte.js';
14
15
  import type { TriggerMenuController } from '../triggers/TriggerController.svelte.js';
15
- import { withRulesAndTriggers } from '../triggers/triggers.js';
16
+ import { placeAtom, withRulesAndTriggers } from '../triggers/triggers.js';
16
17
  import { pageLinkEnglish, labelsOf } from '../../labels.js';
17
18
  import { sanitizeLinkHref } from '../richtext/richTextOperations.js';
18
19
 
@@ -150,7 +151,7 @@
150
151
  prevent(() => {
151
152
  edytor.dispatcher.run('insertFromPaste', () => {
152
153
  edytor.insertText(text.parent.id, text.segStart + yStart, item);
153
- edytor.dispatcher.caret(text, yStart + item.length);
154
+ edytor.dispatcher.caretIn(text, yStart + item.length);
154
155
  });
155
156
  if (edytor.dispatcher.last?.status === 'applied')
156
157
  swapPasted(edytor, start, item, page, options);
@@ -175,11 +176,8 @@
175
176
  : {}),
176
177
  inlineBlocks: {
177
178
  pageLink: {
178
- snippet: pageLink,
179
- plain: (data) => {
180
- const title = (data as Partial<PageLinkData> | undefined)?.title;
181
- return typeof title === 'string' ? title : '';
182
- }
179
+ ...pageLinkForms(),
180
+ snippet: pageLink
183
181
  }
184
182
  }
185
183
  },
@@ -191,12 +189,7 @@
191
189
  label: (page: PageLinkItem) => page.title,
192
190
  key: (page: PageLinkItem) => page.id,
193
191
  menu: options.menu,
194
- onPick: (page: PageLinkItem, { edytor, block, from, caret }) => {
195
- const atom = atomOf(page, options);
196
- const written = edytor.insertInline(block.id, from, atom).status === 'applied';
197
- if (written) caret(from + 1);
198
- return written;
199
- }
192
+ onPick: (page: PageLinkItem, ctx) => placeAtom(ctx, atomOf(page, options))
200
193
  }
201
194
  ]
202
195
  }
@@ -11,28 +11,44 @@
11
11
  openLink,
12
12
  pastedLink,
13
13
  richTextOperations,
14
- sanitizeLinkHref,
15
- sanitizeCssColorValue,
16
14
  typedLink,
17
15
  type RichTextMark
18
16
  } from './richTextOperations.js';
19
17
  import { marksForInsertion } from '../../session/editing/text.js';
20
18
  import { firstUriListEntry } from '../../events/dataTransferPayload.js';
21
19
  import { flipToggles, shownSelectionBlocks } from '../../selection/replaceSelection.js';
22
- import { mediaKinds, richTextKinds, richTextMarks } from '../../crdt/semantics.js';
20
+ import { mediaKinds, richTextKinds } from '../../crdt/semantics.js';
23
21
  import { richTextEnglish, keywordsOf, labelsOf, type RichTextLabels } from '../../labels.js';
24
22
  import TodoCheckbox from './TodoCheckbox.svelte';
25
23
  import EmptyBody from './EmptyBody.svelte';
26
24
  import CalloutIcon from './CalloutIcon.svelte';
27
25
  import RichTextControl from './RichTextControl.svelte';
28
- import {
29
- DEFAULT_CALLOUT_ICON,
30
- calloutHtml,
31
- calloutIcons,
32
- calloutPickers,
33
- iconOf
34
- } from './callouts.js';
26
+ import { DEFAULT_CALLOUT_ICON, calloutIcons, calloutPickers, iconOf } from './callouts.js';
35
27
  import { listen } from '../../utils/listen.js';
28
+ import {
29
+ HEADING_LEVELS,
30
+ calloutRead,
31
+ calloutWrite,
32
+ dividerRead,
33
+ dividerWrite,
34
+ headingLevel,
35
+ headingRead,
36
+ headingWrite,
37
+ itemRead,
38
+ itemWrite,
39
+ numberedRead,
40
+ orderedRead,
41
+ orderedWrite,
42
+ quoteRead,
43
+ quoteWrite,
44
+ richTextMarkForms,
45
+ todoWrite,
46
+ toggleHeadingRead,
47
+ toggleHeadingWrite,
48
+ toggleRead,
49
+ unorderedRead,
50
+ unorderedWrite
51
+ } from '../../export/forms.js';
36
52
  import { onLastLine } from '../../session/navigation.js';
37
53
  import { bodyShows } from '../../selection/visibility.js';
38
54
  import { openBody } from './body.js';
@@ -58,62 +74,10 @@
58
74
  formatSubscript: 'subscript'
59
75
  } as const;
60
76
 
61
- const isNativeFormatInputType = (
62
- inputType: string
63
- ): inputType is keyof typeof nativeFormatMarks => inputType in nativeFormatMarks;
64
-
65
- /** A thematic break's clipboard forms. */
66
- const rule = { html: () => '<hr>', plain: () => '---' };
67
77
  /** A native disclosure: the browser owns `open` (declared view state). */
68
- const disclosure = { element: 'details', viewState: ['open'] };
78
+ const disclosure = { viewState: ['open'] };
69
79
  /** The kinds drawn as a disclosure: a toggle this view creates opens (`body.open`). */
70
80
  const DISCLOSURES = new Set(['toggle', 'toggle-heading']);
71
- /**
72
- * A list's elements without list semantics (`role="none"`): a list's items
73
- * are sibling blocks, never the only children of one `ul` or `ol`, so an
74
- * `li` would be an orphan list item to assistive technology (WCAG 1.3.1);
75
- * the tags stay for styles and the HTML export.
76
- */
77
- const unlisted = (tag: 'li' | 'ul' | 'ol') => ({ tag, attributes: { role: 'none' } });
78
- /**
79
- * Notion's three heading levels: a missing level is h1, any other (a
80
- * stored `h4`–`h6`, as HTML import reads them) h3.
81
- */
82
- const headingLevel = (level: unknown) =>
83
- level === 'h1' || level === 'h2' || level === 'h3' ? level : level === undefined ? 'h1' : 'h3';
84
- /** An unsafe scheme drops the href (and its target): an inert anchor still carries the text. */
85
- const linkAttributes = (mark: { href?: unknown; target?: string }) => {
86
- const href = sanitizeLinkHref(mark?.href) ?? undefined;
87
- return { href, target: href && mark.target };
88
- };
89
- /** A hostile color value (a `;` payload would inject declarations) drops the style. */
90
- const styled = (property: string) => (value: unknown) => {
91
- const safe = sanitizeCssColorValue(value);
92
- return { style: safe ? `${property}: ${safe};` : undefined };
93
- };
94
- /**
95
- * HTML import: a pasted color, sanitized, unless it is the page's
96
- * default (Google Docs writes black text and transparent backgrounds on
97
- * every span: they are no mark, and a black mark is unreadable in dark mode).
98
- */
99
- const colorOf = (value: string, none: RegExp) =>
100
- none.test(value.replace(/\s+/g, '').toLowerCase())
101
- ? undefined
102
- : (sanitizeCssColorValue(value) ?? undefined);
103
- const INHERITED = 'initial|inherit|unset|revert|currentcolor';
104
- const NO_COLOR = new RegExp(
105
- `^(${INHERITED}|black|windowtext|#000|#000000|rgba?\\(0,0,0(,1)?\\))$`
106
- );
107
- const NO_HIGHLIGHT = new RegExp(
108
- `^(${INHERITED}|transparent|rgba\\(\\d+,\\d+,\\d+,0\\)|white|#fff|#ffffff|rgba?\\(255,255,255(,1)?\\))$`
109
- );
110
- /** HTML import: a tag alias (unless its own style says otherwise) or a style (Google Docs). */
111
- const alias =
112
- (tags: RegExp, property: 'fontWeight' | 'fontStyle' | 'textDecoration', value: RegExp) =>
113
- (el: HTMLElement) =>
114
- (tags.test(el.localName) && !el.style[property]) ||
115
- value.test(el.style[property]) ||
116
- undefined;
117
81
 
118
82
  /**
119
83
  * The kinds whose empty text names them, by placeholder (a callout's
@@ -220,6 +184,7 @@
220
184
  { callout: calloutOptions, checkbox, calloutIcon, emptyBody }: RichTextPluginOptions
221
185
  ): ReturnType<Plugin> => {
222
186
  const { kinds, marks: words } = labels;
187
+ const marks = richTextMarkForms;
223
188
  /** A new callout's icon. */
224
189
  const icon = calloutOptions?.icon ?? DEFAULT_CALLOUT_ICON;
225
190
  /** The block this view is retyping into a disclosure (`body.open`), until it applied. */
@@ -317,7 +282,7 @@
317
282
  const end = yStart + href.length;
318
283
  edytor.dispatcher.run('insertFromPaste', () => {
319
284
  edytor.insertText(text.parent.id, text.segStart + yStart, href);
320
- edytor.dispatcher.caret(text, end);
285
+ edytor.dispatcher.caretIn(text, end);
321
286
  });
322
287
  if (text.stringContent.slice(yStart, end) === href)
323
288
  operations.linkText(text, yStart, end, href);
@@ -360,7 +325,7 @@
360
325
  } finally {
361
326
  autolinking = false;
362
327
  }
363
- edytor.dispatcher.caret(text, yStart + 1);
328
+ edytor.dispatcher.caretIn(text, yStart + 1);
364
329
  operations.linkText(text, found.start, end, found.href);
365
330
  });
366
331
  },
@@ -392,8 +357,8 @@
392
357
  },
393
358
  onBeforeInput: ({ e, prevent }) => {
394
359
  const { inputType, data } = e;
395
- if (isNativeFormatInputType(inputType)) {
396
- const mark = nativeFormatMarks[inputType];
360
+ if (inputType in nativeFormatMarks) {
361
+ const mark = nativeFormatMarks[inputType as keyof typeof nativeFormatMarks];
397
362
  prevent(() => operations.setMarkAtRange(mark));
398
363
  } else if (inputType === 'formatRemove') {
399
364
  prevent(() => operations.removeAllMarksAtRange());
@@ -418,54 +383,17 @@
418
383
  },
419
384
  // Toolbar buttons and export wrapping follow this order (first innermost).
420
385
  marks: {
421
- bold: {
422
- tag: 'strong',
423
- toolbar: { label: words.bold, icon: 'B' },
424
- parse: alias(/^b$/, 'fontWeight', /^(bold|[6-9]00)$/)
425
- },
426
- italic: {
427
- tag: 'em',
428
- toolbar: { label: words.italic, icon: 'I' },
429
- parse: alias(/^i$/, 'fontStyle', /italic/)
430
- },
431
- underline: {
432
- tag: 'u',
433
- toolbar: { label: words.underline, icon: 'U' },
434
- parse: alias(/^u$/, 'textDecoration', /underline/)
435
- },
436
- strike: {
437
- tag: 's',
438
- toolbar: { label: words.strike, icon: 'S' },
439
- parse: alias(/^(strike|del)$/, 'textDecoration', /line-through/)
440
- },
441
- code: { tag: 'code', toolbar: { label: words.code, icon: '</>' } },
442
- // Typing at a link's trailing edge extends it only from inside the anchor.
443
- link: {
444
- tag: 'a',
445
- attributes: linkAttributes,
446
- ...richTextMarks.link,
447
- parse: (el) => {
448
- const href = el.localName === 'a' && sanitizeLinkHref(el.getAttribute('href'));
449
- const target = el.getAttribute('target');
450
- return href ? { href, ...(target ? { target } : {}) } : undefined;
451
- }
452
- },
453
- superscript: { tag: 'sup' },
454
- subscript: { tag: 'sub' },
455
- color: {
456
- tag: 'span',
457
- attributes: styled('color'),
458
- parse: (el) => colorOf(el.style.color, NO_COLOR)
459
- },
460
- highlight: {
461
- tag: 'span',
462
- attributes: styled('background-color'),
463
- parse: (el) => {
464
- const background = el.style.backgroundColor;
465
- if (background) return colorOf(background, NO_HIGHLIGHT);
466
- return el.localName === 'mark' ? 'yellow' : undefined;
467
- }
468
- }
386
+ ...marks,
387
+ // The toolbar's marks, by their icons.
388
+ ...Object.fromEntries(
389
+ (['bold', 'italic', 'underline', 'strike', 'code'] as const).map((name, i) => [
390
+ name,
391
+ {
392
+ ...marks[name],
393
+ toolbar: { label: words[name], icon: ['B', 'I', 'U', 'S', '</>'][i]! }
394
+ }
395
+ ])
396
+ )
469
397
  },
470
398
  blocks: {
471
399
  paragraph: {
@@ -475,42 +403,25 @@
475
403
  ])
476
404
  },
477
405
  heading: {
406
+ ...headingRead,
407
+ ...headingWrite,
478
408
  snippet: textThenChildren,
479
- contentElement: (data) => headingLevel(data.level),
480
- presets: presets('heading', [
481
- {
482
- label: kinds.heading1,
483
- icon: 'H₁',
484
- keywords: ['h1', 'title'],
485
- data: { level: 'h1' },
486
- markdown: ['# ']
487
- },
488
- {
489
- label: kinds.heading2,
490
- icon: 'H₂',
491
- keywords: ['h2', 'subtitle'],
492
- data: { level: 'h2' },
493
- markdown: ['## ']
494
- },
495
- {
496
- label: kinds.heading3,
497
- icon: 'H₃',
498
- keywords: ['h3'],
499
- data: { level: 'h3' },
500
- markdown: ['### ']
501
- }
502
- ]),
503
- // HTML import: h1–h3 come from the presets; h4–h6 read as h3.
504
- parse: (el) => (/^h[4-6]$/.test(el.localName) ? { level: 'h3' } : undefined),
505
- html: (block, content, children) => {
506
- const tag = headingLevel(block.data?.level);
507
- return `<${tag}>${content}</${tag}>${children}`;
508
- }
409
+ presets: presets(
410
+ 'heading',
411
+ HEADING_LEVELS.map((level, index) => ({
412
+ label: [kinds.heading1, kinds.heading2, kinds.heading3][index]!,
413
+ icon: `H${['₁', '₂', '₃'][index]}`,
414
+ keywords: [level, ...[['title'], ['subtitle'], []][index]!],
415
+ data: { level },
416
+ markdown: [`${'#'.repeat(index + 1)} `]
417
+ }))
418
+ )
509
419
  },
510
420
  'bulleted-list-item': {
421
+ ...itemRead,
422
+ ...itemWrite,
511
423
  continues: true,
512
424
  snippet: listItem,
513
- element: unlisted('li'),
514
425
  presets: presets('bulleted-list-item', [
515
426
  {
516
427
  label: kinds.bulletedList,
@@ -518,13 +429,13 @@
518
429
  keywords: ['bullet', 'ul'],
519
430
  markdown: ['- ', '* ', '+ ']
520
431
  }
521
- ]),
522
- html: 'li'
432
+ ])
523
433
  },
524
434
  'numbered-list-item': {
435
+ ...numberedRead,
436
+ ...itemWrite,
525
437
  continues: true,
526
438
  snippet: listItem,
527
- element: unlisted('li'),
528
439
  presets: presets('numbered-list-item', [
529
440
  {
530
441
  label: kinds.numberedList,
@@ -532,13 +443,10 @@
532
443
  keywords: ['number', 'ol'],
533
444
  markdown: ['1. ', 'a. ', 'i. ']
534
445
  }
535
- ]),
536
- html: 'li',
537
- // HTML import: an `li` is a bulleted item (the first `li` kind) unless its list is ordered.
538
- parse: (el) =>
539
- el.localName === 'li' && el.parentElement?.localName === 'ol' ? {} : undefined
446
+ ])
540
447
  },
541
448
  'todo-item': {
449
+ ...todoWrite,
542
450
  continues: true,
543
451
  // The app's control markup, if any, through its own snippet: the default costs no branch.
544
452
  snippet: checkbox ? todoItemOwn : todoItem,
@@ -550,15 +458,10 @@
550
458
  data: { checked: false },
551
459
  markdown: ['[ ] ', '[] ']
552
460
  }
553
- ]),
554
- html: (block, content, children) =>
555
- `<li data-edytor-todo-item="true"><input type="checkbox"${block.data?.checked === true ? ' checked' : ''}>${content}${children}</li>`,
556
- plain: (block, content, children) =>
557
- [`${block.data?.checked === true ? '[x]' : '[ ]'} ${content}`.trim(), children]
558
- .filter(Boolean)
559
- .join('\n')
461
+ ])
560
462
  },
561
463
  toggle: {
464
+ ...toggleRead,
562
465
  continues: true,
563
466
  container: true,
564
467
  snippet: emptyBody ? detailsOwn : details,
@@ -577,13 +480,14 @@
577
480
  // kept); from a toggle, `#`, `##` or `###` + space; so `> # ` typed on a new
578
481
  // line is a toggle heading 1.
579
482
  'toggle-heading': {
483
+ ...toggleHeadingRead,
484
+ ...toggleHeadingWrite,
580
485
  container: true,
581
486
  snippet: emptyBody ? detailsOwn : details,
582
487
  ...disclosure,
583
- contentElement: (data) => headingLevel(data.level),
584
488
  presets: presets(
585
489
  'toggle-heading',
586
- (['h1', 'h2', 'h3'] as const).map((level, index) => {
490
+ HEADING_LEVELS.map((level, index) => {
587
491
  const hashes = '#'.repeat(index + 1);
588
492
  return {
589
493
  label: [kinds.toggleHeading1, kinds.toggleHeading2, kinds.toggleHeading3][index]!,
@@ -597,28 +501,21 @@
597
501
  }
598
502
  };
599
503
  })
600
- ),
601
- // HTML import: a `details` whose summary holds an h1–h3 (its own export).
602
- parse: (el) => {
603
- if (el.localName !== 'details') return;
604
- const tag = el.querySelector(':scope > summary > :is(h1, h2, h3, h4, h5, h6)');
605
- return tag ? { level: headingLevel(tag.localName) } : undefined;
606
- },
607
- html: (block, content, children) => {
608
- const tag = headingLevel(block.data?.level);
609
- return `<details><summary><${tag}>${content}</${tag}></summary>${children}</details>`;
610
- }
504
+ )
611
505
  },
612
506
  // Notion's callout: an icon (`data.icon`, the picker's), a title (its own
613
507
  // text) and a content (its children, any blocks), which shows even empty
614
508
  // (`body`): Enter at the end of the title goes into it.
615
509
  callout: {
510
+ ...calloutRead,
616
511
  container: true,
617
512
  body: true,
618
513
  snippet: calloutIcon || emptyBody ? calloutOwn : callout,
619
514
  presets: presets('callout', [
620
515
  { label: kinds.callout, icon: '✦', keywords: ['note', 'tip'], data: { icon } }
621
516
  ]),
517
+ // A callout without its own icon exports the view's.
518
+ ...calloutWrite((data) => iconOf(data, edytor)),
622
519
  // The block menu's Change icon, while the view lists a picker: the keyboard's way to it.
623
520
  menu: (block) => {
624
521
  const picker = calloutPickers.get(edytor);
@@ -632,47 +529,38 @@
632
529
  }
633
530
  ]
634
531
  : [];
635
- },
636
- // HTML import: its own export, the icon in its attribute.
637
- parse: (el) => {
638
- const own = el.getAttribute('data-edytor-callout');
639
- return own === null ? undefined : { icon: own };
640
- },
641
- html: (block, content, children) =>
642
- calloutHtml(iconOf(block.data, edytor), content, children)
532
+ }
643
533
  },
644
534
  quote: {
535
+ ...quoteRead,
536
+ ...quoteWrite,
645
537
  container: true,
646
538
  snippet: textThenChildren,
647
- contentElement: 'blockquote',
648
539
  // Notion: `"` + space is a quote; `>` + space is a toggle.
649
- presets: presets('quote', [{ label: kinds.quote, icon: '❝', markdown: ['" '] }]),
650
- html: 'blockquote'
540
+ presets: presets('quote', [{ label: kinds.quote, icon: '❝', markdown: ['" '] }])
651
541
  },
652
542
  divider: {
653
543
  ...richTextKinds.divider,
654
- element: 'hr',
544
+ ...dividerRead,
545
+ ...dividerWrite,
655
546
  presets: presets('divider', [
656
547
  { label: kinds.divider, icon: '—', keywords: ['hr', 'separator'], markdown: ['---'] }
657
548
  ]),
658
- empty: { content: [], children: [] },
659
- ...rule
549
+ empty: { content: [], children: [] }
660
550
  },
661
551
  'ordered-list': {
662
552
  ...richTextKinds['ordered-list'],
663
- itemKind: 'numbered-list-item',
664
- snippet: list,
665
- element: unlisted('ol'),
666
- html: 'ol'
553
+ ...orderedRead,
554
+ ...orderedWrite,
555
+ snippet: list
667
556
  },
668
557
  'unordered-list': {
669
558
  ...richTextKinds['unordered-list'],
670
- itemKind: 'bulleted-list-item',
671
- snippet: list,
672
- element: unlisted('ul'),
673
- html: 'ul'
559
+ ...unorderedRead,
560
+ ...unorderedWrite,
561
+ snippet: list
674
562
  },
675
- 'list-item': { snippet: listItem, element: unlisted('li'), html: 'li' }
563
+ 'list-item': { ...itemRead, ...itemWrite, snippet: listItem }
676
564
  }
677
565
  };
678
566
  };
@@ -10,7 +10,7 @@ export const openBody = (block) => {
10
10
  return edytor.dispatcher.run('openBody', () => {
11
11
  const added = () => edytor.insertBlocks({ parent: block.id, index: 0 }, [{ type: edytor.defaultChild(block) }]);
12
12
  const child = block.children[0] ?? edytor.idToBlock.get(added().ids[0] ?? '');
13
- edytor.dispatcher.caret(child?.firstText, 0);
13
+ edytor.dispatcher.caretIn(child?.firstText, 0);
14
14
  return child ?? undefined;
15
15
  });
16
16
  };
@@ -40,6 +40,8 @@ export declare class CalloutIconPicker extends ChromeList {
40
40
  constructor(edytor: Edytor,
41
41
  /** The picker's choices. */
42
42
  icons?: readonly string[]);
43
+ /** Where the picker stands: the open callout's icon (`ChromeAnchor`), which your `picker` places itself under. */
44
+ get anchor(): import("../chrome.js").ChromeAnchor;
43
45
  /** Whether the picker is open (reactive). */
44
46
  get isOpen(): boolean;
45
47
  /** The callout it is open on (reactive). */
@@ -48,8 +50,6 @@ export declare class CalloutIconPicker extends ChromeList {
48
50
  get current(): string | undefined;
49
51
  /** The keyboard's rows: the icons, then Remove icon. */
50
52
  protected get count(): number;
51
- /** The view is readonly (reactive): the picker closes. */
52
- get readonly(): boolean;
53
53
  /** Open the picker on `block`, or close it when it is open there. */
54
54
  toggle: (block: Block) => void;
55
55
  /** Open the picker on the callout `block`, its icon the keyboard's row (refused in a view that may not write). */
@@ -14,7 +14,6 @@ import { takeKeys } from '../../events/onFocus.js';
14
14
  import { ChromeList, holdKeys, menuKey, optionAttachment } from '../list.svelte.js';
15
15
  import { Anchored } from '../anchored.svelte.js';
16
16
  import { calloutPickers, iconOf } from './callouts.js';
17
- import CalloutIconHost from './CalloutIconPicker.svelte';
18
17
  import { all, listen } from '../../utils/listen.js';
19
18
  /** The picker's choices unless `createCalloutIconsPlugin({ icons })` names others. */
20
19
  export const CALLOUT_ICONS = [
@@ -89,6 +88,10 @@ export class CalloutIconPicker extends ChromeList {
89
88
  return icon ?? node ?? null;
90
89
  }, () => this.close(false));
91
90
  }
91
+ /** Where the picker stands: the open callout's icon (`ChromeAnchor`), which your `picker` places itself under. */
92
+ get anchor() {
93
+ return this.panel.anchor;
94
+ }
92
95
  /** Whether the picker is open (reactive). */
93
96
  get isOpen() {
94
97
  return this.target !== null;
@@ -107,10 +110,6 @@ export class CalloutIconPicker extends ChromeList {
107
110
  get count() {
108
111
  return this.icons.length + 1;
109
112
  }
110
- /** The view is readonly (reactive): the picker closes. */
111
- get readonly() {
112
- return this.edytor.readonly;
113
- }
114
113
  /** Open the picker on `block`, or close it when it is open there. */
115
114
  toggle = (block) => (this.target === block.id ? this.close() : this.open(block));
116
115
  /** Open the picker on the callout `block`, its icon the keyboard's row (refused in a view that may not write). */
@@ -129,7 +128,6 @@ export class CalloutIconPicker extends ChromeList {
129
128
  return;
130
129
  const before = this.#before;
131
130
  this.target = null;
132
- this.panel.box = null;
133
131
  this.#before = null;
134
132
  this.edytor.overlay.invalidate();
135
133
  if (!keys)
@@ -209,6 +207,6 @@ export const createCalloutIconsPlugin = ({ picker: menu, icons } = {}) => (edyto
209
207
  calloutPickers.set(edytor, picker);
210
208
  return {
211
209
  // With the menus: above the handles and the image chrome.
212
- onEdytorAttached: ({ node }) => menu && picker.panel.mount(node, CalloutIconHost, { picker, menu }, 70)
210
+ onEdytorAttached: ({ node }) => menu && picker.panel.mount(node, menu, picker, () => picker.isOpen, 70)
213
211
  };
214
212
  };
@@ -6,25 +6,7 @@ export type RichTextLink = {
6
6
  href: string;
7
7
  target?: string;
8
8
  };
9
- /**
10
- * CSS-color sanitizer for `color`/`highlight` marks. The value lands in
11
- * `style="color: {value}"` — a `;`/`{`/`}` payload escapes the property
12
- * and injects arbitrary declarations (`url()` exfil, `position:fixed`
13
- * overlays). Metacharacters, escape/obfuscation sequences, and
14
- * resource-loading functions are rejected; real `<color>` values
15
- * (`#hex`, `rgb()/hsl()/oklch()/color-mix()/var()`, named colors) pass.
16
- */
17
- export declare const sanitizeCssColorValue: (value: unknown) => string | null;
18
- /**
19
- * Scheme allowlist for link hrefs. `javascript:`/`data:`/`vbscript:`
20
- * payloads can arrive through native `insertLink`, pasted HTML, or a
21
- * malicious collaborator — and Svelte renders `href` verbatim. The href is
22
- * read as the browser's URL parser reads it: it strips C0 controls and
23
- * spaces at both ends and tab/newline/CR anywhere (`\u0001javascript:` is
24
- * `javascript:` to a browser), then resolves it against a base; only an
25
- * allowed scheme, or a relative href that stays relative, passes.
26
- */
27
- export declare const sanitizeLinkHref: (href: unknown) => string | null;
9
+ export { sanitizeCssColorValue, sanitizeLinkHref } from '../../utils/sanitize.js';
28
10
  /**
29
11
  * The URL a paste carries when its plain text is one link and nothing else
30
12
  * (surrounding whitespace aside): `http:`/`https:` with a host, or
@@ -3,54 +3,8 @@ import { holdsNothing, placing } from '../../kinds.js';
3
3
  import { id } from '../../utils.js';
4
4
  import { marksForInsertion } from '../../session/editing/text.js';
5
5
  import { selectedTextSpans, viewOf } from '../../selection/visibility.js';
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
- // Marks arrive from untrusted sources too (synced peers, paste) —
37
- // reject non-strings outright instead of throwing on `.replace`.
38
- if (typeof href !== 'string')
39
- return null;
40
- // eslint-disable-next-line no-control-regex
41
- const trimmed = href.replace(/[\t\n\r]/g, '').replace(/^[\u0000-\u0020]+|[\u0000-\u0020]+$/g, '');
42
- if (!trimmed)
43
- return null;
44
- try {
45
- // A relative href resolves to the base's `https:`: it names no scheme of its own.
46
- return SAFE_LINK_PROTOCOLS.has(new URL(trimmed, 'https://relative.invalid/').protocol)
47
- ? trimmed
48
- : null;
49
- }
50
- catch {
51
- return null;
52
- }
53
- };
6
+ export { sanitizeCssColorValue, sanitizeLinkHref } from '../../utils/sanitize.js';
7
+ import { sanitizeCssColorValue, sanitizeLinkHref } from '../../utils/sanitize.js';
54
8
  /**
55
9
  * The URL a paste carries when its plain text is one link and nothing else
56
10
  * (surrounding whitespace aside): `http:`/`https:` with a host, or
@@ -194,7 +148,7 @@ export const richTextOperations = (edytor) => ({
194
148
  if (dispatcher.plan('insertDivider', {}, prepare, block).status !== 'applied')
195
149
  return null;
196
150
  const text = caret ? edytor.idToBlock.get(caret)?.firstText : startText;
197
- dispatcher.caret(text, caret ? 0 : yStart);
151
+ dispatcher.caretIn(text, caret ? 0 : yStart);
198
152
  return block;
199
153
  },
200
154
  removeAllMarksAtRange: () => {
@@ -19,6 +19,8 @@ export declare class SlashMenuController extends TextTriggerController {
19
19
  /** The selection when a `+` opened the menu, given back when it closes unpicked. */
20
20
  private held;
21
21
  constructor(edytor: Edytor);
22
+ /** A `+`'s menu stands under the `+` (its block while the `+` is gone); a `/` menu at the caret. */
23
+ protected anchorRect(): DOMRect | undefined;
22
24
  /** A `+`'s menu names its highlighted row from its own field, and tells the `+` it opened it. */
23
25
  protected popupOf(active: string | undefined): Popup;
24
26
  /**