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.
- package/README.md +2 -2
- package/dist/kit/ledger/carried.mjs +14 -2
- package/dist/kit/ledger/text-merge.mjs +163 -26
- package/dist/kit/markdown-it.mjs +19 -5
- package/dist/kit/pdf-glyphs.mjs +2 -2
- package/dist/kit/pdf-vectors.mjs +29 -9
- package/dist/kit/render-markdown.mjs +8 -1
- package/dist/kit/render-print.mjs +67 -26
- package/dist/kit/render.mjs +36 -19
- package/dist/layout/hyphenation.mjs +15 -3
- package/dist/layout/model.mjs +9 -1
- package/dist/layout/paragraph-fit.mjs +3 -3
- package/dist/layout/typography.mjs +149 -29
- package/dist/spec/document-fonts.mjs +5 -0
- package/dist/spec/document-settings.mjs +7 -5
- package/dist/spec/html-blocks.mjs +30 -11
- package/dist/spec/template-page.mjs +2 -4
- package/package.json +1 -1
- package/style.css +29 -5
|
@@ -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
|
|
344
|
-
const packed = new Uint8Array(await step(new Response(new Blob([
|
|
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
|
-
|
|
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
|
-
}
|
|
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 &&
|
|
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);
|
|
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
|
|
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
|
-
|
|
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#
|
|
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
|
|
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;
|
package/dist/kit/render.mjs
CHANGED
|
@@ -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)
|
|
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+/=]
|
|
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 =
|
|
218
|
-
const margins = ['top', 'right', 'bottom', 'left'].map(side => (settings
|
|
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,
|
|
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.
|
|
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
|
|
811
|
-
const
|
|
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 -
|
|
814
|
-
image.style.marginTop = image.style.marginBottom = ((boundHeight -
|
|
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-
|
|
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
|
|
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;
|
package/dist/layout/model.mjs
CHANGED
|
@@ -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
|
-
|
|
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 *
|
|
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 *
|
|
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);
|