@quillmark/svelte 0.10.0 → 0.11.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/README.md +1 -1
- package/THEMING.md +4 -2
- package/dist/core/codec/blocks.js +21 -1
- package/dist/core/codec/breaks.d.ts +10 -2
- package/dist/core/codec/breaks.js +65 -9
- package/dist/core/codec/code.d.ts +6 -4
- package/dist/core/codec/code.js +7 -4
- package/dist/core/codec/decode.js +91 -62
- package/dist/core/codec/encode.d.ts +5 -8
- package/dist/core/codec/encode.js +55 -85
- package/dist/core/codec/field.js +43 -24
- package/dist/core/codec/index.d.ts +2 -1
- package/dist/core/codec/index.js +6 -3
- package/dist/core/codec/islands.d.ts +13 -9
- package/dist/core/codec/islands.js +75 -49
- package/dist/core/codec/keymap.js +2 -1
- package/dist/core/codec/marks.d.ts +17 -13
- package/dist/core/codec/marks.js +39 -42
- package/dist/core/codec/positions.d.ts +4 -1
- package/dist/core/codec/positions.js +5 -1
- package/dist/core/codec/prose.css +10 -0
- package/dist/core/codec/reconcile.d.ts +6 -0
- package/dist/core/codec/reconcile.js +19 -0
- package/dist/core/codec/schema.d.ts +5 -8
- package/dist/core/codec/schema.js +30 -156
- package/dist/core/codec/table-view.js +77 -26
- package/dist/core/codec/table.d.ts +6 -5
- package/dist/core/codec/table.js +14 -31
- package/dist/core/codec/urls.d.ts +20 -0
- package/dist/core/codec/urls.js +45 -0
- package/dist/preset/recipes.css +1 -1
- package/dist/preset/scale.css +7 -4
- package/dist/preview/Preview.svelte +6 -0
- package/dist/preview/bridge.d.ts +8 -1
- package/dist/preview/bridge.js +31 -25
- package/dist/preview/controller.js +13 -13
- package/dist/preview/geometry.d.ts +12 -1
- package/dist/preview/geometry.js +14 -1
- package/dist/preview/paint.js +6 -4
- package/dist/preview/strings.d.ts +0 -2
- package/dist/preview/strings.js +5 -6
- package/dist/visual/ArrayField.svelte +11 -13
- package/dist/visual/Card.svelte +5 -8
- package/dist/visual/DateField.svelte +2 -2
- package/dist/visual/Field.svelte +7 -3
- package/dist/visual/FieldHint.svelte +1 -1
- package/dist/visual/ObjectField.svelte +6 -1
- package/dist/visual/VisualEditor.svelte +1 -1
- package/dist/visual/VisualEditorInner.svelte +73 -18
- package/dist/visual/controls.css +8 -9
- package/dist/visual/ext.d.ts +12 -13
- package/dist/visual/ext.js +22 -19
- package/dist/visual/index.js +2 -3
- package/dist/visual/links.d.ts +4 -0
- package/dist/visual/links.js +15 -4
- package/dist/visual/strings.d.ts +7 -4
- package/dist/visual/strings.js +1 -0
- package/dist/visual/structure.d.ts +9 -1
- package/dist/visual/structure.js +26 -4
- package/package.json +2 -2
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { isAnchorMark } from '@quillmark/wasm';
|
|
2
1
|
import { core } from '../lifecycle.js';
|
|
3
2
|
import { codePoints, usvLength } from './decode.js';
|
|
4
3
|
import { ISLAND_SLOT, islandEntryFromNode } from './islands.js';
|
|
5
|
-
import { contentDescriptorFromPM, descriptorOf, markKey } from './marks.js';
|
|
6
|
-
import { valueEqual } from './reconcile.js';
|
|
4
|
+
import { anchorsFromContent, contentDescriptorFromPM, descriptorOf, markKey } from './marks.js';
|
|
5
|
+
import { canonicalJson, valueEqual } from './reconcile.js';
|
|
7
6
|
/** Append `s` to the accumulated text, advancing the running USV length. */
|
|
8
7
|
function appendText(acc, s) {
|
|
9
8
|
acc.text += s;
|
|
@@ -21,7 +20,6 @@ export function scanDoc(doc) {
|
|
|
21
20
|
marks: [],
|
|
22
21
|
islands: [],
|
|
23
22
|
runs: [],
|
|
24
|
-
pmEnd: doc.content.size,
|
|
25
23
|
usvEnd: 0,
|
|
26
24
|
rawMarks: [],
|
|
27
25
|
lastContentEndPm: 0
|
|
@@ -30,10 +28,15 @@ export function scanDoc(doc) {
|
|
|
30
28
|
acc.marks = mergeMarks(acc.rawMarks);
|
|
31
29
|
return acc;
|
|
32
30
|
}
|
|
31
|
+
/** The content projection a scan carries (drops the position runs): what a caller
|
|
32
|
+
* holding a `Scan` reads instead of walking the document a second time. */
|
|
33
|
+
export function scanContent(s) {
|
|
34
|
+
const { text, lines, marks, islands } = s;
|
|
35
|
+
return { text, lines, marks, islands };
|
|
36
|
+
}
|
|
33
37
|
/** `pmToContent`: the pure inverse of decode (drops the position runs). */
|
|
34
38
|
export function pmToContent(doc) {
|
|
35
|
-
|
|
36
|
-
return { text, lines, marks, islands };
|
|
39
|
+
return scanContent(scanDoc(doc));
|
|
37
40
|
}
|
|
38
41
|
/** Walk a block-content fragment; `contentStart` is the PM pos before its first child.
|
|
39
42
|
* Two container siblings of one run shape read as a single container — contiguity plus
|
|
@@ -54,7 +57,7 @@ function scanBlocks(acc, parent, contentStart, containers) {
|
|
|
54
57
|
}
|
|
55
58
|
/** A block's container run shape, `ordinal` and `instance` aside; null where it opens
|
|
56
59
|
* no container. A list's `start` is not in it — the store welds two runs differing only
|
|
57
|
-
* there
|
|
60
|
+
* there. */
|
|
58
61
|
function runShape(node) {
|
|
59
62
|
switch (node.type.name) {
|
|
60
63
|
case 'blockquote':
|
|
@@ -63,8 +66,6 @@ function runShape(node) {
|
|
|
63
66
|
return 'list\u0000bullet';
|
|
64
67
|
case 'ordered_list':
|
|
65
68
|
return 'list\u0000ordered';
|
|
66
|
-
case 'unknown_container':
|
|
67
|
-
return `unknown\u0000${node.attrs.container}\u0000${JSON.stringify(node.attrs.attrs)}`;
|
|
68
69
|
default:
|
|
69
70
|
return null;
|
|
70
71
|
}
|
|
@@ -85,7 +86,7 @@ function scanBlock(acc, node, nodePos, containers, instance) {
|
|
|
85
86
|
}
|
|
86
87
|
case 'code_block': {
|
|
87
88
|
const lang = node.attrs.lang ?? undefined;
|
|
88
|
-
const kind = lang != null ? { kind: 'code', lang } : { kind: 'code' };
|
|
89
|
+
const kind = lang != null ? { kind: 'code', attrs: { lang } } : { kind: 'code' };
|
|
89
90
|
beginLine(acc, contentStart, containers, kind);
|
|
90
91
|
const codeText = node.textContent;
|
|
91
92
|
if (codeText.length)
|
|
@@ -110,12 +111,9 @@ function scanBlock(acc, node, nodePos, containers, instance) {
|
|
|
110
111
|
acc.lastContentEndPm = nodePos + 1;
|
|
111
112
|
break;
|
|
112
113
|
case 'blockquote':
|
|
113
|
-
scanBlocks(acc, node, contentStart, [...containers, { container: 'quote', instance }]);
|
|
114
|
-
break;
|
|
115
|
-
case 'unknown_container':
|
|
116
114
|
scanBlocks(acc, node, contentStart, [
|
|
117
115
|
...containers,
|
|
118
|
-
{ container:
|
|
116
|
+
withInstance({ container: 'quote' }, instance)
|
|
119
117
|
]);
|
|
120
118
|
break;
|
|
121
119
|
case 'bullet_list':
|
|
@@ -124,13 +122,7 @@ function scanBlock(acc, node, nodePos, containers, instance) {
|
|
|
124
122
|
const start = ordered ? node.attrs.start : 1;
|
|
125
123
|
let itemPos = contentStart;
|
|
126
124
|
node.forEach((item, _off, index) => {
|
|
127
|
-
const container = {
|
|
128
|
-
container: 'list_item',
|
|
129
|
-
ordered,
|
|
130
|
-
start,
|
|
131
|
-
ordinal: index,
|
|
132
|
-
instance
|
|
133
|
-
};
|
|
125
|
+
const container = withInstance({ container: 'list_item', attrs: { ordered, start, ordinal: index } }, instance);
|
|
134
126
|
scanBlocks(acc, item, itemPos + 1, [...containers, container]);
|
|
135
127
|
itemPos += item.nodeSize;
|
|
136
128
|
});
|
|
@@ -142,13 +134,17 @@ function scanBlock(acc, node, nodePos, containers, instance) {
|
|
|
142
134
|
acc.lastContentEndPm = contentStart;
|
|
143
135
|
}
|
|
144
136
|
}
|
|
145
|
-
/** A textblock's line kind: a heading's `level`, or (for a paragraph) `para
|
|
146
|
-
* the unknown kind it carries (schema.ts) re-emitted verbatim. */
|
|
137
|
+
/** A textblock's line kind: a heading's `level`, or (for a paragraph) `para`. */
|
|
147
138
|
function textblockKind(node) {
|
|
148
139
|
if (node.type.name === 'heading')
|
|
149
|
-
return { kind: 'heading', level: node.attrs.level };
|
|
150
|
-
|
|
151
|
-
|
|
140
|
+
return { kind: 'heading', attrs: { level: node.attrs.level } };
|
|
141
|
+
return { kind: 'para' };
|
|
142
|
+
}
|
|
143
|
+
/** A container with its run `instance`, spelled only where it is not the zero a
|
|
144
|
+
* canonical read omits: the projection is compared against such a read
|
|
145
|
+
* (`lineMetaEqual`), so the two have to spell one container one way. */
|
|
146
|
+
function withInstance(container, instance) {
|
|
147
|
+
return instance === 0 ? container : { ...container, instance };
|
|
152
148
|
}
|
|
153
149
|
/** Open a new content line: emit the boundary `\n` (except the first line) + record. */
|
|
154
150
|
function beginLine(acc, thisContentStart, containers, kind) {
|
|
@@ -201,11 +197,7 @@ function emitText(acc, s, pmStart, marks) {
|
|
|
201
197
|
acc.runs.push({ kind: 'text', pmStart, usvStart, s });
|
|
202
198
|
appendText(acc, s);
|
|
203
199
|
for (const mark of marks) {
|
|
204
|
-
acc.rawMarks.push({
|
|
205
|
-
start: usvStart,
|
|
206
|
-
end: acc.usvEnd,
|
|
207
|
-
...contentDescriptorFromPM(mark)
|
|
208
|
-
});
|
|
200
|
+
acc.rawMarks.push({ start: usvStart, end: acc.usvEnd, ...contentDescriptorFromPM(mark) });
|
|
209
201
|
}
|
|
210
202
|
}
|
|
211
203
|
/**
|
|
@@ -216,7 +208,7 @@ function emitText(acc, s, pmStart, marks) {
|
|
|
216
208
|
*/
|
|
217
209
|
function mergeMarks(raw) {
|
|
218
210
|
const out = [];
|
|
219
|
-
for (const g of groupFormatting(raw
|
|
211
|
+
for (const g of groupFormatting(raw).values()) {
|
|
220
212
|
for (const iv of union(g.intervals))
|
|
221
213
|
out.push({ ...iv, ...g.descriptor });
|
|
222
214
|
}
|
|
@@ -231,9 +223,10 @@ export function contentEdit(oldRt, newRt) {
|
|
|
231
223
|
/** Lower an edit to a `ChangeBundle`: the old→new content diff, as ops. */
|
|
232
224
|
export function lower(edit, opts = {}) {
|
|
233
225
|
const { oldRt, newRt } = edit;
|
|
234
|
-
const { delta, islandOps
|
|
226
|
+
const { delta, islandOps } = splitIslands(edit);
|
|
235
227
|
const lineOps = diffLines(oldRt, newRt, edit.delta);
|
|
236
|
-
|
|
228
|
+
// The text-moving channels first: the mark diff reads them back off the bundle,
|
|
229
|
+
// so what it rebases against is what `applyChange` will be handed.
|
|
237
230
|
const bundle = {};
|
|
238
231
|
if (delta)
|
|
239
232
|
bundle.delta = delta;
|
|
@@ -241,6 +234,7 @@ export function lower(edit, opts = {}) {
|
|
|
241
234
|
bundle.islandOps = islandOps;
|
|
242
235
|
if (lineOps.length)
|
|
243
236
|
bundle.lineOps = lineOps;
|
|
237
|
+
const markOps = diffMarks(oldRt, newRt, bundle, opts);
|
|
244
238
|
if (markOps.length)
|
|
245
239
|
bundle.markOps = markOps;
|
|
246
240
|
return bundle;
|
|
@@ -321,10 +315,8 @@ function diffLines(oldRt, newRt, delta) {
|
|
|
321
315
|
return ops;
|
|
322
316
|
}
|
|
323
317
|
/** A line minus its `containers` / `continues` envelope: exactly `setKind`'s
|
|
324
|
-
* payload, which the boundary names `ContentLineKind`. Lifting it whole is what
|
|
325
|
-
*
|
|
326
|
-
* have to guess at the unknown arm's payload and would drift on every arm
|
|
327
|
-
* upstream adds. */
|
|
318
|
+
* payload, which the boundary names `ContentLineKind`. Lifting it whole is what keeps
|
|
319
|
+
* this arm-agnostic, so an arm added upstream reaches the op by construction. */
|
|
328
320
|
function kindPart(l) {
|
|
329
321
|
const { containers: _containers, continues: _continues, ...kind } = l;
|
|
330
322
|
return kind;
|
|
@@ -340,15 +332,16 @@ function lineMetaEqual(a, b) {
|
|
|
340
332
|
return false;
|
|
341
333
|
if (kindKey(a[i]) !== kindKey(b[i]))
|
|
342
334
|
return false;
|
|
343
|
-
if (
|
|
335
|
+
if (!valueEqual(a[i].containers, b[i].containers))
|
|
344
336
|
return false;
|
|
345
337
|
}
|
|
346
338
|
return true;
|
|
347
339
|
}
|
|
348
|
-
/** A line kind's comparison key: key-order-insensitive, because the two
|
|
349
|
-
* from different producers (a WASM read and this scan)
|
|
340
|
+
/** A line kind's comparison key: key-order-insensitive at every depth, because the two
|
|
341
|
+
* sides come from different producers (a WASM read and this scan) and a kind carries a
|
|
342
|
+
* nested `attrs` bag. */
|
|
350
343
|
function kindKey(l) {
|
|
351
|
-
return
|
|
344
|
+
return canonicalJson(kindPart(l));
|
|
352
345
|
}
|
|
353
346
|
/**
|
|
354
347
|
* Split the edit's islands off the text splice. `applyChange` throws
|
|
@@ -373,12 +366,12 @@ function splitIslands(edit) {
|
|
|
373
366
|
const { start, end } = insertedRegion(delta);
|
|
374
367
|
const oldById = new Map(oldRt.islands.map((isl) => [isl.id, isl]));
|
|
375
368
|
const islandOps = [];
|
|
376
|
-
|
|
369
|
+
let inserts = 0;
|
|
377
370
|
newRt.islands.forEach((isl, i) => {
|
|
378
371
|
const at = slots[i];
|
|
379
372
|
if (at !== undefined && at >= start && at < end) {
|
|
380
373
|
islandOps.push({ op: 'insert', at, ...isl });
|
|
381
|
-
|
|
374
|
+
inserts++;
|
|
382
375
|
}
|
|
383
376
|
else {
|
|
384
377
|
const before = oldById.get(isl.id);
|
|
@@ -386,11 +379,7 @@ function splitIslands(edit) {
|
|
|
386
379
|
islandOps.push({ op: 'set', ...isl });
|
|
387
380
|
}
|
|
388
381
|
});
|
|
389
|
-
return {
|
|
390
|
-
delta: insertedAt.length && delta ? withoutSlots(delta) : delta,
|
|
391
|
-
islandOps,
|
|
392
|
-
insertedAt
|
|
393
|
-
};
|
|
382
|
+
return { delta: inserts && delta ? withoutSlots(delta) : delta, islandOps };
|
|
394
383
|
}
|
|
395
384
|
/** The USV positions of `text`'s island slots, in document order: the i-th is the
|
|
396
385
|
* i-th island's, since the scan appends an entry per slot it writes. */
|
|
@@ -437,33 +426,17 @@ function withoutSlots(delta) {
|
|
|
437
426
|
}
|
|
438
427
|
return splices ? { ops } : undefined;
|
|
439
428
|
}
|
|
440
|
-
/**
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
|
|
444
|
-
* `at`s apply left to right, so each comparison reads the running position.
|
|
445
|
-
*/
|
|
446
|
-
function shiftPastIslands(pos, insertedAt, assoc) {
|
|
447
|
-
let p = pos;
|
|
448
|
-
for (const at of insertedAt)
|
|
449
|
-
if (assoc === 'after' ? p >= at : p > at)
|
|
450
|
-
p++;
|
|
451
|
-
return p;
|
|
452
|
-
}
|
|
453
|
-
/** Formatting/unknown marks → add/remove ops; anchors → add/removeAnchor by id. */
|
|
454
|
-
function diffMarks(oldRt, newRt, delta, insertedAt, opts) {
|
|
429
|
+
/** Formatting marks → add/remove ops; anchors → add/removeAnchor by id. A `MarkOp`'s
|
|
430
|
+
* `add` / `remove` is a `ContentMark` under an op, so a descriptor spreads in whole and
|
|
431
|
+
* the wire's `attrs` spelling is the one a checker admits. */
|
|
432
|
+
function diffMarks(oldRt, newRt, moved, opts) {
|
|
455
433
|
const ops = [];
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
// as `applyChange` does internally (start assoc `after`, end assoc `before`).
|
|
460
|
-
const rebase = (pos, assoc) => shiftPastIslands(delta ? mapPos(delta, pos, assoc) : pos, insertedAt, assoc);
|
|
434
|
+
// The store's answer for where `moved`'s text channels leave the field's marks;
|
|
435
|
+
// `markOps` are the difference from it.
|
|
436
|
+
const rebased = core().mapMarks(oldRt, moved);
|
|
461
437
|
// Group both sides by descriptor key (excluding anchors).
|
|
462
|
-
const oldGroups = groupFormatting(
|
|
463
|
-
|
|
464
|
-
end: rebase(m.end, 'before')
|
|
465
|
-
}));
|
|
466
|
-
const newGroups = groupFormatting(newRt.marks, (m) => ({ start: m.start, end: m.end }));
|
|
438
|
+
const oldGroups = groupFormatting(rebased);
|
|
439
|
+
const newGroups = groupFormatting(newRt.marks);
|
|
467
440
|
const keys = new Set([...oldGroups.keys(), ...newGroups.keys()]);
|
|
468
441
|
for (const key of keys) {
|
|
469
442
|
const oldG = oldGroups.get(key);
|
|
@@ -477,17 +450,14 @@ function diffMarks(oldRt, newRt, delta, insertedAt, opts) {
|
|
|
477
450
|
ops.push({ op: 'add', start: iv.start, end: iv.end, ...descriptor });
|
|
478
451
|
}
|
|
479
452
|
// Anchors: diff the decoration sets by id (positions already in final coords).
|
|
480
|
-
if (opts.
|
|
481
|
-
const oldA = new Map((
|
|
482
|
-
|
|
483
|
-
shiftPastIslands(delta ? mapPos(delta, a.pos, 'after') : a.pos, insertedAt, 'before')
|
|
484
|
-
]));
|
|
485
|
-
const newA = new Map((opts.newAnchors ?? []).map((a) => [a.id, a.pos]));
|
|
453
|
+
if (opts.newAnchors) {
|
|
454
|
+
const oldA = new Map(anchorsFromContent({ marks: rebased }).map((a) => [a.id, a.pos]));
|
|
455
|
+
const newA = new Map(opts.newAnchors.map((a) => [a.id, a.pos]));
|
|
486
456
|
for (const [id, pos] of newA) {
|
|
487
457
|
if (!oldA.has(id) || oldA.get(id) !== pos) {
|
|
488
458
|
if (oldA.has(id))
|
|
489
459
|
ops.push({ op: 'removeAnchor', id });
|
|
490
|
-
ops.push({ op: 'add', start: pos, end: pos, type: 'anchor', id });
|
|
460
|
+
ops.push({ op: 'add', start: pos, end: pos, type: 'anchor', attrs: { id } });
|
|
491
461
|
}
|
|
492
462
|
}
|
|
493
463
|
for (const id of oldA.keys())
|
|
@@ -496,11 +466,11 @@ function diffMarks(oldRt, newRt, delta, insertedAt, opts) {
|
|
|
496
466
|
}
|
|
497
467
|
return ops;
|
|
498
468
|
}
|
|
499
|
-
/** Group non-anchor marks by descriptor key
|
|
500
|
-
function groupFormatting(marks
|
|
469
|
+
/** Group non-anchor marks by descriptor key. */
|
|
470
|
+
function groupFormatting(marks) {
|
|
501
471
|
const groups = new Map();
|
|
502
472
|
for (const m of marks) {
|
|
503
|
-
if (
|
|
473
|
+
if (m.type === 'anchor')
|
|
504
474
|
continue;
|
|
505
475
|
const descriptor = descriptorOf(m);
|
|
506
476
|
const key = markKey(descriptor);
|
|
@@ -509,7 +479,7 @@ function groupFormatting(marks, toInterval) {
|
|
|
509
479
|
g = { descriptor, intervals: [] };
|
|
510
480
|
groups.set(key, g);
|
|
511
481
|
}
|
|
512
|
-
g.intervals.push(
|
|
482
|
+
g.intervals.push({ start: m.start, end: m.end });
|
|
513
483
|
}
|
|
514
484
|
return groups;
|
|
515
485
|
}
|
package/dist/core/codec/field.js
CHANGED
|
@@ -20,8 +20,8 @@ import { EditorState, NodeSelection, Plugin, PluginKey, Selection, TextSelection
|
|
|
20
20
|
import { Decoration, DecorationSet, EditorView } from 'prosemirror-view';
|
|
21
21
|
import { reportError, errorMessage } from '../errors.js';
|
|
22
22
|
import { decode } from './decode.js';
|
|
23
|
-
import { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
|
|
24
|
-
import { lower, pmToContent, contentEdit } from './encode.js';
|
|
23
|
+
import { usvToPM, pmToUsv, buildLineIndex, lineIndexOf } from './positions.js';
|
|
24
|
+
import { lower, pmToContent, scanContent, scanDoc, contentEdit } from './encode.js';
|
|
25
25
|
import { islandPastePlugin } from './islands.js';
|
|
26
26
|
import { anchorsFromContent } from './marks.js';
|
|
27
27
|
import { createReconciler } from './reconcile.js';
|
|
@@ -33,7 +33,11 @@ import { DEFAULT_TABLE_STRINGS, tableNodeView } from './table-view.js';
|
|
|
33
33
|
import { focusSlashItem, runSlashItem, slashPlugin } from './slash.js';
|
|
34
34
|
const anchorKey = new PluginKey('quill-anchors');
|
|
35
35
|
/** Plugin-held anchor positions (PM coords), mapped through every edit's `StepMap`
|
|
36
|
-
* and mutated by an {@link AnchorEdit} meta (an insert/remove at a selection).
|
|
36
|
+
* and mutated by an {@link AnchorEdit} meta (an insert/remove at a selection).
|
|
37
|
+
*
|
|
38
|
+
* The `-1` is the store's rule for a point, not PM's default: `applyChange` rebases a
|
|
39
|
+
* zero-width mark `before`, so text typed at an anchor's own position lands after it
|
|
40
|
+
* and the mark diff has no op to spend reconciling this set with the store. */
|
|
37
41
|
function anchorPlugin(seed) {
|
|
38
42
|
return new Plugin({
|
|
39
43
|
key: anchorKey,
|
|
@@ -41,7 +45,7 @@ function anchorPlugin(seed) {
|
|
|
41
45
|
init: () => seed,
|
|
42
46
|
apply: (tr, anchors) => {
|
|
43
47
|
let next = tr.docChanged
|
|
44
|
-
? anchors.map((a) => ({ id: a.id, pos: tr.mapping.map(a.pos) }))
|
|
48
|
+
? anchors.map((a) => ({ id: a.id, pos: tr.mapping.map(a.pos, -1) }))
|
|
45
49
|
: anchors;
|
|
46
50
|
const edit = tr.getMeta(anchorKey);
|
|
47
51
|
if (edit?.op === 'add')
|
|
@@ -134,8 +138,9 @@ export function createField(opts) {
|
|
|
134
138
|
// The menu's report channel, and the whole of what a leaf holds of it: the
|
|
135
139
|
// vocabulary is the codec's own constant (`slash.ts`), not a wording to derive.
|
|
136
140
|
const slash = inline ? undefined : opts.onSlash;
|
|
137
|
-
const
|
|
138
|
-
|
|
141
|
+
const seeded = buildState(reconciler.last);
|
|
142
|
+
const state = seeded.state;
|
|
143
|
+
index = seeded.index;
|
|
139
144
|
// The clearance PM's reveal keeps around the caret, in this leaf's own line box.
|
|
140
145
|
// Its default is 5px, which is a caret visible and unusable: the next line typed
|
|
141
146
|
// lands past the fold, the reason the preview's own scroll clears a target by the
|
|
@@ -173,13 +178,19 @@ export function createField(opts) {
|
|
|
173
178
|
const oldRt = reconciler.last;
|
|
174
179
|
const next = view.state.apply(tr);
|
|
175
180
|
view.updateState(next); // (a) optimistic
|
|
176
|
-
index
|
|
181
|
+
// One walk per structural change: the index and the projection the commit
|
|
182
|
+
// diffs both come off it. A selection-only transaction leaves the document
|
|
183
|
+
// it was built from, so the index over it still holds.
|
|
184
|
+
const scan = tr.docChanged ? scanDoc(next.doc) : undefined;
|
|
185
|
+
if (scan)
|
|
186
|
+
index = lineIndexOf(scan);
|
|
177
187
|
// Commit a content edit or an anchor mutation. An anchor insert/remove is
|
|
178
188
|
// zero-width, so `docChanged` is false: the `anchorKey` meta is what
|
|
179
189
|
// routes it through the same commit path (the diff emits the anchor op).
|
|
180
190
|
if (tr.docChanged || tr.getMeta(anchorKey)) {
|
|
181
191
|
// (d) the change signal, only for what actually landed.
|
|
182
|
-
|
|
192
|
+
const newRt = scan ? scanContent(scan) : pmToContent(next.doc);
|
|
193
|
+
if (commitEdit(oldRt, newRt))
|
|
183
194
|
opts.onChange?.(addr);
|
|
184
195
|
}
|
|
185
196
|
// The caret, for both structural and selection-only changes: a host
|
|
@@ -195,6 +206,8 @@ export function createField(opts) {
|
|
|
195
206
|
}
|
|
196
207
|
}
|
|
197
208
|
});
|
|
209
|
+
/** The state, and the index over the document it was built from: the seed reads one
|
|
210
|
+
* and every caller wants the same one, and `buildLineIndex` is a whole walk. */
|
|
198
211
|
function buildState(rt) {
|
|
199
212
|
const pmDoc = decode(rt, schema);
|
|
200
213
|
const anchors = plaintext ? [] : anchorsFromContent(rt);
|
|
@@ -204,7 +217,7 @@ export function createField(opts) {
|
|
|
204
217
|
id: a.id,
|
|
205
218
|
pos: usvToPM(seedIndex, a.pos)
|
|
206
219
|
}));
|
|
207
|
-
|
|
220
|
+
const built = EditorState.create({
|
|
208
221
|
doc: pmDoc,
|
|
209
222
|
plugins: proseLeafPlugins(schema, {
|
|
210
223
|
inline,
|
|
@@ -216,15 +229,17 @@ export function createField(opts) {
|
|
|
216
229
|
afterHistory: [anchorPlugin(seededAnchors)]
|
|
217
230
|
})
|
|
218
231
|
});
|
|
232
|
+
return { state: built, index: seedIndex };
|
|
219
233
|
}
|
|
220
234
|
// (b)+(c): lower the edit to ops and commit, or `overwrite` a field not yet at
|
|
221
235
|
// content rest. Keep the optimistic PM on throw.
|
|
222
236
|
// Returns whether anything landed: the caller's change signal, which must not
|
|
223
237
|
// be a property of which branch the commit took.
|
|
224
|
-
function commitEdit(oldRt,
|
|
225
|
-
// The
|
|
226
|
-
//
|
|
227
|
-
|
|
238
|
+
function commitEdit(oldRt, newRt) {
|
|
239
|
+
// The text splice, computed once per keystroke: the gate below, both overwrite
|
|
240
|
+
// fallbacks, and `lower` all read this one `edit`. The projection is the
|
|
241
|
+
// caller's, off the same walk that rebuilt the index.
|
|
242
|
+
const edit = contentEdit(oldRt, newRt);
|
|
228
243
|
try {
|
|
229
244
|
// A field not yet at content rest is the only edit that overwrites by choice:
|
|
230
245
|
// `applyChange` throws on an absent declared field (verified) and mis-reads
|
|
@@ -235,11 +250,10 @@ export function createField(opts) {
|
|
|
235
250
|
doc.overwrite(addr, edit.newRt); // brings the field to content rest
|
|
236
251
|
}
|
|
237
252
|
else {
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
doc.applyChange(addr, lower(edit, { oldAnchors, newAnchors }));
|
|
253
|
+
// Post-edit anchors are the plugin's positions (mapped through the tr) as
|
|
254
|
+
// USV; the pre-edit set is `oldRt`'s own, which `lower` reads for itself.
|
|
255
|
+
const newAnchors = plaintext ? [] : readAnchorsUsv();
|
|
256
|
+
doc.applyChange(addr, lower(edit, { newAnchors }));
|
|
243
257
|
}
|
|
244
258
|
reconciler.commit(readLeaf(reader, addr));
|
|
245
259
|
return true;
|
|
@@ -275,8 +289,9 @@ export function createField(opts) {
|
|
|
275
289
|
}
|
|
276
290
|
/** The anchors the plugin currently holds (PM coords); `[]` before the first apply. */
|
|
277
291
|
const heldAnchors = () => anchorKey.getState(view.state) ?? [];
|
|
278
|
-
/**
|
|
279
|
-
|
|
292
|
+
/** The plugin's anchor positions as USV, against `index` — which `dispatchTransaction`
|
|
293
|
+
* rebuilt over the new document before the commit this reads for. */
|
|
294
|
+
function readAnchorsUsv() {
|
|
280
295
|
return heldAnchors().map((a) => ({ id: a.id, pos: pmToUsv(index, a.pos) }));
|
|
281
296
|
}
|
|
282
297
|
const controller = {
|
|
@@ -303,11 +318,11 @@ export function createField(opts) {
|
|
|
303
318
|
return; // own edit / no change
|
|
304
319
|
const caretUsv = pmToUsv(index, view.state.selection.head);
|
|
305
320
|
const fresh = buildState(current);
|
|
306
|
-
view.updateState(fresh);
|
|
307
|
-
index =
|
|
321
|
+
view.updateState(fresh.state);
|
|
322
|
+
index = fresh.index;
|
|
308
323
|
// Best-effort caret continuity across an external change: keep the USV.
|
|
309
324
|
const pm = usvToPM(index, caretUsv);
|
|
310
|
-
view.dispatch(view.state.tr.setSelection(Selection.near(fresh.doc.resolve(pm))));
|
|
325
|
+
view.dispatch(view.state.tr.setSelection(Selection.near(fresh.state.doc.resolve(pm))));
|
|
311
326
|
reconciler.commit(current);
|
|
312
327
|
},
|
|
313
328
|
setPlaceholder(text) {
|
|
@@ -511,8 +526,12 @@ function editorKeymap(schema, inline, slash) {
|
|
|
511
526
|
if (schema.marks.underline)
|
|
512
527
|
map['Mod-u'] = toggleMark(schema.marks.underline);
|
|
513
528
|
if (inline) {
|
|
514
|
-
// One textblock only: swallow Enter so no second block is attempted.
|
|
529
|
+
// One textblock only: swallow Enter so no second block is attempted. One *line*
|
|
530
|
+
// only too (`Content::is_inline`), so Shift-Enter goes with it: the schema
|
|
531
|
+
// declares no break node, and a declined key is the browser's, whose native
|
|
532
|
+
// `<br>` is the one thing that could put a second line in a field that holds one.
|
|
515
533
|
map['Enter'] = () => true;
|
|
534
|
+
map['Shift-Enter'] = () => true;
|
|
516
535
|
}
|
|
517
536
|
else {
|
|
518
537
|
// Each binding falls through to `baseKeymap` when no link claims the key.
|
|
@@ -9,7 +9,8 @@ export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
|
9
9
|
export type { TableChromeStrings } from './table-view.js';
|
|
10
10
|
export { DEFAULT_SLASH_STRINGS } from './slash.js';
|
|
11
11
|
export type { SlashState, SlashStrings } from './slash.js';
|
|
12
|
-
export { blockSchema, inlineSchema, plaintextSchema, hasMarks
|
|
12
|
+
export { blockSchema, inlineSchema, plaintextSchema, hasMarks } from './schema.js';
|
|
13
|
+
export { rendersHref, storableUrl } from './urls.js';
|
|
13
14
|
export { decode, renderContent, usvLength } from './decode.js';
|
|
14
15
|
export { pmToContent, contentEdit, lower } from './encode.js';
|
|
15
16
|
export type { ContentEdit } from './encode.js';
|
package/dist/core/codec/index.js
CHANGED
|
@@ -21,9 +21,12 @@ export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
|
21
21
|
// the pixels (`visual/SlashMenu.svelte` is the only off-barrel caller).
|
|
22
22
|
export { DEFAULT_SLASH_STRINGS } from './slash.js';
|
|
23
23
|
// Schemas (the decode/encode target; the VisualEditor mounts them). `hasMarks` is
|
|
24
|
-
// what a mark surface asks before offering itself over a leaf
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// what a mark surface asks before offering itself over a leaf.
|
|
25
|
+
export { blockSchema, inlineSchema, plaintextSchema, hasMarks } from './schema.js';
|
|
26
|
+
// The url edge: `rendersHref` is the link gate the prompt re-asks on submit, and
|
|
27
|
+
// `storableUrl` the encode the store's own refusal requires of the value it takes
|
|
28
|
+
// (`visual/links.ts`).
|
|
29
|
+
export { rendersHref, storableUrl } from './urls.js';
|
|
27
30
|
// Decode / encode / positions (tests + VisualEditor).
|
|
28
31
|
//
|
|
29
32
|
// The barrel carries what has an off-barrel caller, and nothing else. A symbol
|
|
@@ -8,22 +8,26 @@ export declare const islandBlockSpec: NodeSpec;
|
|
|
8
8
|
/** Inline island node (an image): a `U+FFFC` slot inside a `para` line. */
|
|
9
9
|
export declare const islandInlineSpec: NodeSpec;
|
|
10
10
|
/** A PM island node's attributes: the content entry, spelled the way a node spec
|
|
11
|
-
* spells it (`islandType`, because `type` on a PM node is the node type). `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* spells it (`islandType`, because `type` on a PM node is the node type). `islandType`
|
|
12
|
+
* and `loss` read their sets off the entry rather than restating them, the public entry
|
|
13
|
+
* point naming neither (DOCUMENT_MODEL §Stability seams). */
|
|
14
14
|
export interface IslandNodeAttrs {
|
|
15
15
|
id: string;
|
|
16
|
-
islandType:
|
|
16
|
+
islandType: ContentIsland['type'];
|
|
17
17
|
props: unknown;
|
|
18
18
|
loss: ContentIsland['loss'];
|
|
19
19
|
}
|
|
20
|
-
/** Build a content island entry from a PM island node's attrs (block or inline).
|
|
20
|
+
/** Build a content island entry from a PM island node's attrs (block or inline). The
|
|
21
|
+
* cast pairs a closed `type` with a `props` that crossed the DOM as opaque JSON. The
|
|
22
|
+
* store is lenient about a table's shape — it fills what it can and keeps the rest —
|
|
23
|
+
* so a payload the arm does not hold is stored rather than refused, and `undefined` at
|
|
24
|
+
* its reader is what the surface draws the placeholder for (§`tablePropsOfNode`). */
|
|
21
25
|
export declare function islandEntryFromNode(attrs: IslandNodeAttrs): ContentIsland;
|
|
22
26
|
/** A node's `TableProps`, or `undefined` for any other island and for a payload of the
|
|
23
|
-
* wrong shape: the boundary's own guard over the entry the node carries, so
|
|
24
|
-
*
|
|
25
|
-
* the shape says whether it can read; every caller draws the placeholder
|
|
26
|
-
* `undefined` (`table-view.ts` §`render`). */
|
|
27
|
+
* wrong shape: the boundary's own guard over the entry the node carries, so a `props`
|
|
28
|
+
* that crossed the DOM is read once here rather than at each reader. The type says
|
|
29
|
+
* which reader, the shape says whether it can read; every caller draws the placeholder
|
|
30
|
+
* for `undefined` (`table-view.ts` §`render`). */
|
|
27
31
|
export declare function tablePropsOfNode(node: PMNode): TableProps | undefined;
|
|
28
32
|
/**
|
|
29
33
|
* A minter over one document: the positional `isl-{n}` sequence continued past the
|