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.
- package/README.md +6 -5
- package/dist/block/block.svelte.d.ts +26 -36
- package/dist/block/block.svelte.js +10 -7
- package/dist/block/block.utils.d.ts +19 -1
- package/dist/block/block.utils.js +2 -1
- package/dist/block/inlineBlock.svelte.d.ts +1 -4
- package/dist/block/inlineBlock.svelte.js +2 -1
- package/dist/clipboard/htmlFlow.d.ts +4 -2
- package/dist/clipboard/htmlFlow.js +101 -17
- package/dist/cloudflare/DocumentRoom.d.ts +250 -72
- package/dist/cloudflare/DocumentRoom.js +653 -91
- package/dist/cloudflare/history.d.ts +4 -4
- package/dist/cloudflare/history.js +3 -3
- package/dist/cloudflare/index.d.ts +3 -3
- package/dist/cloudflare/index.js +2 -2
- package/dist/cloudflare/locks.d.ts +1 -1
- package/dist/cloudflare/locks.js +1 -1
- package/dist/cloudflare/move.d.ts +11 -1
- package/dist/cloudflare/move.js +8 -3
- package/dist/cloudflare/routeDocumentSocket.d.ts +29 -5
- package/dist/cloudflare/routeDocumentSocket.js +30 -9
- package/dist/collaboration/awarenessSelection.d.ts +2 -2
- package/dist/collaboration/awarenessSelection.js +2 -2
- package/dist/collaboration/index.d.ts +1 -1
- package/dist/collaboration/index.js +1 -1
- package/dist/collaboration/providers.d.ts +0 -3
- package/dist/collaboration/providers.js +6 -11
- package/dist/components/Announcer.svelte +28 -0
- package/dist/components/Announcer.svelte.d.ts +7 -0
- package/dist/components/Block.svelte +15 -0
- package/dist/components/Edytor.svelte +46 -6
- package/dist/components/Edytor.svelte.d.ts +20 -4
- package/dist/crdt/attribution/attribution.d.ts +3 -3
- package/dist/crdt/data.d.ts +11 -0
- package/dist/crdt/data.js +62 -12
- package/dist/crdt/document.d.ts +31 -49
- package/dist/crdt/document.js +38 -53
- package/dist/crdt/edytor-doc.d.ts +34 -37
- package/dist/crdt/edytor-doc.js +18 -18
- package/dist/crdt/engine-api.d.ts +4 -5
- package/dist/crdt/flow.d.ts +1 -1
- package/dist/crdt/index.d.ts +39 -49
- package/dist/crdt/index.js +33 -54
- package/dist/crdt/migration/migrate.d.ts +5 -5
- package/dist/crdt/nodes.d.ts +2 -2
- package/dist/crdt/placement/model.d.ts +6 -6
- package/dist/crdt/protocol.d.ts +44 -0
- package/dist/crdt/protocol.js +62 -0
- package/dist/crdt/protocols/envelope.d.ts +1 -1
- package/dist/crdt/providers/bound.d.ts +2 -0
- package/dist/crdt/providers/bound.js +9 -0
- package/dist/crdt/providers/index.d.ts +6 -6
- package/dist/crdt/providers/indexeddb.d.ts +6 -6
- package/dist/crdt/providers/indexeddb.js +1 -1
- package/dist/crdt/providers/room.d.ts +19 -3
- package/dist/crdt/providers/room.js +54 -34
- package/dist/crdt/providers/websocket.d.ts +3 -4
- package/dist/crdt/rangeDelete.d.ts +1 -1
- package/dist/crdt/semantics.d.ts +112 -12
- package/dist/crdt/semantics.js +172 -14
- package/dist/crdt/text/runs.d.ts +22 -12
- package/dist/crdt/text/runs.js +17 -0
- package/dist/edytor.svelte.d.ts +32 -164
- package/dist/edytor.svelte.js +96 -48
- package/dist/events/beforeInputCommands.js +8 -6
- package/dist/events/onBeforeInput.js +6 -6
- package/dist/events/onCopy.js +2 -2
- package/dist/events/onDrop.js +2 -2
- package/dist/events/onKeyDown.js +4 -3
- package/dist/events/onPaste.js +5 -4
- package/dist/index.d.ts +26 -24
- package/dist/index.js +27 -23
- package/dist/kinds.d.ts +8 -3
- package/dist/kinds.js +13 -2
- package/dist/plugins/blockHandles/BlockHandle.svelte +15 -3
- package/dist/plugins/blockHandles/BlockHandleController.svelte.d.ts +11 -3
- package/dist/plugins/blockHandles/BlockHandleController.svelte.js +14 -3
- package/dist/plugins/blockHandles/blockHandlesPlugin.d.ts +3 -1
- package/dist/plugins/blockHandles/blockHandlesPlugin.js +1 -1
- package/dist/plugins/blockMenu/BlockMenu.svelte +52 -18
- package/dist/plugins/blockMenu/BlockMenuController.svelte.d.ts +9 -1
- package/dist/plugins/blockMenu/BlockMenuController.svelte.js +13 -2
- package/dist/plugins/code/CodeHeader.svelte +65 -0
- package/dist/plugins/code/CodeHeader.svelte.d.ts +9 -0
- package/dist/plugins/code/CodePlugin.svelte +222 -224
- package/dist/plugins/code/CodePlugin.svelte.d.ts +10 -0
- package/dist/plugins/code/code.css +27 -8
- package/dist/plugins/code/languages.d.ts +51 -0
- package/dist/plugins/code/languages.js +188 -0
- package/dist/plugins/columns/ColumnResize.svelte +24 -2
- package/dist/plugins/columns/ColumnsPlugin.svelte +2 -0
- package/dist/plugins/columns/resize.svelte.d.ts +34 -1
- package/dist/plugins/columns/resize.svelte.js +92 -23
- package/dist/plugins/find/FindBar.svelte +169 -0
- package/dist/plugins/find/FindBar.svelte.d.ts +9 -0
- package/dist/plugins/find/FindController.svelte.d.ts +62 -0
- package/dist/plugins/find/FindController.svelte.js +242 -0
- package/dist/plugins/find/FindHighlights.svelte +75 -0
- package/dist/plugins/find/FindHighlights.svelte.d.ts +9 -0
- package/dist/plugins/find/findPlugin.d.ts +25 -0
- package/dist/plugins/find/findPlugin.js +55 -0
- package/dist/plugins/find/search.d.ts +18 -0
- package/dist/plugins/find/search.js +41 -0
- package/dist/plugins/icons.js +10 -1
- package/dist/plugins/image/ImageChrome.svelte +212 -0
- package/dist/plugins/image/ImageChrome.svelte.d.ts +7 -0
- package/dist/plugins/image/ImageEmpty.svelte +38 -7
- package/dist/plugins/image/ImageEmpty.svelte.d.ts +4 -0
- package/dist/plugins/image/ImagePlugin.svelte +196 -40
- package/dist/plugins/image/ImagePlugin.svelte.d.ts +17 -4
- package/dist/plugins/image/controls.svelte.d.ts +111 -0
- package/dist/plugins/image/controls.svelte.js +284 -0
- package/dist/plugins/image/image.d.ts +19 -1
- package/dist/plugins/image/image.js +31 -1
- package/dist/plugins/image/uploads.svelte.d.ts +50 -0
- package/dist/plugins/image/uploads.svelte.js +111 -0
- package/dist/plugins/index.d.ts +11 -2
- package/dist/plugins/index.js +10 -2
- package/dist/plugins/media/AudioPlugin.svelte +86 -0
- package/dist/plugins/media/AudioPlugin.svelte.d.ts +28 -0
- package/dist/plugins/media/BookmarkPlugin.svelte +182 -0
- package/dist/plugins/media/BookmarkPlugin.svelte.d.ts +45 -0
- package/dist/plugins/media/EmbedPlugin.svelte +146 -0
- package/dist/plugins/media/EmbedPlugin.svelte.d.ts +37 -0
- package/dist/plugins/media/FilePlugin.svelte +112 -0
- package/dist/plugins/media/FilePlugin.svelte.d.ts +28 -0
- package/dist/plugins/media/MediaEmpty.svelte +107 -0
- package/dist/plugins/media/MediaEmpty.svelte.d.ts +21 -0
- package/dist/plugins/media/UrlPasteMenu.svelte +83 -0
- package/dist/plugins/media/UrlPasteMenu.svelte.d.ts +7 -0
- package/dist/plugins/media/VideoPlugin.svelte +95 -0
- package/dist/plugins/media/VideoPlugin.svelte.d.ts +29 -0
- package/dist/plugins/media/media.d.ts +72 -0
- package/dist/plugins/media/media.js +226 -0
- package/dist/plugins/media/urlPaste.svelte.d.ts +76 -0
- package/dist/plugins/media/urlPaste.svelte.js +197 -0
- package/dist/plugins/richtext/RichTextPlugin.svelte +102 -15
- package/dist/plugins/richtext/richTextOperations.d.ts +37 -2
- package/dist/plugins/richtext/richTextOperations.js +66 -1
- package/dist/plugins/slashMenu/SlashMenu.svelte +101 -42
- package/dist/plugins/slashMenu/SlashMenuController.svelte.d.ts +7 -0
- package/dist/plugins/slashMenu/SlashMenuController.svelte.js +8 -0
- package/dist/plugins/slashMenu/slashMenuPlugin.d.ts +2 -0
- package/dist/plugins/suggestions/SuggestionBars.svelte +1 -2
- package/dist/plugins/toolbar/LinkCard.svelte +117 -0
- package/dist/plugins/toolbar/LinkCard.svelte.d.ts +7 -0
- package/dist/plugins/toolbar/Toolbar.svelte +59 -3
- package/dist/plugins/toolbar/ToolbarController.svelte.d.ts +39 -1
- package/dist/plugins/toolbar/ToolbarController.svelte.js +122 -2
- package/dist/plugins/toolbar/toolbarPlugin.d.ts +5 -0
- package/dist/plugins/toolbar/toolbarPlugin.js +81 -0
- package/dist/plugins.d.ts +45 -44
- package/dist/selection/replaceSelection.d.ts +7 -1
- package/dist/selection/replaceSelection.js +20 -9
- package/dist/selection/selection.svelte.d.ts +11 -121
- package/dist/selection/selection.svelte.js +41 -15
- package/dist/session/announcer.svelte.d.ts +27 -0
- package/dist/session/announcer.svelte.js +34 -0
- package/dist/session/bindings.js +3 -1
- package/dist/session/commands.d.ts +38 -14
- package/dist/session/commands.js +95 -25
- package/dist/session/editing/text.d.ts +1 -1
- package/dist/session/history.d.ts +8 -1
- package/dist/session/history.js +10 -3
- package/dist/session/keymap.d.ts +1 -1
- package/dist/session/keymap.js +2 -2
- package/dist/session/moves.d.ts +3 -2
- package/dist/session/moves.js +6 -2
- package/dist/session/navigation.js +1 -1
- package/dist/session/selection.d.ts +3 -3
- package/dist/session/suggestions.svelte.d.ts +0 -2
- package/dist/session/virtual.js +3 -2
- package/dist/surface/attributes.d.ts +13 -0
- package/dist/surface/attributes.js +20 -0
- package/dist/surface/cells.d.ts +1 -1
- package/dist/surface/popups.svelte.d.ts +28 -0
- package/dist/surface/popups.svelte.js +32 -0
- package/dist/text/text.svelte.d.ts +2 -5
- package/dist/text/text.svelte.js +3 -2
- package/dist/text/text.utils.js +2 -1
- package/dist/themes/notion.css +171 -2
- package/dist/utils/json.d.ts +2 -0
- package/dist/utils/json.js +15 -0
- package/dist/utils.d.ts +10 -1
- package/dist/utils.js +22 -2
- 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
|
+
[](https://github.com/beynar/edytor/actions/workflows/ci.yml)
|
|
6
7
|
[](https://opensource.org/licenses/MIT)
|
|
7
8
|
[](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.
|
|
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)
|
|
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
|
|
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
|
-
*
|
|
55
|
-
* a container (a list) is never converted, its items are
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: (
|
|
115
|
+
addChildBlock: (payload: {
|
|
118
116
|
block: JSONBlock;
|
|
119
117
|
index: number;
|
|
120
118
|
}) => Block;
|
|
121
|
-
addChildBlocks: (
|
|
119
|
+
addChildBlocks: (payload: {
|
|
122
120
|
blocks: JSONBlock[];
|
|
123
121
|
index: number;
|
|
124
122
|
}) => Block[];
|
|
125
123
|
insertBlockAfter: (payload: {
|
|
126
124
|
block: JSONBlock;
|
|
127
|
-
}
|
|
125
|
+
}) => Block | null | undefined;
|
|
128
126
|
insertBlockBefore: (payload: {
|
|
129
127
|
block: JSONBlock;
|
|
130
|
-
}
|
|
128
|
+
}) => Block | null | undefined;
|
|
131
129
|
splitBlock: (payload: {
|
|
132
130
|
index: number;
|
|
133
131
|
text: Text;
|
|
134
|
-
}
|
|
135
|
-
duplicateBlock: (
|
|
132
|
+
}) => Block | null | undefined;
|
|
133
|
+
duplicateBlock: (payload?: {} | undefined) => Block | null | undefined;
|
|
136
134
|
removeBlock: (payload?: {
|
|
137
135
|
keepChildren?: boolean;
|
|
138
|
-
} | undefined
|
|
139
|
-
unNestBlock: (this: Block,
|
|
140
|
-
mergeBlockBackward: (
|
|
141
|
-
mergeBlockForward: (
|
|
142
|
-
nestBlock: (this: Block,
|
|
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
|
-
}
|
|
143
|
+
}) => void;
|
|
146
144
|
moveBlock: (this: Block, payload: {
|
|
147
145
|
path: number[];
|
|
148
146
|
beside?: import("./block.utils.js").BlockBeside;
|
|
149
|
-
}
|
|
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
|
-
}
|
|
155
|
-
pushContentIntoBlock: (
|
|
152
|
+
}) => Block[];
|
|
153
|
+
pushContentIntoBlock: (payload: {
|
|
156
154
|
value: (Text | InlineBlock)[];
|
|
157
155
|
}) => void;
|
|
158
156
|
removeInlineBlock: (payload: {
|
|
159
157
|
index: number;
|
|
160
|
-
}
|
|
158
|
+
}) => void;
|
|
161
159
|
patchData: (payload: {
|
|
162
160
|
ops: import("../crdt/index.js").DataPatch[];
|
|
163
161
|
atom?: string;
|
|
164
|
-
}
|
|
165
|
-
addInlineBlock: (
|
|
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
|
-
}
|
|
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
|
|
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
|
-
*
|
|
106
|
-
* a container (a list) is never converted, its items are
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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
|
-
*
|
|
35
|
-
*
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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 (
|
|
156
|
-
|
|
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 (!
|
|
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,
|
|
240
|
+
inline(child, at, {}, pre);
|
|
161
241
|
else
|
|
162
|
-
children.push(...blockOf(node, childType));
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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);
|