slate#Operation TypeScript Examples

The following examples show how to use slate#Operation. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: utils.ts    From slate-ot with MIT License 7 votes vote down vote up
checkOp = (snapshot: Editor, op: Operation) => {
  switch (op.type) {
    case 'remove_text': {
      const leaf = Node.leaf(snapshot, op.path);
      const textToRemove = leaf.text.slice(
        op.offset,
        op.offset + op.text.length
      );

      expect(textToRemove).toBe(op.text);

      break;
    }

    case 'merge_node': {
      const prev = Node.get(snapshot, Path.previous(op.path));

      const prevLen = Text.isText(prev)
        ? prev.text.length
        : prev.children.length;

      expect(prevLen).toBe(op.position);

      break;
    }

    case 'remove_node': {
      // op.node needs to be checked
      break;
    }

    default:
      return;
  }
}
Example #2
Source File: utils.ts    From slate-ot with MIT License 7 votes vote down vote up
applyOp = (
  snapshot: Editor,
  ops: Operation[] | Operation
): Editor => {
  slateType.normalize(ops).forEach((op) => {
    checkOp(snapshot, op);
    slateType.apply(snapshot, op);
  });
  return snapshot;
}
Example #3
Source File: SlateType.ts    From slate-ot with MIT License 6 votes vote down vote up
doTransform = (
  leftOp: Operation,
  rightOp: Operation,
  side: 'left' | 'right'
): Operation[] => {
  // return side === 'left' ? leftOp : rightOp;
  switch (leftOp.type) {
    case 'insert_text':
      return OT.transInsertText(leftOp, rightOp, side);
    case 'remove_text':
      return OT.transRemoveText(leftOp, rightOp, side);
    case 'insert_node':
      return OT.transInsertNode(leftOp, rightOp, side);
    case 'remove_node':
      return OT.transRemoveNode(leftOp, rightOp, side);
    case 'split_node':
      return OT.transSplitNode(leftOp, rightOp, side);
    case 'merge_node':
      return OT.transMergeNode(leftOp, rightOp, side);
    case 'move_node':
      return OT.transMoveNode(leftOp, rightOp, side);
    case 'set_node':
      return OT.transSetNode(leftOp, rightOp, side);
    default:
      throw new Error('Unsupported OP');
  }
}
Example #4
Source File: op-generator.ts    From slate-ot with MIT License 6 votes vote down vote up
generateRandomRemoveTextOp = (snapshot): Operation | null => {
  const randomLeaf = getRandomLeafWithPath(snapshot);

  if (!randomLeaf) return null;

  const offset = fuzzer.randomInt(randomLeaf.text.length);
  const textLength = fuzzer.randomInt(randomLeaf.text.length - offset);

  return {
    type: 'remove_text',
    path: randomLeaf.path,
    offset,
    text: randomLeaf.text.slice(offset, offset + textLength),
  };
}
Example #5
Source File: op-generator.ts    From slate-ot with MIT License 6 votes vote down vote up
generateRandomInsertTextOp = (snapshot): Operation | null => {
  const randomLeaf = getRandomLeafWithPath(snapshot);

  return randomLeaf
    ? {
        type: 'insert_text',
        path: randomLeaf.path,
        offset: fuzzer.randomInt(randomLeaf.text.length),
        text: fuzzer.randomWord(),
      }
    : null;
}
Example #6
Source File: op-generator.ts    From slate-ot with MIT License 6 votes vote down vote up
generateAndApplyRandomOp = function (snapshot) {
  const result = _.cloneDeep(snapshot);

  let op: Operation | null = null;
  while (!op) {
    let index = fuzzer.randomInt(genRandOp.length);
    op = genRandOp[index](snapshot);
  }

  Transforms.transform(result, op);
  return [[op], result];
}
Example #7
Source File: op-generator.ts    From slate-ot with MIT License 6 votes vote down vote up
generateRandomInsertNodeOp = (snapshot): Operation => {
  const randomPath = getRandomPathTo(snapshot);

  const parent = <Ancestor>Node.get(snapshot, Path.parent(randomPath));

  let node;

  if (parent.children[0] && Text.isText(parent.children[0])) {
    node = { text: fuzzer.randomWord() };
  } else if (!parent.children[0] && fuzzer.randomInt(3) === 0) {
    node = { text: fuzzer.randomWord() };
  } else if (fuzzer.randomInt(2) === 0) {
    node = {
      type: BLOCKS[fuzzer.randomInt(BLOCKS.length)],
      children: [{ text: fuzzer.randomWord() }, { text: fuzzer.randomWord() }],
    };
  } else {
    node = {
      type: BLOCKS[fuzzer.randomInt(BLOCKS.length)],
      children: [
        {
          type: BLOCKS[fuzzer.randomInt(BLOCKS.length)],
          children: [
            { text: fuzzer.randomWord() },
            { text: fuzzer.randomWord() },
          ],
        },
      ],
    };
  }

  return {
    type: 'insert_node',
    path: randomPath,
    node,
  };
}
Example #8
Source File: op-generator.ts    From slate-ot with MIT License 6 votes vote down vote up
generateRandomRemoveNodeOp = (snapshot): Operation | null => {
  const randomPath = getRandomPathFrom(snapshot);

  return randomPath.length
    ? {
        type: 'remove_node',
        path: randomPath,
        node: Node.get(snapshot, randomPath),
      }
    : null;
}
Example #9
Source File: op-generator.ts    From slate-ot with MIT License 6 votes vote down vote up
generateRandomSplitNodeOp = (snapshot): Operation | null => {
  const randomPath = getRandomPathFrom(snapshot);

  const node = Node.get(snapshot, randomPath);
  const position = Text.isText(node)
    ? <number>fuzzer.randomInt(node.text.length + 1)
    : <number>fuzzer.randomInt(node.children.length + 1);

  return randomPath.length
    ? {
        type: 'split_node',
        path: randomPath,
        position,
        target: null,
        properties: {},
      }
    : null;
}
Example #10
Source File: op-generator.ts    From slate-ot with MIT License 6 votes vote down vote up
generateRandomMoveNodeOp = (snapshot): Operation | null => {
  let count = 0;
  while (count < 10) {
    count++;
    const path = getRandomPathFrom(snapshot);
    const newPath = getRandomPathTo(snapshot);

    if (Path.isSibling(path, newPath)) {
      const parent = <Ancestor>Node.get(snapshot, Path.parent(newPath));
      if (newPath[newPath.length - 1] == parent.children.length) {
        newPath[newPath.length - 1]--;
      }
    }

    if (!Path.isAncestor(path, newPath)) {
      return {
        type: 'move_node',
        path,
        newPath,
      };
    }
  }
  return null;
}
Example #11
Source File: op-generator.ts    From slate-ot with MIT License 6 votes vote down vote up
generateRandomSetNodeOp = (snapshot): Operation | null => {
  const path = getRandomPathFrom(snapshot);

  if (path.length === 0) return null;

  const newProperties = {};

  KEYS.forEach((key) => {
    if (fuzzer.randomInt(2) === 0) {
      newProperties[key] = VALUES[fuzzer.randomInt(VALUES.length)];
    }
  });

  return {
    type: 'set_node',
    path,
    properties: {},
    newProperties,
  };
}
Example #12
Source File: SlateType.ts    From slate-ot with MIT License 6 votes vote down vote up
xTransform1x1 = (
  leftOp: Operation,
  rightOp: Operation,
  side: 'left' | 'right'
): [Operation[], Operation[]] => {
  const other = side === 'left' ? 'right' : 'left';
  return [
    doTransform(leftOp, rightOp, side),
    doTransform(rightOp, leftOp, other),
  ];
}
Example #13
Source File: SlateType.ts    From slate-ot with MIT License 6 votes vote down vote up
xTransform1xN = (
  leftOp: Operation,
  rightOps: Operation[],
  side: 'left' | 'right'
): [Operation[], Operation[]] => {
  let rRes: Operation[] = [];

  for (let n = 0; n < rightOps.length; n++) {
    let rightOp: Operation = rightOps[n];

    let [l, r] = xTransform1x1(leftOp, rightOp, side);
    rRes = rRes.concat(r);

    if (l.length === 0) {
      rRes = rRes.concat(rightOps.slice(n + 1));
      return [[], rRes];
    }

    if (l.length > 1) {
      [l, r] = xTransformMxN(l, rightOps.slice(n + 1), side);
      rRes = rRes.concat(r);
      return [l, rRes];
    }

    // l.length == 1
    leftOp = l[0];
  }
  return [[leftOp], rRes];
}
Example #14
Source File: SlateType.ts    From slate-ot with MIT License 6 votes vote down vote up
xTransformMxN = (
  leftOps: Operation[],
  rightOps: Operation[],
  side: 'left' | 'right'
): [Operation[], Operation[]] => {
  let leftRes: Operation[] = [];

  for (let m = 0; m < leftOps.length; m++) {
    let leftOp: Operation = leftOps[m];

    let [lRes, rRes] = xTransform1xN(leftOp, rightOps, side);

    leftRes = leftRes.concat(lRes);

    rightOps = rRes;
  }

  return [leftRes, rightOps];
}
Example #15
Source File: OT.ts    From slate-ot with MIT License 6 votes vote down vote up
pathTransform = (
  leftOp: NodeOperation | TextOperation,
  rightOp: Operation
): (NodeOperation | TextOperation)[] => {
  let path = Path.transform(leftOp.path, rightOp);

  return path
    ? [
        {
          ...leftOp,
          path,
        },
      ]
    : [];
}
Example #16
Source File: client.ts    From slate-ot with MIT License 6 votes vote down vote up
doc.subscribe((err: any) => {
  if (err) {
    throw err;
  }

  e.children = doc.data.children;
  console.log(JSON.stringify(e.children));

  doc.on('op', (op: Operation | Operation[], options: any) => {
    if (options.source === clientId) return;

    const ops = Array.isArray(op) ? op : [op];

    for (const o of ops) {
      console.log(op);
      Transforms.transform(e, o);
    }
  });

  e.apply({
    type: 'insert_node',
    path: [0],
    node: { children: [{ text: 'a quick brown fox' }] },
  });
});
Example #17
Source File: slate-plugin.ts    From slate-vue with MIT License 6 votes vote down vote up
elementWatcherPlugin = (vm: any, type: string) => {
  const update = vm._watcher.update;
  vm._watcher.update = () => {
    const op: Operation = vm.$editor._operation;
    // some op doesn't change element, so prevent updating
    if(op) {
      if(op.type === 'remove_text' || op.type === 'insert_text' || op.type === 'set_selection') {
        return
      }
      if(op.type === 'remove_node' && type === 'element') {
        return
      }
    }
    update.call(vm._watcher)
  }
  // gvm.$on('forceUpdate', ()=>{
  //   update.call(vm._watcher)
  // })
}
Example #18
Source File: SlateType.ts    From slate-ot with MIT License 5 votes vote down vote up
slateType = {
  name: 'slate-ot-type',

  uri: 'http://sharejs.org/types/slate-ot-type',

  create(init: Element): Editor {
    const e = createEditor();
    e.children = init.children;
    return <Editor>init;
  },

  apply(snapshot: Editor, op: Operation[] | Operation) {
    slateType.normalize(op).forEach((o) => {
      if (o.type === 'set_node' && o.path.length === 0) {
        for (const key in o.newProperties) {
          if (key === 'id' || key === 'type' || key === 'children') {
            throw new Error(`Cannot set the "${key}" property of nodes!`);
          }

          const value = o.newProperties[key];

          if (value == null) {
            delete snapshot[key];
          } else {
            snapshot[key] = value;
          }
        }
      } else {
        Transforms.transform(snapshot, o);
      }
    });
    return snapshot;
  },

  transform(
    leftOps: Operation[],
    rightOps: Operation[],
    side: 'left' | 'right'
  ): Operation[] {
    let [leftRes] = xTransformMxN(leftOps, rightOps, side);
    return leftRes;
  },

  // serialize(snapshot) {
  //   return JSON.stringify(snapshot);
  // },

  // deserialize(data) {
  //   // return Value.fromJSON(data);
  // },

  normalize(op: Operation | Operation[]): Operation[] {
    return Array.isArray(op) ? op : [op];
  },
}
Example #19
Source File: op-generator.ts    From slate-ot with MIT License 5 votes vote down vote up
generateRandomMergeNodeOp = (snapshot): Operation | null => {
  const randomPath = getRandomPathFrom(snapshot);

  if (randomPath.length == 0 || randomPath[randomPath.length - 1] == 0) {
    return null;
  }

  const prev = Node.get(snapshot, Path.previous(randomPath));

  const node = Node.get(snapshot, randomPath);

  const properties = {};

  // Object.keys(prev).forEach((key) => {
  //   if (key !== 'text' && key !== 'children') {
  //     properties[key] = null;
  //   }
  // });
  // Object.keys(node).forEach((key) => {
  //   if (key !== 'text' && key !== 'children') {
  //     properties[key] = node[key];
  //   }
  // });

  if (Text.isText(prev) && Text.isText(node)) {
    return {
      type: 'merge_node',
      path: randomPath,
      position: prev.text.length,
      target: null,
      properties,
    };
  }

  if (!Text.isText(prev) && !Text.isText(node)) {
    return {
      type: 'merge_node',
      path: randomPath,
      position: prev.children.length,
      target: null,
      properties,
    };
  }

  return null;
}
Example #20
Source File: client.ts    From slate-ot with MIT License 5 votes vote down vote up
e.onChange = () => {
  e.operations.forEach((o: Operation) => {
    if (o.type !== 'set_selection') {
      doc.submitOp(o, { source: clientId });
    }
  });
};
Example #21
Source File: transSetNode.ts    From slate-ot with MIT License 5 votes vote down vote up
transSetNode = (
  leftOp: SetNodeOperation,
  rightOp: Operation,
  side: 'left' | 'right'
): SetNodeOperation[] => {
  switch (rightOp.type) {
    case 'split_node': {
      if (Path.equals(leftOp.path, rightOp.path)) {
        return [
          leftOp,
          {
            ...leftOp,
            path: Path.next(leftOp.path),
          },
        ];
      }

      return <SetNodeOperation[]>pathTransform(leftOp, rightOp);
    }

    case 'merge_node': {
      if (Path.equals(leftOp.path, rightOp.path)) {
        return [];
      }

      return <SetNodeOperation[]>pathTransform(leftOp, rightOp);
    }

    case 'set_node': {
      if (!Path.equals(leftOp.path, rightOp.path)) {
        return [leftOp];
      }

      return side === 'left'
        ? [
            {
              ...leftOp,
              newProperties: {
                ...rightOp.newProperties,
                ...leftOp.newProperties,
              },
            },
          ]
        : [
            {
              ...leftOp,
              newProperties: {
                ...leftOp.newProperties,
                ...rightOp.newProperties,
              },
            },
          ];
    }

    // insert_text
    // remove_text
    // insert_node
    // remove_node
    // move_node
    default:
      return <SetNodeOperation[]>pathTransform(leftOp, rightOp);
  }
}
Example #22
Source File: runtime-util.ts    From slate-vue with MIT License 5 votes vote down vote up
transform = function(editor: Editor, op: Operation, Vue?: any) {
  switch (op.type) {
    case 'insert_node': {
      const { path, node } = op
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]
      getChildren(parent).splice(index, 0, clone(node))

      break
    }

    case 'insert_text': {
      const { path, offset, text } = op
      const node = Node.leaf(editor, path)
      const before = node.text.slice(0, offset)
      const after = node.text.slice(offset)
      node.text = before + text + after

      break
    }

    case 'merge_node': {
      const { path } = op
      const node = Node.get(editor, path)
      const prevPath = Path.previous(path)
      const prev = Node.get(editor, prevPath)
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]

      if (Text.isText(node) && Text.isText(prev)) {
        prev.text += node.text
      } else if (!Text.isText(node) && !Text.isText(prev)) {
        getChildren(prev).push(...getChildren(node))
      } else {
        throw new Error(
          `Cannot apply a "merge_node" operation at path [${path}] to nodes of different interaces: ${node} ${prev}`
        )
      }

      getChildren(parent).splice(index, 1)

      break
    }

    case 'move_node': {
      const { path, newPath } = op

      if (Path.isAncestor(path, newPath)) {
        throw new Error(
          `Cannot move a path [${path}] to new path [${newPath}] because the destination is inside itself.`
        )
      }

      const node = Node.get(editor, path)
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]

      // This is tricky, but since the `path` and `newPath` both refer to
      // the same snapshot in time, there's a mismatch. After either
      // removing the original position, the second step's path can be out
      // of date. So instead of using the `op.newPath` directly, we
      // transform `op.path` to ascertain what the `newPath` would be after
      // the operation was applied.
      getChildren(parent).splice(index, 1)
      const truePath = Path.transform(path, op)!
      const newParent = Node.get(editor, Path.parent(truePath))
      const newIndex = truePath[truePath.length - 1]

      getChildren(newParent).splice(newIndex, 0, node)

      break
    }

    case 'remove_node': {
      const { path } = op
      NODE_TO_KEY.delete(Node.get(editor, path))
      const index = path[path.length - 1]
      const parent = Node.parent(editor, path)
      getChildren(parent).splice(index, 1)

      break
    }

    case 'remove_text': {
      const { path, offset, text } = op
      const node = Node.leaf(editor, path)
      const before = node.text.slice(0, offset)
      const after = node.text.slice(offset + text.length)
      node.text = before + after

      break
    }

    case 'set_node': {
      const { path, newProperties } = op

      if (path.length === 0) {
        throw new Error(`Cannot set properties on the root node!`)
      }

      const node = Node.get(editor, path)

      for (const key in newProperties) {
        if (key === 'children' || key === 'text') {
          throw new Error(`Cannot set the "${key}" property of nodes!`)
        }

        const value = (newProperties as any)[key]

        if(Vue) {
          if (value == null) {
            Vue.delete(node, key)
          } else {
            Vue.set(node, key, value)
          }
        }
      }

      break
    }

    case 'set_selection': {
      break
    }

    case 'split_node': {
      const { path, position, properties } = op

      if (path.length === 0) {
        throw new Error(
          `Cannot apply a "split_node" operation at path [${path}] because the root node cannot be split.`
        )
      }

      const node = Node.get(editor, path)
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]
      let newNode: Descendant

      if (Text.isText(node)) {
        const before = node.text.slice(0, position)
        const after = node.text.slice(position)
        node.text = before
        newNode = {
          ...node,
          ...(properties as Partial<Text>),
          text: after,
        }
      } else {
        const before = node.children.slice(0, position)
        const after = node.children.slice(position)
        node.children = before

        newNode = {
          ...node,
          ...(properties as Partial<Element>),
          children: after,
        }
      }

      getChildren(parent).splice(index + 1, 0, newNode)

      break
    }
  }
}
Example #23
Source File: runtime-util.ts    From slate-vue with MIT License 5 votes vote down vote up
transform = function(editor: Editor, op: Operation) {
  switch (op.type) {
    case 'insert_node': {
      const { path, node } = op
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]
      getChildren(parent).splice(index, 0, clone(node))

      break
    }

    case 'insert_text': {
      const { path, offset, text } = op
      const node = Node.leaf(editor, path)
      const before = node.text.slice(0, offset)
      const after = node.text.slice(offset)
      node.text = before + text + after

      break
    }

    case 'merge_node': {
      const { path } = op
      const node = Node.get(editor, path)
      const prevPath = Path.previous(path)
      const prev = Node.get(editor, prevPath)
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]

      if (Text.isText(node) && Text.isText(prev)) {
        prev.text += node.text
      } else if (!Text.isText(node) && !Text.isText(prev)) {
        getChildren(prev).push(...getChildren(node))
      } else {
        throw new Error(
          `Cannot apply a "merge_node" operation at path [${path}] to nodes of different interaces: ${node} ${prev}`
        )
      }

      getChildren(parent).splice(index, 1)

      break
    }

    case 'move_node': {
      const { path, newPath } = op

      if (Path.isAncestor(path, newPath)) {
        throw new Error(
          `Cannot move a path [${path}] to new path [${newPath}] because the destination is inside itself.`
        )
      }

      const node = Node.get(editor, path)
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]

      // This is tricky, but since the `path` and `newPath` both refer to
      // the same snapshot in time, there's a mismatch. After either
      // removing the original position, the second step's path can be out
      // of date. So instead of using the `op.newPath` directly, we
      // transform `op.path` to ascertain what the `newPath` would be after
      // the operation was applied.
      getChildren(parent).splice(index, 1)
      const truePath = Path.transform(path, op)!
      const newParent = Node.get(editor, Path.parent(truePath))
      const newIndex = truePath[truePath.length - 1]

      getChildren(newParent).splice(newIndex, 0, node)

      break
    }

    case 'remove_node': {
      const { path } = op
      NODE_TO_KEY.delete(Node.get(editor, path))
      const index = path[path.length - 1]
      const parent = Node.parent(editor, path)
      getChildren(parent).splice(index, 1)

      break
    }

    case 'remove_text': {
      const { path, offset, text } = op
      const node = Node.leaf(editor, path)
      const before = node.text.slice(0, offset)
      const after = node.text.slice(offset + text.length)
      node.text = before + after

      break
    }

    case 'set_node': {
      const { path, newProperties } = op

      if (path.length === 0) {
        throw new Error(`Cannot set properties on the root node!`)
      }

      const node = Node.get(editor, path)

      for (const key in newProperties) {
        if (key === 'children' || key === 'text') {
          throw new Error(`Cannot set the "${key}" property of nodes!`)
        }

        const value = (newProperties as any)[key]

        if (value == null) {
          Vue.delete(node, key)
        } else {
          Vue.set(node, key, value)
        }
      }

      break
    }

    case 'set_selection': {
      break
    }

    case 'split_node': {
      const { path, position, properties } = op

      if (path.length === 0) {
        throw new Error(
          `Cannot apply a "split_node" operation at path [${path}] because the root node cannot be split.`
        )
      }

      const node = Node.get(editor, path)
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]
      let newNode: Descendant

      if (Text.isText(node)) {
        const before = node.text.slice(0, position)
        const after = node.text.slice(position)
        node.text = before
        newNode = {
          ...node,
          ...(properties as Partial<Text>),
          text: after,
        }
      } else {
        const before = node.children.slice(0, position)
        const after = node.children.slice(position)
        node.children = before

        newNode = {
          ...node,
          ...(properties as Partial<Element>),
          children: after,
        }
      }

      getChildren(parent).splice(index + 1, 0, newNode)

      break
    }
  }
}
Example #24
Source File: with-angular.ts    From slate-angular with MIT License 4 votes vote down vote up
withAngular = <T extends Editor>(editor: T, clipboardFormatKey = 'x-slate-fragment') => {
  const e = editor as T & AngularEditor;
  const { apply, onChange, deleteBackward } = e;

  e.deleteBackward = unit => {
    if (unit !== 'line') {
      return deleteBackward(unit);
    }

    if (editor.selection && Range.isCollapsed(editor.selection)) {
      const parentBlockEntry = Editor.above(editor, {
        match: n => Editor.isBlock(editor, n),
        at: editor.selection,
      });

      if (parentBlockEntry) {
        const [, parentBlockPath] = parentBlockEntry;
        const parentElementRange = Editor.range(
          editor,
          parentBlockPath,
          editor.selection.anchor
        );

        const currentLineRange = findCurrentLineRange(e, parentElementRange);

        if (!Range.isCollapsed(currentLineRange)) {
          Transforms.delete(editor, { at: currentLineRange });
        }
      }
    }
  };

  e.apply = (op: Operation) => {
    const matches: [Path | PathRef, Key][] = [];

    switch (op.type) {
      case 'insert_text':
      case 'remove_text':
      case 'set_node': {
        for (const [node, path] of Editor.levels(e, { at: op.path })) {
          const key = AngularEditor.findKey(e, node);
          matches.push([path, key]);
        }

        break;
      }

      case 'insert_node':
      case 'remove_node':
      case 'merge_node':
      case 'split_node': {
        for (const [node, path] of Editor.levels(e, {
          at: Path.parent(op.path),
        })) {
          const key = AngularEditor.findKey(e, node);
          matches.push([path, key]);
        }

        break;
      }

      case 'move_node': {
        const commonPath = Path.common(Path.parent(op.path), Path.parent(op.newPath));
        for (const [node, path] of Editor.levels(e, { at: Path.parent(op.path) })) {
          const key = AngularEditor.findKey(e, node);
          matches.push([Editor.pathRef(editor, path), key]);
        }
        for (const [node, path] of Editor.levels(e, { at: Path.parent(op.newPath) })) {
          if(path.length > commonPath.length){
            const key = AngularEditor.findKey(e, node);
            matches.push([Editor.pathRef(editor, path), key]);
          }
        }
        break;
      }
    }

    apply(op);

    for (const [source, key] of matches) {
      const [node] = Editor.node(e, Path.isPath(source) ? source: source.current);
      NODE_TO_KEY.set(node, key);
    }
  };

  e.onChange = () => {
    const onContextChange = EDITOR_TO_ON_CHANGE.get(e);

    if (onContextChange) {
      onContextChange();
    }

    onChange();
  };

  e.setFragmentData = (data: Pick<DataTransfer, 'getData' | 'setData'>) => {
    const { selection } = e;

    if (!selection) {
      return;
    }

    const [start, end] = Range.edges(selection);
    const startVoid = Editor.void(e, { at: start.path });
    const endVoid = Editor.void(e, { at: end.path });

    if (Range.isCollapsed(selection) && !startVoid) {
      return;
    }

    // Create a fake selection so that we can add a Base64-encoded copy of the
    // fragment to the HTML, to decode on future pastes.
    const domRange = AngularEditor.toDOMRange(e, selection);
    let contents = domRange.cloneContents();
    let attach = contents.childNodes[0] as HTMLElement;

    // Make sure attach is non-empty, since empty nodes will not get copied.
    const contentsArray = Array.from(contents.children);
    contentsArray.forEach(node => {
      if (node.textContent && node.textContent.trim() !== '') {
        attach = node as HTMLElement;
      }
    });

    // COMPAT: If the end node is a void node, we need to move the end of the
    // range from the void node's spacer span, to the end of the void node's
    // content, since the spacer is before void's content in the DOM.
    if (endVoid) {
      const [voidNode] = endVoid;
      const r = domRange.cloneRange();
      const domNode = AngularEditor.toDOMNode(e, voidNode);
      r.setEndAfter(domNode);
      contents = r.cloneContents();
    }

    // COMPAT: If the start node is a void node, we need to attach the encoded
    // fragment to the void node's content node instead of the spacer, because
    // attaching it to empty `<div>/<span>` nodes will end up having it erased by
    // most browsers. (2018/04/27)
    if (startVoid) {
      attach = contents.querySelector('[data-slate-spacer]')! as HTMLElement;
    }

    // Remove any zero-width space spans from the cloned DOM so that they don't
    // show up elsewhere when pasted.
    Array.from(contents.querySelectorAll('[data-slate-zero-width]')).forEach(
      zw => {
        const isNewline = zw.getAttribute('data-slate-zero-width') === 'n';
        zw.textContent = isNewline ? '\n' : '';
      }
    );

    // Set a `data-slate-fragment` attribute on a non-empty node, so it shows up
    // in the HTML, and can be used for intra-Slate pasting. If it's a text
    // node, wrap it in a `<span>` so we have something to set an attribute on.
    if (isDOMText(attach)) {
      const span = attach.ownerDocument.createElement('span');
      // COMPAT: In Chrome and Safari, if we don't add the `white-space` style
      // then leading and trailing spaces will be ignored. (2017/09/21)
      span.style.whiteSpace = 'pre';
      span.appendChild(attach);
      contents.appendChild(span);
      attach = span;
    }

    const fragment = e.getFragment();
    const stringObj = JSON.stringify(fragment);
    const encoded = window.btoa(encodeURIComponent(stringObj));
    attach.setAttribute('data-slate-fragment', encoded);
    data.setData(`application/${clipboardFormatKey}`, encoded);

    // Add the content to a <div> so that we can get its inner HTML.
    const div = contents.ownerDocument.createElement('div');
    div.appendChild(contents);
    div.setAttribute('hidden', 'true');
    contents.ownerDocument.body.appendChild(div);
    data.setData('text/html', div.innerHTML);
    data.setData('text/plain', getPlainText(div));
    contents.ownerDocument.body.removeChild(div);
    return data;
  };

  e.deleteCutData = () => {
    const { selection } = editor;
    if (selection) {
      if (Range.isExpanded(selection)) {
        Editor.deleteFragment(editor);
      } else {
        const node = Node.parent(editor, selection.anchor.path);
        if (Editor.isVoid(editor, node)) {
          Transforms.delete(editor);
        }
      }
    }
  };

  e.insertData = (data: DataTransfer) => {
    if (!e.insertFragmentData(data)) {
      e.insertTextData(data);
    }
  };

  e.insertFragmentData = (data: DataTransfer): boolean => {
    /**
     * Checking copied fragment from application/x-slate-fragment or data-slate-fragment
     */
    const fragment =
      data.getData(`application/${clipboardFormatKey}`) ||
      getSlateFragmentAttribute(data);

    if (fragment) {
      const decoded = decodeURIComponent(window.atob(fragment));
      const parsed = JSON.parse(decoded) as Node[];
      e.insertFragment(parsed);
      return true;
    }
    return false;
  };

  e.insertTextData = (data: DataTransfer): boolean => {
    const text = data.getData('text/plain');

    if (text) {
      const lines = text.split(/\r\n|\r|\n/);
      let split = false;

      for (const line of lines) {
        if (split) {
          Transforms.splitNodes(e, { always: true });
        }

        e.insertText(line);
        split = true;
      }
      return true;
    }
    return false;
  };

  e.onKeydown = () => { };

  e.onClick = () => { };

  e.isBlockCard = (element) => false;

  e.onError = (errorData: SlateError) => {
    if (errorData.nativeError) {
      console.error(errorData.nativeError);
    } else {
      console.error(errorData);
    }
  };

  return e;
}
Example #25
Source File: transSplitNode.ts    From slate-ot with MIT License 4 votes vote down vote up
transSplitNode = (
  leftOp: SplitNodeOperation,
  rightOp: Operation,
  _side: 'left' | 'right'
): SplitNodeOperation[] => {
  switch (rightOp.type) {
    case 'insert_text': {
      if (!Path.equals(leftOp.path, rightOp.path)) {
        return [leftOp];
      }

      if (leftOp.position < rightOp.offset) {
        return [leftOp];
      }

      return [
        {
          ...leftOp,
          position: leftOp.position + rightOp.text.length,
        },
      ];
    }

    case 'remove_text': {
      if (!Path.equals(leftOp.path, rightOp.path)) {
        return [leftOp];
      }

      if (leftOp.position < rightOp.offset) {
        return [leftOp];
      }

      if (leftOp.position >= rightOp.offset + rightOp.text.length) {
        return [
          {
            ...leftOp,
            position: leftOp.position - rightOp.text.length,
          },
        ];
      }

      return [
        {
          ...leftOp,
          position: rightOp.offset,
        },
      ];
    }

    case 'insert_node': {
      if (Path.isParent(leftOp.path, rightOp.path)) {
        let offset = rightOp.path[rightOp.path.length - 1];

        if (leftOp.position <= offset) {
          return [leftOp];
        }

        return [
          {
            ...leftOp,
            position: leftOp.position + 1,
          },
        ];
      }

      return <SplitNodeOperation[]>pathTransform(leftOp, rightOp);
    }

    case 'remove_node': {
      if (Path.isParent(leftOp.path, rightOp.path)) {
        let offset = rightOp.path[rightOp.path.length - 1];

        if (leftOp.position <= offset) {
          return [leftOp];
        }

        return [
          {
            ...leftOp,
            position: leftOp.position - 1,
          },
        ];
      }

      return <SplitNodeOperation[]>pathTransform(leftOp, rightOp);
    }

    case 'split_node': {
      if (Path.equals(leftOp.path, rightOp.path)) {
        if (leftOp.position < rightOp.position) {
          return [leftOp];
        }

        if (leftOp.position === rightOp.position) {
          return [];
        }

        return [
          {
            ...leftOp,
            path: Path.next(leftOp.path),
            position: leftOp.position - rightOp.position,
          },
        ];
      }

      if (Path.isParent(leftOp.path, rightOp.path)) {
        const offset = rightOp.path[rightOp.path.length - 1];

        if (leftOp.position <= offset) {
          return [leftOp];
        }

        return [
          {
            ...leftOp,
            position: leftOp.position + 1,
          },
        ];
      }

      return <SplitNodeOperation[]>pathTransform(leftOp, rightOp);
    }

    case 'merge_node': {
      if (Path.equals(leftOp.path, rightOp.path)) {
        return [];
      }

      if (Path.isParent(leftOp.path, rightOp.path)) {
        const offset = rightOp.path[rightOp.path.length - 1];

        if (leftOp.position < offset) {
          return [leftOp];
        }

        if (leftOp.position > offset) {
          return [
            {
              ...leftOp,
              position: leftOp.position - 1,
            },
          ];
        }

        // conflicting ops, discard split
        return [];
      }

      return <SplitNodeOperation[]>pathTransform(leftOp, rightOp);
    }

    case 'move_node': {
      if (Path.equals(rightOp.path, rightOp.newPath)) {
        return [leftOp];
      }

      let position = leftOp.position;
      let offset = rightOp.path[rightOp.path.length - 1];
      let newOffset = rightOp.newPath[rightOp.newPath.length - 1];

      // only src path is leftOp's child
      if (
        Path.isParent(leftOp.path, rightOp.path) &&
        offset < leftOp.position
      ) {
        position--;
      }

      // only dst path is leftOp's child
      if (
        Path.isParent(leftOp.path, rightOp.newPath) &&
        newOffset < leftOp.position
      ) {
        position++;
      }

      return [
        {
          ...leftOp,
          path: Path.transform(leftOp.path, rightOp)!,
          position,
        },
      ];
    }

    // set_node
    default:
      return <SplitNodeOperation[]>pathTransform(leftOp, rightOp);
  }
}
Example #26
Source File: transRemoveText.ts    From slate-ot with MIT License 4 votes vote down vote up
transRemoveText = (
  leftOp: RemoveTextOperation,
  rightOp: Operation,
  _side: 'left' | 'right'
): RemoveTextOperation[] => {
  switch (rightOp.type) {
    case 'insert_text': {
      if (!Path.equals(leftOp.path, rightOp.path)) {
        return [leftOp];
      }

      if (leftOp.offset + leftOp.text.length <= rightOp.offset) {
        return [leftOp];
      }

      if (rightOp.offset <= leftOp.offset) {
        return [
          {
            ...leftOp,
            offset: leftOp.offset + rightOp.text.length,
          },
        ];
      }

      const intersectingIndex = rightOp.offset - leftOp.offset;
      const leftText = leftOp.text.slice(0, intersectingIndex);
      const rightText = leftOp.text.slice(intersectingIndex);
      return [
        {
          ...leftOp,
          text: leftText + rightOp.text + rightText,
        },
      ];
    }

    case 'remove_text': {
      if (!Path.equals(leftOp.path, rightOp.path)) {
        return [leftOp];
      }
      if (leftOp.offset + leftOp.text.length <= rightOp.offset) {
        return [leftOp];
      }
      if (rightOp.offset + rightOp.text.length <= leftOp.offset) {
        return [
          {
            ...leftOp,
            offset: leftOp.offset - rightOp.text.length,
          },
        ];
      }

      // leftText and rightText both come from leftOp
      const leftTextEnd = Math.max(rightOp.offset - leftOp.offset, 0);
      const leftText = leftOp.text.slice(0, leftTextEnd);
      const rightTextStart = Math.min(
        leftOp.text.length,
        rightOp.offset + rightOp.text.length - leftOp.offset
      );
      const rightText = leftOp.text.slice(rightTextStart);
      return [
        {
          ...leftOp,
          offset: Math.min(leftOp.offset, rightOp.offset),
          text: leftText + rightText,
        },
      ];
    }

    case 'split_node': {
      if (Path.equals(leftOp.path, rightOp.path)) {
        // text to remove all within the former segment
        if (leftOp.offset + leftOp.text.length <= rightOp.position) {
          return [leftOp];
        }

        // text to remove all within the latter segment
        if (leftOp.offset >= rightOp.position) {
          return [
            {
              ...leftOp,
              path: Path.next(rightOp.path),
              offset: leftOp.offset - rightOp.position,
            },
          ];
        }

        // text to remove in both segments
        return [
          {
            ...leftOp,
            text: leftOp.text.slice(0, rightOp.position - leftOp.offset),
          },
          {
            ...leftOp,
            path: Path.next(rightOp.path),
            offset: 0,
            text: leftOp.text.slice(rightOp.position - leftOp.offset),
          },
        ];
      }

      return <RemoveTextOperation[]>pathTransform(leftOp, rightOp);
    }

    case 'merge_node': {
      if (Path.equals(leftOp.path, rightOp.path)) {
        return [
          {
            ...leftOp,
            path: Path.previous(rightOp.path),
            offset: leftOp.offset + rightOp.position,
          },
        ];
      }

      return <RemoveTextOperation[]>pathTransform(leftOp, rightOp);
    }

    // insert_node
    // remove_node
    // move_node
    // set_node
    default:
      return <RemoveTextOperation[]>pathTransform(leftOp, rightOp);
  }
}
Example #27
Source File: transRemoveNode.ts    From slate-ot with MIT License 4 votes vote down vote up
transRemoveNode = (
  leftOp: RemoveNodeOperation,
  rightOp: Operation,
  side: 'left' | 'right'
): (RemoveNodeOperation | SplitNodeOperation)[] => {
  switch (rightOp.type) {
    case 'split_node': {
      if (Path.equals(leftOp.path, rightOp.path)) {
        // should remove the target node && the split node
        //   however, after removing the target node,
        //   the split node becomes the same path.
        // TODO: node within op should be split.
        return [leftOp, leftOp];
      }

      return <RemoveNodeOperation[]>pathTransform(leftOp, rightOp);
    }

    case 'merge_node': {
      // One of the to-merge nodes are removed, we have to discard merging.
      // It might be better to keep the unremoved node,
      //   but it is tricky to keep properties by merge-and-split,
      //   therefore we choose to remove the entired merged node
      if (
        Path.equals(leftOp.path, Path.previous(rightOp.path)) ||
        Path.equals(leftOp.path, rightOp.path)
      ) {
        return [
          {
            ...leftOp,
            path: Path.previous(rightOp.path),
          },
        ];
      }

      return <RemoveNodeOperation[]>pathTransform(leftOp, rightOp);
    }

    case 'move_node': {
      if (Path.equals(rightOp.path, rightOp.newPath)) {
        return [leftOp];
      }

      let [rr, ri] = decomposeMove(rightOp);
      let [l, r] = xTransformMxN([leftOp], [rr, ri], side);

      // leftOp removes a node within the moved zone
      // ri must have survived and not been changed
      // we use ri's path to compute the true path to remove
      if (l.length === 0) {
        return [
          {
            ...leftOp,
            path: ri.path.concat(leftOp.path.slice(rr.path.length)),
          },
        ];
      }

      // now we have l.length === 1
      // in most cases we can return, but to be consist with move-remove
      // we need to handle the case that rightOp moved a branch out
      if (r.length === 1 && r[0].type === 'insert_node') {
        l = [
          ...l,
          {
            type: 'remove_node',
            path: r[0].path,
            node: { text: '' },
          },
        ];
      }

      return <RemoveNodeOperation[]>l;
    }

    // insert_text
    // remove_text
    // insert_node
    // remove_node
    // set_node
    default:
      return <RemoveNodeOperation[]>pathTransform(leftOp, rightOp);
  }
}
Example #28
Source File: transMoveNode.ts    From slate-ot with MIT License 4 votes vote down vote up
transMoveNode = (
  leftOp: MoveNodeOperation,
  rightOp: Operation,
  side: 'left' | 'right'
): (MoveNodeOperation | RemoveNodeOperation | SplitNodeOperation)[] => {
  if (Path.equals(leftOp.path, leftOp.newPath)) {
    return [];
  }

  let [lr, li] = decomposeMove(leftOp);

  switch (rightOp.type) {
    case 'insert_node': {
      let [l] = xTransformMxN([lr, li], [rightOp], side);

      return [
        composeMove(<RemoveNodeOperation>l[0], <InsertNodeOperation>l[1]),
      ];
    }

    case 'remove_node': {
      let [l] = xTransformMxN([lr, li], [rightOp], side);

      // normal case
      if (l.length === 2) {
        return [
          {
            ...leftOp,
            ...composeMove(
              <RemoveNodeOperation>l[0],
              <InsertNodeOperation>l[1]
            ),
          },
        ];
      }

      // leftOp moves a branch into the removed zone
      else if (l.length === 1 && l[0].type === 'remove_node') {
        return [l[0]];
      }

      // leftOp moves a branch out of the removed zone
      // we choose NOT to keep it
      else if (l.length === 1 && l[0].type === 'insert_node') {
        return [];
      }

      // l.length === 0, move within the removed zone
      else {
        return [];
      }
    }

    case 'split_node': {
      const after: boolean =
        Path.isSibling(leftOp.path, leftOp.newPath) &&
        Path.endsBefore(leftOp.path, leftOp.newPath);

      // the split nodes have to move separately
      if (Path.equals(leftOp.path, rightOp.path)) {
        const newPath = Path.transform(leftOp.newPath, rightOp)!;
        // the split nodes are moved AFTER newPath
        if (after) {
          return [
            {
              ...leftOp, // move first node
              newPath,
            },
            {
              ...leftOp, // move second node
              newPath,
            },
          ];
        }

        // the split nodes are moved BEFORE newPath
        else {
          const firstMove: MoveNodeOperation = {
            ...leftOp, // move second node
            path: Path.next(leftOp.path),
            newPath,
          };

          const secondMove: MoveNodeOperation = {
            ...leftOp, // move first node
            path: Path.transform(leftOp.path, firstMove)!,
            newPath: Path.previous(Path.transform(newPath, firstMove)!),
          };

          return [firstMove, secondMove];
        }
      }

      let newPath = Path.transform(leftOp.newPath, rightOp)!;

      // the newPath is between the split nodes
      // note that it is impossible for newPath == rightOp.path
      if (Path.equals(newPath, Path.next(rightOp.path)) && !after) {
        newPath = rightOp.path;
      }

      // a tricky case:
      //   when after is true, and the splitOp separated path and newPath
      //   to no-longer be siblings, the after becomes false
      //   in this case we should move one step after
      else if (after && !Path.isSibling(leftOp.path, newPath)) {
        newPath = Path.next(newPath);
      }

      // finally, the normal case
      return [
        {
          ...leftOp,
          path: Path.transform(leftOp.path, rightOp)!,
          newPath,
        },
      ];
    }

    case 'merge_node': {
      let path = rightOp.path;
      let prevPath = Path.previous(path);

      path = Path.transform(path, leftOp)!;
      prevPath = Path.transform(prevPath, leftOp)!;

      // ops conflict with each other, discard merge
      // Note that the merge-and-split node cannot keep properties,
      //   so we have to remove it.
      if (!Path.equals(path, Path.next(prevPath))) {
        return [
          {
            ...rightOp,
            type: 'split_node',
            path: Path.previous(rightOp.path),
          },
          leftOp,
          {
            type: 'remove_node',
            path,
            node: { text: '' },
          },
        ];
      }

      // a tricky case:
      //   if leftOp.path is a child of rightOp.prevPath,
      //   and leftOp.newPath is a child of rightOp.path.
      //   intentionally, leftOp wants to insert BEFORE leftOp.newPath;
      //   but after merging, leftOp's path and newPath become siblings.
      //   the move dst turns out to be AFTER the transformed newPath.
      //   therefore we should move one step ahead
      if (
        Path.isParent(Path.previous(rightOp.path), leftOp.path) &&
        Path.isParent(rightOp.path, leftOp.newPath)
      ) {
        return [
          {
            ...leftOp,
            path: Path.transform(leftOp.path, rightOp)!,
            newPath: Path.previous(Path.transform(leftOp.newPath, rightOp)!),
          },
        ];
      }

      return [
        {
          ...leftOp,
          path: Path.transform(leftOp.path, rightOp)!,
          newPath: Path.transform(leftOp.newPath, rightOp)!,
        },
      ];
    }

    case 'move_node': {
      // the other side didn't do anything
      if (Path.equals(rightOp.path, rightOp.newPath)) {
        return [leftOp];
      }

      let [rr, ri] = decomposeMove(rightOp);

      let [l, r] = xTransformMxN([lr, li], [rr, ri], side);

      // normal case
      if (l.length === 2) {
        return [
          composeMove(<RemoveNodeOperation>l[0], <InsertNodeOperation>l[1]),
        ];
      }

      // handling conflict
      if (r.length === 1) {
        // must have l.length === 1 && l[0].type === r[0].type)
        return side === 'left' ? [reverseMove(rr, ri), leftOp] : [];
      }

      // for the rest we have r.length === 2
      if (l.length === 0) {
        l[0] = {
          type: 'remove_node',
          path: ri.path.concat(lr.path.slice(rr.path.length)),
          node: { text: '' },
        };
        l[1] = {
          type: 'insert_node',
          path: ri.path.concat(li.path.slice(rr.path.length)),
          node: { text: '' },
        };
      }

      // for the rest we have l.length === 1
      else if (l[0].type === 'remove_node') {
        l[1] = {
          type: 'insert_node',
          path: (<InsertNodeOperation>r[1]).path.concat(
            li.path.slice((<RemoveNodeOperation>r[0]).path.length)
          ),
          node: { text: '' },
        };
      }

      // for the rest we have l[0].type === 'insert_node'
      else {
        l[1] = l[0];
        l[0] = {
          type: 'remove_node',
          path: ri.path.concat(lr.path.slice(rr.path.length)),
          node: { text: '' },
        };
      }

      return [
        composeMove(<RemoveNodeOperation>l[0], <InsertNodeOperation>l[1]),
      ];
    }

    // insert_text
    // remove_text
    // set_node
    default:
      return [leftOp];
  }
}
Example #29
Source File: transMergeNode.ts    From slate-ot with MIT License 4 votes vote down vote up
transMergeNode = (
  leftOp: MergeNodeOperation,
  rightOp: Operation,
  _side: 'left' | 'right'
): (MergeNodeOperation | MoveNodeOperation | RemoveNodeOperation)[] => {
  switch (rightOp.type) {
    case 'insert_text': {
      if (Path.equals(leftOp.path, Path.next(rightOp.path))) {
        return [
          {
            ...leftOp,
            position: leftOp.position + rightOp.text.length,
          },
        ];
      }

      return [leftOp];
    }

    case 'remove_text': {
      if (Path.equals(leftOp.path, Path.next(rightOp.path))) {
        return [
          {
            ...leftOp,
            position: leftOp.position - rightOp.text.length,
          },
        ];
      }

      return [leftOp];
    }

    case 'insert_node': {
      if (Path.equals(leftOp.path, rightOp.path)) {
        const offset = Text.isText(rightOp.node)
          ? rightOp.node.text.length
          : rightOp.node.children.length;

        return [
          leftOp, // merge the inserted node
          {
            ...leftOp, // merge the original node
            position: leftOp.position + offset,
          },
        ];
      }

      if (Path.isParent(Path.previous(leftOp.path), rightOp.path)) {
        return [
          {
            ...leftOp,
            position: leftOp.position + 1,
          },
        ];
      }

      return <MergeNodeOperation[]>pathTransform(leftOp, rightOp);
    }

    case 'remove_node': {
      if (Path.isParent(Path.previous(leftOp.path), rightOp.path)) {
        return [
          {
            ...leftOp,
            position: leftOp.position - 1,
          },
        ];
      }

      const path = Path.transform(leftOp.path, rightOp);
      const prevPath = Path.transform(Path.previous(leftOp.path), rightOp);

      if (path && prevPath) {
        return [
          {
            ...leftOp,
            path,
          },
        ];
      }

      // conflicting ops, we have to discard merge
      // for now we simply remove the merged node
      else if (!path && prevPath) {
        return [
          {
            ...rightOp,
            path: prevPath,
          },
        ];
      } else if (path && !prevPath) {
        return [
          {
            ...rightOp,
            path,
          },
        ];
      }

      // both to-merge nodes are removed
      else {
        return [];
      }
    }

    case 'split_node': {
      if (Path.equals(leftOp.path, rightOp.path)) {
        return [
          leftOp,
          {
            ...leftOp,
            position: leftOp.position + rightOp.position,
          },
        ];
      }

      if (Path.equals(Path.previous(leftOp.path), rightOp.path)) {
        return [
          {
            ...leftOp,
            path: Path.next(leftOp.path),
            position: leftOp.position - rightOp.position,
          },
        ];
      }

      if (Path.isParent(Path.previous(leftOp.path), rightOp.path)) {
        return [
          {
            ...leftOp,
            position: leftOp.position + 1,
          },
        ];
      }

      const path = Path.transform(leftOp.path, rightOp)!;

      // conflicting ops, we choose to discard split
      if (path[path.length - 1] === 0) {
        return [
          {
            ...rightOp,
            type: 'merge_node',
            path: Path.next(rightOp.path),
          },
          leftOp,
        ];
      }

      return <MergeNodeOperation[]>pathTransform(leftOp, rightOp);
    }

    case 'merge_node': {
      if (Path.equals(leftOp.path, rightOp.path)) {
        return [];
      }

      if (Path.equals(Path.previous(leftOp.path), rightOp.path)) {
        return [
          {
            ...leftOp,
            path: Path.previous(leftOp.path),
            position: leftOp.position + rightOp.position,
          },
        ];
      }

      if (Path.isParent(Path.previous(leftOp.path), rightOp.path)) {
        return [
          {
            ...leftOp,
            position: leftOp.position - 1,
          },
        ];
      }

      return <MergeNodeOperation[]>pathTransform(leftOp, rightOp);
    }

    case 'move_node': {
      if (Path.equals(rightOp.path, rightOp.newPath)) {
        return [leftOp];
      }

      const path = leftOp.path;
      const prevPath = Path.previous(path);

      const newPath = Path.transform(path, rightOp)!;
      const newPrevPath = Path.transform(prevPath, rightOp)!;

      // Ops conflict with each other, discard merge.
      //   Note that the merge-and-split node cannot keep properties,
      //   so we have to remove it.
      if (!Path.equals(newPath, Path.next(newPrevPath))) {
        return [
          {
            type: 'remove_node',
            path: newPath,
            node: { text: '' },
          },
        ];
      }

      let position = leftOp.position;

      // only src path is leftOp's child
      if (Path.isParent(prevPath, rightOp.path)) {
        position--;
      }

      // only dst path is leftOp's child
      if (Path.isParent(prevPath, rightOp.newPath)) {
        position++;
      }

      return [
        {
          ...leftOp,
          path: newPath,
          position,
        },
      ];
    }

    // set_node
    default:
      return <MergeNodeOperation[]>pathTransform(leftOp, rightOp);
  }
}