braid-text 0.5.7 → 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 +6 -4
- package/server-demo.js +12 -4
- package/server.js +57 -47
- package/client/simpleton-sync.js +0 -508
- package/client/web-utils.js +0 -75
package/client/simpleton-sync.js
DELETED
|
@@ -1,508 +0,0 @@
|
|
|
1
|
-
// ************************************************************************
|
|
2
|
-
// ******* Reference Implementation of Simpleton Client Algorithm *********
|
|
3
|
-
// ************************************************************************
|
|
4
|
-
//
|
|
5
|
-
// This is the canonical JS reference for implementing a simpleton client.
|
|
6
|
-
// Other language implementations should mirror this logic exactly, with
|
|
7
|
-
// adaptations only for language-specific details (e.g., string encoding).
|
|
8
|
-
//
|
|
9
|
-
// requires braid-http@~1.3/braid-http-client.js
|
|
10
|
-
//
|
|
11
|
-
// --- API ---
|
|
12
|
-
//
|
|
13
|
-
// url: resource endpoint
|
|
14
|
-
//
|
|
15
|
-
// headers: custom headers that get forwarded through into the fetch
|
|
16
|
-
//
|
|
17
|
-
// on_patches?: (patches) => void
|
|
18
|
-
// processes incoming patches by applying them to the UI/textarea.
|
|
19
|
-
// Patches are guaranteed to be in-order and non-overlapping.
|
|
20
|
-
//
|
|
21
|
-
// IMPORTANT: Patches have ABSOLUTE positions — each patch's range
|
|
22
|
-
// refers to positions in the original state (before any patches in
|
|
23
|
-
// this update). When applying multiple patches sequentially, you MUST
|
|
24
|
-
// track a cumulative offset to adjust positions:
|
|
25
|
-
//
|
|
26
|
-
// var offset = 0
|
|
27
|
-
// for (var p of patches) {
|
|
28
|
-
// apply_at(p.range[0] + offset, p.range[1] + offset, p.content)
|
|
29
|
-
// offset += p.content.length - (p.range[1] - p.range[0])
|
|
30
|
-
// }
|
|
31
|
-
//
|
|
32
|
-
// Without offset tracking, multi-patch updates will corrupt the state.
|
|
33
|
-
//
|
|
34
|
-
// Optional. If not provided, simpleton applies patches internally
|
|
35
|
-
// to its own copy of the state, and you can get the new state via
|
|
36
|
-
// on_state (see "Local Edit Absorption" below).
|
|
37
|
-
//
|
|
38
|
-
// When provided, simpleton calls this to apply patches externally,
|
|
39
|
-
// then reads back the state via get_state(). This means any un-flushed
|
|
40
|
-
// local edits in the UI are absorbed into client_state after each server
|
|
41
|
-
// update (see "Local Edit Absorption" below).
|
|
42
|
-
//
|
|
43
|
-
// on_state?: (state) => void
|
|
44
|
-
// called after each server update with the new state
|
|
45
|
-
//
|
|
46
|
-
// get_patches?: (client_state) => patches
|
|
47
|
-
// returns patches representing diff between client_state and current state,
|
|
48
|
-
// which are guaranteed to be different if this method is being called.
|
|
49
|
-
// (the default does this in a fast/simple way, finding a common prefix
|
|
50
|
-
// and suffix, but you can supply something better, or possibly keep
|
|
51
|
-
// track of patches as they come from your editor)
|
|
52
|
-
//
|
|
53
|
-
// get_state: () => current_state
|
|
54
|
-
// returns the current state (e.g., textarea.value)
|
|
55
|
-
//
|
|
56
|
-
// content_type: used for Accept and Content-Type headers
|
|
57
|
-
//
|
|
58
|
-
// on_error?: (error) => void
|
|
59
|
-
// called when an error occurs (e.g., network failure, digest mismatch)
|
|
60
|
-
//
|
|
61
|
-
// on_online?: (is_online) => void
|
|
62
|
-
// called when the connection status changes
|
|
63
|
-
//
|
|
64
|
-
// on_ack?: () => void
|
|
65
|
-
// called when all outstanding PUTs have been acknowledged
|
|
66
|
-
//
|
|
67
|
-
// send_digests?: boolean
|
|
68
|
-
// if truthy, includes a Repr-Digest header with each PUT
|
|
69
|
-
//
|
|
70
|
-
// returns { changed, abort }
|
|
71
|
-
// call changed() whenever there is a local change,
|
|
72
|
-
// and the system will call a combination of get_state and
|
|
73
|
-
// get_patches when it needs to. (get_state is required;
|
|
74
|
-
// get_patches is optional.)
|
|
75
|
-
// call abort() to abort the subscription.
|
|
76
|
-
//
|
|
77
|
-
// --- Retry and Reconnection Behavior ---
|
|
78
|
-
//
|
|
79
|
-
// Simpleton relies on braid_fetch for retry/reconnection:
|
|
80
|
-
//
|
|
81
|
-
// Subscription (GET):
|
|
82
|
-
// retry: () => true — always reconnect on any error (network failure,
|
|
83
|
-
// HTTP error, etc.). Reconnection backs off:
|
|
84
|
-
// delay = Math.min(retry_count + 1, 3) * 1000 ms
|
|
85
|
-
// i.e., 1s, 2s, 3s, 3s, 3s, ...
|
|
86
|
-
// On reconnect, sends Parents via the parents callback to resume
|
|
87
|
-
// from where the client left off.
|
|
88
|
-
//
|
|
89
|
-
// PUT requests:
|
|
90
|
-
// retry: (res) => res.status !== 550 — retry all errors EXCEPT
|
|
91
|
-
// HTTP 550 (Repr-Digest mismatch, meaning client is out of sync).
|
|
92
|
-
// This means:
|
|
93
|
-
// - Connection failure: retried with backoff
|
|
94
|
-
// - HTTP 401, 403, 408, 429, 500, 502, 503, 504, etc.: retried
|
|
95
|
-
// - HTTP 550: out of sync — stop retrying, throw error. The
|
|
96
|
-
// client must be torn down and restarted from scratch.
|
|
97
|
-
//
|
|
98
|
-
// --- Local Edit Absorption ---
|
|
99
|
-
//
|
|
100
|
-
// When on_patches is provided, after applying server patches via
|
|
101
|
-
// on_patches(), client_state is set to get_state(). If the UI has
|
|
102
|
-
// un-flushed local edits (typed but changed() not yet called), those
|
|
103
|
-
// edits are silently absorbed into client_state and will never be sent
|
|
104
|
-
// as a diff. In practice, the JS avoids this because changed() is
|
|
105
|
-
// called on every keystroke, and additionally, each time a PUT
|
|
106
|
-
// completes, the code re-diffs and sends any edits that accumulated
|
|
107
|
-
// while the PUT was in flight — so local edits are never stuck
|
|
108
|
-
// waiting; they flush as soon as a PUT slot opens up.
|
|
109
|
-
//
|
|
110
|
-
// When on_patches is NOT provided, client_state is updated by applying
|
|
111
|
-
// patches to the old client_state only. In this case, you should
|
|
112
|
-
// provide on_state to receive the updated state after each server
|
|
113
|
-
// update; otherwise your UI will not reflect remote changes.
|
|
114
|
-
//
|
|
115
|
-
function simpleton_client(url, {
|
|
116
|
-
on_patches,
|
|
117
|
-
on_state,
|
|
118
|
-
get_patches,
|
|
119
|
-
get_state,
|
|
120
|
-
content_type,
|
|
121
|
-
headers, // The user can pass in custom headers
|
|
122
|
-
// that are forwarded into fetches
|
|
123
|
-
on_error,
|
|
124
|
-
on_online,
|
|
125
|
-
on_ack,
|
|
126
|
-
send_digests
|
|
127
|
-
}) {
|
|
128
|
-
var peer = Math.random().toString(36).slice(2)
|
|
129
|
-
var client_version = [] // sorted version strings
|
|
130
|
-
var client_state = "" // text as of client_version
|
|
131
|
-
var char_counter = -1 // char-delta for version IDs
|
|
132
|
-
var outstanding_changes = 0 // PUTs sent, not yet ACKed
|
|
133
|
-
var max_outstanding_changes = 10 // throttle limit
|
|
134
|
-
var throttled = false
|
|
135
|
-
var throttled_updates = []
|
|
136
|
-
|
|
137
|
-
// extend the headers with merge-type and peer
|
|
138
|
-
headers = {
|
|
139
|
-
...headers,
|
|
140
|
-
"Merge-Type": "simpleton",
|
|
141
|
-
Peer: peer,
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Manages both the GET subscription and PUT requests through a single
|
|
145
|
-
// channel with automatic reconnection and PUT queuing.
|
|
146
|
-
var channel = reliable_update_channel(url, {
|
|
147
|
-
reconnect_from_parents: () => client_version.length ? client_version : null,
|
|
148
|
-
get_headers: {
|
|
149
|
-
...headers,
|
|
150
|
-
...content_type && {Accept: content_type}
|
|
151
|
-
},
|
|
152
|
-
put_headers: {
|
|
153
|
-
...headers,
|
|
154
|
-
...content_type && {"Content-Type": content_type}
|
|
155
|
-
},
|
|
156
|
-
on_update: async update => {
|
|
157
|
-
// ── Parent check ────────────────────────────────────────
|
|
158
|
-
// Core simpleton invariant: only accept updates whose
|
|
159
|
-
// parents form a continuous chain. We compare against the
|
|
160
|
-
// last queued update's version (if throttled) or
|
|
161
|
-
// client_version. When throttled, matching updates are
|
|
162
|
-
// queued but not applied — they'll be applied later when
|
|
163
|
-
// the throttle clears (see changed()).
|
|
164
|
-
update.parents.sort()
|
|
165
|
-
var last_queued = throttled_updates.length
|
|
166
|
-
? throttled_updates[throttled_updates.length - 1].version
|
|
167
|
-
: client_version
|
|
168
|
-
if (versions_eq(last_queued, update.parents))
|
|
169
|
-
if (throttled) throttled_updates.push(update)
|
|
170
|
-
else await apply_update(update)
|
|
171
|
-
},
|
|
172
|
-
on_status: status => on_online && on_online(status.online),
|
|
173
|
-
on_error: err => on_error && on_error(err),
|
|
174
|
-
|
|
175
|
-
// this api is preliminary and undocumented;
|
|
176
|
-
// we use it to tell the reliable_update_channel to die,
|
|
177
|
-
// if there is a digest mismatch on the server,
|
|
178
|
-
// which will result in a 550 status code
|
|
179
|
-
no_retry_status_codes: [550]
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
async function apply_update(update) {
|
|
183
|
-
// ── Parse and convert patches ───────────────────────────────
|
|
184
|
-
// braid_fetch provides body and patch content as bytes;
|
|
185
|
-
// body_text and content_text are dynamic properties that
|
|
186
|
-
// decode bytes to a string via a UTF-8 decoder.
|
|
187
|
-
var patches
|
|
188
|
-
if (update.patches) {
|
|
189
|
-
for (let patch of update.patches) {
|
|
190
|
-
patch.range = patch.range.match(/\d+/g).map((x) => 1 * x)
|
|
191
|
-
patch.content = patch.content_text
|
|
192
|
-
}
|
|
193
|
-
patches = update.patches.sort((a, b) => a.range[0] - b.range[0])
|
|
194
|
-
|
|
195
|
-
// ── JS-SPECIFIC: Convert code-point ranges to UTF-16 indices ──
|
|
196
|
-
// The wire protocol uses Unicode code-point offsets.
|
|
197
|
-
// JS strings are UTF-16, so we must convert.
|
|
198
|
-
//
|
|
199
|
-
// OTHER LANGUAGES: Skip this conversion if your strings
|
|
200
|
-
// are natively indexed by code points (e.g., Emacs Lisp,
|
|
201
|
-
// Python, Rust's char iterator).
|
|
202
|
-
convert_ranges_codepoints_to_utf16(patches, client_state)
|
|
203
|
-
} else {
|
|
204
|
-
// Initial snapshot: convert body to a patch replacing
|
|
205
|
-
// [0,0] so it follows the same code path as incremental
|
|
206
|
-
// patches.
|
|
207
|
-
patches = [{range: [0, 0], content: update.body_text}]
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ── Apply the update ────────────────────────────────────────
|
|
211
|
-
if (on_patches) {
|
|
212
|
-
// Apply patches to the UI, then read back the
|
|
213
|
-
// full state. Warning: if changed() hasn't been
|
|
214
|
-
// called for recent local edits, get_state() will
|
|
215
|
-
// absorb them into client_state silently — call
|
|
216
|
-
// changed() after every local edit to avoid this.
|
|
217
|
-
on_patches(patches)
|
|
218
|
-
client_state = get_state()
|
|
219
|
-
} else {
|
|
220
|
-
// Apply patches to our internal state; the
|
|
221
|
-
// result is delivered via on_state below.
|
|
222
|
-
client_state = apply_patches(client_state, patches)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ── Advance version ─────────────────────────────────────────
|
|
226
|
-
// IMPORTANT: This must happen synchronously (before any await)
|
|
227
|
-
// to prevent the changed() accumulation loop from interleaving
|
|
228
|
-
// and capturing a stale client_version during a yield point.
|
|
229
|
-
client_version = update.version
|
|
230
|
-
|
|
231
|
-
// ── Notify listener ─────────────────────────────────────────
|
|
232
|
-
// IMPORTANT: No changed() / flush is called here.
|
|
233
|
-
// The JS does NOT send edits after receiving a server
|
|
234
|
-
// update. The PUT response handler's async accumulation
|
|
235
|
-
// loop handles flushing accumulated edits.
|
|
236
|
-
if (on_state) on_state(client_state)
|
|
237
|
-
|
|
238
|
-
// ── Digest verification ─────────────────────────────────────
|
|
239
|
-
// If the server sent a repr-digest, verify our state
|
|
240
|
-
// matches. On mismatch, THROW — this halts the
|
|
241
|
-
// subscription handler. The document is corrupted and
|
|
242
|
-
// continuing would compound the problem.
|
|
243
|
-
//
|
|
244
|
-
// This is placed after advancing client_version and calling
|
|
245
|
-
// on_state so that the await does not create a yield point
|
|
246
|
-
// between applying patches and advancing client_version.
|
|
247
|
-
// That yield point previously allowed the changed() PUT loop
|
|
248
|
-
// to interleave, capturing a stale client_version and causing
|
|
249
|
-
// edit loss.
|
|
250
|
-
if (update.extra_headers &&
|
|
251
|
-
update.extra_headers["repr-digest"] &&
|
|
252
|
-
update.extra_headers["repr-digest"].startsWith('sha-256=') &&
|
|
253
|
-
update.extra_headers["repr-digest"] !== await get_digest(client_state)) {
|
|
254
|
-
console.log('repr-digest mismatch!')
|
|
255
|
-
console.log('repr-digest: ' + update.extra_headers["repr-digest"])
|
|
256
|
-
console.log('state: ' + client_state)
|
|
257
|
-
throw new Error('repr-digest mismatch')
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// ── Public interface ────────────────────────────────────────────────
|
|
262
|
-
return {
|
|
263
|
-
// ── abort() — cancel the subscription ─────────────────────────
|
|
264
|
-
abort: () => channel.close(),
|
|
265
|
-
|
|
266
|
-
// ── changed() — call when local edits occur ───────────────────
|
|
267
|
-
// This is the entry point for sending local edits. It:
|
|
268
|
-
// 1. Diffs client_state vs current state
|
|
269
|
-
// 2. Checks the throttle (outstanding_changes >= max)
|
|
270
|
-
// 3. Sends a PUT with the diff
|
|
271
|
-
// 4. After the PUT completes, loops to check for MORE accumulated
|
|
272
|
-
// edits (the async accumulation loop), sending them too.
|
|
273
|
-
//
|
|
274
|
-
// The async accumulation loop (while(true) {...}) is equivalent
|
|
275
|
-
// to a callback-driven flush: after each PUT ACK, re-diff and
|
|
276
|
-
// send again if changed. This ensures edits that accumulate
|
|
277
|
-
// during a PUT round-trip are eventually sent.
|
|
278
|
-
changed: () => {
|
|
279
|
-
function get_change() {
|
|
280
|
-
var new_state = get_state()
|
|
281
|
-
if (new_state === client_state) return null
|
|
282
|
-
var patches = get_patches ? get_patches(client_state) :
|
|
283
|
-
[simple_diff(client_state, new_state)]
|
|
284
|
-
return {patches, new_state}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
var change = get_change()
|
|
288
|
-
if (!change) {
|
|
289
|
-
if (throttled) {
|
|
290
|
-
throttled = false
|
|
291
|
-
for (var update of throttled_updates)
|
|
292
|
-
if (versions_eq(client_version, update.parents))
|
|
293
|
-
apply_update(update).catch(on_error)
|
|
294
|
-
throttled_updates = []
|
|
295
|
-
}
|
|
296
|
-
return
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (outstanding_changes >= max_outstanding_changes) {
|
|
300
|
-
throttled = true
|
|
301
|
-
return
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
var {patches, new_state} = change
|
|
305
|
-
|
|
306
|
-
// Save JS-index patches before code-point conversion mutates them
|
|
307
|
-
var js_patches = patches.map(p => ({range: [...p.range], content: p.content}))
|
|
308
|
-
|
|
309
|
-
;(async () => {
|
|
310
|
-
while (true) {
|
|
311
|
-
// ── JS-SPECIFIC: Convert JS UTF-16 ranges to code-points ──
|
|
312
|
-
// The wire protocol uses code-point offsets. See the
|
|
313
|
-
// inverse conversion in the receive path above.
|
|
314
|
-
//
|
|
315
|
-
// OTHER LANGUAGES: Skip this if your strings are
|
|
316
|
-
// natively code-point indexed.
|
|
317
|
-
convert_ranges_utf16_to_codepoints(patches, client_state)
|
|
318
|
-
|
|
319
|
-
for (let patch of patches) {
|
|
320
|
-
// ── Update char_counter ─────────────────────────
|
|
321
|
-
// Increment by deleted chars + inserted chars
|
|
322
|
-
char_counter += patch.range[1] - patch.range[0]
|
|
323
|
-
char_counter += count_code_points(patch.content)
|
|
324
|
-
|
|
325
|
-
patch.unit = "text"
|
|
326
|
-
patch.range = `[${patch.range[0]}:${patch.range[1]}]`
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ── Compute version and advance optimistically ──────
|
|
330
|
-
var version = [peer + "-" + char_counter]
|
|
331
|
-
|
|
332
|
-
var parents = client_version
|
|
333
|
-
client_version = version // optimistic advance
|
|
334
|
-
client_state = new_state // update client_state
|
|
335
|
-
|
|
336
|
-
// ── Send PUT ────────────────────────────────────────
|
|
337
|
-
// Uses braid_fetch with retry: (res) => res.status !== 550
|
|
338
|
-
// This means:
|
|
339
|
-
// - Network failures: retried with backoff
|
|
340
|
-
// - HTTP 401, 403, 408, 429, 500, 502, 503, 504: retried
|
|
341
|
-
// - HTTP 550 (Repr-Digest mismatch / out of sync):
|
|
342
|
-
// give up, throw — client must be re-created
|
|
343
|
-
outstanding_changes++
|
|
344
|
-
|
|
345
|
-
await channel.put({
|
|
346
|
-
version, parents, patches,
|
|
347
|
-
headers: {
|
|
348
|
-
...send_digests && {
|
|
349
|
-
"Repr-Digest": await get_digest(client_state) }
|
|
350
|
-
}
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
throttled = false
|
|
354
|
-
outstanding_changes--
|
|
355
|
-
if (on_ack && !outstanding_changes) on_ack()
|
|
356
|
-
|
|
357
|
-
// ── Check for accumulated edits ─────────────────────
|
|
358
|
-
// While the PUT was in flight, more local edits may
|
|
359
|
-
// have occurred. Diff again and loop if changed.
|
|
360
|
-
var more = get_change()
|
|
361
|
-
if (!more) return
|
|
362
|
-
;({patches, new_state} = more)
|
|
363
|
-
}
|
|
364
|
-
})()
|
|
365
|
-
|
|
366
|
-
return js_patches
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function versions_eq(a, b) {
|
|
371
|
-
return a.length === b.length && a.every((v, i) => v === b[i])
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// ── JS-SPECIFIC: UTF-16 helpers ─────────────────────────────────────
|
|
375
|
-
// These handle surrogate pairs in UTF-16 JS strings. Characters
|
|
376
|
-
// outside the Basic Multilingual Plane (BMP) are encoded as two
|
|
377
|
-
// 16-bit code units (a surrogate pair: high 0xD800-0xDBFF, low
|
|
378
|
-
// 0xDC00-0xDFFF). Such a pair represents one Unicode code point.
|
|
379
|
-
//
|
|
380
|
-
// OTHER LANGUAGES: You don't need these if your string type is
|
|
381
|
-
// natively indexed by code points.
|
|
382
|
-
|
|
383
|
-
function get_char_size(str, utf16_index) {
|
|
384
|
-
const char_code = str.charCodeAt(utf16_index)
|
|
385
|
-
return (char_code >= 0xd800 && char_code <= 0xdbff) ? 2 : 1
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function count_code_points(str) {
|
|
389
|
-
let code_points = 0
|
|
390
|
-
for (let i = 0; i < str.length; i++) {
|
|
391
|
-
if (str.charCodeAt(i) >= 0xd800 && str.charCodeAt(i) <= 0xdbff) i++
|
|
392
|
-
code_points++
|
|
393
|
-
}
|
|
394
|
-
return code_points
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Converts patch ranges from code-point offsets to UTF-16 indices.
|
|
398
|
-
// Patches must be sorted by range[0].
|
|
399
|
-
function convert_ranges_codepoints_to_utf16(patches, str) {
|
|
400
|
-
let codepoint_index = 0
|
|
401
|
-
let utf16_index = 0
|
|
402
|
-
for (let patch of patches) {
|
|
403
|
-
while (codepoint_index < patch.range[0]) {
|
|
404
|
-
utf16_index += get_char_size(str, utf16_index)
|
|
405
|
-
codepoint_index++
|
|
406
|
-
}
|
|
407
|
-
patch.range[0] = utf16_index
|
|
408
|
-
|
|
409
|
-
while (codepoint_index < patch.range[1]) {
|
|
410
|
-
utf16_index += get_char_size(str, utf16_index)
|
|
411
|
-
codepoint_index++
|
|
412
|
-
}
|
|
413
|
-
patch.range[1] = utf16_index
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Converts patch ranges from UTF-16 indices to code-point offsets.
|
|
418
|
-
// Patches must be sorted by range[0].
|
|
419
|
-
function convert_ranges_utf16_to_codepoints(patches, str) {
|
|
420
|
-
let codepoint_index = 0
|
|
421
|
-
let utf16_index = 0
|
|
422
|
-
for (let patch of patches) {
|
|
423
|
-
while (utf16_index < patch.range[0]) {
|
|
424
|
-
utf16_index += get_char_size(str, utf16_index)
|
|
425
|
-
codepoint_index++
|
|
426
|
-
}
|
|
427
|
-
patch.range[0] = codepoint_index
|
|
428
|
-
|
|
429
|
-
while (utf16_index < patch.range[1]) {
|
|
430
|
-
utf16_index += get_char_size(str, utf16_index)
|
|
431
|
-
codepoint_index++
|
|
432
|
-
}
|
|
433
|
-
patch.range[1] = codepoint_index
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// ── simple_diff ─────────────────────────────────────────────────────
|
|
438
|
-
// Finds the longest common prefix and suffix between two strings,
|
|
439
|
-
// returning the minimal edit that transforms `old_str` into `new_str`.
|
|
440
|
-
//
|
|
441
|
-
// Returns: { range: [prefix_len, old_str.length - suffix_len],
|
|
442
|
-
// content: new_str.slice(prefix_len, new_str.length - suffix_len) }
|
|
443
|
-
//
|
|
444
|
-
// This produces a single contiguous edit. For multi-cursor or
|
|
445
|
-
// multi-region edits, supply a custom get_patches function instead.
|
|
446
|
-
function simple_diff(old_str, new_str) {
|
|
447
|
-
// Find common prefix length
|
|
448
|
-
var prefix_len = 0
|
|
449
|
-
var min_len = Math.min(old_str.length, new_str.length)
|
|
450
|
-
while (prefix_len < min_len && old_str[prefix_len] === new_str[prefix_len]) prefix_len++
|
|
451
|
-
|
|
452
|
-
// Don't split a surrogate pair: if prefix ends on a low surrogate,
|
|
453
|
-
// the preceding high surrogate only matched by coincidence (same
|
|
454
|
-
// Unicode block), so back up to include the whole pair in the diff.
|
|
455
|
-
if (prefix_len > 0 && is_low_surrogate(old_str, prefix_len))
|
|
456
|
-
prefix_len--
|
|
457
|
-
|
|
458
|
-
// Find common suffix length (from what remains after prefix)
|
|
459
|
-
var suffix_len = 0
|
|
460
|
-
min_len -= prefix_len
|
|
461
|
-
while (suffix_len < min_len && old_str[old_str.length - suffix_len - 1] === new_str[new_str.length - suffix_len - 1]) suffix_len++
|
|
462
|
-
|
|
463
|
-
// Same guard for suffixes: if the range end (old_str.length - suffix_len)
|
|
464
|
-
// lands on a low surrogate, the suffix consumed it without its high
|
|
465
|
-
// surrogate, so back up.
|
|
466
|
-
if (suffix_len > 0 && is_low_surrogate(old_str, old_str.length - suffix_len))
|
|
467
|
-
suffix_len--
|
|
468
|
-
|
|
469
|
-
return {range: [prefix_len, old_str.length - suffix_len], content: new_str.slice(prefix_len, new_str.length - suffix_len)}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function is_low_surrogate(str, i) {
|
|
473
|
-
var c = str.charCodeAt(i)
|
|
474
|
-
return c >= 0xdc00 && c <= 0xdfff
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function is_high_surrogate(str, i) {
|
|
478
|
-
var c = str.charCodeAt(i)
|
|
479
|
-
return c >= 0xd800 && c <= 0xdbff
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// ── apply_patches ───────────────────────────────────────────────────
|
|
483
|
-
// Applies patches to a string, tracking cumulative offset.
|
|
484
|
-
// Used when on_patches is not provided, to update
|
|
485
|
-
// client_state directly.
|
|
486
|
-
//
|
|
487
|
-
// Patches must have absolute coordinates (relative to the original
|
|
488
|
-
// string, not to the string after previous patches). The offset
|
|
489
|
-
// variable tracks the cumulative shift from previous patches.
|
|
490
|
-
function apply_patches(state, patches) {
|
|
491
|
-
var offset = 0
|
|
492
|
-
for (var patch of patches) {
|
|
493
|
-
state = state.substring(0, patch.range[0] + offset) + patch.content +
|
|
494
|
-
state.substring(patch.range[1] + offset)
|
|
495
|
-
offset += patch.content.length - (patch.range[1] - patch.range[0])
|
|
496
|
-
}
|
|
497
|
-
return state
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// ── get_digest ──────────────────────────────────────────────────────
|
|
501
|
-
// Computes SHA-256 of the UTF-8 encoding of the state string,
|
|
502
|
-
// formatted as the Repr-Digest header value:
|
|
503
|
-
// sha-256=:<base64-encoded-hash>:
|
|
504
|
-
async function get_digest(str) {
|
|
505
|
-
var bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str))
|
|
506
|
-
return `sha-256=:${btoa(String.fromCharCode(...new Uint8Array(bytes)))}:`
|
|
507
|
-
}
|
|
508
|
-
}
|
package/client/web-utils.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
function set_acked_state(textarea, binary = true) {
|
|
3
|
-
if (!binary) {
|
|
4
|
-
textarea.old_caretColor = textarea.style.caretColor
|
|
5
|
-
|
|
6
|
-
textarea.style.caretColor = 'red'
|
|
7
|
-
} else {
|
|
8
|
-
textarea.style.caretColor = textarea.old_caretColor
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function set_error_state(textarea, binary = true) {
|
|
13
|
-
if (binary) {
|
|
14
|
-
textarea.old_disabled = textarea.disabled
|
|
15
|
-
textarea.old_background = textarea.style.background
|
|
16
|
-
textarea.old_border = textarea.style.border
|
|
17
|
-
|
|
18
|
-
textarea.disabled = true
|
|
19
|
-
textarea.style.background = '#fee'
|
|
20
|
-
textarea.style.border = '4px solid red'
|
|
21
|
-
} else {
|
|
22
|
-
textarea.disabled = textarea.old_disabled
|
|
23
|
-
textarea.style.background = textarea.old_background
|
|
24
|
-
textarea.style.border = textarea.old_border
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function diff(before, after) {
|
|
29
|
-
let diff = diff_main(before, after)
|
|
30
|
-
let patches = []
|
|
31
|
-
let offset = 0
|
|
32
|
-
for (let d of diff) {
|
|
33
|
-
let p = null
|
|
34
|
-
if (d[0] === 1) p = { range: [offset, offset], content: d[1] }
|
|
35
|
-
else if (d[0] === -1) {
|
|
36
|
-
p = { range: [offset, offset + d[1].length], content: "" }
|
|
37
|
-
offset += d[1].length
|
|
38
|
-
} else offset += d[1].length
|
|
39
|
-
if (p) {
|
|
40
|
-
p.unit = "text"
|
|
41
|
-
patches.push(p)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return patches
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function apply_patches_and_update_selection(textarea, patches) {
|
|
48
|
-
let offset = 0
|
|
49
|
-
for (let p of patches) {
|
|
50
|
-
p.range[0] += offset
|
|
51
|
-
p.range[1] += offset
|
|
52
|
-
offset -= p.range[1] - p.range[0]
|
|
53
|
-
offset += p.content.length
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
let original = textarea.value
|
|
57
|
-
let sel = [textarea.selectionStart, textarea.selectionEnd]
|
|
58
|
-
|
|
59
|
-
for (var p of patches) {
|
|
60
|
-
let range = p.range
|
|
61
|
-
|
|
62
|
-
for (let i = 0; i < sel.length; i++)
|
|
63
|
-
if (sel[i] > range[0])
|
|
64
|
-
if (sel[i] > range[1]) sel[i] -= range[1] - range[0]
|
|
65
|
-
else sel[i] = range[0]
|
|
66
|
-
|
|
67
|
-
for (let i = 0; i < sel.length; i++) if (sel[i] > range[0]) sel[i] += p.content.length
|
|
68
|
-
|
|
69
|
-
original = original.substring(0, range[0]) + p.content + original.substring(range[1])
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
textarea.value = original
|
|
73
|
-
textarea.selectionStart = sel[0]
|
|
74
|
-
textarea.selectionEnd = sel[1]
|
|
75
|
-
}
|