@quillmark/svelte 0.21.0 → 0.23.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 (69) hide show
  1. package/NOTICE +6 -7
  2. package/README.md +1 -1
  3. package/dist/core/address.d.ts +6 -1
  4. package/dist/core/address.js +12 -3
  5. package/dist/core/bloom.d.ts +9 -4
  6. package/dist/core/bloom.js +18 -49
  7. package/dist/core/codec/atoms.d.ts +1 -1
  8. package/dist/core/codec/atoms.js +1 -1
  9. package/dist/core/codec/blocks.js +4 -2
  10. package/dist/core/codec/cap.d.ts +21 -0
  11. package/dist/core/codec/cap.js +63 -0
  12. package/dist/core/codec/code.d.ts +1 -1
  13. package/dist/core/codec/code.js +1 -1
  14. package/dist/core/codec/decode.d.ts +5 -5
  15. package/dist/core/codec/decode.js +18 -12
  16. package/dist/core/codec/encode.js +6 -1
  17. package/dist/core/codec/field.d.ts +22 -10
  18. package/dist/core/codec/field.js +54 -18
  19. package/dist/core/codec/index.d.ts +3 -1
  20. package/dist/core/codec/index.js +3 -2
  21. package/dist/core/codec/inputrules.js +1 -1
  22. package/dist/core/codec/lists.d.ts +3 -2
  23. package/dist/core/codec/lists.js +21 -5
  24. package/dist/core/codec/nesting.d.ts +20 -0
  25. package/dist/core/codec/nesting.js +62 -0
  26. package/dist/core/codec/prose.css +74 -20
  27. package/dist/core/codec/schema.d.ts +2 -2
  28. package/dist/core/codec/schema.js +16 -7
  29. package/dist/core/codec/table-copy.d.ts +17 -0
  30. package/dist/core/codec/table-copy.js +84 -0
  31. package/dist/core/codec/table-view.d.ts +1 -7
  32. package/dist/core/codec/table-view.js +70 -119
  33. package/dist/core/codec/table.d.ts +2 -9
  34. package/dist/core/codec/table.js +10 -19
  35. package/dist/core/codec/tasks.d.ts +19 -0
  36. package/dist/core/codec/tasks.js +140 -0
  37. package/dist/core/errors.d.ts +2 -2
  38. package/dist/visual/ArrayField.svelte +103 -63
  39. package/dist/visual/ArrayField.svelte.d.ts +0 -2
  40. package/dist/visual/Card.svelte +9 -3
  41. package/dist/visual/Card.svelte.d.ts +2 -2
  42. package/dist/visual/Field.svelte +33 -17
  43. package/dist/visual/Field.svelte.d.ts +4 -2
  44. package/dist/visual/FieldLabel.svelte +19 -33
  45. package/dist/visual/FieldLabel.svelte.d.ts +4 -2
  46. package/dist/visual/FormatPopover.svelte +54 -73
  47. package/dist/visual/MatrixField.svelte +18 -12
  48. package/dist/visual/NumberField.svelte +32 -3
  49. package/dist/visual/ObjectField.svelte +50 -22
  50. package/dist/visual/ObjectField.svelte.d.ts +6 -0
  51. package/dist/visual/ProseField.svelte +20 -14
  52. package/dist/visual/ProseField.svelte.d.ts +8 -2
  53. package/dist/visual/ProseValue.svelte +30 -19
  54. package/dist/visual/ProseValue.svelte.d.ts +7 -4
  55. package/dist/visual/TextField.svelte +3 -2
  56. package/dist/visual/TipsCard.svelte +5 -2
  57. package/dist/visual/VariantField.svelte +1 -1
  58. package/dist/visual/VariantField.svelte.d.ts +1 -1
  59. package/dist/visual/VisualEditorInner.svelte +13 -13
  60. package/dist/visual/controls.css +19 -1
  61. package/dist/visual/icons/nodes.d.ts +0 -21
  62. package/dist/visual/icons/nodes.js +1 -7
  63. package/dist/visual/leaves.d.ts +12 -9
  64. package/dist/visual/leaves.js +19 -0
  65. package/dist/visual/strings.d.ts +16 -5
  66. package/dist/visual/strings.js +17 -9
  67. package/dist/visual/structure.d.ts +26 -32
  68. package/dist/visual/structure.js +71 -53
  69. package/package.json +3 -3
package/NOTICE CHANGED
@@ -6,14 +6,13 @@ Copyright 2026 Nibs
6
6
  This package contains SVG glyph geometry derived from Lucide, carried in source
7
7
  rather than installed.
8
8
 
9
- `visual/icons/nodes.ts` holds nineteen verbatim from @lucide/svelte 1.28.0:
9
+ `visual/icons/nodes.ts` holds eighteen verbatim from @lucide/svelte 1.28.0:
10
10
  chevron-right, arrow-right, chevron-down, chevron-up, bold, italic, underline,
11
- strikethrough, code, link, hash, info, plus, x, trash-2, minus, check, calendar
12
- and calendar-check.
11
+ strikethrough, code, link, info, plus, x, trash-2, minus, check, calendar and
12
+ calendar-check.
13
13
 
14
14
  `core/codec/table-view.ts` holds grip-horizontal, grip-vertical, align-left,
15
- align-center, align-right, align-start-horizontal, align-center-horizontal,
16
- align-end-horizontal and panel-top, derived from an earlier release of the same
15
+ align-center, align-right and panel-top, derived from an earlier release of the same
17
16
  project.
18
17
 
19
18
  ISC License
@@ -34,8 +33,8 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
34
33
 
35
34
  --------------------------------------------------------------------------------
36
35
 
37
- Thirteen of those glyphs — arrow-right, chevron-down, chevron-right, chevron-up,
38
- code, hash, info, italic, link, minus, plus, trash-2 and x — are drawn exactly as
36
+ Twelve of those glyphs — arrow-right, chevron-down, chevron-right, chevron-up,
37
+ code, info, italic, link, minus, plus, trash-2 and x — are drawn exactly as
39
38
  the Feather project draws them and carry its license as well.
40
39
 
41
40
  The MIT License (MIT)
package/README.md CHANGED
@@ -149,7 +149,7 @@ Hand it a different `doc` and the editor **re-keys itself**: every leaf remounts
149
149
 
150
150
  ### Wording
151
151
 
152
- The package ships English and a seam to replace it. `strings` is keyed and **partial**: set what you have translations for, and the rest stay the package's. Several keys are **accessible names** rather than decoration (the card controls, the add trigger, the required marker), so an untranslated surface reads the wrong language to a screen reader. `DEFAULT_VISUAL_STRINGS` is the English, exported so you can compose against it. `<Preview>` takes its own three-key `strings` for the states it shows when there is nothing to paint.
152
+ The package ships English and a seam to replace it. `strings` is keyed and **partial**: set what you have translations for, and the rest stay the package's. Several keys are **accessible names** rather than decoration (the card controls, the add trigger), so an untranslated surface reads the wrong language to a screen reader. `DEFAULT_VISUAL_STRINGS` is the English, exported so you can compose against it. `<Preview>` takes its own three-key `strings` for the states it shows when there is nothing to paint.
153
153
 
154
154
  ```svelte
155
155
  <VisualEditor
@@ -50,10 +50,15 @@ export interface Landing {
50
50
  * - `{card: i}` → `"cards.<kind>[i].body"`
51
51
  * - `{card: i, field}` → `"cards.<kind>[i].<field>"`
52
52
  *
53
+ * `steps` descend from the field to a leaf inside it, the inverse of
54
+ * {@link nestedAddrForFieldPath}: `{field: 'contact'}` with `['email']` is
55
+ * `"main.contact.email"`, `{field: 'keywords'}` with `[0]` is `"main.keywords[0]"`. A body
56
+ * holds no cells, so a field-less address with steps is `undefined`.
57
+ *
53
58
  * `cards[i]` stands in for an unknown or blank kind. The index is the addr's own: no
54
59
  * per-kind counting, since `DocPath` addresses cards by document-array index.
55
60
  */
56
- export declare function fieldPathForAddr(addr: Addr, kinds: readonly string[]): DocPath | undefined;
61
+ export declare function fieldPathForAddr(addr: Addr, kinds: readonly string[], steps?: readonly PathStep[]): DocPath | undefined;
57
62
  /**
58
63
  * A card's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
59
64
  * where the change is the card rather than anything inside it. `undefined` for an
@@ -15,15 +15,24 @@ import { core } from './lifecycle.js';
15
15
  * - `{card: i}` → `"cards.<kind>[i].body"`
16
16
  * - `{card: i, field}` → `"cards.<kind>[i].<field>"`
17
17
  *
18
+ * `steps` descend from the field to a leaf inside it, the inverse of
19
+ * {@link nestedAddrForFieldPath}: `{field: 'contact'}` with `['email']` is
20
+ * `"main.contact.email"`, `{field: 'keywords'}` with `[0]` is `"main.keywords[0]"`. A body
21
+ * holds no cells, so a field-less address with steps is `undefined`.
22
+ *
18
23
  * `cards[i]` stands in for an unknown or blank kind. The index is the addr's own: no
19
24
  * per-kind counting, since `DocPath` addresses cards by document-array index.
20
25
  */
21
- export function fieldPathForAddr(addr, kinds) {
26
+ export function fieldPathForAddr(addr, kinds, steps = []) {
22
27
  const head = cardHead(addr.card, kinds);
23
28
  if (!head)
24
29
  return undefined;
25
- const tail = addr.field != null ? { seg: 'field', name: addr.field } : { seg: 'body' };
26
- return core().formatDocPath([head, tail]);
30
+ if (addr.field == null)
31
+ return steps.length ? undefined : core().formatDocPath([head, { seg: 'body' }]);
32
+ const segs = [head, { seg: 'field', name: addr.field }];
33
+ for (const step of steps)
34
+ segs.push(typeof step === 'number' ? { seg: 'index', index: step } : { seg: 'field', name: step });
35
+ return core().formatDocPath(segs);
27
36
  }
28
37
  /**
29
38
  * A card's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
@@ -1,8 +1,13 @@
1
1
  /**
2
- * Bloom over `host`'s content: an inset child, so the wash neither tints the host's
3
- * own background nor fades the text under it. `host` must be positioned.
2
+ * Run one wash over `host`, a `.qm-wash` box, resolving the dwell off its root so the
3
+ * number is single-sourced in CSS (`core/rungs.ts`).
4
4
  *
5
- * One wash node per host, reused while it lives: two quick landings on the same leaf
5
+ * One property, both motion preferences: `--_qm-bloom-dwell` already shortens under
6
+ * `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
7
+ * the shape: a hold and a cut carry no ramps, which is a frame list rather than a
8
+ * number and the one half CSS cannot hand over.
9
+ *
10
+ * A run in flight on the same host is cancelled first: two quick landings on one leaf
6
11
  * must not stack two alphas over it.
7
12
  */
8
- export declare function bloomInside(host: HTMLElement): void;
13
+ export declare function bloom(host: HTMLElement): void;
@@ -6,9 +6,11 @@
6
6
  // fixed by `--_qm-accent-wash`, so interpolating 0→1→0 keeps the scale in CSS where
7
7
  // `color-mix` resolves, instead of in keyframe values a script would have to
8
8
  // pre-resolve and re-resolve on a theme change.
9
+ //
10
+ // The wash is the host's `::after` (`.qm-wash`, controls.css), drawn from mount: this
11
+ // module plays it and adds nothing to the tree.
9
12
  import { rungStyle } from './rungs.js';
10
- const BLOOM_CLASS = 'qm-bloom';
11
- const WASH = 'var(--_qm-accent-wash)';
13
+ const WASH = '::after';
12
14
  /** The dwell used when the derivation is out of reach (an unstyled root, or jsdom,
13
15
  * where `getComputedStyle` reports custom properties as empty). A fallback is the
14
16
  * absence of the scale, not a copy of it. */
@@ -25,61 +27,28 @@ const FRAMES = [
25
27
  ];
26
28
  const FRAMES_REDUCED = [{ opacity: 1 }, { opacity: 1 }];
27
29
  /**
28
- * Run one wash on `el`, resolving the dwell off its root so the number is
29
- * single-sourced in CSS (`core/rungs.ts`).
30
+ * Run one wash over `host`, a `.qm-wash` box, resolving the dwell off its root so the
31
+ * number is single-sourced in CSS (`core/rungs.ts`).
30
32
  *
31
33
  * One property, both motion preferences: `--_qm-bloom-dwell` already shortens under
32
34
  * `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
33
35
  * the shape: a hold and a cut carry no ramps, which is a frame list rather than a
34
36
  * number and the one half CSS cannot hand over.
35
- */
36
- function bloom(el) {
37
- if (typeof el.animate !== 'function')
38
- return undefined;
39
- const reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
40
- const raw = rungStyle(el).getPropertyValue('--_qm-bloom-dwell').trim();
41
- const n = Number.parseFloat(raw);
42
- const duration = !Number.isFinite(n) ? FALLBACK_MS : raw.endsWith('ms') ? n : n * 1000;
43
- return el.animate(reduced ? FRAMES_REDUCED : FRAMES, { duration });
44
- }
45
- /**
46
- * Bloom over `host`'s content: an inset child, so the wash neither tints the host's
47
- * own background nor fades the text under it. `host` must be positioned.
48
37
  *
49
- * One wash node per host, reused while it lives: two quick landings on the same leaf
38
+ * A run in flight on the same host is cancelled first: two quick landings on one leaf
50
39
  * must not stack two alphas over it.
51
40
  */
52
- export function bloomInside(host) {
53
- let el = host.querySelector(`:scope > .${BLOOM_CLASS}`);
54
- if (el) {
55
- // Drop the in-flight run's handlers before cancelling: its `oncancel` would
56
- // otherwise remove the node the new run is about to animate.
57
- for (const a of el.getAnimations()) {
58
- a.onfinish = null;
59
- a.oncancel = null;
60
- a.cancel();
61
- }
62
- }
63
- else {
64
- el = document.createElement('div');
65
- el.className = BLOOM_CLASS;
66
- Object.assign(el.style, {
67
- position: 'absolute',
68
- inset: '0',
69
- pointerEvents: 'none',
70
- borderRadius: 'inherit',
71
- background: WASH,
72
- opacity: '0'
73
- });
74
- host.appendChild(el);
75
- }
76
- const node = el;
77
- const anim = bloom(node);
78
- if (!anim) {
79
- node.remove();
41
+ export function bloom(host) {
42
+ if (typeof host.animate !== 'function')
80
43
  return;
44
+ for (const a of host.getAnimations({ subtree: true })) {
45
+ const effect = a.effect;
46
+ if (effect?.target === host && effect.pseudoElement === WASH)
47
+ a.cancel();
81
48
  }
82
- const drop = () => node.remove();
83
- anim.onfinish = drop;
84
- anim.oncancel = drop;
49
+ const reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
50
+ const raw = rungStyle(host).getPropertyValue('--_qm-bloom-dwell').trim();
51
+ const n = Number.parseFloat(raw);
52
+ const duration = !Number.isFinite(n) ? FALLBACK_MS : raw.endsWith('ms') ? n : n * 1000;
53
+ host.animate(reduced ? FRAMES_REDUCED : FRAMES, { duration, pseudoElement: WASH });
85
54
  }
@@ -1,7 +1,7 @@
1
1
  import { type Command } from 'prosemirror-state';
2
2
  import type { Schema } from 'prosemirror-model';
3
3
  /**
4
- * The block-atom bindings for a block-schema leaf: `{}` for the inline/plaintext
4
+ * The block-atom bindings for a block-schema leaf: `{}` for the one-line and plain
5
5
  * schemas, which hold no block to be against.
6
6
  *
7
7
  * Both keys are the same command on opposite sides, because what a delete against an
@@ -77,7 +77,7 @@ function againstAtom(side) {
77
77
  };
78
78
  }
79
79
  /**
80
- * The block-atom bindings for a block-schema leaf: `{}` for the inline/plaintext
80
+ * The block-atom bindings for a block-schema leaf: `{}` for the one-line and plain
81
81
  * schemas, which hold no block to be against.
82
82
  *
83
83
  * Both keys are the same command on opposite sides, because what a delete against an
@@ -1,6 +1,7 @@
1
1
  import { Selection } from 'prosemirror-state';
2
2
  import { chainCommands, splitBlock } from 'prosemirror-commands';
3
3
  import { canJoin, findWrapping } from 'prosemirror-transform';
4
+ import { withinNesting } from './nesting.js';
4
5
  import { takesLineBreak } from './schema.js';
5
6
  /** The head of a textblock: where a shorthand fires, and where a pick lands once its
6
7
  * run is consumed. */
@@ -59,7 +60,8 @@ function spaceBreaks(tr, $from, type) {
59
60
  }
60
61
  /** `wrappingInputRule`'s body: wrap the block range at `pos`, then join a preceding
61
62
  * sibling of the same type — the one boundary the wrap itself opens (`lists.ts`
62
- * §cleanup: command-local, never a global pass). */
63
+ * §cleanup: command-local, never a global pass). False where no wrap fits, or where
64
+ * the one that does nests past `MAX_NESTING_DEPTH` (`nesting.ts`). */
63
65
  function wrapAt(tr, pos, type, attrs, joinBefore) {
64
66
  const range = tr.doc.resolve(pos).blockRange();
65
67
  const wrapping = range && findWrapping(range, type, attrs);
@@ -73,7 +75,7 @@ function wrapAt(tr, pos, type, attrs, joinBefore) {
73
75
  (!joinBefore || joinBefore(before))) {
74
76
  tr.join(pos - 1);
75
77
  }
76
- return true;
78
+ return withinNesting(tr.doc);
77
79
  }
78
80
  /** `# `: the heading `level` deep. `list_item` is `block+`, so one inside an item is a
79
81
  * shape the content holds and `importMarkdown` produces from `- # title`. */
@@ -0,0 +1,21 @@
1
+ import type { Node as PMNode } from 'prosemirror-model';
2
+ import { Plugin } from 'prosemirror-state';
3
+ /** A leaf's character cap: the schema's `max:`, and whether the count drops trailing
4
+ * whitespace, which a one-line `text` does and nothing else. */
5
+ export interface TextCap {
6
+ max: number;
7
+ trimEnd?: boolean;
8
+ /** The count, at mount and after every change to the document. */
9
+ onCount?: (count: number) => void;
10
+ }
11
+ /** The characters `doc` holds as `FieldSchema::char_count` counts them: Unicode scalar
12
+ * values of the plain projection, islands counting nothing and trailing line breaks
13
+ * none. */
14
+ export declare function capCount(doc: PMNode, trimEnd?: boolean): number;
15
+ /**
16
+ * Refuses a transaction leaving the leaf past `max`, so a keystroke at the cap lands
17
+ * nothing, and cuts a paste to what fits. Past the cap, a transaction passes where it
18
+ * leaves the count no higher, so a value stored over the cap still edits down; undo and
19
+ * redo pass, each restoring a state the leaf already held.
20
+ */
21
+ export declare function capPlugin(cap: TextCap): Plugin;
@@ -0,0 +1,63 @@
1
+ import { isHistoryTransaction } from 'prosemirror-history';
2
+ import { Plugin } from 'prosemirror-state';
3
+ import { usvLength } from './decode.js';
4
+ import { scanDoc } from './encode.js';
5
+ import { ISLAND_SLOT } from './islands.js';
6
+ /** The characters `doc` holds as `FieldSchema::char_count` counts them: Unicode scalar
7
+ * values of the plain projection, islands counting nothing and trailing line breaks
8
+ * none. */
9
+ export function capCount(doc, trimEnd = false) {
10
+ const text = scanDoc(doc).text.replaceAll(ISLAND_SLOT, '');
11
+ return usvLength(trimEnd ? text.trimEnd() : text.replace(/[\n\r]+$/, ''));
12
+ }
13
+ /**
14
+ * Refuses a transaction leaving the leaf past `max`, so a keystroke at the cap lands
15
+ * nothing, and cuts a paste to what fits. Past the cap, a transaction passes where it
16
+ * leaves the count no higher, so a value stored over the cap still edits down; undo and
17
+ * redo pass, each restoring a state the leaf already held.
18
+ */
19
+ export function capPlugin(cap) {
20
+ const count = (doc) => capCount(doc, cap.trimEnd);
21
+ return new Plugin({
22
+ filterTransaction: (tr) => {
23
+ if (!tr.docChanged || isHistoryTransaction(tr))
24
+ return true;
25
+ const after = count(tr.doc);
26
+ return after <= cap.max || after <= count(tr.before);
27
+ },
28
+ props: {
29
+ handlePaste: (view, _event, slice) => {
30
+ const { state } = view;
31
+ const pasted = () => state.tr.replaceSelection(slice);
32
+ if (count(pasted().doc) <= cap.max)
33
+ return false;
34
+ const tr = pasted();
35
+ const start = tr.mapping.map(state.selection.from, -1);
36
+ const end = tr.mapping.map(state.selection.to, 1);
37
+ const fits = (cut) => count(pasted().delete(cut, end).doc) <= cap.max;
38
+ if (!fits(start))
39
+ return true;
40
+ let lo = start;
41
+ let hi = end;
42
+ while (hi - lo > 1) {
43
+ const mid = (lo + hi) >> 1;
44
+ if (fits(mid))
45
+ lo = mid;
46
+ else
47
+ hi = mid;
48
+ }
49
+ view.dispatch(tr.delete(lo, end).scrollIntoView().setMeta('uiEvent', 'paste'));
50
+ return true;
51
+ }
52
+ },
53
+ view: (v) => {
54
+ cap.onCount?.(count(v.state.doc));
55
+ return {
56
+ update: (next, prev) => {
57
+ if (next.state.doc !== prev.doc)
58
+ cap.onCount?.(count(next.state.doc));
59
+ }
60
+ };
61
+ }
62
+ });
63
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Schema } from 'prosemirror-model';
2
2
  import { type Command } from 'prosemirror-state';
3
3
  /**
4
- * The `code_block` link of the body's key chains: `{}` for the inline/plaintext
4
+ * The `code_block` link of the body's key chains: `{}` for the one-line and plain
5
5
  * schemas, which declare no code node.
6
6
  *
7
7
  * `Enter` is upstream's `newlineInCode`, and it is load-bearing rather than
@@ -182,7 +182,7 @@ const belowFinalFence = (state, dispatch) => {
182
182
  return true;
183
183
  };
184
184
  /**
185
- * The `code_block` link of the body's key chains: `{}` for the inline/plaintext
185
+ * The `code_block` link of the body's key chains: `{}` for the one-line and plain
186
186
  * schemas, which declare no code node.
187
187
  *
188
188
  * `Enter` is upstream's `newlineInCode`, and it is load-bearing rather than
@@ -18,10 +18,10 @@ export declare function renderContent(rt: Content, schema: Schema): Node;
18
18
  /** Decode a `Content` to a PM document under `schema`. */
19
19
  export declare function decode(rt: Content, schema: Schema): PMNode;
20
20
  /** Whether `rt` decodes under an inline schema with nothing but whitespace dropped:
21
- * upstream's `inline` rule, or one plain line and the empty one a trailing `\n`
22
- * opens. That newline is the one a YAML `|` scalar keeps in a `plaintext` value,
23
- * which upstream refuses (`trailingNewline`) and the inline decode drops. Marks are
24
- * the schema's own business (`plaintextSchema` declares none). */
21
+ * upstream's one-line rule, or one plain line and the empty one a trailing `\n`
22
+ * opens. That newline is the one a YAML `|` scalar keeps in a `text` value, which
23
+ * upstream ignores and the inline decode drops. Marks are the schema's own business
24
+ * (`plaintextSchema` declares none). */
25
25
  export declare function fitsInline(rt: Content): boolean;
26
26
  /** Whether `rt` decodes under `plainSchema` losing nothing but its marks, which the
27
27
  * next commit heals (CODEC §Inline mode): upstream's `isPlain`, asked of the content
@@ -29,7 +29,7 @@ export declare function fitsInline(rt: Content): boolean;
29
29
  export declare function fitsPlain(rt: Content): boolean;
30
30
  /** Whether a leaf so declared edits `rt` losing nothing: {@link fitsInline} narrowed
31
31
  * and {@link fitsPlain} plain, past which the decode drops what the first commit would
32
- * store; on the block schema anything but an element or a task item ({@link editable}),
32
+ * store; on the block schema anything but an element ({@link editable}),
33
33
  * and an island where the leaf mounts no island view (`islands`), which draws as its
34
34
  * placeholder tag, an atom a keystroke deletes unseen. What a leaf held over the rest
35
35
  * reads. */
@@ -47,9 +47,9 @@ export function decode(rt, schema) {
47
47
  acc += usvLength(lineTexts[i]) + 1; // +1 for the `\n` boundary
48
48
  }
49
49
  // A mark-free schema (`plaintextSchema`) has nothing to apply them with, and a
50
- // plaintext field that acquired one anyway — an older build's popover, a hand-
51
- // written document — is exactly the content that must open and then heal on the
52
- // next commit, not throw here.
50
+ // `text` field that acquired one anyway — an older build's popover, a hand-written
51
+ // document — is exactly the content that must open and then heal on the next
52
+ // commit, not throw here.
53
53
  const marks = hasMarks(schema) ? rt.marks.filter((m) => m.type !== 'anchor') : [];
54
54
  const cursor = { i: 0, rt };
55
55
  if (isInlineSchema(schema)) {
@@ -74,10 +74,10 @@ export function decode(rt, schema) {
74
74
  return schema.nodes.doc.create(null, blocks.length ? blocks : schema.nodes.paragraph.create());
75
75
  }
76
76
  /** Whether `rt` decodes under an inline schema with nothing but whitespace dropped:
77
- * upstream's `inline` rule, or one plain line and the empty one a trailing `\n`
78
- * opens. That newline is the one a YAML `|` scalar keeps in a `plaintext` value,
79
- * which upstream refuses (`trailingNewline`) and the inline decode drops. Marks are
80
- * the schema's own business (`plaintextSchema` declares none). */
77
+ * upstream's one-line rule, or one plain line and the empty one a trailing `\n`
78
+ * opens. That newline is the one a YAML `|` scalar keeps in a `text` value, which
79
+ * upstream ignores and the inline decode drops. Marks are the schema's own business
80
+ * (`plaintextSchema` declares none). */
81
81
  export function fitsInline(rt) {
82
82
  if (core().isInline(rt))
83
83
  return true;
@@ -96,7 +96,7 @@ export function fitsPlain(rt) {
96
96
  }
97
97
  /** Whether a leaf so declared edits `rt` losing nothing: {@link fitsInline} narrowed
98
98
  * and {@link fitsPlain} plain, past which the decode drops what the first commit would
99
- * store; on the block schema anything but an element or a task item ({@link editable}),
99
+ * store; on the block schema anything but an element ({@link editable}),
100
100
  * and an island where the leaf mounts no island view (`islands`), which draws as its
101
101
  * placeholder tag, an atom a keystroke deletes unseen. What a leaf held over the rest
102
102
  * reads. */
@@ -107,10 +107,10 @@ export function fitsLeaf(rt, leaf) {
107
107
  return fitsPlain(rt);
108
108
  return (leaf.islands || rt.islands.length === 0) && rt.lines.every(editable);
109
109
  }
110
- /** Whether every container on `line` is one the block schema edits: an element, and a
111
- * task item's `checked`, decode to no node, so the first commit would drop them. */
110
+ /** Whether every container on `line` is one the block schema edits: an element decodes
111
+ * to no node, so the first commit would drop it. */
112
112
  function editable(line) {
113
- return line.containers.every((c) => c.container === 'quote' || (c.container === 'list_item' && c.attrs.checked === undefined));
113
+ return line.containers.every((c) => c.container === 'quote' || c.container === 'list_item');
114
114
  }
115
115
  /** Inline decode: one paragraph, containers and islands stripped. */
116
116
  function decodeInline(rt, schema, lineTexts, starts, marks, cursor) {
@@ -184,7 +184,7 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
184
184
  let l = k + 1;
185
185
  while (l < run.length && ordinalAt(run[l], depth) === ord)
186
186
  l++;
187
- items.push(schema.nodes.list_item.create(null, groupBlocks(schema, run.slice(k, l), depth + 1, marks, cursor)));
187
+ items.push(schema.nodes.list_item.create({ checked: checkedAt(run[k], depth) }, groupBlocks(schema, run.slice(k, l), depth + 1, marks, cursor)));
188
188
  k = l;
189
189
  }
190
190
  const listType = ordered ? schema.nodes.ordered_list : schema.nodes.bullet_list;
@@ -235,6 +235,12 @@ function ordinalAt(leaf, depth) {
235
235
  const c = atDepth(leaf, depth);
236
236
  return c?.container === 'list_item' ? c.attrs.ordinal : -1;
237
237
  }
238
+ /** A task item's `checked` at `depth`, `null` on any other item: what the item's lines
239
+ * carry alike, so its first line answers for it. */
240
+ function checkedAt(leaf, depth) {
241
+ const c = atDepth(leaf, depth);
242
+ return c?.container === 'list_item' ? (c.attrs.checked ?? null) : null;
243
+ }
238
244
  /** A container's `instance`, absent meaning the zero a canonical read omits. It is what
239
245
  * tells one container from an adjacent sibling of identical shape, which contiguity
240
246
  * alone reads as one, so absent and `0` have to answer alike. */
@@ -122,7 +122,12 @@ function scanBlock(acc, node, nodePos, containers, instance) {
122
122
  const start = ordered ? node.attrs.start : 1;
123
123
  let itemPos = contentStart;
124
124
  node.forEach((item, _off, index) => {
125
- const container = withInstance({ container: 'list_item', attrs: { ordered, start, ordinal: index } }, instance);
125
+ const checked = item.attrs.checked;
126
+ const attrs = { ordered, start, ordinal: index };
127
+ const container = withInstance({
128
+ container: 'list_item',
129
+ attrs: checked === null ? attrs : { ...attrs, checked }
130
+ }, instance);
126
131
  scanBlocks(acc, item, itemPos + 1, [...containers, container]);
127
132
  itemPos += item.nodeSize;
128
133
  });
@@ -3,6 +3,7 @@ import { Plugin } from 'prosemirror-state';
3
3
  import { EditorView } from 'prosemirror-view';
4
4
  import type { Document, Content, Addr, Quill } from '@quillmark/wasm';
5
5
  import type { EditorErrorHandler } from '../errors.js';
6
+ import { type TextCap } from './cap.js';
6
7
  import { type TableChromeStrings } from './table-view.js';
7
8
  import { type SlashState, type SlashStrings } from './slash.js';
8
9
  /** Options for {@link createField}. */
@@ -14,20 +15,20 @@ export interface CreateFieldOpts {
14
15
  quill: Quill;
15
16
  addr: Addr;
16
17
  container: HTMLElement;
17
- /** Constrained single-textblock schema (a field declaring `inline`). */
18
+ /** Constrained single-textblock schema (a one-line field). */
18
19
  inline?: boolean;
19
- /** A mark-free schema (a `plaintext` field): literal text, no formatting and no
20
+ /** A mark-free schema (a `text` field): literal text, no formatting and no
20
21
  * anchors, in paragraphs and hard breaks or, with `inline`, one textblock. */
21
22
  plaintext?: boolean;
22
23
  /** The id of the note the caller draws inside `container` for a held leaf →
23
24
  * `aria-describedby` while it holds. */
24
25
  heldNoteId?: string;
25
26
  /**
26
- * Fired when the leaf holds or releases, and at mount when it mounts held. A leaf
27
- * declaring `inline`, or a `plaintext` one, is held over a value its schema cannot
27
+ * Fired when the leaf holds or releases, and at mount when it mounts held. A one-line
28
+ * leaf, or a `text` one, is held over a value its schema cannot
28
29
  * hold (`fitsLeaf`, upstream's `isInline` and `isPlain`): the decode would join lines
29
30
  * and drop containers and islands for the first commit to store. Any leaf is held
30
- * over an element or a task item, which no node holds. A held leaf draws
31
+ * over an element, which no node holds. A held leaf draws
31
32
  * its content on the block schema, read-only, and commits nothing. Judged of the value
32
33
  * each mount and re-hydrate reads, so {@link FieldController.applyExternal} is what
33
34
  * re-evaluates it.
@@ -75,10 +76,16 @@ export interface CreateFieldOpts {
75
76
  * a body's invitation. The initial value; {@link FieldController.setPlaceholder}
76
77
  * moves it after mount. Empty/absent shows no ghost. */
77
78
  placeholder?: string;
79
+ /** The schema's `max:` on the field: a character count no edit takes the leaf past
80
+ * (`cap.ts`). */
81
+ cap?: TextCap;
78
82
  /** The placeholder goes at the leaf's first edit: the edit answers the leaf, and
79
83
  * emptied it holds an empty answer, which prints empty. Absent, the placeholder
80
84
  * returns whenever the leaf is empty. */
81
85
  placeholderUntilEdit?: boolean;
86
+ /** An edit leaving the leaf empty removes the field: an optional cell (`t?`), whose
87
+ * unanswered `none` is what the empty leaf draws and an empty answer would hide. */
88
+ emptyUnsets?: boolean;
82
89
  onFocus?(addr: Addr): void;
83
90
  /** Fired with the new USV caret after an edit or a selection move. */
84
91
  onCaretMove?(addr: Addr, pos: number): void;
@@ -88,8 +95,8 @@ export interface CreateFieldOpts {
88
95
  * move, so a host driving a recompile off it recompiles on every arrow key.
89
96
  *
90
97
  * Fires for every commit regardless of which branch it took (`applyChange`, the
91
- * first-edit `overwrite`, the fallback `overwrite`, a `plaintext` leaf's typed
92
- * write) and for an anchor mutation,
98
+ * first-edit `overwrite`, the fallback `overwrite`, a `text` leaf's typed write)
99
+ * and for an anchor mutation,
93
100
  * which changes no text. A commit that failed outright (`commit-lost`) does not
94
101
  * fire it: nothing landed.
95
102
  */
@@ -168,6 +175,8 @@ export interface LeafViews {
168
175
  /** The canonical empty `Content`: one empty `para` line. The empty a prose
169
176
  * leaf (or an array's prose element) seeds from. */
170
177
  export declare function emptyContent(): Content;
178
+ /** Whether `rt` is the empty leaf: {@link emptyContent}, nothing typed and no shape left. */
179
+ export declare function isEmptyContent(rt: Content): boolean;
171
180
  /** The naming attributes for the `contenteditable`, shared by every prose leaf
172
181
  * (`ProseValue` takes it too, so an addressed leaf and a by-value one name their
173
182
  * regions by one rule). `aria-labelledby` supersedes `aria-label`: a leaf carrying
@@ -197,10 +206,10 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
197
206
  * Backspace that takes one back, the field keymap over the base keymap, the URL paste
198
207
  * that links a selection (`link-paste.ts`), and last the ones that answer to a block
199
208
  * leaf's own shapes: the gap cursor, {@link pastAtomPlugin}, the island paste pass
200
- * (`islands.ts`), the table paste (`table-paste.ts`) and the markdown paste
201
- * (`markdown-paste.ts`).
209
+ * (`islands.ts`), the table paste (`table-paste.ts`), the markdown paste
210
+ * (`markdown-paste.ts`) and the task item's checkbox (`tasks.ts`).
202
211
  *
203
- * Every mark-shaped plugin reads the schema rather than a flag: over either plaintext
212
+ * Every mark-shaped plugin reads the schema rather than a flag: over either mark-free
204
213
  * schema the shorthand rules build nothing (each is guarded on its mark type), the
205
214
  * toggles bind no key and a pasted URL stays text, so a `**bold**` keeps the delimiters
206
215
  * its author typed without a second rule saying so.
@@ -219,5 +228,8 @@ export declare function proseLeafPlugins(schema: Schema, opts: {
219
228
  placeholder?: () => string | undefined;
220
229
  /** {@link CreateFieldOpts.placeholderUntilEdit}. */
221
230
  placeholderUntilEdit?: boolean;
231
+ /** {@link CreateFieldOpts.cap}. Ahead of every other paste handler, which a paste
232
+ * it cuts never reaches. */
233
+ cap?: TextCap;
222
234
  afterHistory?: Plugin[];
223
235
  }): Plugin[];