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 1709x                       9x 766x   766x 766x     3597x 3597x 3597x 1888x 1888x 1888x 1888x 368x 368x 496x 496x 496x   496x     496x 496x 496x       1709x 1709x 1709x 1709x 1709x 1709x 1709x 1709x 1508x   1508x     1508x 1508x   1709x         2475x 2475x 2475x 2475x 3597x 3597x 3597x 3597x 3597x         766x 766x 766x      
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;
  }
}