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 | 11x 11x 554x 554x 4804x 4804x 4804x 4804x 4804x 4804x 4733x 900x 900x 11x 10925x 10925x 7386x 11x 554x 554x 833x 833x 833x 833x 833x 833x 4804x 4804x 4804x 4804x 153x 153x 4651x 4651x 4651x 833x 833x 833x 10925x 10925x 10925x 10925x 10925x 10925x 3599x 3599x 3599x 6274x 3599x 7326x 7326x 7326x 14438x 14438x 10178x 10178x 7326x 10178x 10178x 10178x 10178x 5308x 5308x 9838x 9838x 9838x 9838x 9838x 9837x 9837x 9837x 5308x | /** 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;
}
}
|