rapier-markdown-kit 1.2.31 → 1.2.33

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.
@@ -3,7 +3,7 @@ import {collectPdfVectors} from './pdf-vectors.mjs';
3
3
  import {collectPdfGlyphs} from './pdf-glyphs.mjs';
4
4
  import {pdfFontSources, waitForFonts} from './export-fonts.mjs';
5
5
  import {apply as applyTypography} from '../layout/typography.mjs';
6
- import {documentLanguage} from '../spec/document-settings.mjs';
6
+ import {documentLanguage, pageSizeOf} from '../spec/document-settings.mjs';
7
7
  import {applyPrintCapitals} from './print-capitals.mjs';
8
8
  import {stripOneTerminator as _rapierWillStripOneTerminator} from '../agent/will.mjs';
9
9
  import {quotedWillSpan} from '../spec/md-marks.mjs';
@@ -196,14 +196,31 @@ function wrapMasks(main) {
196
196
  }
197
197
  }
198
198
 
199
+ const REFUSALS = {
200
+ visual_too_large: 'This document is too large for a PDF. Use Word or HTML, or split it.',
201
+ visual_resources_unavailable: 'A picture or font could not be read for the PDF.',
202
+ visual_capture_expired: 'The PDF took too long to draw. Try again, or split the document.',
203
+ pdf_render_timeout: 'The PDF took too long to draw. Try again, or split the document.',
204
+ pdf_render_unavailable: 'This browser could not draw the PDF.',
205
+ pdf_page_geometry_invalid: 'The page size is not one a PDF can use.',
206
+ pdf_text_glyph_limit: 'The PDF cannot hold this many different letters. Use Word or HTML.',
207
+ pdf_template_border_unavailable: 'The page border could not be drawn.',
208
+ document_changed: 'The document changed while the PDF was drawn. Try again.',
209
+ export_too_large: 'The file is larger than the limit. Split the document.',
210
+ };
211
+
199
212
  async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings, templatePage = null}, {capture, signal, current, maxBytes = 8 * 1024 * 1024, film, ink} = {}) {
200
213
  const refuse = code => { throw Object.assign(new Error(code), {code}); };
214
+ const spoken = error => {
215
+ const code = error?.code || error?.message, words = REFUSALS[code];
216
+ return words && error.message !== words ? Object.assign(new Error(words), {code}) : error;
217
+ };
201
218
  const check = () => {
202
219
  if (signal?.aborted) refuse('cancelled');
203
220
  if (current?.() === false) refuse('document_changed');
204
221
  };
205
222
  if (!root?.ownerDocument || typeof capture !== 'function') refuse('pdf_render_unavailable');
206
- const paper = settings?.papersize || {width: 11906, height: 16838};
223
+ const paper = settings?.papersize || (templatePage ? pageSizeOf(settings) : {width: 11906, height: 16838});
207
224
  const width = paper.width / 20, height = paper.height / 20;
208
225
  const margins = Object.fromEntries(['top', 'right', 'bottom', 'left'].map(side =>
209
226
  [side, settings?.geometry ? settings.geometry[side] / 20 : (side === 'top' ? 12 : 14) * 72 / 25.4]));
@@ -301,7 +318,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
301
318
  nativeGlyphs = await collectPdfGlyphs(main, glyphOptions);
302
319
  const textPages = await _rapierPrintTextPages(main, bodyWidth, count, check, undefined, nativeGlyphs.nodes, nativeGlyphs.markers);
303
320
  const linkPages = _rapierPrintLinkPages(main, bodyWidth, bodyHeight, count);
304
- vectors = await collectPdfVectors(main, {width: bodyWidth, height: bodyHeight, count, capture, signal, current, check, maxBytes,
321
+ vectors = await collectPdfVectors(main, {width: bodyWidth, height: bodyHeight, count, capture, signal, current, check, maxBytes, underlay: true,
305
322
  sanitizeSvg: globalThis.RapierImageAssets?.sanitizeSvgText,
306
323
  collectText: root => collectPdfGlyphs(root, {...glyphOptions, origin: main.getBoundingClientRect()})});
307
324
  for (const {root, original, text} of vectors.textRoots) {
@@ -322,6 +339,8 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
322
339
  strip.style.width = count * bodyWidth + 'px';
323
340
  strip.style.transform = 'none';
324
341
  if (typeof film !== 'function') refuse('pdf_render_unavailable');
342
+ const underlay = vectors.pages.some(items => items.some(drawing => drawing.under));
343
+ if (underlay) surface.style.setProperty('background', 'transparent');
325
344
  let pendingFilm = null, abandonFilm = false;
326
345
  const trackedFilm = Promise.resolve(film({root: surface, signal, current, frame: {width: count * bodyWidth, height: bodyHeight}})).then(session => {
327
346
  if (abandonFilm) session?.close?.();
@@ -340,22 +359,23 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
340
359
  let share = Math.max(1, Math.min(Math.floor(CAPTURE_EDGE / bodyWidth), Math.floor(CAPTURE_PIXELS / (bodyWidth * bodyHeight))));
341
360
  const pages = [];
342
361
  let imageBytes = 0;
343
- const pack = async rgb => {
344
- const packed = new Uint8Array(await step(new Response(new Blob([rgb]).stream().pipeThrough(new CompressionStream('deflate'))).arrayBuffer()));
362
+ const deflate = async bytes => {
363
+ const packed = new Uint8Array(await step(new Response(new Blob([bytes]).stream().pipeThrough(new CompressionStream('deflate'))).arrayBuffer()));
345
364
  imageBytes += packed.byteLength;
346
365
  if (imageBytes > maxBytes) refuse('export_too_large');
347
- pages.push({rgb: packed});
366
+ return packed;
348
367
  };
368
+ const pack = async (rgb, alpha = null) => pages.push({rgb: await deflate(rgb), ...(alpha && {alpha: await deflate(alpha)})});
349
369
  const tileWidth = Math.min(bodyWidth, CAPTURE_EDGE), tileHeight = Math.min(bodyHeight, CAPTURE_EDGE, Math.floor(CAPTURE_PIXELS / tileWidth));
350
370
  for (let index = 0; tiled && index < count; index++) {
351
371
  const tiles = [];
352
372
  for (let y = 0; y < bodyHeight; y += tileHeight) for (let x = 0; x < bodyWidth; x += tileWidth) {
353
373
  check();
354
374
  const width = Math.min(tileWidth, bodyWidth - x), height = Math.min(tileHeight, bodyHeight - y);
355
- const shot = await step(filmSession.raster({x: index * bodyWidth + x, y, width, height}));
356
- if (shot?.width !== width || shot?.height !== height || !(shot.rgb instanceof Uint8Array)) refuse('pdf_render_unavailable');
357
- await pack(shot.rgb);
358
- shot.rgb = null;
375
+ const shot = await step(filmSession.raster({x: index * bodyWidth + x, y, width, height}, {transparent: underlay}));
376
+ if (shot?.width !== width || shot?.height !== height || !(shot.rgb instanceof Uint8Array) || underlay && !(shot.alpha instanceof Uint8Array)) refuse('pdf_render_unavailable');
377
+ await pack(shot.rgb, shot.alpha);
378
+ shot.rgb = shot.alpha = null;
359
379
  tiles.push({...pages.pop(), x, y, width, height});
360
380
  }
361
381
  pages.push({tiles});
@@ -364,21 +384,22 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
364
384
  check();
365
385
  const span = Math.min(share, count - first);
366
386
  try {
367
- const shot = await step(filmSession.raster({x: first * bodyWidth, y: 0, width: span * bodyWidth, height: bodyHeight}));
368
- if (shot?.width !== span * bodyWidth || shot?.height !== bodyHeight || !(shot.rgb instanceof Uint8Array)) refuse('pdf_render_unavailable');
369
- if (span === 1) await pack(shot.rgb);
387
+ const shot = await step(filmSession.raster({x: first * bodyWidth, y: 0, width: span * bodyWidth, height: bodyHeight}, {transparent: underlay}));
388
+ if (shot?.width !== span * bodyWidth || shot?.height !== bodyHeight || !(shot.rgb instanceof Uint8Array) || underlay && !(shot.alpha instanceof Uint8Array)) refuse('pdf_render_unavailable');
389
+ if (span === 1) await pack(shot.rgb, shot.alpha);
370
390
  else {
371
391
  const row = bodyWidth * 3;
372
392
  for (let offset = 0; offset < span; offset++) {
373
- const rgb = new Uint8Array(bodyWidth * bodyHeight * 3);
393
+ const rgb = new Uint8Array(bodyWidth * bodyHeight * 3), alpha = shot.alpha ? new Uint8Array(bodyWidth * bodyHeight) : null;
374
394
  for (let y = 0; y < bodyHeight; y++) {
375
395
  const from = (y * shot.width + offset * bodyWidth) * 3;
376
396
  rgb.set(shot.rgb.subarray(from, from + row), y * row);
397
+ if (alpha) alpha.set(shot.alpha.subarray(y * shot.width + offset * bodyWidth, y * shot.width + (offset + 1) * bodyWidth), y * bodyWidth);
377
398
  }
378
- await pack(rgb);
399
+ await pack(rgb, alpha);
379
400
  }
380
401
  }
381
- shot.rgb = null;
402
+ shot.rgb = shot.alpha = null;
382
403
  } catch (error) {
383
404
  if (error?.code === 'visual_too_large' && span > 1) { share = Math.ceil(span / 2); continue; }
384
405
  throw error;
@@ -394,14 +415,15 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
394
415
  holder.append(picture); into.body.append(holder);
395
416
  await step(picture.decode());
396
417
  backdrop = await collectPdfVectors(holder, {width: sheetWidth, height: sheetHeight, count: 1, capture, signal, current, check, maxBytes,
397
- sanitizeSvg: globalThis.RapierImageAssets?.sanitizeSvgText});
418
+ sanitizeSvg: globalThis.RapierImageAssets?.sanitizeSvgText, pictures: true});
398
419
  if (!backdrop.pages[0].length) refuse('pdf_template_border_unavailable');
399
420
  }
400
421
  const bytes = await writeRasterPdf({pages, textPages, linkPages, vectorPages: vectors.pages, canonical, bom, filename, title, font, width, height, margins, backdrop: backdrop?.pages[0] || null,
401
422
  pixelWidth: bodyWidth, pixelHeight: bodyHeight, pageNumbers: settings?.pagestyle === 'plain', maxBytes, check});
402
423
  check();
403
424
  return {bytes, pages: count, painted: nativeGlyphs.painted};
404
- } finally { filmSession?.close(); backdrop?.restore(); vectors?.restore(); nativeGlyphs?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
425
+ } catch (error) { throw spoken(error); }
426
+ finally { filmSession?.close(); backdrop?.restore(); vectors?.restore(); nativeGlyphs?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
405
427
  }
406
428
 
407
429
  async function _rapierPrintTextPages(main, pageWidth, count, check, origin = main.getBoundingClientRect(), nativeNodes = null, markerNodes = null) {
@@ -471,14 +493,14 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
471
493
  parts = joined;
472
494
  }
473
495
  const last = pages[page].at(-1);
474
- 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))) {
496
+ if (last && !formula && !last.math && !last.marker && last.block === block && last.carrier === carrier && Math.abs(last.y - y) < Math.max(2, Math.min(last.height, bottom - y) * .2)) {
475
497
  if (!preserve && last.text.endsWith(' ') && text.startsWith(' ') && parts.length) {
476
498
  const first = parts[0];
477
499
  if (first.text === ' ') parts.shift();
478
500
  else {const advance = first.width / Array.from(first.text).length; first.text = first.text.slice(1); first.x += advance; first.width -= advance;}
479
501
  }
480
502
  last.text = preserve ? last.text + text : (last.text + text).replace(/[ \t\r\n\f]+/g, ' ');
481
- last.parts.push(...parts); last.marker = false;
503
+ last.parts.push(...parts);
482
504
  last.width = Math.max(last.x + last.width, right) - Math.min(last.x, x);
483
505
  last.height = Math.max(last.y + last.height, bottom) - Math.min(last.y, y);
484
506
  last.x = Math.min(last.x, x); last.y = Math.min(last.y, y);
@@ -512,13 +534,29 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
512
534
  }
513
535
  const ROLES = {TABLE: 'Table', TR: 'TR', TD: 'TD', TH: 'TH', UL: 'L', OL: 'L', LI: 'LI', BLOCKQUOTE: 'BlockQuote', P: 'P', PRE: 'Code', FIGCAPTION: 'Caption',
514
536
  H1: 'H1', H2: 'H2', H3: 'H3', H4: 'H4', H5: 'H5', H6: 'H6'};
515
- const tags = block => {
537
+ const bodies = new WeakMap(), labels = new WeakMap();
538
+ const tags = (block, marker) => {
516
539
  const chain = [];
517
540
  for (let node = block; node && node !== main; node = node.parentElement)
518
541
  if (ROLES[node.tagName] || node === block && node.matches('.rapier-will-carrier')) chain.unshift({node, role: ROLES[node.tagName] || 'P'});
519
- return chain;
542
+ const out = [];
543
+ chain.forEach((entry, at) => {
544
+ const note = entry.node.localName === 'li' && entry.node.classList.contains('footnote-item');
545
+ out.push(note ? {...entry, role: 'Note'} : entry);
546
+ if (entry.node.localName !== 'li') return;
547
+ const last = !chain.slice(at + 1).some(inner => inner.node.localName === 'li');
548
+ if (marker && last) {
549
+ if (!labels.has(entry.node)) labels.set(entry.node, {});
550
+ out.push({node: labels.get(entry.node), role: 'Lbl'});
551
+ }
552
+ else if (!note) {
553
+ if (!bodies.has(entry.node)) bodies.set(entry.node, {});
554
+ out.push({node: bodies.get(entry.node), role: 'LBody'});
555
+ }
556
+ });
557
+ return out;
520
558
  };
521
- return pages.map(page => page.map(({block, ...run}) => ({...run, tags: tags(block)})));
559
+ return pages.map(page => page.map(({block, ...run}) => ({...run, tags: tags(block, run.marker)})));
522
560
  }
523
561
 
524
562
  function _rapierPrintLinkPages(main, width, height, count) {
@@ -595,7 +633,7 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
595
633
  const hex = value => Array.from({length: value.length}, (_, index) => value.charCodeAt(index).toString(16).padStart(4, '0')).join('');
596
634
  const literal = value => '(' + String(value).replace(/[\\()\r\n]/g, character => ({'\\': '\\\\', '(': '\\(', ')': '\\)', '\r': '\\r', '\n': '\\n'}[character])) + ')';
597
635
  const source = encoder.encode((bom ? '\uFEFF' : '') + canonical);
598
- const embedded = stream('/Type /EmbeddedFile /Subtype /text#2Fplain /Params <</Size ' + source.length + '>> /Filter /FlateDecode', await compressed(source));
636
+ const embedded = stream('/Type /EmbeddedFile /Subtype /text#2Fmarkdown /Params <</Size ' + source.length + '>> /Filter /FlateDecode', await compressed(source));
599
637
  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 >> >>');
600
638
  for (const page of textPages) for (const run of page) {
601
639
  let previous = null;
@@ -741,7 +779,9 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
741
779
  const held = pages[index], flate = !!held?.rgb;
742
780
  const tiles = held?.tiles || [{...(flate ? held : {jpeg: held}), x: 0, y: 0, width: pixelWidth, height: pixelHeight}];
743
781
  const pictures = tiles.map((tile, at) => '/Im0_' + at + ' ' + stream('/Type /XObject /Subtype /Image /Width ' + tile.width + ' /Height ' + tile.height +
744
- ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (tile.rgb ? 'FlateDecode' : 'DCTDecode'), tile.rgb || tile.jpeg) + ' 0 R'), states = [];
782
+ ' /ColorSpace /DeviceRGB /BitsPerComponent 8' + (tile.alpha ? ' /SMask ' + stream('/Type /XObject /Subtype /Image /Width ' + tile.width + ' /Height ' + tile.height +
783
+ ' /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode', tile.alpha) + ' 0 R' : '') +
784
+ ' /Filter /' + (tile.rgb ? 'FlateDecode' : 'DCTDecode'), tile.rgb || tile.jpeg) + ' 0 R'), states = [];
745
785
  pages[index] = null;
746
786
  const emit = (drawing, left, top, clipWidth, clipHeight) => {
747
787
  let out = `q\n.75 0 0 -.75 ${decimal(left)} ${decimal(top)} cm\n0 0 ${clipWidth} ${clipHeight} re W n\n1 0 0 1 ${decimal(drawing.x)} 0 cm\n`;
@@ -770,9 +810,10 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
770
810
  };
771
811
  let commands = '';
772
812
  for (const drawing of backdrop || []) commands += emit(drawing, 0, height, Math.round(width * 4 / 3), Math.round(height * 4 / 3));
813
+ for (const drawing of vectorPages[index] || []) if (drawing.under) commands += emit(drawing, margins.left, height - margins.top, pixelWidth, pixelHeight);
773
814
  commands += '/Artifact BMC\n' + tiles.map((tile, at) => 'q\n' + [decimal(tile.width * .75), 0, 0, decimal(tile.height * .75), decimal(margins.left + tile.x * .75),
774
815
  decimal(height - margins.top - (tile.y + tile.height) * .75)].join(' ') + ' cm\n/Im0_' + at + ' Do\nQ\n').join('') + 'EMC\n';
775
- for (const drawing of vectorPages[index] || []) commands += emit(drawing, margins.left, height - margins.top, pixelWidth, pixelHeight);
816
+ for (const drawing of vectorPages[index] || []) if (!drawing.under) commands += emit(drawing, margins.left, height - margins.top, pixelWidth, pixelHeight);
776
817
  const textAlphas = new Map();
777
818
  for (const run of textPages[index]) {
778
819
  if (!run.text && !run.parts?.some(part => part.font)) continue;
@@ -4,7 +4,7 @@ import {finish, finishAsync, cloneTree, cleanTree, serializeTree, encodeUtf8Step
4
4
  import {fontFamilyCss} from '../spec/md-marks.mjs';
5
5
  import {readFontAssets, documentFontsCss} from '../spec/document-fonts.mjs';
6
6
  import {fontRequests, requireDocumentFonts} from './export-fonts.mjs';
7
- import {documentLanguage, documentSettingsFonts} from '../spec/document-settings.mjs';
7
+ import {documentLanguage, documentSettingsFonts, pageSizeOf} from '../spec/document-settings.mjs';
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';
@@ -205,23 +205,26 @@ async function _rapierBuildArtifact(options, providedContext) {
205
205
  }
206
206
 
207
207
  function _rapierTemplatePage(canonical, settings, root, darkPaper = false) {
208
- if (!isTemplatePage(canonical) || !settings?.papersize) return null;
208
+ if (!isTemplatePage(canonical)) return null;
209
+ const paper = pageSizeOf(settings);
209
210
  let url = '';
210
211
  for (const paragraph of root.querySelectorAll(':scope > p[data-md-layout]')) {
211
212
  const image = paragraph.querySelector('img');
212
213
  const layout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(paragraph.getAttribute('data-md-layout'));
213
214
  const source = image?.getAttribute('src') || '';
214
- if (isPageBorderLayout(layout) && /^data:image\/svg\+xml;base64,[A-Za-z0-9+/=]+$/.test(source)) { url = source; paragraph.remove(); break; }
215
+ if (isPageBorderLayout(layout) && /^data:image\/(?:png|jpeg|webp|gif|svg\+xml|jxl);base64,[A-Za-z0-9+/=]+$/i.test(source)) { url = source; paragraph.remove(); break; }
215
216
  }
216
217
  if (!url) return null;
217
- const width = settings.papersize.width / 15, height = settings.papersize.height / 15;
218
- const margins = ['top', 'right', 'bottom', 'left'].map(side => (settings.geometry?.[side] ?? 1440) / 15);
218
+ const width = paper.width / 15, height = paper.height / 15;
219
+ const margins = ['top', 'right', 'bottom', 'left'].map(side => (settings?.geometry?.[side] ?? 1440) / 15);
219
220
  const padding = margins.map(value => value + 'px').join(' ');
220
221
  const ground = 'background:var(--rapier-template-border) 0 0/100% 100% no-repeat';
221
- const shown = darkPaper ? _rapierDarkPaperSvgUrl(url, '#fafafa') : url;
222
+ const shown = darkPaper && url.startsWith('data:image/svg+xml;base64,') ? _rapierDarkPaperSvgUrl(url, '#fafafa') : url;
222
223
  const css = ':root{--rapier-template-border:url("' + url + '")}\n'
223
224
  + '@media screen{' + (shown !== url ? ':root{--rapier-template-border:url("' + shown + '")}' : '') + '.rapier-template-border{display:none}body{background:var(--md-color-inset)}.rapier-page{box-sizing:border-box;width:min(100%,' + width + 'px);min-height:' + height + 'px;margin:0 auto;padding:' + padding
224
225
  + ';background:var(--rapier-template-border) 0 0/100% ' + height + 'px repeat-y,var(--md-color-bg);color:var(--md-color-text)}}\n'
226
+ + '@media screen and (max-width:' + width + 'px){.rapier-page{min-height:auto;aspect-ratio:' + width + '/' + height + ';padding:'
227
+ + margins.map(value => 'min(' + value + 'px,' + Number((value / width * 100).toFixed(4)) + '%)').join(' ') + ';background-size:100% auto,auto}}\n'
225
228
  + '@media print{@page{size:' + width + 'px ' + height + 'px;margin:0}html,body{margin:0;-webkit-print-color-adjust:exact;print-color-adjust:exact}'
226
229
  + '.rapier-page{box-sizing:border-box;width:auto;max-width:none;margin:0;padding:' + padding + '!important;position:relative!important;z-index:1;box-decoration-break:clone;-webkit-box-decoration-break:clone}'
227
230
  + '.rapier-template-border{display:block;position:fixed;left:0;top:0;width:' + width + 'px;height:' + height + 'px;z-index:0;' + ground + '}}';
@@ -256,11 +259,7 @@ async function _rapierBuildPublishedPage(context) {
256
259
  throw new TypeError('Publishing requires a captured document.');
257
260
  const current = {...context, ledger: null, carried: 'none', comparisonBase: null};
258
261
  return _rapierBuildArtifact({
259
- kind: 'standalone', scripts: false, extraCss: _rapierSharedPageFallbackCss(),
260
- async afterRoot(root) {
261
- if (current.work) await finishAsync(_rapierSharedPageLayoutSteps(root), current.work);
262
- else _rapierSharedPageLayout(root);
263
- },
262
+ kind: 'standalone', scripts: false,
264
263
  afterFonts: (root, style) => freezePublishedInk(root, {...style, document, work: current.work,
265
264
  spec: globalThis.RapierMarkdownSpec, ink: globalThis.RapierInk, draw: globalThis.RapierInkDraw}),
266
265
  }, current);
@@ -729,6 +728,14 @@ function* _rapierSharedPageLayoutSteps(root) {
729
728
  }
730
729
  image.style.margin = '0';
731
730
  const naturalWidth = Number(image.getAttribute('width')) || 0, naturalHeight = Number(image.getAttribute('height')) || 0;
731
+ let turned = '';
732
+ const turnedCorners = (bounds, width, height, rad) => {
733
+ const cos = Math.cos(rad), sin = Math.sin(rad), round = value => Math.round(value * 100) / 100 + '%';
734
+ return [[-1, -1], [1, -1], [1, 1], [-1, 1]].map(([x, y]) => {
735
+ const dx = x * width / 2, dy = y * height / 2;
736
+ return round(50 + (dx * cos - dy * sin) / bounds.width * 100) + ' ' + round(50 + (dx * sin + dy * cos) / bounds.height * 100);
737
+ }).join(', ');
738
+ };
732
739
  if (layout.rotate && naturalWidth > 0 && naturalHeight > 0) {
733
740
  const rad = layout.rotate * Math.PI / 180, aspect = naturalHeight / naturalWidth;
734
741
  image.style.maxWidth = 'none';
@@ -741,6 +748,7 @@ function* _rapierSharedPageLayoutSteps(root) {
741
748
  height *= scale;
742
749
  bounds = globalThis.RapierImageLayout.rotatedBoundsRad(width, height, rad);
743
750
  }
751
+ turned = turnedCorners(bounds, width, height, rad);
744
752
  paragraph.style.width = bounds.width + '%';
745
753
  paragraph.style.paddingTop = ((bounds.height - height) / 2) + '%';
746
754
  paragraph.style.paddingBottom = ((bounds.height - height) / 2) + '%';
@@ -748,6 +756,7 @@ function* _rapierSharedPageLayoutSteps(root) {
748
756
  image.style.marginLeft = image.style.marginRight = (((bounds.width - width) / 2) / bounds.width * 100) + '%';
749
757
  } else {
750
758
  const bounds = globalThis.RapierImageLayout.rotatedBoundsRad(naturalWidth, naturalHeight, rad);
759
+ turned = turnedCorners(bounds, naturalWidth, naturalHeight, rad);
751
760
  paragraph.style.width = bounds.width + 'px';
752
761
  paragraph.style.paddingTop = ((bounds.height - naturalHeight) / 2) + 'px';
753
762
  paragraph.style.paddingBottom = ((bounds.height - naturalHeight) / 2) + 'px';
@@ -763,6 +772,7 @@ function* _rapierSharedPageLayoutSteps(root) {
763
772
  if (layout.width != null || image.getAttribute('width')) image.style.width = '100%';
764
773
  }
765
774
  if (layout.rotate) { image.style.transform = 'rotate(' + layout.rotate + 'deg)'; image.style.transformOrigin = '50% 50%'; }
775
+ if (turned) paragraph.style.setProperty('shape-outside', 'polygon(' + turned + ') padding-box');
766
776
  if (layout.wrap === 'around' && !layout.rotate && !layout.y && layout.width == null && !image.dataset.rapierContour?.endsWith('-box')) {
767
777
  const src = image.getAttribute('src');
768
778
  if (src) {
@@ -779,15 +789,16 @@ function* _rapierSharedPageLayoutSteps(root) {
779
789
  const layout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
780
790
  if (layout?.wrap !== 'behind' && layout?.wrap !== 'front') continue;
781
791
  const paragraph = _rapierSharePictureParagraph(image);
782
- if (paragraph) positionedAssignments.push({image, paragraph, layout});
792
+ if (paragraph) positionedAssignments.push({image, paragraph, layout, owner: globalThis.RapierMarkdownLayout.wrapNeighbour(paragraph, _rapierShareWrapKind)});
783
793
  }
784
- for (const {image, paragraph, layout} of positionedAssignments) {
794
+ for (const {image, paragraph, layout, owner} of positionedAssignments) {
785
795
  yield;
786
796
  paragraph.style.cssText = 'position:relative;height:0;margin:0;padding:0;line-height:0';
787
797
  const x = layout.x, naturalWidth = Number(image.getAttribute('width')) || 0;
788
- if (layout.width != null) image.style.width = layout.width + '%';
798
+ if (layout.lines != null) { image.style.width = 'auto'; image.style.height = globalThis.RapierMarkdownLayout.linesHeightCss(layout.lines); }
799
+ else if (layout.width != null) image.style.width = layout.width + '%';
789
800
  else if (naturalWidth > 0) image.style.width = naturalWidth + 'px';
790
- image.style.left = x == null ? '0' : layout.width != null ? (x - layout.width / 2) + '%'
801
+ image.style.left = x == null ? '0' : layout.width != null ? Math.max(0, Math.min(100 - layout.width, x - layout.width / 2)) + '%'
791
802
  : naturalWidth > 0 ? 'calc(' + x + '% - ' + (naturalWidth / 2) + 'px)' : x + '%';
792
803
  image.style.position = 'absolute';
793
804
  image.style.top = (layout.y || 0) + 'em';
@@ -795,6 +806,7 @@ function* _rapierSharedPageLayoutSteps(root) {
795
806
  if (layout.rotate) { image.style.transform = 'rotate(' + layout.rotate + 'deg)'; image.style.transformOrigin = '50% 50%'; }
796
807
  if (layout.wrap === 'behind') image.style.zIndex = '-1';
797
808
  paragraph.classList.add('rapier-positioned-picture');
809
+ if (owner && owner !== paragraph.nextElementSibling) owner.before(paragraph);
798
810
  }
799
811
  const positioned = new Set([...assignments, ...positionedAssignments].map(row => row.image));
800
812
  for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
@@ -807,11 +819,13 @@ function* _rapierSharedPageLayoutSteps(root) {
807
819
  const naturalWidth = Number(image.getAttribute('width')) || 0, naturalHeight = Number(image.getAttribute('height')) || 0;
808
820
  if (naturalWidth > 0 && naturalHeight > 0) {
809
821
  const rad = layout.rotate * Math.PI / 180;
810
- const boundWidth = naturalWidth * Math.abs(Math.cos(rad)) + naturalHeight * Math.abs(Math.sin(rad));
811
- const boundHeight = naturalWidth * Math.abs(Math.sin(rad)) + naturalHeight * Math.abs(Math.cos(rad));
822
+ const width = layout.width != null && !layout.lines ? layout.width : naturalWidth, height = layout.width != null && !layout.lines ? layout.width * naturalHeight / naturalWidth : naturalHeight;
823
+ const unit = layout.width != null && !layout.lines ? '%' : 'px';
824
+ const boundWidth = width * Math.abs(Math.cos(rad)) + height * Math.abs(Math.sin(rad));
825
+ const boundHeight = width * Math.abs(Math.sin(rad)) + height * Math.abs(Math.cos(rad));
812
826
  image.style.display = 'block';
813
- image.style.marginLeft = image.style.marginRight = ((boundWidth - naturalWidth) / 2) + 'px';
814
- image.style.marginTop = image.style.marginBottom = ((boundHeight - naturalHeight) / 2) + 'px';
827
+ image.style.marginLeft = image.style.marginRight = ((boundWidth - width) / 2) + unit;
828
+ image.style.marginTop = image.style.marginBottom = ((boundHeight - height) / 2) + unit;
815
829
  }
816
830
  }
817
831
  }
@@ -1349,6 +1363,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1349
1363
  balance: 0};
1350
1364
  }
1351
1365
 
1366
+ let inkIds = 0;
1352
1367
  function fragmentNode(record, fragment, idsKept) {
1353
1368
  const run = record.runs[fragment.itemIndex], mapped = geometry.mapFragment(run, fragment, {whiteSpace: record.whiteSpace});
1354
1369
  if (!mapped) return null;
@@ -1374,6 +1389,8 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1374
1389
  if (!plan) return false;
1375
1390
  const output = doc.createDocumentFragment();
1376
1391
  const idsKept = new Set();
1392
+ for (const run of record.runs) for (const parent of run.parents)
1393
+ if (parent.matches?.('span.rapier-ink-mark[data-rapier-ink]') && !parent.hasAttribute('data-rapier-ink-id')) parent.setAttribute('data-rapier-ink-id', String(++inkIds));
1377
1394
  let previous = null;
1378
1395
  for (const line of plan.lines) {
1379
1396
  const element = doc.createElement('span');
@@ -1,11 +1,23 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import {next, weight, exceptions, maximum} from '../agent/vendor/hyphen-en-us/patterns.js';
3
3
 
4
- function englishCuts(word) {
5
- if (!/^[A-Z]?[a-z]+$/.test(word)) return null;
4
+ export function englishCuts(word) {
5
+ if (!/^[A-Za-z]+$/.test(word)) return null;
6
+ if (word.length < 6) return [];
7
+ const cuts = [];
8
+ let offset = 0, before = 0;
9
+ for (const part of word.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+/g)) {
10
+ if (before >= 3 && part.length >= 3) cuts.push(offset);
11
+ for (const at of partCuts(part)) cuts.push(offset+at);
12
+ offset += part.length; before = part.length;
13
+ }
14
+ return cuts.filter(at => at >= 3 && at <= word.length-3).sort((a, b) => a-b).filter((at, index, all) => at !== all[index-1]);
15
+ }
16
+
17
+ function partCuts(word) {
6
18
  if (word.length < 6) return [];
7
19
  const lower = word.toLowerCase(), known = exceptions[lower];
8
- if (Array.isArray(known)) return known.filter(at => at >= 3 && at <= word.length-3);
20
+ if (Array.isArray(known)) return known;
9
21
  const source = '.'+lower+'.', weights = new Uint8Array(source.length+1);
10
22
  for (let start = 0; start < source.length; start++) {
11
23
  let node = 0;
@@ -12,7 +12,8 @@ export function mapFragment(run, fragment, {whiteSpace = 'normal'} = {}) {
12
12
  const raw = typeof run === 'string' ? run : run?.raw;
13
13
  if (run?.break === 'never') whiteSpace = 'normal';
14
14
  const start = fragment?.sourceStart, end = fragment?.sourceEnd;
15
- if (typeof raw !== 'string' || typeof fragment?.text !== 'string' ||
15
+ const derived = fragment?.derive === true;
16
+ if (typeof raw !== 'string' || !derived && typeof fragment?.text !== 'string' ||
16
17
  !Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > raw.length ||
17
18
  whiteSpace !== 'normal' && whiteSpace !== 'pre-wrap') return null;
18
19
  const pieces = [], offsets = [start];
@@ -33,6 +34,13 @@ export function mapFragment(run, fragment, {whiteSpace = 'normal'} = {}) {
33
34
  }
34
35
  }
35
36
  let text = pieces.join('');
37
+ if (derived) {
38
+ if (fragment.glyph) {
39
+ if (lastSoftHyphen === end - 1) offsets[offsets.length - 1] = lastSoftHyphen;
40
+ offsets.push(end); text += fragment.glyph;
41
+ }
42
+ return {text, offsets, start, end};
43
+ }
36
44
  if (lastSoftHyphen === end - 1 && fragment.text === text + '-') {
37
45
  offsets[offsets.length - 1] = lastSoftHyphen;
38
46
  offsets.push(end); text += '-';
@@ -22,7 +22,7 @@ export function createParagraphFitter() {
22
22
  return handle;
23
23
  }
24
24
 
25
- function* solveSteps(handle, width, {indent = 0, left = 0, right = 0, widths = null} = {}, known) {
25
+ function* solveSteps(handle, width, {indent = 0, left = 0, right = 0, widths = null, shrink = .4, stretch = .8} = {}, known) {
26
26
  const prepared = indexes.get(handle);
27
27
  if (!prepared || ![width, indent, left, right].every(Number.isFinite) || width <= 0) return null;
28
28
  const measures = Array.isArray(widths) && widths.every(value => Number.isFinite(value) && value > 0) ? widths : [];
@@ -44,7 +44,7 @@ export function createParagraphFitter() {
44
44
  const closing = Math.min(Math.max(0, right), words.at(-1).closing);
45
45
  const target = measure(base) - (segmentIndex === 0 ? indent : 0) + opening + closing;
46
46
  const count = words.length - 1;
47
- const delta = !count ? 0 : Math.max(-smallest * .4, Math.min(0, (target - natural) / count));
47
+ const delta = !count ? 0 : Math.max(-smallest * shrink, Math.min(0, (target - natural) / count));
48
48
  const overflow = natural + delta * count - target;
49
49
  const cost = 1 + 100 * Math.max(0, overflow / target) ** 2;
50
50
  if (Number.isFinite(natural) && target > 0 && cost < 2 && minimum[words.length - 1] - minimum[Math.min(words.length - 1, 1)] <= ceiling + .02 &&
@@ -111,7 +111,7 @@ export function createParagraphFitter() {
111
111
  const target = measure(base + lineClass) - (segmentIndex === 0 && startIndex === 0 ? indent : 0) + opening + closing;
112
112
  if (target <= 0) continue;
113
113
  const remaining = target - natural;
114
- const delta = !count ? 0 : Math.max(-smallest * .4, Math.min(final ? 0 : smallest * .8, remaining / count));
114
+ const delta = !count ? 0 : Math.max(-smallest * shrink, Math.min(final ? 0 : smallest * stretch, remaining / count));
115
115
  const overflow = natural + delta * count - target;
116
116
  if (overflow > .02 && (count || start.cut || end.cut || words[lastWord].breaks.length)) continue;
117
117
  const residual = Math.max(0, remaining - delta * count);