rapier-markdown-kit 1.2.8 → 1.2.10
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/dist/agent/will.mjs +0 -6
- package/dist/images/gif.mjs +0 -3
- package/dist/images/playback.mjs +0 -10
- package/dist/kit/assets.mjs +0 -1
- package/dist/kit/conformance/measurer.mjs +0 -0
- package/dist/kit/conformance/run.mjs +1 -8
- package/dist/kit/export-fonts.mjs +0 -14
- package/dist/kit/ledger/authored-origin.mjs +0 -5
- package/dist/kit/ledger/authorship.mjs +0 -2
- package/dist/kit/ledger/carried.mjs +4 -7
- package/dist/kit/ledger/data.mjs +0 -8
- package/dist/kit/ledger/format.mjs +0 -25
- package/dist/kit/ledger/hash.mjs +11 -7
- package/dist/kit/ledger/history.mjs +10 -29
- package/dist/kit/ledger/inverse-source.mjs +0 -46
- package/dist/kit/ledger/journal-records.mjs +24 -56
- package/dist/kit/ledger/merge.mjs +0 -27
- package/dist/kit/ledger/text-merge.mjs +0 -20
- package/dist/kit/ledger/transport.mjs +0 -47
- package/dist/kit/ledger.mjs +0 -4
- package/dist/kit/markdown-it.mjs +0 -3
- package/dist/kit/pdf-glyphs.mjs +1 -10
- package/dist/kit/pdf-source.mjs +5 -5
- package/dist/kit/pdf-vectors.mjs +0 -3
- package/dist/kit/print-capitals.mjs +0 -4
- package/dist/kit/render-entry.mjs +0 -1
- package/dist/kit/render-ink.mjs +0 -5
- package/dist/kit/render-markdown.mjs +12 -117
- package/dist/kit/render-print.mjs +30 -61
- package/dist/kit/render-sanitize.mjs +7 -59
- package/dist/kit/render-styles.mjs +0 -7
- package/dist/kit/render-work.mjs +0 -12
- package/dist/kit/render.mjs +46 -176
- package/dist/layout/hyphenation.mjs +0 -23
- package/dist/layout/line-plan.mjs +0 -20
- package/dist/layout/model.mjs +0 -55
- package/dist/layout/paragraph-fit.mjs +50 -60
- package/dist/layout/preparation.mjs +23 -56
- package/dist/layout/prepared-paragraph.mjs +0 -25
- package/dist/layout/typography.mjs +103 -41
- package/dist/packages/rapier-diff/concerns.mjs +0 -16
- package/dist/packages/rapier-diff/diff.mjs +0 -7
- package/dist/packages/rapier-diff/index.mjs +0 -1
- package/dist/packages/rapier-diff/matching.mjs +0 -19
- package/dist/packages/rapier-diff/merge.mjs +0 -9
- package/dist/packages/rapier-diff/moves.mjs +0 -7
- package/dist/packages/rapier-diff/structure.mjs +0 -2
- package/dist/packages/rapier-diff/tokens.mjs +0 -6
- package/dist/packages/rapier-diff/walk.mjs +0 -12
- package/dist/spec/document-fonts.mjs +0 -9
- package/dist/spec/document-settings.mjs +5 -24
- package/dist/spec/frontmatter.mjs +0 -1
- package/dist/spec/html-blocks.mjs +0 -36
- package/dist/spec/md-assets.mjs +2 -61
- package/dist/spec/md-layout.mjs +0 -34
- package/dist/spec/md-marks.mjs +0 -51
- package/dist/spec/md-source.mjs +0 -16
- package/dist/spec/template-page.mjs +6 -0
- package/dist/spec/word-sidecar.mjs +0 -2
- package/package.json +1 -1
- package/style.css +32 -16
|
@@ -9,7 +9,6 @@ import {stripOneTerminator as _rapierWillStripOneTerminator} from '../agent/will
|
|
|
9
9
|
import {quotedWillSpan} from '../spec/md-marks.mjs';
|
|
10
10
|
const RAPIER_WILL_SENTINEL_MARK = '';
|
|
11
11
|
// SPDX-License-Identifier: MIT
|
|
12
|
-
// The document's renderer. DOM, codecs and host state are explicit inputs.
|
|
13
12
|
function createPrintRenderer(runtime) {
|
|
14
13
|
const {Node, _rapierWillParse, crypto, document, globalThis} = runtime;
|
|
15
14
|
function _rapierWillSentinelPlan(canonical, kind) {
|
|
@@ -46,8 +45,6 @@ function _rapierPrintWillAdmit(canonical, plan) {
|
|
|
46
45
|
function _rapierPrintWillFont(markers, advanceOverride) {
|
|
47
46
|
const metrics = _rapierPrintWillMetrics(markers);
|
|
48
47
|
const size = metrics.size;
|
|
49
|
-
// The page's carrier face keeps the short marker advance. Searchable text is
|
|
50
|
-
// positioned at 500 units, which is also /DW, so this copy can be asked to match it.
|
|
51
48
|
const advance = advanceOverride ?? metrics.advance;
|
|
52
49
|
const scalars = Array.from(new Set(markers.flatMap(marker => Array.from(marker, ch => ch.codePointAt(0)))));
|
|
53
50
|
const order = scalars.filter(code => code < 0x10000).sort((a, b) => a - b)
|
|
@@ -75,8 +72,6 @@ function _rapierPrintWillFont(markers, advanceOverride) {
|
|
|
75
72
|
const cmap = narrow.length <= 0xffff
|
|
76
73
|
? cat(u16(0), u16(2), u16(3), u16(1), u32(20), u16(3), u16(10), u32(20 + narrow.length), narrow, full)
|
|
77
74
|
: cat(u16(0), u16(1), u16(3), u16(10), u32(12), full);
|
|
78
|
-
// The collinear contour has zero area and enough bounds for native print at 2px.
|
|
79
|
-
// Keep it below the baseline so a carrier at a page break does not straddle both page clips.
|
|
80
75
|
const bounds = [0, -400, 10, 0];
|
|
81
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]);
|
|
82
77
|
const utf16 = text => Array.from(text, ch => u16(ch.charCodeAt(0)));
|
|
@@ -120,7 +115,6 @@ function _rapierPrintWillMetrics(markers) {
|
|
|
120
115
|
}
|
|
121
116
|
|
|
122
117
|
function _rapierPrintWillPlace(root, plan) {
|
|
123
|
-
// A marker the page holds inside other content (a code block, say) is quoted text, which a reader of the PDF's text could not tell from a carried marker.
|
|
124
118
|
const lost = () => { throw _rapierPrintWillLost('a marker stands inside other content, where a reader of the PDF\'s text could not tell it from a carried marker', 'Take the quoted marker out'); };
|
|
125
119
|
const measure = _rapierPrintWillMetrics(plan.markers);
|
|
126
120
|
const tokens = new Map(plan.markers.map((marker, index) => [plan.sentinel + index + RAPIER_WILL_SENTINEL_MARK, index]));
|
|
@@ -153,8 +147,6 @@ function _rapierPrintWillPlace(root, plan) {
|
|
|
153
147
|
carrier.textContent = plan.markers[row.index];
|
|
154
148
|
return carrier;
|
|
155
149
|
});
|
|
156
|
-
// A rule's own top margin is a line of the sheet's (none at the head of the page, where the sheet takes it off the first child): it
|
|
157
|
-
// moves to the run, so the gap the rule makes with the block above is the one it made.
|
|
158
150
|
if (next && next.tagName === 'HR') {
|
|
159
151
|
if (first.previousElementSibling || first.parentNode !== root) carriers[0].style.marginTop = 'var(--md-line)';
|
|
160
152
|
next.style.marginTop = '0';
|
|
@@ -177,18 +169,15 @@ function _rapierPrintWillPageCss() {
|
|
|
177
169
|
+ '}';
|
|
178
170
|
}
|
|
179
171
|
|
|
180
|
-
// The capture's own limits (agent/visual.mjs VISUAL_LIMITS): the longest edge and the most pixels one capture returns.
|
|
181
172
|
const CAPTURE_EDGE = 4096, CAPTURE_PIXELS = 4 * 1024 * 1024;
|
|
182
173
|
|
|
183
|
-
async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings}, {capture, signal, current, maxBytes = 8 * 1024 * 1024, film, ink} = {}) {
|
|
174
|
+
async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings, templatePage = null}, {capture, signal, current, maxBytes = 8 * 1024 * 1024, film, ink} = {}) {
|
|
184
175
|
const refuse = code => { throw Object.assign(new Error(code), {code}); };
|
|
185
176
|
const check = () => {
|
|
186
177
|
if (signal?.aborted) refuse('cancelled');
|
|
187
178
|
if (current?.() === false) refuse('document_changed');
|
|
188
179
|
};
|
|
189
180
|
if (!root?.ownerDocument || typeof capture !== 'function') refuse('pdf_render_unavailable');
|
|
190
|
-
// The native printer chooses its default paper. A file made without a printer uses A4,
|
|
191
|
-
// with the same explicit document settings and default margins as the print artifact.
|
|
192
181
|
const paper = settings?.papersize || {width: 11906, height: 16838};
|
|
193
182
|
const width = paper.width / 20, height = paper.height / 20;
|
|
194
183
|
const margin = settings?.geometry ? settings.geometry.twips / 20 : null;
|
|
@@ -198,7 +187,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
198
187
|
const bodyHeight = Math.floor((height - margins.top - margins.bottom) * 4 / 3);
|
|
199
188
|
if (bodyWidth < 1 || bodyHeight < 1 || bodyWidth * bodyHeight > 4 * 1024 * 1024) refuse('pdf_page_geometry_invalid');
|
|
200
189
|
const frame = document.createElement('iframe');
|
|
201
|
-
let vectors, nativeGlyphs, filmSession;
|
|
190
|
+
let vectors, backdrop = null, nativeGlyphs, filmSession;
|
|
202
191
|
let rejectAbort;
|
|
203
192
|
const aborted = new Promise((_, reject) => { rejectAbort = reject; });
|
|
204
193
|
aborted.catch(() => {});
|
|
@@ -221,8 +210,6 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
221
210
|
check();
|
|
222
211
|
const into = frame.contentDocument;
|
|
223
212
|
if (!into?.body) refuse('pdf_render_unavailable');
|
|
224
|
-
// The capture and native printer use standards layout. An initial blank
|
|
225
|
-
// iframe otherwise measures preformatted inline struts in quirks mode.
|
|
226
213
|
into.open(); into.write('<!doctype html><html><head></head><body></body></html>'); into.close();
|
|
227
214
|
into.documentElement.setAttribute('data-rapier-theme', 'light');
|
|
228
215
|
into.documentElement.setAttribute('lang', documentLanguage(settings, document.defaultView?.navigator?.language));
|
|
@@ -246,7 +233,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
246
233
|
strip.append(root); surface.append(strip); into.body.append(surface);
|
|
247
234
|
const main = root.querySelector('main');
|
|
248
235
|
if (!main) refuse('pdf_render_unavailable');
|
|
249
|
-
|
|
236
|
+
if (templatePage) for (const image of main.querySelectorAll('img')) if (image.getAttribute('src') === templatePage.url) (image.closest('p,figure') || image).remove();
|
|
250
237
|
main.setAttribute('data-md-theme', 'light');
|
|
251
238
|
for (const details of main.querySelectorAll('details')) details.open = true;
|
|
252
239
|
main.getBoundingClientRect();
|
|
@@ -254,12 +241,8 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
254
241
|
await step(into.fonts.ready);
|
|
255
242
|
for (const image of main.querySelectorAll('img')) { image.loading = 'eager'; await step(image.decode()); }
|
|
256
243
|
check();
|
|
257
|
-
// The print sheet owns the words and styles. CSS fragmentation supplies the file's
|
|
258
|
-
// pages without a print dialog; its break constraints become column constraints here.
|
|
259
244
|
for (const node of [main, ...main.querySelectorAll('*')]) {
|
|
260
245
|
const style = into.defaultView.getComputedStyle(node);
|
|
261
|
-
// Clipped, out-of-flow source has no page ink. Leaving it in a multicolumn
|
|
262
|
-
// container can still create blank columns and expose its text to PDF search.
|
|
263
246
|
if (!node.closest('.rapier-will-carrier') && ['absolute', 'fixed'].includes(style.position) && /^rect\(/.test(style.clip)) {
|
|
264
247
|
const box = style.clip.slice(5, -1).trim().split(/[,\s]+/).map(Number.parseFloat);
|
|
265
248
|
if (box.length === 4 && box.every(Number.isFinite) && (box[1] <= box[3] || box[2] <= box[0])) {
|
|
@@ -273,17 +256,14 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
273
256
|
}
|
|
274
257
|
for (const [name, value] of Object.entries({width: bodyWidth + 'px', height: bodyHeight + 'px',
|
|
275
258
|
'max-width': 'none', 'min-height': '0', padding: '0', margin: '0', overflow: 'visible',
|
|
276
|
-
'column-width': bodyWidth + 'px', 'column-gap': '0', 'column-count': 'auto', 'column-fill': 'auto'}))
|
|
259
|
+
'column-width': bodyWidth + 'px', 'column-gap': '0', 'column-count': 'auto', 'column-fill': 'auto', background: 'none'}))
|
|
277
260
|
main.style.setProperty(name, value, 'important');
|
|
278
261
|
globalThis.RapierMath?.fit(main);
|
|
279
262
|
applyPrintCapitals(main);
|
|
280
263
|
applyTypography(main, {all: true});
|
|
281
|
-
// The document's ink marks, drawn on the words where they now stand on the page.
|
|
282
264
|
ink?.(main);
|
|
283
265
|
const count = Math.max(1, Math.ceil((main.scrollWidth - 1) / bodyWidth));
|
|
284
266
|
if (!Number.isSafeInteger(count)) refuse('pdf_page_geometry_invalid');
|
|
285
|
-
// Captured document font bytes own a same-name family; factory glyphs must
|
|
286
|
-
// never replace their page paint. This uses the artifact, not a live catalog.
|
|
287
267
|
const customFamilies = new Set(fontCss ? fontRequests(main, {fontCss, css, settings}).map(face => face.family.toLowerCase()) : []);
|
|
288
268
|
nativeGlyphs = await collectPdfGlyphs(main, {width: bodyWidth, check,
|
|
289
269
|
faces: globalThis.RapierDocxFonts?.DOCX_FONTS?.filter(face => !customFamilies.has(face.family.toLowerCase()))});
|
|
@@ -304,11 +284,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
304
284
|
const font = _rapierPrintWillFont(text, 500);
|
|
305
285
|
strip.style.width = count * bodyWidth + 'px';
|
|
306
286
|
strip.style.transform = 'none';
|
|
307
|
-
// One styled film is reused for every page window. Batches stay inside the capture edge
|
|
308
|
-
// and pixel limits. A picture no longer forces its own capture. Pixels are deflated once.
|
|
309
287
|
if (typeof film !== 'function') refuse('pdf_render_unavailable');
|
|
310
|
-
// step() can reject after the film resolves (the document changed, or the attempt was
|
|
311
|
-
// cancelled). The session would otherwise keep its abort listener with no owner.
|
|
312
288
|
let pendingFilm = null, abandonFilm = false;
|
|
313
289
|
const trackedFilm = Promise.resolve(film({root: surface, signal, current, frame: {width: count * bodyWidth, height: bodyHeight}})).then(session => {
|
|
314
290
|
if (abandonFilm) session?.close?.();
|
|
@@ -359,15 +335,24 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
359
335
|
first += span;
|
|
360
336
|
}
|
|
361
337
|
check();
|
|
362
|
-
|
|
338
|
+
if (templatePage) {
|
|
339
|
+
const sheetWidth = Math.round(width * 4 / 3), sheetHeight = Math.round(height * 4 / 3);
|
|
340
|
+
const holder = into.createElement('div'), picture = into.createElement('img');
|
|
341
|
+
holder.style.cssText = `position:fixed;left:0;top:0;width:${sheetWidth}px;height:${sheetHeight}px;pointer-events:none`;
|
|
342
|
+
picture.style.cssText = 'display:block;width:100%;height:100%'; picture.src = templatePage.url;
|
|
343
|
+
holder.append(picture); into.body.append(holder);
|
|
344
|
+
await step(picture.decode());
|
|
345
|
+
backdrop = await collectPdfVectors(holder, {width: sheetWidth, height: sheetHeight, count: 1, capture, signal, current, check, maxBytes,
|
|
346
|
+
sanitizeSvg: globalThis.RapierImageAssets?.sanitizeSvgText});
|
|
347
|
+
if (!backdrop.pages[0].length) refuse('pdf_template_border_unavailable');
|
|
348
|
+
}
|
|
349
|
+
const bytes = await writeRasterPdf({pages, textPages, linkPages, vectorPages: vectors.pages, canonical, bom, filename, title, font, width, height, margins, backdrop: backdrop?.pages[0] || null,
|
|
363
350
|
pixelWidth: bodyWidth, pixelHeight: bodyHeight, pageNumbers: settings?.pagestyle === 'plain', maxBytes, check});
|
|
364
351
|
check();
|
|
365
352
|
return {bytes, pages: count};
|
|
366
|
-
} finally { filmSession?.close(); nativeGlyphs?.restore(); vectors?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
|
|
353
|
+
} finally { filmSession?.close(); nativeGlyphs?.restore(); vectors?.restore(); backdrop?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
|
|
367
354
|
}
|
|
368
355
|
|
|
369
|
-
// Ranges locate the actual text fragments after column layout. Splitting a text node at
|
|
370
|
-
// its line and page boundaries keeps a PDF search on the page that carries those words.
|
|
371
356
|
async function _rapierPrintTextPages(main, pageWidth, count, check, origin = main.getBoundingClientRect(), nativeNodes = null) {
|
|
372
357
|
const into = main.ownerDocument;
|
|
373
358
|
const pages = Array.from({length: count}, () => []), walker = into.createTreeWalker(main, 5);
|
|
@@ -382,8 +367,6 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
382
367
|
if (node.nodeType === 1 && !formula || node.nodeType === 3 && element.closest('.math-rendered[data-math-src]')) continue;
|
|
383
368
|
let value;
|
|
384
369
|
if (formula) {
|
|
385
|
-
// The math owner paints SVG paths and retains the equation's exact TeX alternative.
|
|
386
|
-
// Carry that semantic text at the SVG's measured position for search and copy.
|
|
387
370
|
value = formula.getAttribute('aria-label') || '';
|
|
388
371
|
} else value = node.data;
|
|
389
372
|
const style = into.defaultView.getComputedStyle(element);
|
|
@@ -397,14 +380,11 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
397
380
|
const visit = (start, end) => {
|
|
398
381
|
if (++operations % 128 === 0) check();
|
|
399
382
|
if (!formula) { range.setStart(node, start); range.setEnd(node, end); }
|
|
400
|
-
// A tiny carrier font can have its advance rounded to zero by a browser. Its
|
|
401
|
-
// measured baseline still locates the marker; the font's authored metrics keep its text width.
|
|
402
383
|
const rects = Array.from(formula ? [formula.getBoundingClientRect()] : range.getClientRects()).filter(rect => rect.height > 0 && (rect.width > 0 || carrier));
|
|
403
384
|
if (!rects.length) { if (carrier) markerLost(); return; }
|
|
404
385
|
const first = rects[0], page = pageOf(first.left);
|
|
405
386
|
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);
|
|
406
387
|
let middle = formula ? start : Math.floor((start + end) / 2);
|
|
407
|
-
// A line never breaks inside a character: halve between whole ones (a letter and its marks, a conjunct, an emoji).
|
|
408
388
|
if (middle > start) middle = (characters ||= segmenter.segment(value)).containing(middle).index;
|
|
409
389
|
const cluster = native?.find(item => item.start < middle && item.end > middle);
|
|
410
390
|
if (cluster) middle = cluster.start > start ? cluster.start : cluster.end;
|
|
@@ -436,9 +416,6 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
436
416
|
visit(0, value.length);
|
|
437
417
|
}
|
|
438
418
|
check();
|
|
439
|
-
// Trailing-carrier print CSS pulls a last marker up so it stays on the sheet.
|
|
440
|
-
// Keep that paint; put the searchable object after the preceding words so
|
|
441
|
-
// reading order matches the source (the marker itself has no visible ink).
|
|
442
419
|
const pageHeight = origin.height;
|
|
443
420
|
for (const page of pages) {
|
|
444
421
|
let prev = null;
|
|
@@ -454,8 +431,6 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
454
431
|
prev = run;
|
|
455
432
|
}
|
|
456
433
|
}
|
|
457
|
-
// Each run carries the tags of its block's structure (outermost first): a PDF reader finds the document's headings, lists and
|
|
458
|
-
// tables from them rather than from positions.
|
|
459
434
|
const ROLES = {TABLE: 'Table', TR: 'TR', TD: 'TD', TH: 'TH', UL: 'L', OL: 'L', LI: 'LI', BLOCKQUOTE: 'BlockQuote', P: 'P', PRE: 'Code', FIGCAPTION: 'Caption',
|
|
460
435
|
H1: 'H1', H2: 'H2', H3: 'H3', H4: 'H4', H5: 'H5', H6: 'H6'};
|
|
461
436
|
const tags = block => {
|
|
@@ -498,8 +473,6 @@ function _rapierPrintLinkPages(main, width, height, count) {
|
|
|
498
473
|
return {_rapierWillSentinelPlan, _rapierPrintWillAdmit, _rapierPrintWillFont, _rapierPrintWillMetrics, _rapierPrintWillPlace, _rapierPrintWillLost, _rapierPrintWillPageCss, _rapierWritePrintPdf};
|
|
499
474
|
}
|
|
500
475
|
|
|
501
|
-
// Where each scalar of an invisible text run stands, in character slots from the run's left edge: a mark shares its letter's
|
|
502
|
-
// 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).
|
|
503
476
|
function _rapierPrintSlots(text, rtl) {
|
|
504
477
|
const pieces = [];
|
|
505
478
|
let scalars = 0, count = 0;
|
|
@@ -520,9 +493,7 @@ function _rapierPrintSlots(text, rtl) {
|
|
|
520
493
|
return {slots, count};
|
|
521
494
|
}
|
|
522
495
|
|
|
523
|
-
|
|
524
|
-
// It accepts already-rendered pages, so there is no second document grammar or layout engine.
|
|
525
|
-
async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [], canonical, bom = false, filename, title, font, width, height, margins, pixelWidth, pixelHeight, pageNumbers, maxBytes, check}) {
|
|
496
|
+
async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [], backdrop = null, canonical, bom = false, filename, title, font, width, height, margins, pixelWidth, pixelHeight, pageNumbers, maxBytes, check}) {
|
|
526
497
|
const encoder = new TextEncoder(), encode = value => encoder.encode(value);
|
|
527
498
|
const fail = code => { throw Object.assign(new Error(code), {code}); };
|
|
528
499
|
const concat = parts => {
|
|
@@ -589,7 +560,6 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
589
560
|
nativeResources.push(entry.name + ' ' + type + ' 0 R');
|
|
590
561
|
}
|
|
591
562
|
const sharedImages = new Map();
|
|
592
|
-
// The structure tree (PDF 1.7, 14.7): one element per tagged DOM element, each run marked with its page's next MCID.
|
|
593
563
|
const structs = new Map(), documentElement = {role: 'Document', kids: []}, parentTree = [];
|
|
594
564
|
const structOf = tags => {
|
|
595
565
|
let parent = documentElement;
|
|
@@ -607,12 +577,10 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
607
577
|
const held = pages[index], flate = !!held?.rgb;
|
|
608
578
|
const picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (flate ? 'FlateDecode' : 'DCTDecode'), flate ? held.rgb : held);
|
|
609
579
|
pages[index] = null;
|
|
610
|
-
// The page's picture is how its tagged text looks, not a figure of its own: an artifact.
|
|
611
|
-
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';
|
|
612
580
|
const pictures = ['/Im0 ' + picture + ' 0 R'], states = [];
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
581
|
+
const emit = (drawing, left, top, clipWidth, clipHeight) => {
|
|
582
|
+
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`;
|
|
583
|
+
out += [drawing.clip.x, drawing.clip.y, drawing.clip.width, drawing.clip.height].map(decimal).join(' ') + ' re W n\n';
|
|
616
584
|
for (const piece of drawing.sequence) {
|
|
617
585
|
if (piece.image) {
|
|
618
586
|
let shared = sharedImages.get(piece.image);
|
|
@@ -626,21 +594,22 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
626
594
|
sharedImages.set(piece.image, shared);
|
|
627
595
|
}
|
|
628
596
|
if (!pictures.includes(shared.resource)) pictures.push(shared.resource);
|
|
629
|
-
|
|
597
|
+
out += `q\n${[piece.width, 0, 0, -piece.height, piece.x, piece.y + piece.height].map(decimal).join(' ')} cm\n${shared.name} Do\nQ\n`;
|
|
630
598
|
} else {
|
|
631
599
|
const state = '/Gs' + states.length;
|
|
632
600
|
states.push(state + ' ' + add('<< /Type /ExtGState /ca ' + decimal(piece.fillAlpha) + ' /CA ' + decimal(piece.strokeAlpha) + ' >>') + ' 0 R');
|
|
633
|
-
|
|
601
|
+
out += 'q\n' + state + ' gs\n' + piece.commands + 'Q\n';
|
|
634
602
|
}
|
|
635
603
|
}
|
|
636
|
-
|
|
637
|
-
}
|
|
604
|
+
return out + 'Q\n';
|
|
605
|
+
};
|
|
606
|
+
let commands = '';
|
|
607
|
+
for (const drawing of backdrop || []) commands += emit(drawing, 0, height, Math.round(width * 4 / 3), Math.round(height * 4 / 3));
|
|
608
|
+
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';
|
|
609
|
+
for (const drawing of vectorPages[index] || []) commands += emit(drawing, margins.left, height - margins.top, pixelWidth, pixelHeight);
|
|
638
610
|
const textAlphas = new Map();
|
|
639
611
|
for (const run of textPages[index]) {
|
|
640
612
|
if (!run.text) continue;
|
|
641
|
-
// Keep one logical Unicode text object, resetting its font before each scalar's direction
|
|
642
|
-
// can change. Readers that ignore ActualText otherwise discard earlier words in mixed RTL/LTR.
|
|
643
|
-
// Express width in the font size: some readers ignore horizontal text scaling when joining.
|
|
644
613
|
const element = structOf(run.tags || []);
|
|
645
614
|
if (element) {element.kids.push({page: index, mcid: marked.length}); commands += `/${element.role} << /MCID ${marked.length} >> BDC\n`; marked.push(element);}
|
|
646
615
|
commands += `q\n/Span << /ActualText <feff${hex(run.text)}> >> BDC\nBT\n100 Tz\n`;
|
|
@@ -683,7 +652,7 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
683
652
|
const parents = add('<< /Nums [' + parentTree.map((marked, index) => index + ' [' + marked.map(element => ids.get(element) + ' 0 R').join(' ') + ']').join(' ') + '] >>');
|
|
684
653
|
put(tree, '<< /Type /StructTreeRoot /K [' + ids.get(documentElement) + ' 0 R] /ParentTree ' + parents + ' 0 R /ParentTreeNextKey ' + parentTree.length + ' >>');
|
|
685
654
|
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] >>');
|
|
686
|
-
const info = add('<< /Producer (Rapier) /Title <feff' + hex(title || filename.replace(/\.[^.]*$/, '')) + '> >>');
|
|
655
|
+
const info = add('<< /Producer (Rapier) /Creator (Rapier) /Title <feff' + hex(title || filename.replace(/\.[^.]*$/, '')) + '> >>');
|
|
687
656
|
const header = encode('%PDF-1.7\n%\u00e2\u00e3\u00cf\u00d3\n'), offsets = new Array(objects.length);
|
|
688
657
|
let cursor = header.length;
|
|
689
658
|
for (let id = 1; id < objects.length; id++) {
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import {typography} from '../spec/md-marks.mjs';
|
|
3
3
|
const RAPIER_SANITIZE_FORBID_TAGS = Object.freeze(['form', 'style', 'dialog', 'template', 'iframe']);
|
|
4
|
-
// Invokers (DOMPurify 3.4.16 allows them) act on another element by id, and a document shares the page with the editor's own
|
|
5
|
-
// controls: a document's button could open or close them.
|
|
6
4
|
const RAPIER_SANITIZE_FORBID_ATTR = Object.freeze(['action', 'formaction', 'form', 'for', 'command', 'commandfor', 'popover', 'popovertarget', 'popovertargetaction']);
|
|
7
|
-
// The document profiles use one allowlist in a browser and in an inert server parser.
|
|
8
5
|
const RAPIER_SANITIZE_PROFILES = Object.freeze({
|
|
9
6
|
htmlTags: Object.freeze(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),
|
|
10
7
|
svgTags: Object.freeze(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),
|
|
@@ -56,7 +53,6 @@ const DIAGRAM_LABEL_SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
|
56
53
|
const DIAGRAM_LABEL_HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
57
54
|
const DIAGRAM_LABEL_MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
|
|
58
55
|
|
|
59
|
-
// The profile each DOMPurify instance holds (setConfig), shared by every renderer made over it.
|
|
60
56
|
const purifyConfigured = new WeakMap();
|
|
61
57
|
|
|
62
58
|
function _rapierImageDataAttributeAllowed(tag, attribute, value, verify) {
|
|
@@ -64,9 +60,6 @@ function _rapierImageDataAttributeAllowed(tag, attribute, value, verify) {
|
|
|
64
60
|
&& /^data:image\/(?:png|jpeg|webp|jxl);base64,/i.test(value)) && verify(value);
|
|
65
61
|
}
|
|
66
62
|
|
|
67
|
-
// Snapshot current math text before a Markdown writer collapses whitespace. Rendered SVG
|
|
68
|
-
// glyphs have no textContent: put their retained source in this inert copy so the writer's
|
|
69
|
-
// blank-node rule cannot discard an equation or its containing paragraph/list item.
|
|
70
63
|
function prepareMathSource(input, DOMParser) {
|
|
71
64
|
const html = typeof input === 'string';
|
|
72
65
|
const MARK = 'span[data-rapier-math-source],span.math-rendered[data-math-src]';
|
|
@@ -82,18 +75,17 @@ function prepareMathSource(input, DOMParser) {
|
|
|
82
75
|
try { node.textContent = decodeURIComponent(node.getAttribute('data-math-src')); } catch (_) {}
|
|
83
76
|
continue;
|
|
84
77
|
}
|
|
78
|
+
try {
|
|
79
|
+
const held = node.getAttribute('data-rapier-math-source'), source = decodeURIComponent(held);
|
|
80
|
+
if (held && (source === node.textContent || source.replace(/\r\n?/g, '\n').replace(/\0/g, '') === node.textContent)) continue;
|
|
81
|
+
} catch (_) {}
|
|
85
82
|
try { node.setAttribute('data-rapier-math-source', encodeURIComponent(node.textContent || '')); }
|
|
86
83
|
catch (_) { node.removeAttribute('data-rapier-math-source'); }
|
|
87
84
|
}
|
|
88
85
|
return sourceRoot ? root.outerHTML : root;
|
|
89
86
|
}
|
|
90
87
|
|
|
91
|
-
// An inert parser and a string sanitizer use the same profile as DOMPurify. Neither port may
|
|
92
|
-
// execute document scripts or fetch resources. HTML5 parsing precedes filtering so foreign
|
|
93
|
-
// content, entities and malformed table markup cross the same boundary as a browser's parser.
|
|
94
88
|
function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeRasterDataUrl, chromeOwnsId = id => /^rapier-(?!anchor-)/.test(id)}) {
|
|
95
|
-
// chromeOwnsId defaults to _rapierChromeOwnsId's prefix rule: every page Rapier writes carries rapier- elements
|
|
96
|
-
// (rapier-document, rapier-ledger). The factories are serialized into written pages, so neither reads a module constant.
|
|
97
89
|
if (![parseFragment, serialize, sanitizeHtml, safeRasterDataUrl, chromeOwnsId].every(port => typeof port === 'function'))
|
|
98
90
|
throw new TypeError('The inert HTML sanitizer requires every parser and filtering port');
|
|
99
91
|
const uri = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
|
|
@@ -103,8 +95,6 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
|
|
|
103
95
|
const svgTags = new Set(RAPIER_SANITIZE_PROFILES.svgTags), mathTags = new Set(RAPIER_SANITIZE_PROFILES.mathTags);
|
|
104
96
|
const mathIntegration = new Set(['mi', 'mo', 'mn', 'ms', 'mtext']), htmlIntegration = new Set(['annotation-xml']);
|
|
105
97
|
const sharedHtmlSvg = new Set(['title', 'style', 'font', 'a', 'script']);
|
|
106
|
-
// DOMPurify's default namespace admission applies in addition to its tag profile. In
|
|
107
|
-
// particular a MathML name parsed as HTML, or HTML inside SVG desc, is not admitted.
|
|
108
98
|
const validNamespace = (node, parent) => {
|
|
109
99
|
const tag = node.tagName.toLowerCase(), parentTag = (parent.tagName || 'template').toLowerCase();
|
|
110
100
|
const namespace = node.namespaceURI, parentNamespace = parent.namespaceURI || htmlNamespace;
|
|
@@ -158,6 +148,7 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
|
|
|
158
148
|
const key = name.toLowerCase();
|
|
159
149
|
const value = name === 'value' ? inputValue : inputValue.trim();
|
|
160
150
|
if (forbiddenAttributes.has(key) || key.startsWith('on')) continue;
|
|
151
|
+
if (key === 'id' && chromeOwnsId(value)) continue;
|
|
161
152
|
if (/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i.test(value)
|
|
162
153
|
|| key === 'attributename' && value.includes('href')) continue;
|
|
163
154
|
const data = /^data-[\-\w.\u00b7-\uffff]+$/.test(key), aria = /^aria-[\-\w]+$/.test(key);
|
|
@@ -179,13 +170,9 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
|
|
|
179
170
|
};
|
|
180
171
|
}
|
|
181
172
|
|
|
182
|
-
// The document's renderer. DOM, codecs and host state are explicit inputs.
|
|
183
173
|
function createRenderSanitizer(runtime) {
|
|
184
174
|
const {CSSStyleSheet, DOMPurify, RAPIER_RASTER_DATA_URL_RE, URL, _rapierCssDeclaration, _rapierDropRemoteDeclarations, _rapierRemoteContent, _rapierRuleDescriptorIsRemote, _rapierSanitizeRuntime, document, globalThis, location} = runtime;
|
|
185
175
|
|
|
186
|
-
// Only the diagram reader and its styled export admit static foreignObject labels. The
|
|
187
|
-
// ordinary SVG reader still rejects HTML subdocuments. The export class selects scope;
|
|
188
|
-
// every descendant is checked again, so a copied class never confers authority.
|
|
189
176
|
function diagramLabelRoot(node) {
|
|
190
177
|
for (let current = node; current?.nodeType === 1; current = current.parentElement) {
|
|
191
178
|
if (current.localName?.toLowerCase() === 'foreignobject') return current;
|
|
@@ -205,7 +192,6 @@ function diagramLabelStyle(property, value) {
|
|
|
205
192
|
declaration.setProperty(property, value);
|
|
206
193
|
const parsed = declaration.getPropertyValue(property);
|
|
207
194
|
if (!parsed || /(?:url|image-set|element|paint|attr)\s*\(/i.test(parsed)) return false;
|
|
208
|
-
// SVG paint accepts resource references; a custom property must not smuggle one in.
|
|
209
195
|
return !/^(?:fill|stroke)$/.test(property) || !/var\s*\(/i.test(parsed);
|
|
210
196
|
}
|
|
211
197
|
|
|
@@ -279,14 +265,6 @@ function _rapierInstallSanitizeHooks() {
|
|
|
279
265
|
&& _rapierImageDataAttributeAllowed(tag, data.attrName.toLowerCase(), compact, _rapierSafeRasterDataUrl);
|
|
280
266
|
if (!rasterAllowed) data.keepAttr = false;
|
|
281
267
|
});
|
|
282
|
-
// Paste, render and export sanitise: a remote <img src> would fetch the instant its markup lands in the page,
|
|
283
|
-
// so the src the hook below is about to strip never survives. Move it to a data-* attribute first
|
|
284
|
-
// (DOMPurify already leaves data-* alone, and the sanitizer's own inert document never fetches it
|
|
285
|
-
// either), so the picture and its alt text still reach Turndown's rapierImage rule as a live
|
|
286
|
-
// pasted-page picture and write out `` -- not vanish -- and an <img> in a document's own
|
|
287
|
-
// HTML (a table kept as HTML) is written again with its URL: the raw profile admits no data-*, so
|
|
288
|
-
// only the three this hook sets on the node it holds back stay. A picture inside a link is the same
|
|
289
|
-
// node with the same fix: the outer <a> rule reads this img's own converted markdown as its content.
|
|
290
268
|
DOMPurify.addHook('uponSanitizeElement', node => {
|
|
291
269
|
if (_rapierRemoteContent.allowed || !/^(?:paste|render|raw|export)$/.test(_rapierSanitizeRuntime.context)) return;
|
|
292
270
|
if (!node || node.nodeName !== 'IMG' || node.hasAttribute('data-rapier-remote-src')) return;
|
|
@@ -308,18 +286,12 @@ function _rapierInstallSanitizeHooks() {
|
|
|
308
286
|
node.setAttribute('data-rapier-remote-src', href);
|
|
309
287
|
}
|
|
310
288
|
});
|
|
311
|
-
// A list from Word is named by the style of its paragraph and of the span that holds its marker (mso-list,
|
|
312
|
-
// spec/html-reading.mjs), and the CSS pass below empties a style of every property the browser does not
|
|
313
|
-
// know: a marker's style is only `mso-list:Ignore`. The paste carries both as data-* first, where they stay,
|
|
314
|
-
// for the conversion that reads them (_rapierPasteWordLists).
|
|
315
289
|
DOMPurify.addHook('uponSanitizeElement', node => {
|
|
316
290
|
if (_rapierSanitizeRuntime.context !== 'paste' || !node || !/^(?:P|LI|H[1-6]|SPAN)$/.test(node.nodeName)) return;
|
|
317
|
-
// Only the style names them: a page cannot bring these attributes of its own.
|
|
318
291
|
node.removeAttribute('data-rapier-word-level');
|
|
319
292
|
node.removeAttribute('data-rapier-word-marker');
|
|
320
293
|
const style = node.getAttribute('style');
|
|
321
294
|
if (!style || !/mso-list/i.test(style)) return;
|
|
322
|
-
// Word's list reading is the editor's (AGPL) grammar; a host without it keeps the paste's paragraphs as they are.
|
|
323
295
|
const spec = globalThis.RapierMarkdownSpec;
|
|
324
296
|
if (!spec) return;
|
|
325
297
|
const {wordListLevel, wordListIsMarker} = spec;
|
|
@@ -334,9 +306,7 @@ function _rapierInstallSanitizeHooks() {
|
|
|
334
306
|
data.keepAttr = !!data.attrValue;
|
|
335
307
|
return;
|
|
336
308
|
}
|
|
337
|
-
|
|
338
|
-
// allow is about what the page fetches, and this rule is about what the page's chrome finds.
|
|
339
|
-
if (String(data.attrName || '').toLowerCase() === 'id' && _rapierSanitizeRuntime.context === 'raw' && _rapierChromeOwnsId(value)) { data.keepAttr = false; return; }
|
|
309
|
+
if (String(data.attrName || '').toLowerCase() === 'id' && _rapierChromeOwnsId(value)) { data.keepAttr = false; return; }
|
|
340
310
|
if (_rapierRemoteContent.allowed || _rapierSanitizeRuntime.context === 'source') return;
|
|
341
311
|
if (String(data.attrName || '').toLowerCase() === 'style') {
|
|
342
312
|
const cleaned = _rapierStyleWithoutRemoteUrls(value);
|
|
@@ -344,7 +314,6 @@ function _rapierInstallSanitizeHooks() {
|
|
|
344
314
|
return;
|
|
345
315
|
}
|
|
346
316
|
const attribute = String(data.attrName || '').toLowerCase();
|
|
347
|
-
// These attributes carry authored text, never resource requests.
|
|
348
317
|
if (attribute === 'alt' || attribute === 'title') return;
|
|
349
318
|
if (/^data-rapier-remote-(?:src|alt|title)$/.test(attribute) && node && node._rapierHeldBack === true) { data.forceKeepAttr = true; return; }
|
|
350
319
|
if (attribute.startsWith('data-')) return;
|
|
@@ -363,9 +332,6 @@ function _rapierInstallSanitizeHooks() {
|
|
|
363
332
|
data.forceKeepAttr = true;
|
|
364
333
|
}
|
|
365
334
|
});
|
|
366
|
-
// A link that opens a new tab never hands that tab a window.opener or a Referer: the editor's own
|
|
367
|
-
// click router already opens with noopener,noreferrer; the sanitized markup that Share and the
|
|
368
|
-
// standalone export carry gets the same rule in its bytes.
|
|
369
335
|
DOMPurify.addHook('afterSanitizeAttributes', node => {
|
|
370
336
|
if (!node || !/^(?:A|AREA)$/i.test(node.nodeName) || !node.hasAttribute('target')) return;
|
|
371
337
|
if (String(node.getAttribute('target') || '').trim().toLowerCase() !== '_blank') return;
|
|
@@ -387,25 +353,17 @@ function sanitizeRapierHtml(value, context = 'render') {
|
|
|
387
353
|
return sanitize(value, context, false);
|
|
388
354
|
}
|
|
389
355
|
|
|
390
|
-
// The same parser, profile and hooks admit both results. A private renderer can
|
|
391
|
-
// adopt the sanitized nodes without serializing and parsing them again.
|
|
392
356
|
function sanitizeRapierFragment(value, context = 'render') {
|
|
393
357
|
return sanitize(value, context, true);
|
|
394
358
|
}
|
|
395
359
|
|
|
396
360
|
function sanitize(value, context, fragment) {
|
|
397
|
-
// 'export' is the render profile under the export hooks; 'raw' is a document's own HTML (initMarkdownIt, html_block):
|
|
398
|
-
// the render profile without data-* attributes, which Rapier's delegated handlers read as their own controls, and
|
|
399
|
-
// (the hook) without an id the chrome owns. 'source' is Markdown source HTML, written and never shown: the render
|
|
400
|
-
// profile's admission, and the block on remote content stays the reader's (a remote picture's URL is the person's words).
|
|
401
361
|
const selected = context === 'raw' || context === 'export' || context === 'source' || RAPIER_SANITIZE_OPTIONS[context] ? context : 'render';
|
|
402
362
|
const labels = selected === 'diagram' || selected === 'export', key = selected + (fragment ? ' fragment' : '');
|
|
403
363
|
let options = purifyOptions.get(key);
|
|
404
364
|
if (!options) {
|
|
405
365
|
options = selected === 'raw' ? { ...RAPIER_SANITIZE_OPTIONS.render, ALLOW_DATA_ATTR: false } : RAPIER_SANITIZE_OPTIONS[selected] || RAPIER_SANITIZE_OPTIONS.render;
|
|
406
366
|
if (selected === 'source') options = {...options, ADD_DATA_URI_TAGS: ['image']};
|
|
407
|
-
// DOMPurify's USE_PROFILES replaces explicit allowlists. Add MathML to the
|
|
408
|
-
// selected profile without discarding its admitted HTML and SVG vocabulary.
|
|
409
367
|
if (labels) options = {...options, ALLOWED_TAGS: [...new Set([...options.ALLOWED_TAGS, ...RAPIER_SANITIZE_PROFILES.mathTags])],
|
|
410
368
|
ALLOWED_ATTR: [...new Set([...options.ALLOWED_ATTR, ...RAPIER_SANITIZE_PROFILES.mathAttributes])], ADD_TAGS: ['foreignobject'], ADD_DATA_URI_TAGS: ['image'],
|
|
411
369
|
HTML_INTEGRATION_POINTS: {foreignobject: true}, FORBID_TAGS: options.FORBID_TAGS.filter(tag => tag !== 'foreignobject')};
|
|
@@ -417,8 +375,6 @@ function sanitize(value, context, fragment) {
|
|
|
417
375
|
DOMPurify.addHook('uponSanitizeAttribute', diagramLabelAttribute);
|
|
418
376
|
DOMPurify.addHook('afterSanitizeAttributes', diagramLabelContained);
|
|
419
377
|
}
|
|
420
|
-
// DOMPurify reparses a per-call configuration on every call (its cache compares its own copy), about 0.45 ms
|
|
421
|
-
// each in Chrome 155. A profile is set once and kept until another profile is asked for; every caller comes here.
|
|
422
378
|
if (purifyConfigured.get(DOMPurify) !== options) { DOMPurify.setConfig(options); purifyConfigured.set(DOMPurify, options); }
|
|
423
379
|
const previous = _rapierSanitizeRuntime.context;
|
|
424
380
|
_rapierSanitizeRuntime.context = selected;
|
|
@@ -444,15 +400,10 @@ function escapeRapierHtmlText(value) {
|
|
|
444
400
|
}
|
|
445
401
|
|
|
446
402
|
function _rapierChromeOwnsId(id) {
|
|
447
|
-
// Every id the shell's own markup carries, read once before any document renders (initMarkdownIt primes it),
|
|
448
|
-
// and every rapier- id: the chrome makes its dialogs, overlays, parts and plug-in list later, under that prefix
|
|
449
|
-
// (rapier-anchor- is the renderer's own name for a repeated document anchor, and stays the document's).
|
|
450
403
|
if (!_rapierSanitizeRuntime.chromeIds) _rapierSanitizeRuntime.chromeIds = new Set(Array.from(typeof document?.querySelectorAll === 'function' ? document.querySelectorAll('[id]') : [], element => element.id));
|
|
451
404
|
return _rapierSanitizeRuntime.chromeIds.has(String(id)) || /^rapier-(?!anchor-)/.test(id);
|
|
452
405
|
}
|
|
453
406
|
|
|
454
|
-
// A link whose words or address carry a bidi control, or whose host is spelled in punycode, can show a destination it does
|
|
455
|
-
// not have. Opening one names its real address first; the answer is that address, or '' when the link opens at once.
|
|
456
407
|
const RAPIER_BIDI_CONTROL = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/;
|
|
457
408
|
function _rapierLinkDestinationToConfirm(href, text = '') {
|
|
458
409
|
let url, address = String(href || '');
|
|
@@ -484,9 +435,6 @@ function _rapierCssPresentationIsRemote(property, value) {
|
|
|
484
435
|
return declaration.length ? _rapierDropRemoteDeclarations(declaration) : false;
|
|
485
436
|
}
|
|
486
437
|
|
|
487
|
-
// A reference with no scheme and no host of its own ("photo.png", "#top", each word of a class) resolves on the base's scheme and
|
|
488
|
-
// host whatever its path, so its answer is the base's, found once for each base and page: a long document's render asked
|
|
489
|
-
// this of every token of every attribute, a URL built each time.
|
|
490
438
|
const relativeReference = {base: null, here: null, origin: ''};
|
|
491
439
|
function _rapierRemoteSubresourceOrigin(value) {
|
|
492
440
|
const raw = String(value == null ? '' : value)
|
|
@@ -544,7 +492,7 @@ function _rapierDropRemoteRules(rules, owner) {
|
|
|
544
492
|
}
|
|
545
493
|
|
|
546
494
|
function _rapierVerifyRasterBytes(bytes, mime) {
|
|
547
|
-
if (mime === 'image/jxl') return globalThis.RapierImageAssets?.isJxl(bytes) === true;
|
|
495
|
+
if (mime === 'image/jxl') return globalThis.RapierImageAssets?.isJxl(bytes) === true;
|
|
548
496
|
|
|
549
497
|
// declaration, doctype or generator/license comment before <svg itself, of whatever length its
|
|
550
498
|
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// The document's renderer. DOM, codecs and host state are explicit inputs.
|
|
3
2
|
function createRenderStyles(runtime) {
|
|
4
3
|
const {_rapierArtifactAccent, _rapierStyleText} = runtime;
|
|
5
4
|
function _rapierArtifactThemeCss(houseDefaults = false) {
|
|
6
|
-
// Geometry and the selected accent belong to this page; palette and type belong to the
|
|
7
5
|
// MIT reference sheet. Static code highlighting reads the app token names.
|
|
8
|
-
// The live editor resolves System to Light before applying the reference heading tokens;
|
|
9
|
-
// a carried System page keeps following the reader's device, including those light tokens.
|
|
10
6
|
const accent = houseDefaults ? '' : ':root,.md-render{--md-color-accent:' + _rapierArtifactAccent() + '}';
|
|
11
7
|
return `
|
|
12
8
|
${accent}
|
|
@@ -82,7 +78,6 @@ function _rapierArtifactStyles(theme, includeFonts, printMode, houseDefaults = f
|
|
|
82
78
|
else if (includeFonts) parts.push(_rapierStyleText('rapier-font-style'));
|
|
83
79
|
parts.push(_rapierStyleText('rapier-content-style'));
|
|
84
80
|
parts.push(_rapierArtifactThemeCss(houseDefaults));
|
|
85
|
-
// Local faces keep an offline fallback. The HTML writer may append a small factory Sans subset.
|
|
86
81
|
if (!includeFonts && fontCss === null) parts.push(`@font-face{font-family:'Geist/Roboto';src:local('Roboto');font-weight:400 500;size-adjust:105%;ascent-override:95.7%;descent-override:28.1%;line-gap-override:0%}
|
|
87
82
|
@font-face{font-family:'Geist/Roboto';src:local('Roboto Bold');font-weight:600 700;size-adjust:105%;ascent-override:95.7%;descent-override:28.1%;line-gap-override:0%}
|
|
88
83
|
@font-face{font-family:'Geist/Helvetica Neue';src:local('Helvetica Neue');font-weight:400 500;size-adjust:103.8%;ascent-override:96.8%;descent-override:28.4%;line-gap-override:0%}
|
|
@@ -102,8 +97,6 @@ function _rapierArtifactStyles(theme, includeFonts, printMode, houseDefaults = f
|
|
|
102
97
|
:root,.md-render{--md-font-sans:'Geist','Geist/Roboto','Geist/Helvetica Neue','Geist/Segoe UI','Geist/Arial','Geist/Noto Sans','Geist/Ubuntu','Geist/Liberation Sans','Geist/DejaVu Sans',system-ui,sans-serif}`);
|
|
103
98
|
const highlightCss = _rapierStyleText('rapier-highlight-style');
|
|
104
99
|
parts.push(highlightCss);
|
|
105
|
-
// A page written for the system theme has no body.light to switch: under a light system
|
|
106
|
-
// setting each palette takes its light values from the sheet's own body.light rule.
|
|
107
100
|
if (theme === 'system') for (const [, palette, light] of highlightCss.matchAll(/html\[data-highlights="(standard|accent)"\] body\.light\s*\{([^}]*)\}/g))
|
|
108
101
|
parts.push('@media (prefers-color-scheme:light){html[data-rapier-theme="system"][data-highlights="' + palette + '"] body{' + light.trim() + '}}');
|
|
109
102
|
if (printMode) parts.push(_rapierArtifactPrintCss());
|
package/dist/kit/render-work.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// The synchronous and cooperative writers consume the same document transformations.
|
|
3
2
|
export function finish(iterator) {
|
|
4
3
|
let next;
|
|
5
4
|
do { next = iterator.next(); } while (!next.done);
|
|
@@ -19,8 +18,6 @@ export async function pause(work, fraction, label) {
|
|
|
19
18
|
check(work);
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
// This locates complete fragments; the existing HTML parser and sanitizer still admit
|
|
23
|
-
// every byte. Uncertain nesting stays together instead of guessing a new boundary.
|
|
24
21
|
export function htmlBoundary() {
|
|
25
22
|
const stack = [], voids = new Set('area base br col embed hr img input link meta param source track wbr'.split(' '));
|
|
26
23
|
const rawTags = new Set('script style textarea title xmp iframe noembed noframes'.split(' '));
|
|
@@ -101,9 +98,6 @@ export function* cloneTree(root) {
|
|
|
101
98
|
return copy;
|
|
102
99
|
}
|
|
103
100
|
|
|
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
101
|
export function* htmlParts(root, limit = 16384, consume = false) {
|
|
108
102
|
limit = Math.max(2, limit);
|
|
109
103
|
const text = root.ownerDocument.createElement('div');
|
|
@@ -135,11 +129,7 @@ export function* htmlParts(root, limit = 16384, consume = false) {
|
|
|
135
129
|
if (at.nodeType === 1) {
|
|
136
130
|
if (small(at)) pieces.push(at.outerHTML);
|
|
137
131
|
else {
|
|
138
|
-
// Native shallow serialization owns tag/attribute spelling and
|
|
139
|
-
// escaping. Native text serialization below keeps its parent context.
|
|
140
132
|
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
133
|
if (at.firstChild) empty.appendChild(root.ownerDocument.createTextNode(''));
|
|
144
134
|
const shell = empty.outerHTML, close = /<\/[^<>]+>$/.exec(shell)?.[0] || '';
|
|
145
135
|
if (close && at.firstChild) {
|
|
@@ -182,7 +172,6 @@ export function* htmlParts(root, limit = 16384, consume = false) {
|
|
|
182
172
|
export function admitHtml(root, html, sanitize, fragment = null, context = 'export') {
|
|
183
173
|
if (fragment) root.appendChild(fragment(html, context));
|
|
184
174
|
else {
|
|
185
|
-
// Inert string-only hosts retain their explicitly supported admission path.
|
|
186
175
|
const part = root.ownerDocument.createElement('div');
|
|
187
176
|
part.innerHTML = sanitize(html, context);
|
|
188
177
|
while (part.firstChild) root.appendChild(part.firstChild);
|
|
@@ -208,7 +197,6 @@ export function* serializeTree(root, sanitize = null, trim = false) {
|
|
|
208
197
|
return trim ? result.trim() : result;
|
|
209
198
|
}
|
|
210
199
|
|
|
211
|
-
// Chunk boundaries never split a UTF-16 pair; TextEncoder remains the byte authority.
|
|
212
200
|
export function* encodeUtf8Steps(source, size = 65536) {
|
|
213
201
|
const encoder = new TextEncoder(), parts = [];
|
|
214
202
|
let length = 0;
|