All files / collaborative-presence/src str.ts

94.73% Statements 54/57
86.95% Branches 20/23
100% Functions 3/3
97.95% Lines 48/49

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  2x                                             2x 123x 123x 123x 123x 123x 145x 145x 145x 102x 145x   145x 145x 145x 145x 94x 94x 94x   51x         145x   123x 123x               2x 211x 211x 174x 174x 174x 174x 174x                       2x 105x 105x 105x 104x 104x 104x 104x 122x 122x 122x 122x 89x 89x 89x   33x     104x    
import {JsonCrdtDataType} from 'json-joy/lib/json-crdt-patch/constants';
import * as id from './id';
import type {ITimestampStruct, StrApi, StrNode, Model} from 'json-joy/lib/json-crdt';
import type {PresenceIdShorthand, PresencePoint, RgaSelection, PresenceCursor} from './types';
 
export type StrSelectionStrict = [anchor: number, focus?: number];
 
export type StrSelection =
  /** A single *caret* selection. */
  | number
  /** A *range* selection, with an anchor and focus. */
  | StrSelectionStrict;
 
/**
 * Converts offset-based string selections to CRDT ID-based selections. The
 * offset-based selections are relative to the current state of the string, and
 * will be converted to stable CRDT ID-based selections, which are stable across
 * concurrent edits.
 *
 * @param str The "str" node instance.
 * @param selections Collection of offset-based selections in the string.
 * @returns RGA selection entry, where all offset-based selection converted to
 *     stable CRDT ID-based selections.
 */
export const toDto = (str: StrApi, selections: StrSelection[]): RgaSelection => {
  const clock = str.api.model.clock;
  const sid = clock.sid;
  const nodeId: PresenceIdShorthand = id.toDto(sid, str.node.id);
  const cursors: PresenceCursor[] = [];
  for (const selection of selections) {
    let anchor: number = 0,
      focus: number = -1;
    if (typeof selection === 'number') anchor = selection;
    else [anchor, focus = anchor] = selection;
    if (focus === anchor) focus = -1;
    let cursor: PresenceCursor;
    try {
      const anchorId: ITimestampStruct = str.findId(anchor - 1);
      const anchorPoint: PresencePoint = [id.toDto(sid, anchorId)];
      if (focus >= 0) {
        const focusId: ITimestampStruct = str.findId(focus - 1);
        const focusPoint: PresencePoint = [id.toDto(sid, focusId)];
        cursor = [anchorPoint, focusPoint];
      } else {
        cursor = [anchorPoint];
      }
    } catch {
      cursor = [[id.toDto(sid, str.node.id)]];
    }
    cursors.push(cursor);
  }
  const selection: RgaSelection = ['', '', sid, clock.time, {}, JsonCrdtDataType.str, nodeId, cursors];
  return selection;
};
 
/**
 * Convert a CRDT ID to a view offset (the cursor position after that
 * character). Returns 0 when the ID matches the node itself (i.e. before
 * the first character).
 */
const findOffset = (str: StrNode, tsId: ITimestampStruct): number => {
  const nodeId = str.id;
  if (nodeId.sid === tsId.sid && nodeId.time === tsId.time) return 0;
  const chunk = str.findById(tsId);
  Iif (!chunk) return 0;
  const pos = str.pos(chunk);
  const charIndex = pos + (chunk.del ? 0 : tsId.time - chunk.id.time);
  return charIndex + 1;
};
 
/**
 * Converts a CRDT ID-based RGA selection back to offset-based string
 * selections. This is the inverse of {@link toDto}.
 *
 * @param model The JSON CRDT model containing the "str" node.
 * @param selection The RGA selection DTO to convert.
 * @returns Collection of offset-based selections, or an empty array if the
 *     node is not found or is not a "str" node.
 */
export const fromDto = (model: Model<any>, selection: RgaSelection): StrSelectionStrict[] => {
  const [_documentId, _uiLocationId, sid, _time, _meta, type, nodeIdDto, cursors] = selection;
  const result: StrSelectionStrict[] = [];
  if (type !== JsonCrdtDataType.str) return result;
  const nodeId = id.fromDto(sid, nodeIdDto);
  const str = model.index.get(nodeId) as StrNode | undefined;
  Iif (!str || str.name() !== 'str') return result;
  for (const cursor of cursors) {
    const [anchorPointDto, focusPointDto] = cursor;
    const anchorId = id.fromDto(sid, anchorPointDto[0]);
    const anchorOffset = findOffset(str, anchorId);
    if (focusPointDto) {
      const focusId = id.fromDto(sid, focusPointDto[0]);
      const focusOffset = findOffset(str, focusId);
      result.push([anchorOffset, focusOffset]);
    } else {
      result.push([anchorOffset]);
    }
  }
  return result;
};