Skip to content

Commit

Permalink
Merge pull request #786 from streamich/peritext-fragment-export
Browse files Browse the repository at this point in the history
Peritext `Fragment` export/import
  • Loading branch information
streamich authored Dec 1, 2024
2 parents ec9c02f + 8e48422 commit bd13acc
Show file tree
Hide file tree
Showing 19 changed files with 541 additions and 30 deletions.
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"useIsArray": "off",
"noAssignInExpressions": "off",
"noConfusingLabels": "off",
"noConfusingVoidType": "off"
"noConfusingVoidType": "off",
"noConstEnum": "off"
},
"complexity": {
"noStaticOnlyClass": "off",
Expand Down
39 changes: 36 additions & 3 deletions src/json-crdt-extensions/peritext/block/Block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {Printable} from 'tree-dump';
import type {Peritext} from '../Peritext';
import type {Stateful} from '../types';
import type {OverlayTuple} from '../overlay/types';
import type {JsonMlNode} from '../../../json-ml';
import type {PeritextMlAttributes, PeritextMlElement} from './types';

export interface IBlock {
readonly path: Path;
Expand Down Expand Up @@ -52,6 +52,33 @@ 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 @@ -143,8 +170,14 @@ export class Block<Attr = unknown> extends Range implements IBlock, Printable, S

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

toJsonMl(): JsonMlNode {
throw new Error('not implemented');
public toJson(): PeritextMlElement {
const data = this.attr();
const attr: PeritextMlAttributes | null = data !== void 0 ? {data} : null;
const node: PeritextMlElement = [this.tag(), attr];
const children = this.children;
const length = children.length;
for (let i = 0; i < length; i++) node.push(children[i].toJson());
return node;
}

// ----------------------------------------------------------------- Stateful
Expand Down
8 changes: 5 additions & 3 deletions src/json-crdt-extensions/peritext/block/Fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {Stateful} from '../types';
import type {Printable} from 'tree-dump/lib/types';
import type {Peritext} from '../Peritext';
import type {Point} from '../rga/Point';
import type {JsonMlNode} from '../../../json-ml/types';
import type {PeritextMlElement} from './types';

/**
* A *fragment* represents a structural slice of a rich-text document. A
Expand All @@ -32,8 +32,10 @@ export class Fragment extends Range implements Printable, Stateful {

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

toJsonMl(): JsonMlNode {
throw new Error('not implemented');
public toJson(): PeritextMlElement {
const node = this.root.toJson();
node[0] = '';
return node;
}

// ---------------------------------------------------------------- Printable
Expand Down
23 changes: 20 additions & 3 deletions src/json-crdt-extensions/peritext/block/Inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {Printable} from 'tree-dump/lib/types';
import type {PathStep} from '@jsonjoy.com/json-pointer';
import type {Peritext} from '../Peritext';
import type {Slice} from '../slice/types';
import type {JsonMlNode} from '../../../json-ml';
import type {PeritextMlAttributes, PeritextMlNode} from './types';

/** The attribute started before this inline and ends after this inline. */
export class InlineAttrPassing {
Expand Down Expand Up @@ -245,8 +245,25 @@ export class Inline extends Range implements Printable {

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

toJsonMl(): JsonMlNode {
throw new Error('not implemented');
public toJson(): PeritextMlNode {
let node: PeritextMlNode = this.text();
const attrs = this.attr();
for (const key in attrs) {
const keyNum = Number(key);
if (keyNum === SliceTypeName.Cursor || keyNum === SliceTypeName.RemoteCursor) continue;
const attr = attrs[key];
if (!attr.length) node = [key, {inline: true}, node];
else {
const length = attr.length;
for (let i = 0; i < length; i++) {
const slice = attr[i].slice;
const data = slice.data();
const attributes: PeritextMlAttributes = data === void 0 ? {inline: true} : {inline: true, data};
node = [key === keyNum + '' ? keyNum : key, attributes, node];
}
}
}
return node;
}

// ---------------------------------------------------------------- Printable
Expand Down
14 changes: 14 additions & 0 deletions src/json-crdt-extensions/peritext/block/LeafBlock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +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';

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

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

public toJson(): PeritextMlElement {
const data = this.attr();
const attr: PeritextMlAttributes | null = data !== void 0 ? {data} : null;
const node: PeritextMlElement = [this.tag(), attr];
for (const inline of this.texts()) {
const child = inline.toJson();
if (child) node.push(child);
}
return node;
}

// ---------------------------------------------------------------- Printable
public toStringName(): string {
return 'LeafBlock';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {type Kit, runAlphabetKitTestSuite} from '../../__tests__/setup';
import {toHtml, toJsonMl} from '../../export/export';
import {CommonSliceType} from '../../slice';

const runTests = (setup: () => Kit) => {
describe('JSON-ML', () => {
test('can export two paragraphs', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(10);
editor.saved.insMarker(CommonSliceType.p);
peritext.refresh();
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
fragment.refresh();
const html = toJsonMl(fragment.toJson());
expect(html).toEqual(['', null, ['p', null, 'efghij'], ['p', null, 'klm']]);
});

test('can export two paragraphs with inline formatting', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(10);
editor.saved.insMarker(CommonSliceType.p);
editor.cursor.setAt(6, 2);
editor.saved.insOverwrite(CommonSliceType.b);
editor.cursor.setAt(7, 2);
editor.saved.insOverwrite(CommonSliceType.i);
peritext.refresh();
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
fragment.refresh();
const html = toJsonMl(fragment.toJson());
expect(html).toEqual([
'',
null,
['p', null, 'ef', ['b', null, 'g'], ['i', null, ['b', null, 'h']], ['i', null, 'i'], 'j'],
['p', null, 'klm'],
]);
});
});

describe('HTML', () => {
test('can export two paragraphs', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(10);
editor.saved.insMarker(CommonSliceType.p);
peritext.refresh();
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
fragment.refresh();
const html = toHtml(fragment.toJson());
expect(html).toBe('<p>efghij</p><p>klm</p>');
});

test('can export two paragraphs (formatted)', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(10);
editor.saved.insMarker(CommonSliceType.p);
peritext.refresh();
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
fragment.refresh();
const html = toHtml(fragment.toJson(), ' ');
expect(html).toBe('<p>efghij</p>\n<p>klm</p>');
});

test('can export two paragraphs (formatted and wrapped in <div>)', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(10);
editor.saved.insMarker(CommonSliceType.p);
peritext.refresh();
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
fragment.refresh();
const json = fragment.toJson();
json[0] = 'div';
const html = toHtml(json, ' ');
expect(html).toBe('<div>\n <p>efghij</p>\n <p>klm</p>\n</div>');
});

test('can export two paragraphs with inline formatting', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(10);
editor.saved.insMarker(CommonSliceType.p);
editor.cursor.setAt(6, 2);
editor.saved.insOverwrite(CommonSliceType.b);
editor.cursor.setAt(7, 2);
editor.saved.insOverwrite(CommonSliceType.i);
peritext.refresh();
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
fragment.refresh();
const json = fragment.toJson();
const html = toHtml(json, '');
expect(html).toEqual('<p>ef<b>g</b><i><b>h</b></i><i>i</i>j</p><p>klm</p>');
});

test('can export two paragraphs with inline formatting (formatted)', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(10);
editor.saved.insMarker(CommonSliceType.p);
editor.cursor.setAt(6, 2);
editor.saved.insOverwrite(CommonSliceType.b);
editor.cursor.setAt(7, 2);
editor.saved.insOverwrite(CommonSliceType.i);
peritext.refresh();
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
fragment.refresh();
const json = fragment.toJson();
const html = toHtml(json, ' ');
expect(html).toEqual(
'<p>\n ef\n <b>g</b>\n <i>\n <b>h</b>\n </i>\n <i>i</i>\n j\n</p>\n<p>klm</p>',
);
});

test('can export two paragraphs with inline formatting (formatted, wrapped in <div>)', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(10);
editor.saved.insMarker(CommonSliceType.p);
editor.cursor.setAt(6, 2);
editor.saved.insOverwrite(CommonSliceType.b);
editor.cursor.setAt(7, 2);
editor.saved.insOverwrite(CommonSliceType.i);
peritext.refresh();
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
fragment.refresh();
const json = fragment.toJson();
json[0] = 'div';
const html = toHtml(json, ' ');
expect('\n' + html).toEqual(`
<div>
<p>
ef
<b>g</b>
<i>
<b>h</b>
</i>
<i>i</i>
j
</p>
<p>klm</p>
</div>`);
});
});
};

describe('Fragment.toJson()', () => {
runAlphabetKitTestSuite(runTests);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {type Kit, runAlphabetKitTestSuite} from '../../__tests__/setup';
import {CommonSliceType} from '../../slice';

const runTests = (setup: () => Kit) => {
test('can export two paragraphs', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(10);
editor.saved.insMarker(CommonSliceType.p);
peritext.refresh();
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
fragment.refresh();
const json = fragment.toJson();
expect(json).toEqual(['', null, [0, null, 'efghij'], [0, null, 'klm']]);
});

test('can export two paragraphs with inline formatting', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(10);
editor.saved.insMarker(CommonSliceType.p);
editor.cursor.setAt(6, 2);
editor.saved.insOverwrite(CommonSliceType.b);
editor.cursor.setAt(7, 2);
editor.saved.insOverwrite(CommonSliceType.i);
peritext.refresh();
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
fragment.refresh();
const json = fragment.toJson();
expect(json).toEqual([
'',
null,
[
0,
null,
'ef',
[CommonSliceType.b, {inline: true}, 'g'],
[CommonSliceType.i, {inline: true}, [CommonSliceType.b, {inline: true}, 'h']],
[CommonSliceType.i, {inline: true}, 'i'],
'j',
],
[0, null, 'klm'],
]);
});
};

describe('Fragment.toJson()', () => {
runAlphabetKitTestSuite(runTests);
});
10 changes: 10 additions & 0 deletions src/json-crdt-extensions/peritext/block/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type PeritextMlNode = string | PeritextMlElement;
export type PeritextMlElement = [
tag: string | number,
attrs: null | PeritextMlAttributes,
...children: PeritextMlNode[],
];
export interface PeritextMlAttributes {
inline?: boolean;
data?: unknown;
}
Loading

0 comments on commit bd13acc

Please sign in to comment.