Skip to content

Commit ce93fab

Browse files
authored
Merge pull request #837 from streamich/peritext-registry-improvements
Peritext registry improvements
2 parents 5529c90 + 98f56ec commit ce93fab

File tree

13 files changed

+411
-147
lines changed

13 files changed

+411
-147
lines changed

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import {formatType} from '../slice/util';
77
import {Range} from '../rga/Range';
88
import type {Point} from '../rga/Point';
99
import type {OverlayPoint} from '../overlay/OverlayPoint';
10-
import type {Path} from '@jsonjoy.com/json-pointer';
1110
import type {Printable} from 'tree-dump';
1211
import type {Peritext} from '../Peritext';
1312
import type {Stateful} from '../types';
1413
import type {OverlayTuple} from '../overlay/types';
1514
import type {PeritextMlAttributes, PeritextMlElement} from './types';
15+
import type {SliceTypeSteps} from '../slice';
1616

1717
export interface IBlock {
18-
readonly path: Path;
18+
readonly path: SliceTypeSteps;
1919
readonly parent: IBlock | null;
2020
}
2121

@@ -26,7 +26,7 @@ export class Block<T = string, Attr = unknown> extends Range<T> implements IBloc
2626

2727
constructor(
2828
public readonly txt: Peritext<T>,
29-
public readonly path: Path,
29+
public readonly path: SliceTypeSteps,
3030
public readonly marker: MarkerOverlayPoint<T> | undefined,
3131
public start: Point<T>,
3232
public end: Point<T>,
@@ -47,7 +47,9 @@ export class Block<T = string, Attr = unknown> extends Range<T> implements IBloc
4747
public tag(): number | string {
4848
const path = this.path;
4949
const length = path.length;
50-
return length ? path[length - 1] : '';
50+
if (!length) return '';
51+
const step = path[length - 1];
52+
return Array.isArray(step) ? step[0] : step;
5153
}
5254

5355
public attr(): Attr | undefined {

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

+2-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import {commonLength} from '../util/commonLength';
33
import {printTree} from 'tree-dump/lib/printTree';
44
import {LeafBlock} from './LeafBlock';
55
import {Range} from '../rga/Range';
6-
import {CommonSliceType} from '../slice';
6+
import {CommonSliceType, type SliceTypeSteps} from '../slice';
77
import type {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint';
8-
import type {Path} from '@jsonjoy.com/json-pointer';
98
import type {Stateful} from '../types';
109
import type {Printable} from 'tree-dump/lib/types';
1110
import type {Peritext} from '../Peritext';
@@ -55,7 +54,7 @@ export class Fragment<T = string> extends Range<T> implements Printable, Statefu
5554

5655
private insertBlock(
5756
parent: Block<T>,
58-
path: Path,
57+
path: SliceTypeSteps,
5958
marker: undefined | MarkerOverlayPoint<T>,
6059
end: Point<T> = this.end,
6160
): Block<T> {

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

+35
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,38 @@ describe('refresh()', () => {
4040
expect(hash2).toBe(block.hash);
4141
});
4242
});
43+
44+
describe('discriminants', () => {
45+
test('can construct a blockquote with two paragraphs', () => {
46+
const {peritext, editor} = setupHelloWorldKit();
47+
editor.cursor.setAt(8);
48+
editor.saved.insMarker(['blockquote', 'p']);
49+
editor.cursor.setAt(4);
50+
editor.saved.insMarker(['blockquote', 'p']);
51+
peritext.refresh();
52+
const block2 = peritext.blocks.root.children[1];
53+
expect(block2.children.length).toBe(2);
54+
expect(block2.children[0].path).toEqual(['blockquote', 'p']);
55+
expect(block2.children[0].text()).toBe('o wo');
56+
expect(block2.children[1].path).toEqual(['blockquote', 'p']);
57+
expect(block2.children[1].text()).toBe('rld');
58+
});
59+
60+
test('can construct a two blockquotes with a paragraphs in each', () => {
61+
const {peritext, editor} = setupHelloWorldKit();
62+
editor.cursor.setAt(8);
63+
editor.saved.insMarker(['blockquote', 'p']);
64+
editor.cursor.setAt(4);
65+
editor.saved.insMarker([['blockquote', 1], 'p']);
66+
peritext.refresh();
67+
expect(peritext.blocks.root.children.length).toBe(3);
68+
const block2 = peritext.blocks.root.children[1];
69+
const block3 = peritext.blocks.root.children[2];
70+
expect(block2.children.length).toBe(1);
71+
expect(block2.children[0].path).toEqual([['blockquote', 1], 'p']);
72+
expect(block2.children[0].text()).toBe('o wo');
73+
expect(block3.children.length).toBe(1);
74+
expect(block3.children[0].path).toEqual(['blockquote', 'p']);
75+
expect(block3.children[0].text()).toBe('rld');
76+
});
77+
});

src/json-crdt-extensions/peritext/editor/Editor.ts

+46
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {Point} from '../rga/Point';
1919
import type {Range} from '../rga/Range';
2020
import type {CharIterator, CharPredicate, Position, TextRangeUnit, ViewStyle, ViewRange, ViewSlice} from './types';
2121
import type {Printable} from 'tree-dump';
22+
import type {MarkerSlice} from '../slice/MarkerSlice';
2223

2324
/**
2425
* For inline boolean ("Overwrite") slices, both range endpoints should be
@@ -693,6 +694,51 @@ export class Editor<T = string> implements Printable {
693694
}
694695
}
695696

697+
/**
698+
* Returns block split marker of the block inside which the point is located.
699+
*
700+
* @param point The point to get the marker at.
701+
* @returns The split marker at the point, if any.
702+
*/
703+
public getMarker(point: Point<T>): MarkerSlice<T> | undefined {
704+
return this.txt.overlay.getOrNextLowerMarker(point)?.marker;
705+
}
706+
707+
/**
708+
* Insert a block split at the start of the document. The start of the
709+
* document is defined as immediately after all deleted characters starting
710+
* from the beginning of the document, or as the ABS start of the document if
711+
* there are no deleted characters.
712+
*
713+
* @param type The type of the marker.
714+
* @returns The inserted marker slice.
715+
*/
716+
public insStartMarker(type: SliceType): MarkerSlice<T> {
717+
const txt = this.txt;
718+
const start = txt.pointStart() ?? txt.pointAbsStart();
719+
start.refAfter();
720+
return this.txt.savedSlices.insMarkerAfter(start.id, type);
721+
}
722+
723+
/**
724+
* Find the block split marker which contains the point and sets the block
725+
* type of the marker. If there is no block split marker at the point, a new
726+
* block split marker is inserted at the beginning of the document with the
727+
* specified block type.
728+
*
729+
* @param point The point at which to set the block type.
730+
* @param type The new block type.
731+
* @returns The marker slice at the point, or a new marker slice if there is none.
732+
*/
733+
public setBlockType(point: Point<T>, type: SliceType): MarkerSlice<T> {
734+
const marker = this.getMarker(point);
735+
if (marker) {
736+
marker.update({type});
737+
return marker;
738+
}
739+
return this.insStartMarker(type);
740+
}
741+
696742
// ---------------------------------------------------------- export / import
697743

698744
public export(range: Range<T>): ViewRange {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {type Kit, runAlphabetKitTestSuite} from '../../__tests__/setup';
2+
import {SliceTypeCon} from '../../slice/constants';
3+
import {create} from '../../transfer/create';
4+
5+
const runTests = (setup: () => Kit) => {
6+
test('can join two blockquotes', () => {
7+
const {peritext, editor} = setup();
8+
editor.cursor.setAt(3);
9+
editor.saved.insMarker([SliceTypeCon.blockquote, SliceTypeCon.p]);
10+
editor.cursor.setAt(7);
11+
editor.saved.insMarker([[SliceTypeCon.blockquote, 1], SliceTypeCon.p]);
12+
peritext.refresh();
13+
const transfer = create(peritext);
14+
const html1 = transfer.toHtml(peritext.rangeAll()!);
15+
expect(html1).toBe(
16+
'<p>abc</p><blockquote><p>def</p></blockquote><blockquote><p>ghijklmnopqrstuvwxyz</p></blockquote>',
17+
);
18+
editor.cursor.setAt(10);
19+
editor.setBlockType(editor.cursor.start, [SliceTypeCon.blockquote, SliceTypeCon.p]);
20+
peritext.refresh();
21+
const html2 = transfer.toHtml(peritext.rangeAll()!);
22+
expect(html2).toBe('<p>abc</p><blockquote><p>def</p><p>ghijklmnopqrstuvwxyz</p></blockquote>');
23+
});
24+
25+
test('can split two blockquotes', () => {
26+
const {peritext, editor} = setup();
27+
editor.cursor.setAt(3);
28+
editor.saved.insMarker([SliceTypeCon.blockquote, SliceTypeCon.p]);
29+
editor.cursor.setAt(7);
30+
editor.saved.insMarker([SliceTypeCon.blockquote, SliceTypeCon.p]);
31+
peritext.refresh();
32+
const transfer = create(peritext);
33+
const html1 = transfer.toHtml(peritext.rangeAll()!);
34+
expect(html1).toBe('<p>abc</p><blockquote><p>def</p><p>ghijklmnopqrstuvwxyz</p></blockquote>');
35+
const point = peritext.pointAt(10);
36+
editor.setBlockType(point, [[SliceTypeCon.blockquote, 1], SliceTypeCon.p]);
37+
peritext.refresh();
38+
const html2 = transfer.toHtml(peritext.rangeAll()!);
39+
expect(html2).toBe(
40+
'<p>abc</p><blockquote><p>def</p></blockquote><blockquote><p>ghijklmnopqrstuvwxyz</p></blockquote>',
41+
);
42+
});
43+
};
44+
45+
runAlphabetKitTestSuite(runTests);

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

+110-37
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,137 @@
1-
import {SliceBehavior} from '../slice/constants';
1+
import {SliceBehavior, type SliceTypeCon} from '../slice/constants';
22
import {CommonSliceType} from '../slice';
33
import type {PeritextMlElement} from '../block/types';
44
import type {NodeBuilder} from '../../../json-crdt-patch';
55
import type {JsonMlElement} from 'very-small-parser/lib/html/json-ml/types';
6-
import type {FromHtmlConverter, SliceTypeDefinition, ToHtmlConverter} from './types';
6+
import type {FromHtmlConverter, ToHtmlConverter} from './types';
7+
import type {JsonNodeView} from '../../../json-crdt/nodes';
8+
import type {SchemaToJsonNode} from '../../../json-crdt/schema/types';
9+
10+
export type TagType = SliceTypeCon | number | string;
11+
12+
export class SliceRegistryEntry<
13+
Behavior extends SliceBehavior = SliceBehavior,
14+
Tag extends TagType = TagType,
15+
Schema extends NodeBuilder = NodeBuilder,
16+
> {
17+
public isInline(): boolean {
18+
return this.behavior !== SliceBehavior.Marker;
19+
}
20+
21+
constructor(
22+
public readonly behavior: Behavior,
23+
24+
/**
25+
* The tag name of this slice. The tag is one step in the type path of the
26+
* slice. For example, below is a type path composed of three steps:
27+
*
28+
* ```js
29+
* ['ul', 'li', 'p']
30+
* ```
31+
*
32+
* Tag types are normally numbers of type {@link SliceTypeCon}, however,
33+
* they can also be any arbitrary strings or numbers.
34+
*/
35+
public readonly tag: Tag,
36+
37+
/**
38+
* Default expected schema of the slice data.
39+
*/
40+
public readonly schema: Schema,
41+
42+
/**
43+
* This property is relevant only for block split markers. It specifies
44+
* whether the block split marker is a container for other block elements.
45+
*
46+
* For example, a `blockquote` is a container for `paragraph` elements,
47+
* however, a `paragraph` is not a container (it can only contain inline
48+
* elements).
49+
*
50+
* If the marker slice is of the container sort, they tag can appear in the
51+
* path steps of the type:
52+
*
53+
* ```
54+
*
55+
* ```
56+
*/
57+
public readonly container: boolean = false,
58+
59+
/**
60+
* Converts a node of this type to HTML representation: returns the HTML tag
61+
* and attributes. The method receives {@link PeritextMlElement} as an
62+
* argument, which is a tuple of internal HTML-like representation of the
63+
* node.
64+
*/
65+
public readonly toHtml:
66+
| ToHtmlConverter<
67+
PeritextMlElement<
68+
Tag,
69+
JsonNodeView<SchemaToJsonNode<Schema>>,
70+
Behavior extends SliceBehavior.Marker ? false : true
71+
>
72+
>
73+
| undefined = void 0,
74+
75+
/**
76+
* Specifies a mapping of converters from HTML {@link JsonMlElement} to
77+
* {@link PeritextMlElement}. This way a slice type can specify multiple
78+
* HTML tags that are converted to the same slice type.
79+
*
80+
* For example, both, `<b>` and `<strong>` tags can be converted to the
81+
* {@link SliceTypeCon.b} slice type.
82+
*/
83+
public readonly fromHtml?: {
84+
[htmlTag: string]: FromHtmlConverter<
85+
PeritextMlElement<
86+
Tag,
87+
JsonNodeView<SchemaToJsonNode<Schema>>,
88+
Behavior extends SliceBehavior.Marker ? false : true
89+
>
90+
>;
91+
},
92+
) {}
93+
}
794

895
/**
9-
* @todo Consider moving the registry under the `/transfer` directory.
96+
* @todo Consider moving the registry under the `/transfer` directory. Or maybe
97+
* `/slices` directory.
1098
*/
1199
export class SliceRegistry {
12-
private map: Map<string | number, SliceTypeDefinition<any, any, any>> = new Map();
13-
private toHtmlMap: Map<string | number, ToHtmlConverter<any>> = new Map();
14-
private fromHtmlMap: Map<string, [def: SliceTypeDefinition<any, any, any>, converter: FromHtmlConverter][]> =
15-
new Map();
100+
private map: Map<TagType, SliceRegistryEntry> = new Map();
101+
private _fromHtml: Map<string, [entry: SliceRegistryEntry, converter: FromHtmlConverter][]> = new Map();
16102

17-
public add<Type extends number | string, Schema extends NodeBuilder, Inline extends boolean = true>(
18-
def: SliceTypeDefinition<Type, Schema, Inline>,
19-
): void {
20-
const {type, toHtml, fromHtml} = def;
21-
const fromHtmlMap = this.fromHtmlMap;
22-
if (toHtml) this.toHtmlMap.set(type, toHtml);
103+
public add(entry: SliceRegistryEntry<any, any, any>): void {
104+
const {tag, fromHtml} = entry;
105+
const _fromHtml = this._fromHtml;
23106
if (fromHtml) {
24107
for (const htmlTag in fromHtml) {
25108
const converter = fromHtml[htmlTag];
26-
const converters = fromHtmlMap.get(htmlTag) ?? [];
27-
converters.push([def, converter]);
28-
fromHtmlMap.set(htmlTag, converters);
109+
const converters = _fromHtml.get(htmlTag) ?? [];
110+
converters.push([entry, converter]);
111+
_fromHtml.set(htmlTag, converters);
29112
}
30113
}
31-
const tag = CommonSliceType[type as any];
32-
if (tag && typeof tag === 'string') {
33-
fromHtmlMap.set(tag, [[def, () => [type, null]]]);
34-
}
35-
}
36-
37-
public def<Type extends number | string, Schema extends NodeBuilder, Inline extends boolean = true>(
38-
type: Type,
39-
schema: Schema,
40-
behavior: SliceBehavior,
41-
inline: boolean,
42-
rest: Omit<SliceTypeDefinition<Type, Schema, Inline>, 'type' | 'schema' | 'behavior' | 'inline'> = {},
43-
): void {
44-
this.add({type, schema, behavior, inline, ...rest});
114+
const tagStr = CommonSliceType[tag as SliceTypeCon];
115+
if (tagStr && typeof tagStr === 'string') _fromHtml.set(tagStr, [[entry, () => [tag, null]]]);
45116
}
46117

47118
public toHtml(el: PeritextMlElement): ReturnType<ToHtmlConverter<any>> | undefined {
48-
const converter = this.toHtmlMap.get(el[0]);
49-
return converter ? converter(el) : undefined;
119+
const entry = this.map.get(el[0]);
120+
return entry?.toHtml ? entry?.toHtml(el) : void 0;
50121
}
51122

52123
public fromHtml(el: JsonMlElement): PeritextMlElement | undefined {
53124
const tag = el[0] + '';
54-
const converters = this.fromHtmlMap.get(tag);
125+
const converters = this._fromHtml.get(tag);
55126
if (converters) {
56-
for (const [def, converter] of converters) {
127+
for (const [entry, converter] of converters) {
57128
const result = converter(el);
58129
if (result) {
59-
const attr = result[1] ?? (result[1] = {});
60-
attr.inline = def.inline ?? def.type < 0;
61-
attr.behavior = !attr.inline ? SliceBehavior.Marker : (def.behavior ?? SliceBehavior.Many);
130+
if (entry.isInline()) {
131+
const attr = result[1] ?? (result[1] = {});
132+
attr.inline = entry.isInline();
133+
attr.behavior = !attr.inline ? SliceBehavior.Marker : (entry.behavior ?? SliceBehavior.Many);
134+
}
62135
return result;
63136
}
64137
}

0 commit comments

Comments
 (0)