@quillmark/svelte 0.1.1 → 0.3.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 (124) hide show
  1. package/README.md +16 -12
  2. package/THEMING.md +48 -25
  3. package/dist/core/address.d.ts +42 -7
  4. package/dist/core/address.js +49 -21
  5. package/dist/core/bloom.d.ts +5 -5
  6. package/dist/core/bloom.js +8 -8
  7. package/dist/core/codec/anchor.d.ts +1 -1
  8. package/dist/core/codec/anchor.js +1 -1
  9. package/dist/core/codec/atoms.d.ts +10 -0
  10. package/dist/core/codec/atoms.js +78 -0
  11. package/dist/core/codec/clipboard.d.ts +59 -0
  12. package/dist/core/codec/clipboard.js +202 -0
  13. package/dist/core/codec/code.d.ts +1 -1
  14. package/dist/core/codec/code.js +4 -4
  15. package/dist/core/codec/decode.d.ts +1 -1
  16. package/dist/core/codec/decode.js +8 -8
  17. package/dist/core/codec/encode.d.ts +4 -4
  18. package/dist/core/codec/encode.js +14 -11
  19. package/dist/core/codec/field.d.ts +22 -27
  20. package/dist/core/codec/field.js +33 -32
  21. package/dist/core/codec/index.js +5 -8
  22. package/dist/core/codec/inputrules.js +56 -18
  23. package/dist/core/codec/islands.d.ts +11 -7
  24. package/dist/core/codec/islands.js +16 -10
  25. package/dist/core/codec/keymap.d.ts +5 -6
  26. package/dist/core/codec/keymap.js +8 -7
  27. package/dist/core/codec/lists.d.ts +3 -3
  28. package/dist/core/codec/lists.js +32 -13
  29. package/dist/core/codec/marks.d.ts +1 -1
  30. package/dist/core/codec/marks.js +1 -1
  31. package/dist/core/codec/positions.js +1 -1
  32. package/dist/core/codec/prose.css +266 -228
  33. package/dist/core/codec/schema.js +4 -4
  34. package/dist/core/codec/slash.d.ts +39 -41
  35. package/dist/core/codec/slash.js +0 -0
  36. package/dist/core/codec/table-view.d.ts +12 -16
  37. package/dist/core/codec/table-view.js +488 -292
  38. package/dist/core/codec/table.d.ts +42 -15
  39. package/dist/core/codec/table.js +73 -27
  40. package/dist/core/errors.d.ts +17 -7
  41. package/dist/core/index.d.ts +1 -1
  42. package/dist/core/index.js +6 -5
  43. package/dist/core/lifecycle.d.ts +21 -5
  44. package/dist/core/lifecycle.js +34 -8
  45. package/dist/core/rebind.svelte.d.ts +18 -4
  46. package/dist/core/rebind.svelte.js +30 -12
  47. package/dist/core/teardown.d.ts +1 -1
  48. package/dist/core/teardown.js +1 -1
  49. package/dist/core/theme.css +173 -58
  50. package/dist/preset/index.css +1 -1
  51. package/dist/preset/recipes.css +79 -30
  52. package/dist/preset/scale.css +40 -28
  53. package/dist/preview/Preview.svelte +22 -19
  54. package/dist/preview/Preview.svelte.d.ts +8 -8
  55. package/dist/preview/bridge.d.ts +6 -4
  56. package/dist/preview/bridge.js +19 -9
  57. package/dist/preview/controller.d.ts +13 -6
  58. package/dist/preview/controller.js +8 -5
  59. package/dist/preview/geometry.d.ts +17 -2
  60. package/dist/preview/geometry.js +21 -1
  61. package/dist/preview/index.d.ts +1 -0
  62. package/dist/preview/index.js +9 -2
  63. package/dist/preview/overlay.d.ts +2 -1
  64. package/dist/preview/overlay.js +26 -11
  65. package/dist/preview/paint.js +19 -17
  66. package/dist/visual/ArrayField.svelte +103 -83
  67. package/dist/visual/ArrayField.svelte.d.ts +5 -4
  68. package/dist/visual/BooleanField.svelte +8 -6
  69. package/dist/visual/BooleanField.svelte.d.ts +2 -2
  70. package/dist/visual/Card.svelte +161 -333
  71. package/dist/visual/Card.svelte.d.ts +5 -4
  72. package/dist/visual/CardControls.svelte +1 -1
  73. package/dist/visual/DateField.svelte +19 -19
  74. package/dist/visual/DateField.svelte.d.ts +3 -3
  75. package/dist/visual/DiagnosticList.svelte +2 -2
  76. package/dist/visual/EnumField.svelte +48 -28
  77. package/dist/visual/EnumField.svelte.d.ts +5 -4
  78. package/dist/visual/Field.svelte +207 -132
  79. package/dist/visual/Field.svelte.d.ts +11 -7
  80. package/dist/visual/FieldHint.svelte +79 -48
  81. package/dist/visual/FieldHint.svelte.d.ts +1 -1
  82. package/dist/visual/FieldLabel.svelte +8 -8
  83. package/dist/visual/FormatPopover.svelte +82 -35
  84. package/dist/visual/NumberField.svelte +5 -5
  85. package/dist/visual/NumberField.svelte.d.ts +1 -1
  86. package/dist/visual/ObjectField.svelte +28 -9
  87. package/dist/visual/ObjectField.svelte.d.ts +4 -2
  88. package/dist/visual/ProseArrayElement.svelte +12 -19
  89. package/dist/visual/ProseArrayElement.svelte.d.ts +1 -2
  90. package/dist/visual/ProseField.svelte +62 -85
  91. package/dist/visual/ProseField.svelte.d.ts +14 -17
  92. package/dist/visual/SlashMenu.svelte +42 -16
  93. package/dist/visual/TextField.svelte +5 -5
  94. package/dist/visual/TextField.svelte.d.ts +1 -1
  95. package/dist/visual/TipsCard.svelte +43 -28
  96. package/dist/visual/VisualEditor.svelte +18 -19
  97. package/dist/visual/VisualEditor.svelte.d.ts +2 -3
  98. package/dist/visual/VisualEditorInner.svelte +251 -184
  99. package/dist/visual/VisualEditorInner.svelte.d.ts +2 -3
  100. package/dist/visual/controls.css +85 -38
  101. package/dist/visual/diagnostics.d.ts +13 -21
  102. package/dist/visual/diagnostics.js +7 -19
  103. package/dist/visual/domid.js +3 -3
  104. package/dist/visual/ext.d.ts +1 -1
  105. package/dist/visual/ext.js +1 -1
  106. package/dist/visual/icons/nodes.d.ts +3 -0
  107. package/dist/visual/icons/nodes.js +2 -1
  108. package/dist/visual/index.d.ts +1 -0
  109. package/dist/visual/index.js +10 -3
  110. package/dist/visual/leaves.d.ts +47 -0
  111. package/dist/visual/leaves.js +31 -0
  112. package/dist/visual/links.d.ts +24 -0
  113. package/dist/visual/links.js +67 -0
  114. package/dist/visual/props.d.ts +33 -12
  115. package/dist/visual/signals.d.ts +6 -6
  116. package/dist/visual/strings.d.ts +14 -19
  117. package/dist/visual/strings.js +5 -7
  118. package/dist/visual/structure.d.ts +24 -17
  119. package/dist/visual/structure.js +8 -8
  120. package/dist/visual/synced.svelte.d.ts +1 -1
  121. package/dist/visual/synced.svelte.js +4 -4
  122. package/dist/visual/tips.d.ts +4 -2
  123. package/dist/visual/tips.js +7 -4
  124. package/package.json +2 -2
@@ -6,11 +6,11 @@
6
6
  // (b) lower the tr to a `ChangeBundle` (or `overwrite` for a field not yet at
7
7
  // content rest, which has nothing to splice),
8
8
  // (c) commit via `doc.applyChange(addr, bundle)`,
9
- // (d) fire `onChange` when the transaction COMMITTED, then `onCaretMove` with
9
+ // (d) fire `onChange` when the transaction committed, then `onCaretMove` with
10
10
  // the new USV caret, which a bare selection move fires on its own.
11
11
  // On an `applyChange` throw the optimistic PM state stays and the failure reports
12
12
  // through `onError`: never a crash. Caret continuity across own-edits is the PM
13
- // `StepMap`; an EXTERNAL content change re-hydrates through `applyExternal`, gated
13
+ // `StepMap`; an external content change re-hydrates through `applyExternal`, gated
14
14
  // by `reconcile`.
15
15
  import { baseKeymap, toggleMark } from 'prosemirror-commands';
16
16
  import { gapCursor } from 'prosemirror-gapcursor';
@@ -19,6 +19,7 @@ import { keymap } from 'prosemirror-keymap';
19
19
  import { EditorState, NodeSelection, Plugin, PluginKey, Selection, TextSelection } from 'prosemirror-state';
20
20
  import { Decoration, DecorationSet, EditorView } from 'prosemirror-view';
21
21
  import { reportError, errorMessage } from '../errors.js';
22
+ import { tableClipboardPlugin } from './clipboard.js';
22
23
  import { decode } from './decode.js';
23
24
  import { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
24
25
  import { lower, pmToContent, contentEdit } from './encode.js';
@@ -28,7 +29,7 @@ import { inputRulesPlugin } from './inputrules.js';
28
29
  import { bodyKeymap } from './keymap.js';
29
30
  import { blockSchema, inlineSchema } from './schema.js';
30
31
  import { DEFAULT_TABLE_STRINGS, tableNodeView } from './table-view.js';
31
- import { DEFAULT_SLASH_STRINGS, focusSlashItem, runSlashItem, slashItems, slashPlugin } from './slash.js';
32
+ import { focusSlashItem, runSlashItem, slashPlugin } from './slash.js';
32
33
  const anchorKey = new PluginKey('quill-anchors');
33
34
  /** Plugin-held anchor positions (PM coords), mapped through every edit's `StepMap`
34
35
  * and mutated by an {@link AnchorEdit} meta (an insert/remove at a selection). */
@@ -62,8 +63,8 @@ function anchorPlugin(seed) {
62
63
  *
63
64
  * Bound rather than verbatim, because the storage form is not the leaf's business
64
65
  * and the verbatim read cannot answer it alone. A field the editor committed rests
65
- * as `Content`; a field a markdown parse left rests as the authored STRING, and
66
- * what that string MEANS is the declared type's to say: `richtext` is markdown,
66
+ * as `Content`; a field a markdown parse left rests as the authored string, and
67
+ * what that string means is the declared type's to say: `richtext` is markdown,
67
68
  * `plaintext` is literal text, and reading one as the other silently eats every
68
69
  * `*` the author typed. */
69
70
  function readLeaf(reader, addr) {
@@ -77,9 +78,9 @@ export function emptyContent() {
77
78
  /** Whether ops may commit here: the stored value is a `Content` object, so
78
79
  * `applyChange` splices exactly the content the leaf read and PM is showing.
79
80
  *
80
- * The two other rest forms both take `overwrite` instead. An UNSET field (`undefined`;
81
+ * The two other rest forms both take `overwrite` instead. An unset field (`undefined`;
81
82
  * a default-only richtext field before its first edit) has nothing to splice.
82
- * An AUTHORED STRING is the trap: `applyChange` reads it as markdown whatever the
83
+ * An authored string is the trap: `applyChange` reads it as markdown whatever the
83
84
  * declared type is, so on a `plaintext` field its pre-image is not the content the
84
85
  * leaf read and a delta computed against that content lands at the wrong offsets.
85
86
  * Installing over it costs nothing either way, since content-only marks do not
@@ -119,15 +120,14 @@ export function createField(opts) {
119
120
  // The ghost's live cell: the placeholder plugin reads it per pass, so moving it
120
121
  // is an assignment plus a re-render rather than a rebuilt plugin stack.
121
122
  let placeholderText = opts.placeholder;
122
- // The views nested INSIDE this leaf: one per table cell (`table-view.ts`). The
123
+ // The views nested inside this leaf: one per table cell (`table-view.ts`). The
123
124
  // leaf holds the set because chrome asks the leaf, not the island, which view
124
125
  // holds the caret: the format popover raises over a cell exactly as over the
125
126
  // body, and only the leaf knows both.
126
127
  const nested = new Set();
127
- // The menu's offers, derived from the wording per read: the chrome names an item
128
- // and the codec runs it, so a locale swap changes the labels and not the picks.
129
- const menuItems = () => slashItems(opts.slashStrings?.() ?? DEFAULT_SLASH_STRINGS);
130
- const slash = !inline && opts.onSlash ? { items: menuItems, onState: opts.onSlash } : undefined;
128
+ // The menu's report channel, and the whole of what a leaf holds of it: the
129
+ // vocabulary is the codec's own constant (`slash.ts`), not a wording to derive.
130
+ const slash = inline ? undefined : opts.onSlash;
131
131
  const state = buildState(reconciler.last);
132
132
  index = buildLineIndex(state.doc);
133
133
  view = new EditorView(container, {
@@ -154,7 +154,7 @@ export function createField(opts) {
154
154
  const next = view.state.apply(tr);
155
155
  view.updateState(next); // (a) optimistic
156
156
  index = buildLineIndex(next.doc);
157
- // Commit a content edit OR an anchor mutation. An anchor insert/remove is
157
+ // Commit a content edit or an anchor mutation. An anchor insert/remove is
158
158
  // zero-width, so `docChanged` is false: the `anchorKey` meta is what
159
159
  // routes it through the same commit path (the diff emits the anchor op).
160
160
  if (tr.docChanged || tr.getMeta(anchorKey)) {
@@ -203,7 +203,7 @@ export function createField(opts) {
203
203
  // Returns whether anything landed: the caller's change signal, which must not
204
204
  // be a property of which branch the commit took.
205
205
  function commitEdit(oldRt, newDoc) {
206
- // The projection and its text splice, each computed ONCE per keystroke: the gate
206
+ // The projection and its text splice, each computed once per keystroke: the gate
207
207
  // below, both overwrite fallbacks, and `lower` all read this one `edit`.
208
208
  const edit = contentEdit(oldRt, pmToContent(newDoc));
209
209
  try {
@@ -226,7 +226,7 @@ export function createField(opts) {
226
226
  return true;
227
227
  }
228
228
  catch (e) {
229
- // Bound the damage: an op path the gates missed leaves the store STALE
229
+ // Bound the damage: an op path the gates missed leaves the store stale
230
230
  // while PM keeps the edit; and because the reconciler then re-diffs
231
231
  // from that stale content, every later edit re-throws and the field
232
232
  // silently stops persisting. Install the full projection instead
@@ -256,7 +256,7 @@ export function createField(opts) {
256
256
  }
257
257
  /** The anchors the plugin currently holds (PM coords); `[]` before the first apply. */
258
258
  const heldAnchors = () => anchorKey.getState(view.state) ?? [];
259
- /** Anchor positions in the CURRENT (new) doc, as USV. */
259
+ /** Anchor positions in the current (new) doc, as USV. */
260
260
  function readAnchorsUsv(newDoc) {
261
261
  return heldAnchors().map((a) => ({ id: a.id, pos: pmToUsv(index, a.pos) }));
262
262
  }
@@ -326,13 +326,13 @@ export function createField(opts) {
326
326
  })
327
327
  .map((a) => a.id);
328
328
  },
329
- slashFocus(id) {
329
+ slashFocus(name) {
330
330
  if (slash)
331
- focusSlashItem(view, id, slash.items());
331
+ focusSlashItem(view, name);
332
332
  },
333
- slashPick(id) {
333
+ slashPick(name) {
334
334
  if (slash)
335
- runSlashItem(view, id);
335
+ runSlashItem(view, name);
336
336
  },
337
337
  selectionRange() {
338
338
  const { from, to } = view.state.selection;
@@ -365,13 +365,13 @@ export function createField(opts) {
365
365
  * leaf-specific plugins (`afterHistory`: the addressed leaf passes its
366
366
  * anchor-position plugin; a by-value array element passes none), the
367
367
  * markdown-shorthand input rules, then the field keymap over the base keymap, and
368
- * last the two that answer to a caret or a selection BESIDE a block: the gap cursor
368
+ * last the two that answer to a caret or a selection beside a block: the gap cursor
369
369
  * and {@link pastAtomPlugin}.
370
370
  *
371
371
  * A `plaintext` field carries no marks (decode strips them, the keymap suppresses
372
372
  * Mod-b/i/u), so it also skips the input rules: `inlineSchema` still declares the
373
- * mark types, so a `**bold**` rule would apply a strong mark AND eat the literal
374
- * delimiters. The inline schema has no block nodes, so those rules are the ONLY
373
+ * mark types, so a `**bold**` rule would apply a strong mark and eat the literal
374
+ * delimiters. The inline schema has no block nodes, so those rules are the only
375
375
  * ones it would add: skip all.
376
376
  */
377
377
  export function proseLeafPlugins(schema, opts) {
@@ -380,21 +380,22 @@ export function proseLeafPlugins(schema, opts) {
380
380
  list.push(slashPlugin(opts.slash));
381
381
  if (!opts.noInputRules && !opts.plaintext)
382
382
  list.push(inputRulesPlugin(schema));
383
- list.push(keymap(editorKeymap(schema, opts.inline, opts.plaintext, opts.slash?.items)));
383
+ list.push(keymap(editorKeymap(schema, opts.inline, opts.plaintext, !!opts.slash)));
384
384
  list.push(keymap(baseKeymap));
385
- // Both answer to a selection over an ATOM, which only a block leaf holds: an
386
- // inline leaf is one textblock with no island and no gap to put a cursor in.
385
+ // All three answer to something only a block leaf holds: an inline leaf is one
386
+ // textblock with no island, no gap to put a cursor in, and nowhere for a pasted
387
+ // table to land.
387
388
  if (!opts.inline)
388
- list.push(gapCursor(), pastAtomPlugin());
389
+ list.push(gapCursor(), pastAtomPlugin(), tableClipboardPlugin(schema));
389
390
  if (opts.placeholder)
390
391
  list.push(placeholderPlugin(opts.placeholder));
391
392
  return list;
392
393
  }
393
394
  /**
394
- * A printable key over a selected atom writes PAST it. A selection is the subject
395
+ * A printable key over a selected atom writes past it. A selection is the subject
395
396
  * of the next command, never a thing armed for
396
397
  * replacement: Backspace deletes it, Mod-C copies it, and a character lands beside
397
- * it. A BLOCK island takes a new paragraph after it, an INLINE one the caret after
398
+ * it. A block island takes a new paragraph after it, an inline one the caret after
398
399
  * the image in the same line: one rule, both node types.
399
400
  *
400
401
  * PM routes text input over a non-`TextSelection` through `handleTextInput` before
@@ -461,13 +462,13 @@ function placeholderPlugin(read) {
461
462
  * The field's keymap: history, mark toggles, and the body's structural keys; Enter
462
463
  * suppressed inline.
463
464
  *
464
- * Tab forks on the leaf's ROLE, not on the caret's position. An inline/plaintext
465
+ * Tab forks on the leaf's role, not on the caret's position. An inline/plaintext
465
466
  * leaf is a form field: Tab stays unbound, so field navigation is open for a shell
466
467
  * keymap (VISUAL_EDITOR §Settled and open). A block-schema body is a document: Tab
467
468
  * is structural, a chain each nested surface prepends to (`keymap.ts`). One key
468
469
  * never means two things within one surface.
469
470
  */
470
- function editorKeymap(schema, inline, plaintext, slashItems) {
471
+ function editorKeymap(schema, inline, plaintext, slash) {
471
472
  const map = {
472
473
  'Mod-z': undo,
473
474
  'Mod-y': redo,
@@ -487,7 +488,7 @@ function editorKeymap(schema, inline, plaintext, slashItems) {
487
488
  }
488
489
  else {
489
490
  // Each binding falls through to `baseKeymap` when no link claims the key.
490
- Object.assign(map, bodyKeymap(schema, slashItems));
491
+ Object.assign(map, bodyKeymap(schema, slash));
491
492
  }
492
493
  return map;
493
494
  }
@@ -1,9 +1,9 @@
1
1
  // The codec barrel: the bidirectional bridge between one content field (`Content`)
2
- // and one ProseMirror document (CODEC). Decode (content → PM), lower (PM tr → a
2
+ // and one ProseMirror document (CODEC.md). Decode (content → PM), lower (PM tr → a
3
3
  // `ChangeBundle` for `applyChange`), the USV↔PM position map, and the `createField`
4
4
  // prose leaf. Consumed by the VisualEditor and the codec test suite.
5
5
  // ProseMirror's structural base styles and the gap cursor's, then the retint of the
6
- // hues they mint. Imported HERE because `createField` is what mounts a view: a
6
+ // hues they mint. Imported here because `createField` is what mounts a view: a
7
7
  // consumer reaching the codec directly gets them with it, and the order is what makes
8
8
  // the retint win.
9
9
  import 'prosemirror-view/style/prosemirror.css';
@@ -24,12 +24,9 @@ export { DEFAULT_SLASH_STRINGS } from './slash.js';
24
24
  export { blockSchema, inlineSchema } from './schema.js';
25
25
  // Decode / encode / positions (tests + VisualEditor).
26
26
  //
27
- // THE BARREL CARRIES WHAT HAS AN OFF-BARREL CALLER, and nothing else. The mark
28
- // algebra, the island bridge, the schema predicate, the `diff*` helpers,
29
- // `scanDoc`, `codePoints`: every one of them is reached by RELATIVE import
30
- // within `codec/`, so none of them is here. An export nothing imports is surface
31
- // that still has to stay honest; a symbol earns its line when a caller outside
32
- // this folder wants it.
27
+ // The barrel carries what has an off-barrel caller, and nothing else. A symbol
28
+ // reached only by relative import within `codec/` stays off it: an export nothing
29
+ // imports is surface that still has to stay honest.
33
30
  export { decode, renderContent, usvLength } from './decode.js';
34
31
  export { pmToContent, contentEdit, lower } from './encode.js';
35
32
  export { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
@@ -1,10 +1,11 @@
1
1
  // Input rules: the markdown-shorthand plugin the codec owns (`**`→strong, `*`→em,
2
2
  // `~~`→strike, `` ` ``→code, `# `→heading, `- `/`1. `→lists, `> `→blockquote,
3
3
  // `---`→divider).
4
- // Mark rules are hand-rolled (prosemirror-inputrules ships only node rules); block
5
- // rules reuse `wrappingInputRule` / `textblockTypeInputRule`. These produce
6
- // transactions the codec already lowers: they add no new lowering surface.
7
- import { InputRule, inputRules, textblockTypeInputRule, wrappingInputRule } from 'prosemirror-inputrules';
4
+ // Mark rules are hand-rolled (prosemirror-inputrules ships only node rules); the one
5
+ // block rule with no guard of its own reuses `wrappingInputRule`, and the rest mirror
6
+ // an upstream body around one. These produce transactions the codec already lowers:
7
+ // they add no new lowering surface.
8
+ import { InputRule, inputRules, wrappingInputRule } from 'prosemirror-inputrules';
8
9
  import { canJoin, findWrapping } from 'prosemirror-transform';
9
10
  import { Selection } from 'prosemirror-state';
10
11
  /**
@@ -12,8 +13,8 @@ import { Selection } from 'prosemirror-state';
12
13
  * before the caret, wrap capture group 1 in `markType` and drop the delimiters.
13
14
  * `delimLen` is the delimiter length (opening == closing for every rule here).
14
15
  * `match[0]` anatomy is `prefix? open captured close`: the em rule's
15
- * `(?:^|[^*])` guard consumes ONE non-delimiter prefix char into the match, so
16
- * positions are derived from the KNOWN delimiter/capture lengths, never from
16
+ * `(?:^|[^*])` guard consumes one non-delimiter prefix char into the match, so
17
+ * positions are derived from the known delimiter/capture lengths, never from
17
18
  * `indexOf` (which both deletes that prefix char and mis-anchors when it
18
19
  * happens to equal the captured text). `[start, end)` spans `match[0]` minus
19
20
  * the just-typed text (not yet inserted when the rule fires), so the trailing
@@ -48,9 +49,14 @@ function markInputRule(regexp, markType, delimLen) {
48
49
  * body paragraphs (`usaf_memo`'s `render-body`), where a heading resolves to
49
50
  * nothing. This is the wrap-side route into that shape; the `# ` rule guards the
50
51
  * other, in {@link markdownInputRules}.
52
+ *
53
+ * `item` is the second guard, and it is about the gesture rather than the shape: the
54
+ * rule declines at the start of an item's own first block ({@link openingAnItem}).
51
55
  */
52
- function listWrappingRule(regexp, listType, paragraph, getAttrs, joinPredicate) {
56
+ function listWrappingRule(regexp, listType, paragraph, item, getAttrs, joinPredicate) {
53
57
  return new InputRule(regexp, (state, match, start, end) => {
58
+ if (openingAnItem(state.doc.resolve(start), item))
59
+ return null;
54
60
  const attrs = getAttrs ? getAttrs(match) : null;
55
61
  const tr = state.tr.delete(start, end);
56
62
  // Positions survive the retype: same content, same size.
@@ -75,10 +81,28 @@ function listWrappingRule(regexp, listType, paragraph, getAttrs, joinPredicate)
75
81
  return tr;
76
82
  });
77
83
  }
84
+ /**
85
+ * Whether `$start` is the head of an item that already exists: offset 0 of a
86
+ * `list_item`'s first block, which is where a list shorthand is the text an author
87
+ * typed rather than a shorthand at all.
88
+ *
89
+ * Firing there wraps the item's own paragraph in a fresh list, minting an item whose
90
+ * only content is another item — a level with nothing on it. Tab is the indent gesture
91
+ * and lands the shape a nesting is supposed to have, under the previous sibling, so
92
+ * what the rule adds here is a second door onto a worse answer. A later block of the
93
+ * item is not this case: wrapping a continuation paragraph is how a sub-list opens
94
+ * under text.
95
+ */
96
+ function openingAnItem($start, item) {
97
+ if (!item || $start.depth < 2 || $start.parentOffset !== 0)
98
+ return false;
99
+ return $start.node(-1).type === item && $start.index(-1) === 0;
100
+ }
78
101
  /** The full markdown-shorthand rule set for a block schema. */
79
102
  export function markdownInputRules(schema) {
80
103
  const rules = [];
81
104
  const m = schema.marks;
105
+ const item = schema.nodes.list_item;
82
106
  // Inline marks. Non-greedy capture between matching delimiters.
83
107
  if (m.strong)
84
108
  rules.push(markInputRule(/\*\*([^*]+)\*\*$/, m.strong, 2));
@@ -95,7 +119,6 @@ export function markdownInputRules(schema) {
95
119
  // declines (null) and stays literal text, rather than minting the unrenderable
96
120
  // `list_item > heading` {@link listWrappingRule} normalizes away on the wrap side.
97
121
  const heading = schema.nodes.heading;
98
- const item = schema.nodes.list_item;
99
122
  rules.push(new InputRule(/^(#{1,6})\s$/, (state, match, start, end) => {
100
123
  const $start = state.doc.resolve(start);
101
124
  for (let d = $start.depth; d > 0; d--) {
@@ -110,18 +133,34 @@ export function markdownInputRules(schema) {
110
133
  .setBlockType(start, start, heading, { level: match[1].length });
111
134
  }));
112
135
  }
113
- if (schema.nodes.code_block) {
114
- rules.push(textblockTypeInputRule(/^```$/, schema.nodes.code_block));
136
+ if (schema.nodes.code_block && schema.nodes.paragraph) {
137
+ // `textblockTypeInputRule`'s body plus the exit the `---` rule mints below, for a
138
+ // sharper version of its reason: a code block is the one block a GAP CURSOR will
139
+ // not sit beside either (`closedBefore`/`closedAfter` both fail on a textblock), so
140
+ // a fence at the end of a body leaves the caret with no way forward at all.
141
+ const code = schema.nodes.code_block;
142
+ const paragraph = schema.nodes.paragraph;
143
+ rules.push(new InputRule(/^```$/, (state, _match, start, end) => {
144
+ const $start = state.doc.resolve(start);
145
+ if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), code)) {
146
+ return null;
147
+ }
148
+ const tr = state.tr.delete(start, end).setBlockType(start, start, code);
149
+ const at = tr.doc.resolve(start).after();
150
+ if (!tr.doc.nodeAt(at))
151
+ tr.insert(at, paragraph.create());
152
+ // The caret stays in the fence, not in the exit it minted.
153
+ return tr;
154
+ }));
115
155
  }
116
156
  if (schema.nodes.horizontal_rule && schema.nodes.paragraph) {
117
- // A divider is a whole block, so this rule REPLACES its textblock rather than
157
+ // A divider is a whole block, so this rule replaces its textblock rather than
118
158
  // retyping one: `horizontal_rule` holds no content to retype into.
119
159
  const rule = schema.nodes.horizontal_rule;
120
160
  const paragraph = schema.nodes.paragraph;
121
- const item = schema.nodes.list_item;
122
161
  rules.push(new InputRule(/^---$/, (state, _match, start, end) => {
123
162
  const $start = state.doc.resolve(start);
124
- // The block ENTIRELY, not a prefix of one. `[start, end)` is the match minus
163
+ // The block entirely, not a prefix of one. `[start, end)` is the match minus
125
164
  // the just-typed `-`, so the block holds `--` when this fires; anything else
126
165
  // in it is a paragraph being edited, which a replace would eat.
127
166
  if ($start.parentOffset !== 0 || $start.parent.content.size !== end - start) {
@@ -148,14 +187,13 @@ export function markdownInputRules(schema) {
148
187
  return tr;
149
188
  }));
150
189
  }
151
- // `- ` / `1. ` at the start of a block, here or nested inside an item. These are
152
- // the one pair of shorthands the slash menu ALSO carries: a list opens an item the
153
- // caret then lives in, which is what that menu is the set of (`slash.ts`).
190
+ // `- ` / `1. ` at the start of a block, here or nested inside an item — but not at
191
+ // the start of an item's own first block, where {@link openingAnItem} declines.
154
192
  if (schema.nodes.bullet_list && schema.nodes.paragraph) {
155
- rules.push(listWrappingRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list, schema.nodes.paragraph));
193
+ rules.push(listWrappingRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list, schema.nodes.paragraph, item));
156
194
  }
157
195
  if (schema.nodes.ordered_list && schema.nodes.paragraph) {
158
- rules.push(listWrappingRule(/^(\d+)\.\s$/, schema.nodes.ordered_list, schema.nodes.paragraph, (match) => ({ start: +match[1] }), (match, node) => node.childCount + node.attrs.start === +match[1]));
196
+ rules.push(listWrappingRule(/^(\d+)\.\s$/, schema.nodes.ordered_list, schema.nodes.paragraph, item, (match) => ({ start: +match[1] }), (match, node) => node.childCount + node.attrs.start === +match[1]));
159
197
  }
160
198
  if (schema.nodes.blockquote) {
161
199
  rules.push(wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote));
@@ -23,14 +23,18 @@ export declare function islandEntryFromNode(attrs: IslandNodeAttrs): ContentIsla
23
23
  * rather than at each reader. */
24
24
  export declare function tablePropsOfNode(node: PMNode): TableProps | undefined;
25
25
  /**
26
- * The next island id for a field: the positional `isl-{n}` sequence continued past
27
- * the highest one the field already holds. A minted id is this tier's to produce
28
- * and it is part of the document's canonical bytes (CODEC §Islands), so it is a
29
- * counter over the projection in hand — never a UUID, never a clock reading, and
30
- * never a re-numbering of the ids already there, which are identities.
26
+ * A minter over one document: the positional `isl-{n}` sequence continued past the
27
+ * highest id the field already holds, handing out consecutive ids, so a caller placing
28
+ * several islands at once keeps one minter for the lot. A minted id is this tier's to
29
+ * produce and it is part of the document's canonical bytes (CODEC §Islands), so it is a
30
+ * counter over the projection in hand — never a UUID, never a clock reading, and never a
31
+ * re-numbering of the ids already there, which are identities.
31
32
  *
32
- * Reads the PM doc rather than the stored content because that is what the caller
33
- * (an insert command) holds and what the commit will project: an id minted against
33
+ * Reads the PM doc rather than the stored content because that is what the caller (an
34
+ * insert command, a paste) holds and what the commit will project: an id minted against
34
35
  * a stale content could collide with one the same transaction places.
35
36
  */
37
+ export declare function islandMinter(doc: PMNode): () => string;
38
+ /** The next island id for a field: one draw from {@link islandMinter}, which is what a
39
+ * command placing exactly one island wants. */
36
40
  export declare function mintIslandId(doc: PMNode): string;
@@ -4,7 +4,7 @@ export const ISLAND_SLOT = '';
4
4
  // The four island node attributes carry the content entry verbatim: `id` is the
5
5
  // stable identity (rebases like an anchor), `islandType` the discriminator, `props`
6
6
  // the opaque payload the codec preserves byte-for-byte across a round-trip, and
7
- // `loss` how faithfully markdown can carry it. `loss` is AUTHORED, not derived:
7
+ // `loss` how faithfully markdown can carry it. `loss` is authored, not derived:
8
8
  // `applyChange` stores the class an island op gives it and re-derives nothing, so
9
9
  // an entry that did not carry its own would promote a degraded table to lossless
10
10
  // on the first cell edit. The default is the class an unknown one reads as, so a
@@ -58,17 +58,18 @@ export function tablePropsOfNode(node) {
58
58
  return isTableIsland(entry) ? entry.props : undefined;
59
59
  }
60
60
  /**
61
- * The next island id for a field: the positional `isl-{n}` sequence continued past
62
- * the highest one the field already holds. A minted id is this tier's to produce
63
- * and it is part of the document's canonical bytes (CODEC §Islands), so it is a
64
- * counter over the projection in hand — never a UUID, never a clock reading, and
65
- * never a re-numbering of the ids already there, which are identities.
61
+ * A minter over one document: the positional `isl-{n}` sequence continued past the
62
+ * highest id the field already holds, handing out consecutive ids, so a caller placing
63
+ * several islands at once keeps one minter for the lot. A minted id is this tier's to
64
+ * produce and it is part of the document's canonical bytes (CODEC §Islands), so it is a
65
+ * counter over the projection in hand — never a UUID, never a clock reading, and never a
66
+ * re-numbering of the ids already there, which are identities.
66
67
  *
67
- * Reads the PM doc rather than the stored content because that is what the caller
68
- * (an insert command) holds and what the commit will project: an id minted against
68
+ * Reads the PM doc rather than the stored content because that is what the caller (an
69
+ * insert command, a paste) holds and what the commit will project: an id minted against
69
70
  * a stale content could collide with one the same transaction places.
70
71
  */
71
- export function mintIslandId(doc) {
72
+ export function islandMinter(doc) {
72
73
  let next = 0;
73
74
  doc.descendants((node) => {
74
75
  if (node.type.name !== 'island_block' && node.type.name !== 'island_inline')
@@ -78,5 +79,10 @@ export function mintIslandId(doc) {
78
79
  next = Math.max(next, Number(m[1]) + 1);
79
80
  return false;
80
81
  });
81
- return `isl-${next}`;
82
+ return () => `isl-${next++}`;
83
+ }
84
+ /** The next island id for a field: one draw from {@link islandMinter}, which is what a
85
+ * command placing exactly one island wants. */
86
+ export function mintIslandId(doc) {
87
+ return islandMinter(doc)();
82
88
  }
@@ -1,13 +1,12 @@
1
1
  import type { Schema } from 'prosemirror-model';
2
2
  import type { Command } from 'prosemirror-state';
3
- import { type SlashItem } from './slash.js';
4
3
  /** The block-schema body's structural keys: `{}` for the inline/plaintext schemas,
5
- * whose leaves declare neither lists nor code blocks. The argument list IS the
4
+ * whose leaves declare neither lists nor code blocks. The argument list is the
6
5
  * precedence order VISUAL_EDITOR §Chrome states, so a new link is one more argument
7
6
  * at its place in that order rather than a nesting to read inside out.
8
7
  *
9
8
  * The slash menu is innermost of the three, and it is conditional rather than
10
- * schema-gated: the keys exist only where a menu can be DRAWN (`slashItems` present),
11
- * because Enter and Escape claimed by a surface nobody can see is a body that
12
- * swallows two keys for no visible reason. */
13
- export declare function bodyKeymap(schema: Schema, slashItems?: () => SlashItem[]): Record<string, Command>;
9
+ * schema-gated: the keys exist only where a menu can be drawn (`slash`), because Enter
10
+ * and Escape claimed by a surface nobody can see is a body that swallows two keys for
11
+ * no visible reason. */
12
+ export declare function bodyKeymap(schema: Schema, slash?: boolean): Record<string, Command>;
@@ -1,4 +1,5 @@
1
1
  import { chainCommands } from 'prosemirror-commands';
2
+ import { atomKeymap } from './atoms.js';
2
3
  import { codeKeymap } from './code.js';
3
4
  import { listKeymap } from './lists.js';
4
5
  import { slashKeymap } from './slash.js';
@@ -15,15 +16,15 @@ function chainKeymaps(...maps) {
15
16
  return out;
16
17
  }
17
18
  /** The block-schema body's structural keys: `{}` for the inline/plaintext schemas,
18
- * whose leaves declare neither lists nor code blocks. The argument list IS the
19
+ * whose leaves declare neither lists nor code blocks. The argument list is the
19
20
  * precedence order VISUAL_EDITOR §Chrome states, so a new link is one more argument
20
21
  * at its place in that order rather than a nesting to read inside out.
21
22
  *
22
23
  * The slash menu is innermost of the three, and it is conditional rather than
23
- * schema-gated: the keys exist only where a menu can be DRAWN (`slashItems` present),
24
- * because Enter and Escape claimed by a surface nobody can see is a body that
25
- * swallows two keys for no visible reason. */
26
- export function bodyKeymap(schema, slashItems) {
27
- const links = slashItems ? [slashKeymap(slashItems)] : [];
28
- return chainKeymaps(...links, codeKeymap(schema), listKeymap(schema));
24
+ * schema-gated: the keys exist only where a menu can be drawn (`slash`), because Enter
25
+ * and Escape claimed by a surface nobody can see is a body that swallows two keys for
26
+ * no visible reason. */
27
+ export function bodyKeymap(schema, slash) {
28
+ const links = slash ? [slashKeymap()] : [];
29
+ return chainKeymaps(...links, codeKeymap(schema), listKeymap(schema), atomKeymap(schema));
29
30
  }
@@ -8,14 +8,14 @@ import type { Command } from 'prosemirror-state';
8
8
  * Tab more locally under the same rule (`prose/canon/VISUAL_EDITOR.md` §Chrome)
9
9
  * prepend a link ahead of these in `keymap.ts`: a `code_block` takes literal
10
10
  * indentation (`code.ts`), an island takes cell traversal once added. Outside all
11
- * of them every link returns false and the key is NOT swallowed: Tab keeps its
11
+ * of them every link returns false and the key is not swallowed: Tab keeps its
12
12
  * default meaning, which is the body's only keyboard exit and the open seam for a
13
13
  * shell structural keymap (VISUAL_EDITOR §Settled and open).
14
14
  *
15
- * `Enter` is a chain in precedence order: the paragraph-above gesture, then
15
+ * `Enter` is a chain in precedence order: the escape-above gesture, then
16
16
  * `splitListItem`, then `liftListItem`. The middle link carries two behaviors of
17
17
  * its own: it splits a non-empty item, and on an empty item it either splits the
18
- * WRAPPING item (a nested empty item, so Enter lifts exactly one level) or bails
18
+ * wrapping item (a nested empty item, so Enter lifts exactly one level) or bails
19
19
  * so the third link lifts a top-level empty item out to a paragraph. `Backspace`
20
20
  * forks on the item's index: the first item lifts, any later one merges.
21
21
  */
@@ -1,7 +1,7 @@
1
1
  import { liftListItem, sinkListItem, splitListItem } from 'prosemirror-schema-list';
2
2
  import { chainCommands, joinTextblockBackward } from 'prosemirror-commands';
3
3
  /** The caret's enclosing `list_item` when it sits at the very start of that item's
4
- * FIRST block: the position both structural keys branch on. `null` otherwise
4
+ * first block: the position both structural keys branch on. `null` otherwise
5
5
  * (mid-text, a non-first block of a multi-paragraph item, a non-empty selection),
6
6
  * which is where the key keeps its ordinary meaning. */
7
7
  function atItemStart(state, itemType) {
@@ -21,13 +21,30 @@ function atItemStart(state, itemType) {
21
21
  };
22
22
  }
23
23
  /**
24
- * Enter at the very start of a top-level list's first item → an empty paragraph
25
- * above the list, caret staying with the text it pushed down.
24
+ * Whether a caret already fits immediately above the list at `pos`. The node ending
25
+ * there answers for its last child, so a quote counts through its final paragraph;
26
+ * an atom (an island, a rule) does not, and neither does a list opening its parent.
27
+ * Those are the shapes `prosemirror-gapcursor` declines too, its `closedBefore`
28
+ * asking this question from the other side.
29
+ */
30
+ function writableAbove(doc, pos) {
31
+ let before = doc.resolve(pos).nodeBefore;
32
+ while (before && !before.isTextblock && !before.isAtom)
33
+ before = before.lastChild;
34
+ return !!before?.isTextblock;
35
+ }
36
+ /**
37
+ * Enter at the very start of a top-level list's first item, where nothing above the
38
+ * list takes a caret → an empty paragraph above it, caret staying with the text it
39
+ * pushed down. A list that opens a document, or one under an island or a rule, has
40
+ * nowhere above to write and nothing else that opens one. Where a caret already
41
+ * fits above, the key is the conventional split instead, at every item alike: an
42
+ * escape a writer reaches by pressing Up needs no key of its own.
26
43
  *
27
- * Only where the list itself is not inside an item: in a NESTED list this pushes
28
- * an empty paragraph into the parent item (`list_item` is `block+`, so the shape
29
- * is representable and wrong), where the conventional split (an empty item above)
30
- * is what the next link does. An empty first item falls through to that same
44
+ * Also only where the list itself is not inside an item: in a nested list this
45
+ * pushes an empty paragraph into the parent item (`list_item` is `block+`, so the
46
+ * shape is representable and wrong), where the conventional split (an empty item
47
+ * above) is what the next link does. An empty first item falls through to that same
31
48
  * link, which exits the list instead.
32
49
  */
33
50
  function paragraphAboveList(itemType, paragraph) {
@@ -37,13 +54,15 @@ function paragraphAboveList(itemType, paragraph) {
37
54
  return false;
38
55
  if (state.selection.$from.parent.content.size === 0)
39
56
  return false;
57
+ if (writableAbove(state.doc, at.listPos))
58
+ return false;
40
59
  if (dispatch)
41
60
  dispatch(state.tr.insert(at.listPos, paragraph.create()).scrollIntoView());
42
61
  return true;
43
62
  };
44
63
  }
45
64
  /**
46
- * Backspace at the start of a list's FIRST item → lift it: one level out for a
65
+ * Backspace at the start of a list's first item → lift it: one level out for a
47
66
  * nested item, out of the list entirely (to a paragraph) at the top.
48
67
  */
49
68
  function liftAtListStart(itemType) {
@@ -55,10 +74,10 @@ function liftAtListStart(itemType) {
55
74
  };
56
75
  }
57
76
  /**
58
- * Backspace at the start of any LATER item → join its text into the previous
77
+ * Backspace at the start of any later item → join its text into the previous
59
78
  * item's last block, so the two items become one line.
60
79
  *
61
- * Not the base keymap's `joinBackward`, which merges the BLOCK and leaves
80
+ * Not the base keymap's `joinBackward`, which merges the block and leaves
62
81
  * `list_item(paragraph, paragraph)`: the item's marker gone while its text stays
63
82
  * on its own line, which the reference quill typesets as an unnumbered
64
83
  * continuation paragraph.
@@ -79,14 +98,14 @@ function mergeIntoPreviousItem(itemType) {
79
98
  * Tab more locally under the same rule (`prose/canon/VISUAL_EDITOR.md` §Chrome)
80
99
  * prepend a link ahead of these in `keymap.ts`: a `code_block` takes literal
81
100
  * indentation (`code.ts`), an island takes cell traversal once added. Outside all
82
- * of them every link returns false and the key is NOT swallowed: Tab keeps its
101
+ * of them every link returns false and the key is not swallowed: Tab keeps its
83
102
  * default meaning, which is the body's only keyboard exit and the open seam for a
84
103
  * shell structural keymap (VISUAL_EDITOR §Settled and open).
85
104
  *
86
- * `Enter` is a chain in precedence order: the paragraph-above gesture, then
105
+ * `Enter` is a chain in precedence order: the escape-above gesture, then
87
106
  * `splitListItem`, then `liftListItem`. The middle link carries two behaviors of
88
107
  * its own: it splits a non-empty item, and on an empty item it either splits the
89
- * WRAPPING item (a nested empty item, so Enter lifts exactly one level) or bails
108
+ * wrapping item (a nested empty item, so Enter lifts exactly one level) or bails
90
109
  * so the third link lifts a top-level empty item out to a paragraph. `Backspace`
91
110
  * forks on the item's index: the first item lifts, any later one merges.
92
111
  */
@@ -18,7 +18,7 @@ export declare function descriptorOf(m: ContentMark): Record<string, unknown>;
18
18
  /**
19
19
  * A stable grouping key for the mark diff: marks sharing a key union into one
20
20
  * coverage set. Formatting keys on its type; a link keys on type+url and an
21
- * unknown on type+attrs, because `applyChange`'s `remove` matches type AND attrs
21
+ * unknown on type+attrs, because `applyChange`'s `remove` matches type and attrs
22
22
  * (verified), so different urls / attrs are independent mark families.
23
23
  */
24
24
  export declare function markKey(descriptor: Record<string, unknown>): string;
@@ -49,7 +49,7 @@ export function descriptorOf(m) {
49
49
  /**
50
50
  * A stable grouping key for the mark diff: marks sharing a key union into one
51
51
  * coverage set. Formatting keys on its type; a link keys on type+url and an
52
- * unknown on type+attrs, because `applyChange`'s `remove` matches type AND attrs
52
+ * unknown on type+attrs, because `applyChange`'s `remove` matches type and attrs
53
53
  * (verified), so different urls / attrs are independent mark families.
54
54
  */
55
55
  export function markKey(descriptor) {