Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading