rapier-markdown-kit 1.2.40 → 1.2.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -215,7 +215,7 @@ export function willTransition(beforeText, afterText, splices, markerSplices = s
215
215
  return null;
216
216
  }
217
217
 
218
- export function evaluateWill(beforeText, splices) {
218
+ export function evaluateWill(beforeText, splices, {restores = null} = {}) {
219
219
  const invalid = rule => ({outcome: 'invalid', reason: 'precondition', rule});
220
220
  if (typeof beforeText !== 'string' || !beforeText.isWellFormed()) return invalid('invalid_unicode');
221
221
  if (!Array.isArray(splices)) return invalid('malformed_splice');
@@ -231,7 +231,7 @@ export function evaluateWill(beforeText, splices) {
231
231
  if (afterText.slice(row.pos, end) !== row.removed) return invalid('source_mismatch');
232
232
  afterText = afterText.slice(0, row.pos) + row.inserted + afterText.slice(end);
233
233
  }
234
- const refusal = splices.length && willTransition(beforeText, afterText, splices);
234
+ const refusal = splices.length && willTransition(beforeText, afterText, splices, splices, {restores});
235
235
  return refusal ? {outcome: 'refused', reason: 'document_law', ...refusal} : {outcome: 'applied'};
236
236
  }
237
237
 
@@ -44,11 +44,13 @@ function* quoted(text, whole = true) {
44
44
  }
45
45
  yield '"';
46
46
  }
47
- function* dataParts(value, depth, active, sorted) {
47
+ function* dataParts(value, depth, active, sorted, known = null) {
48
48
  if (typeof value === 'string') { yield* quoted(value); return; }
49
49
  if (value === null || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0)) {
50
50
  yield JSON.stringify(value); return;
51
51
  }
52
+ const text = known?.(value, depth);
53
+ if (text !== undefined) { yield text; return; }
52
54
  const {array, descriptors, keys} = fields(value, depth, active, sorted);
53
55
  active.add(value);
54
56
  yield array ? '[' : '{';
@@ -56,14 +58,69 @@ function* dataParts(value, depth, active, sorted) {
56
58
  if (i) yield ',';
57
59
  const key = keys[i];
58
60
  if (!array) { yield* quoted(key, false); yield ':'; }
59
- yield* dataParts(descriptors[key].value, depth + 1, active, sorted);
61
+ yield* dataParts(descriptors[key].value, depth + 1, active, sorted, known);
60
62
  }
61
63
  yield array ? ']' : '}';
62
64
  active.delete(value);
63
65
  }
64
66
  export function canonicalParts(value, depth = 0, active = new Set()) { return dataParts(value, depth, active, true); }
65
67
  export function jsonParts(value) { return dataParts(value, 0, new Set(), false); }
68
+ export function jsonPartsKnown(value, known) { return dataParts(value, 0, new Set(), false, known); }
69
+ export function jsonTextAt(value, depth) { return Array.from(dataParts(value, depth, new Set(), false)).join(''); }
66
70
  export function canonicalJSON(value, depth = 0, active = new Set()) { return Array.from(canonicalParts(value, depth, active)).join(''); }
71
+ const ownedRows = new WeakSet(), rowCanonical = new Map();
72
+ let rowCanonicalChars = 0;
73
+ const ROW_CANONICAL_LIMIT = 32 * 1024 * 1024;
74
+ function freezeData(value) {
75
+ if (!value || typeof value !== 'object' || ownedRows.has(value) || ownedLedgers.has(value)) return value;
76
+ for (const key of Object.keys(value)) freezeData(value[key]);
77
+ return Object.freeze(value);
78
+ }
79
+ export function ownRow(row) { if (!ownedRows.has(row)) { freezeData(row); ownedRows.add(row); } return row; }
80
+ const ownedLedgers = new WeakSet();
81
+ export function ownLedger(ledger) {
82
+ if (ledger && typeof ledger === 'object' && !ownedLedgers.has(ledger)) { freezeData(ledger); ownedLedgers.add(ledger); }
83
+ return ledger;
84
+ }
85
+ export function isOwnedLedger(ledger) { return !!ledger && ownedLedgers.has(ledger); }
86
+ const rowText = new WeakMap();
87
+ function canonicalRow(row, depth) {
88
+ if (!ownedRows.has(row)) return canonicalJSON(row, depth);
89
+ const own = rowText.get(row);
90
+ if (own?.depth === depth) return own.json;
91
+ const json = keyedRow(row, depth);
92
+ rowText.set(row, {depth, json});
93
+ return json;
94
+ }
95
+ function keyedRow(row, depth) {
96
+ const id = typeof row.transaction?.id === 'string' ? row.transaction.id : null, seen = JSON.stringify(row);
97
+ const hit = id === null ? null : rowCanonical.get(id);
98
+ if (hit && hit.depth === depth && hit.seen.includes(seen)) return hit.json;
99
+ const json = canonicalJSON(row, depth);
100
+ if (id !== null && 2 * seen.length + json.length <= ROW_CANONICAL_LIMIT / 4) {
101
+ const size = entry => entry.seen.reduce((sum, text) => sum + text.length, entry.json.length);
102
+ const seenAlso = hit && hit.depth === depth && hit.json === json ? [hit.seen.at(-1), seen] : [seen];
103
+ if (hit) { rowCanonical.delete(id); rowCanonicalChars -= size(hit); }
104
+ const entry = {depth, seen: seenAlso, json};
105
+ rowCanonical.set(id, entry); rowCanonicalChars += size(entry);
106
+ for (const [key, value] of rowCanonical) {
107
+ if (rowCanonicalChars <= ROW_CANONICAL_LIMIT) break;
108
+ rowCanonical.delete(key); rowCanonicalChars -= size(value);
109
+ }
110
+ }
111
+ return json;
112
+ }
113
+ const plainRowArrays = new WeakSet();
114
+ export function* canonicalRowsParts(rows, depth = 0) {
115
+ if (!(depth <= 64 && plainRowArrays.has(rows))) {
116
+ const {array} = fields(rows, depth, new Set());
117
+ if (!array) { yield* canonicalParts(rows, depth); return; }
118
+ if (Object.isFrozen(rows) && depth <= 64) plainRowArrays.add(rows);
119
+ }
120
+ yield '[';
121
+ for (let i = 0; i < rows.length; i++) { if (i) yield ','; yield canonicalRow(rows[i], depth + 1); }
122
+ yield ']';
123
+ }
67
124
  export function exactKeys(value, keys) {
68
125
  return value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
69
126
  }
@@ -4,16 +4,16 @@ import {pictureHistorySplices} from './history.mjs';
4
4
  import {HistorySource} from './source.mjs';
5
5
  import {authoredHistory} from './transport.mjs';
6
6
  import {textRoot, rootAfter, sha256, sha256PartsAsync, createSHA256} from './hash.mjs';
7
- import {canonicalJSON, canonicalParts, plainData, documentText, utf8Length, exactKeys, nonnegative, fault} from './data.mjs';
7
+ import {canonicalJSON, canonicalParts, canonicalRowsParts, ownRow, ownLedger, isOwnedLedger, plainData, documentText, utf8Length, exactKeys, nonnegative, fault} from './data.mjs';
8
8
  import {diff} from '../../packages/rapier-diff/diff.mjs';
9
9
  import {minimalHistorySplice} from './inverse-source.mjs';
10
10
  import {evaluateWill} from '../../agent/will.mjs';
11
11
  export {textRoot, rootAfter};
12
12
  const rootShape = value => typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value);
13
- function* checkedRecords(ledger, prepared) {
13
+ function* checkedRecords(ledger, prepared, from = null) {
14
14
  if (!prepared.ok || ledger.records.length !== prepared.records.length) throw fault('record chain differs');
15
- let root = ledger.start.root, revision = ledger.start.revision;
16
- for (let index = 0; index < prepared.records.length; index++) {
15
+ let root = from ? from.root : ledger.start.root, revision = from ? from.revision : ledger.start.revision;
16
+ for (let index = from ? from.index : 0; index < prepared.records.length; index++) {
17
17
  const record = ledger.records[index];
18
18
  if (!Array.isArray(record.splices) || !prepared.validRecord(index,
19
19
  ledger.documentAuthority, 64 * 1024 * 1024, utf8Length, !ledger.complete) ||
@@ -27,32 +27,47 @@ function* checkedRecords(ledger, prepared) {
27
27
  }
28
28
  if (ledger.head.revision !== revision || ledger.head.root !== root) throw fault('history does not describe this document');
29
29
  }
30
+ function* envelopeParts(envelope) {
31
+ const keys = Object.keys(envelope).sort();
32
+ yield '{';
33
+ for (let i = 0; i < keys.length; i++) {
34
+ if (i) yield ',';
35
+ yield JSON.stringify(keys[i]); yield ':';
36
+ yield* keys[i] === 'records' ? canonicalRowsParts(envelope.records, 1) : canonicalParts(envelope[keys[i]], 1, new Set());
37
+ }
38
+ yield '}';
39
+ }
30
40
  const proofs = new WeakMap();
41
+ function ownedRows(ledger) {
42
+ if (ledger && Array.isArray(ledger.records)) for (const row of ledger.records) if (row && typeof row === 'object') ownRow(row);
43
+ return ledger;
44
+ }
31
45
  function copiedLedger(input, proof) {
32
- if (!proof || !input || typeof input !== 'object') return plainData(input);
46
+ if (!proof || !input || typeof input !== 'object') return ownedRows(plainData(input));
33
47
  const fields = Object.getOwnPropertyDescriptors(input), start = fields.start;
34
- if (!start || !Object.hasOwn(start, 'value') || !start.value || typeof start.value !== 'object') return plainData(input);
48
+ if (!start || !Object.hasOwn(start, 'value') || !start.value || typeof start.value !== 'object') return ownedRows(plainData(input));
35
49
  const parts = Object.getOwnPropertyDescriptors(start.value), text = parts.text;
36
- if (!text || !Object.hasOwn(text, 'value') || text.value !== proof.startText) return plainData(input);
50
+ if (!text || !Object.hasOwn(text, 'value') || text.value !== proof.startText) return ownedRows(plainData(input));
37
51
  const shell = Object.create(Object.getPrototypeOf(input), {...fields, start: {...start,
38
52
  value: Object.create(Object.getPrototypeOf(start.value), {...parts, text: {...text, value: ''}})}});
39
- const ledger = plainData(shell);
53
+ const ledger = ownedRows(plainData(shell));
40
54
  ledger.start.text = proof.startText;
41
55
  return ledger;
42
56
  }
43
57
  function ledgerAdmission(input) {
44
58
  const held = input && typeof input === 'object' ? proofs.get(input) : undefined;
59
+ if (held && isOwnedLedger(input)) return {ledger: input, shell: held.shell, proof: held};
45
60
  const ledger = copiedLedger(input, held);
46
61
  if (!exactKeys(ledger, ['format','documentAuthority','start','records','head','complete','sha256']) || ledger.format !== 'rapier-ledger/2' ||
47
62
  typeof ledger.documentAuthority !== 'string' || !ledger.documentAuthority || ledger.documentAuthority.length > 256 ||
48
63
  typeof ledger.complete !== 'boolean' || !Array.isArray(ledger.records) ||
49
64
  !exactKeys(ledger.start, ['text','revision','root','sha256','metadata']) || !exactKeys(ledger.head, ['revision','root','sha256','metadata'])) throw fault('unsupported or invalid envelope');
50
- const shell = canonicalJSON({...ledger, start: {...ledger.start, text: ''}});
65
+ const shell = Array.from(envelopeParts({...ledger, start: {...ledger.start, text: ''}})).join('');
51
66
  const proof = held?.shell === shell && held.startText === ledger.start.text ? held : null;
52
67
  return {ledger, shell, proof};
53
68
  }
54
69
  function ledgerResult(ledger, proof, expectedText, expectedMetadata) {
55
- proofs.set(ledger, proof);
70
+ proofs.set(ledger, proof); ownLedger(ledger);
56
71
  const {text, metadata, root, revision, prepared, source} = proof;
57
72
  if (expectedMetadata !== undefined && (!_rapierValidMetadata(expectedMetadata) || canonicalJSON(metadata) !== canonicalJSON(expectedMetadata)))
58
73
  throw fault('metadata does not describe this document');
@@ -67,7 +82,7 @@ function readLedgerState(input, expectedText, expectedMetadata) {
67
82
  }
68
83
  function* proveLedger(ledger, native = false) {
69
84
  const {sha256: claimed, ...payload} = ledger;
70
- if (claimed !== (yield* checksum(canonicalParts(payload), native))) throw fault('envelope checksum differs');
85
+ if (claimed !== (yield* checksum(envelopeParts(payload), native))) throw fault('envelope checksum differs');
71
86
  let text = ledger.start.text, root = ledger.start.root, revision = ledger.start.revision;
72
87
  const startFacts = yield* sourceFacts(text, native);
73
88
  if (!_rapierValidMetadata(ledger.start.metadata) || !_rapierValidMetadata(ledger.head.metadata)) throw fault('invalid document metadata');
@@ -151,6 +166,11 @@ function* sourceFacts(text, native = false) {
151
166
  return {utf8Bytes: bytes, sha256: digest, contentRoot: text.length + ':' +
152
167
  (fnv >>> 0).toString(36) + ':' + (((b << 16) | a) >>> 0).toString(36)};
153
168
  }
169
+ const expandedRows = new WeakMap();
170
+ const sameSplices = (a, b) => a === b || a.length === b.length &&
171
+ a.every((row, i) => row.pos === b[i].pos && row.removed === b[i].removed && row.inserted === b[i].inserted);
172
+ const sameEffect = (a, b) => a === b || !!a && !!b && canonicalJSON(a) === canonicalJSON(b);
173
+ let recentLedgers = [];
154
174
  function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}, native = false) {
155
175
  if (!_rapierValidMetadata(metadata)) throw fault('document metadata required');
156
176
  metadata = {...metadata};
@@ -162,23 +182,40 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
162
182
  const expanded = [];
163
183
  for (const {record, splices: rows, metadata: effect} of prepared.rows) {
164
184
  if (!rows || effect === undefined) throw fault('navigation target was not retained');
165
- expanded.push({transaction: plainData(record.transaction), splices: rows.map(row => ({pos: row.pos, removed: row.removed, inserted: row.inserted})),
185
+ const held = expandedRows.get(record);
186
+ if (held && sameSplices(held.rows, rows) && sameEffect(held.effect, effect)) { expanded.push(held.row); yield; continue; }
187
+ expanded.push(ownRow({transaction: plainData(record.transaction), splices: rows.map(row => ({pos: row.pos, removed: row.removed, inserted: row.inserted})),
166
188
  beforeHash: record.beforeHash, afterHash: record.afterHash, ...(Object.hasOwn(record, 'authored') ? {authored: plainData(record.authored)} : {}), ...(effect ? {metadata: plainData(effect)} : {}),
167
189
  ...(record.changeSet && Object.hasOwn(record.changeSet, 'label') ? {changeSet: {label: typeof record.changeSet.label === 'string' ? record.changeSet.label : plainData(record.changeSet.label)}} : {}),
168
- ...(record.derivedCommentIndex == null ? {} : {derivedCommentIndex: typeof record.derivedCommentIndex === 'string' ? record.derivedCommentIndex : plainData(record.derivedCommentIndex)})});
190
+ ...(record.derivedCommentIndex == null ? {} : {derivedCommentIndex: typeof record.derivedCommentIndex === 'string' ? record.derivedCommentIndex : plainData(record.derivedCommentIndex)})}));
191
+ expandedRows.set(record, {row: expanded.at(-1), rows, effect});
169
192
  yield;
170
193
  }
171
194
  const headBytes = bytes;
195
+ let base = recentLedgers.find(row => row.rows.length <= expanded.length && row.rows.every((value, at) => value === expanded[at]));
172
196
  let startSource = new HistorySource(text), startMetadata = {...metadata};
173
- for (let i = expanded.length - 1; i >= 0; i--) {
197
+ for (let i = expanded.length - 1; i >= (base ? base.rows.length : 0); i--) {
174
198
  startSource = startSource.apply(expanded[i].splices, true);
175
- startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
199
+ if (expanded[i].metadata) startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
176
200
  if (startSource === null || !startMetadata) throw fault('history does not reverse from this document');
177
201
  for (const row of expanded[i].splices) bytes += utf8Length(row.removed) - utf8Length(row.inserted);
178
202
  if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
179
203
  yield;
180
204
  }
181
- const startText = startSource.slice();
205
+ if (base && (bytes !== base.headBytes || startSource.length !== base.headText.length || !startSource.matches(0, base.headText) ||
206
+ canonicalJSON(startMetadata) !== base.headMetadata)) {
207
+ base = null; bytes = headBytes; startSource = new HistorySource(text); startMetadata = {...metadata};
208
+ for (let i = expanded.length - 1; i >= 0; i--) {
209
+ startSource = startSource.apply(expanded[i].splices, true);
210
+ if (expanded[i].metadata) startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
211
+ if (startSource === null || !startMetadata) throw fault('history does not reverse from this document');
212
+ for (const row of expanded[i].splices) bytes += utf8Length(row.removed) - utf8Length(row.inserted);
213
+ if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
214
+ yield;
215
+ }
216
+ }
217
+ if (base) { startMetadata = {...base.startMetadata}; bytes = base.startBytes; }
218
+ const startText = base ? base.startText : startSource.slice();
182
219
  const unchangedText = expanded.every(row => !row.splices.length);
183
220
  const contentRoot = headFacts.contentRoot;
184
221
  revision ??= expanded.at(-1)?.transaction.revision ?? 0;
@@ -187,20 +224,26 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
187
224
  const startRevision = expanded[0]?.transaction.baseRevision ?? revision, startRoot = expanded[0]?.beforeHash ?? root;
188
225
  if (typeof documentAuthority !== 'string' || !documentAuthority || documentAuthority.length > 256) throw fault('unsupported or invalid envelope');
189
226
  if (!nonnegative(startRevision) || !rootShape(startRoot)) throw fault('invalid start');
190
- const startFacts = unchangedText ? headFacts : yield* sourceFacts(startText, native);
227
+ const startFacts = unchangedText ? headFacts : base ? base.startFacts : yield* sourceFacts(startText, native);
191
228
  if (startRevision === 0 && startRoot !== startFacts.contentRoot) throw fault('invalid start');
192
229
  const headSHA = headFacts.sha256, startSHA = startFacts.sha256;
193
230
  const payload = {format: 'rapier-ledger/2', documentAuthority,
194
231
  start: {text: startText, revision: startRevision, root: startRoot, sha256: startSHA, metadata: startMetadata},
195
232
  records: expanded, head: {revision, root, sha256: headSHA, metadata: {...metadata}}, complete: complete === true && startRevision === 0 && expanded.every((row, i) => !i || row.transaction.baseRevision === expanded[i - 1].transaction.revision)};
196
- for (const _record of checkedRecords(payload, prepared)) yield;
233
+ const from = base && base.documentAuthority === documentAuthority && base.complete === payload.complete &&
234
+ base.startRoot === startRoot && base.startRevision === startRevision ? {index: base.rows.length, root: base.chainRoot, revision: base.chainRevision} : null;
235
+ for (const _record of checkedRecords(payload, prepared, from)) yield;
197
236
  if (expanded.some(row => Object.hasOwn(row, 'authored'))) {
198
237
  try { authoredHistory(startText, expanded); } catch (_) { throw fault('authored origin differs'); }
199
238
  }
200
239
  const replayedMetadata = prepared.replayMetadata(startMetadata);
201
240
  if (!replayedMetadata || canonicalJSON(replayedMetadata) !== canonicalJSON(metadata)) throw fault('metadata does not describe this document');
202
- const ledger = {...payload, sha256: yield* checksum(canonicalParts(payload), native)};
203
- proofs.set(ledger, {shell: canonicalJSON({...ledger, start: {...ledger.start, text: ''}}), startText,
241
+ const ledger = {...payload, sha256: yield* checksum(envelopeParts(payload), native)};
242
+ recentLedgers = [{rows: expanded, headText: text, headMetadata: canonicalJSON(metadata), headBytes, startText, startMetadata: {...startMetadata},
243
+ startFacts, startBytes: bytes, documentAuthority, complete: payload.complete, startRoot, startRevision, chainRoot: payload.head.root,
244
+ chainRevision: payload.head.revision}, ...recentLedgers.filter(row => row.rows[0] !== expanded[0] && row.documentAuthority === documentAuthority)].slice(0, 4);
245
+ ownLedger(ledger);
246
+ proofs.set(ledger, {shell: Array.from(envelopeParts({...ledger, start: {...ledger.start, text: ''}})).join(''), startText,
204
247
  text, metadata: {...metadata}, root, revision, prepared: _rapierPrepareHistoryRecords(ledger.records),
205
248
  source: new HistorySource(text)});
206
249
  return {ledger, contentRoot, utf8Bytes: headBytes};
@@ -250,7 +293,7 @@ function historyNavigation(ledger, text, metadata, prepared, source) {
250
293
  let traversed = source, traversedMetadata = {...metadata};
251
294
  for (let i = cursor - 1; i >= 0; i--) {
252
295
  traversed = step(traversed, branch[i], false);
253
- traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata, true);
296
+ if (byId.get(branch[i]).metadata) traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata, true);
254
297
  if (traversed === null || !traversedMetadata) throw fault('undo branch does not reverse');
255
298
  }
256
299
  if (ledger.complete) {
@@ -261,7 +304,7 @@ function historyNavigation(ledger, text, metadata, prepared, source) {
261
304
  traversed = source; traversedMetadata = {...metadata};
262
305
  for (let i = cursor; i < branch.length; i++) {
263
306
  traversed = step(traversed, branch[i], true);
264
- traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata);
307
+ if (byId.get(branch[i]).metadata) traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata);
265
308
  if (traversed === null || !traversedMetadata) throw fault('redo branch does not replay');
266
309
  }
267
310
  if (ledger.complete && pictures.size) {
@@ -363,7 +363,7 @@ export function selectiveUndo(input, actIds, options = {}) {
363
363
  const result = projectInverseSource(transported ? {...prepared, rows, records: transported.records} : prepared, selected, projectedEffects, options);
364
364
  if (!result.ok) return result;
365
365
  if (options.actor?.kind === 'agent' && prepared.metadata.docKind === 'markdown' &&
366
- evaluateWill(prepared.source, result.splices).outcome !== 'applied')
366
+ evaluateWill(prepared.source, result.splices, {restores: result.splices}).outcome !== 'applied')
367
367
  return historyFailure('will_refused');
368
368
  return {...result, actIds: [...requested], sourceTransactionIds: [...selected], metadata, afterMetadata,
369
369
  effectChanged, unchanged: result.after === prepared.source && !metadata && !effectChanged};
@@ -32,6 +32,8 @@ function historyHunks(row) {
32
32
  }
33
33
 
34
34
 
35
+ const differentActors = (a, b) => !!a && !!b && (a.kind !== b.kind || a.id !== b.id);
36
+
35
37
  export function projectInverseSource(prepared, selected, projectedEffects, options = {}) {
36
38
  const records = prepared.records || prepared.rows.map(({record, splices}) => record.splices === splices ? record : {...record, splices});
37
39
  if (!options.origins && !records.some(record => !prepared.effects.references.get(record.transaction.id)?.length &&
@@ -40,6 +42,7 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
40
42
  try {
41
43
  let pieces = prepared.initialSource ? [{text: prepared.initialSource, owner: null, lineage: []}] : [];
42
44
  const all = [], byId = new Map(), byAct = new Map(), processed = [], keptInverseAncestors = new Set();
45
+ const actorOf = new Map(records.map(record => [record.transaction.id, record.transaction.actor]));
43
46
  const recordsForCover = prepared.records || prepared.rows.map(({record, splices}) => record.splices === splices ? record : {...record, splices});
44
47
  const coverGroups = new Map();
45
48
  let coverOrigins = null;
@@ -280,6 +283,12 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
280
283
  const id = tx.id + '#' + rowIndex + ':' + index;
281
284
  const lineage = new Set([id]);
282
285
  for (const piece of removed) for (const parent of piece.lineage || piece.mark?.lineage || []) lineage.add(parent);
286
+ if (!row.removed.length && !count && at > 0 && /[\p{L}\p{N}\p{M}]/u.test(row.inserted)) {
287
+ const left = pieces[at - 1], right = pieces[at];
288
+ const inner = left?.text && right?.text && left.owner === right.owner ? byId.get(left.owner) : null;
289
+ if (inner && inner.actId !== tx.id && inner.removed && differentActors(actorOf.get(inner.actId), tx.actor) && /[\p{L}\p{N}\p{M}]$/u.test(left.text) && /^[\p{L}\p{N}\p{M}]/u.test(right.text))
290
+ for (const parent of [inner.id, ...(left.lineage || [])]) lineage.add(parent);
291
+ }
283
292
  const hunk = {id, actId: tx.id, removed: row.removed, inserted: row.inserted, before: removed, derived,
284
293
  chosen, kept: selected.has(tx.id) && !chosen, lineage: [...lineage], supersededBy: new Set(), active: true,
285
294
  ...(coverIdsFor(tx.id) ? {coverIds: coverIdsFor(tx.id)} : {})};
@@ -76,7 +76,7 @@ function _rapierValidAuthored(origin) {
76
76
  !/[\uD800-\uDFFF]/u.test(row.removed) && !/[\uD800-\uDFFF]/u.test(row.inserted));
77
77
  }
78
78
 
79
- function _rapierRecordIndex(records) {
79
+ function _rapierRecordIndex(records, shared = false) {
80
80
  const byId = new Map(), splices = new Map(), metadata = new Map();
81
81
  for (const record of records) {
82
82
  const id = record?.transaction?.id;
@@ -100,20 +100,41 @@ function _rapierRecordIndex(records) {
100
100
  }
101
101
  cache.set(current, value);
102
102
  for (let i = path.length - 1; i >= 0; i--) {
103
- if (path[i].transaction.reverts && value != null) value = effects
104
- ? Object.fromEntries(Object.entries(value).map(([key, pair]) => [key, {before: pair.after, after: pair.before}]))
105
- : value.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed}));
103
+ if (path[i].transaction.reverts && value != null) {
104
+ value = effects
105
+ ? Object.fromEntries(Object.entries(value).map(([key, pair]) => [key, {before: pair.after, after: pair.before}]))
106
+ : value.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed}));
107
+ if (shared) { for (const row of Object.values(value)) Object.freeze(row); Object.freeze(value); }
108
+ }
106
109
  cache.set(path[i], value);
107
110
  }
108
111
  return value;
109
112
  };
110
- return {byId, splices: record => resolve(record, false), metadata: record => resolve(record, true)};
113
+ const extend = more => { for (const record of more) { const id = record?.transaction?.id; if (!byId.has(id)) byId.set(id, []); byId.get(id).push(record); } };
114
+ return {byId, extend, splices: record => resolve(record, false), metadata: record => resolve(record, true)};
115
+ }
116
+
117
+ const _rapierLedgerIndexes = new WeakMap();
118
+ function _rapierLedgerIndex(ledger) {
119
+ const records = ledger || [];
120
+ if (!Array.isArray(records)) return _rapierRecordIndex(records);
121
+ const held = _rapierLedgerIndexes.get(records);
122
+ if (held && held.records.length === records.length && held.records.at(-1) === records.at(-1) &&
123
+ (held.checked || held.records.every((record, at) => record === records[at]))) {
124
+ if (!held.checked) { held.checked = true; queueMicrotask(() => { held.checked = false; }); }
125
+ return held.index;
126
+ }
127
+ if (!records.every(record => record && typeof record === 'object' && Object.isFrozen(record) && Object.isFrozen(record.transaction ?? {})))
128
+ return _rapierRecordIndex(records);
129
+ const index = _rapierRecordIndex(records, true);
130
+ _rapierLedgerIndexes.set(records, {records: records.slice(), index});
131
+ return index;
111
132
  }
112
133
 
113
134
  function _rapierRecordSplices(entry, ledger) {
114
135
  try {
115
136
  if (entry && Array.isArray(entry.splices)) return entry.splices;
116
- return _rapierRecordIndex(ledger || []).splices(entry);
137
+ return _rapierLedgerIndex(ledger).splices(entry);
117
138
  } catch (_) { return null; }
118
139
  }
119
140
 
@@ -163,7 +184,7 @@ function _rapierRecordMetadata(entry, ledger) {
163
184
  try {
164
185
  if (Object.hasOwn(entry, 'metadata')) return _rapierValidMetadataEffect(entry.metadata) ? entry.metadata : undefined;
165
186
  if (Array.isArray(entry.splices)) return null;
166
- return _rapierRecordIndex(ledger || []).metadata(entry);
187
+ return _rapierLedgerIndex(ledger).metadata(entry);
167
188
  } catch (_) { return undefined; }
168
189
  }
169
190
 
@@ -213,87 +234,89 @@ function _rapierPictureFinish(record, original, earlier, lookup) {
213
234
  });
214
235
  }
215
236
 
216
- function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null) {
217
- try {
218
- if (!Array.isArray(records)) return {ok: false};
219
- const nodes = new Map(), references = new Map(), pictures = new Map(), byId = new Map(), finished = new Set(), earlier = [];
220
- const update = (target, delta) => {
221
- const pending = [[target, delta]];
222
- while (pending.length) {
223
- const [id, change] = pending.pop(), node = nodes.get(id), was = node.active;
224
- node.blockers += change;
225
- if (node.blockers < 0) throw Error('effect');
226
- node.active = node.blockers === 0;
227
- if (was !== node.active) for (const ref of node.references) pending.push([ref, node.active ? 1 : -1]);
228
- }
229
- };
230
- const activity = new Map();
231
- const active = id => {
232
- if (visit) return nodes.get(id).active;
233
- const pending = [[id, 0]];
234
- while (pending.length) {
235
- const frame = pending.at(-1), target = frame[0];
236
- if (activity.has(target)) { pending.pop(); continue; }
237
- const dependents = nodes.get(target).dependents;
238
- if (frame[1] === dependents.length) { activity.set(target, true); pending.pop(); continue; }
239
- const next = dependents[frame[1]];
240
- if (!activity.has(next)) pending.push([next, 0]);
241
- else if (activity.get(next)) { activity.set(target, false); pending.pop(); }
242
- else frame[1]++;
243
- }
244
- return activity.get(id);
245
- };
246
- const lastUndo = new Map(), contexts = new Map(), order = [];
247
- const contextOf = origin => {
248
- if (!origin) return null;
249
- if (!_rapierValidAuthored(origin)) throw Error('origin');
250
- const known = new Set(), pending = origin.basis.slice();
251
- while (pending.length) {
252
- const id = pending.pop(), context = contexts.get(id);
253
- if (!context) throw Error('origin');
254
- if (known.has(id)) continue;
255
- known.add(id);
256
- if (context.basis) pending.push(...context.basis);
257
- else for (let index = 0; index < context.index; index++) known.add(order[index]);
258
- }
259
- const inactive = new Set();
260
- for (let index = order.length - 1; index >= 0; index--) {
261
- const id = order[index];
262
- if (!known.has(id) || inactive.has(id)) continue;
263
- for (const ref of references.get(id)) {
264
- if (!known.has(ref)) throw Error('origin');
265
- inactive.add(ref);
266
- }
237
+ function _rapierHistoryWalker(records, lookup, visit = null, observe = null) {
238
+ let nodes = new Map(), references = new Map(), pictures = new Map();
239
+ const byId = new Map(), finished = new Set(), earlier = [];
240
+ const update = (target, delta) => {
241
+ const pending = [[target, delta]];
242
+ while (pending.length) {
243
+ const [id, change] = pending.pop(), node = nodes.get(id), was = node.active;
244
+ node.blockers += change;
245
+ if (node.blockers < 0) throw Error('effect');
246
+ node.active = node.blockers === 0;
247
+ if (was !== node.active) for (const ref of node.references) pending.push([ref, node.active ? 1 : -1]);
248
+ }
249
+ };
250
+ const activity = new Map();
251
+ const active = id => {
252
+ if (visit) return nodes.get(id).active;
253
+ const pending = [[id, 0]];
254
+ while (pending.length) {
255
+ const frame = pending.at(-1), target = frame[0];
256
+ if (activity.has(target)) { pending.pop(); continue; }
257
+ const dependents = nodes.get(target).dependents;
258
+ if (frame[1] === dependents.length) { activity.set(target, true); pending.pop(); continue; }
259
+ const next = dependents[frame[1]];
260
+ if (!activity.has(next)) pending.push([next, 0]);
261
+ else if (activity.get(next)) { activity.set(target, false); pending.pop(); }
262
+ else frame[1]++;
263
+ }
264
+ return activity.get(id);
265
+ };
266
+ const lastUndo = new Map(), contexts = new Map(), order = [];
267
+ const contextOf = origin => {
268
+ if (!origin) return null;
269
+ if (!_rapierValidAuthored(origin)) throw Error('origin');
270
+ const known = new Set(), pending = origin.basis.slice();
271
+ while (pending.length) {
272
+ const id = pending.pop(), context = contexts.get(id);
273
+ if (!context) throw Error('origin');
274
+ if (known.has(id)) continue;
275
+ known.add(id);
276
+ if (context.basis) pending.push(...context.basis);
277
+ else for (let index = 0; index < context.index; index++) known.add(order[index]);
278
+ }
279
+ const inactive = new Set();
280
+ for (let index = order.length - 1; index >= 0; index--) {
281
+ const id = order[index];
282
+ if (!known.has(id) || inactive.has(id)) continue;
283
+ for (const ref of references.get(id)) {
284
+ if (!known.has(ref)) throw Error('origin');
285
+ inactive.add(ref);
267
286
  }
268
- return {known, inactive};
269
- };
270
- for (const record of records) {
287
+ }
288
+ return {known, inactive};
289
+ };
290
+ const walker = {observe, failed: false};
291
+ walker.add = record => {
292
+ if (walker.failed) return false;
293
+ try {
271
294
  const tx = record?.transaction;
272
- if (!tx || typeof tx.id !== 'string' || !tx.id || nodes.has(tx.id) || (tx.reverts && tx.reapplies)) return {ok: false};
295
+ if (!tx || typeof tx.id !== 'string' || !tx.id || nodes.has(tx.id) || (tx.reverts && tx.reapplies)) { walker.failed = true; return false; }
273
296
  const supplied = tx.sourceTransactionIds || (tx.sourceTransactionId ? [tx.sourceTransactionId] : []);
274
297
  if (!Array.isArray(supplied) || new Set(supplied).size !== supplied.length ||
275
- (tx.sourceTransactionIds && tx.sourceTransactionId && (supplied.length !== 1 || supplied[0] !== tx.sourceTransactionId))) return {ok: false};
298
+ (tx.sourceTransactionIds && tx.sourceTransactionId && (supplied.length !== 1 || supplied[0] !== tx.sourceTransactionId))) { walker.failed = true; return false; }
276
299
  let refs = supplied;
277
300
  if (tx.operation === 'document.finish-picture' && tx.sourceTransactionId) {
278
301
  const owner = tx.sourceTransactionId;
279
302
  if (!nodes.has(owner) || !active(owner) || finished.has(owner) ||
280
- !_rapierPictureFinish(record, byId.get(owner), earlier, lookup ||= _rapierRecordIndex(records))) return {ok: false};
303
+ !_rapierPictureFinish(record, byId.get(owner), earlier, lookup ||= _rapierRecordIndex(records))) { walker.failed = true; return false; }
281
304
  pictures.set(tx.id, owner); finished.add(owner); refs = [];
282
305
  }
283
306
  if (tx.reverts || tx.reapplies) {
284
307
  const target = tx.reverts || tx.reapplies;
285
- if (!nodes.has(target) || (supplied.length && (supplied.length !== 1 || supplied[0] !== target))) return {ok: false};
308
+ if (!nodes.has(target) || (supplied.length && (supplied.length !== 1 || supplied[0] !== target))) { walker.failed = true; return false; }
286
309
  if (tx.reverts) refs = [target];
287
310
  else {
288
311
  const context = contextOf(record.authored || tx.authored);
289
312
  const undo = (lastUndo.get(target) || []).findLast(id => context
290
313
  ? context.known.has(id) && !context.inactive.has(id) : active(id));
291
- if (!undo) return {ok: false};
314
+ if (!undo) { walker.failed = true; return false; }
292
315
  refs = [undo];
293
316
  }
294
317
  }
295
- if (refs.some(id => typeof id !== 'string' || !nodes.has(id) || pictures.has(id))) return {ok: false};
296
- if (tx.actor?.kind === 'agent' && refs.some(id => byId.get(id).transaction.actor?.kind !== 'agent')) return {ok: false};
318
+ if (refs.some(id => typeof id !== 'string' || !nodes.has(id) || pictures.has(id))) { walker.failed = true; return false; }
319
+ if (tx.actor?.kind === 'agent' && refs.some(id => byId.get(id).transaction.actor?.kind !== 'agent')) { walker.failed = true; return false; }
297
320
  nodes.set(tx.id, {active: true, blockers: 0, references: refs, dependents: []}); references.set(tx.id, refs.slice());
298
321
  byId.set(tx.id, record); earlier.push(record);
299
322
  for (const ref of refs) {
@@ -308,23 +331,45 @@ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null
308
331
  lastUndo.get(tx.reverts).push(tx.id);
309
332
  }
310
333
  if (visit) visit(record, nodes, refs);
311
- if (observe) observe(record, refs, active);
312
- }
313
- if (!visit) for (let i = order.length - 1; i >= 0; i--) {
314
- const node = nodes.get(order[i]); node.active = node.blockers === 0;
315
- if (node.active) for (const ref of node.references) nodes.get(ref).blockers++;
334
+ if (walker.observe) walker.observe(record, refs, active);
335
+ return true;
336
+ } catch (_) { walker.failed = true; return false; }
337
+ };
338
+ walker.finish = () => {
339
+ if (walker.failed) return {ok: false};
340
+ const blockers = new Map(), effectiveActIds = [];
341
+ for (let i = order.length - 1; i >= 0; i--) {
342
+ const id = order[i], node = nodes.get(id), on = visit ? node.active : !(blockers.get(id) > 0);
343
+ if (!visit && on) for (const ref of node.references) blockers.set(ref, (blockers.get(ref) || 0) + 1);
344
+ if (on && !pictures.has(id)) effectiveActIds.push(id);
316
345
  }
317
- const effectiveActIds = [...nodes].filter(([id, node]) => node.active && !pictures.has(id)).map(([id]) => id);
318
- return {ok: true, effectiveActIds, active: new Set(effectiveActIds), references, pictures};
319
- } catch (_) { return {ok: false}; }
346
+ effectiveActIds.reverse();
347
+ const result = {ok: true, effectiveActIds, active: new Set(effectiveActIds), references, pictures};
348
+ walker.shared = true;
349
+ return result;
350
+ };
351
+ const add = walker.add;
352
+ walker.add = record => {
353
+ if (walker.shared) { references = new Map(references); pictures = new Map(pictures); walker.shared = false; }
354
+ return add(record);
355
+ };
356
+ return walker;
357
+ }
358
+
359
+ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null) {
360
+ if (!Array.isArray(records)) return {ok: false};
361
+ const walker = _rapierHistoryWalker(records, lookup, visit, observe);
362
+ for (const record of records) if (!walker.add(record)) return {ok: false};
363
+ return walker.finish();
320
364
  }
321
365
 
322
366
  function _rapierHistoryEffects(records, visit = null) {
323
367
  return _rapierWalkHistoryEffects(records, null, visit);
324
368
  }
325
369
 
326
- function _rapierMetadataProof(lookup) {
327
- const values = {}, required = {}, assignments = {filename: [], docKind: []};
370
+ function _rapierMetadataProof(lookup, from = null) {
371
+ const values = {...from?.values}, required = {...from?.required},
372
+ assignments = {filename: [...(from?.assignments.filename || [])], docKind: [...(from?.assignments.docKind || [])]};
328
373
  const requireInitial = (key, value) => {
329
374
  if (Object.hasOwn(required, key) && required[key] !== value) throw Error('metadata_effect');
330
375
  required[key] = value;
@@ -351,7 +396,7 @@ function _rapierMetadataProof(lookup) {
351
396
  if (!_rapierValidMetadata(initial) || Object.keys(required).some(key => initial[key] !== required[key])) return null;
352
397
  return {...initial, ...values};
353
398
  };
354
- return {observe, replay};
399
+ return {observe, replay, fork: other => _rapierMetadataProof(other || lookup, {values, required, assignments})};
355
400
  }
356
401
 
357
402
  function _rapierMetadataState(initial, records, effects = _rapierHistoryEffects(records)) {
@@ -458,16 +503,79 @@ const _rapierPrepareHistoryRecords = (() => {
458
503
  frame.descriptors = descriptors; frame.fields = fields;
459
504
  frame.leaving = true; active.add(value); pending.push(frame);
460
505
  for (let at = fields.length - 1; at >= 0; at--) {
461
- const key = fields[at]; pending.push({value: descriptors[key].value, parent: frame, key, depth: frame.depth + 1});
506
+ const key = fields[at], child = descriptors[key].value;
507
+ const prior = array && child && typeof child === 'object' ? copies.get(child) : undefined;
508
+ if (prior?.immutable && !active.has(child) && frame.depth + 1 + prior.height <= 64) {
509
+ finish({parent: frame, key}, prior.copy, true, prior.height); continue;
510
+ }
511
+ pending.push({value: child, parent: frame, key, depth: frame.depth + 1});
462
512
  }
463
513
  }
464
514
  return result.value;
465
515
  };
516
+ let resumable = [];
517
+ const rowOf = (lookup, record) => {
518
+ const splices = lookup.splices(record), metadata = lookup.metadata(record);
519
+ if (splices && !Object.isFrozen(splices)) {
520
+ for (const row of splices) Object.freeze(row);
521
+ Object.freeze(splices);
522
+ }
523
+ if (metadata && !Object.isFrozen(metadata)) {
524
+ for (const pair of Object.values(metadata)) Object.freeze(pair);
525
+ Object.freeze(metadata);
526
+ }
527
+ return Object.freeze({record, splices, metadata});
528
+ };
529
+ const observe = (records, basis, byId, rows, effects, proof) => {
530
+ for (const refs of effects.references.values()) Object.freeze(refs);
531
+ const effectView = Object.freeze({ok: true, effectiveActIds: Object.freeze(effects.effectiveActIds),
532
+ active: view(effects.active), references: view(effects.references), pictures: view(effects.pictures)});
533
+ const observation = Object.freeze({ok: effects.ok, records, rows: Object.freeze(rows), effects: effectView, byId: view(byId),
534
+ replayMetadata: initial => effects.ok ? proof.replay(initial) : null,
535
+ validRecord: (index, documentAuthority, maxDocumentBytes, utf8Length, allowGap = false) => {
536
+ if (!effects.ok || !Number.isSafeInteger(index) || index < 0 || index >= records.length) return false;
537
+ const row = rows[index], tx = row.record.transaction;
538
+ return _rapierCheckLedgerRecord(row.record, records[index - 1] || null, documentAuthority, maxDocumentBytes, utf8Length, allowGap,
539
+ {...row, duplicate: false, semantic: !!(tx.reverts || tx.reapplies || tx.sourceTransactionId || tx.sourceTransactionIds?.length)});
540
+ }});
541
+ prepared = new WeakMap([[records, {basis, observation}]]);
542
+ return observation;
543
+ };
544
+ const authorityOf = state => state.records[0]?.transaction?.documentAuthority;
545
+ const keep = state => { resumable = [state, ...resumable.filter(row => row !== state && row.records[0] !== state.records[0] &&
546
+ authorityOf(row) === authorityOf(state))].slice(0, 4); };
547
+ const extend = records => {
548
+ const base = resumable.find(row => row.records.length < records.length && row.records[0] === records[0] &&
549
+ row.records.every((record, at) => record === records[at]));
550
+ if (!base) return null;
551
+ resumable = resumable.filter(row => row !== base);
552
+ const more = records.slice(base.records.length), byId = new Map(base.byId);
553
+ for (const record of more) {
554
+ const id = record?.transaction?.id;
555
+ if (typeof id !== 'string' || !id || byId.has(id)) return null;
556
+ byId.set(id, record);
557
+ }
558
+ const {lookup, walker} = base;
559
+ lookup.extend(more);
560
+ const rows = base.rows.concat(more.map(record => rowOf(lookup, record)));
561
+ const proof = base.proof.fork(lookup);
562
+ walker.observe = proof.observe;
563
+ for (const record of more) if (!walker.add(record)) return null;
564
+ const effects = walker.finish();
565
+ if (!effects.ok) return null;
566
+ if (rows.every(row => row.splices && row.metadata !== undefined)) keep({records, lookup, walker, proof, byId, rows});
567
+ return observe(records, records, byId, rows, effects, proof);
568
+ };
466
569
  return (input, earlier = input) => {
467
570
  try {
468
571
  if (!Array.isArray(input) || !Array.isArray(earlier)) return {ok: false};
469
572
  const records = capture(input), basis = input === earlier ? records : capture(earlier), previous = prepared.get(records);
470
573
  if (previous?.basis === basis) return previous.observation;
574
+ if (basis === records) {
575
+ let grown = null;
576
+ try { grown = extend(records); } catch (_) { grown = null; }
577
+ if (grown) return grown;
578
+ }
471
579
  const byId = new Map();
472
580
  for (const record of records) {
473
581
  const id = record?.transaction?.id;
@@ -475,41 +583,24 @@ const _rapierPrepareHistoryRecords = (() => {
475
583
  byId.set(id, record);
476
584
  }
477
585
  const lookup = _rapierRecordIndex(basis), proof = _rapierMetadataProof(lookup);
478
- const rows = records.map(record => {
479
- const splices = lookup.splices(record), metadata = lookup.metadata(record);
480
- if (splices && !Object.isFrozen(splices)) {
481
- for (const row of splices) Object.freeze(row);
482
- Object.freeze(splices);
483
- }
484
- if (metadata && !Object.isFrozen(metadata)) {
485
- for (const pair of Object.values(metadata)) Object.freeze(pair);
486
- Object.freeze(metadata);
487
- }
488
- return Object.freeze({record, splices, metadata});
489
- });
490
- const effects = _rapierWalkHistoryEffects(records, lookup, null, proof.observe);
586
+ const rows = records.map(record => rowOf(lookup, record));
587
+ const walker = _rapierHistoryWalker(records, lookup, null, proof.observe);
588
+ for (const record of records) if (!walker.add(record)) return {ok: false};
589
+ const effects = walker.finish();
491
590
  if (!effects.ok) return {ok: false};
492
- for (const refs of effects.references.values()) Object.freeze(refs);
493
- const effectView = Object.freeze({ok: true, effectiveActIds: Object.freeze(effects.effectiveActIds),
494
- active: view(effects.active), references: view(effects.references), pictures: view(effects.pictures)});
495
- const observation = Object.freeze({ok: effects.ok, records, rows: Object.freeze(rows), effects: effectView, byId: view(byId),
496
- replayMetadata: initial => effects.ok ? proof.replay(initial) : null,
497
- validRecord: (index, documentAuthority, maxDocumentBytes, utf8Length, allowGap = false) => {
498
- if (!effects.ok || !Number.isSafeInteger(index) || index < 0 || index >= records.length) return false;
499
- const row = rows[index], tx = row.record.transaction;
500
- return _rapierCheckLedgerRecord(row.record, records[index - 1] || null, documentAuthority, maxDocumentBytes, utf8Length, allowGap,
501
- {...row, duplicate: false, semantic: !!(tx.reverts || tx.reapplies || tx.sourceTransactionId || tx.sourceTransactionIds?.length)});
502
- }});
503
- prepared = new WeakMap([[records, {basis, observation}]]); return observation;
591
+ if (basis === records && rows.every(row => row.splices && row.metadata !== undefined))
592
+ keep({records, lookup, walker, proof, byId, rows});
593
+ return observe(records, basis, byId, rows, effects, proof);
504
594
  } catch (_) { return {ok: false}; }
505
595
  };
506
596
  })();
507
597
 
508
598
  const _rapierJournalRecord = (() => {
509
599
  const copies = new WeakMap();
600
+ const fixed = value => { if (value && typeof value === 'object') { for (const key of Object.keys(value)) fixed(value[key]); Object.freeze(value); } return value; };
510
601
  return record => {
511
602
  let copy = copies.get(record);
512
- if (!copy) { copy = JSON.parse(JSON.stringify(record)); copies.set(record, copy); }
603
+ if (!copy) { copy = fixed(JSON.parse(JSON.stringify(record))); copies.set(record, copy); }
513
604
  return copy;
514
605
  };
515
606
  })();
@@ -55,18 +55,23 @@ function createMarkdownRenderer(runtime) {
55
55
  window?.addEventListener?.('rapier:original-file-ready', scheduleWikiEmbeds);
56
56
  const wikiMode = options => options.wikiEmbedding ? 'links' : options.wikiLinks !== undefined ? options.wikiLinks
57
57
  : globalThis.RapierOriginalFolderCore?.currentOriginal?.() ? 'label' : false;
58
+ let codeLeft = Infinity;
58
59
  function initMarkdownIt() {
59
60
  if (typeof window.markdownit !== 'function') throw new Error('markdownit');
60
61
  if (typeof _rapierMarkdownPreview === 'function') globalThis.rapierRenderPreview = _rapierMarkdownPreview;
61
62
 
62
63
  md = window.markdownit({
63
64
  ...RAPIER_MARKDOWN_SPEC.options,
64
- highlight: (code, lang) => lang && _rapierHighlightAdmitted(code) ? _rapierCodeHtml(code, lang) : '',
65
+ highlight: (code, lang) => lang && _rapierHighlightAdmitted(code) && (codeLeft -= code.length) >= 0 ? _rapierCodeHtml(code, lang) : '',
65
66
  });
66
67
 
67
68
  _rapierApplyMarkdownSpec(md, window);
68
69
  globalThis.RapierImageAssets.installMarkdownImages(md);
69
70
  _rapierInstallSemanticProbe(md);
71
+ md.core.ruler.after('block', 'rapier_part_decorations', state => {
72
+ if (state.env.rapierPartDecorations && !/[<$\\]/.test(state.src) && !state.src.includes('^['))
73
+ state.tokens = _rapierPartDecorationGroups(state.tokens).flat();
74
+ });
70
75
  _rapierInstallMarkdownMath(md, RAPIER_MARKDOWN_SPEC.rules.mathBlock);
71
76
 
72
77
  const markOpenDefault = md.renderer.rules.mark_open ||
@@ -138,7 +143,7 @@ function initMarkdownIt() {
138
143
  '" data-rapier-image-source="' + esc(encodeURIComponent(token.content)) + '">' + esc(label) + '</span>';
139
144
  }
140
145
  const note = embed && !env.rapierWikiEmbedding;
141
- if (note) scheduleWikiEmbeds();
146
+ if (note && !env.rapierPassive) scheduleWikiEmbeds();
142
147
  return '<span class="rapier-source-token rapier-source-token--wiki' + (note ? '-embed' : '') +
143
148
  '" data-rapier-source="' + esc(encodeURIComponent(token.content)) + '" data-rapier-visible="' + esc(encodeURIComponent(label)) +
144
149
  '"><a href="' + esc(href) + '">' + esc(label) + '</a></span>';
@@ -340,6 +345,7 @@ function initMarkdownIt() {
340
345
  };
341
346
 
342
347
  const _fenceDefault = md.renderer.rules.fence;
348
+ let _mathPromptedOnce = false;
343
349
  {
344
350
  let _diagramPromptedOnce = false;
345
351
  md.renderer.rules.fence = function (tokens, idx, opts, env, self) {
@@ -366,8 +372,16 @@ function initMarkdownIt() {
366
372
  }
367
373
  const provider = env.rapierProviders === false ? null : _rapierProviders.mermaid;
368
374
  const native = globalThis.RapierFlowchart?.parseFlowchart(src).ok === true;
369
- const ready = native || !!(provider && provider.status === 'ready' && typeof provider.renderToString === 'function');
370
- if (!ready && !provider?.request?.() && env.rapierProviders !== false && !_diagramPromptedOnce) {
375
+ const diagramReady = native || !!(provider && provider.status === 'ready' && typeof provider.renderToString === 'function');
376
+ const mathProvider = env.rapierProviders === false ? null : _rapierProviders.math;
377
+ const mathNeeded = provider?.requiresMath?.(src) === true;
378
+ const mathReady = !mathNeeded || mathProvider?.status === 'ready';
379
+ if (!mathReady && !env.rapierPassive && !mathProvider?.request?.() && env.rapierProviders !== false && !_mathPromptedOnce) {
380
+ _mathPromptedOnce = true;
381
+ try { _rapierUiMath.request(); } catch (_) {}
382
+ }
383
+ const ready = diagramReady && mathReady;
384
+ if (!diagramReady && !env.rapierPassive && !provider?.request?.() && env.rapierProviders !== false && !_diagramPromptedOnce) {
371
385
  _diagramPromptedOnce = true;
372
386
  try { _rapierUiDiagram.request(); } catch (_) {}
373
387
  }
@@ -401,7 +415,6 @@ function initMarkdownIt() {
401
415
  preserveEmphasisMarkup('strong_open', 'data-rapier-strong');
402
416
 
403
417
  {
404
- let _mathPromptedOnce = false;
405
418
  const _renderMath = (src, displayMode, env, authored) => {
406
419
  const marker = displayMode ? '$$' : '$';
407
420
  const source = authored || marker + src + marker, literal = md.utils.escapeHtml(source);
@@ -412,7 +425,7 @@ function initMarkdownIt() {
412
425
  if (env.rapierPage) return sourceHtml;
413
426
  const mathProvider = env.rapierProviders === false ? null : _rapierProviders.math;
414
427
  if (!mathProvider || mathProvider.status !== 'ready' || typeof mathProvider.renderToString !== 'function') {
415
- if (!mathProvider?.request?.() && env.rapierProviders !== false && !_mathPromptedOnce) {
428
+ if (!env.rapierPassive && !mathProvider?.request?.() && env.rapierProviders !== false && !_mathPromptedOnce) {
416
429
  _mathPromptedOnce = true;
417
430
  try { _rapierUiMath.request(); } catch (_) {}
418
431
  }
@@ -444,6 +457,10 @@ function initMarkdownIt() {
444
457
  }
445
458
  globalThis.RapierMarkdownLayout.installMarkdownLayout(md);
446
459
  globalThis.RapierImageAssets.configureParser(md, _rapierSplitOpeningFrontmatter);
460
+ const render = md.renderer.render;
461
+ md.renderer.render = function (tokens, options, env) {
462
+ return render.call(this, _rapierCloseOpenDetails(tokens), options, env);
463
+ };
447
464
  }
448
465
 
449
466
  function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
@@ -464,42 +481,56 @@ function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
464
481
  }
465
482
 
466
483
  const markdown = _rapierSplitOpeningFrontmatter(canonical).body;
467
- const env = {rapierImagePresentation: options.imagePresentation === true, ...(options.page ? {rapierPage: true} : {}), ...(options.providers === false ? {rapierProviders: false} : {})};
484
+ const env = {rapierImagePresentation: options.imagePresentation === true, rapierPassive: options.passive === true, ...(options.page ? {rapierPage: true} : {}), ...(options.providers === false ? {rapierProviders: false} : {})};
468
485
  if (options.wikiEmbedding) env.rapierWikiEmbedding = true;
469
486
  const wikiLinks = wikiMode(options);
470
487
  if (wikiLinks) env.rapierWikiLinks = wikiLinks;
471
488
  if (options.mathEquations) env.rapierMathEquations = options.mathEquations;
472
489
  const nesting = md && options.maxNesting > 0 ? options.maxNesting : 0;
473
- let rendered;
490
+ let cutAt = -1, rendered;
491
+ codeLeft = options.codeBudget >= 0 ? options.codeBudget : Infinity;
492
+ try {
474
493
  if (nesting || md && options.closeDetails) {
475
494
  const saved = md.options.maxNesting;
476
495
  if (nesting) md.options.maxNesting = nesting;
477
496
  try {
478
497
  const tokens = md.parse(markdown, env);
498
+ if (nesting) cutAt = _rapierNestingCutAt(tokens, nesting);
479
499
  rendered = md.renderer.render(options.closeDetails ? _rapierCloseOpenDetails(tokens) : tokens, md.options, env);
480
500
  } finally { md.options.maxNesting = saved; }
481
501
  } else rendered = md
482
502
  ? md.render(markdown, env)
483
503
  : '<pre><code>' + escapeRapierHtmlText(markdown) + '</code></pre>';
504
+ } finally { codeLeft = Infinity; }
484
505
  if (md && env.footnotes && Array.isArray(env.footnotes.list) && env.footnotes.list.length) {
485
506
  const labels = env.footnotes.list.map((item, index) =>
486
507
  String(item && item.label || ('inline-' + (index + 1))));
487
508
  rendered = _relabelFootnoteHtml(rendered, labels);
488
509
  }
489
510
  root.innerHTML = sanitizeRapierHtml(rendered, options.page ? 'source' : 'export');
511
+ if (cutAt >= 0) _rapierAppendNestingCut(root, markdown, cutAt);
490
512
  _rapierApplyBlockDirection(root);
491
513
 
492
- _rapierAssignHeadingSlugs(root, RAPIER_RENDERED_HEADING_SELECTOR, false);
514
+ _rapierAssignHeadingSlugs(root, RAPIER_RENDERED_HEADING_SELECTOR, false, options.carry?.slugs, options.carry?.headingIds);
493
515
  _rapierDisambiguateRenderedAnchors(root);
494
516
 
495
517
  root.querySelectorAll('pre > code').forEach(_rapierNormalizeCodeElement);
496
- _rapierMarkTableCaptions(root);
518
+ _rapierMarkTableCaptions(root, options.carry);
497
519
  _rapierMarkFigureCaptions(root);
498
520
  _rapierMarkListContinuations(_rapierChildElements(root));
499
- _rapierDecorateJumpLists(root);
521
+ _rapierDecorateJumpLists(root, options.carry);
500
522
  return root;
501
523
  }
502
524
 
525
+ function _rapierAppendNestingCut(root, markdown, cutAt) {
526
+ const note = root.ownerDocument.createElement('p'), pre = root.ownerDocument.createElement('pre'), code = root.ownerDocument.createElement('code');
527
+ note.setAttribute('data-rapier-nesting-cut', 'true');
528
+ note.textContent = 'Nesting is too deep to show here. The rest of the document follows as plain text.';
529
+ code.textContent = String(markdown).split('\n').slice(cutAt).join('\n');
530
+ pre.append(code);
531
+ root.append(note, pre);
532
+ }
533
+ const _rapierNestingCutAt = (tokens, nesting) => { const deepest = tokens.find(token => token.level >= nesting - 1 && token.map); return deepest ? deepest.map[0] + 1 : -1; };
503
534
  async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {}) {
504
535
  const work = options.work || options;
505
536
  check(work);
@@ -517,15 +548,16 @@ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {})
517
548
  let parsed;
518
549
  try { parsed = !wikiLinks && options.parsed || (runtime.parseAsync && !wikiLinks && !nesting ? await runtime.parseAsync(markdown, options) : {tokens: md.parse(markdown, fallbackEnv), env: fallbackEnv}); }
519
550
  finally { md.options.maxNesting = saved; }
520
- let {tokens, env} = parsed;
521
- if (options.closeDetails) tokens = _rapierCloseOpenDetails(tokens);
522
- let labels = env.footnotes?.list?.map((item, index) => String(item?.label || ('inline-' + (index + 1))));
523
- env = {...env, rapierImagePresentation: options.imagePresentation === true};
551
+ let {tokens, env} = parsed, labels = env.footnotes?.list?.map((item, index) => String(item?.label || ('inline-' + (index + 1))));
552
+ const cutAt = nesting ? _rapierNestingCutAt(tokens, nesting) : -1;
553
+ tokens = _rapierCloseOpenDetails(tokens);
554
+ env = {...env, rapierImagePresentation: options.imagePresentation === true, rapierPassive: options.passive === true};
524
555
  await pause(work, .3);
525
556
  const inert = document.createElement('template').content.ownerDocument;
526
557
  const root = inert.createElement('div'), part = inert.createElement('div'), boundary = htmlBoundary();
527
558
  root.setAttribute('data-rapier-semantic-root', 'true');
528
559
  let html = '', began = performance.now(), complete = false;
560
+ codeLeft = options.codeBudget >= 0 ? options.codeBudget : Infinity;
529
561
  const append = () => {
530
562
  if (md.renderer.rapierTransformHtml) html = md.renderer.rapierTransformHtml(html);
531
563
  if (labels?.length) html = _relabelFootnoteHtml(html, labels);
@@ -546,23 +578,72 @@ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {})
546
578
  }
547
579
  }
548
580
  if (html) append();
581
+ if (cutAt >= 0) _rapierAppendNestingCut(root, markdown, cutAt);
549
582
  parsed = tokens = env = labels = null;
550
- await finishAsync(_rapierSemanticDecorations(root), work);
583
+ await finishAsync(_rapierSemanticDecorations(root, options.carry), work);
551
584
  await pause(work, .6);
552
585
  complete = true;
553
586
  return root;
554
- } finally { if (!complete) { root.replaceChildren(); part.replaceChildren(); } }
587
+ } finally { codeLeft = Infinity; if (!complete) { root.replaceChildren(); part.replaceChildren(); } }
588
+ }
589
+
590
+ function _rapierPartDecorationGroups(tokens) {
591
+ const groups = [];
592
+ for (let at = 0; at < tokens.length; at++) {
593
+ const first = tokens[at];
594
+ if (first.type !== 'heading_open' && first.type !== 'table_open') continue;
595
+ const close = first.type === 'heading_open' ? 'heading_close' : 'table_close';
596
+ let end = at + 1;
597
+ while (end < tokens.length && !(tokens[end].type === close && tokens[end].level === first.level)) end++;
598
+ end++;
599
+ if (first.type === 'table_open' && tokens[end]?.type === 'paragraph_open' && tokens[end].level === first.level) {
600
+ while (end < tokens.length && !(tokens[end].type === 'paragraph_close' && tokens[end].level === first.level)) end++;
601
+ end++;
602
+ }
603
+ groups.push(tokens.slice(at, end));
604
+ at = end - 1;
605
+ }
606
+ return groups;
607
+ }
608
+
609
+ function _rapierReadPartDecorations(canonical, metadata, options = {}) {
610
+ const carry = options.carry || {slugs: Object.create(null), captions: 0, continued: true}, captionStart = carry.captions || 0;
611
+ const markdown = _rapierSplitOpeningFrontmatter(canonical).body, env = {rapierImagePresentation: options.imagePresentation === true, rapierPassive: true, rapierPartDecorations: true,
612
+ ...(options.page ? {rapierPage: true} : {}), ...(options.providers === false ? {rapierProviders: false} : {})};
613
+ if (options.wikiEmbedding) env.rapierWikiEmbedding = true;
614
+ const wikiLinks = wikiMode(options);
615
+ if (wikiLinks) env.rapierWikiLinks = wikiLinks;
616
+ if (options.mathEquations) env.rapierMathEquations = options.mathEquations;
617
+ const saved = md.options.maxNesting;
618
+ let tokens;
619
+ try {
620
+ if (options.maxNesting > 0) md.options.maxNesting = options.maxNesting;
621
+ tokens = md.parse(markdown, env);
622
+ } finally { md.options.maxNesting = saved; }
623
+ if (tokens.some(token => token.type === 'html_block' || token.type === 'math_block' || token.type.startsWith('footnote_') ||
624
+ token.type === 'inline' && token.children?.some(child => child.type === 'html_inline' || child.type === 'math_inline' || child.type.startsWith('footnote_')))) {
625
+ const root = _rapierRenderSemanticRoot(canonical, metadata, {...options, carry, codeBudget: 0, passive: true});
626
+ return Object.freeze({headingIds: Object.freeze([...root.querySelectorAll(RAPIER_RENDERED_HEADING_SELECTOR)].map(heading => heading.id)), captionStart});
627
+ }
628
+ const html = [];
629
+ for (const group of _rapierPartDecorationGroups(tokens)) html.push('<div>', md.renderer.render(group, md.options, env), '</div>');
630
+ const root = document.createElement('template').content.ownerDocument.createElement('div');
631
+ root.innerHTML = sanitizeRapierHtml(html.join(''), 'export');
632
+ _rapierAssignHeadingSlugs(root, RAPIER_RENDERED_HEADING_SELECTOR, false, carry.slugs);
633
+ const headingIds = Object.freeze([...root.querySelectorAll(RAPIER_RENDERED_HEADING_SELECTOR)].map(heading => heading.id));
634
+ _rapierMarkTableCaptions(root, carry);
635
+ return Object.freeze({headingIds, captionStart});
555
636
  }
556
637
 
557
- function* _rapierSemanticDecorations(root) {
638
+ function* _rapierSemanticDecorations(root, carry = null) {
558
639
  yield* _rapierApplyBlockDirectionSteps(root); yield;
559
- yield* _rapierAssignHeadingSlugsSteps(root, RAPIER_RENDERED_HEADING_SELECTOR, false); yield;
640
+ yield* _rapierAssignHeadingSlugsSteps(root, RAPIER_RENDERED_HEADING_SELECTOR, false, carry?.slugs, carry?.headingIds); yield;
560
641
  yield* _rapierDisambiguateRenderedAnchorsSteps(root); yield;
561
642
  for (const code of root.querySelectorAll('pre > code')) { _rapierNormalizeCodeElement(code); yield; }
562
- yield* _rapierMarkTableCaptionsSteps(root); yield;
643
+ yield* _rapierMarkTableCaptionsSteps(root, carry); yield;
563
644
  yield* _rapierMarkFigureCaptionsSteps(root); yield;
564
645
  yield* _rapierMarkListContinuationsSteps(_rapierChildElements(root)); yield;
565
- yield* _rapierDecorateJumpListsSteps(root); yield;
646
+ yield* _rapierDecorateJumpListsSteps(root, carry); yield;
566
647
  }
567
648
 
568
649
  function _rapierInstallSemanticProbe(instance) {
@@ -815,7 +896,18 @@ function _rapierCloseOpenDetails(tokens) {
815
896
  const tags = token => {
816
897
  const found = [];
817
898
  if (token.type === 'html_inline') found.push(token.content);
818
- else if (token.type === 'html_block') _rapierRawHtmlFragment(token.content, tag => { found.push(tag); return tag; }, md);
899
+ else if (token.type === 'html_block') {
900
+ let raw = '';
901
+ _rapierRawHtmlFragment(token.content, tag => {
902
+ if (raw) { if (new RegExp('^</' + raw + '[\\t\\n\\f\\r ]*>$', 'i').test(tag)) raw = ''; }
903
+ else {
904
+ const literal = /^<(script|style|textarea|title|xmp|iframe|noembed|noframes|plaintext)(?=[\t\n\f\r />])/i.exec(tag);
905
+ if (literal) raw = literal[1];
906
+ else found.push(tag);
907
+ }
908
+ return tag;
909
+ }, md);
910
+ }
819
911
  else if (token.type === 'inline') for (const child of token.children || []) if (child.type === 'html_inline') found.push(child.content);
820
912
  return found;
821
913
  };
@@ -896,17 +988,23 @@ function _rapierFirstStrongDir(text) {
896
988
 
897
989
  const _RAPIER_DIR_BLOCKS = 'p,h1,h2,h3,h4,h5,h6,li,blockquote,ul,ol,dt,dd,td,th';
898
990
 
899
- function _rapierAssignHeadingSlugs(root, selector, reserveExisting) { return finish(_rapierAssignHeadingSlugsSteps(root, selector, reserveExisting)); }
991
+ function _rapierAssignHeadingSlugs(root, selector, reserveExisting, taken, assigned) { return finish(_rapierAssignHeadingSlugsSteps(root, selector, reserveExisting, taken, assigned)); }
900
992
 
901
- function* _rapierAssignHeadingSlugsSteps(root, selector = RAPIER_RENDERED_HEADING_SELECTOR, reserveExisting = true) {
993
+ function* _rapierAssignHeadingSlugsSteps(root, selector = RAPIER_RENDERED_HEADING_SELECTOR, reserveExisting = true, taken = null, assigned = null) {
902
994
  if (!root || !root.querySelectorAll) return;
903
- const used = Object.create(null);
995
+ const used = taken || Object.create(null);
904
996
  const headings = _rapierDormantHeadings(root, selector, true);
997
+ if (assigned && assigned.length !== headings.length) {
998
+ try { console.warn('[rapier-reader] Part heading facts do not match its rendered headings; assigning ordinary heading IDs.'); } catch (_) {}
999
+ assigned = null;
1000
+ }
905
1001
  const headingSet = new Set(headings);
906
1002
  if (reserveExisting) for (const element of _rapierDormantHeadings(root.isConnected ? document : root, '[id]', true)) { yield;
907
1003
  if (!headingSet.has(element) && element.id) used[element.id] = 0;
908
1004
  }
1005
+ let index = 0;
909
1006
  for (const heading of headings) { yield;
1007
+ if (assigned) { heading.id = assigned[index++]; continue; }
910
1008
  const base = _rapierHeadingSlugBase(heading.textContent);
911
1009
  const owned = typeof _rapierChromeOwnsId === 'function' && _rapierChromeOwnsId(base);
912
1010
  heading.id = _rapierNextHeadingSlug(owned ? 'heading-' + base : base, used);
@@ -1007,9 +1105,9 @@ function _rapierDecodeHash(hash) {
1007
1105
  catch (_) { return raw; }
1008
1106
  }
1009
1107
 
1010
- function _rapierDecorateJumpLists(root) { return finish(_rapierDecorateJumpListsSteps(root)); }
1108
+ function _rapierDecorateJumpLists(root, carry) { return finish(_rapierDecorateJumpListsSteps(root, carry)); }
1011
1109
 
1012
- function* _rapierDecorateJumpListsSteps(root) {
1110
+ function* _rapierDecorateJumpListsSteps(root, carry = null) {
1013
1111
  for (const list of root.querySelectorAll('ul')) { yield;
1014
1112
  if (list.closest('li')) continue;
1015
1113
  const items = Array.from(list.children).filter(item => item.tagName === 'LI');
@@ -1032,7 +1130,7 @@ function* _rapierDecorateJumpListsSteps(root) {
1032
1130
  }
1033
1131
  }
1034
1132
 
1035
- const first = root.firstElementChild, only = first && !first.nextElementSibling ? first : null;
1133
+ const first = root.firstElementChild, only = !carry && first && !first.nextElementSibling ? first : null;
1036
1134
  if (only && only.tagName === 'P' && /^(?:jump\s+to|contents|table\s+of\s+contents):?$/i.test(String(only.textContent || '').trim())) {
1037
1135
  only.classList.add('rapier-jump-label-candidate');
1038
1136
  }
@@ -1074,11 +1172,11 @@ function* _rapierMarkListContinuationsSteps(blocks, mark = (element, name) => el
1074
1172
  }
1075
1173
  }
1076
1174
 
1077
- function _rapierMarkTableCaptions(root) { return finish(_rapierMarkTableCaptionsSteps(root)); }
1175
+ function _rapierMarkTableCaptions(root, carry) { return finish(_rapierMarkTableCaptionsSteps(root, carry)); }
1078
1176
 
1079
- function* _rapierMarkTableCaptionsSteps(root) {
1177
+ function* _rapierMarkTableCaptionsSteps(root, carry = null) {
1080
1178
  const used = new Set(Array.from(root.querySelectorAll('[id]'), element => element.id));
1081
- let ordinal = 0;
1179
+ let ordinal = carry?.captions || 0;
1082
1180
  for (const table of root.querySelectorAll('table')) { yield;
1083
1181
  const sibling = table.nextElementSibling;
1084
1182
  if (!sibling || sibling.tagName !== 'P') continue;
@@ -1091,6 +1189,7 @@ function* _rapierMarkTableCaptionsSteps(root) {
1091
1189
  const existing = (table.getAttribute('aria-describedby') || '').trim();
1092
1190
  table.setAttribute('aria-describedby', existing ? existing + ' ' + id : id);
1093
1191
  }
1192
+ if (carry) carry.captions = ordinal;
1094
1193
  }
1095
1194
 
1096
1195
 
@@ -1794,7 +1893,7 @@ function renderWithMathML(source, metadata = {}) {
1794
1893
  return {html, equations};
1795
1894
  }
1796
1895
 
1797
- return {renderWithMathML, _rapierClipboardPayloadAsync, _rapierPlainTextFileAsync, _rapierCloneRootAsync, _rapierPortablePlainTextAsync, _rapierHeldPicturesAsWordsAsync, _rapierRenderSemanticRootAsync, _rapierProjectPortableRootAsync, _rapierPortableHtmlAsync, _rapierCalloutInfo, _rapierRemoveCalloutMarker, _rapierMathSource, _rapierFlattenPortableDetails, _rapierProjectPortableRoot, _rapierPortableHtml, _rapierNormalizeInlineText, _rapierPortableInlineText, _rapierPortableListText, _rapierPortableTableText, _rapierPortablePlainText, _rapierPlainTextFile, _rapierHeldPicturesAsWords, _rapierSourceTokenValue, _rapierResolveSoftBreakTokens, _rapierInertParse, _rapierUnwrapElement, _rapierBidiStrong, render: (source, metadata = {}) => { if (!md) initMarkdownIt(); return _rapierRenderSemanticRoot(source, metadata).innerHTML; }, initMarkdownIt, _rapierRenderSemanticRoot, _rapierInstallSemanticProbe, _rapierApplyImageGeometry, _rapierImageAltSize, _rapierImageAltSourceParts, _rapierImageAltText, _rapierImageSizeAttributes, _rapierRawHtmlFragment, _rapierApplyBlockDirection, _rapierFirstStrongDir, _rapierAssignHeadingSlugs, _rapierDisambiguateRenderedAnchors, _rapierDecodeHash, _rapierDecorateJumpLists, _rapierMarkFigureCaptions, _rapierMarkListContinuations, _rapierMarkTableCaptions, _rapierNormalizeCodeElement, _rapierLanguageClass, _relabelFootnoteHtml, _footnoteIdFromLabel, wrapDisplayMath, parser: () => md};
1896
+ return {_rapierReadPartDecorations, renderWithMathML, _rapierClipboardPayloadAsync, _rapierPlainTextFileAsync, _rapierCloneRootAsync, _rapierPortablePlainTextAsync, _rapierHeldPicturesAsWordsAsync, _rapierRenderSemanticRootAsync, _rapierProjectPortableRootAsync, _rapierPortableHtmlAsync, _rapierCalloutInfo, _rapierRemoveCalloutMarker, _rapierMathSource, _rapierFlattenPortableDetails, _rapierProjectPortableRoot, _rapierPortableHtml, _rapierNormalizeInlineText, _rapierPortableInlineText, _rapierPortableListText, _rapierPortableTableText, _rapierPortablePlainText, _rapierPlainTextFile, _rapierHeldPicturesAsWords, _rapierSourceTokenValue, _rapierResolveSoftBreakTokens, _rapierInertParse, _rapierUnwrapElement, _rapierBidiStrong, render: (source, metadata = {}) => { if (!md) initMarkdownIt(); return _rapierRenderSemanticRoot(source, metadata).innerHTML; }, initMarkdownIt, _rapierRenderSemanticRoot, _rapierInstallSemanticProbe, _rapierApplyImageGeometry, _rapierImageAltSize, _rapierImageAltSourceParts, _rapierImageAltText, _rapierImageSizeAttributes, _rapierRawHtmlFragment, _rapierApplyBlockDirection, _rapierFirstStrongDir, _rapierAssignHeadingSlugs, _rapierDisambiguateRenderedAnchors, _rapierDecodeHash, _rapierDecorateJumpLists, _rapierMarkFigureCaptions, _rapierMarkListContinuations, _rapierMarkTableCaptions, _rapierNormalizeCodeElement, _rapierLanguageClass, _relabelFootnoteHtml, _footnoteIdFromLabel, wrapDisplayMath, parser: () => md};
1798
1897
  }
1799
1898
  export {RAPIER_TABLE_CAPTION_PREFIX_RE as tableCaptionPrefix, RAPIER_PORTABLE_HIGHLIGHT_HEX as portableHighlightHex, _RAPIER_RTL_SCRIPT as rtlScripts, createMarkdownRenderer};
1800
1899
 
@@ -52,7 +52,7 @@ const DIAGRAM_LABEL_CSS = new Set(('color fill stroke background-color font-fami
52
52
  const DOCUMENT_STYLE_PROPERTIES = new Set([...DIAGRAM_LABEL_CSS, ...('background display visibility opacity '
53
53
  + 'border border-top border-right border-bottom border-left border-color border-style border-width border-collapse border-spacing '
54
54
  + 'border-top-color border-right-color border-bottom-color border-left-color border-top-width border-right-width border-bottom-width border-left-width '
55
- + 'table-layout caption-side empty-cells list-style-type list-style-position direction unicode-bidi '
55
+ + 'table-layout caption-side empty-cells list-style-type list-style-position direction unicode-bidi hyphens '
56
56
  + 'stroke-width stroke-opacity stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit '
57
57
  + 'fill-opacity fill-rule clip-rule clip-path filter stop-color stop-opacity text-anchor dominant-baseline alignment-baseline '
58
58
  + 'paint-order vector-effect shape-rendering color-interpolation color-interpolation-filters').split(' ')]);
@@ -289,15 +289,21 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
289
289
  if (code) {
290
290
  const lang = _rapierLanguageClass(code) || String(metadata.codeLang || '');
291
291
  code.classList.add('artifact-code');
292
- if (metadata.docKind === 'code') _rapierPaintCode(code, _rapierArtifactHighlight(code.textContent || '', lang));
292
+ if (metadata.docKind === 'code') {
293
+ const colored = _rapierArtifactHighlight(code.textContent || '', lang);
294
+ _rapierPaintCode(code, opts.sanitized ? sanitizeRapierHtml(colored, 'export') : colored);
295
+ }
293
296
  if (lang) code.classList.add('language-' + String(lang).replace(/[^a-z0-9_-]/gi, ''));
294
297
  }
295
298
  } else {
299
+ let codeLeft = opts.codeBudget >= 0 ? opts.codeBudget : Infinity;
296
300
  for (const code of root.querySelectorAll('pre > code')) { yield;
297
301
  if (code.parentElement?.classList.contains('diagram-source') &&
298
302
  code.closest('.diagram-block')?.getAttribute('data-diagram-state') === 'ready') continue;
299
303
  const lang = _rapierLanguageClass(code) || 'text';
300
- _rapierPaintCode(code, _rapierArtifactHighlight(code.textContent || '', lang));
304
+ if ((codeLeft -= code.textContent.length) < 0) { code.replaceChildren(document.createTextNode(code.textContent)); continue; }
305
+ const colored = _rapierArtifactHighlight(code.textContent || '', lang);
306
+ _rapierPaintCode(code, opts.sanitized ? sanitizeRapierHtml(colored, 'export') : colored);
301
307
  }
302
308
 
303
309
  for (const table of root.querySelectorAll('table')) { yield;
@@ -336,6 +342,13 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
336
342
 
337
343
  yield* _rapierPrefixPortableAnchorsSteps(root, opts.baseName || metadata.filename || 'document');
338
344
 
345
+ if (opts.sanitized) {
346
+ for (const cache of root.querySelectorAll('.diagram-cache')) {
347
+ yield;
348
+ cache.innerHTML = sanitizeRapierHtml(cache.innerHTML, 'export');
349
+ }
350
+ return root;
351
+ }
339
352
  return yield* cleanTree(root, sanitizeRapierHtml, 'export');
340
353
  }
341
354
 
@@ -100,7 +100,7 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
100
100
  return end < body.length ? sourceOffset(end + 1) : undefined;
101
101
  }});
102
102
  let lines = 0;
103
- for (let at = 0; at < body.length; at++) if (body.charCodeAt(at) === 10) lines++;
103
+ for (let at = body.indexOf('\n'); at !== -1; at = body.indexOf('\n', at + 1)) lines++;
104
104
  if (lines <= BLOCK_WINDOW) {
105
105
  state = new parser.block.State(body, parser, env, tokens);
106
106
  state.rapierSourceSlice = (from, to) => source.slice(sourceOffset(from), sourceOffset(to));
@@ -179,8 +179,7 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
179
179
  }
180
180
  }
181
181
 
182
- const PERCENT_PAYLOAD = Array.from({length: 128}, (_, code) => code >= 48 && code <= 57 || code >= 65 && code <= 90 ||
183
- code >= 97 && code <= 122 || '-._~:/?@+$,;='.includes(String.fromCharCode(code)));
182
+ const INVALID_PERCENT_PAYLOAD = /[^A-Za-z0-9._~:\/?@+$,;=%-]/;
184
183
  const isHexUnit = code => code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
185
184
  let lastScan = null;
186
185
  export function dataImage(url) {
@@ -197,13 +196,11 @@ function scanDataImage(url) {
197
196
  ...(prepared.encoding ? {encoding: prepared.encoding} : {})};
198
197
  if (/^data:image\/svg\+xml,/i.test(url)) {
199
198
  const payloadStart = 19;
200
- let byteLength = 0;
201
- for (let at = payloadStart; at < url.length; at++, byteLength++) {
202
- const code = url.charCodeAt(at);
203
- if (code === 37) {
204
- if (!isHexUnit(url.charCodeAt(at + 1)) || !isHexUnit(url.charCodeAt(at + 2))) return null;
205
- at += 2;
206
- } else if (!(code < 128 && PERCENT_PAYLOAD[code])) return null;
199
+ if (INVALID_PERCENT_PAYLOAD.test(url)) return null;
200
+ let byteLength = url.length - payloadStart;
201
+ for (let at = url.indexOf('%', payloadStart); at !== -1; at = url.indexOf('%', at + 3)) {
202
+ if (!isHexUnit(url.charCodeAt(at + 1)) || !isHexUnit(url.charCodeAt(at + 2))) return null;
203
+ byteLength -= 2;
207
204
  }
208
205
  return byteLength && byteLength <= IMAGE_LIMITS.bytes
209
206
  ? {codec: 'image/svg+xml', byteLength, payloadStart, encoding: 'percent'} : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.2.40",
3
+ "version": "1.2.43",
4
4
  "description": "Read, render and write Self-contained Markdown: one .md file that carries its pictures, drawings and layout, and that any Markdown app can read. HTML renderer, stylesheet, text layout, Will/1 agent permissions, and an edit history with authorship and offline merge. MIT, no dependencies.",
5
5
  "keywords": [
6
6
  "markdown",
package/style.css CHANGED
@@ -391,6 +391,14 @@
391
391
  -webkit-mask: var(--md-glyph-task-box) center / 100% 100% no-repeat;
392
392
  mask: var(--md-glyph-task-box) center / 100% 100% no-repeat;
393
393
  }
394
+ /* The box is a background through a mask, and two modes drop backgrounds: forced colors (Windows High Contrast) paints them as the page,
395
+ and print without "background graphics" leaves them out, so the boxes vanished. In those two the same mask is painted in the system's
396
+ text color and printed as drawn; nowhere else does anything change. */
397
+ .md-render .task-list-item input[type="checkbox"] { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
398
+ @media (forced-colors: active) {
399
+ .md-render .task-list-item input[type="checkbox"] { forced-color-adjust: none; }
400
+ .md-render .task-list-item input[type="checkbox"]::before,.md-render .task-list-item input[type="checkbox"]:checked::after { background: CanvasText; }
401
+ }
394
402
  .md-render .task-list-item input[type="checkbox"]:checked::before {
395
403
  -webkit-mask-image: var(--md-glyph-task-open);
396
404
  mask-image: var(--md-glyph-task-open);