@quillmark/svelte 0.20.1 → 0.22.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.
- package/NOTICE +6 -7
- package/README.md +1 -1
- package/THEMING.md +1 -1
- package/dist/core/address.d.ts +6 -1
- package/dist/core/address.js +12 -3
- package/dist/core/bloom.d.ts +9 -4
- package/dist/core/bloom.js +18 -49
- package/dist/core/codec/atoms.d.ts +1 -1
- package/dist/core/codec/atoms.js +26 -11
- package/dist/core/codec/blocks.d.ts +5 -5
- package/dist/core/codec/blocks.js +27 -8
- package/dist/core/codec/code.d.ts +4 -4
- package/dist/core/codec/code.js +52 -22
- package/dist/core/codec/decode.d.ts +5 -5
- package/dist/core/codec/decode.js +18 -12
- package/dist/core/codec/encode.js +6 -1
- package/dist/core/codec/field.d.ts +22 -15
- package/dist/core/codec/field.js +66 -26
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +2 -2
- package/dist/core/codec/inputrules.d.ts +4 -0
- package/dist/core/codec/inputrules.js +44 -6
- package/dist/core/codec/link-paste.d.ts +11 -0
- package/dist/core/codec/link-paste.js +32 -0
- package/dist/core/codec/lists.d.ts +5 -4
- package/dist/core/codec/lists.js +87 -31
- package/dist/core/codec/nesting.d.ts +20 -0
- package/dist/core/codec/nesting.js +62 -0
- package/dist/core/codec/prose.css +145 -63
- package/dist/core/codec/schema.d.ts +2 -2
- package/dist/core/codec/schema.js +71 -18
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-copy.d.ts +17 -0
- package/dist/core/codec/table-copy.js +84 -0
- package/dist/core/codec/table-view.d.ts +6 -7
- package/dist/core/codec/table-view.js +281 -201
- package/dist/core/codec/table.d.ts +2 -9
- package/dist/core/codec/table.js +10 -19
- package/dist/core/codec/tasks.d.ts +19 -0
- package/dist/core/codec/tasks.js +140 -0
- package/dist/core/errors.d.ts +2 -2
- package/dist/core/rungs.d.ts +6 -0
- package/dist/core/rungs.js +11 -0
- package/dist/core/theme.css +24 -12
- package/dist/visual/ArrayField.svelte +236 -78
- package/dist/visual/ArrayField.svelte.d.ts +0 -2
- package/dist/visual/BooleanField.svelte +12 -1
- package/dist/visual/Card.svelte +41 -23
- package/dist/visual/Card.svelte.d.ts +3 -2
- package/dist/visual/CardControls.svelte +15 -15
- package/dist/visual/DateField.svelte +93 -64
- package/dist/visual/EnumField.svelte +9 -4
- package/dist/visual/Field.svelte +20 -16
- package/dist/visual/Field.svelte.d.ts +4 -2
- package/dist/visual/FieldHint.svelte +22 -5
- package/dist/visual/FieldLabel.svelte +5 -33
- package/dist/visual/FieldLabel.svelte.d.ts +0 -2
- package/dist/visual/FormatPopover.svelte +78 -81
- package/dist/visual/MatrixField.svelte +28 -19
- package/dist/visual/NumberField.svelte +32 -3
- package/dist/visual/ObjectField.svelte +40 -24
- package/dist/visual/ObjectField.svelte.d.ts +6 -0
- package/dist/visual/ProseField.svelte +10 -13
- package/dist/visual/ProseField.svelte.d.ts +5 -2
- package/dist/visual/ProseValue.svelte +25 -19
- package/dist/visual/ProseValue.svelte.d.ts +4 -4
- package/dist/visual/TextField.svelte +3 -2
- package/dist/visual/TipsCard.svelte +7 -7
- package/dist/visual/VariantField.svelte +1 -1
- package/dist/visual/VariantField.svelte.d.ts +1 -1
- package/dist/visual/VisualEditorInner.svelte +121 -39
- package/dist/visual/controls.css +21 -2
- package/dist/visual/icons/Icon.svelte +3 -2
- package/dist/visual/icons/Icon.svelte.d.ts +3 -2
- package/dist/visual/icons/nodes.d.ts +25 -21
- package/dist/visual/icons/nodes.js +9 -7
- package/dist/visual/leaves.d.ts +12 -9
- package/dist/visual/leaves.js +19 -0
- package/dist/visual/motion.d.ts +25 -3
- package/dist/visual/motion.js +58 -9
- package/dist/visual/strings.d.ts +15 -9
- package/dist/visual/strings.js +19 -12
- package/dist/visual/structure.d.ts +21 -31
- package/dist/visual/structure.js +54 -51
- 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
|
|
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,
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
38
|
-
code,
|
|
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
|
|
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
|
package/THEMING.md
CHANGED
|
@@ -56,7 +56,7 @@ The poles take a colour, not `transparent` or `inherit`: the raised surfaces, th
|
|
|
56
56
|
| `--qm-bg` | `#fff` / `#272729` | Base surface: a card, and the preview's sheet. Fields, the popover and the column behind the cards each step off it. |
|
|
57
57
|
| `--qm-fg` | `#1c1c1c` / `#d7dadc` | Base ink. Body text, labels, and borders step off it. |
|
|
58
58
|
| `--qm-accent` | `#71717a` / `#a1a1aa` | Focus rings, active marks, the preview's active field box. The surface spends no hue of its own, so this is the one it wears. |
|
|
59
|
-
| `--qm-danger` | `#9e1518` / `#f9786c` | Error diagnostics
|
|
59
|
+
| `--qm-danger` | `#9e1518` / `#f9786c` | Error diagnostics and the delete glyph. |
|
|
60
60
|
| `--qm-font` | `ui-sans-serif, system-ui, sans-serif` | The editor surface's font family. Controls and buttons take it too, in place of the UA face. |
|
|
61
61
|
| `--qm-font-mono` | `ui-monospace, monospace` | The monospace face: the JSON array control, the tips card. |
|
|
62
62
|
| `--qm-font-size` | `0.875rem` | Chrome text: every control's size, the anchor the ramp derives up (title) and down (label/meta) from, and the anchor a document body sits one small step over. |
|
package/dist/core/address.d.ts
CHANGED
|
@@ -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
|
package/dist/core/address.js
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
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,
|
package/dist/core/bloom.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
|
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
|
|
13
|
+
export declare function bloom(host: HTMLElement): void;
|
package/dist/core/bloom.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
53
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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
|
package/dist/core/codec/atoms.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// The block-atom link of the body's Backspace/Delete chains: a delete against an
|
|
2
|
-
// island or a divider selects it, and the press after that deletes it.
|
|
2
|
+
// island or a divider selects it, and the press after that deletes it. A table is the
|
|
3
|
+
// exception, having cells a caret can enter: the press steps into it instead, so a
|
|
4
|
+
// delete against one edits nothing (§`againstAtom`).
|
|
3
5
|
//
|
|
4
6
|
// This is the neighbour half of the rule the leaf already holds for a key typed over a
|
|
5
7
|
// selection (CODEC §"The table island"): a selection is the subject of the next
|
|
@@ -12,6 +14,7 @@
|
|
|
12
14
|
// first item lifts the item, and that stays the item's answer whatever sits above the
|
|
13
15
|
// list.
|
|
14
16
|
import { NodeSelection, TextSelection } from 'prosemirror-state';
|
|
17
|
+
import { tableAt } from './table-view.js';
|
|
15
18
|
/** The caret, when it sits at a textblock edge with an empty selection: the only place
|
|
16
19
|
* a delete is about a neighbour rather than about a character. `null` everywhere else,
|
|
17
20
|
* which is where the key keeps its ordinary meaning. */
|
|
@@ -44,25 +47,37 @@ function cutAt($pos, side) {
|
|
|
44
47
|
}
|
|
45
48
|
return null;
|
|
46
49
|
}
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
-
* a
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
/** The press against a block atom: select it, rather than deleting it, or step into a
|
|
51
|
+
* mounted table, its last cell from below and its first from above. An empty line under
|
|
52
|
+
* a table goes with the step, its Backspace having nothing of its own to edit. Declines
|
|
53
|
+
* when the neighbour is anything else a caret can enter, so a paragraph, a heading, a
|
|
54
|
+
* quote and a list all keep the join the base keymap gives them. */
|
|
55
|
+
function againstAtom(side) {
|
|
56
|
+
return (state, dispatch, view) => {
|
|
52
57
|
const $head = atEdge(state, side);
|
|
53
58
|
const $cut = $head && cutAt($head, side);
|
|
54
|
-
if (!$cut)
|
|
59
|
+
if (!$head || !$cut)
|
|
55
60
|
return false;
|
|
56
61
|
const node = side < 0 ? $cut.nodeBefore : $cut.nodeAfter;
|
|
57
62
|
if (!node || !node.isAtom || !node.isBlock || !NodeSelection.isSelectable(node))
|
|
58
63
|
return false;
|
|
59
64
|
const at = side < 0 ? $cut.pos - node.nodeSize : $cut.pos;
|
|
60
|
-
|
|
65
|
+
const table = view && tableAt(view, at);
|
|
66
|
+
if (!table) {
|
|
67
|
+
dispatch?.(state.tr.setSelection(NodeSelection.create(state.doc, at)).scrollIntoView());
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (!dispatch)
|
|
71
|
+
return true;
|
|
72
|
+
const index = $head.index(-1);
|
|
73
|
+
if (side < 0 && $head.parent.content.size === 0 && $head.node(-1).canReplace(index, index + 1))
|
|
74
|
+
dispatch(state.tr.delete($head.before(), $head.after()));
|
|
75
|
+
table.enter(side);
|
|
61
76
|
return true;
|
|
62
77
|
};
|
|
63
78
|
}
|
|
64
79
|
/**
|
|
65
|
-
* The block-atom bindings for a block-schema leaf: `{}` for the
|
|
80
|
+
* The block-atom bindings for a block-schema leaf: `{}` for the one-line and plain
|
|
66
81
|
* schemas, which hold no block to be against.
|
|
67
82
|
*
|
|
68
83
|
* Both keys are the same command on opposite sides, because what a delete against an
|
|
@@ -72,7 +87,7 @@ export function atomKeymap(schema) {
|
|
|
72
87
|
if (!schema.nodes.island_block)
|
|
73
88
|
return {};
|
|
74
89
|
return {
|
|
75
|
-
Backspace:
|
|
76
|
-
Delete:
|
|
90
|
+
Backspace: againstAtom(-1),
|
|
91
|
+
Delete: againstAtom(1)
|
|
77
92
|
};
|
|
78
93
|
}
|
|
@@ -3,11 +3,11 @@ 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`. */
|
|
5
5
|
export declare function toHeading(level: number): Command;
|
|
6
|
-
/** A fence. The exit paragraph after it is the one a
|
|
7
|
-
* reason, sharpened: a code block is the one block a
|
|
8
|
-
* either, so a fence at the end of a body leaves the
|
|
9
|
-
* stays in the fence, not in the exit it minted. */
|
|
10
|
-
export declare function toCodeBlock(): Command;
|
|
6
|
+
/** A fence, carrying `attrs` (its `lang`). The exit paragraph after it is the one a
|
|
7
|
+
* block island mints for the same reason, sharpened: a code block is the one block a
|
|
8
|
+
* gap cursor will not sit beside either, so a fence at the end of a body leaves the
|
|
9
|
+
* caret nowhere to go. The caret stays in the fence, not in the exit it minted. */
|
|
10
|
+
export declare function toCodeBlock(attrs?: Attrs): Command;
|
|
11
11
|
/** `---`. A divider is a whole block, so this replaces its textblock rather than
|
|
12
12
|
* retyping one: `horizontal_rule` holds no content to retype into, and an empty block
|
|
13
13
|
* is the whole of what it may replace — anything else in one is a paragraph being
|
|
@@ -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,24 +75,24 @@ function wrapAt(tr, pos, type, attrs, joinBefore) {
|
|
|
73
75
|
(!joinBefore || joinBefore(before))) {
|
|
74
76
|
tr.join(pos - 1);
|
|
75
77
|
}
|
|
76
|
-
return
|
|
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`. */
|
|
80
82
|
export function toHeading(level) {
|
|
81
83
|
return retype('heading', { level });
|
|
82
84
|
}
|
|
83
|
-
/** A fence. The exit paragraph after it is the one a
|
|
84
|
-
* reason, sharpened: a code block is the one block a
|
|
85
|
-
* either, so a fence at the end of a body leaves the
|
|
86
|
-
* stays in the fence, not in the exit it minted. */
|
|
87
|
-
export function toCodeBlock() {
|
|
85
|
+
/** A fence, carrying `attrs` (its `lang`). The exit paragraph after it is the one a
|
|
86
|
+
* block island mints for the same reason, sharpened: a code block is the one block a
|
|
87
|
+
* gap cursor will not sit beside either, so a fence at the end of a body leaves the
|
|
88
|
+
* caret nowhere to go. The caret stays in the fence, not in the exit it minted. */
|
|
89
|
+
export function toCodeBlock(attrs) {
|
|
88
90
|
return (state, dispatch) => {
|
|
89
91
|
const { code_block: code, paragraph } = state.schema.nodes;
|
|
90
92
|
const { $from } = state.selection;
|
|
91
93
|
if (!code || !paragraph || !atHead(state) || !fits($from, code))
|
|
92
94
|
return false;
|
|
93
|
-
const tr = state.tr.setBlockType($from.pos, $from.pos, code);
|
|
95
|
+
const tr = state.tr.setBlockType($from.pos, $from.pos, code, attrs);
|
|
94
96
|
const at = tr.doc.resolve($from.pos).after();
|
|
95
97
|
if (!tr.doc.nodeAt(at))
|
|
96
98
|
tr.insert(at, paragraph.create());
|
|
@@ -322,6 +324,22 @@ const takeEmptyLine = (state, dispatch) => {
|
|
|
322
324
|
dispatch?.(state.tr.delete($from.before(), $from.after()).scrollIntoView());
|
|
323
325
|
return true;
|
|
324
326
|
};
|
|
327
|
+
/**
|
|
328
|
+
* Backspace at a heading's head → a paragraph, where it stands, and the press after is
|
|
329
|
+
* the join. The first press takes off the block's own shape, which is the whole of what
|
|
330
|
+
* it edits: the base keymap's join would merge the heading's text into the block above
|
|
331
|
+
* and lose the heading with it, and at the body's start it does nothing at all.
|
|
332
|
+
*/
|
|
333
|
+
const unheading = (state, dispatch) => {
|
|
334
|
+
const paragraph = state.schema.nodes.paragraph;
|
|
335
|
+
const { $from } = state.selection;
|
|
336
|
+
if (!paragraph || !atHead(state) || $from.parent.type.name !== 'heading')
|
|
337
|
+
return false;
|
|
338
|
+
if (!fits($from, paragraph))
|
|
339
|
+
return false;
|
|
340
|
+
dispatch?.(state.tr.setBlockType($from.pos, $from.pos, paragraph).scrollIntoView());
|
|
341
|
+
return true;
|
|
342
|
+
};
|
|
325
343
|
/** The body-wide link of the leaf's key chains: `{}` for a schema with no paragraph
|
|
326
344
|
* to mint. Outermost of the structural links, being about the body, or about the
|
|
327
345
|
* caret's own line, rather than about the surface either stands in. */
|
|
@@ -330,6 +348,7 @@ export function blockKeymap(schema) {
|
|
|
330
348
|
return {};
|
|
331
349
|
return {
|
|
332
350
|
Enter: chainCommands(paragraphAtBodyStart, paragraphFromEmptyFence, splitAcrossBlocks),
|
|
351
|
+
Backspace: unheading,
|
|
333
352
|
Delete: takeEmptyLine
|
|
334
353
|
};
|
|
335
354
|
}
|
|
@@ -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
|
|
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
|
|
@@ -9,8 +9,8 @@ import { type Command } from 'prosemirror-state';
|
|
|
9
9
|
* otherwise splits the item in two, each half holding a code block. `Shift-Enter` is
|
|
10
10
|
* the same command because it is the same edit: the line break the key means
|
|
11
11
|
* everywhere else (`breaks.ts`) is, inside a fence, the newline Enter already takes.
|
|
12
|
-
* Every command here declines away from a code block — inside one for the
|
|
13
|
-
*
|
|
14
|
-
* keys elsewhere.
|
|
12
|
+
* Every command here declines away from a code block — inside one for the keys about
|
|
13
|
+
* its text and its last line, against one for the two about its edge — so the list
|
|
14
|
+
* links keep the keys elsewhere.
|
|
15
15
|
*/
|
|
16
16
|
export declare function codeKeymap(schema: Schema): Record<string, Command>;
|
package/dist/core/codec/code.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TextSelection } from 'prosemirror-state';
|
|
1
|
+
import { Selection, TextSelection } from 'prosemirror-state';
|
|
2
2
|
import { newlineInCode } from 'prosemirror-commands';
|
|
3
3
|
/** One indent level. */
|
|
4
4
|
const UNIT = ' ';
|
|
@@ -107,7 +107,7 @@ const outdentInCode = (state, dispatch) => {
|
|
|
107
107
|
* The textblock the caret's own block stands directly against on `side`: its sibling
|
|
108
108
|
* under one parent, which is the only neighbour a press joins the text of. `null` at a
|
|
109
109
|
* container's edge, where the base keymap moves the whole block in or out rather than
|
|
110
|
-
* joining anything, and so
|
|
110
|
+
* joining anything, and so changes nothing here.
|
|
111
111
|
*/
|
|
112
112
|
function blockAgainst(state, side) {
|
|
113
113
|
const { $from, empty } = state.selection;
|
|
@@ -122,22 +122,22 @@ function blockAgainst(state, side) {
|
|
|
122
122
|
return { node, pos: side < 0 ? $edge.pos - node.nodeSize : $edge.pos };
|
|
123
123
|
}
|
|
124
124
|
/**
|
|
125
|
-
* Backspace at a fence's
|
|
126
|
-
*
|
|
127
|
-
* press after that is an ordinary join between two prose blocks.
|
|
125
|
+
* Backspace or Delete at a fence's edge, from either side, edits only the block the caret
|
|
126
|
+
* is in.
|
|
128
127
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* `
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
128
|
+
* Inside the fence, a press at its head or its end retypes it to a paragraph and joins
|
|
129
|
+
* nothing: `toCodeBlock`'s inverse in its place (`blocks.ts`), taking no text with it,
|
|
130
|
+
* since `linebreakReplacement` carries the fence's newlines across as breaks
|
|
131
|
+
* (`schema.ts`). The press after that is an ordinary join between two prose blocks.
|
|
132
|
+
*
|
|
133
|
+
* From the prose block against it, the caret steps into the fence, to the end it faces,
|
|
134
|
+
* and the fence is left as it is: a join would retype one side's whole text as the
|
|
135
|
+
* other's kind, and a retype from outside edits a block the caret is not in. An empty
|
|
136
|
+
* line under a fence goes with the step, its Backspace having nothing of its own to edit.
|
|
135
137
|
*
|
|
136
138
|
* 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
139
|
*/
|
|
140
|
-
function
|
|
140
|
+
function atProseEdge(side) {
|
|
141
141
|
return (state, dispatch) => {
|
|
142
142
|
const paragraph = state.schema.nodes.paragraph;
|
|
143
143
|
const edge = blockAgainst(state, side);
|
|
@@ -147,13 +147,42 @@ function retypeAtProseEdge(side) {
|
|
|
147
147
|
const inside = !!$from.parent.type.spec.code;
|
|
148
148
|
if (inside === !!edge.node.type.spec.code)
|
|
149
149
|
return false;
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
if (inside) {
|
|
151
|
+
dispatch?.(state.tr.setBlockType($from.pos, $from.pos, paragraph).scrollIntoView());
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
const tr = state.tr;
|
|
155
|
+
const index = $from.index(-1);
|
|
156
|
+
if (side < 0 && $from.parent.content.size === 0 && $from.node(-1).canReplace(index, index + 1))
|
|
157
|
+
tr.delete($from.before(), $from.after());
|
|
158
|
+
const into = side < 0 ? edge.pos + edge.node.nodeSize - 1 : edge.pos + 1;
|
|
159
|
+
dispatch?.(tr.setSelection(TextSelection.create(tr.doc, into)).scrollIntoView());
|
|
152
160
|
return true;
|
|
153
161
|
};
|
|
154
162
|
}
|
|
163
|
+
/** ↓ on the last line of a fence nothing follows → a paragraph after it, the caret
|
|
164
|
+
* there: no block stands below to arrow to, and a gap cursor will not sit beside a
|
|
165
|
+
* fence. */
|
|
166
|
+
const belowFinalFence = (state, dispatch) => {
|
|
167
|
+
const paragraph = state.schema.nodes.paragraph;
|
|
168
|
+
const { $from, empty } = state.selection;
|
|
169
|
+
if (!paragraph || !empty || !$from.parent.type.spec.code)
|
|
170
|
+
return false;
|
|
171
|
+
if ($from.parent.textBetween($from.parentOffset, $from.parent.content.size).includes('\n'))
|
|
172
|
+
return false;
|
|
173
|
+
const $after = state.doc.resolve($from.after());
|
|
174
|
+
if (Selection.findFrom($after, 1))
|
|
175
|
+
return false;
|
|
176
|
+
if (!$after.parent.canReplaceWith($after.index(), $after.index(), paragraph))
|
|
177
|
+
return false;
|
|
178
|
+
if (dispatch) {
|
|
179
|
+
const tr = state.tr.insert($after.pos, paragraph.create());
|
|
180
|
+
dispatch(tr.setSelection(TextSelection.create(tr.doc, $after.pos + 1)).scrollIntoView());
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
};
|
|
155
184
|
/**
|
|
156
|
-
* The `code_block` link of the body's key chains: `{}` for the
|
|
185
|
+
* The `code_block` link of the body's key chains: `{}` for the one-line and plain
|
|
157
186
|
* schemas, which declare no code node.
|
|
158
187
|
*
|
|
159
188
|
* `Enter` is upstream's `newlineInCode`, and it is load-bearing rather than
|
|
@@ -161,9 +190,9 @@ function retypeAtProseEdge(side) {
|
|
|
161
190
|
* otherwise splits the item in two, each half holding a code block. `Shift-Enter` is
|
|
162
191
|
* the same command because it is the same edit: the line break the key means
|
|
163
192
|
* everywhere else (`breaks.ts`) is, inside a fence, the newline Enter already takes.
|
|
164
|
-
* Every command here declines away from a code block — inside one for the
|
|
165
|
-
*
|
|
166
|
-
* keys elsewhere.
|
|
193
|
+
* Every command here declines away from a code block — inside one for the keys about
|
|
194
|
+
* its text and its last line, against one for the two about its edge — so the list
|
|
195
|
+
* links keep the keys elsewhere.
|
|
167
196
|
*/
|
|
168
197
|
export function codeKeymap(schema) {
|
|
169
198
|
if (!schema.nodes.code_block)
|
|
@@ -173,7 +202,8 @@ export function codeKeymap(schema) {
|
|
|
173
202
|
'Shift-Tab': outdentInCode,
|
|
174
203
|
Enter: newlineInCode,
|
|
175
204
|
'Shift-Enter': newlineInCode,
|
|
176
|
-
Backspace:
|
|
177
|
-
Delete:
|
|
205
|
+
Backspace: atProseEdge(-1),
|
|
206
|
+
Delete: atProseEdge(1),
|
|
207
|
+
ArrowDown: belowFinalFence
|
|
178
208
|
};
|
|
179
209
|
}
|
|
@@ -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
|
|
22
|
-
* opens. That newline is the one a YAML `|` scalar keeps in a `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
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
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
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
|
|
78
|
-
* opens. That newline is the one a YAML `|` scalar keeps in a `
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
|
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
|
|
111
|
-
*
|
|
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' ||
|
|
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(
|
|
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. */
|