@quillmark/svelte 0.11.0 → 0.13.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/dist/core/address.d.ts +17 -10
- package/dist/core/address.js +26 -12
- package/dist/core/bloom.js +4 -3
- package/dist/core/codec/decode.d.ts +17 -0
- package/dist/core/codec/decode.js +78 -19
- package/dist/core/codec/encode.js +1 -1
- package/dist/core/codec/field.d.ts +64 -15
- package/dist/core/codec/field.js +201 -65
- package/dist/core/codec/index.d.ts +3 -3
- package/dist/core/codec/index.js +3 -3
- package/dist/core/codec/islands.d.ts +5 -4
- package/dist/core/codec/islands.js +23 -38
- package/dist/core/codec/keymap.d.ts +5 -4
- package/dist/core/codec/keymap.js +5 -4
- package/dist/core/codec/prose.css +42 -0
- package/dist/core/codec/schema.d.ts +24 -5
- package/dist/core/codec/schema.js +40 -9
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/errors.d.ts +3 -2
- package/dist/core/rungs.d.ts +8 -0
- package/dist/core/rungs.js +24 -0
- package/dist/core/theme.css +19 -6
- package/dist/preview/paint.js +21 -17
- package/dist/visual/ArrayField.svelte +649 -176
- package/dist/visual/ArrayField.svelte.d.ts +16 -1
- package/dist/visual/BooleanField.svelte +36 -11
- package/dist/visual/BooleanField.svelte.d.ts +2 -0
- package/dist/visual/Card.svelte +6 -25
- package/dist/visual/DateField.svelte +66 -31
- package/dist/visual/EnumField.svelte +27 -9
- package/dist/visual/EnumField.svelte.d.ts +5 -0
- package/dist/visual/Field.svelte +113 -38
- package/dist/visual/FormatPopover.svelte +4 -3
- package/dist/visual/MatrixField.svelte +353 -0
- package/dist/visual/MatrixField.svelte.d.ts +36 -0
- package/dist/visual/NumberField.svelte +31 -14
- package/dist/visual/NumberField.svelte.d.ts +5 -1
- package/dist/visual/ObjectField.svelte +404 -139
- package/dist/visual/ObjectField.svelte.d.ts +17 -2
- package/dist/visual/ProseField.svelte +53 -23
- package/dist/visual/ProseField.svelte.d.ts +15 -3
- package/dist/visual/ProseValue.svelte +102 -46
- package/dist/visual/ProseValue.svelte.d.ts +17 -5
- package/dist/visual/TextField.svelte +35 -18
- package/dist/visual/TextField.svelte.d.ts +7 -0
- package/dist/visual/VariantField.svelte +56 -2
- package/dist/visual/VariantField.svelte.d.ts +10 -1
- package/dist/visual/VisualEditorInner.svelte +77 -73
- package/dist/visual/controls.css +47 -5
- package/dist/visual/diagnostics.d.ts +26 -2
- package/dist/visual/diagnostics.js +52 -5
- package/dist/visual/hold.js +2 -2
- package/dist/visual/icons/nodes.d.ts +3 -0
- package/dist/visual/icons/nodes.js +2 -1
- package/dist/visual/leaves.d.ts +25 -12
- package/dist/visual/motion.d.ts +14 -5
- package/dist/visual/motion.js +13 -11
- package/dist/visual/strings.d.ts +27 -10
- package/dist/visual/strings.js +8 -2
- package/dist/visual/structure.d.ts +159 -40
- package/dist/visual/structure.js +337 -45
- package/package.json +2 -2
package/dist/core/address.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Addr, HitGranularity } from '@quillmark/wasm';
|
|
1
|
+
import type { Addr, HitGranularity, PathStep } from '@quillmark/wasm';
|
|
2
2
|
/**
|
|
3
3
|
* A canonical field address: `main.<field>` / `main.body` /
|
|
4
4
|
* `cards.<kind>[<i>].<field>`, cards keyed by absolute document index.
|
|
@@ -63,7 +63,7 @@ export declare function cardPath(index: number, kinds: readonly string[]): DocPa
|
|
|
63
63
|
/**
|
|
64
64
|
* The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
|
|
65
65
|
* `undefined` for a path that names no single commit address — a nested or
|
|
66
|
-
* array-element path (`main.keywords[0]`, which {@link
|
|
66
|
+
* array-element path (`main.keywords[0]`, which {@link nestedAddrForFieldPath} and
|
|
67
67
|
* {@link nearestAddrForFieldPath} take instead), a field-rooted one, or a malformed
|
|
68
68
|
* one. A bare card and a `.body` terminal both land on the field-less `{card: i}`
|
|
69
69
|
* the body leaf answers to.
|
|
@@ -86,14 +86,21 @@ export declare function addrForFieldPath(path: DocPath): Addr | undefined;
|
|
|
86
86
|
*/
|
|
87
87
|
export declare function nearestAddrForFieldPath(path: DocPath): Addr | undefined;
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
89
|
+
* A nested address, split: the field's `Addr`, and the steps from it to the leaf.
|
|
90
|
+
* `main.keywords[0]` is `{field: keywords, steps: [0]}`,
|
|
91
|
+
* `main.vectors[0].tours[2].title` is `[0, 'tours', 2, 'title']`, and
|
|
92
|
+
* `main.contact.email` is `['email']`: an index segment reads as a number and a key as
|
|
93
|
+
* a string, the `PathStep` grammar `reader.getContentAt` walks. Anything
|
|
94
|
+
* {@link addrForFieldPath} can name — a field, a body, a card — has no steps and lands
|
|
95
|
+
* here as `undefined`; so does a body with something after it, a body holding no
|
|
96
|
+
* cells. Whether the schema declares each step is the caller's to check: this module
|
|
97
|
+
* holds the grammar and no schema. `regions()`, `positionAt` and `formatDocPath` all
|
|
98
|
+
* spell the index segment bracketed, so there is one spelling to read and none to
|
|
99
|
+
* bridge.
|
|
93
100
|
*/
|
|
94
|
-
export declare function
|
|
95
|
-
/**
|
|
96
|
-
export interface
|
|
101
|
+
export declare function nestedAddrForFieldPath(path: DocPath): NestedAddr | undefined;
|
|
102
|
+
/** A nested address: the field's `Addr`, and the steps from it to the leaf. */
|
|
103
|
+
export interface NestedAddr {
|
|
97
104
|
field: Addr;
|
|
98
|
-
|
|
105
|
+
steps: PathStep[];
|
|
99
106
|
}
|
package/dist/core/address.js
CHANGED
|
@@ -37,7 +37,7 @@ export function cardPath(index, kinds) {
|
|
|
37
37
|
/**
|
|
38
38
|
* The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
|
|
39
39
|
* `undefined` for a path that names no single commit address — a nested or
|
|
40
|
-
* array-element path (`main.keywords[0]`, which {@link
|
|
40
|
+
* array-element path (`main.keywords[0]`, which {@link nestedAddrForFieldPath} and
|
|
41
41
|
* {@link nearestAddrForFieldPath} take instead), a field-rooted one, or a malformed
|
|
42
42
|
* one. A bare card and a `.body` terminal both land on the field-less `{card: i}`
|
|
43
43
|
* the body leaf answers to.
|
|
@@ -66,21 +66,35 @@ export function nearestAddrForFieldPath(path) {
|
|
|
66
66
|
return segs && addrForSegs(segs.slice(0, 2));
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
69
|
+
* A nested address, split: the field's `Addr`, and the steps from it to the leaf.
|
|
70
|
+
* `main.keywords[0]` is `{field: keywords, steps: [0]}`,
|
|
71
|
+
* `main.vectors[0].tours[2].title` is `[0, 'tours', 2, 'title']`, and
|
|
72
|
+
* `main.contact.email` is `['email']`: an index segment reads as a number and a key as
|
|
73
|
+
* a string, the `PathStep` grammar `reader.getContentAt` walks. Anything
|
|
74
|
+
* {@link addrForFieldPath} can name — a field, a body, a card — has no steps and lands
|
|
75
|
+
* here as `undefined`; so does a body with something after it, a body holding no
|
|
76
|
+
* cells. Whether the schema declares each step is the caller's to check: this module
|
|
77
|
+
* holds the grammar and no schema. `regions()`, `positionAt` and `formatDocPath` all
|
|
78
|
+
* spell the index segment bracketed, so there is one spelling to read and none to
|
|
79
|
+
* bridge.
|
|
73
80
|
*/
|
|
74
|
-
export function
|
|
81
|
+
export function nestedAddrForFieldPath(path) {
|
|
75
82
|
const segs = segsOf(path);
|
|
76
|
-
if (!segs)
|
|
83
|
+
if (!segs || segs.length < 3)
|
|
77
84
|
return undefined;
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
85
|
+
const field = addrForSegs(segs.slice(0, 2));
|
|
86
|
+
if (field?.field == null)
|
|
80
87
|
return undefined;
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
const steps = [];
|
|
89
|
+
for (const seg of segs.slice(2)) {
|
|
90
|
+
if (seg.seg === 'index')
|
|
91
|
+
steps.push(seg.index);
|
|
92
|
+
else if (seg.seg === 'field')
|
|
93
|
+
steps.push(seg.name);
|
|
94
|
+
else
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return { field, steps };
|
|
84
98
|
}
|
|
85
99
|
/** `parseDocPath`, with a malformed path as `undefined` rather than a throw. The gate
|
|
86
100
|
* is read outside the try: an uninitialized core is not a malformed path, and would
|
package/dist/core/bloom.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
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
|
+
import { rungStyle } from './rungs.js';
|
|
9
10
|
const BLOOM_CLASS = 'qm-bloom';
|
|
10
11
|
const WASH = 'var(--_qm-accent-wash)';
|
|
11
12
|
/** The dwell used when the derivation is out of reach (an unstyled root, or jsdom,
|
|
@@ -24,8 +25,8 @@ const FRAMES = [
|
|
|
24
25
|
];
|
|
25
26
|
const FRAMES_REDUCED = [{ opacity: 1 }, { opacity: 1 }];
|
|
26
27
|
/**
|
|
27
|
-
* Run one wash on `el`, resolving the dwell off
|
|
28
|
-
* in CSS.
|
|
28
|
+
* Run one wash on `el`, resolving the dwell off its root so the number is
|
|
29
|
+
* single-sourced in CSS (`core/rungs.ts`).
|
|
29
30
|
*
|
|
30
31
|
* One property, both motion preferences: `--_qm-bloom-dwell` already shortens under
|
|
31
32
|
* `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
|
|
@@ -36,7 +37,7 @@ function bloom(el) {
|
|
|
36
37
|
if (typeof el.animate !== 'function')
|
|
37
38
|
return undefined;
|
|
38
39
|
const reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
39
|
-
const raw =
|
|
40
|
+
const raw = rungStyle(el).getPropertyValue('--_qm-bloom-dwell').trim();
|
|
40
41
|
const n = Number.parseFloat(raw);
|
|
41
42
|
const duration = !Number.isFinite(n) ? FALLBACK_MS : raw.endsWith('ms') ? n : n * 1000;
|
|
42
43
|
return el.animate(reduced ? FRAMES_REDUCED : FRAMES, { duration });
|
|
@@ -17,3 +17,20 @@ export declare function usvLength(s: string): number;
|
|
|
17
17
|
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
|
+
/** 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). */
|
|
25
|
+
export declare function fitsInline(rt: Content): boolean;
|
|
26
|
+
/** Whether `rt` decodes under `plainSchema` losing nothing but its marks, which the
|
|
27
|
+
* next commit heals (CODEC §Inline mode): upstream's `isPlain`, asked of the content
|
|
28
|
+
* with them set aside. */
|
|
29
|
+
export declare function fitsPlain(rt: Content): boolean;
|
|
30
|
+
/** Whether `rt` decodes under the schema a leaf so declared mounts, losing nothing its
|
|
31
|
+
* first commit would store: {@link fitsInline} narrowed, {@link fitsPlain} plain, and
|
|
32
|
+
* anything on the block schema. What a leaf held over the rest reads. */
|
|
33
|
+
export declare function fitsLeaf(rt: Content, leaf: {
|
|
34
|
+
inline: boolean;
|
|
35
|
+
plaintext: boolean;
|
|
36
|
+
}): boolean;
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
// nodes; a code fence's lines → one `code_block`), nest by shared `containers`
|
|
4
4
|
// prefix (`list_item`/`quote` → lists/blockquote), select the block node by
|
|
5
5
|
// `kind`, apply marks over their `[start,end)` USV ranges (PM splits inline nodes
|
|
6
|
-
// at mark boundaries), and lower island slots to leaf nodes (
|
|
7
|
-
//
|
|
8
|
-
// decorations (field.ts). Positions throughout are USV; `Array.from` iterates by
|
|
6
|
+
// at mark boundaries), and lower island slots to leaf nodes (block where a block
|
|
7
|
+
// island's slot stands alone on its line, inline otherwise). Anchors are not
|
|
8
|
+
// applied here; they are decorations (field.ts). Positions throughout are USV; `Array.from` iterates by
|
|
9
9
|
// code point so an astral char is one unit, never a surrogate half.
|
|
10
10
|
import { DOMSerializer } from 'prosemirror-model';
|
|
11
|
-
import { ISLAND_SLOT } from './islands.js';
|
|
11
|
+
import { ISLAND_SLOT, isBlockIsland } from './islands.js';
|
|
12
12
|
import { descriptorOf, markKey, pmMarkFromContent } from './marks.js';
|
|
13
|
-
import {
|
|
13
|
+
import { core } from '../lifecycle.js';
|
|
14
|
+
import { hasMarks, isBlockSchema, isInlineSchema, takesLineBreak } from './schema.js';
|
|
14
15
|
/** Code points of `s` (USV units): the iteration granularity the content speaks. */
|
|
15
16
|
export function codePoints(s) {
|
|
16
17
|
return Array.from(s);
|
|
@@ -54,6 +55,9 @@ export function decode(rt, schema) {
|
|
|
54
55
|
if (isInlineSchema(schema)) {
|
|
55
56
|
return decodeInline(rt, schema, lineTexts, starts, marks, cursor);
|
|
56
57
|
}
|
|
58
|
+
if (!isBlockSchema(schema)) {
|
|
59
|
+
return decodePlain(rt, schema, lineTexts, starts, marks, cursor);
|
|
60
|
+
}
|
|
57
61
|
// Build leaves (fold `continues` runs), then nest by container prefix.
|
|
58
62
|
const leaves = [];
|
|
59
63
|
for (let i = 0; i < rt.lines.length; i++) {
|
|
@@ -69,13 +73,46 @@ export function decode(rt, schema) {
|
|
|
69
73
|
const blocks = groupBlocks(schema, leaves, 0, marks, cursor);
|
|
70
74
|
return schema.nodes.doc.create(null, blocks.length ? blocks : schema.nodes.paragraph.create());
|
|
71
75
|
}
|
|
72
|
-
/**
|
|
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). */
|
|
81
|
+
export function fitsInline(rt) {
|
|
82
|
+
if (core().isInline(rt))
|
|
83
|
+
return true;
|
|
84
|
+
const plain = (line) => line.kind === 'para' && line.containers.length === 0;
|
|
85
|
+
return (rt.islands.length === 0 &&
|
|
86
|
+
rt.lines.length === 2 &&
|
|
87
|
+
rt.lines.every(plain) &&
|
|
88
|
+
!rt.lines[1].continues &&
|
|
89
|
+
rt.text.endsWith('\n'));
|
|
90
|
+
}
|
|
91
|
+
/** Whether `rt` decodes under `plainSchema` losing nothing but its marks, which the
|
|
92
|
+
* next commit heals (CODEC §Inline mode): upstream's `isPlain`, asked of the content
|
|
93
|
+
* with them set aside. */
|
|
94
|
+
export function fitsPlain(rt) {
|
|
95
|
+
return core().isPlain({ ...rt, marks: [] });
|
|
96
|
+
}
|
|
97
|
+
/** Whether `rt` decodes under the schema a leaf so declared mounts, losing nothing its
|
|
98
|
+
* first commit would store: {@link fitsInline} narrowed, {@link fitsPlain} plain, and
|
|
99
|
+
* anything on the block schema. What a leaf held over the rest reads. */
|
|
100
|
+
export function fitsLeaf(rt, leaf) {
|
|
101
|
+
if (leaf.inline)
|
|
102
|
+
return fitsInline(rt);
|
|
103
|
+
return !leaf.plaintext || fitsPlain(rt);
|
|
104
|
+
}
|
|
105
|
+
/** Inline decode: one paragraph, containers and islands stripped. */
|
|
73
106
|
function decodeInline(rt, schema, lineTexts, starts, marks, cursor) {
|
|
74
107
|
// An inline field is single-line; join any stray lines with a space (no
|
|
75
|
-
// hard_break node exists in this schema).
|
|
76
|
-
//
|
|
108
|
+
// hard_break node exists in this schema). The empty line a trailing `\n` opens is
|
|
109
|
+
// dropped rather than joined, so a first edit stores no space nobody typed. Islands
|
|
110
|
+
// are not representable inline: the slot char is dropped and its entry skipped.
|
|
77
111
|
const inline = [];
|
|
112
|
+
const last = lineTexts.length - 1;
|
|
78
113
|
for (let i = 0; i < lineTexts.length; i++) {
|
|
114
|
+
if (i > 0 && i === last && lineTexts[i] === '')
|
|
115
|
+
break;
|
|
79
116
|
if (i > 0)
|
|
80
117
|
inline.push(schema.text(' '));
|
|
81
118
|
inline.push(...buildInline(schema, lineTexts[i], starts[i], marks, cursor, true));
|
|
@@ -83,6 +120,22 @@ function decodeInline(rt, schema, lineTexts, starts, marks, cursor) {
|
|
|
83
120
|
const para = schema.nodes.paragraph.create(null, inline);
|
|
84
121
|
return schema.nodes.doc.create(null, para);
|
|
85
122
|
}
|
|
123
|
+
/** Plain decode: a `continues` line is a `hard_break` in the paragraph above and every
|
|
124
|
+
* other line opens one. A line's kind and containers and every island have no node
|
|
125
|
+
* here and are dropped, so a content {@link fitsPlain} refuses opens flattened. */
|
|
126
|
+
function decodePlain(rt, schema, lineTexts, starts, marks, cursor) {
|
|
127
|
+
const paras = [];
|
|
128
|
+
for (let i = 0; i < rt.lines.length; i++) {
|
|
129
|
+
const inline = buildInline(schema, lineTexts[i] ?? '', starts[i], marks, cursor, true);
|
|
130
|
+
const open = paras[paras.length - 1];
|
|
131
|
+
if (rt.lines[i].continues && open)
|
|
132
|
+
open.push(schema.nodes.hard_break.create(), ...inline);
|
|
133
|
+
else
|
|
134
|
+
paras.push(inline);
|
|
135
|
+
}
|
|
136
|
+
const blocks = paras.map((inline) => schema.nodes.paragraph.create(null, inline));
|
|
137
|
+
return schema.nodes.doc.create(null, blocks.length ? blocks : schema.nodes.paragraph.create());
|
|
138
|
+
}
|
|
86
139
|
/** Nest a run of leaves that share a `depth`-length container prefix into blocks. */
|
|
87
140
|
function groupBlocks(schema, leaves, depth, marks, cursor) {
|
|
88
141
|
const out = [];
|
|
@@ -177,10 +230,6 @@ function makeLeaf(schema, leaf, marks, cursor) {
|
|
|
177
230
|
switch (line.kind) {
|
|
178
231
|
case 'rule':
|
|
179
232
|
return schema.nodes.horizontal_rule.create();
|
|
180
|
-
case 'island': {
|
|
181
|
-
const attrs = islandAttrs(cursor);
|
|
182
|
-
return attrs ? schema.nodes.island_block.create(attrs) : schema.nodes.paragraph.create();
|
|
183
|
-
}
|
|
184
233
|
case 'code': {
|
|
185
234
|
// One code_block: the segments' texts joined by literal `\n`, no marks.
|
|
186
235
|
const text = leaf.segments.map((s) => s.text).join('\n');
|
|
@@ -189,6 +238,8 @@ function makeLeaf(schema, leaf, marks, cursor) {
|
|
|
189
238
|
}
|
|
190
239
|
case 'para':
|
|
191
240
|
case 'heading': {
|
|
241
|
+
if (line.kind === 'para' && blockIslandLine(leaf, cursor))
|
|
242
|
+
return schema.nodes.island_block.create(islandAttrs(cursor));
|
|
192
243
|
// Inline content, the segment boundary a `hard_break` in a paragraph and a space
|
|
193
244
|
// in a heading, which takes none (`schema.ts` §`takesLineBreak`). A stored heading
|
|
194
245
|
// carries no continuation — the store clears one after a block of a single line —
|
|
@@ -210,15 +261,23 @@ function makeLeaf(schema, leaf, marks, cursor) {
|
|
|
210
261
|
}
|
|
211
262
|
}
|
|
212
263
|
}
|
|
213
|
-
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
264
|
+
/** Whether a `para` leaf is one block island: a single line holding its slot alone,
|
|
265
|
+
* backed by an island whose markup is a block. The island is the one fact; the line
|
|
266
|
+
* carries no kind of its own for it. */
|
|
267
|
+
function blockIslandLine(leaf, cursor) {
|
|
268
|
+
if (leaf.segments.length !== 1 || leaf.segments[0].text !== ISLAND_SLOT)
|
|
269
|
+
return false;
|
|
270
|
+
const isl = cursor.rt.islands[cursor.i];
|
|
271
|
+
return isl != null && isBlockIsland(isl.type);
|
|
272
|
+
}
|
|
273
|
+
/** Consume the next island entry as PM node attrs (text-order matched to slots): the
|
|
274
|
+
* whole entry, which an island edit has to write back (`islands.ts`). `null` where a
|
|
275
|
+
* slot has no entry behind it, which is a malformed content: the slot draws nothing
|
|
276
|
+
* and its line an empty paragraph, so the decode stays total without minting a node
|
|
277
|
+
* whose `type` the vocabulary would refuse on the way back. */
|
|
219
278
|
function islandAttrs(cursor) {
|
|
220
279
|
const isl = cursor.rt.islands[cursor.i++];
|
|
221
|
-
return isl ? { id: isl.id, islandType: isl.type, props: isl.props
|
|
280
|
+
return isl ? { id: isl.id, islandType: isl.type, props: isl.props } : null;
|
|
222
281
|
}
|
|
223
282
|
/**
|
|
224
283
|
* Inline nodes for one segment: split text into maximal runs of a constant mark
|
|
@@ -104,7 +104,7 @@ function scanBlock(acc, node, nodePos, containers, instance) {
|
|
|
104
104
|
acc.lastContentEndPm = nodePos + 1;
|
|
105
105
|
break;
|
|
106
106
|
case 'island_block':
|
|
107
|
-
beginLine(acc, nodePos, containers, { kind: '
|
|
107
|
+
beginLine(acc, nodePos, containers, { kind: 'para' });
|
|
108
108
|
acc.runs.push({ kind: 'atom', pmStart: nodePos, usvStart: acc.usvEnd });
|
|
109
109
|
appendText(acc, ISLAND_SLOT);
|
|
110
110
|
acc.islands.push(islandEntryFromNode(node.attrs));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Schema } from 'prosemirror-model';
|
|
2
2
|
import { Plugin } from 'prosemirror-state';
|
|
3
3
|
import { EditorView } from 'prosemirror-view';
|
|
4
4
|
import type { Document, Content, Addr, Quill } from '@quillmark/wasm';
|
|
@@ -14,11 +14,24 @@ export interface CreateFieldOpts {
|
|
|
14
14
|
quill: Quill;
|
|
15
15
|
addr: Addr;
|
|
16
16
|
container: HTMLElement;
|
|
17
|
-
/** Constrained single-textblock schema (a `
|
|
17
|
+
/** Constrained single-textblock schema (a field declaring `inline`). */
|
|
18
18
|
inline?: boolean;
|
|
19
|
-
/**
|
|
20
|
-
* and
|
|
19
|
+
/** A mark-free schema (a `plaintext` field): literal text, no formatting and no
|
|
20
|
+
* anchors, in paragraphs and hard breaks or, with `inline`, one textblock. */
|
|
21
21
|
plaintext?: boolean;
|
|
22
|
+
/** The id of the note the caller draws inside `container` for a held leaf →
|
|
23
|
+
* `aria-describedby` while it holds. */
|
|
24
|
+
heldNoteId?: string;
|
|
25
|
+
/**
|
|
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
|
|
28
|
+
* hold (`fitsLeaf`, upstream's `isInline` and `isPlain`): the decode would join lines
|
|
29
|
+
* and drop containers and islands for the first commit to store. A held leaf draws
|
|
30
|
+
* its content on the block schema, read-only, and commits nothing. Judged of the value
|
|
31
|
+
* each mount and re-hydrate reads, so {@link FieldController.applyExternal} is what
|
|
32
|
+
* re-evaluates it.
|
|
33
|
+
*/
|
|
34
|
+
onHold?(held: boolean): void;
|
|
22
35
|
/** Suppress the markdown-shorthand input rules. */
|
|
23
36
|
noInputRules?: boolean;
|
|
24
37
|
/** The island chrome's wording, read live (per render) so a consumer swapping
|
|
@@ -46,11 +59,28 @@ export interface CreateFieldOpts {
|
|
|
46
59
|
labelledBy?: string;
|
|
47
60
|
/** The parked `description` → `aria-describedby`; announced after the name. */
|
|
48
61
|
describedBy?: string;
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
62
|
+
/**
|
|
63
|
+
* The resolved `default:` where it prints: the content an unset field's leaf holds,
|
|
64
|
+
* marked `data-default` on the editable for the default rung. Nothing is written
|
|
65
|
+
* until an edit, which commits the whole leaf as authored: the default taken, with
|
|
66
|
+
* the edit in it. The initial value; {@link FieldController.setFallback} moves it
|
|
67
|
+
* after mount.
|
|
68
|
+
*/
|
|
69
|
+
fallback?: Content;
|
|
70
|
+
/** Ghost text shown on the empty leaf: the `none` an unset optional field prints, or
|
|
71
|
+
* a body's invitation. The initial value; {@link FieldController.setPlaceholder}
|
|
72
|
+
* moves it after mount. Empty/absent shows no ghost. */
|
|
53
73
|
placeholder?: string;
|
|
74
|
+
/** The placeholder is what the unset field prints, so it goes at the leaf's first
|
|
75
|
+
* edit: the edit answers the field, and emptied the leaf holds an empty answer, which
|
|
76
|
+
* prints empty. Absent, the placeholder is an invitation (a body's) and returns
|
|
77
|
+
* whenever the leaf is empty. */
|
|
78
|
+
placeholderUntilEdit?: boolean;
|
|
79
|
+
/** Ghost text shown on the empty leaf until its first edit: an unset field's
|
|
80
|
+
* `example:`, at rest where there is no placeholder and in the placeholder's stead
|
|
81
|
+
* while the leaf holds the focus. The initial value; {@link FieldController.setExample}
|
|
82
|
+
* moves it after mount. */
|
|
83
|
+
example?: string;
|
|
54
84
|
onFocus?(addr: Addr): void;
|
|
55
85
|
/** Fired with the new USV caret after an edit or a selection move. */
|
|
56
86
|
onCaretMove?(addr: Addr, pos: number): void;
|
|
@@ -60,7 +90,8 @@ export interface CreateFieldOpts {
|
|
|
60
90
|
* move, so a host driving a recompile off it recompiles on every arrow key.
|
|
61
91
|
*
|
|
62
92
|
* Fires for every commit regardless of which branch it took (`applyChange`, the
|
|
63
|
-
* first-edit `overwrite`, the fallback `overwrite
|
|
93
|
+
* first-edit `overwrite`, the fallback `overwrite`, a `plaintext` leaf's typed
|
|
94
|
+
* write) and for an anchor mutation,
|
|
64
95
|
* which changes no text. A commit that failed outright (`commit-lost`) does not
|
|
65
96
|
* fire it: nothing landed.
|
|
66
97
|
*/
|
|
@@ -75,7 +106,8 @@ export interface FieldController {
|
|
|
75
106
|
readonly el: HTMLElement;
|
|
76
107
|
/** Place the caret at USV `pos` (preview onPick → usvToPM → here). */
|
|
77
108
|
setCaret(pos: number): void;
|
|
78
|
-
/** External content change → re-hydrate this leaf (gated by reconcile)
|
|
109
|
+
/** External content change → re-hydrate this leaf (gated by reconcile), and
|
|
110
|
+
* re-evaluate its hold against the value now stored. */
|
|
79
111
|
applyExternal(): void;
|
|
80
112
|
/**
|
|
81
113
|
* Move the empty-leaf ghost after mount: a card retyped to another kind takes
|
|
@@ -86,6 +118,12 @@ export interface FieldController {
|
|
|
86
118
|
* a transaction would fire `onCaretMove` at a moment the caret did not move.
|
|
87
119
|
*/
|
|
88
120
|
setPlaceholder(text: string | undefined): void;
|
|
121
|
+
/** Move the example ghost after mount, the way {@link setPlaceholder} moves the
|
|
122
|
+
* other one. */
|
|
123
|
+
setExample(text: string | undefined): void;
|
|
124
|
+
/** Move the default an unset leaf holds after mount: a retype or a quill swap
|
|
125
|
+
* re-seats an unset leaf on the new kind's default, and leaves a written one be. */
|
|
126
|
+
setFallback(content: Content | undefined): void;
|
|
89
127
|
focus(): void;
|
|
90
128
|
/** The current stored content for this addr (for tests / reconcile). */
|
|
91
129
|
getContent(): Content;
|
|
@@ -146,10 +184,17 @@ export declare function proseAttributes(opts: {
|
|
|
146
184
|
labelledBy?: string;
|
|
147
185
|
describedBy?: string;
|
|
148
186
|
}): Record<string, string> | undefined;
|
|
187
|
+
/** A held leaf's attributes, described by its note as well. A non-editable view is no
|
|
188
|
+
* textbox to assistive tech or to Tab, so a held leaf states both itself. */
|
|
189
|
+
export declare function heldAttributes(opts: {
|
|
190
|
+
label?: string;
|
|
191
|
+
labelledBy?: string;
|
|
192
|
+
describedBy?: string;
|
|
193
|
+
}, noteId: string | undefined): Record<string, string>;
|
|
149
194
|
export declare function createField(opts: CreateFieldOpts): FieldController;
|
|
150
195
|
/**
|
|
151
196
|
* The prose-leaf plugin stack (VISUAL_EDITOR §Surface): shared by
|
|
152
|
-
* {@link createField} and the by-value
|
|
197
|
+
* {@link createField} and the by-value editor (`ProseValue`), so the two
|
|
153
198
|
* never fork the keymap/plugin ordering. History first, then any leaf-specific
|
|
154
199
|
* plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
|
|
155
200
|
* a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
|
|
@@ -158,10 +203,10 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
|
|
|
158
203
|
* shapes: the gap cursor, {@link pastAtomPlugin}, and the island paste pass
|
|
159
204
|
* (`islands.ts`).
|
|
160
205
|
*
|
|
161
|
-
* Every mark-shaped plugin reads the schema rather than a flag: over
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
206
|
+
* Every mark-shaped plugin reads the schema rather than a flag: over either plaintext
|
|
207
|
+
* schema the shorthand rules build nothing (each is guarded on its mark type) and the
|
|
208
|
+
* toggles bind no key, so a `**bold**` keeps the delimiters its author typed without
|
|
209
|
+
* a second rule saying so.
|
|
165
210
|
*/
|
|
166
211
|
export declare function proseLeafPlugins(schema: Schema, opts: {
|
|
167
212
|
inline: boolean;
|
|
@@ -173,5 +218,9 @@ export declare function proseLeafPlugins(schema: Schema, opts: {
|
|
|
173
218
|
* ({@link FieldController.setPlaceholder}), and a re-hydration rebuilds this
|
|
174
219
|
* stack, so the plugin must ask rather than hold. */
|
|
175
220
|
placeholder?: () => string | undefined;
|
|
221
|
+
/** {@link CreateFieldOpts.placeholderUntilEdit}. */
|
|
222
|
+
placeholderUntilEdit?: boolean;
|
|
223
|
+
/** The focused leaf's ghost, read live for the same reason. */
|
|
224
|
+
example?: () => string | undefined;
|
|
176
225
|
afterHistory?: Plugin[];
|
|
177
226
|
}): Plugin[];
|