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
@@ -0,0 +1,585 @@
1
+ /**
2
+ * A plain or markdown string as blocks — AI output, a suggestion's string
3
+ * content (`edytor.suggestions`), a markdown file. Pure: no view, no
4
+ * document, no DOM; Worker-safe.
5
+ *
6
+ * Plain (`markdown: false`): blank lines split paragraphs; a single newline
7
+ * stays in the text. Markdown (the default) is parsed by
8
+ * `@tanstack/markdown` (`parseMarkdown`, its serializable AST) and the AST
9
+ * mapped to blocks: headings (deeper than `###` read as `###`), bulleted,
10
+ * numbered and to-do items nested as the list nests, quotes (their first
11
+ * paragraph their text), fenced code (its language in `data.language`),
12
+ * GFM tables (a table whose first row is a header), dividers, a paragraph
13
+ * holding only an image an image, and paragraphs (a soft break a space, a
14
+ * hard break a newline). Inline: bold, italic, strike, code, links
15
+ * (`[text](url)`, `<https://…>`; an href through the link sanitizer: a
16
+ * `javascript:` URL is its text), nested. Raw HTML is never trusted: it
17
+ * stays text.
18
+ *
19
+ * Edytor's own syntax is a TanStack extension each, not a second parser:
20
+ * a GitHub alert (`> [!NOTE]`) is a callout (a leading emoji its icon),
21
+ * `<details>`/`<summary>` a toggle, `$$ … $$` an equation, an autolink a
22
+ * link, and, given `components`, an MDX-lite tag (`<Endpoint id="x" />`)
23
+ * a block of the kind it names.
24
+ *
25
+ * `streaming: true` reads a text that is still arriving (a model's answer
26
+ * so far): the last line, while it has no newline yet, is held back while
27
+ * it is only a block marker (`#`, `-`, `1.`, a table row, a tag being
28
+ * typed), and from an inline marker it has not closed yet (`**bol`); a
29
+ * table's header row waits for its separator row; then TanStack's streaming
30
+ * profile drops a trailing empty heading or list item. Each call then
31
+ * shows only what will not change kind or marks when more arrives.
32
+ */
33
+ import { parseMarkdown } from '@tanstack/markdown/parser';
34
+ import { streamingMarkdownExtension } from '@tanstack/markdown/extensions/streaming';
35
+ import { sanitizeLinkHref } from '../utils/sanitize.js';
36
+ import { safeImageSrc } from '../plugins/image/image.js';
37
+ import { tableBlock } from '../crdt/tables.js';
38
+ import { headingLevel, languageOf } from './forms.js';
39
+ /** The data a component node carries, typed (a node's `attributes` are strings). */
40
+ const dataOf = new WeakMap();
41
+ /** The components of Edytor's own syntax: their names cannot be a tag's (`:`). */
42
+ const CALLOUT = 'edytor:callout';
43
+ const TOGGLE = 'edytor:toggle';
44
+ const EQUATION = 'edytor:equation';
45
+ const component = (name, data, children) => {
46
+ const node = {
47
+ type: 'component',
48
+ name,
49
+ attributes: Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v)])),
50
+ children
51
+ };
52
+ dataOf.set(node, data);
53
+ return node;
54
+ };
55
+ /** The lines from `from` up to the closing line `close` matches (nesting by `open`), and where it is. */
56
+ const blockUntil = (lines, from, close, open) => {
57
+ let depth = 0;
58
+ for (let i = from; i < lines.length; i++) {
59
+ const line = lines[i];
60
+ if (close.test(line)) {
61
+ if (depth === 0)
62
+ return { body: lines.slice(from, i), end: i };
63
+ depth--;
64
+ }
65
+ else if (open?.test(line))
66
+ depth++;
67
+ }
68
+ return { body: lines.slice(from), end: -1 };
69
+ };
70
+ /** `<https://…>` and `<mailto:…>` autolinks (the parser keeps them as text). */
71
+ const autolinks = {
72
+ name: 'edytor-autolinks',
73
+ inlineParser: {
74
+ markers: '<',
75
+ parse(context) {
76
+ if (context.inLink)
77
+ return undefined;
78
+ const auto = /^<((?:https?|mailto):[^\s<>]+)>/i.exec(context.source.slice(context.index));
79
+ if (!auto)
80
+ return undefined;
81
+ return {
82
+ node: { type: 'link', href: auto[1], children: [{ type: 'text', value: auto[1] }] },
83
+ length: auto[0].length
84
+ };
85
+ }
86
+ }
87
+ };
88
+ const ALERT = /^ {0,3}>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$/i;
89
+ const ICON = /^(\p{Extended_Pictographic}(?:️|‍\p{Extended_Pictographic}|\p{Emoji_Modifier})*)\s+/u;
90
+ /** A GitHub alert as a callout, a leading emoji on its first line its icon. */
91
+ const callouts = {
92
+ name: 'edytor-callouts',
93
+ parseBlock(context) {
94
+ if (!ALERT.test(context.lines[context.index] ?? ''))
95
+ return undefined;
96
+ const body = [];
97
+ let i = context.index + 1;
98
+ for (; i < context.lines.length; i++) {
99
+ const quoted = /^ {0,3}>\s?(.*)$/.exec(context.lines[i]);
100
+ if (!quoted)
101
+ break;
102
+ body.push(quoted[1]);
103
+ }
104
+ context.consume(i - context.index);
105
+ const icon = ICON.exec(body[0] ?? '');
106
+ if (icon)
107
+ body[0] = body[0].slice(icon[0].length);
108
+ return component(CALLOUT, icon ? { icon: icon[1] } : {}, context.parseBlocks(body.join('\n')));
109
+ }
110
+ };
111
+ /** A to-do box without its bullet (`[ ] task`) as a to-do item, as the editor's paste reads it. */
112
+ const bareTodos = {
113
+ name: 'edytor-bare-todos',
114
+ parseBlock(context) {
115
+ const todo = /^\[([ xX])\]\s+(.*)$/.exec(context.lines[context.index] ?? '');
116
+ if (!todo)
117
+ return undefined;
118
+ context.consume(1);
119
+ const children = [{ type: 'paragraph', children: context.parseInline(todo[2]) }];
120
+ return {
121
+ type: 'list',
122
+ ordered: false,
123
+ items: [{ type: 'listItem', checked: todo[1] !== ' ', children }]
124
+ };
125
+ }
126
+ };
127
+ const DETAILS = /^\s*<details>\s*$/i;
128
+ const DETAILS_END = /^\s*<\/details>\s*$/i;
129
+ const SUMMARY = /^\s*<summary>(?:<(h[1-6])>)?(.*?)(?:<\/h[1-6]>)?<\/summary>\s*$/i;
130
+ /** `<details>` and its `<summary>` as a toggle (a `<summary><h2>` a toggle heading). */
131
+ const details = {
132
+ name: 'edytor-details',
133
+ parseBlock(context) {
134
+ if (!DETAILS.test(context.lines[context.index] ?? ''))
135
+ return undefined;
136
+ const { body, end } = blockUntil(context.lines, context.index + 1, DETAILS_END, DETAILS);
137
+ context.consume((end === -1 ? context.lines.length : end + 1) - context.index);
138
+ const first = body.findIndex((line) => line.trim());
139
+ const summary = first === -1 ? null : SUMMARY.exec(body[first]);
140
+ const level = summary?.[1]?.toLowerCase();
141
+ const data = level ? { level: headingLevel(level) } : {};
142
+ const head = summary
143
+ ? [{ type: 'paragraph', children: context.parseInline(summary[2]) }]
144
+ : [];
145
+ if (summary)
146
+ body.splice(first, 1);
147
+ if (summary)
148
+ data.summary = true;
149
+ return component(TOGGLE, data, [...head, ...context.parseBlocks(body.join('\n'))]);
150
+ }
151
+ };
152
+ /** `$$` … `$$` lines as an equation (an unclosed one runs to the end: a stream cut mid-block). */
153
+ const equations = {
154
+ name: 'edytor-equations',
155
+ parseBlock(context) {
156
+ if (!/^\s*\$\$\s*$/.test(context.lines[context.index] ?? ''))
157
+ return undefined;
158
+ const { body, end } = blockUntil(context.lines, context.index + 1, /^\s*\$\$\s*$/);
159
+ context.consume((end === -1 ? context.lines.length : end + 1) - context.index);
160
+ return component(EQUATION, { expression: body.join('\n').trim() }, []);
161
+ }
162
+ };
163
+ /** The tag opening `line` (`<Name a="x" b={1} c>`), its literal attributes as data. */
164
+ const openingTag = (line) => {
165
+ const head = /^\s*<([A-Z][\w.]*)/.exec(line);
166
+ if (!head)
167
+ return null;
168
+ const name = head[1];
169
+ const data = {};
170
+ let i = head[0].length;
171
+ for (;;) {
172
+ const space = /^\s*/.exec(line.slice(i))[0];
173
+ i += space.length;
174
+ if (line.startsWith('/>', i))
175
+ return { name, data, selfClosing: true, rest: line.slice(i + 2).trim() };
176
+ if (line[i] === '>')
177
+ return { name, data, selfClosing: false, rest: line.slice(i + 1) };
178
+ const attr = space || i === head[0].length ? /^[A-Za-z_][\w:-]*/.exec(line.slice(i)) : null;
179
+ if (!attr)
180
+ return { name, issue: 'expression' };
181
+ i += attr[0].length;
182
+ if (line[i] !== '=') {
183
+ data[attr[0]] = true;
184
+ continue;
185
+ }
186
+ i++;
187
+ const quote = line[i];
188
+ if (quote === '"' || quote === "'") {
189
+ const close = line.indexOf(quote, i + 1);
190
+ if (close === -1)
191
+ return { name, issue: 'expression' };
192
+ data[attr[0]] = line.slice(i + 1, close);
193
+ i = close + 1;
194
+ continue;
195
+ }
196
+ const literal = /^\{\s*(true|false|-?\d+(?:\.\d+)?)\s*\}/.exec(line.slice(i));
197
+ if (!literal)
198
+ return { name, issue: 'expression' };
199
+ data[attr[0]] =
200
+ literal[1] === 'true' ? true : literal[1] === 'false' ? false : Number(literal[1]);
201
+ i += literal[0].length;
202
+ }
203
+ };
204
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
205
+ /**
206
+ * MDX-lite: a tag `components` names as a component node; any other tag,
207
+ * an expression, an import or an export declines (the parser keeps the
208
+ * line as text) and is reported.
209
+ */
210
+ const mdx = (components, report, streaming) => ({
211
+ name: 'edytor-mdx',
212
+ parseBlock(context) {
213
+ const line = context.lines[context.index] ?? '';
214
+ const statement = /^(import|export)\s/.exec(line);
215
+ if (statement) {
216
+ report({ reason: statement[1], source: line });
217
+ return undefined;
218
+ }
219
+ const tag = openingTag(line);
220
+ if (!tag)
221
+ return undefined;
222
+ if (!Object.hasOwn(components, tag.name)) {
223
+ report({ reason: 'component', name: tag.name, source: line });
224
+ return undefined;
225
+ }
226
+ if ('issue' in tag) {
227
+ report({ reason: tag.issue, name: tag.name, source: line });
228
+ return undefined;
229
+ }
230
+ const closing = `</${tag.name}>`;
231
+ if (tag.selfClosing || tag.rest.trimEnd().endsWith(closing)) {
232
+ if (tag.selfClosing && tag.rest)
233
+ return undefined;
234
+ context.consume(1);
235
+ const text = tag.selfClosing ? '' : tag.rest.trimEnd().slice(0, -closing.length).trim();
236
+ const children = text
237
+ ? [{ type: 'paragraph', children: context.parseInline(text) }]
238
+ : [];
239
+ return component(tag.name, tag.data, children);
240
+ }
241
+ if (tag.rest.trim()) {
242
+ report({ reason: 'unclosed', name: tag.name, source: line });
243
+ return undefined;
244
+ }
245
+ const name = escapeRegExp(tag.name);
246
+ const { body, end } = blockUntil(context.lines, context.index + 1, new RegExp(`^\\s*</${name}>\\s*$`), new RegExp(`^\\s*<${name}(?:\\s[^>]*)?>\\s*$`));
247
+ if (end === -1 && !streaming) {
248
+ report({ reason: 'unclosed', name: tag.name, source: line });
249
+ return undefined;
250
+ }
251
+ context.consume((end === -1 ? context.lines.length : end + 1) - context.index);
252
+ return component(tag.name, tag.data, context.parseBlocks(body.join('\n')));
253
+ }
254
+ });
255
+ /** Inline nodes as text runs, `marks` on every run (nested marks add up), equal runs merged. */
256
+ const inline = (nodes, cell = false) => {
257
+ const out = [];
258
+ const push = (value, marks) => {
259
+ if (!value)
260
+ return;
261
+ const last = out.at(-1);
262
+ if (last && JSON.stringify(last.marks ?? {}) === JSON.stringify(marks))
263
+ last.text += value;
264
+ else
265
+ out.push(Object.keys(marks).length ? { text: value, marks } : { text: value });
266
+ };
267
+ const walk = (nodes, marks) => {
268
+ for (const node of nodes)
269
+ switch (node.type) {
270
+ case 'text': {
271
+ const text = node.value.replace(/\n/g, ' ');
272
+ push(cell ? text.replace(/<br\s*\/?>/gi, '\n') : text, marks);
273
+ break;
274
+ }
275
+ case 'inlineCode':
276
+ push(node.value, { ...marks, code: true });
277
+ break;
278
+ case 'strong':
279
+ walk(node.children, { ...marks, bold: true });
280
+ break;
281
+ case 'emphasis':
282
+ walk(node.children, { ...marks, italic: true });
283
+ break;
284
+ case 'strike':
285
+ walk(node.children, { ...marks, strike: true });
286
+ break;
287
+ case 'link': {
288
+ const href = sanitizeLinkHref(node.href);
289
+ walk(node.children, href ? { ...marks, link: { href } } : marks);
290
+ break;
291
+ }
292
+ case 'image':
293
+ push(node.alt, marks);
294
+ break;
295
+ case 'break':
296
+ push('\n', marks);
297
+ break;
298
+ case 'inlineHtml':
299
+ push(node.value, marks);
300
+ break;
301
+ case 'footnoteReference':
302
+ push(`[^${node.id}]`, marks);
303
+ break;
304
+ case 'inlineComponent':
305
+ walk(node.children, marks);
306
+ break;
307
+ }
308
+ };
309
+ walk(nodes, {});
310
+ const last = out.at(-1);
311
+ if (last && !last.marks)
312
+ last.text = last.text.replace(/ +$/, '');
313
+ if (last && !last.text)
314
+ out.pop();
315
+ if (!out.length)
316
+ out.push({ text: '' });
317
+ return out;
318
+ };
319
+ /** A container's blocks as its own text (a leading paragraph's) and its children. */
320
+ const split = (blocks, reading) => {
321
+ const first = blocks[0]?.type === reading.type ? blocks.shift() : undefined;
322
+ return { content: first?.content ?? [], children: blocks };
323
+ };
324
+ const withChildren = (block, children) => children.length ? { ...block, children } : block;
325
+ /** AST blocks as Edytor blocks. */
326
+ const blocks = (nodes, reading) => nodes.flatMap((node) => block(node, reading));
327
+ const block = (node, reading) => {
328
+ switch (node.type) {
329
+ case 'heading':
330
+ return [
331
+ {
332
+ type: 'heading',
333
+ data: { level: `h${Math.min(3, node.depth)}` },
334
+ content: inline(node.children)
335
+ }
336
+ ];
337
+ case 'paragraph': {
338
+ const only = node.children.filter((child) => child.type !== 'text' || child.value.trim());
339
+ const image = only.length === 1 && only[0].type === 'image' ? only[0] : null;
340
+ // A safe source, or a relative one (a markdown file's own `./cover.png`).
341
+ const src = image && (safeImageSrc(image.src) ?? (/^[^:]*$/.test(image.src) && image.src));
342
+ if (image && src)
343
+ return [
344
+ {
345
+ type: 'image',
346
+ data: { src, ...(image.alt && { alt: image.alt }) },
347
+ content: image.title ? [{ text: image.title }] : []
348
+ }
349
+ ];
350
+ return [{ type: reading.type, content: inline(node.children) }];
351
+ }
352
+ case 'code': {
353
+ const language = languageOf(node.lang);
354
+ return [
355
+ {
356
+ type: 'code',
357
+ ...(language && { data: { language } }),
358
+ content: [],
359
+ children: node.value
360
+ ? node.value.split('\n').map((text) => ({ type: 'codeLine', content: [{ text }] }))
361
+ : []
362
+ }
363
+ ];
364
+ }
365
+ case 'list':
366
+ return node.items.map((item) => {
367
+ const { content, children } = split(blocks(item.children, reading), reading);
368
+ const own = item.checked !== undefined
369
+ ? { type: 'todo-item', data: { checked: item.checked }, content }
370
+ : { type: node.ordered ? 'numbered-list-item' : 'bulleted-list-item', content };
371
+ return withChildren(own, children);
372
+ });
373
+ case 'blockquote': {
374
+ const { content, children } = split(blocks(node.children, reading), reading);
375
+ return [withChildren({ type: 'quote', content }, children)];
376
+ }
377
+ case 'table': {
378
+ const cells = [node.header, ...node.rows].map((row) => row.map((cell) => {
379
+ const runs = inline(cell.children, true);
380
+ return runs.length === 1 && !runs[0].text ? '' : runs;
381
+ }));
382
+ return [tableBlock({ cells, headerRow: true })];
383
+ }
384
+ case 'thematicBreak':
385
+ return [{ type: 'divider', content: [] }];
386
+ case 'html':
387
+ return [{ type: reading.type, content: [{ text: node.value }] }];
388
+ case 'footnotes':
389
+ return node.items.flatMap((item) => {
390
+ const inner = blocks(item.children, reading);
391
+ const head = inner[0]?.type === reading.type ? inner.shift() : null;
392
+ const label = { text: `[^${item.id}]: ` };
393
+ return [{ type: reading.type, content: [label, ...(head?.content ?? [])] }, ...inner];
394
+ });
395
+ case 'callout':
396
+ return [withChildren({ type: 'callout', content: [] }, blocks(node.children, reading))];
397
+ case 'component': {
398
+ const data = dataOf.get(node) ?? node.attributes;
399
+ const inner = blocks(node.children, reading);
400
+ if (node.name === EQUATION)
401
+ return [{ type: 'equation', data, content: [] }];
402
+ if (node.name === TOGGLE) {
403
+ const { summary, ...rest } = data;
404
+ const head = summary ? inner.shift() : undefined;
405
+ const level = Object.keys(rest).length ? { data: rest } : {};
406
+ return [
407
+ withChildren({
408
+ type: rest.level ? 'toggle-heading' : 'toggle',
409
+ ...level,
410
+ content: head?.content ?? []
411
+ }, inner)
412
+ ];
413
+ }
414
+ const type = node.name === CALLOUT ? 'callout' : reading.components[node.name];
415
+ const { content, children } = split(inner, reading);
416
+ const own = Object.keys(data).length ? { type, data, content } : { type, content };
417
+ return [withChildren(own, children)];
418
+ }
419
+ }
420
+ };
421
+ // ——— Streaming: what of a text still arriving shows now ———
422
+ const isSpace = (c) => c === undefined || /\s/.test(c);
423
+ const isWord = (c) => c !== undefined && /[\p{L}\p{N}]/u.test(c);
424
+ /** The end of the code span opened by the backtick run at `at`, or -1. */
425
+ const codeEnd = (text, at, length) => {
426
+ for (let i = at + length; i < text.length;) {
427
+ if (text[i] !== '`') {
428
+ i++;
429
+ continue;
430
+ }
431
+ let j = i;
432
+ while (text[j] === '`')
433
+ j++;
434
+ if (j - i === length)
435
+ return i;
436
+ i = j;
437
+ }
438
+ return -1;
439
+ };
440
+ /**
441
+ * The `close` closing the `open` that `text` opens at `at` (a link label's
442
+ * `]`, its destination's `)`), skipping escapes and nested pairs; or -1.
443
+ */
444
+ const closing = (text, at, open, close) => {
445
+ let depth = 0;
446
+ for (let i = at + 1; i < text.length; i++) {
447
+ const c = text[i];
448
+ if (c === '\\')
449
+ i++;
450
+ else if (c === open)
451
+ depth++;
452
+ else if (c === close && depth-- === 0)
453
+ return i;
454
+ }
455
+ return -1;
456
+ };
457
+ /** An inline marker the last line opened and has not closed: where it starts, else -1. */
458
+ const unclosedAt = (line) => {
459
+ const open = [];
460
+ for (let i = 0; i < line.length;) {
461
+ const c = line[i];
462
+ if (c === '\\') {
463
+ i += 2;
464
+ continue;
465
+ }
466
+ if (c === '`') {
467
+ let n = 0;
468
+ while (line[i + n] === '`')
469
+ n++;
470
+ const end = codeEnd(line, i, n);
471
+ if (end === -1)
472
+ return open[0]?.at ?? i;
473
+ i = end + n;
474
+ continue;
475
+ }
476
+ if (c === '[' || (c === '!' && line[i + 1] === '[')) {
477
+ const at = c === '!' ? i + 1 : i;
478
+ const end = closing(line, at, '[', ']');
479
+ if (end === -1)
480
+ return open[0]?.at ?? i;
481
+ if (line[end + 1] === '(') {
482
+ const to = closing(line, end + 1, '(', ')');
483
+ if (to === -1)
484
+ return open[0]?.at ?? i;
485
+ i = to + 1;
486
+ }
487
+ else
488
+ i = end + 1;
489
+ continue;
490
+ }
491
+ if (c === '*' || c === '_' || c === '~') {
492
+ let n = 0;
493
+ while (line[i + n] === c)
494
+ n++;
495
+ const top = open.at(-1);
496
+ if (top && top.char === c && !isSpace(line[i - 1])) {
497
+ if (n >= top.length)
498
+ open.pop();
499
+ }
500
+ else if (!isSpace(line[i + n]) &&
501
+ !(c === '_' && isWord(line[i - 1])) &&
502
+ (c !== '~' || n >= 2))
503
+ open.push({ char: c, length: n, at: i });
504
+ i += n;
505
+ continue;
506
+ }
507
+ i++;
508
+ }
509
+ return open[0]?.at ?? -1;
510
+ };
511
+ const FENCE = /^(\s*)(`{3,}|~{3,})\s*([^`\s]*)[^`]*$/;
512
+ /** Whether `line` opens a fence, or closes the one open (`fenced`). */
513
+ const fences = (line, fenced) => FENCE.test(line) || (fenced && /^\s*(`{3,}|~{3,})\s*$/.test(line));
514
+ const TABLE_SEPARATOR = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;
515
+ /**
516
+ * A line that is only a block marker so far: `#`, `-`, `1.`, `>`, a
517
+ * fence's backticks, a to-do box, a tag being typed (`<Endpoint id="a`).
518
+ */
519
+ const MARKER_ONLY = /^\s*(?:#{1,6}|[-*+]|\d+[.)]?|>|`{1,2}|~{1,2}|\$\$?|(?:[-*+]\s+)?\[[ xX]?\]?|<\/?[A-Za-z]?[^>]*)\s*$/;
520
+ /** What of a text still arriving shows now (see `streaming`). */
521
+ const settled = (text) => {
522
+ const lines = text.split(/\r?\n/);
523
+ let partial = lines.pop() ?? '';
524
+ // Inside an open fence, everything is code: only a fence being typed waits.
525
+ let fenced = false;
526
+ for (const line of lines)
527
+ if (fences(line, fenced))
528
+ fenced = !fenced;
529
+ if (fenced)
530
+ return [...lines, /^\s*[`~]{1,}$/.test(partial) ? '' : partial].join('\n');
531
+ if (MARKER_ONLY.test(partial) || /^\s*\|/.test(partial))
532
+ partial = '';
533
+ else {
534
+ const at = unclosedAt(partial);
535
+ if (at !== -1)
536
+ partial = partial.slice(0, at);
537
+ }
538
+ // A table's header row waits for its separator row.
539
+ let end = lines.length;
540
+ while (end > 0 && lines[end - 1].includes('|') && lines[end - 1].trim())
541
+ end--;
542
+ const rows = lines.slice(end);
543
+ if (rows.length && !rows.some((line) => TABLE_SEPARATOR.test(line) && line.includes('-'))) {
544
+ lines.length = end;
545
+ partial = '';
546
+ }
547
+ return [...lines, partial].join('\n');
548
+ };
549
+ /**
550
+ * A line ending in two spaces as one ending in `\\` (both a hard break in
551
+ * CommonMark; the parser reads only the backslash), outside fenced code.
552
+ */
553
+ const hardBreaks = (text) => {
554
+ let fenced = false;
555
+ return text
556
+ .split(/\r?\n/)
557
+ .map((line, i, lines) => {
558
+ if (fences(line, fenced))
559
+ fenced = !fenced;
560
+ return !fenced && /\S {2,}$/.test(line) && lines[i + 1]?.trim()
561
+ ? `${line.trimEnd()}\\`
562
+ : line;
563
+ })
564
+ .join('\n');
565
+ };
566
+ const STREAMING = streamingMarkdownExtension();
567
+ const OWN = [autolinks, callouts, details, equations, bareTodos];
568
+ /** `text` as blocks (see the module comment). */
569
+ export const textToBlocks = (text, { markdown = true, type = 'paragraph', streaming = false, components, onUnknown } = {}) => {
570
+ if (!markdown)
571
+ return text
572
+ .split(/\r?\n[ \t]*(?:\r?\n[ \t]*)+/)
573
+ .filter((part) => part.trim())
574
+ .map((part) => ({ type, content: [{ text: part }] }));
575
+ const extensions = [...OWN];
576
+ if (components || onUnknown)
577
+ extensions.unshift(mdx(components ?? {}, onUnknown ?? (() => { }), streaming));
578
+ if (streaming)
579
+ extensions.push(STREAMING);
580
+ const document = parseMarkdown(hardBreaks(streaming ? settled(text) : text), {
581
+ extensions,
582
+ headingIds: false
583
+ });
584
+ return blocks(document.children, { type, components: components ?? {} });
585
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Blocks as Markdown (`toMarkdown`): each block by its kind's `markdown`
3
+ * form (else its `plain` form, else its text), text by its marks' and
4
+ * atoms' forms. The export's alone: a view ships none of it.
5
+ */
6
+ import type { JSONBlock } from '../utils/json.js';
7
+ import { type ExportKinds } from '../clipboard/serializeClipboardFragment.js';
8
+ /** Blocks as markdown (`toMarkdown`), by each kind's, mark's and atom's `markdown` form. */
9
+ export declare const blocksToMarkdown: (blocks: JSONBlock[], kinds: ExportKinds) => string;