braid-text 0.5.8 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/editor.html +7 -26
- package/client/markdown-editor.html +15 -47
- package/client/myers-diff.js +691 -0
- package/client/simpleton.js +168 -0
- package/client/syncarea.js +221 -0
- package/client/text-client.js +269 -0
- package/package.json +5 -3
- package/server-demo.js +11 -3
- package/client/simpleton-sync.js +0 -434
- package/client/web-utils.js +0 -76
package/server-demo.js
CHANGED
|
@@ -20,9 +20,17 @@ var server = require("http").createServer(async (req, res) => {
|
|
|
20
20
|
return
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
var libs = new Set([
|
|
24
|
+
'simpleton.js',
|
|
25
|
+
'text-client.js',
|
|
26
|
+
'yjs-sync.js',
|
|
27
|
+
'cursor-sync.js',
|
|
28
|
+
'textarea-highlights.js',
|
|
29
|
+
'myers-diff.js',
|
|
30
|
+
'syncarea.js'
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
if (libs.has(req.url.substr(1))) {
|
|
26
34
|
res.writeHead(200, { "Content-Type": "text/javascript", "Cache-Control": "no-cache" })
|
|
27
35
|
require("fs").createReadStream("./client" + req.url).pipe(res)
|
|
28
36
|
return
|
package/client/simpleton-sync.js
DELETED
|
@@ -1,434 +0,0 @@
|
|
|
1
|
-
// Simpleton Javascript Client
|
|
2
|
-
//
|
|
3
|
-
// requires braid-http@~1.3/braid-http-client.js
|
|
4
|
-
|
|
5
|
-
// --- API ---
|
|
6
|
-
//
|
|
7
|
-
// on_patches?: (patches) => void
|
|
8
|
-
// processes incoming patches by applying them to the UI/textarea.
|
|
9
|
-
// Patches are guaranteed to be in-order and non-overlapping.
|
|
10
|
-
//
|
|
11
|
-
// IMPORTANT: Patches have ABSOLUTE positions — each patch's range
|
|
12
|
-
// refers to positions in the original state (before any patches in
|
|
13
|
-
// this update). When applying multiple patches sequentially, you MUST
|
|
14
|
-
// track a cumulative offset to adjust positions:
|
|
15
|
-
//
|
|
16
|
-
// var offset = 0
|
|
17
|
-
// for (var p of patches) {
|
|
18
|
-
// apply_at(p.range[0] + offset, p.range[1] + offset, p.content)
|
|
19
|
-
// offset += p.content.length - (p.range[1] - p.range[0])
|
|
20
|
-
// }
|
|
21
|
-
//
|
|
22
|
-
// Without offset tracking, multi-patch updates will corrupt the state.
|
|
23
|
-
//
|
|
24
|
-
// Optional. If not provided, simpleton applies patches internally
|
|
25
|
-
// to its own copy of the state, and you can get the new state via
|
|
26
|
-
// on_state (see "Local Edit Absorption" below).
|
|
27
|
-
//
|
|
28
|
-
// When provided, simpleton calls this to apply patches externally,
|
|
29
|
-
// then reads back the state via get_state(). This means any un-flushed
|
|
30
|
-
// local edits in the UI are absorbed into client_state after each server
|
|
31
|
-
// update (see "Local Edit Absorption" below).
|
|
32
|
-
//
|
|
33
|
-
// on_state?: (state) => void
|
|
34
|
-
// called after each server update with the new state
|
|
35
|
-
//
|
|
36
|
-
// get_patches?: (client_state) => patches
|
|
37
|
-
// returns patches representing diff between client_state and current state,
|
|
38
|
-
// which are guaranteed to be different if this method is being called.
|
|
39
|
-
// (the default does this in a fast/simple way, finding a common prefix
|
|
40
|
-
// and suffix, but you can supply something better, or possibly keep
|
|
41
|
-
// track of patches as they come from your editor)
|
|
42
|
-
//
|
|
43
|
-
// get_state: () => current_state
|
|
44
|
-
// returns the current state (e.g., textarea.value)
|
|
45
|
-
//
|
|
46
|
-
// content_type: used for Accept and Content-Type headers
|
|
47
|
-
//
|
|
48
|
-
// on_error?: (error) => void
|
|
49
|
-
// called when an error occurs (e.g., network failure, digest mismatch)
|
|
50
|
-
//
|
|
51
|
-
// on_online?: (is_online) => void
|
|
52
|
-
// called when the connection status changes
|
|
53
|
-
//
|
|
54
|
-
// on_ack?: () => void
|
|
55
|
-
// called when all outstanding PUTs have been acknowledged
|
|
56
|
-
//
|
|
57
|
-
// send_digests?: boolean
|
|
58
|
-
// if truthy, includes a Repr-Digest header with each PUT
|
|
59
|
-
//
|
|
60
|
-
// returns { changed, abort }
|
|
61
|
-
// call changed() whenever there is a local change,
|
|
62
|
-
// and the system will call a combination of get_state and
|
|
63
|
-
// get_patches when it needs to. (get_state is required;
|
|
64
|
-
// get_patches is optional.)
|
|
65
|
-
// call abort() to abort the subscription.
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// --- Local Edit Absorption ---
|
|
69
|
-
//
|
|
70
|
-
// When on_patches is provided, after applying server patches via
|
|
71
|
-
// on_patches(), client_state is set to get_state(). If the UI has
|
|
72
|
-
// un-flushed local edits (typed but changed() not yet called), those
|
|
73
|
-
// edits are silently absorbed into client_state and will never be sent
|
|
74
|
-
// as a diff. In practice, the JS avoids this because changed() is
|
|
75
|
-
// called on every keystroke, and additionally, each time a PUT
|
|
76
|
-
// completes, the code re-diffs and sends any edits that accumulated
|
|
77
|
-
// while the PUT was in flight — so local edits are never stuck
|
|
78
|
-
// waiting; they flush as soon as a PUT slot opens up.
|
|
79
|
-
//
|
|
80
|
-
// When on_patches is NOT provided, client_state is updated by applying
|
|
81
|
-
// patches to the old client_state only. In this case, you should
|
|
82
|
-
// provide on_state to receive the updated state after each server
|
|
83
|
-
// update; otherwise your UI will not reflect remote changes.
|
|
84
|
-
//
|
|
85
|
-
function simpleton_client(url, {
|
|
86
|
-
get_patches,
|
|
87
|
-
get_state,
|
|
88
|
-
|
|
89
|
-
on_patches,
|
|
90
|
-
on_state,
|
|
91
|
-
on_error,
|
|
92
|
-
on_online,
|
|
93
|
-
on_ack,
|
|
94
|
-
|
|
95
|
-
headers, // The user can pass in custom headers
|
|
96
|
-
// that are forwarded into fetches
|
|
97
|
-
content_type,
|
|
98
|
-
send_digests
|
|
99
|
-
}) {
|
|
100
|
-
var peer = Math.random().toString(36).slice(2)
|
|
101
|
-
var client_version = [] // sorted version strings
|
|
102
|
-
var client_state = "" // text as of client_version
|
|
103
|
-
var char_counter = -1 // char-delta for version IDs
|
|
104
|
-
var dirty = false // true when local edits exist but haven't been sent
|
|
105
|
-
var is_online = false
|
|
106
|
-
var outstanding_puts = 0
|
|
107
|
-
|
|
108
|
-
// extend the headers with merge-type and peer
|
|
109
|
-
headers = {
|
|
110
|
-
...headers,
|
|
111
|
-
"Merge-Type": "simpleton",
|
|
112
|
-
Peer: peer,
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Manages both the GET subscription and PUT requests through a single
|
|
116
|
-
// channel with automatic reconnection and PUT queuing.
|
|
117
|
-
var channel = reliable_update_channel(url, {
|
|
118
|
-
reconnect_from_parents: () => client_version.length ? client_version : null,
|
|
119
|
-
get_headers: { ...headers, ...content_type && {Accept: content_type} },
|
|
120
|
-
put_headers: { ...headers, ...content_type && {"Content-Type": content_type} },
|
|
121
|
-
on_update: async update => {
|
|
122
|
-
// ── Parent check ────────────────────────────────────────
|
|
123
|
-
// Core simpleton invariant: only accept updates whose
|
|
124
|
-
// parents match our current version. If we're dirty
|
|
125
|
-
// (have unsent local edits), skip — we'll reconnect
|
|
126
|
-
// once the edits are flushed.
|
|
127
|
-
update.parents.sort()
|
|
128
|
-
if (!dirty && versions_eq(client_version, update.parents))
|
|
129
|
-
await apply_update(update)
|
|
130
|
-
},
|
|
131
|
-
on_status: status => {
|
|
132
|
-
is_online = status.online
|
|
133
|
-
outstanding_puts = status.outstanding_puts
|
|
134
|
-
if (on_online) on_online(status.online)
|
|
135
|
-
if (on_ack && outstanding_puts === 0) on_ack()
|
|
136
|
-
if (dirty && is_online && outstanding_puts < 10)
|
|
137
|
-
try_send()
|
|
138
|
-
},
|
|
139
|
-
on_error: err => on_error && on_error(err),
|
|
140
|
-
|
|
141
|
-
// this api is preliminary and undocumented;
|
|
142
|
-
// we use it to tell the reliable_update_channel to die,
|
|
143
|
-
// if there is a digest mismatch on the server,
|
|
144
|
-
// which will result in a 550 status code
|
|
145
|
-
no_retry_status_codes: [550]
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
async function apply_update(update) {
|
|
149
|
-
// ── Parse and convert patches ───────────────────────────────
|
|
150
|
-
// braid_fetch provides body and patch content as bytes;
|
|
151
|
-
// body_text and content_text are dynamic properties that
|
|
152
|
-
// decode bytes to a string via a UTF-8 decoder.
|
|
153
|
-
var patches
|
|
154
|
-
if (update.patches) {
|
|
155
|
-
for (let patch of update.patches) {
|
|
156
|
-
patch.range = patch.range.match(/\d+/g).map((x) => 1 * x)
|
|
157
|
-
patch.content = patch.content_text
|
|
158
|
-
}
|
|
159
|
-
patches = update.patches.sort((a, b) => a.range[0] - b.range[0])
|
|
160
|
-
|
|
161
|
-
// ── JS-SPECIFIC: Convert code-point ranges to UTF-16 indices ──
|
|
162
|
-
// The wire protocol uses Unicode code-point offsets.
|
|
163
|
-
// JS strings are UTF-16, so we must convert.
|
|
164
|
-
//
|
|
165
|
-
// OTHER LANGUAGES: Skip this conversion if your strings
|
|
166
|
-
// are natively indexed by code points (e.g., Emacs Lisp,
|
|
167
|
-
// Python, Rust's char iterator).
|
|
168
|
-
convert_ranges_codepoints_to_utf16(patches, client_state)
|
|
169
|
-
} else
|
|
170
|
-
// Initial snapshot: convert body to a patch replacing
|
|
171
|
-
// [0,0] so it follows the same code path as incremental
|
|
172
|
-
// patches.
|
|
173
|
-
patches = [{range: [0, 0], content: update.body_text}]
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// ── Apply the update ────────────────────────────────────────
|
|
177
|
-
if (on_patches) {
|
|
178
|
-
// Apply patches to the UI, then read back the
|
|
179
|
-
// full state. Warning: if changed() hasn't been
|
|
180
|
-
// called for recent local edits, get_state() will
|
|
181
|
-
// absorb them into client_state silently — call
|
|
182
|
-
// changed() after every local edit to avoid this.
|
|
183
|
-
on_patches(patches)
|
|
184
|
-
client_state = get_state()
|
|
185
|
-
} else
|
|
186
|
-
// Apply patches to our internal state; the
|
|
187
|
-
// result is delivered via on_state below.
|
|
188
|
-
client_state = apply_patches(client_state, patches)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// ── Advance version ─────────────────────────────────────────
|
|
192
|
-
// IMPORTANT: This must happen synchronously (before any await)
|
|
193
|
-
// to prevent the changed() accumulation loop from interleaving
|
|
194
|
-
// and capturing a stale client_version during a yield point.
|
|
195
|
-
client_version = update.version
|
|
196
|
-
|
|
197
|
-
// ── Notify listener ─────────────────────────────────────────
|
|
198
|
-
// IMPORTANT: No changed() / flush is called here.
|
|
199
|
-
// The JS does NOT send edits after receiving a server
|
|
200
|
-
// update. The PUT response handler's async accumulation
|
|
201
|
-
// loop handles flushing accumulated edits.
|
|
202
|
-
if (on_state) on_state(client_state)
|
|
203
|
-
|
|
204
|
-
// Now verify that we did this correct, and are in sync. We do this
|
|
205
|
-
// at the end, so the prior updating is atomic.
|
|
206
|
-
await check_digest(update, client_state)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ── try_send — attempt to flush local edits ───────────────────────
|
|
210
|
-
// Called from changed() and on_status. Diffs client_state vs current
|
|
211
|
-
// state and sends a PUT if there's a change. If dirty but no diff,
|
|
212
|
-
// we may have missed updates while dirty, so reconnect to re-sync.
|
|
213
|
-
function try_send() {
|
|
214
|
-
var new_state = get_state()
|
|
215
|
-
if (new_state === client_state) {
|
|
216
|
-
// No local diff — but we were dirty, meaning we may have
|
|
217
|
-
// skipped incoming updates. Reconnect to catch up.
|
|
218
|
-
dirty = false
|
|
219
|
-
channel.reconnect()
|
|
220
|
-
return
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
var patches = get_patches ? get_patches(client_state) :
|
|
224
|
-
[simple_diff(client_state, new_state)]
|
|
225
|
-
|
|
226
|
-
// Save JS-index patches before code-point conversion mutates them
|
|
227
|
-
var js_patches = patches.map(p => ({range: [...p.range], content: p.content}))
|
|
228
|
-
|
|
229
|
-
// ── JS-SPECIFIC: Convert JS UTF-16 ranges to code-points ────
|
|
230
|
-
// The wire protocol uses code-point offsets. See the
|
|
231
|
-
// inverse conversion in the receive path above.
|
|
232
|
-
//
|
|
233
|
-
// OTHER LANGUAGES: Skip this if your strings are
|
|
234
|
-
// natively code-point indexed.
|
|
235
|
-
convert_ranges_utf16_to_codepoints(patches, client_state)
|
|
236
|
-
|
|
237
|
-
for (let patch of patches) {
|
|
238
|
-
// ── Update char_counter ─────────────────────────────────
|
|
239
|
-
// Increment by deleted chars + inserted chars
|
|
240
|
-
char_counter += patch.range[1] - patch.range[0]
|
|
241
|
-
char_counter += count_code_points(patch.content)
|
|
242
|
-
|
|
243
|
-
patch.unit = "text"
|
|
244
|
-
patch.range = `[${patch.range[0]}:${patch.range[1]}]`
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ── Compute version and advance optimistically ──────────────
|
|
248
|
-
var version = [peer + "-" + char_counter]
|
|
249
|
-
|
|
250
|
-
var parents = client_version
|
|
251
|
-
client_version = version // optimistic advance
|
|
252
|
-
client_state = new_state // update client_state
|
|
253
|
-
dirty = false
|
|
254
|
-
|
|
255
|
-
// Send Update — when the PUT completes, on_status fires
|
|
256
|
-
// with updated outstanding_puts, which will call try_send
|
|
257
|
-
// again if dirty.
|
|
258
|
-
if (send_digests)
|
|
259
|
-
get_digest(client_state).then(digest =>
|
|
260
|
-
channel.put({ version, parents, patches,
|
|
261
|
-
headers: { "Repr-Digest": digest } }))
|
|
262
|
-
else
|
|
263
|
-
channel.put({ version, parents, patches })
|
|
264
|
-
|
|
265
|
-
return js_patches
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// ── Public interface ────────────────────────────────────────────────
|
|
269
|
-
return {
|
|
270
|
-
// ── abort() — cancel the subscription ─────────────────────────
|
|
271
|
-
abort: () => channel.close(),
|
|
272
|
-
|
|
273
|
-
// ── changed() — call when local edits occur ───────────────────
|
|
274
|
-
// If online and under the PUT limit, sends immediately.
|
|
275
|
-
// Otherwise marks dirty — on_status will flush later.
|
|
276
|
-
changed: () => {
|
|
277
|
-
if (is_online && outstanding_puts < 10)
|
|
278
|
-
return try_send()
|
|
279
|
-
else
|
|
280
|
-
dirty = true
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function versions_eq(a, b) {
|
|
285
|
-
return a.length === b.length && a.every((v, i) => v === b[i])
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// ── JS-SPECIFIC: UTF-16 helpers ─────────────────────────────────────
|
|
289
|
-
// These handle surrogate pairs in UTF-16 JS strings. Characters
|
|
290
|
-
// outside the Basic Multilingual Plane (BMP) are encoded as two
|
|
291
|
-
// 16-bit code units (a surrogate pair: high 0xD800-0xDBFF, low
|
|
292
|
-
// 0xDC00-0xDFFF). Such a pair represents one Unicode code point.
|
|
293
|
-
//
|
|
294
|
-
// OTHER LANGUAGES: You don't need these if your string type is
|
|
295
|
-
// natively indexed by code points.
|
|
296
|
-
|
|
297
|
-
function get_char_size(str, utf16_index) {
|
|
298
|
-
const char_code = str.charCodeAt(utf16_index)
|
|
299
|
-
return (char_code >= 0xd800 && char_code <= 0xdbff) ? 2 : 1
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function count_code_points(str) {
|
|
303
|
-
let code_points = 0
|
|
304
|
-
for (let i = 0; i < str.length; i++) {
|
|
305
|
-
if (str.charCodeAt(i) >= 0xd800 && str.charCodeAt(i) <= 0xdbff) i++
|
|
306
|
-
code_points++
|
|
307
|
-
}
|
|
308
|
-
return code_points
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Converts patch ranges from code-point offsets to UTF-16 indices.
|
|
312
|
-
// Patches must be sorted by range[0].
|
|
313
|
-
function convert_ranges_codepoints_to_utf16(patches, str) {
|
|
314
|
-
let codepoint_index = 0
|
|
315
|
-
let utf16_index = 0
|
|
316
|
-
for (let patch of patches) {
|
|
317
|
-
while (codepoint_index < patch.range[0]) {
|
|
318
|
-
utf16_index += get_char_size(str, utf16_index)
|
|
319
|
-
codepoint_index++
|
|
320
|
-
}
|
|
321
|
-
patch.range[0] = utf16_index
|
|
322
|
-
|
|
323
|
-
while (codepoint_index < patch.range[1]) {
|
|
324
|
-
utf16_index += get_char_size(str, utf16_index)
|
|
325
|
-
codepoint_index++
|
|
326
|
-
}
|
|
327
|
-
patch.range[1] = utf16_index
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Converts patch ranges from UTF-16 indices to code-point offsets.
|
|
332
|
-
// Patches must be sorted by range[0].
|
|
333
|
-
function convert_ranges_utf16_to_codepoints(patches, str) {
|
|
334
|
-
let codepoint_index = 0
|
|
335
|
-
let utf16_index = 0
|
|
336
|
-
for (let patch of patches) {
|
|
337
|
-
while (utf16_index < patch.range[0]) {
|
|
338
|
-
utf16_index += get_char_size(str, utf16_index)
|
|
339
|
-
codepoint_index++
|
|
340
|
-
}
|
|
341
|
-
patch.range[0] = codepoint_index
|
|
342
|
-
|
|
343
|
-
while (utf16_index < patch.range[1]) {
|
|
344
|
-
utf16_index += get_char_size(str, utf16_index)
|
|
345
|
-
codepoint_index++
|
|
346
|
-
}
|
|
347
|
-
patch.range[1] = codepoint_index
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// ── simple_diff ─────────────────────────────────────────────────────
|
|
352
|
-
// Finds the longest common prefix and suffix between two strings,
|
|
353
|
-
// returning the minimal edit that transforms `old_str` into `new_str`.
|
|
354
|
-
//
|
|
355
|
-
// Returns: { range: [prefix_len, old_str.length - suffix_len],
|
|
356
|
-
// content: new_str.slice(prefix_len, new_str.length - suffix_len) }
|
|
357
|
-
//
|
|
358
|
-
// This produces a single contiguous edit. For multi-cursor or
|
|
359
|
-
// multi-region edits, supply a custom get_patches function instead.
|
|
360
|
-
function simple_diff(old_str, new_str) {
|
|
361
|
-
// Find common prefix length
|
|
362
|
-
var prefix_len = 0
|
|
363
|
-
var min_len = Math.min(old_str.length, new_str.length)
|
|
364
|
-
while (prefix_len < min_len && old_str[prefix_len] === new_str[prefix_len]) prefix_len++
|
|
365
|
-
|
|
366
|
-
// Don't split a surrogate pair: if prefix ends on a low surrogate,
|
|
367
|
-
// the preceding high surrogate only matched by coincidence (same
|
|
368
|
-
// Unicode block), so back up to include the whole pair in the diff.
|
|
369
|
-
if (prefix_len > 0 && is_low_surrogate(old_str, prefix_len))
|
|
370
|
-
prefix_len--
|
|
371
|
-
|
|
372
|
-
// Find common suffix length (from what remains after prefix)
|
|
373
|
-
var suffix_len = 0
|
|
374
|
-
min_len -= prefix_len
|
|
375
|
-
while (suffix_len < min_len && old_str[old_str.length - suffix_len - 1] === new_str[new_str.length - suffix_len - 1]) suffix_len++
|
|
376
|
-
|
|
377
|
-
// Same guard for suffixes: if the range end (old_str.length - suffix_len)
|
|
378
|
-
// lands on a low surrogate, the suffix consumed it without its high
|
|
379
|
-
// surrogate, so back up.
|
|
380
|
-
if (suffix_len > 0 && is_low_surrogate(old_str, old_str.length - suffix_len))
|
|
381
|
-
suffix_len--
|
|
382
|
-
|
|
383
|
-
return {range: [prefix_len, old_str.length - suffix_len], content: new_str.slice(prefix_len, new_str.length - suffix_len)}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function is_low_surrogate(str, i) {
|
|
387
|
-
var c = str.charCodeAt(i)
|
|
388
|
-
return c >= 0xdc00 && c <= 0xdfff
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function is_high_surrogate(str, i) {
|
|
392
|
-
var c = str.charCodeAt(i)
|
|
393
|
-
return c >= 0xd800 && c <= 0xdbff
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// ── apply_patches ───────────────────────────────────────────────────
|
|
397
|
-
// Applies patches to a string, tracking cumulative offset.
|
|
398
|
-
// Used when on_patches is not provided, to update
|
|
399
|
-
// client_state directly.
|
|
400
|
-
//
|
|
401
|
-
// Patches must have absolute coordinates (relative to the original
|
|
402
|
-
// string, not to the string after previous patches). The offset
|
|
403
|
-
// variable tracks the cumulative shift from previous patches.
|
|
404
|
-
function apply_patches(state, patches) {
|
|
405
|
-
var offset = 0
|
|
406
|
-
for (var patch of patches) {
|
|
407
|
-
state = state.substring(0, patch.range[0] + offset) + patch.content +
|
|
408
|
-
state.substring(patch.range[1] + offset)
|
|
409
|
-
offset += patch.content.length - (patch.range[1] - patch.range[0])
|
|
410
|
-
}
|
|
411
|
-
return state
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// get_digest():
|
|
415
|
-
// - Computes SHA-256 of the UTF-8 encoding of the string
|
|
416
|
-
// - Formatted as Repr-Digest: sha-256=:<base64-encoded-hash>:
|
|
417
|
-
async function get_digest(str) {
|
|
418
|
-
var bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str))
|
|
419
|
-
return `sha-256=:${btoa(String.fromCharCode(...new Uint8Array(bytes)))}:`
|
|
420
|
-
}
|
|
421
|
-
// check_digest():
|
|
422
|
-
// - Makes sure the current state matches the digest in the update
|
|
423
|
-
async function check_digest(update, client_state) {
|
|
424
|
-
// If the server sent a repr-digest, verify our state matches. Throw
|
|
425
|
-
// exception if it fails.
|
|
426
|
-
if (update.extra_headers?.["repr-digest"]?.startsWith('sha-256=')
|
|
427
|
-
&& update.extra_headers["repr-digest"] !== await get_digest(client_state)) {
|
|
428
|
-
console.log('repr-digest mismatch!')
|
|
429
|
-
console.log('repr-digest: ' + update.extra_headers["repr-digest"])
|
|
430
|
-
console.log('state: ' + client_state)
|
|
431
|
-
throw new Error('repr-digest mismatch')
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
package/client/web-utils.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
function set_acked_state(textarea, on = true) {
|
|
3
|
-
if (on)
|
|
4
|
-
textarea.style.caretColor = textarea.old_caretColor
|
|
5
|
-
else {
|
|
6
|
-
textarea.old_caretColor = textarea.style.caretColor
|
|
7
|
-
textarea.style.caretColor = 'red'
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function set_error_state(textarea, on = true) {
|
|
12
|
-
if (on) {
|
|
13
|
-
textarea.old_disabled = textarea.disabled
|
|
14
|
-
textarea.old_background = textarea.style.background
|
|
15
|
-
textarea.old_border = textarea.style.border
|
|
16
|
-
|
|
17
|
-
textarea.disabled = true
|
|
18
|
-
textarea.style.background = '#fee'
|
|
19
|
-
textarea.style.border = '4px solid red'
|
|
20
|
-
} else {
|
|
21
|
-
textarea.disabled = textarea.old_disabled
|
|
22
|
-
textarea.style.background = textarea.old_background
|
|
23
|
-
textarea.style.border = textarea.old_border
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// A convenient wrapper around the myers-diff.js library's "diff_main()" function,
|
|
28
|
-
// which is defined in https://braid.org/code/myers-diff1.js.
|
|
29
|
-
function diff(before, after) {
|
|
30
|
-
let diff = diff_main(before, after)
|
|
31
|
-
let patches = []
|
|
32
|
-
let offset = 0
|
|
33
|
-
for (let d of diff) {
|
|
34
|
-
let p = null
|
|
35
|
-
if (d[0] === 1) p = { range: [offset, offset], content: d[1] }
|
|
36
|
-
else if (d[0] === -1) {
|
|
37
|
-
p = { range: [offset, offset + d[1].length], content: "" }
|
|
38
|
-
offset += d[1].length
|
|
39
|
-
} else offset += d[1].length
|
|
40
|
-
if (p) {
|
|
41
|
-
p.unit = "text"
|
|
42
|
-
patches.push(p)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return patches
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function apply_patches_and_update_selection(textarea, patches) {
|
|
49
|
-
let offset = 0
|
|
50
|
-
for (let p of patches) {
|
|
51
|
-
p.range[0] += offset
|
|
52
|
-
p.range[1] += offset
|
|
53
|
-
offset -= p.range[1] - p.range[0]
|
|
54
|
-
offset += p.content.length
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let original = textarea.value
|
|
58
|
-
let sel = [textarea.selectionStart, textarea.selectionEnd]
|
|
59
|
-
|
|
60
|
-
for (var p of patches) {
|
|
61
|
-
let range = p.range
|
|
62
|
-
|
|
63
|
-
for (let i = 0; i < sel.length; i++)
|
|
64
|
-
if (sel[i] > range[0])
|
|
65
|
-
if (sel[i] > range[1]) sel[i] -= range[1] - range[0]
|
|
66
|
-
else sel[i] = range[0]
|
|
67
|
-
|
|
68
|
-
for (let i = 0; i < sel.length; i++) if (sel[i] > range[0]) sel[i] += p.content.length
|
|
69
|
-
|
|
70
|
-
original = original.substring(0, range[0]) + p.content + original.substring(range[1])
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
textarea.value = original
|
|
74
|
-
textarea.selectionStart = sel[0]
|
|
75
|
-
textarea.selectionEnd = sel[1]
|
|
76
|
-
}
|