rapier-markdown-kit 1.1.102 → 1.1.104

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.
Files changed (60) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +213 -53
  3. package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
  4. package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
  5. package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
  6. package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
  7. package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
  8. package/dist/agent/vendor/pretext/LICENSE +208 -0
  9. package/dist/agent/vendor/pretext/NOTICE +26 -13
  10. package/dist/agent/vendor/pretext/SOURCE.json +213 -45
  11. package/dist/agent/vendor/pretext/analysis.js +675 -1224
  12. package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
  13. package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
  14. package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
  15. package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
  16. package/dist/agent/vendor/pretext/graphemes.js +98 -0
  17. package/dist/agent/vendor/pretext/han-kerning.js +253 -0
  18. package/dist/agent/vendor/pretext/layout.js +352 -290
  19. package/dist/agent/vendor/pretext/line-break.js +1154 -588
  20. package/dist/agent/vendor/pretext/line-breaks.js +892 -0
  21. package/dist/agent/vendor/pretext/line-text.js +81 -39
  22. package/dist/agent/vendor/pretext/measurement.js +1105 -176
  23. package/dist/agent/vendor/pretext/prepare.js +807 -0
  24. package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
  25. package/dist/agent/will.mjs +2 -0
  26. package/dist/kit/assets.mjs +5 -1
  27. package/dist/kit/conformance/run.mjs +4 -4
  28. package/dist/kit/export-fonts.mjs +26 -11
  29. package/dist/kit/index.mjs +1 -0
  30. package/dist/kit/ledger/carried.mjs +9 -7
  31. package/dist/kit/ledger/data.mjs +73 -13
  32. package/dist/kit/ledger/format.mjs +166 -53
  33. package/dist/kit/ledger/hash.mjs +75 -18
  34. package/dist/kit/ledger/history.mjs +143 -115
  35. package/dist/kit/ledger/inverse-source.mjs +158 -19
  36. package/dist/kit/ledger/journal-records.mjs +292 -67
  37. package/dist/kit/model.mjs +1 -1
  38. package/dist/kit/pdf-glyphs.mjs +4 -4
  39. package/dist/kit/render-markdown.mjs +259 -122
  40. package/dist/kit/render-sanitize.mjs +24 -4
  41. package/dist/kit/render-work.mjs +99 -20
  42. package/dist/kit/render.mjs +101 -65
  43. package/dist/kit/typography.mjs +2 -0
  44. package/dist/layout/hyphenation.mjs +127 -0
  45. package/dist/layout/line-plan.mjs +88 -96
  46. package/dist/layout/model.mjs +61 -80
  47. package/dist/layout/paragraph-fit.mjs +191 -0
  48. package/dist/layout/preparation.mjs +373 -0
  49. package/dist/layout/prepared-paragraph.mjs +223 -0
  50. package/dist/layout/typography.mjs +345 -111
  51. package/dist/packages/rapier-diff/index.mjs +288 -196
  52. package/dist/spec/document-fonts.mjs +13 -4
  53. package/dist/spec/md-assets.mjs +149 -30
  54. package/dist/spec/md-layout.mjs +5 -2
  55. package/dist/spec/md-marks.mjs +218 -3
  56. package/dist/spec/md-source.mjs +122 -0
  57. package/package.json +2 -1
  58. package/style.css +9 -0
  59. package/dist/agent/vendor/pretext/bidi.js +0 -151
  60. package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
@@ -1,12 +1,20 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // The editor's path-dependent root, plus synchronous SHA-256 for file readers (including offline wrappers).
3
3
  export function textRoot(text) {
4
- let fnv = 0x811c9dc5, a = 1, b = 0;
5
- for (let i = 0; i < text.length; i++) {
6
- const c = text.charCodeAt(i);
7
- fnv = Math.imul(fnv ^ c, 16777619); a = (a + c) % 65521; b = (b + a) % 65521;
4
+ return textRootParts([text]);
5
+ }
6
+ // The same UTF-16 root over shared source pieces, without joining a historical body.
7
+ export function textRootParts(parts) {
8
+ let fnv = 0x811c9dc5, a = 1, b = 0, length = 0;
9
+ for (const text of parts) {
10
+ length += text.length;
11
+ for (let i = 0; i < text.length;) {
12
+ const end = Math.min(text.length, i + 4096);
13
+ for (; i < end; i++) { const c = text.charCodeAt(i); fnv = Math.imul(fnv ^ c, 16777619); a += c; b += a; }
14
+ a %= 65521; b %= 65521;
15
+ }
8
16
  }
9
- return text.length + ':' + (fnv >>> 0).toString(36) + ':' + (((b << 16) | a) >>> 0).toString(36);
17
+ return length + ':' + (fnv >>> 0).toString(36) + ':' + (((b << 16) | a) >>> 0).toString(36);
10
18
  }
11
19
  export function rootAfter(root, row) {
12
20
  return textRoot(String(root || '') + ':' + row.pos + ':' + textRoot(row.removed) + ':' + textRoot(row.inserted));
@@ -22,26 +30,75 @@ const K = new Uint32Array([
22
30
  0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,
23
31
  ]);
24
32
  const rotate = (x, n) => (x >>> n) | (x << (32 - n));
25
- export function sha256(text) {
26
- const bytes = new TextEncoder().encode(text);
27
- const padded = new Uint8Array(Math.ceil((bytes.length + 9) / 64) * 64);
28
- padded.set(bytes); padded[bytes.length] = 0x80;
29
- const data = new DataView(padded.buffer), bits = bytes.length * 8;
30
- data.setUint32(padded.length - 8, Math.floor(bits / 0x100000000)); data.setUint32(padded.length - 4, bits >>> 0);
31
- const h = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]), w = new Uint32Array(64);
32
- for (let offset = 0; offset < padded.length; offset += 64) {
33
- for (let i = 0; i < 16; i++) w[i] = data.getUint32(offset + i * 4);
33
+ // A bounded encoder and one compression block. No full UTF-8 or padded-input copy.
34
+ // Updates retain a trailing high surrogate so an arbitrary string boundary stays exact.
35
+ export function createSHA256() {
36
+ const h = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]);
37
+ const w = new Uint32Array(64), block = new Uint8Array(64), encoded = new Uint8Array(65536);
38
+ const encoder = new TextEncoder();
39
+ let used = 0, bytes = 0, high = '', finished = false;
40
+ const compress = (input, offset) => {
41
+ for (let i = 0; i < 16; i++) {
42
+ const at = offset + i * 4;
43
+ w[i] = (input[at] << 24) | (input[at + 1] << 16) | (input[at + 2] << 8) | input[at + 3];
44
+ }
34
45
  for (let i = 16; i < 64; i++) {
35
46
  const x = w[i - 15], y = w[i - 2];
36
47
  w[i] = w[i - 16] + (rotate(x, 7) ^ rotate(x, 18) ^ (x >>> 3)) + w[i - 7] + (rotate(y, 17) ^ rotate(y, 19) ^ (y >>> 10));
37
48
  }
38
- let [a,b,c,d,e,f,g,k] = h;
49
+ let a=h[0], b=h[1], c=h[2], d=h[3], e=h[4], f=h[5], g=h[6], k=h[7];
39
50
  for (let i = 0; i < 64; i++) {
40
51
  const t1 = (k + (rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0;
41
52
  const t2 = ((rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
42
53
  k = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
43
54
  }
44
- for (const [i, value] of [a,b,c,d,e,f,g,k].entries()) h[i] += value;
45
- }
46
- return [...h].map(x => x.toString(16).padStart(8, '0')).join('');
55
+ h[0]+=a; h[1]+=b; h[2]+=c; h[3]+=d; h[4]+=e; h[5]+=f; h[6]+=g; h[7]+=k;
56
+ };
57
+ const feed = (input, size) => {
58
+ bytes += size;
59
+ let at = 0;
60
+ if (used) {
61
+ const take = Math.min(64 - used, size);
62
+ block.set(input.subarray(0, take), used); used += take; at = take;
63
+ if (used === 64) { compress(block, 0); used = 0; }
64
+ }
65
+ for (; at + 64 <= size; at += 64) compress(input, at);
66
+ if (at < size) { block.set(input.subarray(at, size), 0); used = size - at; }
67
+ };
68
+ const write = text => {
69
+ for (let at = 0; at < text.length;) {
70
+ // Bound both the substring and byte allocation; encodeInto never cuts a scalar.
71
+ let end = Math.min(text.length, at + 32768);
72
+ if (end < text.length && (text.charCodeAt(end - 1) & 0xfc00) === 0xd800 && (text.charCodeAt(end) & 0xfc00) === 0xdc00) end--;
73
+ const part = encoder.encodeInto(text.slice(at, end), encoded);
74
+ feed(encoded, part.written); at += part.read;
75
+ }
76
+ };
77
+ return {
78
+ update(value) {
79
+ if (finished) throw new Error('SHA-256 is finished');
80
+ let text = high + String(value === undefined ? '' : value); high = '';
81
+ if (text.length && (text.charCodeAt(text.length - 1) & 0xfc00) === 0xd800) { high = text.slice(-1); text = text.slice(0, -1); }
82
+ write(text); return this;
83
+ },
84
+ digest() {
85
+ if (finished) throw new Error('SHA-256 is finished');
86
+ if (high) { write(high); high = ''; }
87
+ finished = true;
88
+ const bits = bytes * 8;
89
+ block[used++] = 0x80; block.fill(0, used);
90
+ if (used > 56) { compress(block, 0); block.fill(0); }
91
+ const view = new DataView(block.buffer);
92
+ view.setUint32(56, Math.floor(bits / 0x100000000)); view.setUint32(60, bits >>> 0);
93
+ compress(block, 0);
94
+ return Array.from(h, x => x.toString(16).padStart(8, '0')).join('');
95
+ },
96
+ get bytes() { return bytes; },
97
+ };
98
+ }
99
+ export function sha256Parts(parts) {
100
+ const hash = createSHA256();
101
+ for (const part of parts) hash.update(part);
102
+ return hash.digest();
47
103
  }
104
+ export function sha256(text) { return createSHA256().update(text).digest(); }
@@ -1,8 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Transient projections and selective inverses of the existing canonical journal.
3
- import {_rapierTransformSplices, _rapierRecordSplices, _rapierRecordMetadata, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierReplayMetadata} from './journal-records.mjs';
4
- import {projectInverseSource} from './inverse-source.mjs';
5
- import {textRoot, rootAfter} from './hash.mjs';
3
+ import {_rapierTransformSplices, _rapierRecordSplices, _rapierRecordMetadata, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierReplayMetadata, _rapierPrepareHistoryRecords} from './journal-records.mjs';
4
+ import {projectInverseSource, HistorySource} from './inverse-source.mjs';
5
+ import {textRootParts, rootAfter} from './hash.mjs';
6
6
  import {authoredHistory} from './transport.mjs';
7
7
  import {plainData} from './data.mjs';
8
8
 
@@ -12,16 +12,16 @@ const historyBoundary = (source, at) => historyInteger(at) && at <= source.lengt
12
12
  (source.charCodeAt(at - 1) & 0xFC00) === 0xD800 && (source.charCodeAt(at) & 0xFC00) === 0xDC00);
13
13
 
14
14
  function historyAct(record, effects = null) {
15
- const tx = record.transaction;
16
- return {id: tx.id, actor: {...tx.actor}, createdAt: tx.createdAt, baseRevision: tx.baseRevision, revision: tx.revision,
17
- operation: tx.operation || 'document.change',
15
+ const tx = record.transaction, copy = value => value && typeof value === 'object' ? plainData(value) : value;
16
+ return {id: tx.id, actor: Object.fromEntries(Object.entries(tx.actor).map(([key, value]) => [key, copy(value)])), createdAt: tx.createdAt, baseRevision: tx.baseRevision, revision: tx.revision,
17
+ operation: copy(tx.operation) || 'document.change',
18
18
  ...(tx.turnId ? {turnId: tx.turnId} : {}),
19
- ...(tx.contribution ? {contribution: tx.contribution} : {}),
19
+ ...(tx.contribution ? {contribution: copy(tx.contribution)} : {}),
20
20
  ...(tx.sourceTransactionId ? {sourceTransactionId: tx.sourceTransactionId} : {}),
21
21
  ...(tx.sourceTransactionIds?.length ? {sourceTransactionIds: tx.sourceTransactionIds.slice()} : {}),
22
22
  ...(tx.reverts ? {reverts: tx.reverts} : {}), ...(tx.reapplies ? {reapplies: tx.reapplies} : {}),
23
- ...(record.changeSet && Object.hasOwn(record.changeSet, 'label') ? {label: record.changeSet.label} : {}),
24
- ...(Array.isArray(tx.affectedBlockIds) ? {affectedBlockIds: tx.affectedBlockIds.slice()} : {}),
23
+ ...(record.changeSet && Object.hasOwn(record.changeSet, 'label') ? {label: copy(record.changeSet.label)} : {}),
24
+ ...(Array.isArray(tx.affectedBlockIds) ? {affectedBlockIds: tx.affectedBlockIds.map(copy)} : {}),
25
25
  ...(record.metadata ? {metadata: plainData(record.metadata)} : {}),
26
26
  ...(effects ? {active: effects.active.has(tx.id)} : {})};
27
27
  }
@@ -74,76 +74,75 @@ export function pictureHistorySplices({source, records}, target, redo) {
74
74
  try { return plan(true); } catch (_) { return null; }
75
75
  }
76
76
 
77
- // Reading an admitted repair can recover pre-existing lone UTF-16 units. This is
78
- // not a document mutation; the forward replay and roots still validate every row.
79
- function historyPreviousSource(source, splices) {
80
- let value = source;
81
- for (let index = splices.length - 1; index >= 0; index--) {
82
- const row = splices[index];
83
- if (!row || !historyInteger(row.pos) || typeof row.removed !== 'string' || typeof row.inserted !== 'string' ||
84
- row.pos > value.length || row.inserted.length > value.length - row.pos ||
85
- value.slice(row.pos, row.pos + row.inserted.length) !== row.inserted) return null;
86
- value = value.slice(0, row.pos) + row.removed + value.slice(row.pos + row.inserted.length);
87
- }
88
- return value;
89
- }
90
-
91
- // This is a projection of the existing records. No source snapshots or provenance survive the call.
77
+ // Keep only the latest source observation. Inactive live documents must not retain
78
+ // every revision's source tree. The weak key also lets the latest input be collected.
79
+ // Mutable caller data is compared/captured before this cache can be consulted.
80
+ let preparedHistories = new WeakMap();
92
81
  function prepareHistory(input) {
93
82
  try {
94
83
  if (!input || typeof input.source !== 'string' || !Array.isArray(input.records) || !_rapierValidMetadata(input.metadata)) return historyFailure('history_invalid');
95
- const source = input.source, records = plainData(input.records), ids = new Set(), rows = [];
96
- const effects = _rapierHistoryEffects(records);
97
- if (!effects.ok) return historyFailure('history_invalid');
84
+ const source = input.source, metadata = {...input.metadata}, index = _rapierPrepareHistoryRecords(input.records);
85
+ if (!index.ok) return historyFailure('history_invalid');
86
+ const {records, effects} = index;
98
87
  const revision = input.revision ?? records.at(-1)?.transaction?.revision ?? 0;
99
- if (!historyInteger(revision)) return historyFailure('history_invalid');
100
- let prior = input.earliestRevision ?? records[0]?.transaction?.baseRevision ?? revision, complete = prior === 0;
101
- if (!historyInteger(prior)) return historyFailure('history_invalid');
102
- for (const record of records) {
103
- const tx = record?.transaction, actor = tx?.actor;
104
- if (!tx || typeof tx.id !== 'string' || !tx.id || ids.has(tx.id) || !historyInteger(tx.baseRevision) ||
88
+ let prior = input.earliestRevision ?? records[0]?.transaction?.baseRevision ?? revision;
89
+ if (!historyInteger(revision) || !historyInteger(prior)) return historyFailure('history_invalid');
90
+ const earliest = prior, suppliedComplete = input.complete !== false, cached = preparedHistories.get(records);
91
+ if (cached && cached.source === source && cached.revision === revision && cached.earliest === earliest &&
92
+ cached.suppliedComplete === suppliedComplete && cached.metadata.filename === metadata.filename && cached.metadata.docKind === metadata.docKind) return cached;
93
+ let complete = prior === 0;
94
+ const rows = [], byId = new Map();
95
+ for (const owned of index.rows) {
96
+ const {record, splices, metadata: effect} = owned, tx = record?.transaction, actor = tx?.actor;
97
+ if (!tx || typeof tx.id !== 'string' || !tx.id || byId.has(tx.id) || !historyInteger(tx.baseRevision) ||
105
98
  !historyInteger(tx.revision) || tx.revision !== tx.baseRevision + 1 || tx.baseRevision < prior ||
106
99
  !actor || !['human', 'agent', 'system'].includes(actor.kind) || typeof actor.id !== 'string' || !actor.id ||
107
100
  !(tx.createdAt === null || historyInteger(tx.createdAt)) ||
108
101
  (tx.turnId != null && (typeof tx.turnId !== 'string' || !tx.turnId || tx.turnId.length > 160))) return historyFailure('history_invalid');
109
102
  if (tx.baseRevision !== prior) complete = false;
110
- const splices = _rapierRecordSplices(record, records), metadata = _rapierRecordMetadata(record, records);
111
- if (!splices || metadata === undefined || (!splices.length &&
112
- !Object.values(metadata || {}).some(pair => pair.before !== pair.after) && !effects.references.get(tx.id)?.length)) return historyFailure('history_unavailable');
113
- rows.push({record, splices, metadata}); ids.add(tx.id); prior = tx.revision;
103
+ if (!splices || effect === undefined || (!splices.length &&
104
+ !Object.values(effect || {}).some(pair => pair.before !== pair.after) && !effects.references.get(tx.id)?.length)) return historyFailure('history_unavailable');
105
+ const row = {record, splices, metadata: effect};
106
+ rows.push(row); byId.set(tx.id, row); prior = tx.revision;
114
107
  }
115
108
  if (prior !== revision) return historyFailure('history_unavailable');
116
- let initialSource = source, initialMetadata = {...input.metadata};
117
- for (let index = rows.length - 1; index >= 0; index--) {
118
- initialSource = historyPreviousSource(initialSource, rows[index].splices);
119
- initialMetadata = _rapierTransformMetadata(initialMetadata, rows[index].metadata, true);
120
- if (initialSource === null || !initialMetadata) return historyFailure('history_invalid');
109
+ let initial = new HistorySource(source), initialMetadata = {...metadata};
110
+ for (let at = rows.length - 1; at >= 0; at--) {
111
+ const row = rows[at];
112
+ row.afterSource = initial; row.afterMetadata = initialMetadata;
113
+ initial = initial.before(row.splices);
114
+ initialMetadata = _rapierTransformMetadata(initialMetadata, row.metadata, true);
115
+ if (!initial || !initialMetadata) return historyFailure('history_invalid');
116
+ row.beforeSource = initial; row.beforeMetadata = initialMetadata;
121
117
  }
122
- const earliestRevision = rows[0]?.record.transaction.baseRevision ?? revision;
123
- let replay = initialSource, root = earliestRevision === 0 ? textRoot(initialSource) : null, rootRevision = earliestRevision;
118
+ const earliestRevision = records[0]?.transaction.baseRevision ?? revision;
119
+ let root = earliestRevision === 0 ? textRootParts(initial.parts()) : null, rootRevision = earliestRevision;
124
120
  for (const {record, splices} of rows) {
125
121
  const hashed = Object.hasOwn(record, 'beforeHash') || Object.hasOwn(record, 'afterHash');
126
122
  if (hashed && (![record.beforeHash, record.afterHash].every(value =>
127
123
  typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value)) ||
128
- (splices.length ? record.beforeHash === record.afterHash : record.beforeHash !== record.afterHash)))
129
- return historyFailure('history_invalid');
130
- // Roots bind revision zero to the full source. A retained suffix has only a path root;
131
- // omitted neutral pairs change that root, so a stated revision gap begins a new chain.
124
+ (splices.length ? record.beforeHash === record.afterHash : record.beforeHash !== record.afterHash))) return historyFailure('history_invalid');
125
+ // Revision gaps begin a new path root; a continuous revision-zero prefix
126
+ // still binds every untouched source byte through the initial text root.
132
127
  if (record.transaction.baseRevision !== rootRevision) root = null;
133
128
  if (hashed) {
134
129
  if (root !== null && record.beforeHash !== root) return historyFailure('history_invalid');
135
130
  root = record.beforeHash;
136
131
  }
137
- replay = _rapierTransformSplices(replay, splices);
138
- if (replay === null) return historyFailure('history_invalid');
139
- if (root !== null) for (const row of splices) root = rootAfter(root, row);
132
+ if (root !== null) for (const splice of splices) root = rootAfter(root, splice);
140
133
  if (hashed && root !== record.afterHash) return historyFailure('history_invalid');
141
134
  rootRevision = record.transaction.revision;
142
135
  }
143
- if (records.some(row => Object.hasOwn(row, 'authored'))) authoredHistory(initialSource, rows.map(({record, splices}) => ({...record, splices})));
144
- const metadata = _rapierReplayMetadata(initialMetadata, records);
145
- if (replay !== source || !metadata || ['filename', 'docKind'].some(key => metadata[key] !== input.metadata[key])) return historyFailure('history_invalid');
146
- return {ok: true, source, initialSource, metadata, initialMetadata, effects, rows, revision, earliestRevision, complete: input.complete !== false && complete && earliestRevision === 0};
136
+ let initialText;
137
+ const initialSource = () => initialText ??= initial.slice();
138
+ if (records.some(row => Object.hasOwn(row, 'authored'))) authoredHistory(initialSource(), rows.map(({record, splices}) => ({...record, splices})));
139
+ const replayedMetadata = index.replayMetadata(initialMetadata);
140
+ if (!replayedMetadata || ['filename', 'docKind'].some(key => replayedMetadata[key] !== metadata[key])) return historyFailure('history_invalid');
141
+ const resolvedRecords = Object.freeze(rows.map(({record, splices}) => record.splices === splices ? record : Object.freeze({...record, splices})));
142
+ const prepared = {ok: true, source, records: resolvedRecords, get initialSource() { return initialSource(); }, initial, metadata, initialMetadata, effects, rows, byId,
143
+ revision, earliest, suppliedComplete, earliestRevision, complete: suppliedComplete && complete && earliestRevision === 0};
144
+ preparedHistories = new WeakMap([[records, prepared]]);
145
+ return prepared;
147
146
  } catch (_) { return historyFailure('history_invalid'); }
148
147
  }
149
148
 
@@ -152,23 +151,17 @@ export function replayHistory(input) {
152
151
  if (!prepared.ok) return prepared;
153
152
  const {source, initialSource, metadata, initialMetadata, revision, earliestRevision, complete, rows, effects} = prepared;
154
153
  const acts = rows.filter(({record}) => !effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record, effects));
155
- return {ok: true, source, initialSource, metadata, initialMetadata, revision, earliestRevision, complete, acts,
154
+ return {ok: true, source, initialSource, metadata: {...metadata}, initialMetadata: {...initialMetadata}, revision, earliestRevision, complete, acts,
156
155
  effectiveActIds: effects.effectiveActIds.slice(), groups: groupHistoryActs(acts)};
157
156
  }
158
157
 
159
158
  export function sourceBefore(input, actId) {
160
159
  const prepared = prepareHistory(input);
161
160
  if (!prepared.ok) return prepared;
162
- let source = prepared.source, metadata = {...prepared.metadata};
163
- for (let index = prepared.rows.length - 1; index >= 0; index--) {
164
- const {record, splices} = prepared.rows[index];
165
- source = historyPreviousSource(source, splices);
166
- if (source === null) return historyFailure('history_invalid');
167
- metadata = _rapierTransformMetadata(metadata, prepared.rows[index].metadata, true);
168
- if (record.transaction.id === actId) return {ok: true, source, metadata, revision: record.transaction.baseRevision,
169
- act: historyAct(record, prepared.effects), complete: prepared.complete, earliestRevision: prepared.earliestRevision};
170
- }
171
- return historyFailure('act_unavailable');
161
+ const row = prepared.byId.get(actId);
162
+ if (!row) return historyFailure('act_unavailable');
163
+ return {ok: true, source: row.beforeSource.slice(), metadata: {...row.beforeMetadata}, revision: row.record.transaction.baseRevision,
164
+ act: historyAct(row.record, prepared.effects), complete: prepared.complete, earliestRevision: prepared.earliestRevision};
172
165
  }
173
166
 
174
167
  function historyRangeBefore(range, splice) {
@@ -186,21 +179,23 @@ function historyRangeBefore(range, splice) {
186
179
 
187
180
  function projectHistoryRange(prepared, from, to, actId) {
188
181
  if (!historyBoundary(prepared.source, from) || !historyBoundary(prepared.source, to) || to < from) return historyFailure('range_invalid');
189
- let text = prepared.source, range = {from, to}, before = null;
182
+ let range = {from, to}, before = null;
190
183
  const acts = [];
191
184
  for (let index = prepared.rows.length - 1; index >= 0; index--) {
192
- const {record, splices} = prepared.rows[index], after = {...range, source: text.slice(range.from, range.to)};
185
+ const {record, splices, beforeSource, afterSource} = prepared.rows[index], afterRange = range;
193
186
  let touched = false;
194
187
  for (let at = splices.length - 1; at >= 0; at--) {
195
188
  const mapped = historyRangeBefore(range, splices[at]); touched ||= mapped.touched;
196
189
  range = {from: mapped.from, to: mapped.to};
197
190
  }
198
- text = historyPreviousSource(text, splices);
199
- if (text === null) return historyFailure('history_invalid');
200
- const previous = {...range, source: text.slice(range.from, range.to)};
201
- if (record.transaction.id === actId) before = previous;
202
- if (!prepared.effects.pictures.has(record.transaction.id) && (touched || record.metadata || prepared.effects.references.get(record.transaction.id)?.length))
203
- acts.push({...historyAct(record, prepared.effects), before: previous, after});
191
+ const include = !prepared.effects.pictures.has(record.transaction.id) &&
192
+ (touched || record.metadata || prepared.effects.references.get(record.transaction.id)?.length);
193
+ if (include || record.transaction.id === actId) {
194
+ const previous = {...range, source: beforeSource.slice(range.from, range.to)};
195
+ if (record.transaction.id === actId) before = previous;
196
+ if (include) acts.push({...historyAct(record, prepared.effects), before: previous,
197
+ after: {...afterRange, source: afterSource.slice(afterRange.from, afterRange.to)}});
198
+ }
204
199
  }
205
200
  if (actId != null && !before) return historyFailure('act_unavailable');
206
201
  acts.reverse();
@@ -220,10 +215,47 @@ export function historyProjections(input, ranges) {
220
215
  if (!prepared.ok) return prepared;
221
216
  if (!Array.isArray(ranges)) return historyFailure('range_invalid');
222
217
  return {ok: true, projections: ranges.map(({from = 0, to = prepared.source.length, actId} = {}) =>
223
- structuredClone(projectHistoryRange(prepared, from, to, actId)))};
218
+ projectHistoryRange(prepared, from, to, actId))};
219
+ }
220
+
221
+ // Block intervals stay ordered under reverse splices. A fixed balanced tree can
222
+ // shift an untouched suffix lazily and visit only intervals crossing the edit.
223
+ function historyBlockIndex(projected) {
224
+ const items = projected.map(block => ({block, range: {from: block.from, to: block.to}, shift: 0}))
225
+ .sort((a, b) => a.range.from - b.range.from);
226
+ const refresh = node => {
227
+ node.minFrom = node.left?.minFrom ?? node.range.from;
228
+ node.maxTo = Math.max(node.range.to, node.left?.maxTo ?? -1, node.right?.maxTo ?? -1);
229
+ };
230
+ const build = (from, to, parent = null) => {
231
+ if (from === to) return null;
232
+ const mid = Math.floor((from + to) / 2), node = items[mid];
233
+ node.parent = parent; node.left = build(from, mid, node); node.right = build(mid + 1, to, node);
234
+ refresh(node); return node;
235
+ };
236
+ const root = build(0, items.length);
237
+ const shift = (node, delta) => {
238
+ if (!node || !delta) return;
239
+ node.range = {from: node.range.from + delta, to: node.range.to + delta};
240
+ node.minFrom += delta; node.maxTo += delta; node.shift += delta;
241
+ };
242
+ const move = (node, splice, touch) => {
243
+ if (!node || node.maxTo < splice.pos) return;
244
+ const end = splice.pos + splice.inserted.length;
245
+ if (node.minFrom > end) { shift(node, splice.removed.length - splice.inserted.length); return; }
246
+ if (node.shift) { shift(node.left, node.shift); shift(node.right, node.shift); node.shift = 0; }
247
+ const mapped = historyRangeBefore(node.range, splice);
248
+ if (mapped.touched) touch(node);
249
+ node.range = {from: mapped.from, to: mapped.to};
250
+ move(node.left, splice, touch); move(node.right, splice, touch); refresh(node);
251
+ };
252
+ return {move: (splice, touch) => move(root, splice, touch), range(node) {
253
+ let delta = 0;
254
+ for (let parent = node.parent; parent; parent = parent.parent) delta += parent.shift;
255
+ return {from: node.range.from + delta, to: node.range.to + delta};
256
+ }};
224
257
  }
225
258
 
226
- // Project all reading places in one replay. Typing only invalidates this transient view.
227
259
  export function historyPlaces(input, blocks) {
228
260
  const prepared = prepareHistory(input);
229
261
  if (!prepared.ok) return prepared;
@@ -231,47 +263,43 @@ export function historyPlaces(input, blocks) {
231
263
  !historyBoundary(prepared.source, block.from) || !historyBoundary(prepared.source, block.to) || block.to < block.from))
232
264
  return historyFailure('range_invalid');
233
265
  const projected = blocks.map(block => ({id: block.id, from: block.from, to: block.to,
234
- source: prepared.source.slice(block.from, block.to), range: {from: block.from, to: block.to}, acts: []}));
235
- let text = prepared.source;
266
+ source: prepared.source.slice(block.from, block.to), acts: []}));
267
+ const ranges = historyBlockIndex(projected);
236
268
  for (let index = prepared.rows.length - 1; index >= 0; index--) {
237
- const {record, splices} = prepared.rows[index], before = historyPreviousSource(text, splices);
238
- if (before === null) return historyFailure('history_invalid');
239
- for (const block of projected) {
240
- const after = {...block.range, source: text.slice(block.range.from, block.range.to)};
241
- let touched = false;
242
- for (let at = splices.length - 1; at >= 0; at--) {
243
- const mapped = historyRangeBefore(block.range, splices[at]);
244
- block.range = {from: mapped.from, to: mapped.to}; touched ||= mapped.touched;
245
- }
246
- const previous = {...block.range, source: before.slice(block.range.from, block.range.to)};
247
- if (touched && previous.source !== after.source && !prepared.effects.pictures.has(record.transaction.id)) block.acts.push({...historyAct(record), before: previous, after});
248
- }
249
- text = before;
250
- }
251
- const acts = prepared.rows.filter(({record}) => !prepared.effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record));
252
- const present = new Map(), effects = new Map();
253
- for (const act of acts) {
254
- const changes = [];
255
- const set = (id, value) => {
256
- const prior = present.get(id) === true;
257
- present.set(id, value); changes.push({id, before: prior, after: value});
258
- };
259
- if (act.reapplies) {
260
- for (const change of effects.get(act.reapplies) || [{id: act.reapplies, after: true}]) set(change.id, change.after);
261
- } else {
262
- const ids = act.sourceTransactionIds || (act.sourceTransactionId ? [act.sourceTransactionId] : act.reverts ? [act.reverts] : []);
263
- for (const id of ids) {
264
- for (const change of (effects.get(id) || [{id, before: false, after: true}]).slice().reverse()) {
265
- if (present.get(change.id) === change.after) set(change.id, change.before);
269
+ const {record, splices, beforeSource, afterSource} = prepared.rows[index], touched = new Map();
270
+ for (let at = splices.length - 1; at >= 0; at--) ranges.move(splices[at], node => {
271
+ if (touched.has(node) || prepared.effects.pictures.has(record.transaction.id)) return;
272
+ // Earlier splices in this reverse walk only shifted this interval. Recover
273
+ // its coordinates at the record's after-state before recording the first touch.
274
+ let after = {...node.range};
275
+ for (let prior = at + 1; prior < splices.length; prior++) {
276
+ const row = splices[prior];
277
+ if (after.from >= row.pos + row.removed.length) {
278
+ const delta = row.inserted.length - row.removed.length;
279
+ after = {from: after.from + delta, to: after.to + delta};
266
280
  }
267
281
  }
282
+ touched.set(node, after);
283
+ });
284
+ for (const [node, afterRange] of touched) {
285
+ const beforeRange = ranges.range(node), previous = {...beforeRange, source: beforeSource.slice(beforeRange.from, beforeRange.to)},
286
+ after = {...afterRange, source: afterSource.slice(afterRange.from, afterRange.to)};
287
+ if (previous.source !== after.source) node.block.acts.push({...historyAct(record), before: previous, after});
268
288
  }
269
- set(act.id, true); effects.set(act.id, changes);
270
289
  }
271
- for (const block of projected) { block.acts.reverse(); delete block.range; }
290
+
291
+ const acts = prepared.rows.filter(({record}) => !prepared.effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record));
292
+ // Native Redo consumes its Undo as navigation while keeping that receipt in
293
+ // the reading history. All material withdrawals use the canonical blockers,
294
+ // including independent inverses holding the same original act suppressed.
295
+ const reversed = new Set();
296
+ for (const {record} of prepared.rows) if (prepared.effects.active.has(record.transaction.id) && !record.transaction.reapplies)
297
+ for (const id of prepared.effects.references.get(record.transaction.id) || []) reversed.add(id);
298
+
299
+ for (const block of projected) block.acts.reverse();
272
300
  return {ok: true, source: prepared.source, initialSource: prepared.initialSource, revision: prepared.revision,
273
301
  earliestRevision: prepared.earliestRevision, complete: prepared.complete, acts, blocks: projected,
274
- reversed: acts.filter(act => present.get(act.id) !== true).map(act => act.id)};
302
+ reversed: acts.filter(act => reversed.has(act.id)).map(act => act.id)};
275
303
  }
276
304
 
277
305
  // Runs retain only exact replacement ancestry. A later replacement owns an ambiguous hunk whole.
@@ -282,26 +310,26 @@ export function selectiveUndo(input, actIds, options = {}) {
282
310
  if (!options || typeof options !== 'object' || Array.isArray(options) ||
283
311
  (options.keepHunk !== undefined && typeof options.keepHunk !== 'function') ||
284
312
  (options.splitSplice !== undefined && typeof options.splitSplice !== 'function')) return historyFailure('act_invalid');
285
- const requested = new Set(actIds), available = new Set(prepared.rows.map(({record}) => record.transaction.id));
313
+ const requested = new Set(actIds), available = prepared.byId;
286
314
  if ([...requested].some(id => !available.has(id))) return historyFailure('act_unavailable');
287
315
  const selected = new Set([...requested].filter(id => prepared.effects.active.has(id)));
288
- const records = prepared.rows.map(({record, splices}) => ({...record, splices}));
316
+ const records = prepared.records;
289
317
  let inverseId = 'planned-inverse'; while (available.has(inverseId)) inverseId += ':';
290
318
  const inverse = {splices: [], transaction: {id: inverseId, sourceTransactionIds: [...selected]}};
291
319
  const projectedEffects = selected.size ? _rapierHistoryEffects([...records, inverse]) : prepared.effects;
292
320
  const afterMetadata = _rapierMetadataState(prepared.initialMetadata, records, projectedEffects);
293
321
  if (!afterMetadata) return historyFailure('history_invalid');
294
322
  const metadata = _rapierMetadataDelta(prepared.metadata, afterMetadata), effectChanged = selected.size > 0;
295
- const authoredEffectChanged = records.some(record => !prepared.effects.references.get(record.transaction.id)?.length &&
323
+ const sourceEffectChanged = records.some(record => record.splices.length && !prepared.effects.references.get(record.transaction.id)?.length &&
296
324
  prepared.effects.active.has(record.transaction.id) !== projectedEffects.active.has(record.transaction.id));
297
- if (!authoredEffectChanged) return {ok: true, splices: [], after: prepared.source, actIds: [...requested],
325
+ if (!sourceEffectChanged) return {ok: true, splices: [], after: prepared.source, actIds: [...requested],
298
326
  sourceTransactionIds: [...selected], metadata, afterMetadata, effectChanged, unchanged: !metadata && !effectChanged,
299
327
  superseded: [], overlaps: []};
300
328
  try {
301
329
  const transported = records.some(row => row.authored) ? authoredHistory(prepared.initialSource, records, selected) : null;
302
330
  const rows = transported ? transported.records.map(record => ({record, splices: record.splices,
303
331
  sourceIndices: transported.sourceIndices.get(record.transaction.id)})) : prepared.rows;
304
- const result = projectInverseSource({...prepared, rows}, selected, projectedEffects, options);
332
+ const result = projectInverseSource(transported ? {...prepared, rows, records: transported.records} : prepared, selected, projectedEffects, options);
305
333
  if (!result.ok) return result;
306
334
  return {...result, actIds: [...requested], sourceTransactionIds: [...selected], metadata, afterMetadata,
307
335
  effectChanged, unchanged: result.after === prepared.source && !metadata && !effectChanged};