rapier-markdown-kit 1.2.10 → 1.2.12

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.
@@ -121,9 +121,10 @@ function initMarkdownIt() {
121
121
  const alt = suffix && semanticAlt.endsWith(suffix) ? semanticAlt.slice(0, -suffix.length) : semanticAlt;
122
122
  const layout = token.meta?.mdLayout?.imageOnly ? token.meta.mdLayout.marker : '';
123
123
  const asset = globalThis.RapierImageAssets.dataImage(src), reference = token.meta?.mdImage?.reference;
124
+ const originalRelative = globalThis.RapierPlatform?.files.relativeCandidate?.(src) || globalThis.RapierSiblingPictures?.has(src);
124
125
  if (!env.rapierPage && typeof globalThis.RapierEmbeddedImages?.imageHtml === 'function' &&
125
- (_rapierEmbedAssetSource?.(src) || asset && (reference || asset.codec === 'image/jxl' || asset.codec === 'image/gif' || asset.codec === 'image/svg+xml'))) return globalThis.RapierEmbeddedImages.imageHtml(
126
- reference, alt, title, size, layout, rawAlt, token.meta?.mdImage?.source, src, env.rapierImagePresentation !== false);
126
+ (originalRelative || _rapierEmbedAssetSource?.(src) || asset && (reference || asset.codec === 'image/jxl' || asset.codec === 'image/gif' || asset.codec === 'image/svg+xml'))) return globalThis.RapierEmbeddedImages.imageHtml(
127
+ originalRelative ? null : reference, alt, title, size, layout, rawAlt, token.meta?.mdImage?.source, src, env.rapierImagePresentation !== false);
127
128
  const drawingAlt = typeof rawAlt === 'string' && alt === '' && asset?.codec === 'image/svg+xml' ? globalThis.RapierImageAssets.drawingImageAlt(src) : null;
128
129
  const shownAlt = drawingAlt ?? alt;
129
130
  let loadable = false;
@@ -901,14 +902,14 @@ function* _rapierDecorateJumpListsSteps(root) {
901
902
  if (isWayfinder) {
902
903
  list.classList.add('rapier-jump-list');
903
904
  const label = list.previousElementSibling;
904
- if (label && /^jump\s+to:?$/i.test(String(label.textContent || '').trim())) {
905
+ if (label && /^(?:jump\s+to|contents|table\s+of\s+contents):?$/i.test(String(label.textContent || '').trim())) {
905
906
  label.classList.add('rapier-jump-label');
906
907
  }
907
908
  }
908
909
  }
909
910
 
910
911
  const first = root.firstElementChild, only = first && !first.nextElementSibling ? first : null;
911
- if (only && only.tagName === 'P' && /^jump\s+to:?$/i.test(String(only.textContent || '').trim())) {
912
+ if (only && only.tagName === 'P' && /^(?:jump\s+to|contents|table\s+of\s+contents):?$/i.test(String(only.textContent || '').trim())) {
912
913
  only.classList.add('rapier-jump-label-candidate');
913
914
  }
914
915
  }
@@ -1176,7 +1177,7 @@ function* _rapierProjectPortableNodesSteps(semanticRoot, options) {
1176
1177
  math.replaceWith(code);
1177
1178
  }
1178
1179
 
1179
- yield* _rapierFlattenPortableDetailsSteps(root);
1180
+ if (!keepLayoutData) yield* _rapierFlattenPortableDetailsSteps(root);
1180
1181
 
1181
1182
  for (const section of root.querySelectorAll('section.footnotes')) { yield;
1182
1183
  for (const node of section.querySelectorAll('.footnotes-sep')) { yield;node.remove();}
@@ -1227,8 +1228,10 @@ function* _rapierProjectPortableNodesSteps(semanticRoot, options) {
1227
1228
 
1228
1229
  const typed = globalThis.RapierMarkdownSpec.typography, typography = typed.nodeStyle(element);
1229
1230
 
1231
+ if (keepLayoutData && element.tagName === 'MARK' && !element.style.backgroundColor && !element.hasAttribute('data-rapier-highlight'))
1232
+ element.setAttribute('data-rapier-highlight', 'default');
1230
1233
  const highlightHex = element.tagName === 'MARK'
1231
- ? (RAPIER_PORTABLE_HIGHLIGHT_HEX[String(element.getAttribute('data-rapier-highlight') || '').toLowerCase()] || RAPIER_PORTABLE_HIGHLIGHT_HEX.yellow)
1234
+ ? (element.style.backgroundColor || RAPIER_PORTABLE_HIGHLIGHT_HEX[String(element.getAttribute('data-rapier-highlight') || '').toLowerCase()] || RAPIER_PORTABLE_HIGHLIGHT_HEX.yellow)
1232
1235
  : null;
1233
1236
 
1234
1237
  const isTableCaption = element.tagName === 'P' && element.classList.contains('rapier-table-caption');
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import {collectPdfVectors} from './pdf-vectors.mjs';
3
3
  import {collectPdfGlyphs} from './pdf-glyphs.mjs';
4
- import {fontRequests, waitForFonts} from './export-fonts.mjs';
4
+ import {pdfFontSources, waitForFonts} from './export-fonts.mjs';
5
5
  import {apply as applyTypography} from '../layout/typography.mjs';
6
6
  import {documentLanguage} from '../spec/document-settings.mjs';
7
7
  import {applyPrintCapitals} from './print-capitals.mjs';
@@ -264,17 +264,28 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
264
264
  ink?.(main);
265
265
  const count = Math.max(1, Math.ceil((main.scrollWidth - 1) / bodyWidth));
266
266
  if (!Number.isSafeInteger(count)) refuse('pdf_page_geometry_invalid');
267
- const customFamilies = new Set(fontCss ? fontRequests(main, {fontCss, css, settings}).map(face => face.family.toLowerCase()) : []);
268
- nativeGlyphs = await collectPdfGlyphs(main, {width: bodyWidth, check,
269
- faces: globalThis.RapierDocxFonts?.DOCX_FONTS?.filter(face => !customFamilies.has(face.family.toLowerCase()))});
270
- const textPages = await _rapierPrintTextPages(main, bodyWidth, count, check, undefined, nativeGlyphs.nodes);
267
+ const customFonts = pdfFontSources(fontCss), customFamilies = new Set(customFonts.map(face => face.family.toLowerCase()));
268
+ const glyphOptions = {width: bodyWidth, check,
269
+ prepareFont: globalThis.RapierExportFonts?.preparePdfFont && ((bytes, text, style, face) =>
270
+ globalThis.RapierExportFonts.preparePdfFont(bytes, text, style, face, {subset: globalThis.RapierFontSubsetPlugin?.subset})),
271
+ systemFonts: globalThis.RapierExportFonts?.systemPdfFonts,
272
+ maskFont: globalThis.RapierExportFonts?.maskPdfFont,
273
+ faces: [...customFonts, ...(globalThis.RapierDocxFonts?.DOCX_FONTS || []).filter(face => !customFamilies.has(face.family.toLowerCase())),
274
+ ...pdfFontSources(css).filter(face => !customFamilies.has(face.family.toLowerCase()))]};
275
+ nativeGlyphs = await collectPdfGlyphs(main, glyphOptions);
276
+ const textPages = await _rapierPrintTextPages(main, bodyWidth, count, check, undefined, nativeGlyphs.nodes, nativeGlyphs.markers);
271
277
  const linkPages = _rapierPrintLinkPages(main, bodyWidth, bodyHeight, count);
272
278
  vectors = await collectPdfVectors(main, {width: bodyWidth, height: bodyHeight, count, capture, signal, current, check, maxBytes,
273
- sanitizeSvg: globalThis.RapierImageAssets?.sanitizeSvgText});
274
- for (const root of vectors.textRoots) {
275
- const held = await _rapierPrintTextPages(root, bodyWidth, count, check, main.getBoundingClientRect());
276
- for (const [page, runs] of held.entries()) textPages[page].push(...runs);
277
- }
279
+ sanitizeSvg: globalThis.RapierImageAssets?.sanitizeSvgText,
280
+ collectText: root => collectPdfGlyphs(root, {...glyphOptions, origin: main.getBoundingClientRect()})});
281
+ for (const {root, original, text} of vectors.textRoots) {
282
+ const held = await _rapierPrintTextPages(root, bodyWidth, count, check, main.getBoundingClientRect(), text?.nodes);
283
+ nativeGlyphs.painted.push(...(text?.painted || []));
284
+ for (const [page, runs] of held.entries()) {
285
+ const at = textPages[page].findIndex(run => run.imageRoot === original);
286
+ if (at >= 0) textPages[page].splice(at, 1, ...runs); else textPages[page].push(...runs);
287
+ }
288
+ }
278
289
  const text = textPages.flatMap(page => page.map(run => run.text));
279
290
  const glyphs = new Set();
280
291
  for (const line of text) for (const character of line) {
@@ -349,11 +360,11 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
349
360
  const bytes = await writeRasterPdf({pages, textPages, linkPages, vectorPages: vectors.pages, canonical, bom, filename, title, font, width, height, margins, backdrop: backdrop?.pages[0] || null,
350
361
  pixelWidth: bodyWidth, pixelHeight: bodyHeight, pageNumbers: settings?.pagestyle === 'plain', maxBytes, check});
351
362
  check();
352
- return {bytes, pages: count};
353
- } finally { filmSession?.close(); nativeGlyphs?.restore(); vectors?.restore(); backdrop?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
363
+ return {bytes, pages: count, painted: nativeGlyphs.painted};
364
+ } finally { filmSession?.close(); backdrop?.restore(); vectors?.restore(); nativeGlyphs?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
354
365
  }
355
366
 
356
- async function _rapierPrintTextPages(main, pageWidth, count, check, origin = main.getBoundingClientRect(), nativeNodes = null) {
367
+ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = main.getBoundingClientRect(), nativeNodes = null, markerNodes = null) {
357
368
  const into = main.ownerDocument;
358
369
  const pages = Array.from({length: count}, () => []), walker = into.createTreeWalker(main, 5);
359
370
  const markerMetrics = _rapierPrintWillMetrics(Array.from(main.querySelectorAll('.rapier-will-carrier'), node => node.textContent));
@@ -363,6 +374,12 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
363
374
  for (let node; (node = walker.nextNode());) {
364
375
  if (++nodes % 128 === 0) { await new Promise(resolve => setTimeout(resolve, 0)); check(); }
365
376
  const element = node.nodeType === 1 ? node : node.parentElement;
377
+ if (node.nodeType === 1 && node.localName === 'img') {
378
+ const rect = node.getBoundingClientRect(), page = Math.max(0, Math.min(count - 1, Math.floor((rect.left - origin.left + .01) / pageWidth)));
379
+ pages[page].push({imageRoot: node, text: '', parts: []});
380
+ }
381
+ const marker = markerNodes?.get(node);
382
+ if (marker && marker.page < count) pages[marker.page].push({...marker, block: node, carrier: null, marker: true});
366
383
  const formula = node.nodeType === 1 && node.matches('.math-rendered[data-math-src]') && node.querySelector('svg');
367
384
  if (node.nodeType === 1 && !formula || node.nodeType === 3 && element.closest('.math-rendered[data-math-src]')) continue;
368
385
  let value;
@@ -377,10 +394,12 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
377
394
  const native = nativeNodes?.get(node);
378
395
  let characters = null;
379
396
  const pageOf = x => Math.max(0, Math.min(count - 1, Math.floor((x - origin.left + .01) / pageWidth)));
380
- const visit = (start, end) => {
397
+ const visit = (start, end, measured = null) => {
381
398
  if (++operations % 128 === 0) check();
382
399
  if (!formula) { range.setStart(node, start); range.setEnd(node, end); }
383
- const rects = Array.from(formula ? [formula.getBoundingClientRect()] : range.getClientRects()).filter(rect => rect.height > 0 && (rect.width > 0 || carrier));
400
+ const rects = measured ? measured.map(part => ({left: origin.left + part.page * pageWidth + part.x, right: origin.left + part.page * pageWidth + part.x + part.width,
401
+ top: origin.top + part.y, bottom: origin.top + part.y + part.height, width: part.width, height: part.height})) :
402
+ Array.from(formula ? [formula.getBoundingClientRect()] : range.getClientRects()).filter(rect => rect.height > 0 && (rect.width > 0 || carrier));
384
403
  if (!rects.length) { if (carrier) markerLost(); return; }
385
404
  const first = rects[0], page = pageOf(first.left);
386
405
  const oneLine = rects.every(rect => pageOf(rect.left) === page && pageOf(rect.width ? rect.right - .02 : rect.left) === page && Math.abs(rect.top - first.top) < .5);
@@ -390,30 +409,50 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
390
409
  if (cluster) middle = cluster.start > start ? cluster.start : cluster.end;
391
410
  if (!oneLine && middle > start && middle < end) { visit(start, middle); visit(middle, end); return; }
392
411
  let text = value.slice(start, end);
393
- if (!formula) text = preserve ? text.replace(/\r\n?|\n/g, '') : text.replace(/[ \t\r\n\f]+/g, ' ');
394
- if (!text) return;
412
+ if (!formula) text = (preserve ? text.replace(/\r\n?|\n/g, '') : text.replace(/[ \t\r\n\f]+/g, ' ')).replace(/\u00ad/g, '');
413
+ if (!text && !native?.some(item => item.start >= start && item.end <= end && item.soft)) return;
395
414
  const x = Math.min(...rects.map(rect => rect.left)) - origin.left - page * pageWidth;
396
415
  const y = Math.min(...rects.map(rect => rect.top)) - origin.top;
397
416
  let right = Math.max(...rects.map(rect => rect.right)) - origin.left - page * pageWidth;
398
417
  if (carrier && right === x) right += Array.from(text).length * markerMetrics.advance * markerMetrics.size / 1000;
399
418
  const bottom = Math.max(...rects.map(rect => rect.bottom)) - origin.top;
400
- const parts = native ? native.filter(item => item.start >= start && item.end <= end && item.text).map(item => ({...item}))
401
- : [{text, x, y, width: right - x, height: bottom - y, rtl: style.direction === 'rtl'}];
419
+ let parts = native ? native.filter(item => item.start >= start && item.end <= end && item.text).map(item => ({...item, rtl: style.direction === 'rtl'}))
420
+ : [{text, x, y, width: right - x, height: bottom - y, rtl: style.direction === 'rtl'}];
421
+ if (native) {
422
+ const joined = [];
423
+ for (const part of parts) {
424
+ const last = joined.at(-1);
425
+ if (last && !last.font && !part.font) {
426
+ const right = Math.max(last.x + last.width, part.x + part.width);
427
+ last.x = Math.min(last.x, part.x); last.width = right - last.x;
428
+ last.text += part.text; last.end = part.end;
429
+ } else joined.push(part);
430
+ }
431
+ parts = joined;
432
+ }
402
433
  const last = pages[page].at(-1);
403
- if (last && !formula && !last.math && last.block === block && last.carrier === carrier && Math.abs(last.y - y) < Math.max(2, Math.min(last.height, bottom - y) * .2)) {
434
+ if (last && !formula && !last.math && last.block === block && last.carrier === carrier && (last.marker || Math.abs(last.y - y) < Math.max(2, Math.min(last.height, bottom - y) * .2))) {
404
435
  if (!preserve && last.text.endsWith(' ') && text.startsWith(' ') && parts.length) {
405
436
  const first = parts[0];
406
437
  if (first.text === ' ') parts.shift();
407
438
  else {const advance = first.width / Array.from(first.text).length; first.text = first.text.slice(1); first.x += advance; first.width -= advance;}
408
439
  }
409
440
  last.text = preserve ? last.text + text : (last.text + text).replace(/[ \t\r\n\f]+/g, ' ');
410
- last.parts.push(...parts);
441
+ last.parts.push(...parts); last.marker = false;
411
442
  last.width = Math.max(last.x + last.width, right) - Math.min(last.x, x);
412
443
  last.height = Math.max(last.y + last.height, bottom) - Math.min(last.y, y);
413
444
  last.x = Math.min(last.x, x); last.y = Math.min(last.y, y);
414
445
  } else pages[page].push({text, x, y, width: right - x, height: bottom - y, block, carrier, math: !!formula, parts});
415
446
  };
416
- visit(0, value.length);
447
+ if (native) {
448
+ const lines = [];
449
+ for (const part of native) {
450
+ const last = lines.at(-1);
451
+ if (last && last[0].page === part.page && Math.abs(last[0].y - part.y) < .5) last.push(part);
452
+ else lines.push([part]);
453
+ }
454
+ for (const line of lines) visit(line[0].start, line.at(-1).end, line);
455
+ } else visit(0, value.length);
417
456
  }
418
457
  check();
419
458
  const pageHeight = origin.height;
@@ -518,44 +557,129 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
518
557
  const source = encoder.encode((bom ? '\uFEFF' : '') + canonical);
519
558
  const embedded = stream('/Type /EmbeddedFile /Subtype /text#2Fplain /Params <</Size ' + source.length + '>> /Filter /FlateDecode', await compressed(source));
520
559
  const file = add('<< /Type /Filespec /F ' + literal(String(filename).replace(/[^\x20-\x7e]/g, '_')) + ' /UF <feff' + hex(filename) + '> /AFRelationship /Source /EF << /F ' + embedded + ' 0 R /UF ' + embedded + ' 0 R >> >>');
560
+ for (const page of textPages) for (const run of page) {
561
+ let previous = null;
562
+ for (const part of run.parts || []) {
563
+ part.unicode = part.text.normalize('NFC'); part.actual = part.text.replace(/\u00ad/g, '');
564
+ if (part.font && /^ +$/.test(part.text) && previous) {
565
+ if (previous.font && /[\p{Script=Hebrew}]/u.test(previous.text) && previous.font.glyph(32)) {
566
+ part.font = previous.font; part.glyph = previous.font.glyph(32); previous = part;
567
+ } else {
568
+ if (previous.font) previous.unicode += part.text; else previous.text += part.text;
569
+ previous.actual += part.actual;
570
+ const right = Math.max(previous.x + previous.width, part.x + part.width);
571
+ if (!previous.font) previous.x = Math.min(previous.x, part.x);
572
+ previous.width = right - previous.x; part.unicode = '';
573
+ }
574
+ }
575
+ else if (part.text !== '\u00ad') previous = part;
576
+ }
577
+ if (previous && !run.carrier && !run.parts?.at(-1)?.soft && previous === run.parts?.findLast(part => part.text.trim() && part.text !== '\u00ad')) {
578
+ if (previous.font) {
579
+ let ending = previous;
580
+ const tail = [];
581
+ for (let at = run.parts.indexOf(previous); at >= 0; at--) {
582
+ const part = run.parts[at];
583
+ if (part.font && !part.unicode) continue;
584
+ if (part.font !== previous.font) break;
585
+ tail.push(part);
586
+ }
587
+ if (tail.some(part => /\p{Script=Hebrew}/u.test(part.text))) ending = tail.reduce((a, b) => a.x > b.x ? a : b);
588
+ ending.unicode += '\n';
589
+ } else previous.copyEnd = '\n';
590
+ }
591
+ }
521
592
  const codes = new Map(font.codes.map((code, index) => [code, index + 1]));
522
593
  const rows = font.codes.map((code, index) => '<' + (index + 1).toString(16).padStart(4, '0') + '> <' + hex(String.fromCodePoint(code)) + '>');
594
+ const fallbackCharacters = (part, carrier) => {
595
+ const characters = Array.from(part.text);
596
+ if (!carrier && !/[\p{Script=Arabic}\p{Script=Hebrew}\p{Script=Syriac}\p{Script=Thaana}\p{Script=Nko}]/u.test([...part.text.trimEnd()].at(-1) || '')) {
597
+ let suffix = part.copyEnd || '';
598
+ while (characters.length > 1 && characters.at(-1) === ' ') suffix = characters.pop() + suffix;
599
+ if (suffix && characters.length) characters[characters.length - 1] += suffix;
600
+ }
601
+ return characters;
602
+ };
603
+ const fallbackIds = font.codes.map((_, index) => index + 1);
604
+ for (const page of textPages) for (const run of page) for (const part of run.parts || [run]) if (!part.font)
605
+ for (const character of fallbackCharacters(part, run.carrier)) if ([...character].length > 1 && !codes.has(character)) {
606
+ const code = rows.length + 1; if (code > 65535) fail('pdf_text_glyph_limit');
607
+ codes.set(character, code); fallbackIds.push(codes.get(character.codePointAt(0)));
608
+ rows.push('<' + code.toString(16).padStart(4, '0') + '> <' + hex(character) + '>');
609
+ }
523
610
  let cmap = '/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n/CMapName /RapierText def\n/CMapType 2 def\n1 begincodespacerange\n<0000> <ffff>\nendcodespacerange\n';
524
611
  for (let at = 0; at < rows.length; at += 100) cmap += Math.min(100, rows.length - at) + ' beginbfchar\n' + rows.slice(at, at + 100).join('\n') + '\nendbfchar\n';
525
612
  cmap += 'endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend';
526
613
  const mapping = stream('/Filter /FlateDecode', await compressed(encode(cmap)));
527
614
  const fontFile = stream('/Length1 ' + font.bytes.length + ' /Filter /FlateDecode', await compressed(font.bytes));
528
615
  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 >>');
529
- const descendant = add('<< /Type /Font /Subtype /CIDFontType2 /BaseFont /RapierText /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ' + descriptor + ' 0 R /DW 500 /CIDToGIDMap /Identity >>');
616
+ const fallbackMap = new Uint8Array((fallbackIds.length + 1) * 2);
617
+ fallbackIds.forEach((id, index) => {fallbackMap[(index + 1) * 2] = id >> 8; fallbackMap[(index + 1) * 2 + 1] = id & 255;});
618
+ const fallbackGlyphs = stream('/Filter /FlateDecode', await compressed(fallbackMap));
619
+ const descendant = add('<< /Type /Font /Subtype /CIDFontType2 /BaseFont /RapierText /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ' + descriptor + ' 0 R /DW 500 /CIDToGIDMap ' + fallbackGlyphs + ' 0 R >>');
530
620
  const type = add('<< /Type /Font /Subtype /Type0 /BaseFont /RapierText /Encoding /Identity-H /DescendantFonts [' + descendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
531
621
  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 >>');
532
622
  const carrierDescendant = add('<< /Type /Font /Subtype /CIDFontType2 /BaseFont /RapierWillCarrier /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ' + carrierDescriptor + ' 0 R /DW 500 /CIDToGIDMap /Identity >>');
533
623
  const carrierType = add('<< /Type /Font /Subtype /Type0 /BaseFont /RapierWillCarrier /Encoding /Identity-H /DescendantFonts [' + carrierDescendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
534
624
  const numberFont = pageNumbers ? add('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>') : null;
535
625
  const kids = pages.map(() => { objects.push(null); return objects.length - 1; }), decimal = value => Number(value.toFixed(4));
536
- const nativeFonts = new Map();
537
- const glyphKey = part => part.glyph + ':' + part.text;
538
- for (const page of textPages) for (const run of page) for (const part of run.parts || []) if (part.font) {
539
- let entry = nativeFonts.get(part.font);
540
- if (!entry) {entry = {font: part.font, name: '/Fn' + nativeFonts.size, codes: new Map(), rows: []}; nativeFonts.set(part.font, entry);}
541
- const key = glyphKey(part);
542
- if (!entry.codes.has(key)) {const code = entry.rows.length + 1; if (code > 65000) fail('pdf_text_glyph_limit'); entry.codes.set(key, code); entry.rows.push({...part, code});}
626
+ const nativeFonts = new Map(), nativeEntries = [], fontFiles = new Map();
627
+ const glyphKey = part => part.glyph + ':' + part.unicode + ':' + decimal(part.width / part.size / (part.scaleX || 1));
628
+ for (const page of textPages) for (const run of page) for (const part of run.parts || []) if (part.font && part.unicode) {
629
+ const key = glyphKey(part), cff = part.font.outline === 'cff';
630
+ const cid = cff ? part.font.cids?.[part.glyph] ?? part.glyph : null;
631
+ let variants = nativeFonts.get(part.font);
632
+ if (!variants) {variants = []; nativeFonts.set(part.font, variants);}
633
+ let entry = variants.find(entry => !cff || !entry.rows.some(row => row.code === cid && glyphKey(row) !== key));
634
+ if (!entry) {
635
+ entry = {font: part.font, name: '/Fn' + nativeEntries.length, codes: new Map(), rows: []};
636
+ variants.push(entry); nativeEntries.push(entry);
637
+ }
638
+ if (!entry.codes.has(key)) {
639
+ const code = cff ? cid : entry.rows.length + 1;
640
+ if (code > 65535) fail('pdf_text_glyph_limit');
641
+ entry.codes.set(key, code); entry.rows.push({...part, code});
642
+ }
643
+ part.pdfFont = entry;
543
644
  }
544
- const nativeResources = [];
545
- for (const entry of nativeFonts.values()) {
645
+ const nativeResources = [], fontNames = new Map();
646
+ for (const entry of nativeEntries) {
546
647
  check();
547
- const face = entry.font, name = face.asset.family.replace(/\s/g, '') + '-' + face.asset.style, scale = 1000 / face.units;
548
- const file = stream('/Length1 ' + face.bytes.length + ' /Filter /FlateDecode', await compressed(face.bytes));
549
- const descriptor = add('<< /Type /FontDescriptor /FontName /' + name + ' /Flags 32 /FontBBox [' + face.bbox.map(value => decimal(value * scale)).join(' ') + '] /ItalicAngle 0 /Ascent ' + decimal(face.ascent * scale)
550
- + ' /Descent ' + decimal(face.descent * scale) + ' /CapHeight ' + decimal(face.capHeight * scale) + ' /StemV 80 /FontFile2 ' + file + ' 0 R >>');
551
- const ids = new Uint8Array((entry.rows.length + 1) * 2);
552
- for (const row of entry.rows) {ids[row.code * 2] = row.glyph >> 8; ids[row.code * 2 + 1] = row.glyph & 255;}
553
- const glyphMap = stream('/Filter /FlateDecode', await compressed(ids));
554
- let unicode = '/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n/CMapName /' + name + 'Unicode def\n/CMapType 2 def\n1 begincodespacerange\n<0000> <ffff>\nendcodespacerange\n';
555
- for (let at = 0; at < entry.rows.length; at += 100) {const rows = entry.rows.slice(at, at + 100); unicode += rows.length + ' beginbfchar\n' + rows.map(row => '<' + row.code.toString(16).padStart(4, '0') + '> <' + hex(row.text) + '>').join('\n') + '\nendbfchar\n';}
648
+ const face = entry.font, scale = 1000 / face.units;
649
+ if (!fontNames.has(face)) {
650
+ const prefix = face.asset.subset ? fontNames.size.toString(26).padStart(6, '0').replace(/[0-9a-p]/g, c => String.fromCharCode(65 + parseInt(c, 26))) + '+' : '';
651
+ fontNames.set(face, prefix + (face.asset.postscriptName || 'Rapier' + entry.name.slice(1) + '-' + face.asset.family).replace(/[^a-z0-9-]/gi, ''));
652
+ }
653
+ const name = fontNames.get(face);
654
+ let unicode = '/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n/CMapName /' + name + 'Unicode def\n/CMapType 2 def\n1 begincodespacerange\n' + (face.type3 ? '<00> <ff>' : '<0000> <ffff>') + '\nendcodespacerange\n';
655
+ for (let at = 0; at < entry.rows.length; at += 100) {const rows = entry.rows.slice(at, at + 100); unicode += rows.length + ' beginbfchar\n' + rows.map(row => '<' + row.code.toString(16).padStart(face.type3 ? 2 : 4, '0') + '> <' + hex(row.unicode) + '>').join('\n') + '\nendbfchar\n';}
556
656
  unicode += 'endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend';
557
657
  const mapping = stream('/Filter /FlateDecode', await compressed(encode(unicode)));
558
- const descendant = add('<< /Type /Font /Subtype /CIDFontType2 /BaseFont /' + name + ' /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ' + descriptor + ' 0 R /CIDToGIDMap ' + glyphMap + ' 0 R /W [1 [' + entry.rows.map(row => decimal(face.advance(row.glyph) * scale)).join(' ') + ']] >>');
658
+ if (face.type3) {
659
+ if (entry.rows.length > 255) fail('pdf_text_glyph_limit');
660
+ const widths = entry.rows.map(row => decimal(row.width / row.size / (row.scaleX || 1) * 1000));
661
+ const paths = [];
662
+ for (let index = 0; index < entry.rows.length; index++) {
663
+ const row = entry.rows[index], drawing = widths[index] + ' 0 d0\n' + face.commands;
664
+ paths[index] = '/g' + row.code + ' ' + stream('/Filter /FlateDecode', await compressed(encode(drawing))) + ' 0 R';
665
+ }
666
+ const type = add('<< /Type /Font /Subtype /Type3 /Name /' + name + ' /FontBBox [' + face.bbox.join(' ') + '] /FontMatrix [.001 0 0 .001 0 0] /CharProcs << ' + paths.join(' ') + ' >> /Encoding << /Type /Encoding /Differences [1 ' + entry.rows.map(row => '/g' + row.code).join(' ') + '] >> /FirstChar 1 /LastChar ' + entry.rows.length + ' /Widths [' + widths.join(' ') + '] /Resources << >> /ToUnicode ' + mapping + ' 0 R >>');
667
+ nativeResources.push(entry.name + ' ' + type + ' 0 R'); continue;
668
+ }
669
+ let descriptor = fontFiles.get(face);
670
+ if (!descriptor) {
671
+ const file = stream((face.outline === 'cff' ? '/Subtype /OpenType ' : '/Length1 ' + face.bytes.length) + ' /Filter /FlateDecode', await compressed(face.bytes));
672
+ descriptor = add('<< /Type /FontDescriptor /FontName /' + name + ' /Flags ' + (face.italic ? 96 : 32) + ' /FontBBox [' + face.bbox.map(value => decimal(value * scale)).join(' ') + '] /ItalicAngle ' + decimal(face.italicAngle || 0) + ' /Ascent ' + decimal(face.ascent * scale)
673
+ + ' /Descent ' + decimal(face.descent * scale) + ' /CapHeight ' + decimal(face.capHeight * scale) + ' /StemV 80 /' + (face.outline === 'cff' ? 'FontFile3' : 'FontFile2') + ' ' + file + ' 0 R >>');
674
+ fontFiles.set(face, descriptor);
675
+ }
676
+ let glyphMap = '';
677
+ if (face.outline !== 'cff') {
678
+ const ids = new Uint8Array((entry.rows.length + 1) * 2);
679
+ for (const row of entry.rows) {ids[row.code * 2] = row.glyph >> 8; ids[row.code * 2 + 1] = row.glyph & 255;}
680
+ glyphMap = ' /CIDToGIDMap ' + stream('/Filter /FlateDecode', await compressed(ids)) + ' 0 R';
681
+ }
682
+ const descendant = add('<< /Type /Font /Subtype /' + (face.outline === 'cff' ? 'CIDFontType0' : 'CIDFontType2') + ' /BaseFont /' + name + ' /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ' + descriptor + ' 0 R' + glyphMap + ' /W [' + entry.rows.map(row => row.code + ' [' + decimal(row.width / row.size / (row.scaleX || 1) * 1000) + ']').join(' ') + '] >>');
559
683
  const type = add('<< /Type /Font /Subtype /Type0 /BaseFont /' + name + ' /Encoding /Identity-H /DescendantFonts [' + descendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
560
684
  nativeResources.push(entry.name + ' ' + type + ' 0 R');
561
685
  }
@@ -609,28 +733,46 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
609
733
  for (const drawing of vectorPages[index] || []) commands += emit(drawing, margins.left, height - margins.top, pixelWidth, pixelHeight);
610
734
  const textAlphas = new Map();
611
735
  for (const run of textPages[index]) {
612
- if (!run.text) continue;
736
+ if (!run.text && !run.parts?.some(part => part.font)) continue;
613
737
  const element = structOf(run.tags || []);
614
738
  if (element) {element.kids.push({page: index, mcid: marked.length}); commands += `/${element.role} << /MCID ${marked.length} >> BDC\n`; marked.push(element);}
615
- commands += `q\n/Span << /ActualText <feff${hex(run.text)}> >> BDC\nBT\n100 Tz\n`;
739
+ commands += 'q\n';
740
+ const groups = [];
616
741
  for (const part of run.parts || [run]) {
742
+ if (part.font && !part.unicode) continue;
743
+ const last = groups.at(-1), previous = last?.at(-1);
744
+ if (previous && previous.font === part.font && previous.skew === part.skew && previous.size === part.size && previous.scaleX === part.scaleX) last.push(part);
745
+ else groups.push([part]);
746
+ }
747
+ for (const group of groups) {
748
+ const actual = group.map(part => part.actual ?? part.text).join('');
749
+ let text = `/Span << /ActualText <feff${hex(actual)}> >> BDC\nBT\n100 Tz\n`;
750
+ const shown = group.some(part => part.font && /\p{Script=Hebrew}/u.test(part.text)) ? [...group].sort((a, b) => a.x - b.x) : group;
751
+ for (const part of shown) {
617
752
  if (part.font) {
618
- const entry = nativeFonts.get(part.font), value = entry.codes.get(glyphKey(part)).toString(16).padStart(4, '0'), alpha = decimal(part.rgba[3]);
753
+ const entry = part.pdfFont, value = entry.codes.get(glyphKey(part)).toString(16).padStart(part.font.type3 ? 2 : 4, '0'), alpha = decimal(part.rgba[3]);
619
754
  let state = textAlphas.get(alpha);
620
755
  if (!state) {state = '/Gs' + states.length; states.push(state + ' ' + add('<< /Type /ExtGState /ca ' + alpha + ' >>') + ' 0 R'); textAlphas.set(alpha, state);}
621
- commands += `${state} gs\n${part.rgba.slice(0, 3).map(decimal).join(' ')} rg\n${entry.name} ${decimal(part.size * .75)} Tf\n0 Tr\n1 0 0 1 ${decimal(margins.left + part.x * .75)} ${decimal(height - margins.top - part.baseline * .75)} Tm\n<${value}> Tj\n`;
756
+ text += `${state} gs\n${part.rgba.slice(0, 3).map(decimal).join(' ')} rg\n${entry.name} ${decimal(part.size * .75)} Tf\n${decimal((part.scaleX || 1) * 100)} Tz\n0 Tr\n1 0 ${decimal((part.skew || 0) * (part.scaleX || 1))} 1 ${decimal(margins.left + part.x * .75)} ${decimal(height - margins.top - part.baseline * .75)} Tm\n<${value}> Tj\n`;
622
757
  } else {
623
- const characters = Array.from(part.text); if (!characters.length) continue;
758
+ const characters = fallbackCharacters(part, run.carrier); if (!characters.length) continue;
624
759
  const {slots, count} = _rapierPrintSlots(part.text, part.rtl);
625
- const advance = part.width * .75 / count, size = Math.max(.1, advance * 2), scaleY = Math.max(.1, part.height * .75) / size;
760
+ const advance = part.width * .75 / count;
626
761
  const baseline = decimal(height - margins.top - (part.y + part.height * .8) * .75);
627
762
  for (let letter = 0; letter < characters.length; letter++) {
628
- const value = codes.get(characters[letter].codePointAt(0)).toString(16).padStart(4, '0');
629
- 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`;
763
+ const size = Math.max(.1, advance * 2 * [...characters[letter].replace(/\n/g, '')].length), scaleY = Math.max(.1, part.height * .75) / size;
764
+ const value = (codes.get(characters[letter]) || codes.get(characters[letter].codePointAt(0))).toString(16).padStart(4, '0');
765
+ text += `${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`;
630
766
  }
631
767
  }
632
768
  }
633
- commands += 'ET\nEMC\nQ\n' + (element ? 'EMC\n' : '');
769
+ text += 'ET\nEMC\n';
770
+ const name = '/Tx' + pictures.length;
771
+ const object = stream('/Type /XObject /Subtype /Form /BBox [0 0 ' + decimal(width) + ' ' + decimal(height) + '] /Resources << /Font << /F1 ' + type + ' 0 R /Fc ' + carrierType + ' 0 R ' + nativeResources.join(' ') + ' >> /ExtGState << ' + states.join(' ') + ' >> >> /Filter /FlateDecode', await compressed(encode(text)));
772
+ pictures.push(name + ' ' + object + ' 0 R');
773
+ commands += name + ' Do\n';
774
+ }
775
+ commands += 'Q\n' + (element ? 'EMC\n' : '');
634
776
  }
635
777
  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`;
636
778
  const content = stream('/Filter /FlateDecode', await compressed(encode(commands)));
@@ -3,13 +3,13 @@ import {typography} from '../spec/md-marks.mjs';
3
3
  const RAPIER_SANITIZE_FORBID_TAGS = Object.freeze(['form', 'style', 'dialog', 'template', 'iframe']);
4
4
  const RAPIER_SANITIZE_FORBID_ATTR = Object.freeze(['action', 'formaction', 'form', 'for', 'command', 'commandfor', 'popover', 'popovertarget', 'popovertargetaction']);
5
5
  const RAPIER_SANITIZE_PROFILES = Object.freeze({
6
- htmlTags: Object.freeze(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),
7
- svgTags: Object.freeze(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),
8
- mathTags: Object.freeze(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),
9
- htmlAttributes: Object.freeze(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","command","commandfor","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns"]),
10
- svgAttributes: Object.freeze(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dominant-baseline","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","pointer-events","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-orientation","text-rendering","textlength","type","u1","u2","unicode","values","vector-effect","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),
11
- mathAttributes: Object.freeze(["accent","accentunder","align","bevelled","close","columnalign","columnlines","columnspacing","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lquote","lspace","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),
12
- xmlAttributes: Object.freeze(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),
6
+ htmlTags: Object.freeze('a abbr acronym address area article aside audio b bdi bdo big blink blockquote body br button canvas caption center cite code col colgroup content data datalist dd decorator del details dfn dialog dir div dl dt element em fieldset figcaption figure font footer form h1 h2 h3 h4 h5 h6 head header hgroup hr html i img input ins kbd label legend li main map mark marquee menu menuitem meter nav nobr ol optgroup option output p picture pre progress q rp rt ruby s samp search section select shadow slot small source spacer span strike strong style sub summary sup table tbody td template textarea tfoot th thead time tr track tt u ul var video wbr'.split(' ')),
7
+ svgTags: Object.freeze('svg a altglyph altglyphdef altglyphitem animatecolor animatemotion animatetransform circle clippath defs desc ellipse enterkeyhint exportparts filter font g glyph glyphref hkern image inputmode line lineargradient marker mask metadata mpath part path pattern polygon polyline radialgradient rect stop style switch symbol text textpath title tref tspan view vkern'.split(' ')),
8
+ mathTags: Object.freeze('math menclose merror mfenced mfrac mglyph mi mlabeledtr mmultiscripts mn mo mover mpadded mphantom mroot mrow ms mspace msqrt mstyle msub msup msubsup mtable mtd mtext mtr munder munderover mprescripts'.split(' ')),
9
+ htmlAttributes: Object.freeze('accept action align alt autocapitalize autocomplete autopictureinpicture autoplay background bgcolor border capture cellpadding cellspacing checked cite class clear color cols colspan command commandfor controls controlslist coords crossorigin datetime decoding default dir disabled disablepictureinpicture disableremoteplayback download draggable enctype enterkeyhint exportparts face for headers height hidden high href hreflang id inert inputmode integrity ismap kind label lang list loading loop low max maxlength media method min minlength multiple muted name nonce noshade novalidate nowrap open optimum part pattern placeholder playsinline popover popovertarget popovertargetaction poster preload pubdate radiogroup readonly rel required rev reversed role rows rowspan spellcheck scope selected shape size sizes slot span srclang start src srcset step style summary tabindex title translate type usemap valign value width wrap xmlns'.split(' ')),
10
+ svgAttributes: Object.freeze('accent-height accumulate additive alignment-baseline amplitude ascent attributename attributetype azimuth basefrequency baseline-shift begin bias by class clip clippathunits clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cx cy d dx dy diffuseconstant direction display divisor dominant-baseline dur edgemode elevation end exponent fill fill-opacity fill-rule filter filterunits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight fx fy g1 g2 glyph-name glyphref gradientunits gradienttransform height href id image-rendering in in2 intercept k k1 k2 k3 k4 kerning keypoints keysplines keytimes lang lengthadjust letter-spacing kernelmatrix kernelunitlength lighting-color local marker-end marker-mid marker-start markerheight markerunits markerwidth maskcontentunits maskunits max mask mask-type media method mode min name numoctaves offset operator opacity order orient orientation origin overflow paint-order path pathlength patterncontentunits patterntransform patternunits pointer-events points preservealpha preserveaspectratio primitiveunits r rx ry radius refx refy repeatcount repeatdur restart result rotate scale seed shape-rendering slope specularconstant specularexponent spreadmethod startoffset stddeviation stitchtiles stop-color stop-opacity stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke stroke-width style surfacescale systemlanguage tabindex tablevalues targetx targety transform transform-origin text-anchor text-decoration text-orientation text-rendering textlength type u1 u2 unicode values vector-effect viewbox visibility version vert-adv-y vert-origin-x vert-origin-y width word-spacing wrap writing-mode xchannelselector ychannelselector x x1 x2 xmlns y y1 y2 z zoomandpan'.split(' ')),
11
+ mathAttributes: Object.freeze('accent accentunder align bevelled close columnalign columnlines columnspacing columnspan denomalign depth dir display displaystyle encoding fence frame height href id largeop length linethickness lquote lspace mathbackground mathcolor mathsize mathvariant maxsize minsize movablelimits notation numalign open rowalign rowlines rowspacing rowspan rspace rquote scriptlevel scriptminsize scriptsizemultiplier selection separator separators stretchy subscriptshift supscriptshift symmetric voffset width xmlns'.split(' ')),
12
+ xmlAttributes: Object.freeze('xlink:href xml:id xlink:title xml:space xmlns:xlink'.split(' ')),
13
13
  });
14
14
  const RAPIER_SANITIZE_DROP_CONTENTS = Object.freeze('annotation-xml.audio.colgroup.desc.foreignobject.head.iframe.math.mi.mn.mo.ms.mtext.noembed.noframes.noscript.plaintext.script.selectedcontent.style.svg.template.thead.title.video.xmp'.split('.'));
15
15
  function _rapierSanitizeProfile(...kinds) {
@@ -330,7 +330,8 @@ function _rapierDocxRenderStyles(root) {
330
330
  }
331
331
  const coloured = codes.filter(code => code.querySelector('span'));
332
332
  const quotes = Array.from(root.querySelectorAll('blockquote'));
333
- if (!coloured.length && !quotes.length) return;
333
+ const diagrams = Array.from(root.querySelectorAll('svg.pele'));
334
+ if (!coloured.length && !quotes.length && !diagrams.length) return;
334
335
  const frame = document.createElement('iframe');
335
336
  frame.setAttribute('aria-hidden', 'true');
336
337
  frame.style.cssText = 'position:fixed;left:-100000px;top:0;width:1px;height:1px;border:0;pointer-events:none';
@@ -358,6 +359,20 @@ function _rapierDocxRenderStyles(root) {
358
359
  }
359
360
  return colours.get(colour);
360
361
  };
362
+ for (const diagram of diagrams) {
363
+ const host = into.createElement('div'); host.className = 'diagram-block';
364
+ const probe = into.importNode(diagram, true); host.appendChild(probe); surface.appendChild(host);
365
+ const originals = [diagram, ...diagram.querySelectorAll('*')];
366
+ [probe, ...probe.querySelectorAll('*')].forEach((element, index) => {
367
+ const computed = into.defaultView.getComputedStyle(element), target = originals[index];
368
+ for (const property of ['fill', 'stroke', 'color', 'stop-color', 'stroke-width', 'stroke-dasharray',
369
+ 'font-family', 'font-size', 'font-weight', 'font-style', 'rx', 'ry']) {
370
+ const value = computed.getPropertyValue(property);
371
+ if (value && !/url\(/i.test(value)) target.style.setProperty(property, value);
372
+ }
373
+ });
374
+ host.remove();
375
+ }
361
376
  for (const code of coloured) {
362
377
  const pre = into.createElement('pre'), probe = into.importNode(code, true);
363
378
  pre.appendChild(probe); surface.appendChild(pre);
@@ -95,7 +95,7 @@ export function supportsRichStyle(style, contentLanguage = '') {
95
95
  !oneOf('fontStretch', ['normal', '100%']) || !oneOf('fontSizeAdjust', ['none']) ||
96
96
  !oneOf('textRendering', ['auto']) || !oneOf('verticalAlign', ['baseline']) ||
97
97
  !oneOf('whiteSpace', ['normal', 'pre-wrap', 'nowrap']) || !oneOf('wordBreak', ['normal', 'keep-all']) ||
98
- !oneOf('hyphens', ['manual']) || !oneOf('lineBreak', ['auto']) || !oneOf('hangingPunctuation', ['none']) ||
98
+ !oneOf('hyphens', ['manual']) || !oneOf('lineBreak', ['auto', 'after-white-space']) || !oneOf('hangingPunctuation', ['none']) ||
99
99
  !oneOf('tabSize', ['8']) || !oneOf('textWrapStyle', ['auto', 'pretty'])) return false;
100
100
  for (const name of ['fontFeatureSettings', 'fontVariationSettings', 'fontVariant', 'fontVariantLigatures',
101
101
  'fontVariantCaps', 'fontVariantNumeric', 'fontVariantEastAsian', 'fontVariantPosition', 'fontVariantAlternates']) {