@quillmark/svelte 0.8.0 → 0.10.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.
@@ -1,4 +1,4 @@
1
- import type { Attrs, Node as PMNode } from 'prosemirror-model';
1
+ import type { Attrs, Node as PMNode, 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`. */
@@ -35,3 +35,7 @@ export declare function consuming(state: EditorState, from: number, to: number,
35
35
  tr: Transaction;
36
36
  produced: Transaction;
37
37
  } | null;
38
+ /** The body-wide link of the leaf's key chains: `{}` for a schema with no paragraph
39
+ * to mint. Outermost of the structural links, being about the body, or about the
40
+ * caret's own line, rather than about the surface either stands in. */
41
+ export declare function blockKeymap(schema: Schema): Record<string, Command>;
@@ -1,4 +1,5 @@
1
1
  import { Selection } from 'prosemirror-state';
2
+ import { chainCommands, splitBlock } from 'prosemirror-commands';
2
3
  import { canJoin, findWrapping } from 'prosemirror-transform';
3
4
  /** The head of a textblock: where a shorthand fires, and where a pick lands once its
4
5
  * run is consumed. */
@@ -161,3 +162,123 @@ export function consuming(state, from, to, command) {
161
162
  tr.setSelection(Selection.fromJSON(tr.doc, produced.selection.toJSON()));
162
163
  return { tr, produced };
163
164
  }
165
+ /** The head of the body's first block: one position per open token above the caret,
166
+ * and nothing before it. */
167
+ function atBodyStart($from) {
168
+ return $from.pos === $from.depth;
169
+ }
170
+ /**
171
+ * Enter at the very start of a body whose first block takes no caret above it → an
172
+ * empty paragraph there, the caret staying with the text it pushed down.
173
+ *
174
+ * A code block, a quote and a list are the blocks `prosemirror-gapcursor` declines
175
+ * to sit beside, and nothing else mints a block above one: three keystrokes on an
176
+ * empty body (` ``` `, `> `, `- `) put one first, and a document carrying one there
177
+ * can never be given a title. The argument is `paragraphAboveList`'s, about the
178
+ * block rather than about lists, so this link comes first and that one holds the
179
+ * position it alone reaches, a list under an island or a rule.
180
+ *
181
+ * Declines wherever the first block is a plain textblock, whose Enter-split already
182
+ * leaves a paragraph above, and on an empty one, which the links after it answer.
183
+ */
184
+ const paragraphAtBodyStart = (state, dispatch) => {
185
+ const paragraph = state.schema.nodes.paragraph;
186
+ const { $from, empty } = state.selection;
187
+ if (!paragraph || !empty || !atBodyStart($from))
188
+ return false;
189
+ if ($from.parent.content.size === 0)
190
+ return false;
191
+ if ($from.depth === 1 && $from.parent.isTextblock && !$from.parent.type.spec.code)
192
+ return false;
193
+ dispatch?.(state.tr.insert(0, paragraph.create()).scrollIntoView());
194
+ return true;
195
+ };
196
+ /**
197
+ * Enter in an empty fence that opens the body → the fence, retyped to a paragraph.
198
+ *
199
+ * An empty first item exits its list and an empty first quoted paragraph lifts out of
200
+ * the quote, each leaving the one paragraph a caret above needs. A fence answers with
201
+ * `newlineInCode` instead — a line inside the block, in the direction already open —
202
+ * so the empty fence a body's first ` ``` ` mints holds a caret with nothing above it
203
+ * and no key that mints one.
204
+ *
205
+ * Retyping rather than inserting above, for the reason those exits leave one paragraph
206
+ * and not two: an empty fence carries nothing to keep.
207
+ */
208
+ const paragraphFromEmptyFence = (state, dispatch) => {
209
+ const paragraph = state.schema.nodes.paragraph;
210
+ const { $from, empty } = state.selection;
211
+ if (!paragraph || !empty || !atBodyStart($from))
212
+ return false;
213
+ if (!$from.parent.type.spec.code || $from.parent.content.size !== 0)
214
+ return false;
215
+ if (!fits($from, paragraph))
216
+ return false;
217
+ dispatch?.(state.tr.setBlockType($from.pos, $from.pos, paragraph).scrollIntoView());
218
+ return true;
219
+ };
220
+ /**
221
+ * Enter over a selection spanning two textblocks: take the selection, then split
222
+ * what is left, both in one transaction.
223
+ *
224
+ * The edit upstream's `splitBlock` means, computed against a document that exists.
225
+ * It deletes the selection inside its own transaction and then reads the default
226
+ * block's fit off the *pre-delete* document — `$from.node(splitDepth - 1)` against an
227
+ * index resolved in the new one — so where the range starts at the head of a
228
+ * `defining` block and ends inside a list, it retypes a `list_item` to a paragraph
229
+ * and throws `RangeError` out of the keystroke.
230
+ */
231
+ const splitAcrossBlocks = (state, dispatch) => {
232
+ const { $from, $to, empty } = state.selection;
233
+ if (empty || $from.parent === $to.parent)
234
+ return false;
235
+ if (!dispatch)
236
+ return true;
237
+ const tr = state.tr.deleteSelection();
238
+ splitBlock(state.apply(tr), (split) => {
239
+ for (const step of split.steps)
240
+ tr.step(step);
241
+ tr.setSelection(Selection.fromJSON(tr.doc, split.selection.toJSON()));
242
+ });
243
+ dispatch(tr.scrollIntoView());
244
+ return true;
245
+ };
246
+ /**
247
+ * Delete on an empty textblock → take the line, whatever stands below moving up
248
+ * whole and keeping what it is.
249
+ *
250
+ * The links below it leave the line standing: the base keymap joins the block below
251
+ * into it — a list's first item lifted out to fill it, its marker gone — and the atom
252
+ * link arms an island for the press after. Upstream takes the line only where the
253
+ * sibling holds the same kind of content, so a heading below keeps its level where a
254
+ * bullet loses its marker.
255
+ *
256
+ * Declines where the parent cannot spare the block: an empty item and an empty quote
257
+ * are the whole of what holds them, and the key is whichever link answers a construct
258
+ * with nothing in it. An empty fence is not that case — the doc spares it, and it
259
+ * carries no text to lose. Declines with nothing after it too: a line with no next
260
+ * line is Backspace's.
261
+ */
262
+ const takeEmptyLine = (state, dispatch) => {
263
+ const { $from, empty } = state.selection;
264
+ if (!empty || !$from.parent.isTextblock || $from.parent.content.size !== 0)
265
+ return false;
266
+ const index = $from.index(-1);
267
+ if (!$from.node(-1).canReplace(index, index + 1))
268
+ return false;
269
+ if (!Selection.findFrom(state.doc.resolve($from.after()), 1))
270
+ return false;
271
+ dispatch?.(state.tr.delete($from.before(), $from.after()).scrollIntoView());
272
+ return true;
273
+ };
274
+ /** The body-wide link of the leaf's key chains: `{}` for a schema with no paragraph
275
+ * to mint. Outermost of the structural links, being about the body, or about the
276
+ * caret's own line, rather than about the surface either stands in. */
277
+ export function blockKeymap(schema) {
278
+ if (!schema.nodes.paragraph)
279
+ return {};
280
+ return {
281
+ Enter: chainCommands(paragraphAtBodyStart, paragraphFromEmptyFence, splitAcrossBlocks),
282
+ Delete: takeEmptyLine
283
+ };
284
+ }
@@ -0,0 +1,10 @@
1
+ import type { Schema } from 'prosemirror-model';
2
+ import { Plugin } from 'prosemirror-state';
3
+ /**
4
+ * Rewrite every `\n` a transaction leaves in a non-`code` textblock to a `hard_break`.
5
+ *
6
+ * Reads the schema rather than a flag: the inline schemas declare neither node, and
7
+ * nothing puts a `\n` in one — a paste parses its whitespace away and the keys type
8
+ * characters.
9
+ */
10
+ export declare function linebreakPlugin(schema: Schema): Plugin;
@@ -0,0 +1,37 @@
1
+ import { Plugin } from 'prosemirror-state';
2
+ /**
3
+ * Rewrite every `\n` a transaction leaves in a non-`code` textblock to a `hard_break`.
4
+ *
5
+ * Reads the schema rather than a flag: the inline schemas declare neither node, and
6
+ * nothing puts a `\n` in one — a paste parses its whitespace away and the keys type
7
+ * characters.
8
+ */
9
+ export function linebreakPlugin(schema) {
10
+ const br = schema.nodes.hard_break;
11
+ return new Plugin({
12
+ appendTransaction(trs, _before, state) {
13
+ if (!br || !trs.some((tr) => tr.docChanged))
14
+ return null;
15
+ const at = [];
16
+ state.doc.descendants((node, pos) => {
17
+ // A code block's newlines are its content (`whitespace: 'pre'`).
18
+ if (node.type.spec.code)
19
+ return false;
20
+ if (!node.isText)
21
+ return true;
22
+ const text = node.text ?? '';
23
+ for (let i = text.indexOf('\n'); i >= 0; i = text.indexOf('\n', i + 1))
24
+ at.push(pos + i);
25
+ return true;
26
+ });
27
+ if (!at.length)
28
+ return null;
29
+ // A break and the `\n` it replaces are both one position wide, so an earlier
30
+ // replacement leaves every later one addressed.
31
+ const tr = state.tr;
32
+ for (const pos of at)
33
+ tr.replaceWith(pos, pos + 1, br.create());
34
+ return tr;
35
+ }
36
+ });
37
+ }
@@ -7,6 +7,8 @@ import { type Command } from 'prosemirror-state';
7
7
  * `Enter` is upstream's `newlineInCode`, and it is load-bearing rather than
8
8
  * cosmetic: inside a `list_item > code_block` the list link's `splitListItem`
9
9
  * otherwise splits the item in two, each half holding a code block. Every command
10
- * here declines outside a code block, so the list links keep the keys elsewhere.
10
+ * here declines away from a code block — inside one for the three keys about its
11
+ * text, against one for the two about its edge — so the list links keep the keys
12
+ * elsewhere.
11
13
  */
12
14
  export declare function codeKeymap(schema: Schema): Record<string, Command>;
@@ -76,8 +76,13 @@ const indentInCode = (state, dispatch) => {
76
76
  * Shift-Tab in a code block: remove one indent level from every covered line.
77
77
  *
78
78
  * Declines when no covered line carries an indent, so the key is not swallowed: it
79
- * falls through to the list link and then out of the leaf, which is the body's
80
- * keyboard exit from inside a code block (VISUAL_EDITOR §Chrome).
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.
81
86
  */
82
87
  const outdentInCode = (state, dispatch) => {
83
88
  const block = codeBlockAt(state);
@@ -98,6 +103,55 @@ const outdentInCode = (state, dispatch) => {
98
103
  }
99
104
  return true;
100
105
  };
106
+ /**
107
+ * The textblock the caret's own block stands directly against on `side`: its sibling
108
+ * under one parent, which is the only neighbour a press joins the text of. `null` at a
109
+ * container's edge, where the base keymap moves the whole block in or out rather than
110
+ * joining anything, and so retypes nothing.
111
+ */
112
+ function blockAgainst(state, side) {
113
+ const { $from, empty } = state.selection;
114
+ if (!empty || !$from.parent.isTextblock)
115
+ return null;
116
+ if ($from.parentOffset !== (side < 0 ? 0 : $from.parent.content.size))
117
+ return null;
118
+ const $edge = state.doc.resolve(side < 0 ? $from.before() : $from.after());
119
+ const node = side < 0 ? $edge.nodeBefore : $edge.nodeAfter;
120
+ if (!node?.isTextblock)
121
+ return null;
122
+ return { node, pos: side < 0 ? $edge.pos - node.nodeSize : $edge.pos };
123
+ }
124
+ /**
125
+ * Backspace at a fence's head or Delete at its end, and the same two presses from the
126
+ * block on the other side: retype the code block to a paragraph, and join nothing. The
127
+ * press after that is an ordinary join between two prose blocks.
128
+ *
129
+ * The join itself retypes one side's whole text as the other's kind on one keystroke —
130
+ * a fence's lines as prose, or a typed line as code — which is the destruction
131
+ * `atoms.ts` refuses for a neighbouring island, with nothing drawn first to say what is
132
+ * about to go. This is `toCodeBlock`'s inverse in its place (`blocks.ts`), and it takes
133
+ * no text with it: `linebreakReplacement` carries the fence's newlines across as breaks
134
+ * (`schema.ts`). The caret stays in the block it was in.
135
+ *
136
+ * Declines where both sides are code and where neither is: those joins retype nothing.
137
+ * An empty block on either side never reaches this at all: the block link takes the
138
+ * empty line ahead of the chain (`blocks.ts`), and there is no text to retype.
139
+ */
140
+ function retypeAtProseEdge(side) {
141
+ return (state, dispatch) => {
142
+ const paragraph = state.schema.nodes.paragraph;
143
+ const edge = blockAgainst(state, side);
144
+ if (!paragraph || !edge)
145
+ return false;
146
+ const { $from } = state.selection;
147
+ const inside = !!$from.parent.type.spec.code;
148
+ if (inside === !!edge.node.type.spec.code)
149
+ return false;
150
+ const at = inside ? $from.pos : edge.pos + 1;
151
+ dispatch?.(state.tr.setBlockType(at, at, paragraph).scrollIntoView());
152
+ return true;
153
+ };
154
+ }
101
155
  /**
102
156
  * The `code_block` link of the body's key chains: `{}` for the inline/plaintext
103
157
  * schemas, which declare no code node.
@@ -105,7 +159,9 @@ const outdentInCode = (state, dispatch) => {
105
159
  * `Enter` is upstream's `newlineInCode`, and it is load-bearing rather than
106
160
  * cosmetic: inside a `list_item > code_block` the list link's `splitListItem`
107
161
  * otherwise splits the item in two, each half holding a code block. Every command
108
- * here declines outside a code block, so the list links keep the keys elsewhere.
162
+ * here declines away from a code block — inside one for the three keys about its
163
+ * text, against one for the two about its edge — so the list links keep the keys
164
+ * elsewhere.
109
165
  */
110
166
  export function codeKeymap(schema) {
111
167
  if (!schema.nodes.code_block)
@@ -113,6 +169,8 @@ export function codeKeymap(schema) {
113
169
  return {
114
170
  Tab: indentInCode,
115
171
  'Shift-Tab': outdentInCode,
116
- Enter: newlineInCode
172
+ Enter: newlineInCode,
173
+ Backspace: retypeAtProseEdge(-1),
174
+ Delete: retypeAtProseEdge(1)
117
175
  };
118
176
  }
@@ -103,19 +103,16 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
103
103
  if (isListItemContainer(here)) {
104
104
  // Gather the maximal run of sibling `list_item` leaves at this depth
105
105
  // (same ordered/start), then split it into items by `ordinal`.
106
- const { ordered, start } = here;
106
+ const { ordered, start, instance } = here;
107
107
  let j = i + 1;
108
- let prevOrd = here.ordinal;
109
108
  while (j < leaves.length) {
110
109
  const c = atDepth(leaves[j], depth);
111
110
  if (!c || !isListItemContainer(c) || c.ordered !== ordered || c.start !== start)
112
111
  break;
113
- // An ordinal reset is an adjacent sibling list (content preserves the
114
- // shape): merging it here would re-encode `[0,1,0]` as `[0,1,2]`,
115
- // breaking the decode round-trip on the first edit.
116
- if (c.ordinal < prevOrd)
112
+ // `instance` is the boundary between two adjacent lists; the normalizer
113
+ // numbers a run's `ordinal`s gaplessly from 0, so a reset carries none.
114
+ if (c.instance !== instance)
117
115
  break;
118
- prevOrd = c.ordinal;
119
116
  j++;
120
117
  }
121
118
  const run = leaves.slice(i, j);
@@ -162,11 +159,14 @@ function ordinalAt(leaf, depth) {
162
159
  const c = atDepth(leaf, depth);
163
160
  return c && isListItemContainer(c) ? c.ordinal : -1;
164
161
  }
165
- /** Identity of a container for run gathering: its name plus its payload, NUL-joined
166
- * as `markKey` joins a mark's, so no `attrs` content can forge a name boundary.
167
- * Every arm but `list_item` (which has its own run rule) keys here. */
162
+ /** Identity of a container for run gathering: its name, its `instance` and its payload,
163
+ * NUL-joined as `markKey` joins a mark's, so no `attrs` content can forge a name
164
+ * boundary. `instance` is what tells one container from an adjacent sibling of identical
165
+ * shape, which contiguity alone reads as one. Every arm but `list_item` (which has its
166
+ * own run rule) keys here. */
168
167
  function containerKey(c) {
169
- return 'attrs' in c ? `${c.container}\u0000${JSON.stringify(c.attrs)}` : c.container;
168
+ const id = `${c.container}\u0000${c.instance}`;
169
+ return 'attrs' in c ? `${id}\u0000${JSON.stringify(c.attrs)}` : id;
170
170
  }
171
171
  /** A single leaf block node (para/heading/code/rule/island, or an unknown kind
172
172
  * carried on a paragraph) from its segments. `kind` is an open set, so the two
@@ -35,15 +35,41 @@ export function pmToContent(doc) {
35
35
  const { text, lines, marks, islands } = scanDoc(doc);
36
36
  return { text, lines, marks, islands };
37
37
  }
38
- /** Walk a block-content fragment; `contentStart` is the PM pos before its first child. */
38
+ /** Walk a block-content fragment; `contentStart` is the PM pos before its first child.
39
+ * Two container siblings of one run shape read as a single container — contiguity plus
40
+ * an equal path is what the store means by "the same one" — so each alternates its
41
+ * `instance` against the sibling before it. Sibling adjacency is the whole scope: a
42
+ * container one level in has a differing path above it, which already tells it apart. */
39
43
  function scanBlocks(acc, parent, contentStart, containers) {
40
44
  let pm = contentStart;
45
+ let prevShape = null;
46
+ let instance = 0;
41
47
  parent.forEach((child) => {
42
- scanBlock(acc, child, pm, containers);
48
+ const shape = runShape(child);
49
+ instance = shape !== null && shape === prevShape ? instance ^ 1 : 0;
50
+ prevShape = shape;
51
+ scanBlock(acc, child, pm, containers, instance);
43
52
  pm += child.nodeSize;
44
53
  });
45
54
  }
46
- function scanBlock(acc, node, nodePos, containers) {
55
+ /** A block's container run shape, `ordinal` and `instance` aside; null where it opens
56
+ * no container. A list's `start` is not in it — the store welds two runs differing only
57
+ * there — and an unknown's `attrs` is, as `containerKey` reads it on the way back. */
58
+ function runShape(node) {
59
+ switch (node.type.name) {
60
+ case 'blockquote':
61
+ return 'quote';
62
+ case 'bullet_list':
63
+ return 'list\u0000bullet';
64
+ case 'ordered_list':
65
+ return 'list\u0000ordered';
66
+ case 'unknown_container':
67
+ return `unknown\u0000${node.attrs.container}\u0000${JSON.stringify(node.attrs.attrs)}`;
68
+ default:
69
+ return null;
70
+ }
71
+ }
72
+ function scanBlock(acc, node, nodePos, containers, instance) {
47
73
  const name = node.type.name;
48
74
  const contentStart = nodePos + 1;
49
75
  switch (name) {
@@ -84,12 +110,12 @@ function scanBlock(acc, node, nodePos, containers) {
84
110
  acc.lastContentEndPm = nodePos + 1;
85
111
  break;
86
112
  case 'blockquote':
87
- scanBlocks(acc, node, contentStart, [...containers, { container: 'quote' }]);
113
+ scanBlocks(acc, node, contentStart, [...containers, { container: 'quote', instance }]);
88
114
  break;
89
115
  case 'unknown_container':
90
116
  scanBlocks(acc, node, contentStart, [
91
117
  ...containers,
92
- { container: node.attrs.container, attrs: node.attrs.attrs }
118
+ { container: node.attrs.container, attrs: node.attrs.attrs, instance }
93
119
  ]);
94
120
  break;
95
121
  case 'bullet_list':
@@ -102,7 +128,8 @@ function scanBlock(acc, node, nodePos, containers) {
102
128
  container: 'list_item',
103
129
  ordered,
104
130
  start,
105
- ordinal: index
131
+ ordinal: index,
132
+ instance
106
133
  };
107
134
  scanBlocks(acc, item, itemPos + 1, [...containers, container]);
108
135
  itemPos += item.nodeSize;
@@ -141,7 +168,16 @@ function scanInline(acc, block, contentStart, containers, kind) {
141
168
  let pm = contentStart;
142
169
  block.forEach((child) => {
143
170
  if (child.isText) {
144
- emitText(acc, child.text ?? '', pm, child.marks);
171
+ const text = child.text ?? '';
172
+ emitText(acc, text, pm, child.marks);
173
+ // A `\n` in a textblock that is not `whitespace: 'pre'` is the same line
174
+ // boundary a hard break is: `paragraph` is `inline*`, so the schema admits
175
+ // one, and `joinTextblocksAround` — a bare `replaceStep` running neither
176
+ // `clearIncompatible` nor `replaceNewlines` — leaves one. It rides the text
177
+ // run, where PM and USV advance together.
178
+ const breaks = text.split('\n').length - 1;
179
+ for (let i = 0; i < breaks; i++)
180
+ acc.lines.push({ containers, continues: true, ...kind });
145
181
  }
146
182
  else if (child.type.name === 'hard_break') {
147
183
  // A within-block hard break: the content `\n` (a `continues` line).
@@ -196,7 +232,7 @@ export function contentEdit(oldRt, newRt) {
196
232
  export function lower(edit, opts = {}) {
197
233
  const { oldRt, newRt } = edit;
198
234
  const { delta, islandOps, insertedAt } = splitIslands(edit);
199
- const lineOps = diffLines(oldRt, newRt);
235
+ const lineOps = diffLines(oldRt, newRt, edit.delta);
200
236
  const markOps = diffMarks(oldRt, newRt, delta, insertedAt, opts);
201
237
  const bundle = {};
202
238
  if (delta)
@@ -235,9 +271,39 @@ function diffText(oldText, newText) {
235
271
  ops.push({ retain: s });
236
272
  return { ops };
237
273
  }
238
- /** Per-line `setContainers` / `setKind` / `setContinues` when line metadata changed; else none. */
239
- function diffLines(oldRt, newRt) {
240
- if (lineMetaEqual(oldRt.lines, newRt.lines))
274
+ /**
275
+ * Whether the splice moves a line boundary — a `\n` deleted, or one inserted. Where
276
+ * it does not, every line keeps the metadata it had and equal metadata on both sides
277
+ * is the whole story. Where it does, the store's split/join inheritance decides what
278
+ * each line ends up carrying, and an array that still matches position for position
279
+ * says nothing about it: one splice that both joins a boundary and opens another
280
+ * leaves the line count and the metadata array unchanged while the kinds slide.
281
+ */
282
+ function movesBoundary(oldText, delta) {
283
+ if (!delta)
284
+ return false;
285
+ let at = 0;
286
+ let cps;
287
+ for (const op of delta.ops) {
288
+ if ('retain' in op)
289
+ at += op.retain;
290
+ else if ('insert' in op) {
291
+ if (op.insert.includes('\n'))
292
+ return true;
293
+ }
294
+ else {
295
+ cps ??= codePoints(oldText);
296
+ if (cps.slice(at, at + op.delete).includes('\n'))
297
+ return true;
298
+ at += op.delete;
299
+ }
300
+ }
301
+ return false;
302
+ }
303
+ /** Per-line `setContainers` / `setKind` / `setContinues` when line metadata changed or
304
+ * the splice moved a boundary; else none. */
305
+ function diffLines(oldRt, newRt, delta) {
306
+ if (!movesBoundary(oldRt.text, delta) && lineMetaEqual(oldRt.lines, newRt.lines))
241
307
  return [];
242
308
  const ops = [];
243
309
  // Force every new line's metadata. Redundant ops are safe no-ops (verified),
@@ -152,10 +152,11 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
152
152
  * {@link createField} and the by-value inline editor (`ProseValue`), so the two
153
153
  * never fork the keymap/plugin ordering. History first, then any leaf-specific
154
154
  * plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
155
- * a by-value leaf passes none), the
156
- * markdown-shorthand input rules, then the field keymap over the base keymap, and
157
- * last the two that answer to a caret or a selection beside a block: the gap cursor
158
- * and {@link pastAtomPlugin}.
155
+ * a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
156
+ * the rest of the stack leaves, then the markdown-shorthand input rules, the field
157
+ * keymap over the base keymap, and last the three that answer to a block leaf's own
158
+ * shapes: the gap cursor, {@link pastAtomPlugin}, and the island paste pass
159
+ * (`islands.ts`).
159
160
  *
160
161
  * Every mark-shaped plugin reads the schema rather than a flag: over
161
162
  * `plaintextSchema` the shorthand rules build nothing (each is guarded on its mark
@@ -22,9 +22,11 @@ import { reportError, errorMessage } from '../errors.js';
22
22
  import { decode } from './decode.js';
23
23
  import { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
24
24
  import { lower, pmToContent, contentEdit } from './encode.js';
25
+ import { islandPastePlugin } from './islands.js';
25
26
  import { anchorsFromContent } from './marks.js';
26
27
  import { createReconciler } from './reconcile.js';
27
28
  import { inputRulesPlugin } from './inputrules.js';
29
+ import { linebreakPlugin } from './breaks.js';
28
30
  import { bodyKeymap } from './keymap.js';
29
31
  import { blockSchema, inlineSchema, plaintextSchema } from './schema.js';
30
32
  import { DEFAULT_TABLE_STRINGS, tableNodeView } from './table-view.js';
@@ -391,10 +393,11 @@ export function createField(opts) {
391
393
  * {@link createField} and the by-value inline editor (`ProseValue`), so the two
392
394
  * never fork the keymap/plugin ordering. History first, then any leaf-specific
393
395
  * plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
394
- * a by-value leaf passes none), the
395
- * markdown-shorthand input rules, then the field keymap over the base keymap, and
396
- * last the two that answer to a caret or a selection beside a block: the gap cursor
397
- * and {@link pastAtomPlugin}.
396
+ * a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
397
+ * the rest of the stack leaves, then the markdown-shorthand input rules, the field
398
+ * keymap over the base keymap, and last the three that answer to a block leaf's own
399
+ * shapes: the gap cursor, {@link pastAtomPlugin}, and the island paste pass
400
+ * (`islands.ts`).
398
401
  *
399
402
  * Every mark-shaped plugin reads the schema rather than a flag: over
400
403
  * `plaintextSchema` the shorthand rules build nothing (each is guarded on its mark
@@ -402,17 +405,17 @@ export function createField(opts) {
402
405
  * typed without a second rule saying so.
403
406
  */
404
407
  export function proseLeafPlugins(schema, opts) {
405
- const list = [history(), ...(opts.afterHistory ?? [])];
408
+ const list = [history(), ...(opts.afterHistory ?? []), linebreakPlugin(schema)];
406
409
  if (opts.slash)
407
410
  list.push(slashPlugin(opts.slash));
408
411
  if (!opts.noInputRules)
409
412
  list.push(inputRulesPlugin(schema));
410
413
  list.push(keymap(editorKeymap(schema, opts.inline, !!opts.slash)));
411
414
  list.push(keymap(baseKeymap));
412
- // Both answer to something only a block leaf holds: an inline leaf is one textblock
413
- // with no island and no gap to put a cursor in.
415
+ // All three answer to something only a block leaf holds: an inline leaf is one
416
+ // textblock with no island and no gap to put a cursor in.
414
417
  if (!opts.inline)
415
- list.push(gapCursor(), pastAtomPlugin());
418
+ list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin());
416
419
  if (opts.placeholder)
417
420
  list.push(placeholderPlugin(opts.placeholder));
418
421
  return list;
@@ -1,4 +1,5 @@
1
- import type { Node as PMNode, NodeSpec } from 'prosemirror-model';
1
+ import { type Node as PMNode, type NodeSpec } from 'prosemirror-model';
2
+ import { Plugin } from 'prosemirror-state';
2
3
  import type { ContentIsland, TableProps } from '@quillmark/wasm';
3
4
  /** The `U+FFFC` object-replacement char that occupies one island slot in `text`. */
4
5
  export declare const ISLAND_SLOT = "\uFFFC";
@@ -18,9 +19,11 @@ export interface IslandNodeAttrs {
18
19
  }
19
20
  /** Build a content island entry from a PM island node's attrs (block or inline). */
20
21
  export declare function islandEntryFromNode(attrs: IslandNodeAttrs): ContentIsland;
21
- /** A node's `TableProps`, or `undefined` for any other island: the boundary's own
22
- * guard over the entry the node carries, so the open `type` arm narrows once here
23
- * rather than at each reader. */
22
+ /** A node's `TableProps`, or `undefined` for any other island and for a payload of the
23
+ * wrong shape: the boundary's own guard over the entry the node carries, so the open
24
+ * `type` arm narrows once here rather than at each reader. The type says which reader,
25
+ * the shape says whether it can read; every caller draws the placeholder for
26
+ * `undefined` (`table-view.ts` §`render`). */
24
27
  export declare function tablePropsOfNode(node: PMNode): TableProps | undefined;
25
28
  /**
26
29
  * A minter over one document: the positional `isl-{n}` sequence continued past the
@@ -38,3 +41,20 @@ export declare function islandMinter(doc: PMNode): () => string;
38
41
  /** The next island id for a field: one draw from {@link islandMinter}, which is what a
39
42
  * command placing exactly one island wants. */
40
43
  export declare function mintIslandId(doc: PMNode): string;
44
+ /**
45
+ * The paste's island pass: an id the field already holds is re-minted on the way in.
46
+ *
47
+ * An id is an identity in the content and unique across the field, so two islands
48
+ * wearing one is a projection the store has no shape for. A copy rather than a cut is
49
+ * where that arrives, and a paste out of a second field is the other; a cut pasted back
50
+ * collides with nothing and keeps the identity it had.
51
+ *
52
+ * The test is the document as it stands, which a drag moving a selection over an island
53
+ * reads one too many: the original is still there when the slice is transformed and gone
54
+ * when it lands, so that island arrives under a fresh id. A fresh id costs it nothing a
55
+ * duplicate would cost the whole field.
56
+ *
57
+ * A plugin prop rather than a view option: the leaf mounts the codec's plugin stack, and
58
+ * which nodes carry an identity is the codec's to know.
59
+ */
60
+ export declare function islandPastePlugin(): Plugin;