@quillmark/svelte 0.2.0 → 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 (116) hide show
  1. package/README.md +2 -2
  2. package/THEMING.md +11 -7
  3. package/dist/core/address.d.ts +7 -7
  4. package/dist/core/address.js +6 -6
  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 +10 -10
  19. package/dist/core/codec/field.d.ts +21 -26
  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 +230 -215
  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 +70 -26
  40. package/dist/core/errors.d.ts +5 -5
  41. package/dist/core/index.js +2 -2
  42. package/dist/core/lifecycle.js +1 -1
  43. package/dist/core/rebind.svelte.d.ts +2 -2
  44. package/dist/core/rebind.svelte.js +4 -4
  45. package/dist/core/teardown.d.ts +1 -1
  46. package/dist/core/teardown.js +1 -1
  47. package/dist/core/theme.css +144 -54
  48. package/dist/preset/index.css +1 -1
  49. package/dist/preset/recipes.css +31 -29
  50. package/dist/preset/scale.css +36 -25
  51. package/dist/preview/Preview.svelte +12 -10
  52. package/dist/preview/Preview.svelte.d.ts +3 -3
  53. package/dist/preview/bridge.js +4 -4
  54. package/dist/preview/controller.js +3 -3
  55. package/dist/preview/geometry.d.ts +2 -2
  56. package/dist/preview/geometry.js +2 -2
  57. package/dist/preview/index.js +3 -3
  58. package/dist/preview/overlay.d.ts +1 -1
  59. package/dist/preview/overlay.js +3 -3
  60. package/dist/preview/paint.js +15 -17
  61. package/dist/visual/ArrayField.svelte +80 -66
  62. package/dist/visual/ArrayField.svelte.d.ts +1 -1
  63. package/dist/visual/BooleanField.svelte +8 -6
  64. package/dist/visual/BooleanField.svelte.d.ts +2 -2
  65. package/dist/visual/Card.svelte +151 -325
  66. package/dist/visual/Card.svelte.d.ts +2 -6
  67. package/dist/visual/CardControls.svelte +1 -1
  68. package/dist/visual/DateField.svelte +19 -19
  69. package/dist/visual/DateField.svelte.d.ts +3 -3
  70. package/dist/visual/DiagnosticList.svelte +1 -1
  71. package/dist/visual/EnumField.svelte +16 -16
  72. package/dist/visual/EnumField.svelte.d.ts +2 -2
  73. package/dist/visual/Field.svelte +128 -115
  74. package/dist/visual/Field.svelte.d.ts +1 -1
  75. package/dist/visual/FieldHint.svelte +24 -42
  76. package/dist/visual/FieldHint.svelte.d.ts +1 -1
  77. package/dist/visual/FieldLabel.svelte +8 -8
  78. package/dist/visual/FormatPopover.svelte +80 -35
  79. package/dist/visual/NumberField.svelte +5 -5
  80. package/dist/visual/NumberField.svelte.d.ts +1 -1
  81. package/dist/visual/ObjectField.svelte +9 -9
  82. package/dist/visual/ObjectField.svelte.d.ts +1 -1
  83. package/dist/visual/ProseArrayElement.svelte +11 -13
  84. package/dist/visual/ProseArrayElement.svelte.d.ts +1 -1
  85. package/dist/visual/ProseField.svelte +53 -84
  86. package/dist/visual/ProseField.svelte.d.ts +12 -17
  87. package/dist/visual/SlashMenu.svelte +42 -16
  88. package/dist/visual/TextField.svelte +5 -5
  89. package/dist/visual/TextField.svelte.d.ts +1 -1
  90. package/dist/visual/TipsCard.svelte +43 -28
  91. package/dist/visual/VisualEditor.svelte +6 -6
  92. package/dist/visual/VisualEditorInner.svelte +122 -129
  93. package/dist/visual/controls.css +74 -30
  94. package/dist/visual/diagnostics.d.ts +5 -5
  95. package/dist/visual/diagnostics.js +3 -3
  96. package/dist/visual/domid.js +3 -3
  97. package/dist/visual/ext.d.ts +1 -1
  98. package/dist/visual/ext.js +1 -1
  99. package/dist/visual/icons/nodes.d.ts +3 -0
  100. package/dist/visual/icons/nodes.js +2 -1
  101. package/dist/visual/index.js +4 -4
  102. package/dist/visual/leaves.d.ts +3 -3
  103. package/dist/visual/leaves.js +1 -1
  104. package/dist/visual/links.d.ts +24 -0
  105. package/dist/visual/links.js +67 -0
  106. package/dist/visual/props.d.ts +8 -8
  107. package/dist/visual/signals.d.ts +6 -6
  108. package/dist/visual/strings.d.ts +14 -19
  109. package/dist/visual/strings.js +5 -7
  110. package/dist/visual/structure.d.ts +24 -17
  111. package/dist/visual/structure.js +8 -8
  112. package/dist/visual/synced.svelte.d.ts +1 -1
  113. package/dist/visual/synced.svelte.js +4 -4
  114. package/dist/visual/tips.d.ts +2 -2
  115. package/dist/visual/tips.js +3 -3
  116. package/package.json +1 -1
@@ -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) {
@@ -51,7 +51,7 @@ export function usvToPM(index, usvPos) {
51
51
  const usv = clamp(usvPos, 0, index.usvEnd);
52
52
  if (usv >= index.usvEnd)
53
53
  return index.usvEnd === 0 ? index.firstContentStart : index.pmEndContent;
54
- // The runs TILE the USV space from 0 (every text the walk appends pushes its run
54
+ // The runs tile the USV space from 0 (every text the walk appends pushes its run
55
55
  // first), so below `usvEnd` the run at or before `usv` is the run that owns it.
56
56
  const run = index.runs[lastRunAtOrBefore(index.runs, usv, usvStartOf)];
57
57
  if (run.kind === 'text')