Skip to content

Commit ba8e6a8

Browse files
authored
Merge pull request #903 from streamich/peritext-block-attributes
Peritext formatting event and API improvements
2 parents 44eb970 + 6edbe87 commit ba8e6a8

File tree

25 files changed

+702
-179
lines changed

25 files changed

+702
-179
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"bench:json-ot:ot-string:apply": "cd src/json-ot/__bench__ && yarn && yarn bench:ot-string:apply",
6767
"bench:json-ot:ot-string:compose-and-transform": "yarn build && cd src/json-ot/__bench__ && yarn && yarn bench:ot-string:compose-and-transform",
6868
"coverage": "yarn test --collectCoverage",
69-
"typedoc": "npx typedoc@0.25.13 --tsconfig tsconfig.build.json",
69+
"typedoc": "npx typedoc@0.28.5 --tsconfig tsconfig.build.json",
7070
"build:pages": "npx rimraf@5.0.5 gh-pages && mkdir -p gh-pages && cp -r typedocs/* gh-pages && cp -r coverage gh-pages/coverage",
7171
"deploy:pages": "gh-pages -d gh-pages",
7272
"publish-coverage-and-typedocs": "yarn typedoc && yarn coverage && yarn build:pages && yarn deploy:pages",
@@ -96,7 +96,7 @@
9696
"sonic-forest": "^1.2.0",
9797
"thingies": "^2.1.1",
9898
"tree-dump": "^1.0.2",
99-
"very-small-parser": "^1.12.0"
99+
"very-small-parser": "^1.13.0"
100100
},
101101
"devDependencies": {
102102
"@biomejs/biome": "^1.9.4",

src/json-crdt-diff/JsonCrdtDiff.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {deepEqual} from '@jsonjoy.com/util/lib/json-equal/deepEqual';
22
import {cmpUint8Array} from '@jsonjoy.com/util/lib/buffers/cmpUint8Array';
3-
import {type ITimespanStruct, type ITimestampStruct, type Patch, PatchBuilder, Timespan} from '../json-crdt-patch';
3+
import {type ITimespanStruct, type ITimestampStruct, type Patch, PatchBuilder} from '../json-crdt-patch';
44
import {ArrNode, BinNode, ConNode, ObjNode, StrNode, ValNode, VecNode, type JsonNode} from '../json-crdt/nodes';
55
import * as str from '../util/diff/str';
66
import * as bin from '../util/diff/bin';

src/json-crdt-extensions/peritext/Peritext.ts

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ import {updateRga} from '../../json-crdt/hash';
1919
import type {ITimestampStruct} from '../../json-crdt-patch/clock';
2020
import type {Printable} from 'tree-dump/lib/types';
2121
import type {MarkerSlice} from './slice/MarkerSlice';
22-
import type {SliceSchema, SliceType} from './slice/types';
22+
import type {SliceSchema, SliceTypeSteps} from './slice/types';
2323
import type {SchemaToJsonNode} from '../../json-crdt/schema/types';
2424
import type {AbstractRga} from '../../json-crdt/nodes/rga';
2525
import type {ChunkSlice} from './util/ChunkSlice';
2626
import type {Stateful} from './types';
27+
import type {PersistedSlice} from './slice/PersistedSlice';
2728

2829
const EXTRA_SLICES_SCHEMA = s.vec(s.arr<SliceSchema>([]));
2930
const LOCAL_DATA_SCHEMA = EXTRA_SLICES_SCHEMA;
@@ -36,26 +37,6 @@ export type LocalModel = Model<SchemaToJsonNode<typeof LOCAL_DATA_SCHEMA>>;
3637
* interact with the text.
3738
*/
3839
export class Peritext<T = string> implements Printable, Stateful {
39-
/**
40-
* *Slices* are rich-text annotations that appear in the text. The "saved"
41-
* slices are the ones that are persisted in the document.
42-
*/
43-
public readonly savedSlices: Slices<T>;
44-
45-
/**
46-
* *Extra slices* are slices that are not persisted in the document. However,
47-
* they are still shared across users, i.e. they are ephemerally persisted
48-
* during the editing session.
49-
*/
50-
public readonly extraSlices: Slices<T>;
51-
52-
/**
53-
* *Local slices* are slices that are not persisted in the document and are
54-
* not shared with other users. They are used only for local annotations for
55-
* the current user.
56-
*/
57-
public readonly localSlices: Slices<T>;
58-
5940
public readonly editor: Editor<T>;
6041
public readonly overlay = new Overlay<T>(this);
6142
public readonly blocks: Fragment<T>;
@@ -98,17 +79,6 @@ export class Peritext<T = string> implements Printable, Stateful {
9879
throw new Error('INVALID_STR');
9980
}
10081

101-
/** Select a single character before a point. */
102-
public findCharBefore(point: Point<T>): Range<T> | undefined {
103-
if (point.anchor === Anchor.After) {
104-
const chunk = point.chunk();
105-
if (chunk && !chunk.del) return this.range(this.point(point.id, Anchor.Before), point);
106-
}
107-
const id = point.prevId();
108-
if (!id) return;
109-
return this.range(this.point(id, Anchor.Before), this.point(id, Anchor.After));
110-
}
111-
11282
// ------------------------------------------------------------------- points
11383

11484
/**
@@ -307,12 +277,38 @@ export class Peritext<T = string> implements Printable, Stateful {
307277
return deleted;
308278
}
309279

280+
// ------------------------------------------------------------------- slices
281+
282+
/**
283+
* *Slices* are rich-text annotations that appear in the text. The "saved"
284+
* slices are the ones that are persisted in the document.
285+
*/
286+
public readonly savedSlices: Slices<T>;
287+
288+
/**
289+
* *Extra slices* are slices that are not persisted in the document. However,
290+
* they are still shared across users, i.e. they are ephemerally persisted
291+
* during the editing session.
292+
*/
293+
public readonly extraSlices: Slices<T>;
294+
295+
/**
296+
* *Local slices* are slices that are not persisted in the document and are
297+
* not shared with other users. They are used only for local annotations for
298+
* the current user.
299+
*/
300+
public readonly localSlices: Slices<T>;
301+
302+
public getSlice(id: ITimestampStruct): PersistedSlice<T> | undefined {
303+
return this.savedSlices.get(id) || this.localSlices.get(id) || this.extraSlices.get(id);
304+
}
305+
310306
// ------------------------------------------------------------------ markers
311307

312308
/** @deprecated Use the method in `Editor` and `Cursor` instead. */
313309
public insMarker(
314310
after: ITimestampStruct,
315-
type: SliceType,
311+
type: SliceTypeSteps,
316312
data?: unknown,
317313
char: string = Chars.BlockSplitSentinel,
318314
): MarkerSlice<T> {

src/json-crdt-extensions/peritext/block/Block.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {printTree} from 'tree-dump/lib/printTree';
22
import {CONST, updateJson, updateNum} from '../../../json-hash/hash';
33
import {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint';
4-
import {UndefEndIter, type UndefIterator} from '../../../util/iterator';
4+
import {UndEndIterator, type UndEndNext} from '../../../util/iterator';
55
import {Inline} from './Inline';
66
import {formatType, getTag} from '../slice/util';
77
import {Range} from '../rga/Range';
@@ -60,7 +60,7 @@ export class Block<T = string, Attr = unknown> extends Range<T> implements IBloc
6060
* Iterate through all overlay points of this block, until the next marker
6161
* (regardless if that marker is a child or not).
6262
*/
63-
public points0(withMarker: boolean = false): UndefIterator<OverlayPoint<T>> {
63+
public points0(withMarker: boolean = false): UndEndNext<OverlayPoint<T>> {
6464
const txt = this.txt;
6565
const overlay = txt.overlay;
6666
const iterator = overlay.points0(this.marker);
@@ -82,10 +82,10 @@ export class Block<T = string, Attr = unknown> extends Range<T> implements IBloc
8282
}
8383

8484
public points(withMarker?: boolean): IterableIterator<OverlayPoint<T>> {
85-
return new UndefEndIter(this.points0(withMarker));
85+
return new UndEndIterator(this.points0(withMarker));
8686
}
8787

88-
protected tuples0(): UndefIterator<OverlayTuple<T>> {
88+
protected tuples0(): UndEndNext<OverlayTuple<T>> {
8989
const overlay = this.txt.overlay;
9090
const marker = this.marker;
9191
const iterator = overlay.tuples0(marker);
@@ -103,7 +103,7 @@ export class Block<T = string, Attr = unknown> extends Range<T> implements IBloc
103103
/**
104104
* @todo Consider moving inline-related methods to {@link LeafBlock}.
105105
*/
106-
public texts0(): UndefIterator<Inline<T>> {
106+
public texts0(): UndEndNext<Inline<T>> {
107107
const txt = this.txt;
108108
const overlay = txt.overlay;
109109
const iterator = this.tuples0();
@@ -114,7 +114,7 @@ export class Block<T = string, Attr = unknown> extends Range<T> implements IBloc
114114
let isFirst = true;
115115
let next = iterator();
116116
let closed = false;
117-
const newIterator: UndefIterator<Inline<T>> = () => {
117+
const newIterator: UndEndNext<Inline<T>> = () => {
118118
if (closed) return;
119119
const pair = next;
120120
next = iterator();
@@ -146,7 +146,7 @@ export class Block<T = string, Attr = unknown> extends Range<T> implements IBloc
146146
* @todo Consider moving inline-related methods to {@link LeafBlock}.
147147
*/
148148
public texts(): IterableIterator<Inline<T>> {
149-
return new UndefEndIter(this.texts0());
149+
return new UndEndIterator(this.texts0());
150150
}
151151

152152
public text(): string {

src/json-crdt-extensions/peritext/editor/Editor.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {CommonSliceType, type SliceTypeSteps, type SliceType, type SliceTypeStep
1111
import {isLetter, isPunctuation, isWhitespace, stepsEqual} from './util';
1212
import {ValueSyncStore} from '../../../util/events/sync-store';
1313
import {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint';
14-
import {UndefEndIter, type UndefIterator} from '../../../util/iterator';
14+
import {UndEndIterator, type UndEndNext} from '../../../util/iterator';
1515
import {tick, Timespan, type ITimespanStruct} from '../../../json-crdt-patch';
1616
import {CursorAnchor, SliceStacking, SliceHeaderMask, SliceHeaderShift, SliceTypeCon} from '../slice/constants';
1717
import type {Point} from '../rga/Point';
@@ -114,7 +114,7 @@ export class Editor<T = string> implements Printable {
114114
return cursor ?? this.addCursor();
115115
}
116116

117-
public cursors0(): UndefIterator<Cursor<T>> {
117+
public cursors0(): UndEndNext<Cursor<T>> {
118118
const iterator = this.txt.localSlices.iterator0();
119119
return () => {
120120
while (true) {
@@ -130,7 +130,7 @@ export class Editor<T = string> implements Printable {
130130
}
131131

132132
public cursors() {
133-
return new UndefEndIter(this.cursors0());
133+
return new UndEndIterator(this.cursors0());
134134
}
135135

136136
public forCursor(callback: (cursor: Cursor<T>) => void): void {
@@ -777,7 +777,7 @@ export class Editor<T = string> implements Printable {
777777
* @param type The type of the marker.
778778
* @returns The inserted marker slice.
779779
*/
780-
public insStartMarker(type: SliceType): MarkerSlice<T> {
780+
public insStartMarker(type: SliceTypeSteps): MarkerSlice<T> {
781781
const txt = this.txt;
782782
const start = txt.pointStart() ?? txt.pointAbsStart();
783783
start.refAfter();
@@ -794,7 +794,7 @@ export class Editor<T = string> implements Printable {
794794
* @param type The new block type.
795795
* @returns The marker slice at the point, or a new marker slice if there is none.
796796
*/
797-
public setBlockType(point: Point<T>, type: SliceType): MarkerSlice<T> {
797+
public setBlockType(point: Point<T>, type: SliceTypeSteps): MarkerSlice<T> {
798798
const marker = this.getMarker(point);
799799
if (marker) {
800800
marker.update({type});
@@ -894,16 +894,15 @@ export class Editor<T = string> implements Printable {
894894
}
895895
}
896896

897-
public setStartMarker(type: SliceType, data?: unknown, slices: EditorSlices<T> = this.saved): MarkerSlice<T> {
897+
public setStartMarker(type: SliceTypeSteps, data?: unknown, slices: EditorSlices<T> = this.saved): MarkerSlice<T> {
898898
const after = this.txt.pointStart() ?? this.txt.pointAbsStart();
899899
after.refAfter();
900-
if (Array.isArray(type) && type.length === 1) type = type[0];
901900
return slices.slices.insMarkerAfter(after.id, type, data);
902901
}
903902

904903
public tglMarkerAt(
905904
point: Point<T>,
906-
type: SliceType,
905+
type: SliceTypeSteps,
907906
data?: unknown,
908907
slices: EditorSlices<T> = this.saved,
909908
def: SliceTypeStep = SliceTypeCon.p,
@@ -912,21 +911,24 @@ export class Editor<T = string> implements Printable {
912911
const markerPoint = overlay.getOrNextLowerMarker(point);
913912
if (markerPoint) {
914913
const marker = markerPoint.marker;
915-
const tag = marker.tag();
916-
if (!Array.isArray(type)) type = [type];
917-
const typeTag = type[type.length - 1];
918-
if (tag === typeTag) type = [...type.slice(0, -1), def];
919-
if (Array.isArray(type) && type.length === 1) type = type[0];
914+
const markerTag = marker.tag();
915+
const tagStep = type[type.length - 1];
916+
const tag = Array.isArray(tagStep) ? tagStep[0] : tagStep;
917+
if (markerTag === tag) type = [...type.slice(0, -1), def];
920918
marker.update({type});
921919
} else this.setStartMarker(type, data, slices);
922920
}
923921

924-
public updMarkerAt(point: Point<T>, type: SliceType, data?: unknown, slices: EditorSlices<T> = this.saved): void {
922+
public updMarkerAt(
923+
point: Point<T>,
924+
type: SliceTypeSteps,
925+
data?: unknown,
926+
slices: EditorSlices<T> = this.saved,
927+
): void {
925928
const overlay = this.txt.overlay;
926929
const markerPoint = overlay.getOrNextLowerMarker(point);
927930
if (markerPoint) {
928931
const marker = markerPoint.marker;
929-
if (Array.isArray(type) && type.length === 1) type = type[0];
930932
marker.update({type});
931933
} else this.setStartMarker(type, data, slices);
932934
}
@@ -939,7 +941,7 @@ export class Editor<T = string> implements Printable {
939941
* @param data Custom data of the slice.
940942
*/
941943
public tglMarker(
942-
type: SliceType,
944+
type: SliceTypeSteps,
943945
data?: unknown,
944946
selection: Range<T>[] | IterableIterator<Range<T>> = this.cursors(),
945947
slices: EditorSlices<T> = this.saved,
@@ -957,7 +959,7 @@ export class Editor<T = string> implements Printable {
957959
* of the document.
958960
*/
959961
public updMarker(
960-
type: SliceType,
962+
type: SliceTypeSteps,
961963
data?: unknown,
962964
selection: Range<T>[] | IterableIterator<Range<T>> = this.cursors(),
963965
slices: EditorSlices<T> = this.saved,
@@ -1103,7 +1105,7 @@ export class Editor<T = string> implements Printable {
11031105
const [, , , type, data] = split;
11041106
const after = txt.pointAt(curr);
11051107
after.refAfter();
1106-
txt.savedSlices.insMarkerAfter(after.id, type, data);
1108+
txt.savedSlices.insMarkerAfter(after.id, type as SliceTypeSteps, data);
11071109
curr += 1;
11081110
}
11091111
}

src/json-crdt-extensions/peritext/editor/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type {UndefIterator} from '../../../util/iterator';
1+
import type {UndEndNext} from '../../../util/iterator';
22
import type {Point} from '../rga/Point';
33
import type {Range} from '../rga/Range';
44
import type {SliceType} from '../slice';
55
import type {SliceStacking} from '../slice/constants';
66
import type {ChunkSlice} from '../util/ChunkSlice';
77

8-
export type CharIterator<T> = UndefIterator<ChunkSlice<T>>;
8+
export type CharIterator<T> = UndEndNext<ChunkSlice<T>>;
99
export type CharPredicate<T> = (char: T) => boolean;
1010

1111
export type EditorPosition<T = string> = Point<T> | number | [at: number, anchor: 0 | 1];

src/json-crdt-extensions/peritext/events/__tests__/cursor.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@ const testSuite = (getKit: () => Kit) => {
1212
return {...kit, et};
1313
};
1414

15+
describe('clearing all cursors', () => {
16+
test('can remove a single caret cursor', () => {
17+
const kit = setup();
18+
kit.et.cursor({at: [2]});
19+
expect(kit.editor.cursorCount()).toBe(1);
20+
kit.et.cursor({clear: true});
21+
expect(kit.editor.cursorCount()).toBe(0);
22+
});
23+
24+
test('can remove multiple cursors', () => {
25+
const kit = setup();
26+
kit.et.cursor({at: [2]});
27+
expect(kit.editor.cursorCount()).toBe(1);
28+
kit.et.cursor({at: [3, 5], add: true});
29+
expect(kit.editor.cursorCount()).toBe(2);
30+
kit.et.cursor({clear: true});
31+
expect(kit.editor.cursorCount()).toBe(0);
32+
});
33+
});
34+
1535
describe('with absolute position ("at" property set)', () => {
1636
describe('caret', () => {
1737
test('can set the caret at the document start', () => {

0 commit comments

Comments
 (0)