Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/editors/slate/slate-diff/split-text-nodes.ts
1698 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Operation, Text } from "slate";
7
import { diff_main } from "@cocalc/sync/editor/generic/util";
8
import { len } from "@cocalc/util/misc";
9
10
export function nextPath(path: number[]): number[] {
11
return [...path.slice(0, path.length - 1), path[path.length - 1] + 1];
12
}
13
14
interface Op {
15
type: "insert_text" | "remove_text";
16
offset: number;
17
text: string;
18
}
19
20
export function slateTextDiff(a: string, b: string): Op[] {
21
const diff = diff_main(a, b);
22
23
const operations: Op[] = [];
24
25
let offset = 0;
26
let i = 0;
27
while (i < diff.length) {
28
const chunk = diff[i];
29
const op = chunk[0]; // -1 = delete, 0 = leave unchanged, 1 = insert
30
const text = chunk[1];
31
if (op === 0) {
32
// skip over context, since this diff applies cleanly
33
offset += text.length;
34
} else if (op === -1) {
35
// remove some text.
36
operations.push({ type: "remove_text", offset, text });
37
} else if (op == 1) {
38
// insert some text
39
operations.push({ type: "insert_text", offset, text });
40
offset += text.length;
41
}
42
i += 1;
43
}
44
//console.log("slateTextDiff", { a, b, diff, operations });
45
46
return operations;
47
}
48
49
/* Accomplish something like this
50
51
node={"text":"xyz A **B** C"} ->
52
split={"text":"A "} {"text":"B","bold":true} {"text":" C"}
53
54
via a combination of remove_text/insert_text as above and split_node
55
operations.
56
*/
57
58
export function splitTextNodes(
59
node: Text,
60
split: Text[],
61
path: number[] // the path to node.
62
): Operation[] {
63
if (split.length == 0) {
64
// easy special case
65
return [
66
{
67
type: "remove_node",
68
node,
69
path,
70
},
71
];
72
}
73
// First operation: transform the text node to the concatenation of result.
74
let splitText = "";
75
for (const { text } of split) {
76
splitText += text;
77
}
78
const nodeText = node.text;
79
const operations: Operation[] = [];
80
if (splitText != nodeText) {
81
// Use diff-match-pach to transform the text in the source node to equal
82
// the text in the sequence of target nodes. Once we do this transform,
83
// we can then worry about splitting up the resulting source node.
84
for (const op of slateTextDiff(nodeText, splitText)) {
85
// TODO: maybe path has to be changed if there are multiple OPS?
86
operations.push({ ...{ path }, ...op });
87
}
88
}
89
90
// Set properties on initial text to be those for split[0], if necessary.
91
const newProperties = getProperties(split[0], node);
92
if (len(newProperties) > 0) {
93
operations.push({
94
type: "set_node",
95
path,
96
properties: getProperties(node),
97
newProperties,
98
});
99
}
100
let properties = getProperties(split[0]);
101
// Rest of the operations to split up node as required.
102
let splitPath = path;
103
for (let i = 0; i < split.length - 1; i++) {
104
const part = split[i];
105
const nextPart = split[i + 1];
106
const newProperties = getProperties(nextPart, properties);
107
108
operations.push({
109
type: "split_node",
110
path: splitPath,
111
position: part.text.length,
112
properties: newProperties,
113
});
114
115
splitPath = nextPath(splitPath);
116
properties = getProperties(nextPart);
117
}
118
return operations;
119
}
120
121
/*
122
NOTE: the set_node api lets you delete properties by setting
123
them to null, but the split_node api doesn't (I guess Ian forgot to
124
implement that... or there is a good reason). So if there are any
125
property deletes, then we have to also do a set_node... or just be
126
ok with undefined values. For text where values are treated as
127
booleans, this is fine and that's what we do. Maybe the reason
128
is just to keep the operations simple and minimal.
129
Also setting to undefined / false-ish for a *text* node property
130
is equivalent to not having it regarding everything else.
131
*/
132
133
134
// Get object that will set the properties of before
135
// to equal the properties of node, in terms of the
136
// slatejs set_node operation. If before is not given,
137
// just gives all the non-text propers of goal.
138
function getProperties(goal: Text, before?: Text): any {
139
const props: any = {};
140
for (const x in goal) {
141
if (x != "text") {
142
if (before == null) {
143
if (goal[x]) {
144
props[x] = goal[x];
145
}
146
continue;
147
} else {
148
if (goal[x] !== before[x]) {
149
if (goal[x]) {
150
props[x] = goal[x];
151
} else {
152
props[x] = undefined; // remove property...
153
}
154
}
155
}
156
}
157
}
158
if (before != null) {
159
// also be sure to explicitly remove props not in goal
160
// WARNING: this might change in slatejs; I saw a discussion about this.
161
for (const x in before) {
162
if (x != "text" && goal[x] == null) {
163
props[x] = undefined;
164
}
165
}
166
}
167
return props;
168
}
169
170