json-joy
Version:
Collection of libraries for building collaborative editing apps.
304 lines (303 loc) • 11.5 kB
JavaScript
"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;