Skip to content

Commit

Permalink
feat(json-crdt-extensions): 馃幐 improve overlay point layer insertion
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Apr 26, 2024
1 parent 00fa557 commit 70748ac
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 9 deletions.
1 change: 1 addition & 0 deletions src/json-crdt-extensions/peritext/Peritext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class Peritext implements Printable {
*
* @param pos Position of the character in the text.
* @param anchor Whether the point should attach before or after a character.
* Defaults to "before".
* @returns The point.
*/
public pointAt(pos: number, anchor: Anchor = Anchor.Before): Point {
Expand Down
42 changes: 33 additions & 9 deletions src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,27 @@ import type {HeadlessNode} from 'sonic-forest/lib/types';
import type {Printable} from '../../../util/print/types';
import type {Slice} from '../slice/types';

/**
* A {@link Point} which is indexed in the {@link Overlay} tree. Represents
* sparse locations in the string of the places where annotation slices start,
* end, or are broken down by other intersecting slices.
*/
export class OverlayPoint extends Point implements Printable, HeadlessNode {
/**
* Sorted list of references to rich-text constructs.
* Sorted list of all references to rich-text constructs.
*/
public readonly refs: OverlayRef[] = [];

/**
* Sorted list of layers, contain the interval from this point to the next one.
* Sorted list of layers, contains the interval from this point to the next
* one. A *layer* is a part of a slice from the current point to the next one.
* This interval can contain many layers, as the slices can be overlap.
*/
public readonly layers: Slice[] = [];

/**
* Collapsed slices.
* Collapsed slices - markers/block splits, which represent a single point in
* the text, even if the start and end of the slice are different.
*
* @todo Rename to `markers`?
*/
Expand All @@ -45,9 +53,14 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode {
this.removePoint(slice);
}

// ------------------------------------------------------------------- layers

/**
* Inserts a slice to the list of layers which contains the area from this
* point to the next one.
* point until the next one. The operation is idempotent, so inserting the
* same slice twice will not change the state of the point. The layers are
* sorted by the slice ID.
*
* @param slice Slice to add to the layer list.
*/
public addLayer(slice: Slice): void {
Expand All @@ -57,23 +70,32 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode {
layers.push(slice);
return;
}
// We attempt to insert from the end of the list, as it is the most likely.
// We attempt to insert from the end of the list, as it is the most likely
// scenario. And `.push()` is more efficient than `.unshift()`.
const lastSlice = layers[length - 1];
const sliceId = slice.id;
if (compare(lastSlice.id, sliceId) < 0) {
const cmp = compare(lastSlice.id, sliceId);
if (cmp < 0) {
layers.push(slice);
return;
}
} else if (!cmp) return;
for (let i = length - 2; i >= 0; i--) {
const currSlice = layers[i];
if (compare(currSlice.id, sliceId) < 0) {
const cmp = compare(currSlice.id, sliceId);
if (cmp < 0) {
layers.splice(i + 1, 0, slice);
return;
}
} else if (!cmp) return;
}
layers.unshift(slice);
}

/**
* Removes a slice from the list of layers, which start from this overlay
* point.
*
* @param slice Slice to remove from the layer list.
*/
public removeLayer(slice: Slice): void {
const layers = this.layers;
const length = layers.length;
Expand All @@ -85,6 +107,8 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode {
}
}

// ------------------------------------------------------------------ markers

public addPoint(slice: Slice): void {
const points = this.points;
const length = points.length;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {Point} from "../../rga/Point";
import {setup} from "../../slice/__tests__/setup";
import {OverlayPoint} from "../OverlayPoint";

const setupOverlayPoint = () => {
const deps = setup();
const getPoint = (point: Point) => {
return new OverlayPoint(deps.peritext.str, point.id, point.anchor);
};
return {
...deps,
getPoint,
};
};

describe('layers', () => {
test('can add a layer', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const point = getPoint(slice.start);
expect(point.layers.length).toBe(0);
point.addLayer(slice);
expect(point.layers.length).toBe(1);
expect(point.layers[0]).toBe(slice);
});

test('inserting same slice twice is a no-op', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const point = getPoint(slice.start);
expect(point.layers.length).toBe(0);
point.addLayer(slice);
point.addLayer(slice);
point.addLayer(slice);
expect(point.layers.length).toBe(1);
expect(point.layers[0]).toBe(slice);
});

test('can add two layers with the same start position', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const point = getPoint(slice1.start);
expect(point.layers.length).toBe(0);
point.addLayer(slice1);
expect(point.layers.length).toBe(1);
point.addLayer(slice2);
point.addLayer(slice2);
expect(point.layers.length).toBe(2);
expect(point.layers[0]).toBe(slice1);
expect(point.layers[1]).toBe(slice2);
});

test('orders slices by their ID', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const point = getPoint(slice1.start);
point.addLayer(slice2);
point.addLayer(slice1);
expect(point.layers[0]).toBe(slice1);
expect(point.layers[1]).toBe(slice2);
});

test('can add tree layers and sort them correctly', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), '<u>');
const point = getPoint(slice1.start);
point.addLayer(slice3);
point.addLayer(slice3);
point.addLayer(slice2);
point.addLayer(slice3);
point.addLayer(slice1);
point.addLayer(slice3);
point.addLayer(slice3);
expect(point.layers.length).toBe(3);
expect(point.layers[0]).toBe(slice1);
expect(point.layers[1]).toBe(slice2);
expect(point.layers[2]).toBe(slice3);
});

test('can add tree layers by appending them', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), '<u>');
const point = getPoint(slice1.start);
point.addLayer(slice1);
point.addLayer(slice2);
point.addLayer(slice3);
expect(point.layers[0]).toBe(slice1);
expect(point.layers[1]).toBe(slice2);
expect(point.layers[2]).toBe(slice3);
});

test('can remove layers', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), '<u>');
const point = getPoint(slice1.start);
point.addLayer(slice2);
point.addLayer(slice1);
point.addLayer(slice1);
point.addLayer(slice1);
point.addLayer(slice3);
expect(point.layers[0]).toBe(slice1);
expect(point.layers[1]).toBe(slice2);
expect(point.layers[2]).toBe(slice3);
point.removeLayer(slice2);
expect(point.layers[0]).toBe(slice1);
expect(point.layers[1]).toBe(slice3);
point.removeLayer(slice1);
expect(point.layers[0]).toBe(slice3);
point.removeLayer(slice1);
point.removeLayer(slice3);
expect(point.layers.length).toBe(0);
});
});

0 comments on commit 70748ac

Please sign in to comment.