Skip to content

Commit bd13acc

Browse files
authored
Merge pull request #786 from streamich/peritext-fragment-export
Peritext `Fragment` export/import
2 parents ec9c02f + 8e48422 commit bd13acc

File tree

19 files changed

+541
-30
lines changed

19 files changed

+541
-30
lines changed

biome.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"useIsArray": "off",
3535
"noAssignInExpressions": "off",
3636
"noConfusingLabels": "off",
37-
"noConfusingVoidType": "off"
37+
"noConfusingVoidType": "off",
38+
"noConstEnum": "off"
3839
},
3940
"complexity": {
4041
"noStaticOnlyClass": "off",

src/json-crdt-extensions/peritext/block/Block.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {Printable} from 'tree-dump';
1212
import type {Peritext} from '../Peritext';
1313
import type {Stateful} from '../types';
1414
import type {OverlayTuple} from '../overlay/types';
15-
import type {JsonMlNode} from '../../../json-ml';
15+
import type {PeritextMlAttributes, PeritextMlElement} from './types';
1616

1717
export interface IBlock {
1818
readonly path: Path;
@@ -52,6 +52,33 @@ export class Block<Attr = unknown> extends Range implements IBlock, Printable, S
5252
return length ? path[length - 1] : '';
5353
}
5454

55+
// public htmlTag(): string {
56+
// const tag = this.tag();
57+
// switch (typeof tag) {
58+
// case 'string': return tag.toLowerCase();
59+
// case 'number': return SliceTypeName[tag] || 'div';
60+
// default: return 'div';
61+
// }
62+
// }
63+
64+
// protected jsonMlNode(): JsonMlElement {
65+
// const props: Record<string, string> = {};
66+
// const node: JsonMlElement = ['div', props];
67+
// const tag = this.tag();
68+
// switch (typeof tag) {
69+
// case 'string':
70+
// node[0] = tag;
71+
// break;
72+
// case 'number':
73+
// const tag0 = SliceTypeName[tag];
74+
// if (tag0) node[0] = tag0; else props['data-tag'] = tag + '';
75+
// break;
76+
// }
77+
// const attr = this.attr();
78+
// if (attr !== undefined) props['data-attr'] = JSON.stringify(attr);
79+
// return node;
80+
// }
81+
5582
public attr(): Attr | undefined {
5683
return this.marker?.data() as Attr | undefined;
5784
}
@@ -143,8 +170,14 @@ export class Block<Attr = unknown> extends Range implements IBlock, Printable, S
143170

144171
// ------------------------------------------------------------------- export
145172

146-
toJsonMl(): JsonMlNode {
147-
throw new Error('not implemented');
173+
public toJson(): PeritextMlElement {
174+
const data = this.attr();
175+
const attr: PeritextMlAttributes | null = data !== void 0 ? {data} : null;
176+
const node: PeritextMlElement = [this.tag(), attr];
177+
const children = this.children;
178+
const length = children.length;
179+
for (let i = 0; i < length; i++) node.push(children[i].toJson());
180+
return node;
148181
}
149182

150183
// ----------------------------------------------------------------- Stateful

src/json-crdt-extensions/peritext/block/Fragment.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {Stateful} from '../types';
1010
import type {Printable} from 'tree-dump/lib/types';
1111
import type {Peritext} from '../Peritext';
1212
import type {Point} from '../rga/Point';
13-
import type {JsonMlNode} from '../../../json-ml/types';
13+
import type {PeritextMlElement} from './types';
1414

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

3333
// ------------------------------------------------------------------- export
3434

35-
toJsonMl(): JsonMlNode {
36-
throw new Error('not implemented');
35+
public toJson(): PeritextMlElement {
36+
const node = this.root.toJson();
37+
node[0] = '';
38+
return node;
3739
}
3840

3941
// ---------------------------------------------------------------- Printable

src/json-crdt-extensions/peritext/block/Inline.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type {Printable} from 'tree-dump/lib/types';
1313
import type {PathStep} from '@jsonjoy.com/json-pointer';
1414
import type {Peritext} from '../Peritext';
1515
import type {Slice} from '../slice/types';
16-
import type {JsonMlNode} from '../../../json-ml';
16+
import type {PeritextMlAttributes, PeritextMlNode} from './types';
1717

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

246246
// ------------------------------------------------------------------- export
247247

248-
toJsonMl(): JsonMlNode {
249-
throw new Error('not implemented');
248+
public toJson(): PeritextMlNode {
249+
let node: PeritextMlNode = this.text();
250+
const attrs = this.attr();
251+
for (const key in attrs) {
252+
const keyNum = Number(key);
253+
if (keyNum === SliceTypeName.Cursor || keyNum === SliceTypeName.RemoteCursor) continue;
254+
const attr = attrs[key];
255+
if (!attr.length) node = [key, {inline: true}, node];
256+
else {
257+
const length = attr.length;
258+
for (let i = 0; i < length; i++) {
259+
const slice = attr[i].slice;
260+
const data = slice.data();
261+
const attributes: PeritextMlAttributes = data === void 0 ? {inline: true} : {inline: true, data};
262+
node = [key === keyNum + '' ? keyNum : key, attributes, node];
263+
}
264+
}
265+
}
266+
return node;
250267
}
251268

252269
// ---------------------------------------------------------------- Printable

src/json-crdt-extensions/peritext/block/LeafBlock.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {printTree} from 'tree-dump/lib/printTree';
22
import {Block} from './Block';
33
import type {Path} from '@jsonjoy.com/json-pointer';
4+
import type {PeritextMlAttributes, PeritextMlElement, PeritextMlNode} from './types';
45

56
export interface IBlock<Attr = unknown> {
67
readonly path: Path;
@@ -9,6 +10,19 @@ export interface IBlock<Attr = unknown> {
910
}
1011

1112
export class LeafBlock<Attr = unknown> extends Block<Attr> {
13+
// ------------------------------------------------------------------- export
14+
15+
public toJson(): PeritextMlElement {
16+
const data = this.attr();
17+
const attr: PeritextMlAttributes | null = data !== void 0 ? {data} : null;
18+
const node: PeritextMlElement = [this.tag(), attr];
19+
for (const inline of this.texts()) {
20+
const child = inline.toJson();
21+
if (child) node.push(child);
22+
}
23+
return node;
24+
}
25+
1226
// ---------------------------------------------------------------- Printable
1327
public toStringName(): string {
1428
return 'LeafBlock';
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import {type Kit, runAlphabetKitTestSuite} from '../../__tests__/setup';
2+
import {toHtml, toJsonMl} from '../../export/export';
3+
import {CommonSliceType} from '../../slice';
4+
5+
const runTests = (setup: () => Kit) => {
6+
describe('JSON-ML', () => {
7+
test('can export two paragraphs', () => {
8+
const {editor, peritext} = setup();
9+
editor.cursor.setAt(10);
10+
editor.saved.insMarker(CommonSliceType.p);
11+
peritext.refresh();
12+
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
13+
fragment.refresh();
14+
const html = toJsonMl(fragment.toJson());
15+
expect(html).toEqual(['', null, ['p', null, 'efghij'], ['p', null, 'klm']]);
16+
});
17+
18+
test('can export two paragraphs with inline formatting', () => {
19+
const {editor, peritext} = setup();
20+
editor.cursor.setAt(10);
21+
editor.saved.insMarker(CommonSliceType.p);
22+
editor.cursor.setAt(6, 2);
23+
editor.saved.insOverwrite(CommonSliceType.b);
24+
editor.cursor.setAt(7, 2);
25+
editor.saved.insOverwrite(CommonSliceType.i);
26+
peritext.refresh();
27+
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
28+
fragment.refresh();
29+
const html = toJsonMl(fragment.toJson());
30+
expect(html).toEqual([
31+
'',
32+
null,
33+
['p', null, 'ef', ['b', null, 'g'], ['i', null, ['b', null, 'h']], ['i', null, 'i'], 'j'],
34+
['p', null, 'klm'],
35+
]);
36+
});
37+
});
38+
39+
describe('HTML', () => {
40+
test('can export two paragraphs', () => {
41+
const {editor, peritext} = setup();
42+
editor.cursor.setAt(10);
43+
editor.saved.insMarker(CommonSliceType.p);
44+
peritext.refresh();
45+
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
46+
fragment.refresh();
47+
const html = toHtml(fragment.toJson());
48+
expect(html).toBe('<p>efghij</p><p>klm</p>');
49+
});
50+
51+
test('can export two paragraphs (formatted)', () => {
52+
const {editor, peritext} = setup();
53+
editor.cursor.setAt(10);
54+
editor.saved.insMarker(CommonSliceType.p);
55+
peritext.refresh();
56+
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
57+
fragment.refresh();
58+
const html = toHtml(fragment.toJson(), ' ');
59+
expect(html).toBe('<p>efghij</p>\n<p>klm</p>');
60+
});
61+
62+
test('can export two paragraphs (formatted and wrapped in <div>)', () => {
63+
const {editor, peritext} = setup();
64+
editor.cursor.setAt(10);
65+
editor.saved.insMarker(CommonSliceType.p);
66+
peritext.refresh();
67+
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
68+
fragment.refresh();
69+
const json = fragment.toJson();
70+
json[0] = 'div';
71+
const html = toHtml(json, ' ');
72+
expect(html).toBe('<div>\n <p>efghij</p>\n <p>klm</p>\n</div>');
73+
});
74+
75+
test('can export two paragraphs with inline formatting', () => {
76+
const {editor, peritext} = setup();
77+
editor.cursor.setAt(10);
78+
editor.saved.insMarker(CommonSliceType.p);
79+
editor.cursor.setAt(6, 2);
80+
editor.saved.insOverwrite(CommonSliceType.b);
81+
editor.cursor.setAt(7, 2);
82+
editor.saved.insOverwrite(CommonSliceType.i);
83+
peritext.refresh();
84+
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
85+
fragment.refresh();
86+
const json = fragment.toJson();
87+
const html = toHtml(json, '');
88+
expect(html).toEqual('<p>ef<b>g</b><i><b>h</b></i><i>i</i>j</p><p>klm</p>');
89+
});
90+
91+
test('can export two paragraphs with inline formatting (formatted)', () => {
92+
const {editor, peritext} = setup();
93+
editor.cursor.setAt(10);
94+
editor.saved.insMarker(CommonSliceType.p);
95+
editor.cursor.setAt(6, 2);
96+
editor.saved.insOverwrite(CommonSliceType.b);
97+
editor.cursor.setAt(7, 2);
98+
editor.saved.insOverwrite(CommonSliceType.i);
99+
peritext.refresh();
100+
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
101+
fragment.refresh();
102+
const json = fragment.toJson();
103+
const html = toHtml(json, ' ');
104+
expect(html).toEqual(
105+
'<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>',
106+
);
107+
});
108+
109+
test('can export two paragraphs with inline formatting (formatted, wrapped in <div>)', () => {
110+
const {editor, peritext} = setup();
111+
editor.cursor.setAt(10);
112+
editor.saved.insMarker(CommonSliceType.p);
113+
editor.cursor.setAt(6, 2);
114+
editor.saved.insOverwrite(CommonSliceType.b);
115+
editor.cursor.setAt(7, 2);
116+
editor.saved.insOverwrite(CommonSliceType.i);
117+
peritext.refresh();
118+
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
119+
fragment.refresh();
120+
const json = fragment.toJson();
121+
json[0] = 'div';
122+
const html = toHtml(json, ' ');
123+
expect('\n' + html).toEqual(`
124+
<div>
125+
<p>
126+
ef
127+
<b>g</b>
128+
<i>
129+
<b>h</b>
130+
</i>
131+
<i>i</i>
132+
j
133+
</p>
134+
<p>klm</p>
135+
</div>`);
136+
});
137+
});
138+
};
139+
140+
describe('Fragment.toJson()', () => {
141+
runAlphabetKitTestSuite(runTests);
142+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {type Kit, runAlphabetKitTestSuite} from '../../__tests__/setup';
2+
import {CommonSliceType} from '../../slice';
3+
4+
const runTests = (setup: () => Kit) => {
5+
test('can export two paragraphs', () => {
6+
const {editor, peritext} = setup();
7+
editor.cursor.setAt(10);
8+
editor.saved.insMarker(CommonSliceType.p);
9+
peritext.refresh();
10+
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
11+
fragment.refresh();
12+
const json = fragment.toJson();
13+
expect(json).toEqual(['', null, [0, null, 'efghij'], [0, null, 'klm']]);
14+
});
15+
16+
test('can export two paragraphs with inline formatting', () => {
17+
const {editor, peritext} = setup();
18+
editor.cursor.setAt(10);
19+
editor.saved.insMarker(CommonSliceType.p);
20+
editor.cursor.setAt(6, 2);
21+
editor.saved.insOverwrite(CommonSliceType.b);
22+
editor.cursor.setAt(7, 2);
23+
editor.saved.insOverwrite(CommonSliceType.i);
24+
peritext.refresh();
25+
const fragment = peritext.fragment(peritext.rangeAt(4, 10));
26+
fragment.refresh();
27+
const json = fragment.toJson();
28+
expect(json).toEqual([
29+
'',
30+
null,
31+
[
32+
0,
33+
null,
34+
'ef',
35+
[CommonSliceType.b, {inline: true}, 'g'],
36+
[CommonSliceType.i, {inline: true}, [CommonSliceType.b, {inline: true}, 'h']],
37+
[CommonSliceType.i, {inline: true}, 'i'],
38+
'j',
39+
],
40+
[0, null, 'klm'],
41+
]);
42+
});
43+
};
44+
45+
describe('Fragment.toJson()', () => {
46+
runAlphabetKitTestSuite(runTests);
47+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export type PeritextMlNode = string | PeritextMlElement;
2+
export type PeritextMlElement = [
3+
tag: string | number,
4+
attrs: null | PeritextMlAttributes,
5+
...children: PeritextMlNode[],
6+
];
7+
export interface PeritextMlAttributes {
8+
inline?: boolean;
9+
data?: unknown;
10+
}

0 commit comments

Comments
 (0)