rapier-markdown-kit 1.2.8 → 1.2.10

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 (61) hide show
  1. package/dist/agent/will.mjs +0 -6
  2. package/dist/images/gif.mjs +0 -3
  3. package/dist/images/playback.mjs +0 -10
  4. package/dist/kit/assets.mjs +0 -1
  5. package/dist/kit/conformance/measurer.mjs +0 -0
  6. package/dist/kit/conformance/run.mjs +1 -8
  7. package/dist/kit/export-fonts.mjs +0 -14
  8. package/dist/kit/ledger/authored-origin.mjs +0 -5
  9. package/dist/kit/ledger/authorship.mjs +0 -2
  10. package/dist/kit/ledger/carried.mjs +4 -7
  11. package/dist/kit/ledger/data.mjs +0 -8
  12. package/dist/kit/ledger/format.mjs +0 -25
  13. package/dist/kit/ledger/hash.mjs +11 -7
  14. package/dist/kit/ledger/history.mjs +10 -29
  15. package/dist/kit/ledger/inverse-source.mjs +0 -46
  16. package/dist/kit/ledger/journal-records.mjs +24 -56
  17. package/dist/kit/ledger/merge.mjs +0 -27
  18. package/dist/kit/ledger/text-merge.mjs +0 -20
  19. package/dist/kit/ledger/transport.mjs +0 -47
  20. package/dist/kit/ledger.mjs +0 -4
  21. package/dist/kit/markdown-it.mjs +0 -3
  22. package/dist/kit/pdf-glyphs.mjs +1 -10
  23. package/dist/kit/pdf-source.mjs +5 -5
  24. package/dist/kit/pdf-vectors.mjs +0 -3
  25. package/dist/kit/print-capitals.mjs +0 -4
  26. package/dist/kit/render-entry.mjs +0 -1
  27. package/dist/kit/render-ink.mjs +0 -5
  28. package/dist/kit/render-markdown.mjs +12 -117
  29. package/dist/kit/render-print.mjs +30 -61
  30. package/dist/kit/render-sanitize.mjs +7 -59
  31. package/dist/kit/render-styles.mjs +0 -7
  32. package/dist/kit/render-work.mjs +0 -12
  33. package/dist/kit/render.mjs +46 -176
  34. package/dist/layout/hyphenation.mjs +0 -23
  35. package/dist/layout/line-plan.mjs +0 -20
  36. package/dist/layout/model.mjs +0 -55
  37. package/dist/layout/paragraph-fit.mjs +50 -60
  38. package/dist/layout/preparation.mjs +23 -56
  39. package/dist/layout/prepared-paragraph.mjs +0 -25
  40. package/dist/layout/typography.mjs +103 -41
  41. package/dist/packages/rapier-diff/concerns.mjs +0 -16
  42. package/dist/packages/rapier-diff/diff.mjs +0 -7
  43. package/dist/packages/rapier-diff/index.mjs +0 -1
  44. package/dist/packages/rapier-diff/matching.mjs +0 -19
  45. package/dist/packages/rapier-diff/merge.mjs +0 -9
  46. package/dist/packages/rapier-diff/moves.mjs +0 -7
  47. package/dist/packages/rapier-diff/structure.mjs +0 -2
  48. package/dist/packages/rapier-diff/tokens.mjs +0 -6
  49. package/dist/packages/rapier-diff/walk.mjs +0 -12
  50. package/dist/spec/document-fonts.mjs +0 -9
  51. package/dist/spec/document-settings.mjs +5 -24
  52. package/dist/spec/frontmatter.mjs +0 -1
  53. package/dist/spec/html-blocks.mjs +0 -36
  54. package/dist/spec/md-assets.mjs +2 -61
  55. package/dist/spec/md-layout.mjs +0 -34
  56. package/dist/spec/md-marks.mjs +0 -51
  57. package/dist/spec/md-source.mjs +0 -16
  58. package/dist/spec/template-page.mjs +6 -0
  59. package/dist/spec/word-sidecar.mjs +0 -2
  60. package/package.json +1 -1
  61. package/style.css +32 -16
@@ -1,5 +1,4 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // Transient projections and selective inverses of the existing canonical journal.
3
2
  import {_rapierTransformSplices, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierPrepareHistoryRecords} from './journal-records.mjs';
4
3
  import {projectInverseSource, HistorySource} from './inverse-source.mjs';
5
4
  import {textRootParts, rootAfter} from './hash.mjs';
@@ -41,16 +40,12 @@ export function groupHistoryActs(acts) {
41
40
  return groups;
42
41
  }
43
42
 
44
- // A picture's codec replacement travels with its original navigation step.
45
- // Match every complete replacement exactly before moving any source bytes.
46
43
  export function pictureHistorySplices(input, target, redo) {
47
44
  const {records} = input;
48
45
  const id = target?.transaction?.id, at = records.indexOf(target);
49
46
  if (!id || at < 0 || target.transaction.operation === 'document.finish-picture' || !Array.isArray(target.splices)) return null;
50
47
  const later = records.slice(at + 1).filter(row => row?.transaction?.operation === 'document.finish-picture' && Array.isArray(row.splices) && row.splices.length);
51
48
  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
49
  const source = input.source;
55
50
  const own = new Set(later.filter(row => row.transaction.sourceTransactionId === id));
56
51
  const step = redo ? target.splices : target.splices.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed}));
@@ -78,9 +73,6 @@ export function pictureHistorySplices(input, target, redo) {
78
73
  try { return plan(true); } catch (_) { return null; }
79
74
  }
80
75
 
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
76
  let preparedHistories = new WeakMap();
85
77
  function prepareHistory(input) {
86
78
  try {
@@ -123,7 +115,6 @@ function prepareHistory(input) {
123
115
  row.beforeSource = initial; row.beforeMetadata = initialMetadata;
124
116
  }
125
117
  const earliestRevision = records[0]?.transaction.baseRevision ?? revision;
126
- // The window's owner may name its root (the source store mints it once per text); otherwise hash the initial text.
127
118
  let root = earliestRevision === 0 ? (typeof input.earliestRoot === 'string' && input.earliestRoot || textRootParts(initial.parts())) : null,
128
119
  rootRevision = earliestRevision;
129
120
  for (const {record, splices} of rows) {
@@ -131,8 +122,6 @@ function prepareHistory(input) {
131
122
  if (hashed && (![record.beforeHash, record.afterHash].every(value =>
132
123
  typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value)) ||
133
124
  (splices.length ? record.beforeHash === record.afterHash : record.beforeHash !== record.afterHash))) return historyFailure('history_invalid');
134
- // Revision gaps begin a new path root; a continuous revision-zero prefix
135
- // still binds every untouched source byte through the initial text root.
136
125
  if (record.transaction.baseRevision !== rootRevision) root = null;
137
126
  if (hashed) {
138
127
  if (root !== null && record.beforeHash !== root) return historyFailure('history_invalid');
@@ -155,12 +144,18 @@ function prepareHistory(input) {
155
144
  } catch (_) { return historyFailure('history_invalid'); }
156
145
  }
157
146
 
147
+ function settle(result, value) {
148
+ Object.defineProperty(result, 'initialSource', {value, writable: true, enumerable: true, configurable: true});
149
+ return value;
150
+ }
151
+
158
152
  export function replayHistory(input) {
159
153
  const prepared = prepareHistory(input);
160
154
  if (!prepared.ok) return prepared;
161
- const {source, initialSource, metadata, initialMetadata, revision, earliestRevision, complete, rows, effects} = prepared;
155
+ const {source, metadata, initialMetadata, revision, earliestRevision, complete, rows, effects} = prepared;
162
156
  const acts = rows.filter(({record}) => !effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record, effects));
163
- return {ok: true, source, initialSource, metadata: {...metadata}, initialMetadata: {...initialMetadata}, revision, earliestRevision, complete, acts,
157
+ return {ok: true, source, get initialSource() { return settle(this, prepared.initialSource); }, set initialSource(value) { settle(this, value); },
158
+ metadata: {...metadata}, initialMetadata: {...initialMetadata}, revision, earliestRevision, complete, acts,
164
159
  effectiveActIds: effects.effectiveActIds.slice(), groups: groupHistoryActs(acts)};
165
160
  }
166
161
 
@@ -218,7 +213,6 @@ export function historyProjection(input, {from = 0, to = input?.source?.length,
218
213
  return projectHistoryRange(prepared, from, to, actId);
219
214
  }
220
215
 
221
- // Several ranges share one proved source snapshot for this call.
222
216
  export function historyProjections(input, ranges) {
223
217
  const prepared = prepareHistory(input);
224
218
  if (!prepared.ok) return prepared;
@@ -227,8 +221,6 @@ export function historyProjections(input, ranges) {
227
221
  projectHistoryRange(prepared, from, to, actId))};
228
222
  }
229
223
 
230
- // Block intervals stay ordered under reverse splices. A fixed balanced tree can
231
- // shift an untouched suffix lazily and visit only intervals crossing the edit.
232
224
  function historyBlockIndex(projected) {
233
225
  const items = projected.map(block => ({block, range: {from: block.from, to: block.to}, shift: 0}))
234
226
  .sort((a, b) => a.range.from - b.range.from);
@@ -278,8 +270,6 @@ export function historyPlaces(input, blocks) {
278
270
  const {record, splices, beforeSource, afterSource} = prepared.rows[index], touched = new Map();
279
271
  for (let at = splices.length - 1; at >= 0; at--) ranges.move(splices[at], node => {
280
272
  if (touched.has(node) || prepared.effects.pictures.has(record.transaction.id)) return;
281
- // Earlier splices in this reverse walk only shifted this interval. Recover
282
- // its coordinates at the record's after-state before recording the first touch.
283
273
  let after = {...node.range};
284
274
  for (let prior = at + 1; prior < splices.length; prior++) {
285
275
  const row = splices[prior];
@@ -290,37 +280,31 @@ export function historyPlaces(input, blocks) {
290
280
  }
291
281
  touched.set(node, after);
292
282
  });
293
- // Blocks one splice covers share its range (a whole-document delete maps every block to all it removed): slice each range once.
294
283
  const slices = new Map(), cut = (source, side, from, to) => {
295
284
  const key = side + from + ':' + to;
296
285
  if (!slices.has(key)) slices.set(key, source.slice(from, to));
297
286
  return slices.get(key);
298
287
  };
299
- // One record's act is read once; the blocks it touched share its fields (a whole-document delete touches every block).
300
288
  let act = null;
301
289
  for (const [node, afterRange] of touched) {
302
290
  const beforeRange = ranges.range(node), previous = {...beforeRange, source: cut(beforeSource, 'b', beforeRange.from, beforeRange.to)},
303
291
  after = {...afterRange, source: cut(afterSource, 'a', afterRange.from, afterRange.to)};
304
- // A point (where words were deleted) has no words to compare: the act that touched it wrote there.
305
292
  if (previous.source !== after.source || afterRange.from === afterRange.to) node.block.acts.push({...(act ||= historyAct(record)), before: previous, after});
306
293
  }
307
294
  }
308
295
 
309
296
  const acts = prepared.rows.filter(({record}) => !prepared.effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record));
310
- // Native Redo consumes its Undo as navigation while keeping that receipt in
311
- // the reading history. All material withdrawals use the canonical blockers,
312
- // including independent inverses holding the same original act suppressed.
313
297
  const reversed = new Set();
314
298
  for (const {record} of prepared.rows) if (prepared.effects.active.has(record.transaction.id) && !record.transaction.reapplies)
315
299
  for (const id of prepared.effects.references.get(record.transaction.id) || []) reversed.add(id);
316
300
 
317
301
  for (const block of projected) block.acts.reverse();
318
- return {ok: true, source: prepared.source, initialSource: prepared.initialSource, revision: prepared.revision,
302
+ return {ok: true, source: prepared.source, get initialSource() { return settle(this, prepared.initialSource); },
303
+ set initialSource(value) { settle(this, value); }, revision: prepared.revision,
319
304
  earliestRevision: prepared.earliestRevision, complete: prepared.complete, acts, blocks: projected,
320
305
  reversed: acts.filter(act => reversed.has(act.id)).map(act => act.id)};
321
306
  }
322
307
 
323
- // Runs retain only exact replacement ancestry. A later replacement owns an ambiguous hunk whole.
324
308
  export function selectiveUndo(input, actIds, options = {}) {
325
309
  const prepared = prepareHistory(input);
326
310
  if (!prepared.ok) return prepared;
@@ -340,9 +324,6 @@ export function selectiveUndo(input, actIds, options = {}) {
340
324
  const metadata = _rapierMetadataDelta(prepared.metadata, afterMetadata), effectChanged = selected.size > 0;
341
325
  const sourceEffectChanged = records.some(record => record.splices.length && !prepared.effects.references.get(record.transaction.id)?.length &&
342
326
  prepared.effects.active.has(record.transaction.id) !== projectedEffects.active.has(record.transaction.id));
343
- // One shared replacement is visible while any of its authors is active. That
344
- // includes an author stored with no second physical splice, and an inverse
345
- // whose only effect is to make such an author active again.
346
327
  let coverVisibilityChanged = false;
347
328
  if (!sourceEffectChanged && selected.size) {
348
329
  let origins = null;
@@ -1,6 +1,4 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // Exact source projection shared by canonical history and causal transport.
3
- // Callers admit and order the records first; this projection retains no state.
4
2
  import {_rapierTransformSplices, _rapierHistoryEffects} from './journal-records.mjs';
5
3
  import {authoredPlacements} from './authored-origin.mjs';
6
4
  import {canonicalJSON} from './data.mjs';
@@ -11,8 +9,6 @@ const historyBoundary = (source, at) => Number.isSafeInteger(at) && at >= 0 && !
11
9
  !(at > 0 && at < source.length && (source.charCodeAt(at - 1) & 0xFC00) === 0xD800 &&
12
10
  (source.charCodeAt(at) & 0xFC00) === 0xDC00);
13
11
 
14
- // Persistent AVL pieces retain original strings and coordinates. A revision costs
15
- // only the edited paths; reading a range visits its leaves, never earlier bodies.
16
12
  const sourceLeaf = (text, from = 0, to = text.length) => to > from
17
13
  ? {text, from, length: to - from, height: 1} : null;
18
14
  const sourceHeight = node => node?.height || 0;
@@ -101,8 +97,6 @@ export class HistorySource {
101
97
  for (const text of parts) for (let at = 0; at < text.length;) {
102
98
  if (!leaf) return false;
103
99
  const length = Math.min(leaf.length - offset, text.length - at), from = leaf.from + offset;
104
- // Untouched runs share their string and coordinates. Only changed fragments
105
- // require a text comparison; no full-body join is needed to prove the plan.
106
100
  if ((leaf.text !== text || from !== at) && leaf.text.slice(from, from + length) !== text.slice(at, at + length)) return false;
107
101
  offset += length; at += length;
108
102
  if (offset === leaf.length) { leaf = leaves.next().value; offset = 0; }
@@ -113,8 +107,6 @@ export class HistorySource {
113
107
  const [left, rest] = sourceSplit(this.#root, pos), [, right] = sourceSplit(rest, length);
114
108
  return new HistorySource('', sourceJoin(sourceJoin(left, sourceLeaf(text)), right));
115
109
  }
116
- // Undoing an admitted repair is observation: its after-state can join a scalar
117
- // across the deleted run. Prove the forward boundaries on the reconstructed view.
118
110
  before(splices) {
119
111
  let source = this;
120
112
  for (let index = splices.length - 1; index >= 0; index--) {
@@ -151,8 +143,6 @@ function minimalHistorySplice(before, after, pos = 0) {
151
143
  return {pos: pos + first, removed: before.slice(first, a), inserted: after.slice(first, b)};
152
144
  }
153
145
 
154
- // Split recorded replacements only at exact unchanged word or line tokens.
155
- // Shared letters inside changed words do not establish surviving ownership.
156
146
  function historyHunks(row) {
157
147
  if (row.removed === row.inserted) return [];
158
148
  if (!row.removed || !row.inserted) return [{...row}];
@@ -166,8 +156,6 @@ function historyHunks(row) {
166
156
  }
167
157
 
168
158
 
169
- // Rows are already proved source placements, optionally carrying sourceIndices from
170
- // causal projection. Multiple inverse blockers can change history with no source edit.
171
159
  export function projectInverseSource(prepared, selected, projectedEffects, options = {}) {
172
160
  const records = prepared.records || prepared.rows.map(({record, splices}) => record.splices === splices ? record : {...record, splices});
173
161
  if (!records.some(record => !prepared.effects.references.get(record.transaction.id)?.length &&
@@ -179,8 +167,6 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
179
167
  const recordsForCover = prepared.records || prepared.rows.map(({record, splices}) => record.splices === splices ? record : {...record, splices});
180
168
  const coverGroups = new Map();
181
169
  let coverOrigins = null;
182
- // An empty document is a real causal source. Fresh notes start there, and a
183
- // shared replacement authored against that source still has to stay covered.
184
170
  if (typeof prepared.initialSource === 'string' && recordsForCover.some(record => record.authored?.splices?.some(row => row.removed.length))) {
185
171
  try { coverOrigins = authoredPlacements(prepared.initialSource, recordsForCover); } catch { coverOrigins = null; }
186
172
  }
@@ -254,9 +240,6 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
254
240
  return next && effects.active.has(next.actId) && !prepared.effects.references.get(next.actId)?.length;
255
241
  });
256
242
  const reactivate = (actIds, effects) => {
257
- // Any author of one shared removal or replacement restores that one physical
258
- // hunk, including an author who stored no splice. A later independent act
259
- // keeps the bytes it wrote until that act itself is withdrawn.
260
243
  const wanted = new Set(actIds);
261
244
  for (const id of actIds) for (const hunk of all) {
262
245
  if (hunk.reversedHunk || hunk.derived || !hunk.coverIds?.includes(id) || wanted.has(hunk.actId)) continue;
@@ -269,8 +252,6 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
269
252
  (!hunk.inserted && hunk.active)) continue;
270
253
  const marker = pieces.indexOf(hunk.marker);
271
254
  if (marker < 0) {
272
- // A zero-row withdrawal can release source still carried by another
273
- // inverse's marker. Its exact ancestry also preserves later inserted runs.
274
255
  const inverse = all.findLast(prior => prior.reversedHunk === hunk.id && pieces.includes(prior.marker));
275
256
  if (!inverse) throw Error('reactivation');
276
257
  const runs = all.filter(prior => prior.actId === inverse.actId && prior.reversedHunk === hunk.id && pieces.includes(prior.marker));
@@ -302,8 +283,6 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
302
283
  processed.push(record);
303
284
  const picture = prepared.effects.pictures.get(tx.id);
304
285
  if (picture) {
305
- // The new codec bytes keep the exact inserted run's ownership. Do not
306
- // cross a later edit, combine authors, or create another reversible act.
307
286
  for (const row of splices) {
308
287
  boundary(row.pos + row.removed.length);
309
288
  const start = boundary(row.pos), end = boundary(row.pos + row.removed.length);
@@ -318,15 +297,11 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
318
297
  continue;
319
298
  }
320
299
  if (references.size) {
321
- // Exact inverse rows restore the previous run identities. Retain the inverse's
322
- // own transient marker too, so reversing it restores that same ancestry again.
323
300
  const invert = targets => {
324
301
  const restored = pieces.slice(), inverses = [], priorSplices = options.keepHunk ? [] : null;
325
302
  for (let index = targets.length - 1; index >= 0; index--) {
326
303
  const target = targets[index], first = restored.indexOf(target.marker);
327
304
  if (first < 0) return null;
328
- // Each run of the target's own pieces inverts alone: a later author's pieces between
329
- // its runs stay in place, outside every inverse, so reversing it cannot move them.
330
305
  const runs = [];
331
306
  for (let at = first; at < restored.length; at++) if (owns(restored[at], target))
332
307
  if (runs.at(-1)?.[1] === at) runs.at(-1)[1]++; else runs.push([at, at + 1]);
@@ -346,8 +321,6 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
346
321
  hunk.chosen = !keep;
347
322
  }
348
323
  hunk.kept = selected.has(tx.id) && !hunk.chosen;
349
- // A semantic owner retaining this inverse also retains the exact ancestors
350
- // it restored; effect reactivation must not change them behind that owner.
351
324
  if (hunk.kept) for (let prior = target; prior; prior = byId.get(prior.reversedHunk)) keptInverseAncestors.add(prior.id);
352
325
  hunk.marker = {mark: hunk};
353
326
  const insertion = replacement.map(piece => piece.text ? {...piece,
@@ -359,10 +332,6 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
359
332
  }
360
333
  return replays(pieces, restored, splices) ? {restored, inverses} : null;
361
334
  };
362
- // An inverse taken while a later edit had replaced some of the act's hunks restored only
363
- // the rest. Those replaced hunks survive it: no later projection treats them as undone.
364
- // Recorded splices decide. An author who stored no splice still inverts the one
365
- // shared hunk; an empty inverse does not match that text change and leaves it.
366
335
  const direct = all.filter(hunk => hunk.active && references.has(hunk.actId));
367
336
  const targets = direct.length ? direct : all.filter(hunk => hunk.active && !hunk.reversedHunk && !hunk.derived &&
368
337
  hunk.coverIds?.some(id => references.has(id)));
@@ -399,16 +368,12 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
399
368
  }
400
369
  if (!references.size) coverIdsFor(tx.id);
401
370
  const priorSplices = options.keepHunk ? [] : null;
402
- // An admitted codec finish replaces the complete embedded definition/reference.
403
- // Keep its inserted run whole, including byte-identical prefixes of a re-edit;
404
- // the previous picture stays in `before` for Undo, not as mixed live ownership.
405
371
  const hunks = row => pictureInsertions.get(tx.id)?.some(text => row.inserted.includes(text)) ? [{...row}] : historyHunks(row);
406
372
  for (let rowIndex = 0; rowIndex < splices.length; rowIndex++) {
407
373
  const original = splices[rowIndex];
408
374
  let rows;
409
375
  if (options.splitSplice) {
410
376
  const beforeSource = textOf(pieces), parts = options.splitSplice({actId: tx.id, beforeSource, splice: original});
411
- // Semantic boundaries only refine this act's exact bytes, never its history.
412
377
  if (!Array.isArray(parts) || _rapierTransformSplices(beforeSource, parts) !== _rapierTransformSplices(beforeSource, [original]))
413
378
  throw Error('hunk_projection');
414
379
  rows = parts.flatMap(hunks);
@@ -421,14 +386,11 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
421
386
  const start = boundary(row.pos), end = boundary(row.pos + row.removed.length);
422
387
  let at = start.last, count = Math.max(0, end.first - at);
423
388
  if (!row.removed.length) {
424
- // A new insertion at a replacement's left boundary stays before that replacement.
425
389
  const marker = pieces.slice(start.first, start.last).findIndex(item => item.mark?.inserted.length);
426
390
  at = marker < 0 ? start.last : start.first + marker; count = 0;
427
391
  }
428
392
  const removed = pieces.slice(at, at + count);
429
393
  if (textOf(removed) !== row.removed) throw Error('replay');
430
- // A semantic owner may retain a drawing hunk while prose in that same act undoes.
431
- // The predicate only selects transient hunks; every original row still replays.
432
394
  if (chosen && options.keepHunk) {
433
395
  const keep = options.keepHunk({actId: tx.id, spliceIndex: rowIndex, hunkIndex: index,
434
396
  splice: {pos: row.pos, removed: row.removed, inserted: row.inserted}, beforeSource: textOf(pieces), actSource, priorSplices});
@@ -459,10 +421,6 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
459
421
  let offset = 0;
460
422
  for (const piece of pieces) if (piece.text) { origins.set(piece, offset); offset += piece.text.length; }
461
423
  const superseded = [];
462
- // Survival means an inverse left this hunk in place because a later independent
463
- // edit still covered it. Once that edit is gone, deactivating its last owner through
464
- // any inverse path withdraws every exposed hunk. Undoing only the later
465
- // edit reveals the survived insertion and does not discard it.
466
424
  for (const hunk of all) if (hunk.survived) {
467
425
  const covered = [...hunk.supersededBy].some(id => {
468
426
  const next = byId.get(id);
@@ -486,12 +444,8 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
486
444
  reason: hunk.active ? 'later_edit' : 'already_undone'});
487
445
  continue;
488
446
  }
489
- // Restoring an inverse can restore a deletion marker without any owned text.
490
447
  restoreHunk(hunk);
491
448
  }
492
- // A hidden withdrawal can have no splice of its own. Reversing that withdrawal
493
- // reactivates the shared hunk through the inverse that still carries it.
494
- // Withdrawing a later act reveals that same hunk while any original author remains.
495
449
  const reactivated = new Set(records.filter(record => !prepared.effects.active.has(record.transaction.id) &&
496
450
  projectedEffects.active.has(record.transaction.id)).map(record => record.transaction.id));
497
451
  for (const hunk of all) if (!hunk.reversedHunk && !hunk.derived && hunk.coverIds?.length > 1 &&
@@ -5,14 +5,10 @@ const _RAPIER_TRANSACTION_ACTOR_LIMIT = 160;
5
5
  const _RAPIER_TRANSACTION_OPERATION_LIMIT = 96;
6
6
  const _RAPIER_TRANSACTION_REQUEST_LIMIT = 160;
7
7
 
8
- /* Validate every row, not only the final string: a half-pair replacement can produce a whole
9
- character while leaving unencodable fragments in the ledger; two invalid rows can also repair
10
- each other. Refusal is atomic. Pre-existing damage elsewhere stays untouched and editable. */
11
8
  function _rapierTransformSplices(text, splices, inverse = false) {
12
9
  try {
13
10
  if (!Array.isArray(splices)) return null;
14
11
  let value = String(text == null ? '' : text);
15
- // In Unicode mode a complete pair is one scalar, outside the surrogate range.
16
12
  const whole = text => typeof text === 'string' && !/[\uD800-\uDFFF]/u.test(text);
17
13
  const splitsPair = at => at > 0 && at < value.length &&
18
14
  (value.charCodeAt(at - 1) & 0xFC00) === 0xD800 &&
@@ -81,7 +77,6 @@ function _rapierTransformMetadata(value, effect, inverse = false) {
81
77
  return next;
82
78
  }
83
79
 
84
- // An origin binds transported placement to exact authored rows and their causal basis.
85
80
  function _rapierValidAuthored(origin) {
86
81
  if (!_rapierPlainLedgerData(origin) || !origin || Array.isArray(origin) ||
87
82
  Object.keys(origin).length !== 3 || !Array.isArray(origin.basis) ||
@@ -95,9 +90,6 @@ function _rapierValidAuthored(origin) {
95
90
  !/[\uD800-\uDFFF]/u.test(row.removed) && !/[\uD800-\uDFFF]/u.test(row.inserted));
96
91
  }
97
92
 
98
- // The lookup is an observation of one call's supplied records. Material rows keep
99
- // their existing borrowed-row API; an inverse owns fresh rows. Navigation only
100
- // follows strictly earlier revisions, including when an unadmitted list is unordered.
101
93
  function _rapierRecordIndex(records) {
102
94
  const byId = new Map(), splices = new Map(), metadata = new Map();
103
95
  for (const record of records) {
@@ -139,8 +131,6 @@ function _rapierRecordSplices(entry, ledger) {
139
131
  } catch (_) { return null; }
140
132
  }
141
133
 
142
- // Source identifies a retained checkpoint even when host revision numbers differ.
143
- // The History Slider compares now → earlier: that answer is the earlier → now walk, inverted.
144
134
  function journalOperations(before, after, journal) {
145
135
  if (typeof before !== 'string' || typeof after !== 'string' || !Array.isArray(journal)) return;
146
136
  if (before === after) return [];
@@ -151,15 +141,34 @@ function journalOperations(before, after, journal) {
151
141
 
152
142
  function journalWalk(before, after, journal) {
153
143
  try {
154
- let text = after;
144
+ const pieces = [after];
155
145
  const lookup = _rapierRecordIndex(journal), groups = [];
146
+ let length = after.length;
147
+ const locate = at => { let index = 0; for (; index < pieces.length - 1 && at >= pieces[index].length; index++) at -= pieces[index].length; return [index, at]; };
148
+ const codeAt = at => { const [index, offset] = locate(at); return pieces[index].charCodeAt(offset); };
149
+ const read = (from, to) => { let out = ''; for (let [index, offset] = locate(from); from < to && index < pieces.length; index++, offset = 0) { const part = pieces[index].slice(offset, offset + to - from); out += part; from += part.length; } return out; };
150
+ const cut = at => { const [index, offset] = locate(at); if (offset > 0 && offset < pieces[index].length) pieces.splice(index, 1, pieces[index].slice(0, offset), pieces[index].slice(offset)); };
151
+ const whole = text => typeof text === 'string' && !/[\uD800-\uDFFF]/u.test(text);
152
+ const splitsPair = at => at > 0 && at < length && (codeAt(at - 1) & 0xFC00) === 0xD800 && (codeAt(at) & 0xFC00) === 0xDC00;
156
153
  for (let index = journal.length - 1; index >= 0; index--) {
157
154
  const rows = lookup.splices(journal[index]);
158
- const prior = _rapierTransformSplices(text, rows, true);
159
- if (prior === null) return;
155
+ if (!Array.isArray(rows)) return;
156
+ for (const row of rows.slice().reverse()) {
157
+ if (!row || !Number.isSafeInteger(row.pos) || row.pos < 0 || Object.is(row.pos, -0)) return;
158
+ const pos = row.pos, removed = row.inserted, inserted = row.removed;
159
+ if (!whole(removed) || !whole(inserted) || pos > length || pos + removed.length > length ||
160
+ splitsPair(pos) || splitsPair(pos + removed.length) || read(pos, pos + removed.length) !== removed) return;
161
+ cut(pos); cut(pos + removed.length);
162
+ let [first] = locate(pos), count = 0;
163
+ if (pos === length) first = pieces.length;
164
+ else if (locate(pos)[1] !== 0) first++;
165
+ for (let left = removed.length; left > 0; count++) left -= pieces[first + count].length;
166
+ pieces.splice(first, count, ...(inserted ? [inserted] : []));
167
+ length += inserted.length - removed.length;
168
+ for (let at = pieces.length - 1; at >= 0; at--) if (!pieces[at] && pieces.length > 1) pieces.splice(at, 1);
169
+ }
160
170
  groups.push(rows);
161
- if (prior === before) return groups.reverse().flat().map(({pos, removed, inserted}) => ({pos, removed, inserted}));
162
- text = prior;
171
+ if (length === before.length && pieces.join('') === before) return groups.reverse().flat().map(({pos, removed, inserted}) => ({pos, removed, inserted}));
163
172
  }
164
173
  } catch (_) { return; }
165
174
  }
@@ -167,14 +176,11 @@ function journalWalk(before, after, journal) {
167
176
  function _rapierRecordMetadata(entry, ledger) {
168
177
  try {
169
178
  if (Object.hasOwn(entry, 'metadata')) return _rapierValidMetadataEffect(entry.metadata) ? entry.metadata : undefined;
170
- // An expanded record owns its complete effect; only compact navigation inherits one.
171
179
  if (Array.isArray(entry.splices)) return null;
172
180
  return _rapierRecordIndex(ledger || []).metadata(entry);
173
181
  } catch (_) { return undefined; }
174
182
  }
175
183
 
176
- // A codec replacement owns only one inserted definition and its reference. Its
177
- // source act remains the picture's author; an arbitrary edit cannot claim upkeep.
178
184
  function _rapierPictureFinish(record, original, earlier, lookup) {
179
185
  const tx = record?.transaction, rows = record?.splices;
180
186
  if (tx?.operation !== 'document.finish-picture' || !tx.sourceTransactionId || tx.sourceTransactionIds ||
@@ -221,9 +227,6 @@ function _rapierPictureFinish(record, original, earlier, lookup) {
221
227
  });
222
228
  }
223
229
 
224
- // Every active inverse keeps its original targets suppressed. Independent inverses
225
- // can share a target; withdrawing one restores it only after the last blocker leaves.
226
- // This includes assignments hidden by later choices. No second history is retained.
227
230
  function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null) {
228
231
  try {
229
232
  if (!Array.isArray(records)) return {ok: false};
@@ -238,9 +241,6 @@ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null
238
241
  if (was !== node.active) for (const ref of node.references) pending.push([ref, node.active ? 1 : -1]);
239
242
  }
240
243
  };
241
- // Most admission needs only the final effect graph. A backwards pass proves it
242
- // in linear work; prefix activity is asked only by native Redo, picture custody,
243
- // or a metadata assignment. The explicit stack also bounds long inverse chains.
244
244
  const activity = new Map();
245
245
  const active = id => {
246
246
  if (visit) return nodes.get(id).active;
@@ -258,8 +258,6 @@ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null
258
258
  return activity.get(id);
259
259
  };
260
260
  const lastUndo = new Map(), contexts = new Map(), order = [];
261
- // Native Redo names an original act. Its inverse target is the last Undo
262
- // in the author's proved causal view, not an unrelated later arrival.
263
261
  const contextOf = origin => {
264
262
  if (!origin) return null;
265
263
  if (!_rapierValidAuthored(origin)) throw Error('origin');
@@ -338,8 +336,6 @@ function _rapierHistoryEffects(records, visit = null) {
338
336
  return _rapierWalkHistoryEffects(records, null, visit);
339
337
  }
340
338
 
341
- // Every transition constrains the two initial metadata scalars. This proves each
342
- // inverse's result once, before the caller supplies the exact initial metadata.
343
339
  function _rapierMetadataProof(lookup) {
344
340
  const values = {}, required = {}, assignments = {filename: [], docKind: []};
345
341
  const requireInitial = (key, value) => {
@@ -355,7 +351,6 @@ function _rapierMetadataProof(lookup) {
355
351
  values[key] = pair.after;
356
352
  if (!refs.length) assignments[key].push({id: record.transaction.id, value: pair.after});
357
353
  }
358
- // Source-only rows cannot change a previously proved metadata assignment.
359
354
  if (!effect && !refs.length) return;
360
355
  for (const key of ['filename', 'docKind']) {
361
356
  const expected = assignments[key].findLast(row => active(row.id));
@@ -385,7 +380,6 @@ function _rapierMetadataState(initial, records, effects = _rapierHistoryEffects(
385
380
  } catch (_) { return null; }
386
381
  }
387
382
 
388
- // Prove each recorded transition and each inverse's semantic result, not only the final values.
389
383
  function _rapierReplayMetadata(initial, records) {
390
384
  if (!_rapierValidMetadata(initial)) return null;
391
385
  try {
@@ -405,16 +399,9 @@ function _rapierHasHistoryEffect(record, earlier = []) {
405
399
  return _rapierHistoryEffects([...earlier, {...record, transaction: {...tx, id}}]).ok;
406
400
  }
407
401
 
408
- // A cached observation owns every byte-bearing record. Mutable graphs are compared
409
- // through descriptors on every call; only a proved, recursively frozen graph can
410
- // bypass that walk. Per-object weak entries keep one latest copy, not a revision log.
411
402
  const _rapierPrepareHistoryRecords = (() => {
412
403
  const copies = new WeakMap();
413
- // Repeated queries share the latest proved snapshot. A live but inactive input
414
- // does not need to retain its record/effect indexes; immutable copies remain weak.
415
404
  let prepared = new WeakMap();
416
- // Object.freeze does not freeze Map/Set entries. Keep those collections inside
417
- // the preparation and expose only immutable values through reading closures.
418
405
  const view = collection => Object.freeze({size: collection.size, has: key => collection.has(key),
419
406
  ...(collection instanceof Map ? {get: key => collection.get(key)} : {}),
420
407
  keys: () => collection.keys(), values: () => collection.values(), entries: () => collection.entries(),
@@ -456,8 +443,6 @@ const _rapierPrepareHistoryRecords = (() => {
456
443
  frame.previous = previous?.copy;
457
444
  frame.height = 0;
458
445
  if (previous?.frozen) {
459
- // A frozen shell fixes its descriptors, but every mutable child is still
460
- // observed afresh. Fixed scalar fields need no second Unicode/data proof.
461
446
  frame.frozen = frame.immutable = true;
462
447
  frame.descriptors = previous.descriptors; frame.fields = previous.fields;
463
448
  frame.out = null; frame.leaving = true; active.add(value); pending.push(frame);
@@ -467,8 +452,6 @@ const _rapierPrepareHistoryRecords = (() => {
467
452
  }
468
453
  continue;
469
454
  }
470
- // Check frozen-ness before capturing descriptors. A caller freezing a mutable
471
- // input later cannot retroactively certify an earlier view of its properties.
472
455
  frame.frozen = frame.immutable = Object.isFrozen(value);
473
456
  const array = Array.isArray(value), proto = Object.getPrototypeOf(value);
474
457
  if (!array && !plainPrototype(proto)) throw Error('history_data');
@@ -535,12 +518,6 @@ const _rapierPrepareHistoryRecords = (() => {
535
518
  };
536
519
  })();
537
520
 
538
- // A ledger record as the journal and the Notes return validate it below: plain data, nothing
539
- // undefined anywhere in it. A live record could carry a row whose type or order is not yet known (a
540
- // block just inserted, before its first render: `type: undefined`), and the validator would refuse
541
- // the whole ledger for that one field -- silently at boot, where a refused ledger is cleared, and
542
- // as "Rapier could not be reopened: the document history could not be restored" on a return from
543
- // Notes. The copy is made once per record; records are frozen, so it never goes stale.
544
521
  const _rapierJournalRecord = (() => {
545
522
  const copies = new WeakMap();
546
523
  return record => {
@@ -551,10 +528,6 @@ const _rapierJournalRecord = (() => {
551
528
  })();
552
529
 
553
530
  function _rapierValidLedgerRecord(record, prior, earlier, documentAuthority, maxDocumentBytes, utf8Length, allowGap = false) {
554
- // Recovery records are data, not executable objects. Inspect descriptors without invoking
555
- // accessors, and use an ancestor set (not a global seen set) so shared acyclic data stays valid.
556
- // Iterative traversal refuses cycles without exhausting the call stack. Proxies/counters that
557
- // throw are refused by the same gate rather than escaping into document recovery.
558
531
  try {
559
532
  if (!_rapierPlainLedgerData(record) || !Array.isArray(earlier)) return false;
560
533
  const own = Array.isArray(record?.splices), lookup = own ? null : _rapierRecordIndex(earlier);
@@ -566,8 +539,6 @@ function _rapierValidLedgerRecord(record, prior, earlier, documentAuthority, max
566
539
  } catch (_) { return false; }
567
540
  }
568
541
 
569
- // Both callers proved plain data themselves: the public single-row validator above,
570
- // or the private owned-snapshot preparation. No caller-supplied trusted flag exists.
571
542
  function _rapierCheckLedgerRecord(record, prior, documentAuthority, maxDocumentBytes, utf8Length, allowGap,
572
543
  {splices, metadata, semantic, duplicate}) {
573
544
  try {
@@ -579,8 +550,6 @@ function _rapierCheckLedgerRecord(record, prior, documentAuthority, maxDocumentB
579
550
  if ((!own && !navigation) || (own && record.splices.length > 64) ||
580
551
  (record.transaction?.reverts && record.transaction?.reapplies)) return false;
581
552
  const metadataChanged = Object.values(metadata || {}).some(pair => pair.before !== pair.after);
582
- // An identical concurrent replacement keeps its authored rows and an empty
583
- // physical splice. The origin proof, not this gate, shows the effect is already present.
584
553
  const coincidentWitness = !splices?.length && record.beforeHash === record.afterHash &&
585
554
  record.authored && _rapierValidAuthored(record.authored) && record.authored.splices.some(row => row.removed.length);
586
555
  if (metadata === undefined || (!splices?.length && !metadataChanged && !semantic && !coincidentWitness) ||
@@ -597,7 +566,6 @@ function _rapierCheckLedgerRecord(record, prior, documentAuthority, maxDocumentB
597
566
  const transaction = record.transaction;
598
567
  const nullableText = (value, limit, empty = true) => value === null ||
599
568
  (typeof value === 'string' && value.length <= limit && (empty || value.length > 0));
600
- // Identity fields are bounded individually; durable history has no record-count limit.
601
569
  const optional = (value, valid) => value === undefined || valid(value);
602
570
  const identity = (value, limit) => typeof value === 'string' && value.length > 0 && value.length <= limit;
603
571
  if (!transaction || typeof transaction.id !== 'string' || !transaction.id.length ||