Path: blob/master/src/packages/frontend/editors/slate/slate-diff/split-text-nodes.ts
1698 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Operation, Text } from "slate";6import { diff_main } from "@cocalc/sync/editor/generic/util";7import { len } from "@cocalc/util/misc";89export function nextPath(path: number[]): number[] {10return [...path.slice(0, path.length - 1), path[path.length - 1] + 1];11}1213interface Op {14type: "insert_text" | "remove_text";15offset: number;16text: string;17}1819export function slateTextDiff(a: string, b: string): Op[] {20const diff = diff_main(a, b);2122const operations: Op[] = [];2324let offset = 0;25let i = 0;26while (i < diff.length) {27const chunk = diff[i];28const op = chunk[0]; // -1 = delete, 0 = leave unchanged, 1 = insert29const text = chunk[1];30if (op === 0) {31// skip over context, since this diff applies cleanly32offset += text.length;33} else if (op === -1) {34// remove some text.35operations.push({ type: "remove_text", offset, text });36} else if (op == 1) {37// insert some text38operations.push({ type: "insert_text", offset, text });39offset += text.length;40}41i += 1;42}43//console.log("slateTextDiff", { a, b, diff, operations });4445return operations;46}4748/* Accomplish something like this4950node={"text":"xyz A **B** C"} ->51split={"text":"A "} {"text":"B","bold":true} {"text":" C"}5253via a combination of remove_text/insert_text as above and split_node54operations.55*/5657export function splitTextNodes(58node: Text,59split: Text[],60path: number[] // the path to node.61): Operation[] {62if (split.length == 0) {63// easy special case64return [65{66type: "remove_node",67node,68path,69},70];71}72// First operation: transform the text node to the concatenation of result.73let splitText = "";74for (const { text } of split) {75splitText += text;76}77const nodeText = node.text;78const operations: Operation[] = [];79if (splitText != nodeText) {80// Use diff-match-pach to transform the text in the source node to equal81// the text in the sequence of target nodes. Once we do this transform,82// we can then worry about splitting up the resulting source node.83for (const op of slateTextDiff(nodeText, splitText)) {84// TODO: maybe path has to be changed if there are multiple OPS?85operations.push({ ...{ path }, ...op });86}87}8889// Set properties on initial text to be those for split[0], if necessary.90const newProperties = getProperties(split[0], node);91if (len(newProperties) > 0) {92operations.push({93type: "set_node",94path,95properties: getProperties(node),96newProperties,97});98}99let properties = getProperties(split[0]);100// Rest of the operations to split up node as required.101let splitPath = path;102for (let i = 0; i < split.length - 1; i++) {103const part = split[i];104const nextPart = split[i + 1];105const newProperties = getProperties(nextPart, properties);106107operations.push({108type: "split_node",109path: splitPath,110position: part.text.length,111properties: newProperties,112});113114splitPath = nextPath(splitPath);115properties = getProperties(nextPart);116}117return operations;118}119120/*121NOTE: the set_node api lets you delete properties by setting122them to null, but the split_node api doesn't (I guess Ian forgot to123implement that... or there is a good reason). So if there are any124property deletes, then we have to also do a set_node... or just be125ok with undefined values. For text where values are treated as126booleans, this is fine and that's what we do. Maybe the reason127is just to keep the operations simple and minimal.128Also setting to undefined / false-ish for a *text* node property129is equivalent to not having it regarding everything else.130*/131132133// Get object that will set the properties of before134// to equal the properties of node, in terms of the135// slatejs set_node operation. If before is not given,136// just gives all the non-text propers of goal.137function getProperties(goal: Text, before?: Text): any {138const props: any = {};139for (const x in goal) {140if (x != "text") {141if (before == null) {142if (goal[x]) {143props[x] = goal[x];144}145continue;146} else {147if (goal[x] !== before[x]) {148if (goal[x]) {149props[x] = goal[x];150} else {151props[x] = undefined; // remove property...152}153}154}155}156}157if (before != null) {158// also be sure to explicitly remove props not in goal159// WARNING: this might change in slatejs; I saw a discussion about this.160for (const x in before) {161if (x != "text" && goal[x] == null) {162props[x] = undefined;163}164}165}166return props;167}168169170