Skip to content

Commit

Permalink
feat(json-crdt-extensions): 🎸 construct a blocks layer out of Overlay
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 7, 2024
1 parent 05acc64 commit 067fed6
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 18 deletions.
7 changes: 5 additions & 2 deletions src/json-crdt-extensions/peritext/Peritext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {CONST, updateNum} from '../../json-hash';
import {SESSION} from '../../json-crdt-patch/constants';
import {s} from '../../json-crdt-patch';
import {ExtraSlices} from './slice/ExtraSlices';
import {Blocks} from './block/Blocks';
import type {ITimestampStruct} from '../../json-crdt-patch/clock';
import type {Printable} from 'tree-dump/lib/types';
import type {MarkerSlice} from './slice/MarkerSlice';
Expand Down Expand Up @@ -52,6 +53,7 @@ export class Peritext<T = string> implements Printable {

public readonly editor: Editor<T>;
public readonly overlay = new Overlay<T>(this);
public readonly blocks: Blocks;

/**
* Creates a new Peritext context.
Expand Down Expand Up @@ -82,6 +84,7 @@ export class Peritext<T = string> implements Printable {
});
this.localSlices = new LocalSlices(this, localSlicesModel.root.node().get(0)!);
this.editor = new Editor<T>(this);
this.blocks = new Blocks(this as Peritext);
}

public strApi(): StrApi {
Expand Down Expand Up @@ -286,8 +289,8 @@ export class Peritext<T = string> implements Printable {

public refresh(): number {
let state: number = CONST.START_STATE;
this.overlay.refresh();
state = updateNum(state, this.overlay.hash);
state = updateNum(state, this.overlay.refresh());
state = updateNum(state, this.blocks.refresh());
return (this.hash = state);
}
}
64 changes: 64 additions & 0 deletions src/json-crdt-extensions/peritext/block/Blocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {Block} from './Block';
import {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint';
import {commonLength} from '../util/commonLength';
import {printTree} from 'tree-dump/lib/printTree';
import {LeafBlock} from './LeafBlock';
import type {Path} from '../../../json-pointer';
import type {Stateful} from '../types';
import type {Printable} from 'tree-dump/lib/types';
import type {Peritext} from '../Peritext';

export class Blocks implements Printable, Stateful {
public readonly root: Block;

constructor(public readonly txt: Peritext) {
this.root = new Block(txt, [], undefined);
}

// ---------------------------------------------------------------- Printable

public toString(tab: string = ''): string {
return this.constructor.name + printTree(tab, [(tab) => this.root.toString(tab)]);
}

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

public hash: number = 0;

public refresh(): number {
this.refreshBlocks();
return (this.hash = this.root.refresh());
}

private insertBlock(parent: Block, path: Path, marker: undefined | MarkerOverlayPoint): Block {
const txt = this.txt;
const common = commonLength(path, parent.path);
while (parent.path.length > common && parent.parent) parent = parent.parent as Block;
while (parent.path.length + 1 < path.length) {
const block = new Block(txt, path.slice(0, parent.path.length + 1), undefined);
block.parent = parent;
parent.children.push(block);
parent = block;
}
const block = new LeafBlock(txt, path, marker);
block.parent = parent;
parent.children.push(block);
return block;
}

protected refreshBlocks(): void {
this.root.children = [];
let parent = this.root;
let markerPoint: undefined | MarkerOverlayPoint;
const txt = this.txt;
const overlay = txt.overlay;
this.insertBlock(parent, [0], undefined);
const iterator = overlay.markers0(undefined);
while ((markerPoint = iterator())) {
const type = markerPoint.type();
const path = type instanceof Array ? type : [type];
const block = this.insertBlock(parent, path, markerPoint);
if (block.parent) parent = block.parent;
}
}
}
26 changes: 26 additions & 0 deletions src/json-crdt-extensions/peritext/block/LeafBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {printTree} from 'tree-dump/lib/printTree';
import {Block} from './Block';
import type {Path} from '../../../json-pointer';

export interface IBlock<Attr = unknown> {
readonly path: Path;
readonly attr?: Attr;
readonly parent: IBlock | null;
}

export class LeafBlock<Attr = unknown> extends Block<Attr> {
protected toStringHeader(): string {
const header = `${super.toStringHeader()}`;
return header;
}

public toString(tab: string = ''): string {
const header = this.toStringHeader();
return (
header +
printTree(tab, [
this.marker ? (tab) => this.marker!.toString(tab) : null,
])
);
}
}
18 changes: 18 additions & 0 deletions src/json-crdt-extensions/peritext/block/__tests__/Blocks.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {setupHelloWorldKit} from '../../__tests__/setup';
import {Block} from '../Block';
import {LeafBlock} from '../LeafBlock';

test('can construct a two-paragraph document', () => {
const {peritext} = setupHelloWorldKit();
peritext.editor.cursor.setAt(6);
peritext.editor.saved.insMarker('p');
peritext.refresh();
const blocks = peritext.blocks;
const paragraph1 = blocks.root.children[0];
const paragraph2 = blocks.root.children[1];
expect(blocks.root instanceof Block).toBe(true);
expect(paragraph1 instanceof LeafBlock).toBe(true);
expect(paragraph2 instanceof LeafBlock).toBe(true);
expect(paragraph1.path).toEqual([0]);
expect(paragraph2.path).toEqual(['p']);
});
2 changes: 1 addition & 1 deletion src/json-crdt-extensions/peritext/overlay/Overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ export class Overlay<T = string> implements Printable, Stateful {
};
}

public markers(): IterableIterator<MarkerOverlayPoint<T>> {
public markers(): UndefEndIter<MarkerOverlayPoint<T>> {
return new UndefEndIter(this.markers0(undefined));
}

Expand Down
14 changes: 0 additions & 14 deletions src/json-crdt-extensions/peritext/overlay/types.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,5 @@
import type {OverlayPoint} from './OverlayPoint';

export type BlockTag = [
/**
* Developer specified type of the block. For example, 'title', 'paragraph',
* 'image', etc. For performance reasons, it is better to use a number to
* represent the type.
*/
type: number | number[],

/**
* Any custom attributes that the developer wants to add to the block.
*/
attr?: undefined | unknown,
];

/**
* Represents a two adjacent overlay points. The first point is the point
* that is closer to the start of the document, and the second point is the
Expand Down
7 changes: 7 additions & 0 deletions src/json-crdt-extensions/peritext/util/commonLength.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type {Path} from '../../../json-pointer';

export const commonLength = (a: Path, b: Path): number => {
let i = 0;
while (i < a.length && i < b.length && a[i] === b[i]) i++;
return i;
};
2 changes: 1 addition & 1 deletion src/util/iterator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export type UndefIterator<T> = () => undefined | T;
export class UndefEndIter<T> implements IterableIterator<T> {
constructor(private readonly i: UndefIterator<T>) {}

public next(): IteratorResult<T> {
public next(): IteratorResult<T, T> {
const value = this.i();
return new IterRes(value, value === undefined) as IteratorResult<T>;
}
Expand Down

0 comments on commit 067fed6

Please sign in to comment.