Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"useOptionalChain": "off",
"noCommaOperator": "off",
"noUselessLabel": "off",
"noBannedTypes": "off"
"noBannedTypes": "off",
"noUselessUndefinedInitialization": "off"
},
"security": {
"noGlobalEval": "off"
Expand Down
54 changes: 29 additions & 25 deletions packages/json-joy/src/json-crdt-extensions/peritext/Peritext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import {type ArrNode, StrNode} from '../../json-crdt/nodes';
import {Slices} from './slice/Slices';
import {LocalSlices} from './slice/LocalSlices';
import {Overlay} from './overlay/Overlay';
import {Chars} from './constants';
import {interval, tick} from '../../json-crdt-patch/clock';
import {tick} from '../../json-crdt-patch/clock';
import {Model, type StrApi} from '../../json-crdt/model';
import {CONST, updateNum} from '../../json-hash/hash';
import {SESSION} from '../../json-crdt-patch/constants';
Expand All @@ -18,13 +17,12 @@ import {Fragment} from './block/Fragment';
import {updateRga} from '../../json-crdt/hash';
import type {ITimestampStruct} from '../../json-crdt-patch/clock';
import type {Printable} from 'tree-dump/lib/types';
import type {MarkerSlice} from './slice/MarkerSlice';
import type {SliceSchema, SliceTypeSteps} from './slice/types';
import type {SchemaToJsonNode} from '../../json-crdt/schema/types';
import type {AbstractRga} from '../../json-crdt/nodes/rga';
import type {ChunkSlice} from './util/ChunkSlice';
import type {Stateful} from './types';
import type {PersistedSlice} from './slice/PersistedSlice';
import type {Slice} from './slice/Slice';

const EXTRA_SLICES_SCHEMA = s.vec(s.arr<SliceSchema>([]));
const LOCAL_DATA_SCHEMA = EXTRA_SLICES_SCHEMA;
Expand Down Expand Up @@ -75,7 +73,8 @@ export class Peritext<T = string> implements Printable, Stateful {
}

public strApi(): StrApi {
if (this.str instanceof StrNode) return this.model.api.wrap(this.str);
const str = this.str;
if (str instanceof StrNode) return this.model.api.wrap(str);
throw new Error('INVALID_STR');
}

Expand All @@ -98,7 +97,7 @@ export class Peritext<T = string> implements Printable, Stateful {
*
* @param pos Position of the character in the text.
* @param anchor Whether the point should attach before or after a character.
* Defaults to "before".
* Defaults to "before".
* @returns The point.
*/
public pointAt(pos: number, anchor: Anchor = Anchor.Before): Point<T> {
Expand All @@ -110,6 +109,27 @@ export class Peritext<T = string> implements Printable, Stateful {
return this.point(id, anchor);
}

/**
* Creates a point at a view position in the text, between characters.
*
* @param pos Position between characters in the text.
* @param anchor Whether the point should attach before or after a character.
* Defaults to "after".
* @returns The point.
*/
public pointIn(pos: number, anchor: Anchor = Anchor.After): Point<T> {
const str = this.str;
if (anchor === Anchor.After) {
if (!pos) return this.pointAbsStart();
const id = str.find(pos - 1);
if (!id) return this.pointEnd() ?? this.pointAbsStart();
return this.point(id, Anchor.After);
} else {
const id = str.find(pos);
return id ? this.point(id, Anchor.Before) : this.pointAbsEnd();
}
}

/**
* Creates a point which is attached to the start of the text, before the
* first character.
Expand Down Expand Up @@ -299,31 +319,15 @@ export class Peritext<T = string> implements Printable, Stateful {
*/
public readonly localSlices: Slices<T>;

public getSlice(id: ITimestampStruct): PersistedSlice<T> | undefined {
public getSlice(id: ITimestampStruct): Slice<T> | undefined {
return this.savedSlices.get(id) || this.localSlices.get(id) || this.extraSlices.get(id);
}

// ------------------------------------------------------------------ markers

/** @deprecated Use the method in `Editor` and `Cursor` instead. */
public insMarker(
after: ITimestampStruct,
type: SliceTypeSteps,
data?: unknown,
char: string = Chars.BlockSplitSentinel,
): MarkerSlice<T> {
return this.savedSlices.insMarkerAfter(after, type, data, char);
}

/** @todo This can probably use .del() */
public delMarker(split: MarkerSlice<T>): void {
const str = this.str;
const api = this.model.api;
const builder = api.builder;
const strChunk = split.start.chunk();
if (strChunk) builder.del(str.id, [interval(strChunk.id, 0, 1)]);
builder.del(this.savedSlices.set.id, [interval(split.id, 0, 1)]);
api.apply();
public insMarker(after: ITimestampStruct, type: SliceTypeSteps, data?: unknown): Slice<T> {
return this.savedSlices.insMarkerAfter(after, type, data);
}

/** ----------------------------------------------------- {@link Printable} */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ test('cursor can move across block boundary forwards', () => {
expect([...peritext.blocks.root.children[0].texts()][0].attr()[SliceTypeName.Cursor][0]).toBeInstanceOf(
InlineAttrStartPoint,
);

editor.cursor.move(1);
peritext.refresh();
expect(peritext.blocks.root.children.length).toBe(2);
Expand All @@ -113,15 +112,14 @@ test('cursor can move across block boundary forwards', () => {
expect([...peritext.blocks.root.children[1].texts()][0].attr()).toEqual({});
editor.cursor.move(1);
peritext.refresh();
// console.log(peritext + '');
expect(peritext.blocks.root.children.length).toBe(2);
expect([...peritext.blocks.root.children[0].texts()].length).toBe(1);
expect([...peritext.blocks.root.children[0].texts()][0].text()).toBe('a');
expect([...peritext.blocks.root.children[0].texts()][0].attr()).toEqual({});
expect([...peritext.blocks.root.children[1].texts()].length).toBe(2);
expect([...peritext.blocks.root.children[1].texts()][0].text()).toBe('');
expect([...peritext.blocks.root.children[1].texts()][0].attr()).toEqual({});
expect([...peritext.blocks.root.children[1].texts()][1].text()).toBe('b');
expect([...peritext.blocks.root.children[1].texts()][1].attr()[SliceTypeName.Cursor][0]).toBeInstanceOf(
expect([...peritext.blocks.root.children[1].texts()].length).toBe(1);
expect([...peritext.blocks.root.children[1].texts()][0].text()).toBe('b');
expect([...peritext.blocks.root.children[1].texts()][0].attr()[SliceTypeName.Cursor][0]).toBeInstanceOf(
InlineAttrStartPoint,
);
editor.cursor.move(1);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {type Kit, runNumbersKitTestSuite} from './setup';
import {Anchor} from '../rga/constants';

const testSuite = (setup: () => Kit): void => {
describe('.pointIn()', () => {
test('can infer point between characters', () => {
const {peritext} = setup();
for (let i = 0; i <= 9; i++) {
const p0 = peritext.pointIn(i);
expect(p0.anchor).toBe(Anchor.After);
expect(p0.viewPos()).toBe(i);
expect(p0.rightChar()?.view()).toBe(i.toString());
const p1 = peritext.pointIn(i, Anchor.Before);
expect(p1.anchor).toBe(Anchor.Before);
expect(p1.viewPos()).toBe(i);
expect(p1.rightChar()?.view()).toBe(i.toString());
}
});
});
};

runNumbersKitTestSuite(testSuite);
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,26 @@ const runInlineSlicesTests = (
const {view, editor} = setup();
editor.cursor.setAt(10);
editor.saved.insMarker([['p', 0, {foo: 'bar'}]]);
expect(view()).toMatchSnapshot();
expect(view()).toMatchInlineSnapshot(`
"<>
<0>
"abcdefghij" {}
<p> { foo = "bar" }
"klmnopqrstuvwxyz" {}
"
`);
});

test('can insert at the beginning of text', () => {
const {view, editor} = setup();
editor.cursor.setAt(0);
editor.saved.insMarker([['p', 0, {foo: 'bar'}]]);
expect(view()).toMatchSnapshot();
expect(view()).toMatchInlineSnapshot(`
"<>
<p> { foo = "bar" }
"abcdefghijklmnopqrstuvwxyz" {}
"
`);
});

test('nested block data', () => {
Expand All @@ -53,7 +65,17 @@ const runInlineSlicesTests = (
['ul', 0, {type: 'tasks'}],
['li', 1, {completed: true}],
]);
expect(view()).toMatchSnapshot();
expect(view()).toMatchInlineSnapshot(`
"<>
<0>
"abcde" {}
<ul> { type = "tasks" }
<li> { completed = !f }
"fghi" {}
<li> { completed = !t }
"jklmnopqrstuvwxyz" {}
"
`);
});

test('nested block data - 2', () => {
Expand All @@ -68,14 +90,31 @@ const runInlineSlicesTests = (
['ul', 1, {type: 'tasks'}],
['li', 0, {completed: true}],
]);
expect(view()).toMatchSnapshot();
expect(view()).toMatchInlineSnapshot(`
"<>
<0>
"abcde" {}
<ul> { type = "tasks" }
<li> { completed = !f }
"fghi" {}
<ul> { type = "tasks" }
<li> { completed = !t }
"jklmnopqrstuvwxyz" {}
"
`);
});

test('can insert at the end of text', () => {
const {view, editor} = setup();
editor.cursor.setAt(26);
editor.saved.insMarker([['unfurl', 0, {link: 'foobar'}]]);
expect(view()).toMatchSnapshot();
expect(view()).toMatchInlineSnapshot(`
"<>
<0>
"abcdefghijklmnopqrstuvwxyz" {}
<unfurl> { link = "foobar" }
"
`);
});

test('can split text after slice', () => {
Expand All @@ -84,7 +123,16 @@ const runInlineSlicesTests = (
editor.saved.insOne('BOLD');
editor.cursor.setAt(15);
editor.saved.insMarker(['paragraph']);
expect(view()).toMatchSnapshot();
expect(view()).toMatchInlineSnapshot(`
"<>
<0>
"abcde" {}
"fghij" { BOLD = [ !u ] }
"klmno" {}
<paragraph>
"pqrstuvwxyz" {}
"
`);
});

test('can split text right after slice', () => {
Expand All @@ -93,7 +141,16 @@ const runInlineSlicesTests = (
editor.saved.insOne('BOLD');
editor.cursor.setAt(10);
editor.saved.insMarker(['paragraph']);
expect(view()).toMatchSnapshot();
expect(view()).toMatchInlineSnapshot(`
"<>
<0>
"abcde" {}
"fghij" { BOLD = [ !u ] }
"" {}
<paragraph>
"klmnopqrstuvwxyz" {}
"
`);
});

test('can split text before slice', () => {
Expand All @@ -102,7 +159,16 @@ const runInlineSlicesTests = (
editor.saved.insOne('BOLD');
editor.cursor.setAt(10);
editor.saved.insMarker(['paragraph']);
expect(view()).toMatchSnapshot();
expect(view()).toMatchInlineSnapshot(`
"<>
<0>
"abcdefghij" {}
<paragraph>
"klmno" {}
"pqrst" { BOLD = [ !u ] }
"uvwxyz" {}
"
`);
});

test('can split text right before slice', () => {
Expand All @@ -111,7 +177,15 @@ const runInlineSlicesTests = (
editor.saved.insOne('BOLD');
editor.cursor.setAt(15);
editor.saved.insMarker(['paragraph']);
expect(view()).toMatchSnapshot();
expect(view()).toMatchInlineSnapshot(`
"<>
<0>
"abcdefghijklmno" {}
<paragraph>
"pqrst" { BOLD = [ !u ] }
"uvwxyz" {}
"
`);
});

test('can split text in the middle of a slice', () => {
Expand All @@ -120,7 +194,16 @@ const runInlineSlicesTests = (
editor.saved.insOne('BOLD');
editor.cursor.setAt(10);
editor.saved.insMarker(['paragraph']);
expect(view()).toMatchSnapshot();
expect(view()).toMatchInlineSnapshot(`
"<>
<0>
"abcde" {}
"fghij" { BOLD = [ !u ] }
<paragraph>
"klmno" { BOLD = [ !u ] }
"pqrstuvwxyz" {}
"
`);
});

test('can annotate with slice over two block splits', () => {
Expand All @@ -131,7 +214,18 @@ const runInlineSlicesTests = (
editor.saved.insMarker(['p']);
editor.cursor.setAt(8, 15);
editor.saved.insOne('BOLD');
expect(view()).toMatchSnapshot();
expect(view()).toMatchInlineSnapshot(`
"<>
<0>
"abcdefgh" {}
"ij" { BOLD = [ !u ] }
<p>
"klmn" { BOLD = [ !u ] }
<p>
"opqrstu" { BOLD = [ !u ] }
"vwxyz" {}
"
`);
});

test('can insert two blocks', () => {
Expand All @@ -140,7 +234,16 @@ const runInlineSlicesTests = (
editor.saved.insMarker('p');
editor.cursor.setAt(10 + 10 + 1);
editor.saved.insMarker('p');
expect(view()).toMatchSnapshot();
expect(view()).toMatchInlineSnapshot(`
"<>
<0>
"abcdefghij" {}
<p>
"klmnopqrst" {}
<p>
"uvwxyz" {}
"
`);
});
});
};
Expand Down
Loading