rapier-html 1.2.40 → 1.2.43

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.
@@ -76,7 +76,7 @@ function _rapierValidAuthored(origin) {
76
76
  !/[\uD800-\uDFFF]/u.test(row.removed) && !/[\uD800-\uDFFF]/u.test(row.inserted));
77
77
  }
78
78
 
79
- function _rapierRecordIndex(records) {
79
+ function _rapierRecordIndex(records, shared = false) {
80
80
  const byId = new Map(), splices = new Map(), metadata = new Map();
81
81
  for (const record of records) {
82
82
  const id = record?.transaction?.id;
@@ -100,20 +100,41 @@ function _rapierRecordIndex(records) {
100
100
  }
101
101
  cache.set(current, value);
102
102
  for (let i = path.length - 1; i >= 0; i--) {
103
- if (path[i].transaction.reverts && value != null) value = effects
104
- ? Object.fromEntries(Object.entries(value).map(([key, pair]) => [key, {before: pair.after, after: pair.before}]))
105
- : value.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed}));
103
+ if (path[i].transaction.reverts && value != null) {
104
+ value = effects
105
+ ? Object.fromEntries(Object.entries(value).map(([key, pair]) => [key, {before: pair.after, after: pair.before}]))
106
+ : value.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed}));
107
+ if (shared) { for (const row of Object.values(value)) Object.freeze(row); Object.freeze(value); }
108
+ }
106
109
  cache.set(path[i], value);
107
110
  }
108
111
  return value;
109
112
  };
110
- return {byId, splices: record => resolve(record, false), metadata: record => resolve(record, true)};
113
+ const extend = more => { for (const record of more) { const id = record?.transaction?.id; if (!byId.has(id)) byId.set(id, []); byId.get(id).push(record); } };
114
+ return {byId, extend, splices: record => resolve(record, false), metadata: record => resolve(record, true)};
115
+ }
116
+
117
+ const _rapierLedgerIndexes = new WeakMap();
118
+ function _rapierLedgerIndex(ledger) {
119
+ const records = ledger || [];
120
+ if (!Array.isArray(records)) return _rapierRecordIndex(records);
121
+ const held = _rapierLedgerIndexes.get(records);
122
+ if (held && held.records.length === records.length && held.records.at(-1) === records.at(-1) &&
123
+ (held.checked || held.records.every((record, at) => record === records[at]))) {
124
+ if (!held.checked) { held.checked = true; queueMicrotask(() => { held.checked = false; }); }
125
+ return held.index;
126
+ }
127
+ if (!records.every(record => record && typeof record === 'object' && Object.isFrozen(record) && Object.isFrozen(record.transaction ?? {})))
128
+ return _rapierRecordIndex(records);
129
+ const index = _rapierRecordIndex(records, true);
130
+ _rapierLedgerIndexes.set(records, {records: records.slice(), index});
131
+ return index;
111
132
  }
112
133
 
113
134
  function _rapierRecordSplices(entry, ledger) {
114
135
  try {
115
136
  if (entry && Array.isArray(entry.splices)) return entry.splices;
116
- return _rapierRecordIndex(ledger || []).splices(entry);
137
+ return _rapierLedgerIndex(ledger).splices(entry);
117
138
  } catch (_) { return null; }
118
139
  }
119
140
 
@@ -163,7 +184,7 @@ function _rapierRecordMetadata(entry, ledger) {
163
184
  try {
164
185
  if (Object.hasOwn(entry, 'metadata')) return _rapierValidMetadataEffect(entry.metadata) ? entry.metadata : undefined;
165
186
  if (Array.isArray(entry.splices)) return null;
166
- return _rapierRecordIndex(ledger || []).metadata(entry);
187
+ return _rapierLedgerIndex(ledger).metadata(entry);
167
188
  } catch (_) { return undefined; }
168
189
  }
169
190
 
@@ -213,87 +234,89 @@ function _rapierPictureFinish(record, original, earlier, lookup) {
213
234
  });
214
235
  }
215
236
 
216
- function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null) {
217
- try {
218
- if (!Array.isArray(records)) return {ok: false};
219
- const nodes = new Map(), references = new Map(), pictures = new Map(), byId = new Map(), finished = new Set(), earlier = [];
220
- const update = (target, delta) => {
221
- const pending = [[target, delta]];
222
- while (pending.length) {
223
- const [id, change] = pending.pop(), node = nodes.get(id), was = node.active;
224
- node.blockers += change;
225
- if (node.blockers < 0) throw Error('effect');
226
- node.active = node.blockers === 0;
227
- if (was !== node.active) for (const ref of node.references) pending.push([ref, node.active ? 1 : -1]);
228
- }
229
- };
230
- const activity = new Map();
231
- const active = id => {
232
- if (visit) return nodes.get(id).active;
233
- const pending = [[id, 0]];
234
- while (pending.length) {
235
- const frame = pending.at(-1), target = frame[0];
236
- if (activity.has(target)) { pending.pop(); continue; }
237
- const dependents = nodes.get(target).dependents;
238
- if (frame[1] === dependents.length) { activity.set(target, true); pending.pop(); continue; }
239
- const next = dependents[frame[1]];
240
- if (!activity.has(next)) pending.push([next, 0]);
241
- else if (activity.get(next)) { activity.set(target, false); pending.pop(); }
242
- else frame[1]++;
243
- }
244
- return activity.get(id);
245
- };
246
- const lastUndo = new Map(), contexts = new Map(), order = [];
247
- const contextOf = origin => {
248
- if (!origin) return null;
249
- if (!_rapierValidAuthored(origin)) throw Error('origin');
250
- const known = new Set(), pending = origin.basis.slice();
251
- while (pending.length) {
252
- const id = pending.pop(), context = contexts.get(id);
253
- if (!context) throw Error('origin');
254
- if (known.has(id)) continue;
255
- known.add(id);
256
- if (context.basis) pending.push(...context.basis);
257
- else for (let index = 0; index < context.index; index++) known.add(order[index]);
258
- }
259
- const inactive = new Set();
260
- for (let index = order.length - 1; index >= 0; index--) {
261
- const id = order[index];
262
- if (!known.has(id) || inactive.has(id)) continue;
263
- for (const ref of references.get(id)) {
264
- if (!known.has(ref)) throw Error('origin');
265
- inactive.add(ref);
266
- }
237
+ function _rapierHistoryWalker(records, lookup, visit = null, observe = null) {
238
+ let nodes = new Map(), references = new Map(), pictures = new Map();
239
+ const byId = new Map(), finished = new Set(), earlier = [];
240
+ const update = (target, delta) => {
241
+ const pending = [[target, delta]];
242
+ while (pending.length) {
243
+ const [id, change] = pending.pop(), node = nodes.get(id), was = node.active;
244
+ node.blockers += change;
245
+ if (node.blockers < 0) throw Error('effect');
246
+ node.active = node.blockers === 0;
247
+ if (was !== node.active) for (const ref of node.references) pending.push([ref, node.active ? 1 : -1]);
248
+ }
249
+ };
250
+ const activity = new Map();
251
+ const active = id => {
252
+ if (visit) return nodes.get(id).active;
253
+ const pending = [[id, 0]];
254
+ while (pending.length) {
255
+ const frame = pending.at(-1), target = frame[0];
256
+ if (activity.has(target)) { pending.pop(); continue; }
257
+ const dependents = nodes.get(target).dependents;
258
+ if (frame[1] === dependents.length) { activity.set(target, true); pending.pop(); continue; }
259
+ const next = dependents[frame[1]];
260
+ if (!activity.has(next)) pending.push([next, 0]);
261
+ else if (activity.get(next)) { activity.set(target, false); pending.pop(); }
262
+ else frame[1]++;
263
+ }
264
+ return activity.get(id);
265
+ };
266
+ const lastUndo = new Map(), contexts = new Map(), order = [];
267
+ const contextOf = origin => {
268
+ if (!origin) return null;
269
+ if (!_rapierValidAuthored(origin)) throw Error('origin');
270
+ const known = new Set(), pending = origin.basis.slice();
271
+ while (pending.length) {
272
+ const id = pending.pop(), context = contexts.get(id);
273
+ if (!context) throw Error('origin');
274
+ if (known.has(id)) continue;
275
+ known.add(id);
276
+ if (context.basis) pending.push(...context.basis);
277
+ else for (let index = 0; index < context.index; index++) known.add(order[index]);
278
+ }
279
+ const inactive = new Set();
280
+ for (let index = order.length - 1; index >= 0; index--) {
281
+ const id = order[index];
282
+ if (!known.has(id) || inactive.has(id)) continue;
283
+ for (const ref of references.get(id)) {
284
+ if (!known.has(ref)) throw Error('origin');
285
+ inactive.add(ref);
267
286
  }
268
- return {known, inactive};
269
- };
270
- for (const record of records) {
287
+ }
288
+ return {known, inactive};
289
+ };
290
+ const walker = {observe, failed: false};
291
+ walker.add = record => {
292
+ if (walker.failed) return false;
293
+ try {
271
294
  const tx = record?.transaction;
272
- if (!tx || typeof tx.id !== 'string' || !tx.id || nodes.has(tx.id) || (tx.reverts && tx.reapplies)) return {ok: false};
295
+ if (!tx || typeof tx.id !== 'string' || !tx.id || nodes.has(tx.id) || (tx.reverts && tx.reapplies)) { walker.failed = true; return false; }
273
296
  const supplied = tx.sourceTransactionIds || (tx.sourceTransactionId ? [tx.sourceTransactionId] : []);
274
297
  if (!Array.isArray(supplied) || new Set(supplied).size !== supplied.length ||
275
- (tx.sourceTransactionIds && tx.sourceTransactionId && (supplied.length !== 1 || supplied[0] !== tx.sourceTransactionId))) return {ok: false};
298
+ (tx.sourceTransactionIds && tx.sourceTransactionId && (supplied.length !== 1 || supplied[0] !== tx.sourceTransactionId))) { walker.failed = true; return false; }
276
299
  let refs = supplied;
277
300
  if (tx.operation === 'document.finish-picture' && tx.sourceTransactionId) {
278
301
  const owner = tx.sourceTransactionId;
279
302
  if (!nodes.has(owner) || !active(owner) || finished.has(owner) ||
280
- !_rapierPictureFinish(record, byId.get(owner), earlier, lookup ||= _rapierRecordIndex(records))) return {ok: false};
303
+ !_rapierPictureFinish(record, byId.get(owner), earlier, lookup ||= _rapierRecordIndex(records))) { walker.failed = true; return false; }
281
304
  pictures.set(tx.id, owner); finished.add(owner); refs = [];
282
305
  }
283
306
  if (tx.reverts || tx.reapplies) {
284
307
  const target = tx.reverts || tx.reapplies;
285
- if (!nodes.has(target) || (supplied.length && (supplied.length !== 1 || supplied[0] !== target))) return {ok: false};
308
+ if (!nodes.has(target) || (supplied.length && (supplied.length !== 1 || supplied[0] !== target))) { walker.failed = true; return false; }
286
309
  if (tx.reverts) refs = [target];
287
310
  else {
288
311
  const context = contextOf(record.authored || tx.authored);
289
312
  const undo = (lastUndo.get(target) || []).findLast(id => context
290
313
  ? context.known.has(id) && !context.inactive.has(id) : active(id));
291
- if (!undo) return {ok: false};
314
+ if (!undo) { walker.failed = true; return false; }
292
315
  refs = [undo];
293
316
  }
294
317
  }
295
- if (refs.some(id => typeof id !== 'string' || !nodes.has(id) || pictures.has(id))) return {ok: false};
296
- if (tx.actor?.kind === 'agent' && refs.some(id => byId.get(id).transaction.actor?.kind !== 'agent')) return {ok: false};
318
+ if (refs.some(id => typeof id !== 'string' || !nodes.has(id) || pictures.has(id))) { walker.failed = true; return false; }
319
+ if (tx.actor?.kind === 'agent' && refs.some(id => byId.get(id).transaction.actor?.kind !== 'agent')) { walker.failed = true; return false; }
297
320
  nodes.set(tx.id, {active: true, blockers: 0, references: refs, dependents: []}); references.set(tx.id, refs.slice());
298
321
  byId.set(tx.id, record); earlier.push(record);
299
322
  for (const ref of refs) {
@@ -308,23 +331,45 @@ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null
308
331
  lastUndo.get(tx.reverts).push(tx.id);
309
332
  }
310
333
  if (visit) visit(record, nodes, refs);
311
- if (observe) observe(record, refs, active);
312
- }
313
- if (!visit) for (let i = order.length - 1; i >= 0; i--) {
314
- const node = nodes.get(order[i]); node.active = node.blockers === 0;
315
- if (node.active) for (const ref of node.references) nodes.get(ref).blockers++;
334
+ if (walker.observe) walker.observe(record, refs, active);
335
+ return true;
336
+ } catch (_) { walker.failed = true; return false; }
337
+ };
338
+ walker.finish = () => {
339
+ if (walker.failed) return {ok: false};
340
+ const blockers = new Map(), effectiveActIds = [];
341
+ for (let i = order.length - 1; i >= 0; i--) {
342
+ const id = order[i], node = nodes.get(id), on = visit ? node.active : !(blockers.get(id) > 0);
343
+ if (!visit && on) for (const ref of node.references) blockers.set(ref, (blockers.get(ref) || 0) + 1);
344
+ if (on && !pictures.has(id)) effectiveActIds.push(id);
316
345
  }
317
- const effectiveActIds = [...nodes].filter(([id, node]) => node.active && !pictures.has(id)).map(([id]) => id);
318
- return {ok: true, effectiveActIds, active: new Set(effectiveActIds), references, pictures};
319
- } catch (_) { return {ok: false}; }
346
+ effectiveActIds.reverse();
347
+ const result = {ok: true, effectiveActIds, active: new Set(effectiveActIds), references, pictures};
348
+ walker.shared = true;
349
+ return result;
350
+ };
351
+ const add = walker.add;
352
+ walker.add = record => {
353
+ if (walker.shared) { references = new Map(references); pictures = new Map(pictures); walker.shared = false; }
354
+ return add(record);
355
+ };
356
+ return walker;
357
+ }
358
+
359
+ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null) {
360
+ if (!Array.isArray(records)) return {ok: false};
361
+ const walker = _rapierHistoryWalker(records, lookup, visit, observe);
362
+ for (const record of records) if (!walker.add(record)) return {ok: false};
363
+ return walker.finish();
320
364
  }
321
365
 
322
366
  function _rapierHistoryEffects(records, visit = null) {
323
367
  return _rapierWalkHistoryEffects(records, null, visit);
324
368
  }
325
369
 
326
- function _rapierMetadataProof(lookup) {
327
- const values = {}, required = {}, assignments = {filename: [], docKind: []};
370
+ function _rapierMetadataProof(lookup, from = null) {
371
+ const values = {...from?.values}, required = {...from?.required},
372
+ assignments = {filename: [...(from?.assignments.filename || [])], docKind: [...(from?.assignments.docKind || [])]};
328
373
  const requireInitial = (key, value) => {
329
374
  if (Object.hasOwn(required, key) && required[key] !== value) throw Error('metadata_effect');
330
375
  required[key] = value;
@@ -351,7 +396,7 @@ function _rapierMetadataProof(lookup) {
351
396
  if (!_rapierValidMetadata(initial) || Object.keys(required).some(key => initial[key] !== required[key])) return null;
352
397
  return {...initial, ...values};
353
398
  };
354
- return {observe, replay};
399
+ return {observe, replay, fork: other => _rapierMetadataProof(other || lookup, {values, required, assignments})};
355
400
  }
356
401
 
357
402
  function _rapierMetadataState(initial, records, effects = _rapierHistoryEffects(records)) {
@@ -458,16 +503,79 @@ const _rapierPrepareHistoryRecords = (() => {
458
503
  frame.descriptors = descriptors; frame.fields = fields;
459
504
  frame.leaving = true; active.add(value); pending.push(frame);
460
505
  for (let at = fields.length - 1; at >= 0; at--) {
461
- const key = fields[at]; pending.push({value: descriptors[key].value, parent: frame, key, depth: frame.depth + 1});
506
+ const key = fields[at], child = descriptors[key].value;
507
+ const prior = array && child && typeof child === 'object' ? copies.get(child) : undefined;
508
+ if (prior?.immutable && !active.has(child) && frame.depth + 1 + prior.height <= 64) {
509
+ finish({parent: frame, key}, prior.copy, true, prior.height); continue;
510
+ }
511
+ pending.push({value: child, parent: frame, key, depth: frame.depth + 1});
462
512
  }
463
513
  }
464
514
  return result.value;
465
515
  };
516
+ let resumable = [];
517
+ const rowOf = (lookup, record) => {
518
+ const splices = lookup.splices(record), metadata = lookup.metadata(record);
519
+ if (splices && !Object.isFrozen(splices)) {
520
+ for (const row of splices) Object.freeze(row);
521
+ Object.freeze(splices);
522
+ }
523
+ if (metadata && !Object.isFrozen(metadata)) {
524
+ for (const pair of Object.values(metadata)) Object.freeze(pair);
525
+ Object.freeze(metadata);
526
+ }
527
+ return Object.freeze({record, splices, metadata});
528
+ };
529
+ const observe = (records, basis, byId, rows, effects, proof) => {
530
+ for (const refs of effects.references.values()) Object.freeze(refs);
531
+ const effectView = Object.freeze({ok: true, effectiveActIds: Object.freeze(effects.effectiveActIds),
532
+ active: view(effects.active), references: view(effects.references), pictures: view(effects.pictures)});
533
+ const observation = Object.freeze({ok: effects.ok, records, rows: Object.freeze(rows), effects: effectView, byId: view(byId),
534
+ replayMetadata: initial => effects.ok ? proof.replay(initial) : null,
535
+ validRecord: (index, documentAuthority, maxDocumentBytes, utf8Length, allowGap = false) => {
536
+ if (!effects.ok || !Number.isSafeInteger(index) || index < 0 || index >= records.length) return false;
537
+ const row = rows[index], tx = row.record.transaction;
538
+ return _rapierCheckLedgerRecord(row.record, records[index - 1] || null, documentAuthority, maxDocumentBytes, utf8Length, allowGap,
539
+ {...row, duplicate: false, semantic: !!(tx.reverts || tx.reapplies || tx.sourceTransactionId || tx.sourceTransactionIds?.length)});
540
+ }});
541
+ prepared = new WeakMap([[records, {basis, observation}]]);
542
+ return observation;
543
+ };
544
+ const authorityOf = state => state.records[0]?.transaction?.documentAuthority;
545
+ const keep = state => { resumable = [state, ...resumable.filter(row => row !== state && row.records[0] !== state.records[0] &&
546
+ authorityOf(row) === authorityOf(state))].slice(0, 4); };
547
+ const extend = records => {
548
+ const base = resumable.find(row => row.records.length < records.length && row.records[0] === records[0] &&
549
+ row.records.every((record, at) => record === records[at]));
550
+ if (!base) return null;
551
+ resumable = resumable.filter(row => row !== base);
552
+ const more = records.slice(base.records.length), byId = new Map(base.byId);
553
+ for (const record of more) {
554
+ const id = record?.transaction?.id;
555
+ if (typeof id !== 'string' || !id || byId.has(id)) return null;
556
+ byId.set(id, record);
557
+ }
558
+ const {lookup, walker} = base;
559
+ lookup.extend(more);
560
+ const rows = base.rows.concat(more.map(record => rowOf(lookup, record)));
561
+ const proof = base.proof.fork(lookup);
562
+ walker.observe = proof.observe;
563
+ for (const record of more) if (!walker.add(record)) return null;
564
+ const effects = walker.finish();
565
+ if (!effects.ok) return null;
566
+ if (rows.every(row => row.splices && row.metadata !== undefined)) keep({records, lookup, walker, proof, byId, rows});
567
+ return observe(records, records, byId, rows, effects, proof);
568
+ };
466
569
  return (input, earlier = input) => {
467
570
  try {
468
571
  if (!Array.isArray(input) || !Array.isArray(earlier)) return {ok: false};
469
572
  const records = capture(input), basis = input === earlier ? records : capture(earlier), previous = prepared.get(records);
470
573
  if (previous?.basis === basis) return previous.observation;
574
+ if (basis === records) {
575
+ let grown = null;
576
+ try { grown = extend(records); } catch (_) { grown = null; }
577
+ if (grown) return grown;
578
+ }
471
579
  const byId = new Map();
472
580
  for (const record of records) {
473
581
  const id = record?.transaction?.id;
@@ -475,41 +583,24 @@ const _rapierPrepareHistoryRecords = (() => {
475
583
  byId.set(id, record);
476
584
  }
477
585
  const lookup = _rapierRecordIndex(basis), proof = _rapierMetadataProof(lookup);
478
- const rows = records.map(record => {
479
- const splices = lookup.splices(record), metadata = lookup.metadata(record);
480
- if (splices && !Object.isFrozen(splices)) {
481
- for (const row of splices) Object.freeze(row);
482
- Object.freeze(splices);
483
- }
484
- if (metadata && !Object.isFrozen(metadata)) {
485
- for (const pair of Object.values(metadata)) Object.freeze(pair);
486
- Object.freeze(metadata);
487
- }
488
- return Object.freeze({record, splices, metadata});
489
- });
490
- const effects = _rapierWalkHistoryEffects(records, lookup, null, proof.observe);
586
+ const rows = records.map(record => rowOf(lookup, record));
587
+ const walker = _rapierHistoryWalker(records, lookup, null, proof.observe);
588
+ for (const record of records) if (!walker.add(record)) return {ok: false};
589
+ const effects = walker.finish();
491
590
  if (!effects.ok) return {ok: false};
492
- for (const refs of effects.references.values()) Object.freeze(refs);
493
- const effectView = Object.freeze({ok: true, effectiveActIds: Object.freeze(effects.effectiveActIds),
494
- active: view(effects.active), references: view(effects.references), pictures: view(effects.pictures)});
495
- const observation = Object.freeze({ok: effects.ok, records, rows: Object.freeze(rows), effects: effectView, byId: view(byId),
496
- replayMetadata: initial => effects.ok ? proof.replay(initial) : null,
497
- validRecord: (index, documentAuthority, maxDocumentBytes, utf8Length, allowGap = false) => {
498
- if (!effects.ok || !Number.isSafeInteger(index) || index < 0 || index >= records.length) return false;
499
- const row = rows[index], tx = row.record.transaction;
500
- return _rapierCheckLedgerRecord(row.record, records[index - 1] || null, documentAuthority, maxDocumentBytes, utf8Length, allowGap,
501
- {...row, duplicate: false, semantic: !!(tx.reverts || tx.reapplies || tx.sourceTransactionId || tx.sourceTransactionIds?.length)});
502
- }});
503
- prepared = new WeakMap([[records, {basis, observation}]]); return observation;
591
+ if (basis === records && rows.every(row => row.splices && row.metadata !== undefined))
592
+ keep({records, lookup, walker, proof, byId, rows});
593
+ return observe(records, basis, byId, rows, effects, proof);
504
594
  } catch (_) { return {ok: false}; }
505
595
  };
506
596
  })();
507
597
 
508
598
  const _rapierJournalRecord = (() => {
509
599
  const copies = new WeakMap();
600
+ const fixed = value => { if (value && typeof value === 'object') { for (const key of Object.keys(value)) fixed(value[key]); Object.freeze(value); } return value; };
510
601
  return record => {
511
602
  let copy = copies.get(record);
512
- if (!copy) { copy = JSON.parse(JSON.stringify(record)); copies.set(record, copy); }
603
+ if (!copy) { copy = fixed(JSON.parse(JSON.stringify(record))); copies.set(record, copy); }
513
604
  return copy;
514
605
  };
515
606
  })();
@@ -100,7 +100,7 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
100
100
  return end < body.length ? sourceOffset(end + 1) : undefined;
101
101
  }});
102
102
  let lines = 0;
103
- for (let at = 0; at < body.length; at++) if (body.charCodeAt(at) === 10) lines++;
103
+ for (let at = body.indexOf('\n'); at !== -1; at = body.indexOf('\n', at + 1)) lines++;
104
104
  if (lines <= BLOCK_WINDOW) {
105
105
  state = new parser.block.State(body, parser, env, tokens);
106
106
  state.rapierSourceSlice = (from, to) => source.slice(sourceOffset(from), sourceOffset(to));
@@ -179,8 +179,7 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
179
179
  }
180
180
  }
181
181
 
182
- const PERCENT_PAYLOAD = Array.from({length: 128}, (_, code) => code >= 48 && code <= 57 || code >= 65 && code <= 90 ||
183
- code >= 97 && code <= 122 || '-._~:/?@+$,;='.includes(String.fromCharCode(code)));
182
+ const INVALID_PERCENT_PAYLOAD = /[^A-Za-z0-9._~:\/?@+$,;=%-]/;
184
183
  const isHexUnit = code => code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
185
184
  let lastScan = null;
186
185
  export function dataImage(url) {
@@ -197,13 +196,11 @@ function scanDataImage(url) {
197
196
  ...(prepared.encoding ? {encoding: prepared.encoding} : {})};
198
197
  if (/^data:image\/svg\+xml,/i.test(url)) {
199
198
  const payloadStart = 19;
200
- let byteLength = 0;
201
- for (let at = payloadStart; at < url.length; at++, byteLength++) {
202
- const code = url.charCodeAt(at);
203
- if (code === 37) {
204
- if (!isHexUnit(url.charCodeAt(at + 1)) || !isHexUnit(url.charCodeAt(at + 2))) return null;
205
- at += 2;
206
- } else if (!(code < 128 && PERCENT_PAYLOAD[code])) return null;
199
+ if (INVALID_PERCENT_PAYLOAD.test(url)) return null;
200
+ let byteLength = url.length - payloadStart;
201
+ for (let at = url.indexOf('%', payloadStart); at !== -1; at = url.indexOf('%', at + 3)) {
202
+ if (!isHexUnit(url.charCodeAt(at + 1)) || !isHexUnit(url.charCodeAt(at + 2))) return null;
203
+ byteLength -= 2;
207
204
  }
208
205
  return byteLength && byteLength <= IMAGE_LIMITS.bytes
209
206
  ? {codec: 'image/svg+xml', byteLength, payloadStart, encoding: 'percent'} : null;
@@ -215,7 +215,7 @@ export function willTransition(beforeText, afterText, splices, markerSplices = s
215
215
  return null;
216
216
  }
217
217
 
218
- export function evaluateWill(beforeText, splices) {
218
+ export function evaluateWill(beforeText, splices, {restores = null} = {}) {
219
219
  const invalid = rule => ({outcome: 'invalid', reason: 'precondition', rule});
220
220
  if (typeof beforeText !== 'string' || !beforeText.isWellFormed()) return invalid('invalid_unicode');
221
221
  if (!Array.isArray(splices)) return invalid('malformed_splice');
@@ -231,7 +231,7 @@ export function evaluateWill(beforeText, splices) {
231
231
  if (afterText.slice(row.pos, end) !== row.removed) return invalid('source_mismatch');
232
232
  afterText = afterText.slice(0, row.pos) + row.inserted + afterText.slice(end);
233
233
  }
234
- const refusal = splices.length && willTransition(beforeText, afterText, splices);
234
+ const refusal = splices.length && willTransition(beforeText, afterText, splices, splices, {restores});
235
235
  return refusal ? {outcome: 'refused', reason: 'document_law', ...refusal} : {outcome: 'applied'};
236
236
  }
237
237