rapier-markdown-kit 1.2.3 → 1.2.6

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 (41) hide show
  1. package/LICENSE +6 -5
  2. package/README.md +28 -16
  3. package/dist/images/gif.mjs +3 -1
  4. package/dist/images/playback.mjs +6 -4
  5. package/dist/kit/conformance/run.mjs +1 -1
  6. package/dist/kit/ledger/authored-origin.mjs +49 -0
  7. package/dist/kit/ledger/authorship.mjs +18 -9
  8. package/dist/kit/ledger/data.mjs +7 -4
  9. package/dist/kit/ledger/format.mjs +25 -8
  10. package/dist/kit/ledger/history.mjs +31 -5
  11. package/dist/kit/ledger/inverse-source.mjs +111 -49
  12. package/dist/kit/ledger/journal-records.mjs +24 -29
  13. package/dist/kit/ledger/merge.mjs +48 -13
  14. package/dist/kit/ledger/text-merge.mjs +75 -29
  15. package/dist/kit/ledger/transport.mjs +30 -45
  16. package/dist/kit/pdf-vectors.mjs +2 -1
  17. package/dist/kit/print-capitals.mjs +0 -1
  18. package/dist/kit/rapier-ledger.mjs +2 -0
  19. package/dist/kit/render-markdown.mjs +12 -3
  20. package/dist/kit/render-print.mjs +43 -7
  21. package/dist/kit/render-sanitize.mjs +19 -12
  22. package/dist/kit/render-styles.mjs +3 -3
  23. package/dist/kit/render-work.mjs +1 -1
  24. package/dist/kit/render.mjs +6 -41
  25. package/dist/layout/paragraph-fit.mjs +1 -15
  26. package/dist/layout/typography.mjs +2 -1
  27. package/dist/packages/rapier-diff/concerns.mjs +163 -0
  28. package/dist/packages/rapier-diff/diff.mjs +68 -0
  29. package/dist/packages/rapier-diff/index.mjs +4 -744
  30. package/dist/packages/rapier-diff/matching.mjs +157 -0
  31. package/dist/packages/rapier-diff/merge.mjs +226 -0
  32. package/dist/packages/rapier-diff/moves.mjs +130 -0
  33. package/dist/packages/rapier-diff/structure.mjs +50 -0
  34. package/dist/packages/rapier-diff/tokens.mjs +72 -0
  35. package/dist/packages/rapier-diff/walk.mjs +219 -0
  36. package/dist/spec/html-blocks.mjs +3 -3
  37. package/dist/spec/md-layout.mjs +1 -1
  38. package/dist/spec/md-marks.mjs +5 -5
  39. package/dist/spec/md-source.mjs +15 -7
  40. package/package.json +1 -1
  41. package/style.css +11 -11
@@ -2,6 +2,7 @@
2
2
  // Paired source placement, shared by live editing and canonical-history proof.
3
3
  import {_rapierTransformSplices, _rapierValidAuthored, _rapierHistoryEffects} from './journal-records.mjs';
4
4
  import {projectInverseSource} from './inverse-source.mjs';
5
+ import {authoredBasis, authoredPlacement, authoredPlacements} from './authored-origin.mjs';
5
6
  import {sha256} from './hash.mjs';
6
7
  import {canonicalJSON} from './data.mjs';
7
8
 
@@ -74,6 +75,18 @@ export function transposeRecord(base, splices, id, records) {
74
75
  if (next === null) throw new RangeError('canonical_transport_invalid');
75
76
  current = next;
76
77
  }
78
+ // A concurrent act that already removed these same authored units and wrote the
79
+ // same replacement is one physical edit. This act stays in history with no second
80
+ // deletion. Pure insertions are never collapsed: each one is its own unit. An
81
+ // inverse of that act means the replacement is not still in force.
82
+ const sameRows = (left, right) => left.length === right.length && left.every((row, index) =>
83
+ row.pos === right[index].pos && row.removed === right[index].removed && row.inserted === right[index].inserted);
84
+ const matched = crossed.filter(record => sameRows(record.splices, splices));
85
+ const targetsOf = record => record.transaction.sourceTransactionIds || (record.transaction.sourceTransactionId ? [record.transaction.sourceTransactionId] : []);
86
+ const withdrawn = matched.some(record => crossed.some(other => targetsOf(other).includes(record.transaction.id) ||
87
+ other.transaction.reverts === record.transaction.id || other.transaction.reapplies === record.transaction.id));
88
+ if (splices.some(row => row.removed.length) && matched.length && !withdrawn)
89
+ return {splices: [], sourceIndices: [], against: crossed, text: current};
77
90
  const moved = [], sourceIndices = [];
78
91
  for (const [sourceIndex, row] of splices.entries()) {
79
92
  const submitted = _rapierTransformSplices(source, [row]);
@@ -120,49 +133,7 @@ export function transposeSource(base, splices, client, log) {
120
133
  return transposeRecord(base, splices, client, records);
121
134
  }
122
135
 
123
- // The frontier names only causal maxima. Linear history needs one predecessor,
124
- // and concurrent predecessors remain separate until an act observes both.
125
- function authoredFrontier(records, visit = null) {
126
- const dependencies = new Map(), frontier = new Set();
127
- for (const record of records) {
128
- const id = record.transaction.id, basis = record.authored?.basis;
129
- if (visit) visit(record, basis ?? [...frontier].sort());
130
- if (!basis) { dependencies.set(id, [...frontier]); frontier.clear(); }
131
- else {
132
- const pending = [...basis], seen = new Set();
133
- while (pending.length) {
134
- const prior = pending.pop();
135
- if (seen.has(prior)) continue;
136
- if (!dependencies.has(prior)) throw new RangeError('canonical_origin_invalid');
137
- seen.add(prior); frontier.delete(prior); pending.push(...dependencies.get(prior));
138
- }
139
- dependencies.set(id, basis);
140
- }
141
- frontier.add(id);
142
- }
143
- return [...frontier].sort();
144
- }
145
-
146
- export function authoredBasis(records) { return authoredFrontier(records); }
147
-
148
- export function authoredPlacements(base, records) {
149
- const origins = new Map(); let source = base;
150
- authoredFrontier(records, (record, basis) => {
151
- origins.set(record.transaction.id, record.authored ?? authoredPlacement(source, record.splices, basis));
152
- source = _rapierTransformSplices(source, record.splices);
153
- if (source === null) throw new RangeError('canonical_origin_invalid');
154
- });
155
- return origins;
156
- }
157
-
158
- // Immutable authored placement stays on its own act. Basis IDs refer only to this
159
- // canonical history; no source snapshot or duplicate history is stored.
160
- export function authoredPlacement(source, splices, basis) {
161
- const origin = {basis: basis.slice().sort(), source: sha256(source), splices: splices.map(row => ({...row}))};
162
- if (!_rapierValidAuthored(origin) || _rapierTransformSplices(source, splices) === null)
163
- throw new RangeError('canonical_origin_invalid');
164
- return origin;
165
- }
136
+ export {authoredBasis, authoredPlacement, authoredPlacements};
166
137
 
167
138
  // Reconstruct an authored view from the same records, then prove its exact physical
168
139
  // placement. Equality of resulting text alone cannot prove a repeated-text anchor.
@@ -233,6 +204,19 @@ function authoredReplay(base, records, append = null) {
233
204
  if (source === null) throw new RangeError('canonical_origin_invalid');
234
205
  }
235
206
  }
207
+ const coverKey = origin => origin?.splices.some(row => row.removed.length)
208
+ ? canonicalJSON({basis: origin.basis, splices: origin.splices}) : null;
209
+ const coverCounts = new Map();
210
+ for (const [id, origin] of origins) {
211
+ if (effects.references.get(id)?.length) continue;
212
+ const key = coverKey(origin);
213
+ if (key) coverCounts.set(key, (coverCounts.get(key) || 0) + 1);
214
+ }
215
+ // Two inverses can name different acts and still withdraw one physical replacement.
216
+ const sharesCover = id => (effects.references.get(id) || []).some(ref => {
217
+ const key = coverKey(origins.get(ref));
218
+ return !!key && coverCounts.get(key) > 1;
219
+ });
236
220
  const ordered = (ids, selected = null) => {
237
221
  const order = [], known = new Set(ids), waiting = new Map(), dependents = new Map(), ready = [];
238
222
  const rank = id => selected ? Number(selected.has(id)) : Number(origins.get(id).splices.some(row => row.removed.length));
@@ -351,10 +335,11 @@ function authoredReplay(base, records, append = null) {
351
335
  if (aligned.text !== text) throw new RangeError('canonical_origin_invalid');
352
336
  const crossed = aligned.records.slice(basisOrder.length);
353
337
  let moved;
354
- if (sharedInverses && effects.references.get(id)?.length) {
338
+ if (effects.references.get(id)?.length && (sharedInverses || sharesCover(id))) {
355
339
  // Each inverse retains its own targets. First prove the exact intention
356
340
  // in its causal source, then place that choice in the accepted effect
357
- // graph. Another inverse may already suppress the same original bytes.
341
+ // graph. Another inverse may already suppress the same original bytes,
342
+ // including when the two inverses name different authors of one replacement.
358
343
  const authored = projectInverse(basis.text, basis.records, id);
359
344
  if (!same(origin.splices, authored.splices) ||
360
345
  _rapierTransformSplices(basis.text, origin.splices) !== authored.after)
@@ -132,7 +132,8 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
132
132
  root = document.importNode(parsed.documentElement, true);
133
133
  root.style.setProperty('width', originalBox.width + 'px', 'important'); root.style.setProperty('height', originalBox.height + 'px', 'important');
134
134
  mount = document.createElement('div'); mount.style.cssText = `position:fixed;left:${originalBox.left}px;top:${originalBox.top}px;width:${originalBox.width}px;height:${originalBox.height}px;pointer-events:none`;
135
- mount.append(root); document.body.append(mount); temporary.push(mount); await document.fonts.ready;
135
+ // A shadow root keeps the picture's own <style> on the picture, as an <img> does.
136
+ mount.attachShadow({mode: 'open'}).append(root); document.body.append(mount); temporary.push(mount); await document.fonts.ready;
136
137
  textRoots.push(root);
137
138
  }
138
139
  const box = root.getBoundingClientRect(), nodes = [root, ...root.querySelectorAll('*')], sequence = [];
@@ -82,4 +82,3 @@ export function createPrintCapitals(lineMetrics, linesHeight, followingProse) {
82
82
 
83
83
  const capitals = createPrintCapitals(lineMetrics, linesHeight, followingPrintProse);
84
84
  export const applyPrintCapitals = capitals.apply;
85
- export const clearPrintCapitals = capitals.clear;
@@ -10,6 +10,8 @@ export {_rapierTransformSplices, _rapierRecordSplices, _rapierValidLedgerRecord,
10
10
  import {replayHistory, sourceBefore, historyProjection, historyProjections, historyPlaces, selectiveUndo, groupHistoryActs, pictureHistorySplices} from './ledger/history.mjs';
11
11
  export {replayHistory, sourceBefore, historyProjection, historyProjections, historyPlaces, selectiveUndo, groupHistoryActs, pictureHistorySplices};
12
12
 
13
+ import {sha256State} from './ledger/hash.mjs';
14
+ export {sha256State};
13
15
  import {transposeSource} from './ledger/transport.mjs';
14
16
  export {transposeSource};
15
17
 
@@ -325,7 +325,7 @@ function initMarkdownIt() {
325
325
  const provider = env.rapierProviders === false ? null : _rapierProviders.mermaid;
326
326
  const native = globalThis.RapierFlowchart?.parseFlowchart(src).ok === true;
327
327
  const ready = native || !!(provider && provider.status === 'ready' && typeof provider.renderToString === 'function');
328
- if (!ready && env.rapierProviders !== false && !_diagramPromptedOnce) {
328
+ if (!ready && !provider?.request?.() && env.rapierProviders !== false && !_diagramPromptedOnce) {
329
329
  _diagramPromptedOnce = true;
330
330
  try { _rapierUiDiagram.request(); } catch (_) {}
331
331
  }
@@ -366,7 +366,7 @@ function initMarkdownIt() {
366
366
  if (env.rapierPage) return '<span data-rapier-math-source="">' + literal + '</span>';
367
367
  const mathProvider = env.rapierProviders === false ? null : _rapierProviders.math;
368
368
  if (!mathProvider || mathProvider.status !== 'ready' || typeof mathProvider.renderToString !== 'function') {
369
- if (env.rapierProviders !== false && !_mathPromptedOnce) {
369
+ if (!mathProvider?.request?.() && env.rapierProviders !== false && !_mathPromptedOnce) {
370
370
  _mathPromptedOnce = true;
371
371
  try { _rapierUiMath.request(); } catch (_) {}
372
372
  }
@@ -1716,6 +1716,15 @@ function* _rapierResolveSoftBreakTokensSteps(root) {
1716
1716
  return root;
1717
1717
  }
1718
1718
 
1719
+ // Markup parsed in a document of its own, which runs no script and loads nothing (Copy's clipboard tree): one element per
1720
+ // markup, or no boxes where the joined parse did not keep them apart.
1721
+ function _rapierInertParse(markups) {
1722
+ const doc = new DOMParser().parseFromString(markups.map((html, at) => '<div data-rapier-batch="' + at + '">' + html + '</div>').join(''), 'text/html');
1723
+ const boxes = [...doc.body.childNodes];
1724
+ const intact = boxes.length === markups.length && boxes.every((box, at) => box.getAttribute?.('data-rapier-batch') === String(at));
1725
+ return {doc, boxes: intact ? boxes : null};
1726
+ }
1727
+
1719
1728
  function _rapierUnwrapElement(element) {
1720
1729
  while (element.firstChild) element.parentNode.insertBefore(element.firstChild, element);
1721
1730
  element.remove();
@@ -1742,7 +1751,7 @@ function renderWithMathML(source, metadata = {}) {
1742
1751
  return {html, equations};
1743
1752
  }
1744
1753
 
1745
- 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, _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};
1754
+ 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};
1746
1755
  }
1747
1756
  export {RAPIER_TABLE_CAPTION_PREFIX_RE as tableCaptionPrefix, RAPIER_PORTABLE_HIGHLIGHT_HEX as portableHighlightHex, _RAPIER_RTL_SCRIPT as rtlScripts, createMarkdownRenderer};
1748
1757
 
@@ -75,7 +75,7 @@ function _rapierPrintWillFont(markers) {
75
75
  const bounds = [0, -400, 10, 0];
76
76
  const outline = cat(u16(1), bounds.map(u16), u16(2), u16(0), [1, 1, 1], u16(0), u16(10), u16(-5), u16(bounds[1]), u16(bounds[3] - bounds[1]), u16((bounds[1] - bounds[3]) / 2), [0]);
77
77
  const utf16 = text => Array.from(text, ch => u16(ch.charCodeAt(0)));
78
- const names = [[1, 'RapierWill'], [2, 'Regular'], [4, 'RapierWill Regular'], [6, 'RapierWill-Regular']];
78
+ const names = [[1, 'RapierWillCarrier'], [2, 'Regular'], [4, 'RapierWillCarrier Regular'], [6, 'RapierWillCarrier-Regular']];
79
79
  let offset = 0;
80
80
  const tables = {
81
81
  'OS/2': cat(u16(4), u16(advance), u16(400), u16(5), u16(0), u16(650), u16(600), u16(0), u16(75), u16(650), u16(600), u16(0), u16(350),
@@ -449,7 +449,17 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
449
449
  prev = run;
450
450
  }
451
451
  }
452
- return pages.map(page => page.map(({block, carrier, ...run}) => run));
452
+ // Each run carries the tags of its block's structure (outermost first): a PDF reader finds the document's headings, lists and
453
+ // tables from them rather than from positions.
454
+ const ROLES = {TABLE: 'Table', TR: 'TR', TD: 'TD', TH: 'TH', UL: 'L', OL: 'L', LI: 'LI', BLOCKQUOTE: 'BlockQuote', P: 'P', PRE: 'Code', FIGCAPTION: 'Caption',
455
+ H1: 'H1', H2: 'H2', H3: 'H3', H4: 'H4', H5: 'H5', H6: 'H6'};
456
+ const tags = block => {
457
+ const chain = [];
458
+ for (let node = block; node && node !== main; node = node.parentElement)
459
+ if (ROLES[node.tagName] || node === block && node.matches('.rapier-will-carrier')) chain.unshift({node, role: ROLES[node.tagName] || 'P'});
460
+ return chain;
461
+ };
462
+ return pages.map(page => page.map(({block, ...run}) => ({...run, tags: tags(block)})));
453
463
  }
454
464
 
455
465
  function _rapierPrintLinkPages(main, width, height, count) {
@@ -542,6 +552,9 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
542
552
  const descriptor = add('<< /Type /FontDescriptor /FontName /RapierText /Flags 4 /FontBBox [' + font.bounds.join(' ') + '] /ItalicAngle 0 /Ascent 500 /Descent -500 /CapHeight 500 /StemV 80 /FontFile2 ' + fontFile + ' 0 R >>');
543
553
  const descendant = add('<< /Type /Font /Subtype /CIDFontType2 /BaseFont /RapierText /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ' + descriptor + ' 0 R /DW 500 /CIDToGIDMap /Identity >>');
544
554
  const type = add('<< /Type /Font /Subtype /Type0 /BaseFont /RapierText /Encoding /Identity-H /DescendantFonts [' + descendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
555
+ const carrierDescriptor = add('<< /Type /FontDescriptor /FontName /RapierWillCarrier /Flags 4 /FontBBox [' + font.bounds.join(' ') + '] /ItalicAngle 0 /Ascent 500 /Descent -500 /CapHeight 500 /StemV 80 /FontFile2 ' + fontFile + ' 0 R >>');
556
+ const carrierDescendant = add('<< /Type /Font /Subtype /CIDFontType2 /BaseFont /RapierWillCarrier /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ' + carrierDescriptor + ' 0 R /DW 500 /CIDToGIDMap /Identity >>');
557
+ const carrierType = add('<< /Type /Font /Subtype /Type0 /BaseFont /RapierWillCarrier /Encoding /Identity-H /DescendantFonts [' + carrierDescendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
545
558
  const numberFont = pageNumbers ? add('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>') : null;
546
559
  const kids = pages.map(() => { objects.push(null); return objects.length - 1; }), decimal = value => Number(value.toFixed(4));
547
560
  const nativeFonts = new Map();
@@ -571,12 +584,26 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
571
584
  nativeResources.push(entry.name + ' ' + type + ' 0 R');
572
585
  }
573
586
  const sharedImages = new Map();
587
+ // The structure tree (PDF 1.7, 14.7): one element per tagged DOM element, each run marked with its page's next MCID.
588
+ const structs = new Map(), documentElement = {role: 'Document', kids: []}, parentTree = [];
589
+ const structOf = tags => {
590
+ let parent = documentElement;
591
+ for (const {node, role} of tags) {
592
+ let element = structs.get(node);
593
+ if (!element) {element = {role, kids: [], parent}; structs.set(node, element); parent.kids.push(element);}
594
+ parent = element;
595
+ }
596
+ return parent === documentElement ? null : parent;
597
+ };
574
598
  for (let index = 0; index < pages.length; index++) {
575
599
  check();
600
+ const marked = [];
601
+ parentTree.push(marked);
576
602
  const held = pages[index], flate = !!held?.rgb;
577
603
  const picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (flate ? 'FlateDecode' : 'DCTDecode'), flate ? held.rgb : held);
578
604
  pages[index] = null;
579
- let commands = 'q\n' + [decimal(pixelWidth * .75), 0, 0, decimal(pixelHeight * .75), decimal(margins.left), decimal(height - margins.top - pixelHeight * .75)].join(' ') + ' cm\n/Im0 Do\nQ\n';
605
+ // The page's picture is how its tagged text looks, not a figure of its own: an artifact.
606
+ let commands = '/Artifact BMC\nq\n' + [decimal(pixelWidth * .75), 0, 0, decimal(pixelHeight * .75), decimal(margins.left), decimal(height - margins.top - pixelHeight * .75)].join(' ') + ' cm\n/Im0 Do\nQ\nEMC\n';
580
607
  const pictures = ['/Im0 ' + picture + ' 0 R'], states = [];
581
608
  for (const drawing of vectorPages[index] || []) {
582
609
  commands += `q\n.75 0 0 -.75 ${decimal(margins.left)} ${decimal(height - margins.top)} cm\n0 0 ${pixelWidth} ${pixelHeight} re W n\n1 0 0 1 ${decimal(drawing.x)} 0 cm\n`;
@@ -609,6 +636,8 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
609
636
  // Keep one logical Unicode text object, resetting its font before each scalar's direction
610
637
  // can change. Readers that ignore ActualText otherwise discard earlier words in mixed RTL/LTR.
611
638
  // Express width in the font size: some readers ignore horizontal text scaling when joining.
639
+ const element = structOf(run.tags || []);
640
+ if (element) {element.kids.push({page: index, mcid: marked.length}); commands += `/${element.role} << /MCID ${marked.length} >> BDC\n`; marked.push(element);}
612
641
  commands += `q\n/Span << /ActualText <feff${hex(run.text)}> >> BDC\nBT\n100 Tz\n`;
613
642
  for (const part of run.parts || [run]) {
614
643
  if (part.font) {
@@ -623,11 +652,11 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
623
652
  const baseline = decimal(height - margins.top - (part.y + part.height * .8) * .75);
624
653
  for (let letter = 0; letter < characters.length; letter++) {
625
654
  const value = codes.get(characters[letter].codePointAt(0)).toString(16).padStart(4, '0');
626
- commands += `/F1 ${decimal(size)} Tf\n3 Tr\n1 0 0 ${decimal(scaleY)} ${decimal(margins.left + part.x * .75 + slots[letter] * advance)} ${baseline} Tm\n<${value}> Tj\n`;
655
+ commands += `${run.carrier ? '/Fc' : '/F1'} ${decimal(size)} Tf\n3 Tr\n1 0 0 ${decimal(scaleY)} ${decimal(margins.left + part.x * .75 + slots[letter] * advance)} ${baseline} Tm\n<${value}> Tj\n`;
627
656
  }
628
657
  }
629
658
  }
630
- commands += 'ET\nEMC\nQ\n';
659
+ commands += 'ET\nEMC\nQ\n' + (element ? 'EMC\n' : '');
631
660
  }
632
661
  if (numberFont) commands += `BT\n/F2 9 Tf\n0 Tr\n1 0 0 1 ${decimal(width / 2 - String(index + 1).length * 2.5)} ${decimal(margins.bottom / 2)} Tm\n(${index + 1}) Tj\nET\n`;
633
662
  const content = stream('/Filter /FlateDecode', await compressed(encode(commands)));
@@ -638,10 +667,17 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
638
667
  : '/Dest [' + kids[link.target.page] + ' 0 R /XYZ ' + decimal(margins.left + link.target.x * .75) + ' ' + decimal(height - margins.top - link.target.y * .75) + ' null]';
639
668
  return add('<< /Type /Annot /Subtype /Link /Border [0 0 0] /Rect [' + rect.join(' ') + '] ' + target + ' >>');
640
669
  });
641
- put(kids[index], '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ' + decimal(width) + ' ' + decimal(height) + '] /Resources << /XObject << ' + pictures.join(' ') + ' >> /ExtGState << ' + states.join(' ') + ' >> /Font << /F1 ' + type + ' 0 R' + (numberFont ? ' /F2 ' + numberFont + ' 0 R' : '') + ' ' + nativeResources.join(' ') + ' >> >> /Contents ' + content + ' 0 R' + (annotations.length ? ' /Annots [' + annotations.map(id => id + ' 0 R').join(' ') + ']' : '') + ' >>');
670
+ put(kids[index], '<< /Type /Page /Parent 2 0 R /StructParents ' + index + ' /MediaBox [0 0 ' + decimal(width) + ' ' + decimal(height) + '] /Resources << /XObject << ' + pictures.join(' ') + ' >> /ExtGState << ' + states.join(' ') + ' >> /Font << /F1 ' + type + ' 0 R /Fc ' + carrierType + ' 0 R' + (numberFont ? ' /F2 ' + numberFont + ' 0 R' : '') + ' ' + nativeResources.join(' ') + ' >> >> /Contents ' + content + ' 0 R' + (annotations.length ? ' /Annots [' + annotations.map(id => id + ' 0 R').join(' ') + ']' : '') + ' >>');
642
671
  }
643
672
  put(2, '<< /Type /Pages /Count ' + kids.length + ' /Kids [' + kids.map(id => id + ' 0 R').join(' ') + '] >>');
644
- put(1, '<< /Type /Catalog /Pages 2 0 R /Names << /EmbeddedFiles << /Names [<feff' + hex(filename) + '> ' + file + ' 0 R] >> >> /AF [' + file + ' 0 R] >>');
673
+ const tree = add(''), ids = new Map([[documentElement, add('')]]);
674
+ const reserve = element => { for (const kid of element.kids) if (kid.kids) { ids.set(kid, add('')); reserve(kid); } };
675
+ reserve(documentElement);
676
+ for (const [element, id] of ids) put(id, '<< /Type /StructElem /S /' + element.role + ' /P ' + (element.parent ? ids.get(element.parent) : tree) + ' 0 R /K [' +
677
+ element.kids.map(kid => kid.kids ? ids.get(kid) + ' 0 R' : '<< /Type /MCR /Pg ' + kids[kid.page] + ' 0 R /MCID ' + kid.mcid + ' >>').join(' ') + '] >>');
678
+ const parents = add('<< /Nums [' + parentTree.map((marked, index) => index + ' [' + marked.map(element => ids.get(element) + ' 0 R').join(' ') + ']').join(' ') + '] >>');
679
+ put(tree, '<< /Type /StructTreeRoot /K [' + ids.get(documentElement) + ' 0 R] /ParentTree ' + parents + ' 0 R /ParentTreeNextKey ' + parentTree.length + ' >>');
680
+ put(1, '<< /Type /Catalog /Pages 2 0 R /MarkInfo << /Marked true >> /StructTreeRoot ' + tree + ' 0 R /Names << /EmbeddedFiles << /Names [<feff' + hex(filename) + '> ' + file + ' 0 R] >> >> /AF [' + file + ' 0 R] >>');
645
681
  const info = add('<< /Producer (Rapier) /Title <feff' + hex(title || filename.replace(/\.[^.]*$/, '')) + '> >>');
646
682
  const header = encode('%PDF-1.7\n%\u00e2\u00e3\u00cf\u00d3\n'), offsets = new Array(objects.length);
647
683
  let cursor = header.length;
@@ -29,11 +29,6 @@ const RAPIER_SANITIZE_OPTIONS = Object.freeze({
29
29
  FORBID_TAGS: RAPIER_SANITIZE_FORBID_TAGS,
30
30
  FORBID_ATTR: RAPIER_SANITIZE_FORBID_ATTR,
31
31
  }),
32
- snippet: Object.freeze({
33
- ..._rapierSanitizeProfile('html'),
34
- FORBID_TAGS: RAPIER_SANITIZE_FORBID_TAGS,
35
- FORBID_ATTR: Object.freeze([...'style']),
36
- }),
37
32
  diagram: Object.freeze({
38
33
  ..._rapierSanitizeProfile('html', 'svg'),
39
34
  FORBID_TAGS: Object.freeze(['form', 'dialog', 'template', 'iframe', 'script', 'foreignobject']),
@@ -59,6 +54,9 @@ const DIAGRAM_LABEL_SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
59
54
  const DIAGRAM_LABEL_HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
60
55
  const DIAGRAM_LABEL_MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
61
56
 
57
+ // The profile each DOMPurify instance holds (setConfig), shared by every renderer made over it.
58
+ const purifyConfigured = new WeakMap();
59
+
62
60
  function _rapierImageDataAttributeAllowed(tag, attribute, value, verify) {
63
61
  return (tag === 'img' && attribute === 'src' || tag === 'image' && /^(?:href|xlink:href)$/.test(attribute)
64
62
  && /^data:image\/(?:png|jpeg|webp|jxl);base64,/i.test(value)) && verify(value);
@@ -376,6 +374,8 @@ function _rapierInstallSanitizeHooks() {
376
374
  });
377
375
  }
378
376
 
377
+ const purifyOptions = new Map();
378
+
379
379
  function sanitizeRapierHtml(value, context = 'render') {
380
380
  return sanitize(value, context, false);
381
381
  }
@@ -392,24 +392,31 @@ function sanitize(value, context, fragment) {
392
392
  // (the hook) without an id the chrome owns. 'source' is Markdown source HTML, written and never shown: the render
393
393
  // profile's admission, and the block on remote content stays the reader's (a remote picture's URL is the person's words).
394
394
  const selected = context === 'raw' || context === 'export' || context === 'source' || RAPIER_SANITIZE_OPTIONS[context] ? context : 'render';
395
- let options = selected === 'raw' ? { ...RAPIER_SANITIZE_OPTIONS.render, ALLOW_DATA_ATTR: false } : RAPIER_SANITIZE_OPTIONS[selected] || RAPIER_SANITIZE_OPTIONS.render;
396
- if (selected === 'source') options = {...options, ADD_DATA_URI_TAGS: ['image']};
397
- const labels = selected === 'diagram' || selected === 'export';
398
- if (labels) {
395
+ const labels = selected === 'diagram' || selected === 'export', key = selected + (fragment ? ' fragment' : '');
396
+ let options = purifyOptions.get(key);
397
+ if (!options) {
398
+ options = selected === 'raw' ? { ...RAPIER_SANITIZE_OPTIONS.render, ALLOW_DATA_ATTR: false } : RAPIER_SANITIZE_OPTIONS[selected] || RAPIER_SANITIZE_OPTIONS.render;
399
+ if (selected === 'source') options = {...options, ADD_DATA_URI_TAGS: ['image']};
399
400
  // DOMPurify's USE_PROFILES replaces explicit allowlists. Add MathML to the
400
401
  // selected profile without discarding its admitted HTML and SVG vocabulary.
401
- options = {...options, ALLOWED_TAGS: [...new Set([...options.ALLOWED_TAGS, ...RAPIER_SANITIZE_PROFILES.mathTags])],
402
+ if (labels) options = {...options, ALLOWED_TAGS: [...new Set([...options.ALLOWED_TAGS, ...RAPIER_SANITIZE_PROFILES.mathTags])],
402
403
  ALLOWED_ATTR: [...new Set([...options.ALLOWED_ATTR, ...RAPIER_SANITIZE_PROFILES.mathAttributes])], ADD_TAGS: ['foreignobject'], ADD_DATA_URI_TAGS: ['image'],
403
404
  HTML_INTEGRATION_POINTS: {foreignobject: true}, FORBID_TAGS: options.FORBID_TAGS.filter(tag => tag !== 'foreignobject')};
405
+ if (fragment) options = {...options, RETURN_DOM_FRAGMENT: true};
406
+ purifyOptions.set(key, options);
407
+ }
408
+ if (labels) {
404
409
  DOMPurify.addHook('uponSanitizeElement', diagramLabelElement);
405
410
  DOMPurify.addHook('uponSanitizeAttribute', diagramLabelAttribute);
406
411
  DOMPurify.addHook('afterSanitizeAttributes', diagramLabelContained);
407
412
  }
408
- if (fragment) options = {...options, RETURN_DOM_FRAGMENT: true};
413
+ // DOMPurify reparses a per-call configuration on every call (its cache compares its own copy), about 0.45 ms
414
+ // each in Chrome 155. A profile is set once and kept until another profile is asked for; every caller comes here.
415
+ if (purifyConfigured.get(DOMPurify) !== options) { DOMPurify.setConfig(options); purifyConfigured.set(DOMPurify, options); }
409
416
  const previous = _rapierSanitizeRuntime.context;
410
417
  _rapierSanitizeRuntime.context = selected;
411
418
  try {
412
- return DOMPurify.sanitize(value == null ? '' : String(value), options);
419
+ return DOMPurify.sanitize(value == null ? '' : String(value));
413
420
  } finally {
414
421
  if (labels) {
415
422
  DOMPurify.removeHook('uponSanitizeElement', diagramLabelElement);
@@ -103,9 +103,9 @@ function _rapierArtifactStyles(theme, includeFonts, printMode, houseDefaults = f
103
103
  const highlightCss = _rapierStyleText('rapier-highlight-style');
104
104
  parts.push(highlightCss);
105
105
  // A page written for the system theme has no body.light to switch: under a light system
106
- // setting it takes the Standard palette's light values from the sheet's own body.light rule.
107
- const light = theme === 'system' && /html\[data-highlights="standard"\] body\.light\s*\{([^}]*)\}/.exec(highlightCss);
108
- if (light) parts.push('@media (prefers-color-scheme:light){html[data-rapier-theme="system"][data-highlights="standard"] body{' + light[1].trim() + '}}');
106
+ // setting each palette takes its light values from the sheet's own body.light rule.
107
+ if (theme === 'system') for (const [, palette, light] of highlightCss.matchAll(/html\[data-highlights="(standard|accent)"\] body\.light\s*\{([^}]*)\}/g))
108
+ parts.push('@media (prefers-color-scheme:light){html[data-rapier-theme="system"][data-highlights="' + palette + '"] body{' + light.trim() + '}}');
109
109
  if (printMode) parts.push(_rapierArtifactPrintCss());
110
110
  return parts.filter(Boolean).join('\n\n');
111
111
  }
@@ -8,7 +8,7 @@ export function finish(iterator) {
8
8
 
9
9
  export function check(work) {
10
10
  work?.check?.();
11
- if (work?.signal?.aborted) throw Object.assign(new Error('The operation was cancelled.'), {name: 'AbortError', code: 'cancelled'});
11
+ if (work?.signal?.aborted) throw Object.assign(new Error('The operation was canceled.'), {name: 'AbortError', code: 'cancelled'});
12
12
  }
13
13
 
14
14
  export async function pause(work, fraction, label) {
@@ -8,43 +8,6 @@ import {documentLanguage, documentSettingsFonts} from '../spec/document-settings
8
8
  import {createLineMeasurer, linesHeight} from '../layout/model.mjs';
9
9
  import {markPrintCapitals, createPrintCapitals, followingPrintProse} from './print-capitals.mjs';
10
10
  import {freezePublishedInk} from './render-ink.mjs';
11
- // The document's renderer. DOM, codecs and host state are explicit inputs.
12
- // Rapier generates attribute selectors for dark paper. Project those owned rules onto their
13
- // SVG elements before export removes stylesheets; the inert host and the editor share this pass.
14
- export function presentNativeDiagram(drawing) {
15
- const winners=new Map();
16
- for(const style of drawing.querySelectorAll('style')){
17
- const media=/^\s*@media\s*\(\s*prefers-color-scheme\s*:\s*dark\s*\)\s*\{([\s\S]*)\}\s*$/.exec(style.textContent);
18
- if(media)for(const rule of media[1].matchAll(/([^{}]+)\{([^{}]*)\}/g)){
19
- // The browser's CSS sanitizer serializes owned hex colours as rgb()/rgba().
20
- const properties=[...rule[2].matchAll(/(?:^|;)\s*(fill|stroke|color|stop-color)\s*:\s*(#[\da-f]{3,8}|rgba?\([\d.,%/\s]+\)|none)\s*(?:!important)?(?=;|$)/gi)];
21
- for(const selector of rule[1].split(',')){
22
- // Draw's selectors contain only attributes, descendants and immediate children.
23
- if(!/^\s*\[[^\[\]{};]+\](?:\s*(?:>\s*)?\[[^\[\]{};]+\])*\s*$/.test(selector))continue;
24
- const rank=(selector.match(/\[/g)||[]).length;
25
- for(const element of [...(drawing.matches(selector)?[drawing]:[]),...drawing.querySelectorAll(selector)]){
26
- let paints=winners.get(element);if(!paints)winners.set(element,paints=new Map());
27
- for(const [,property,dark] of properties){const light=element.getAttribute(property);
28
- if(light&&(paints.get(property)?.rank??-1)<=rank)paints.set(property,{light,dark,rank});}
29
- }
30
- }
31
- }
32
- style.remove();
33
- }
34
- for(const [element,paints] of winners)for(const [property,{light,dark}] of paints)
35
- element.style.setProperty(property,'light-dark('+light+', '+dark+')','important');
36
- drawing.classList.add('rapier-native-flowchart');drawing.setAttribute('role','img');
37
- drawing.setAttribute('aria-roledescription',drawing.getAttribute('data-rapier-diagram-kind')||'flowchart');
38
- const width=Number(drawing.getAttribute('width')),
39
- sizes=[...drawing.querySelectorAll('text[font-size]')].map(text=>Number(text.getAttribute('font-size'))).filter(size=>Number.isFinite(size)&&size>0),
40
- minimum=sizes.reduce((smallest,size)=>Math.min(smallest,size),Infinity);
41
- if(Number.isFinite(width)&&width>0){
42
- const readable=Math.ceil(width*14/(Number.isFinite(minimum)?minimum:14)*1000)/1000;
43
- drawing.style.width='clamp('+readable+'px, 100%, '+width+'px)';
44
- }
45
- drawing.style.maxWidth='none';
46
- }
47
-
48
11
  // Set the first view once. Subsequent rendering keeps the person's horizontal position.
49
12
  export function positionNativeDiagram(drawing) {
50
13
  const surface=drawing?.closest?.('.diagram-block,.diagram-live-preview');
@@ -67,6 +30,7 @@ export function positionNativeDiagram(drawing) {
67
30
  surface.scrollLeft=Math.max(0,Math.min(surface.scrollWidth-surface.clientWidth,left));
68
31
  }
69
32
 
33
+ // The document's renderer. DOM, codecs and host state are explicit inputs.
70
34
  function createRenderer(runtime) {
71
35
  const {RapierLedgerCarried, RapierPageReturnAddress, RapierTextCodec, _rapierArtifactHighlight, _rapierArtifactPreference, _rapierArtifactStyles, _rapierBlobDataUrl, _rapierBuildInterchangeContext, _rapierDocumentNameIsAdmissible, _rapierLedgerParts, _rapierDrawReadSVGRecipe, _rapierDrawShapeProfileFor, _rapierExportFontCss, _rapierFillDiagram, _rapierFinalizeExportCode, _rapierLanguageClass, _rapierLineStartOffsets, _rapierMarkdownEnvironment, _rapierPortableHtml, _rapierPrepareInterchangeContext, _rapierProjectPortableRoot, _rapierProviders, _rapierSourceCharEscaped, _rapierSourceLineSpan, crypto, document, escapeRapierHtmlText, globalThis, sanitizeRapierHtml} = runtime;
72
36
  let md = runtime.md;
@@ -376,7 +340,7 @@ function _rapierDocxRenderStyles(root) {
376
340
  const surface = into.createElement('div'); surface.className = 'md-render'; into.body.appendChild(surface);
377
341
  const canvas = into.createElement('canvas'); canvas.width = canvas.height = 1;
378
342
  const paint = canvas.getContext('2d'), colours = new Map();
379
- if (!paint) throw new Error('Word text colours could not be read.');
343
+ if (!paint) throw new Error('Word text colors could not be read.');
380
344
  const hex = colour => {
381
345
  if (!colours.has(colour)) {
382
346
  // CSS may resolve an accent to oklch/color-mix. The browser supplies its
@@ -659,11 +623,12 @@ function _rapierSharedResolve(text, ids, images, definitions) {
659
623
  // Authored # characters were encoded. A raw id after ]( can only be a structural
660
624
  // substitution; matching its destination lets nested alt brackets survive unchanged.
661
625
  const inline = new RegExp('(\\]\\([ \\t\\r\\n]*(?:&lt;)?)#' + id + '(?=>?[ \\t\\r\\n]*\\)|>?[ \\t\\r\\n]+["\'(])', 'g');
662
- // A definition may sit in a quote or a list item: its line starts with that container's markers.
663
- const definition = new RegExp('(^[ \\t>]*\\[((?:\\\\.|[^\\]\\\\])+)\\]:[ \\t]*(?:(?:\\r\\n|\\r|\\n)[ \\t>]*)?(?:&lt;)?)#' + id + '(?=>?[ \\t]*$|>?[ \\t]+["\'(])', 'gm');
626
+ // A definition may sit in a quote or a list item, as the writer found it: its line starts with their markers.
627
+ const definition = new RegExp('(^[ \\t>]*(?:(?:[-+*]|\\d{1,9}[.)])[ \\t]+[ \\t>]*)*\\[((?:\\\\.|[^\\]\\\\])+)\\]:[ \\t]*(?:(?:\\r\\n|\\r|\\n)[ \\t>]*)?(?:&lt;)?)#' + id + '(?=>?[ \\t]*$|>?[ \\t]+["\'(])', 'gm');
664
628
  const swapInline = (match, lead, value) => { const found = src(value); return found == null ? match : lead + found; };
665
629
  const swapDefinition = (match, lead, label, value) => {
666
- if (!definitions.has(md.utils.normalizeReference(_rapierSharedDecode(label)))) return match;
630
+ const decoded = _rapierSharedDecode(label), quoted = decoded.replace(/\r?\n[ \t]*(?:>[ \t]*)+/g, ' ');
631
+ if (!definitions.has(md.utils.normalizeReference(decoded)) && !definitions.has(md.utils.normalizeReference(quoted))) return match;
667
632
  const found = src(value);
668
633
  return found == null ? match : lead + found;
669
634
  };
@@ -172,20 +172,6 @@ export function createParagraphFitter() {
172
172
  return {lines, work, cost: totalCost, approximate: false};
173
173
  }
174
174
 
175
- function solve(prepared, width, options, observe) {
176
- const work = solveSteps(prepared, width, options);
177
- try {
178
- let step = work.next();
179
- while (!step.done) {
180
- if (step.value && !observe) return null;
181
- step = work.next(step.value ? observe(step.value) : undefined);
182
- }
183
- return step.value;
184
- } finally { work.return(); }
185
- }
186
- return {prepare, solve, solveSteps};
175
+ return {prepare, solveSteps};
187
176
  }
188
177
 
189
- const fitter = createParagraphFitter();
190
- export const prepareParagraph = fitter.prepare;
191
- export const fitParagraph = fitter.solve;
@@ -3,7 +3,8 @@ import {createPreparationScope, supportsRichStyle, captureFontAuthority} from '.
3
3
  import {createParagraphFitter} from './paragraph-fit.mjs';
4
4
 
5
5
  // Layout projections keep the original text in place. The factory also runs in an exported page.
6
- export function createTypography(fitter = createParagraphFitter()) {
6
+ export function createTypography() {
7
+ const fitter = createParagraphFitter();
7
8
  const mark = 'data-rapier-typography', boundary = mark + '-break', hang = mark + '-hang';
8
9
  const protectedInitial = mark + '-protected-initial';
9
10
  const gapMark = mark + '-gap', fitMark = mark + '-fit';