rapier-markdown-kit 1.1.102 → 1.2.3

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 (82) hide show
  1. package/LICENSE +5 -2
  2. package/README.md +245 -54
  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/images/gif.mjs +47 -0
  27. package/dist/images/playback.mjs +101 -0
  28. package/dist/kit/assets.mjs +5 -1
  29. package/dist/kit/conformance/03-wrap-around-silhouette.md +1 -1
  30. package/dist/kit/conformance/04-wrap-box.md +1 -1
  31. package/dist/kit/conformance/05-behind.md +1 -1
  32. package/dist/kit/conformance/06-front.md +1 -1
  33. package/dist/kit/conformance/07-rotate-raster.md +1 -1
  34. package/dist/kit/conformance/08-drawing-rotation.md +1 -1
  35. package/dist/kit/conformance/09-drawing-ring-interior.expected.json +18 -18
  36. package/dist/kit/conformance/09-drawing-ring-interior.md +1 -1
  37. package/dist/kit/conformance/10-both-sides.md +1 -1
  38. package/dist/kit/conformance/11-neighbour-skips-image.md +1 -1
  39. package/dist/kit/conformance/{12-heading-barrier.expected.json → 12-rule-barrier.expected.json} +1 -1
  40. package/dist/kit/conformance/{12-heading-barrier.md → 12-rule-barrier.md} +2 -2
  41. package/dist/kit/conformance/README.md +19 -28
  42. package/dist/kit/conformance/run.mjs +13 -11
  43. package/dist/kit/export-fonts.mjs +26 -11
  44. package/dist/kit/index.mjs +1 -0
  45. package/dist/kit/ledger/carried.mjs +17 -10
  46. package/dist/kit/ledger/data.mjs +73 -13
  47. package/dist/kit/ledger/format.mjs +176 -54
  48. package/dist/kit/ledger/hash.mjs +91 -18
  49. package/dist/kit/ledger/history.mjs +156 -116
  50. package/dist/kit/ledger/inverse-source.mjs +158 -19
  51. package/dist/kit/ledger/journal-records.mjs +292 -67
  52. package/dist/kit/ledger/merge.mjs +4 -3
  53. package/dist/kit/model.mjs +1 -1
  54. package/dist/kit/pdf-glyphs.mjs +4 -4
  55. package/dist/kit/pdf-vectors.mjs +4 -1
  56. package/dist/kit/rapier-ledger.mjs +2 -2
  57. package/dist/kit/render-ink.mjs +1 -1
  58. package/dist/kit/render-markdown.mjs +400 -148
  59. package/dist/kit/render-print.mjs +131 -55
  60. package/dist/kit/render-sanitize.mjs +30 -7
  61. package/dist/kit/render-work.mjs +99 -20
  62. package/dist/kit/render.mjs +133 -140
  63. package/dist/kit/typography.mjs +2 -0
  64. package/dist/layout/hyphenation.mjs +127 -0
  65. package/dist/layout/line-plan.mjs +88 -96
  66. package/dist/layout/model.mjs +71 -88
  67. package/dist/layout/paragraph-fit.mjs +191 -0
  68. package/dist/layout/preparation.mjs +373 -0
  69. package/dist/layout/prepared-paragraph.mjs +223 -0
  70. package/dist/layout/typography.mjs +345 -111
  71. package/dist/packages/rapier-diff/index.mjs +359 -224
  72. package/dist/spec/document-fonts.mjs +14 -5
  73. package/dist/spec/document-settings.mjs +11 -164
  74. package/dist/spec/html-blocks.mjs +618 -79
  75. package/dist/spec/md-assets.mjs +178 -36
  76. package/dist/spec/md-layout.mjs +13 -2
  77. package/dist/spec/md-marks.mjs +220 -127
  78. package/dist/spec/md-source.mjs +157 -0
  79. package/package.json +2 -1
  80. package/style.css +15 -50
  81. package/dist/agent/vendor/pretext/bidi.js +0 -151
  82. package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
@@ -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, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _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
  }
@@ -43,11 +43,15 @@ export function groupHistoryActs(acts) {
43
43
 
44
44
  // A picture's codec replacement travels with its original navigation step.
45
45
  // Match every complete replacement exactly before moving any source bytes.
46
- export function pictureHistorySplices({source, records}, target, redo) {
46
+ export function pictureHistorySplices(input, target, redo) {
47
+ const {records} = input;
47
48
  const id = target?.transaction?.id, at = records.indexOf(target);
48
49
  if (!id || at < 0 || target.transaction.operation === 'document.finish-picture' || !Array.isArray(target.splices)) return null;
49
50
  const later = records.slice(at + 1).filter(row => row?.transaction?.operation === 'document.finish-picture' && Array.isArray(row.splices) && row.splices.length);
50
51
  if (!later.length) return null;
52
+ // A caller may hold a persistent source tree: request its text only when
53
+ // a later picture replacement can actually affect this navigation step.
54
+ const source = input.source;
51
55
  const own = new Set(later.filter(row => row.transaction.sourceTransactionId === id));
52
56
  const step = redo ? target.splices : target.splices.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed}));
53
57
  const plan = moveOthers => {
@@ -74,76 +78,77 @@ export function pictureHistorySplices({source, records}, target, redo) {
74
78
  try { return plan(true); } catch (_) { return null; }
75
79
  }
76
80
 
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.
81
+ // Keep only the latest source observation. Inactive live documents must not retain
82
+ // every revision's source tree. The weak key also lets the latest input be collected.
83
+ // Mutable caller data is compared/captured before this cache can be consulted.
84
+ let preparedHistories = new WeakMap();
92
85
  function prepareHistory(input) {
93
86
  try {
94
87
  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');
88
+ const source = input.source, metadata = {...input.metadata}, index = _rapierPrepareHistoryRecords(input.records);
89
+ if (!index.ok) return historyFailure('history_invalid');
90
+ const {records, effects} = index;
98
91
  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) ||
92
+ let prior = input.earliestRevision ?? records[0]?.transaction?.baseRevision ?? revision;
93
+ if (!historyInteger(revision) || !historyInteger(prior)) return historyFailure('history_invalid');
94
+ const earliest = prior, suppliedComplete = input.complete !== false, cached = preparedHistories.get(records);
95
+ if (cached && cached.source === source && cached.revision === revision && cached.earliest === earliest &&
96
+ cached.suppliedComplete === suppliedComplete && cached.metadata.filename === metadata.filename && cached.metadata.docKind === metadata.docKind) return cached;
97
+ let complete = prior === 0;
98
+ const rows = [], byId = new Map();
99
+ for (const owned of index.rows) {
100
+ const {record, splices, metadata: effect} = owned, tx = record?.transaction, actor = tx?.actor;
101
+ if (!tx || typeof tx.id !== 'string' || !tx.id || byId.has(tx.id) || !historyInteger(tx.baseRevision) ||
105
102
  !historyInteger(tx.revision) || tx.revision !== tx.baseRevision + 1 || tx.baseRevision < prior ||
106
103
  !actor || !['human', 'agent', 'system'].includes(actor.kind) || typeof actor.id !== 'string' || !actor.id ||
107
104
  !(tx.createdAt === null || historyInteger(tx.createdAt)) ||
108
105
  (tx.turnId != null && (typeof tx.turnId !== 'string' || !tx.turnId || tx.turnId.length > 160))) return historyFailure('history_invalid');
109
106
  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;
107
+ if (!splices || effect === undefined || (!splices.length &&
108
+ !Object.values(effect || {}).some(pair => pair.before !== pair.after) && !effects.references.get(tx.id)?.length)) return historyFailure('history_unavailable');
109
+ const row = {record, splices, metadata: effect};
110
+ rows.push(row); byId.set(tx.id, row); prior = tx.revision;
114
111
  }
115
112
  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');
113
+ let initial = new HistorySource(source), initialMetadata = {...metadata};
114
+ for (let at = rows.length - 1; at >= 0; at--) {
115
+ const row = rows[at];
116
+ row.afterSource = initial; row.afterMetadata = initialMetadata;
117
+ initial = initial.before(row.splices);
118
+ initialMetadata = _rapierTransformMetadata(initialMetadata, row.metadata, true);
119
+ if (!initial || !initialMetadata) return historyFailure('history_invalid');
120
+ row.beforeSource = initial; row.beforeMetadata = initialMetadata;
121
121
  }
122
- const earliestRevision = rows[0]?.record.transaction.baseRevision ?? revision;
123
- let replay = initialSource, root = earliestRevision === 0 ? textRoot(initialSource) : null, rootRevision = earliestRevision;
122
+ const earliestRevision = records[0]?.transaction.baseRevision ?? revision;
123
+ // The window's owner may name its root (the source store mints it once per text); otherwise hash the initial text.
124
+ let root = earliestRevision === 0 ? (typeof input.earliestRoot === 'string' && input.earliestRoot || textRootParts(initial.parts())) : null,
125
+ rootRevision = earliestRevision;
124
126
  for (const {record, splices} of rows) {
125
127
  const hashed = Object.hasOwn(record, 'beforeHash') || Object.hasOwn(record, 'afterHash');
126
128
  if (hashed && (![record.beforeHash, record.afterHash].every(value =>
127
129
  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.
130
+ (splices.length ? record.beforeHash === record.afterHash : record.beforeHash !== record.afterHash))) return historyFailure('history_invalid');
131
+ // Revision gaps begin a new path root; a continuous revision-zero prefix
132
+ // still binds every untouched source byte through the initial text root.
132
133
  if (record.transaction.baseRevision !== rootRevision) root = null;
133
134
  if (hashed) {
134
135
  if (root !== null && record.beforeHash !== root) return historyFailure('history_invalid');
135
136
  root = record.beforeHash;
136
137
  }
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);
138
+ if (root !== null) for (const splice of splices) root = rootAfter(root, splice);
140
139
  if (hashed && root !== record.afterHash) return historyFailure('history_invalid');
141
140
  rootRevision = record.transaction.revision;
142
141
  }
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};
142
+ let initialText;
143
+ const initialSource = () => initialText ??= initial.slice();
144
+ if (records.some(row => Object.hasOwn(row, 'authored'))) authoredHistory(initialSource(), rows.map(({record, splices}) => ({...record, splices})));
145
+ const replayedMetadata = index.replayMetadata(initialMetadata);
146
+ if (!replayedMetadata || ['filename', 'docKind'].some(key => replayedMetadata[key] !== metadata[key])) return historyFailure('history_invalid');
147
+ const resolvedRecords = Object.freeze(rows.map(({record, splices}) => record.splices === splices ? record : Object.freeze({...record, splices})));
148
+ const prepared = {ok: true, source, records: resolvedRecords, get initialSource() { return initialSource(); }, initial, metadata, initialMetadata, effects, rows, byId,
149
+ revision, earliest, suppliedComplete, earliestRevision, complete: suppliedComplete && complete && earliestRevision === 0};
150
+ preparedHistories = new WeakMap([[records, prepared]]);
151
+ return prepared;
147
152
  } catch (_) { return historyFailure('history_invalid'); }
148
153
  }
149
154
 
@@ -152,23 +157,17 @@ export function replayHistory(input) {
152
157
  if (!prepared.ok) return prepared;
153
158
  const {source, initialSource, metadata, initialMetadata, revision, earliestRevision, complete, rows, effects} = prepared;
154
159
  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,
160
+ return {ok: true, source, initialSource, metadata: {...metadata}, initialMetadata: {...initialMetadata}, revision, earliestRevision, complete, acts,
156
161
  effectiveActIds: effects.effectiveActIds.slice(), groups: groupHistoryActs(acts)};
157
162
  }
158
163
 
159
164
  export function sourceBefore(input, actId) {
160
165
  const prepared = prepareHistory(input);
161
166
  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');
167
+ const row = prepared.byId.get(actId);
168
+ if (!row) return historyFailure('act_unavailable');
169
+ return {ok: true, source: row.beforeSource.slice(), metadata: {...row.beforeMetadata}, revision: row.record.transaction.baseRevision,
170
+ act: historyAct(row.record, prepared.effects), complete: prepared.complete, earliestRevision: prepared.earliestRevision};
172
171
  }
173
172
 
174
173
  function historyRangeBefore(range, splice) {
@@ -186,21 +185,23 @@ function historyRangeBefore(range, splice) {
186
185
 
187
186
  function projectHistoryRange(prepared, from, to, actId) {
188
187
  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;
188
+ let range = {from, to}, before = null;
190
189
  const acts = [];
191
190
  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)};
191
+ const {record, splices, beforeSource, afterSource} = prepared.rows[index], afterRange = range;
193
192
  let touched = false;
194
193
  for (let at = splices.length - 1; at >= 0; at--) {
195
194
  const mapped = historyRangeBefore(range, splices[at]); touched ||= mapped.touched;
196
195
  range = {from: mapped.from, to: mapped.to};
197
196
  }
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});
197
+ const include = !prepared.effects.pictures.has(record.transaction.id) &&
198
+ (touched || record.metadata || prepared.effects.references.get(record.transaction.id)?.length);
199
+ if (include || record.transaction.id === actId) {
200
+ const previous = {...range, source: beforeSource.slice(range.from, range.to)};
201
+ if (record.transaction.id === actId) before = previous;
202
+ if (include) acts.push({...historyAct(record, prepared.effects), before: previous,
203
+ after: {...afterRange, source: afterSource.slice(afterRange.from, afterRange.to)}});
204
+ }
204
205
  }
205
206
  if (actId != null && !before) return historyFailure('act_unavailable');
206
207
  acts.reverse();
@@ -220,10 +221,47 @@ export function historyProjections(input, ranges) {
220
221
  if (!prepared.ok) return prepared;
221
222
  if (!Array.isArray(ranges)) return historyFailure('range_invalid');
222
223
  return {ok: true, projections: ranges.map(({from = 0, to = prepared.source.length, actId} = {}) =>
223
- structuredClone(projectHistoryRange(prepared, from, to, actId)))};
224
+ projectHistoryRange(prepared, from, to, actId))};
225
+ }
226
+
227
+ // Block intervals stay ordered under reverse splices. A fixed balanced tree can
228
+ // shift an untouched suffix lazily and visit only intervals crossing the edit.
229
+ function historyBlockIndex(projected) {
230
+ const items = projected.map(block => ({block, range: {from: block.from, to: block.to}, shift: 0}))
231
+ .sort((a, b) => a.range.from - b.range.from);
232
+ const refresh = node => {
233
+ node.minFrom = node.left?.minFrom ?? node.range.from;
234
+ node.maxTo = Math.max(node.range.to, node.left?.maxTo ?? -1, node.right?.maxTo ?? -1);
235
+ };
236
+ const build = (from, to, parent = null) => {
237
+ if (from === to) return null;
238
+ const mid = Math.floor((from + to) / 2), node = items[mid];
239
+ node.parent = parent; node.left = build(from, mid, node); node.right = build(mid + 1, to, node);
240
+ refresh(node); return node;
241
+ };
242
+ const root = build(0, items.length);
243
+ const shift = (node, delta) => {
244
+ if (!node || !delta) return;
245
+ node.range = {from: node.range.from + delta, to: node.range.to + delta};
246
+ node.minFrom += delta; node.maxTo += delta; node.shift += delta;
247
+ };
248
+ const move = (node, splice, touch) => {
249
+ if (!node || node.maxTo < splice.pos) return;
250
+ const end = splice.pos + splice.inserted.length;
251
+ if (node.minFrom > end) { shift(node, splice.removed.length - splice.inserted.length); return; }
252
+ if (node.shift) { shift(node.left, node.shift); shift(node.right, node.shift); node.shift = 0; }
253
+ const mapped = historyRangeBefore(node.range, splice);
254
+ if (mapped.touched) touch(node);
255
+ node.range = {from: mapped.from, to: mapped.to};
256
+ move(node.left, splice, touch); move(node.right, splice, touch); refresh(node);
257
+ };
258
+ return {move: (splice, touch) => move(root, splice, touch), range(node) {
259
+ let delta = 0;
260
+ for (let parent = node.parent; parent; parent = parent.parent) delta += parent.shift;
261
+ return {from: node.range.from + delta, to: node.range.to + delta};
262
+ }};
224
263
  }
225
264
 
226
- // Project all reading places in one replay. Typing only invalidates this transient view.
227
265
  export function historyPlaces(input, blocks) {
228
266
  const prepared = prepareHistory(input);
229
267
  if (!prepared.ok) return prepared;
@@ -231,47 +269,49 @@ export function historyPlaces(input, blocks) {
231
269
  !historyBoundary(prepared.source, block.from) || !historyBoundary(prepared.source, block.to) || block.to < block.from))
232
270
  return historyFailure('range_invalid');
233
271
  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;
272
+ source: prepared.source.slice(block.from, block.to), acts: []}));
273
+ const ranges = historyBlockIndex(projected);
236
274
  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);
275
+ const {record, splices, beforeSource, afterSource} = prepared.rows[index], touched = new Map();
276
+ for (let at = splices.length - 1; at >= 0; at--) ranges.move(splices[at], node => {
277
+ if (touched.has(node) || prepared.effects.pictures.has(record.transaction.id)) return;
278
+ // Earlier splices in this reverse walk only shifted this interval. Recover
279
+ // its coordinates at the record's after-state before recording the first touch.
280
+ let after = {...node.range};
281
+ for (let prior = at + 1; prior < splices.length; prior++) {
282
+ const row = splices[prior];
283
+ if (after.from >= row.pos + row.removed.length) {
284
+ const delta = row.inserted.length - row.removed.length;
285
+ after = {from: after.from + delta, to: after.to + delta};
266
286
  }
267
287
  }
288
+ touched.set(node, after);
289
+ });
290
+ // Blocks one splice covers share its range (a whole-document delete maps every block to all it removed): slice each range once.
291
+ const slices = new Map(), cut = (source, side, from, to) => {
292
+ const key = side + from + ':' + to;
293
+ if (!slices.has(key)) slices.set(key, source.slice(from, to));
294
+ return slices.get(key);
295
+ };
296
+ for (const [node, afterRange] of touched) {
297
+ const beforeRange = ranges.range(node), previous = {...beforeRange, source: cut(beforeSource, 'b', beforeRange.from, beforeRange.to)},
298
+ after = {...afterRange, source: cut(afterSource, 'a', afterRange.from, afterRange.to)};
299
+ if (previous.source !== after.source) node.block.acts.push({...historyAct(record), before: previous, after});
268
300
  }
269
- set(act.id, true); effects.set(act.id, changes);
270
301
  }
271
- for (const block of projected) { block.acts.reverse(); delete block.range; }
302
+
303
+ const acts = prepared.rows.filter(({record}) => !prepared.effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record));
304
+ // Native Redo consumes its Undo as navigation while keeping that receipt in
305
+ // the reading history. All material withdrawals use the canonical blockers,
306
+ // including independent inverses holding the same original act suppressed.
307
+ const reversed = new Set();
308
+ for (const {record} of prepared.rows) if (prepared.effects.active.has(record.transaction.id) && !record.transaction.reapplies)
309
+ for (const id of prepared.effects.references.get(record.transaction.id) || []) reversed.add(id);
310
+
311
+ for (const block of projected) block.acts.reverse();
272
312
  return {ok: true, source: prepared.source, initialSource: prepared.initialSource, revision: prepared.revision,
273
313
  earliestRevision: prepared.earliestRevision, complete: prepared.complete, acts, blocks: projected,
274
- reversed: acts.filter(act => present.get(act.id) !== true).map(act => act.id)};
314
+ reversed: acts.filter(act => reversed.has(act.id)).map(act => act.id)};
275
315
  }
276
316
 
277
317
  // Runs retain only exact replacement ancestry. A later replacement owns an ambiguous hunk whole.
@@ -282,26 +322,26 @@ export function selectiveUndo(input, actIds, options = {}) {
282
322
  if (!options || typeof options !== 'object' || Array.isArray(options) ||
283
323
  (options.keepHunk !== undefined && typeof options.keepHunk !== 'function') ||
284
324
  (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));
325
+ const requested = new Set(actIds), available = prepared.byId;
286
326
  if ([...requested].some(id => !available.has(id))) return historyFailure('act_unavailable');
287
327
  const selected = new Set([...requested].filter(id => prepared.effects.active.has(id)));
288
- const records = prepared.rows.map(({record, splices}) => ({...record, splices}));
328
+ const records = prepared.records;
289
329
  let inverseId = 'planned-inverse'; while (available.has(inverseId)) inverseId += ':';
290
330
  const inverse = {splices: [], transaction: {id: inverseId, sourceTransactionIds: [...selected]}};
291
331
  const projectedEffects = selected.size ? _rapierHistoryEffects([...records, inverse]) : prepared.effects;
292
332
  const afterMetadata = _rapierMetadataState(prepared.initialMetadata, records, projectedEffects);
293
333
  if (!afterMetadata) return historyFailure('history_invalid');
294
334
  const metadata = _rapierMetadataDelta(prepared.metadata, afterMetadata), effectChanged = selected.size > 0;
295
- const authoredEffectChanged = records.some(record => !prepared.effects.references.get(record.transaction.id)?.length &&
335
+ const sourceEffectChanged = records.some(record => record.splices.length && !prepared.effects.references.get(record.transaction.id)?.length &&
296
336
  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],
337
+ if (!sourceEffectChanged) return {ok: true, splices: [], after: prepared.source, actIds: [...requested],
298
338
  sourceTransactionIds: [...selected], metadata, afterMetadata, effectChanged, unchanged: !metadata && !effectChanged,
299
339
  superseded: [], overlaps: []};
300
340
  try {
301
341
  const transported = records.some(row => row.authored) ? authoredHistory(prepared.initialSource, records, selected) : null;
302
342
  const rows = transported ? transported.records.map(record => ({record, splices: record.splices,
303
343
  sourceIndices: transported.sourceIndices.get(record.transaction.id)})) : prepared.rows;
304
- const result = projectInverseSource({...prepared, rows}, selected, projectedEffects, options);
344
+ const result = projectInverseSource(transported ? {...prepared, rows, records: transported.records} : prepared, selected, projectedEffects, options);
305
345
  if (!result.ok) return result;
306
346
  return {...result, actIds: [...requested], sourceTransactionIds: [...selected], metadata, afterMetadata,
307
347
  effectChanged, unchanged: result.after === prepared.source && !metadata && !effectChanged};