UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

304 lines (303 loc) 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Inline = exports.InlineAttrEndPoint = exports.InlineAttrStartPoint = exports.InlineAttrContained = exports.InlineAttrEnd = exports.InlineAttrStart = exports.InlineAttrPassing = exports.AbstractInlineAttr = void 0; const printTree_1 = require("tree-dump/lib/printTree"); const stringify_1 = require("../../../json-text/stringify"); const constants_1 = require("../slice/constants"); const Range_1 = require("../rga/Range"); const ChunkSlice_1 = require("../util/ChunkSlice"); const Cursor_1 = require("../editor/Cursor"); const hash_1 = require("../../../json-crdt/hash"); const util_1 = require("../slice/util"); class AbstractInlineAttr { 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; } } exports.AbstractInlineAttr = AbstractInlineAttr; /** The attribute started before this inline and ends after this inline. */ class InlineAttrPassing extends AbstractInlineAttr { } exports.InlineAttrPassing = InlineAttrPassing; /** The attribute starts at the beginning of this inline. */ class InlineAttrStart extends AbstractInlineAttr { isStart() { return true; } } exports.InlineAttrStart = InlineAttrStart; /** The attribute ends at the end of this inline. */ class InlineAttrEnd extends AbstractInlineAttr { isEnd() { return true; } } exports.InlineAttrEnd = InlineAttrEnd; /** The attribute starts and ends in this inline, exactly contains it. */ class InlineAttrContained extends AbstractInlineAttr { isStart() { return true; } isEnd() { return true; } } exports.InlineAttrContained = InlineAttrContained; /** The attribute is collapsed at start of this inline. */ class InlineAttrStartPoint extends AbstractInlineAttr { isStart() { return true; } isCollapsed() { return true; } } exports.InlineAttrStartPoint = InlineAttrStartPoint; /** The attribute is collapsed at end of this inline. */ class InlineAttrEndPoint extends AbstractInlineAttr { isEnd() { return true; } isCollapsed() { return true; } } exports.InlineAttrEndPoint = InlineAttrEndPoint; /** * 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. */ class Inline extends Range_1.Range { 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 (0, hash_1.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); } /** * @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_1.Range) { const type = slice.type; switch (slice.stacking) { case constants_1.SliceStacking.Cursor: { const stack = attr[constants_1.SliceTypeName.Cursor] ?? (attr[constants_1.SliceTypeName.Cursor] = []); stack.push(this.createAttr(slice)); break; } case constants_1.SliceStacking.Many: { const stack = attr[type] ?? (attr[type] = []); stack.push(this.createAttr(slice)); break; } case constants_1.SliceStacking.One: { attr[type] = [this.createAttr(slice)]; break; } case constants_1.SliceStacking.Erase: { delete attr[type]; break; } } } } return attr; } hasCursor() { return !!this.attr()[constants_1.SliceTypeName.Cursor]; } /** @todo Make this return a list of cursors. */ cursorStart() { const attributes = this.attr(); const stack = attributes[constants_1.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_1.Cursor ? slice : void 0; } return; } cursorEnd() { const attributes = this.attr(); const stack = attributes[constants_1.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_1.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[constants_1.SliceTypeName.Cursor]; if (!stack) return; const attribute = stack[0]; const cursor = attribute.slice; if (!(cursor instanceof Cursor_1.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_1.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 === constants_1.SliceTypeName.Cursor || keyNum === constants_1.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 + (0, printTree_1.printTree)(tab, [ !attrKeys.length ? null : (tab) => 'attributes' + (0, printTree_1.printTree)(tab, attrKeys.map((key) => () => { return key === '-1' ? '▚ (cursor)' : (0, util_1.formatType)(key) + ' = ' + (0, stringify_1.stringify)(attr[key].map((attr) => attr.slice instanceof Cursor_1.Cursor ? [attr.slice.type, attr.slice.data()] : attr.slice.data())); })), !texts.length ? null : (tab) => 'texts' + (0, printTree_1.printTree)(tab, this.texts().map((text) => (tab) => text.toString(tab))), ])); } } exports.Inline = Inline;