-
Notifications
You must be signed in to change notification settings - Fork 63
/
Copy pathtext0.js
260 lines (222 loc) · 7.7 KB
/
text0.js
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
// DEPRECATED!
//
// This type works, but is not exported. Its included here because the JSON0
// embedded string operations use this library.
// A simple text implementation
//
// Operations are lists of components. Each component either inserts or deletes
// at a specified position in the document.
//
// Components are either:
// {i:'str', p:100}: Insert 'str' at position 100 in the document
// {d:'str', p:100}: Delete 'str' at position 100 in the document
//
// Components in an operation are executed sequentially, so the position of components
// assumes previous components have already executed.
//
// Eg: This op:
// [{i:'abc', p:0}]
// is equivalent to this op:
// [{i:'a', p:0}, {i:'b', p:1}, {i:'c', p:2}]
var text = module.exports = {
name: 'text0',
uri: 'http://sharejs.org/types/textv0',
create: function(initial) {
if ((initial != null) && typeof initial !== 'string') {
throw new Error('Initial data must be a string');
}
return initial || '';
}
};
/** Insert s2 into s1 at pos. */
var strInject = function(s1, pos, s2) {
return s1.slice(0, pos) + s2 + s1.slice(pos);
};
/** Check that an operation component is valid. Throws if its invalid. */
var checkValidComponent = function(c) {
if (typeof c.p !== 'number')
throw new Error('component missing position field');
if ((typeof c.i === 'string') === (typeof c.d === 'string'))
throw new Error('component needs an i or d field');
if (c.p < 0)
throw new Error('position cannot be negative');
};
/** Check that an operation is valid */
var checkValidOp = function(op) {
for (var i = 0; i < op.length; i++) {
checkValidComponent(op[i]);
}
};
/** Apply op to snapshot */
text.apply = function(snapshot, op) {
var deleted;
var type = typeof snapshot;
if (type !== 'string')
throw new Error('text0 operations cannot be applied to type: ' + type);
checkValidOp(op);
for (var i = 0; i < op.length; i++) {
var component = op[i];
if (component.i != null) {
snapshot = strInject(snapshot, component.p, component.i);
} else {
deleted = snapshot.slice(component.p, component.p + component.d.length);
if (component.d !== deleted)
throw new Error("Delete component '" + component.d + "' does not match deleted text '" + deleted + "'");
snapshot = snapshot.slice(0, component.p) + snapshot.slice(component.p + component.d.length);
}
}
return snapshot;
};
/**
* Append a component to the end of newOp. Exported for use by the random op
* generator and the JSON0 type.
*/
var append = text._append = function(newOp, c) {
if (c.i === '' || c.d === '') return;
if (newOp.length === 0) {
newOp.push(c);
} else {
var last = newOp[newOp.length - 1];
if (last.i != null && c.i != null && last.p <= c.p && c.p <= last.p + last.i.length) {
// Compose the insert into the previous insert
newOp[newOp.length - 1] = {i:strInject(last.i, c.p - last.p, c.i), p:last.p};
} else if (last.d != null && c.d != null && c.p <= last.p && last.p <= c.p + c.d.length) {
// Compose the deletes together
newOp[newOp.length - 1] = {d:strInject(c.d, last.p - c.p, last.d), p:c.p};
} else {
newOp.push(c);
}
}
};
/** Compose op1 and op2 together */
text.compose = function(op1, op2) {
checkValidOp(op1);
checkValidOp(op2);
var newOp = op1.slice();
for (var i = 0; i < op2.length; i++) {
append(newOp, op2[i]);
}
return newOp;
};
/** Clean up an op */
text.normalize = function(op) {
var newOp = [];
// Normalize should allow ops which are a single (unwrapped) component:
// {i:'asdf', p:23}.
// There's no good way to test if something is an array:
// http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/
// so this is probably the least bad solution.
if (op.i != null || op.p != null) op = [op];
for (var i = 0; i < op.length; i++) {
var c = op[i];
if (c.p == null) c.p = 0;
append(newOp, c);
}
return newOp;
};
// This helper method transforms a position by an op component.
//
// If c is an insert, insertAfter specifies whether the transform
// is pushed after the insert (true) or before it (false).
//
// insertAfter is optional for deletes.
var transformPosition = function(pos, c, insertAfter) {
// This will get collapsed into a giant ternary by uglify.
if (c.i != null) {
if (c.p < pos || (c.p === pos && insertAfter)) {
return pos + c.i.length;
} else {
return pos;
}
} else {
// I think this could also be written as: Math.min(c.p, Math.min(c.p -
// otherC.p, otherC.d.length)) but I think its harder to read that way, and
// it compiles using ternary operators anyway so its no slower written like
// this.
if (pos <= c.p) {
return pos;
} else if (pos <= c.p + c.d.length) {
return c.p;
} else {
return pos - c.d.length;
}
}
};
// Helper method to transform a cursor position as a result of an op.
//
// Like transformPosition above, if c is an insert, insertAfter specifies
// whether the cursor position is pushed after an insert (true) or before it
// (false).
text.transformCursor = function(position, op, side) {
var insertAfter = side === 'right';
for (var i = 0; i < op.length; i++) {
position = transformPosition(position, op[i], insertAfter);
}
return position;
};
// Transform an op component by another op component. Asymmetric.
// The result will be appended to destination.
//
// exported for use in JSON type
var transformComponent = text._tc = function(dest, c, otherC, side) {
//var cIntersect, intersectEnd, intersectStart, newC, otherIntersect, s;
checkValidComponent(c);
checkValidComponent(otherC);
if (c.i != null) {
// Insert.
append(dest, {i:c.i, p:transformPosition(c.p, otherC, side === 'right')});
} else {
// Delete
if (otherC.i != null) {
// Delete vs insert
var s = c.d;
if (c.p < otherC.p) {
append(dest, {d:s.slice(0, otherC.p - c.p), p:c.p});
s = s.slice(otherC.p - c.p);
}
if (s !== '')
append(dest, {d: s, p: c.p + otherC.i.length});
} else {
// Delete vs delete
if (c.p >= otherC.p + otherC.d.length)
append(dest, {d: c.d, p: c.p - otherC.d.length});
else if (c.p + c.d.length <= otherC.p)
append(dest, c);
else {
// They overlap somewhere.
var newC = {d: '', p: c.p};
if (c.p < otherC.p)
newC.d = c.d.slice(0, otherC.p - c.p);
if (c.p + c.d.length > otherC.p + otherC.d.length)
newC.d += c.d.slice(otherC.p + otherC.d.length - c.p);
// This is entirely optional - I'm just checking the deleted text in
// the two ops matches
var intersectStart = Math.max(c.p, otherC.p);
var intersectEnd = Math.min(c.p + c.d.length, otherC.p + otherC.d.length);
var cIntersect = c.d.slice(intersectStart - c.p, intersectEnd - c.p);
var otherIntersect = otherC.d.slice(intersectStart - otherC.p, intersectEnd - otherC.p);
if (cIntersect !== otherIntersect)
throw new Error('Delete ops delete different text in the same region of the document');
if (newC.d !== '') {
newC.p = transformPosition(newC.p, otherC);
append(dest, newC);
}
}
}
}
return dest;
};
var invertComponent = function(c) {
return (c.i != null) ? {d:c.i, p:c.p} : {i:c.d, p:c.p};
};
// No need to use append for invert, because the components won't be able to
// cancel one another.
text.invert = function(op) {
// Shallow copy & reverse that sucka.
op = op.slice().reverse();
for (var i = 0; i < op.length; i++) {
op[i] = invertComponent(op[i]);
}
return op;
};
require('./bootstrapTransform')(text, transformComponent, checkValidOp, append);