json-joy
Version:
Collection of libraries for building collaborative editing apps.
295 lines • 11.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Slice = void 0;
const hasOwnProperty_1 = require("@jsonjoy.com/util/lib/hasOwnProperty");
const Point_1 = require("../rga/Point");
const Range_1 = require("../rga/Range");
const hash_1 = require("../../../json-crdt/hash");
const printTree_1 = require("tree-dump/lib/printTree");
const constants_1 = require("./constants");
const hash_2 = require("../../../json-hash/hash");
const clock_1 = require("../../../json-crdt-patch/clock");
const json_pretty_1 = require("../../../json-pretty");
const util_1 = require("./util");
const json_crdt_patch_1 = require("../../../json-crdt-patch");
const JsonCrdtDiff_1 = require("../../../json-crdt-diff/JsonCrdtDiff");
const NestedType_1 = require("./NestedType");
const constants_2 = require("../rga/constants");
const model_1 = require("../../../json-crdt/model");
/**
* A slice is stored in a {@link Model} as a "vec" node. It is used for
* rich-text formatting annotations and block splits.
*
* Slices represent Peritext's rich-text formatting/splits. The "slice"
* concept captures both: (1) range annotations; as well as, (2) *markers*,
* which are a single-point annotations. The markers are used as block splits,
* e.g. paragraph, heading, blockquote, etc. In markers, the start and end
* positions of the range are normally the same, but could also wrap around
* a single RGA chunk.
*/
class Slice extends Range_1.Range {
static deserialize(model, txt, arr, chunk, tuple) {
const header = +tuple.get(0).view();
const id1 = tuple.get(1).view();
const id2 = (tuple.get(2).view() || id1);
if (typeof header !== 'number')
throw new Error('INVALID_HEADER');
if (!(id1 instanceof clock_1.Timestamp))
throw new Error('INVALID_ID');
if (!(id2 instanceof clock_1.Timestamp))
throw new Error('INVALID_ID');
const anchor1 = (header & constants_1.SliceHeaderMask.X1Anchor) >>> constants_1.SliceHeaderShift.X1Anchor;
const anchor2 = (header & constants_1.SliceHeaderMask.X2Anchor) >>> constants_1.SliceHeaderShift.X2Anchor;
const stacking = (header & constants_1.SliceHeaderMask.Stacking) >>> constants_1.SliceHeaderShift.Stacking;
const rga = txt.str;
const p1 = new Point_1.Point(rga, id1, anchor1);
const p2 = new Point_1.Point(rga, id2, anchor2);
const slice = new Slice(model, txt, arr, chunk, tuple, stacking, p1, p2);
(0, util_1.validateType)(slice.type());
return slice;
}
constructor(
/** The `Model` where the slice is stored. */
model,
/** The Peritext context. */
txt,
/** The "arr" node where the slice is stored. */
arr,
/** The `arr` chunk of `arr` where the slice is stored. */
chunk,
/** The `vec` node which stores the serialized contents of this slice. */
tuple, stacking, start, end) {
super(txt.str, start, end);
this.model = model;
this.txt = txt;
this.arr = arr;
this.chunk = chunk;
this.tuple = tuple;
this.start = start;
this.end = end;
/** ------------------------------------------------------ {@link Stateful} */
this.hash = 0;
this.rga = txt.str;
// TODO: Chunk could potentially contain multiple entries, handle that case.
this.id = chunk.id;
this.stacking = stacking;
}
/**
* Represents a block split in the text, i.e. it is a *marker* that shows
* where a block was split. Markers also insert one "\n" new line character.
* Both marker ends are attached to the "before" anchor fo the "\n" new line
* character, i.e. it is *collapsed* to the "before" anchor.
*/
isMarker() {
return this.stacking === constants_1.SliceStacking.Marker;
}
tupleApi() {
return this.model.api.wrap(this.tuple);
}
pos() {
return this.arr.posById(this.id) || 0;
}
/**
* Returns the {@link Range} which exactly contains the block boundary of this
* marker.
*/
boundary() {
const start = this.start;
const end = start.clone();
end.anchor = constants_2.Anchor.After;
return this.txt.range(start, end);
}
// ---------------------------------------------------------------- mutations
set(start, end = start) {
super.set(start, end);
this.update({ range: this });
}
/**
* Expand range left and right to contain all invisible space: (1) tombstones,
* (2) anchors of non-deleted adjacent chunks.
*/
expand() {
super.expand();
this.update({ range: this });
}
update(params) {
let updateHeader = false;
const changes = [];
const stacking = params.stacking;
if (stacking !== undefined) {
this.stacking = stacking;
updateHeader = true;
}
const range = params.range;
if (range) {
updateHeader = true;
changes.push([constants_1.SliceTupleIndex.X1, json_crdt_patch_1.s.con(range.start.id)], [constants_1.SliceTupleIndex.X2, json_crdt_patch_1.s.con(range.end.id)]);
this.start = range.start;
this.end = range.start === range.end ? range.end.clone() : range.end;
}
if (params.type !== undefined) {
changes.push([constants_1.SliceTupleIndex.Type, params.type instanceof json_crdt_patch_1.NodeBuilder ? params.type : json_crdt_patch_1.s.jsonCon(params.type)]);
}
if ((0, hasOwnProperty_1.hasOwnProperty)(params, 'data'))
changes.push([constants_1.SliceTupleIndex.Data, params.data]);
if (updateHeader) {
const header = (this.stacking << constants_1.SliceHeaderShift.Stacking) +
(this.start.anchor << constants_1.SliceHeaderShift.X1Anchor) +
(this.end.anchor << constants_1.SliceHeaderShift.X2Anchor);
changes.push([constants_1.SliceTupleIndex.Header, json_crdt_patch_1.s.con(header)]);
}
this.tupleApi().set(changes);
}
isSaved() {
return this.tuple.id.sid === this.txt.model.clock.sid;
}
getStore() {
const txt = this.txt;
const sid = this.id.sid;
let store = txt.savedSlices;
if (sid === store.set.doc.clock.sid)
return store;
store = txt.localSlices;
if (sid === store.set.doc.clock.sid)
return store;
store = txt.extraSlices;
if (sid === store.set.doc.clock.sid)
return store;
return;
}
/**
* Delete this slice from its backing store.
*/
del() {
const store = this.getStore();
if (!store)
return;
store.del(this.id);
if (this.isMarker()) {
const txt = this.txt;
const range = txt.range(this.start, this.start.copy((p) => (p.anchor = constants_2.Anchor.After)));
txt.delStr(range);
}
}
/**
* Whether the slice is deleted.
*/
isDel() {
return this.chunk.del;
}
// -------------------------------------------------- slice type manipulation
typeNode() {
return this.tuple.get(constants_1.SliceTupleIndex.Type);
}
typeApi() {
const node = this.typeNode();
if (!node)
return;
return this.model.api.wrap(node);
}
nestedType() {
return new NestedType_1.NestedType(this);
}
/**
* The high-level behavior identifier of the slice. Specifies the
* user-defined type of the slice, e.g. paragraph, heading, blockquote, etc.
*
* Usually the type is a number or string primitive, in which case it is
* referred to as *tag*.
*
* The type is a list only for nested blocks, e.g. `['ul', 'li']`, in which
* case the type is a list of tags. The last tag in the list is the
* "leaf" tag, which is the type of the leaf block element.
*/
type() {
return this.typeNode()?.view();
}
typeSteps() {
const type = this.type() ?? 0 /* SliceTypeCon.p */;
return Array.isArray(type) ? type : [type];
}
// -------------------------------------------------- slice data manipulation
/**
* High-level user-defined metadata of the slice, which accompanies the slice
* type.
*/
data() {
return this.tuple.get(constants_1.SliceTupleIndex.Data)?.view();
}
dataNode() {
const node = this.tuple.get(constants_1.SliceTupleIndex.Data);
return node && this.model.api.wrap(node);
}
dataAsObj() {
const node = this.dataNode();
if (!(node instanceof model_1.ObjApi)) {
this.tupleApi().set([[constants_1.SliceTupleIndex.Data, json_crdt_patch_1.s.obj({})]]);
}
return this.dataNode();
}
/**
* Overwrites the data of this slice with the given data.
*
* @param data Data to set for this slice. The data can be any JSON value, but
* it is recommended to use an object.
*/
setData(data) {
this.tupleApi().set([[constants_1.SliceTupleIndex.Data, json_crdt_patch_1.s.jsonCon(data)]]);
}
/**
* Merges object data into the slice's data using JSON CRDT diffing.
*
* @param data Data to merge into the slice. If the data is an object, it will be
* merged with the existing data of the slice using JSON CRDT diffing.
*/
mergeData(data) {
const { model } = this;
const diff = new JsonCrdtDiff_1.JsonCrdtDiff(model);
if (this.dataNode() instanceof model_1.ObjApi && !!data && typeof data === 'object' && !Array.isArray(data)) {
const dataNode = this.dataAsObj();
const patch = diff.diff(dataNode.node, data);
model.applyPatch(patch);
}
else
this.setData(data);
}
refresh() {
let state = hash_2.CONST.START_STATE;
state = (0, hash_1.updateNode)(state, this.tuple);
const changed = state !== this.hash;
this.hash = state;
if (changed) {
const tuple = this.tuple;
const slice = Slice.deserialize(this.model, this.txt, this.arr, this.chunk, tuple);
this.stacking = slice.stacking;
this.start = slice.start;
this.end = slice.end;
}
return this.hash;
}
/** ----------------------------------------------------- {@link Printable} */
toStringName() {
const typeFormatted = (0, util_1.formatType)(this.type());
const stackingFormatted = constants_1.SliceStackingName[this.stacking];
return `Slice::${stackingFormatted} ${typeFormatted}`;
}
toStringHeaderName() {
const data = this.data();
const dataFormatted = data ? (0, json_pretty_1.prettyOneLine)(data) : '∅';
const dataLengthBreakpoint = 32;
const typeFormatted = (0, util_1.formatType)(this.type());
const stackingFormatted = constants_1.SliceStackingName[this.stacking];
const dataFormattedShort = dataFormatted.length < dataLengthBreakpoint ? `, ${dataFormatted}` : '';
const header = `${this.toStringName()} ${super.toString('', true)}, ${stackingFormatted}, ${typeFormatted}${dataFormattedShort}`;
return header;
}
toStringHeader(tab = '') {
const data = this.data();
const dataFormatted = data ? (0, json_pretty_1.prettyOneLine)(data) : '';
const dataLengthBreakpoint = 32;
return (this.toStringHeaderName() +
(0, printTree_1.printTree)(tab, [dataFormatted.length < dataLengthBreakpoint ? null : (tab) => dataFormatted]));
}
}
exports.Slice = Slice;
//# sourceMappingURL=Slice.js.map