-
-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #631 from streamich/peritext-block
Initial Peritext blocks implementation
- Loading branch information
Showing
10 changed files
with
254 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import {printTree} from 'tree-dump/lib/printTree'; | ||
import {CONST, updateJson, updateNum} from '../../../json-hash'; | ||
import {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint'; | ||
import type {Path} from '../../../json-pointer'; | ||
import type {Printable} from 'tree-dump'; | ||
import type {Peritext} from '../Peritext'; | ||
import type {Stateful} from '../types'; | ||
|
||
export interface IBlock { | ||
readonly path: Path; | ||
readonly parent: IBlock | null; | ||
} | ||
|
||
export class Block<Attr = unknown> implements IBlock, Printable, Stateful { | ||
public parent: Block | null = null; | ||
|
||
public children: Block[] = []; | ||
|
||
constructor( | ||
public readonly txt: Peritext, | ||
public readonly path: Path, | ||
/** @todo rename this */ | ||
public readonly marker: MarkerOverlayPoint | undefined, | ||
) {} | ||
|
||
/** | ||
* @returns Stable unique identifier within a list of blocks. Used for React | ||
* or other rendering library keys. | ||
*/ | ||
public key(): number | string { | ||
if (!this.marker) return this.tag(); | ||
const id = this.marker.id; | ||
return id.sid.toString(36) + id.time.toString(36); | ||
} | ||
|
||
public tag(): number | string { | ||
const path = this.path; | ||
const length = path.length; | ||
return length ? path[length - 1] : ''; | ||
} | ||
|
||
public attr(): Attr | undefined { | ||
return this.marker?.data() as Attr | undefined; | ||
} | ||
|
||
// ----------------------------------------------------------------- Stateful | ||
|
||
public hash: number = 0; | ||
|
||
public refresh(): number { | ||
const {path, children} = this; | ||
let state = CONST.START_STATE; | ||
state = updateJson(state, path); | ||
const marker = this.marker; | ||
if (marker) { | ||
state = updateNum(state, marker.marker.refresh()); | ||
state = updateNum(state, marker.textHash); | ||
} else { | ||
state = updateNum(state, this.txt.overlay.leadingTextHash); | ||
} | ||
for (let i = 0; i < children.length; i++) state = updateNum(state, children[i].refresh()); | ||
return (this.hash = state); | ||
} | ||
|
||
// ---------------------------------------------------------------- Printable | ||
|
||
protected toStringHeader(): string { | ||
const hash = `#${this.hash.toString(36).slice(-4)}`; | ||
const tag = `<${this.path.join('.')}>`; | ||
const header = `${this.constructor.name} ${hash} ${tag}`; | ||
return header; | ||
} | ||
|
||
public toString(tab: string = ''): string { | ||
const header = this.toStringHeader(); | ||
const hasChildren = !!this.children.length; | ||
return ( | ||
header + | ||
printTree(tab, [ | ||
this.marker ? (tab) => this.marker!.toString(tab) : null, | ||
this.marker && hasChildren ? () => '' : null, | ||
hasChildren | ||
? (tab) => | ||
'children' + | ||
printTree( | ||
tab, | ||
this.children.map( | ||
(child, i) => (tab) => `${i + 1}. ` + child.toString(tab + ' ' + ' '.repeat(String(i + 1).length)), | ||
), | ||
) | ||
: null, | ||
]) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
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]); | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
src/json-crdt-extensions/peritext/block/__tests__/Block.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import {setupHelloWorldKit} from '../../__tests__/setup'; | ||
import {Block} from '../Block'; | ||
|
||
const setup = () => { | ||
const kit = setupHelloWorldKit(); | ||
kit.peritext.editor.cursor.setAt(6); | ||
const data = { | ||
source: 'http://example.com', | ||
}; | ||
kit.peritext.editor.saved.insMarker(['li', 'blockquote'], data); | ||
kit.peritext.refresh(); | ||
const marker = kit.peritext.overlay.markers().next().value!; | ||
const type = marker.type(); | ||
const path = type instanceof Array ? type : [type]; | ||
const block = new Block(kit.peritext, path, marker); | ||
return { | ||
...kit, | ||
block, | ||
marker, | ||
}; | ||
}; | ||
|
||
test('can retrieve the "tag"', () => { | ||
const {block} = setup(); | ||
expect(block.tag()).toBe('blockquote'); | ||
}); | ||
|
||
test('can retrieve marker data as "attributes"', () => { | ||
const {block} = setup(); | ||
expect(block.attr()).toEqual({source: 'http://example.com'}); | ||
}); | ||
|
||
describe('refresh()', () => { | ||
test('returns the same hash, when no changes were made', () => { | ||
const {block} = setup(); | ||
const hash1 = block.refresh(); | ||
expect(hash1).toBe(block.hash); | ||
const hash2 = block.refresh(); | ||
expect(hash2).toBe(hash1); | ||
expect(hash2).toBe(block.hash); | ||
}); | ||
}); |
18 changes: 18 additions & 0 deletions
18
src/json-crdt-extensions/peritext/block/__tests__/Blocks.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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']); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters