Skip to content

Commit

Permalink
Merge pull request #632 from streamich/peritext-block-2
Browse files Browse the repository at this point in the history
Peritext blocks layer iteration methods
  • Loading branch information
streamich authored Jun 8, 2024
2 parents 5577c5d + 23120c9 commit 3605e0a
Show file tree
Hide file tree
Showing 14 changed files with 316 additions and 22 deletions.
2 changes: 2 additions & 0 deletions src/json-crdt-extensions/peritext/Peritext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ export class Peritext<T = string> implements Printable {
localSlices.size() ? (tab) => localSlices.toString(tab) : null,
nl,
(tab) => this.overlay.toString(tab),
nl,
(tab) => this.blocks.toString(tab),
])
);
}
Expand Down
77 changes: 76 additions & 1 deletion src/json-crdt-extensions/peritext/block/Block.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import {printTree} from 'tree-dump/lib/printTree';
import {CONST, updateJson, updateNum} from '../../../json-hash';
import {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint';
import {OverlayPoint} from '../overlay/OverlayPoint';
import {UndefEndIter, type UndefIterator} from '../../../util/iterator';
import {Inline} from './Inline';
import type {Path} from '../../../json-pointer';
import type {Printable} from 'tree-dump';
import type {Peritext} from '../Peritext';
import type {Stateful} from '../types';
import type {OverlayTuple} from '../overlay/types';

export interface IBlock {
readonly path: Path;
readonly parent: IBlock | null;
}

type T = string;

export class Block<Attr = unknown> implements IBlock, Printable, Stateful {
public parent: Block | null = null;

Expand All @@ -19,7 +25,6 @@ export class Block<Attr = unknown> implements IBlock, Printable, Stateful {
constructor(
public readonly txt: Peritext,
public readonly path: Path,
/** @todo rename this */
public readonly marker: MarkerOverlayPoint | undefined,
) {}

Expand All @@ -43,6 +48,76 @@ export class Block<Attr = unknown> implements IBlock, Printable, Stateful {
return this.marker?.data() as Attr | undefined;
}

/**
* Iterate through all overlay points of this block, until the next marker
* (regardless if that marker is a child or not).
*/
public points0(withMarker: boolean = false): UndefIterator<OverlayPoint<T>> {
const txt = this.txt;
const overlay = txt.overlay;
const iterator = overlay.points0(this.marker);
let closed = false;
return () => {
if (withMarker) {
withMarker = false;
return this.marker ?? overlay.START;
}
if (closed) return;
const point = iterator();
if (!point) return;
if (point instanceof MarkerOverlayPoint) {
closed = true;
return;
}
return point;
};
}

public points(withMarker?: boolean): IterableIterator<OverlayPoint<T>> {
return new UndefEndIter(this.points0(withMarker));
}

public tuples0(): UndefIterator<OverlayTuple<T>> {
const overlay = this.txt.overlay;
const iterator = overlay.tuples0(this.marker);
let closed = false;
return () => {
if (closed) return;
const pair = iterator();
if (!pair) return;
if (!pair[1] || pair[1] instanceof MarkerOverlayPoint) closed = true;
return pair;
};
}

public tuples(): IterableIterator<OverlayTuple<T>> {
return new UndefEndIter(this.tuples0());
}

public texts0(): UndefIterator<Inline> {
const txt = this.txt;
const iterator = this.tuples0();
return () => {
const pair = iterator();
return pair && Inline.create(txt, pair[0], pair[1]);
};
}

public texts(): IterableIterator<Inline> {
return new UndefEndIter(this.texts0());
}

public text(): string {
let str = '';
const iterator = this.texts0();
let text = iterator();
while (text) {
str += text.text();
text = iterator();
}
return str;
}

// ----------------------------------------------------------------- Stateful

public hash: number = 0;
Expand Down
14 changes: 2 additions & 12 deletions src/json-crdt-extensions/peritext/block/Inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,6 @@ export class Inline extends Range implements Printable {
return updateNum(this.start.refresh(), this.end.refresh());
}

/**
* @returns The full text content of the inline, which is the concatenation
* of all the underlying {@link ChunkSlice}s.
*/
public str(): string {
let str = '';
for (const slice of this.texts) str += slice.view();
return str;
}

/**
* @returns The position of the inline withing the text.
*/
Expand Down Expand Up @@ -118,7 +108,7 @@ export class Inline extends Range implements Printable {
// ---------------------------------------------------------------- Printable

public toString(tab: string = ''): string {
const str = this.str();
const str = this.text();
const truncate = str.length > 32;
const text = JSON.stringify(truncate ? str.slice(0, 32) : str) + (truncate ? ' …' : '');
const startFormatted = this.start.toString(tab, true);
Expand All @@ -130,7 +120,7 @@ export class Inline extends Range implements Printable {
return (
header +
printTree(tab, [
!marks
!markKeys.length
? null
: (tab) =>
'attributes' +
Expand Down
24 changes: 22 additions & 2 deletions src/json-crdt-extensions/peritext/block/LeafBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,33 @@ export interface IBlock<Attr = unknown> {
}

export class LeafBlock<Attr = unknown> extends Block<Attr> {
// ---------------------------------------------------------------- Printable

protected toStringHeader(): string {
const header = `${super.toStringHeader()}`;
const str = this.text();
const truncate = str.length > 32;
const text = JSON.stringify(truncate ? str.slice(0, 32) : str) + (truncate ? ' …' : '');
const header = `${super.toStringHeader()} ${text}`;
return header;
}

public toString(tab: string = ''): string {
const header = this.toStringHeader();
return header + printTree(tab, [this.marker ? (tab) => this.marker!.toString(tab) : null]);
const texts = [...this.texts()];
const hasSlices = !!texts.length;
return (
header +
printTree(tab, [
this.marker ? (tab) => this.marker!.toString(tab) : null,
!hasSlices
? null
: (tab) =>
'nodes' +
printTree(
tab,
texts.map((inline) => (tab) => inline.toString(tab)),
),
])
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {setupHelloWorldKit} from '../../__tests__/setup';
import {MarkerOverlayPoint} from '../../overlay/MarkerOverlayPoint';
import {OverlayPoint} from '../../overlay/OverlayPoint';
import {Inline} from '../Inline';

const setupTwoBlockDocument = () => {
const kit = setupHelloWorldKit();
const {peritext} = kit;
peritext.editor.cursor.setAt(1, 2);
peritext.editor.saved.insStack('bold');
peritext.editor.cursor.setAt(7, 2);
peritext.editor.saved.insStack('italic');
peritext.editor.cursor.setAt(8, 2);
peritext.editor.saved.insStack('underline');
peritext.editor.cursor.setAt(6);
peritext.editor.saved.insMarker('p');
peritext.editor.delCursors();
peritext.refresh();
return kit;
};

describe('points', () => {
test('no iteration in empty document', () => {
const {peritext} = setupHelloWorldKit();
peritext.refresh();
const blocks = peritext.blocks;
const block = blocks.root.children[0]!;
const iterator = block.points0();
expect(iterator()).toBe(undefined);
});

test('using flag, can receive START marker in empty document', () => {
const {peritext} = setupHelloWorldKit();
peritext.refresh();
const blocks = peritext.blocks;
const block = blocks.root.children[0]!;
const iterator = block.points0(true);
expect(iterator()).toBeInstanceOf(OverlayPoint);
expect(iterator()).toBe(undefined);
});

test('returns all overlay points in single block document', () => {
const {peritext} = setupHelloWorldKit();
peritext.editor.cursor.setAt(3, 3);
peritext.editor.saved.insStack('bold');
peritext.refresh();
const block = peritext.blocks.root.children[0]!;
const iterator = block.points0();
const point1 = iterator();
const point2 = iterator();
const point3 = iterator();
expect(point1).toBeInstanceOf(OverlayPoint);
expect(point2).toBeInstanceOf(OverlayPoint);
expect(point3).toBe(undefined);
});

test('returns all overlay points in single block document, including start marker', () => {
const {peritext} = setupHelloWorldKit();
peritext.editor.cursor.setAt(3, 3);
peritext.editor.saved.insStack('bold');
peritext.refresh();
const block = peritext.blocks.root.children[0]!;
const iterator = block.points0(true);
expect(iterator()).toBeInstanceOf(OverlayPoint);
expect(iterator()).toBeInstanceOf(OverlayPoint);
expect(iterator()).toBeInstanceOf(OverlayPoint);
expect(iterator()).toBe(undefined);
});

test('returns only points within that block, in two-block document', () => {
const {peritext} = setupTwoBlockDocument();
expect(peritext.blocks.root.children.length).toBe(2);
const block1 = peritext.blocks.root.children[0]!;
const block2 = peritext.blocks.root.children[1]!;
const points1 = [...block1.points()];
const points2 = [...block2.points()];
expect(points1.length).toBe(2);
expect(points2.length).toBe(4);
});

test('can iterate including marker points, in two-block document', () => {
const {peritext} = setupTwoBlockDocument();
expect(peritext.blocks.root.children.length).toBe(2);
const block1 = peritext.blocks.root.children[0]!;
const block2 = peritext.blocks.root.children[1]!;
const points1 = [...block1.points(true)];
const points2 = [...block2.points(true)];
expect(points1.length).toBe(3);
expect(points2.length).toBe(5);
expect(points1[0]).toBeInstanceOf(OverlayPoint);
expect(points1[0]).toBe(peritext.overlay.START);
expect(points2[0]).toBeInstanceOf(MarkerOverlayPoint);
});
});

describe('tuples', () => {
test('in markup-less document, returns a single pair', () => {
const {peritext} = setupHelloWorldKit();
peritext.refresh();
const blocks = peritext.blocks;
const block = blocks.root.children[0]!;
const pairs = [...block.tuples()];
expect(pairs.length).toBe(1);
expect(pairs[0]).toEqual([peritext.overlay.START, peritext.overlay.END]);
});

test('can iterate through all text chunks in two-block documents', () => {
const {peritext} = setupTwoBlockDocument();
expect(peritext.blocks.root.children.length).toBe(2);
const block1 = peritext.blocks.root.children[0]!;
const block2 = peritext.blocks.root.children[1]!;
const tuples1 = [...block1.tuples()];
const tuples2 = [...block2.tuples()];
expect(tuples1.length).toBe(3);
const text1 = tuples1.map(([p1, p2]) => Inline.create(peritext, p1, p2).text()).join('');
const text2 = tuples2.map(([p1, p2]) => Inline.create(peritext, p1, p2).text()).join('');
expect(text1).toBe('hello ');
expect(text2).toBe('\nworld');
});
});

describe('texts', () => {
test('in markup-less document', () => {
const {peritext} = setupHelloWorldKit();
peritext.refresh();
const blocks = peritext.blocks;
const block = blocks.root.children[0]!;
const text = [...block.texts()].map((inline) => inline.text()).join('');
expect(text).toBe('hello world');
});

test('can iterate through all text chunks in two-block documents', () => {
const {peritext} = setupTwoBlockDocument();
expect(peritext.blocks.root.children.length).toBe(2);
const block1 = peritext.blocks.root.children[0]!;
const block2 = peritext.blocks.root.children[1]!;
const text1 = [...block1.texts()].map((inline) => inline.text()).join('');
const text2 = [...block2.texts()].map((inline) => inline.text()).join('');
expect(text1).toBe('hello ');
expect(text2).toBe('\nworld');
});
});
15 changes: 15 additions & 0 deletions src/json-crdt-extensions/peritext/block/__tests__/Blocks.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import {setupHelloWorldKit} from '../../__tests__/setup';
import {MarkerOverlayPoint} from '../../overlay/MarkerOverlayPoint';
import {Block} from '../Block';
import {LeafBlock} from '../LeafBlock';

test('can construct block representation of a document without markers', () => {
const {peritext} = setupHelloWorldKit();
peritext.refresh();
const blocks = peritext.blocks;
expect(blocks.root.children.length).toBe(1);
const block = blocks.root.children[0];
expect(block instanceof LeafBlock).toBe(true);
expect(block.path).toEqual([0]);
expect(block.marker).toBe(undefined);
});

test('can construct a two-paragraph document', () => {
const {peritext} = setupHelloWorldKit();
peritext.editor.cursor.setAt(6);
Expand All @@ -11,8 +23,11 @@ test('can construct a two-paragraph document', () => {
const paragraph1 = blocks.root.children[0];
const paragraph2 = blocks.root.children[1];
expect(blocks.root instanceof Block).toBe(true);
expect(blocks.root.children.length).toBe(2);
expect(paragraph1 instanceof LeafBlock).toBe(true);
expect(paragraph2 instanceof LeafBlock).toBe(true);
expect(paragraph1.path).toEqual([0]);
expect(paragraph2.path).toEqual(['p']);
expect(paragraph1.marker).toBe(undefined);
expect(paragraph2.marker instanceof MarkerOverlayPoint).toBe(true);
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const runStrTests = (setup: () => Kit) => {
overlay.refresh();
const [start, end] = [...overlay.points()];
const inline = Inline.create(peritext, start, end);
const str = inline.str();
const str = inline.text();
expect(str).toBe(
peritext
.strApi()
Expand Down
2 changes: 1 addition & 1 deletion src/json-crdt-extensions/peritext/editor/Cursor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ITimestampStruct, tick} from '../../../json-crdt-patch';
import {tick} from '../../../json-crdt-patch';
import {Anchor} from '../rga/constants';
import {Point} from '../rga/Point';
import {CursorAnchor} from '../slice/constants';
Expand Down
Loading

0 comments on commit 3605e0a

Please sign in to comment.