All files / collaborative-slate/src/sync/__tests__/tools traces.ts

85.71% Statements 42/49
57.14% Branches 4/7
76.92% Functions 10/13
90.47% Lines 38/42

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 1015x     548x                                 5x                     10x 10x     10x 10x 10x     10x 513x 513x       10x 562x 562x   168x   562x         10x               5x 25x 25x 25x   5x   25x 25x     25x 1228x 1228x 1228x 1228x     25x       25x 587x 587x 1228x 587x     25x 612x     587x    
import {createEditor, Transforms, type Editor} from 'slate';
import type {SlateDocument, SlateOperation} from '../../../types';
 
export const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
 
export interface SlateTrace {
  start: SlateDocument;
  operations: SlateOperation[];
 
  /**
   * High-level Slate.js operations are often composed of multiple low-level
   * atomic operations. All the atomic operations are stored in the `operations`
   * array, this array records the indices in `operations` where each high-level
   * operation (most likely) begins. It records the index of the next operation
   * when Slate `.normalize()` is called, which usually happens after each
   * high-level operation.
   */
  checkpoints: number[];
}
 
export class SlateTraceRecorder {
  public static create(
    initialValue: SlateDocument = [{type: 'paragraph', children: [{text: ''}]}],
  ): SlateTraceRecorder {
    const editor = createEditor();
    (editor.children as unknown) = clone(initialValue);
    return new SlateTraceRecorder(editor);
  }
 
  public readonly editor: Editor;
  public readonly start: SlateDocument;
  public readonly operations: SlateOperation[] = [];
  public readonly checkpoints: number[] = [];
 
  constructor(editor: Editor) {
    this.editor = editor;
    this.start = clone(editor.children as SlateDocument);
    const {apply, normalizeNode} = this.editor;
 
    // 1. Capture every operation into our permanent log
    this.editor.apply = (op: any) => {
      this.operations.push(clone(op)); // Clone op to prevent reference issues
      apply(op);
    };
 
    // 2. Mark high-level boundaries
    this.editor.normalizeNode = (entry) => {
      const [, path] = entry;
      if (path.length === 0) {
        // Record where the NEXT batch of operations will begin
        this.checkpoints.push(this.operations.length);
      }
      normalizeNode(entry);
    };
  }
 
  public getTrace(): SlateTrace {
    return {
      start: this.start,
      operations: this.operations,
      checkpoints: this.checkpoints,
    };
  }
}
 
export class SlateTraceRunner {
  public readonly editor = createEditor();
  public nextOpIdx: number = 0;
  public nextCheckpointIdx: number = 0;
 
  public static readonly from = (trace: SlateTrace): SlateTraceRunner => new SlateTraceRunner(trace);
 
  constructor(readonly trace: SlateTrace) {
    (this.editor.children as unknown) = clone(trace.start);
  }
 
  public readonly next = (): SlateOperation | undefined => {
    const operation = this.trace.operations[this.nextOpIdx++];
    Iif (!operation) return;
    Transforms.transform(this.editor, operation as any);
    return operation;
  };
 
  public runToEnd = (): void => {
    while (this.next());
  };
 
  public readonly toNextCheckpoint = (): void => {
    const checkpointIdx = this.trace.checkpoints[this.nextCheckpointIdx++];
    Iif (checkpointIdx === undefined) return;
    while (this.nextOpIdx < checkpointIdx) this.next();
    this.editor.normalize();
  };
 
  public endReached = (): boolean => {
    return this.nextCheckpointIdx >= this.trace.checkpoints.length;
  };
 
  public readonly state = (): SlateDocument => this.editor.children as SlateDocument;
}