@quillmark/svelte 0.23.0 → 0.25.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.
- package/README.md +7 -8
- package/THEMING.md +1 -1
- package/dist/core/codec/blocks.d.ts +23 -6
- package/dist/core/codec/blocks.js +163 -38
- package/dist/core/codec/code.js +3 -8
- package/dist/core/codec/decode.d.ts +2 -1
- package/dist/core/codec/decode.js +19 -11
- package/dist/core/codec/field.d.ts +6 -3
- package/dist/core/codec/field.js +12 -4
- package/dist/core/codec/index.d.ts +2 -0
- package/dist/core/codec/index.js +4 -0
- package/dist/core/codec/inputrules.js +40 -10
- package/dist/core/codec/islands.d.ts +4 -1
- package/dist/core/codec/islands.js +43 -16
- package/dist/core/codec/keymap.js +19 -1
- package/dist/core/codec/line-paste.d.ts +7 -0
- package/dist/core/codec/line-paste.js +36 -0
- package/dist/core/codec/list-move.d.ts +6 -0
- package/dist/core/codec/list-move.js +30 -0
- package/dist/core/codec/list-paste.d.ts +2 -0
- package/dist/core/codec/list-paste.js +142 -0
- package/dist/core/codec/list-toggle.d.ts +4 -0
- package/dist/core/codec/list-toggle.js +170 -0
- package/dist/core/codec/lists.d.ts +22 -15
- package/dist/core/codec/lists.js +404 -97
- package/dist/core/codec/markdown-paste.d.ts +13 -3
- package/dist/core/codec/markdown-paste.js +70 -6
- package/dist/core/codec/nesting.d.ts +5 -1
- package/dist/core/codec/nesting.js +1 -1
- package/dist/core/codec/prose.css +36 -20
- package/dist/core/codec/slash.d.ts +2 -1
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/touching.d.ts +12 -0
- package/dist/core/codec/touching.js +33 -0
- package/dist/core/codec/undo.d.ts +5 -0
- package/dist/core/codec/undo.js +21 -0
- package/dist/index.js +1 -1
- package/dist/preview/Preview.svelte +88 -13
- package/dist/preview/Preview.svelte.d.ts +2 -2
- package/dist/preview/bridge.js +8 -2
- package/dist/preview/controller.d.ts +8 -4
- package/dist/preview/controller.js +10 -2
- package/dist/preview/index.js +1 -1
- package/dist/preview/paint.d.ts +6 -2
- package/dist/preview/paint.js +67 -56
- package/dist/preview/strings.d.ts +2 -0
- package/dist/preview/strings.js +4 -2
- package/dist/preview/viewport.d.ts +10 -0
- package/dist/preview/viewport.js +271 -0
- package/dist/preview/zoom.d.ts +41 -0
- package/dist/preview/zoom.js +52 -0
- package/dist/visual/ArrayField.svelte +24 -4
- package/dist/visual/Card.svelte +5 -4
- package/dist/visual/DateField.svelte +26 -13
- package/dist/visual/Field.svelte +2 -2
- package/dist/visual/FormatPopover.svelte +41 -2
- package/dist/visual/ObjectField.svelte +4 -7
- package/dist/visual/ObjectField.svelte.d.ts +2 -2
- package/dist/visual/ProseValue.svelte +35 -3
- package/dist/visual/ProseValue.svelte.d.ts +7 -0
- package/dist/visual/VisualEditorInner.svelte +5 -11
- package/dist/visual/icons/nodes.d.ts +26 -0
- package/dist/visual/icons/nodes.js +16 -0
- package/dist/visual/strings.d.ts +6 -4
- package/dist/visual/strings.js +9 -3
- package/dist/visual/structure.d.ts +22 -10
- package/dist/visual/structure.js +35 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,12 +14,12 @@ npm install @quillmark/svelte
|
|
|
14
14
|
|
|
15
15
|
Each subpath is its own module root; a bundler pulls only what the entry you import reaches.
|
|
16
16
|
|
|
17
|
-
| Import | Surface
|
|
18
|
-
| --------------------------- |
|
|
19
|
-
| `@quillmark/svelte/core` | What the surfaces share: the `DocPath`/`Place` address vocabulary, the `EditorError` channel, `init`.
|
|
20
|
-
| `@quillmark/svelte/preview` | The live preview: `createPreview` + `<Preview>`. Reaches nothing editor-side.
|
|
21
|
-
| `@quillmark/svelte/visual` | The federated WYSIWYG: `<VisualEditor>`, the codec's `createField` prose leaf.
|
|
22
|
-
| `@quillmark/svelte` | Re-exports `/core`.
|
|
17
|
+
| Import | Surface |
|
|
18
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| `@quillmark/svelte/core` | What the surfaces share: the `DocPath`/`Place` address vocabulary, the `EditorError` channel, `init`. No UI framework. |
|
|
20
|
+
| `@quillmark/svelte/preview` | The live preview: `createPreview` + `<Preview>`. Reaches nothing editor-side. |
|
|
21
|
+
| `@quillmark/svelte/visual` | The federated WYSIWYG: `<VisualEditor>`, the codec's `createField` prose leaf. |
|
|
22
|
+
| `@quillmark/svelte` | Re-exports `/core`. |
|
|
23
23
|
|
|
24
24
|
## Open a session
|
|
25
25
|
|
|
@@ -29,8 +29,7 @@ The **consumer** owns the session and the handles, and drives every edit; the su
|
|
|
29
29
|
import { Engine } from '@quillmark/wasm';
|
|
30
30
|
import { init } from '@quillmark/svelte/core';
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
// neither statically.
|
|
32
|
+
// Quill and Document are exported only through init(), which loads the wasm once.
|
|
34
33
|
const { Quill, Document } = await init();
|
|
35
34
|
|
|
36
35
|
const quill = Quill.fromTree(tree); // tree: Map<string, Uint8Array> of the quill dir
|
package/THEMING.md
CHANGED
|
@@ -175,7 +175,7 @@ An app that mounts an editor beside a preview over one session needs a screen to
|
|
|
175
175
|
| `.qm-switch` | The band that says which track is showing, and nothing at the widths where both are. Its controls read as even halves and stand at the touch floor (`--qmh-tap`, 44px); depth past that is yours. |
|
|
176
176
|
| `.qm-frame` | The mounting site: an edge, a corner, and the clip that holds a surface to them. What is inside it is the surface's own. |
|
|
177
177
|
|
|
178
|
-
The narrow shape is one mount at a time, not two of them stacked: half a narrow viewport is under the width either surface reads at. The track you are not showing is `display: none`, so it leaves the tab order, hit-testing and the accessibility tree together: you need no `inert` on it, and no breakpoint in your JS, since the only state you hold is which of the two you last asked for. Keep both mounted: the hidden pane keeps its pixels, its caret and its scroll, and comes back to them — it rasters nothing at a box it has none of, and repaints only where it returns to a box
|
|
178
|
+
The narrow shape is one mount at a time, not two of them stacked: half a narrow viewport is under the width either surface reads at. The track you are not showing is `display: none`, so it leaves the tab order, hit-testing and the accessibility tree together: you need no `inert` on it, and no breakpoint in your JS, since the only state you hold is which of the two you last asked for. Keep both mounted: the hidden pane keeps its pixels, its caret and its scroll, and comes back to them — it rasters nothing at a box it has none of, and repaints only where it returns to a box that draws its pages at another scale. A preview scroll asked for while it was hidden is held and run on the way back, so it opens at the caret rather than several edits behind it. A click that lands in the hidden pane is the one thing the shape adds to your wiring, so send the caret and reveal the track together, or the hit goes somewhere the reader cannot see.
|
|
179
179
|
|
|
180
180
|
The shape is shared; the look of a band is not. Your wordmark, your nav and the depth of your head are the most app-specific things on the screen, and a preset that drew them would hand every tool built on this the same face, so they read `--qmh-*` like everything else and you write the rules. `.qm-split`'s gap is a default rather than a fact about splitting: close it to `0` and carry a hairline on one pane if you want the two mounts to meet instead of stand apart.
|
|
181
181
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Attrs,
|
|
1
|
+
import type { Attrs, Schema } from 'prosemirror-model';
|
|
2
2
|
import type { Command, EditorState, Transaction } from 'prosemirror-state';
|
|
3
3
|
/** `# `: the heading `level` deep. `list_item` is `block+`, so one inside an item is a
|
|
4
4
|
* shape the content holds and `importMarkdown` produces from `- # title`. */
|
|
@@ -17,19 +17,36 @@ export declare function insertDivider(): Command;
|
|
|
17
17
|
* content holds, where a list wrapping one mints an item whose only content is an
|
|
18
18
|
* item. */
|
|
19
19
|
export declare function wrapInQuote(): Command;
|
|
20
|
-
/** `- ` / `1. `.
|
|
21
|
-
*
|
|
22
|
-
|
|
20
|
+
/** `- ` / `1. `. Beside a list of its kind the line joins it, whatever `N` is typed: a
|
|
21
|
+
* number comes from position. Elsewhere `N. ` is the number the new list counts from,
|
|
22
|
+
* and at the head of an item the shorthand is the item's (§`listAtItem`), `N` reading
|
|
23
|
+
* the same way; a marker the item already shows is taken as typed and changes nothing. */
|
|
24
|
+
export declare function wrapInList(ordered: boolean, attrs?: Attrs | null): Command;
|
|
25
|
+
/**
|
|
26
|
+
* `a. `, `(1) `, `(a) `: the markers a nested level draws, typed at the head of a nested
|
|
27
|
+
* item's first line, where the item already is the thing typed. Taken as typed and
|
|
28
|
+
* changing nothing, as `N. ` on an item of a numbered list is: the marker comes from the
|
|
29
|
+
* item's position and the quill's style for its depth, never from the text. Anywhere
|
|
30
|
+
* else these stay text, being no markdown list marker.
|
|
31
|
+
*/
|
|
32
|
+
export declare function takeNestedMarker(): Command;
|
|
23
33
|
/**
|
|
24
34
|
* A list pick, at whatever caret the writer has. `atHead` is the shorthand's guard,
|
|
25
35
|
* where a prefix firing mid-sentence would eat a dash; a pick is a gesture at the caret.
|
|
26
36
|
*
|
|
27
37
|
* At the end of a non-empty block it opens a fresh empty item below it, Enter then `- `
|
|
28
38
|
* as one gesture. Anywhere else it wraps the whole block, the smaller surprise: a writer
|
|
29
|
-
* who meant to split presses Enter first.
|
|
30
|
-
*
|
|
39
|
+
* who meant to split presses Enter first. In an item's first block it is the shorthand's
|
|
40
|
+
* item gesture (§`listAtItem`), offered only where that changes something.
|
|
31
41
|
*/
|
|
32
42
|
export declare function pickList(ordered: boolean): Command;
|
|
43
|
+
/**
|
|
44
|
+
* The bullet pick where it puts a line under an item one level deeper than the caret
|
|
45
|
+
* stood, and nowhere else: the pick a subparagraph names. Outside every item the bullet
|
|
46
|
+
* it would mint stands at the first level, and on a list's first item the bullet pick
|
|
47
|
+
* retypes the list instead.
|
|
48
|
+
*/
|
|
49
|
+
export declare function pickSubList(): Command;
|
|
33
50
|
/**
|
|
34
51
|
* Run `command` in place of the text at `[from, to)`: one transaction carrying the
|
|
35
52
|
* delete and the command's own steps. One rather than two because two would be two
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Selection } from 'prosemirror-state';
|
|
2
2
|
import { chainCommands, splitBlock } from 'prosemirror-commands';
|
|
3
|
+
import { sinkListItem } from 'prosemirror-schema-list';
|
|
3
4
|
import { canJoin, findWrapping } from 'prosemirror-transform';
|
|
4
5
|
import { withinNesting } from './nesting.js';
|
|
5
6
|
import { takesLineBreak } from './schema.js';
|
|
7
|
+
import { joinLists } from './touching.js';
|
|
6
8
|
/** The head of a textblock: where a shorthand fires, and where a pick lands once its
|
|
7
9
|
* run is consumed. */
|
|
8
10
|
function atHead(state) {
|
|
@@ -15,12 +17,9 @@ function fits($from, type) {
|
|
|
15
17
|
return $from.node(-1).canReplaceWith($from.index(-1), $from.indexAfter(-1), type);
|
|
16
18
|
}
|
|
17
19
|
/**
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* Firing there wraps the item's own paragraph in a fresh list, minting an item whose
|
|
22
|
-
* only content is another item — a level with nothing on it. Tab is the indent gesture
|
|
23
|
-
* and lands the shape a nesting is supposed to have, under the previous sibling. A
|
|
20
|
+
* The first block of an item that already exists: where a list gesture retypes the
|
|
21
|
+
* item's list rather than wrapping the block, which would mint an item whose only
|
|
22
|
+
* content is another item, a level with nothing on it. Tab is the indent gesture. A
|
|
24
23
|
* later block of the item is not this case: wrapping a continuation paragraph is how a
|
|
25
24
|
* sub-list opens under text.
|
|
26
25
|
*/
|
|
@@ -58,23 +57,23 @@ function spaceBreaks(tr, $from, type) {
|
|
|
58
57
|
at += child.nodeSize;
|
|
59
58
|
});
|
|
60
59
|
}
|
|
61
|
-
/** `wrappingInputRule`'s body: wrap the block range at `pos`, then join a
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* the one that does nests past
|
|
65
|
-
|
|
60
|
+
/** `wrappingInputRule`'s body: wrap the block range at `pos`, then join a sibling of
|
|
61
|
+
* the same type — the boundaries the wrap itself opens (`lists.ts` §cleanup:
|
|
62
|
+
* command-local, never a global pass). A list joins on both sides (`touching.ts`), a
|
|
63
|
+
* quote the one above. False where no wrap fits, or where the one that does nests past
|
|
64
|
+
* `MAX_NESTING_DEPTH` (`nesting.ts`). */
|
|
65
|
+
function wrapAt(tr, pos, type, attrs) {
|
|
66
66
|
const range = tr.doc.resolve(pos).blockRange();
|
|
67
67
|
const wrapping = range && findWrapping(range, type, attrs);
|
|
68
68
|
if (!range || !wrapping)
|
|
69
69
|
return false;
|
|
70
70
|
tr.wrap(range, wrapping);
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
tr.join(
|
|
77
|
-
}
|
|
71
|
+
const wrapped = tr.doc.nodeAt(range.start);
|
|
72
|
+
if (wrapped)
|
|
73
|
+
joinLists(tr, range.start + wrapped.nodeSize);
|
|
74
|
+
const before = tr.doc.resolve(range.start).nodeBefore;
|
|
75
|
+
if (before && before.type === type && canJoin(tr.doc, range.start))
|
|
76
|
+
tr.join(range.start);
|
|
78
77
|
return withinNesting(tr.doc);
|
|
79
78
|
}
|
|
80
79
|
/** `# `: the heading `level` deep. `list_item` is `block+`, so one inside an item is a
|
|
@@ -138,10 +137,96 @@ export function wrapInQuote() {
|
|
|
138
137
|
return true;
|
|
139
138
|
};
|
|
140
139
|
}
|
|
141
|
-
/** `- ` / `1. `.
|
|
142
|
-
*
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
/** `- ` / `1. `. Beside a list of its kind the line joins it, whatever `N` is typed: a
|
|
141
|
+
* number comes from position. Elsewhere `N. ` is the number the new list counts from,
|
|
142
|
+
* and at the head of an item the shorthand is the item's (§`listAtItem`), `N` reading
|
|
143
|
+
* the same way; a marker the item already shows is taken as typed and changes nothing. */
|
|
144
|
+
export function wrapInList(ordered, attrs = null) {
|
|
145
|
+
return (state, dispatch) => {
|
|
146
|
+
if (!atHead(state))
|
|
147
|
+
return false;
|
|
148
|
+
const { $from } = state.selection;
|
|
149
|
+
if (openingAnItem($from, state.schema.nodes.list_item))
|
|
150
|
+
return listAtItem(ordered, attrs?.start ?? null, true)(state, dispatch);
|
|
151
|
+
return wrapBlock(state, dispatch, ordered, attrs);
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* `a. `, `(1) `, `(a) `: the markers a nested level draws, typed at the head of a nested
|
|
156
|
+
* item's first line, where the item already is the thing typed. Taken as typed and
|
|
157
|
+
* changing nothing, as `N. ` on an item of a numbered list is: the marker comes from the
|
|
158
|
+
* item's position and the quill's style for its depth, never from the text. Anywhere
|
|
159
|
+
* else these stay text, being no markdown list marker.
|
|
160
|
+
*/
|
|
161
|
+
export function takeNestedMarker() {
|
|
162
|
+
return (state, dispatch) => {
|
|
163
|
+
const item = state.schema.nodes.list_item;
|
|
164
|
+
const { $from } = state.selection;
|
|
165
|
+
if (!atHead(state) || !openingAnItem($from, item) || $from.depth < 3)
|
|
166
|
+
return false;
|
|
167
|
+
if ($from.node(-3).type !== item)
|
|
168
|
+
return false;
|
|
169
|
+
dispatch?.(state.tr);
|
|
170
|
+
return true;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A list gesture in an item's first block. On a list's first item it retypes the list,
|
|
175
|
+
* every item with it: a list is one kind, and that item is where a writer changes it.
|
|
176
|
+
* `number` is the shorthand's `N`, the list's `start`, the one door back to 1 for a list
|
|
177
|
+
* that arrived counting from elsewhere. On a later item, a list of the other kind is a
|
|
178
|
+
* sub-list: the item sinks under the one above, as Tab sinks it, into a list of that
|
|
179
|
+
* kind — `- ` in a new numbered paragraph is its first lettered subparagraph, never the
|
|
180
|
+
* whole memo turned to bullets. A sub-list already standing there of that kind takes the
|
|
181
|
+
* item as its next.
|
|
182
|
+
*
|
|
183
|
+
* A gesture that changes nothing — the kind the item's list already is, and on a first
|
|
184
|
+
* item the number it already counts from — goes unoffered as a pick. As a shorthand
|
|
185
|
+
* (`take`) it is claimed and changes nothing, so the run is consumed: a writer typing
|
|
186
|
+
* `2. Second` into the second item meant the number the item already shows, and an item
|
|
187
|
+
* reading `2. 2. Second` is the one outcome no writer typed for. Backspace on the press
|
|
188
|
+
* after gives the text back.
|
|
189
|
+
*/
|
|
190
|
+
function listAtItem(ordered, number, take = false) {
|
|
191
|
+
return (state, dispatch) => {
|
|
192
|
+
const { $from } = state.selection;
|
|
193
|
+
const type = state.schema.nodes[ordered ? 'ordered_list' : 'bullet_list'];
|
|
194
|
+
const item = state.schema.nodes.list_item;
|
|
195
|
+
if (!type || !item || $from.depth < 2)
|
|
196
|
+
return false;
|
|
197
|
+
const list = $from.node(-2);
|
|
198
|
+
const unchanged = () => {
|
|
199
|
+
if (take)
|
|
200
|
+
dispatch?.(state.tr);
|
|
201
|
+
return take;
|
|
202
|
+
};
|
|
203
|
+
if ($from.index(-2) === 0) {
|
|
204
|
+
const start = number ?? (list.type === type ? list.attrs.start : 1);
|
|
205
|
+
if (list.type === type && (!ordered || start === list.attrs.start))
|
|
206
|
+
return unchanged();
|
|
207
|
+
dispatch?.(state.tr.setNodeMarkup($from.before(-2), type, ordered ? { start } : null));
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
if (list.type === type)
|
|
211
|
+
return unchanged();
|
|
212
|
+
let sunk;
|
|
213
|
+
if (!sinkListItem(item)(state, (tr) => (sunk = tr)) || !sunk)
|
|
214
|
+
return false;
|
|
215
|
+
const tr = sunk;
|
|
216
|
+
const $at = tr.doc.resolve(tr.mapping.map($from.pos));
|
|
217
|
+
const nested = $at.node(-2);
|
|
218
|
+
if (nested.childCount === 1 && nested.type !== type) {
|
|
219
|
+
const at = $at.before(-2);
|
|
220
|
+
tr.setNodeMarkup(at, type, ordered ? { start: number ?? 1 } : null);
|
|
221
|
+
const before = tr.doc.resolve(at).nodeBefore;
|
|
222
|
+
if (before?.type === type && canJoin(tr.doc, at))
|
|
223
|
+
tr.join(at);
|
|
224
|
+
}
|
|
225
|
+
if (!withinNesting(tr.doc))
|
|
226
|
+
return false;
|
|
227
|
+
dispatch?.(tr.scrollIntoView());
|
|
228
|
+
return true;
|
|
229
|
+
};
|
|
145
230
|
}
|
|
146
231
|
/**
|
|
147
232
|
* A list pick, at whatever caret the writer has. `atHead` is the shorthand's guard,
|
|
@@ -149,8 +234,8 @@ export function wrapInList(ordered, attrs = null, joinBefore) {
|
|
|
149
234
|
*
|
|
150
235
|
* At the end of a non-empty block it opens a fresh empty item below it, Enter then `- `
|
|
151
236
|
* as one gesture. Anywhere else it wraps the whole block, the smaller surprise: a writer
|
|
152
|
-
* who meant to split presses Enter first.
|
|
153
|
-
*
|
|
237
|
+
* who meant to split presses Enter first. In an item's first block it is the shorthand's
|
|
238
|
+
* item gesture (§`listAtItem`), offered only where that changes something.
|
|
154
239
|
*/
|
|
155
240
|
export function pickList(ordered) {
|
|
156
241
|
return (state, dispatch) => {
|
|
@@ -172,14 +257,47 @@ export function pickList(ordered) {
|
|
|
172
257
|
return true;
|
|
173
258
|
};
|
|
174
259
|
}
|
|
260
|
+
/** How many items the caret stands in. */
|
|
261
|
+
function itemDepth($pos, item) {
|
|
262
|
+
let n = 0;
|
|
263
|
+
for (let depth = $pos.depth; depth > 0; depth--)
|
|
264
|
+
if ($pos.node(depth).type === item)
|
|
265
|
+
n++;
|
|
266
|
+
return n;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The bullet pick where it puts a line under an item one level deeper than the caret
|
|
270
|
+
* stood, and nowhere else: the pick a subparagraph names. Outside every item the bullet
|
|
271
|
+
* it would mint stands at the first level, and on a list's first item the bullet pick
|
|
272
|
+
* retypes the list instead.
|
|
273
|
+
*/
|
|
274
|
+
export function pickSubList() {
|
|
275
|
+
const pick = pickList(false);
|
|
276
|
+
return (state, dispatch) => {
|
|
277
|
+
const item = state.schema.nodes.list_item;
|
|
278
|
+
const depth = item ? itemDepth(state.selection.$from, item) : 0;
|
|
279
|
+
if (!depth)
|
|
280
|
+
return false;
|
|
281
|
+
let picked;
|
|
282
|
+
if (!pick(state, (tr) => (picked = tr)) || !picked)
|
|
283
|
+
return false;
|
|
284
|
+
if (itemDepth(picked.selection.$from, item) <= depth)
|
|
285
|
+
return false;
|
|
286
|
+
dispatch?.(picked);
|
|
287
|
+
return true;
|
|
288
|
+
};
|
|
289
|
+
}
|
|
175
290
|
/** Wrap the caret's whole block in a list, wherever in the block the caret sits: the
|
|
176
|
-
* wrap opens at the block's head, so it joins the
|
|
177
|
-
|
|
291
|
+
* wrap opens at the block's head, so it joins the lists beside it as the shorthand's
|
|
292
|
+
* does. */
|
|
293
|
+
function wrapBlock(state, dispatch, ordered, attrs) {
|
|
178
294
|
const { paragraph, list_item: item } = state.schema.nodes;
|
|
179
295
|
const list = state.schema.nodes[ordered ? 'ordered_list' : 'bullet_list'];
|
|
180
296
|
const { $from } = state.selection;
|
|
181
|
-
if (!list || !paragraph
|
|
297
|
+
if (!list || !paragraph)
|
|
182
298
|
return false;
|
|
299
|
+
if (openingAnItem($from, item))
|
|
300
|
+
return listAtItem(ordered, null)(state, dispatch);
|
|
183
301
|
const pos = $from.start();
|
|
184
302
|
const tr = state.tr;
|
|
185
303
|
// A paragraph item wherever this fires: `# ` inside the item is the gesture that
|
|
@@ -187,7 +305,7 @@ function wrapBlock(state, dispatch, ordered, attrs, joinBefore) {
|
|
|
187
305
|
// that shape. Positions survive the retype — same content, same size.
|
|
188
306
|
if ($from.parent.type !== paragraph)
|
|
189
307
|
tr.setBlockType(pos, pos, paragraph);
|
|
190
|
-
if (!wrapAt(tr, pos, list, attrs
|
|
308
|
+
if (!wrapAt(tr, pos, list, attrs))
|
|
191
309
|
return false;
|
|
192
310
|
dispatch?.(tr);
|
|
193
311
|
return true;
|
|
@@ -224,18 +342,18 @@ function atBodyStart($from) {
|
|
|
224
342
|
* Enter at the very start of a body whose first block takes no caret above it → an
|
|
225
343
|
* empty paragraph there, the caret staying with the text it pushed down.
|
|
226
344
|
*
|
|
227
|
-
* A code block
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* can never be given a title. The argument is `paragraphAboveList`'s, about the
|
|
231
|
-
* block rather than about lists, so this link comes first and that one holds the
|
|
232
|
-
* position it alone reaches, a list under an island or a rule.
|
|
345
|
+
* A code block and a quote are blocks `prosemirror-gapcursor` declines to sit beside,
|
|
346
|
+
* and nothing else mints a block above one: two keystrokes on an empty body (` ``` `,
|
|
347
|
+
* `> `) put one first, and a document carrying one there could never be given a title.
|
|
233
348
|
*
|
|
234
349
|
* Declines wherever the first block is a plain textblock, whose Enter-split already
|
|
235
|
-
* leaves a paragraph above, and on an empty one, which the links after it answer.
|
|
350
|
+
* leaves a paragraph above, and on an empty one, which the links after it answer. An
|
|
351
|
+
* item's first line of prose declines too: Enter at its head is the list's new first
|
|
352
|
+
* item, as at every item, and the empty item's own Enter lifts it out to the paragraph a
|
|
353
|
+
* title takes. A fence opening that item is the fence case and takes the paragraph.
|
|
236
354
|
*/
|
|
237
355
|
const paragraphAtBodyStart = (state, dispatch) => {
|
|
238
|
-
const paragraph = state.schema.nodes
|
|
356
|
+
const { paragraph, list_item: item } = state.schema.nodes;
|
|
239
357
|
const { $from, empty } = state.selection;
|
|
240
358
|
if (!paragraph || !empty || !atBodyStart($from))
|
|
241
359
|
return false;
|
|
@@ -243,6 +361,8 @@ const paragraphAtBodyStart = (state, dispatch) => {
|
|
|
243
361
|
return false;
|
|
244
362
|
if ($from.depth === 1 && $from.parent.isTextblock && !$from.parent.type.spec.code)
|
|
245
363
|
return false;
|
|
364
|
+
if (item && $from.depth >= 2 && $from.node(-1).type === item && !$from.parent.type.spec.code)
|
|
365
|
+
return false;
|
|
246
366
|
dispatch?.(state.tr.insert(0, paragraph.create()).scrollIntoView());
|
|
247
367
|
return true;
|
|
248
368
|
};
|
|
@@ -310,7 +430,8 @@ const splitAcrossBlocks = (state, dispatch) => {
|
|
|
310
430
|
* are the whole of what holds them, and the key is whichever link answers a construct
|
|
311
431
|
* with nothing in it. An empty fence is not that case — the doc spares it, and it
|
|
312
432
|
* carries no text to lose. Declines with nothing after it too: a line with no next
|
|
313
|
-
* line is Backspace's.
|
|
433
|
+
* line is Backspace's. The line was the one thing between two lists of a kind, they
|
|
434
|
+
* are one list after it.
|
|
314
435
|
*/
|
|
315
436
|
const takeEmptyLine = (state, dispatch) => {
|
|
316
437
|
const { $from, empty } = state.selection;
|
|
@@ -321,7 +442,11 @@ const takeEmptyLine = (state, dispatch) => {
|
|
|
321
442
|
return false;
|
|
322
443
|
if (!Selection.findFrom(state.doc.resolve($from.after()), 1))
|
|
323
444
|
return false;
|
|
324
|
-
dispatch
|
|
445
|
+
if (dispatch) {
|
|
446
|
+
const tr = state.tr.delete($from.before(), $from.after());
|
|
447
|
+
joinLists(tr, $from.before());
|
|
448
|
+
dispatch(tr.scrollIntoView());
|
|
449
|
+
}
|
|
325
450
|
return true;
|
|
326
451
|
};
|
|
327
452
|
/**
|
package/dist/core/codec/code.js
CHANGED
|
@@ -75,14 +75,9 @@ const indentInCode = (state, dispatch) => {
|
|
|
75
75
|
/**
|
|
76
76
|
* Shift-Tab in a code block: remove one indent level from every covered line.
|
|
77
77
|
*
|
|
78
|
-
* Declines when no covered line carries an indent, so the key
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* a `list_item > code_block` the list link claims it and lifts the item out
|
|
82
|
-
* (`tests/codec/code-keys.test.ts`), so a fence in an item has no keyboard exit in
|
|
83
|
-
* either direction — Tab is a code block's whole point and takes the key
|
|
84
|
-
* unconditionally. What a body's keyboard exit should be settles in the shell keymap
|
|
85
|
-
* (VISUAL_EDITOR §Settled and open), not here.
|
|
78
|
+
* Declines when no covered line carries an indent, so the key falls through to the
|
|
79
|
+
* list link: inside a `list_item > code_block` it lifts the item out
|
|
80
|
+
* (`tests/codec/code-keys.test.ts`), and outside a list the body holds it (`keymap.ts`).
|
|
86
81
|
*/
|
|
87
82
|
const outdentInCode = (state, dispatch) => {
|
|
88
83
|
const block = codeBlockAt(state);
|
|
@@ -29,7 +29,8 @@ export declare function fitsInline(rt: Content): boolean;
|
|
|
29
29
|
export declare function fitsPlain(rt: Content): boolean;
|
|
30
30
|
/** Whether a leaf so declared edits `rt` losing nothing: {@link fitsInline} narrowed
|
|
31
31
|
* and {@link fitsPlain} plain, past which the decode drops what the first commit would
|
|
32
|
-
* store; on the block schema anything but an element
|
|
32
|
+
* store; on the block schema anything but a construct read and not edited — an element,
|
|
33
|
+
* a definition list ({@link editable}), a footnote or an equation (`isEditedIsland`) —
|
|
33
34
|
* and an island where the leaf mounts no island view (`islands`), which draws as its
|
|
34
35
|
* placeholder tag, an atom a keystroke deletes unseen. What a leaf held over the rest
|
|
35
36
|
* reads. */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// applied here; they are decorations (field.ts). Positions throughout are USV; `Array.from` iterates by
|
|
9
9
|
// code point so an astral char is one unit, never a surrogate half.
|
|
10
10
|
import { DOMSerializer } from 'prosemirror-model';
|
|
11
|
-
import { ISLAND_SLOT, isBlockIsland } from './islands.js';
|
|
11
|
+
import { ISLAND_SLOT, isBlockIsland, isEditedIsland } from './islands.js';
|
|
12
12
|
import { descriptorOf, markKey, pmMarkFromContent } from './marks.js';
|
|
13
13
|
import { core } from '../lifecycle.js';
|
|
14
14
|
import { hasMarks, isBlockSchema, isInlineSchema, takesLineBreak } from './schema.js';
|
|
@@ -96,7 +96,8 @@ export function fitsPlain(rt) {
|
|
|
96
96
|
}
|
|
97
97
|
/** Whether a leaf so declared edits `rt` losing nothing: {@link fitsInline} narrowed
|
|
98
98
|
* and {@link fitsPlain} plain, past which the decode drops what the first commit would
|
|
99
|
-
* store; on the block schema anything but an element
|
|
99
|
+
* store; on the block schema anything but a construct read and not edited — an element,
|
|
100
|
+
* a definition list ({@link editable}), a footnote or an equation (`isEditedIsland`) —
|
|
100
101
|
* and an island where the leaf mounts no island view (`islands`), which draws as its
|
|
101
102
|
* placeholder tag, an atom a keystroke deletes unseen. What a leaf held over the rest
|
|
102
103
|
* reads. */
|
|
@@ -105,11 +106,16 @@ export function fitsLeaf(rt, leaf) {
|
|
|
105
106
|
return fitsInline(rt);
|
|
106
107
|
if (leaf.plaintext)
|
|
107
108
|
return fitsPlain(rt);
|
|
108
|
-
|
|
109
|
+
if (!leaf.islands && rt.islands.length)
|
|
110
|
+
return false;
|
|
111
|
+
return rt.islands.every((isl) => isEditedIsland(isl.type)) && rt.lines.every(editable);
|
|
109
112
|
}
|
|
110
|
-
/** Whether
|
|
111
|
-
*
|
|
113
|
+
/** Whether the block schema edits `line`'s kind and every container on it: a term draws
|
|
114
|
+
* as a paragraph and an element and a description as the blocks they wrap, so the first
|
|
115
|
+
* commit would drop each. */
|
|
112
116
|
function editable(line) {
|
|
117
|
+
if (line.kind === 'term')
|
|
118
|
+
return false;
|
|
113
119
|
return line.containers.every((c) => c.container === 'quote' || c.container === 'list_item');
|
|
114
120
|
}
|
|
115
121
|
/** Inline decode: one paragraph, containers and islands stripped. */
|
|
@@ -192,9 +198,9 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
|
|
|
192
198
|
i = j;
|
|
193
199
|
continue;
|
|
194
200
|
}
|
|
195
|
-
if (here.container === 'element') {
|
|
196
|
-
// No node holds an element, and a leaf holding one is held
|
|
197
|
-
// what it wraps draws in place, read-only.
|
|
201
|
+
if (here.container === 'element' || here.container === 'description') {
|
|
202
|
+
// No node holds an element or a description, and a leaf holding one is held
|
|
203
|
+
// (`fitsLeaf`), so what it wraps draws in place, read-only.
|
|
198
204
|
const key = containerKey(here);
|
|
199
205
|
let j = i + 1;
|
|
200
206
|
while (j < leaves.length) {
|
|
@@ -249,7 +255,7 @@ function instanceOf(c) {
|
|
|
249
255
|
}
|
|
250
256
|
/** Identity of a container for run gathering: its name, its `instance` and an element's
|
|
251
257
|
* attributes, NUL-joined as `markKey` joins a mark's. `list_item` carries its own run
|
|
252
|
-
* rule, so the
|
|
258
|
+
* rule, so the rest are what key here. */
|
|
253
259
|
function containerKey(c) {
|
|
254
260
|
const attrs = c.container === 'element' ? JSON.stringify(c.attrs) : '';
|
|
255
261
|
return `${c.container}\u0000${instanceOf(c)}\u0000${attrs}`;
|
|
@@ -269,13 +275,15 @@ function makeLeaf(schema, leaf, marks, cursor) {
|
|
|
269
275
|
return schema.nodes.code_block.create({ lang: line.attrs?.lang ?? null }, content);
|
|
270
276
|
}
|
|
271
277
|
case 'para':
|
|
278
|
+
case 'term':
|
|
272
279
|
case 'heading': {
|
|
273
|
-
if (line.kind
|
|
280
|
+
if (line.kind !== 'heading' && blockIslandLine(leaf, cursor))
|
|
274
281
|
return schema.nodes.island_block.create(islandAttrs(cursor));
|
|
275
282
|
// Inline content, the segment boundary a `hard_break` in a paragraph and a space
|
|
276
283
|
// in a heading, which takes none (`schema.ts` §`takesLineBreak`). A stored heading
|
|
277
284
|
// carries no continuation — the store clears one after a block of a single line —
|
|
278
|
-
// so the space is what keeps a hand-built content decodable.
|
|
285
|
+
// so the space is what keeps a hand-built content decodable. A term draws as a
|
|
286
|
+
// paragraph, read-only (`fitsLeaf`).
|
|
279
287
|
const type = line.kind === 'heading' ? schema.nodes.heading : schema.nodes.paragraph;
|
|
280
288
|
const inline = [];
|
|
281
289
|
leaf.segments.forEach((seg, idx) => {
|
|
@@ -28,7 +28,8 @@ export interface CreateFieldOpts {
|
|
|
28
28
|
* leaf, or a `text` one, is held over a value its schema cannot
|
|
29
29
|
* hold (`fitsLeaf`, upstream's `isInline` and `isPlain`): the decode would join lines
|
|
30
30
|
* and drop containers and islands for the first commit to store. Any leaf is held
|
|
31
|
-
* over an element,
|
|
31
|
+
* over what is read and not edited: an element, a definition list, a footnote and an
|
|
32
|
+
* equation (CODEC §Decode). A held leaf draws
|
|
32
33
|
* its content on the block schema, read-only, and commits nothing. Judged of the value
|
|
33
34
|
* each mount and re-hydrate reads, so {@link FieldController.applyExternal} is what
|
|
34
35
|
* re-evaluates it.
|
|
@@ -199,7 +200,8 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
|
|
|
199
200
|
/**
|
|
200
201
|
* The prose-leaf plugin stack (VISUAL_EDITOR §Surface): shared by
|
|
201
202
|
* {@link createField} and the by-value editor (`ProseValue`), so the two
|
|
202
|
-
* never fork the keymap/plugin ordering. History first,
|
|
203
|
+
* never fork the keymap/plugin ordering. History first, with the plugin that starts an
|
|
204
|
+
* undo step at each block-moving edit (`undo.ts`), then any leaf-specific
|
|
203
205
|
* plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
|
|
204
206
|
* a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
|
|
205
207
|
* the rest of the stack leaves, then the markdown-shorthand input rules and the
|
|
@@ -207,7 +209,8 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
|
|
|
207
209
|
* that links a selection (`link-paste.ts`), and last the ones that answer to a block
|
|
208
210
|
* leaf's own shapes: the gap cursor, {@link pastAtomPlugin}, the island paste pass
|
|
209
211
|
* (`islands.ts`), the table paste (`table-paste.ts`), the markdown paste
|
|
210
|
-
* (`markdown-paste.ts`) and the task item's checkbox (`tasks.ts`).
|
|
212
|
+
* (`markdown-paste.ts`) and the task item's checkbox (`tasks.ts`). A one-line leaf's
|
|
213
|
+
* paste joins its lines (`line-paste.ts`).
|
|
211
214
|
*
|
|
212
215
|
* Every mark-shaped plugin reads the schema rather than a flag: over either mark-free
|
|
213
216
|
* schema the shorthand rules build nothing (each is guarded on its mark type), the
|
package/dist/core/codec/field.js
CHANGED
|
@@ -31,10 +31,13 @@ import { contentEqual, createReconciler } from './reconcile.js';
|
|
|
31
31
|
import { inputRulesPlugin, undoRuleKeymap } from './inputrules.js';
|
|
32
32
|
import { linebreakPlugin } from './breaks.js';
|
|
33
33
|
import { bodyKeymap } from './keymap.js';
|
|
34
|
-
import { blockSchema, isBlockSchema, leafSchema, plainSchema } from './schema.js';
|
|
34
|
+
import { blockSchema, isBlockSchema, isInlineSchema, leafSchema, plainSchema } from './schema.js';
|
|
35
35
|
import { linkPastePlugin } from './link-paste.js';
|
|
36
|
+
import { linePastePlugin } from './line-paste.js';
|
|
37
|
+
import { listPastePlugin } from './list-paste.js';
|
|
36
38
|
import { markdownPastePlugin } from './markdown-paste.js';
|
|
37
39
|
import { nestingPlugin } from './nesting.js';
|
|
40
|
+
import { undoStepPlugin } from './undo.js';
|
|
38
41
|
import { tablePastePlugin } from './table-paste.js';
|
|
39
42
|
import { tableCopyPlugin } from './table-copy.js';
|
|
40
43
|
import { taskPlugin } from './tasks.js';
|
|
@@ -533,7 +536,8 @@ export function createField(opts) {
|
|
|
533
536
|
/**
|
|
534
537
|
* The prose-leaf plugin stack (VISUAL_EDITOR §Surface): shared by
|
|
535
538
|
* {@link createField} and the by-value editor (`ProseValue`), so the two
|
|
536
|
-
* never fork the keymap/plugin ordering. History first,
|
|
539
|
+
* never fork the keymap/plugin ordering. History first, with the plugin that starts an
|
|
540
|
+
* undo step at each block-moving edit (`undo.ts`), then any leaf-specific
|
|
537
541
|
* plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
|
|
538
542
|
* a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
|
|
539
543
|
* the rest of the stack leaves, then the markdown-shorthand input rules and the
|
|
@@ -541,7 +545,8 @@ export function createField(opts) {
|
|
|
541
545
|
* that links a selection (`link-paste.ts`), and last the ones that answer to a block
|
|
542
546
|
* leaf's own shapes: the gap cursor, {@link pastAtomPlugin}, the island paste pass
|
|
543
547
|
* (`islands.ts`), the table paste (`table-paste.ts`), the markdown paste
|
|
544
|
-
* (`markdown-paste.ts`) and the task item's checkbox (`tasks.ts`).
|
|
548
|
+
* (`markdown-paste.ts`) and the task item's checkbox (`tasks.ts`). A one-line leaf's
|
|
549
|
+
* paste joins its lines (`line-paste.ts`).
|
|
545
550
|
*
|
|
546
551
|
* Every mark-shaped plugin reads the schema rather than a flag: over either mark-free
|
|
547
552
|
* schema the shorthand rules build nothing (each is guarded on its mark type), the
|
|
@@ -551,6 +556,7 @@ export function createField(opts) {
|
|
|
551
556
|
export function proseLeafPlugins(schema, opts) {
|
|
552
557
|
const list = [
|
|
553
558
|
history(),
|
|
559
|
+
undoStepPlugin(),
|
|
554
560
|
...(opts.cap ? [capPlugin(opts.cap)] : []),
|
|
555
561
|
...(opts.afterHistory ?? []),
|
|
556
562
|
linebreakPlugin(schema)
|
|
@@ -566,9 +572,11 @@ export function proseLeafPlugins(schema, opts) {
|
|
|
566
572
|
// Each answers to something only the block schema holds: any other leaf is
|
|
567
573
|
// textblocks alone, with no island and no gap to put a cursor in.
|
|
568
574
|
if (isBlockSchema(schema))
|
|
569
|
-
list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin(), tablePastePlugin(), tableCopyPlugin(), markdownPastePlugin(), taskPlugin(), nestingPlugin());
|
|
575
|
+
list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin(), tablePastePlugin(), tableCopyPlugin(), markdownPastePlugin(), listPastePlugin(), taskPlugin(), nestingPlugin());
|
|
570
576
|
if (schema === plainSchema)
|
|
571
577
|
list.push(plainClipboardPlugin());
|
|
578
|
+
if (isInlineSchema(schema))
|
|
579
|
+
list.push(linePastePlugin());
|
|
572
580
|
if (opts.placeholder)
|
|
573
581
|
list.push(ghostPlugin(opts));
|
|
574
582
|
return list;
|
|
@@ -3,6 +3,7 @@ export type { RangeAnchor } from './anchor.js';
|
|
|
3
3
|
export { createField, emptyContent, heldAttributes, isEmptyContent, proseAttributes, proseLeafPlugins } from './field.js';
|
|
4
4
|
export type { CreateFieldOpts, FieldController, LeafViews } from './field.js';
|
|
5
5
|
export { capCount } from './cap.js';
|
|
6
|
+
export { joinLines, pastedLines } from './line-paste.js';
|
|
6
7
|
export type { TextCap } from './cap.js';
|
|
7
8
|
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
8
9
|
export type { TableChromeStrings } from './table-view.js';
|
|
@@ -20,3 +21,4 @@ export type { Reconciler } from './reconcile.js';
|
|
|
20
21
|
export { inputRulesPlugin } from './inputrules.js';
|
|
21
22
|
export { bodyKeymap } from './keymap.js';
|
|
22
23
|
export { listKeymap } from './lists.js';
|
|
24
|
+
export { toggleList } from './list-toggle.js';
|
package/dist/core/codec/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { rangeAnchor } from './anchor.js';
|
|
|
15
15
|
// The prose leaf.
|
|
16
16
|
export { createField, emptyContent, heldAttributes, isEmptyContent, proseAttributes, proseLeafPlugins } from './field.js';
|
|
17
17
|
export { capCount } from './cap.js';
|
|
18
|
+
export { joinLines, pastedLines } from './line-paste.js';
|
|
18
19
|
// The table island's chrome vocabulary: the visual tier's `strings` set extends it,
|
|
19
20
|
// so the island's wording is overridden beside every other key.
|
|
20
21
|
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
@@ -44,3 +45,6 @@ export { inputRulesPlugin } from './inputrules.js';
|
|
|
44
45
|
// (`bodyKeymap`); the suite drives both it and the list link directly.
|
|
45
46
|
export { bodyKeymap } from './keymap.js';
|
|
46
47
|
export { listKeymap } from './lists.js';
|
|
48
|
+
// The list toggle the keymap binds to Mod-Shift-7/8, and the selection toolbar's list
|
|
49
|
+
// buttons run (`visual/FormatPopover.svelte`).
|
|
50
|
+
export { toggleList } from './list-toggle.js';
|