Skip to content

Commit

Permalink
Merge pull request #794 from streamich/fragment-converters
Browse files Browse the repository at this point in the history
`Fragment` exports to other formats
  • Loading branch information
streamich authored Dec 21, 2024
2 parents 37aacc1 + 8f5be6c commit 7e3619d
Show file tree
Hide file tree
Showing 18 changed files with 1,253 additions and 286 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@
"hyperdyperid": "^1.2.0",
"sonic-forest": "^1.0.3",
"thingies": "^2.1.1",
"tree-dump": "^1.0.2"
"tree-dump": "^1.0.2",
"very-small-parser": "^1.8.0"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
Expand Down
52 changes: 37 additions & 15 deletions src/json-crdt-extensions/peritext/__tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,28 @@ export const setupNumbersWithMultipleChunksAndDeletesKit = (): Kit => {
});
};

export const runNumbersKitTestSuite = (runTestSuite: (getKit: () => Kit) => void) => {
describe('numbers "0123456789", no edits', () => {
runTestSuite(setupNumbersKit);
});

describe('numbers "0123456789", with default schema and tombstones', () => {
runTestSuite(setupNumbersWithTombstonesKit);
});

describe('numbers "0123456789", two RGA chunks', () => {
runTestSuite(setupNumbersWithTwoChunksKit);
});

describe('numbers "0123456789", with RGA split', () => {
runTestSuite(setupNumbersWithRgaSplitKit);
});

describe('numbers "0123456789", with multiple deletes', () => {
runTestSuite(setupNumbersWithMultipleChunksAndDeletesKit);
});
};

/**
* Creates a Peritext instance with text "abcdefghijklmnopqrstuvwxyz", no edits.
*/
Expand Down Expand Up @@ -262,19 +284,19 @@ export const runAlphabetKitTestSuite = (runTestSuite: (getKit: () => Kit) => voi
describe('basic alphabet', () => {
runTestSuite(setupAlphabetKit);
});
// describe('alphabet with two chunks', () => {
// runTestSuite(setupAlphabetWithTwoChunksKit);
// });
// describe('alphabet with chunk split', () => {
// runTestSuite(setupAlphabetChunkSplitKit);
// });
// describe('alphabet with deletes', () => {
// runTestSuite(setupAlphabetWithDeletesKit);
// });
// describe('alphabet written in reverse', () => {
// runTestSuite(setupAlphabetWrittenInReverse);
// });
// describe('alphabet written in reverse with deletes', () => {
// runTestSuite(setupAlphabetWrittenInReverseWithDeletes);
// });
describe('alphabet with two chunks', () => {
runTestSuite(setupAlphabetWithTwoChunksKit);
});
describe('alphabet with chunk split', () => {
runTestSuite(setupAlphabetChunkSplitKit);
});
describe('alphabet with deletes', () => {
runTestSuite(setupAlphabetWithDeletesKit);
});
describe('alphabet written in reverse', () => {
runTestSuite(setupAlphabetWrittenInReverse);
});
describe('alphabet written in reverse with deletes', () => {
runTestSuite(setupAlphabetWrittenInReverseWithDeletes);
});
};
85 changes: 35 additions & 50 deletions src/json-crdt-extensions/peritext/block/Block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,33 +52,6 @@ export class Block<Attr = unknown> extends Range implements IBlock, Printable, S
return length ? path[length - 1] : '';
}

// public htmlTag(): string {
// const tag = this.tag();
// switch (typeof tag) {
// case 'string': return tag.toLowerCase();
// case 'number': return SliceTypeName[tag] || 'div';
// default: return 'div';
// }
// }

// protected jsonMlNode(): JsonMlElement {
// const props: Record<string, string> = {};
// const node: JsonMlElement = ['div', props];
// const tag = this.tag();
// switch (typeof tag) {
// case 'string':
// node[0] = tag;
// break;
// case 'number':
// const tag0 = SliceTypeName[tag];
// if (tag0) node[0] = tag0; else props['data-tag'] = tag + '';
// break;
// }
// const attr = this.attr();
// if (attr !== undefined) props['data-attr'] = JSON.stringify(attr);
// return node;
// }

public attr(): Attr | undefined {
return this.marker?.data() as Attr | undefined;
}
Expand Down Expand Up @@ -112,59 +85,70 @@ export class Block<Attr = unknown> extends Range implements IBlock, Printable, S
return new UndefEndIter(this.points0(withMarker));
}

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

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

/**
* @todo Consider moving inline-related methods to {@link LeafBlock}.
*/
public texts0(): UndefIterator<Inline> {
const txt = this.txt;
const iterator = this.tuples0();
const blockStart = this.start;
const blockEnd = this.end;
const start = this.start;
const end = this.end;
const startIsMarker = txt.overlay.isMarker(start.id);
const endIsMarker = txt.overlay.isMarker(end.id);
let isFirst = true;
let next = iterator();
let closed = false;
return () => {
if (closed) return;
const pair = next;
next = iterator();
if (!pair) return;
const [p1, p2] = pair;
let start: Point = p1;
let end: Point = p2;
const [overlayPoint1, overlayPoint2] = pair;
let point1: Point = overlayPoint1;
let point2: Point = overlayPoint2;
if (isFirst) {
isFirst = false;
if (blockStart.cmpSpatial(p1) > 0) start = blockStart;
if (start.cmpSpatial(overlayPoint1) > 0) point1 = start;
if (startIsMarker) {
point1 = point1.clone();
point1.step(1);
}
}
const isLast = !next;
if (isLast) if (blockEnd.cmpSpatial(p2) < 0) end = blockEnd;
return new Inline(txt, p1, p2, start, end);
if (!endIsMarker && end.cmpSpatial(overlayPoint2) < 0) {
closed = true;
point2 = end;
}
return new Inline(txt, overlayPoint1, overlayPoint2, point1, point2);
};
}

/**
* @todo Consider moving inline-related methods to {@link LeafBlock}.
*/
public texts(): IterableIterator<Inline> {
return new UndefEndIter(this.texts0());
}

public text(): string {
let str = '';
const iterator = this.texts0();
let inline = iterator();
while (inline) {
str += inline.text();
inline = iterator();
}
const children = this.children;
const length = children.length;
for (let i = 0; i < length; i++) str += children[i].text();
return str;
}

Expand Down Expand Up @@ -204,6 +188,7 @@ export class Block<Attr = unknown> extends Range implements IBlock, Printable, S
public toStringName(): string {
return 'Block';
}

protected toStringHeader(): string {
const hash = `#${this.hash.toString(36).slice(-4)}`;
const tag = this.path.map((step) => formatType(step)).join('.');
Expand Down
12 changes: 2 additions & 10 deletions src/json-crdt-extensions/peritext/block/Fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,16 @@ export class Fragment extends Range implements Printable, Stateful {
}

protected build(): void {
const {end, root} = this;
const {root} = this;
root.children = [];
let parent = this.root;
const txt = this.txt;
const overlay = txt.overlay;
/**
* @todo This line always inserts a markerless block at the beginning of
* the fragment. But what happens if one actually exists?
*/
this.insertBlock(parent, [CommonSliceType.p], void 0, void 0);
const iterator = overlay.markerPairs0(this.start, this.end);
const checkEnd = !end.isAbsEnd();
let pair: ReturnType<typeof iterator>;
while ((pair = iterator())) {
const [p1, p2] = pair;
if (!p1) break;
if (checkEnd && p1.cmpSpatial(end) > 0) break;
const type = p1.type();
const type = p1 ? p1.type() : CommonSliceType.p;
const path = type instanceof Array ? type : [type];
const block = this.insertBlock(parent, path, p1, p2);
if (block.parent) parent = block.parent;
Expand Down
12 changes: 1 addition & 11 deletions src/json-crdt-extensions/peritext/block/Inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,6 @@ export class Inline extends Range implements Printable {
return texts;
}

public text(): string {
const str = super.text();
return this.p1 instanceof MarkerOverlayPoint ? str.slice(1) : str;
}

// ------------------------------------------------------------------- export

public toJson(): PeritextMlNode {
Expand Down Expand Up @@ -273,12 +268,7 @@ export class Inline extends Range implements Printable {
}

public toString(tab: string = ''): string {
const str = this.text();
const truncate = str.length > 32;
const text = JSON.stringify(truncate ? str.slice(0, 32) : str) + (truncate ? ' …' : '');
const startFormatted = this.p1.toString(tab, true);
const range = this.p1.cmp(this.end) === 0 ? startFormatted : `${startFormatted}${this.end.toString(tab, true)}`;
const header = `Inline ${range} ${text}`;
const header = `${super.toString(tab)}`;
const attr = this.attr();
const attrKeys = Object.keys(attr);
const texts = this.texts();
Expand Down
8 changes: 7 additions & 1 deletion src/json-crdt-extensions/peritext/block/LeafBlock.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {printTree} from 'tree-dump/lib/printTree';
import {Block} from './Block';
import type {Path} from '@jsonjoy.com/json-pointer';
import type {PeritextMlAttributes, PeritextMlElement, PeritextMlNode} from './types';
import type {PeritextMlAttributes, PeritextMlElement} from './types';

export interface IBlock<Attr = unknown> {
readonly path: Path;
Expand All @@ -10,6 +10,12 @@ export interface IBlock<Attr = unknown> {
}

export class LeafBlock<Attr = unknown> extends Block<Attr> {
public text(): string {
let str = '';
for (let iterator = this.texts0(), inline = iterator(); inline; inline = iterator()) str += inline.text();
return str;
}

// ------------------------------------------------------------------- export

public toJson(): PeritextMlElement {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,32 +93,6 @@ describe('points', () => {
});
});

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]) => new Inline(peritext, p1, p2, p1, p2).text()).join('');
const text2 = tuples2.map(([p1, p2]) => new Inline(peritext, p1, p2, p1, p2).text()).join('');
expect(text1).toBe('hello ');
expect(text2).toBe('world');
});
});

describe('texts', () => {
test('in markup-less document', () => {
const {peritext} = setupHelloWorldKit();
Expand Down
Loading

0 comments on commit 7e3619d

Please sign in to comment.