@quillmark/svelte 0.24.0 → 0.26.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/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/field.d.ts +4 -2
- package/dist/core/codec/field.js +13 -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/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 +197 -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 +9 -0
- package/dist/core/codec/markdown-paste.js +63 -2
- package/dist/core/codec/nesting.d.ts +5 -1
- package/dist/core/codec/nesting.js +1 -1
- package/dist/core/codec/prose.css +21 -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/core/codec/word-paste.d.ts +5 -0
- package/dist/core/codec/word-paste.js +87 -0
- 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/DateField.svelte +26 -13
- package/dist/visual/FormatPopover.svelte +41 -2
- package/dist/visual/ProseValue.svelte +35 -3
- package/dist/visual/ProseValue.svelte.d.ts +7 -0
- package/dist/visual/icons/nodes.d.ts +26 -0
- package/dist/visual/icons/nodes.js +16 -0
- package/dist/visual/strings.d.ts +4 -2
- package/dist/visual/strings.js +9 -3
- package/package.json +1 -1
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);
|
|
@@ -200,7 +200,8 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
|
|
|
200
200
|
/**
|
|
201
201
|
* The prose-leaf plugin stack (VISUAL_EDITOR §Surface): shared by
|
|
202
202
|
* {@link createField} and the by-value editor (`ProseValue`), so the two
|
|
203
|
-
* 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
|
|
204
205
|
* plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
|
|
205
206
|
* a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
|
|
206
207
|
* the rest of the stack leaves, then the markdown-shorthand input rules and the
|
|
@@ -208,7 +209,8 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
|
|
|
208
209
|
* that links a selection (`link-paste.ts`), and last the ones that answer to a block
|
|
209
210
|
* leaf's own shapes: the gap cursor, {@link pastAtomPlugin}, the island paste pass
|
|
210
211
|
* (`islands.ts`), the table paste (`table-paste.ts`), the markdown paste
|
|
211
|
-
* (`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`).
|
|
212
214
|
*
|
|
213
215
|
* Every mark-shaped plugin reads the schema rather than a flag: over either mark-free
|
|
214
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,14 @@ 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';
|
|
39
|
+
import { wordPastePlugin } from './word-paste.js';
|
|
37
40
|
import { nestingPlugin } from './nesting.js';
|
|
41
|
+
import { undoStepPlugin } from './undo.js';
|
|
38
42
|
import { tablePastePlugin } from './table-paste.js';
|
|
39
43
|
import { tableCopyPlugin } from './table-copy.js';
|
|
40
44
|
import { taskPlugin } from './tasks.js';
|
|
@@ -533,7 +537,8 @@ export function createField(opts) {
|
|
|
533
537
|
/**
|
|
534
538
|
* The prose-leaf plugin stack (VISUAL_EDITOR §Surface): shared by
|
|
535
539
|
* {@link createField} and the by-value editor (`ProseValue`), so the two
|
|
536
|
-
* never fork the keymap/plugin ordering. History first,
|
|
540
|
+
* never fork the keymap/plugin ordering. History first, with the plugin that starts an
|
|
541
|
+
* undo step at each block-moving edit (`undo.ts`), then any leaf-specific
|
|
537
542
|
* plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
|
|
538
543
|
* a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
|
|
539
544
|
* the rest of the stack leaves, then the markdown-shorthand input rules and the
|
|
@@ -541,7 +546,8 @@ export function createField(opts) {
|
|
|
541
546
|
* that links a selection (`link-paste.ts`), and last the ones that answer to a block
|
|
542
547
|
* leaf's own shapes: the gap cursor, {@link pastAtomPlugin}, the island paste pass
|
|
543
548
|
* (`islands.ts`), the table paste (`table-paste.ts`), the markdown paste
|
|
544
|
-
* (`markdown-paste.ts`) and the task item's checkbox (`tasks.ts`).
|
|
549
|
+
* (`markdown-paste.ts`) and the task item's checkbox (`tasks.ts`). A one-line leaf's
|
|
550
|
+
* paste joins its lines (`line-paste.ts`).
|
|
545
551
|
*
|
|
546
552
|
* Every mark-shaped plugin reads the schema rather than a flag: over either mark-free
|
|
547
553
|
* schema the shorthand rules build nothing (each is guarded on its mark type), the
|
|
@@ -551,6 +557,7 @@ export function createField(opts) {
|
|
|
551
557
|
export function proseLeafPlugins(schema, opts) {
|
|
552
558
|
const list = [
|
|
553
559
|
history(),
|
|
560
|
+
undoStepPlugin(),
|
|
554
561
|
...(opts.cap ? [capPlugin(opts.cap)] : []),
|
|
555
562
|
...(opts.afterHistory ?? []),
|
|
556
563
|
linebreakPlugin(schema)
|
|
@@ -566,9 +573,11 @@ export function proseLeafPlugins(schema, opts) {
|
|
|
566
573
|
// Each answers to something only the block schema holds: any other leaf is
|
|
567
574
|
// textblocks alone, with no island and no gap to put a cursor in.
|
|
568
575
|
if (isBlockSchema(schema))
|
|
569
|
-
list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin(), tablePastePlugin(), tableCopyPlugin(), markdownPastePlugin(), taskPlugin(), nestingPlugin());
|
|
576
|
+
list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin(), tablePastePlugin(), tableCopyPlugin(), markdownPastePlugin(), wordPastePlugin(), listPastePlugin(), taskPlugin(), nestingPlugin());
|
|
570
577
|
if (schema === plainSchema)
|
|
571
578
|
list.push(plainClipboardPlugin());
|
|
579
|
+
if (isInlineSchema(schema))
|
|
580
|
+
list.push(linePastePlugin());
|
|
572
581
|
if (opts.placeholder)
|
|
573
582
|
list.push(ghostPlugin(opts));
|
|
574
583
|
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';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Input rules: the markdown-shorthand plugin the codec owns (
|
|
2
|
-
// `~~`→strike, `` ` ``→code, `# `→heading, `- `/`1. `→lists,
|
|
3
|
-
// `---`→divider), and the fence, which fires on Enter
|
|
1
|
+
// Input rules: the markdown-shorthand plugin the codec owns (`**`/`__`→strong,
|
|
2
|
+
// `*`/`_`→em, `~~`→strike, `` ` ``→code, `# `→heading, `- `/`1. `/`1) `→lists,
|
|
3
|
+
// `> `→blockquote, `---`→divider, a typed URL→link), and the fence, which fires on Enter
|
|
4
|
+
// rather than as typed.
|
|
4
5
|
// Mark rules are hand-rolled (prosemirror-inputrules ships only node rules); every
|
|
5
6
|
// block rule is one regexp over a command in `blocks.ts`, which is the slash menu's
|
|
6
7
|
// implementation too. These produce transactions the codec already lowers: they add no
|
|
@@ -10,11 +11,14 @@
|
|
|
10
11
|
// another gesture owns — never on how a quill renders it. What a quill typesets is not
|
|
11
12
|
// knowable here (a quill this build has not seen is the open-sets case, CODEC §"Open
|
|
12
13
|
// sets"), and a construct that arrives through `importMarkdown` must stay authorable or
|
|
13
|
-
// a document opens carrying a shape the editor refuses to make. So one
|
|
14
|
-
// `- ` / `1. ` at the head of an existing item
|
|
14
|
+
// a document opens carrying a shape the editor refuses to make. So one rule answers
|
|
15
|
+
// otherwise: `- ` / `1. ` at the head of an existing item retypes the item's list, where
|
|
16
|
+
// a wrap would nest and Tab owns the nesting. A marker the item already shows, and `a. `,
|
|
17
|
+
// `(1) `, `(a) ` in a nested item, are taken as typed and change nothing.
|
|
15
18
|
import { InputRule, inputRules, undoInputRule } from 'prosemirror-inputrules';
|
|
16
19
|
import { keymap } from 'prosemirror-keymap';
|
|
17
|
-
import {
|
|
20
|
+
import { rendersHref } from './urls.js';
|
|
21
|
+
import { consuming, insertDivider, takeNestedMarker, toCodeBlock, toHeading, wrapInList, wrapInQuote } from './blocks.js';
|
|
18
22
|
/**
|
|
19
23
|
* A mark input rule: when `regexp` (ending in the closing delimiter) matches
|
|
20
24
|
* before the caret, wrap capture group 1 in `markType` and drop the delimiters.
|
|
@@ -46,6 +50,25 @@ function markInputRule(regexp, markType, delimLen) {
|
|
|
46
50
|
return tr;
|
|
47
51
|
});
|
|
48
52
|
}
|
|
53
|
+
/** A typed URL, closed by the space after it: a scheme `rendersHref` follows, or a bare
|
|
54
|
+
* `www.`, less the punctuation around it. */
|
|
55
|
+
const TYPED_URL = /(?:^|[\s(["'])((?:[a-z][a-z0-9+.-]*:\/\/|www\.)[^\s.]\S*?)([.,;:!?)\]'"]*)\s$/i;
|
|
56
|
+
/** The link a typed URL takes, as a paste over a selection does (`link-paste.ts`): the
|
|
57
|
+
* URL's own text, with a bare `www.` read as `https://`. The space that closed it is
|
|
58
|
+
* typed after the link. Declined over a span already linked or set as code. */
|
|
59
|
+
function autolinkRule(link, code) {
|
|
60
|
+
return new InputRule(TYPED_URL, (state, match, start, end) => {
|
|
61
|
+
const [whole, url, trail] = match;
|
|
62
|
+
const from = start + whole.length - 1 - trail.length - url.length;
|
|
63
|
+
const to = from + url.length;
|
|
64
|
+
const href = /^www\./i.test(url) ? `https://${url}` : url;
|
|
65
|
+
if (!rendersHref(href) || state.doc.rangeHasMark(from, to, link))
|
|
66
|
+
return null;
|
|
67
|
+
if (code && state.doc.rangeHasMark(from, to, code))
|
|
68
|
+
return null;
|
|
69
|
+
return state.tr.addMark(from, to, link.create({ href })).insertText(whole.slice(-1), end);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
49
72
|
/** A block shorthand: the regexp that fires it, and the command it runs in the match's
|
|
50
73
|
* place. Every one is `^`-anchored, so the command's own head-of-a-textblock guard is
|
|
51
74
|
* the same position the regexp already stands at. The caret it places is scrolled to
|
|
@@ -59,23 +82,30 @@ export function markdownInputRules(schema) {
|
|
|
59
82
|
const m = schema.marks;
|
|
60
83
|
// Inline marks. Non-greedy capture between matching delimiters. Built only where the
|
|
61
84
|
// schema has the mark: the inline schema omits them, and a `text` one declares none
|
|
62
|
-
// at all.
|
|
85
|
+
// at all. An underscore pair opens only past a word, so `snake_case_name` stays text.
|
|
63
86
|
if (m.strong)
|
|
64
87
|
rules.push(markInputRule(/\*\*([^*]+)\*\*$/, m.strong, 2));
|
|
88
|
+
if (m.strong)
|
|
89
|
+
rules.push(markInputRule(/(?:^|[^\w])__([^_]+)__$/, m.strong, 2));
|
|
65
90
|
if (m.em)
|
|
66
91
|
rules.push(markInputRule(/(?:^|[^*])\*([^*]+)\*$/, m.em, 1));
|
|
92
|
+
if (m.em)
|
|
93
|
+
rules.push(markInputRule(/(?:^|[^\w])_([^_]+)_$/, m.em, 1));
|
|
67
94
|
if (m.strike)
|
|
68
95
|
rules.push(markInputRule(/~~([^~]+)~~$/, m.strike, 2));
|
|
69
96
|
if (m.code)
|
|
70
97
|
rules.push(markInputRule(/`([^`]+)`$/, m.code, 1));
|
|
98
|
+
if (m.link)
|
|
99
|
+
rules.push(autolinkRule(m.link, m.code));
|
|
71
100
|
// Block shorthands. Each command answers for the schema it is run against, so a
|
|
72
101
|
// schema without the node declines rather than being guarded here.
|
|
73
102
|
rules.push(blockRule(/^(#{1,6})\s$/, (match) => toHeading(match[1].length)));
|
|
74
103
|
rules.push(blockRule(/^---$/, insertDivider));
|
|
75
|
-
// `- ` / `1. ` at the start of a block, here or nested inside an item
|
|
76
|
-
//
|
|
104
|
+
// `- ` / `1. ` / `1) ` at the start of a block, here or nested inside an item; at the
|
|
105
|
+
// start of an item's own first block the command retypes the item's list instead.
|
|
77
106
|
rules.push(blockRule(/^\s*([-+*])\s$/, () => wrapInList(false)));
|
|
78
|
-
rules.push(blockRule(/^(\d+)
|
|
107
|
+
rules.push(blockRule(/^(\d+)[.)]\s$/, (match) => wrapInList(true, { start: +match[1] })));
|
|
108
|
+
rules.push(blockRule(/^(?:[a-z]\.|\((?:\d+|[a-z])\))\s$/, takeNestedMarker));
|
|
79
109
|
rules.push(blockRule(/^\s*>\s$/, wrapInQuote));
|
|
80
110
|
return rules;
|
|
81
111
|
}
|
|
@@ -4,6 +4,7 @@ import { blockKeymap } from './blocks.js';
|
|
|
4
4
|
import { breakKeymap } from './breaks.js';
|
|
5
5
|
import { codeKeymap } from './code.js';
|
|
6
6
|
import { listKeymap } from './lists.js';
|
|
7
|
+
import { listMoveKeymap } from './list-move.js';
|
|
7
8
|
import { slashKeymap } from './slash.js';
|
|
8
9
|
/** One chain per key, the maps taken in precedence order: on a key several bind, an
|
|
9
10
|
* earlier map's command runs first and the next runs only if it declines. A key one
|
|
@@ -17,6 +18,23 @@ function chainKeymaps(...maps) {
|
|
|
17
18
|
}
|
|
18
19
|
return out;
|
|
19
20
|
}
|
|
21
|
+
/** The last link of a document body: Tab and Shift-Tab held wherever no surface took
|
|
22
|
+
* them, and Escape giving the focus up, so the browser's next Tab leaves from the body.
|
|
23
|
+
* `{}` over `plainSchema`, a form field whose Tab is field navigation. */
|
|
24
|
+
function exitKeymap(schema) {
|
|
25
|
+
if (!schema.nodes.list_item)
|
|
26
|
+
return {};
|
|
27
|
+
return {
|
|
28
|
+
Tab: () => true,
|
|
29
|
+
'Shift-Tab': () => true,
|
|
30
|
+
Escape: (_state, _dispatch, view) => {
|
|
31
|
+
if (!view)
|
|
32
|
+
return false;
|
|
33
|
+
view.dom.blur();
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
20
38
|
/** A multi-block leaf's structural keys, each link binding only what its schema
|
|
21
39
|
* declares: over `plainSchema`, which holds neither lists nor code blocks nor islands,
|
|
22
40
|
* the block link and the break link alone. The argument list is the precedence order
|
|
@@ -29,5 +47,5 @@ function chainKeymaps(...maps) {
|
|
|
29
47
|
* no visible reason. */
|
|
30
48
|
export function bodyKeymap(schema, slash) {
|
|
31
49
|
const links = slash ? [slashKeymap()] : [];
|
|
32
|
-
return chainKeymaps(...links, blockKeymap(schema), codeKeymap(schema), breakKeymap(schema), listKeymap(schema), atomKeymap(schema));
|
|
50
|
+
return chainKeymaps(...links, blockKeymap(schema), codeKeymap(schema), breakKeymap(schema), listKeymap(schema), listMoveKeymap(schema), atomKeymap(schema), exitKeymap(schema));
|
|
33
51
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Fragment, Slice, type Node as PMNode, type Schema } from 'prosemirror-model';
|
|
2
|
+
import { Plugin } from 'prosemirror-state';
|
|
3
|
+
/** The lines a slice holds that hold anything, in order. */
|
|
4
|
+
export declare function pastedLines(slice: Slice): PMNode[];
|
|
5
|
+
/** `lines` as the content of one line, a space between each two. */
|
|
6
|
+
export declare function joinLines(lines: readonly PMNode[], schema: Schema): Fragment;
|
|
7
|
+
export declare function linePastePlugin(): Plugin;
|