Skip to content

Commit

Permalink
Merge pull request #795 from streamich/fragment-imports
Browse files Browse the repository at this point in the history
Fragment imports
  • Loading branch information
streamich authored Dec 22, 2024
2 parents 8447aab + 6f2988b commit 90608c7
Show file tree
Hide file tree
Showing 2 changed files with 272 additions and 6 deletions.
56 changes: 50 additions & 6 deletions src/json-crdt-extensions/peritext/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,9 +688,12 @@ export class Editor<T = string> implements Printable {
const offset = r.start.viewPos();
const viewSlices: ViewSlice[] = [];
const view: ViewRange = [text, offset, viewSlices];
const overlay = this.txt.overlay;
const txt = this.txt;
const overlay = txt.overlay;
const slices = overlay.findOverlapping(r);
for (const slice of slices) {
const isSavedSlice = slice.id.sid === txt.model.clock.sid;
if (!isSavedSlice) continue;
const behavior = slice.behavior;
switch (behavior) {
case SliceBehavior.One:
Expand All @@ -715,19 +718,60 @@ export class Editor<T = string> implements Printable {
public import(pos: number, view: ViewRange): void {
const [text, offset, slices] = view;
const txt = this.txt;
txt.insAt(pos, text);
const length = slices.length;
const splits: ViewSlice[] = [];
const annotations: ViewSlice[] = [];
const texts: string[] = [];
let start = 0;
for (let i = 0; i < length; i++) {
const slice = slices[i];
const [header, x1] = slice;
const behavior: SliceBehavior = (header & SliceHeaderMask.Behavior) >>> SliceHeaderShift.Behavior;
if (behavior === SliceBehavior.Marker) {
const end = x1 - offset;
texts.push(text.slice(start, end));
start = end + 1;
splits.push(slice);
} else annotations.push(slice);
}
const lastText = text.slice(start);
const splitLength = splits.length;
start = pos;
for (let i = 0; i < splitLength; i++) {
const str = texts[i];
const split = splits[i];
if (str) {
txt.insAt(start, str);
start += str.length;
}
if (split) {
const [, , , type, data] = split;
const after = txt.pointAt(start);
after.refAfter();
txt.savedSlices.insMarkerAfter(after.id, type, data);
start += 1;
}
}
if (lastText) txt.insAt(start, lastText);
const annotationsLength = annotations.length;
for (let i = 0; i < annotationsLength; i++) {
const slice = annotations[i];
const [header, x1, x2, type, data] = slice;
const anchor1: Anchor = (header & SliceHeaderMask.X1Anchor) >>> SliceHeaderShift.X1Anchor;
const anchor2: Anchor = (header & SliceHeaderMask.X2Anchor) >>> SliceHeaderShift.X2Anchor;
const behavior: SliceBehavior = (header & SliceHeaderMask.Behavior) >>> SliceHeaderShift.Behavior;
const range = txt.rangeAt(Math.max(0, x1 - offset + pos), x2 - x1);
if (anchor1 === Anchor.Before) range.start.refBefore();
else range.start.refAfter();
const x1Src = x1 - offset;
const x2Src = x2 - offset;
const x1Capped = Math.max(0, x1Src);
const x2Capped = Math.min(text.length, x2Src);
const x1Dest = x1Capped + pos;
const annotationLength = x2Capped - x1Capped;
const range = txt.rangeAt(x1Dest, annotationLength);
if (!!x1Dest && anchor1 === Anchor.After) range.start.refAfter();
// else range.start.refBefore();
if (anchor2 === Anchor.Before) range.end.refBefore();
else range.end.refAfter();
// else range.end.refAfter();
if (range.end.isAbs()) range.end.refAfter();
txt.savedSlices.ins(range, behavior, type, data);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {type Kit, runAlphabetKitTestSuite} from '../../__tests__/setup';
import {Anchor} from '../../rga/constants';
import {CommonSliceType} from '../../slice';
import {SliceBehavior, SliceHeaderShift} from '../../slice/constants';

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

test('can export part of un-annotated document', () => {
const {editor} = setup();
editor.cursor.setAt(5, 5);
const json = editor.export(editor.cursor);
expect(json).toEqual(['fghij', 5, []]);
});

test('range which contains bold text', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(3, 3);
Expand All @@ -20,6 +29,18 @@ const testSuite = (setup: () => Kit) => {
expect(json).toEqual(['cdefg', 2, [[expect.any(Number), 3, 6, 'bold']]]);
});

test('exports only "saved" slices', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(3, 3);
editor.local.insOverwrite('italic');
editor.saved.insOverwrite('bold');
editor.extra.insOverwrite('underline');
const range = peritext.rangeAt(2, 5);
peritext.refresh();
const json = editor.export(range);
expect(json).toEqual(['cdefg', 2, [[expect.any(Number), 3, 6, 'bold']]]);
});

test('range which start in bold text', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(3, 10);
Expand All @@ -39,6 +60,54 @@ const testSuite = (setup: () => Kit) => {
const json = editor.export(range);
expect(json).toEqual(['abcde', 0, [[expect.any(Number), 3, 13, CommonSliceType.b]]]);
});

test('can export <p> marker', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(10);
editor.saved.insMarker(CommonSliceType.p);
const range = peritext.rangeAt(8, 5);
peritext.refresh();
const json = editor.export(range);
const header =
(SliceBehavior.Marker << SliceHeaderShift.Behavior) +
(Anchor.Before << SliceHeaderShift.X1Anchor) +
(Anchor.Before << SliceHeaderShift.X2Anchor);
expect(json).toEqual(['ij\nkl', 8, [[header, 10, 10, CommonSliceType.p]]]);
});

test('can export <p> marker, <blockquote> marker, and italic text', () => {
const {editor, peritext} = setup();
editor.cursor.setAt(15);
editor.saved.insMarker(CommonSliceType.blockquote);
editor.cursor.setAt(10);
editor.saved.insMarker(CommonSliceType.p);
editor.cursor.setAt(12, 2);
editor.saved.insOverwrite(CommonSliceType.i);
const range = peritext.rangeAt(8, 12);
peritext.refresh();
const json = editor.export(range);
const pHeader =
(SliceBehavior.Marker << SliceHeaderShift.Behavior) +
(Anchor.Before << SliceHeaderShift.X1Anchor) +
(Anchor.Before << SliceHeaderShift.X2Anchor);
const iHeader =
(SliceBehavior.One << SliceHeaderShift.Behavior) +
(Anchor.Before << SliceHeaderShift.X1Anchor) +
(Anchor.After << SliceHeaderShift.X2Anchor);
const blockquoteHeader =
(SliceBehavior.Marker << SliceHeaderShift.Behavior) +
(Anchor.Before << SliceHeaderShift.X1Anchor) +
(Anchor.Before << SliceHeaderShift.X2Anchor);
expect(json).toEqual([
'ij\nklmno\npqr',
8,
[
[pHeader, 10, 10, CommonSliceType.p],
[iHeader, 12, 14, CommonSliceType.i],
[blockquoteHeader, 16, 16, CommonSliceType.blockquote],
],
]);
});
});

describe('.import()', () => {
Expand Down Expand Up @@ -66,6 +135,159 @@ const testSuite = (setup: () => Kit) => {
expect(i2.text()).toBe('fghij');
expect(!!i2.attr().bold).toBe(true);
});

test('can import a contained <b> annotation', () => {
const kit1 = setup();
kit1.editor.cursor.setAt(0, 3);
kit1.editor.saved.insOverwrite(CommonSliceType.b);
kit1.peritext.refresh();
const range = kit1.peritext.rangeAt(1, 1);
const view = kit1.editor.export(range);
kit1.editor.import(5, view);
kit1.peritext.refresh();
const jsonml = kit1.peritext.blocks.toJson();
expect(jsonml).toEqual([
'',
null,
[
CommonSliceType.p,
expect.any(Object),
[CommonSliceType.b, expect.any(Object), 'abc'],
'de',
[CommonSliceType.b, expect.any(Object), 'b'],
'fghijklmnopqrstuvwxyz',
],
]);
const block = kit1.peritext.blocks.root.children[0];
const inlines = [...block.texts()];
const inline = inlines.find((i) => i.text() === 'b')!;
expect(inline.start.anchor).toBe(Anchor.Before);
expect(inline.end.anchor).toBe(Anchor.After);
});

test('can import a contained <b> annotation (with end edge anchored to neighbor chars)', () => {
const kit1 = setup();
kit1.editor.cursor.setAt(0, 3);
const start = kit1.editor.cursor.start.clone();
const end = kit1.editor.cursor.end.clone();
start.refAfter();
end.refBefore();
kit1.editor.cursor.set(start, end);
kit1.editor.saved.insOverwrite(CommonSliceType.b);
kit1.peritext.refresh();
const range = kit1.peritext.rangeAt(1, 1);
const view = kit1.editor.export(range);
kit1.editor.import(5, view);
kit1.peritext.refresh();
const jsonml = kit1.peritext.blocks.toJson();
expect(jsonml).toEqual([
'',
null,
[
CommonSliceType.p,
expect.any(Object),
[CommonSliceType.b, expect.any(Object), 'abc'],
'de',
[CommonSliceType.b, expect.any(Object), 'b'],
'fghijklmnopqrstuvwxyz',
],
]);
const block = kit1.peritext.blocks.root.children[0];
const inlines = [...block.texts()];
const inline = inlines.find((i) => i.text() === 'b')!;
expect(inline.start.anchor).toBe(Anchor.After);
expect(inline.end.anchor).toBe(Anchor.Before);
});

test('annotation start edge cannot point to ABS start', () => {
const kit1 = setup();
kit1.editor.cursor.setAt(1, 2);
const start = kit1.editor.cursor.start.clone();
const end = kit1.editor.cursor.end.clone();
start.refAfter();
end.refBefore();
kit1.editor.cursor.set(start, end);
kit1.editor.saved.insOverwrite(CommonSliceType.b);
kit1.editor.delCursors();
kit1.peritext.refresh();
const range = kit1.peritext.rangeAt(1, 1);
const view = kit1.editor.export(range);
kit1.editor.import(0, view);
kit1.peritext.refresh();
const jsonml = kit1.peritext.blocks.toJson();
expect(jsonml).toEqual([
'',
null,
[
CommonSliceType.p,
expect.any(Object),
[CommonSliceType.b, expect.any(Object), 'b'],
'a',
[CommonSliceType.b, expect.any(Object), 'bc'],
'defghijklmnopqrstuvwxyz',
],
]);
const block = kit1.peritext.blocks.root.children[0];
const inlines = [...block.texts()];
const inline = inlines.find((i) => i.text() === 'b')!;
expect(inline.start.anchor).toBe(Anchor.Before);
expect(inline.end.anchor).toBe(Anchor.Before);
});

test('annotation end edge cannot point to ABS end', () => {
const kit1 = setup();
kit1.editor.cursor.setAt(1, 2);
const start = kit1.editor.cursor.start.clone();
const end = kit1.editor.cursor.end.clone();
start.refAfter();
end.refBefore();
kit1.editor.cursor.set(start, end);
kit1.editor.saved.insOverwrite(CommonSliceType.b);
kit1.editor.delCursors();
kit1.peritext.refresh();
const range = kit1.peritext.rangeAt(1, 1);
const view = kit1.editor.export(range);
const length = kit1.peritext.strApi().length();
kit1.editor.import(length, view);
kit1.peritext.refresh();
const jsonml = kit1.peritext.blocks.toJson();
expect(jsonml).toEqual([
'',
null,
[
CommonSliceType.p,
expect.any(Object),
'a',
[CommonSliceType.b, expect.any(Object), 'bc'],
'defghijklmnopqrstuvwxyz',
[CommonSliceType.b, expect.any(Object), 'b'],
],
]);
const block = kit1.peritext.blocks.root.children[0];
const inlines = [...block.texts()];
const inline = inlines.find((i) => i.text() === 'b')!;
expect(inline.start.anchor).toBe(Anchor.After);
expect(inline.end.anchor).toBe(Anchor.After);
});

test('can copy a paragraph split', () => {
const kit1 = setup();
const kit2 = setup();
kit1.editor.cursor.setAt(5);
kit1.editor.saved.insMarker(CommonSliceType.p);
kit1.editor.cursor.setAt(3, 5);
kit1.peritext.refresh();
const json = kit1.editor.export(kit1.editor.cursor);
kit2.editor.import(0, json);
kit2.peritext.refresh();
const json2 = kit2.peritext.blocks.toJson();
expect(json2).toEqual([
'',
null,
[CommonSliceType.p, null, 'de'],
[CommonSliceType.p, null, 'fgabcdefghijklmnopqrstuvwxyz'],
]);
});
});
};

Expand Down

0 comments on commit 90608c7

Please sign in to comment.