@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.
Files changed (68) hide show
  1. package/README.md +7 -8
  2. package/THEMING.md +1 -1
  3. package/dist/core/codec/blocks.d.ts +23 -6
  4. package/dist/core/codec/blocks.js +163 -38
  5. package/dist/core/codec/code.js +3 -8
  6. package/dist/core/codec/decode.d.ts +2 -1
  7. package/dist/core/codec/decode.js +19 -11
  8. package/dist/core/codec/field.d.ts +6 -3
  9. package/dist/core/codec/field.js +12 -4
  10. package/dist/core/codec/index.d.ts +2 -0
  11. package/dist/core/codec/index.js +4 -0
  12. package/dist/core/codec/inputrules.js +40 -10
  13. package/dist/core/codec/islands.d.ts +4 -1
  14. package/dist/core/codec/islands.js +43 -16
  15. package/dist/core/codec/keymap.js +19 -1
  16. package/dist/core/codec/line-paste.d.ts +7 -0
  17. package/dist/core/codec/line-paste.js +36 -0
  18. package/dist/core/codec/list-move.d.ts +6 -0
  19. package/dist/core/codec/list-move.js +30 -0
  20. package/dist/core/codec/list-paste.d.ts +2 -0
  21. package/dist/core/codec/list-paste.js +142 -0
  22. package/dist/core/codec/list-toggle.d.ts +4 -0
  23. package/dist/core/codec/list-toggle.js +170 -0
  24. package/dist/core/codec/lists.d.ts +22 -15
  25. package/dist/core/codec/lists.js +404 -97
  26. package/dist/core/codec/markdown-paste.d.ts +13 -3
  27. package/dist/core/codec/markdown-paste.js +70 -6
  28. package/dist/core/codec/nesting.d.ts +5 -1
  29. package/dist/core/codec/nesting.js +1 -1
  30. package/dist/core/codec/prose.css +36 -20
  31. package/dist/core/codec/slash.d.ts +2 -1
  32. package/dist/core/codec/slash.js +0 -0
  33. package/dist/core/codec/touching.d.ts +12 -0
  34. package/dist/core/codec/touching.js +33 -0
  35. package/dist/core/codec/undo.d.ts +5 -0
  36. package/dist/core/codec/undo.js +21 -0
  37. package/dist/index.js +1 -1
  38. package/dist/preview/Preview.svelte +88 -13
  39. package/dist/preview/Preview.svelte.d.ts +2 -2
  40. package/dist/preview/bridge.js +8 -2
  41. package/dist/preview/controller.d.ts +8 -4
  42. package/dist/preview/controller.js +10 -2
  43. package/dist/preview/index.js +1 -1
  44. package/dist/preview/paint.d.ts +6 -2
  45. package/dist/preview/paint.js +67 -56
  46. package/dist/preview/strings.d.ts +2 -0
  47. package/dist/preview/strings.js +4 -2
  48. package/dist/preview/viewport.d.ts +10 -0
  49. package/dist/preview/viewport.js +271 -0
  50. package/dist/preview/zoom.d.ts +41 -0
  51. package/dist/preview/zoom.js +52 -0
  52. package/dist/visual/ArrayField.svelte +24 -4
  53. package/dist/visual/Card.svelte +5 -4
  54. package/dist/visual/DateField.svelte +26 -13
  55. package/dist/visual/Field.svelte +2 -2
  56. package/dist/visual/FormatPopover.svelte +41 -2
  57. package/dist/visual/ObjectField.svelte +4 -7
  58. package/dist/visual/ObjectField.svelte.d.ts +2 -2
  59. package/dist/visual/ProseValue.svelte +35 -3
  60. package/dist/visual/ProseValue.svelte.d.ts +7 -0
  61. package/dist/visual/VisualEditorInner.svelte +5 -11
  62. package/dist/visual/icons/nodes.d.ts +26 -0
  63. package/dist/visual/icons/nodes.js +16 -0
  64. package/dist/visual/strings.d.ts +6 -4
  65. package/dist/visual/strings.js +9 -3
  66. package/dist/visual/structure.d.ts +22 -10
  67. package/dist/visual/structure.js +35 -11
  68. 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`. Framework-free. |
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
- // One-time, and the only door to Quill and Document: the artifact exports
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 of another width. 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.
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, Node as PMNode, Schema } from 'prosemirror-model';
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. `. `joinBefore` is the shorthand's ordinal test: `2. ` continues the list
21
- * above where `7. ` opens one. */
22
- export declare function wrapInList(ordered: boolean, attrs?: Attrs | null, joinBefore?: (node: PMNode) => boolean): Command;
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. It passes no `joinBefore`, so it continues
30
- * whatever list is above.
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 head of an item that already exists: where a list shorthand is the text an author
19
- * typed rather than a shorthand at all.
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 preceding
62
- * sibling of the same type — the one boundary the wrap itself opens (`lists.ts`
63
- * §cleanup: command-local, never a global pass). False where no wrap fits, or where
64
- * the one that does nests past `MAX_NESTING_DEPTH` (`nesting.ts`). */
65
- function wrapAt(tr, pos, type, attrs, joinBefore) {
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 before = tr.doc.resolve(pos - 1).nodeBefore;
72
- if (before &&
73
- before.type === type &&
74
- canJoin(tr.doc, pos - 1) &&
75
- (!joinBefore || joinBefore(before))) {
76
- tr.join(pos - 1);
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. `. `joinBefore` is the shorthand's ordinal test: `2. ` continues the list
142
- * above where `7. ` opens one. */
143
- export function wrapInList(ordered, attrs = null, joinBefore) {
144
- return (state, dispatch) => atHead(state) && wrapBlock(state, dispatch, ordered, attrs, joinBefore);
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. It passes no `joinBefore`, so it continues
153
- * whatever list is above.
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 list above as the shorthand's does. */
177
- function wrapBlock(state, dispatch, ordered, attrs, joinBefore) {
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 || openingAnItem($from, item))
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, joinBefore))
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, a quote and a list are the blocks `prosemirror-gapcursor` declines
228
- * to sit beside, and nothing else mints a block above one: three keystrokes on an
229
- * empty body (` ``` `, `> `, `- `) put one first, and a document carrying one there
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.paragraph;
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?.(state.tr.delete($from.before(), $from.after()).scrollIntoView());
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
  /**
@@ -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 is not swallowed: it
79
- * falls through to the list link and, outside a list, on out of the leaf — the
80
- * body's keyboard exit from inside a code block (VISUAL_EDITOR §Chrome). Inside
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 ({@link editable}),
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 ({@link editable}),
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
- return (leaf.islands || rt.islands.length === 0) && rt.lines.every(editable);
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 every container on `line` is one the block schema edits: an element decodes
111
- * to no node, so the first commit would drop it. */
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 (`fitsLeaf`), so
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 quote and the element are what key here. */
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 === 'para' && blockIslandLine(leaf, cursor))
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, which no node holds. A held leaf draws
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, then any leaf-specific
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
@@ -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, then any leaf-specific
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';
@@ -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';