Skip to content

Commit 90608c7

Browse files
authored
Merge pull request #795 from streamich/fragment-imports
Fragment imports
2 parents 8447aab + 6f2988b commit 90608c7

File tree

2 files changed

+272
-6
lines changed

2 files changed

+272
-6
lines changed

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

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -688,9 +688,12 @@ export class Editor<T = string> implements Printable {
688688
const offset = r.start.viewPos();
689689
const viewSlices: ViewSlice[] = [];
690690
const view: ViewRange = [text, offset, viewSlices];
691-
const overlay = this.txt.overlay;
691+
const txt = this.txt;
692+
const overlay = txt.overlay;
692693
const slices = overlay.findOverlapping(r);
693694
for (const slice of slices) {
695+
const isSavedSlice = slice.id.sid === txt.model.clock.sid;
696+
if (!isSavedSlice) continue;
694697
const behavior = slice.behavior;
695698
switch (behavior) {
696699
case SliceBehavior.One:
@@ -715,19 +718,60 @@ export class Editor<T = string> implements Printable {
715718
public import(pos: number, view: ViewRange): void {
716719
const [text, offset, slices] = view;
717720
const txt = this.txt;
718-
txt.insAt(pos, text);
719721
const length = slices.length;
722+
const splits: ViewSlice[] = [];
723+
const annotations: ViewSlice[] = [];
724+
const texts: string[] = [];
725+
let start = 0;
720726
for (let i = 0; i < length; i++) {
721727
const slice = slices[i];
728+
const [header, x1] = slice;
729+
const behavior: SliceBehavior = (header & SliceHeaderMask.Behavior) >>> SliceHeaderShift.Behavior;
730+
if (behavior === SliceBehavior.Marker) {
731+
const end = x1 - offset;
732+
texts.push(text.slice(start, end));
733+
start = end + 1;
734+
splits.push(slice);
735+
} else annotations.push(slice);
736+
}
737+
const lastText = text.slice(start);
738+
const splitLength = splits.length;
739+
start = pos;
740+
for (let i = 0; i < splitLength; i++) {
741+
const str = texts[i];
742+
const split = splits[i];
743+
if (str) {
744+
txt.insAt(start, str);
745+
start += str.length;
746+
}
747+
if (split) {
748+
const [, , , type, data] = split;
749+
const after = txt.pointAt(start);
750+
after.refAfter();
751+
txt.savedSlices.insMarkerAfter(after.id, type, data);
752+
start += 1;
753+
}
754+
}
755+
if (lastText) txt.insAt(start, lastText);
756+
const annotationsLength = annotations.length;
757+
for (let i = 0; i < annotationsLength; i++) {
758+
const slice = annotations[i];
722759
const [header, x1, x2, type, data] = slice;
723760
const anchor1: Anchor = (header & SliceHeaderMask.X1Anchor) >>> SliceHeaderShift.X1Anchor;
724761
const anchor2: Anchor = (header & SliceHeaderMask.X2Anchor) >>> SliceHeaderShift.X2Anchor;
725762
const behavior: SliceBehavior = (header & SliceHeaderMask.Behavior) >>> SliceHeaderShift.Behavior;
726-
const range = txt.rangeAt(Math.max(0, x1 - offset + pos), x2 - x1);
727-
if (anchor1 === Anchor.Before) range.start.refBefore();
728-
else range.start.refAfter();
763+
const x1Src = x1 - offset;
764+
const x2Src = x2 - offset;
765+
const x1Capped = Math.max(0, x1Src);
766+
const x2Capped = Math.min(text.length, x2Src);
767+
const x1Dest = x1Capped + pos;
768+
const annotationLength = x2Capped - x1Capped;
769+
const range = txt.rangeAt(x1Dest, annotationLength);
770+
if (!!x1Dest && anchor1 === Anchor.After) range.start.refAfter();
771+
// else range.start.refBefore();
729772
if (anchor2 === Anchor.Before) range.end.refBefore();
730-
else range.end.refAfter();
773+
// else range.end.refAfter();
774+
if (range.end.isAbs()) range.end.refAfter();
731775
txt.savedSlices.ins(range, behavior, type, data);
732776
}
733777
}

src/json-crdt-extensions/peritext/editor/__tests__/Editor-export.spec.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {type Kit, runAlphabetKitTestSuite} from '../../__tests__/setup';
2+
import {Anchor} from '../../rga/constants';
23
import {CommonSliceType} from '../../slice';
4+
import {SliceBehavior, SliceHeaderShift} from '../../slice/constants';
35

46
const testSuite = (setup: () => Kit) => {
57
describe('.export()', () => {
@@ -10,6 +12,13 @@ const testSuite = (setup: () => Kit) => {
1012
expect(json).toEqual(['abcdefghijklmnopqrstuvwxyz', 0, []]);
1113
});
1214

15+
test('can export part of un-annotated document', () => {
16+
const {editor} = setup();
17+
editor.cursor.setAt(5, 5);
18+
const json = editor.export(editor.cursor);
19+
expect(json).toEqual(['fghij', 5, []]);
20+
});
21+
1322
test('range which contains bold text', () => {
1423
const {editor, peritext} = setup();
1524
editor.cursor.setAt(3, 3);
@@ -20,6 +29,18 @@ const testSuite = (setup: () => Kit) => {
2029
expect(json).toEqual(['cdefg', 2, [[expect.any(Number), 3, 6, 'bold']]]);
2130
});
2231

32+
test('exports only "saved" slices', () => {
33+
const {editor, peritext} = setup();
34+
editor.cursor.setAt(3, 3);
35+
editor.local.insOverwrite('italic');
36+
editor.saved.insOverwrite('bold');
37+
editor.extra.insOverwrite('underline');
38+
const range = peritext.rangeAt(2, 5);
39+
peritext.refresh();
40+
const json = editor.export(range);
41+
expect(json).toEqual(['cdefg', 2, [[expect.any(Number), 3, 6, 'bold']]]);
42+
});
43+
2344
test('range which start in bold text', () => {
2445
const {editor, peritext} = setup();
2546
editor.cursor.setAt(3, 10);
@@ -39,6 +60,54 @@ const testSuite = (setup: () => Kit) => {
3960
const json = editor.export(range);
4061
expect(json).toEqual(['abcde', 0, [[expect.any(Number), 3, 13, CommonSliceType.b]]]);
4162
});
63+
64+
test('can export <p> marker', () => {
65+
const {editor, peritext} = setup();
66+
editor.cursor.setAt(10);
67+
editor.saved.insMarker(CommonSliceType.p);
68+
const range = peritext.rangeAt(8, 5);
69+
peritext.refresh();
70+
const json = editor.export(range);
71+
const header =
72+
(SliceBehavior.Marker << SliceHeaderShift.Behavior) +
73+
(Anchor.Before << SliceHeaderShift.X1Anchor) +
74+
(Anchor.Before << SliceHeaderShift.X2Anchor);
75+
expect(json).toEqual(['ij\nkl', 8, [[header, 10, 10, CommonSliceType.p]]]);
76+
});
77+
78+
test('can export <p> marker, <blockquote> marker, and italic text', () => {
79+
const {editor, peritext} = setup();
80+
editor.cursor.setAt(15);
81+
editor.saved.insMarker(CommonSliceType.blockquote);
82+
editor.cursor.setAt(10);
83+
editor.saved.insMarker(CommonSliceType.p);
84+
editor.cursor.setAt(12, 2);
85+
editor.saved.insOverwrite(CommonSliceType.i);
86+
const range = peritext.rangeAt(8, 12);
87+
peritext.refresh();
88+
const json = editor.export(range);
89+
const pHeader =
90+
(SliceBehavior.Marker << SliceHeaderShift.Behavior) +
91+
(Anchor.Before << SliceHeaderShift.X1Anchor) +
92+
(Anchor.Before << SliceHeaderShift.X2Anchor);
93+
const iHeader =
94+
(SliceBehavior.One << SliceHeaderShift.Behavior) +
95+
(Anchor.Before << SliceHeaderShift.X1Anchor) +
96+
(Anchor.After << SliceHeaderShift.X2Anchor);
97+
const blockquoteHeader =
98+
(SliceBehavior.Marker << SliceHeaderShift.Behavior) +
99+
(Anchor.Before << SliceHeaderShift.X1Anchor) +
100+
(Anchor.Before << SliceHeaderShift.X2Anchor);
101+
expect(json).toEqual([
102+
'ij\nklmno\npqr',
103+
8,
104+
[
105+
[pHeader, 10, 10, CommonSliceType.p],
106+
[iHeader, 12, 14, CommonSliceType.i],
107+
[blockquoteHeader, 16, 16, CommonSliceType.blockquote],
108+
],
109+
]);
110+
});
42111
});
43112

44113
describe('.import()', () => {
@@ -66,6 +135,159 @@ const testSuite = (setup: () => Kit) => {
66135
expect(i2.text()).toBe('fghij');
67136
expect(!!i2.attr().bold).toBe(true);
68137
});
138+
139+
test('can import a contained <b> annotation', () => {
140+
const kit1 = setup();
141+
kit1.editor.cursor.setAt(0, 3);
142+
kit1.editor.saved.insOverwrite(CommonSliceType.b);
143+
kit1.peritext.refresh();
144+
const range = kit1.peritext.rangeAt(1, 1);
145+
const view = kit1.editor.export(range);
146+
kit1.editor.import(5, view);
147+
kit1.peritext.refresh();
148+
const jsonml = kit1.peritext.blocks.toJson();
149+
expect(jsonml).toEqual([
150+
'',
151+
null,
152+
[
153+
CommonSliceType.p,
154+
expect.any(Object),
155+
[CommonSliceType.b, expect.any(Object), 'abc'],
156+
'de',
157+
[CommonSliceType.b, expect.any(Object), 'b'],
158+
'fghijklmnopqrstuvwxyz',
159+
],
160+
]);
161+
const block = kit1.peritext.blocks.root.children[0];
162+
const inlines = [...block.texts()];
163+
const inline = inlines.find((i) => i.text() === 'b')!;
164+
expect(inline.start.anchor).toBe(Anchor.Before);
165+
expect(inline.end.anchor).toBe(Anchor.After);
166+
});
167+
168+
test('can import a contained <b> annotation (with end edge anchored to neighbor chars)', () => {
169+
const kit1 = setup();
170+
kit1.editor.cursor.setAt(0, 3);
171+
const start = kit1.editor.cursor.start.clone();
172+
const end = kit1.editor.cursor.end.clone();
173+
start.refAfter();
174+
end.refBefore();
175+
kit1.editor.cursor.set(start, end);
176+
kit1.editor.saved.insOverwrite(CommonSliceType.b);
177+
kit1.peritext.refresh();
178+
const range = kit1.peritext.rangeAt(1, 1);
179+
const view = kit1.editor.export(range);
180+
kit1.editor.import(5, view);
181+
kit1.peritext.refresh();
182+
const jsonml = kit1.peritext.blocks.toJson();
183+
expect(jsonml).toEqual([
184+
'',
185+
null,
186+
[
187+
CommonSliceType.p,
188+
expect.any(Object),
189+
[CommonSliceType.b, expect.any(Object), 'abc'],
190+
'de',
191+
[CommonSliceType.b, expect.any(Object), 'b'],
192+
'fghijklmnopqrstuvwxyz',
193+
],
194+
]);
195+
const block = kit1.peritext.blocks.root.children[0];
196+
const inlines = [...block.texts()];
197+
const inline = inlines.find((i) => i.text() === 'b')!;
198+
expect(inline.start.anchor).toBe(Anchor.After);
199+
expect(inline.end.anchor).toBe(Anchor.Before);
200+
});
201+
202+
test('annotation start edge cannot point to ABS start', () => {
203+
const kit1 = setup();
204+
kit1.editor.cursor.setAt(1, 2);
205+
const start = kit1.editor.cursor.start.clone();
206+
const end = kit1.editor.cursor.end.clone();
207+
start.refAfter();
208+
end.refBefore();
209+
kit1.editor.cursor.set(start, end);
210+
kit1.editor.saved.insOverwrite(CommonSliceType.b);
211+
kit1.editor.delCursors();
212+
kit1.peritext.refresh();
213+
const range = kit1.peritext.rangeAt(1, 1);
214+
const view = kit1.editor.export(range);
215+
kit1.editor.import(0, view);
216+
kit1.peritext.refresh();
217+
const jsonml = kit1.peritext.blocks.toJson();
218+
expect(jsonml).toEqual([
219+
'',
220+
null,
221+
[
222+
CommonSliceType.p,
223+
expect.any(Object),
224+
[CommonSliceType.b, expect.any(Object), 'b'],
225+
'a',
226+
[CommonSliceType.b, expect.any(Object), 'bc'],
227+
'defghijklmnopqrstuvwxyz',
228+
],
229+
]);
230+
const block = kit1.peritext.blocks.root.children[0];
231+
const inlines = [...block.texts()];
232+
const inline = inlines.find((i) => i.text() === 'b')!;
233+
expect(inline.start.anchor).toBe(Anchor.Before);
234+
expect(inline.end.anchor).toBe(Anchor.Before);
235+
});
236+
237+
test('annotation end edge cannot point to ABS end', () => {
238+
const kit1 = setup();
239+
kit1.editor.cursor.setAt(1, 2);
240+
const start = kit1.editor.cursor.start.clone();
241+
const end = kit1.editor.cursor.end.clone();
242+
start.refAfter();
243+
end.refBefore();
244+
kit1.editor.cursor.set(start, end);
245+
kit1.editor.saved.insOverwrite(CommonSliceType.b);
246+
kit1.editor.delCursors();
247+
kit1.peritext.refresh();
248+
const range = kit1.peritext.rangeAt(1, 1);
249+
const view = kit1.editor.export(range);
250+
const length = kit1.peritext.strApi().length();
251+
kit1.editor.import(length, view);
252+
kit1.peritext.refresh();
253+
const jsonml = kit1.peritext.blocks.toJson();
254+
expect(jsonml).toEqual([
255+
'',
256+
null,
257+
[
258+
CommonSliceType.p,
259+
expect.any(Object),
260+
'a',
261+
[CommonSliceType.b, expect.any(Object), 'bc'],
262+
'defghijklmnopqrstuvwxyz',
263+
[CommonSliceType.b, expect.any(Object), 'b'],
264+
],
265+
]);
266+
const block = kit1.peritext.blocks.root.children[0];
267+
const inlines = [...block.texts()];
268+
const inline = inlines.find((i) => i.text() === 'b')!;
269+
expect(inline.start.anchor).toBe(Anchor.After);
270+
expect(inline.end.anchor).toBe(Anchor.After);
271+
});
272+
273+
test('can copy a paragraph split', () => {
274+
const kit1 = setup();
275+
const kit2 = setup();
276+
kit1.editor.cursor.setAt(5);
277+
kit1.editor.saved.insMarker(CommonSliceType.p);
278+
kit1.editor.cursor.setAt(3, 5);
279+
kit1.peritext.refresh();
280+
const json = kit1.editor.export(kit1.editor.cursor);
281+
kit2.editor.import(0, json);
282+
kit2.peritext.refresh();
283+
const json2 = kit2.peritext.blocks.toJson();
284+
expect(json2).toEqual([
285+
'',
286+
null,
287+
[CommonSliceType.p, null, 'de'],
288+
[CommonSliceType.p, null, 'fgabcdefghijklmnopqrstuvwxyz'],
289+
]);
290+
});
69291
});
70292
};
71293

0 commit comments

Comments
 (0)