UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

298 lines (297 loc) 10.6 kB
import { printTree } from 'tree-dump/lib/printTree'; import { stringify } from '../../../json-text/stringify'; import { SliceStacking, SliceTypeName } from '../slice/constants'; import { Range } from '../rga/Range'; import { ChunkSlice } from '../util/ChunkSlice'; import { Cursor } from '../editor/Cursor'; import { hashId } from '../../../json-crdt/hash'; import { formatType } from '../slice/util'; export class AbstractInlineAttr { slice; constructor(slice) { this.slice = slice; } /** @returns Whether the attribute starts at the start of the inline. */ isStart() { return false; } /** @returns Whether the attribute ends at the end of the inline. */ isEnd() { return false; } /** @returns Whether the attribute is collapsed to a point. */ isCollapsed() { return false; } } /** The attribute started before this inline and ends after this inline. */ export class InlineAttrPassing extends AbstractInlineAttr { } /** The attribute starts at the beginning of this inline. */ export class InlineAttrStart extends AbstractInlineAttr { isStart() { return true; } } /** The attribute ends at the end of this inline. */ export class InlineAttrEnd extends AbstractInlineAttr { isEnd() { return true; } } /** The attribute starts and ends in this inline, exactly contains it. */ export class InlineAttrContained extends AbstractInlineAttr { isStart() { return true; } isEnd() { return true; } } /** The attribute is collapsed at start of this inline. */ export class InlineAttrStartPoint extends AbstractInlineAttr { isStart() { return true; } isCollapsed() { return true; } } /** The attribute is collapsed at end of this inline. */ export class InlineAttrEndPoint extends AbstractInlineAttr { isEnd() { return true; } isCollapsed() { return true; } } /** * The `Inline` class represents a range of inline text within a block, which * has the same annotations and formatting for all of its text contents, i.e. * its text contents can be rendered as a single (`<span>`) element. However, * the text contents might still be composed of multiple {@link ChunkSlice}s, * which are the smallest units of text and need to be concatenated to get the * full text content of the inline. */ export class Inline extends Range { txt; p1; p2; constructor(txt, p1, p2, start, end) { super(txt.str, start, end); this.txt = txt; this.p1 = p1; this.p2 = p2; } /** * @returns A stable unique identifier of this *inline* within a list of other * inlines of the parent block. Can be used for UI libraries to track the * identity of the inline across renders. */ key() { const start = this.start; return hashId(start.id) + (start.anchor ? 0 : 1); } /** * @returns The position of the inline within the text. */ pos() { const chunkSlice = this.texts(1)[0]; if (!chunkSlice) return -1; const chunk = chunkSlice.chunk; const pos = this.rga.pos(chunk); return pos + chunkSlice.off; } createAttr(slice) { const p1 = this.p1; const p2 = this.p2; return !slice.start.cmp(slice.end) ? !slice.start.cmp(p1) ? new InlineAttrStartPoint(slice) : new InlineAttrEndPoint(slice) : !p1.cmp(slice.start) ? !p2.cmp(slice.end) ? new InlineAttrContained(slice) : new InlineAttrStart(slice) : !p2.cmp(slice.end) ? new InlineAttrEnd(slice) : new InlineAttrPassing(slice); } _attr; /** * @returns Returns the attributes of the inline, which are the slice * annotations and formatting applied to the inline. * * @todo Rename to `.stat()`. * @todo Create a more efficient way to compute inline stats, separate: (1) * boolean flags, (2) cursor, (3) other attributes. */ attr() { if (this._attr) return this._attr; const attr = (this._attr = {}); const p1 = this.p1; const p2 = this.p2; const slices1 = p1.layers; const slices2 = p1.markers; const slices3 = p2.isAbsEnd() ? p2.markers : []; const length1 = slices1.length; const length2 = slices2.length; const length3 = slices3.length; const length12 = length1 + length2; const length123 = length12 + length3; for (let i = 0; i < length123; i++) { const slice = i >= length12 ? slices3[i - length12] : i >= length1 ? slices2[i - length1] : slices1[i]; if (slice instanceof Range) { const type = slice.type; switch (slice.stacking) { case SliceStacking.Cursor: { const stack = attr[SliceTypeName.Cursor] ?? (attr[SliceTypeName.Cursor] = []); stack.push(this.createAttr(slice)); break; } case SliceStacking.Many: { const stack = attr[type] ?? (attr[type] = []); stack.push(this.createAttr(slice)); break; } case SliceStacking.One: { attr[type] = [this.createAttr(slice)]; break; } case SliceStacking.Erase: { delete attr[type]; break; } } } } return attr; } hasCursor() { return !!this.attr()[SliceTypeName.Cursor]; } /** @todo Make this return a list of cursors. */ cursorStart() { const attributes = this.attr(); const stack = attributes[SliceTypeName.Cursor]; if (!stack) return; const attribute = stack[0]; if (attribute instanceof InlineAttrStart || attribute instanceof InlineAttrContained || attribute instanceof InlineAttrStartPoint) { const slice = attribute.slice; return slice instanceof Cursor ? slice : void 0; } return; } cursorEnd() { const attributes = this.attr(); const stack = attributes[SliceTypeName.Cursor]; if (!stack) return; const attribute = stack[0]; if (attribute instanceof InlineAttrEnd || attribute instanceof InlineAttrContained || attribute instanceof InlineAttrEndPoint) { const slice = attribute.slice; return slice instanceof Cursor ? slice : void 0; } return; } /** * Returns a 2-tuple if this inline is part of a selection. The 2-tuple sides * specify how selection ends on each side. Empty string means the selection * continues past that edge, `focus` and `anchor` specify that the edge * is either a focus caret or an anchor, respectively. * * @returns Selection state of this inline. */ selection() { const attributes = this.attr(); const stack = attributes[SliceTypeName.Cursor]; if (!stack) return; const attribute = stack[0]; const cursor = attribute.slice; if (!(cursor instanceof Cursor)) return; if (attribute instanceof InlineAttrPassing) return ['', '']; if (attribute instanceof InlineAttrStart) return [cursor.isStartFocused() ? 'focus' : 'anchor', '']; if (attribute instanceof InlineAttrEnd) return ['', cursor.isEndFocused() ? 'focus' : 'anchor']; if (attribute instanceof InlineAttrContained) return cursor.isStartFocused() ? ['focus', 'anchor'] : ['anchor', 'focus']; return; } texts(limit = 1e6) { const texts = []; const txt = this.txt; const overlay = txt.overlay; let cnt = 0; overlay.chunkSlices0(this.start.chunk(), this.start, this.end, (chunk, off, len) => { if (overlay.isMarker(chunk.id)) return; cnt++; texts.push(new ChunkSlice(chunk, off, len)); if (cnt === limit) return true; }); return texts; } // ------------------------------------------------------------------- export toJson() { let node = this.text(); const attrs = this.attr(); for (const key in attrs) { const keyNum = Number(key); if (keyNum === SliceTypeName.Cursor || keyNum === SliceTypeName.RemoteCursor) continue; const attr = attrs[key]; if (!attr.length) node = [key, { inline: true }, node]; else { const length = attr.length; for (let i = 0; i < length; i++) { const slice = attr[i].slice; const data = slice.data(); const attributes = data === void 0 ? { inline: true } : { inline: true, data }; node = [key === keyNum + '' ? keyNum : key, attributes, node]; } } } return node; } // ---------------------------------------------------------------- Printable toStringName() { return 'Inline'; } toString(tab = '') { const header = `${super.toString(tab)}`; const attr = this.attr(); const attrKeys = Object.keys(attr); const texts = this.texts(); return (header + printTree(tab, [ !attrKeys.length ? null : (tab) => 'attributes' + printTree(tab, attrKeys.map((key) => () => { return key === '-1' ? '▚ (cursor)' : formatType(key) + ' = ' + stringify(attr[key].map((attr) => attr.slice instanceof Cursor ? [attr.slice.type, attr.slice.data()] : attr.slice.data())); })), !texts.length ? null : (tab) => 'texts' + printTree(tab, this.texts().map((text) => (tab) => text.toString(tab))), ])); } }