rapier-markdown-kit 1.1.92 → 1.1.94

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.
@@ -21,27 +21,33 @@ const K = new Uint32Array([
21
21
  0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
22
22
  0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,
23
23
  ]);
24
- const rotate = (x, n) => (x >>> n) | (x << (32 - n));
24
+ // The rounds run on plain 32-bit locals: no array is made per block, so a megabyte (a painting's text) hashes about five times
25
+ // faster than through destructured arrays, with the same digest.
26
+ const W = new Int32Array(64);
25
27
  export function sha256(text) {
26
- const bytes = new TextEncoder().encode(text);
27
- const padded = new Uint8Array(Math.ceil((bytes.length + 9) / 64) * 64);
28
- padded.set(bytes); padded[bytes.length] = 0x80;
29
- const data = new DataView(padded.buffer), bits = bytes.length * 8;
30
- data.setUint32(padded.length - 8, Math.floor(bits / 0x100000000)); data.setUint32(padded.length - 4, bits >>> 0);
31
- const h = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]), w = new Uint32Array(64);
32
- for (let offset = 0; offset < padded.length; offset += 64) {
33
- for (let i = 0; i < 16; i++) w[i] = data.getUint32(offset + i * 4);
28
+ const bytes = new TextEncoder().encode(text), length = bytes.length;
29
+ const padded = new Uint8Array(Math.ceil((length + 9) / 64) * 64), end = padded.length;
30
+ padded.set(bytes); padded[length] = 0x80;
31
+ const high = Math.floor(length * 8 / 0x100000000), low = (length * 8) >>> 0;
32
+ padded[end - 8] = high >>> 24; padded[end - 7] = high >>> 16; padded[end - 6] = high >>> 8; padded[end - 5] = high;
33
+ padded[end - 4] = low >>> 24; padded[end - 3] = low >>> 16; padded[end - 2] = low >>> 8; padded[end - 1] = low;
34
+ let h0 = 0x6a09e667 | 0, h1 = 0xbb67ae85 | 0, h2 = 0x3c6ef372 | 0, h3 = 0xa54ff53a | 0, h4 = 0x510e527f | 0, h5 = 0x9b05688c | 0, h6 = 0x1f83d9ab | 0, h7 = 0x5be0cd19 | 0;
35
+ const w = W;
36
+ for (let offset = 0; offset < end; offset += 64) {
37
+ for (let i = 0, j = offset; i < 16; i++, j += 4) w[i] = padded[j] << 24 | padded[j + 1] << 16 | padded[j + 2] << 8 | padded[j + 3];
34
38
  for (let i = 16; i < 64; i++) {
35
39
  const x = w[i - 15], y = w[i - 2];
36
- w[i] = w[i - 16] + (rotate(x, 7) ^ rotate(x, 18) ^ (x >>> 3)) + w[i - 7] + (rotate(y, 17) ^ rotate(y, 19) ^ (y >>> 10));
40
+ w[i] = (w[i - 16] + ((x >>> 7 | x << 25) ^ (x >>> 18 | x << 14) ^ x >>> 3) + w[i - 7] + ((y >>> 17 | y << 15) ^ (y >>> 19 | y << 13) ^ y >>> 10)) | 0;
37
41
  }
38
- let [a,b,c,d,e,f,g,k] = h;
42
+ let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, k = h7;
39
43
  for (let i = 0; i < 64; i++) {
40
- const t1 = (k + (rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0;
41
- const t2 = ((rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
42
- k = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
44
+ const t1 = (k + ((e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7)) + (e & f ^ ~e & g) + K[i] + w[i]) | 0;
45
+ const t2 = (((a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10)) + (a & b ^ a & c ^ b & c)) | 0;
46
+ k = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0;
43
47
  }
44
- for (const [i, value] of [a,b,c,d,e,f,g,k].entries()) h[i] += value;
48
+ h0 = (h0 + a) | 0; h1 = (h1 + b) | 0; h2 = (h2 + c) | 0; h3 = (h3 + d) | 0; h4 = (h4 + e) | 0; h5 = (h5 + f) | 0; h6 = (h6 + g) | 0; h7 = (h7 + k) | 0;
45
49
  }
46
- return [...h].map(x => x.toString(16).padStart(8, '0')).join('');
50
+ let out = '';
51
+ for (const x of [h0, h1, h2, h3, h4, h5, h6, h7]) out += (x >>> 0).toString(16).padStart(8, '0');
52
+ return out;
47
53
  }
@@ -673,9 +673,9 @@ function _rapierAssignHeadingSlugs(root, selector, reserveExisting) { return fin
673
673
  function* _rapierAssignHeadingSlugsSteps(root, selector = RAPIER_RENDERED_HEADING_SELECTOR, reserveExisting = true) {
674
674
  if (!root || !root.querySelectorAll) return;
675
675
  const used = Object.create(null);
676
- const headings = Array.from(root.querySelectorAll(selector)).concat(_rapierDormantHeadings(root, selector));
676
+ const headings = _rapierDormantHeadings(root, selector, true);
677
677
  const headingSet = new Set(headings);
678
- if (reserveExisting) for (const element of (root.isConnected ? document : root).querySelectorAll('[id]')) { yield;
678
+ if (reserveExisting) for (const element of _rapierDormantHeadings(root.isConnected ? document : root, '[id]', true)) { yield;
679
679
  if (!headingSet.has(element) && element.id) used[element.id] = 0;
680
680
  }
681
681
  for (const heading of headings) { yield;
@@ -687,8 +687,8 @@ function _rapierDisambiguateRenderedAnchors(root) { return finish(_rapierDisambi
687
687
 
688
688
  function* _rapierDisambiguateRenderedAnchorsSteps(root) {
689
689
  if (!root || !root.querySelectorAll) return;
690
- const headings = new Set(root.querySelectorAll(RAPIER_RENDERED_HEADING_SELECTOR));
691
- const candidates = Array.from(root.querySelectorAll('[id]'))
690
+ const headings = new Set(_rapierDormantHeadings(root, RAPIER_RENDERED_HEADING_SELECTOR, true));
691
+ const candidates = _rapierDormantHeadings(root, '[id]', true)
692
692
  .filter(element => !headings.has(element));
693
693
  for (const element of candidates) { yield;
694
694
  const original = _rapierRenderedAnchorOriginalIds.get(element);
@@ -718,7 +718,7 @@ function* _rapierDisambiguateRenderedAnchorsSteps(root) {
718
718
  const add = (map, key, link) => { if (!map.has(key)) map.set(key, new Set()); map.get(key).add(link); };
719
719
  for (const selector of ['.footnote-ref a[href^="#"]', '.footnote-backref[href^="#"]']) {
720
720
  const index = {hash: new Map(), label: new Map(), order: new Map()};
721
- for (const link of root.querySelectorAll(selector)) {
721
+ for (const link of _rapierDormantHeadings(root, selector, true)) {
722
722
  index.order.set(link, index.order.size);
723
723
  add(index.hash, _rapierDecodeHash(link.getAttribute('href')), link);
724
724
  add(index.label, link.getAttribute('data-footnote-label'), link);
@@ -1022,6 +1022,11 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1022
1022
  const root = yield* cloneTree(semanticRoot), nativeElements = new Set();
1023
1023
  // A source soft break is a reading space, never the editor's invisible token.
1024
1024
  yield* _rapierResolveSoftBreakTokensSteps(root);
1025
+ // Word consumes these source-bearing islands as pictures with TeX alternatives. Keep
1026
+ // their SVG through this projection; the normal export sanitizer still owns admission.
1027
+ if (opts.keepMath) for (const math of root.querySelectorAll('.math-rendered[data-math-src],.math-placeholder,[data-rapier-math-source]')) {
1028
+ for (const element of [math, ...math.querySelectorAll('*')]) nativeElements.add(element);
1029
+ }
1025
1030
  if (opts.keepDiagrams) for (const figure of root.querySelectorAll('.diagram-block[data-diagram-native]')) {
1026
1031
  const svg = figure.querySelector('.diagram-cache > svg.rapier-native-flowchart');
1027
1032
  if (figure.getAttribute('data-diagram-state') === 'ready' && svg) {
@@ -1049,6 +1054,13 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1049
1054
  }
1050
1055
 
1051
1056
  for (const wrapper of root.querySelectorAll('.math-display-wrap')) { yield;
1057
+ if (opts.keepMath && wrapper.querySelector('.math-rendered,.math-placeholder,[data-rapier-math-source]')) {
1058
+ const paragraph = document.createElement('p');
1059
+ paragraph.setAttribute('data-md-align', 'center');
1060
+ while (wrapper.firstChild) paragraph.appendChild(wrapper.firstChild);
1061
+ wrapper.replaceWith(paragraph);
1062
+ continue;
1063
+ }
1052
1064
  const source = _rapierMathSource(wrapper);
1053
1065
  const p = document.createElement('p');
1054
1066
  const code = document.createElement('code');
@@ -1057,6 +1069,7 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1057
1069
  wrapper.replaceWith(p);
1058
1070
  }
1059
1071
  for (const math of root.querySelectorAll('.math-rendered,.math-placeholder')) { yield;
1072
+ if (nativeElements.has(math)) continue;
1060
1073
  const code = document.createElement('code');
1061
1074
  code.textContent = _rapierMathSource(math) || '[equation]';
1062
1075
  math.replaceWith(code);
@@ -247,6 +247,7 @@ async function _rapierWritePrintPdf({root, css, canonical, filename, title, sett
247
247
  'max-width': 'none', 'min-height': '0', padding: '0', margin: '0', overflow: 'visible',
248
248
  'column-width': bodyWidth + 'px', 'column-gap': '0', 'column-count': 'auto', 'column-fill': 'auto'}))
249
249
  main.style.setProperty(name, value, 'important');
250
+ globalThis.RapierMath?.fit(main);
250
251
  const count = Math.max(1, Math.ceil((main.scrollWidth - 1) / bodyWidth));
251
252
  if (!Number.isSafeInteger(count)) refuse('pdf_page_geometry_invalid');
252
253
  const textPages = await _rapierPrintTextPages(main, bodyWidth, count, check);
@@ -310,18 +311,34 @@ async function _rapierWritePrintPdf({root, css, canonical, filename, title, sett
310
311
  // its line and page boundaries keeps a PDF search on the page that carries those words.
311
312
  async function _rapierPrintTextPages(main, pageWidth, count, check) {
312
313
  const into = main.ownerDocument, origin = main.getBoundingClientRect();
313
- const pages = Array.from({length: count}, () => []), walker = into.createTreeWalker(main, 4);
314
+ const pages = Array.from({length: count}, () => []);
315
+ // The renderer's outlines have no text ranges. Read each equation's exact TeX
316
+ // alternative at its SVG box, and reject its subtree so titles cannot duplicate it.
317
+ const walker = into.createTreeWalker(main, 5, {acceptNode(node) {
318
+ if (node.parentElement?.closest('.math-rendered svg[aria-label]')) return 2;
319
+ return node.nodeType === 3 || node.matches('.math-rendered svg[aria-label]') ? 1 : 3;
320
+ }});
314
321
  const range = into.createRange();
322
+ const pageOf = x => Math.max(0, Math.min(count - 1, Math.floor((x - origin.left + .01) / pageWidth)));
315
323
  let nodes = 0, operations = 0;
316
324
  for (let node; (node = walker.nextNode());) {
317
325
  if (++nodes % 128 === 0) { await new Promise(resolve => setTimeout(resolve, 0)); check(); }
318
- const style = into.defaultView.getComputedStyle(node.parentElement);
326
+ const math = node.nodeType === 1;
327
+ const style = into.defaultView.getComputedStyle(math ? node : node.parentElement);
319
328
  if (style.visibility === 'hidden' || style.visibility === 'collapse') continue;
329
+ if (math) {
330
+ const rect = node.getBoundingClientRect(), text = node.getAttribute('aria-label');
331
+ if (rect.width > 0 && rect.height > 0 && text) {
332
+ const page = pageOf(rect.left);
333
+ pages[page].push({text, x: rect.left - origin.left - page * pageWidth, y: rect.top - origin.top,
334
+ width: rect.width, height: rect.height, math: true});
335
+ }
336
+ continue;
337
+ }
320
338
  const preserve = /pre|break-spaces/.test(style.whiteSpace);
321
339
  const block = node.parentElement.closest('p,h1,h2,h3,h4,h5,h6,li,pre,td,th,figcaption,blockquote,.rapier-will-carrier') || node.parentElement;
322
340
  const carrier = node.parentElement.closest('.rapier-will-carrier');
323
341
  const value = node.data;
324
- const pageOf = x => Math.max(0, Math.min(count - 1, Math.floor((x - origin.left + .01) / pageWidth)));
325
342
  const visit = (start, end) => {
326
343
  if (++operations % 128 === 0) check();
327
344
  range.setStart(node, start); range.setEnd(node, end);
@@ -340,7 +357,7 @@ async function _rapierPrintTextPages(main, pageWidth, count, check) {
340
357
  const right = Math.max(...rects.map(rect => rect.right)) - origin.left - page * pageWidth;
341
358
  const bottom = Math.max(...rects.map(rect => rect.bottom)) - origin.top;
342
359
  const last = pages[page].at(-1);
343
- if (last && last.block === block && last.carrier === carrier && Math.abs(last.y - y) < Math.max(2, Math.min(last.height, bottom - y) * .2)) {
360
+ if (last && !last.math && last.block === block && last.carrier === carrier && Math.abs(last.y - y) < Math.max(2, Math.min(last.height, bottom - y) * .2)) {
344
361
  last.text = preserve ? last.text + text : (last.text + text).replace(/[ \t\r\n\f]+/g, ' ');
345
362
  last.width = Math.max(last.x + last.width, right) - Math.min(last.x, x);
346
363
  last.height = Math.max(last.y + last.height, bottom) - Math.min(last.y, y);
@@ -404,7 +421,9 @@ async function writeRasterPdf({pages, textPages, canonical, filename, title, fon
404
421
  const value = characters.map(character => codes.get(character.codePointAt(0)).toString(16).padStart(4, '0')).join('');
405
422
  const size = Math.max(.1, run.height * .75);
406
423
  const scale = run.width * .75 / (characters.length * size * .5) * 100;
424
+ if (run.math) commands += '/Span << /ActualText <feff' + hex(run.text) + '> >> BDC\n';
407
425
  commands += `BT\n/F1 ${decimal(size)} Tf\n${decimal(scale)} Tz\n3 Tr\n1 0 0 1 ${decimal(margins.left + run.x * .75)} ${decimal(height - margins.top - (run.y + run.height * .8) * .75)} Tm\n<${value}> Tj\nET\n`;
426
+ if (run.math) commands += 'EMC\n';
408
427
  }
409
428
  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`;
410
429
  const content = stream('/Filter /FlateDecode', await compressed(encode(commands)));
@@ -423,16 +423,32 @@ function _rapierCssPresentationIsRemote(property, value) {
423
423
  return declaration.length ? _rapierDropRemoteDeclarations(declaration) : false;
424
424
  }
425
425
 
426
+ // A reference with no scheme and no host of its own ("photo.png", "#top", each word of a class) resolves on the base's scheme and
427
+ // host whatever its path, so its answer is the base's, found once for each base and page: a long document's render asked
428
+ // this of every token of every attribute, a URL built each time.
429
+ const relativeReference = {base: null, here: null, origin: ''};
426
430
  function _rapierRemoteSubresourceOrigin(value) {
427
431
  const raw = String(value == null ? '' : value)
428
432
  .replace(/[\u0009\u000a\u000d]+/g, '').replace(/^[\u0000-\u0020]+|[\u0000-\u0020]+$/g, '');
429
433
  if (!raw) return '';
434
+ if (!/^[a-z][a-z0-9+.\-]*:/i.test(raw) && !/^[\\/]{2}/.test(raw)) {
435
+ const base = document.baseURI, here = location.protocol.toLowerCase() + '//' + location.host;
436
+ if (relativeReference.base !== base || relativeReference.here !== here) {
437
+ relativeReference.origin = _rapierRemoteSubresourceOriginOf('a', base, here);
438
+ relativeReference.base = base; relativeReference.here = here;
439
+ }
440
+ return relativeReference.origin;
441
+ }
442
+ return _rapierRemoteSubresourceOriginOf(raw, document.baseURI, location.protocol.toLowerCase() + '//' + location.host);
443
+ }
444
+
445
+ function _rapierRemoteSubresourceOriginOf(raw, base, here) {
430
446
  let url;
431
- try { url = new URL(raw, document.baseURI); } catch (_) { return ''; }
447
+ try { url = new URL(raw, base); } catch (_) { return ''; }
432
448
  const host = String(url.host || '');
433
449
  if (!host) return '';
434
450
  const place = String(url.protocol || '').toLowerCase() + '//' + host;
435
- return place === location.protocol.toLowerCase() + '//' + location.host ? '' : place;
451
+ return place === here ? '' : place;
436
452
  }
437
453
 
438
454
  function _rapierStyleWithoutRemoteUrls(value) {
@@ -56,7 +56,7 @@ function _rapierDocumentPrintCss(hostSelector) {
56
56
  ${list('.md-render pre','.artifact-flat')}{white-space:pre-wrap!important;overflow-wrap:anywhere!important;word-break:break-word!important;overflow:visible!important;max-height:none!important}
57
57
  ${p('.artifact-flat')}{margin:0;font:400 12pt/1.55 var(--md-font-sans);tab-size:2}
58
58
  ${p('.artifact-flat--code')}{font:400 10pt/1.5 var(--md-font-mono)}
59
- ${p('.table-scroll-wrap')}{overflow:visible!important;padding:0!important}
59
+ ${list('.table-scroll-wrap','.math-display-wrap')}{overflow:visible!important;padding:0!important}
60
60
  ${list('.table-scroll-wrap table','.md-render table')}{width:100%!important;min-width:0!important;max-width:100%!important;table-layout:auto}
61
61
  ${list('.md-render th','.md-render td')}{overflow-wrap:anywhere;word-break:normal}
62
62
  ${p('.md-render thead')}{display:table-header-group}
@@ -48,7 +48,7 @@ async function _rapierBuildArtifact(options, providedContext) {
48
48
  if (opts.kind === 'standalone') {
49
49
  if (context.semanticRoot.querySelector('.math-placeholder'))
50
50
  throw new Error('Web page not written; the math plug-in is required to include its equations');
51
- // MathJax represents invalid TeX as a static error SVG. A source-only span means
51
+ // Invalid TeX renders as a static error SVG. A source-only span means
52
52
  // rendering itself failed, so publishing it would silently omit the equation.
53
53
  if (context.semanticRoot.querySelector('span[data-rapier-math-source]'))
54
54
  throw new Error('Web page not written; a required equation could not be rendered');
@@ -375,13 +375,13 @@ export function slotsForBand(width, obstacles, top, height, minWidth = 40) {
375
375
 
376
376
  // A picture moves a block's words only where its outline (with its standoff) meets them. The block is laid as it lies with no
377
377
  // picture beside it, balanced and aligned in the whole column; when no line of words there meets an obstacle, that is its
378
- // layout. Otherwise its lines flow around the outline. A block set from the side its lines start on, and not balanced, lies
379
- // the same both ways whenever the outline misses its words, so it is laid once.
378
+ // layout. Otherwise its lines flow around the outline. A short heading can fit beside the outline in a slot narrower
379
+ // than minWidth, so start-aligned, unbalanced words need the same contact check.
380
380
  export function flowLines(flow, width, top, obstacles, lineHeight, minWidth, direction = 'ltr', balance = 0, align = 'start') {
381
381
  const plan = linePlan(flow, width, top, obstacles, lineHeight, minWidth, direction, balance, slotsForBand, layoutNextRichInlineLineRange, materializeRichInlineLineRange);
382
382
  const side = align === 'center' ? 'center' : align === 'left' || align === 'right' ? align
383
383
  : (align === 'end') === (direction === 'rtl') ? 'left' : 'right';
384
- if (!plan || !obstacles?.length || !balance && side === (direction === 'rtl' ? 'right' : 'left')) return plan;
384
+ if (!plan || !obstacles?.length) return plan;
385
385
  const free = linePlan(flow, width, top, [], lineHeight, minWidth, direction, balance, slotsForBand, layoutNextRichInlineLineRange, materializeRichInlineLineRange);
386
386
  if (!free) return plan;
387
387
  const meets = free.lines.some(line => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.1.92",
3
+ "version": "1.1.94",
4
4
  "description": "Read, render and write Self-contained Markdown: one .md file that carries its pictures, drawings and layout, and that any Markdown app can read. HTML renderer, stylesheet, text layout, Will/1 agent permissions, and an edit history with authorship and offline merge. MIT, no dependencies.",
5
5
  "keywords": [
6
6
  "markdown",
package/style.css CHANGED
@@ -467,6 +467,8 @@
467
467
  -webkit-overflow-scrolling:touch;
468
468
  text-align:center;
469
469
  }
470
+ .md-render .math-display-wrap:has(svg[data-rapier-math]){overflow:visible}
471
+ .md-render svg[data-rapier-math]{max-width:100%;height:auto}
470
472
  .md-render .math-display-wrap>math{
471
473
  display:inline-block;
472
474
  width:auto;