All files / collaborative-slate/src/sync FromSlate.ts

98.18% Statements 54/55
85.71% Branches 30/35
100% Functions 6/6
100% Lines 49/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 889x 9x         9x 1764x                       9x 788x   788x 788x     3737x 3737x 3737x 1973x 1973x 1973x 1973x 378x 378x 551x 551x 551x   551x     551x 551x 551x       1764x 1764x 1764x 1764x 1764x 1764x 1764x 1764x 1563x   1563x     1563x 1563x   1764x         2552x 2552x 2552x 2552x 3737x 3737x 3737x 3737x 3737x         788x 788x 788x      
import {Anchor} from 'json-joy/lib/json-crdt-extensions/peritext/rga/constants';
import {SliceHeaderShift, SliceStacking} from 'json-joy/lib/json-crdt-extensions/peritext/slice/constants';
import type {ViewRange, ViewSlice} from 'json-joy/lib/json-crdt-extensions/peritext/editor/types';
import type {SlateDocument, SlateDescendantNode, SlateTextNode, SlateElementNode} from '../types';
import type {SliceTypeStep, SliceTypeSteps} from 'json-joy/lib/json-crdt-extensions/peritext';
 
const isInline = (node: unknown): node is SlateTextNode =>
  typeof node === 'object' && !!node && typeof (node as SlateTextNode).text === 'string';
 
/**
 * Converts Slate.js state to a {@link ViewRange} flat string with
 * annotation ranges, which is the natural view format for a Peritext model.
 *
 * Usage:
 *
 * ```typescript
 * FromSlate.convert(node);
 * ```
 */
export class FromSlate {
  static readonly convert = (doc: SlateDocument): ViewRange => new FromSlate().convert(doc);
 
  private text = '';
  private slices: ViewSlice[] = [];
 
  private conv(node: SlateDescendantNode, path: SliceTypeSteps, nodeDiscriminator: number): void {
    Iif (!node || typeof node !== 'object') return;
    const start = this.text.length;
    if ('text' in node) {
      const {text, ...tagMap} = node as SlateTextNode;
      this.text += text;
      const tags = Object.keys(tagMap);
      if (tags.length) {
        const end = start + text.length;
        for (const tag of tags) {
          const data = tagMap[tag];
          const dataEmpty = !data || data === true;
          const stacking: SliceStacking = dataEmpty ? SliceStacking.One : SliceStacking.Many;
          const header =
            (stacking << SliceHeaderShift.Stacking) +
            (Anchor.Before << SliceHeaderShift.X1Anchor) +
            (Anchor.After << SliceHeaderShift.X2Anchor);
          const slice: ViewSlice = [header, start, end, tag];
          if (!dataEmpty) slice.push(data);
          this.slices.push(slice);
        }
      }
    } else {
      const element = node as SlateElementNode;
      const {type, children, ...data} = element;
      const step: SliceTypeStep = nodeDiscriminator || data ? [type, nodeDiscriminator, data] : type;
      const length = children?.length ?? 0;
      const hasNoChildren = length === 0;
      const isFirstChildInline = isInline((children as SlateElementNode['children'])?.[0]);
      const doEmitSplitMarker = hasNoChildren || isFirstChildInline;
      if (doEmitSplitMarker) {
        this.text += '\n';
        const header =
          (SliceStacking.Marker << SliceHeaderShift.Stacking) +
          (Anchor.Before << SliceHeaderShift.X1Anchor) +
          (Anchor.Before << SliceHeaderShift.X2Anchor);
        const slice: ViewSlice = [header, start, start, [...path, step]];
        this.slices.push(slice);
      }
      Eif (length > 0) this.cont([...path, step], children!);
    }
  }
 
  private cont(path: SliceTypeSteps, content: SlateDescendantNode[]): void {
    let prevTag: string = '';
    let discriminator: number = 0;
    const length = content.length;
    for (let i = 0; i < length; i++) {
      const child = content[i];
      const tag = child.type as string;
      discriminator = tag === prevTag ? discriminator + 1 : 0;
      this.conv(child, path, discriminator);
      prevTag = tag;
    }
  }
 
  public convert(node: SlateDocument): ViewRange {
    let length = 0;
    Eif (node && (length = node.length) > 0) this.cont([], node);
    return [this.text, 0, this.slices] as ViewRange;
  }
}