rapier-markdown-kit 1.1.104 → 1.2.5
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/LICENSE +8 -4
- package/README.md +50 -7
- package/dist/images/gif.mjs +49 -0
- package/dist/images/playback.mjs +103 -0
- package/dist/kit/conformance/03-wrap-around-silhouette.md +1 -1
- package/dist/kit/conformance/04-wrap-box.md +1 -1
- package/dist/kit/conformance/05-behind.md +1 -1
- package/dist/kit/conformance/06-front.md +1 -1
- package/dist/kit/conformance/07-rotate-raster.md +1 -1
- package/dist/kit/conformance/08-drawing-rotation.md +1 -1
- package/dist/kit/conformance/09-drawing-ring-interior.expected.json +18 -18
- package/dist/kit/conformance/09-drawing-ring-interior.md +1 -1
- package/dist/kit/conformance/10-both-sides.md +1 -1
- package/dist/kit/conformance/11-neighbour-skips-image.md +1 -1
- package/dist/kit/conformance/{12-heading-barrier.expected.json → 12-rule-barrier.expected.json} +1 -1
- package/dist/kit/conformance/{12-heading-barrier.md → 12-rule-barrier.md} +2 -2
- package/dist/kit/conformance/README.md +19 -28
- package/dist/kit/conformance/run.mjs +10 -8
- package/dist/kit/ledger/authored-origin.mjs +49 -0
- package/dist/kit/ledger/authorship.mjs +18 -9
- package/dist/kit/ledger/carried.mjs +10 -5
- package/dist/kit/ledger/data.mjs +7 -4
- package/dist/kit/ledger/format.mjs +37 -11
- package/dist/kit/ledger/hash.mjs +31 -15
- package/dist/kit/ledger/history.mjs +48 -10
- package/dist/kit/ledger/inverse-source.mjs +111 -49
- package/dist/kit/ledger/journal-records.mjs +24 -29
- package/dist/kit/ledger/merge.mjs +51 -15
- package/dist/kit/ledger/text-merge.mjs +75 -29
- package/dist/kit/ledger/transport.mjs +30 -45
- package/dist/kit/pdf-vectors.mjs +6 -2
- package/dist/kit/print-capitals.mjs +0 -1
- package/dist/kit/rapier-ledger.mjs +4 -2
- package/dist/kit/render-ink.mjs +1 -1
- package/dist/kit/render-markdown.mjs +155 -31
- package/dist/kit/render-print.mjs +173 -61
- package/dist/kit/render-sanitize.mjs +25 -15
- package/dist/kit/render-styles.mjs +3 -3
- package/dist/kit/render.mjs +37 -115
- package/dist/layout/model.mjs +10 -8
- package/dist/layout/paragraph-fit.mjs +1 -15
- package/dist/layout/typography.mjs +2 -1
- package/dist/packages/rapier-diff/concerns.mjs +163 -0
- package/dist/packages/rapier-diff/diff.mjs +68 -0
- package/dist/packages/rapier-diff/index.mjs +4 -701
- package/dist/packages/rapier-diff/matching.mjs +157 -0
- package/dist/packages/rapier-diff/merge.mjs +226 -0
- package/dist/packages/rapier-diff/moves.mjs +130 -0
- package/dist/packages/rapier-diff/structure.mjs +50 -0
- package/dist/packages/rapier-diff/tokens.mjs +72 -0
- package/dist/packages/rapier-diff/walk.mjs +219 -0
- package/dist/spec/document-fonts.mjs +1 -1
- package/dist/spec/document-settings.mjs +11 -164
- package/dist/spec/html-blocks.mjs +618 -79
- package/dist/spec/md-assets.mjs +29 -6
- package/dist/spec/md-layout.mjs +9 -1
- package/dist/spec/md-marks.mjs +25 -147
- package/dist/spec/md-source.mjs +69 -26
- package/package.json +1 -1
- package/style.css +7 -51
|
@@ -75,7 +75,7 @@ function _rapierPrintWillFont(markers) {
|
|
|
75
75
|
const bounds = [0, -400, 10, 0];
|
|
76
76
|
const outline = cat(u16(1), bounds.map(u16), u16(2), u16(0), [1, 1, 1], u16(0), u16(10), u16(-5), u16(bounds[1]), u16(bounds[3] - bounds[1]), u16((bounds[1] - bounds[3]) / 2), [0]);
|
|
77
77
|
const utf16 = text => Array.from(text, ch => u16(ch.charCodeAt(0)));
|
|
78
|
-
const names = [[1, '
|
|
78
|
+
const names = [[1, 'RapierWillCarrier'], [2, 'Regular'], [4, 'RapierWillCarrier Regular'], [6, 'RapierWillCarrier-Regular']];
|
|
79
79
|
let offset = 0;
|
|
80
80
|
const tables = {
|
|
81
81
|
'OS/2': cat(u16(4), u16(advance), u16(400), u16(5), u16(0), u16(650), u16(600), u16(0), u16(75), u16(650), u16(600), u16(0), u16(350),
|
|
@@ -175,7 +175,7 @@ function _rapierPrintWillPageCss() {
|
|
|
175
175
|
// The capture's own limits (agent/visual.mjs VISUAL_LIMITS): the longest edge and the most pixels one capture returns.
|
|
176
176
|
const CAPTURE_EDGE = 4096, CAPTURE_PIXELS = 4 * 1024 * 1024;
|
|
177
177
|
|
|
178
|
-
async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings}, {capture, signal, current, maxBytes = 8 * 1024 * 1024} = {}) {
|
|
178
|
+
async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings}, {capture, signal, current, maxBytes = 8 * 1024 * 1024, film, ink} = {}) {
|
|
179
179
|
const refuse = code => { throw Object.assign(new Error(code), {code}); };
|
|
180
180
|
const check = () => {
|
|
181
181
|
if (signal?.aborted) refuse('cancelled');
|
|
@@ -193,7 +193,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
193
193
|
const bodyHeight = Math.floor((height - margins.top - margins.bottom) * 4 / 3);
|
|
194
194
|
if (bodyWidth < 1 || bodyHeight < 1 || bodyWidth * bodyHeight > 4 * 1024 * 1024) refuse('pdf_page_geometry_invalid');
|
|
195
195
|
const frame = document.createElement('iframe');
|
|
196
|
-
let vectors, nativeGlyphs;
|
|
196
|
+
let vectors, nativeGlyphs, filmSession;
|
|
197
197
|
let rejectAbort;
|
|
198
198
|
const aborted = new Promise((_, reject) => { rejectAbort = reject; });
|
|
199
199
|
aborted.catch(() => {});
|
|
@@ -273,6 +273,8 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
273
273
|
globalThis.RapierMath?.fit(main);
|
|
274
274
|
applyPrintCapitals(main);
|
|
275
275
|
applyTypography(main, {all: true});
|
|
276
|
+
// The document's ink marks, drawn on the words where they now stand on the page.
|
|
277
|
+
ink?.(main);
|
|
276
278
|
const count = Math.max(1, Math.ceil((main.scrollWidth - 1) / bodyWidth));
|
|
277
279
|
if (!Number.isSafeInteger(count)) refuse('pdf_page_geometry_invalid');
|
|
278
280
|
// Captured document font bytes own a same-name family; factory glyphs must
|
|
@@ -296,48 +298,59 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
296
298
|
}
|
|
297
299
|
const font = _rapierPrintWillFont(text);
|
|
298
300
|
strip.style.width = count * bodyWidth + 'px';
|
|
299
|
-
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
strip.style.transform = 'none';
|
|
302
|
+
// One styled film is reused for every page window. Batches stay inside the capture edge
|
|
303
|
+
// and pixel limits. A picture no longer forces its own capture. Pixels are deflated once.
|
|
304
|
+
if (typeof film !== 'function') refuse('pdf_render_unavailable');
|
|
305
|
+
// step() can reject after the film resolves (the document changed, or the attempt was
|
|
306
|
+
// cancelled). The session would otherwise keep its abort listener with no owner.
|
|
307
|
+
let pendingFilm = null, abandonFilm = false;
|
|
308
|
+
const trackedFilm = Promise.resolve(film({root: surface, signal, current, frame: {width: count * bodyWidth, height: bodyHeight}})).then(session => {
|
|
309
|
+
if (abandonFilm) session?.close?.();
|
|
310
|
+
else pendingFilm = session;
|
|
311
|
+
return session;
|
|
312
|
+
});
|
|
313
|
+
try {
|
|
314
|
+
filmSession = await step(trackedFilm);
|
|
315
|
+
pendingFilm = null;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
abandonFilm = true;
|
|
318
|
+
pendingFilm?.close?.();
|
|
319
|
+
pendingFilm = null;
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
let share = Math.max(1, Math.min(Math.floor(CAPTURE_EDGE / bodyWidth), Math.floor(CAPTURE_PIXELS / (bodyWidth * bodyHeight))));
|
|
304
323
|
const pages = [];
|
|
305
324
|
let imageBytes = 0;
|
|
325
|
+
const pack = async rgb => {
|
|
326
|
+
const packed = new Uint8Array(await step(new Response(new Blob([rgb]).stream().pipeThrough(new CompressionStream('deflate'))).arrayBuffer()));
|
|
327
|
+
imageBytes += packed.byteLength;
|
|
328
|
+
if (imageBytes > maxBytes) refuse('export_too_large');
|
|
329
|
+
pages.push({rgb: packed});
|
|
330
|
+
};
|
|
306
331
|
for (let first = 0; first < count;) {
|
|
307
332
|
check();
|
|
308
333
|
const span = Math.min(share, count - first);
|
|
309
|
-
surface.style.width = span * bodyWidth + 'px';
|
|
310
|
-
strip.style.transform = `translateX(${-first * bodyWidth}px)`;
|
|
311
|
-
let captured;
|
|
312
|
-
try { captured = await capture({root: surface, clip: {x: 0, y: 0, width: span * bodyWidth, height: bodyHeight}, signal, current}); }
|
|
313
|
-
catch (error) {
|
|
314
|
-
if (error?.code === 'visual_too_large' && span > 1) { share = Math.ceil(span / 2); continue; }
|
|
315
|
-
throw error;
|
|
316
|
-
}
|
|
317
|
-
check();
|
|
318
|
-
const image = new into.defaultView.Image();
|
|
319
|
-
image.src = 'data:' + captured.mimeType + ';base64,' + captured.data;
|
|
320
334
|
try {
|
|
321
|
-
await step(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
context.fillStyle = '#fff'; context.fillRect(0, 0, bodyWidth, bodyHeight);
|
|
328
|
-
context.drawImage(image, offset * bodyWidth, 0, bodyWidth, bodyHeight, 0, 0, bodyWidth, bodyHeight);
|
|
329
|
-
// Preserve the capture's exact pixels. A JPEG re-encode changes
|
|
330
|
-
// glyph edges, pictures and paint even at its highest quality.
|
|
331
|
-
const rgba = context.getImageData(0, 0, bodyWidth, bodyHeight).data;
|
|
335
|
+
const shot = await step(filmSession.raster({x: first * bodyWidth, y: 0, width: span * bodyWidth, height: bodyHeight}));
|
|
336
|
+
if (shot?.width !== span * bodyWidth || shot?.height !== bodyHeight || !(shot.rgb instanceof Uint8Array)) refuse('pdf_render_unavailable');
|
|
337
|
+
if (span === 1) await pack(shot.rgb);
|
|
338
|
+
else {
|
|
339
|
+
const row = bodyWidth * 3;
|
|
340
|
+
for (let offset = 0; offset < span; offset++) {
|
|
332
341
|
const rgb = new Uint8Array(bodyWidth * bodyHeight * 3);
|
|
333
|
-
for (let
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
342
|
+
for (let y = 0; y < bodyHeight; y++) {
|
|
343
|
+
const from = (y * shot.width + offset * bodyWidth) * 3;
|
|
344
|
+
rgb.set(shot.rgb.subarray(from, from + row), y * row);
|
|
345
|
+
}
|
|
346
|
+
await pack(rgb);
|
|
347
|
+
}
|
|
339
348
|
}
|
|
340
|
-
|
|
349
|
+
shot.rgb = null;
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (error?.code === 'visual_too_large' && span > 1) { share = Math.ceil(span / 2); continue; }
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
341
354
|
first += span;
|
|
342
355
|
}
|
|
343
356
|
check();
|
|
@@ -345,7 +358,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
345
358
|
pixelWidth: bodyWidth, pixelHeight: bodyHeight, pageNumbers: settings?.pagestyle === 'plain', maxBytes, check});
|
|
346
359
|
check();
|
|
347
360
|
return {bytes, pages: count};
|
|
348
|
-
} finally { nativeGlyphs?.restore(); vectors?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
|
|
361
|
+
} finally { filmSession?.close(); nativeGlyphs?.restore(); vectors?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
|
|
349
362
|
}
|
|
350
363
|
|
|
351
364
|
// Ranges locate the actual text fragments after column layout. Splitting a text node at
|
|
@@ -355,7 +368,7 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
355
368
|
const pages = Array.from({length: count}, () => []), walker = into.createTreeWalker(main, 5);
|
|
356
369
|
const markerMetrics = _rapierPrintWillMetrics(Array.from(main.querySelectorAll('.rapier-will-carrier'), node => node.textContent));
|
|
357
370
|
const markerLost = () => { throw _rapierPrintWillLost('a marker has no readable position on the page', 'Try another browser'); };
|
|
358
|
-
const range = into.createRange();
|
|
371
|
+
const range = into.createRange(), segmenter = new Intl.Segmenter(undefined, {granularity: 'grapheme'});
|
|
359
372
|
let nodes = 0, operations = 0;
|
|
360
373
|
for (let node; (node = walker.nextNode());) {
|
|
361
374
|
if (++nodes % 128 === 0) { await new Promise(resolve => setTimeout(resolve, 0)); check(); }
|
|
@@ -374,6 +387,7 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
374
387
|
const preserve = /pre|break-spaces/.test(style.whiteSpace);
|
|
375
388
|
const block = element.closest('p,h1,h2,h3,h4,h5,h6,li,pre,td,th,figcaption,blockquote,.rapier-will-carrier') || element;
|
|
376
389
|
const native = nativeNodes?.get(node);
|
|
390
|
+
let characters = null;
|
|
377
391
|
const pageOf = x => Math.max(0, Math.min(count - 1, Math.floor((x - origin.left + .01) / pageWidth)));
|
|
378
392
|
const visit = (start, end) => {
|
|
379
393
|
if (++operations % 128 === 0) check();
|
|
@@ -385,7 +399,8 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
385
399
|
const first = rects[0], page = pageOf(first.left);
|
|
386
400
|
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);
|
|
387
401
|
let middle = formula ? start : Math.floor((start + end) / 2);
|
|
388
|
-
|
|
402
|
+
// A line never breaks inside a character: halve between whole ones (a letter and its marks, a conjunct, an emoji).
|
|
403
|
+
if (middle > start) middle = (characters ||= segmenter.segment(value)).containing(middle).index;
|
|
389
404
|
const cluster = native?.find(item => item.start < middle && item.end > middle);
|
|
390
405
|
if (cluster) middle = cluster.start > start ? cluster.start : cluster.end;
|
|
391
406
|
if (!oneLine && middle > start && middle < end) { visit(start, middle); visit(middle, end); return; }
|
|
@@ -398,7 +413,7 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
398
413
|
if (carrier && right === x) right += Array.from(text).length * markerMetrics.advance * markerMetrics.size / 1000;
|
|
399
414
|
const bottom = Math.max(...rects.map(rect => rect.bottom)) - origin.top;
|
|
400
415
|
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}];
|
|
416
|
+
: [{text, x, y, width: right - x, height: bottom - y, rtl: style.direction === 'rtl'}];
|
|
402
417
|
const last = pages[page].at(-1);
|
|
403
418
|
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)) {
|
|
404
419
|
if (!preserve && last.text.endsWith(' ') && text.startsWith(' ') && parts.length) {
|
|
@@ -416,7 +431,35 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
416
431
|
visit(0, value.length);
|
|
417
432
|
}
|
|
418
433
|
check();
|
|
419
|
-
|
|
434
|
+
// Trailing-carrier print CSS pulls a last marker up so it stays on the sheet.
|
|
435
|
+
// Keep that paint; put the searchable object after the preceding words so
|
|
436
|
+
// reading order matches the source (the marker itself has no visible ink).
|
|
437
|
+
const pageHeight = origin.height;
|
|
438
|
+
for (const page of pages) {
|
|
439
|
+
let prev = null;
|
|
440
|
+
for (const run of page) {
|
|
441
|
+
if (run.carrier && prev && run.y + 0.25 < prev.y + prev.height * .8) {
|
|
442
|
+
const next = Math.min(pageHeight - 0.5, prev.y + prev.height * .85);
|
|
443
|
+
if (next > run.y) {
|
|
444
|
+
const dy = next - run.y;
|
|
445
|
+
run.y += dy;
|
|
446
|
+
for (const part of run.parts || []) part.y += dy;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
prev = run;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Each run carries the tags of its block's structure (outermost first): a PDF reader finds the document's headings, lists and
|
|
453
|
+
// tables from them rather than from positions.
|
|
454
|
+
const ROLES = {TABLE: 'Table', TR: 'TR', TD: 'TD', TH: 'TH', UL: 'L', OL: 'L', LI: 'LI', BLOCKQUOTE: 'BlockQuote', P: 'P', PRE: 'Code', FIGCAPTION: 'Caption',
|
|
455
|
+
H1: 'H1', H2: 'H2', H3: 'H3', H4: 'H4', H5: 'H5', H6: 'H6'};
|
|
456
|
+
const tags = block => {
|
|
457
|
+
const chain = [];
|
|
458
|
+
for (let node = block; node && node !== main; node = node.parentElement)
|
|
459
|
+
if (ROLES[node.tagName] || node === block && node.matches('.rapier-will-carrier')) chain.unshift({node, role: ROLES[node.tagName] || 'P'});
|
|
460
|
+
return chain;
|
|
461
|
+
};
|
|
462
|
+
return pages.map(page => page.map(({block, ...run}) => ({...run, tags: tags(block)})));
|
|
420
463
|
}
|
|
421
464
|
|
|
422
465
|
function _rapierPrintLinkPages(main, width, height, count) {
|
|
@@ -450,6 +493,28 @@ function _rapierPrintLinkPages(main, width, height, count) {
|
|
|
450
493
|
return {_rapierWillSentinelPlan, _rapierPrintWillAdmit, _rapierPrintWillFont, _rapierPrintWillMetrics, _rapierPrintWillPlace, _rapierPrintWillLost, _rapierPrintWillPageCss, _rapierWritePrintPdf};
|
|
451
494
|
}
|
|
452
495
|
|
|
496
|
+
// Where each scalar of an invisible text run stands, in character slots from the run's left edge: a mark shares its letter's
|
|
497
|
+
// slot, a right-to-left word runs from the right, and so do the words of a right-to-left line (strong letters only; one line).
|
|
498
|
+
function _rapierPrintSlots(text, rtl) {
|
|
499
|
+
const pieces = [];
|
|
500
|
+
let scalars = 0, count = 0;
|
|
501
|
+
for (const {segment} of new Intl.Segmenter(undefined, {granularity: 'grapheme'}).segment(text)) {
|
|
502
|
+
const dir = /[\p{Script=Arabic}\p{Script=Hebrew}\p{Script=Syriac}\p{Script=Thaana}\p{Script=Nko}]/u.test(segment) ? 'r' : /\p{L}/u.test(segment) ? 'l' : '';
|
|
503
|
+
const cluster = {from: scalars, length: Array.from(segment).length}, last = pieces.at(-1);
|
|
504
|
+
scalars += cluster.length; count++;
|
|
505
|
+
if (last && (!dir || !last.dir || last.dir === dir)) { last.clusters.push(cluster); last.dir ||= dir; }
|
|
506
|
+
else pieces.push({dir, clusters: [cluster]});
|
|
507
|
+
}
|
|
508
|
+
const slots = new Array(scalars);
|
|
509
|
+
let slot = 0;
|
|
510
|
+
for (const piece of rtl ? pieces.reverse() : pieces) {
|
|
511
|
+
for (const cluster of (piece.dir || (rtl ? 'r' : 'l')) === 'r' ? piece.clusters.reverse() : piece.clusters) {
|
|
512
|
+
slots.fill(slot++, cluster.from, cluster.from + cluster.length);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return {slots, count};
|
|
516
|
+
}
|
|
517
|
+
|
|
453
518
|
// A standard PDF page tree, image streams, Unicode text and an associated source file.
|
|
454
519
|
// It accepts already-rendered pages, so there is no second document grammar or layout engine.
|
|
455
520
|
async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [], canonical, bom = false, filename, title, font, width, height, margins, pixelWidth, pixelHeight, pageNumbers, maxBytes, check}) {
|
|
@@ -487,6 +552,9 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
487
552
|
const descriptor = add('<< /Type /FontDescriptor /FontName /RapierText /Flags 4 /FontBBox [' + font.bounds.join(' ') + '] /ItalicAngle 0 /Ascent 500 /Descent -500 /CapHeight 500 /StemV 80 /FontFile2 ' + fontFile + ' 0 R >>');
|
|
488
553
|
const descendant = add('<< /Type /Font /Subtype /CIDFontType2 /BaseFont /RapierText /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ' + descriptor + ' 0 R /DW 500 /CIDToGIDMap /Identity >>');
|
|
489
554
|
const type = add('<< /Type /Font /Subtype /Type0 /BaseFont /RapierText /Encoding /Identity-H /DescendantFonts [' + descendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
|
|
555
|
+
const carrierDescriptor = add('<< /Type /FontDescriptor /FontName /RapierWillCarrier /Flags 4 /FontBBox [' + font.bounds.join(' ') + '] /ItalicAngle 0 /Ascent 500 /Descent -500 /CapHeight 500 /StemV 80 /FontFile2 ' + fontFile + ' 0 R >>');
|
|
556
|
+
const carrierDescendant = add('<< /Type /Font /Subtype /CIDFontType2 /BaseFont /RapierWillCarrier /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ' + carrierDescriptor + ' 0 R /DW 500 /CIDToGIDMap /Identity >>');
|
|
557
|
+
const carrierType = add('<< /Type /Font /Subtype /Type0 /BaseFont /RapierWillCarrier /Encoding /Identity-H /DescendantFonts [' + carrierDescendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
|
|
490
558
|
const numberFont = pageNumbers ? add('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>') : null;
|
|
491
559
|
const kids = pages.map(() => { objects.push(null); return objects.length - 1; }), decimal = value => Number(value.toFixed(4));
|
|
492
560
|
const nativeFonts = new Map();
|
|
@@ -515,24 +583,45 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
515
583
|
const type = add('<< /Type /Font /Subtype /Type0 /BaseFont /' + name + ' /Encoding /Identity-H /DescendantFonts [' + descendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
|
|
516
584
|
nativeResources.push(entry.name + ' ' + type + ' 0 R');
|
|
517
585
|
}
|
|
586
|
+
const sharedImages = new Map();
|
|
587
|
+
// The structure tree (PDF 1.7, 14.7): one element per tagged DOM element, each run marked with its page's next MCID.
|
|
588
|
+
const structs = new Map(), documentElement = {role: 'Document', kids: []}, parentTree = [];
|
|
589
|
+
const structOf = tags => {
|
|
590
|
+
let parent = documentElement;
|
|
591
|
+
for (const {node, role} of tags) {
|
|
592
|
+
let element = structs.get(node);
|
|
593
|
+
if (!element) {element = {role, kids: [], parent}; structs.set(node, element); parent.kids.push(element);}
|
|
594
|
+
parent = element;
|
|
595
|
+
}
|
|
596
|
+
return parent === documentElement ? null : parent;
|
|
597
|
+
};
|
|
518
598
|
for (let index = 0; index < pages.length; index++) {
|
|
519
599
|
check();
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
const
|
|
600
|
+
const marked = [];
|
|
601
|
+
parentTree.push(marked);
|
|
602
|
+
const held = pages[index], flate = !!held?.rgb;
|
|
603
|
+
const picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (flate ? 'FlateDecode' : 'DCTDecode'), flate ? held.rgb : held);
|
|
604
|
+
pages[index] = null;
|
|
605
|
+
// The page's picture is how its tagged text looks, not a figure of its own: an artifact.
|
|
606
|
+
let commands = '/Artifact BMC\nq\n' + [decimal(pixelWidth * .75), 0, 0, decimal(pixelHeight * .75), decimal(margins.left), decimal(height - margins.top - pixelHeight * .75)].join(' ') + ' cm\n/Im0 Do\nQ\nEMC\n';
|
|
607
|
+
const pictures = ['/Im0 ' + picture + ' 0 R'], states = [];
|
|
523
608
|
for (const drawing of vectorPages[index] || []) {
|
|
524
609
|
commands += `q\n.75 0 0 -.75 ${decimal(margins.left)} ${decimal(height - margins.top)} cm\n0 0 ${pixelWidth} ${pixelHeight} re W n\n1 0 0 1 ${decimal(drawing.x)} 0 cm\n`;
|
|
525
610
|
commands += [drawing.clip.x, drawing.clip.y, drawing.clip.width, drawing.clip.height].map(decimal).join(' ') + ' re W n\n';
|
|
526
611
|
for (const piece of drawing.sequence) {
|
|
527
612
|
if (piece.image) {
|
|
528
|
-
let
|
|
529
|
-
if (!
|
|
613
|
+
let shared = sharedImages.get(piece.image);
|
|
614
|
+
if (!shared) {
|
|
530
615
|
const image = piece.image;
|
|
531
616
|
const mask = stream('/Type /XObject /Subtype /Image /Width ' + image.width + ' /Height ' + image.height + ' /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode', image.alpha);
|
|
532
617
|
const object = stream('/Type /XObject /Subtype /Image /Width ' + image.width + ' /Height ' + image.height + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /SMask ' + mask + ' 0 R /Filter /FlateDecode', image.rgb);
|
|
533
|
-
|
|
618
|
+
image.alpha = image.rgb = null;
|
|
619
|
+
const name = '/Im' + (sharedImages.size + 1);
|
|
620
|
+
shared = {name, resource: name + ' ' + object + ' 0 R'};
|
|
621
|
+
sharedImages.set(piece.image, shared);
|
|
534
622
|
}
|
|
535
|
-
|
|
623
|
+
if (!pictures.includes(shared.resource)) pictures.push(shared.resource);
|
|
624
|
+
commands += `q\n${[piece.width, 0, 0, -piece.height, piece.x, piece.y + piece.height].map(decimal).join(' ')} cm\n${shared.name} Do\nQ\n`;
|
|
536
625
|
} else {
|
|
537
626
|
const state = '/Gs' + states.length;
|
|
538
627
|
states.push(state + ' ' + add('<< /Type /ExtGState /ca ' + decimal(piece.fillAlpha) + ' /CA ' + decimal(piece.strokeAlpha) + ' >>') + ' 0 R');
|
|
@@ -547,6 +636,8 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
547
636
|
// Keep one logical Unicode text object, resetting its font before each scalar's direction
|
|
548
637
|
// can change. Readers that ignore ActualText otherwise discard earlier words in mixed RTL/LTR.
|
|
549
638
|
// Express width in the font size: some readers ignore horizontal text scaling when joining.
|
|
639
|
+
const element = structOf(run.tags || []);
|
|
640
|
+
if (element) {element.kids.push({page: index, mcid: marked.length}); commands += `/${element.role} << /MCID ${marked.length} >> BDC\n`; marked.push(element);}
|
|
550
641
|
commands += `q\n/Span << /ActualText <feff${hex(run.text)}> >> BDC\nBT\n100 Tz\n`;
|
|
551
642
|
for (const part of run.parts || [run]) {
|
|
552
643
|
if (part.font) {
|
|
@@ -556,15 +647,16 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
556
647
|
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`;
|
|
557
648
|
} else {
|
|
558
649
|
const characters = Array.from(part.text); if (!characters.length) continue;
|
|
559
|
-
const
|
|
650
|
+
const {slots, count} = _rapierPrintSlots(part.text, part.rtl);
|
|
651
|
+
const advance = part.width * .75 / count, size = Math.max(.1, advance * 2), scaleY = Math.max(.1, part.height * .75) / size;
|
|
560
652
|
const baseline = decimal(height - margins.top - (part.y + part.height * .8) * .75);
|
|
561
653
|
for (let letter = 0; letter < characters.length; letter++) {
|
|
562
654
|
const value = codes.get(characters[letter].codePointAt(0)).toString(16).padStart(4, '0');
|
|
563
|
-
commands +=
|
|
655
|
+
commands += `${run.carrier ? '/Fc' : '/F1'} ${decimal(size)} Tf\n3 Tr\n1 0 0 ${decimal(scaleY)} ${decimal(margins.left + part.x * .75 + slots[letter] * advance)} ${baseline} Tm\n<${value}> Tj\n`;
|
|
564
656
|
}
|
|
565
657
|
}
|
|
566
658
|
}
|
|
567
|
-
commands += 'ET\nEMC\nQ\n';
|
|
659
|
+
commands += 'ET\nEMC\nQ\n' + (element ? 'EMC\n' : '');
|
|
568
660
|
}
|
|
569
661
|
if (numberFont) commands += `BT\n/F2 9 Tf\n0 Tr\n1 0 0 1 ${decimal(width / 2 - String(index + 1).length * 2.5)} ${decimal(margins.bottom / 2)} Tm\n(${index + 1}) Tj\nET\n`;
|
|
570
662
|
const content = stream('/Filter /FlateDecode', await compressed(encode(commands)));
|
|
@@ -575,19 +667,39 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
575
667
|
: '/Dest [' + kids[link.target.page] + ' 0 R /XYZ ' + decimal(margins.left + link.target.x * .75) + ' ' + decimal(height - margins.top - link.target.y * .75) + ' null]';
|
|
576
668
|
return add('<< /Type /Annot /Subtype /Link /Border [0 0 0] /Rect [' + rect.join(' ') + '] ' + target + ' >>');
|
|
577
669
|
});
|
|
578
|
-
put(kids[index], '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ' + decimal(width) + ' ' + decimal(height) + '] /Resources << /XObject << ' + pictures.join(' ') + ' >> /ExtGState << ' + states.join(' ') + ' >> /Font << /F1 ' + type + ' 0 R' + (numberFont ? ' /F2 ' + numberFont + ' 0 R' : '') + ' ' + nativeResources.join(' ') + ' >> >> /Contents ' + content + ' 0 R' + (annotations.length ? ' /Annots [' + annotations.map(id => id + ' 0 R').join(' ') + ']' : '') + ' >>');
|
|
670
|
+
put(kids[index], '<< /Type /Page /Parent 2 0 R /StructParents ' + index + ' /MediaBox [0 0 ' + decimal(width) + ' ' + decimal(height) + '] /Resources << /XObject << ' + pictures.join(' ') + ' >> /ExtGState << ' + states.join(' ') + ' >> /Font << /F1 ' + type + ' 0 R /Fc ' + carrierType + ' 0 R' + (numberFont ? ' /F2 ' + numberFont + ' 0 R' : '') + ' ' + nativeResources.join(' ') + ' >> >> /Contents ' + content + ' 0 R' + (annotations.length ? ' /Annots [' + annotations.map(id => id + ' 0 R').join(' ') + ']' : '') + ' >>');
|
|
579
671
|
}
|
|
580
672
|
put(2, '<< /Type /Pages /Count ' + kids.length + ' /Kids [' + kids.map(id => id + ' 0 R').join(' ') + '] >>');
|
|
581
|
-
|
|
673
|
+
const tree = add(''), ids = new Map([[documentElement, add('')]]);
|
|
674
|
+
const reserve = element => { for (const kid of element.kids) if (kid.kids) { ids.set(kid, add('')); reserve(kid); } };
|
|
675
|
+
reserve(documentElement);
|
|
676
|
+
for (const [element, id] of ids) put(id, '<< /Type /StructElem /S /' + element.role + ' /P ' + (element.parent ? ids.get(element.parent) : tree) + ' 0 R /K [' +
|
|
677
|
+
element.kids.map(kid => kid.kids ? ids.get(kid) + ' 0 R' : '<< /Type /MCR /Pg ' + kids[kid.page] + ' 0 R /MCID ' + kid.mcid + ' >>').join(' ') + '] >>');
|
|
678
|
+
const parents = add('<< /Nums [' + parentTree.map((marked, index) => index + ' [' + marked.map(element => ids.get(element) + ' 0 R').join(' ') + ']').join(' ') + '] >>');
|
|
679
|
+
put(tree, '<< /Type /StructTreeRoot /K [' + ids.get(documentElement) + ' 0 R] /ParentTree ' + parents + ' 0 R /ParentTreeNextKey ' + parentTree.length + ' >>');
|
|
680
|
+
put(1, '<< /Type /Catalog /Pages 2 0 R /MarkInfo << /Marked true >> /StructTreeRoot ' + tree + ' 0 R /Names << /EmbeddedFiles << /Names [<feff' + hex(filename) + '> ' + file + ' 0 R] >> >> /AF [' + file + ' 0 R] >>');
|
|
582
681
|
const info = add('<< /Producer (Rapier) /Title <feff' + hex(title || filename.replace(/\.[^.]*$/, '')) + '> >>');
|
|
583
|
-
const
|
|
584
|
-
let
|
|
682
|
+
const header = encode('%PDF-1.7\n%\u00e2\u00e3\u00cf\u00d3\n'), offsets = new Array(objects.length);
|
|
683
|
+
let cursor = header.length;
|
|
684
|
+
for (let id = 1; id < objects.length; id++) {
|
|
685
|
+
offsets[id] = cursor;
|
|
686
|
+
cursor += (id + ' 0 obj\n').length + objects[id].length + 8;
|
|
687
|
+
}
|
|
688
|
+
const xrefAt = cursor;
|
|
689
|
+
const xref = encode('xref\n0 ' + objects.length + '\n0000000000 65535 f \n' + offsets.slice(1).map(at => String(at).padStart(10, '0') + ' 00000 n \n').join('') + 'trailer\n<< /Size ' + objects.length + ' /Root 1 0 R /Info ' + info + ' 0 R >>\nstartxref\n' + xrefAt + '\n%%EOF\n');
|
|
690
|
+
if (cursor + xref.length > maxBytes) fail('export_too_large');
|
|
691
|
+
const bytes = new Uint8Array(cursor + xref.length);
|
|
692
|
+
bytes.set(header, 0);
|
|
693
|
+
let at = header.length;
|
|
585
694
|
for (let id = 1; id < objects.length; id++) {
|
|
586
|
-
|
|
587
|
-
|
|
695
|
+
const prefix = encode(id + ' 0 obj\n');
|
|
696
|
+
bytes.set(prefix, at); at += prefix.length;
|
|
697
|
+
bytes.set(objects[id], at); at += objects[id].length;
|
|
698
|
+
objects[id] = null;
|
|
699
|
+
const suffix = encode('\nendobj\n');
|
|
700
|
+
bytes.set(suffix, at); at += suffix.length;
|
|
588
701
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const bytes = concat(parts); if (bytes.length > maxBytes) fail('export_too_large'); return bytes;
|
|
702
|
+
bytes.set(xref, at);
|
|
703
|
+
return bytes;
|
|
592
704
|
}
|
|
593
705
|
export {createPrintRenderer, applyPrintCapitals, RAPIER_WILL_SENTINEL_MARK as sentinel};
|
|
@@ -7,7 +7,7 @@ const RAPIER_SANITIZE_PROFILES = Object.freeze({
|
|
|
7
7
|
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"]),
|
|
8
8
|
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"]),
|
|
9
9
|
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"]),
|
|
10
|
-
htmlAttributes: Object.freeze(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","
|
|
10
|
+
htmlAttributes: Object.freeze(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","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","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"]),
|
|
11
11
|
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"]),
|
|
12
12
|
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"]),
|
|
13
13
|
xmlAttributes: Object.freeze(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),
|
|
@@ -29,11 +29,6 @@ const RAPIER_SANITIZE_OPTIONS = Object.freeze({
|
|
|
29
29
|
FORBID_TAGS: RAPIER_SANITIZE_FORBID_TAGS,
|
|
30
30
|
FORBID_ATTR: RAPIER_SANITIZE_FORBID_ATTR,
|
|
31
31
|
}),
|
|
32
|
-
snippet: Object.freeze({
|
|
33
|
-
..._rapierSanitizeProfile('html'),
|
|
34
|
-
FORBID_TAGS: RAPIER_SANITIZE_FORBID_TAGS,
|
|
35
|
-
FORBID_ATTR: Object.freeze([...'style']),
|
|
36
|
-
}),
|
|
37
32
|
diagram: Object.freeze({
|
|
38
33
|
..._rapierSanitizeProfile('html', 'svg'),
|
|
39
34
|
FORBID_TAGS: Object.freeze(['form', 'dialog', 'template', 'iframe', 'script', 'foreignobject']),
|
|
@@ -59,6 +54,9 @@ const DIAGRAM_LABEL_SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
|
59
54
|
const DIAGRAM_LABEL_HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
60
55
|
const DIAGRAM_LABEL_MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
|
|
61
56
|
|
|
57
|
+
// The profile each DOMPurify instance holds (setConfig), shared by every renderer made over it.
|
|
58
|
+
const purifyConfigured = new WeakMap();
|
|
59
|
+
|
|
62
60
|
function _rapierImageDataAttributeAllowed(tag, attribute, value, verify) {
|
|
63
61
|
return (tag === 'img' && attribute === 'src' || tag === 'image' && /^(?:href|xlink:href)$/.test(attribute)
|
|
64
62
|
&& /^data:image\/(?:png|jpeg|webp|jxl);base64,/i.test(value)) && verify(value);
|
|
@@ -376,6 +374,8 @@ function _rapierInstallSanitizeHooks() {
|
|
|
376
374
|
});
|
|
377
375
|
}
|
|
378
376
|
|
|
377
|
+
const purifyOptions = new Map();
|
|
378
|
+
|
|
379
379
|
function sanitizeRapierHtml(value, context = 'render') {
|
|
380
380
|
return sanitize(value, context, false);
|
|
381
381
|
}
|
|
@@ -392,24 +392,31 @@ function sanitize(value, context, fragment) {
|
|
|
392
392
|
// (the hook) without an id the chrome owns. 'source' is Markdown source HTML, written and never shown: the render
|
|
393
393
|
// profile's admission, and the block on remote content stays the reader's (a remote picture's URL is the person's words).
|
|
394
394
|
const selected = context === 'raw' || context === 'export' || context === 'source' || RAPIER_SANITIZE_OPTIONS[context] ? context : 'render';
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
395
|
+
const labels = selected === 'diagram' || selected === 'export', key = selected + (fragment ? ' fragment' : '');
|
|
396
|
+
let options = purifyOptions.get(key);
|
|
397
|
+
if (!options) {
|
|
398
|
+
options = selected === 'raw' ? { ...RAPIER_SANITIZE_OPTIONS.render, ALLOW_DATA_ATTR: false } : RAPIER_SANITIZE_OPTIONS[selected] || RAPIER_SANITIZE_OPTIONS.render;
|
|
399
|
+
if (selected === 'source') options = {...options, ADD_DATA_URI_TAGS: ['image']};
|
|
399
400
|
// DOMPurify's USE_PROFILES replaces explicit allowlists. Add MathML to the
|
|
400
401
|
// selected profile without discarding its admitted HTML and SVG vocabulary.
|
|
401
|
-
options = {...options, ALLOWED_TAGS: [...new Set([...options.ALLOWED_TAGS, ...RAPIER_SANITIZE_PROFILES.mathTags])],
|
|
402
|
+
if (labels) options = {...options, ALLOWED_TAGS: [...new Set([...options.ALLOWED_TAGS, ...RAPIER_SANITIZE_PROFILES.mathTags])],
|
|
402
403
|
ALLOWED_ATTR: [...new Set([...options.ALLOWED_ATTR, ...RAPIER_SANITIZE_PROFILES.mathAttributes])], ADD_TAGS: ['foreignobject'], ADD_DATA_URI_TAGS: ['image'],
|
|
403
404
|
HTML_INTEGRATION_POINTS: {foreignobject: true}, FORBID_TAGS: options.FORBID_TAGS.filter(tag => tag !== 'foreignobject')};
|
|
405
|
+
if (fragment) options = {...options, RETURN_DOM_FRAGMENT: true};
|
|
406
|
+
purifyOptions.set(key, options);
|
|
407
|
+
}
|
|
408
|
+
if (labels) {
|
|
404
409
|
DOMPurify.addHook('uponSanitizeElement', diagramLabelElement);
|
|
405
410
|
DOMPurify.addHook('uponSanitizeAttribute', diagramLabelAttribute);
|
|
406
411
|
DOMPurify.addHook('afterSanitizeAttributes', diagramLabelContained);
|
|
407
412
|
}
|
|
408
|
-
|
|
413
|
+
// DOMPurify reparses a per-call configuration on every call (its cache compares its own copy), about 0.45 ms
|
|
414
|
+
// each in Chrome 155. A profile is set once and kept until another profile is asked for; every caller comes here.
|
|
415
|
+
if (purifyConfigured.get(DOMPurify) !== options) { DOMPurify.setConfig(options); purifyConfigured.set(DOMPurify, options); }
|
|
409
416
|
const previous = _rapierSanitizeRuntime.context;
|
|
410
417
|
_rapierSanitizeRuntime.context = selected;
|
|
411
418
|
try {
|
|
412
|
-
return DOMPurify.sanitize(value == null ? '' : String(value)
|
|
419
|
+
return DOMPurify.sanitize(value == null ? '' : String(value));
|
|
413
420
|
} finally {
|
|
414
421
|
if (labels) {
|
|
415
422
|
DOMPurify.removeHook('uponSanitizeElement', diagramLabelElement);
|
|
@@ -430,9 +437,11 @@ function escapeRapierHtmlText(value) {
|
|
|
430
437
|
}
|
|
431
438
|
|
|
432
439
|
function _rapierChromeOwnsId(id) {
|
|
433
|
-
// Every id the shell's own markup carries, read once before any document renders (initMarkdownIt primes it)
|
|
440
|
+
// Every id the shell's own markup carries, read once before any document renders (initMarkdownIt primes it),
|
|
441
|
+
// and every rapier- id: the chrome makes its dialogs, overlays, parts and plug-in list later, under that prefix
|
|
442
|
+
// (rapier-anchor- is the renderer's own name for a repeated document anchor, and stays the document's).
|
|
434
443
|
if (!_rapierSanitizeRuntime.chromeIds) _rapierSanitizeRuntime.chromeIds = new Set(Array.from(typeof document?.querySelectorAll === 'function' ? document.querySelectorAll('[id]') : [], element => element.id));
|
|
435
|
-
return _rapierSanitizeRuntime.chromeIds.has(String(id));
|
|
444
|
+
return _rapierSanitizeRuntime.chromeIds.has(String(id)) || /^rapier-(?!anchor-)/.test(id);
|
|
436
445
|
}
|
|
437
446
|
|
|
438
447
|
function _rapierSafeRasterDataUrl(value) {
|
|
@@ -522,6 +531,7 @@ function _rapierVerifyRasterBytes(bytes, mime) {
|
|
|
522
531
|
// declaration, doctype or generator/license comment before <svg itself, of whatever length its
|
|
523
532
|
|
|
524
533
|
if (mime === 'image/svg+xml') return /<svg[\s>]/i.test(new TextDecoder('utf-8', {fatal: false}).decode(bytes));
|
|
534
|
+
if (mime === 'image/gif') return bytes.length >= 13 && /^GIF8[79]a$/.test(String.fromCharCode(...bytes.subarray(0, 6)));
|
|
525
535
|
if (mime === 'image/png') return bytes.length >= 8 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47 && bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a;
|
|
526
536
|
if (mime === 'image/jpeg') return bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff;
|
|
527
537
|
if (mime === 'image/webp') return bytes.length >= 12 && String.fromCharCode(...bytes.slice(0, 4)) === 'RIFF' && String.fromCharCode(...bytes.slice(8, 12)) === 'WEBP';
|
|
@@ -103,9 +103,9 @@ function _rapierArtifactStyles(theme, includeFonts, printMode, houseDefaults = f
|
|
|
103
103
|
const highlightCss = _rapierStyleText('rapier-highlight-style');
|
|
104
104
|
parts.push(highlightCss);
|
|
105
105
|
// A page written for the system theme has no body.light to switch: under a light system
|
|
106
|
-
// setting
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
// setting each palette takes its light values from the sheet's own body.light rule.
|
|
107
|
+
if (theme === 'system') for (const [, palette, light] of highlightCss.matchAll(/html\[data-highlights="(standard|accent)"\] body\.light\s*\{([^}]*)\}/g))
|
|
108
|
+
parts.push('@media (prefers-color-scheme:light){html[data-rapier-theme="system"][data-highlights="' + palette + '"] body{' + light.trim() + '}}');
|
|
109
109
|
if (printMode) parts.push(_rapierArtifactPrintCss());
|
|
110
110
|
return parts.filter(Boolean).join('\n\n');
|
|
111
111
|
}
|