@quillmark/svelte 0.27.0 → 0.28.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 (95) hide show
  1. package/NOTICE +4 -4
  2. package/README.md +26 -8
  3. package/THEMING.md +10 -8
  4. package/dist/core/address.d.ts +35 -64
  5. package/dist/core/address.js +35 -55
  6. package/dist/core/bloom.d.ts +4 -10
  7. package/dist/core/bloom.js +14 -27
  8. package/dist/core/codec/blocks.d.ts +41 -43
  9. package/dist/core/codec/blocks.js +97 -130
  10. package/dist/core/codec/code.d.ts +7 -10
  11. package/dist/core/codec/code.js +37 -46
  12. package/dist/core/codec/decode.d.ts +19 -23
  13. package/dist/core/codec/decode.js +76 -111
  14. package/dist/core/codec/encode.d.ts +11 -15
  15. package/dist/core/codec/encode.js +60 -67
  16. package/dist/core/codec/field.d.ts +62 -95
  17. package/dist/core/codec/field.js +107 -179
  18. package/dist/core/codec/islands.d.ts +33 -41
  19. package/dist/core/codec/islands.js +76 -86
  20. package/dist/core/codec/lists.d.ts +17 -26
  21. package/dist/core/codec/lists.js +117 -157
  22. package/dist/core/codec/marks.d.ts +4 -3
  23. package/dist/core/codec/marks.js +8 -3
  24. package/dist/core/codec/prose.css +84 -46
  25. package/dist/core/codec/schema.d.ts +18 -23
  26. package/dist/core/codec/schema.js +84 -76
  27. package/dist/core/codec/slash.d.ts +42 -48
  28. package/dist/core/codec/slash.js +0 -0
  29. package/dist/core/codec/table-copy.d.ts +4 -3
  30. package/dist/core/codec/table-copy.js +25 -7
  31. package/dist/core/codec/table-view.d.ts +12 -12
  32. package/dist/core/codec/table-view.js +460 -383
  33. package/dist/core/codec/table.d.ts +94 -61
  34. package/dist/core/codec/table.js +331 -83
  35. package/dist/core/codec/tasks.d.ts +6 -0
  36. package/dist/core/codec/tasks.js +30 -3
  37. package/dist/core/errors.d.ts +15 -30
  38. package/dist/core/errors.js +2 -6
  39. package/dist/core/index.js +7 -12
  40. package/dist/core/lifecycle.d.ts +7 -16
  41. package/dist/core/lifecycle.js +10 -21
  42. package/dist/core/rebind.svelte.d.ts +10 -20
  43. package/dist/core/rebind.svelte.js +15 -27
  44. package/dist/core/rungs.d.ts +14 -11
  45. package/dist/core/rungs.js +17 -19
  46. package/dist/core/teardown.d.ts +4 -6
  47. package/dist/core/teardown.js +7 -10
  48. package/dist/core/theme.css +74 -54
  49. package/dist/index.js +3 -8
  50. package/dist/preset/app.css +3 -1
  51. package/dist/preset/index.css +6 -8
  52. package/dist/preset/recipes.css +12 -15
  53. package/dist/preset/scale.css +18 -17
  54. package/dist/preset/shell.css +10 -0
  55. package/dist/preview/bridge.d.ts +18 -22
  56. package/dist/preview/bridge.js +29 -58
  57. package/dist/preview/controller.d.ts +21 -29
  58. package/dist/preview/controller.js +14 -39
  59. package/dist/preview/geometry.d.ts +13 -34
  60. package/dist/preview/geometry.js +13 -34
  61. package/dist/preview/index.js +4 -16
  62. package/dist/preview/paint.d.ts +3 -3
  63. package/dist/preview/paint.js +28 -68
  64. package/dist/preview/strings.d.ts +1 -1
  65. package/dist/preview/strings.js +2 -14
  66. package/dist/preview/zoom.d.ts +18 -23
  67. package/dist/preview/zoom.js +28 -27
  68. package/dist/visual/ArrayField.svelte +77 -16
  69. package/dist/visual/BooleanField.svelte +27 -14
  70. package/dist/visual/Card.svelte +61 -48
  71. package/dist/visual/CardControls.svelte +5 -11
  72. package/dist/visual/DateField.svelte +20 -4
  73. package/dist/visual/EnumField.svelte +3 -0
  74. package/dist/visual/FieldHint.svelte +2 -1
  75. package/dist/visual/FormatPopover.svelte +82 -15
  76. package/dist/visual/FormatPopover.svelte.d.ts +4 -0
  77. package/dist/visual/MatrixAdd.svelte +30 -12
  78. package/dist/visual/MatrixField.svelte +24 -16
  79. package/dist/visual/NumberField.svelte +7 -0
  80. package/dist/visual/ObjectField.svelte +4 -4
  81. package/dist/visual/ProseField.svelte +0 -5
  82. package/dist/visual/TipsCard.svelte +11 -10
  83. package/dist/visual/VariantField.svelte +3 -1
  84. package/dist/visual/VisualEditorInner.svelte +19 -3
  85. package/dist/visual/controls.css +7 -2
  86. package/dist/visual/fonts.d.ts +18 -0
  87. package/dist/visual/fonts.js +59 -0
  88. package/dist/visual/icons/nodes.d.ts +0 -5
  89. package/dist/visual/icons/nodes.js +0 -4
  90. package/dist/visual/props.d.ts +14 -1
  91. package/dist/visual/strings.d.ts +7 -0
  92. package/dist/visual/strings.js +5 -2
  93. package/dist/visual/structure.d.ts +43 -87
  94. package/dist/visual/structure.js +42 -83
  95. package/package.json +2 -2
@@ -1,18 +1,12 @@
1
- // The prose leaf: `createField` (VISUAL_EDITOR §Surface). One content field, one
2
- // PM `EditorState`/`EditorView`, wired to the WASM edit surface. It reads the
3
- // leaf's `Content`, decodes to a PM state, mounts a view + plugin stack (history,
4
- // keymap, input rules, the anchor-position plugin), and `dispatchTransaction`:
5
- // (a) apply optimistically to the view,
6
- // (b) lower the tr to a `ChangeBundle` (or `overwrite` for a field not yet at
7
- // content rest, which has nothing to splice),
8
- // (c) commit via `doc.applyChange(addr, bundle)`, or a `text` leaf's whole text
9
- // through the typed writer,
10
- // (d) fire `onChange` when the transaction committed, then `onCaretMove` with
11
- // the new USV caret, which a bare selection move fires on its own.
12
- // On an `applyChange` throw the optimistic PM state stays and the failure reports
13
- // through `onError`: never a crash. Caret continuity across own-edits is the PM
14
- // `StepMap`; an external content change re-hydrates through `applyExternal`, gated
15
- // by `reconcile` except where the hold it re-evaluates flips.
1
+ // The prose leaf: `createField` (VISUAL_EDITOR §Surface). One content field, one PM view wired to
2
+ // the WASM edit surface.
3
+ //
4
+ // `dispatchTransaction` applies the transaction optimistically, lowers it to a `ChangeBundle` (or
5
+ // `overwrite` for a field not yet at content rest, which has nothing to splice), commits via
6
+ // `doc.applyChange` (a `text` leaf's whole text through the typed writer), then fires `onChange` if
7
+ // it committed and `onCaretMove`. A throw keeps the optimistic PM state and reports through
8
+ // `onError`. An external change re-hydrates through `applyExternal`, gated by `reconcile` except
9
+ // where the hold it re-evaluates flips.
16
10
  import { baseKeymap, toggleMark } from 'prosemirror-commands';
17
11
  import { gapCursor } from 'prosemirror-gapcursor';
18
12
  import { history, redo, undo } from 'prosemirror-history';
@@ -70,26 +64,18 @@ function anchorPlugin(seed) {
70
64
  }
71
65
  });
72
66
  }
73
- /** Read the leaf's `Content` for `addr`: `reader.getContent` (DOCUMENT_MODEL),
74
- * the schema-plane read that decodes through the codec the field's declared type
75
- * names. A field value, or the body `Content` when `addr.field` is absent, without
76
- * materializing the whole card. Reads are total over the field axis, so an absent
77
- * field, a default-only richtext field (e.g. `tag_line`, no stored value until
78
- * first edited), reads `undefined`; decode an empty content rather than crash, and
79
- * the first edit overwrites it. Only an out-of-range `addr.card` throws, unreachable
80
- * here: a removed card unmounts its keyed leaf before a stale index is read.
81
- *
82
- * Bound rather than verbatim, because the storage form is not the leaf's business
83
- * and the verbatim read cannot answer it alone. A field the editor committed rests
84
- * as `Content`; a field a markdown parse left rests as the authored string, and
85
- * what that string means is the declared type's to say: `richtext` is markdown,
86
- * `text` is literal text, and reading one as the other silently eats every `*` the
87
- * author typed. */
67
+ /**
68
+ * Read the leaf's `Content` for `addr` through `reader.getContent` (DOCUMENT_MODEL): the body when
69
+ * `addr.field` is absent. Bound by the declared type rather than a verbatim read, since a string a
70
+ * markdown parse left means markdown for `richtext` and literal text for `text`; reading one as the
71
+ * other eats every `*` the author typed. An absent field (a default-only richtext field before its
72
+ * first edit) reads `undefined` and decodes empty; the first edit overwrites it. Only an
73
+ * out-of-range `addr.card` throws, unreachable since a removed card unmounts its keyed leaf.
74
+ */
88
75
  function readLeaf(reader, addr) {
89
76
  return reader.getContent(addr) ?? emptyContent();
90
77
  }
91
- /** The canonical empty `Content`: one empty `para` line. The empty a prose
92
- * leaf (or an array's prose element) seeds from. */
78
+ /** The canonical empty `Content`: one empty `para` line. */
93
79
  export function emptyContent() {
94
80
  return { text: '', lines: [{ containers: [], kind: 'para' }], marks: [], islands: [] };
95
81
  }
@@ -97,24 +83,21 @@ export function emptyContent() {
97
83
  export function isEmptyContent(rt) {
98
84
  return contentEqual(rt, emptyContent());
99
85
  }
100
- /** Whether ops may commit here: the stored value is a `Content` object, so
101
- * `applyChange` splices exactly the content the leaf read and PM is showing.
102
- *
103
- * The two other rest forms both take `overwrite` instead. An unset field (`undefined`;
104
- * a default-only richtext field before its first edit) has nothing to splice, and an
105
- * authored string nothing to lose: content-only marks do not survive markdown, so a
106
- * string carries no anchors to pay. A body always rests as `Content`. A `text` leaf
107
- * never asks: it commits through the writer ({@link createField}). */
86
+ /**
87
+ * Whether ops may commit here: the stored value is a `Content` object, so `applyChange` splices
88
+ * exactly what PM shows. The other rest forms take `overwrite`: an unset field has nothing to
89
+ * splice, and an authored string carries no anchors to pay. A body always rests as `Content`; a
90
+ * `text` leaf commits through the writer ({@link createField}).
91
+ */
108
92
  function opsCommittable(doc, addr) {
109
93
  const stored = doc.getStored(addr);
110
94
  return stored !== null && typeof stored === 'object';
111
95
  }
112
- /** The naming attributes for the `contenteditable`, shared by every prose leaf
113
- * (`ProseValue` takes it too, so an addressed leaf and a by-value one name their
114
- * regions by one rule). `aria-labelledby` supersedes `aria-label`: a leaf carrying
115
- * both is the ambiguity where implementations disagree about which wins, and
116
- * `undefined` is returned whole when a leaf takes neither, since ProseMirror reads
117
- * the absence, not an empty object. */
96
+ /**
97
+ * The naming attributes for the `contenteditable`, shared with `ProseValue`. `aria-labelledby`
98
+ * supersedes `aria-label`, and `undefined` is returned whole when a leaf takes neither, since
99
+ * ProseMirror reads the absence.
100
+ */
118
101
  export function proseAttributes(opts) {
119
102
  const attrs = {};
120
103
  if (opts.labelledBy)
@@ -141,13 +124,10 @@ export function createField(opts) {
141
124
  const { doc, addr, container } = opts;
142
125
  const inline = !!opts.inline;
143
126
  const plaintext = !!opts.plaintext;
144
- // The declared type picks the schema, and the schema is the whole of what
145
- // `plaintext` suppresses: no mark types to toggle, to paste in, or to mint a
146
- // shorthand with (`schema.ts`).
127
+ // The declared type picks the schema; `plaintext` suppresses every mark (`schema.ts`).
147
128
  const schema = leafSchema({ plaintext, inline });
148
129
  const block = isBlockSchema(schema);
149
- // Bound once and held: the reader is a `{quill, doc}` pair that reads live, so
150
- // every read below sees the commit before it.
130
+ // Bound once: the reader reads live, so every read sees the commit before it.
151
131
  const reader = opts.quill.reader(doc);
152
132
  const writer = opts.quill.writer(doc);
153
133
  let fallback = opts.fallback;
@@ -164,13 +144,10 @@ export function createField(opts) {
164
144
  // The ghost's live cell: the ghost plugin reads it per pass, so moving it is an
165
145
  // assignment plus a re-render rather than a rebuilt plugin stack.
166
146
  let placeholderText = opts.placeholder;
167
- // The views nested inside this leaf: one per table cell (`table-view.ts`). The
168
- // leaf holds the set because chrome asks the leaf, not the island, which view
169
- // holds the caret: the format popover raises over a cell exactly as over the
170
- // body, and only the leaf knows both.
147
+ // The views nested inside this leaf, one per table cell (`table-view.ts`): chrome asks the leaf
148
+ // which view holds the caret.
171
149
  const nested = new Set();
172
- // The menu's report channel, and the whole of what a leaf holds of it: the
173
- // vocabulary is the codec's own constant (`slash.ts`), not a wording to derive.
150
+ // The menu's report channel; the vocabulary is the codec's own constant (`slash.ts`).
174
151
  const slash = block ? opts.onSlash : undefined;
175
152
  // Judged of the value being mounted rather than read off a diagnostic: a routed set
176
153
  // can predate the value it arrives with, and `validation::not_plain` stands in for
@@ -183,13 +160,9 @@ export function createField(opts) {
183
160
  const seeded = buildState(reconciler.last);
184
161
  const state = seeded.state;
185
162
  index = seeded.index;
186
- // The clearance PM's reveal keeps around the caret, in this leaf's own line box.
187
- // Its default is 5px, which is a caret visible and unusable: the next line typed
188
- // lands past the fold, the reason the preview's own scroll clears a target by the
189
- // target's height (PREVIEW.md §"Follow-the-caret scroll"). Measured off the
190
- // mounted box rather than restated from the rung, and dropped where the
191
- // derivation is out of reach (jsdom), a fallback there being the scale written
192
- // where `check:style` cannot read it.
163
+ // The clearance PM's reveal keeps around the caret, in this leaf's own line box: PM's 5px
164
+ // default leaves the next typed line past the fold. Measured off the mounted box and dropped
165
+ // where the derivation is out of reach (jsdom).
193
166
  const lineBox = Number.parseFloat(getComputedStyle(container).lineHeight);
194
167
  const clearance = Number.isFinite(lineBox) ? lineBox : undefined;
195
168
  // Prepended, so what the caller draws inside `container` (a held leaf's note)
@@ -211,9 +184,8 @@ export function createField(opts) {
211
184
  nested.add(cellView);
212
185
  return () => nested.delete(cellView);
213
186
  },
214
- // A focus event does not bubble, so the leaf's own `focus` handler
215
- // never fires for a cell: without this the active address would not
216
- // follow a caret clicked straight into a table.
187
+ // A focus event does not bubble, so the leaf's `focus` handler never fires
188
+ // for a cell.
217
189
  onCellFocus: () => opts.onFocus?.(addr),
218
190
  clearance
219
191
  })
@@ -230,25 +202,20 @@ export function createField(opts) {
230
202
  if (!held && (tr.docChanged || tr.getMeta(anchorKey)))
231
203
  defaulted = false;
232
204
  view.updateState(next); // (a) optimistic
233
- // One walk per structural change: the index and the projection the commit
234
- // diffs both come off it. A selection-only transaction leaves the document
235
- // it was built from, so the index over it still holds.
205
+ // One walk per structural change; a selection-only transaction leaves the document, so
206
+ // the index still holds.
236
207
  const scan = tr.docChanged ? scanDoc(next.doc) : undefined;
237
208
  if (scan)
238
209
  index = lineIndexOf(scan);
239
- // Commit a content edit or an anchor mutation. An anchor insert/remove is
240
- // zero-width, so `docChanged` is false: the `anchorKey` meta is what
241
- // routes it through the same commit path (the diff emits the anchor op).
210
+ // Commit a content edit or an anchor mutation; an anchor edit is zero-width
211
+ // (`docChanged` false), so the `anchorKey` meta routes it.
242
212
  if (!held && (tr.docChanged || tr.getMeta(anchorKey))) {
243
213
  // (d) the change signal, only for what actually landed.
244
214
  const newRt = scan ? scanContent(scan) : pmToContent(next.doc);
245
215
  if (commitEdit(oldRt, newRt))
246
216
  opts.onChange?.(addr);
247
217
  }
248
- // The caret, for both structural and selection-only changes: a host
249
- // following the caret wants an arrow key, and a host recompiling wants
250
- // `onChange`. Reported after the commit, so the edit has landed by the
251
- // time the caret names where it is.
218
+ // The caret, for structural and selection-only changes, reported after the commit.
252
219
  opts.onCaretMove?.(addr, pmToUsv(index, next.selection.head));
253
220
  },
254
221
  handleDOMEvents: {
@@ -258,8 +225,10 @@ export function createField(opts) {
258
225
  }
259
226
  }
260
227
  });
261
- /** The state, and the index over the document it was built from: the seed reads one
262
- * and every caller wants the same one, and `buildLineIndex` is a whole walk. */
228
+ /**
229
+ * The state, and the index over the document it was built from (`buildLineIndex` is a whole
230
+ * walk).
231
+ */
263
232
  function buildState(rt) {
264
233
  if (held) {
265
234
  const pmDoc = decode(rt, blockSchema);
@@ -293,25 +262,22 @@ export function createField(opts) {
293
262
  });
294
263
  return { state: built, index: seedIndex };
295
264
  }
296
- // (b)+(c): lower the edit to ops and commit, or `overwrite` a field not yet at
297
- // content rest. Keep the optimistic PM on throw.
298
- // Returns whether anything landed: the caller's change signal, which must not
299
- // be a property of which branch the commit took.
265
+ // (b)+(c): lower the edit to ops and commit, or `overwrite` a field not yet at content rest.
266
+ // Keeps the optimistic PM on throw. Returns whether anything landed, independent of which
267
+ // branch ran.
300
268
  function commitEdit(oldRt, newRt) {
301
269
  if (opts.emptyUnsets && heldAnchors().length === 0 && isEmptyContent(newRt))
302
270
  return commitUnset();
303
271
  if (plaintext)
304
272
  return commitPlain(newRt.text);
305
- // The text splice, computed once per keystroke: the gate below, both overwrite
306
- // fallbacks, and `lower` all read this one `edit`. The projection is the
307
- // caller's, off the same walk that rebuilt the index.
273
+ // The text splice, computed once per keystroke and shared by the gate, both overwrite
274
+ // fallbacks and `lower`.
308
275
  const edit = contentEdit(oldRt, newRt);
309
276
  try {
310
- // A field not yet at content rest is the only edit that overwrites by choice:
311
- // `applyChange` throws on an absent declared field (verified), and neither an
312
- // absent field nor an authored string has anchors to lose (`opsCommittable`).
313
- // Every structural edit lowers, island creation included: the island channel
314
- // places a slot the `delta` may not carry.
277
+ // Only a field not yet at content rest overwrites by choice: `applyChange` throws on an
278
+ // absent declared field, and neither it nor an authored string has anchors to lose
279
+ // (`opsCommittable`). Every structural edit lowers; the island channel places a slot
280
+ // the `delta` may not carry.
315
281
  if (!opsCommittable(doc, addr)) {
316
282
  doc.overwrite(addr, edit.newRt); // brings the field to content rest
317
283
  }
@@ -324,10 +290,8 @@ export function createField(opts) {
324
290
  return true;
325
291
  }
326
292
  catch (e) {
327
- // Bound the damage: an op path the gates missed leaves the store stale
328
- // while PM keeps the edit; and because the reconciler then re-diffs
329
- // from that stale content, every later edit re-throws and the field
330
- // silently stops persisting. Install the full projection instead
293
+ // Bound the damage: after an op path the gates missed, the reconciler re-diffs from
294
+ // stale content and every later edit would re-throw. Install the full projection
331
295
  // (correct store, pays this field's anchors).
332
296
  reportError(opts.onError, {
333
297
  code: 'commit-fallback',
@@ -353,14 +317,10 @@ export function createField(opts) {
353
317
  }
354
318
  }
355
319
  /**
356
- * A `text` leaf's commit: its whole text through the typed writer, which rests the
357
- * field as that literal string (CODEC §Inline mode). A content object is the
358
- * other rest form, and `toMarkdown` emits one as markdown: a break spelled as a
359
- * backslash, a `*` and a trailing space escaped, a blank line doubled, so the value
360
- * reads back changed. The leaf carries no anchors, so the op path has none to keep.
361
- *
362
- * A refusal lands nothing and has no fallback, the other rest form being the
363
- * corruption; the next edit writes the whole text again.
320
+ * A `text` leaf's commit: its whole text through the typed writer, resting the field as that
321
+ * literal string (CODEC §Inline mode). The content-object rest form would emit markdown
322
+ * (escaped `*`, backslash breaks) and read back changed. A refusal lands nothing and has no
323
+ * fallback; the next edit writes the whole text again.
364
324
  */
365
325
  function commitPlain(text) {
366
326
  try {
@@ -409,17 +369,12 @@ export function createField(opts) {
409
369
  el: container,
410
370
  setCaret(pos) {
411
371
  const pm = usvToPM(index, pos);
412
- // `Selection.near`, not `TextSelection.create`: the mapped position can
413
- // be non-inline (before an island/rule block, or doc-level in an empty
414
- // nested textblock), where a raw TextSelection is invalid.
372
+ // `Selection.near`: the mapped position can be non-inline, where a raw TextSelection is
373
+ // invalid.
415
374
  const sel = Selection.near(view.state.doc.resolve(pm));
416
- // Focus first, then the caret: the order a click's own pair arrives in, and the
417
- // one a consumer that ends its caret follow on the arrival needs — the other
418
- // way round, the arrival ends the follow this call just started.
419
- //
420
- // Flagged, because PM focuses with `preventScroll` and a landing is a caret
421
- // move like any other: unflagged, the caret an arrow key would have revealed
422
- // is left off screen by the click that placed it.
375
+ // Focus first, then the caret, the order a click arrives in: the other way round the
376
+ // arrival ends the follow this call just started. Flagged, since PM focuses with
377
+ // `preventScroll` and an unflagged landing leaves the caret off screen.
423
378
  view.focus();
424
379
  view.dispatch(view.state.tr.setSelection(sel).scrollIntoView());
425
380
  },
@@ -463,9 +418,8 @@ export function createField(opts) {
463
418
  },
464
419
  focus() {
465
420
  view.focus();
466
- // The placement rung's reveal: no caret to flag a transaction with, so the
467
- // trip is the element's, `nearest` and no clearance — the one the control
468
- // lane's own focus takes, for the landing that means the same thing there.
421
+ // The placement rung's reveal: no caret to flag a transaction with, so the trip is the
422
+ // element's, `nearest`, no clearance.
469
423
  view.dom.scrollIntoView({ block: 'nearest' });
470
424
  },
471
425
  getContent() {
@@ -477,9 +431,8 @@ export function createField(opts) {
477
431
  return;
478
432
  if (heldAnchors().some((a) => a.id === id))
479
433
  return; // ids are unique + invariant (0.97 policy)
480
- // An anchor commits through `applyChange`, so the field is brought to content
481
- // rest first; else the commit's overwrite branch (value semantics) would drop
482
- // the just-added anchor.
434
+ // An anchor commits through `applyChange`, so the field is brought to content rest
435
+ // first or the overwrite branch would drop it.
483
436
  if (!opsCommittable(doc, addr))
484
437
  doc.overwrite(addr, reconciler.last);
485
438
  const pm = usvToPM(index, pos);
@@ -519,8 +472,7 @@ export function createField(opts) {
519
472
  view.destroy();
520
473
  }
521
474
  };
522
- // The underlying views, exposed as available (undocumented) handles: the
523
- // VisualEditor composes views, and tests drive edits through them.
475
+ // The underlying views, exposed for VisualEditor and tests.
524
476
  const handles = controller;
525
477
  handles.view = view;
526
478
  handles.nestedViews = () => [...nested];
@@ -535,24 +487,17 @@ export function createField(opts) {
535
487
  return controller;
536
488
  }
537
489
  /**
538
- * The prose-leaf plugin stack (VISUAL_EDITOR §Surface): shared by
539
- * {@link createField} and the by-value editor (`ProseValue`), so the two
540
- * never fork the keymap/plugin ordering. History first, with the plugin that starts an
541
- * undo step at each block-moving edit (`undo.ts`), then any leaf-specific
542
- * plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
543
- * a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
544
- * the rest of the stack leaves, then the markdown-shorthand input rules and the
545
- * Backspace that takes one back, the field keymap over the base keymap, the URL paste
546
- * that links a selection (`link-paste.ts`), and last the ones that answer to a block
547
- * leaf's own shapes: the gap cursor, {@link pastAtomPlugin}, the island paste pass
548
- * (`islands.ts`), the table paste (`table-paste.ts`), the markdown paste
549
- * (`markdown-paste.ts`) and the task item's checkbox (`tasks.ts`). A one-line leaf's
550
- * paste joins its lines (`line-paste.ts`).
490
+ * The prose-leaf plugin stack (VISUAL_EDITOR §Surface), shared by {@link createField} and
491
+ * `ProseValue` so the two never fork the plugin ordering. History first with the step-splitting
492
+ * plugin (`undo.ts`), then `afterHistory` plugins (the addressed leaf's anchor plugin), {@link
493
+ * linebreakPlugin}, the markdown-shorthand input rules and their Backspace, the field keymap over
494
+ * the base keymap, the URL paste that links a selection (`link-paste.ts`), and last the block
495
+ * leaf's own shapes: the gap cursor, {@link pastAtomPlugin}, the island paste (`islands.ts`), table
496
+ * paste (`table-paste.ts`), markdown paste (`markdown-paste.ts`) and the task checkbox
497
+ * (`tasks.ts`). A one-line leaf's paste joins its lines (`line-paste.ts`).
551
498
  *
552
- * Every mark-shaped plugin reads the schema rather than a flag: over either mark-free
553
- * schema the shorthand rules build nothing (each is guarded on its mark type), the
554
- * toggles bind no key and a pasted URL stays text, so a `**bold**` keeps the delimiters
555
- * its author typed without a second rule saying so.
499
+ * Mark-shaped plugins read the schema rather than a flag: over a mark-free schema the shorthand
500
+ * rules build nothing, the toggles bind no key and a pasted URL stays text.
556
501
  */
557
502
  export function proseLeafPlugins(schema, opts) {
558
503
  const list = [
@@ -570,8 +515,7 @@ export function proseLeafPlugins(schema, opts) {
570
515
  list.push(keymap(baseKeymap));
571
516
  if (schema.marks.link)
572
517
  list.push(linkPastePlugin(schema.marks.link));
573
- // Each answers to something only the block schema holds: any other leaf is
574
- // textblocks alone, with no island and no gap to put a cursor in.
518
+ // Each answers to something only the block schema holds.
575
519
  if (isBlockSchema(schema))
576
520
  list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin(), tablePastePlugin(), tableCopyPlugin(), markdownPastePlugin(), wordPastePlugin(), listPastePlugin(), taskPlugin(), nestingPlugin());
577
521
  if (schema === plainSchema)
@@ -583,10 +527,8 @@ export function proseLeafPlugins(schema, opts) {
583
527
  return list;
584
528
  }
585
529
  /**
586
- * A plain leaf's clipboard text is literal, one line per `\n` both ways. Upstream's
587
- * defaults read pasted text as blocks split on any run of newlines, which drops a
588
- * blank line the value holds as content, and write a copy's blocks joined by a blank
589
- * line and its breaks as nothing.
530
+ * A plain leaf's clipboard text is literal, one line per `\n` both ways; upstream's defaults split
531
+ * pasted text on any run of newlines and write a copy's blocks joined by a blank line.
590
532
  */
591
533
  function plainClipboardPlugin() {
592
534
  const { paragraph } = plainSchema.nodes;
@@ -600,18 +542,11 @@ function plainClipboardPlugin() {
600
542
  });
601
543
  }
602
544
  /**
603
- * A printable key over a selected atom writes past it. A selection is the subject
604
- * of the next command, never a thing armed for
605
- * replacement: Backspace deletes it, Mod-C copies it, and a character lands beside
606
- * it. A block island takes the empty paragraph after it, or a new one where there is
607
- * none; an inline one the caret after the image in the same line: one rule, both node
608
- * types. Only a paragraph is reused, since an empty heading or fence would set the
609
- * text in a shape the key never asked for.
610
- *
611
- * PM routes text input over a non-`TextSelection` through `handleTextInput` before
612
- * falling back to replacing the selection, so this one prop is the whole of it.
613
- * Paste keeps replacing: a paste is deliberate, and it is the gesture that swaps one
614
- * island for another.
545
+ * A printable key over a selected atom writes past it: a selection is the subject of the next
546
+ * command, not armed for replacement. A block island takes the empty paragraph after it, or a new
547
+ * one; an inline one the caret after the image. Only a paragraph is reused. PM routes text input
548
+ * over a non-`TextSelection` through `handleTextInput`, so this one prop is the whole of it. Paste
549
+ * keeps replacing, as the gesture that swaps one island for another.
615
550
  */
616
551
  function pastAtomPlugin() {
617
552
  return new Plugin({
@@ -641,17 +576,11 @@ function pastAtomPlugin() {
641
576
  });
642
577
  }
643
578
  /**
644
- * The empty leaf's ghost. A node decoration stamps the sole empty textblock with a
645
- * class and the ghost text, which `prose.css` draws from the attribute: so the text
646
- * never enters the document, the caret path, or a `pmToContent` export, and it
647
- * vanishes the instant the leaf holds any content (the emptiness test fails).
648
- *
649
- * Under `placeholderUntilEdit` it is stamped only until the state's first edit, after
650
- * which the leaf has been answered. An inline leaf's ghost keeps to the one line the
651
- * leaf is (`qm-prose-placeholder-line`).
652
- *
653
- * The text is read per decoration pass rather than closed over, so moving the ghost is
654
- * a re-render and never a document edit.
579
+ * The empty leaf's ghost: a node decoration stamps the sole empty textblock with a class and the
580
+ * ghost text, which `prose.css` draws from the attribute, so the text never enters the document or
581
+ * a `pmToContent` export. Under `placeholderUntilEdit` it is stamped only until the state's first
582
+ * edit. An inline leaf's ghost keeps to one line (`qm-prose-placeholder-line`). The text is read
583
+ * per decoration pass, so moving the ghost is a re-render.
655
584
  */
656
585
  function ghostPlugin(opts) {
657
586
  const key = new PluginKey('qm-ghost');
@@ -684,15 +613,12 @@ function ghostPlugin(opts) {
684
613
  });
685
614
  }
686
615
  /**
687
- * The field's keymap: history, mark toggles, and the body's structural keys; Enter
688
- * suppressed inline. The toggles are bound per mark type the schema declares, so a
689
- * `text` leaf leaves Mod-b/i/u to the browser.
616
+ * The field's keymap: history, mark toggles (bound per mark type the schema declares), and the
617
+ * body's structural keys; Enter suppressed inline.
690
618
  *
691
- * Tab forks on the leaf's role, not on the caret's position. A one-line or `text`
692
- * leaf is a form field: Tab stays unbound, so field navigation is open for a shell
693
- * keymap (VISUAL_EDITOR §Settled and open). A block-schema body is a document: Tab
694
- * is structural, a chain each nested surface prepends to (`keymap.ts`). One key
695
- * never means two things within one surface.
619
+ * Tab forks on the leaf's role: a one-line or `text` leaf is a form field, Tab unbound so field
620
+ * navigation is open for a shell keymap (VISUAL_EDITOR §Settled and open); a block-schema body
621
+ * takes Tab structurally, a chain each nested surface prepends to (`keymap.ts`).
696
622
  */
697
623
  function editorKeymap(schema, inline, slash) {
698
624
  const map = {
@@ -706,11 +632,13 @@ function editorKeymap(schema, inline, slash) {
706
632
  map['Mod-i'] = toggleMark(schema.marks.em);
707
633
  if (schema.marks.underline)
708
634
  map['Mod-u'] = toggleMark(schema.marks.underline);
635
+ if (schema.marks.strike)
636
+ map['Mod-Shift-x'] = toggleMark(schema.marks.strike);
637
+ if (schema.marks.code)
638
+ map['Mod-e'] = toggleMark(schema.marks.code);
709
639
  if (inline) {
710
- // One textblock only: swallow Enter so no second block is attempted. One *line*
711
- // only too (`Content::is_inline`), so Shift-Enter goes with it: the schema
712
- // declares no break node, and a declined key is the browser's, whose native
713
- // `<br>` is the one thing that could put a second line in a field that holds one.
640
+ // One textblock only: swallow Enter, and Shift-Enter with it (the schema declares no break
641
+ // node; a declined key is the browser's native `<br>`).
714
642
  map['Enter'] = () => true;
715
643
  map['Shift-Enter'] = () => true;
716
644
  }
@@ -7,64 +7,56 @@ export declare const ISLAND_SLOT = "\uFFFC";
7
7
  export declare function isBlockIsland(type: ContentIsland['type']): boolean;
8
8
  /** Whether a leaf edits an island of `type`; one holding any other is held (`fitsLeaf`). */
9
9
  export declare function isEditedIsland(type: ContentIsland['type']): boolean;
10
- /** Block island node (a table): a `U+FFFC` slot alone on its `para` line. Atom, unselectable content. */
10
+ /** Block island node (a table): a `U+FFFC` slot alone on its `para` line. Atom. */
11
11
  export declare const islandBlockSpec: NodeSpec;
12
- /** Inline island node (an image, a footnote, an equation): a `U+FFFC` slot inside a
13
- * `para` line. */
12
+ /** Inline island node (an image, a footnote, an equation): a `U+FFFC` slot inside a `para` line. */
14
13
  export declare const islandInlineSpec: NodeSpec;
15
- /** A PM island node's attributes: the content entry, spelled the way a node spec
16
- * spells it (`islandType`, because `type` on a PM node is the node type). `islandType`
17
- * reads its set off the entry rather than restating it (DOCUMENT_MODEL §Stability
18
- * seams). */
14
+ /**
15
+ * A PM island node's attributes: the content entry, with `islandType` because `type` on a PM node
16
+ * is the node type; its set comes off the entry (DOCUMENT_MODEL §Stability seams).
17
+ */
19
18
  export interface IslandNodeAttrs {
20
19
  id: string;
21
20
  islandType: ContentIsland['type'];
22
21
  props: unknown;
23
22
  }
24
- /** Build a content island entry from a PM island node's attrs (block or inline). The
25
- * cast pairs a closed `type` with a `props` that crossed the DOM as opaque JSON. The
26
- * store is lenient about a table's shape — it fills what it can and keeps the rest —
27
- * so a payload the arm does not hold is stored rather than refused, and `undefined` at
28
- * its reader is what the surface draws the placeholder for (§`tablePropsOfNode`). */
23
+ /**
24
+ * Build a content island entry from a PM island node's attrs. The cast pairs a closed `type` with a
25
+ * `props` that crossed the DOM as opaque JSON. The store is lenient about a table's shape, so a
26
+ * payload the arm does not hold is stored, and `undefined` at its reader draws the placeholder
27
+ * (§`tablePropsOfNode`).
28
+ */
29
29
  export declare function islandEntryFromNode(attrs: IslandNodeAttrs): ContentIsland;
30
- /** A node's `TableProps`, or `undefined` for any other island and for a payload of the
31
- * wrong shape: the boundary's own guard over the entry the node carries, so a `props`
32
- * that crossed the DOM is read once here rather than at each reader. The type says
33
- * which reader, the shape says whether it can read; every caller draws the placeholder
34
- * for `undefined` (`table-view.ts` §`render`). */
30
+ /**
31
+ * A node's `TableProps`, or `undefined` for any other island and for a payload of the wrong shape:
32
+ * the boundary's guard, so a `props` that crossed the DOM is read once here. Every caller draws the
33
+ * placeholder for `undefined` (`table-view.ts` §`render`).
34
+ */
35
35
  export declare function tablePropsOfNode(node: PMNode): TableProps | undefined;
36
36
  /**
37
- * A minter over one document: the positional `isl-{n}` sequence continued past the
38
- * highest id the field already holds, handing out consecutive ids, so a caller placing
39
- * several islands at once keeps one minter for the lot. A minted id is this tier's to
40
- * produce and it is part of the document's canonical bytes (CODEC §Islands), so it is a
41
- * counter over the projection in hand — never a UUID, never a clock reading, and never a
42
- * re-numbering of the ids already there, which are identities.
37
+ * A minter over one document: the positional `isl-{n}` sequence continued past the highest id the
38
+ * field holds, so a caller placing several islands keeps one minter. A minted id is part of the
39
+ * document's canonical bytes (CODEC §Islands): a counter over the projection in hand, never a UUID
40
+ * or clock reading, and never a re-numbering of existing ids.
43
41
  *
44
- * Reads the PM doc rather than the stored content because that is what the caller (an
45
- * insert command, a paste) holds and what the commit will project: an id minted against
46
- * a stale content could collide with one the same transaction places.
42
+ * Reads the PM doc rather than stored content because that is what the caller holds and the commit
43
+ * will project; a stale content could collide with an id the same transaction places.
47
44
  */
48
45
  export declare function islandMinter(doc: PMNode): () => string;
49
- /** The next island id for a field: one draw from {@link islandMinter}, which is what a
50
- * command placing exactly one island wants. */
46
+ /** The next island id for a field: one draw from {@link islandMinter}. */
51
47
  export declare function mintIslandId(doc: PMNode): string;
52
48
  /**
53
- * The paste's island pass: an id the field already holds is re-minted on the way in, and
54
- * an island arriving with none, a table read off foreign markup (`table-paste.ts`), takes
55
- * its first.
49
+ * The paste's island pass: an id the field already holds is re-minted on the way in, and an island
50
+ * arriving with none (a table read off foreign markup, `table-paste.ts`) takes its first.
56
51
  *
57
- * An id is an identity in the content and unique across the field, so two islands
58
- * wearing one is a projection the store has no shape for. A copy rather than a cut is
59
- * where that arrives, and a paste out of a second field is the other; a cut pasted back
60
- * collides with nothing and keeps the identity it had.
52
+ * Ids are unique across the field, so two islands wearing one is a projection the store has no
53
+ * shape for. That arrives by a copy or a paste from a second field; a cut pasted back collides with
54
+ * nothing and keeps its identity.
61
55
  *
62
- * The test is the document as it stands, which a drag moving a selection over an island
63
- * reads one too many: the original is still there when the slice is transformed and gone
64
- * when it lands, so that island arrives under a fresh id. A fresh id costs it nothing a
65
- * duplicate would cost the whole field.
56
+ * The test is the document as it stands, so a drag moving a selection over an island sees the
57
+ * original still there and mints a fresh id for the arrival, which costs nothing next to a
58
+ * duplicate.
66
59
  *
67
- * A plugin prop rather than a view option: the leaf mounts the codec's plugin stack, and
68
- * which nodes carry an identity is the codec's to know.
60
+ * A plugin prop rather than a view option: which nodes carry an identity is the codec's to know.
69
61
  */
70
62
  export declare function islandPastePlugin(): Plugin;