Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | 12x 12x 554x 554x 4805x 4805x 4805x 4805x 4805x 4805x 4734x 900x 900x 12x 11516x 11516x 7923x 12x 554x 554x 833x 833x 833x 833x 833x 833x 4805x 4805x 4805x 4805x 153x 153x 4652x 4652x 4652x 833x 833x 833x 11516x 11516x 11516x 11516x 11516x 11516x 3920x 3920x 3920x 6864x 3920x 7596x 7596x 7596x 14487x 14487x 10259x 10259x 7596x 10259x 10259x 10259x 10259x 5331x 5331x 9957x 9957x 9957x 9957x 9957x 9956x 9956x 9956x 5331x | /** Direct Peritext Fragment to ProseMirror Node converter with caching. */
import * as pmm from 'prosemirror-model';
import {type Block, LeafBlock, type Inline, Slice} from 'json-joy/lib/json-crdt-extensions';
import type {Fragment} from 'json-joy/lib/json-crdt-extensions/peritext/block/Fragment';
/**
* Double-buffered cache for ProseMirror nodes, keyed by Peritext Block hash.
* Designed to maximize reuse of unchanged nodes across renders while allowing
* GC of stale entries without needing to track usage counts or timestamps.
*/
class PmNodeCache {
/** Entries written/read during the *current* render pass. */
private curr: Map<number, pmm.Node> = new Map();
/** Entries from the *previous* render pass (read-only during current). */
private prev: Map<number, pmm.Node> = new Map();
/**
* Look up a cached ProseMirror node by Peritext Block hash.
* Automatically promotes a hit from `prev` into `curr`.
*/
get(hash: number): pmm.Node | undefined {
const curr = this.curr;
let node = curr.get(hash);
Iif (node !== undefined) return node;
node = this.prev.get(hash);
// Promote to current generation so it survives the next GC.
if (node !== undefined) curr.set(hash, node);
return node;
}
/** Store a freshly-built node. */
set(hash: number, node: pmm.Node): void {
this.curr.set(hash, node);
}
/**
* Call once at the **end** of each render pass.
* Drops entries that were not accessed during this render.
*/
gc(): void {
this.prev = this.curr;
this.curr = new Map();
}
}
const blockAttrs = (block: Block | LeafBlock): pmm.Attrs | null => {
const data = block.attr();
if (data && typeof data === 'object') for (const _ in data) return data as pmm.Attrs;
return null;
};
/**
* Stateful converter that turns a Peritext `Fragment` into a ProseMirror `doc`
* Node, reusing unchanged sub-trees via {@link PmNodeCache}.
*/
export class ToPmNode {
public readonly cache = new PmNodeCache();
constructor(public readonly schema: pmm.Schema) {}
/**
* Convert a Peritext `Fragment` into a full ProseMirror document node.
* Assumes `fragment.refresh()` has already been called so that all
* `Block.hash` values are up-to-date.
*/
convert(fragment: Fragment<string>): pmm.Node {
const root = fragment.root;
const children = root.children;
const length = children.length;
const pmChildren: pmm.Node[] = [];
const cache = this.cache;
for (let i = 0; i < length; i++) {
const block = children[i];
const hash = block.hash;
const cached = cache.get(hash);
if (cached) {
pmChildren.push(cached);
continue;
}
const pmNode = this.convBlock(block);
cache.set(hash, pmNode);
pmChildren.push(pmNode);
}
cache.gc();
const docType = this.schema.nodes.doc;
return docType.create(null, pmChildren);
}
private convBlock(block: Block | LeafBlock): pmm.Node {
const schema = this.schema;
const tag = block.tag();
const typeName = tag ? tag + '' : 'paragraph';
const nodeType = schema.nodes[typeName] ?? schema.nodes.paragraph;
const attrs = blockAttrs(block);
if (block instanceof LeafBlock) return nodeType.create(attrs, this.convInlines(block));
const children = block.children;
const length = children.length;
const pmChildren: pmm.Node[] = new Array(length);
for (let i = 0; i < length; i++) pmChildren[i] = this.convBlock(children[i]);
return nodeType.create(attrs, pmChildren);
}
private convInlines(leaf: LeafBlock): pmm.Node[] {
const schema = this.schema;
const result: pmm.Node[] = [];
for (let iterator = leaf.texts0(), inline: Inline<any> | undefined; (inline = iterator()); ) {
const text = inline.text();
if (!text) continue;
const marks = this.convMarks(inline);
result.push(schema.text(text, marks.length ? marks : undefined));
}
return result;
}
private convMarks(inline: Inline): readonly pmm.Mark[] {
const schema = this.schema;
const layers = inline.p1.layers;
const length = layers.length;
if (!length) return pmm.Mark.none;
const marks: pmm.Mark[] = [];
for (let i = 0; i < length; i++) {
const slice = layers[i];
Iif (!(slice instanceof Slice)) continue;
const tag = slice.type() + '';
const markType = schema.marks[tag];
if (!markType) continue;
const data = slice.data();
const attrs: pmm.Attrs | null =
data && typeof data === 'object' && !Array.isArray(data) ? (data as pmm.Attrs) : null;
marks.push(markType.create(attrs));
}
return marks;
}
}
|