Skip to content

Commit a05b09b

Browse files
committed
feat(json-crdt-extensions): 🎸 improve HTML import
1 parent 594ed9a commit a05b09b

File tree

6 files changed

+121
-15
lines changed

6 files changed

+121
-15
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {SliceBehavior} from '../slice/constants';
2+
13
export type PeritextMlNode = string | PeritextMlElement;
24

35
export type PeritextMlElement<Tag extends string | number = string | number, Data = unknown, Inline = boolean> = [
@@ -9,4 +11,5 @@ export type PeritextMlElement<Tag extends string | number = string | number, Dat
911
export interface PeritextMlAttributes<Data = unknown, Inline = boolean> {
1012
data?: Data;
1113
inline?: Inline;
14+
behavior?: SliceBehavior;
1215
}
Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import {Anchor} from '../../rga/constants';
12
import {CommonSliceType} from '../../slice';
2-
import {fromHtml} from '../import-html';
3+
import {SliceBehavior, SliceHeaderShift} from '../../slice/constants';
4+
import {fromHtml, toViewRange} from '../import-html';
35

46
describe('.fromHtml()', () => {
5-
it('a single paragraph', () => {
7+
test('a single paragraph', () => {
68
const html = '<p>Hello world</p>';
79
const peritextMl = fromHtml(html);
810
expect(peritextMl).toEqual([
@@ -12,7 +14,7 @@ describe('.fromHtml()', () => {
1214
]);
1315
});
1416

15-
it('a paragraph with trailing text', () => {
17+
test('a paragraph with trailing text', () => {
1618
const html = '<p>Hello world</p> more text';
1719
const peritextMl = fromHtml(html);
1820
expect(peritextMl).toEqual([
@@ -23,7 +25,7 @@ describe('.fromHtml()', () => {
2325
]);
2426
});
2527

26-
it('text formatted as italic', () => {
28+
test('text formatted as italic', () => {
2729
const html = '<p>Hello world</p>\n<p><em>italic</em> text, <i>more italic</i></p>';
2830
const peritextMl = fromHtml(html);
2931
expect(peritextMl).toEqual([
@@ -32,10 +34,38 @@ describe('.fromHtml()', () => {
3234
[CommonSliceType.p, null, 'Hello world'],
3335
'\n',
3436
[CommonSliceType.p, null,
35-
[CommonSliceType.i, null, 'italic'],
37+
[CommonSliceType.i, {behavior: SliceBehavior.One, inline:true}, 'italic'],
3638
' text, ',
37-
[CommonSliceType.i, null, 'more italic'],
39+
[CommonSliceType.i, {behavior: SliceBehavior.One, inline:true}, 'more italic'],
3840
],
3941
]);
4042
});
43+
});
44+
45+
describe('.toViewRange()', () => {
46+
test('plain text', () => {
47+
const html = 'this is plain text';
48+
const peritextMl = fromHtml(html);
49+
const view = toViewRange(peritextMl);
50+
expect(view).toEqual(['this is plain text', 0, []]);
51+
});
52+
53+
test('a single paragraph', () => {
54+
const html = '<p>Hello world</p>';
55+
const peritextMl = fromHtml(html);
56+
const view = toViewRange(peritextMl);
57+
expect(view).toEqual(['Hello world', 0, [[0, 0, 0, 0]]]);
58+
});
59+
60+
test('single inline annotation', () => {
61+
const html = 'here is some <em>italic</em> text';
62+
const peritextMl = fromHtml(html);
63+
const view = toViewRange(peritextMl);
64+
expect(view).toEqual(['here is some italic text', 0, [
65+
[(SliceBehavior.One << SliceHeaderShift.Behavior) +
66+
(Anchor.Before << SliceHeaderShift.X1Anchor) +
67+
(Anchor.After << SliceHeaderShift.X2Anchor),
68+
13, 19, CommonSliceType.i],
69+
]]);
70+
});
4171
});

src/json-crdt-extensions/peritext/lazy/import-html.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,66 @@ import {html as _html} from 'very-small-parser/lib/html';
22
import {fromHast as _fromHast} from 'very-small-parser/lib/html/json-ml/fromHast';
33
import {SliceTypeName} from '../slice';
44
import {registry as defaultRegistry} from '../registry/registry';
5+
import {SliceBehavior, SliceHeaderShift} from '../slice/constants';
6+
import {Anchor} from '../rga/constants';
57
import type {JsonMlNode} from 'very-small-parser/lib/html/json-ml/types';
68
import type {THtmlToken} from 'very-small-parser/lib/html/types';
79
import type {PeritextMlNode} from '../block/types';
810
import type {SliceRegistry} from '../registry/SliceRegistry';
11+
import type {ViewRange, ViewSlice} from '../editor/types';
12+
13+
/**
14+
* Flattens a {@link PeritextMlNode} tree structure into a {@link ViewRange}
15+
* flat string with annotation ranges.
16+
*/
17+
class ViewRangeBuilder {
18+
private text = '';
19+
private slices: ViewSlice[] = [];
20+
21+
constructor (private registry: SliceRegistry) {}
22+
23+
private build0(node: PeritextMlNode): void {
24+
if (typeof node === 'string') {
25+
this.text += node;
26+
return;
27+
}
28+
const [type, attr] = node;
29+
const start = this.text.length;
30+
const length = node.length;
31+
const inline = !!attr?.inline;
32+
for (let i = 2; i < length; i++) this.build0(node[i] as PeritextMlNode);
33+
if (!!type || type === 0) {
34+
let end: number = 0, header: number = 0;
35+
if (!inline) {
36+
end = start;
37+
header =
38+
(SliceBehavior.Marker << SliceHeaderShift.Behavior) +
39+
(Anchor.Before << SliceHeaderShift.X1Anchor) +
40+
(Anchor.Before << SliceHeaderShift.X2Anchor);
41+
} else {
42+
end = this.text.length;
43+
const behavior: SliceBehavior = attr?.behavior ?? SliceBehavior.Many;
44+
header =
45+
(behavior << SliceHeaderShift.Behavior) +
46+
(Anchor.Before << SliceHeaderShift.X1Anchor) +
47+
(Anchor.After << SliceHeaderShift.X2Anchor);
48+
}
49+
const slice: ViewSlice = [header, start, end, type];
50+
const data = attr?.data;
51+
if (data) slice.push(data);
52+
this.slices.push(slice);
53+
}
54+
}
55+
56+
public build(node: PeritextMlNode): ViewRange {
57+
this.build0(node);
58+
const view: ViewRange = [this.text, 0, this.slices];
59+
return view;
60+
}
61+
}
62+
63+
export const toViewRange = (node: PeritextMlNode, registry: SliceRegistry = defaultRegistry): ViewRange =>
64+
new ViewRangeBuilder(registry).build(node);
965

1066
export const fromJsonMl = (jsonml: JsonMlNode, registry: SliceRegistry = defaultRegistry): PeritextMlNode => {
1167
if (typeof jsonml === 'string') return jsonml;
@@ -29,9 +85,13 @@ export const fromJsonMl = (jsonml: JsonMlNode, registry: SliceRegistry = default
2985
const inline = attr['data-inline'] === 'true';
3086
if (data || inline) node[1] = {data, inline};
3187
}
32-
if (typeof node[0] === 'number' && node[0] < 0 && node[1]) node[1].inline = true;
88+
if (typeof node[0] === 'number' && node[0] < 0) {
89+
const attr = node[1] || {};
90+
attr.inline = true;
91+
node[1] = attr;
92+
}
3393
return node;
34-
};
94+
}
3595

3696
export const fromHast = (hast: THtmlToken, registry?: SliceRegistry): PeritextMlNode => {
3797
const jsonml = _fromHast(hast);

src/json-crdt-extensions/peritext/registry/SliceRegistry.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,26 @@ import {PeritextMlElement} from '../block/types';
22
import {NodeBuilder} from '../../../json-crdt-patch';
33
import type {JsonMlElement} from 'very-small-parser/lib/html/json-ml/types';
44
import type {FromHtmlConverter, SliceTypeDefinition, ToHtmlConverter} from './types';
5+
import {SliceBehavior} from '../slice/constants';
56

67
export class SliceRegistry {
78
private map: Map<string | number, SliceTypeDefinition<any, any, any>> = new Map();
89
private toHtmlMap: Map<string | number, ToHtmlConverter<any>> = new Map();
9-
private fromHtmlMap: Map<string, FromHtmlConverter[]> = new Map();
10+
private fromHtmlMap: Map<string, [def: SliceTypeDefinition<any, any, any>, converter: FromHtmlConverter][]> = new Map();
1011

1112
public add<Type extends number | string, Schema extends NodeBuilder, Inline extends boolean = true>(def: SliceTypeDefinition<Type, Schema, Inline>): void {
1213
const {type, toHtml, fromHtml} = def;
1314
this.map.set(type, def);
1415
if (toHtml) this.toHtmlMap.set(type, toHtml);
15-
if (fromHtml)
16+
if (fromHtml) {
17+
const fromHtmlMap = this.fromHtmlMap;
1618
for (const htmlTag in fromHtml) {
1719
const converter = fromHtml[htmlTag];
18-
const converters = this.fromHtmlMap.get(htmlTag) ?? [];
19-
converters.push(converter);
20-
this.fromHtmlMap.set(htmlTag, converters);
20+
const converters = fromHtmlMap.get(htmlTag) ?? [];
21+
converters.push([def, converter]);
22+
fromHtmlMap.set(htmlTag, converters);
2123
}
24+
}
2225
}
2326

2427
public toHtml(el: PeritextMlElement): ReturnType<ToHtmlConverter<any>> | undefined {
@@ -30,9 +33,14 @@ export class SliceRegistry {
3033
const tag = el[0] + '';
3134
const converters = this.fromHtmlMap.get(tag);
3235
if (converters) {
33-
for (const converter of converters) {
36+
for (const [def, converter] of converters) {
3437
const result = converter(el);
35-
if (result) return result;
38+
if (result) {
39+
const attr = result[1] ?? (result[1] = {});
40+
attr.inline = def.inline ?? (def.type < 0);
41+
attr.behavior = !attr.inline ? SliceBehavior.Marker : (def.behavior ?? SliceBehavior.Many);
42+
return result;
43+
}
3644
}
3745
}
3846
return;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {s} from "../../../json-crdt-patch";
22
import {JsonNodeView} from "../../../json-crdt/nodes";
33
import {SchemaToJsonNode} from "../../../json-crdt/schema/types";
44
import {CommonSliceType} from "../slice";
5+
import {SliceBehavior} from '../slice/constants';
56
import {SliceRegistry} from "./SliceRegistry";
67

78
/**
@@ -32,6 +33,7 @@ registry.add({
3233

3334
registry.add({
3435
type: CommonSliceType.i,
36+
behavior: SliceBehavior.One,
3537
schema: s.con(undefined),
3638
fromHtml: {
3739
em: () => [CommonSliceType.i, null],

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ import type {JsonNodeView} from '../../../json-crdt/nodes';
33
import type {SchemaToJsonNode} from '../../../json-crdt/schema/types';
44
import type {PeritextMlElement} from '../block/types';
55
import type {JsonMlElement} from 'very-small-parser/lib/html/json-ml/types';
6+
import type {SliceBehavior} from '../slice/constants';
67

78
export interface SliceTypeDefinition<Type extends number | string = number | string, Schema extends NodeBuilder = NodeBuilder, Inline extends boolean = true> {
89
type: Type;
910
schema: Schema;
11+
behavior?: SliceBehavior;
12+
inline?: boolean;
1013
toHtml?: ToHtmlConverter<PeritextMlElement<Type, JsonNodeView<SchemaToJsonNode<Schema>>, Inline>>;
1114
fromHtml?: {
1215
[htmlTag: string]: FromHtmlConverter<PeritextMlElement<Type, JsonNodeView<SchemaToJsonNode<Schema>>, Inline>>;

0 commit comments

Comments
 (0)