@helping-ai-workflow/md2doc 3.3.0 → 3.4.2

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.
@@ -0,0 +1,2631 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * wave-codec — a tolerant WaveJSON reader that remembers where every value came
5
+ * from, and a write-back that changes only those bytes.
6
+ *
7
+ * WaveJSON is a JavaScript object literal, not strict JSON. Real wavedrom code
8
+ * blocks use unquoted keys, single-quoted strings, trailing commas and line or
9
+ * block comments; `JSON.parse` rejects all four. So this is a small
10
+ * recursive-descent parser instead.
11
+ *
12
+ * It also records, for every value it parses, the half-open source range
13
+ * `[start, end)` of that value's literal text. That is what lets `patchSource`
14
+ * rewrite only the bytes of the one value that changed, leaving the author's
15
+ * indentation, quote characters and comments untouched. Nothing is normalised
16
+ * on the way in: `0..0` and `0...` render differently in wavedrom, so a parser
17
+ * that "tidied" a wave string would be destroying information.
18
+ *
19
+ * Alongside those it records a second table, `members`, holding the range of
20
+ * each whole member — key, colon, value and trailing comma. Value spans alone
21
+ * are enough to change a lane's wave string, and adding a cycle is only that:
22
+ * each lane's wave string gets rewritten and nothing else moves. Adding or
23
+ * deleting a *lane* is the one genuinely structural thing a wave GUI does, and
24
+ * member spans turn it into a local splice too, so it does not have to fall
25
+ * back on rewriting the file.
26
+ *
27
+ * Security: the source text comes out of the user's markdown file, so it is
28
+ * never handed to any code-executing API. This module builds no code at run
29
+ * time and imports nothing at all; `test/wave-codec.test.js` greps it for the
30
+ * spellings that would introduce either, so keep the prose here clear of them.
31
+ *
32
+ * Contract: this does not throw on any input. Every malformed source —
33
+ * including one nested past `MAX_DEPTH` — comes back as
34
+ * `{ok: false, message, offset}`, so callers in the layers above are entitled
35
+ * to call it without a `try`.
36
+ *
37
+ * "On any input" is the honest form of that promise, not "never": if the
38
+ * CALLER arrives with its stack already at the limit, the engine can raise
39
+ * RangeError in a frame where the catch handler itself has no stack left to
40
+ * run. Measured under `--stack-size=200`. Nothing in this file can close that,
41
+ * and the input is not what decides it.
42
+ *
43
+ * @typedef {Array<string|number>} DocPath
44
+ * Object keys are strings, array indices are numbers — `['signal', 0, 'wave']`.
45
+ * `spans` is keyed by `JSON.stringify(path)`, so index types must match:
46
+ * `['signal','0','wave']` is a different key and is never produced here.
47
+ */
48
+
49
+ /**
50
+ * Every character ECMAScript counts as WhiteSpace or a LineTerminator. The
51
+ * exotic ones matter: a wavedrom block pasted out of a rendered web page
52
+ * carries U+00A0, and one pasted out of a CJK document carries U+3000. JS
53
+ * itself treats both as whitespace, so this parser does too — otherwise a lane
54
+ * ends up with the key `'name '`, the GUI shows no name, and the rename
55
+ * the user then types has no span to write through.
56
+ */
57
+ const SPACE = {};
58
+ for (const c of ' \t\n\r\f\v        ' +
59
+ '     

   ') {
60
+ SPACE[c] = 1;
61
+ }
62
+
63
+ /**
64
+ * U+200B is neither JS whitespace nor a valid identifier character, and it is
65
+ * the other invisible that survives a web paste. Excluding it here turns it
66
+ * into a reported error rather than a silently wrong key.
67
+ */
68
+ const ZERO_WIDTH_SPACE = '​';
69
+
70
+ /**
71
+ * Nesting ceiling. The deepest legitimate WaveJSON is about ten containers —
72
+ * root, `signal`, a few nested groups, a lane, its `data`, an element — so 64
73
+ * leaves roughly six times the headroom any real document needs, while sitting
74
+ * ~49x below the depth at which this host's stack actually gives out (measured:
75
+ * 3124 levels parse, 3125 throws). Choosing a fixed ceiling rather than
76
+ * catching the stack overflow is what makes the failure deterministic: it does
77
+ * not move with `--stack-size` or with how deep the caller's own stack already
78
+ * was, and it can report a real `offset`.
79
+ */
80
+ const MAX_DEPTH = 64;
81
+
82
+ const NUMBER_RE = /[+-]?(?:0[xX][0-9a-fA-F]+|0[oO][0-7]+|0[bB][01]+|Infinity|(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)/y;
83
+ const IDENT_START_RE = /[A-Za-z_$€-￿]/;
84
+ const IDENT_PART_RE = /[A-Za-z0-9_$€-￿]/;
85
+
86
+ /**
87
+ * Parse a WaveJSON source string.
88
+ *
89
+ * @param {string} text
90
+ * @returns {{ok: true, doc: any, spans: Map<string, [number, number]>}
91
+ * |{ok: false, message: string, offset: number}}
92
+ */
93
+ function parseSource(text) {
94
+ if (typeof text !== 'string') {
95
+ return {
96
+ ok: false,
97
+ offset: 0,
98
+ message: 'parseSource expects a string, got ' + (text === null ? 'null' : typeof text),
99
+ };
100
+ }
101
+ const st = {
102
+ text: text, i: 0, depth: 0,
103
+ spans: new Map(), members: new Map(), leads: new Map(), dups: new Set(),
104
+ };
105
+ try {
106
+ skipTrivia(st);
107
+ if (st.i >= text.length) fail(st, st.i, 'Empty source: expected a WaveJSON value');
108
+ const doc = parseValue(st, []);
109
+ skipTrivia(st);
110
+ if (st.i < text.length) {
111
+ fail(st, st.i, 'Unexpected trailing content after the top-level value');
112
+ }
113
+ // `source` is what lets `patchSource` refuse a stale parse result. Spans are
114
+ // offsets into one particular string; applied to a text that has moved on,
115
+ // they land mid-token and write a file that no longer parses. Holding the
116
+ // string costs nothing — strings are immutable, so this is the same one the
117
+ // caller already has, not a copy.
118
+ return {
119
+ ok: true,
120
+ doc: doc,
121
+ spans: st.spans,
122
+ members: st.members,
123
+ leads: st.leads,
124
+ dups: st.dups,
125
+ source: text,
126
+ };
127
+ } catch (err) {
128
+ if (err && err.isWaveParseError) {
129
+ return { ok: false, message: err.message, offset: err.offset };
130
+ }
131
+ if (err instanceof RangeError) {
132
+ // MAX_DEPTH normally gets here first, so this looks unreachable and was
133
+ // reported as unproven when it was written. It is not: a review reached
134
+ // it under `--stack-size=200` from 12 of 13 sampled caller depths, and
135
+ // again at 500. The earlier negative result came from probing at the
136
+ // default stack size, where V8 re-tiers the calibrating harness between
137
+ // calibration and test. This branch is what holds the no-`try` contract
138
+ // when a caller arrives on a deep stack — which a GUI event path does.
139
+ return { ok: false, message: 'Source is too deeply nested to parse', offset: st.i };
140
+ }
141
+ throw err;
142
+ }
143
+ }
144
+
145
+ /** Throw a positioned parse error. `parseSource` turns it into `{ok:false}`. */
146
+ function fail(st, offset, message) {
147
+ const before = st.text.slice(0, offset);
148
+ const line = before.split('\n').length;
149
+ const col = offset - (before.lastIndexOf('\n') + 1) + 1;
150
+ const err = new Error(message + ' (line ' + line + ', column ' + col + ')');
151
+ err.isWaveParseError = true;
152
+ err.offset = offset;
153
+ throw err;
154
+ }
155
+
156
+ /** Skip whitespace, line comments and block comments. */
157
+ function skipTrivia(st) {
158
+ const t = st.text;
159
+ for (;;) {
160
+ const c = t[st.i];
161
+ if (c === undefined) return;
162
+ if (SPACE[c] === 1) { st.i++; continue; }
163
+ if (c === '/' && t[st.i + 1] === '/') {
164
+ st.i += 2;
165
+ while (st.i < t.length && t[st.i] !== '\n' && t[st.i] !== '\r') st.i++;
166
+ continue;
167
+ }
168
+ if (c === '/' && t[st.i + 1] === '*') {
169
+ const start = st.i;
170
+ const end = t.indexOf('*/', st.i + 2);
171
+ if (end === -1) fail(st, start, 'Unterminated block comment');
172
+ st.i = end + 2;
173
+ continue;
174
+ }
175
+ return;
176
+ }
177
+ }
178
+
179
+ /** Parse one value and record its span under `path`. */
180
+ function parseValue(st, path) {
181
+ skipTrivia(st);
182
+ const start = st.i;
183
+ const c = st.text[st.i];
184
+ if (c === undefined) fail(st, start, 'Unexpected end of input: expected a value');
185
+ let value;
186
+ if (c === '{' || c === '[') {
187
+ if (st.depth >= MAX_DEPTH) {
188
+ fail(st, start, 'Nesting is too deep: more than ' + MAX_DEPTH +
189
+ ' levels of nested objects and arrays');
190
+ }
191
+ st.depth++;
192
+ value = c === '{' ? parseObject(st, path) : parseArray(st, path);
193
+ st.depth--;
194
+ } else if (c === '"' || c === "'") value = parseString(st);
195
+ else if (c === '-' || c === '+' || c === '.' || (c >= '0' && c <= '9')) value = parseNumber(st);
196
+ else value = parseWord(st);
197
+ st.spans.set(JSON.stringify(path), [start, st.i]);
198
+ return value;
199
+ }
200
+
201
+ /**
202
+ * Record the span of a whole member — the key, the colon, the value and the
203
+ * trailing comma if one is there.
204
+ *
205
+ * This is recorded here, while the cursor is walking the member, rather than by
206
+ * a later scan over the source. A scan cannot do it: looking forward from a
207
+ * value's end for the separator finds the comma written inside a block comment,
208
+ * and looking backward from a value for its key has to walk past a line comment
209
+ * that is allowed to contain a colon of its own. Both are already handled here
210
+ * for free, because `skipTrivia` has been over that text.
211
+ *
212
+ * The range starts at the first character of the key (for an array, of the
213
+ * element), so leading indentation and any comment sitting above the member are
214
+ * outside it. It ends after the comma, so the trivia between the value and that
215
+ * comma — including a comment written there — is inside it. Without a trailing
216
+ * comma it ends at the last character of the value, and the trailing comment on
217
+ * the same line stays outside.
218
+ */
219
+ function recordMember(st, memberKey, start, end, lead) {
220
+ st.members.set(memberKey, [start, end]);
221
+ st.leads.set(memberKey, lead);
222
+ }
223
+
224
+ function parseObject(st, path) {
225
+ const obj = {};
226
+ const seen = new Set();
227
+ st.i++; // consume the opening brace
228
+ for (;;) {
229
+ const lead = st.i; // where this member's leading trivia starts
230
+ skipTrivia(st);
231
+ const c = st.text[st.i];
232
+ if (c === undefined) fail(st, st.i, 'Unterminated object: expected a closing brace');
233
+ if (c === '}') { st.i++; return obj; }
234
+ const memberStart = st.i;
235
+ const key = parseKey(st);
236
+ skipTrivia(st);
237
+ if (st.text[st.i] !== ':') {
238
+ fail(st, st.i, "Expected ':' after key " + JSON.stringify(key));
239
+ }
240
+ st.i++;
241
+ const memberKey = JSON.stringify(path.concat([key]));
242
+ // `{a: {b: 1}, a: 2}` is legal here and the later member wins in `doc`, so
243
+ // both the value span and the member span at ["a"] end up describing the
244
+ // second one while the span at ["a","b"] still describes text inside the
245
+ // first. Writing a value through a span stays correct; splicing a member
246
+ // out does not, so the paths involved are remembered and refused there.
247
+ if (seen.has(memberKey)) st.dups.add(memberKey);
248
+ else seen.add(memberKey);
249
+ const value = parseValue(st, path.concat([key]));
250
+ assign(obj, key, value);
251
+ const valueEnd = st.i;
252
+ skipTrivia(st);
253
+ const sep = st.text[st.i];
254
+ if (sep === ',') {
255
+ st.i++;
256
+ recordMember(st, memberKey, memberStart, st.i, lead);
257
+ continue; // a trailing comma just loops round to the brace
258
+ }
259
+ recordMember(st, memberKey, memberStart, valueEnd, lead);
260
+ if (sep === '}') { st.i++; return obj; }
261
+ if (sep === undefined) fail(st, st.i, 'Unterminated object: expected a closing brace');
262
+ fail(st, st.i, "Expected ',' or '}' but found " + JSON.stringify(sep));
263
+ }
264
+ }
265
+
266
+ function parseArray(st, path) {
267
+ const arr = [];
268
+ st.i++; // consume the opening bracket
269
+ for (;;) {
270
+ const lead = st.i; // where this element's leading trivia starts
271
+ skipTrivia(st);
272
+ const c = st.text[st.i];
273
+ if (c === undefined) fail(st, st.i, 'Unterminated array: expected a closing bracket');
274
+ if (c === ']') { st.i++; return arr; }
275
+ const memberStart = st.i;
276
+ const memberKey = JSON.stringify(path.concat([arr.length]));
277
+ arr.push(parseValue(st, path.concat([arr.length])));
278
+ const valueEnd = st.i;
279
+ skipTrivia(st);
280
+ const sep = st.text[st.i];
281
+ if (sep === ',') {
282
+ st.i++;
283
+ recordMember(st, memberKey, memberStart, st.i, lead);
284
+ continue; // a trailing comma just loops round to the bracket
285
+ }
286
+ recordMember(st, memberKey, memberStart, valueEnd, lead);
287
+ if (sep === ']') { st.i++; return arr; }
288
+ if (sep === undefined) fail(st, st.i, 'Unterminated array: expected a closing bracket');
289
+ fail(st, st.i, "Expected ',' or ']' but found " + JSON.stringify(sep));
290
+ }
291
+ }
292
+
293
+ /** Object keys: quoted string, bare identifier, or numeric literal. */
294
+ function parseKey(st) {
295
+ const c = st.text[st.i];
296
+ if (c === '"' || c === "'") return parseString(st);
297
+ if (c === '-' || c === '+' || c === '.' || (c >= '0' && c <= '9')) return String(parseNumber(st));
298
+ const word = scanWord(st);
299
+ if (word === '') {
300
+ fail(st, st.i, 'Expected a property name but found ' + JSON.stringify(c === undefined ? '' : c));
301
+ }
302
+ return word;
303
+ }
304
+
305
+ /**
306
+ * Scan a bare identifier. Stops at anything `SPACE` calls trivia, so a space
307
+ * character can never be absorbed into a key, and at U+200B. Returns '' (with
308
+ * the position unmoved) when there is no identifier here.
309
+ */
310
+ function scanWord(st) {
311
+ const t = st.text;
312
+ if (!isIdentChar(t[st.i], true)) return '';
313
+ let j = st.i + 1;
314
+ while (isIdentChar(t[j], false)) j++;
315
+ const word = t.slice(st.i, j);
316
+ st.i = j;
317
+ return word;
318
+ }
319
+
320
+ function isIdentChar(c, isFirst) {
321
+ if (c === undefined) return false;
322
+ if (SPACE[c] === 1 || c === ZERO_WIDTH_SPACE) return false;
323
+ return (isFirst ? IDENT_START_RE : IDENT_PART_RE).test(c);
324
+ }
325
+
326
+ /**
327
+ * Write a key without letting a `__proto__` key in the source reach the
328
+ * prototype chain — the text is untrusted.
329
+ */
330
+ function assign(obj, key, value) {
331
+ if (key === '__proto__') {
332
+ Object.defineProperty(obj, key, {
333
+ value: value, enumerable: true, writable: true, configurable: true,
334
+ });
335
+ } else {
336
+ obj[key] = value;
337
+ }
338
+ }
339
+
340
+ function parseString(st) {
341
+ const quote = st.text[st.i];
342
+ const start = st.i;
343
+ st.i++;
344
+ let out = '';
345
+ for (;;) {
346
+ if (st.i >= st.text.length) fail(st, start, 'Unterminated string literal');
347
+ const c = st.text[st.i];
348
+ if (c === quote) { st.i++; return out; }
349
+ if (c === '\n' || c === '\r') {
350
+ fail(st, st.i, 'Unterminated string literal: line break inside a string');
351
+ }
352
+ if (c === '\\') { st.i++; out += readEscape(st); continue; }
353
+ out += c;
354
+ st.i++;
355
+ }
356
+ }
357
+
358
+ function readEscape(st) {
359
+ if (st.i >= st.text.length) fail(st, st.i, 'Unterminated escape sequence');
360
+ const c = st.text[st.i];
361
+ st.i++;
362
+ switch (c) {
363
+ case 'n': return '\n';
364
+ case 't': return '\t';
365
+ case 'r': return '\r';
366
+ case 'b': return '\b';
367
+ case 'f': return '\f';
368
+ case 'v': return '\v';
369
+ case '0':
370
+ if (st.text[st.i] >= '0' && st.text[st.i] <= '9') {
371
+ fail(st, st.i - 2, 'Octal escape sequences are not supported');
372
+ }
373
+ return '\0';
374
+ case 'x': return String.fromCharCode(readHex(st, 2, st.i - 2));
375
+ case 'u': {
376
+ if (st.text[st.i] === '{') {
377
+ const open = st.i;
378
+ st.i++;
379
+ let digits = '';
380
+ while (st.i < st.text.length && st.text[st.i] !== '}') digits += st.text[st.i++];
381
+ if (st.text[st.i] !== '}' || !/^[0-9a-fA-F]{1,6}$/.test(digits)) {
382
+ fail(st, open - 2, 'Invalid unicode escape sequence');
383
+ }
384
+ st.i++;
385
+ const code = parseInt(digits, 16);
386
+ if (code > 0x10ffff) fail(st, open - 2, 'Unicode escape sequence out of range');
387
+ return String.fromCodePoint(code);
388
+ }
389
+ return String.fromCharCode(readHex(st, 4, st.i - 2));
390
+ }
391
+ case '\r':
392
+ if (st.text[st.i] === '\n') st.i++;
393
+ return ''; // line continuation
394
+ case '\n': return '';
395
+ default: return c; // an escaped quote, backslash or slash stands for itself
396
+ }
397
+ }
398
+
399
+ function readHex(st, count, errAt) {
400
+ const digits = st.text.slice(st.i, st.i + count);
401
+ if (digits.length < count || !/^[0-9a-fA-F]+$/.test(digits)) {
402
+ fail(st, errAt, 'Invalid hexadecimal escape sequence');
403
+ }
404
+ st.i += count;
405
+ return parseInt(digits, 16);
406
+ }
407
+
408
+ function parseNumber(st) {
409
+ NUMBER_RE.lastIndex = st.i;
410
+ const m = NUMBER_RE.exec(st.text);
411
+ if (!m) fail(st, st.i, 'Invalid number literal');
412
+ const start = st.i;
413
+ st.i += m[0].length;
414
+ let body = m[0];
415
+ let sign = 1;
416
+ if (body[0] === '+' || body[0] === '-') {
417
+ if (body[0] === '-') sign = -1;
418
+ body = body.slice(1);
419
+ }
420
+ const n = Number(body);
421
+ if (Number.isNaN(n)) fail(st, start, 'Invalid number literal ' + JSON.stringify(m[0]));
422
+ return sign * n;
423
+ }
424
+
425
+ function parseWord(st) {
426
+ const start = st.i;
427
+ const word = scanWord(st);
428
+ if (word === '') fail(st, st.i, 'Unexpected character ' + JSON.stringify(st.text[st.i]));
429
+ switch (word) {
430
+ case 'true': return true;
431
+ case 'false': return false;
432
+ case 'null': return null;
433
+ case 'undefined': return undefined;
434
+ case 'NaN': return NaN;
435
+ case 'Infinity': return Infinity;
436
+ default:
437
+ return fail(st, start, 'Unexpected token ' + JSON.stringify(word));
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Write a set of edits back into the source as the smallest replacement that
443
+ * expresses them.
444
+ *
445
+ * Every edit names a value by path; the span recorded for that path says which
446
+ * bytes are that value, and only those bytes are rewritten. Everything else in
447
+ * the file — the author's indentation, the comment two columns to the right of
448
+ * the wave string, whichever quote character that one value happened to use —
449
+ * survives because it is never read and never written. That is not tidiness:
450
+ * against wavedrom 3.5.0 `0..0` and `0...` render as different waveforms (a
451
+ * transition marker against a plain hold), and so do the other levels and data
452
+ * characters, so a write-back that re-serialised or "normalised" a wave string
453
+ * would change the picture the user is looking at.
454
+ *
455
+ * Three kinds of edit:
456
+ *
457
+ * {path, value} or {op:'set', path, value}
458
+ * Replace the value at `path`. Refused when nothing is parsed at that
459
+ * path, or when the new value is a different kind from the old one —
460
+ * turning a string into an array is a structural change, not a local
461
+ * replacement, and this returns `{ok:false}` rather than guessing.
462
+ *
463
+ * {op:'insert', path, value}
464
+ * Splice a new element into an array; the last path segment is the index
465
+ * it should end up at, and may be the array's current length to append.
466
+ * This is what a GUI's "add a lane" does. Adding a *cycle* is not here on
467
+ * purpose: it only rewrites each lane's wave string, which is an ordinary
468
+ * `set`.
469
+ *
470
+ * {op:'insert', path, value} with a named last segment
471
+ * Add a member an object does not have yet — `['signal',0,'name']` on a
472
+ * lane written without a name. It goes in after the object's last member,
473
+ * so nothing already written moves.
474
+ *
475
+ * {op:'remove', path}
476
+ * Splice out one whole member — key, colon, value and trailing comma.
477
+ *
478
+ * {op:'move', path, to}
479
+ * Move one array element. `to` is a destination path of the same shape an
480
+ * insert takes — `laneInsertPath` builds one — read against the document
481
+ * as it stands. The element's own bytes are picked up and put down again,
482
+ * so the comment the author wrote beside it travels with it, which a
483
+ * remove plus an insert cannot do because an insert renders a value and a
484
+ * value carries no comment. An array the element leaves with no lane in it
485
+ * goes too, the same rule `removeLane` follows.
486
+ *
487
+ * {op:'replace', path, value}
488
+ * Replace the value at `path` with a fresh serialization, keeping the key,
489
+ * its position among its siblings and the comment beside it. Unlike `set`
490
+ * it may change the value's type and may write a container, because the
491
+ * whole meaning of asking for it is "I intend to replace this wholesale".
492
+ * That is also its cost: a comment written INSIDE the old value is part of
493
+ * what is replaced. `set` stays the safe default and keeps refusing both,
494
+ * so a silent structural rewrite still cannot happen by accident.
495
+ *
496
+ * Refusals carry `rewroteRange`, which says which stretch of the file the edit
497
+ * wanted, so a caller can show the user what it would have had to touch instead
498
+ * of failing silently or rewriting the file behind them.
499
+ *
500
+ * @param {string} text the same source that was handed to parseSource
501
+ * @param {object} parsed a successful parseSource result
502
+ * @param {Array<object>} edits
503
+ * @returns {{ok: true, text: string}
504
+ * |{ok: false, reason: string, rewroteRange: object|null}}
505
+ */
506
+ function patchSource(text, parsed, edits) {
507
+ if (typeof text !== 'string') {
508
+ return refuse('patchSource expects the source text as a string', null);
509
+ }
510
+ if (!parsed || parsed.ok !== true || !(parsed.spans instanceof Map) ||
511
+ !(parsed.members instanceof Map) || !(parsed.leads instanceof Map) ||
512
+ !(parsed.dups instanceof Set)) {
513
+ return refuse('patchSource needs a successful parseSource result from this same module', null);
514
+ }
515
+ if (parsed.source !== text) {
516
+ // Two patches in a row without re-parsing in between is the shape a GUI
517
+ // reaches for, and it is the one input on which silence would be worst:
518
+ // every span past the first edit is off by however much that edit changed
519
+ // the length, so the write lands mid-token and the user's diagram stops
520
+ // parsing. Refusing is the only honest answer — the spans cannot be
521
+ // adjusted from here, because this has no idea which edits produced `text`.
522
+ return refuse(
523
+ 'this parse result came from a different source text; re-run parseSource on ' +
524
+ 'the text being patched, because spans from the old one point at the wrong bytes',
525
+ null);
526
+ }
527
+ if (!Array.isArray(edits)) return refuse('patchSource expects an array of edits', null);
528
+ if (edits.length === 0) return { ok: true, text: text };
529
+
530
+ const ctx = { text: text, parsed: parsed };
531
+ // Two edits naming one path have no defined result: for a value they would
532
+ // fight over the same bytes, and for two inserts at one index the order they
533
+ // land in is whatever the sort happens to do with two ranges that are both
534
+ // empty at the same offset. The overlap test below catches the first because
535
+ // the ranges genuinely overlap, and cannot catch the second because an empty
536
+ // range does not overlap itself — so both are refused here instead, and the
537
+ // module says the same thing about a repeated path whatever the op was.
538
+ const seenPaths = new Set();
539
+ for (const edit of edits) {
540
+ if (!edit || typeof edit !== 'object' || !Array.isArray(edit.path)) continue;
541
+ const key = JSON.stringify(edit.path);
542
+ if (seenPaths.has(key)) {
543
+ return refuse(
544
+ 'the same path ' + key + ' appears in two edits, so which one wins is not defined',
545
+ nearestRange(ctx, edit.path));
546
+ }
547
+ seenPaths.add(key);
548
+ }
549
+ const resolved = [];
550
+ for (const edit of edits) {
551
+ const r = resolveEdit(ctx, edit);
552
+ if (r.ok !== true) return r;
553
+ // A move is one edit and two replacements — the bytes leave one place and
554
+ // land in another — so an edit is allowed to resolve to several, and the
555
+ // overlap test below then sees each of them separately.
556
+ if (Array.isArray(r.parts)) {
557
+ for (const part of r.parts) resolved.push(part);
558
+ } else {
559
+ resolved.push(r);
560
+ }
561
+ }
562
+
563
+ // Back to front, so applying one edit cannot move the spans of the ones that
564
+ // have not been applied yet.
565
+ resolved.sort(function (a, b) { return (b.start - a.start) || (b.end - a.end); });
566
+ for (let k = 1; k < resolved.length; k++) {
567
+ const later = resolved[k - 1];
568
+ const earlier = resolved[k];
569
+ if (earlier.start === later.start) {
570
+ // Two edits aimed at one offset. The overlap test below cannot see this
571
+ // when both are insertions, because an empty range does not overlap
572
+ // anything — and two member inserts on one object have different paths,
573
+ // so the same-path guard does not see it either. Which one goes in first
574
+ // is whatever the sort does with two equal keys, so neither does.
575
+ return refuse(
576
+ 'two edits land at the same place in the file, so which of them goes in first ' +
577
+ 'is not defined',
578
+ { path: earlier.path, spanPath: earlier.path, start: earlier.start, end: later.end });
579
+ }
580
+ if (earlier.end > later.start) {
581
+ return refuse(
582
+ 'two edits cover overlapping ranges, so applying both would not be a local replacement',
583
+ { path: earlier.path, spanPath: earlier.path, start: earlier.start, end: later.end });
584
+ }
585
+ }
586
+
587
+ let out = text;
588
+ for (const r of resolved) out = out.slice(0, r.start) + r.text + out.slice(r.end);
589
+ return { ok: true, text: out };
590
+ }
591
+
592
+ function refuse(reason, rewroteRange) {
593
+ return { ok: false, reason: reason, rewroteRange: rewroteRange };
594
+ }
595
+
596
+ /** Turn one edit into a `{start, end, text}` replacement, or refuse. */
597
+ function resolveEdit(ctx, edit) {
598
+ if (!edit || typeof edit !== 'object') return refuse('each edit must be an object', null);
599
+ if (!Array.isArray(edit.path)) {
600
+ return refuse('each edit needs a path array, the same shape parseSource keys its spans by', null);
601
+ }
602
+ const op = edit.op === undefined ? 'set' : edit.op;
603
+ if (op === 'set') return resolveSet(ctx, edit);
604
+ if (op === 'insert') return resolveInsert(ctx, edit);
605
+ if (op === 'remove') return resolveRemove(ctx, edit);
606
+ if (op === 'move') return resolveMove(ctx, edit);
607
+ if (op === 'replace') return resolveReplace(ctx, edit);
608
+ return refuse('unknown edit op ' + JSON.stringify(edit.op), nearestRange(ctx, edit.path));
609
+ }
610
+
611
+ function resolveSet(ctx, edit) {
612
+ const path = edit.path;
613
+ const span = ctx.parsed.spans.get(JSON.stringify(path));
614
+ if (!span) {
615
+ return refuse(
616
+ 'nothing is parsed at ' + JSON.stringify(path) +
617
+ ', so this is a structural change rather than a local replacement',
618
+ nearestRange(ctx, path));
619
+ }
620
+ const found = lookup(ctx.parsed.doc, path);
621
+ const oldKind = found.found ? kindOf(found.value) : 'missing';
622
+ const newKind = kindOf(edit.value);
623
+ if (newKind === 'undefined') {
624
+ return refuse('a patch will not write undefined into the source',
625
+ rangeOf(path, path, span));
626
+ }
627
+ if (oldKind !== newKind) {
628
+ return refuse(
629
+ 'the new value is a ' + newKind + ' where the source has a ' + oldKind +
630
+ '; changing a value type is a structural change, not a local replacement',
631
+ rangeOf(path, path, span));
632
+ }
633
+ if (newKind === 'object' || newKind === 'array') {
634
+ // A container `set` is a structural rewrite wearing a value replacement's
635
+ // clothes: its span covers the whole subtree, so writing it re-spells every
636
+ // member and drops every comment inside — the one thing this module exists
637
+ // to prevent. Change the fields, or splice with insert and remove.
638
+ return refuse(
639
+ 'setting a whole ' + newKind + ' at ' + JSON.stringify(path) + ' would re-serialise ' +
640
+ 'that subtree and drop any comment or formatting inside it; set the fields ' +
641
+ 'individually, or use insert and remove',
642
+ rangeOf(path, path, span));
643
+ }
644
+ if (sameScalar(found.value, edit.value)) {
645
+ // Nothing changed, so the bytes that are already there are written back
646
+ // unexamined. Without this, a save that touches every field would re-spell
647
+ // literals it has no business touching: the author's `0xff` would come back
648
+ // as `255`, because a number is rendered from its value and not from the
649
+ // text it was written as. It keeps its range rather than dropping out of
650
+ // the batch, so whether a batch is refused as overlapping stays a question
651
+ // about the batch's shape and not about which values happened to match.
652
+ return {
653
+ ok: true, path: path, start: span[0], end: span[1],
654
+ text: ctx.text.slice(span[0], span[1]),
655
+ };
656
+ }
657
+ const quote = ctx.text[span[0]] === "'" ? "'" : '"';
658
+ const ser = serialize(edit.value, quote, MAX_DEPTH - path.length);
659
+ if (ser.ok !== true) return refuse(ser.why, rangeOf(path, path, span));
660
+ return { ok: true, path: path, start: span[0], end: span[1], text: ser.text };
661
+ }
662
+
663
+ /**
664
+ * Swap one value's bytes for a new serialization.
665
+ *
666
+ * `set` refuses a type change and refuses a container, and both refusals earn
667
+ * their keep: they are what stops a caller that meant to change one field from
668
+ * silently re-spelling a subtree and dropping the comments inside it. But the
669
+ * document half of this file can legitimately produce a retype — a lane's `data`
670
+ * moves from the space-separated string to an array the moment an edit needs a
671
+ * blank or space-bearing label — and with only `set` there was no single call
672
+ * that could write it: `set` refused the type change, and remove-plus-insert was
673
+ * refused as two edits on one path. The two-pass route that remained ate the
674
+ * comment beside `data` and moved the key to the end of the lane.
675
+ *
676
+ * So this is the explicit form of the same thing: it names the value, not the
677
+ * member, and it says the retype is intended. The key, its position and the
678
+ * comment beside it are outside the value's span and are never touched.
679
+ */
680
+ function resolveReplace(ctx, edit) {
681
+ const path = edit.path;
682
+ if (!Array.isArray(path) || path.length === 0) {
683
+ return refuse('replace addresses a value, so it needs a path to one', nearestRange(ctx, path));
684
+ }
685
+ const span = ctx.parsed.spans.get(JSON.stringify(path));
686
+ if (!span) {
687
+ return refuse(
688
+ 'nothing is parsed at ' + JSON.stringify(path) + ', so there is no value to replace',
689
+ nearestRange(ctx, path));
690
+ }
691
+ if (kindOf(edit.value) === 'undefined') {
692
+ return refuse('a patch will not write undefined into the source', rangeOf(path, path, span));
693
+ }
694
+ const found = lookup(ctx.parsed.doc, path);
695
+ // Two lines this will not cross, and they are different lines.
696
+ //
697
+ // The first is structural. A value that is, or contains, an object is the
698
+ // document's shape rather than a field of it: `signal`, every group array and
699
+ // every lane. Replacing one re-spells every member it holds and drops whatever
700
+ // was written between them — which is the wall `set` builds by refusing
701
+ // containers, and the op added to get past that wall for `data` must not
702
+ // become the hole in it.
703
+ if (found.found && holdsObject(found.value)) {
704
+ return refuse(
705
+ 'the value at ' + JSON.stringify(path) + ' is or contains an object, so replacing it ' +
706
+ 'would re-serialise part of the document\'s structure; change the fields ' +
707
+ 'individually, or splice with insert and remove',
708
+ rangeOf(path, path, span));
709
+ }
710
+ // The second is textual, and it is where the reachable harm lives. A value
711
+ // written across more than one line, or with a comment inside it, cannot be
712
+ // swapped out without reflowing bytes nobody asked about — a `data` array with
713
+ // a note beside each label is exactly that, and element-wise insert and remove
714
+ // keep those notes where this cannot. The retype this op exists for never
715
+ // reaches it: a string-form `data` is one string literal on one line.
716
+ if (!isInlineValue(ctx, path, found.found ? found.value : null, span)) {
717
+ return refuse(
718
+ 'the value at ' + JSON.stringify(path) + ' runs across lines or has a comment inside ' +
719
+ 'it, so replacing it would rewrite formatting and drop comments; edit its elements ' +
720
+ 'individually instead',
721
+ rangeOf(path, path, span));
722
+ }
723
+ const quote = ctx.text[span[0]] === "'" ? "'" : '"';
724
+ const ser = serialize(edit.value, quote, MAX_DEPTH - path.length);
725
+ if (ser.ok !== true) return refuse(ser.why, rangeOf(path, path, span));
726
+ return { ok: true, path: path, start: span[0], end: span[1], text: ser.text };
727
+ }
728
+
729
+ /** Whether a value is an object, or an array with one anywhere inside it. */
730
+ function holdsObject(value) {
731
+ if (Array.isArray(value)) {
732
+ for (const item of value) {
733
+ if (holdsObject(item)) return true;
734
+ }
735
+ return false;
736
+ }
737
+ return value !== null && typeof value === 'object';
738
+ }
739
+
740
+ /**
741
+ * Whether a value's source text is one line with nothing but the value in it.
742
+ *
743
+ * A scalar is decided by its own span: a literal cannot contain a comment, and
744
+ * this parser rejects a string that spans lines, so there is nothing to look at.
745
+ * An array is decided by the gaps between its elements' spans — the parser has
746
+ * already walked those, so anything there that is not a comma, a space or a tab
747
+ * is a comment or a line break, with no chance of a slash inside a string being
748
+ * mistaken for one.
749
+ */
750
+ function isInlineValue(ctx, path, value, span) {
751
+ if (!Array.isArray(value)) return true;
752
+ let at = span[0] + 1;
753
+ for (let i = 0; i < value.length; i++) {
754
+ const child = ctx.parsed.spans.get(JSON.stringify(path.concat([i])));
755
+ if (!child || !isPlainGap(ctx.text, at, child[0])) return false;
756
+ at = child[1];
757
+ }
758
+ return isPlainGap(ctx.text, at, span[1] - 1);
759
+ }
760
+
761
+ function isPlainGap(text, from, to) {
762
+ for (let j = from; j < to; j++) {
763
+ const c = text[j];
764
+ if (c !== ',' && c !== ' ' && c !== '\t') return false;
765
+ }
766
+ return true;
767
+ }
768
+
769
+ function resolveInsert(ctx, edit) {
770
+ const path = edit.path;
771
+ if (path.length > 0 && typeof path[path.length - 1] === 'string') {
772
+ return resolveInsertMember(ctx, edit);
773
+ }
774
+ if (path.length === 0 || typeof path[path.length - 1] !== 'number') {
775
+ return refuse('insert addresses an array element or a named member, so its path must ' +
776
+ 'end in a numeric index or a key', nearestRange(ctx, path));
777
+ }
778
+ const index = path[path.length - 1];
779
+ const parentPath = path.slice(0, -1);
780
+ const parentSpan = ctx.parsed.spans.get(JSON.stringify(parentPath));
781
+ const parent = lookup(ctx.parsed.doc, parentPath);
782
+ if (!parentSpan || !parent.found || !Array.isArray(parent.value)) {
783
+ return refuse('insert needs an array at ' + JSON.stringify(parentPath),
784
+ nearestRange(ctx, parentPath));
785
+ }
786
+ if (index !== Math.floor(index) || !(index >= 0 && index <= parent.value.length)) {
787
+ return refuse(
788
+ 'insert index ' + index + ' is outside the array at ' + JSON.stringify(parentPath) +
789
+ ', which holds ' + parent.value.length + ' elements',
790
+ rangeOf(path, parentPath, parentSpan));
791
+ }
792
+ if (touchesDuplicateKey(ctx, path)) {
793
+ return refuse(duplicateWhy(path), rangeOf(path, parentPath, parentSpan));
794
+ }
795
+ const ser = serialize(edit.value, dominantQuote(ctx), MAX_DEPTH - path.length);
796
+ if (ser.ok !== true) return refuse(ser.why, rangeOf(path, parentPath, parentSpan));
797
+
798
+ if (parent.value.length === 0) {
799
+ const at = parentSpan[0] + 1;
800
+ // An empty array has no element to copy a layout from. If it was written
801
+ // across lines, the new element takes a line of its own, indented one step
802
+ // in from the closing bracket; two spaces because there is nothing in the
803
+ // file to measure a step from. A one-line array stays on its line.
804
+ const close = lineIndentBefore(ctx.text, parentSpan[1] - 1);
805
+ if (close !== null) {
806
+ const eol = lineBreakBefore(ctx.text, parentSpan[1] - 1);
807
+ return { ok: true, path: path, start: at, end: at, text: eol + close + ' ' + ser.text };
808
+ }
809
+ return { ok: true, path: path, start: at, end: at, text: ser.text };
810
+ }
811
+ if (index < parent.value.length) {
812
+ const member = ctx.parsed.members.get(JSON.stringify(parentPath.concat([index])));
813
+ if (!member) {
814
+ return refuse('no member span for ' + JSON.stringify(path), rangeOf(path, parentPath, parentSpan));
815
+ }
816
+ // Go in above whatever comments are written over that member, not between
817
+ // them and it — otherwise the new lane silently inherits the note the
818
+ // author wrote about the old one. Same rule the end of the array uses.
819
+ const lead = ctx.parsed.leads.get(JSON.stringify(parentPath.concat([index])));
820
+ const at = commentRunStart(ctx.text, lead === undefined ? member[0] : lead, member[0]);
821
+ const indent = lineIndentBefore(ctx.text, at);
822
+ const gap = indent === null ? ' ' : lineBreakBefore(ctx.text, at) + indent;
823
+ return { ok: true, path: path, start: at, end: at, text: ser.text + ',' + gap };
824
+ }
825
+ const last = ctx.parsed.members.get(JSON.stringify(parentPath.concat([index - 1])));
826
+ if (!last) {
827
+ return refuse('no member span for ' + JSON.stringify(parentPath.concat([index - 1])),
828
+ rangeOf(path, parentPath, parentSpan));
829
+ }
830
+ return appendAfterMember(ctx, path, last, ser.text);
831
+ }
832
+
833
+ /**
834
+ * Put `body` in as a new last member of the container whose current last member
835
+ * is `last`.
836
+ *
837
+ * It goes after whatever that member has left on its own line, so a comment
838
+ * written beside it keeps describing it instead of drifting onto the new one.
839
+ * When that member had no trailing comma the comma has to go in front of the
840
+ * trivia rather than after it — a comma appended after a line comment would be
841
+ * swallowed by the comment and the container would no longer parse — so the
842
+ * trivia is re-emitted, byte for byte, behind it.
843
+ */
844
+ function appendAfterMember(ctx, path, last, body) {
845
+ const indent = lineIndentBefore(ctx.text, last[0]);
846
+ const gap = indent === null ? ' ' : lineBreakBefore(ctx.text, last[0]) + indent;
847
+ const comma = ctx.text[last[1] - 1] === ',' ? '' : ',';
848
+ const at = endOfTrailingTrivia(ctx.text, last[1], true);
849
+ const kept = ctx.text.slice(last[1], at);
850
+ return { ok: true, path: path, start: last[1], end: at, text: comma + kept + gap + body };
851
+ }
852
+
853
+ /**
854
+ * Add a member an object does not have yet.
855
+ *
856
+ * A GUI reaches this the moment it names a lane the author wrote without a
857
+ * name, which is legal wavedrom and common for spacers. Without it the only way
858
+ * to write that rename back is to remove the whole lane and re-insert it, which
859
+ * re-serialises the lane and drops the comment beside it — so the smallest
860
+ * honest edit is this one: the new member goes after the object's last, and not
861
+ * a byte of what is already there moves.
862
+ */
863
+ function resolveInsertMember(ctx, edit) {
864
+ const path = edit.path;
865
+ const key = path[path.length - 1];
866
+ const parentPath = path.slice(0, -1);
867
+ const parentSpan = ctx.parsed.spans.get(JSON.stringify(parentPath));
868
+ const parent = lookup(ctx.parsed.doc, parentPath);
869
+ if (!parentSpan || !parent.found || parent.value === null ||
870
+ typeof parent.value !== 'object' || Array.isArray(parent.value)) {
871
+ return refuse('insert with a named key needs an object at ' + JSON.stringify(parentPath),
872
+ nearestRange(ctx, parentPath));
873
+ }
874
+ if (Object.prototype.hasOwnProperty.call(parent.value, key)) {
875
+ return refuse(JSON.stringify(path) + ' already has a member there, so this is a set ' +
876
+ 'rather than an insert', rangeOf(path, parentPath, parentSpan));
877
+ }
878
+ if (touchesDuplicateKey(ctx, parentPath)) {
879
+ return refuse(duplicateWhy(path), rangeOf(path, parentPath, parentSpan));
880
+ }
881
+ const ser = serialize(edit.value, dominantQuote(ctx), MAX_DEPTH - path.length);
882
+ if (ser.ok !== true) return refuse(ser.why, rangeOf(path, parentPath, parentSpan));
883
+ const body = renderKey(key, '"') + ': ' + ser.text;
884
+ const keys = Object.keys(parent.value);
885
+ if (keys.length === 0) {
886
+ const at = parentSpan[0] + 1;
887
+ const close = lineIndentBefore(ctx.text, parentSpan[1] - 1);
888
+ if (close !== null) {
889
+ const eol = lineBreakBefore(ctx.text, parentSpan[1] - 1);
890
+ return { ok: true, path: path, start: at, end: at, text: eol + close + ' ' + body };
891
+ }
892
+ return { ok: true, path: path, start: at, end: at, text: body };
893
+ }
894
+ const lastPath = parentPath.concat([keys[keys.length - 1]]);
895
+ const last = ctx.parsed.members.get(JSON.stringify(lastPath));
896
+ if (!last) {
897
+ return refuse('no member span for ' + JSON.stringify(lastPath),
898
+ rangeOf(path, parentPath, parentSpan));
899
+ }
900
+ return appendAfterMember(ctx, path, last, body);
901
+ }
902
+
903
+ /**
904
+ * Move one array element, bytes and all.
905
+ *
906
+ * Written as a remove plus an insert, a move loses whatever the author put
907
+ * beside the element: `commentRunStart` gives the comment to that member, so
908
+ * the remove takes it away, and the insert renders a value, which carries no
909
+ * comment. The member span table exists so the member's own text can be picked
910
+ * up and put down again instead — one deletion where it was, one insertion of
911
+ * those same bytes where it goes, and the comment travels because it is part of
912
+ * the bytes.
913
+ *
914
+ * `to` is the index the element should end up at once it has been lifted out,
915
+ * the counting `moveLane` and `Array.prototype.splice` both use.
916
+ */
917
+ function resolveMove(ctx, edit) {
918
+ const path = edit.path;
919
+ if (path.length === 0 || typeof path[path.length - 1] !== 'number') {
920
+ return refuse('move addresses an array element, so its path must end in a numeric index',
921
+ nearestRange(ctx, path));
922
+ }
923
+ const member = ctx.parsed.members.get(JSON.stringify(path));
924
+ if (!member) {
925
+ return refuse('nothing is parsed at ' + JSON.stringify(path) + ', so there is no member to move',
926
+ nearestRange(ctx, path));
927
+ }
928
+ const to = edit.to;
929
+ if (!Array.isArray(to) || to.length === 0 || typeof to[to.length - 1] !== 'number') {
930
+ return refuse('move needs a destination path ending in a numeric index, the same shape ' +
931
+ 'insert takes; laneInsertPath builds one', rangeOf(path, path, member));
932
+ }
933
+ const destParentPath = to.slice(0, -1);
934
+ const destIndex = to[to.length - 1];
935
+ const destParent = lookup(ctx.parsed.doc, destParentPath);
936
+ const destParentSpan = ctx.parsed.spans.get(JSON.stringify(destParentPath));
937
+ if (!destParentSpan || !destParent.found || !Array.isArray(destParent.value) ||
938
+ !isBound(destIndex, destParent.value.length)) {
939
+ return refuse('move destination ' + JSON.stringify(to) +
940
+ ' is not a position in an array', rangeOf(path, path, member));
941
+ }
942
+ if (touchesDuplicateKey(ctx, path) || touchesDuplicateKey(ctx, destParentPath)) {
943
+ return refuse(duplicateWhy(path), rangeOf(path, path, member));
944
+ }
945
+ const here = path.slice(0, -1);
946
+ const index = path[path.length - 1];
947
+ if (JSON.stringify(destParentPath) === JSON.stringify(here) &&
948
+ (destIndex === index || destIndex === index + 1)) {
949
+ // Landing where it already is. The bytes are written back unexamined, and
950
+ // the range is kept so that whether a batch overlaps stays a question about
951
+ // the batch and not about which move happened to be a no-op.
952
+ return { ok: true, path: path, start: member[0], end: member[1],
953
+ text: ctx.text.slice(member[0], member[1]) };
954
+ }
955
+ const lead = ctx.parsed.leads.get(JSON.stringify(path));
956
+ const leadStart = lead === undefined ? member[0] : lead;
957
+ const runStart = commentRunStart(ctx.text, leadStart, member[0]);
958
+ const runEnd = endOfTrailingTrivia(ctx.text, member[1], true);
959
+ const hadComma = ctx.text[member[1] - 1] === ',';
960
+ // The element's own bytes, with a comma guaranteed after the value: it may be
961
+ // landing somewhere that is no longer the end of the array. A trailing comma
962
+ // is legal here and in the engine's own reader, so one is safe wherever it
963
+ // ends up. The comment that follows on the same line comes along behind it.
964
+ const payload = ctx.text.slice(runStart, member[1]) + (hadComma ? '' : ',') +
965
+ ctx.text.slice(member[1], runEnd);
966
+ // What leaves is not always the element: an array the element leaves with no
967
+ // lane in it has nothing left to draw and no position that resolves inside it,
968
+ // so it goes too. `removeLane` reads the same function, which is what keeps
969
+ // the two ends of a move agreeing about how much came out.
970
+ const cutPath = emptiedMemberPath(ctx.parsed.doc, path);
971
+ const cutMember = ctx.parsed.members.get(JSON.stringify(cutPath));
972
+ if (!cutMember) {
973
+ return refuse('no member span for ' + JSON.stringify(cutPath), rangeOf(path, path, member));
974
+ }
975
+ const cutLead = ctx.parsed.leads.get(JSON.stringify(cutPath));
976
+ const cut = removalRange(ctx.text, cutMember[0], cutMember[1],
977
+ cutLead === undefined ? cutMember[0] : cutLead);
978
+ const drop = destParent.value.length === 0
979
+ ? intoEmptyArray(ctx, destParentSpan, payload)
980
+ : (destIndex >= destParent.value.length
981
+ ? appendPayload(ctx, destParentPath, destParent.value.length - 1, payload)
982
+ : beforePayload(ctx, destParentPath, destIndex, payload));
983
+ if (drop.ok !== true) return refuse(drop.why, rangeOf(path, destParentPath, destParentSpan));
984
+ if (drop.at >= cut[0] && drop.at < cut[1]) {
985
+ return refuse('the move would land inside the bytes it is taking out',
986
+ rangeOf(path, path, member));
987
+ }
988
+ return {
989
+ ok: true,
990
+ path: path,
991
+ parts: [
992
+ { ok: true, path: path, start: cut[0], end: cut[1], text: '' },
993
+ { ok: true, path: path, start: drop.at, end: drop.end, text: drop.text },
994
+ ],
995
+ };
996
+ }
997
+
998
+ /** Where a moved member's bytes go when the destination array is empty. */
999
+ function intoEmptyArray(ctx, parentSpan, payload) {
1000
+ const at = parentSpan[0] + 1;
1001
+ const close = lineIndentBefore(ctx.text, parentSpan[1] - 1);
1002
+ if (close !== null) {
1003
+ const eol = lineBreakBefore(ctx.text, parentSpan[1] - 1);
1004
+ return { ok: true, at: at, end: at, text: eol + close + ' ' + payload };
1005
+ }
1006
+ return { ok: true, at: at, end: at, text: payload };
1007
+ }
1008
+
1009
+ /** Where a moved member's bytes go when they land in front of another member. */
1010
+ function beforePayload(ctx, parentPath, index, payload) {
1011
+ const memberPath = JSON.stringify(parentPath.concat([index]));
1012
+ const member = ctx.parsed.members.get(memberPath);
1013
+ if (!member) return { ok: false, why: 'no member span for ' + memberPath };
1014
+ const lead = ctx.parsed.leads.get(memberPath);
1015
+ const at = commentRunStart(ctx.text, lead === undefined ? member[0] : lead, member[0]);
1016
+ const indent = lineIndentBefore(ctx.text, at);
1017
+ const gap = indent === null ? ' ' : lineBreakBefore(ctx.text, at) + indent;
1018
+ return { ok: true, at: at, end: at, text: payload + gap };
1019
+ }
1020
+
1021
+ /** Where they go when they land at the end of the array. */
1022
+ function appendPayload(ctx, parentPath, index, payload) {
1023
+ const memberPath = JSON.stringify(parentPath.concat([index]));
1024
+ const last = ctx.parsed.members.get(memberPath);
1025
+ if (!last) return { ok: false, why: 'no member span for ' + memberPath };
1026
+ const part = appendAfterMember(ctx, parentPath, last, payload);
1027
+ return { ok: true, at: part.start, end: part.end, text: part.text };
1028
+ }
1029
+
1030
+ function resolveRemove(ctx, edit) {
1031
+ const path = edit.path;
1032
+ if (path.length === 0) {
1033
+ return refuse('the whole document is not a member that can be removed', nearestRange(ctx, path));
1034
+ }
1035
+ const member = ctx.parsed.members.get(JSON.stringify(path));
1036
+ if (!member) {
1037
+ return refuse('nothing is parsed at ' + JSON.stringify(path) + ', so there is no member to remove',
1038
+ nearestRange(ctx, path));
1039
+ }
1040
+ if (touchesDuplicateKey(ctx, path)) {
1041
+ return refuse(duplicateWhy(path), rangeOf(path, path, member));
1042
+ }
1043
+ const lead = ctx.parsed.leads.get(JSON.stringify(path));
1044
+ const range = removalRange(ctx.text, member[0], member[1],
1045
+ lead === undefined ? member[0] : lead);
1046
+ return { ok: true, path: path, start: range[0], end: range[1], text: '' };
1047
+ }
1048
+
1049
+ /**
1050
+ * A key written twice in the same object is legal WaveJSON and the later one
1051
+ * wins, which leaves the earlier one's subtree still holding spans that the doc
1052
+ * says nothing about: `{a: {b: 1}, a: 2}` has a span at ["a","b"] while `doc.a`
1053
+ * is `2`. Writing a value through a span is still right, because the span at a
1054
+ * path and the value at that path were recorded by the same pass and describe
1055
+ * the same occurrence. Splicing a member out is not: the member span at ["a"]
1056
+ * describes only the second one, so removing it would leave the first behind
1057
+ * and the key would still be there. So a member-level edit refuses as soon as
1058
+ * the path, or any path above it, is one of the repeated keys.
1059
+ */
1060
+ function touchesDuplicateKey(ctx, path) {
1061
+ for (let n = 1; n <= path.length; n++) {
1062
+ if (ctx.parsed.dups.has(JSON.stringify(path.slice(0, n)))) return true;
1063
+ }
1064
+ return false;
1065
+ }
1066
+
1067
+ function duplicateWhy(path) {
1068
+ return JSON.stringify(path) + ' sits under a duplicate key, where a member span ' +
1069
+ 'describes only one of the occurrences, so splicing there would leave the other behind';
1070
+ }
1071
+
1072
+ /**
1073
+ * The horizontal whitespace between the start of `at`'s line and `at`, or null
1074
+ * when something other than whitespace is in front of it on that line. An
1075
+ * inserted element copies this so it lands on its own line, indented like its
1076
+ * neighbour, without any existing byte being rewritten.
1077
+ */
1078
+ function lineIndentBefore(text, at) {
1079
+ let s = at;
1080
+ while (s > 0 && (text[s - 1] === ' ' || text[s - 1] === '\t')) s--;
1081
+ if (s > 0 && text[s - 1] !== '\n') return null;
1082
+ return text.slice(s, at);
1083
+ }
1084
+
1085
+ /**
1086
+ * The line terminator that ended the line before `at` — the one an inserted
1087
+ * line has to match. Writing a bare newline into a file the author keeps in
1088
+ * CRLF leaves it with mixed endings, which is the same kind of harm as
1089
+ * reindenting it: a change to bytes nobody asked to change.
1090
+ */
1091
+ function lineBreakBefore(text, at) {
1092
+ let s = at;
1093
+ while (s > 0 && (text[s - 1] === ' ' || text[s - 1] === '\t')) s--;
1094
+ if (s >= 2 && text[s - 1] === '\n' && text[s - 2] === '\r') return '\r\n';
1095
+ return '\n';
1096
+ }
1097
+
1098
+ /**
1099
+ * Where the run of comments that belongs to a member begins, or `memberStart`
1100
+ * when none does.
1101
+ *
1102
+ * This is the whole of the comment-ownership rule, and the rule is one
1103
+ * sentence: **a comment belongs to the member it follows on the same line, or —
1104
+ * when it stands on its own line or lines — to the member immediately below it,
1105
+ * provided no blank line comes between.** Insert and remove both read it from
1106
+ * here, which is what keeps them from disagreeing about who owns a given note:
1107
+ * an insert goes in above the run so the note keeps describing the member it
1108
+ * was written for, and a remove takes the run with it so the note does not
1109
+ * outlive its subject and start reading as a description of the next member.
1110
+ *
1111
+ * It is decided by reading `[leadStart, memberStart)` — the trivia the parser
1112
+ * walked over on its way to this member — forwards, one comment at a time.
1113
+ * Reading it backwards a line at a time is what the earlier attempt did, and it
1114
+ * could not see a block comment spanning more than one line: its closing
1115
+ * line starts with a star, not a slash, so the run ended there and the comment
1116
+ * was handed to the wrong member. Forwards, a comment of any shape is one
1117
+ * token, so there is no line shape left to get wrong.
1118
+ *
1119
+ * The one guess left is the first member of a container: a heading written
1120
+ * immediately above it, with no blank line, is taken as that member's and
1121
+ * travels with it when it is deleted. A blank line is how an author says
1122
+ * otherwise, and that is the same signal on both operations.
1123
+ */
1124
+ function commentRunStart(text, leadStart, memberStart) {
1125
+ if (!(leadStart >= 0) || leadStart > memberStart) return memberStart;
1126
+ let j = leadStart;
1127
+ let runStart = -1;
1128
+ let lastEnd = -1;
1129
+ while (j < memberStart) {
1130
+ const c = text[j];
1131
+ if (c === undefined) break;
1132
+ if (SPACE[c] === 1) { j++; continue; }
1133
+ const start = j;
1134
+ if (c === '/' && text[j + 1] === '/') {
1135
+ while (j < memberStart && text[j] !== '\n' && text[j] !== '\r') j++;
1136
+ } else if (c === '/' && text[j + 1] === '*') {
1137
+ const close = text.indexOf('*/', j + 2);
1138
+ if (close === -1 || close + 2 > memberStart) break;
1139
+ j = close + 2;
1140
+ } else {
1141
+ break; // not trivia; the parser says this cannot happen
1142
+ }
1143
+ if (lineBreaksBetween(text, leadStart, start) === 0) {
1144
+ // It starts on the line the previous member (or the opening delimiter)
1145
+ // ended on, so it follows that and not this one.
1146
+ lastEnd = j;
1147
+ continue;
1148
+ }
1149
+ if (runStart === -1 || lineBreaksBetween(text, lastEnd, start) >= 2) runStart = start;
1150
+ lastEnd = j;
1151
+ }
1152
+ if (runStart === -1) return memberStart;
1153
+ // Exactly one line break between the run and the member: no blank line, and
1154
+ // the run is not sitting on the member's own line in front of it.
1155
+ return lineBreaksBetween(text, lastEnd, memberStart) === 1 ? runStart : memberStart;
1156
+ }
1157
+
1158
+ /** How many lines `text[from..to)` ends, counting CRLF once. Stops counting at 2. */
1159
+ function lineBreaksBetween(text, from, to) {
1160
+ let n = 0;
1161
+ for (let j = from; j < to && n < 2; j++) {
1162
+ const c = text[j];
1163
+ if (c === '\r') {
1164
+ n++;
1165
+ if (text[j + 1] === '\n') j++;
1166
+ } else if (c === '\n') {
1167
+ n++;
1168
+ } else {
1169
+ const code = c.charCodeAt(0);
1170
+ if (code === 0x2028 || code === 0x2029) n++; // the other two line terminators
1171
+ }
1172
+ }
1173
+ return n;
1174
+ }
1175
+
1176
+ /**
1177
+ * How much of a removal is the member's own line rather than the member itself,
1178
+ * and what to take with it.
1179
+ *
1180
+ * A member that had its line to itself takes the whole line: its indentation,
1181
+ * the comment that follows it on that line, the comments written above it, and
1182
+ * the line break. Leaving any of that behind is what orphans a note against the
1183
+ * wrong lane, with the deleted lane's indentation fused onto it. A comment
1184
+ * that follows the member on its line but runs on past it goes too — it is
1185
+ * that member's by the same sentence `commentRunStart` states, and the insert
1186
+ * side already treats it that way, so the two would otherwise disagree about
1187
+ * who owns it.
1188
+ *
1189
+ * A member sharing its line with others takes only itself plus the horizontal
1190
+ * whitespace that followed it, so a one-line array closes up instead of keeping
1191
+ * the gap where the element used to be.
1192
+ */
1193
+ function removalRange(text, start, end, leadStart) {
1194
+ const withComment = endOfTrailingTrivia(text, end, true);
1195
+ const e = withComment > end ? withComment : end;
1196
+ const s = commentRunStart(text, leadStart, start);
1197
+
1198
+ let head = s;
1199
+ while (head > 0 && (text[head - 1] === ' ' || text[head - 1] === '\t')) head--;
1200
+ const ownsTheLine = head === 0 || text[head - 1] === '\n';
1201
+
1202
+ let tail = e;
1203
+ while (tail < text.length && (text[tail] === ' ' || text[tail] === '\t')) tail++;
1204
+ if (ownsTheLine && (text[tail] === '\n' || text[tail] === '\r')) {
1205
+ if (text[tail] === '\r') tail++;
1206
+ if (text[tail] === '\n') tail++;
1207
+ return [head, tail];
1208
+ }
1209
+ return [s, tail];
1210
+ }
1211
+
1212
+ /** True when `text[from..to)` holds any character the language ends a line at. */
1213
+ function hasLineBreakBetween(text, from, to) {
1214
+ for (let j = from; j < to; j++) {
1215
+ if (isFormatBreak(text[j]) || text[j] === '\n' || text[j] === '\r') return true;
1216
+ }
1217
+ return false;
1218
+ }
1219
+
1220
+ /**
1221
+ * DEL, and the two separators the language counts as line terminators inside a
1222
+ * string. They are written as code points rather than as themselves so that
1223
+ * nothing invisible sits in this file.
1224
+ */
1225
+ function isFormatBreak(ch) {
1226
+ if (ch === undefined) return false;
1227
+ const code = ch.charCodeAt(0);
1228
+ return code === 0x7f || code === 0x2028 || code === 0x2029;
1229
+ }
1230
+
1231
+ /**
1232
+ * An unpaired surrogate. `quoteString` iterates by code point, so a character
1233
+ * still in the surrogate range here is one that has no partner: writing it out
1234
+ * as itself survives an in-memory round trip but not the trip through UTF-8
1235
+ * that saving the file is, so it is escaped like the other characters that
1236
+ * cannot be written literally.
1237
+ */
1238
+ function isLoneSurrogate(ch) {
1239
+ const code = ch.charCodeAt(0);
1240
+ return code >= 0xd800 && code <= 0xdfff && ch.length === 1;
1241
+ }
1242
+
1243
+ /**
1244
+ * Walk forward from `from` over the trivia that belongs to whatever just ended —
1245
+ * horizontal whitespace and comments — and answer where the line break is.
1246
+ * Anything else, such as the closing bracket of a one-line array, answers
1247
+ * `from`, so nothing is assumed about text this has not accounted for.
1248
+ *
1249
+ * `acrossLines` says whether a block comment may carry the scan onto later
1250
+ * lines. Both callers want that, for the same reason: a comment written after
1251
+ * an element describes that element however many lines it takes, so an insert
1252
+ * that landed in front of it would hand it to the new element, and a remove
1253
+ * that left it behind would hand it to the next one.
1254
+ */
1255
+ function endOfTrailingTrivia(text, from, acrossLines) {
1256
+ let j = from;
1257
+ for (;;) {
1258
+ const c = text[j];
1259
+ if (c === ' ' || c === '\t') { j++; continue; }
1260
+ if (c === '\n' || c === '\r') return j;
1261
+ if (c === '/' && text[j + 1] === '/') {
1262
+ while (j < text.length && text[j] !== '\n' && text[j] !== '\r') j++;
1263
+ return j;
1264
+ }
1265
+ if (c === '/' && text[j + 1] === '*') {
1266
+ const close = text.indexOf('*/', j + 2);
1267
+ if (close === -1) return from;
1268
+ if (!acrossLines && hasLineBreakBetween(text, j, close)) return from;
1269
+ j = close + 2;
1270
+ continue;
1271
+ }
1272
+ return from;
1273
+ }
1274
+ }
1275
+
1276
+ /** Walk `doc` down `path`, distinguishing "absent" from "present and undefined". */
1277
+ function lookup(doc, path) {
1278
+ let cur = doc;
1279
+ for (let k = 0; k < path.length; k++) {
1280
+ if (cur === null || typeof cur !== 'object') return { found: false };
1281
+ const seg = path[k];
1282
+ if (Array.isArray(cur)) {
1283
+ if (typeof seg !== 'number' || !(seg >= 0 && seg < cur.length)) return { found: false };
1284
+ } else if (typeof seg !== 'string' || !Object.prototype.hasOwnProperty.call(cur, seg)) {
1285
+ return { found: false };
1286
+ }
1287
+ cur = cur[seg];
1288
+ }
1289
+ return { found: true, value: cur };
1290
+ }
1291
+
1292
+ /**
1293
+ * Whether two scalars are the same value for the purpose of "did this edit
1294
+ * change anything". NaN counts as equal to itself, and negative zero as equal
1295
+ * to zero — the difference has no meaning in a waveform, and treating them as
1296
+ * different would mean rewriting the author's literal to say the same thing.
1297
+ * Containers never reach this: a container set is refused before it gets here.
1298
+ */
1299
+ function sameScalar(a, b) {
1300
+ if (a === b) return true;
1301
+ return Number.isNaN(a) && Number.isNaN(b);
1302
+ }
1303
+
1304
+ function kindOf(value) {
1305
+ if (value === null) return 'null';
1306
+ if (Array.isArray(value)) return 'array';
1307
+ const t = typeof value;
1308
+ return t === 'object' ? 'object' : t;
1309
+ }
1310
+
1311
+ function rangeOf(path, spanPath, span) {
1312
+ return { path: path, spanPath: spanPath, start: span[0], end: span[1] };
1313
+ }
1314
+
1315
+ /**
1316
+ * The quote character this source spells its strings with, by counting them.
1317
+ *
1318
+ * A REPLACED value inherits the quote of the span it is replacing — that is
1319
+ * `resolveSet` a few hundred lines up, and it is exact. A value that is being
1320
+ * INSERTED has no span to inherit from, and writing it with a hard-coded `"`
1321
+ * put `{ name: "rst", … }` into files whose every other line reads
1322
+ * `{ name: 'clk', … }`. The document is still correct and still parses; it is
1323
+ * simply not the file the author wrote, and this module's whole point is that
1324
+ * the bytes it does not have to touch stay the bytes the author wrote.
1325
+ *
1326
+ * Counted from the PARSE rather than from the raw text, so a `"` inside a
1327
+ * single-quoted string or a comment cannot vote. Ties, and a source with no
1328
+ * strings in it at all, go to `"` — which is what the inserts used
1329
+ * unconditionally before and is JSON's own spelling.
1330
+ */
1331
+ function dominantQuote(ctx) {
1332
+ let single = 0;
1333
+ let double = 0;
1334
+ for (const span of ctx.parsed.spans.values()) {
1335
+ const ch = ctx.text[span[0]];
1336
+ if (ch === "'") single++;
1337
+ else if (ch === '"') double++;
1338
+ }
1339
+ return single > double ? "'" : '"';
1340
+ }
1341
+
1342
+ /** The range of the closest ancestor that does have a span. The root always has one. */
1343
+ function nearestRange(ctx, path) {
1344
+ const p = Array.isArray(path) ? path : [];
1345
+ for (let n = p.length; n >= 0; n--) {
1346
+ const sub = p.slice(0, n);
1347
+ const span = ctx.parsed.spans.get(JSON.stringify(sub));
1348
+ if (span) return rangeOf(p, sub, span);
1349
+ }
1350
+ return null;
1351
+ }
1352
+
1353
+ /**
1354
+ * Render a value as WaveJSON source text. `quote` is the quote character the
1355
+ * span being replaced already used, so a single-quoted value stays single
1356
+ * quoted; the other quote character is then left alone inside the string rather
1357
+ * than escaped, because it needs no escaping there.
1358
+ *
1359
+ * A value with no WaveJSON spelling — undefined, and the other host-only value
1360
+ * kinds — comes back as a refusal instead of being written out as the bare word
1361
+ * undefined, which would read back as something the editor never asked for.
1362
+ *
1363
+ * `budget` is how many containers may be opened below this point: the reading
1364
+ * half of this file stops at `MAX_DEPTH`, so a value written at a path that
1365
+ * already uses some of that may only have the rest. Anything deeper is refused
1366
+ * rather than written, because it would produce a file this very module then
1367
+ * refuses to read. A value that contains itself is refused too, caught on the
1368
+ * stack of containers currently being written rather than by running out of
1369
+ * actual stack. Both matter because the value arrives from an editor's own
1370
+ * model, where a lane pointing back at the document it belongs to is an
1371
+ * ordinary shape — and so is a Date, which is why an object that is not a plain
1372
+ * one is refused as well instead of being written out as `{}`.
1373
+ */
1374
+ function serialize(value, quote, budget) {
1375
+ return writeValue(value, quote, budget, budget, []);
1376
+ }
1377
+
1378
+ /** The recursive half of `serialize`, carrying the depth budget and the stack. */
1379
+ function writeValue(value, quote, depthLeft, budget, stack) {
1380
+ const kind = kindOf(value);
1381
+ if (kind === 'string') return { ok: true, text: quoteString(value, quote) };
1382
+ if (kind === 'number') {
1383
+ if (Number.isNaN(value)) return { ok: true, text: 'NaN' };
1384
+ if (value === Infinity) return { ok: true, text: 'Infinity' };
1385
+ if (value === -Infinity) return { ok: true, text: '-Infinity' };
1386
+ return { ok: true, text: String(value) };
1387
+ }
1388
+ if (kind === 'boolean') return { ok: true, text: String(value) };
1389
+ if (kind === 'null') return { ok: true, text: 'null' };
1390
+ if (kind === 'array' || kind === 'object') {
1391
+ if (kind === 'object' && !isPlainObject(value)) {
1392
+ // A Date, Map, Set, RegExp or class instance answers `Object.keys` with an
1393
+ // empty list, so writing it would emit `{}` and report success — data loss
1394
+ // wearing a successful return, which is the one outcome this module exists
1395
+ // to prevent. It has no WaveJSON spelling, so it is refused like the other
1396
+ // host-only values.
1397
+ return { ok: false, why: 'the value holds something that is not a plain object, ' +
1398
+ 'which has no WaveJSON spelling of its own and would be written as an empty ' +
1399
+ 'object, so nothing was written' };
1400
+ }
1401
+ if (stack.indexOf(value) !== -1) {
1402
+ return { ok: false, why: 'the value contains a cycle — it holds itself, ' +
1403
+ 'which has no WaveJSON spelling, so nothing was written' };
1404
+ }
1405
+ if (!(depthLeft >= 1)) {
1406
+ return { ok: false, why: 'the value is nested too deeply: more than the ' + budget +
1407
+ ' levels left at this path (' + MAX_DEPTH + ' is all this module will read back, ' +
1408
+ 'and the path itself uses ' + (MAX_DEPTH - budget) + '), so nothing was written' };
1409
+ }
1410
+ stack.push(value);
1411
+ let out;
1412
+ if (kind === 'array') {
1413
+ const parts = [];
1414
+ for (const item of value) {
1415
+ const ser = writeValue(item, quote, depthLeft - 1, budget, stack);
1416
+ if (ser.ok !== true) { stack.pop(); return ser; }
1417
+ parts.push(ser.text);
1418
+ }
1419
+ out = '[' + parts.join(', ') + ']';
1420
+ } else {
1421
+ const parts = [];
1422
+ for (const key of Object.keys(value)) {
1423
+ if (key === PROTO_KEY) {
1424
+ // There is no spelling of this key that stores it as data and reads
1425
+ // back. Measured against this host: inside the engine's reader, which
1426
+ // runs WaveJSON as source text, `__proto__: v`, `"__proto__": v`
1427
+ // and `'__proto__': v` all set the loaded object's prototype rather
1428
+ // than adding a member — quoting changes nothing. Only a computed key
1429
+ // stores it, and this file's own parser cannot read that back. So it
1430
+ // is refused like the other values with no WaveJSON spelling, rather
1431
+ // than written as something that reads back as a different thing. The
1432
+ // reading half already neutralises it on the way in, with `assign`.
1433
+ stack.pop();
1434
+ return { ok: false, why: 'the value has a ' + PROTO_KEY + ' key, which has no ' +
1435
+ 'WaveJSON spelling that stores it as data — every spelling of it sets the ' +
1436
+ 'prototype of the object that reads it back, quoted or not — so nothing was written' };
1437
+ }
1438
+ const ser = writeValue(value[key], quote, depthLeft - 1, budget, stack);
1439
+ if (ser.ok !== true) { stack.pop(); return ser; }
1440
+ parts.push(renderKey(key, quote) + ': ' + ser.text);
1441
+ }
1442
+ out = parts.length === 0 ? '{}' : '{ ' + parts.join(', ') + ' }';
1443
+ }
1444
+ stack.pop();
1445
+ return { ok: true, text: out };
1446
+ }
1447
+ return { ok: false, why: 'a value of kind ' + kind + ' has no WaveJSON spelling, so nothing was written' };
1448
+ }
1449
+
1450
+ /**
1451
+ * An object literal's worth of object: one whose prototype is the ordinary one,
1452
+ * or none at all. Anything else carries its content somewhere `Object.keys`
1453
+ * cannot see it.
1454
+ */
1455
+ function isPlainObject(value) {
1456
+ const proto = Object.getPrototypeOf(value);
1457
+ return proto === null || proto === Object.prototype;
1458
+ }
1459
+
1460
+ const BARE_KEY_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1461
+
1462
+ /** Spelled out of its own characters so that nothing in this file carries it literally. */
1463
+ const PROTO_KEY = '__' + 'proto' + '__';
1464
+
1465
+ function renderKey(key, quote) {
1466
+ return BARE_KEY_RE.test(key) ? key : quoteString(key, quote);
1467
+ }
1468
+
1469
+ function quoteString(s, quote) {
1470
+ let out = quote;
1471
+ for (const ch of s) {
1472
+ if (ch === quote || ch === '\\') { out += '\\' + ch; continue; }
1473
+ if (ch === '\n') { out += '\\n'; continue; }
1474
+ if (ch === '\r') { out += '\\r'; continue; }
1475
+ if (ch === '\t') { out += '\\t'; continue; }
1476
+ if (ch === '\b') { out += '\\b'; continue; }
1477
+ if (ch === '\f') { out += '\\f'; continue; }
1478
+ if (ch < ' ' || isFormatBreak(ch) || isLoneSurrogate(ch)) {
1479
+ out += '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0');
1480
+ continue;
1481
+ }
1482
+ out += ch;
1483
+ }
1484
+ return out + quote;
1485
+ }
1486
+
1487
+ /**
1488
+ * The characters a wave string is made of, and what each one means for the
1489
+ * picture.
1490
+ *
1491
+ * `.` is the one that decides the shape of everything below it, and it does NOT
1492
+ * mean "the same character again". Measured against the pinned wavedrom 3.5.0,
1493
+ * over the 19 level characters `01xzpnhlud=23456789`: `ch..ch` and `ch...`
1494
+ * render the same for exactly two of them, the clocks `p` and `n`. For the
1495
+ * other 17 the doubled form puts a transition marker at that cycle where the
1496
+ * dotted form draws a plain hold — `0..0` emits `#0m0` where `0...` emits
1497
+ * `#000`, and `h..h` emits `#pclk` where `h...` emits `#111`. So a model that
1498
+ * resolved `.` down to the character it continues would be throwing away
1499
+ * something the reader can see, and `collapseWave(expandWave(w)) === w` would
1500
+ * be false for 17 characters out of 19.
1501
+ *
1502
+ * `|` is a gap drawn over whatever level is current. It carries no level of its
1503
+ * own, so a `.` behind it continues the level from in front of it — measured,
1504
+ * `0|.` draws 0 at every cycle, while `0|0` draws the transition that says the
1505
+ * level was asserted again.
1506
+ *
1507
+ * `=` and `2` through `9` are the value characters, and each one written out
1508
+ * explicitly consumes one entry of the lane's `data` list. A `.` or a `|`
1509
+ * consumes none: measured, `2.2.` with `['A','B']` labels two runs A and B,
1510
+ * while `2222` with four labels uses all four.
1511
+ */
1512
+ const HELD_CHAR = '.';
1513
+ const GAP_CHAR = '|';
1514
+ const VALUE_CHARS = '=23456789';
1515
+
1516
+ /**
1517
+ * The four characters that draw the same thing whatever comes in front of them,
1518
+ * and the reason a lane beginning with a repeater is not simply "unresolved".
1519
+ *
1520
+ * wavedrom builds each cycle from the PAIR (previous character, this one) —
1521
+ * `node_modules/wavedrom/lib/parse-wave-lane.js` shifts the first character out,
1522
+ * eats the repeater run behind it, and then walks pairs. A `.` or `|` at cycle 0
1523
+ * is therefore a literal unknown character, and so is the pair it forms with
1524
+ * whatever follows. Measured over all nineteen level characters after a leading
1525
+ * repeater: `p n h l` still draw their own glyph (`.p` draws #nclk, `.h` draws
1526
+ * #111), and the other fifteen draw x. From the cycle after that the pairs are
1527
+ * ordinary again — `.0110` draws x x 1 1 0, not x throughout.
1528
+ */
1529
+ const ABSOLUTE_CHARS = 'pnhl';
1530
+
1531
+ function isValueChar(ch) {
1532
+ return typeof ch === 'string' && ch.length === 1 && VALUE_CHARS.indexOf(ch) !== -1;
1533
+ }
1534
+
1535
+ /**
1536
+ * Split a wave string into one cell per cycle, losslessly.
1537
+ *
1538
+ * A cell is `{ch, held}`: `held` is true exactly where the source wrote a `.`,
1539
+ * and `ch` is the level that cycle displays — for a held cell, the level of the
1540
+ * run it continues. Nothing is normalised, so `collapseWave` puts the source
1541
+ * string back character for character. That round trip is the invariant every
1542
+ * layer above rests on, and it is only true because `held` is kept.
1543
+ *
1544
+ * A `.` with nothing in front of it has no level to continue and answers `''`.
1545
+ * wavedrom 3.5.0 draws such a lane entirely as x — measured, `..0` renders six
1546
+ * x segments and never draws the 0 at all — so `''` is the honest answer rather
1547
+ * than a guess at what the author meant.
1548
+ *
1549
+ * Iteration is by code point, so a character outside the basic plane stays one
1550
+ * cell and still round trips.
1551
+ */
1552
+ function expandWave(wave) {
1553
+ const cells = [];
1554
+ if (typeof wave !== 'string') return cells;
1555
+ let level = '';
1556
+ for (const ch of wave) {
1557
+ if (ch === HELD_CHAR) {
1558
+ cells.push({ ch: level, held: true });
1559
+ continue;
1560
+ }
1561
+ cells.push({ ch: ch, held: false });
1562
+ if (ch !== GAP_CHAR) level = ch;
1563
+ }
1564
+ return cells;
1565
+ }
1566
+
1567
+ /**
1568
+ * Put cells back together into a wave string.
1569
+ *
1570
+ * A `.` is written only where the cell says that cycle is a continuation; this
1571
+ * never introduces one on its own. A plain string is accepted as a cell and is
1572
+ * always an explicit character, so `['p','p','p','p']` comes back as `pppp` and
1573
+ * not as `p...` — collapsing repeats into dots would change the picture, for
1574
+ * the reason `HELD_CHAR` states.
1575
+ */
1576
+ function collapseWave(cells) {
1577
+ if (!Array.isArray(cells)) return '';
1578
+ let out = '';
1579
+ for (const cell of cells) out += cellChar(cell);
1580
+ return out;
1581
+ }
1582
+
1583
+ /** The single source character a cell stands for. */
1584
+ function cellChar(cell) {
1585
+ if (typeof cell === 'string') return cell;
1586
+ if (cell === null || typeof cell !== 'object') return '';
1587
+ if (cell.held === true) return HELD_CHAR;
1588
+ return typeof cell.ch === 'string' ? cell.ch : '';
1589
+ }
1590
+
1591
+ /**
1592
+ * What level each cycle displays — what the geometry and rendering layers ask
1593
+ * for, and the half of the model that `expandWave` deliberately does not answer
1594
+ * on its own. `.` and `|` both take the level from in front of them, and a
1595
+ * cycle with nothing in front of it answers `''`.
1596
+ *
1597
+ * Takes a wave string or an array of cells.
1598
+ */
1599
+ function levelsOf(wave) {
1600
+ const cells = typeof wave === 'string' ? expandWave(wave) : wave;
1601
+ const out = [];
1602
+ if (!Array.isArray(cells)) return out;
1603
+ let shown = 'x';
1604
+ let unanchored = cells.length > 0 && isRepeater(cells[0]);
1605
+ for (const cell of cells) {
1606
+ const ch = cellChar(cell);
1607
+ if (ch === HELD_CHAR || ch === GAP_CHAR || ch === '') {
1608
+ out.push(unanchored ? 'x' : shown);
1609
+ continue;
1610
+ }
1611
+ if (ABSOLUTE_CHARS.indexOf(ch) !== -1) {
1612
+ shown = ch;
1613
+ unanchored = false;
1614
+ } else if (unanchored) {
1615
+ shown = 'x'; // the pair this would form is not one the engine knows
1616
+ unanchored = false; // the pair after it is, so ordinary service resumes
1617
+ } else {
1618
+ shown = ch;
1619
+ }
1620
+ out.push(shown);
1621
+ }
1622
+ return out;
1623
+ }
1624
+
1625
+ /** A cycle that takes its level from the one in front of it: `.` or `|`. */
1626
+ function isRepeater(cell) {
1627
+ const ch = cellChar(cell);
1628
+ return ch === HELD_CHAR || ch === GAP_CHAR;
1629
+ }
1630
+ /**
1631
+ * Read one lane into the shape the cycle operations work on: a cell per cycle,
1632
+ * each explicit value cell carrying the `data` label it consumes.
1633
+ *
1634
+ * Folding the labels into the cells is what lets inserting, deleting and
1635
+ * pasting cycles keep them lined up without a second bookkeeping pass — the
1636
+ * label travels with the cell that owns it. `spare` holds any labels the wave
1637
+ * never reaches; wavedrom ignores those, so they are handed back untouched
1638
+ * rather than dropped. The author's own spelling of `data` is kept too — the
1639
+ * raw string and the runs of whitespace inside it — so a write-back that
1640
+ * changes no label can hand the very same bytes straight back.
1641
+ */
1642
+ function readLane(lane) {
1643
+ const cells = expandWave(typeof lane.wave === 'string' ? lane.wave : '');
1644
+ const raw = lane.data;
1645
+ const asText = typeof raw === 'string';
1646
+ let labels = null;
1647
+ let gaps = null;
1648
+ let tail = '';
1649
+ if (Array.isArray(raw)) {
1650
+ labels = raw;
1651
+ } else if (asText) {
1652
+ const split = splitLabels(raw);
1653
+ labels = split.labels;
1654
+ gaps = split.gaps;
1655
+ tail = split.tail;
1656
+ }
1657
+ let slot = 0;
1658
+ const rich = [];
1659
+ for (const cell of cells) {
1660
+ const out = { ch: cell.ch, held: cell.held, label: undefined };
1661
+ if (!cell.held && isValueChar(cell.ch)) {
1662
+ if (labels !== null) out.label = labels[slot];
1663
+ slot++;
1664
+ }
1665
+ rich.push(out);
1666
+ }
1667
+ return {
1668
+ cells: rich,
1669
+ hasLabels: labels !== null,
1670
+ asText: asText,
1671
+ raw: asText ? raw : null,
1672
+ read: labels === null ? [] : labels.slice(),
1673
+ gaps: gaps,
1674
+ tail: tail,
1675
+ spare: labels === null ? [] : labels.slice(slot),
1676
+ };
1677
+ }
1678
+
1679
+ /**
1680
+ * Split a space-separated `data` string the way wavedrom does, keeping the
1681
+ * whitespace that came before each label so a rewrite can put it back.
1682
+ */
1683
+ function splitLabels(text) {
1684
+ const labels = [];
1685
+ const gaps = [];
1686
+ let gap = '';
1687
+ let i = 0;
1688
+ while (i < text.length) {
1689
+ if (SPACE[text[i]] === 1) {
1690
+ gap += text[i];
1691
+ i++;
1692
+ continue;
1693
+ }
1694
+ let j = i;
1695
+ while (j < text.length && SPACE[text[j]] !== 1) j++;
1696
+ labels.push(text.slice(i, j));
1697
+ gaps.push(gap);
1698
+ gap = '';
1699
+ i = j;
1700
+ }
1701
+ return { labels: labels, gaps: gaps, tail: gap };
1702
+ }
1703
+
1704
+ /** Whether the space-separated spelling of `data` can carry this label at all. */
1705
+ function isPlainLabel(label) {
1706
+ if (label === '') return false;
1707
+ for (const ch of label) {
1708
+ if (SPACE[ch] === 1) return false;
1709
+ }
1710
+ return true;
1711
+ }
1712
+
1713
+ /** Write a lane model back out as a new lane object, sharing nothing mutable with the old one. */
1714
+ function writeLane(lane, model) {
1715
+ const next = copyObject(lane);
1716
+ next.wave = collapseWave(model.cells);
1717
+ if (model.hasLabels) {
1718
+ const labels = [];
1719
+ for (const cell of model.cells) {
1720
+ if (!cell.held && isValueChar(cell.ch)) labels.push(cell.label);
1721
+ }
1722
+ // Value cells past the end of the author's list never had a label, so the
1723
+ // list stops where theirs did rather than growing a tail of blanks.
1724
+ while (labels.length > 0 && labels[labels.length - 1] === undefined) labels.pop();
1725
+ for (let k = 0; k < labels.length; k++) {
1726
+ if (labels[k] === undefined) labels[k] = '';
1727
+ }
1728
+ for (const left of model.spare) labels.push(left);
1729
+ next.data = model.asText ? writeLabelText(labels, model) : labels;
1730
+ }
1731
+ return next;
1732
+ }
1733
+
1734
+ /**
1735
+ * The space-separated spelling of `data`, or an array when that spelling cannot
1736
+ * say what the labels now say.
1737
+ *
1738
+ * wavedrom reads the string as `trim().split(/\s+/)`
1739
+ * (`node_modules/wavedrom/lib/parse-wave-lanes.js`), so two labels do not
1740
+ * survive it: a blank one vanishes and slides every later label one value cell
1741
+ * to the left, and one containing a space splits in two and pushes the last
1742
+ * label off the end. Both are silent, and both put a label on a cycle the user
1743
+ * did not touch — the failure this whole module exists to prevent. So where the
1744
+ * string form cannot say it, the lane is converted to array form. That is the
1745
+ * one place this changes how the author wrote something, and it is a deliberate
1746
+ * trade: a spelling change they can see, instead of a label landing on the
1747
+ * wrong cycle, which they cannot.
1748
+ *
1749
+ * When no label changed, the original string is handed back byte for byte, so
1750
+ * an edit that does not touch a label cannot touch its spacing either. When
1751
+ * labels did change and are all representable, the author's own runs of
1752
+ * whitespace are reused from the left and only genuinely new positions get a
1753
+ * single space.
1754
+ */
1755
+ function writeLabelText(labels, model) {
1756
+ if (sameStrings(labels, model.read)) return model.raw;
1757
+ for (const label of labels) {
1758
+ if (!isPlainLabel(label)) return labels;
1759
+ }
1760
+ let out = '';
1761
+ for (let k = 0; k < labels.length; k++) {
1762
+ const known = model.gaps !== null && k < model.gaps.length;
1763
+ out += (known ? model.gaps[k] : (k === 0 ? '' : ' ')) + labels[k];
1764
+ }
1765
+ return out + model.tail;
1766
+ }
1767
+
1768
+ function sameStrings(a, b) {
1769
+ if (a.length !== b.length) return false;
1770
+ for (let k = 0; k < a.length; k++) {
1771
+ if (a[k] !== b[k]) return false;
1772
+ }
1773
+ return true;
1774
+ }
1775
+
1776
+ /**
1777
+ * A shallow copy that keeps key order and cannot be tricked by a `__proto__`
1778
+ * key the source file wrote: `Object.assign` would run that one through the
1779
+ * prototype setter instead of copying it, which is the same hole `assign`
1780
+ * closes on the way in.
1781
+ */
1782
+ function copyObject(obj) {
1783
+ const out = {};
1784
+ if (obj === null || typeof obj !== 'object') return out;
1785
+ for (const key of Object.keys(obj)) assign(out, key, obj[key]);
1786
+ return out;
1787
+ }
1788
+
1789
+ /**
1790
+ * A copy all the way down, for a value arriving from a caller who keeps their
1791
+ * own reference to it. A shallow copy leaves the arrays shared, so a caller
1792
+ * appending to their own `data` afterwards would be writing inside the
1793
+ * document. Depth is bounded by the same ceiling the reader uses, which also
1794
+ * means a value holding itself stops rather than running the stack out; below
1795
+ * the ceiling, and for anything that is not a plain object or an array, the
1796
+ * value is carried over as it is.
1797
+ */
1798
+ function copyValue(value, depth) {
1799
+ if (Array.isArray(value)) {
1800
+ if (!(depth > 0)) return value;
1801
+ const out = [];
1802
+ for (const item of value) out.push(copyValue(item, depth - 1));
1803
+ return out;
1804
+ }
1805
+ if (value === null || typeof value !== 'object') return value;
1806
+ if (!isPlainObject(value) || !(depth > 0)) return value;
1807
+ const out = {};
1808
+ for (const key of Object.keys(value)) assign(out, key, copyValue(value[key], depth - 1));
1809
+ return out;
1810
+ }
1811
+
1812
+ /** The level and the data label in force at each cycle, structurally. */
1813
+ function resolveCells(cells) {
1814
+ const levels = [];
1815
+ const labels = [];
1816
+ let level = '';
1817
+ let label;
1818
+ for (const cell of cells) {
1819
+ if (!cell.held && cell.ch !== GAP_CHAR && cell.ch !== '') {
1820
+ level = cell.ch;
1821
+ label = isValueChar(cell.ch) ? cell.label : undefined;
1822
+ }
1823
+ levels.push(level);
1824
+ labels.push(label);
1825
+ }
1826
+ return { levels: levels, labels: labels };
1827
+ }
1828
+
1829
+ /**
1830
+ * Write out the continuation that a change at `index` would otherwise re-point,
1831
+ * and answer the cells to change instead.
1832
+ *
1833
+ * Painting one cycle moves every cycle that was holding its level. On `0...`,
1834
+ * setting cycle 2 to `1` naively gives `0.1.`, and cycle 3 — which nobody
1835
+ * touched — goes from 0 to 1; measured, `0.1.` and `0.10` are different
1836
+ * pictures. So the first held cell after `index` is written out at the level it
1837
+ * was already showing, and every continuation past it is anchored again by
1838
+ * that one.
1839
+ *
1840
+ * A `|` is stepped over rather than written out. It has no level of its own to
1841
+ * pin, and a gap drawn over the old level has no spelling once the level in
1842
+ * front of it changes; that is wavedrom's shape, not this model's, and it is
1843
+ * why `0|.` repainted at cycle 0 gives `1|0` and not something that keeps the
1844
+ * gap showing 0.
1845
+ */
1846
+ function anchorAfter(cells, index) {
1847
+ for (let j = index + 1; j < cells.length; j++) {
1848
+ if (cells[j].held) {
1849
+ const out = cells.slice();
1850
+ out[j] = anchoredCell(cells, resolveCells(cells), j);
1851
+ return out;
1852
+ }
1853
+ if (cells[j].ch !== GAP_CHAR) break;
1854
+ }
1855
+ return cells;
1856
+ }
1857
+
1858
+ /**
1859
+ * Write out the cell at `index` when it is a continuation, so the cells from
1860
+ * there on stand without whatever is in front of them today. This is what makes
1861
+ * a clipping self-contained, and what protects the cycle a block is about to be
1862
+ * pushed in front of.
1863
+ */
1864
+ function anchorSelf(cells, index) {
1865
+ if (!(index >= 0 && index < cells.length) || !cells[index].held) return cells;
1866
+ const out = cells.slice();
1867
+ out[index] = anchoredCell(cells, resolveCells(cells), index);
1868
+ return out;
1869
+ }
1870
+
1871
+ /**
1872
+ * The same, for a cell that is about to become cycle 0 — which takes a gap as
1873
+ * well as a continuation, because a `|` at cycle 0 has nothing to draw over.
1874
+ *
1875
+ * Measured: `0|0.` draws 0 at every cycle and `|0.` draws x at every cycle, so
1876
+ * deleting the first cycle without this turns a whole lane to x. The gap glyph
1877
+ * is what gets lost, because a gap over a level that no longer has anything in
1878
+ * front of it has no spelling at all; the levels are what is kept, and they are
1879
+ * what the user was looking at.
1880
+ */
1881
+ function anchorAsHead(cells, index) {
1882
+ if (!(index >= 0 && index < cells.length) || !isRepeater(cells[index])) return cells;
1883
+ const out = cells.slice();
1884
+ out[index] = anchoredCell(cells, resolveCells(cells), index);
1885
+ return out;
1886
+ }
1887
+
1888
+ /**
1889
+ * One cell written out at the level and label it was already showing. A cell
1890
+ * whose level is `''` becomes `x`, because x is what wavedrom draws for a cycle
1891
+ * with nothing in front of it.
1892
+ */
1893
+ function anchoredCell(cells, resolved, j) {
1894
+ const cell = cells[j];
1895
+ let level = resolved.levels[j];
1896
+ if (level === '' && cell.held && typeof cell.ch === 'string' &&
1897
+ cell.ch !== '' && cell.ch !== HELD_CHAR && cell.ch !== GAP_CHAR) {
1898
+ // At cycle 0 there is nothing in front to resolve against, but a held cell
1899
+ // carries the level it continues, and a clipping built by hand relies on
1900
+ // that being read rather than thrown away.
1901
+ level = cell.ch;
1902
+ }
1903
+ const ch = level === '' ? 'x' : level;
1904
+ return { ch: ch, held: false, label: isValueChar(ch) ? resolved.labels[j] : undefined };
1905
+ }
1906
+
1907
+ /**
1908
+ * Every lane in the document, in the order the user sees them: depth first
1909
+ * through `signal`, down into each group, skipping the strings a group uses for
1910
+ * its own title.
1911
+ *
1912
+ * This is the one enumeration the whole module counts by — the lane index a
1913
+ * caller passes in, the order a clipping's lanes come out in, and the order a
1914
+ * paste puts them back. Two counts is what let a paste land on a lane nobody
1915
+ * pointed at, so there is deliberately only one, and `flattenLanes` and
1916
+ * `spanOp` both get their ordinals from the same walk rather than from two
1917
+ * walks that happen to agree today.
1918
+ */
1919
+ function flattenLanes(doc) {
1920
+ const out = [];
1921
+ const lanes = lanesOf(doc);
1922
+ if (lanes === null) return out;
1923
+ rebuildLanes(lanes, function (lane, ordinal, container, position, path) {
1924
+ out.push({ lane: lane, container: container, position: position, path: path });
1925
+ return lane;
1926
+ });
1927
+ return out;
1928
+ }
1929
+
1930
+ /**
1931
+ * Walk every lane at any depth in display order, hand each to `fn` with its
1932
+ * ordinal and where it lives, and rebuild the tree around whatever comes back.
1933
+ * An array nothing changed inside comes back as itself, so untouched groups and
1934
+ * lanes stay the same objects and the diff a store has to write stays small.
1935
+ */
1936
+ function rebuildLanes(container, fn, state, prefix) {
1937
+ const st = state === undefined ? { n: 0 } : state;
1938
+ const base = prefix === undefined ? ['signal'] : prefix;
1939
+ let changed = false;
1940
+ const out = [];
1941
+ for (let i = 0; i < container.length; i++) {
1942
+ const item = container[i];
1943
+ if (Array.isArray(item)) {
1944
+ const sub = rebuildLanes(item, fn, st, base.concat([i]));
1945
+ if (sub !== item) changed = true;
1946
+ out.push(sub);
1947
+ } else if (isLane(item)) {
1948
+ const next = fn(item, st.n, container, i, base.concat([i]));
1949
+ st.n++;
1950
+ if (next !== item) changed = true;
1951
+ out.push(next);
1952
+ } else {
1953
+ out.push(item);
1954
+ }
1955
+ }
1956
+ return changed ? out : container;
1957
+ }
1958
+
1959
+ /**
1960
+ * Run `change` over the lane at display index `index` and answer a new doc — or
1961
+ * the very same doc, the identical object rather than an equal one, when
1962
+ * nothing changed.
1963
+ *
1964
+ * That identity is how a store above can tell a no-op from an edit without
1965
+ * comparing anything, and it is what every operation here answers for an index
1966
+ * out of range, a character that is not one character, or an argument it cannot
1967
+ * read. None of them throw, which matches the promise the parsing half of this
1968
+ * file makes. Lanes that were not touched come through as the same objects.
1969
+ *
1970
+ * The cost of that promise, stated rather than left to be discovered: a caller
1971
+ * cannot tell「你傳錯了」from「這裡本來就沒東西可改」— both are the same object
1972
+ * coming back. The two are genuinely different things (one is a bug, the other
1973
+ * is an ordinary state: a cursor at the end of a lane, a paste with an empty
1974
+ * clipboard), and a caller who needs them apart can ask the same question this
1975
+ * file asks — `Number.isInteger(i)` for an index, one code point for a brush —
1976
+ * BEFORE calling. Making the type errors throw instead was considered for
1977
+ * v3.4.0 and rejected: it would overturn the promise above, six assertions in
1978
+ * `test/wave-codec.test.js` that pin it, and the property the UI depends on
1979
+ * (one gesture costs at most one status line, never a page-level error).
1980
+ * `test/wave-codec.test.js` now pins the type case too, so it is a decision on
1981
+ * the record rather than an accident of the argument checks.
1982
+ */
1983
+ function mapLane(doc, index, change) {
1984
+ const flat = flattenLanes(doc);
1985
+ if (!isIndex(index, flat.length)) return doc;
1986
+ const lane = flat[index].lane;
1987
+ const next = change(lane);
1988
+ if (next === null || next === lane || sameLane(lane, next)) return doc;
1989
+ return withLanes(doc, rebuildLanes(lanesOf(doc), function (item, ordinal) {
1990
+ return ordinal === index ? next : item;
1991
+ }));
1992
+ }
1993
+
1994
+ /** A lane is an object; a nested array in `signal` is a group and a string is its title. */
1995
+ function isLane(lane) {
1996
+ return lane !== null && typeof lane === 'object' && !Array.isArray(lane);
1997
+ }
1998
+
1999
+ function lanesOf(doc) {
2000
+ if (doc === null || typeof doc !== 'object') return null;
2001
+ return Array.isArray(doc.signal) ? doc.signal : null;
2002
+ }
2003
+
2004
+ function withLanes(doc, lanes) {
2005
+ const next = copyObject(doc);
2006
+ next.signal = lanes;
2007
+ return next;
2008
+ }
2009
+
2010
+ function isIndex(n, length) {
2011
+ return Number.isInteger(n) && n >= 0 && n < length;
2012
+ }
2013
+
2014
+ function isBound(n, length) {
2015
+ return Number.isInteger(n) && n >= 0 && n <= length;
2016
+ }
2017
+
2018
+ /**
2019
+ * Whether two lanes say the same thing. Only the top level is compared value by
2020
+ * value, with arrays compared element by element; anything deeper was carried
2021
+ * over by reference and so is the same object when it is unchanged.
2022
+ */
2023
+ function sameLane(a, b) {
2024
+ const keys = Object.keys(b);
2025
+ if (keys.length !== Object.keys(a).length) return false;
2026
+ for (const key of keys) {
2027
+ const x = a[key];
2028
+ const y = b[key];
2029
+ if (Array.isArray(x) && Array.isArray(y)) {
2030
+ if (!sameStrings(x, y)) return false;
2031
+ continue;
2032
+ }
2033
+ if (x !== y) return false;
2034
+ }
2035
+ return true;
2036
+ }
2037
+
2038
+ /**
2039
+ * Paint one cycle of one lane, addressed by display index.
2040
+ *
2041
+ * `ch` is a single character: a level, a value character, `|` for a gap, or `.`
2042
+ * to make the cycle continue the one in front of it. Anything else — a longer
2043
+ * string, a cycle outside the lane, a `.` or `|` at cycle 0 where there is
2044
+ * nothing to continue or to draw over — answers the doc unchanged.
2045
+ *
2046
+ * A newly painted value cell keeps the label of the value cell it replaced and
2047
+ * otherwise starts blank; a cell painted with something that is not a value
2048
+ * character gives its label up, so the lane's `data` list stays exactly as long
2049
+ * as the number of value characters written out.
2050
+ */
2051
+ function setCell(doc, laneIndex, cycle, ch) {
2052
+ return paint(doc, laneIndex, cycle, cycle, ch, false);
2053
+ }
2054
+
2055
+ /**
2056
+ * Paint a range of cycles of one lane, `from` and `to` both included and in
2057
+ * either order — a drag that went right to left selects the same cycles.
2058
+ *
2059
+ * The range becomes one run: `ch` at its first cycle and continuations after
2060
+ * it. That is what a bus painted across four cycles means, and it keeps the
2061
+ * range to a single `data` entry instead of one per cycle. `|` is written at
2062
+ * every cycle instead, having no run to start.
2063
+ */
2064
+ function setCellRange(doc, laneIndex, from, to, ch) {
2065
+ return paint(doc, laneIndex, from, to, ch, true);
2066
+ }
2067
+
2068
+ function paint(doc, laneIndex, from, to, ch, asRun) {
2069
+ if (typeof ch !== 'string' || Array.from(ch).length !== 1) return doc;
2070
+ return mapLane(doc, laneIndex, function (lane) {
2071
+ const model = readLane(lane);
2072
+ const n = model.cells.length;
2073
+ let lo = from;
2074
+ let hi = to;
2075
+ if (!Number.isInteger(lo) || !Number.isInteger(hi)) return null;
2076
+ if (lo > hi) {
2077
+ const swap = lo;
2078
+ lo = hi;
2079
+ hi = swap;
2080
+ }
2081
+ if (!isIndex(lo, n) || !isIndex(hi, n)) return null;
2082
+ // A repeater at cycle 0 has nothing to repeat, and the engine answers that
2083
+ // by drawing the run and the cycle after it as x. Refusing is the honest
2084
+ // reply to a brush that cannot mean anything where it was aimed.
2085
+ if (lo === 0 && (ch === HELD_CHAR || ch === GAP_CHAR)) return null;
2086
+ const anchored = anchorAfter(model.cells, hi);
2087
+ const before = resolveCells(anchored);
2088
+ const heldLevel = lo > 0 ? before.levels[lo - 1] : '';
2089
+ const cells = anchored.slice();
2090
+ for (let j = lo; j <= hi; j++) {
2091
+ if (ch === HELD_CHAR) {
2092
+ cells[j] = { ch: heldLevel, held: true, label: undefined };
2093
+ } else if (asRun && j > lo && ch !== GAP_CHAR) {
2094
+ cells[j] = { ch: ch, held: true, label: undefined };
2095
+ } else if (isValueChar(ch)) {
2096
+ const old = anchored[j];
2097
+ // A value cell that replaces another keeps its label exactly, `undefined`
2098
+ // included: a lane whose `data` simply runs out before its value cycles
2099
+ // has no label there, and inventing a blank one would force the whole
2100
+ // lane into array form over a label the author never wrote.
2101
+ cells[j] = { ch: ch, held: false,
2102
+ label: !old.held && isValueChar(old.ch) ? old.label : '' };
2103
+ } else {
2104
+ cells[j] = { ch: ch, held: false, label: undefined };
2105
+ }
2106
+ }
2107
+ model.cells = cells;
2108
+ return writeLane(lane, model);
2109
+ });
2110
+ }
2111
+
2112
+ /**
2113
+ * Widen every lane at the same cycle, so nothing slips out of line.
2114
+ *
2115
+ * The new cycles are continuations, which is what leaves every level on either
2116
+ * side exactly where it was. At cycle 0 there is nothing to continue, so the
2117
+ * lane's first cell is written out a second time in front of itself and the
2118
+ * rest of the new cycles continue that; a lane with no cycles at all starts
2119
+ * with `x`, which is what wavedrom draws where a lane says nothing.
2120
+ */
2121
+ function insertCycles(doc, at, count) {
2122
+ return spanOp(doc, function (lane) {
2123
+ const model = readLane(lane);
2124
+ if (!Number.isInteger(count) || count < 1 || !isBound(at, model.cells.length)) return null;
2125
+ const filler = fillerCells(model.cells, at, count);
2126
+ model.cells = model.cells.slice(0, at).concat(filler, model.cells.slice(at));
2127
+ return writeLane(lane, model);
2128
+ });
2129
+ }
2130
+
2131
+ /**
2132
+ * Narrow every lane at the same cycle.
2133
+ *
2134
+ * The first continuation after the cut is written out first, so the cycle that
2135
+ * follows it keeps the level it was showing instead of picking up whatever now
2136
+ * lands in front of it — deleting cycle 0 of `0.11` gives `011`. When the cut
2137
+ * takes cycle 0 itself, the cycle that inherits the front of the lane is
2138
+ * written out too, gap included: `0|0.` loses its first cycle as `00.`, not as
2139
+ * a `|0.` whose every cycle the engine then draws as x.
2140
+ */
2141
+ function deleteCycles(doc, at, count) {
2142
+ return spanOp(doc, function (lane) {
2143
+ const model = readLane(lane);
2144
+ const n = model.cells.length;
2145
+ if (!Number.isInteger(count) || count < 1 || !isIndex(at, n)) return null;
2146
+ const take = Math.min(count, n - at);
2147
+ let cells = anchorAfter(model.cells, at + take - 1);
2148
+ if (at === 0 && !isRepeater(model.cells[0])) cells = anchorAsHead(cells, take);
2149
+ model.cells = cells.slice(0, at).concat(cells.slice(at + take));
2150
+ return writeLane(lane, model);
2151
+ });
2152
+ }
2153
+
2154
+ /**
2155
+ * Run `change` over every lane at once, at any depth, and answer a new doc or
2156
+ * the same one when no lane changed.
2157
+ *
2158
+ * Every operation that changes how many cycles a lane has goes through here,
2159
+ * because a widening that reached only some lanes would slide the rest of the
2160
+ * picture out of line — the one failure a wave editor cannot let past. Lanes
2161
+ * inside a group are ordinary displayed lanes and are reached like any other;
2162
+ * leaving them out is exactly that failure, with the group's rising edges one
2163
+ * cycle away from everyone else's.
2164
+ */
2165
+ function spanOp(doc, change) {
2166
+ const lanes = lanesOf(doc);
2167
+ if (lanes === null) return doc;
2168
+ const next = rebuildLanes(lanes, function (lane, ordinal) {
2169
+ const out = change(lane, ordinal);
2170
+ return out === null || sameLane(lane, out) ? lane : out;
2171
+ });
2172
+ return next === lanes ? doc : withLanes(doc, next);
2173
+ }
2174
+
2175
+ /**
2176
+ * The cells a widening puts in at `at`: continuations of the level in front of
2177
+ * them, or — at cycle 0, where there is no such level — a second copy of the
2178
+ * lane's first cell followed by continuations of that.
2179
+ */
2180
+ function fillerCells(cells, at, count) {
2181
+ const out = [];
2182
+ if (at > 0) {
2183
+ const level = resolveCells(cells).levels[at - 1];
2184
+ for (let k = 0; k < count; k++) out.push({ ch: level, held: true, label: undefined });
2185
+ return out;
2186
+ }
2187
+ const head = cells.length > 0 ? cells[0] : null;
2188
+ out.push(head === null
2189
+ ? { ch: 'x', held: false, label: undefined }
2190
+ : { ch: head.ch, held: head.held, label: head.label });
2191
+ const level = levelsOf(out)[0];
2192
+ for (let k = 1; k < count; k++) out.push({ ch: level, held: true, label: undefined });
2193
+ return out;
2194
+ }
2195
+
2196
+ /**
2197
+ * Lift a stretch of cycles off every lane, in display order.
2198
+ *
2199
+ * The clipping's first cycle is written out rather than left as a continuation
2200
+ * or a gap, so it carries its own level and can be pasted anywhere without
2201
+ * picking up whatever happens to sit in front of it there. `data` holds the
2202
+ * labels of the value characters inside the stretch, in order, the same way a
2203
+ * lane does.
2204
+ */
2205
+ function copyCycles(doc, at, count) {
2206
+ if (!Number.isInteger(at) || at < 0 || !Number.isInteger(count) || count < 1) {
2207
+ return { kind: 'cycles', count: 0, lanes: [] };
2208
+ }
2209
+ const out = [];
2210
+ for (const spot of flattenLanes(doc)) {
2211
+ const model = readLane(spot.lane);
2212
+ const cells = anchorAsHead(model.cells, at).slice(at, at + count);
2213
+ const chars = [];
2214
+ const data = [];
2215
+ for (const cell of cells) {
2216
+ chars.push({ ch: cell.ch, held: cell.held });
2217
+ if (!cell.held && isValueChar(cell.ch)) data.push(cell.label);
2218
+ }
2219
+ out.push({ chars: chars, data: data });
2220
+ }
2221
+ return { kind: 'cycles', count: count, lanes: out };
2222
+ }
2223
+
2224
+ /**
2225
+ * Put a clipping back at `at`. Clip lanes are matched to document lanes by the
2226
+ * same display order `copyCycles` wrote them in.
2227
+ *
2228
+ * `insert` widens every lane by the clipping's width, including the lanes the
2229
+ * clipping says nothing about — widening only some of them would slide the rest
2230
+ * of the picture out of line, the same reason `insertCycles` works on every
2231
+ * lane at once. `overwrite` replaces cycles in place and never lengthens a
2232
+ * lane: a clipping running past the end is cut off there, and a lane the
2233
+ * clipping does not reach is left alone, because nothing moves.
2234
+ *
2235
+ * A clipping lane whose cells do not number `count` is treated as saying
2236
+ * nothing about that lane rather than as a width to guess at. A clipping that
2237
+ * begins with a repeater is written out when it lands at cycle 0, because there
2238
+ * it would have nothing to repeat.
2239
+ */
2240
+ function pasteCycles(doc, at, clip, mode) {
2241
+ if (mode !== 'insert' && mode !== 'overwrite') return doc;
2242
+ if (clip === null || typeof clip !== 'object' || clip.kind !== 'cycles' ||
2243
+ !Array.isArray(clip.lanes) || !Number.isInteger(clip.count) || clip.count < 1) {
2244
+ return doc;
2245
+ }
2246
+ if (!Number.isInteger(at) || at < 0) return doc;
2247
+ return spanOp(doc, function (lane, ordinal) {
2248
+ const model = readLane(lane);
2249
+ const n = model.cells.length;
2250
+ const source = clipCells(clip.lanes[ordinal], clip.count);
2251
+ // A lane that already begins with a repeater is malformed input, not
2252
+ // something an edit did; repairing it here would change cycles the user did
2253
+ // not touch, so the repair is only for a head this operation itself makes.
2254
+ const wasLoose = n > 0 && isRepeater(model.cells[0]);
2255
+ if (mode === 'overwrite') {
2256
+ if (source === null || !isIndex(at, n)) return null;
2257
+ const width = Math.min(clip.count, n - at);
2258
+ const anchored = anchorAfter(model.cells, at + width - 1);
2259
+ const block = at === 0 && !wasLoose ? anchorAsHead(source, 0) : source;
2260
+ model.cells = anchored.slice(0, at)
2261
+ .concat(block.slice(0, width), anchored.slice(at + width));
2262
+ return writeLane(lane, model);
2263
+ }
2264
+ if (!isBound(at, n)) return null;
2265
+ const anchored = anchorSelf(model.cells, at);
2266
+ let block = source === null ? fillerCells(anchored, at, clip.count) : source;
2267
+ if (at === 0 && !wasLoose) block = anchorAsHead(block, 0);
2268
+ model.cells = anchored.slice(0, at).concat(block, anchored.slice(at));
2269
+ return writeLane(lane, model);
2270
+ });
2271
+ }
2272
+
2273
+ /** One clipping lane as cells with their labels folded back in, or null when it has none. */
2274
+ function clipCells(clipLane, count) {
2275
+ if (clipLane === null || typeof clipLane !== 'object' || !Array.isArray(clipLane.chars)) {
2276
+ return null;
2277
+ }
2278
+ if (clipLane.chars.length !== count) return null;
2279
+ const labels = Array.isArray(clipLane.data) ? clipLane.data : [];
2280
+ let slot = 0;
2281
+ const out = [];
2282
+ for (const cell of clipLane.chars) {
2283
+ const ch = cellChar(cell);
2284
+ const held = ch === HELD_CHAR;
2285
+ const level = held && cell !== null && typeof cell === 'object' &&
2286
+ typeof cell.ch === 'string' ? cell.ch : ch;
2287
+ const rich = { ch: held ? level : ch, held: held, label: undefined };
2288
+ if (!held && isValueChar(ch)) {
2289
+ rich.label = labels[slot];
2290
+ slot++;
2291
+ }
2292
+ out.push(rich);
2293
+ }
2294
+ return out;
2295
+ }
2296
+
2297
+ /**
2298
+ * Put a lane into the document so that it becomes the lane at display position
2299
+ * `at`, as a copy all the way down — the object the caller hands over stays
2300
+ * theirs, and changing it afterwards cannot reach inside the doc.
2301
+ */
2302
+ function addLane(doc, at, lane) {
2303
+ if (!isLane(lane)) return doc;
2304
+ const path = laneInsertPath(doc, at);
2305
+ if (path === null) return doc;
2306
+ return insertAtPath(doc, path, copyValue(lane, MAX_DEPTH));
2307
+ }
2308
+
2309
+ /**
2310
+ * Take the lane at display position `at` out — and with it the group it leaves
2311
+ * with no lanes at all, however many levels of group that empties.
2312
+ *
2313
+ * An empty group draws nothing and, because a position resolves against the
2314
+ * lane sitting there, no display position ever resolves inside one again. Left
2315
+ * behind, it would be a region of the document the user could see in their file
2316
+ * and never address through this module again; that is a trap rather than a
2317
+ * trade, so the group goes with its last lane.
2318
+ */
2319
+ function removeLane(doc, at) {
2320
+ const path = laneRemovePath(doc, at);
2321
+ if (path === null) return doc;
2322
+ return withoutPath(doc, path);
2323
+ }
2324
+
2325
+ /**
2326
+ * Move the lane at display position `from` to `to`, counted after it has been
2327
+ * lifted out, which is what a drag down the lane list does. It may cross into or
2328
+ * out of a group, by the rule `laneInsertPath` states, and it takes an emptied
2329
+ * group with it exactly as `removeLane` does.
2330
+ *
2331
+ * The lane object itself comes through as the same object. To write the move
2332
+ * back, a store asks the bridge for both ends:
2333
+ *
2334
+ * {op: 'move',
2335
+ * path: lanePath(doc, from),
2336
+ * to: laneInsertPath(doc, to < from ? to : to + 1)}
2337
+ *
2338
+ * The `to + 1` is the same counting this function does: a destination measured
2339
+ * after the lift is one place further along while the lane is still in the way,
2340
+ * and a `move` edit is resolved against the document as it stands.
2341
+ */
2342
+ function moveLane(doc, from, to) {
2343
+ const flat = flattenLanes(doc);
2344
+ if (!isIndex(from, flat.length) || !isIndex(to, flat.length) || from === to) return doc;
2345
+ const lane = flat[from].lane;
2346
+ const reduced = removeLane(doc, from);
2347
+ const path = laneInsertPath(reduced, to);
2348
+ if (path === null) return doc;
2349
+ return insertAtPath(reduced, path, lane);
2350
+ }
2351
+
2352
+ /**
2353
+ * Where the lane at display position `index` lives, as a path the patching half
2354
+ * of this file addresses members by — `['signal', 1, 2]` for the second lane of
2355
+ * the group at `signal[1]`. `null` when there is no such lane.
2356
+ *
2357
+ * This is the bridge between the two halves, and it is exported because there is
2358
+ * no other honest way across. The document half counts lanes in display order,
2359
+ * because that is the only thing a click on a waveform can mean; the patching
2360
+ * half addresses container paths, because that is how spans are keyed. They do
2361
+ * not coincide as soon as a group is involved. For
2362
+ * `{signal: [{a}, ['grp', {b}, {c}], {d}]}` the display positions are a=0, b=1,
2363
+ * c=2, d=3, and their paths are `['signal',0]`, `['signal',1,1]`,
2364
+ * `['signal',1,2]`, `['signal',2]`. A caller that builds `['signal', index]` for
2365
+ * itself renames `d` when the user asked for `c`, and nothing refuses, because
2366
+ * that path names a real lane too.
2367
+ *
2368
+ * So every structural edit this module can produce has exactly one supported way
2369
+ * to be written back. All five are pinned as tests against a grouped fixture in
2370
+ * `test/wave-codec.test.js`; this list is a map to them, not the authority:
2371
+ *
2372
+ * renameLane(doc, k, n), lane already has a name
2373
+ * {path: lanePath(doc, k).concat(['name']), value: n}
2374
+ * renameLane(doc, k, n), lane has no name
2375
+ * {op: 'insert', path: lanePath(doc, k).concat(['name']), value: n}
2376
+ * addLane(doc, k, lane)
2377
+ * {op: 'insert', path: laneInsertPath(doc, k), value: lane}
2378
+ * removeLane(doc, k)
2379
+ * {op: 'remove', path: laneRemovePath(doc, k)}
2380
+ * moveLane(doc, f, t)
2381
+ * {op: 'move', path: lanePath(doc, f),
2382
+ * to: laneInsertPath(doc, t < f ? t : t + 1)}
2383
+ *
2384
+ * The rename is the one with a branch, and picking the wrong side of it cannot
2385
+ * go unnoticed: `set` on a lane with no `name` refuses because nothing is parsed
2386
+ * at that path, and `insert` on a lane that has one refuses because the member
2387
+ * is already there. Both refusals are pinned too.
2388
+ */
2389
+ function lanePath(doc, index) {
2390
+ const flat = flattenLanes(doc);
2391
+ return isIndex(index, flat.length) ? flat[index].path : null;
2392
+ }
2393
+
2394
+ /**
2395
+ * Every lane's path, in display order, from ONE walk.
2396
+ *
2397
+ * `lanePath` answers a single position and walks the document to do it, which
2398
+ * is the right shape for an edit — a document op touches one lane. A layer that
2399
+ * needs the whole list, like the geometry that lays out every row, would have
2400
+ * to ask N times and pay N walks for it, and that cost follows the DOCUMENT's
2401
+ * length rather than whatever ceiling the caller puts on its own result.
2402
+ *
2403
+ * So the bridge gets this second door. It is not a second ORDER: both doors are
2404
+ * `flattenLanes`, the single enumeration this module counts by, and
2405
+ * `test/wave-codec.test.js` pins them against each other position by position.
2406
+ * The array is freshly built, so a caller may keep or mutate it.
2407
+ */
2408
+ function lanePaths(doc) {
2409
+ return flattenLanes(doc).map(function (entry) { return entry.path; });
2410
+ }
2411
+
2412
+ /**
2413
+ * Where a lane inserted at display position `index` goes, and what happens at
2414
+ * the two edges.
2415
+ *
2416
+ * A position resolves to a container as well as an offset: inserting at `index`
2417
+ * means going in immediately in front of whichever lane sits there now, in that
2418
+ * lane's own container. Two edges follow, and neither is arbitrary. Inserting at
2419
+ * a group's FIRST lane goes inside the group, in front of it. Inserting at the
2420
+ * position just past a group's LAST lane resolves to the next lane outside the
2421
+ * group, so it lands outside — a group can be joined at its head but not at its
2422
+ * tail. Past the last lane of the document there is no lane to resolve against
2423
+ * at all, so it goes on the end of `signal` itself.
2424
+ */
2425
+ function laneInsertPath(doc, index) {
2426
+ const lanes = lanesOf(doc);
2427
+ if (lanes === null) return null;
2428
+ const flat = flattenLanes(doc);
2429
+ if (!isBound(index, flat.length)) return null;
2430
+ return index === flat.length ? ['signal', lanes.length] : flat[index].path;
2431
+ }
2432
+
2433
+ /**
2434
+ * What a removal of the lane at display position `index` actually has to take
2435
+ * out: the lane, or the group around it when that group would be left holding
2436
+ * no lane at all.
2437
+ */
2438
+ function laneRemovePath(doc, index) {
2439
+ const path = lanePath(doc, index);
2440
+ return path === null ? null : emptiedMemberPath(doc, path);
2441
+ }
2442
+
2443
+ /**
2444
+ * What a removal of a SET of lanes has to take out, decided for the whole set
2445
+ * at once.
2446
+ *
2447
+ * A caller with several lanes to remove cannot get this by asking
2448
+ * `laneRemovePath` once per lane: every answer is measured in a document where
2449
+ * the other doomed lanes are still present, so a group whose lanes are ALL
2450
+ * going is told, each time, that it still holds one. Every answer comes back
2451
+ * "just this lane", and what is left in the file is an empty group — one that
2452
+ * draws nothing and that no display position ever resolves inside again, which
2453
+ * is the trap `removeLane` exists to avoid. It is not a hypothetical: two lanes
2454
+ * of one group is an ordinary multi-select.
2455
+ *
2456
+ * The rule is the one `emptiedMemberPath` and `holdsContent` already state, and
2457
+ * it lives here beside them for the reason their own comment gives — so a
2458
+ * removal and the patch that writes it cannot disagree about how much goes. A
2459
+ * group goes when every lane under it goes, at any depth; a group the author
2460
+ * wrote with no lane in it at all is content of its own and keeps its parent
2461
+ * alive; `signal` itself is never the answer.
2462
+ *
2463
+ * Answers the topmost member that goes for each part of the tree, so the paths
2464
+ * returned never contain one another and can all be handed to `patchSource` in
2465
+ * one batch. `null` when any index is not a lane position, or when `doc` has no
2466
+ * lane list — the same answer the single-lane door gives.
2467
+ *
2468
+ * `laneRemovePaths(doc, [k])` and `[laneRemovePath(doc, k)]` are the same
2469
+ * answer, and `test/wave-codec.test.js` pins that position by position on a
2470
+ * grouped fixture, because two rules that are allowed to drift eventually do.
2471
+ *
2472
+ * @param {any} doc
2473
+ * @param {Array<number>} indexes display positions, in any order
2474
+ * @returns {Array<DocPath>|null}
2475
+ */
2476
+ function laneRemovePaths(doc, indexes) {
2477
+ const lanes = lanesOf(doc);
2478
+ if (lanes === null || !Array.isArray(indexes)) return null;
2479
+ const flat = flattenLanes(doc);
2480
+ const gone = new Set();
2481
+ for (const index of indexes) {
2482
+ if (!isIndex(index, flat.length)) return null;
2483
+ gone.add(JSON.stringify(flat[index].path));
2484
+ }
2485
+ const out = [];
2486
+ emitRemovals(markRemovals(lanes, ['signal'], gone, 0), out);
2487
+ return out;
2488
+ }
2489
+
2490
+ /**
2491
+ * Mark one lane container: whether the whole of it goes, and the same answer
2492
+ * for each child. Depth is bounded by the ceiling the reader uses, so a
2493
+ * document this module could not have parsed cannot run the stack out here.
2494
+ */
2495
+ function markRemovals(container, path, gone, depth) {
2496
+ let hasLane = false;
2497
+ let allGone = true;
2498
+ const kids = [];
2499
+ if (depth <= MAX_DEPTH) {
2500
+ for (let i = 0; i < container.length; i++) {
2501
+ const item = container[i];
2502
+ const here = path.concat([i]);
2503
+ if (Array.isArray(item)) {
2504
+ const sub = markRemovals(item, here, gone, depth + 1);
2505
+ kids.push(sub);
2506
+ if (sub.hasLane) hasLane = true;
2507
+ if (!sub.gone) allGone = false;
2508
+ } else if (isLane(item)) {
2509
+ hasLane = true;
2510
+ const doomed = gone.has(JSON.stringify(here));
2511
+ kids.push({ path: here, gone: doomed, kids: null, hasLane: true });
2512
+ if (!doomed) allGone = false;
2513
+ }
2514
+ // A group's title string is neither, and keeps nothing alive on its own.
2515
+ }
2516
+ }
2517
+ return { path: path, gone: hasLane && allGone, kids: kids, hasLane: hasLane };
2518
+ }
2519
+
2520
+ /** The topmost members that go, from a marked tree. The root is never one. */
2521
+ function emitRemovals(node, out) {
2522
+ for (const kid of node.kids) {
2523
+ if (kid.gone) out.push(kid.path);
2524
+ else if (kid.kids !== null) emitRemovals(kid, out);
2525
+ }
2526
+ }
2527
+
2528
+ /**
2529
+ * The member that has to go when the element at `path` leaves its array: the
2530
+ * element itself, or the innermost enclosing array left with no lane in it,
2531
+ * however many levels up that reaches. `signal` is never the answer — the
2532
+ * document always has a lane list, even an empty one.
2533
+ *
2534
+ * Both halves of the file read this one function rather than each deciding for
2535
+ * itself, so a removal and the patch that writes it cannot disagree about how
2536
+ * much goes.
2537
+ */
2538
+ function emptiedMemberPath(doc, path) {
2539
+ let target = path;
2540
+ for (;;) {
2541
+ const last = target[target.length - 1];
2542
+ if (target.length < 2 || typeof last !== 'number') return target;
2543
+ const parentPath = target.slice(0, -1);
2544
+ if (typeof parentPath[parentPath.length - 1] !== 'number') return target;
2545
+ const parent = lookup(doc, parentPath);
2546
+ if (!parent.found || !Array.isArray(parent.value)) return target;
2547
+ let survives = false;
2548
+ for (let i = 0; i < parent.value.length; i++) {
2549
+ if (i !== last && holdsContent(parent.value[i])) {
2550
+ survives = true;
2551
+ break;
2552
+ }
2553
+ }
2554
+ if (survives) return target;
2555
+ target = parentPath;
2556
+ }
2557
+ }
2558
+
2559
+ /**
2560
+ * Whether this is something that keeps the container around it alive: a lane, or
2561
+ * a group — including one the author wrote with nothing in it.
2562
+ *
2563
+ * An empty group of their own is content, not the space a removal just made, so
2564
+ * taking the last lane out of `['g1', {b}, ['g2']]` leaves `['g1', ['g2']]` and
2565
+ * not nothing; the title they typed is theirs. What is nothing is the run of
2566
+ * title strings left behind by a group this removal itself emptied, and that is
2567
+ * the only case `emptiedMemberPath` climbs through.
2568
+ *
2569
+ * Because a nested group counts whatever is inside it, this needs no recursion,
2570
+ * and a lane two levels down can no longer be lost to a removal one level up.
2571
+ */
2572
+ function holdsContent(node) {
2573
+ return isLane(node) || Array.isArray(node);
2574
+ }
2575
+
2576
+ /** A new document with the member at `path` gone, sharing everything it does not reach. */
2577
+ function withoutPath(node, path) {
2578
+ const seg = path[0];
2579
+ if (path.length === 1) {
2580
+ if (Array.isArray(node)) {
2581
+ const out = node.slice();
2582
+ out.splice(seg, 1);
2583
+ return out;
2584
+ }
2585
+ const out = copyObject(node);
2586
+ delete out[seg];
2587
+ return out;
2588
+ }
2589
+ return replaceSegment(node, seg, withoutPath(node[seg], path.slice(1)));
2590
+ }
2591
+
2592
+ /** A new document with `value` spliced in at `path`, sharing everything else. */
2593
+ function insertAtPath(node, path, value) {
2594
+ const seg = path[0];
2595
+ if (path.length === 1) {
2596
+ const out = node.slice();
2597
+ out.splice(seg, 0, value);
2598
+ return out;
2599
+ }
2600
+ return replaceSegment(node, seg, insertAtPath(node[seg], path.slice(1), value));
2601
+ }
2602
+
2603
+ function replaceSegment(node, seg, child) {
2604
+ if (Array.isArray(node)) {
2605
+ const out = node.slice();
2606
+ out[seg] = child;
2607
+ return out;
2608
+ }
2609
+ const out = copyObject(node);
2610
+ assign(out, seg, child);
2611
+ return out;
2612
+ }
2613
+
2614
+ /** Rename the lane at display position `at`. */
2615
+ function renameLane(doc, at, name) {
2616
+ if (typeof name !== 'string') return doc;
2617
+ return mapLane(doc, at, function (lane) {
2618
+ const next = copyObject(lane);
2619
+ next.name = name;
2620
+ return next;
2621
+ });
2622
+ }
2623
+
2624
+ module.exports = {
2625
+ parseSource, patchSource,
2626
+ expandWave, collapseWave, levelsOf,
2627
+ setCell, setCellRange, insertCycles, deleteCycles, copyCycles, pasteCycles,
2628
+ addLane, removeLane, moveLane, renameLane,
2629
+ lanePath, lanePaths, laneInsertPath, laneRemovePath, laneRemovePaths,
2630
+ MAX_DEPTH,
2631
+ };