edytor 0.1.0-next.32 → 0.1.0-next.34

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 (186) hide show
  1. package/README.md +6 -5
  2. package/dist/block/block.svelte.d.ts +26 -36
  3. package/dist/block/block.svelte.js +10 -7
  4. package/dist/block/block.utils.d.ts +19 -1
  5. package/dist/block/block.utils.js +2 -1
  6. package/dist/block/inlineBlock.svelte.d.ts +1 -4
  7. package/dist/block/inlineBlock.svelte.js +2 -1
  8. package/dist/clipboard/htmlFlow.d.ts +4 -2
  9. package/dist/clipboard/htmlFlow.js +101 -17
  10. package/dist/cloudflare/DocumentRoom.d.ts +250 -72
  11. package/dist/cloudflare/DocumentRoom.js +653 -91
  12. package/dist/cloudflare/history.d.ts +4 -4
  13. package/dist/cloudflare/history.js +3 -3
  14. package/dist/cloudflare/index.d.ts +3 -3
  15. package/dist/cloudflare/index.js +2 -2
  16. package/dist/cloudflare/locks.d.ts +1 -1
  17. package/dist/cloudflare/locks.js +1 -1
  18. package/dist/cloudflare/move.d.ts +11 -1
  19. package/dist/cloudflare/move.js +8 -3
  20. package/dist/cloudflare/routeDocumentSocket.d.ts +29 -5
  21. package/dist/cloudflare/routeDocumentSocket.js +30 -9
  22. package/dist/collaboration/awarenessSelection.d.ts +2 -2
  23. package/dist/collaboration/awarenessSelection.js +2 -2
  24. package/dist/collaboration/index.d.ts +1 -1
  25. package/dist/collaboration/index.js +1 -1
  26. package/dist/collaboration/providers.d.ts +0 -3
  27. package/dist/collaboration/providers.js +6 -11
  28. package/dist/components/Announcer.svelte +28 -0
  29. package/dist/components/Announcer.svelte.d.ts +7 -0
  30. package/dist/components/Block.svelte +15 -0
  31. package/dist/components/Edytor.svelte +46 -6
  32. package/dist/components/Edytor.svelte.d.ts +20 -4
  33. package/dist/crdt/attribution/attribution.d.ts +3 -3
  34. package/dist/crdt/data.d.ts +11 -0
  35. package/dist/crdt/data.js +62 -12
  36. package/dist/crdt/document.d.ts +31 -49
  37. package/dist/crdt/document.js +38 -53
  38. package/dist/crdt/edytor-doc.d.ts +34 -37
  39. package/dist/crdt/edytor-doc.js +18 -18
  40. package/dist/crdt/engine-api.d.ts +4 -5
  41. package/dist/crdt/flow.d.ts +1 -1
  42. package/dist/crdt/index.d.ts +39 -49
  43. package/dist/crdt/index.js +33 -54
  44. package/dist/crdt/migration/migrate.d.ts +5 -5
  45. package/dist/crdt/nodes.d.ts +2 -2
  46. package/dist/crdt/placement/model.d.ts +6 -6
  47. package/dist/crdt/protocol.d.ts +44 -0
  48. package/dist/crdt/protocol.js +62 -0
  49. package/dist/crdt/protocols/envelope.d.ts +1 -1
  50. package/dist/crdt/providers/bound.d.ts +2 -0
  51. package/dist/crdt/providers/bound.js +9 -0
  52. package/dist/crdt/providers/index.d.ts +6 -6
  53. package/dist/crdt/providers/indexeddb.d.ts +6 -6
  54. package/dist/crdt/providers/indexeddb.js +1 -1
  55. package/dist/crdt/providers/room.d.ts +19 -3
  56. package/dist/crdt/providers/room.js +54 -34
  57. package/dist/crdt/providers/websocket.d.ts +3 -4
  58. package/dist/crdt/rangeDelete.d.ts +1 -1
  59. package/dist/crdt/semantics.d.ts +112 -12
  60. package/dist/crdt/semantics.js +172 -14
  61. package/dist/crdt/text/runs.d.ts +22 -12
  62. package/dist/crdt/text/runs.js +17 -0
  63. package/dist/edytor.svelte.d.ts +32 -164
  64. package/dist/edytor.svelte.js +96 -48
  65. package/dist/events/beforeInputCommands.js +8 -6
  66. package/dist/events/onBeforeInput.js +6 -6
  67. package/dist/events/onCopy.js +2 -2
  68. package/dist/events/onDrop.js +2 -2
  69. package/dist/events/onKeyDown.js +4 -3
  70. package/dist/events/onPaste.js +5 -4
  71. package/dist/index.d.ts +26 -24
  72. package/dist/index.js +27 -23
  73. package/dist/kinds.d.ts +8 -3
  74. package/dist/kinds.js +13 -2
  75. package/dist/plugins/blockHandles/BlockHandle.svelte +15 -3
  76. package/dist/plugins/blockHandles/BlockHandleController.svelte.d.ts +11 -3
  77. package/dist/plugins/blockHandles/BlockHandleController.svelte.js +14 -3
  78. package/dist/plugins/blockHandles/blockHandlesPlugin.d.ts +3 -1
  79. package/dist/plugins/blockHandles/blockHandlesPlugin.js +1 -1
  80. package/dist/plugins/blockMenu/BlockMenu.svelte +52 -18
  81. package/dist/plugins/blockMenu/BlockMenuController.svelte.d.ts +9 -1
  82. package/dist/plugins/blockMenu/BlockMenuController.svelte.js +13 -2
  83. package/dist/plugins/code/CodeHeader.svelte +65 -0
  84. package/dist/plugins/code/CodeHeader.svelte.d.ts +9 -0
  85. package/dist/plugins/code/CodePlugin.svelte +222 -224
  86. package/dist/plugins/code/CodePlugin.svelte.d.ts +10 -0
  87. package/dist/plugins/code/code.css +27 -8
  88. package/dist/plugins/code/languages.d.ts +51 -0
  89. package/dist/plugins/code/languages.js +188 -0
  90. package/dist/plugins/columns/ColumnResize.svelte +24 -2
  91. package/dist/plugins/columns/ColumnsPlugin.svelte +2 -0
  92. package/dist/plugins/columns/resize.svelte.d.ts +34 -1
  93. package/dist/plugins/columns/resize.svelte.js +92 -23
  94. package/dist/plugins/find/FindBar.svelte +169 -0
  95. package/dist/plugins/find/FindBar.svelte.d.ts +9 -0
  96. package/dist/plugins/find/FindController.svelte.d.ts +62 -0
  97. package/dist/plugins/find/FindController.svelte.js +242 -0
  98. package/dist/plugins/find/FindHighlights.svelte +75 -0
  99. package/dist/plugins/find/FindHighlights.svelte.d.ts +9 -0
  100. package/dist/plugins/find/findPlugin.d.ts +25 -0
  101. package/dist/plugins/find/findPlugin.js +55 -0
  102. package/dist/plugins/find/search.d.ts +18 -0
  103. package/dist/plugins/find/search.js +41 -0
  104. package/dist/plugins/icons.js +10 -1
  105. package/dist/plugins/image/ImageChrome.svelte +212 -0
  106. package/dist/plugins/image/ImageChrome.svelte.d.ts +7 -0
  107. package/dist/plugins/image/ImageEmpty.svelte +38 -7
  108. package/dist/plugins/image/ImageEmpty.svelte.d.ts +4 -0
  109. package/dist/plugins/image/ImagePlugin.svelte +196 -40
  110. package/dist/plugins/image/ImagePlugin.svelte.d.ts +17 -4
  111. package/dist/plugins/image/controls.svelte.d.ts +111 -0
  112. package/dist/plugins/image/controls.svelte.js +284 -0
  113. package/dist/plugins/image/image.d.ts +19 -1
  114. package/dist/plugins/image/image.js +31 -1
  115. package/dist/plugins/image/uploads.svelte.d.ts +50 -0
  116. package/dist/plugins/image/uploads.svelte.js +111 -0
  117. package/dist/plugins/index.d.ts +11 -2
  118. package/dist/plugins/index.js +10 -2
  119. package/dist/plugins/media/AudioPlugin.svelte +86 -0
  120. package/dist/plugins/media/AudioPlugin.svelte.d.ts +28 -0
  121. package/dist/plugins/media/BookmarkPlugin.svelte +182 -0
  122. package/dist/plugins/media/BookmarkPlugin.svelte.d.ts +45 -0
  123. package/dist/plugins/media/EmbedPlugin.svelte +146 -0
  124. package/dist/plugins/media/EmbedPlugin.svelte.d.ts +37 -0
  125. package/dist/plugins/media/FilePlugin.svelte +112 -0
  126. package/dist/plugins/media/FilePlugin.svelte.d.ts +28 -0
  127. package/dist/plugins/media/MediaEmpty.svelte +107 -0
  128. package/dist/plugins/media/MediaEmpty.svelte.d.ts +21 -0
  129. package/dist/plugins/media/UrlPasteMenu.svelte +83 -0
  130. package/dist/plugins/media/UrlPasteMenu.svelte.d.ts +7 -0
  131. package/dist/plugins/media/VideoPlugin.svelte +95 -0
  132. package/dist/plugins/media/VideoPlugin.svelte.d.ts +29 -0
  133. package/dist/plugins/media/media.d.ts +72 -0
  134. package/dist/plugins/media/media.js +226 -0
  135. package/dist/plugins/media/urlPaste.svelte.d.ts +76 -0
  136. package/dist/plugins/media/urlPaste.svelte.js +197 -0
  137. package/dist/plugins/richtext/RichTextPlugin.svelte +102 -15
  138. package/dist/plugins/richtext/richTextOperations.d.ts +37 -2
  139. package/dist/plugins/richtext/richTextOperations.js +66 -1
  140. package/dist/plugins/slashMenu/SlashMenu.svelte +101 -42
  141. package/dist/plugins/slashMenu/SlashMenuController.svelte.d.ts +7 -0
  142. package/dist/plugins/slashMenu/SlashMenuController.svelte.js +8 -0
  143. package/dist/plugins/slashMenu/slashMenuPlugin.d.ts +2 -0
  144. package/dist/plugins/suggestions/SuggestionBars.svelte +1 -2
  145. package/dist/plugins/toolbar/LinkCard.svelte +117 -0
  146. package/dist/plugins/toolbar/LinkCard.svelte.d.ts +7 -0
  147. package/dist/plugins/toolbar/Toolbar.svelte +59 -3
  148. package/dist/plugins/toolbar/ToolbarController.svelte.d.ts +39 -1
  149. package/dist/plugins/toolbar/ToolbarController.svelte.js +122 -2
  150. package/dist/plugins/toolbar/toolbarPlugin.d.ts +5 -0
  151. package/dist/plugins/toolbar/toolbarPlugin.js +81 -0
  152. package/dist/plugins.d.ts +45 -44
  153. package/dist/selection/replaceSelection.d.ts +7 -1
  154. package/dist/selection/replaceSelection.js +20 -9
  155. package/dist/selection/selection.svelte.d.ts +11 -121
  156. package/dist/selection/selection.svelte.js +41 -15
  157. package/dist/session/announcer.svelte.d.ts +27 -0
  158. package/dist/session/announcer.svelte.js +34 -0
  159. package/dist/session/bindings.js +3 -1
  160. package/dist/session/commands.d.ts +38 -14
  161. package/dist/session/commands.js +95 -25
  162. package/dist/session/editing/text.d.ts +1 -1
  163. package/dist/session/history.d.ts +8 -1
  164. package/dist/session/history.js +10 -3
  165. package/dist/session/keymap.d.ts +1 -1
  166. package/dist/session/keymap.js +2 -2
  167. package/dist/session/moves.d.ts +3 -2
  168. package/dist/session/moves.js +6 -2
  169. package/dist/session/navigation.js +1 -1
  170. package/dist/session/selection.d.ts +3 -3
  171. package/dist/session/suggestions.svelte.d.ts +0 -2
  172. package/dist/session/virtual.js +3 -2
  173. package/dist/surface/attributes.d.ts +13 -0
  174. package/dist/surface/attributes.js +20 -0
  175. package/dist/surface/cells.d.ts +1 -1
  176. package/dist/surface/popups.svelte.d.ts +28 -0
  177. package/dist/surface/popups.svelte.js +32 -0
  178. package/dist/text/text.svelte.d.ts +2 -5
  179. package/dist/text/text.svelte.js +3 -2
  180. package/dist/text/text.utils.js +2 -1
  181. package/dist/themes/notion.css +171 -2
  182. package/dist/utils/json.d.ts +2 -0
  183. package/dist/utils/json.js +15 -0
  184. package/dist/utils.d.ts +10 -1
  185. package/dist/utils.js +22 -2
  186. package/package.json +12 -4
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  <p>A collaborative block editor for Svelte 5, on a Yjs v14 engine</p>
5
5
 
6
+ [![CI](https://github.com/beynar/edytor/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/beynar/edytor/actions/workflows/ci.yml)
6
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
8
  [![Svelte v5](https://img.shields.io/badge/Svelte-v5-FF3E00.svg)](https://svelte.dev)
8
9
 
@@ -15,7 +16,7 @@
15
16
 
16
17
  Edytor aims to be for Svelte what Slate.js is for React: a heavily customizable editor with an API to build any kind of collaborative rich text editor.
17
18
 
18
- > **Work in progress.** Edytor is a pre-release (`0.1.0-next.32`, `edytor@next` on npm) and not ready for production; the API changes between releases without a compatibility layer. The untagged `edytor@0.0.11` on npm predates the current API. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
19
+ > **Work in progress.** Edytor is a pre-release (`0.1.0-next.34`, `edytor@next` on npm) and not ready for production; the API changes between releases without a compatibility layer. The untagged `edytor@0.0.11` on npm predates the current API. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
19
20
 
20
21
  ## Features
21
22
 
@@ -38,7 +39,7 @@ The pre-release is on npm under the `next` tag. Name the tag: a bare `edytor` is
38
39
  pnpm add edytor@next
39
40
  ```
40
41
 
41
- `svelte@^5` is a peer dependency. Do not install `yjs`: the v14 engine is vendored (`edytor/crdt`). See [installation](https://edytor.dev/docs/getting-started).
42
+ `svelte@^5` is a peer dependency. Do not install `yjs`: the v14 engine is vendored (`edytor/crdt`). See [installation](https://edytor.dev/docs/getting-started) and [platform support](https://edytor.dev/docs/reference/platform-support) (Chrome, Edge, Firefox and Safari; real phones are not tested yet).
42
43
 
43
44
  ## Quick start
44
45
 
@@ -78,7 +79,7 @@ pnpm add edytor@next
78
79
  </div>
79
80
  ```
80
81
 
81
- `<Edytor>` adds the rich text, image, arrow-move and suggestions plugins after yours, and block handles. To collaborate, name a room and a server running the [`edytor/cloudflare` room](https://edytor.dev/docs/server/quick-start):
82
+ An editable editor is client-only: in SvelteKit, mount it once the page has hydrated ([quick start](https://edytor.dev/docs/getting-started/quick-start#mount-it-in-a-page)). `<Edytor>` adds the rich text, image, arrow-move and suggestions plugins after yours, and block handles. To collaborate, name a room and a server running the [`edytor/cloudflare` room](https://edytor.dev/docs/server/quick-start):
82
83
 
83
84
  ```svelte
84
85
  <Edytor
@@ -98,11 +99,11 @@ The [documentation site](https://edytor.dev/docs) is the single source for the A
98
99
  - [Editor](https://edytor.dev/docs/editor/edytor-component): the component, commands, selection, history, clipboard, readonly
99
100
  - [Plugins](https://edytor.dev/docs/plugins) and [customization](https://edytor.dev/docs/customization/blocks): bundled plugins, custom blocks and marks, [hotkeys and editing behavior](https://edytor.dev/docs/customization/hotkeys)
100
101
  - [Collaboration](https://edytor.dev/docs/collaboration) and [server](https://edytor.dev/docs/server/quick-start): documents, providers, presence, the Durable Object room and its protocol
101
- - [Reference](https://edytor.dev/docs/reference/document-api): the document API, [troubleshooting](https://edytor.dev/docs/reference/troubleshooting), [migration from 0.0.11](https://edytor.dev/docs/reference/migration), limitations
102
+ - [Reference](https://edytor.dev/docs/reference/document-api): the document API, [troubleshooting](https://edytor.dev/docs/reference/troubleshooting), [migration from 0.0.11](https://edytor.dev/docs/reference/migration), limitations, [platform support](https://edytor.dev/docs/reference/platform-support)
102
103
 
103
104
  ## Contributing
104
105
 
105
- `AGENTS.md` describes the architecture (one owner per fact), where to fix what, and the test lanes. The docs live in `site/content/docs`; a change to public behavior updates them in the same commit.
106
+ `CONTRIBUTING.md` lists the test lanes, what CI runs, and the pull request checklist; `AGENTS.md` describes the architecture (one owner per fact) and where to fix what. The docs live in `site/content/docs`; a change to public behavior updates them in the same commit.
106
107
 
107
108
  ```bash
108
109
  pnpm install
@@ -45,18 +45,16 @@ export declare class Block {
45
45
  setData: (data: Record<string, unknown>) => void;
46
46
  get selected(): boolean;
47
47
  get focused(): boolean;
48
- /** May this block move at all — R5 `canPlace` without a destination (drag handles). */
48
+ /** May this block move at all: `canPlace` without a destination (drag handles). */
49
49
  get movable(): boolean;
50
50
  /**
51
51
  * May text-level structural commands (convert, markdown shortcut, slash
52
52
  * menu) apply: the block is movable — or the virtual paragraph of an
53
53
  * emptied document, which a conversion creates with its kind
54
- * (SW10-lists-3) — has no role of its own and renders its own content —
55
- * a container (a list) is never converted, its items are (ZW-02).
54
+ * — has no role of its own and renders its own content —
55
+ * a container (a list) is never converted, its items are.
56
56
  */
57
57
  get convertible(): boolean;
58
- get firstEditableText(): Text | undefined;
59
- get lastEditableText(): Text | undefined;
60
58
  get index(): number;
61
59
  get depth(): number;
62
60
  get path(): number[];
@@ -79,14 +77,14 @@ export declare class Block {
79
77
  /** It holds nothing: no content and no children (the emptiness Turn into reads, `holdsNothing`). */
80
78
  get isEmpty(): boolean;
81
79
  get isNested(): boolean;
82
- /** This block's JSON — the document's one serializer (`facade.blockJSON`, L14). */
80
+ /** This block's JSON — the document's one serializer (`facade.blockJSON`). */
83
81
  get value(): JSONBlock;
84
82
  isChildOf(block: Block): boolean;
85
- /** Whether this kind renders its own content slot — the adopted capability (R5, O22). */
83
+ /** Whether this kind renders its own content slot — the adopted capability. */
86
84
  get rendersContent(): boolean;
87
85
  /**
88
86
  * A container: it shows only its children (a list), neither void nor an
89
- * island — the document's container rule (YW-02). Its items hold what it
87
+ * island — the document's container rule. Its items hold what it
90
88
  * shows; it is never converted (`convertible`).
91
89
  */
92
90
  get isContainer(): boolean;
@@ -100,7 +98,7 @@ export declare class Block {
100
98
  /**
101
99
  * The list this block shows in: its container when it is an item
102
100
  * (`isListItem`), or — an item outdented out of a nested list into the
103
- * item holding it (SW8-roles-4) — the list of the items of its kind it
101
+ * item holding it — the list of the items of its kind it
104
102
  * sits under. <kbd>Enter</kbd> in an empty one outdents it; the menus
105
103
  * name it by the list's `itemKind`.
106
104
  */
@@ -114,55 +112,55 @@ export declare class Block {
114
112
  get firstText(): Text | undefined;
115
113
  /** The last text of this block's own content (see {@link firstText}). */
116
114
  get lastText(): Text | undefined;
117
- addChildBlock: (args_0: {
115
+ addChildBlock: (payload: {
118
116
  block: JSONBlock;
119
117
  index: number;
120
118
  }) => Block;
121
- addChildBlocks: (args_0: {
119
+ addChildBlocks: (payload: {
122
120
  blocks: JSONBlock[];
123
121
  index: number;
124
122
  }) => Block[];
125
123
  insertBlockAfter: (payload: {
126
124
  block: JSONBlock;
127
- }, plan?: import("../crdt/index.js").Prepared | undefined) => Block | null | undefined;
125
+ }) => Block | null | undefined;
128
126
  insertBlockBefore: (payload: {
129
127
  block: JSONBlock;
130
- }, plan?: import("../crdt/index.js").Prepared | undefined) => Block | null | undefined;
128
+ }) => Block | null | undefined;
131
129
  splitBlock: (payload: {
132
130
  index: number;
133
131
  text: Text;
134
- }, plan?: import("../crdt/index.js").Prepared | undefined) => Block | null | undefined;
135
- duplicateBlock: (_?: {} | undefined, plan?: import("../crdt/index.js").Prepared | undefined) => Block | null | undefined;
132
+ }) => Block | null | undefined;
133
+ duplicateBlock: (payload?: {} | undefined) => Block | null | undefined;
136
134
  removeBlock: (payload?: {
137
135
  keepChildren?: boolean;
138
- } | undefined, plan?: import("../crdt/index.js").Prepared | undefined) => void;
139
- unNestBlock: (this: Block, _?: {} | undefined, plan?: import("../crdt/index.js").Prepared | undefined) => Block | null;
140
- mergeBlockBackward: (_?: {} | undefined, plan?: import("../crdt/index.js").Prepared | undefined) => Block | null | undefined;
141
- mergeBlockForward: (_?: {} | undefined, plan?: import("../crdt/index.js").Prepared | undefined) => Block | null | undefined;
142
- nestBlock: (this: Block, _?: {} | undefined, plan?: import("../crdt/index.js").Prepared | undefined) => Block | null;
136
+ } | undefined) => void;
137
+ unNestBlock: (this: Block, payload?: {} | undefined) => Block | null;
138
+ mergeBlockBackward: (payload?: {} | undefined) => Block | null | undefined;
139
+ mergeBlockForward: (payload?: {} | undefined) => Block | null | undefined;
140
+ nestBlock: (this: Block, payload?: {} | undefined) => Block | null;
143
141
  setBlock: (payload: {
144
142
  value: Partial<JSONBlock>;
145
- }, plan?: import("../crdt/index.js").Prepared | undefined) => void;
143
+ }) => void;
146
144
  moveBlock: (this: Block, payload: {
147
145
  path: number[];
148
146
  beside?: import("./block.utils.js").BlockBeside;
149
- }, plan?: import("../crdt/index.js").Prepared | undefined) => Block | null;
147
+ }) => Block | null;
150
148
  moveBlocks: (this: Block, payload: {
151
149
  blocks: Block[];
152
150
  path: number[];
153
151
  beside?: import("./block.utils.js").BlockBeside;
154
- }, plan?: import("../crdt/index.js").Prepared | undefined) => Block[];
155
- pushContentIntoBlock: (args_0: {
152
+ }) => Block[];
153
+ pushContentIntoBlock: (payload: {
156
154
  value: (Text | InlineBlock)[];
157
155
  }) => void;
158
156
  removeInlineBlock: (payload: {
159
157
  index: number;
160
- }, plan?: import("../crdt/index.js").Prepared | undefined) => void;
158
+ }) => void;
161
159
  patchData: (payload: {
162
160
  ops: import("../crdt/index.js").DataPatch[];
163
161
  atom?: string;
164
- }, plan?: import("../crdt/index.js").Prepared | undefined) => void;
165
- addInlineBlock: (args_0: {
162
+ }) => void;
163
+ addInlineBlock: (payload: {
166
164
  index: number;
167
165
  block: JSONInlineBlock;
168
166
  text: Text;
@@ -176,7 +174,7 @@ export declare class Block {
176
174
  deleteContentAtRange: (payload: {
177
175
  start: [number, number];
178
176
  end: [number, number];
179
- }, plan?: import("../crdt/index.js").Prepared | undefined) => void;
177
+ }) => void;
180
178
  /** Mark an element inside the block's markup as non-editable chrome (a header, a caption bar). */
181
179
  void: (node: HTMLElement) => void;
182
180
  /**
@@ -201,12 +199,4 @@ export declare class Block {
201
199
  * each entry is a text's runs or an atom, as JSON.
202
200
  */
203
201
  insertParts: (index: number, parts: (JSONText[] | JSONInlineBlock)[]) => void;
204
- /**
205
- * Register the element the core rendered for this block (O45) and run the
206
- * attach hooks. A re-rendered block (moved, re-parented) keeps its declared
207
- * view state (R11): an open toggle stays open.
208
- */
209
- attach: (node: HTMLElement) => {
210
- destroy: () => void;
211
- };
212
202
  }
@@ -94,7 +94,7 @@ export class Block {
94
94
  get focused() {
95
95
  return this.edytor.selection.focusedBlocks.has(this) && !this.selected;
96
96
  }
97
- /** May this block move at all — R5 `canPlace` without a destination (drag handles). */
97
+ /** May this block move at all: `canPlace` without a destination (drag handles). */
98
98
  get movable() {
99
99
  return this.edytor.facade.canPlace([this.id]);
100
100
  }
@@ -102,14 +102,15 @@ export class Block {
102
102
  * May text-level structural commands (convert, markdown shortcut, slash
103
103
  * menu) apply: the block is movable — or the virtual paragraph of an
104
104
  * emptied document, which a conversion creates with its kind
105
- * (SW10-lists-3) — has no role of its own and renders its own content —
106
- * a container (a list) is never converted, its items are (ZW-02).
105
+ * — has no role of its own and renders its own content —
106
+ * a container (a list) is never converted, its items are.
107
107
  */
108
108
  get convertible() {
109
109
  const { facade } = this.edytor;
110
110
  const placeable = this.movable || facade.virtual() === this.id;
111
111
  return placeable && this.rendersContent && !facade.isVoid(this.id) && !facade.isIsland(this.id);
112
112
  }
113
+ /** @internal */
113
114
  get firstEditableText() {
114
115
  // Only a RENDERED text part is editable — a container's phantom
115
116
  // content slot holds a Text with no `node`. Scan ALL children: the
@@ -125,6 +126,7 @@ export class Block {
125
126
  }
126
127
  return undefined;
127
128
  }
129
+ /** @internal */
128
130
  get lastEditableText() {
129
131
  const own = this.content.findLast((part) => part instanceof Text && part.node != null);
130
132
  if (own)
@@ -207,7 +209,7 @@ export class Block {
207
209
  get isNested() {
208
210
  return (this.parent && this.parent.type !== 'root') || false;
209
211
  }
210
- /** This block's JSON — the document's one serializer (`facade.blockJSON`, L14). */
212
+ /** This block's JSON — the document's one serializer (`facade.blockJSON`). */
211
213
  get value() {
212
214
  if (!this.isRoot)
213
215
  return this.edytor.facade.blockJSON(this.id);
@@ -220,13 +222,13 @@ export class Block {
220
222
  return true;
221
223
  return false;
222
224
  }
223
- /** Whether this kind renders its own content slot — the adopted capability (R5, O22). */
225
+ /** Whether this kind renders its own content slot — the adopted capability. */
224
226
  get rendersContent() {
225
227
  return this.edytor.document.rendersContent(this.type);
226
228
  }
227
229
  /**
228
230
  * A container: it shows only its children (a list), neither void nor an
229
- * island — the document's container rule (YW-02). Its items hold what it
231
+ * island — the document's container rule. Its items hold what it
230
232
  * shows; it is never converted (`convertible`).
231
233
  */
232
234
  get isContainer() {
@@ -249,7 +251,7 @@ export class Block {
249
251
  /**
250
252
  * The list this block shows in: its container when it is an item
251
253
  * (`isListItem`), or — an item outdented out of a nested list into the
252
- * item holding it (SW8-roles-4) — the list of the items of its kind it
254
+ * item holding it — the list of the items of its kind it
253
255
  * sits under. <kbd>Enter</kbd> in an empty one outdents it; the menus
254
256
  * name it by the list's `itemKind`.
255
257
  */
@@ -373,6 +375,7 @@ export class Block {
373
375
  * Register the element the core rendered for this block (O45) and run the
374
376
  * attach hooks. A re-rendered block (moved, re-parented) keeps its declared
375
377
  * view state (R11): an open toggle stays open.
378
+ * @internal
376
379
  */
377
380
  attach = (node) => {
378
381
  const names = this.definition.viewState ?? [];
@@ -122,6 +122,19 @@ export type BlockOperations = {
122
122
  content: readonly JSONBlock[];
123
123
  };
124
124
  };
125
+ /**
126
+ * Find and replace (`plugins/find`): each match's text replaced by
127
+ * `replacement`, one plan, one undo step (its steps: a
128
+ * `deleteContentAtRange` and an `insertText` per match).
129
+ */
130
+ replaceMatches: {
131
+ matches: {
132
+ block: string;
133
+ offset: number;
134
+ length: number;
135
+ }[];
136
+ replacement: string;
137
+ };
125
138
  deleteContentAtRange: {
126
139
  start: [number, number];
127
140
  end: [number, number];
@@ -153,7 +166,12 @@ export type BlockOperations = {
153
166
  * and answers the op's document result; `resolve` turns it into wrappers
154
167
  * after the commit patched them.
155
168
  */
156
- export declare function batch<T extends (...args: any[]) => any, O extends keyof BlockOperations, R = ReturnType<T>>(operation: O, func: T, prepare?: (payload: BlockOperations[O]) => Prepared, resolve?: (this: any, out: ReturnType<T> | undefined, payload: Parameters<T>[0]) => R): (...args: Parameters<T>) => R;
169
+ export declare function batch<T extends (...args: any[]) => any, O extends keyof BlockOperations, R = ReturnType<T>>(operation: O, func: T, prepare?: (payload: BlockOperations[O]) => Prepared, resolve?: (this: any, out: ReturnType<T> | undefined, payload: Parameters<T>[0]) => R): (...args: PayloadOf<Parameters<T>>) => R;
170
+ /**
171
+ * A mutator's public parameters: its payload alone (optional when the op's
172
+ * is). The plan the dispatcher passes the op is not the caller's.
173
+ */
174
+ export type PayloadOf<A extends unknown[]> = A extends [] ? [] : A extends [infer P, ...unknown[]] ? [payload: P] : A extends [(infer P)?, ...unknown[]] ? [payload?: P] : [];
157
175
  /**
158
176
  * The handle of block `id` (null: refused at preparation, or gone; undefined:
159
177
  * the command did not run). Inside an outer transaction too: the index shows
@@ -12,7 +12,8 @@ import { InlineBlock } from './inlineBlock.svelte.js';
12
12
  export function batch(operation, func, prepare, resolve) {
13
13
  return function (...[payload]) {
14
14
  const out = this.edytor.dispatcher.dispatch(operation, payload, { block: this }, (p, plan) => func.call(this, p, plan), prepare && ((p) => prepare.call(this, p)));
15
- return resolve ? resolve.call(this, out, payload) : out;
15
+ const value = resolve ? resolve.call(this, out, payload) : out;
16
+ return this.edytor.dispatcher.answer(value);
16
17
  };
17
18
  }
18
19
  /**
@@ -3,7 +3,7 @@ import type { InlineBlockDefinition } from '../plugins.js';
3
3
  import type { JSONInlineBlock } from '../utils/json.js';
4
4
  import type { Block } from './block.svelte.js';
5
5
  /**
6
- * An id-only inline-atom handle (§2.4 "Handles", R4): atom `id`, shown in
6
+ * An id-only inline-atom handle: atom `id`, shown in
7
7
  * block `blockId`. Getters read the document index; `node` is the element
8
8
  * that renders it (a Surface fact).
9
9
  */
@@ -31,7 +31,4 @@ export declare class InlineBlock {
31
31
  /** The atom is shown by a live block. */
32
32
  get isInDocument(): boolean;
33
33
  get value(): JSONInlineBlock;
34
- attach: (node: HTMLElement) => {
35
- destroy: () => void;
36
- };
37
34
  }
@@ -3,7 +3,7 @@ import { propsProxy } from '../session/props.js';
3
3
  const INLINE_EDGE_CARET_THRESHOLD_PX = 4;
4
4
  const isTextPart = (part) => part !== undefined && !(part instanceof InlineBlock);
5
5
  /**
6
- * An id-only inline-atom handle (§2.4 "Handles", R4): atom `id`, shown in
6
+ * An id-only inline-atom handle: atom `id`, shown in
7
7
  * block `blockId`. Getters read the document index; `node` is the element
8
8
  * that renders it (a Surface fact).
9
9
  */
@@ -61,6 +61,7 @@ export class InlineBlock {
61
61
  get value() {
62
62
  return { id: this.id, type: this.type, data: { ...this.#item?.data } };
63
63
  }
64
+ /** @internal */
64
65
  attach = (node) => {
65
66
  node.setAttribute('contenteditable', 'false');
66
67
  node.dataset.edytorId = this.id;
@@ -15,8 +15,10 @@ export declare const tagOf: (type: string, kind: BlockDefinition, data: Values)
15
15
  * catalogue kind's tag per preset (a content-less container takes its
16
16
  * default child's tag, one child per text line), a value-less mark's
17
17
  * `tag`, and each record's `parse` hook first (aliases, sanitized values).
18
- * Unknown elements degrade to text runs; whitespace collapses as HTML
19
- * renders it. `null` when the HTML carries nothing (`flow.shape`, F-P10).
18
+ * A void kind with a `parse` hook (an image, an embed, a video) is found
19
+ * by its hook only, and a void takes only its `figcaption`'s text; an
20
+ * `iframe`, `video` or `audio` no hook claims carries nothing. Unknown
21
+ * elements degrade to text runs; whitespace collapses as HTML renders it. `null` when the HTML carries nothing (`flow.shape`, F-P10).
20
22
  */
21
23
  export declare const flowOfHtml: (kinds: ImportKinds, html: string | undefined) => Flow | null;
22
24
  export {};
@@ -3,6 +3,32 @@ import { jsonBlockToSpec } from '../utils/json.js';
3
3
  const SKIP = /^(script|style|template|noscript|svg|math|iframe|object|embed|canvas|video|audio|img|input|button|select|textarea|head|title|meta|link|base)$/;
4
4
  /** HTML's block-level elements: they start a line (a kind's tag does too). */
5
5
  const BLOCK = /^(address|article|aside|blockquote|dd|details|dialog|div|dl|dt|fieldset|figcaption|figure|footer|form|h[1-6]|header|hgroup|hr|li|main|nav|ol|p|pre|section|table|tbody|td|tfoot|th|thead|tr|ul)$/;
6
+ /** The largest side, in CSS px, of an inline glyph image (an emoji, an icon, a tracking pixel). */
7
+ const GLYPH_PX = 32;
8
+ /** An alt made of emoji only (with their joiners, variation selectors and skin tones). */
9
+ const EMOJI_ALT = /^(?:[\p{Extended_Pictographic}\p{Regional_Indicator}\s]|\u200d|\ufe0f|\u20e3|\p{Emoji_Modifier})+$/u;
10
+ const sizeOf = (img, side) => {
11
+ const value = img.getAttribute(side) ?? img.style.getPropertyValue(side);
12
+ const px = /^\s*(\d+(?:\.\d+)?)\s*(px)?\s*$/.exec(value);
13
+ return px ? Number(px[1]) : undefined;
14
+ };
15
+ /**
16
+ * An inline glyph image (`flow.html.glyph`): an emoji drawn as an image (an
17
+ * emoji-only alt, an `emoji` class or `data-emoji`, as X, Gmail, Slack and
18
+ * WordPress write them), or an image no larger than a glyph (an icon, an
19
+ * email's tracking pixel). It is text, its `alt`, never a block.
20
+ */
21
+ const isGlyph = (node) => {
22
+ if (!(node instanceof HTMLElement) || node.localName !== 'img')
23
+ return false;
24
+ const alt = node.getAttribute('alt')?.trim() ?? '';
25
+ if (alt && EMOJI_ALT.test(alt) && /[\p{Extended_Pictographic}\p{Regional_Indicator}]/u.test(alt))
26
+ return true;
27
+ if (/emoji/i.test(node.className) || node.hasAttribute('data-emoji'))
28
+ return true;
29
+ const sides = [sizeOf(node, 'width'), sizeOf(node, 'height')].filter((n) => n !== undefined);
30
+ return sides.length > 0 && sides.every((n) => n <= GLYPH_PX);
31
+ };
6
32
  /** Tags that carry no meaning of their own: a kind rendering one is not found by it. */
7
33
  const GENERIC = /^(div|span)$/;
8
34
  /** The tag a kind writes for `data`: its export form's first tag, else its element's, else its content element's. */
@@ -31,8 +57,10 @@ export const tagOf = (type, kind, data) => {
31
57
  * catalogue kind's tag per preset (a content-less container takes its
32
58
  * default child's tag, one child per text line), a value-less mark's
33
59
  * `tag`, and each record's `parse` hook first (aliases, sanitized values).
34
- * Unknown elements degrade to text runs; whitespace collapses as HTML
35
- * renders it. `null` when the HTML carries nothing (`flow.shape`, F-P10).
60
+ * A void kind with a `parse` hook (an image, an embed, a video) is found
61
+ * by its hook only, and a void takes only its `figcaption`'s text; an
62
+ * `iframe`, `video` or `audio` no hook claims carries nothing. Unknown
63
+ * elements degrade to text runs; whitespace collapses as HTML renders it. `null` when the HTML carries nothing (`flow.shape`, F-P10).
36
64
  */
37
65
  export const flowOfHtml = (kinds, html) => {
38
66
  if (!html || html.length > 8 * 1024 * 1024 || typeof DOMParser === 'undefined')
@@ -40,7 +68,8 @@ export const flowOfHtml = (kinds, html) => {
40
68
  const { blocks, marks } = kinds;
41
69
  const byTag = new Map();
42
70
  for (const [type, kind] of blocks) {
43
- if (!kind.presets?.length)
71
+ // A void found by its hook carries its substance in data (a source): its tag alone is none.
72
+ if (!kind.presets?.length || (kind.void && kind.parse))
44
73
  continue;
45
74
  for (const { data = {} } of kind.presets) {
46
75
  const tag = tagOf(type, kind, data);
@@ -53,6 +82,9 @@ export const flowOfHtml = (kinds, html) => {
53
82
  byTag.set(tag, { type, data: kind.presets[0].data ?? {}, lines: child });
54
83
  }
55
84
  const kindOf = (element) => {
85
+ // A glyph is text: no kind claims it.
86
+ if (isGlyph(element))
87
+ return undefined;
56
88
  for (const [type, kind] of blocks) {
57
89
  const data = kind.parse?.(element);
58
90
  if (data)
@@ -71,6 +103,11 @@ export const flowOfHtml = (kinds, html) => {
71
103
  return found;
72
104
  };
73
105
  const isBlock = (node) => node instanceof HTMLElement && (BLOCK.test(node.localName) || kindOf(node) !== undefined);
106
+ /** A void kind's element (an image): it shows no text, so a line it is met in ends there. */
107
+ const isVoid = (node) => {
108
+ const claim = node instanceof HTMLElement ? kindOf(node) : undefined;
109
+ return claim !== undefined && blocks.get(claim.type)?.void === true;
110
+ };
74
111
  const text = (line, value, marks, pre) => {
75
112
  const last = line.content.at(-1);
76
113
  if (!pre)
@@ -85,16 +122,20 @@ export const flowOfHtml = (kinds, html) => {
85
122
  else
86
123
  line.content.push(Object.keys(marks).length ? { text: value, marks } : { text: value });
87
124
  };
88
- const inline = (node, line, marks, pre) => {
125
+ const inline = (node, at, marks, pre) => {
89
126
  if (node.nodeType === Node.TEXT_NODE)
90
- return text(line, node.textContent ?? '', marks, pre);
127
+ return text(at.line(), node.textContent ?? '', marks, pre);
128
+ if (isGlyph(node))
129
+ return text(at.line(), node.getAttribute('alt') ?? '', marks, pre);
130
+ if (isVoid(node))
131
+ return at.apart(node);
91
132
  if (!(node instanceof HTMLElement) || SKIP.test(node.localName))
92
133
  return;
93
134
  if (node.localName === 'br')
94
- return text(line, '\n', marks, true);
135
+ return text(at.line(), '\n', marks, true);
95
136
  const inner = { ...marks, ...marksOf(node) };
96
137
  for (const child of node.childNodes)
97
- inline(child, line, inner, pre || node.localName === 'pre');
138
+ inline(child, at, inner, pre || node.localName === 'pre');
98
139
  };
99
140
  /** A line ends without its trailing space or `<br>`. */
100
141
  const end = (line) => {
@@ -114,9 +155,16 @@ export const flowOfHtml = (kinds, html) => {
114
155
  lines.push(run);
115
156
  run = null;
116
157
  };
158
+ const at = {
159
+ line: () => (run ??= { type, content: [] }),
160
+ apart: (element) => {
161
+ flush();
162
+ lines.push(...blockOf(element, type));
163
+ }
164
+ };
117
165
  for (const node of parent.childNodes) {
118
166
  if (!isBlock(node))
119
- inline(node, (run ??= { type, content: [] }), {}, false);
167
+ inline(node, at, {}, false);
120
168
  else {
121
169
  flush();
122
170
  lines.push(...blockOf(node, type));
@@ -135,6 +183,17 @@ export const flowOfHtml = (kinds, html) => {
135
183
  }));
136
184
  return [{ type: claim.type, data: claim.data, content: [], children }];
137
185
  }
186
+ // A void's only text is its caption: an `iframe`'s fallback, a `video`'s source list or
187
+ // a bookmark's link text is never content.
188
+ if (claim && blocks.get(claim.type)?.void) {
189
+ const line = { type: claim.type, data: claim.data, content: [] };
190
+ const caption = [...element.children].find((child) => child.localName === 'figcaption');
191
+ // A caption is one line: an element that would end it (an image) adds nothing.
192
+ const at = { line: () => line, apart: () => { } };
193
+ for (const node of caption?.childNodes ?? [])
194
+ inline(node, at, {}, false);
195
+ return [end(line)];
196
+ }
138
197
  // A kind that shows no text of its own (a list, a layout, a column) takes none: its
139
198
  // inline runs and a leading paragraph are lines of its default child, never hidden text.
140
199
  if (claim && blocks.get(claim.type)?.rendersContent === false) {
@@ -149,20 +208,45 @@ export const flowOfHtml = (kinds, html) => {
149
208
  return linesOf(element, type);
150
209
  const line = { type: claim?.type ?? type, data: claim?.data, content: [] };
151
210
  const childType = claim ? kinds.document.defaultChild(claim.type) : type;
152
- const children = [];
153
211
  const pre = element.localName === 'pre';
212
+ // The element's line, then, for each void element met in it, that block
213
+ // and a line of the element's kind for what follows (`flow.html.void`).
214
+ const parts = [line];
215
+ const texts = new Set([line]);
216
+ let current = line;
217
+ const at = {
218
+ line: () => current,
219
+ apart: (inner) => {
220
+ // A void's own media (a figure's `img`) is part of it.
221
+ if (claim && blocks.get(claim.type)?.void)
222
+ return;
223
+ parts.push(...blockOf(inner, type));
224
+ current = { type: line.type, data: line.data, content: [] };
225
+ parts.push(current);
226
+ texts.add(current);
227
+ }
228
+ };
154
229
  for (const node of element.childNodes)
155
- if (!isBlock(node))
156
- inline(node, line, {}, pre);
230
+ if (isVoid(node))
231
+ at.apart(node);
232
+ else if (!isBlock(node))
233
+ inline(node, at, {}, pre);
157
234
  // A leading plain paragraph is the element's own text (`<li><p>Item</p></li>`).
158
- else if (!line.content.length && !children.length && !kindOf(node) && !wraps(node))
235
+ else if (!current.content.length &&
236
+ !current.children?.length &&
237
+ !kindOf(node) &&
238
+ !wraps(node))
159
239
  for (const child of node.childNodes)
160
- inline(child, line, {}, pre);
240
+ inline(child, at, {}, pre);
161
241
  else
162
- children.push(...blockOf(node, childType));
163
- if (children.length)
164
- line.children = children;
165
- return [end(line)];
242
+ (current.children ??= []).push(...blockOf(node, childType));
243
+ for (const part of texts)
244
+ if (!part.children?.length)
245
+ delete part.children;
246
+ if (parts.length === 1)
247
+ return [end(line)];
248
+ // Split: a text line left empty around a void element is not kept.
249
+ return parts.filter((part) => !texts.has(part) || end(part).content.length > 0 || part.children !== undefined);
166
250
  };
167
251
  const body = new DOMParser().parseFromString(html, 'text/html').body;
168
252
  const lines = linesOf(body, undefined);