edytor 0.1.0-next.40 → 0.1.0-next.41
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
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
Edytor aims to be for Svelte what Slate.js is for React: a heavily customizable editor with an API to build any kind of collaborative rich text editor.
|
|
18
18
|
|
|
19
|
-
> **Work in progress.** Edytor is a pre-release (`0.1.0-next.
|
|
19
|
+
> **Work in progress.** Edytor is a pre-release (`0.1.0-next.41`, `edytor@next` on npm) and not ready for production; the API changes between releases without a compatibility layer. The untagged `edytor@0.0.11` on npm predates the current API. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
|
|
20
20
|
|
|
21
21
|
## Features
|
|
22
22
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Text } from '../text/text.svelte.js';
|
|
2
2
|
import type { Edytor } from '../edytor.svelte.js';
|
|
3
|
-
import type { JSONBlock,
|
|
3
|
+
import type { JSONBlock, JSONInlineBlock } from '../utils/json.js';
|
|
4
4
|
import { type BlockOperations } from './block.utils.js';
|
|
5
5
|
import type { BlockDefinition } from '../plugins.js';
|
|
6
6
|
import { InlineBlock } from './inlineBlock.svelte.js';
|
|
@@ -186,16 +186,4 @@ export declare class Block {
|
|
|
186
186
|
text: Text;
|
|
187
187
|
offset: number;
|
|
188
188
|
} | null;
|
|
189
|
-
/**
|
|
190
|
-
* Insert children at `index`: a JSON spec is created (its id kept, minted
|
|
191
|
-
* when missing); a block handle moves there with its identity.
|
|
192
|
-
*/
|
|
193
|
-
insertChildren: (index: number, blocks: (JSONBlock | Block)[]) => void;
|
|
194
|
-
/** Delete `length` visible children from `index`, each with its subtree (a document delete). */
|
|
195
|
-
deleteChildren: (index: number, length?: number) => void;
|
|
196
|
-
/**
|
|
197
|
-
* Insert content before part `index` (a text segment or an inline atom):
|
|
198
|
-
* each entry is a text's runs or an atom, as JSON.
|
|
199
|
-
*/
|
|
200
|
-
insertParts: (index: number, parts: (JSONText[] | JSONInlineBlock)[]) => void;
|
|
201
189
|
}
|
|
@@ -355,9 +355,14 @@ export class Block {
|
|
|
355
355
|
// ── JSON insertion (specs are data; `new Block({block})` is gone) ──
|
|
356
356
|
/**
|
|
357
357
|
* Insert children at `index`: a JSON spec is created (its id kept, minted
|
|
358
|
-
* when missing); a block handle moves there with its identity.
|
|
358
|
+
* when missing); a block handle moves there with its identity. A raw
|
|
359
|
+
* write for the editor's own commands: nothing on a view whose dispatcher
|
|
360
|
+
* refuses writes (readonly, a read-only document).
|
|
361
|
+
* @internal
|
|
359
362
|
*/
|
|
360
363
|
insertChildren = (index, blocks) => {
|
|
364
|
+
if (!this.edytor.dispatcher.permits())
|
|
365
|
+
return;
|
|
361
366
|
const { facade } = this.edytor;
|
|
362
367
|
const parent = this.isRoot ? null : this.id;
|
|
363
368
|
blocks.forEach((block, k) => {
|
|
@@ -367,17 +372,26 @@ export class Block {
|
|
|
367
372
|
facade.insertBlock({ parent, index: index + k }, jsonBlockToSpec(block));
|
|
368
373
|
});
|
|
369
374
|
};
|
|
370
|
-
/**
|
|
375
|
+
/**
|
|
376
|
+
* Delete `length` visible children from `index`, each with its subtree (a
|
|
377
|
+
* document delete). Raw, as `insertChildren`.
|
|
378
|
+
* @internal
|
|
379
|
+
*/
|
|
371
380
|
deleteChildren = (index, length = 1) => {
|
|
381
|
+
if (!this.edytor.dispatcher.permits())
|
|
382
|
+
return;
|
|
372
383
|
const ids = this.edytor.facade.childrenIds(this.isRoot ? null : this.id);
|
|
373
384
|
for (const id of ids.slice(index, index + length).reverse())
|
|
374
385
|
this.edytor.facade.block(id).delete({ keepChildren: false });
|
|
375
386
|
};
|
|
376
387
|
/**
|
|
377
388
|
* Insert content before part `index` (a text segment or an inline atom):
|
|
378
|
-
* each entry is a text's runs or an atom, as JSON.
|
|
389
|
+
* each entry is a text's runs or an atom, as JSON. Raw, as `insertChildren`.
|
|
390
|
+
* @internal
|
|
379
391
|
*/
|
|
380
392
|
insertParts = (index, parts) => {
|
|
393
|
+
if (!this.edytor.dispatcher.permits())
|
|
394
|
+
return;
|
|
381
395
|
const model = this.model;
|
|
382
396
|
if (!model)
|
|
383
397
|
return;
|
|
@@ -28,7 +28,7 @@ recorded patch and document it here.
|
|
|
28
28
|
|
|
29
29
|
The v13 provider stack (`y-protocols`, `y-websocket`, `y-indexeddb`, `lib0@0.2`)
|
|
30
30
|
is no longer a dependency of any kind: `tests/packed-consumer/smoke.js` and
|
|
31
|
-
`src/tests/crdt/
|
|
31
|
+
`src/tests/crdt/boundary-attacks/public-boundary.test.ts` refuse it.
|
|
32
32
|
|
|
33
33
|
## Layout
|
|
34
34
|
|
|
@@ -56,7 +56,7 @@ the run's artifact, says in the summary whether the installed `lib0-v14`
|
|
|
56
56
|
satisfies the newer engine's `lib0` range (a mismatch is a run warning: a
|
|
57
57
|
red check may then be the older lib0, not the fork), then materializes the newer engine as `bench/vendor-baseline/yjs`
|
|
58
58
|
so `bench/lib/interop.mjs` (the fork and the newer engine syncing over the
|
|
59
|
-
wire) and the baseline leg of `src/tests/crdt/hardening/
|
|
59
|
+
wire) and the baseline leg of `src/tests/crdt/hardening/search-marker-format.test.ts`
|
|
60
60
|
(byte-identical stores after every operation) run against it. A red run
|
|
61
61
|
means the re-sync needs care; the re-sync itself is the manual recipe under
|
|
62
62
|
YP8. Locally: `node scripts/upstream-check.mjs`, then those two checks, then
|
|
@@ -204,7 +204,7 @@ added path inert = upstream behavior) and requires **byte-identical**
|
|
|
204
204
|
`encodeStateAsUpdate` output plus identical rendered deltas — covering
|
|
205
205
|
distant inserts, formatted retains, format-marker inserts, deletes, undo/
|
|
206
206
|
redo, remote apply, GC, nested modify ops, merge/split cycles.
|
|
207
|
-
`src/tests/crdt/hardening/
|
|
207
|
+
`src/tests/crdt/hardening/search-marker-format.test.ts` pins the same-key boundary
|
|
208
208
|
rule (the R1 review's YP4 corruption), runs randomized + structured
|
|
209
209
|
differential replays (patched vs disabled vs materialized pre-P4 baseline)
|
|
210
210
|
with per-op `toDelta` + store-byte equality across insert/delete/format/
|
|
@@ -291,7 +291,7 @@ purity (no updates/splits/undo/renderer changes; marker perturbation
|
|
|
291
291
|
invisible to mutations), traversal bounds, and the
|
|
292
292
|
`a[b]b[b=null]c` read-seed-vs-mutation-seed counterexample.
|
|
293
293
|
`src/tests/crdt/runs/range-reads.test.ts` +
|
|
294
|
-
`
|
|
294
|
+
`text-model-probes/range-read-hazards.test.ts` cover the Edytor projection: parity vs
|
|
295
295
|
the pre-WU8 `toDelta().toJSON()` oracle at every range, marker pinning
|
|
296
296
|
(`index` = anchor's left edge, `formats` = fold at that edge), the
|
|
297
297
|
marks-alias boundary (corruption is confined to one cursor — marker
|
|
@@ -486,10 +486,10 @@ Correctness oracle:
|
|
|
486
486
|
every top-level vendored declaration outside the allowlist above.
|
|
487
487
|
- `bench/lib/mk-baseline.sh` now materializes the PRISTINE upstream engine
|
|
488
488
|
(`@y/y@14.0.0-rc.26`, installed as the `@y/protocols` peer, + YP1), and the
|
|
489
|
-
baseline leg of `src/tests/crdt/hardening/
|
|
489
|
+
baseline leg of `src/tests/crdt/hardening/search-marker-format.test.ts` (patched vs
|
|
490
490
|
upstream, per-op `toDelta` + byte-identical `encodeStateAsUpdate`) and
|
|
491
491
|
`bench/lib/interop.mjs` pass against it; the YP4/YP7 differentials
|
|
492
|
-
(`
|
|
492
|
+
(`search-marker-format`, `marker-seed`, `yp7-gap-end`) stay green.
|
|
493
493
|
|
|
494
494
|
Re-syncing with upstream: take the new upstream `src/`, apply YP1, re-apply
|
|
495
495
|
the YP4/YP5/YP7 hunks (diff this tree against the pinned upstream as in
|
|
@@ -507,7 +507,7 @@ triggers `readUpdateV2`'s retry, and the document holds a pending update it
|
|
|
507
507
|
could integrate — forever, until some unrelated update from the recorded
|
|
508
508
|
client arrives. Reproduced on the unmodified `@y/y@14.0.0-rc.26` with four
|
|
509
509
|
updates (A1 ← B1 ← A3, A2 independent) delivered as A3, B1, A1, A2; found by
|
|
510
|
-
the architecture-v2 phase-2 fuzz (`src/tests/crdt/arch-v2/
|
|
510
|
+
the architecture-v2 phase-2 fuzz (`src/tests/crdt/arch-v2/replica-fuzz.test.ts`, an
|
|
511
511
|
observer fed every update in reverse order stayed pending in 74/1,500 seeds).
|
|
512
512
|
|
|
513
513
|
Patch (all hunks marked `// YP9` or delimited by `// YP9 begin` / `// YP9 end`):
|
|
@@ -648,7 +648,7 @@ fork adds paired marks (Peritext / Loro anchor pairing), schema generation 5:
|
|
|
648
648
|
Plain formats (every upstream test) are untouched: the class of a plain
|
|
649
649
|
item is content's, so the comparator reduces to upstream's.
|
|
650
650
|
|
|
651
|
-
Oracle: `src/tests/crdt/arch-v2/
|
|
651
|
+
Oracle: `src/tests/crdt/arch-v2/paired-marks.test.ts` (the Peritext cases on
|
|
652
652
|
every swept client-id pair and delivery order; 7 of its 17 rows fail on
|
|
653
653
|
the tree without YP13), the upstream suite unchanged.
|
|
654
654
|
|
|
@@ -687,7 +687,7 @@ key (`deleteAttr`) delete their subtree as before. `YNode#applyDelta`
|
|
|
687
687
|
still writes nothing to a deleted node, so the kept node's own attrs are
|
|
688
688
|
frozen; its children (a text, a list, a map) are ordinary live types.
|
|
689
689
|
|
|
690
|
-
Oracle: `src/tests/crdt/
|
|
690
|
+
Oracle: `src/tests/crdt/placement/same-id-concurrent.test.ts` (the losing text shows
|
|
691
691
|
on every replica and after a reload; two fuzzes), the upstream suite
|
|
692
692
|
unchanged.
|
|
693
693
|
|
|
@@ -39,12 +39,6 @@ export declare class Text {
|
|
|
39
39
|
* the caret is a render the projector displays after (`mounted`).
|
|
40
40
|
*/
|
|
41
41
|
refreshFromModel: () => void;
|
|
42
|
-
/** Insert `text` (optionally marked) at segment-local `offset`. */
|
|
43
|
-
insertAt: (offset: number, text: string, marks?: Record<string, unknown> | null) => boolean;
|
|
44
|
-
/** Delete `length` atoms at segment-local `offset`. */
|
|
45
|
-
deleteAt: (offset: number, length: number) => boolean;
|
|
46
|
-
/** Multi-mark format over `[offset, offset+length)` — `null` values remove the mark. */
|
|
47
|
-
formatAt: (offset: number, length: number, attributes: Record<string, unknown>) => boolean;
|
|
48
42
|
private batch;
|
|
49
43
|
getMarksAtRange: (yStart: number, yEnd: number) => JSONText[];
|
|
50
44
|
insertText: (args_0: {
|
package/dist/text/text.svelte.js
CHANGED
|
@@ -87,12 +87,18 @@ export class Text {
|
|
|
87
87
|
// are SEGMENT-LOCAL display atoms (UTF-16 units); `segStart` maps them into
|
|
88
88
|
// the block's display space (read from the index, so a write after another
|
|
89
89
|
// in the same transaction lands right).
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
// They write raw (no hooks, no undo policy of their own): the commands
|
|
91
|
+
// that call them passed the dispatcher's admission, and a call a readonly
|
|
92
|
+
// view or a read-only document would refuse writes nothing.
|
|
93
|
+
/** Insert `text` (optionally marked) at segment-local `offset`. @internal */
|
|
94
|
+
insertAt = (offset, text, marks) => this.edytor.dispatcher.permits() &&
|
|
95
|
+
accepted(this.parent.model?.insertText(this.segStart + offset, text, marks ?? undefined));
|
|
96
|
+
/** Delete `length` atoms at segment-local `offset`. @internal */
|
|
97
|
+
deleteAt = (offset, length) => this.edytor.dispatcher.permits() &&
|
|
98
|
+
accepted(this.parent.model?.deleteText(this.segStart + offset, length));
|
|
99
|
+
/** Multi-mark format over `[offset, offset+length)` — `null` values remove the mark. @internal */
|
|
100
|
+
formatAt = (offset, length, attributes) => this.edytor.dispatcher.permits() &&
|
|
101
|
+
accepted(this.parent.model?.format(this.segStart + offset, length, attributes));
|
|
96
102
|
batch = batch.bind(this);
|
|
97
103
|
getMarksAtRange = getMarksAtRange.bind(this);
|
|
98
104
|
insertText = this.batch('insertText', insertText.bind(this));
|