All files / collaborative-prosemirror/src/sync toPmNode.ts

97.67% Statements 84/86
87.09% Branches 27/31
100% Functions 10/10
100% Lines 72/72

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;
  }
}