rapier-markdown-kit 1.1.102 → 1.2.3
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 +5 -2
- package/README.md +245 -54
- package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
- package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
- package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
- package/dist/agent/vendor/pretext/LICENSE +208 -0
- package/dist/agent/vendor/pretext/NOTICE +26 -13
- package/dist/agent/vendor/pretext/SOURCE.json +213 -45
- package/dist/agent/vendor/pretext/analysis.js +675 -1224
- package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
- package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
- package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
- package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
- package/dist/agent/vendor/pretext/graphemes.js +98 -0
- package/dist/agent/vendor/pretext/han-kerning.js +253 -0
- package/dist/agent/vendor/pretext/layout.js +352 -290
- package/dist/agent/vendor/pretext/line-break.js +1154 -588
- package/dist/agent/vendor/pretext/line-breaks.js +892 -0
- package/dist/agent/vendor/pretext/line-text.js +81 -39
- package/dist/agent/vendor/pretext/measurement.js +1105 -176
- package/dist/agent/vendor/pretext/prepare.js +807 -0
- package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
- package/dist/agent/will.mjs +2 -0
- package/dist/images/gif.mjs +47 -0
- package/dist/images/playback.mjs +101 -0
- package/dist/kit/assets.mjs +5 -1
- 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 +13 -11
- package/dist/kit/export-fonts.mjs +26 -11
- package/dist/kit/index.mjs +1 -0
- package/dist/kit/ledger/carried.mjs +17 -10
- package/dist/kit/ledger/data.mjs +73 -13
- package/dist/kit/ledger/format.mjs +176 -54
- package/dist/kit/ledger/hash.mjs +91 -18
- package/dist/kit/ledger/history.mjs +156 -116
- package/dist/kit/ledger/inverse-source.mjs +158 -19
- package/dist/kit/ledger/journal-records.mjs +292 -67
- package/dist/kit/ledger/merge.mjs +4 -3
- package/dist/kit/model.mjs +1 -1
- package/dist/kit/pdf-glyphs.mjs +4 -4
- package/dist/kit/pdf-vectors.mjs +4 -1
- package/dist/kit/rapier-ledger.mjs +2 -2
- package/dist/kit/render-ink.mjs +1 -1
- package/dist/kit/render-markdown.mjs +400 -148
- package/dist/kit/render-print.mjs +131 -55
- package/dist/kit/render-sanitize.mjs +30 -7
- package/dist/kit/render-work.mjs +99 -20
- package/dist/kit/render.mjs +133 -140
- package/dist/kit/typography.mjs +2 -0
- package/dist/layout/hyphenation.mjs +127 -0
- package/dist/layout/line-plan.mjs +88 -96
- package/dist/layout/model.mjs +71 -88
- package/dist/layout/paragraph-fit.mjs +191 -0
- package/dist/layout/preparation.mjs +373 -0
- package/dist/layout/prepared-paragraph.mjs +223 -0
- package/dist/layout/typography.mjs +345 -111
- package/dist/packages/rapier-diff/index.mjs +359 -224
- package/dist/spec/document-fonts.mjs +14 -5
- package/dist/spec/document-settings.mjs +11 -164
- package/dist/spec/html-blocks.mjs +618 -79
- package/dist/spec/md-assets.mjs +178 -36
- package/dist/spec/md-layout.mjs +13 -2
- package/dist/spec/md-marks.mjs +220 -127
- package/dist/spec/md-source.mjs +157 -0
- package/package.json +2 -1
- package/style.css +15 -50
- package/dist/agent/vendor/pretext/bidi.js +0 -151
- package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
|
@@ -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,6 +431,24 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
416
431
|
visit(0, value.length);
|
|
417
432
|
}
|
|
418
433
|
check();
|
|
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
|
+
}
|
|
419
452
|
return pages.map(page => page.map(({block, carrier, ...run}) => run));
|
|
420
453
|
}
|
|
421
454
|
|
|
@@ -450,6 +483,28 @@ function _rapierPrintLinkPages(main, width, height, count) {
|
|
|
450
483
|
return {_rapierWillSentinelPlan, _rapierPrintWillAdmit, _rapierPrintWillFont, _rapierPrintWillMetrics, _rapierPrintWillPlace, _rapierPrintWillLost, _rapierPrintWillPageCss, _rapierWritePrintPdf};
|
|
451
484
|
}
|
|
452
485
|
|
|
486
|
+
// Where each scalar of an invisible text run stands, in character slots from the run's left edge: a mark shares its letter's
|
|
487
|
+
// 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).
|
|
488
|
+
function _rapierPrintSlots(text, rtl) {
|
|
489
|
+
const pieces = [];
|
|
490
|
+
let scalars = 0, count = 0;
|
|
491
|
+
for (const {segment} of new Intl.Segmenter(undefined, {granularity: 'grapheme'}).segment(text)) {
|
|
492
|
+
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' : '';
|
|
493
|
+
const cluster = {from: scalars, length: Array.from(segment).length}, last = pieces.at(-1);
|
|
494
|
+
scalars += cluster.length; count++;
|
|
495
|
+
if (last && (!dir || !last.dir || last.dir === dir)) { last.clusters.push(cluster); last.dir ||= dir; }
|
|
496
|
+
else pieces.push({dir, clusters: [cluster]});
|
|
497
|
+
}
|
|
498
|
+
const slots = new Array(scalars);
|
|
499
|
+
let slot = 0;
|
|
500
|
+
for (const piece of rtl ? pieces.reverse() : pieces) {
|
|
501
|
+
for (const cluster of (piece.dir || (rtl ? 'r' : 'l')) === 'r' ? piece.clusters.reverse() : piece.clusters) {
|
|
502
|
+
slots.fill(slot++, cluster.from, cluster.from + cluster.length);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return {slots, count};
|
|
506
|
+
}
|
|
507
|
+
|
|
453
508
|
// A standard PDF page tree, image streams, Unicode text and an associated source file.
|
|
454
509
|
// It accepts already-rendered pages, so there is no second document grammar or layout engine.
|
|
455
510
|
async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [], canonical, bom = false, filename, title, font, width, height, margins, pixelWidth, pixelHeight, pageNumbers, maxBytes, check}) {
|
|
@@ -515,24 +570,31 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
515
570
|
const type = add('<< /Type /Font /Subtype /Type0 /BaseFont /' + name + ' /Encoding /Identity-H /DescendantFonts [' + descendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
|
|
516
571
|
nativeResources.push(entry.name + ' ' + type + ' 0 R');
|
|
517
572
|
}
|
|
573
|
+
const sharedImages = new Map();
|
|
518
574
|
for (let index = 0; index < pages.length; index++) {
|
|
519
575
|
check();
|
|
520
|
-
const
|
|
576
|
+
const held = pages[index], flate = !!held?.rgb;
|
|
577
|
+
const picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (flate ? 'FlateDecode' : 'DCTDecode'), flate ? held.rgb : held);
|
|
578
|
+
pages[index] = null;
|
|
521
579
|
let commands = 'q\n' + [decimal(pixelWidth * .75), 0, 0, decimal(pixelHeight * .75), decimal(margins.left), decimal(height - margins.top - pixelHeight * .75)].join(' ') + ' cm\n/Im0 Do\nQ\n';
|
|
522
|
-
const pictures = ['/Im0 ' + picture + ' 0 R'], states = []
|
|
580
|
+
const pictures = ['/Im0 ' + picture + ' 0 R'], states = [];
|
|
523
581
|
for (const drawing of vectorPages[index] || []) {
|
|
524
582
|
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
583
|
commands += [drawing.clip.x, drawing.clip.y, drawing.clip.width, drawing.clip.height].map(decimal).join(' ') + ' re W n\n';
|
|
526
584
|
for (const piece of drawing.sequence) {
|
|
527
585
|
if (piece.image) {
|
|
528
|
-
let
|
|
529
|
-
if (!
|
|
586
|
+
let shared = sharedImages.get(piece.image);
|
|
587
|
+
if (!shared) {
|
|
530
588
|
const image = piece.image;
|
|
531
589
|
const mask = stream('/Type /XObject /Subtype /Image /Width ' + image.width + ' /Height ' + image.height + ' /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode', image.alpha);
|
|
532
590
|
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
|
-
|
|
591
|
+
image.alpha = image.rgb = null;
|
|
592
|
+
const name = '/Im' + (sharedImages.size + 1);
|
|
593
|
+
shared = {name, resource: name + ' ' + object + ' 0 R'};
|
|
594
|
+
sharedImages.set(piece.image, shared);
|
|
534
595
|
}
|
|
535
|
-
|
|
596
|
+
if (!pictures.includes(shared.resource)) pictures.push(shared.resource);
|
|
597
|
+
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
598
|
} else {
|
|
537
599
|
const state = '/Gs' + states.length;
|
|
538
600
|
states.push(state + ' ' + add('<< /Type /ExtGState /ca ' + decimal(piece.fillAlpha) + ' /CA ' + decimal(piece.strokeAlpha) + ' >>') + ' 0 R');
|
|
@@ -556,11 +618,12 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
556
618
|
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
619
|
} else {
|
|
558
620
|
const characters = Array.from(part.text); if (!characters.length) continue;
|
|
559
|
-
const
|
|
621
|
+
const {slots, count} = _rapierPrintSlots(part.text, part.rtl);
|
|
622
|
+
const advance = part.width * .75 / count, size = Math.max(.1, advance * 2), scaleY = Math.max(.1, part.height * .75) / size;
|
|
560
623
|
const baseline = decimal(height - margins.top - (part.y + part.height * .8) * .75);
|
|
561
624
|
for (let letter = 0; letter < characters.length; letter++) {
|
|
562
625
|
const value = codes.get(characters[letter].codePointAt(0)).toString(16).padStart(4, '0');
|
|
563
|
-
commands += `/F1 ${decimal(size)} Tf\n3 Tr\n1 0 0 ${decimal(scaleY)} ${decimal(margins.left + part.x * .75 + letter * advance)} ${baseline} Tm\n<${value}> Tj\n`;
|
|
626
|
+
commands += `/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
627
|
}
|
|
565
628
|
}
|
|
566
629
|
}
|
|
@@ -580,14 +643,27 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
580
643
|
put(2, '<< /Type /Pages /Count ' + kids.length + ' /Kids [' + kids.map(id => id + ' 0 R').join(' ') + '] >>');
|
|
581
644
|
put(1, '<< /Type /Catalog /Pages 2 0 R /Names << /EmbeddedFiles << /Names [<feff' + hex(filename) + '> ' + file + ' 0 R] >> >> /AF [' + file + ' 0 R] >>');
|
|
582
645
|
const info = add('<< /Producer (Rapier) /Title <feff' + hex(title || filename.replace(/\.[^.]*$/, '')) + '> >>');
|
|
583
|
-
const
|
|
584
|
-
let
|
|
646
|
+
const header = encode('%PDF-1.7\n%\u00e2\u00e3\u00cf\u00d3\n'), offsets = new Array(objects.length);
|
|
647
|
+
let cursor = header.length;
|
|
648
|
+
for (let id = 1; id < objects.length; id++) {
|
|
649
|
+
offsets[id] = cursor;
|
|
650
|
+
cursor += (id + ' 0 obj\n').length + objects[id].length + 8;
|
|
651
|
+
}
|
|
652
|
+
const xrefAt = cursor;
|
|
653
|
+
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');
|
|
654
|
+
if (cursor + xref.length > maxBytes) fail('export_too_large');
|
|
655
|
+
const bytes = new Uint8Array(cursor + xref.length);
|
|
656
|
+
bytes.set(header, 0);
|
|
657
|
+
let at = header.length;
|
|
585
658
|
for (let id = 1; id < objects.length; id++) {
|
|
586
|
-
|
|
587
|
-
|
|
659
|
+
const prefix = encode(id + ' 0 obj\n');
|
|
660
|
+
bytes.set(prefix, at); at += prefix.length;
|
|
661
|
+
bytes.set(objects[id], at); at += objects[id].length;
|
|
662
|
+
objects[id] = null;
|
|
663
|
+
const suffix = encode('\nendobj\n');
|
|
664
|
+
bytes.set(suffix, at); at += suffix.length;
|
|
588
665
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const bytes = concat(parts); if (bytes.length > maxBytes) fail('export_too_large'); return bytes;
|
|
666
|
+
bytes.set(xref, at);
|
|
667
|
+
return bytes;
|
|
592
668
|
}
|
|
593
669
|
export {createPrintRenderer, applyPrintCapitals, RAPIER_WILL_SENTINEL_MARK as sentinel};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {typography} from '../spec/md-marks.mjs';
|
|
2
3
|
const RAPIER_SANITIZE_FORBID_TAGS = Object.freeze(['form', 'style', 'dialog', 'template', 'iframe']);
|
|
3
4
|
const RAPIER_SANITIZE_FORBID_ATTR = Object.freeze(['action', 'formaction', 'form', 'for']);
|
|
4
5
|
// The document profiles use one allowlist in a browser and in an inert server parser.
|
|
@@ -6,7 +7,7 @@ const RAPIER_SANITIZE_PROFILES = Object.freeze({
|
|
|
6
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"]),
|
|
7
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"]),
|
|
8
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"]),
|
|
9
|
-
htmlAttributes: Object.freeze(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","
|
|
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"]),
|
|
10
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"]),
|
|
11
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"]),
|
|
12
13
|
xmlAttributes: Object.freeze(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),
|
|
@@ -166,7 +167,7 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
|
|
|
166
167
|
if (!_rapierImageDataAttributeAllowed(tag, key, compact, safeRasterDataUrl)) continue;
|
|
167
168
|
} else if (!uri.test(compact)) continue;
|
|
168
169
|
}
|
|
169
|
-
attribs[name] = value;
|
|
170
|
+
attribs[name] = selected === 'raw' && tag === 'span' && key === 'style' ? typography.style(typography.parseStyle(value)) : value;
|
|
170
171
|
}
|
|
171
172
|
if ((tag === 'a' || tag === 'area') && attribs.target?.trim().toLowerCase() === '_blank') attribs.rel = 'noopener noreferrer';
|
|
172
173
|
return {tagName, attribs};
|
|
@@ -323,6 +324,11 @@ function _rapierInstallSanitizeHooks() {
|
|
|
323
324
|
});
|
|
324
325
|
DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
|
|
325
326
|
const value = String(data.attrValue == null ? '' : data.attrValue);
|
|
327
|
+
if (_rapierSanitizeRuntime.context === 'raw' && node?.nodeName === 'SPAN' && data.attrName?.toLowerCase() === 'style') {
|
|
328
|
+
data.attrValue = typography.style(typography.parseStyle(value));
|
|
329
|
+
data.keepAttr = !!data.attrValue;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
326
332
|
// A document's own HTML never carries an id the chrome owns, remote content allowed or not: the
|
|
327
333
|
// allow is about what the page fetches, and this rule is about what the page's chrome finds.
|
|
328
334
|
if (String(data.attrName || '').toLowerCase() === 'id' && _rapierSanitizeRuntime.context === 'raw' && _rapierChromeOwnsId(value)) { data.keepAttr = false; return; }
|
|
@@ -371,6 +377,16 @@ function _rapierInstallSanitizeHooks() {
|
|
|
371
377
|
}
|
|
372
378
|
|
|
373
379
|
function sanitizeRapierHtml(value, context = 'render') {
|
|
380
|
+
return sanitize(value, context, false);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// The same parser, profile and hooks admit both results. A private renderer can
|
|
384
|
+
// adopt the sanitized nodes without serializing and parsing them again.
|
|
385
|
+
function sanitizeRapierFragment(value, context = 'render') {
|
|
386
|
+
return sanitize(value, context, true);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function sanitize(value, context, fragment) {
|
|
374
390
|
// 'export' is the render profile under the export hooks; 'raw' is a document's own HTML (initMarkdownIt, html_block):
|
|
375
391
|
// the render profile without data-* attributes, which Rapier's delegated handlers read as their own controls, and
|
|
376
392
|
// (the hook) without an id the chrome owns. 'source' is Markdown source HTML, written and never shown: the render
|
|
@@ -389,6 +405,8 @@ function sanitizeRapierHtml(value, context = 'render') {
|
|
|
389
405
|
DOMPurify.addHook('uponSanitizeAttribute', diagramLabelAttribute);
|
|
390
406
|
DOMPurify.addHook('afterSanitizeAttributes', diagramLabelContained);
|
|
391
407
|
}
|
|
408
|
+
if (fragment) options = {...options, RETURN_DOM_FRAGMENT: true};
|
|
409
|
+
const previous = _rapierSanitizeRuntime.context;
|
|
392
410
|
_rapierSanitizeRuntime.context = selected;
|
|
393
411
|
try {
|
|
394
412
|
return DOMPurify.sanitize(value == null ? '' : String(value), options);
|
|
@@ -398,7 +416,7 @@ function sanitizeRapierHtml(value, context = 'render') {
|
|
|
398
416
|
DOMPurify.removeHook('uponSanitizeAttribute', diagramLabelAttribute);
|
|
399
417
|
DOMPurify.removeHook('afterSanitizeAttributes', diagramLabelContained);
|
|
400
418
|
}
|
|
401
|
-
_rapierSanitizeRuntime.context =
|
|
419
|
+
_rapierSanitizeRuntime.context = previous;
|
|
402
420
|
}
|
|
403
421
|
}
|
|
404
422
|
|
|
@@ -412,9 +430,11 @@ function escapeRapierHtmlText(value) {
|
|
|
412
430
|
}
|
|
413
431
|
|
|
414
432
|
function _rapierChromeOwnsId(id) {
|
|
415
|
-
// Every id the shell's own markup carries, read once before any document renders (initMarkdownIt primes it)
|
|
433
|
+
// Every id the shell's own markup carries, read once before any document renders (initMarkdownIt primes it),
|
|
434
|
+
// and every rapier- id: the chrome makes its dialogs, overlays, parts and plug-in list later, under that prefix
|
|
435
|
+
// (rapier-anchor- is the renderer's own name for a repeated document anchor, and stays the document's).
|
|
416
436
|
if (!_rapierSanitizeRuntime.chromeIds) _rapierSanitizeRuntime.chromeIds = new Set(Array.from(typeof document?.querySelectorAll === 'function' ? document.querySelectorAll('[id]') : [], element => element.id));
|
|
417
|
-
return _rapierSanitizeRuntime.chromeIds.has(String(id));
|
|
437
|
+
return _rapierSanitizeRuntime.chromeIds.has(String(id)) || /^rapier-(?!anchor-)/.test(id);
|
|
418
438
|
}
|
|
419
439
|
|
|
420
440
|
function _rapierSafeRasterDataUrl(value) {
|
|
@@ -472,7 +492,7 @@ function _rapierStyleWithoutRemoteUrls(value) {
|
|
|
472
492
|
if (!raw) return raw;
|
|
473
493
|
const declaration = _rapierCssDeclaration();
|
|
474
494
|
declaration.cssText = raw;
|
|
475
|
-
if (!declaration.length) return
|
|
495
|
+
if (!declaration.length) return typography.style(typography.parseStyle(raw));
|
|
476
496
|
return _rapierDropRemoteDeclarations(declaration) ? declaration.cssText : raw;
|
|
477
497
|
}
|
|
478
498
|
|
|
@@ -504,12 +524,15 @@ function _rapierVerifyRasterBytes(bytes, mime) {
|
|
|
504
524
|
// declaration, doctype or generator/license comment before <svg itself, of whatever length its
|
|
505
525
|
|
|
506
526
|
if (mime === 'image/svg+xml') return /<svg[\s>]/i.test(new TextDecoder('utf-8', {fatal: false}).decode(bytes));
|
|
527
|
+
if (mime === 'image/gif') return bytes.length >= 13 && /^GIF8[79]a$/.test(String.fromCharCode(...bytes.subarray(0, 6)));
|
|
507
528
|
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;
|
|
508
529
|
if (mime === 'image/jpeg') return bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff;
|
|
509
530
|
if (mime === 'image/webp') return bytes.length >= 12 && String.fromCharCode(...bytes.slice(0, 4)) === 'RIFF' && String.fromCharCode(...bytes.slice(8, 12)) === 'WEBP';
|
|
510
531
|
return false;
|
|
511
532
|
}
|
|
512
533
|
|
|
513
|
-
return {_rapierVerifyRasterBytes, _rapierInstallSanitizeHooks, sanitizeRapierHtml,
|
|
534
|
+
return {_rapierVerifyRasterBytes, _rapierInstallSanitizeHooks, sanitizeRapierHtml,
|
|
535
|
+
sanitizeRapierFragment: typeof DOMPurify?.sanitize === 'function' ? sanitizeRapierFragment : undefined,
|
|
536
|
+
diagramLabelStyle, escapeRapierHtmlText, _rapierChromeOwnsId, _rapierSafeRasterDataUrl, _rapierCssPresentationIsRemote, _rapierRemoteSubresourceOrigin, _rapierStyleWithoutRemoteUrls, _rapierStylesheetWithoutRemoteUrls, _rapierDropRemoteRules};
|
|
514
537
|
}
|
|
515
538
|
export {RAPIER_SANITIZE_FORBID_TAGS as forbidTags, RAPIER_SANITIZE_FORBID_ATTR as forbidAttributes, createRenderSanitizer, createInertHtmlSanitizer, RAPIER_SANITIZE_OPTIONS as options, prepareMathSource};
|
package/dist/kit/render-work.mjs
CHANGED
|
@@ -7,6 +7,7 @@ export function finish(iterator) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function check(work) {
|
|
10
|
+
work?.check?.();
|
|
10
11
|
if (work?.signal?.aborted) throw Object.assign(new Error('The operation was cancelled.'), {name: 'AbortError', code: 'cancelled'});
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -72,16 +73,18 @@ export function htmlBoundary() {
|
|
|
72
73
|
|
|
73
74
|
export async function finishAsync(iterator, work) {
|
|
74
75
|
let began = performance.now(), next;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
try {
|
|
77
|
+
do {
|
|
78
|
+
check(work);
|
|
79
|
+
next = iterator.next();
|
|
80
|
+
if (!next.done && performance.now() - began >= 8) {
|
|
81
|
+
await pause(work);
|
|
82
|
+
began = performance.now();
|
|
83
|
+
}
|
|
84
|
+
} while (!next.done);
|
|
85
|
+
check(work);
|
|
86
|
+
return next.value;
|
|
87
|
+
} finally { if (!next?.done) iterator.return?.(); }
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
export function* cloneTree(root) {
|
|
@@ -98,25 +101,98 @@ export function* cloneTree(root) {
|
|
|
98
101
|
return copy;
|
|
99
102
|
}
|
|
100
103
|
|
|
101
|
-
|
|
104
|
+
// Null entries are traversal checkpoints, never fragment boundaries. A large
|
|
105
|
+
// nested unit is serialized cooperatively but is still admitted as one complete
|
|
106
|
+
// HTML/namespace unit by the synchronous parser and sanitizer.
|
|
107
|
+
export function* htmlParts(root, limit = 16384, consume = false) {
|
|
108
|
+
limit = Math.max(2, limit);
|
|
102
109
|
const text = root.ownerDocument.createElement('div');
|
|
110
|
+
const small = node => {
|
|
111
|
+
let count = 0, chars = 0, at = node;
|
|
112
|
+
while (at) {
|
|
113
|
+
if (++count > 128) return false;
|
|
114
|
+
if (at.nodeType === 1) for (const attribute of at.attributes) chars += attribute.value.length + attribute.name.length;
|
|
115
|
+
else chars += at.nodeValue?.length || 0;
|
|
116
|
+
if (chars > limit) return false;
|
|
117
|
+
if (at.firstChild) at = at.firstChild;
|
|
118
|
+
else {
|
|
119
|
+
while (at !== node && !at.nextSibling) at = at.parentNode;
|
|
120
|
+
at = at === node ? null : at.nextSibling;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
};
|
|
125
|
+
const nativeHtml = node => {
|
|
126
|
+
if (node.nodeType === 1) return node.outerHTML;
|
|
127
|
+
text.replaceChildren(node.cloneNode(false)); return text.innerHTML;
|
|
128
|
+
};
|
|
129
|
+
const nodeHtml = function* (node) {
|
|
130
|
+
if (small(node)) return nativeHtml(node);
|
|
131
|
+
const pieces = [], stack = [];
|
|
132
|
+
let at = node;
|
|
133
|
+
while (at) {
|
|
134
|
+
yield null;
|
|
135
|
+
if (at.nodeType === 1) {
|
|
136
|
+
if (small(at)) pieces.push(at.outerHTML);
|
|
137
|
+
else {
|
|
138
|
+
// Native shallow serialization owns tag/attribute spelling and
|
|
139
|
+
// escaping. Native text serialization below keeps its parent context.
|
|
140
|
+
const empty = at.cloneNode(false);
|
|
141
|
+
// A foreign element with children must retain its native closing tag
|
|
142
|
+
// even in an inert DOM that self-closes an empty SVG element.
|
|
143
|
+
if (at.firstChild) empty.appendChild(root.ownerDocument.createTextNode(''));
|
|
144
|
+
const shell = empty.outerHTML, close = /<\/[^<>]+>$/.exec(shell)?.[0] || '';
|
|
145
|
+
if (close && at.firstChild) {
|
|
146
|
+
pieces.push(shell.slice(0, -close.length));
|
|
147
|
+
stack.push({node: at, close}); at = at.firstChild; continue;
|
|
148
|
+
}
|
|
149
|
+
pieces.push(shell);
|
|
150
|
+
}
|
|
151
|
+
} else if (at.nodeType === 3 && at.nodeValue.length > limit) {
|
|
152
|
+
const parent = at !== node && at.parentNode?.nodeType === 1 ? at.parentNode.cloneNode(false) : text;
|
|
153
|
+
const value = at.nodeValue;
|
|
154
|
+
for (let start = 0; start < value.length;) {
|
|
155
|
+
let end = Math.min(start + limit, value.length);
|
|
156
|
+
if (end < value.length && value.charCodeAt(end - 1) >= 0xd800 && value.charCodeAt(end - 1) <= 0xdbff) end--;
|
|
157
|
+
parent.replaceChildren(root.ownerDocument.createTextNode(value.slice(start, end)));
|
|
158
|
+
pieces.push(parent.innerHTML); start = end; yield null;
|
|
159
|
+
}
|
|
160
|
+
parent.replaceChildren();
|
|
161
|
+
} else if (at.nodeType === 3 && at.parentNode?.nodeType === 1) {
|
|
162
|
+
const parent = at.parentNode.cloneNode(false);
|
|
163
|
+
parent.appendChild(at.cloneNode(false)); pieces.push(parent.innerHTML);
|
|
164
|
+
} else pieces.push(nativeHtml(at));
|
|
165
|
+
while (at !== node && !at.nextSibling) { const frame = stack.pop(); pieces.push(frame.close); at = frame.node; }
|
|
166
|
+
at = at === node ? null : at.nextSibling;
|
|
167
|
+
}
|
|
168
|
+
return pieces.join('');
|
|
169
|
+
};
|
|
103
170
|
let part = '';
|
|
104
|
-
for (let node = root.firstChild; node;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
else { text.replaceChildren(node.cloneNode(false)); html = text.innerHTML; }
|
|
171
|
+
for (let node = root.firstChild; node;) {
|
|
172
|
+
const next = node.nextSibling;
|
|
173
|
+
const html = yield* nodeHtml(node);
|
|
108
174
|
if (part && part.length + html.length > limit) { yield part; part = ''; }
|
|
109
175
|
part += html;
|
|
176
|
+
if (consume) root.removeChild(node);
|
|
177
|
+
node = next;
|
|
110
178
|
}
|
|
111
179
|
if (part) yield part;
|
|
112
180
|
}
|
|
113
181
|
|
|
114
|
-
export function
|
|
182
|
+
export function admitHtml(root, html, sanitize, fragment = null, context = 'export') {
|
|
183
|
+
if (fragment) root.appendChild(fragment(html, context));
|
|
184
|
+
else {
|
|
185
|
+
// Inert string-only hosts retain their explicitly supported admission path.
|
|
186
|
+
const part = root.ownerDocument.createElement('div');
|
|
187
|
+
part.innerHTML = sanitize(html, context);
|
|
188
|
+
while (part.firstChild) root.appendChild(part.firstChild);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function* cleanTree(root, sanitize, context = 'export', fragment = null) {
|
|
115
193
|
const clean = root.ownerDocument.createElement('div');
|
|
116
|
-
const part = root.ownerDocument.createElement('div');
|
|
117
194
|
for (const html of htmlParts(root)) {
|
|
118
|
-
|
|
119
|
-
while (part.firstChild) clean.appendChild(part.firstChild);
|
|
195
|
+
if (html !== null) admitHtml(clean, html, sanitize, fragment, context);
|
|
120
196
|
yield;
|
|
121
197
|
}
|
|
122
198
|
return clean;
|
|
@@ -124,7 +200,10 @@ export function* cleanTree(root, sanitize, context = 'export') {
|
|
|
124
200
|
|
|
125
201
|
export function* serializeTree(root, sanitize = null, trim = false) {
|
|
126
202
|
const parts = [];
|
|
127
|
-
for (const html of htmlParts(root)) {
|
|
203
|
+
for (const html of htmlParts(root)) {
|
|
204
|
+
if (html !== null) parts.push(sanitize ? sanitize(html, 'export') : html);
|
|
205
|
+
yield;
|
|
206
|
+
}
|
|
128
207
|
const result = parts.join('');
|
|
129
208
|
return trim ? result.trim() : result;
|
|
130
209
|
}
|