json-joy
Version:
Collection of libraries for building collaborative editing apps.
202 lines (201 loc) • 6.73 kB
JavaScript
import { printTree } from 'tree-dump/lib/printTree';
import { CONST, updateJson, updateNum } from '../../../json-hash/hash';
import { MarkerOverlayPoint } from '../overlay/MarkerOverlayPoint';
import { UndefEndIter } from '../../../util/iterator';
import { Inline } from './Inline';
import { formatType, getTag } from '../slice/util';
import { Range } from '../rga/Range';
export class Block extends Range {
txt;
path;
marker;
start;
end;
parent = null;
children = [];
constructor(txt, path, marker, start, end) {
super(txt.str, start, end);
this.txt = txt;
this.path = path;
this.marker = marker;
this.start = start;
this.end = end;
}
/**
* @returns Stable unique identifier within a list of blocks. Used for React
* or other rendering library keys.
*/
key() {
if (!this.marker)
return this.tag();
const id = this.marker.id;
return id.sid.toString(36) + id.time.toString(36);
}
tag() {
return getTag(this.path);
}
attr() {
return this.marker?.data();
}
isLeaf() {
return false;
}
/**
* Iterate through all overlay points of this block, until the next marker
* (regardless if that marker is a child or not).
*/
points0(withMarker = false) {
const txt = this.txt;
const overlay = txt.overlay;
const iterator = overlay.points0(this.marker);
let closed = false;
return () => {
if (withMarker) {
withMarker = false;
return this.marker ?? overlay.START;
}
if (closed)
return;
const point = iterator();
if (!point)
return;
if (point instanceof MarkerOverlayPoint) {
closed = true;
return;
}
return point;
};
}
points(withMarker) {
return new UndefEndIter(this.points0(withMarker));
}
tuples0() {
const overlay = this.txt.overlay;
const marker = this.marker;
const iterator = overlay.tuples0(marker);
let closed = false;
return () => {
if (closed)
return;
let pair = iterator();
while (!marker && pair && pair[1] && pair[1].cmpSpatial(this.start) < 0)
pair = iterator();
if (!pair)
return (closed = true), void 0;
if (!pair[1] || pair[1] instanceof MarkerOverlayPoint)
closed = true;
return pair;
};
}
/**
* @todo Consider moving inline-related methods to {@link LeafBlock}.
*/
texts0() {
const txt = this.txt;
const overlay = txt.overlay;
const iterator = this.tuples0();
const start = this.start;
const end = this.end;
const startIsMarker = overlay.isMarker(start.id);
const endIsMarker = overlay.isMarker(end.id);
let isFirst = true;
let next = iterator();
let closed = false;
const newIterator = () => {
if (closed)
return;
const pair = next;
next = iterator();
if (!pair)
return;
const [overlayPoint1, overlayPoint2] = pair;
let point1 = overlayPoint1;
let point2 = overlayPoint2;
if (isFirst) {
isFirst = false;
if (start.cmpSpatial(overlayPoint1) > 0)
point1 = start;
if (startIsMarker) {
point1 = point1.clone();
point1.step(1);
// Skip condition when inline annotations tarts immediately at th
// beginning of the block.
if (point1.cmp(point2) === 0)
return newIterator();
}
}
if (!endIsMarker && end.cmpSpatial(overlayPoint2) < 0) {
closed = true;
point2 = end;
}
return new Inline(txt, overlayPoint1, overlayPoint2, point1, point2);
};
return newIterator;
}
/**
* @todo Consider moving inline-related methods to {@link LeafBlock}.
*/
texts() {
return new UndefEndIter(this.texts0());
}
text() {
let str = '';
const children = this.children;
const length = children.length;
for (let i = 0; i < length; i++)
str += children[i].text();
return str;
}
// ------------------------------------------------------------------- export
toJson() {
const data = this.attr();
const attr = data !== void 0 ? { data } : null;
const node = [this.tag(), attr];
const children = this.children;
const length = children.length;
for (let i = 0; i < length; i++)
node.push(children[i].toJson());
return node;
}
// ----------------------------------------------------------------- Stateful
hash = 0;
refresh() {
const { path, children } = this;
let state = CONST.START_STATE;
state = updateJson(state, path);
const marker = this.marker;
if (marker) {
state = updateNum(state, marker.marker.refresh());
state = updateNum(state, marker.textHash);
}
else {
state = updateNum(state, this.txt.overlay.leadingTextHash);
}
for (let i = 0; i < children.length; i++)
state = updateNum(state, children[i].refresh());
return (this.hash = state);
}
// ---------------------------------------------------------------- Printable
toStringName() {
return 'Block';
}
toStringHeader() {
const hash = `#${this.hash.toString(36).slice(-4)}`;
const tag = this.path.map((step) => formatType(step)).join('.');
const header = `${super.toString('', true)} ${hash} ${tag} `;
return header;
}
toString(tab = '') {
const header = this.toStringHeader();
const hasChildren = !!this.children.length;
return (header +
printTree(tab, [
this.marker ? (tab) => this.marker.toString(tab) : null,
this.marker && hasChildren ? () => '' : null,
hasChildren
? (tab) => 'children' +
printTree(tab, this.children.map((child, i) => (tab) => `${i + 1}. ` + child.toString(tab + ' ' + ' '.repeat(String(i + 1).length))))
: null,
]));
}
}