rapier-markdown-kit 1.2.33 → 1.2.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +15 -9
  2. package/dist/agent/vendor/pretext/NOTICE +2 -2
  3. package/dist/agent/vendor/pretext/SOURCE.json +62 -78
  4. package/dist/agent/vendor/pretext/analysis.js +24 -6
  5. package/dist/agent/vendor/pretext/gecko-line-breaks.js +1 -1
  6. package/dist/agent/vendor/pretext/generated/engine-break-data.js +1 -1
  7. package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +1 -1
  8. package/dist/agent/vendor/pretext/graphemes.js +1 -1
  9. package/dist/agent/vendor/pretext/han-kerning.js +1 -1
  10. package/dist/agent/vendor/pretext/layout.js +14 -11
  11. package/dist/agent/vendor/pretext/line-break.js +170 -195
  12. package/dist/agent/vendor/pretext/line-breaks.js +1 -7
  13. package/dist/agent/vendor/pretext/line-text.js +2 -2
  14. package/dist/agent/vendor/pretext/measurement.js +454 -113
  15. package/dist/agent/vendor/pretext/prepare.js +164 -244
  16. package/dist/agent/vendor/pretext/rich-inline.js +57 -37
  17. package/dist/agent/will.mjs +73 -0
  18. package/dist/kit/ledger/data.mjs +2 -2
  19. package/dist/kit/ledger/format.mjs +86 -42
  20. package/dist/kit/ledger/hash.mjs +43 -2
  21. package/dist/kit/ledger/history.mjs +6 -0
  22. package/dist/kit/ledger/inverse-source.mjs +6 -2
  23. package/dist/kit/ledger/journal-records.mjs +4 -2
  24. package/dist/kit/ledger/merge.mjs +3 -17
  25. package/dist/kit/ledger/text-merge.mjs +28 -2
  26. package/dist/kit/rapier-ledger.mjs +2 -2
  27. package/dist/kit/render-markdown.mjs +133 -10
  28. package/dist/kit/render-print.mjs +3 -3
  29. package/dist/kit/render-sanitize.mjs +66 -23
  30. package/dist/kit/render.mjs +10 -10
  31. package/dist/kit/wiki.mjs +34 -0
  32. package/dist/kit/will.d.mts +3 -0
  33. package/dist/layout/hyphenation.mjs +6 -4
  34. package/dist/layout/line-plan.mjs +96 -0
  35. package/dist/layout/model.mjs +2 -1
  36. package/dist/layout/paragraph-fit.mjs +4 -4
  37. package/dist/layout/prepared-paragraph.mjs +19 -2
  38. package/dist/layout/typography.mjs +57 -63
  39. package/dist/spec/document-fonts.mjs +1 -0
  40. package/dist/spec/document-settings.mjs +1 -0
  41. package/dist/spec/html-blocks.mjs +10 -7
  42. package/dist/spec/md-assets.mjs +52 -12
  43. package/package.json +1 -1
  44. package/style.css +25 -23
  45. package/dist/agent/vendor/pretext/entry-geometry.js +0 -183
@@ -5,6 +5,7 @@ import {sourceView, HistorySource} from './source.mjs';
5
5
  import {textRootParts, rootAfter} from './hash.mjs';
6
6
  import {authoredHistory, authoredPlacements} from './transport.mjs';
7
7
  import {plainData, canonicalJSON} from './data.mjs';
8
+ import {evaluateWill} from '../../agent/will.mjs';
8
9
 
9
10
  const historyFailure = reason => ({ok: false, reason});
10
11
  const historyInteger = value => Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
@@ -323,6 +324,8 @@ export function selectiveUndo(input, actIds, options = {}) {
323
324
  (options.splitSplice !== undefined && typeof options.splitSplice !== 'function')) return historyFailure('act_invalid');
324
325
  const requested = new Set(actIds), available = prepared.byId;
325
326
  if ([...requested].some(id => !available.has(id))) return historyFailure('act_unavailable');
327
+ if (options.actor?.kind === 'agent' && [...requested].some(id => available.get(id).record.transaction.actor.kind !== 'agent'))
328
+ return historyFailure('act_forbidden');
326
329
  const selected = new Set([...requested].filter(id => prepared.effects.active.has(id)));
327
330
  const records = prepared.records;
328
331
  let inverseId = 'planned-inverse'; while (available.has(inverseId)) inverseId += ':';
@@ -359,6 +362,9 @@ export function selectiveUndo(input, actIds, options = {}) {
359
362
  sourceIndices: transported.sourceIndices.get(record.transaction.id)})) : prepared.rows;
360
363
  const result = projectInverseSource(transported ? {...prepared, rows, records: transported.records} : prepared, selected, projectedEffects, options);
361
364
  if (!result.ok) return result;
365
+ if (options.actor?.kind === 'agent' && prepared.metadata.docKind === 'markdown' &&
366
+ evaluateWill(prepared.source, result.splices).outcome !== 'applied')
367
+ return historyFailure('will_refused');
362
368
  return {...result, actIds: [...requested], sourceTransactionIds: [...selected], metadata, afterMetadata,
363
369
  effectChanged, unchanged: result.after === prepared.source && !metadata && !effectChanged};
364
370
  } catch (_) { return historyFailure('history_invalid'); }
@@ -10,7 +10,7 @@ const historyBoundary = (source, at) => Number.isSafeInteger(at) && at >= 0 && !
10
10
  !(at > 0 && at < source.length && (source.charCodeAt(at - 1) & 0xFC00) === 0xD800 &&
11
11
  (source.charCodeAt(at) & 0xFC00) === 0xDC00);
12
12
 
13
- function minimalHistorySplice(before, after, pos = 0) {
13
+ export function minimalHistorySplice(before, after, pos = 0) {
14
14
  let first = 0, a = before.length, b = after.length;
15
15
  while (first < a && first < b && before[first] === after[first]) first++;
16
16
  if (!historyBoundary(before, first) || !historyBoundary(after, first)) first--;
@@ -262,7 +262,11 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
262
262
  const start = boundary(row.pos), end = boundary(row.pos + row.removed.length);
263
263
  let at = start.last, count = Math.max(0, end.first - at);
264
264
  if (!row.removed.length) {
265
- const marker = pieces.slice(start.first, start.last).findIndex(item => item.mark?.inserted.length);
265
+ const run = pieces.slice(start.first, start.last), next = pieces[start.last];
266
+ let before = start.first - 1; while (before >= 0 && !pieces[before].text) before--;
267
+ const leftAct = before >= 0 ? byId.get(pieces[before].owner)?.actId : undefined;
268
+ const sameAct = leftAct !== undefined && run.some(item => item.mark?.actId === leftAct);
269
+ const marker = run.findIndex(item => item.mark?.inserted.length && (next && owns(next, item.mark) || !sameAct));
266
270
  at = marker < 0 ? start.last : start.first + marker; count = 0;
267
271
  }
268
272
  const removed = pieces.slice(at, at + count);
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import {fields, scalar, plainPrototype} from './data.mjs';
3
3
  import {transformText} from './source.mjs';
4
+ import {dataImage} from '../../spec/md-assets.mjs';
4
5
 
5
6
  const _RAPIER_TRANSACTION_ACTOR_LIMIT = 160;
6
7
  const _RAPIER_TRANSACTION_OPERATION_LIMIT = 96;
@@ -175,10 +176,10 @@ function _rapierPictureFinish(record, original, earlier, lookup) {
175
176
  if (!reference || !/^\]\[[a-z0-9-]+\]$/i.test(reference.inserted) || reference.removed === reference.inserted) return false;
176
177
  const definition = rows.find(row => row !== reference), oldLabel = reference.removed.slice(2, -1), newLabel = reference.inserted.slice(2, -1);
177
178
  const body = (text, label) => {
178
- const prefix = '[' + label + ']: data:image/svg+xml;base64,';
179
+ const prefix = '[' + label + ']: ';
179
180
  if (typeof text !== 'string' || !text.startsWith(prefix)) return null;
180
181
  const value = text.slice(prefix.length), end = value.indexOf(' '), bytes = end < 0 ? value : value.slice(0, end), title = end < 0 ? '' : value.slice(end);
181
- return /^[A-Za-z0-9+/]+={0,2}$/.test(bytes) && !/[\r\n]/.test(title) ? title : null;
182
+ return dataImage(bytes)?.codec === 'image/svg+xml' && !/[\r\n]/.test(title) ? title : null;
182
183
  };
183
184
  const title = body(definition.removed, oldLabel);
184
185
  if (title === null || body(definition.inserted, newLabel) !== title) return false;
@@ -292,6 +293,7 @@ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null
292
293
  }
293
294
  }
294
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};
295
297
  nodes.set(tx.id, {active: true, blockers: 0, references: refs, dependents: []}); references.set(tx.id, refs.slice());
296
298
  byId.set(tx.id, record); earlier.push(record);
297
299
  for (const ref of refs) {
@@ -1,9 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import {readLedger, exportLedger, rootAfter} from './format.mjs';
3
3
  import {_rapierTransformSplices, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState} from './journal-records.mjs';
4
- import {mergeText} from './text-merge.mjs';
4
+ import {mergeText, willMergeConflict} from './text-merge.mjs';
5
5
  import {transposeAuthored, authoredPlacements} from './transport.mjs';
6
- import {parseWill, stripOneTerminator} from '../../agent/will.mjs';
7
6
  import {sha256} from './hash.mjs';
8
7
  import {canonicalJSON, fault} from './data.mjs';
9
8
  export function transportInterval(start, end, splices) {
@@ -58,17 +57,6 @@ function checkpointList(read) {
58
57
  }
59
58
  return list;
60
59
  }
61
- function kept(text) {
62
- const will = parseWill(text);
63
- if (will.fault) return {fault: will.fault, text};
64
- return will.regions.filter(region => region.law !== 'edit').map(({law, intent, start, end}) =>
65
- law === 'keep' ? {law, intent, text: text.slice(start, end)} : {law, intent});
66
- }
67
- function appended(from, to) {
68
- const regions = text => parseWill(text).regions.filter(region => region.law === 'append');
69
- const was = regions(from), now = regions(to), core = (text, region) => stripOneTerminator(text.slice(region.start, region.end));
70
- return was.length === now.length && was.every((region, index) => core(to, now[index]).startsWith(core(from, region)));
71
- }
72
60
  function expandRetainedStart(shorter, longer) {
73
61
  if (shorter.ledger.documentAuthority !== longer.ledger.documentAuthority ||
74
62
  shorter.ledger.start.revision <= longer.ledger.start.revision) return null;
@@ -191,10 +179,8 @@ export function merge(first, second) {
191
179
  const local = localIds.get(record.transaction.id);
192
180
  if (local) mappedIds.set(record.transaction.id, local.transaction.id);
193
181
  }
194
- const incomingKept = canonicalJSON(kept(theirs.text));
195
- const keepDiffers = incomingKept !== canonicalJSON(kept(ours.text)) && incomingKept !== canonicalJSON(kept(common.text)) ||
196
- !appended(common.text, theirs.text) && !appended(ours.text, theirs.text);
197
- let conflicted = keepDiffers && unseen.length > 0, incomingSource = common.text;
182
+ const keepDiffers = willMergeConflict(common.text, ours.text, theirs.text);
183
+ let conflicted = keepDiffers, incomingSource = common.text;
198
184
  if (!conflicted) for (const record of incoming) {
199
185
  const beforeSource = incomingSource;
200
186
  incomingSource = _rapierTransformSplices(incomingSource, record.splices);
@@ -1,4 +1,28 @@
1
1
  // SPDX-License-Identifier: MIT
2
+ import {parseWill, stripOneTerminator} from '../../agent/will.mjs';
3
+
4
+ export function willMergeConflict(base, ours, theirs) {
5
+ const snapshot = text => {
6
+ const will = parseWill(text);
7
+ return {fault: will.fault, regions: will.regions.filter(region => region.law !== 'edit').map(region => ({
8
+ law: region.law, intent: region.intent, text: text.slice(region.start, region.end),
9
+ }))};
10
+ };
11
+ const a = snapshot(ours), b = snapshot(theirs);
12
+ if (a.fault || b.fault) return ours !== theirs;
13
+ const kept = value => JSON.stringify(value.regions.map(({law, intent, text}) => ({law, intent, ...(law === 'keep' ? {text} : {})})));
14
+ if (kept(a) !== kept(b)) return true;
15
+ if (base === null) return false;
16
+ const prior = snapshot(base);
17
+ if (prior.fault) return base !== ours || base !== theirs;
18
+ const appended = value => {
19
+ const before = prior.regions.filter(region => region.law === 'append'), after = value.regions.filter(region => region.law === 'append');
20
+ return before.length === after.length && before.every((region, index) =>
21
+ stripOneTerminator(after[index].text).startsWith(stripOneTerminator(region.text)));
22
+ };
23
+ return !appended(a) || !appended(b);
24
+ }
25
+
2
26
  function textFault(code, message) { return Object.assign(new Error(message), {code}); }
3
27
  export function canonicalNote(text) {
4
28
  if (typeof text !== 'string') throw textFault('note_text', 'A note must be text');
@@ -430,8 +454,9 @@ export function mergeText(base, ours, theirs, options = {}) {
430
454
  canonicalNote(ours); canonicalNote(theirs); if (base !== null) canonicalNote(base);
431
455
  const unchanged = value => ({text: value, clean: !value.includes(TEXT_CONFLICT), conflicts: value.includes(TEXT_CONFLICT) ? [{kind: 'existing-conflict', advisory: true}] : []});
432
456
  if (ours === theirs) return unchanged(ours);
433
- if (base === ours) return unchanged(theirs);
434
- if (base === theirs) return unchanged(ours);
457
+ const willConflict = willMergeConflict(base, ours, theirs);
458
+ if (!willConflict && base === ours) return unchanged(theirs);
459
+ if (!willConflict && base === theirs) return unchanged(ours);
435
460
  const conflicts = []; let text = '', a, b;
436
461
  const conflict = (before, a, b, kind) => {
437
462
  const ab = inspectTextConflicts(a), bb = inspectTextConflicts(b);
@@ -468,6 +493,7 @@ export function mergeText(base, ours, theirs, options = {}) {
468
493
  const [units, oursUnits, theirsUnits] = base === null ? [] : [base, ours, theirs].map(textUnits);
469
494
  const declared = base !== null && textDeclarations(units);
470
495
  if (base === null) conflict(null, ours, theirs, 'no-ancestor');
496
+ else if (willConflict) conflict(base, ours, theirs, 'will');
471
497
  else if (textDeclarations(oursUnits) !== declared || textDeclarations(theirsUnits) !== declared) conflict(base, ours, theirs, 'document-bindings');
472
498
  else {
473
499
  const reordered = textPermuted(units, oursUnits, theirsUnits);
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {exportLedger, replaceLedgerText, readLedger, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId} from './ledger/format.mjs';
3
- export {exportLedger, replaceLedgerText, readLedger, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId};
2
+ import {exportLedger, prepareLedgerAsync, replaceLedgerText, readLedger, readLedgerAsync, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId} from './ledger/format.mjs';
3
+ export {exportLedger, prepareLedgerAsync, replaceLedgerText, readLedger, readLedgerAsync, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId};
4
4
  import {authorship, readAuthorship, ledgerFromAuthorship} from './ledger/authorship.mjs';
5
5
  export {authorship, readAuthorship, ledgerFromAuthorship};
6
6
  import {merge, transportInterval, transportTouchedInterval} from './ledger/merge.mjs';
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import {htmlPlaceholder, mountHtmlBlocks, materializeObjects, objectPosterLabel, htmlImageRecord, htmlLayoutEdit, htmlModeEdit, mergeHtmlObjectBlocks} from '../spec/html-blocks.mjs';
3
3
  import {finish, finishAsync, cloneTree, cleanTree, serializeTree, htmlParts, admitHtml, htmlBoundary, pause, check} from './render-work.mjs';
4
+ import {installWikiParser, wikiAddress} from './wiki.mjs';
4
5
  const RAPIER_TABLE_CAPTION_PREFIX_RE = /^(?:Table:|:) /;
5
6
  const RAPIER_PORTABLE_HIGHLIGHT_HEX = Object.freeze({
6
7
  green: '#35c57a', red: '#f05a62', blue: '#4f91f7', yellow: '#e6b91e', purple: '#9b6dea',
@@ -9,6 +10,51 @@ const _RAPIER_RTL_SCRIPT = /[\p{Script=Hebrew}\p{Script=Arabic}\p{Script=Syriac}
9
10
  function createMarkdownRenderer(runtime) {
10
11
  const {Node, _rapierPrefixPortableAnchors, RAPIER_HIGHLIGHT_COLOR_BY_MARKER, RAPIER_MARKDOWN_SPEC, RAPIER_RENDERED_HEADING_SELECTOR, _rapierApplyMarkdownSpec, _rapierChromeOwnsId, _rapierCodeHtml, _rapierDeriveDarkColor, _rapierDormantHeadings, _rapierEmbedAssetSource, _rapierHeadingSlugBase, _rapierNextHeadingSlug, _rapierHighlightAdmitted, _rapierInstallMarkdownMath, _rapierMarkdownPreview, _rapierPlainLayout, _rapierProviders, _rapierRemoteContent, _rapierRemoteImagePlaceholder, _rapierRemoteSubresourceOrigin, _rapierRenderedAnchorOriginalIds, _rapierSourceCharEscaped, _rapierSplitOpeningFrontmatter, _rapierUiDiagram, _rapierUiMath, document, escapeRapierHtmlText, globalThis, sanitizeRapierHtml, window} = runtime;
11
12
  let md = runtime.md;
13
+ let wikiTimer = null, wikiAttempt = 0;
14
+ const wikiBusy = new WeakSet();
15
+ function scheduleWikiEmbeds() {
16
+ if (!globalThis.RapierPlatform || !document || wikiTimer !== null) return;
17
+ wikiTimer = setTimeout(async () => {
18
+ wikiTimer = null;
19
+ const original = globalThis.RapierOriginalFolderCore?.currentOriginal();
20
+ if (!original) { if (++wikiAttempt < 8) scheduleWikiEmbeds(); return; }
21
+ wikiAttempt = 0;
22
+ for (const node of [...document.querySelectorAll('#editor-blocks .rapier-source-token--wiki-embed')].slice(0, 32)) {
23
+ if (wikiBusy.has(node) || node.hasAttribute('data-rapier-wiki-ready')) continue;
24
+ wikiBusy.add(node);
25
+ try {
26
+ const link = node.querySelector('a'), href = link?.getAttribute('href');
27
+ if (!href) continue;
28
+ const before = node.innerHTML;
29
+ const embedded = await original.embed(href);
30
+ if (!node.isConnected || node.innerHTML !== before || globalThis.RapierOriginalFolderCore?.currentOriginal() !== original) continue;
31
+ const holder = _rapierRenderSemanticRoot(embedded.text, {docKind: 'markdown'}, {wikiEmbedding: true});
32
+ let followed = 0;
33
+ for (const anchor of holder.querySelectorAll('a[href]')) {
34
+ const address = anchor.getAttribute('href');
35
+ if (/^(?:[a-z][a-z0-9+.-]*:|\/)/i.test(address)) continue;
36
+ try {
37
+ if (++followed > 64) throw new Error('link limit');
38
+ anchor.setAttribute('href', await original.addressFrom(embedded.displayPath, address));
39
+ } catch (_) { anchor.replaceWith(...anchor.childNodes); }
40
+ }
41
+ if (!node.isConnected || node.innerHTML !== before || globalThis.RapierOriginalFolderCore?.currentOriginal() !== original) continue;
42
+ for (const image of holder.querySelectorAll('img,[data-rapier-remote-src]'))
43
+ image.replaceWith(document.createTextNode(image.getAttribute('alt') || image.textContent || ''));
44
+ for (const block of [...holder.querySelectorAll('p,h1,h2,h3,h4,h5,h6,div,pre,blockquote,ul,ol,li,table,tr,td,th')].reverse())
45
+ block.replaceWith(...block.childNodes, document.createElement('br'));
46
+ node.replaceChildren(...holder.childNodes);
47
+ node.setAttribute('data-rapier-visible', encodeURIComponent(node.textContent));
48
+ node.setAttribute('data-rapier-wiki-ready', '');
49
+ node.contentEditable = 'false';
50
+ } catch (_) { }
51
+ finally { wikiBusy.delete(node); }
52
+ }
53
+ }, 125);
54
+ }
55
+ window?.addEventListener?.('rapier:original-file-ready', scheduleWikiEmbeds);
56
+ const wikiMode = options => options.wikiEmbedding ? 'links' : options.wikiLinks !== undefined ? options.wikiLinks
57
+ : globalThis.RapierOriginalFolderCore?.currentOriginal?.() ? 'label' : false;
12
58
  function initMarkdownIt() {
13
59
  if (typeof window.markdownit !== 'function') throw new Error('markdownit');
14
60
  if (typeof _rapierMarkdownPreview === 'function') globalThis.rapierRenderPreview = _rapierMarkdownPreview;
@@ -82,6 +128,21 @@ function initMarkdownIt() {
82
128
  const sourceToken = (source, visible, className) =>
83
129
  '<span class="rapier-source-token ' + className + '" data-rapier-source="' +
84
130
  encodeURIComponent(source) + '" data-rapier-visible="' + encodeURIComponent(visible) + '">' + md.utils.escapeHtml(visible) + '</span>';
131
+ installWikiParser(md, (token, env) => {
132
+ const {target, label, embed} = token.meta, href = wikiAddress(target), esc = md.utils.escapeHtml;
133
+ if (env.rapierWikiLinks === 'label') return esc(label);
134
+ if (embed && /\.(?:png|jpe?g|gif|webp|jxl)(?:#.*)?$/i.test(target)) {
135
+ if (!env.rapierWikiEmbedding && globalThis.RapierPlatform?.files.relativeCandidate?.(href) && globalThis.RapierEmbeddedImages?.imageHtml)
136
+ return globalThis.RapierEmbeddedImages.imageHtml(null, label, '', null, '', label, token.content, href, env.rapierImagePresentation !== false);
137
+ return '<span data-rapier-remote-src="' + esc(href) + '" data-rapier-remote-alt="' + esc(label) +
138
+ '" data-rapier-image-source="' + esc(encodeURIComponent(token.content)) + '">' + esc(label) + '</span>';
139
+ }
140
+ const note = embed && !env.rapierWikiEmbedding;
141
+ if (note) scheduleWikiEmbeds();
142
+ return '<span class="rapier-source-token rapier-source-token--wiki' + (note ? '-embed' : '') +
143
+ '" data-rapier-source="' + esc(encodeURIComponent(token.content)) + '" data-rapier-visible="' + esc(encodeURIComponent(label)) +
144
+ '"><a href="' + esc(href) + '">' + esc(label) + '</a></span>';
145
+ });
85
146
  md.renderer.rules.emoji = (tokens, idx) => {
86
147
  const token = tokens[idx];
87
148
  const source = token.markup ? ':' + token.markup + ':' : token.content;
@@ -114,7 +175,9 @@ function initMarkdownIt() {
114
175
 
115
176
  md.renderer.rules.image = (tokens, idx, opts, env, self) => {
116
177
  const token = tokens[idx];
117
- const src = token.attrGet('src') || '';
178
+ const written = token.attrGet('src') || '';
179
+ const hosted = globalThis.RapierEmbedPictureBase?.(written) ?? null;
180
+ const src = hosted || written;
118
181
  const title = token.attrGet('title') || '';
119
182
  const rawAlt = String(token.content ?? token.attrGet('alt') ?? '');
120
183
  const size = _rapierImageAltSize(rawAlt);
@@ -140,7 +203,7 @@ function initMarkdownIt() {
140
203
  + (token.meta?.mdImage?.source ? ' data-rapier-image-source="' + escape(encodeURIComponent(token.meta.mdImage.source)) + '"' : '')
141
204
  + '>' + escape(alt || src) + '</span>';
142
205
  }
143
- if (!env.rapierPage && !_rapierRemoteContent.allowed && _rapierRemoteSubresourceOrigin(src)) {
206
+ if (!hosted && !env.rapierPage && !_rapierRemoteContent.allowed && _rapierRemoteSubresourceOrigin(src)) {
144
207
  const html = _rapierRemoteImagePlaceholder(src, alt, title, size, layout, rawAlt);
145
208
  return html.replace('<span ', '<span data-rapier-markdown-image="" ' +
146
209
  (token.meta?.mdImage?.source ? 'data-rapier-image-source="' + md.utils.escapeHtml(encodeURIComponent(token.meta.mdImage.source)) + '" ' : ''));
@@ -402,8 +465,20 @@ function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
402
465
 
403
466
  const markdown = _rapierSplitOpeningFrontmatter(canonical).body;
404
467
  const env = {rapierImagePresentation: options.imagePresentation === true, ...(options.page ? {rapierPage: true} : {}), ...(options.providers === false ? {rapierProviders: false} : {})};
468
+ if (options.wikiEmbedding) env.rapierWikiEmbedding = true;
469
+ const wikiLinks = wikiMode(options);
470
+ if (wikiLinks) env.rapierWikiLinks = wikiLinks;
405
471
  if (options.mathEquations) env.rapierMathEquations = options.mathEquations;
406
- let rendered = md
472
+ const nesting = md && options.maxNesting > 0 ? options.maxNesting : 0;
473
+ let rendered;
474
+ if (nesting || md && options.closeDetails) {
475
+ const saved = md.options.maxNesting;
476
+ if (nesting) md.options.maxNesting = nesting;
477
+ try {
478
+ const tokens = md.parse(markdown, env);
479
+ rendered = md.renderer.render(options.closeDetails ? _rapierCloseOpenDetails(tokens) : tokens, md.options, env);
480
+ } finally { md.options.maxNesting = saved; }
481
+ } else rendered = md
407
482
  ? md.render(markdown, env)
408
483
  : '<pre><code>' + escapeRapierHtmlText(markdown) + '</code></pre>';
409
484
  if (md && env.footnotes && Array.isArray(env.footnotes.list) && env.footnotes.list.length) {
@@ -435,8 +510,16 @@ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {})
435
510
  }
436
511
  const markdown = _rapierSplitOpeningFrontmatter(canonical).body;
437
512
  const fallbackEnv = options.page ? {rapierPage: true} : {};
438
- let parsed = options.parsed || (runtime.parseAsync ? await runtime.parseAsync(markdown, options) : {tokens: md.parse(markdown, fallbackEnv), env: fallbackEnv});
439
- let {tokens, env} = parsed, labels = env.footnotes?.list?.map((item, index) => String(item?.label || ('inline-' + (index + 1))));
513
+ const wikiLinks = markdown.includes('[[') && wikiMode(options);
514
+ if (wikiLinks) fallbackEnv.rapierWikiLinks = wikiLinks;
515
+ const nesting = options.maxNesting > 0 ? options.maxNesting : 0, saved = md.options.maxNesting;
516
+ if (nesting) md.options.maxNesting = nesting;
517
+ let parsed;
518
+ try { parsed = !wikiLinks && options.parsed || (runtime.parseAsync && !wikiLinks && !nesting ? await runtime.parseAsync(markdown, options) : {tokens: md.parse(markdown, fallbackEnv), env: fallbackEnv}); }
519
+ 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))));
440
523
  env = {...env, rapierImagePresentation: options.imagePresentation === true};
441
524
  await pause(work, .3);
442
525
  const inert = document.createElement('template').content.ownerDocument;
@@ -727,6 +810,38 @@ function _rapierImageSizeAttributes(size, marker, geometry, automatic = true) {
727
810
  return attributes + (style ? ' style="' + style + '"' : '');
728
811
  }
729
812
 
813
+ function _rapierCloseOpenDetails(tokens) {
814
+ const opens = /^<details[\t\n\f\r />]/i, closes = /^<\/details[\t\n\f\r ]*>$/i;
815
+ const tags = token => {
816
+ const found = [];
817
+ 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);
819
+ else if (token.type === 'inline') for (const child of token.children || []) if (child.type === 'html_inline') found.push(child.content);
820
+ return found;
821
+ };
822
+ const after = new Map(), add = (index, count) => after.set(index, (after.get(index) || 0) + count);
823
+ const waiting = [];
824
+ for (let index = 0; index < tokens.length; index++) {
825
+ const token = tokens[index];
826
+ let depth = 0;
827
+ for (const tag of tags(token)) {
828
+ if (opens.test(tag)) { if (token.type === 'html_block') waiting.push(index); else depth++; }
829
+ else if (closes.test(tag)) { if (depth) depth--; else waiting.pop(); }
830
+ }
831
+ if (depth) add(index + 1 < tokens.length && tokens[index + 1].nesting === -1 ? index + 1 : index, depth);
832
+ }
833
+ for (const index of waiting) add(index, 1);
834
+ if (!after.size) return tokens;
835
+ const out = [];
836
+ tokens.forEach((token, index) => {
837
+ out.push(token);
838
+ const count = after.get(index);
839
+ if (count) out.push({type: 'html_block', tag: '', attrs: null, map: null, nesting: 0, level: token.level || 0, children: null,
840
+ content: '</details>'.repeat(count) + '\n', markup: '', info: '', meta: null, block: true, hidden: false});
841
+ });
842
+ return out;
843
+ }
844
+
730
845
  function _rapierRawHtmlFragment(source, rewrite, parser) {
731
846
  const state = new parser.inline.State(source, parser, {}, []);
732
847
  const htmlRule = parser.inline.ruler.__rules__.find(rule => rule.name === 'html_inline').fn;
@@ -792,7 +907,9 @@ function* _rapierAssignHeadingSlugsSteps(root, selector = RAPIER_RENDERED_HEADIN
792
907
  if (!headingSet.has(element) && element.id) used[element.id] = 0;
793
908
  }
794
909
  for (const heading of headings) { yield;
795
- heading.id = _rapierNextHeadingSlug(_rapierHeadingSlugBase(heading.textContent), used);
910
+ const base = _rapierHeadingSlugBase(heading.textContent);
911
+ const owned = typeof _rapierChromeOwnsId === 'function' && _rapierChromeOwnsId(base);
912
+ heading.id = _rapierNextHeadingSlug(owned ? 'heading-' + base : base, used);
796
913
  }
797
914
  }
798
915
 
@@ -1622,11 +1739,17 @@ async function _rapierPlainTextFileAsync(context) {
1622
1739
  async function _rapierCloneRootAsync(root, work) { return finishAsync(cloneTree(root), work); }
1623
1740
 
1624
1741
  function _rapierSourceTokenValue(node) {
1625
- if (!node?.hasAttribute('data-rapier-source') || !node.hasAttribute('data-rapier-visible') ||
1626
- Array.from(node.childNodes).some(child => child.nodeType !== Node.TEXT_NODE)) return null;
1742
+ if (!node?.hasAttribute('data-rapier-source') || !node.hasAttribute('data-rapier-visible')) return null;
1627
1743
  try {
1744
+ const source = decodeURIComponent(node.getAttribute('data-rapier-source'));
1745
+ if (node.classList.contains('rapier-source-token--wiki') || node.classList.contains('rapier-source-token--wiki-embed')) {
1746
+ const token = md.parseInline(source, {rapierWikiLinks: 'links'})[0]?.children;
1747
+ if (token?.length !== 1 || token[0].type !== 'rapier_wiki') return null;
1748
+ if (!node.hasAttribute('data-rapier-wiki-ready') && (node.children.length !== 1 || node.firstElementChild.tagName !== 'A' ||
1749
+ node.firstElementChild.getAttribute('href') !== wikiAddress(token[0].meta.target))) return null;
1750
+ } else if (Array.from(node.childNodes).some(child => child.nodeType !== Node.TEXT_NODE)) return null;
1628
1751
  return node.textContent === decodeURIComponent(node.getAttribute('data-rapier-visible'))
1629
- ? decodeURIComponent(node.getAttribute('data-rapier-source')) : null;
1752
+ ? source : null;
1630
1753
  } catch (_) { return null; }
1631
1754
  }
1632
1755
 
@@ -1634,7 +1757,7 @@ function _rapierResolveSoftBreakTokens(root) { return finish(_rapierResolveSoftB
1634
1757
 
1635
1758
  function* _rapierResolveSoftBreakTokensSteps(root) {
1636
1759
  for (const token of root.querySelectorAll('.rapier-source-token--softbreak')) { yield;
1637
- if (_rapierSourceTokenValue(token) === '\n') token.replaceWith(document.createTextNode(' '));
1760
+ if (_rapierSourceTokenValue(token) === '\n') token.replaceWith(document.createTextNode(token.closest('pre') ? '\n' : ' '));
1638
1761
  else token.replaceWith(...token.childNodes);
1639
1762
  }
1640
1763
  return root;
@@ -3,7 +3,7 @@ import {collectPdfVectors} from './pdf-vectors.mjs';
3
3
  import {collectPdfGlyphs} from './pdf-glyphs.mjs';
4
4
  import {pdfFontSources, waitForFonts} from './export-fonts.mjs';
5
5
  import {apply as applyTypography} from '../layout/typography.mjs';
6
- import {documentLanguage, pageSizeOf} from '../spec/document-settings.mjs';
6
+ import {documentLanguage, pageSizeOf, paperForLanguage} from '../spec/document-settings.mjs';
7
7
  import {applyPrintCapitals} from './print-capitals.mjs';
8
8
  import {stripOneTerminator as _rapierWillStripOneTerminator} from '../agent/will.mjs';
9
9
  import {quotedWillSpan} from '../spec/md-marks.mjs';
@@ -220,7 +220,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
220
220
  if (current?.() === false) refuse('document_changed');
221
221
  };
222
222
  if (!root?.ownerDocument || typeof capture !== 'function') refuse('pdf_render_unavailable');
223
- const paper = settings?.papersize || (templatePage ? pageSizeOf(settings) : {width: 11906, height: 16838});
223
+ const paper = settings?.papersize || (templatePage ? pageSizeOf(settings) : paperForLanguage(root.ownerDocument.defaultView?.navigator?.language));
224
224
  const width = paper.width / 20, height = paper.height / 20;
225
225
  const margins = Object.fromEntries(['top', 'right', 'bottom', 'left'].map(side =>
226
226
  [side, settings?.geometry ? settings.geometry[side] / 20 : (side === 'top' ? 12 : 14) * 72 / 25.4]));
@@ -841,7 +841,7 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
841
841
  const characters = fallbackCharacters(part, run.carrier); if (!characters.length) continue;
842
842
  const {slots, count} = _rapierPrintSlots(part.text, part.rtl);
843
843
  const advance = part.width * .75 / count;
844
- const baseline = decimal(height - margins.top - (part.y + part.height * .8) * .75);
844
+ const baseline = decimal(height - margins.top - (part.baseline ?? part.y + part.height * .8) * .75);
845
845
  for (let letter = 0; letter < characters.length; letter++) {
846
846
  const size = Math.max(.1, advance * 2 * [...characters[letter].replace(/\n/g, '')].length), scaleY = Math.max(.1, part.height * .75) / size;
847
847
  const value = (codes.get(characters[letter]) || codes.get(characters[letter].codePointAt(0))).toString(16).padStart(4, '0');
@@ -49,6 +49,41 @@ const DIAGRAM_LABEL_CSS = new Set(('color fill stroke background-color font-fami
49
49
  + 'text-decoration-thickness text-underline-offset align-items align-content justify-content justify-items flex-direction flex-wrap '
50
50
  + 'width height min-width min-height max-width max-height margin margin-top margin-right margin-bottom margin-left '
51
51
  + 'padding padding-top padding-right padding-bottom padding-left').split(' '));
52
+ const DOCUMENT_STYLE_PROPERTIES = new Set([...DIAGRAM_LABEL_CSS, ...('background display visibility opacity '
53
+ + 'border border-top border-right border-bottom border-left border-color border-style border-width border-collapse border-spacing '
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 '
56
+ + 'stroke-width stroke-opacity stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit '
57
+ + 'fill-opacity fill-rule clip-rule clip-path filter stop-color stop-opacity text-anchor dominant-baseline alignment-baseline '
58
+ + 'paint-order vector-effect shape-rendering color-interpolation color-interpolation-filters').split(' ')]);
59
+
60
+ function cleanDocumentStyle(tag, value, context, classes = '') {
61
+ if (context === 'raw' && tag === 'span') return typography.style(typography.parseStyle(value));
62
+ const names = classes.split(/\s+/), diagram = tag === 'svg' && names.includes('pele') &&
63
+ (context === 'diagram' || ['render', 'export', 'source'].includes(context) && names.includes('rapier-diagram'));
64
+ const kept = [];
65
+ let start = 0, quote = '', depth = 0;
66
+ const take = end => {
67
+ const part = value.slice(start, end), colon = part.indexOf(':');
68
+ if (colon < 0) return;
69
+ const property = part.slice(0, colon).trim().toLowerCase(), body = part.slice(colon + 1).trim();
70
+ const diagramProperty = diagram && /^--(?:_[a-z0-9_-]+|pele-[a-z0-9-]+)$/.test(property);
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;
73
+ const withoutLocal = body.replace(/\burl\(\s*(['"]?)#[a-z0-9_.:-]+\1\s*\)/gi, '');
74
+ if (/(?:url|image|image-set|-webkit-image-set|element|paint|attr|expression)\s*\(/i.test(withoutLocal)) return;
75
+ kept.push(part.trim());
76
+ };
77
+ for (let at = 0; at <= value.length; at++) {
78
+ const ch = value[at];
79
+ if (quote) { if (ch === '\\') at++; else if (ch === quote) quote = ''; }
80
+ else if (ch === '"' || ch === "'") quote = ch;
81
+ else if (ch === '(') depth++;
82
+ else if (ch === ')') { if (--depth < 0) return ''; }
83
+ else if ((ch === ';' || at === value.length) && !depth) { take(at); start = at + 1; }
84
+ }
85
+ return quote || depth ? '' : kept.join(';');
86
+ }
52
87
  const DIAGRAM_LABEL_SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
53
88
  const DIAGRAM_LABEL_HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
54
89
  const DIAGRAM_LABEL_MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
@@ -160,7 +195,8 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
160
195
  if (!_rapierImageDataAttributeAllowed(tag, key, compact, safeRasterDataUrl)) continue;
161
196
  } else if (!uri.test(compact)) continue;
162
197
  }
163
- attribs[name] = selected === 'raw' && tag === 'span' && key === 'style' ? typography.style(typography.parseStyle(value)) : value;
198
+ const cleaned = key === 'style' ? cleanDocumentStyle(tag, value, selected, input.class || '') : value;
199
+ if (key !== 'style' || cleaned) attribs[name] = cleaned;
164
200
  }
165
201
  if ((tag === 'a' || tag === 'area') && attribs.target?.trim().toLowerCase() === '_blank') attribs.rel = 'noopener noreferrer';
166
202
  return {tagName, attribs};
@@ -254,6 +290,23 @@ function diagramLabelContained(node) {
254
290
  }
255
291
 
256
292
  function _rapierInstallSanitizeHooks() {
293
+ _rapierInstallDocumentSanitizeHooks();
294
+ DOMPurify.addHook('uponSanitizeElement', node => {
295
+ if (_rapierSanitizeRuntime.context !== 'paste' || !node || !/^(?:P|LI|H[1-6]|SPAN)$/.test(node.nodeName)) return;
296
+ node.removeAttribute('data-rapier-word-level');
297
+ node.removeAttribute('data-rapier-word-marker');
298
+ const style = node.getAttribute('style');
299
+ if (!style || !/mso-list/i.test(style)) return;
300
+ const spec = globalThis.RapierMarkdownSpec;
301
+ if (!spec) return;
302
+ const {wordListLevel, wordListIsMarker} = spec;
303
+ const level = /^(?:P|LI|H[1-6])$/.test(node.nodeName) ? wordListLevel(style) : 0;
304
+ if (level) node.setAttribute('data-rapier-word-level', String(level));
305
+ else if (node.nodeName === 'SPAN' && wordListIsMarker(style)) node.setAttribute('data-rapier-word-marker', '1');
306
+ });
307
+ }
308
+
309
+ function _rapierInstallDocumentSanitizeHooks() {
257
310
  DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
258
311
  if (!/^(?:src|href|xlink:href)$/i.test(data.attrName || '')) return;
259
312
  const compact = String(data.attrValue || '').replace(/[\u0000-\u0020\u00a0]+/g, '');
@@ -269,7 +322,7 @@ function _rapierInstallSanitizeHooks() {
269
322
  if (_rapierRemoteContent.allowed || !/^(?:paste|render|raw|export)$/.test(_rapierSanitizeRuntime.context)) return;
270
323
  if (!node || node.nodeName !== 'IMG' || node.hasAttribute('data-rapier-remote-src')) return;
271
324
  const src = node.getAttribute('src') || '';
272
- if (!src || !_rapierRemoteSubresourceOrigin(src)) return;
325
+ if (!src || !_rapierRemoteSubresourceOrigin(src) || globalThis.RapierEmbedPictureBase?.vouched?.has(src)) return;
273
326
  node._rapierHeldBack = true;
274
327
  node.setAttribute('data-rapier-remote-src', src);
275
328
  const alt = node.getAttribute('alt');
@@ -286,39 +339,22 @@ function _rapierInstallSanitizeHooks() {
286
339
  node.setAttribute('data-rapier-remote-src', href);
287
340
  }
288
341
  });
289
- DOMPurify.addHook('uponSanitizeElement', node => {
290
- if (_rapierSanitizeRuntime.context !== 'paste' || !node || !/^(?:P|LI|H[1-6]|SPAN)$/.test(node.nodeName)) return;
291
- node.removeAttribute('data-rapier-word-level');
292
- node.removeAttribute('data-rapier-word-marker');
293
- const style = node.getAttribute('style');
294
- if (!style || !/mso-list/i.test(style)) return;
295
- const spec = globalThis.RapierMarkdownSpec;
296
- if (!spec) return;
297
- const {wordListLevel, wordListIsMarker} = spec;
298
- const level = /^(?:P|LI|H[1-6])$/.test(node.nodeName) ? wordListLevel(style) : 0;
299
- if (level) node.setAttribute('data-rapier-word-level', String(level));
300
- else if (node.nodeName === 'SPAN' && wordListIsMarker(style)) node.setAttribute('data-rapier-word-marker', '1');
301
- });
302
342
  DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
303
343
  const value = String(data.attrValue == null ? '' : data.attrValue);
304
- if (_rapierSanitizeRuntime.context === 'raw' && node?.nodeName === 'SPAN' && data.attrName?.toLowerCase() === 'style') {
305
- data.attrValue = typography.style(typography.parseStyle(value));
344
+ if (data.attrName?.toLowerCase() === 'style') {
345
+ data.attrValue = cleanDocumentStyle(node?.localName?.toLowerCase(), value, _rapierSanitizeRuntime.context, node?.getAttribute('class') || '');
306
346
  data.keepAttr = !!data.attrValue;
307
347
  return;
308
348
  }
309
349
  if (String(data.attrName || '').toLowerCase() === 'id' && _rapierChromeOwnsId(value)) { data.keepAttr = false; return; }
310
350
  if (_rapierRemoteContent.allowed || _rapierSanitizeRuntime.context === 'source') return;
311
- if (String(data.attrName || '').toLowerCase() === 'style') {
312
- const cleaned = _rapierStyleWithoutRemoteUrls(value);
313
- if (cleaned !== value) data.attrValue = cleaned;
314
- return;
315
- }
316
351
  const attribute = String(data.attrName || '').toLowerCase();
317
352
  if (attribute === 'alt' || attribute === 'title') return;
318
353
  if (/^data-rapier-remote-(?:src|alt|title)$/.test(attribute) && node && node._rapierHeldBack === true) { data.forceKeepAttr = true; return; }
319
354
  if (attribute.startsWith('data-')) return;
320
355
  const element = String(node && node.nodeName || '').toUpperCase();
321
356
  if (attribute === 'href' && (element === 'A' || element === 'AREA')) return;
357
+ if (attribute === 'src' && element === 'IMG' && globalThis.RapierEmbedPictureBase?.vouched?.has(value)) return;
322
358
  if (_rapierCssPresentationIsRemote(attribute, value) ||
323
359
  value.split(/[\s,]+/).some(token => _rapierRemoteSubresourceOrigin(token))) {
324
360
  data.keepAttr = false;
@@ -414,6 +450,13 @@ function _rapierLinkDestinationToConfirm(href, text = '') {
414
450
  }
415
451
 
416
452
  function _rapierSafeRasterDataUrl(value) {
453
+ if (/^data:image\/svg\+xml,/i.test(value)) {
454
+ try {
455
+ const assets = globalThis.RapierImageAssets;
456
+ return assets.dataImage(value)?.codec === 'image/svg+xml' &&
457
+ _rapierVerifyRasterBytes(assets.decodeDataImageBytes(value).bytes, 'image/svg+xml');
458
+ } catch (_) { return false; }
459
+ }
417
460
  const match = RAPIER_RASTER_DATA_URL_RE.exec(value);
418
461
  if (!match) return false;
419
462
  try {
@@ -504,7 +547,7 @@ function _rapierVerifyRasterBytes(bytes, mime) {
504
547
  return false;
505
548
  }
506
549
 
507
- return {_rapierVerifyRasterBytes, _rapierInstallSanitizeHooks, sanitizeRapierHtml,
550
+ return {_rapierVerifyRasterBytes, _rapierInstallSanitizeHooks, _rapierInstallDocumentSanitizeHooks, sanitizeRapierHtml,
508
551
  sanitizeRapierFragment: typeof DOMPurify?.sanitize === 'function' ? sanitizeRapierFragment : undefined,
509
552
  diagramLabelStyle, escapeRapierHtmlText, _rapierChromeOwnsId, _rapierLinkDestinationToConfirm, _rapierSafeRasterDataUrl, _rapierCssPresentationIsRemote, _rapierRemoteSubresourceOrigin, _rapierStyleWithoutRemoteUrls, _rapierStylesheetWithoutRemoteUrls, _rapierDropRemoteRules};
510
553
  }