Skip to content

Commit

Permalink
feat(json-crdt-extensions): 🎸 report slice position relative to the i…
Browse files Browse the repository at this point in the history
…nline
  • Loading branch information
streamich committed Jun 8, 2024
1 parent 6a19cdb commit 2f960db
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 24 deletions.
51 changes: 37 additions & 14 deletions src/json-crdt-extensions/peritext/block/Inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,22 @@ import type {Printable} from 'tree-dump/lib/types';
import type {PathStep} from '../../../json-pointer';
import type {Peritext} from '../Peritext';

export type InlineAttributes = Record<string | number, unknown>;
export const enum InlineAttrPos {
/** The attribute started before this inline and ends after this inline. */
Passing = 0,
/** The attribute starts at the beginning of this inline. */
Start = 1,
/** The attribute ends at the end of this inline. */
End = 2,
/** The attribute starts and ends in this inline. */
Contained = 3,
/** The attribute is collapsed at start of this inline. */
Collapsed = 4,
}

export type InlineAttr<T> = [value: T, position: InlineAttrPos];
export type InlineAttrStack = InlineAttr<unknown[]>;
export type InlineAttrs = Record<string | number, InlineAttr<unknown>>;

/**
* The `Inline` class represents a range of inline text within a block, which
Expand Down Expand Up @@ -66,12 +81,28 @@ export class Inline extends Range implements Printable {
return pos + chunkSlice.off;
}

protected getAttrPos(range: Range<any>): InlineAttrPos {
return !range.start.cmp(range.end)
? InlineAttrPos.Collapsed
: !this.start.cmp(range.start)
? (!this.end.cmp(range.end) ? InlineAttrPos.Contained : InlineAttrPos.Start)
: !this.end.cmp(range.end)
? InlineAttrPos.End : InlineAttrPos.Passing;
}

protected stackAttr(attr: InlineAttrs, type: string | number, data: unknown, slice: Range<any>): void {
let item: InlineAttrStack | undefined = attr[type] as InlineAttrStack | undefined;
if (!item) attr[type] = item = [[], this.getAttrPos(slice)];
const dataList: unknown[] = item[0] instanceof Array ? item[0] as unknown[] : [];
dataList.push(data);
};

/**
* @returns Returns the attributes of the inline, which are the slice
* annotations and formatting applied to the inline.
*/
public attr(): InlineAttributes {
const attr: InlineAttributes = {};
public attr(): InlineAttrs {
const attr: InlineAttrs = {};
const point = this.start as OverlayPoint;
const slices1 = point.layers;
const slices2 = point.markers;
Expand All @@ -84,25 +115,17 @@ export class Inline extends Range implements Printable {
const type = slice.type as PathStep;
switch (slice.behavior) {
case SliceBehavior.Cursor: {
const dataList: unknown[] = (attr[SliceTypes.Cursor] as unknown[]) || (attr[SliceTypes.Cursor] = []);
const data: unknown[] = [type];
const cursorData = slice.data();
if (cursorData !== void 0) data.push(cursorData);
dataList.push(data);
this.stackAttr(attr, SliceTypes.Cursor, [type, slice.data()], slice);
break;
}
case SliceBehavior.Stack: {
let dataList: unknown[] = (attr[type] as unknown[]) || (attr[type] = []);
if (!Array.isArray(dataList)) dataList = attr[type] = [dataList];
let data = slice.data();
if (data === undefined) data = 1;
dataList.push(data);
this.stackAttr(attr, type, slice.data() ?? 1, slice);
break;
}
case SliceBehavior.Overwrite: {
let data = slice.data();
if (data === undefined) data = 1;
attr[type] = data;
attr[type] = [data, this.getAttrPos(slice)];
break;
}
case SliceBehavior.Erase: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ const runStrTests = (setup: () => Kit) => {
const [start, end] = [...overlay.points()];
const inline = Inline.create(peritext, start, end);
const attr = inline.attr();
expect(attr.bold).toEqual([1, 2]);
expect(attr.em).toEqual([1]);
expect(attr.bold[0]).toEqual([1, 2]);
expect(attr.em[0]).toEqual([1]);
});

test('returns latest OVERWRITE annotation', () => {
Expand All @@ -71,8 +71,8 @@ const runStrTests = (setup: () => Kit) => {
const [start, end] = [...overlay.points()];
const inline = Inline.create(peritext, start, end);
const attr = inline.attr();
expect(attr.bold).toBe(2);
expect(attr.em).toBe(1);
expect(attr.bold[0]).toBe(2);
expect(attr.em[0]).toBe(1);
});

test('hides annotation hidden with another ERASE annotation', () => {
Expand All @@ -87,7 +87,7 @@ const runStrTests = (setup: () => Kit) => {
const inline = Inline.create(peritext, start, end);
const attr = inline.attr();
expect(attr.bold).toBe(undefined);
expect(attr.em).toBe(1);
expect(attr.em[0]).toBe(1);
});

test('concatenates with "," steps of nested Path type annotations', () => {
Expand All @@ -100,8 +100,8 @@ const runStrTests = (setup: () => Kit) => {
const [start, end] = [...overlay.points()];
const inline = Inline.create(peritext, start, end);
const attr = inline.attr();
expect(attr['bold,very']).toEqual([1]);
expect(attr['bold,normal']).toEqual([2]);
expect(attr['bold,very'][0]).toEqual([1]);
expect(attr['bold,normal'][0]).toEqual([2]);
});

test('returns collapsed slice (cursor) at marker position', () => {
Expand All @@ -114,7 +114,7 @@ const runStrTests = (setup: () => Kit) => {
const inline = [...block.texts()][0];
const attr = inline.attr();
expect(attr).toEqual({
[SliceTypes.Cursor]: [[CursorAnchor.Start]],
[SliceTypes.Cursor]: [[[CursorAnchor.Start]], expect.any(Number)],
});
});

Expand All @@ -128,10 +128,44 @@ const runStrTests = (setup: () => Kit) => {
const inline = [...block.texts()][1];
const attr = inline.attr();
expect(attr).toEqual({
[SliceTypes.Cursor]: [[CursorAnchor.Start]],
bold: [123],
[SliceTypes.Cursor]: [[[CursorAnchor.Start]], expect.any(Number)],
bold: [[123], expect.any(Number)],
});
});

test('returns slice and cursor at the same range', () => {
const {peritext} = setup();
peritext.editor.cursor.setAt(2, 2);
peritext.editor.saved.insStack('bold', 123);
peritext.refresh();
const block = peritext.blocks.root.children[0]!;
const inline1 = [...block.texts()][0];
const inline2 = [...block.texts()][1];
const inline3 = [...block.texts()][2];
expect(inline1.attr()).toEqual({});
expect(inline2.attr()).toEqual({
[SliceTypes.Cursor]: [[[CursorAnchor.Start]], expect.any(Number)],
bold: [[123], expect.any(Number)],
});
expect(inline3.attr()).toEqual({});
});

test('can infer slice start', () => {
const {peritext} = setup();
peritext.editor.cursor.setAt(2, 2);
peritext.editor.saved.insStack('bold', 123);
peritext.refresh();
const block = peritext.blocks.root.children[0]!;
const inline1 = [...block.texts()][0];
const inline2 = [...block.texts()][1];
const inline3 = [...block.texts()][2];
expect(inline1.attr()).toEqual({});
expect(inline2.attr()).toEqual({
[SliceTypes.Cursor]: [[[CursorAnchor.Start]], expect.any(Number)],
bold: [[123], expect.any(Number)],
});
expect(inline3.attr()).toEqual({});
});
});
};

Expand Down

0 comments on commit 2f960db

Please sign in to comment.