-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathTextPatcher.js
178 lines (152 loc) · 5.64 KB
/
TextPatcher.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
(function () {
var TextPatcher = {};
var fragmentPattern = /([^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/;
var containsEmojiSegment = function (str) {
return fragmentPattern.test(str);
};
var surrogatePattern = /[\uD800-\uDBFF]|[\uDC00-\uDFFF]/;
var hasSurrogate = function (str) {
return surrogatePattern.test(str);
}
/* diff takes two strings, the old content, and the desired content
it returns the difference between these two strings in the form
of an 'Operation' (as defined in chainpad.js).
diff is purely functional.
*/
var diff = TextPatcher.diff = function (oldval, newval) {
// Strings are immutable and have reference equality. I think this test is O(1), so its worth doing.
if (oldval === newval) {
return;
}
var commonStart = 0;
while (oldval.charAt(commonStart) === newval.charAt(commonStart)) {
if (hasSurrogate(oldval.charAt(commonStart)) || hasSurrogate(newval.charAt(commonStart))) {
if (oldval.charAt(commonStart + 1) === newval.charAt(commonStart + 1)) {
commonStart++;
} else {
break;
}
}
commonStart++;
}
var commonEnd = 0;
while (oldval.charAt(oldval.length - 1 - commonEnd) === newval.charAt(newval.length - 1 - commonEnd) &&
commonEnd + commonStart < oldval.length && commonEnd + commonStart < newval.length) {
commonEnd++;
}
var toRemove = 0;
var toInsert = '';
/* throw some assertions in here before dropping patches into the realtime */
if (oldval.length !== commonStart + commonEnd) {
toRemove = oldval.length - commonStart - commonEnd;
}
if (newval.length !== commonStart + commonEnd) {
toInsert = newval.slice(commonStart, newval.length - commonEnd);
}
if (containsEmojiSegment(toInsert)) {
console.error("This would have blown up chainpad: [%s]", toInsert);
window.alert("TextPatcher does not correctly support Emojis. Coming Soon.");
window.location.reload();
return null;
}
return {
type: 'Operation',
offset: commonStart,
toInsert: toInsert,
toRemove: toRemove
};
};
/* patch accepts a realtime facade and an operation (which might be falsey)
it applies the operation to the realtime as components (remove/insert)
patch has no return value, and operates solely through side effects on
the realtime facade.
*/
var patch = TextPatcher.patch = function (ctx, op) {
if (!op) { return; }
if (ctx.patch) {
ctx.patch(op.offset, op.toRemove, op.toInsert);
} else {
console.log("chainpad.remove and chainpad.insert are deprecated. "+
"update your chainpad installation to the latest version.");
if (op.toRemove) { ctx.remove(op.offset, op.toRemove); }
if (op.toInsert) { ctx.insert(op.offset, op.toInsert); }
}
};
/* format has the same signature as log, but doesn't log to the console
use it to get the pretty version of a diff */
var format = TextPatcher.format = function (text, op) {
return op?{
insert: op.toInsert,
remove: text.slice(op.offset, op.offset + op.toRemove)
}: { insert: '', remove: '' };
};
/* log accepts a string and an operation, and prints an object to the console
the object will display the content which is to be removed, and the content
which will be inserted in its place.
log is useful for debugging, but can otherwise be disabled.
*/
var log = TextPatcher.log = function (text, op) {
if (!op) { return; }
console.log(format(text, op));
};
/* applyChange takes:
ctx: the context (aka the realtime)
oldval: the old value
newval: the new value
it performs a diff on the two values, and generates patches
which are then passed into `ctx.remove` and `ctx.insert`.
Due to its reliance on patch, applyChange has side effects on the supplied
realtime facade.
*/
var applyChange = TextPatcher.applyChange = function(ctx, oldval, newval, logging) {
var op = diff(oldval, newval);
if (logging) { log(oldval, op); }
patch(ctx, op);
};
var transformCursor = TextPatcher.transformCursor = function (cursor, op) {
if (!op) { return cursor; }
var pos = op.offset;
var remove = op.toRemove;
var insert = op.toInsert.length;
if (typeof cursor === 'undefined') { return; }
if (typeof remove === 'number' && pos < cursor) {
cursor -= Math.min(remove, cursor - pos);
}
if (typeof insert === 'number' && pos < cursor) {
cursor += insert;
}
return cursor;
};
var create = TextPatcher.create = function(config) {
var ctx = config.realtime;
var logging = config.logging;
// initial state will always fail the !== check in genop.
// because nothing will equal this object
var content = {};
// *** remote -> local changes
ctx.onPatch(function(pos, length) {
content = ctx.getUserDoc();
});
// propogate()
return function (newContent, force) {
if (newContent !== content || force) {
applyChange(ctx, ctx.getUserDoc(), newContent, logging);
if (ctx.getUserDoc() !== newContent) {
console.log("Expected that: `ctx.getUserDoc() === newContent`!");
}
else { content = ctx.getUserDoc(); }
return true;
}
return false;
};
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = TextPatcher;
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define(function () {
return TextPatcher;
});
} else {
window.TextPatcher = TextPatcher;
}
}());