rapier-markdown-kit 1.2.42 → 1.2.44

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.
@@ -110,11 +110,15 @@ function keyedRow(row, depth) {
110
110
  }
111
111
  return json;
112
112
  }
113
+ const plainRowArrays = new WeakSet();
113
114
  export function* canonicalRowsParts(rows, depth = 0) {
114
- const {array, keys} = fields(rows, depth, new Set());
115
- if (!array) { yield* canonicalParts(rows, depth); return; }
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
+ }
116
120
  yield '[';
117
- for (let i = 0; i < keys.length; i++) { if (i) yield ','; yield canonicalRow(rows[keys[i]], depth + 1); }
121
+ for (let i = 0; i < rows.length; i++) { if (i) yield ','; yield canonicalRow(rows[i], depth + 1); }
118
122
  yield ']';
119
123
  }
120
124
  export function exactKeys(value, keys) {
@@ -10,10 +10,10 @@ 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) ||
@@ -170,6 +170,7 @@ const expandedRows = new WeakMap();
170
170
  const sameSplices = (a, b) => a === b || a.length === b.length &&
171
171
  a.every((row, i) => row.pos === b[i].pos && row.removed === b[i].removed && row.inserted === b[i].inserted);
172
172
  const sameEffect = (a, b) => a === b || !!a && !!b && canonicalJSON(a) === canonicalJSON(b);
173
+ let recentLedgers = [];
173
174
  function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}, native = false) {
174
175
  if (!_rapierValidMetadata(metadata)) throw fault('document metadata required');
175
176
  metadata = {...metadata};
@@ -191,16 +192,30 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
191
192
  yield;
192
193
  }
193
194
  const headBytes = bytes;
195
+ let base = recentLedgers.find(row => row.rows.length <= expanded.length && row.rows.every((value, at) => value === expanded[at]));
194
196
  let startSource = new HistorySource(text), startMetadata = {...metadata};
195
- for (let i = expanded.length - 1; i >= 0; i--) {
197
+ for (let i = expanded.length - 1; i >= (base ? base.rows.length : 0); i--) {
196
198
  startSource = startSource.apply(expanded[i].splices, true);
197
- startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
199
+ if (expanded[i].metadata) startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
198
200
  if (startSource === null || !startMetadata) throw fault('history does not reverse from this document');
199
201
  for (const row of expanded[i].splices) bytes += utf8Length(row.removed) - utf8Length(row.inserted);
200
202
  if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
201
203
  yield;
202
204
  }
203
- 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();
204
219
  const unchangedText = expanded.every(row => !row.splices.length);
205
220
  const contentRoot = headFacts.contentRoot;
206
221
  revision ??= expanded.at(-1)?.transaction.revision ?? 0;
@@ -209,19 +224,24 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
209
224
  const startRevision = expanded[0]?.transaction.baseRevision ?? revision, startRoot = expanded[0]?.beforeHash ?? root;
210
225
  if (typeof documentAuthority !== 'string' || !documentAuthority || documentAuthority.length > 256) throw fault('unsupported or invalid envelope');
211
226
  if (!nonnegative(startRevision) || !rootShape(startRoot)) throw fault('invalid start');
212
- const startFacts = unchangedText ? headFacts : yield* sourceFacts(startText, native);
227
+ const startFacts = unchangedText ? headFacts : base ? base.startFacts : yield* sourceFacts(startText, native);
213
228
  if (startRevision === 0 && startRoot !== startFacts.contentRoot) throw fault('invalid start');
214
229
  const headSHA = headFacts.sha256, startSHA = startFacts.sha256;
215
230
  const payload = {format: 'rapier-ledger/2', documentAuthority,
216
231
  start: {text: startText, revision: startRevision, root: startRoot, sha256: startSHA, metadata: startMetadata},
217
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)};
218
- 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;
219
236
  if (expanded.some(row => Object.hasOwn(row, 'authored'))) {
220
237
  try { authoredHistory(startText, expanded); } catch (_) { throw fault('authored origin differs'); }
221
238
  }
222
239
  const replayedMetadata = prepared.replayMetadata(startMetadata);
223
240
  if (!replayedMetadata || canonicalJSON(replayedMetadata) !== canonicalJSON(metadata)) throw fault('metadata does not describe this document');
224
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);
225
245
  ownLedger(ledger);
226
246
  proofs.set(ledger, {shell: Array.from(envelopeParts({...ledger, start: {...ledger.start, text: ''}})).join(''), startText,
227
247
  text, metadata: {...metadata}, root, revision, prepared: _rapierPrepareHistoryRecords(ledger.records),
@@ -273,7 +293,7 @@ function historyNavigation(ledger, text, metadata, prepared, source) {
273
293
  let traversed = source, traversedMetadata = {...metadata};
274
294
  for (let i = cursor - 1; i >= 0; i--) {
275
295
  traversed = step(traversed, branch[i], false);
276
- 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);
277
297
  if (traversed === null || !traversedMetadata) throw fault('undo branch does not reverse');
278
298
  }
279
299
  if (ledger.complete) {
@@ -284,7 +304,7 @@ function historyNavigation(ledger, text, metadata, prepared, source) {
284
304
  traversed = source; traversedMetadata = {...metadata};
285
305
  for (let i = cursor; i < branch.length; i++) {
286
306
  traversed = step(traversed, branch[i], true);
287
- traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata);
307
+ if (byId.get(branch[i]).metadata) traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata);
288
308
  if (traversed === null || !traversedMetadata) throw fault('redo branch does not replay');
289
309
  }
290
310
  if (ledger.complete && pictures.size) {
@@ -110,7 +110,8 @@ function _rapierRecordIndex(records, shared = false) {
110
110
  }
111
111
  return value;
112
112
  };
113
- 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)};
114
115
  }
115
116
 
116
117
  const _rapierLedgerIndexes = new WeakMap();
@@ -233,87 +234,89 @@ function _rapierPictureFinish(record, original, earlier, lookup) {
233
234
  });
234
235
  }
235
236
 
236
- function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null) {
237
- try {
238
- if (!Array.isArray(records)) return {ok: false};
239
- const nodes = new Map(), references = new Map(), pictures = new Map(), 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);
286
- }
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);
287
286
  }
288
- return {known, inactive};
289
- };
290
- 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 {
291
294
  const tx = record?.transaction;
292
- 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; }
293
296
  const supplied = tx.sourceTransactionIds || (tx.sourceTransactionId ? [tx.sourceTransactionId] : []);
294
297
  if (!Array.isArray(supplied) || new Set(supplied).size !== supplied.length ||
295
- (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; }
296
299
  let refs = supplied;
297
300
  if (tx.operation === 'document.finish-picture' && tx.sourceTransactionId) {
298
301
  const owner = tx.sourceTransactionId;
299
302
  if (!nodes.has(owner) || !active(owner) || finished.has(owner) ||
300
- !_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; }
301
304
  pictures.set(tx.id, owner); finished.add(owner); refs = [];
302
305
  }
303
306
  if (tx.reverts || tx.reapplies) {
304
307
  const target = tx.reverts || tx.reapplies;
305
- 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; }
306
309
  if (tx.reverts) refs = [target];
307
310
  else {
308
311
  const context = contextOf(record.authored || tx.authored);
309
312
  const undo = (lastUndo.get(target) || []).findLast(id => context
310
313
  ? context.known.has(id) && !context.inactive.has(id) : active(id));
311
- if (!undo) return {ok: false};
314
+ if (!undo) { walker.failed = true; return false; }
312
315
  refs = [undo];
313
316
  }
314
317
  }
315
- if (refs.some(id => typeof id !== 'string' || !nodes.has(id) || pictures.has(id))) return {ok: false};
316
- 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; }
317
320
  nodes.set(tx.id, {active: true, blockers: 0, references: refs, dependents: []}); references.set(tx.id, refs.slice());
318
321
  byId.set(tx.id, record); earlier.push(record);
319
322
  for (const ref of refs) {
@@ -328,23 +331,45 @@ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null
328
331
  lastUndo.get(tx.reverts).push(tx.id);
329
332
  }
330
333
  if (visit) visit(record, nodes, refs);
331
- if (observe) observe(record, refs, active);
332
- }
333
- if (!visit) for (let i = order.length - 1; i >= 0; i--) {
334
- const node = nodes.get(order[i]); node.active = node.blockers === 0;
335
- 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);
336
345
  }
337
- const effectiveActIds = [...nodes].filter(([id, node]) => node.active && !pictures.has(id)).map(([id]) => id);
338
- return {ok: true, effectiveActIds, active: new Set(effectiveActIds), references, pictures};
339
- } 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();
340
364
  }
341
365
 
342
366
  function _rapierHistoryEffects(records, visit = null) {
343
367
  return _rapierWalkHistoryEffects(records, null, visit);
344
368
  }
345
369
 
346
- function _rapierMetadataProof(lookup) {
347
- 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 || [])]};
348
373
  const requireInitial = (key, value) => {
349
374
  if (Object.hasOwn(required, key) && required[key] !== value) throw Error('metadata_effect');
350
375
  required[key] = value;
@@ -371,7 +396,7 @@ function _rapierMetadataProof(lookup) {
371
396
  if (!_rapierValidMetadata(initial) || Object.keys(required).some(key => initial[key] !== required[key])) return null;
372
397
  return {...initial, ...values};
373
398
  };
374
- return {observe, replay};
399
+ return {observe, replay, fork: other => _rapierMetadataProof(other || lookup, {values, required, assignments})};
375
400
  }
376
401
 
377
402
  function _rapierMetadataState(initial, records, effects = _rapierHistoryEffects(records)) {
@@ -478,16 +503,79 @@ const _rapierPrepareHistoryRecords = (() => {
478
503
  frame.descriptors = descriptors; frame.fields = fields;
479
504
  frame.leaving = true; active.add(value); pending.push(frame);
480
505
  for (let at = fields.length - 1; at >= 0; at--) {
481
- 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});
482
512
  }
483
513
  }
484
514
  return result.value;
485
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
+ };
486
569
  return (input, earlier = input) => {
487
570
  try {
488
571
  if (!Array.isArray(input) || !Array.isArray(earlier)) return {ok: false};
489
572
  const records = capture(input), basis = input === earlier ? records : capture(earlier), previous = prepared.get(records);
490
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
+ }
491
579
  const byId = new Map();
492
580
  for (const record of records) {
493
581
  const id = record?.transaction?.id;
@@ -495,32 +583,14 @@ const _rapierPrepareHistoryRecords = (() => {
495
583
  byId.set(id, record);
496
584
  }
497
585
  const lookup = _rapierRecordIndex(basis), proof = _rapierMetadataProof(lookup);
498
- const rows = records.map(record => {
499
- const splices = lookup.splices(record), metadata = lookup.metadata(record);
500
- if (splices && !Object.isFrozen(splices)) {
501
- for (const row of splices) Object.freeze(row);
502
- Object.freeze(splices);
503
- }
504
- if (metadata && !Object.isFrozen(metadata)) {
505
- for (const pair of Object.values(metadata)) Object.freeze(pair);
506
- Object.freeze(metadata);
507
- }
508
- return Object.freeze({record, splices, metadata});
509
- });
510
- 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();
511
590
  if (!effects.ok) return {ok: false};
512
- for (const refs of effects.references.values()) Object.freeze(refs);
513
- const effectView = Object.freeze({ok: true, effectiveActIds: Object.freeze(effects.effectiveActIds),
514
- active: view(effects.active), references: view(effects.references), pictures: view(effects.pictures)});
515
- const observation = Object.freeze({ok: effects.ok, records, rows: Object.freeze(rows), effects: effectView, byId: view(byId),
516
- replayMetadata: initial => effects.ok ? proof.replay(initial) : null,
517
- validRecord: (index, documentAuthority, maxDocumentBytes, utf8Length, allowGap = false) => {
518
- if (!effects.ok || !Number.isSafeInteger(index) || index < 0 || index >= records.length) return false;
519
- const row = rows[index], tx = row.record.transaction;
520
- return _rapierCheckLedgerRecord(row.record, records[index - 1] || null, documentAuthority, maxDocumentBytes, utf8Length, allowGap,
521
- {...row, duplicate: false, semantic: !!(tx.reverts || tx.reapplies || tx.sourceTransactionId || tx.sourceTransactionIds?.length)});
522
- }});
523
- 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);
524
594
  } catch (_) { return {ok: false}; }
525
595
  };
526
596
  })();
@@ -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>';
@@ -371,12 +376,12 @@ function initMarkdownIt() {
371
376
  const mathProvider = env.rapierProviders === false ? null : _rapierProviders.math;
372
377
  const mathNeeded = provider?.requiresMath?.(src) === true;
373
378
  const mathReady = !mathNeeded || mathProvider?.status === 'ready';
374
- if (!mathReady && !mathProvider?.request?.() && env.rapierProviders !== false && !_mathPromptedOnce) {
379
+ if (!mathReady && !env.rapierPassive && !mathProvider?.request?.() && env.rapierProviders !== false && !_mathPromptedOnce) {
375
380
  _mathPromptedOnce = true;
376
381
  try { _rapierUiMath.request(); } catch (_) {}
377
382
  }
378
383
  const ready = diagramReady && mathReady;
379
- if (!diagramReady && !provider?.request?.() && env.rapierProviders !== false && !_diagramPromptedOnce) {
384
+ if (!diagramReady && !env.rapierPassive && !provider?.request?.() && env.rapierProviders !== false && !_diagramPromptedOnce) {
380
385
  _diagramPromptedOnce = true;
381
386
  try { _rapierUiDiagram.request(); } catch (_) {}
382
387
  }
@@ -420,7 +425,7 @@ function initMarkdownIt() {
420
425
  if (env.rapierPage) return sourceHtml;
421
426
  const mathProvider = env.rapierProviders === false ? null : _rapierProviders.math;
422
427
  if (!mathProvider || mathProvider.status !== 'ready' || typeof mathProvider.renderToString !== 'function') {
423
- if (!mathProvider?.request?.() && env.rapierProviders !== false && !_mathPromptedOnce) {
428
+ if (!env.rapierPassive && !mathProvider?.request?.() && env.rapierProviders !== false && !_mathPromptedOnce) {
424
429
  _mathPromptedOnce = true;
425
430
  try { _rapierUiMath.request(); } catch (_) {}
426
431
  }
@@ -454,10 +459,215 @@ function initMarkdownIt() {
454
459
  globalThis.RapierImageAssets.configureParser(md, _rapierSplitOpeningFrontmatter);
455
460
  const render = md.renderer.render;
456
461
  md.renderer.render = function (tokens, options, env) {
457
- return render.call(this, _rapierCloseOpenDetails(tokens), options, env);
462
+ return render.call(this, _rapierHideForeignParagraphs(_rapierCloseOpenDetails(tokens)), options, env);
458
463
  };
459
464
  }
460
465
 
466
+ function _rapierHtmlTagStack(html, stack = []) {
467
+ if (!html) return stack;
468
+ const swallow = _rapierHtmlTagStack.swallow || (_rapierHtmlTagStack.swallow = new Set('select template textarea title style xmp plaintext noscript iframe noembed noframes script svg math'.split(' ')));
469
+ const rawTags = _rapierHtmlTagStack.raw || (_rapierHtmlTagStack.raw = new Set('script style textarea title xmp iframe noembed noframes noscript plaintext'.split(' ')));
470
+ let i = 0;
471
+ while (i < html.length) {
472
+ const top = stack.at(-1);
473
+ if (top && top.raw) {
474
+ const closer = new RegExp('</' + top.name + '(?=[\\t\\n\\f\\r />])', 'ig');
475
+ closer.lastIndex = i;
476
+ const found = closer.exec(html);
477
+ if (!found) break;
478
+ i = found.index;
479
+ }
480
+ const open = html.indexOf('<', i);
481
+ if (open < 0) break;
482
+ if (html.startsWith('<!--', open)) {
483
+ const end = html.indexOf('-->', open + 4);
484
+ if (end < 0) break;
485
+ i = end + 3;
486
+ continue;
487
+ }
488
+ if (html.startsWith('<!', open) || html.startsWith('<?', open)) {
489
+ const end = html.indexOf('>', open + 2);
490
+ i = end < 0 ? html.length : end + 1;
491
+ continue;
492
+ }
493
+ const match = /^<\/?([A-Za-z][A-Za-z0-9:-]*)/.exec(html.slice(open));
494
+ if (!match) { i = open + 1; continue; }
495
+ const name = match[1].toLowerCase();
496
+ let end = open + match[0].length, quote = '';
497
+ for (; end < html.length; end++) {
498
+ const character = html[end];
499
+ if (quote) { if (character === quote) quote = ''; }
500
+ else if (character === '"' || character === "'") quote = character;
501
+ else if (character === '>') break;
502
+ }
503
+ if (end >= html.length) break;
504
+ if (html[open + 1] === '/') {
505
+ let at = stack.length - 1;
506
+ while (at >= 0 && stack[at].name !== name && !swallow.has(stack[at].name)) at--;
507
+ if (at >= 0 && stack[at].name === name) stack.length = at;
508
+ } else stack.push({name, at: open, tagEnd: end + 1, raw: rawTags.has(name)});
509
+ i = end + 1;
510
+ }
511
+ return stack;
512
+ }
513
+ function _rapierForeignOpen(stack) {
514
+ for (let index = 0; index < stack.length; index++) {
515
+ const name = stack[index].name;
516
+ if (name === 'svg' || name === 'math') return true;
517
+ }
518
+ return false;
519
+ }
520
+ function _rapierSealBlockHtml(html) {
521
+ if (!html || html.indexOf('<') < 0) return html;
522
+ const stack = _rapierHtmlTagStack(html);
523
+ const swallow = _rapierHtmlTagStack.swallow;
524
+ const foreign = _rapierSealBlockHtml.foreign || (_rapierSealBlockHtml.foreign = new Set(['svg', 'math']));
525
+ const cut = stack.find(frame => swallow.has(frame.name) && (frame.raw || foreign.has(frame.name)));
526
+ if (cut) {
527
+ const wrappers = stack.filter(frame => frame.at < cut.at);
528
+ return html.slice(0, cut.at) + _rapierSealBlockTail(html.slice(cut.at), wrappers);
529
+ }
530
+ const stale = stack.filter(frame => swallow.has(frame.name));
531
+ if (!stale.length) return html;
532
+ let out = html;
533
+ for (const frame of stale.sort((left, right) => right.at - left.at))
534
+ out = out.slice(0, frame.at) + escapeRapierHtmlText(out.slice(frame.at, frame.tagEnd)) + out.slice(frame.tagEnd);
535
+ return out;
536
+ }
537
+
538
+ function _rapierSealBlockTail(tail, wrappers) {
539
+ const open = wrappers.map(frame => frame.name);
540
+ let out = '', i = 0;
541
+ while (i < tail.length) {
542
+ const at = tail.indexOf('<', i);
543
+ if (at < 0) { out += escapeRapierHtmlText(tail.slice(i)); break; }
544
+ out += escapeRapierHtmlText(tail.slice(i, at));
545
+ const match = /^<\/([A-Za-z][A-Za-z0-9:-]*)\s*>/.exec(tail.slice(at));
546
+ if (match && open.at(-1) === match[1].toLowerCase()) {
547
+ out += match[0];
548
+ open.pop();
549
+ i = at + match[0].length;
550
+ continue;
551
+ }
552
+ out += escapeRapierHtmlText(tail[at]);
553
+ i = at + 1;
554
+ }
555
+ return out;
556
+ }
557
+
558
+ function _rapierRenderTokenHtml(tokens, index, env) {
559
+ const token = tokens[index], rule = md.renderer.rules[token.type];
560
+ return token.type === 'inline' ? md.renderer.renderInline(token.children, md.options, env)
561
+ : rule ? rule(tokens, index, md.options, env, md.renderer) : md.renderer.renderToken(tokens, index, md.options);
562
+ }
563
+
564
+ function _rapierTokenBlocks(tokens) {
565
+ const blocks = [];
566
+ let start = 0, depth = 0;
567
+ for (let index = 0; index < tokens.length; index++) {
568
+ depth += tokens[index].nesting || 0;
569
+ if (depth <= 0) { blocks.push([start, index + 1]); start = index + 1; depth = 0; }
570
+ }
571
+ if (start < tokens.length) blocks.push([start, tokens.length]);
572
+ return blocks;
573
+ }
574
+ function _rapierScanHtmlBlocks(tokens, start, end, stack) {
575
+ for (let index = start; index < end; index++) {
576
+ if (tokens[index].type === 'html_block') _rapierHtmlTagStack(tokens[index].content || '', stack);
577
+ }
578
+ return stack;
579
+ }
580
+ function _rapierForeignSealRanges(tokens) {
581
+ const blocks = _rapierTokenBlocks(tokens);
582
+ const ranges = [];
583
+ const scanLater = ([start, end], frames) => {
584
+ if (tokens[start].type !== 'paragraph_open') return _rapierScanHtmlBlocks(tokens, start, end, frames);
585
+ for (const child of tokens[start + 1] && tokens[start + 1].children || [])
586
+ if (child.type === 'html_inline') _rapierHtmlTagStack(child.content || '', frames);
587
+ return frames;
588
+ };
589
+ for (let index = 0; index < blocks.length; index++) {
590
+ const stack = _rapierScanHtmlBlocks(tokens, blocks[index][0], blocks[index][1], []);
591
+ if (!_rapierForeignOpen(stack)) { ranges.push(blocks[index]); continue; }
592
+ const frames = stack.map(frame => ({name: frame.name, raw: frame.raw}));
593
+ let joined = -1;
594
+ for (let later = index + 1; later < blocks.length && later <= index + 512; later++) {
595
+ scanLater(blocks[later], frames);
596
+ if (!_rapierForeignOpen(frames)) { joined = later; break; }
597
+ }
598
+ if (joined < 0) { ranges.push(blocks[index]); continue; }
599
+ const range = [blocks[index][0], blocks[joined][1]];
600
+ for (let later = index + 1; later <= joined; later++) {
601
+ const [start, end] = blocks[later];
602
+ if (tokens[start].type === 'paragraph_open' && tokens[end - 1].type === 'paragraph_close' &&
603
+ (tokens[start + 1] && tokens[start + 1].children || []).some(child => child.type === 'html_inline'))
604
+ (range.hide || (range.hide = new Set())).add(start).add(end - 1);
605
+ }
606
+ ranges.push(range);
607
+ index = joined;
608
+ }
609
+ return ranges;
610
+ }
611
+ function _rapierHideForeignParagraphs(tokens) {
612
+ let out = tokens;
613
+ for (const range of _rapierForeignSealRanges(tokens)) {
614
+ if (!range.hide) continue;
615
+ if (out === tokens) out = tokens.slice();
616
+ for (const index of range.hide) out[index] = Object.assign(Object.create(Object.getPrototypeOf(tokens[index])), tokens[index], {hidden: true});
617
+ }
618
+ return out;
619
+ }
620
+ function _rapierMergeForeignBlocks(blocks, source, parser) {
621
+ const text = String(source || '');
622
+ const out = [];
623
+ const inline = new Map();
624
+ const scanLater = (row, frames) => {
625
+ if (!row) return frames;
626
+ if (row.type === 'html_block') return _rapierHtmlTagStack(String(row.raw || ''), frames);
627
+ if (row.type !== 'paragraph' || String(row.raw || '').indexOf('<') < 0) return frames;
628
+ let children = inline.get(row);
629
+ if (!children) {
630
+ if (!parser && !md) initMarkdownIt();
631
+ const parsed = (parser || md).parseInline(String(row.raw || ''), {});
632
+ children = parsed[0] && parsed[0].children || [];
633
+ inline.set(row, children);
634
+ }
635
+ for (const child of children) if (child.type === 'html_inline') _rapierHtmlTagStack(child.content || '', frames);
636
+ return frames;
637
+ };
638
+ let index = 0;
639
+ while (index < blocks.length) {
640
+ const block = blocks[index];
641
+ const stack = [];
642
+ if (block && block.type === 'html_block') _rapierHtmlTagStack(String(block.raw || ''), stack);
643
+ if (!_rapierForeignOpen(stack)) { out.push(block); index++; continue; }
644
+ const frames = stack.map(frame => ({name: frame.name, raw: frame.raw}));
645
+ let joined = -1;
646
+ for (let later = index + 1; later < blocks.length && later <= index + 512; later++) {
647
+ scanLater(blocks[later], frames);
648
+ if (!_rapierForeignOpen(frames)) { joined = later; break; }
649
+ }
650
+ if (joined < 0) { out.push(block); index++; continue; }
651
+ const start = Number(block._sourceStart), end = Number(blocks[joined]._sourceEnd);
652
+ const raw = Number.isFinite(start) && Number.isFinite(end) && start >= 0 && end >= start && end <= text.length
653
+ ? text.slice(start, end)
654
+ : blocks.slice(index, joined + 1).map(row => String(row && row.raw || '')).join('\n\n');
655
+ out.push({...block, raw, _sourceEnd: Number.isFinite(end) ? end : block._sourceEnd, rendered: null});
656
+ index = joined + 1;
657
+ }
658
+ out._rapierFacts = Array.isArray(blocks && blocks._rapierFacts) ? blocks._rapierFacts.slice() : [];
659
+ return out;
660
+ }
661
+ function _rapierRenderSealedHtml(tokens, env) {
662
+ let html = '';
663
+ for (const range of _rapierForeignSealRanges(tokens)) {
664
+ let block = '';
665
+ for (let index = range[0]; index < range[1]; index++) block += range.hide && range.hide.has(index) ? (tokens[index].nesting < 0 ? '\n' : '') : _rapierRenderTokenHtml(tokens, index, env);
666
+ html += _rapierSealBlockHtml(block);
667
+ }
668
+ return html;
669
+ }
670
+
461
671
  function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
462
672
  const root = document.createElement('template').content.ownerDocument.createElement('div');
463
673
  root.setAttribute('data-rapier-semantic-root', 'true');
@@ -476,24 +686,28 @@ function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
476
686
  }
477
687
 
478
688
  const markdown = _rapierSplitOpeningFrontmatter(canonical).body;
479
- const env = {rapierImagePresentation: options.imagePresentation === true, ...(options.page ? {rapierPage: true} : {}), ...(options.providers === false ? {rapierProviders: false} : {})};
689
+ const env = {rapierImagePresentation: options.imagePresentation === true, rapierPassive: options.passive === true, ...(options.page ? {rapierPage: true} : {}), ...(options.providers === false ? {rapierProviders: false} : {})};
480
690
  if (options.wikiEmbedding) env.rapierWikiEmbedding = true;
481
691
  const wikiLinks = wikiMode(options);
482
692
  if (wikiLinks) env.rapierWikiLinks = wikiLinks;
483
693
  if (options.mathEquations) env.rapierMathEquations = options.mathEquations;
484
694
  const nesting = md && options.maxNesting > 0 ? options.maxNesting : 0;
485
- let cutAt = -1, rendered;
486
- if (nesting || md && options.closeDetails) {
695
+ let cutAt = -1, rendered = '';
696
+ codeLeft = options.codeBudget >= 0 ? options.codeBudget : Infinity;
697
+ try {
698
+ if (!md) rendered = '<pre><code>' + escapeRapierHtmlText(markdown) + '</code></pre>';
699
+ else {
487
700
  const saved = md.options.maxNesting;
488
701
  if (nesting) md.options.maxNesting = nesting;
489
702
  try {
490
- const tokens = md.parse(markdown, env);
703
+ let tokens = md.parse(markdown, env);
491
704
  if (nesting) cutAt = _rapierNestingCutAt(tokens, nesting);
492
- rendered = md.renderer.render(options.closeDetails ? _rapierCloseOpenDetails(tokens) : tokens, md.options, env);
705
+ tokens = _rapierCloseOpenDetails(tokens);
706
+ rendered = _rapierRenderSealedHtml(tokens, env);
707
+ if (md.renderer.rapierTransformHtml) rendered = md.renderer.rapierTransformHtml(rendered);
493
708
  } finally { md.options.maxNesting = saved; }
494
- } else rendered = md
495
- ? md.render(markdown, env)
496
- : '<pre><code>' + escapeRapierHtmlText(markdown) + '</code></pre>';
709
+ }
710
+ } finally { codeLeft = Infinity; }
497
711
  if (md && env.footnotes && Array.isArray(env.footnotes.list) && env.footnotes.list.length) {
498
712
  const labels = env.footnotes.list.map((item, index) =>
499
713
  String(item && item.label || ('inline-' + (index + 1))));
@@ -503,14 +717,14 @@ function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
503
717
  if (cutAt >= 0) _rapierAppendNestingCut(root, markdown, cutAt);
504
718
  _rapierApplyBlockDirection(root);
505
719
 
506
- _rapierAssignHeadingSlugs(root, RAPIER_RENDERED_HEADING_SELECTOR, false);
720
+ _rapierAssignHeadingSlugs(root, RAPIER_RENDERED_HEADING_SELECTOR, false, options.carry?.slugs, options.carry?.headingIds);
507
721
  _rapierDisambiguateRenderedAnchors(root);
508
722
 
509
723
  root.querySelectorAll('pre > code').forEach(_rapierNormalizeCodeElement);
510
- _rapierMarkTableCaptions(root);
724
+ _rapierMarkTableCaptions(root, options.carry);
511
725
  _rapierMarkFigureCaptions(root);
512
726
  _rapierMarkListContinuations(_rapierChildElements(root));
513
- _rapierDecorateJumpLists(root);
727
+ _rapierDecorateJumpLists(root, options.carry);
514
728
  return root;
515
729
  }
516
730
 
@@ -543,12 +757,13 @@ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {})
543
757
  let {tokens, env} = parsed, labels = env.footnotes?.list?.map((item, index) => String(item?.label || ('inline-' + (index + 1))));
544
758
  const cutAt = nesting ? _rapierNestingCutAt(tokens, nesting) : -1;
545
759
  tokens = _rapierCloseOpenDetails(tokens);
546
- env = {...env, rapierImagePresentation: options.imagePresentation === true};
760
+ env = {...env, rapierImagePresentation: options.imagePresentation === true, rapierPassive: options.passive === true};
547
761
  await pause(work, .3);
548
762
  const inert = document.createElement('template').content.ownerDocument;
549
763
  const root = inert.createElement('div'), part = inert.createElement('div'), boundary = htmlBoundary();
550
764
  root.setAttribute('data-rapier-semantic-root', 'true');
551
765
  let html = '', began = performance.now(), complete = false;
766
+ codeLeft = options.codeBudget >= 0 ? options.codeBudget : Infinity;
552
767
  const append = () => {
553
768
  if (md.renderer.rapierTransformHtml) html = md.renderer.rapierTransformHtml(html);
554
769
  if (labels?.length) html = _relabelFootnoteHtml(html, labels);
@@ -556,37 +771,90 @@ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {})
556
771
  while (part.firstChild) root.appendChild(part.firstChild);
557
772
  html = '';
558
773
  };
774
+ const finishBlock = block => {
775
+ const sealed = _rapierSealBlockHtml(block);
776
+ html += sealed;
777
+ if (boundary(sealed) && html.length >= 16384) append();
778
+ };
559
779
  try {
560
- for (let index = 0; index < tokens.length; index++) {
561
- const token = tokens[index], rule = md.renderer.rules[token.type];
562
- let rendered = token.type === 'inline' ? md.renderer.renderInline(token.children, md.options, env)
563
- : rule ? rule(tokens, index, md.options, env, md.renderer) : md.renderer.renderToken(tokens, index, md.options);
564
- html += rendered;
565
- if (boundary(rendered) && html.length >= 16384) append();
566
- if (performance.now() - began >= 8) {
567
- await pause(work, .3 + .2 * (index + 1) / tokens.length);
568
- began = performance.now();
780
+ for (const range of _rapierForeignSealRanges(tokens)) {
781
+ let block = '';
782
+ for (let index = range[0]; index < range[1]; index++) {
783
+ block += range.hide && range.hide.has(index) ? (tokens[index].nesting < 0 ? '\n' : '') : _rapierRenderTokenHtml(tokens, index, env);
784
+ if (performance.now() - began >= 8) {
785
+ await pause(work, .3 + .2 * (index + 1) / tokens.length);
786
+ began = performance.now();
787
+ }
569
788
  }
789
+ finishBlock(block);
570
790
  }
571
791
  if (html) append();
572
792
  if (cutAt >= 0) _rapierAppendNestingCut(root, markdown, cutAt);
573
793
  parsed = tokens = env = labels = null;
574
- await finishAsync(_rapierSemanticDecorations(root), work);
794
+ await finishAsync(_rapierSemanticDecorations(root, options.carry), work);
575
795
  await pause(work, .6);
576
796
  complete = true;
577
797
  return root;
578
- } finally { if (!complete) { root.replaceChildren(); part.replaceChildren(); } }
798
+ } finally { codeLeft = Infinity; if (!complete) { root.replaceChildren(); part.replaceChildren(); } }
799
+ }
800
+
801
+ function _rapierPartDecorationGroups(tokens) {
802
+ const groups = [];
803
+ for (let at = 0; at < tokens.length; at++) {
804
+ const first = tokens[at];
805
+ if (first.type !== 'heading_open' && first.type !== 'table_open') continue;
806
+ const close = first.type === 'heading_open' ? 'heading_close' : 'table_close';
807
+ let end = at + 1;
808
+ while (end < tokens.length && !(tokens[end].type === close && tokens[end].level === first.level)) end++;
809
+ end++;
810
+ if (first.type === 'table_open' && tokens[end]?.type === 'paragraph_open' && tokens[end].level === first.level) {
811
+ while (end < tokens.length && !(tokens[end].type === 'paragraph_close' && tokens[end].level === first.level)) end++;
812
+ end++;
813
+ }
814
+ groups.push(tokens.slice(at, end));
815
+ at = end - 1;
816
+ }
817
+ return groups;
818
+ }
819
+
820
+ function _rapierReadPartDecorations(canonical, metadata, options = {}) {
821
+ const carry = options.carry || {slugs: Object.create(null), captions: 0, continued: true}, captionStart = carry.captions || 0;
822
+ const markdown = _rapierSplitOpeningFrontmatter(canonical).body, env = {rapierImagePresentation: options.imagePresentation === true, rapierPassive: true, rapierPartDecorations: true,
823
+ ...(options.page ? {rapierPage: true} : {}), ...(options.providers === false ? {rapierProviders: false} : {})};
824
+ if (options.wikiEmbedding) env.rapierWikiEmbedding = true;
825
+ const wikiLinks = wikiMode(options);
826
+ if (wikiLinks) env.rapierWikiLinks = wikiLinks;
827
+ if (options.mathEquations) env.rapierMathEquations = options.mathEquations;
828
+ const saved = md.options.maxNesting;
829
+ let tokens;
830
+ try {
831
+ if (options.maxNesting > 0) md.options.maxNesting = options.maxNesting;
832
+ tokens = md.parse(markdown, env);
833
+ } finally { md.options.maxNesting = saved; }
834
+ if (tokens.some(token => token.type === 'html_block' || token.type === 'math_block' || token.type.startsWith('footnote_') ||
835
+ token.type === 'inline' && token.children?.some(child => child.type === 'html_inline' || child.type === 'math_inline' || child.type.startsWith('footnote_')))) {
836
+ const root = _rapierRenderSemanticRoot(canonical, metadata, {...options, carry, codeBudget: 0, passive: true});
837
+ return Object.freeze({headingIds: Object.freeze([...root.querySelectorAll(RAPIER_RENDERED_HEADING_SELECTOR)].map(heading => heading.id)), captionStart});
838
+ }
839
+ const html = [];
840
+ for (const group of _rapierPartDecorationGroups(tokens)) html.push('<div>', md.renderer.render(group, md.options, env), '</div>');
841
+ const root = document.createElement('template').content.ownerDocument.createElement('div');
842
+ root.innerHTML = sanitizeRapierHtml(html.join(''), 'export');
843
+ _rapierAssignHeadingSlugs(root, RAPIER_RENDERED_HEADING_SELECTOR, false, carry.slugs);
844
+ const headingIds = Object.freeze([...root.querySelectorAll(RAPIER_RENDERED_HEADING_SELECTOR)].map(heading => heading.id));
845
+ _rapierMarkTableCaptions(root, carry);
846
+ return Object.freeze({headingIds, captionStart});
579
847
  }
580
848
 
581
- function* _rapierSemanticDecorations(root) {
849
+ function* _rapierSemanticDecorations(root, carry = null) {
582
850
  yield* _rapierApplyBlockDirectionSteps(root); yield;
583
- yield* _rapierAssignHeadingSlugsSteps(root, RAPIER_RENDERED_HEADING_SELECTOR, false); yield;
851
+ yield* _rapierAssignHeadingSlugsSteps(root, RAPIER_RENDERED_HEADING_SELECTOR, false, carry?.slugs, carry?.headingIds); yield;
584
852
  yield* _rapierDisambiguateRenderedAnchorsSteps(root); yield;
585
853
  for (const code of root.querySelectorAll('pre > code')) { _rapierNormalizeCodeElement(code); yield; }
586
- yield* _rapierMarkTableCaptionsSteps(root); yield;
854
+ yield* _rapierMarkTableCaptionsSteps(root, carry); yield;
587
855
  yield* _rapierMarkFigureCaptionsSteps(root); yield;
588
856
  yield* _rapierMarkListContinuationsSteps(_rapierChildElements(root)); yield;
589
- yield* _rapierDecorateJumpListsSteps(root); yield;
857
+ yield* _rapierDecorateJumpListsSteps(root, carry); yield;
590
858
  }
591
859
 
592
860
  function _rapierInstallSemanticProbe(instance) {
@@ -931,17 +1199,23 @@ function _rapierFirstStrongDir(text) {
931
1199
 
932
1200
  const _RAPIER_DIR_BLOCKS = 'p,h1,h2,h3,h4,h5,h6,li,blockquote,ul,ol,dt,dd,td,th';
933
1201
 
934
- function _rapierAssignHeadingSlugs(root, selector, reserveExisting) { return finish(_rapierAssignHeadingSlugsSteps(root, selector, reserveExisting)); }
1202
+ function _rapierAssignHeadingSlugs(root, selector, reserveExisting, taken, assigned) { return finish(_rapierAssignHeadingSlugsSteps(root, selector, reserveExisting, taken, assigned)); }
935
1203
 
936
- function* _rapierAssignHeadingSlugsSteps(root, selector = RAPIER_RENDERED_HEADING_SELECTOR, reserveExisting = true) {
1204
+ function* _rapierAssignHeadingSlugsSteps(root, selector = RAPIER_RENDERED_HEADING_SELECTOR, reserveExisting = true, taken = null, assigned = null) {
937
1205
  if (!root || !root.querySelectorAll) return;
938
- const used = Object.create(null);
1206
+ const used = taken || Object.create(null);
939
1207
  const headings = _rapierDormantHeadings(root, selector, true);
1208
+ if (assigned && assigned.length !== headings.length) {
1209
+ try { console.warn('[rapier-reader] Part heading facts do not match its rendered headings; assigning ordinary heading IDs.'); } catch (_) {}
1210
+ assigned = null;
1211
+ }
940
1212
  const headingSet = new Set(headings);
941
1213
  if (reserveExisting) for (const element of _rapierDormantHeadings(root.isConnected ? document : root, '[id]', true)) { yield;
942
1214
  if (!headingSet.has(element) && element.id) used[element.id] = 0;
943
1215
  }
1216
+ let index = 0;
944
1217
  for (const heading of headings) { yield;
1218
+ if (assigned) { heading.id = assigned[index++]; continue; }
945
1219
  const base = _rapierHeadingSlugBase(heading.textContent);
946
1220
  const owned = typeof _rapierChromeOwnsId === 'function' && _rapierChromeOwnsId(base);
947
1221
  heading.id = _rapierNextHeadingSlug(owned ? 'heading-' + base : base, used);
@@ -1042,9 +1316,9 @@ function _rapierDecodeHash(hash) {
1042
1316
  catch (_) { return raw; }
1043
1317
  }
1044
1318
 
1045
- function _rapierDecorateJumpLists(root) { return finish(_rapierDecorateJumpListsSteps(root)); }
1319
+ function _rapierDecorateJumpLists(root, carry) { return finish(_rapierDecorateJumpListsSteps(root, carry)); }
1046
1320
 
1047
- function* _rapierDecorateJumpListsSteps(root) {
1321
+ function* _rapierDecorateJumpListsSteps(root, carry = null) {
1048
1322
  for (const list of root.querySelectorAll('ul')) { yield;
1049
1323
  if (list.closest('li')) continue;
1050
1324
  const items = Array.from(list.children).filter(item => item.tagName === 'LI');
@@ -1067,7 +1341,7 @@ function* _rapierDecorateJumpListsSteps(root) {
1067
1341
  }
1068
1342
  }
1069
1343
 
1070
- const first = root.firstElementChild, only = first && !first.nextElementSibling ? first : null;
1344
+ const first = root.firstElementChild, only = !carry && first && !first.nextElementSibling ? first : null;
1071
1345
  if (only && only.tagName === 'P' && /^(?:jump\s+to|contents|table\s+of\s+contents):?$/i.test(String(only.textContent || '').trim())) {
1072
1346
  only.classList.add('rapier-jump-label-candidate');
1073
1347
  }
@@ -1109,11 +1383,11 @@ function* _rapierMarkListContinuationsSteps(blocks, mark = (element, name) => el
1109
1383
  }
1110
1384
  }
1111
1385
 
1112
- function _rapierMarkTableCaptions(root) { return finish(_rapierMarkTableCaptionsSteps(root)); }
1386
+ function _rapierMarkTableCaptions(root, carry) { return finish(_rapierMarkTableCaptionsSteps(root, carry)); }
1113
1387
 
1114
- function* _rapierMarkTableCaptionsSteps(root) {
1388
+ function* _rapierMarkTableCaptionsSteps(root, carry = null) {
1115
1389
  const used = new Set(Array.from(root.querySelectorAll('[id]'), element => element.id));
1116
- let ordinal = 0;
1390
+ let ordinal = carry?.captions || 0;
1117
1391
  for (const table of root.querySelectorAll('table')) { yield;
1118
1392
  const sibling = table.nextElementSibling;
1119
1393
  if (!sibling || sibling.tagName !== 'P') continue;
@@ -1126,6 +1400,7 @@ function* _rapierMarkTableCaptionsSteps(root) {
1126
1400
  const existing = (table.getAttribute('aria-describedby') || '').trim();
1127
1401
  table.setAttribute('aria-describedby', existing ? existing + ' ' + id : id);
1128
1402
  }
1403
+ if (carry) carry.captions = ordinal;
1129
1404
  }
1130
1405
 
1131
1406
 
@@ -1829,7 +2104,7 @@ function renderWithMathML(source, metadata = {}) {
1829
2104
  return {html, equations};
1830
2105
  }
1831
2106
 
1832
- 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};
2107
+ 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, _rapierSealBlockHtml, _rapierMergeForeignBlocks, 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};
1833
2108
  }
1834
2109
  export {RAPIER_TABLE_CAPTION_PREFIX_RE as tableCaptionPrefix, RAPIER_PORTABLE_HIGHLIGHT_HEX as portableHighlightHex, _RAPIER_RTL_SCRIPT as rtlScripts, createMarkdownRenderer};
1835
2110
 
@@ -69,7 +69,8 @@ function cleanDocumentStyle(tag, value, context, classes = '') {
69
69
  const property = part.slice(0, colon).trim().toLowerCase(), body = part.slice(colon + 1).trim();
70
70
  const diagramProperty = diagram && /^--(?:_[a-z0-9_-]+|pele-[a-z0-9-]+)$/.test(property);
71
71
  if (!DOCUMENT_STYLE_PROPERTIES.has(property) && !/^--md-[a-z0-9-]+$/.test(property) && !diagramProperty && !(diagram && property === 'font')) return;
72
- if (!body || /[\\\u0000-\u0008\u000b\u000e-\u001f{}@]|\/\*|\*\//.test(body)) return;
72
+ if (!body || /[\u0000-\u0008\u000b\u000e-\u001f{}@]|\/\*|\*\//.test(body) ||
73
+ /["'\\]/.test(body.replace(/"(?:[^"\\\n\r\f]|\\["\\])*"|'(?:[^'\\\n\r\f]|\\['\\])*'/g, ''))) return;
73
74
  const withoutLocal = body.replace(/\burl\(\s*(['"]?)#[a-z0-9_.:-]+\1\s*\)/gi, '');
74
75
  if (/(?:url|image|image-set|-webkit-image-set|element|paint|attr|expression)\s*\(/i.test(withoutLocal)) return;
75
76
  kept.push(part.trim());
@@ -304,6 +305,31 @@ function _rapierInstallSanitizeHooks() {
304
305
  if (level) node.setAttribute('data-rapier-word-level', String(level));
305
306
  else if (node.nodeName === 'SPAN' && wordListIsMarker(style)) node.setAttribute('data-rapier-word-marker', '1');
306
307
  });
308
+ DOMPurify.addHook('uponSanitizeElement', node => {
309
+ if (_rapierSanitizeRuntime.context !== 'paste' || !node || node.nodeName !== 'TABLE') return;
310
+ node.removeAttribute('data-rapier-paste-word-header');
311
+ try {
312
+ const own = (node.getAttribute('style') || '') + ' ' + (node.getAttribute('class') || '');
313
+ const word = /mso-yfti-|MsoTable/i.test(own) || !!node.querySelector('[style*="mso-"],[class*="Mso"]');
314
+ if (!word || node.tHead || node.querySelector('th')) return;
315
+ const row = Array.from(node.querySelectorAll('tr')).find(candidate => candidate.closest('table') === node);
316
+ if (!row) return;
317
+ const cells = Array.from(row.children).filter(cell => cell.nodeName === 'TD' || cell.nodeName === 'TH');
318
+ if (!cells.length) return;
319
+ let bold = false;
320
+ for (const cell of cells) {
321
+ const walker = node.ownerDocument.createTreeWalker(cell, NodeFilter.SHOW_TEXT);
322
+ for (let text = walker.nextNode(); text; text = walker.nextNode()) {
323
+ if (!/\S/.test(text.nodeValue || '')) continue;
324
+ const parent = text.parentElement;
325
+ const style = parent?.getAttribute?.('style') || '';
326
+ if (!(parent && (parent.closest('b,strong') || /font-weight\s*:\s*(?:bold|[6-9]00|1000)\b/i.test(style)))) return;
327
+ bold = true;
328
+ }
329
+ }
330
+ if (bold) node.setAttribute('data-rapier-paste-word-header', '');
331
+ } catch (_) {}
332
+ });
307
333
  }
308
334
 
309
335
  function _rapierInstallDocumentSanitizeHooks() {
@@ -414,9 +440,17 @@ function sanitize(value, context, fragment) {
414
440
  if (purifyConfigured.get(DOMPurify) !== options) { DOMPurify.setConfig(options); purifyConfigured.set(DOMPurify, options); }
415
441
  const previous = _rapierSanitizeRuntime.context;
416
442
  _rapierSanitizeRuntime.context = selected;
443
+ ensureScratchParseHook();
444
+ const scratch = [];
445
+ const outerSink = scratchSink;
446
+ scratchSink = scratch;
447
+ let result;
417
448
  try {
418
- return DOMPurify.sanitize(value == null ? '' : String(value));
449
+ result = DOMPurify.sanitize(value == null ? '' : String(value));
450
+ return result;
419
451
  } finally {
452
+ scratchSink = outerSink;
453
+ releaseScratchMedia(scratch, result);
420
454
  if (labels) {
421
455
  DOMPurify.removeHook('uponSanitizeElement', diagramLabelElement);
422
456
  DOMPurify.removeHook('uponSanitizeAttribute', diagramLabelAttribute);
@@ -426,6 +460,37 @@ function sanitize(value, context, fragment) {
426
460
  }
427
461
  }
428
462
 
463
+ let scratchSink = null;
464
+ function ensureScratchParseHook() {
465
+ if (typeof DOMParser !== 'function' || DOMParser.prototype.parseFromString._rapierScratch) return;
466
+ const original = DOMParser.prototype.parseFromString;
467
+ function wrapped(source, type) {
468
+ const doc = original.call(this, source, type);
469
+ scratchSink?.push(doc);
470
+ return doc;
471
+ }
472
+ wrapped._rapierScratch = true;
473
+ DOMParser.prototype.parseFromString = wrapped;
474
+ }
475
+
476
+ function releaseScratchMedia(docs, result) {
477
+ const kept = result && typeof result === 'object' ? result.nodeType === 9 ? result : result.ownerDocument : null;
478
+ for (const doc of docs) {
479
+ if (!doc || doc === kept || !doc.querySelectorAll) continue;
480
+ for (const el of [...doc.querySelectorAll('video, audio')]) releaseMediaElement(el);
481
+ }
482
+ }
483
+ function releaseMediaElement(el) {
484
+ if (!el || (el.nodeName !== 'VIDEO' && el.nodeName !== 'AUDIO')) return;
485
+ try {
486
+ el.pause();
487
+ el.removeAttribute('src');
488
+ for (const source of el.querySelectorAll('source')) source.removeAttribute('src');
489
+ el.load();
490
+ } catch (_) {}
491
+ el.remove();
492
+ }
493
+
429
494
  function escapeRapierHtmlText(value) {
430
495
  return String(value == null ? '' : value)
431
496
  .replace(/&/g, '&amp;')
@@ -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
 
@@ -712,7 +725,7 @@ function* _rapierSharedPageLayoutSteps(root) {
712
725
  for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
713
726
  yield;
714
727
  const layout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
715
- if (layout?.wrap !== 'around' && layout?.wrap !== 'box') continue;
728
+ if (layout?.wrap !== 'around' && layout?.wrap !== 'beside' && layout?.wrap !== 'box') continue;
716
729
  const paragraph = _rapierSharePictureParagraph(image);
717
730
  if (!paragraph) continue;
718
731
  const owner = globalThis.RapierMarkdownLayout.wrapNeighbour(paragraph, _rapierShareWrapKind);
@@ -1445,6 +1458,8 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1445
1458
  if (layout.wrap === 'box') {
1446
1459
  value = boxProfile(image);
1447
1460
  if (!value && rotateRad) value = geometry.rasterTiltProfile(image.naturalWidth, image.naturalHeight, rotateRad);
1461
+ } else if (layout.wrap === 'beside') {
1462
+ value = rotateRad ? geometry.rasterTiltProfile(image.naturalWidth, image.naturalHeight, rotateRad) : null;
1448
1463
  } else {
1449
1464
  const occupancy = typeof geometry.parseProfile === 'function' && geometry.parseProfile(image.getAttribute('data-rapier-occupancy'));
1450
1465
  value = occupancy || geometry.alphaProfile(image);
@@ -1468,7 +1483,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1468
1483
  for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
1469
1484
  if (exclude.has(image) || !image.complete || !image.naturalWidth || !image.naturalHeight) continue;
1470
1485
  const layout = metadata.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
1471
- if (!layout?.rotate || ['around', 'box', 'behind', 'front'].includes(layout.wrap)) continue;
1486
+ if (!layout?.rotate || ['around', 'beside', 'box', 'behind', 'front'].includes(layout.wrap)) continue;
1472
1487
  const width = image.offsetWidth, height = image.offsetHeight;
1473
1488
  if (!(width > 0 && height > 0)) continue;
1474
1489
  const paragraph = image.closest('p');
@@ -1606,7 +1621,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1606
1621
  const origin = box(root).top, obstacles = [], anchors = new Map(), wrapped = new Set(), placed = [], positionedImages = new Set();
1607
1622
  for (const element of children) {
1608
1623
  const layout = metadata.parseLayoutAttribute(element.getAttribute('data-md-layout'));
1609
- const image = ['around', 'box', 'behind', 'front'].includes(layout?.wrap) && element.tagName === 'P'
1624
+ const image = ['around', 'beside', 'box', 'behind', 'front'].includes(layout?.wrap) && element.tagName === 'P'
1610
1625
  ? element.querySelector('img[data-rapier-image-layout]') : null;
1611
1626
  if (!image?.complete || !image.naturalWidth || !image.naturalHeight || !imageOnly(element, image)) continue;
1612
1627
 
@@ -1652,7 +1667,9 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1652
1667
  obstacle.y + obstacle.height > y - room && obstacle.y < y + rectangle.height + room)
1653
1668
  y = obstacle.y + obstacle.height + room;
1654
1669
  }
1655
- obstacles.push(...geometry.linesSlices(geometry.pictureSlices(profile(image, layout), left, y, rectangle.width, rectangle.height, scale), layout, lines, y, rectangle.height));
1670
+ let slices = geometry.pictureSlices(profile(image, layout), left, y, rectangle.width, rectangle.height, scale);
1671
+ if (layout.wrap === 'beside') slices = geometry.besideSlices(slices, bounds.left + inset, bounds.left + inset + width, left, rectangle.width);
1672
+ obstacles.push(...geometry.linesSlices(slices, layout, lines, y, rectangle.height));
1656
1673
  }
1657
1674
  placed.push({source, image, rectangle, left, y, wrap: layout.wrap,
1658
1675
  visualLeft: left + visualDeltaX, visualTop: y + visualDeltaY, visualWidth: fit.fit.width, visualHeight: fit.fit.height,
@@ -1733,7 +1750,7 @@ function _rapierArtifactLayoutScript(root, nonce = '', {pictures = true} = {}) {
1733
1750
  const dependencies = globalThis.RapierArtifactLayoutDependencies;
1734
1751
  const layoutOf = element => globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-md-layout'));
1735
1752
  const pictureLayoutOf = image => globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
1736
- const wrapping = pictures && ([...root.querySelectorAll('p[data-md-layout]')].some(element => ['around', 'box', 'behind', 'front'].includes(layoutOf(element)?.wrap)) ||
1753
+ const wrapping = pictures && ([...root.querySelectorAll('p[data-md-layout]')].some(element => ['around', 'beside', 'box', 'behind', 'front'].includes(layoutOf(element)?.wrap)) ||
1737
1754
  [...root.querySelectorAll('img[data-rapier-image-layout]')].some(image => pictureLayoutOf(image)?.rotate));
1738
1755
  const typography = _rapierArtifactTypographyScript(root);
1739
1756
  if (!wrapping && !typography) return '';
@@ -309,6 +309,19 @@ export function pictureSlices(profile, x, y, width, height, scale = 1) {
309
309
  return slices;
310
310
  }
311
311
 
312
+ export function besideSlices(slices, columnLeft, columnRight, pictureLeft, pictureWidth) {
313
+ if (!Array.isArray(slices) || !(columnRight > columnLeft) || !(pictureWidth > 0) || !finite(pictureLeft)) return slices;
314
+ const blockLeft = pictureLeft + pictureWidth / 2 <= (columnLeft + columnRight) / 2;
315
+ return slices.map(slice => {
316
+ if (!slice || !finite(slice.x) || !finite(slice.width)) return slice;
317
+ if (blockLeft) {
318
+ const right = slice.x + slice.width;
319
+ return {...slice, x: columnLeft, width: Math.max(0, right - columnLeft)};
320
+ }
321
+ return {...slice, width: Math.max(0, columnRight - slice.x)};
322
+ });
323
+ }
324
+
312
325
  export {slotSequence, columnsFilled};
313
326
  export function slotsForBand(width, obstacles, top, height, minWidth = 40) {
314
327
  if (!finite(width) || width <= 0 || !finite(top) || !finite(height) || height <= 0 ||
@@ -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;
@@ -3,7 +3,7 @@
3
3
  export const fields = new Set(['align', 'width', 'lines', 'wrap', 'x', 'y', 'rotate', 'opacity', 'first', 'indent', 'marker', 'lock', 'hyphens', 'caps', 'hanging', 'before', 'after', 'line']);
4
4
  export const SPACING_FIELDS = Object.freeze(['before', 'after', 'line']);
5
5
  export const alignments = new Set(['left', 'center', 'right', 'justify']);
6
- export const wraps = new Set(['around', 'box', 'behind', 'front']);
6
+ export const wraps = new Set(['around', 'beside', 'box', 'behind', 'front']);
7
7
  const has = (value, key) => Object.hasOwn(value, key);
8
8
  const percent = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100;
9
9
  const lineCount = value => Number.isInteger(value) && value >= 1 && value <= 12;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.2.42",
3
+ "version": "1.2.44",
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",