All files / json-crdt-extensions/quill-delta QuillDeltaApi.ts

97.84% Statements 91/93
92% Branches 23/25
100% Functions 7/7
100% Lines 84/84

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 13350x 50x 50x 50x 50x     50x                     50x 7909x 7909x 7909x 7909x 7909x 7909x 9512x 9512x 9512x 2512x   7000x         50x 4624x 4624x 4624x 4624x 4624x 4624x 4624x 3296x   3296x 9539x 9539x 9539x 6141x 6141x   3398x 3398x 1812x         4624x 4624x 4624x 4624x 7311x 7311x 7311x 2253x     4624x     50x           7489x 7489x 7489x 5277x 937x 937x   4340x 4340x 4340x     50x   1x       1x       13443x 13443x 13443x 13443x 13443x 27509x 27509x 27509x 15424x 15424x 15424x 12085x 4596x 7489x 7489x 7489x 7489x 7473x 7473x 7473x 7473x   16x 16x 16x 16x 16x            
import {QuillConst} from './constants';
import {NodeApi} from '../../json-crdt/model/api/nodes';
import {SliceStacking} from '../peritext/slice/constants';
import {PersistedSlice} from '../peritext/slice/PersistedSlice';
import {diffAttributes, getAttributes, removeErasures} from './util';
import type {PathStep} from '@jsonjoy.com/json-pointer';
import type {QuillDeltaNode} from './QuillDeltaNode';
import {s, type ArrApi, type ArrNode, type ExtApi, type StrApi} from '../../json-crdt';
import type {
  QuillDeltaAttributes,
  QuillDeltaOpDelete,
  QuillDeltaOpInsert,
  QuillDeltaOpRetain,
  QuillDeltaPatch,
} from './types';
import type {Peritext} from '../peritext';
import type {SliceNode} from '../peritext/slice/types';
 
const updateAttributes = (txt: Peritext, attributes: QuillDeltaAttributes | undefined, pos: number, len: number) => {
  Iif (!attributes) return;
  const range = txt.rangeAt(pos, len);
  const keys = Object.keys(attributes);
  const length = keys.length;
  const savedSlices = txt.savedSlices;
  for (let i = 0; i < length; i++) {
    const key = keys[i];
    const value = attributes[key];
    if (value === null) {
      savedSlices.ins(range, SliceStacking.Erase, key);
    } else {
      savedSlices.ins(range, SliceStacking.One, key, s.con(value));
    }
  }
};
 
const rewriteAttributes = (txt: Peritext, attributes: QuillDeltaAttributes | undefined, pos: number, len: number) => {
  Iif (typeof attributes !== 'object') return;
  const range = txt.rangeAt(pos, len);
  range.expand();
  const slices = txt.overlay.findOverlapping(range);
  const length = slices.size;
  const relevantOverlappingButNotContained = new Set<PathStep>();
  if (length) {
    const savedSlices = txt.savedSlices;
    // biome-ignore lint: slices is not iterable
    slices.forEach((slice) => {
      if (slice instanceof PersistedSlice) {
        const isContained = range.contains(slice);
        if (!isContained) {
          relevantOverlappingButNotContained.add(slice.type() as PathStep);
          return;
        }
        const type = slice.type() as PathStep;
        if (type in attributes) {
          savedSlices.del(slice.id);
        }
      }
    });
  }
  const keys = Object.keys(attributes);
  const attributeLength = keys.length;
  const attributesCopy = {...attributes};
  for (let i = 0; i < attributeLength; i++) {
    const key = keys[i];
    const value = attributes[key];
    if (value === null && !relevantOverlappingButNotContained.has(key)) {
      delete attributesCopy[key];
    }
  }
  updateAttributes(txt, attributesCopy, pos, len);
};
 
const maybeUpdateAttributes = (
  txt: Peritext,
  attributes: QuillDeltaAttributes | undefined,
  pos: number,
  len: number,
): void => {
  const range = txt.rangeAt(pos, 1);
  const overlayPoint = txt.overlay.getOrNextLower(range.start);
  if (!overlayPoint && !attributes) return;
  if (!overlayPoint) {
    updateAttributes(txt, removeErasures(attributes), pos, len);
    return;
  }
  const pointAttributes = getAttributes(overlayPoint);
  const attributeDiff = diffAttributes(pointAttributes, attributes);
  if (attributeDiff) updateAttributes(txt, attributeDiff, pos, len);
};
 
export class QuillDeltaApi extends NodeApi<QuillDeltaNode> implements ExtApi<QuillDeltaNode> {
  public text(): StrApi {
    return this.api.wrap(this.node.text());
  }
 
  public slices(): ArrApi<ArrNode<SliceNode>> {
    return this.api.wrap(this.node.slices());
  }
 
  public apply(ops: QuillDeltaPatch['ops']) {
    const txt = this.node.txt;
    const overlay = txt.overlay;
    const length = ops.length;
    let pos = 0;
    for (let i = 0; i < length; i++) {
      overlay.refresh(true);
      const op = ops[i];
      if (typeof (<QuillDeltaOpRetain>op).retain === 'number') {
        const {retain, attributes} = <QuillDeltaOpRetain>op;
        if (attributes) rewriteAttributes(txt, attributes, pos, retain);
        pos += retain;
      } else if (typeof (<QuillDeltaOpDelete>op).delete === 'number') {
        txt.delAt(pos, (<QuillDeltaOpDelete>op).delete);
      } else if ((<QuillDeltaOpInsert>op).insert) {
        const {insert} = <QuillDeltaOpInsert>op;
        let {attributes} = <QuillDeltaOpInsert>op;
        if (typeof insert === 'string') {
          txt.insAt(pos, insert);
          const insertLength = insert.length;
          maybeUpdateAttributes(txt, attributes, pos, insertLength);
          pos += insertLength;
        } else {
          txt.insAt(pos, QuillConst.EmbedChar);
          if (!attributes) attributes = {};
          attributes[QuillConst.EmbedSliceType] = insert;
          maybeUpdateAttributes(txt, attributes, pos, 1);
          pos += 1;
        }
      }
    }
  }
}