Skip to content

Commit ba65aa0

Browse files
authored
Merge pull request #631 from streamich/peritext-block
Initial Peritext blocks implementation
2 parents 27cc16a + 1d5e94d commit ba65aa0

File tree

10 files changed

+254
-18
lines changed

10 files changed

+254
-18
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {CONST, updateNum} from '../../json-hash';
1414
import {SESSION} from '../../json-crdt-patch/constants';
1515
import {s} from '../../json-crdt-patch';
1616
import {ExtraSlices} from './slice/ExtraSlices';
17+
import {Blocks} from './block/Blocks';
1718
import type {ITimestampStruct} from '../../json-crdt-patch/clock';
1819
import type {Printable} from 'tree-dump/lib/types';
1920
import type {MarkerSlice} from './slice/MarkerSlice';
@@ -52,6 +53,7 @@ export class Peritext<T = string> implements Printable {
5253

5354
public readonly editor: Editor<T>;
5455
public readonly overlay = new Overlay<T>(this);
56+
public readonly blocks: Blocks;
5557

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

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

287290
public refresh(): number {
288291
let state: number = CONST.START_STATE;
289-
this.overlay.refresh();
290-
state = updateNum(state, this.overlay.hash);
292+
state = updateNum(state, this.overlay.refresh());
293+
state = updateNum(state, this.blocks.refresh());
291294
return (this.hash = state);
292295
}
293296
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {printTree} from 'tree-dump/lib/printTree';
2+
import {CONST, updateJson, updateNum} from '../../../json-hash';
3+
import {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint';
4+
import type {Path} from '../../../json-pointer';
5+
import type {Printable} from 'tree-dump';
6+
import type {Peritext} from '../Peritext';
7+
import type {Stateful} from '../types';
8+
9+
export interface IBlock {
10+
readonly path: Path;
11+
readonly parent: IBlock | null;
12+
}
13+
14+
export class Block<Attr = unknown> implements IBlock, Printable, Stateful {
15+
public parent: Block | null = null;
16+
17+
public children: Block[] = [];
18+
19+
constructor(
20+
public readonly txt: Peritext,
21+
public readonly path: Path,
22+
/** @todo rename this */
23+
public readonly marker: MarkerOverlayPoint | undefined,
24+
) {}
25+
26+
/**
27+
* @returns Stable unique identifier within a list of blocks. Used for React
28+
* or other rendering library keys.
29+
*/
30+
public key(): number | string {
31+
if (!this.marker) return this.tag();
32+
const id = this.marker.id;
33+
return id.sid.toString(36) + id.time.toString(36);
34+
}
35+
36+
public tag(): number | string {
37+
const path = this.path;
38+
const length = path.length;
39+
return length ? path[length - 1] : '';
40+
}
41+
42+
public attr(): Attr | undefined {
43+
return this.marker?.data() as Attr | undefined;
44+
}
45+
46+
// ----------------------------------------------------------------- Stateful
47+
48+
public hash: number = 0;
49+
50+
public refresh(): number {
51+
const {path, children} = this;
52+
let state = CONST.START_STATE;
53+
state = updateJson(state, path);
54+
const marker = this.marker;
55+
if (marker) {
56+
state = updateNum(state, marker.marker.refresh());
57+
state = updateNum(state, marker.textHash);
58+
} else {
59+
state = updateNum(state, this.txt.overlay.leadingTextHash);
60+
}
61+
for (let i = 0; i < children.length; i++) state = updateNum(state, children[i].refresh());
62+
return (this.hash = state);
63+
}
64+
65+
// ---------------------------------------------------------------- Printable
66+
67+
protected toStringHeader(): string {
68+
const hash = `#${this.hash.toString(36).slice(-4)}`;
69+
const tag = `<${this.path.join('.')}>`;
70+
const header = `${this.constructor.name} ${hash} ${tag}`;
71+
return header;
72+
}
73+
74+
public toString(tab: string = ''): string {
75+
const header = this.toStringHeader();
76+
const hasChildren = !!this.children.length;
77+
return (
78+
header +
79+
printTree(tab, [
80+
this.marker ? (tab) => this.marker!.toString(tab) : null,
81+
this.marker && hasChildren ? () => '' : null,
82+
hasChildren
83+
? (tab) =>
84+
'children' +
85+
printTree(
86+
tab,
87+
this.children.map(
88+
(child, i) => (tab) => `${i + 1}. ` + child.toString(tab + ' ' + ' '.repeat(String(i + 1).length)),
89+
),
90+
)
91+
: null,
92+
])
93+
);
94+
}
95+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {Block} from './Block';
2+
import {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint';
3+
import {commonLength} from '../util/commonLength';
4+
import {printTree} from 'tree-dump/lib/printTree';
5+
import {LeafBlock} from './LeafBlock';
6+
import type {Path} from '../../../json-pointer';
7+
import type {Stateful} from '../types';
8+
import type {Printable} from 'tree-dump/lib/types';
9+
import type {Peritext} from '../Peritext';
10+
11+
export class Blocks implements Printable, Stateful {
12+
public readonly root: Block;
13+
14+
constructor(public readonly txt: Peritext) {
15+
this.root = new Block(txt, [], undefined);
16+
}
17+
18+
// ---------------------------------------------------------------- Printable
19+
20+
public toString(tab: string = ''): string {
21+
return this.constructor.name + printTree(tab, [(tab) => this.root.toString(tab)]);
22+
}
23+
24+
// ----------------------------------------------------------------- Stateful
25+
26+
public hash: number = 0;
27+
28+
public refresh(): number {
29+
this.refreshBlocks();
30+
return (this.hash = this.root.refresh());
31+
}
32+
33+
private insertBlock(parent: Block, path: Path, marker: undefined | MarkerOverlayPoint): Block {
34+
const txt = this.txt;
35+
const common = commonLength(path, parent.path);
36+
while (parent.path.length > common && parent.parent) parent = parent.parent as Block;
37+
while (parent.path.length + 1 < path.length) {
38+
const block = new Block(txt, path.slice(0, parent.path.length + 1), undefined);
39+
block.parent = parent;
40+
parent.children.push(block);
41+
parent = block;
42+
}
43+
const block = new LeafBlock(txt, path, marker);
44+
block.parent = parent;
45+
parent.children.push(block);
46+
return block;
47+
}
48+
49+
protected refreshBlocks(): void {
50+
this.root.children = [];
51+
let parent = this.root;
52+
let markerPoint: undefined | MarkerOverlayPoint;
53+
const txt = this.txt;
54+
const overlay = txt.overlay;
55+
this.insertBlock(parent, [0], undefined);
56+
const iterator = overlay.markers0(undefined);
57+
while ((markerPoint = iterator())) {
58+
const type = markerPoint.type();
59+
const path = type instanceof Array ? type : [type];
60+
const block = this.insertBlock(parent, path, markerPoint);
61+
if (block.parent) parent = block.parent;
62+
}
63+
}
64+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {printTree} from 'tree-dump/lib/printTree';
2+
import {Block} from './Block';
3+
import type {Path} from '../../../json-pointer';
4+
5+
export interface IBlock<Attr = unknown> {
6+
readonly path: Path;
7+
readonly attr?: Attr;
8+
readonly parent: IBlock | null;
9+
}
10+
11+
export class LeafBlock<Attr = unknown> extends Block<Attr> {
12+
protected toStringHeader(): string {
13+
const header = `${super.toStringHeader()}`;
14+
return header;
15+
}
16+
17+
public toString(tab: string = ''): string {
18+
const header = this.toStringHeader();
19+
return header + printTree(tab, [this.marker ? (tab) => this.marker!.toString(tab) : null]);
20+
}
21+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {setupHelloWorldKit} from '../../__tests__/setup';
2+
import {Block} from '../Block';
3+
4+
const setup = () => {
5+
const kit = setupHelloWorldKit();
6+
kit.peritext.editor.cursor.setAt(6);
7+
const data = {
8+
source: 'http://example.com',
9+
};
10+
kit.peritext.editor.saved.insMarker(['li', 'blockquote'], data);
11+
kit.peritext.refresh();
12+
const marker = kit.peritext.overlay.markers().next().value!;
13+
const type = marker.type();
14+
const path = type instanceof Array ? type : [type];
15+
const block = new Block(kit.peritext, path, marker);
16+
return {
17+
...kit,
18+
block,
19+
marker,
20+
};
21+
};
22+
23+
test('can retrieve the "tag"', () => {
24+
const {block} = setup();
25+
expect(block.tag()).toBe('blockquote');
26+
});
27+
28+
test('can retrieve marker data as "attributes"', () => {
29+
const {block} = setup();
30+
expect(block.attr()).toEqual({source: 'http://example.com'});
31+
});
32+
33+
describe('refresh()', () => {
34+
test('returns the same hash, when no changes were made', () => {
35+
const {block} = setup();
36+
const hash1 = block.refresh();
37+
expect(hash1).toBe(block.hash);
38+
const hash2 = block.refresh();
39+
expect(hash2).toBe(hash1);
40+
expect(hash2).toBe(block.hash);
41+
});
42+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {setupHelloWorldKit} from '../../__tests__/setup';
2+
import {Block} from '../Block';
3+
import {LeafBlock} from '../LeafBlock';
4+
5+
test('can construct a two-paragraph document', () => {
6+
const {peritext} = setupHelloWorldKit();
7+
peritext.editor.cursor.setAt(6);
8+
peritext.editor.saved.insMarker('p');
9+
peritext.refresh();
10+
const blocks = peritext.blocks;
11+
const paragraph1 = blocks.root.children[0];
12+
const paragraph2 = blocks.root.children[1];
13+
expect(blocks.root instanceof Block).toBe(true);
14+
expect(paragraph1 instanceof LeafBlock).toBe(true);
15+
expect(paragraph2 instanceof LeafBlock).toBe(true);
16+
expect(paragraph1.path).toEqual([0]);
17+
expect(paragraph2.path).toEqual(['p']);
18+
});

src/json-crdt-extensions/peritext/overlay/Overlay.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export class Overlay<T = string> implements Printable, Stateful {
185185
};
186186
}
187187

188-
public markers(): IterableIterator<MarkerOverlayPoint<T>> {
188+
public markers(): UndefEndIter<MarkerOverlayPoint<T>> {
189189
return new UndefEndIter(this.markers0(undefined));
190190
}
191191

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

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,5 @@
11
import type {OverlayPoint} from './OverlayPoint';
22

3-
export type BlockTag = [
4-
/**
5-
* Developer specified type of the block. For example, 'title', 'paragraph',
6-
* 'image', etc. For performance reasons, it is better to use a number to
7-
* represent the type.
8-
*/
9-
type: number | number[],
10-
11-
/**
12-
* Any custom attributes that the developer wants to add to the block.
13-
*/
14-
attr?: undefined | unknown,
15-
];
16-
173
/**
184
* Represents a two adjacent overlay points. The first point is the point
195
* that is closer to the start of the document, and the second point is the
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type {Path} from '../../../json-pointer';
2+
3+
export const commonLength = (a: Path, b: Path): number => {
4+
let i = 0;
5+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
6+
return i;
7+
};

src/util/iterator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export type UndefIterator<T> = () => undefined | T;
33
export class UndefEndIter<T> implements IterableIterator<T> {
44
constructor(private readonly i: UndefIterator<T>) {}
55

6-
public next(): IteratorResult<T> {
6+
public next(): IteratorResult<T, T> {
77
const value = this.i();
88
return new IterRes(value, value === undefined) as IteratorResult<T>;
99
}

0 commit comments

Comments
 (0)