rapier-markdown-kit 1.2.7 → 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -30
- package/dist/agent/will.mjs +1 -6
- package/dist/images/gif.mjs +0 -3
- package/dist/images/playback.mjs +1 -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/index.mjs +1 -1
- package/dist/kit/ledger/authored-origin.mjs +0 -5
- package/dist/kit/ledger/authorship.mjs +0 -2
- package/dist/kit/ledger/carried.d.mts +20 -0
- 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.d.mts +88 -0
- package/dist/kit/ledger.mjs +14 -0
- package/dist/kit/markdown-it.d.mts +4 -0
- package/dist/kit/markdown-it.mjs +36 -0
- package/dist/kit/pdf-glyphs.mjs +39 -45
- package/dist/kit/pdf-source.d.mts +5 -0
- package/dist/kit/pdf-source.mjs +125 -0
- 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 +15 -119
- package/dist/kit/render-print.mjs +4 -49
- package/dist/kit/render-sanitize.mjs +22 -58
- package/dist/kit/render-styles.mjs +0 -7
- package/dist/kit/render-work.mjs +0 -12
- package/dist/kit/render.mjs +18 -173
- package/dist/kit/will.d.mts +42 -0
- 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 +6 -25
- package/dist/spec/frontmatter.mjs +0 -1
- package/dist/spec/html-blocks.mjs +3 -37
- 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/word-sidecar.mjs +0 -2
- package/package.json +21 -4
- package/style.css +38 -21
|
@@ -5,9 +5,10 @@ import {fontRequests, waitForFonts} from './export-fonts.mjs';
|
|
|
5
5
|
import {apply as applyTypography} from '../layout/typography.mjs';
|
|
6
6
|
import {documentLanguage} from '../spec/document-settings.mjs';
|
|
7
7
|
import {applyPrintCapitals} from './print-capitals.mjs';
|
|
8
|
+
import {stripOneTerminator as _rapierWillStripOneTerminator} from '../agent/will.mjs';
|
|
9
|
+
import {quotedWillSpan} from '../spec/md-marks.mjs';
|
|
8
10
|
const RAPIER_WILL_SENTINEL_MARK = '';
|
|
9
11
|
// SPDX-License-Identifier: MIT
|
|
10
|
-
// The document's renderer. DOM, codecs and host state are explicit inputs.
|
|
11
12
|
function createPrintRenderer(runtime) {
|
|
12
13
|
const {Node, _rapierWillParse, crypto, document, globalThis} = runtime;
|
|
13
14
|
function _rapierWillSentinelPlan(canonical, kind) {
|
|
@@ -27,12 +28,11 @@ function _rapierWillSentinelPlan(canonical, kind) {
|
|
|
27
28
|
return { canonical: out + source.slice(cursor), markers, sentinel };
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const _rapierWillStripOneTerminator = globalThis.RapierAgentWill.stripOneTerminator;
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
function _rapierPrintWillAdmit(canonical, plan) {
|
|
35
|
-
const quoted =
|
|
35
|
+
const quoted = quotedWillSpan(canonical, _rapierWillParse(canonical).blocks.map(block => [block.start, block.end]));
|
|
36
36
|
if (quoted && quoted.where === 'frontmatter') throw _rapierPrintWillLost('a marker stands in the front matter, which is not on the printed page', 'Take it out of the front matter');
|
|
37
37
|
if (quoted) 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');
|
|
38
38
|
for (const marker of plan.markers) {
|
|
@@ -45,8 +45,6 @@ function _rapierPrintWillAdmit(canonical, plan) {
|
|
|
45
45
|
function _rapierPrintWillFont(markers, advanceOverride) {
|
|
46
46
|
const metrics = _rapierPrintWillMetrics(markers);
|
|
47
47
|
const size = metrics.size;
|
|
48
|
-
// The page's carrier face keeps the short marker advance. Searchable text is
|
|
49
|
-
// positioned at 500 units, which is also /DW, so this copy can be asked to match it.
|
|
50
48
|
const advance = advanceOverride ?? metrics.advance;
|
|
51
49
|
const scalars = Array.from(new Set(markers.flatMap(marker => Array.from(marker, ch => ch.codePointAt(0)))));
|
|
52
50
|
const order = scalars.filter(code => code < 0x10000).sort((a, b) => a - b)
|
|
@@ -74,8 +72,6 @@ function _rapierPrintWillFont(markers, advanceOverride) {
|
|
|
74
72
|
const cmap = narrow.length <= 0xffff
|
|
75
73
|
? cat(u16(0), u16(2), u16(3), u16(1), u32(20), u16(3), u16(10), u32(20 + narrow.length), narrow, full)
|
|
76
74
|
: cat(u16(0), u16(1), u16(3), u16(10), u32(12), full);
|
|
77
|
-
// The collinear contour has zero area and enough bounds for native print at 2px.
|
|
78
|
-
// Keep it below the baseline so a carrier at a page break does not straddle both page clips.
|
|
79
75
|
const bounds = [0, -400, 10, 0];
|
|
80
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]);
|
|
81
77
|
const utf16 = text => Array.from(text, ch => u16(ch.charCodeAt(0)));
|
|
@@ -119,7 +115,6 @@ function _rapierPrintWillMetrics(markers) {
|
|
|
119
115
|
}
|
|
120
116
|
|
|
121
117
|
function _rapierPrintWillPlace(root, plan) {
|
|
122
|
-
// 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.
|
|
123
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'); };
|
|
124
119
|
const measure = _rapierPrintWillMetrics(plan.markers);
|
|
125
120
|
const tokens = new Map(plan.markers.map((marker, index) => [plan.sentinel + index + RAPIER_WILL_SENTINEL_MARK, index]));
|
|
@@ -152,8 +147,6 @@ function _rapierPrintWillPlace(root, plan) {
|
|
|
152
147
|
carrier.textContent = plan.markers[row.index];
|
|
153
148
|
return carrier;
|
|
154
149
|
});
|
|
155
|
-
// 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
|
|
156
|
-
// moves to the run, so the gap the rule makes with the block above is the one it made.
|
|
157
150
|
if (next && next.tagName === 'HR') {
|
|
158
151
|
if (first.previousElementSibling || first.parentNode !== root) carriers[0].style.marginTop = 'var(--md-line)';
|
|
159
152
|
next.style.marginTop = '0';
|
|
@@ -176,7 +169,6 @@ function _rapierPrintWillPageCss() {
|
|
|
176
169
|
+ '}';
|
|
177
170
|
}
|
|
178
171
|
|
|
179
|
-
// The capture's own limits (agent/visual.mjs VISUAL_LIMITS): the longest edge and the most pixels one capture returns.
|
|
180
172
|
const CAPTURE_EDGE = 4096, CAPTURE_PIXELS = 4 * 1024 * 1024;
|
|
181
173
|
|
|
182
174
|
async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings}, {capture, signal, current, maxBytes = 8 * 1024 * 1024, film, ink} = {}) {
|
|
@@ -186,8 +178,6 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
186
178
|
if (current?.() === false) refuse('document_changed');
|
|
187
179
|
};
|
|
188
180
|
if (!root?.ownerDocument || typeof capture !== 'function') refuse('pdf_render_unavailable');
|
|
189
|
-
// The native printer chooses its default paper. A file made without a printer uses A4,
|
|
190
|
-
// with the same explicit document settings and default margins as the print artifact.
|
|
191
181
|
const paper = settings?.papersize || {width: 11906, height: 16838};
|
|
192
182
|
const width = paper.width / 20, height = paper.height / 20;
|
|
193
183
|
const margin = settings?.geometry ? settings.geometry.twips / 20 : null;
|
|
@@ -220,8 +210,6 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
220
210
|
check();
|
|
221
211
|
const into = frame.contentDocument;
|
|
222
212
|
if (!into?.body) refuse('pdf_render_unavailable');
|
|
223
|
-
// The capture and native printer use standards layout. An initial blank
|
|
224
|
-
// iframe otherwise measures preformatted inline struts in quirks mode.
|
|
225
213
|
into.open(); into.write('<!doctype html><html><head></head><body></body></html>'); into.close();
|
|
226
214
|
into.documentElement.setAttribute('data-rapier-theme', 'light');
|
|
227
215
|
into.documentElement.setAttribute('lang', documentLanguage(settings, document.defaultView?.navigator?.language));
|
|
@@ -245,7 +233,6 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
245
233
|
strip.append(root); surface.append(strip); into.body.append(surface);
|
|
246
234
|
const main = root.querySelector('main');
|
|
247
235
|
if (!main) refuse('pdf_render_unavailable');
|
|
248
|
-
// A file for paper is light whatever the person's own scheme: the page's text takes its colour scheme from this attribute.
|
|
249
236
|
main.setAttribute('data-md-theme', 'light');
|
|
250
237
|
for (const details of main.querySelectorAll('details')) details.open = true;
|
|
251
238
|
main.getBoundingClientRect();
|
|
@@ -253,12 +240,8 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
253
240
|
await step(into.fonts.ready);
|
|
254
241
|
for (const image of main.querySelectorAll('img')) { image.loading = 'eager'; await step(image.decode()); }
|
|
255
242
|
check();
|
|
256
|
-
// The print sheet owns the words and styles. CSS fragmentation supplies the file's
|
|
257
|
-
// pages without a print dialog; its break constraints become column constraints here.
|
|
258
243
|
for (const node of [main, ...main.querySelectorAll('*')]) {
|
|
259
244
|
const style = into.defaultView.getComputedStyle(node);
|
|
260
|
-
// Clipped, out-of-flow source has no page ink. Leaving it in a multicolumn
|
|
261
|
-
// container can still create blank columns and expose its text to PDF search.
|
|
262
245
|
if (!node.closest('.rapier-will-carrier') && ['absolute', 'fixed'].includes(style.position) && /^rect\(/.test(style.clip)) {
|
|
263
246
|
const box = style.clip.slice(5, -1).trim().split(/[,\s]+/).map(Number.parseFloat);
|
|
264
247
|
if (box.length === 4 && box.every(Number.isFinite) && (box[1] <= box[3] || box[2] <= box[0])) {
|
|
@@ -277,12 +260,9 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
277
260
|
globalThis.RapierMath?.fit(main);
|
|
278
261
|
applyPrintCapitals(main);
|
|
279
262
|
applyTypography(main, {all: true});
|
|
280
|
-
// The document's ink marks, drawn on the words where they now stand on the page.
|
|
281
263
|
ink?.(main);
|
|
282
264
|
const count = Math.max(1, Math.ceil((main.scrollWidth - 1) / bodyWidth));
|
|
283
265
|
if (!Number.isSafeInteger(count)) refuse('pdf_page_geometry_invalid');
|
|
284
|
-
// Captured document font bytes own a same-name family; factory glyphs must
|
|
285
|
-
// never replace their page paint. This uses the artifact, not a live catalog.
|
|
286
266
|
const customFamilies = new Set(fontCss ? fontRequests(main, {fontCss, css, settings}).map(face => face.family.toLowerCase()) : []);
|
|
287
267
|
nativeGlyphs = await collectPdfGlyphs(main, {width: bodyWidth, check,
|
|
288
268
|
faces: globalThis.RapierDocxFonts?.DOCX_FONTS?.filter(face => !customFamilies.has(face.family.toLowerCase()))});
|
|
@@ -303,11 +283,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
303
283
|
const font = _rapierPrintWillFont(text, 500);
|
|
304
284
|
strip.style.width = count * bodyWidth + 'px';
|
|
305
285
|
strip.style.transform = 'none';
|
|
306
|
-
// One styled film is reused for every page window. Batches stay inside the capture edge
|
|
307
|
-
// and pixel limits. A picture no longer forces its own capture. Pixels are deflated once.
|
|
308
286
|
if (typeof film !== 'function') refuse('pdf_render_unavailable');
|
|
309
|
-
// step() can reject after the film resolves (the document changed, or the attempt was
|
|
310
|
-
// cancelled). The session would otherwise keep its abort listener with no owner.
|
|
311
287
|
let pendingFilm = null, abandonFilm = false;
|
|
312
288
|
const trackedFilm = Promise.resolve(film({root: surface, signal, current, frame: {width: count * bodyWidth, height: bodyHeight}})).then(session => {
|
|
313
289
|
if (abandonFilm) session?.close?.();
|
|
@@ -365,8 +341,6 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
365
341
|
} finally { filmSession?.close(); nativeGlyphs?.restore(); vectors?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
|
|
366
342
|
}
|
|
367
343
|
|
|
368
|
-
// Ranges locate the actual text fragments after column layout. Splitting a text node at
|
|
369
|
-
// its line and page boundaries keeps a PDF search on the page that carries those words.
|
|
370
344
|
async function _rapierPrintTextPages(main, pageWidth, count, check, origin = main.getBoundingClientRect(), nativeNodes = null) {
|
|
371
345
|
const into = main.ownerDocument;
|
|
372
346
|
const pages = Array.from({length: count}, () => []), walker = into.createTreeWalker(main, 5);
|
|
@@ -381,8 +355,6 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
381
355
|
if (node.nodeType === 1 && !formula || node.nodeType === 3 && element.closest('.math-rendered[data-math-src]')) continue;
|
|
382
356
|
let value;
|
|
383
357
|
if (formula) {
|
|
384
|
-
// The math owner paints SVG paths and retains the equation's exact TeX alternative.
|
|
385
|
-
// Carry that semantic text at the SVG's measured position for search and copy.
|
|
386
358
|
value = formula.getAttribute('aria-label') || '';
|
|
387
359
|
} else value = node.data;
|
|
388
360
|
const style = into.defaultView.getComputedStyle(element);
|
|
@@ -396,14 +368,11 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
396
368
|
const visit = (start, end) => {
|
|
397
369
|
if (++operations % 128 === 0) check();
|
|
398
370
|
if (!formula) { range.setStart(node, start); range.setEnd(node, end); }
|
|
399
|
-
// A tiny carrier font can have its advance rounded to zero by a browser. Its
|
|
400
|
-
// measured baseline still locates the marker; the font's authored metrics keep its text width.
|
|
401
371
|
const rects = Array.from(formula ? [formula.getBoundingClientRect()] : range.getClientRects()).filter(rect => rect.height > 0 && (rect.width > 0 || carrier));
|
|
402
372
|
if (!rects.length) { if (carrier) markerLost(); return; }
|
|
403
373
|
const first = rects[0], page = pageOf(first.left);
|
|
404
374
|
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);
|
|
405
375
|
let middle = formula ? start : Math.floor((start + end) / 2);
|
|
406
|
-
// A line never breaks inside a character: halve between whole ones (a letter and its marks, a conjunct, an emoji).
|
|
407
376
|
if (middle > start) middle = (characters ||= segmenter.segment(value)).containing(middle).index;
|
|
408
377
|
const cluster = native?.find(item => item.start < middle && item.end > middle);
|
|
409
378
|
if (cluster) middle = cluster.start > start ? cluster.start : cluster.end;
|
|
@@ -435,9 +404,6 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
435
404
|
visit(0, value.length);
|
|
436
405
|
}
|
|
437
406
|
check();
|
|
438
|
-
// Trailing-carrier print CSS pulls a last marker up so it stays on the sheet.
|
|
439
|
-
// Keep that paint; put the searchable object after the preceding words so
|
|
440
|
-
// reading order matches the source (the marker itself has no visible ink).
|
|
441
407
|
const pageHeight = origin.height;
|
|
442
408
|
for (const page of pages) {
|
|
443
409
|
let prev = null;
|
|
@@ -453,8 +419,6 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
453
419
|
prev = run;
|
|
454
420
|
}
|
|
455
421
|
}
|
|
456
|
-
// Each run carries the tags of its block's structure (outermost first): a PDF reader finds the document's headings, lists and
|
|
457
|
-
// tables from them rather than from positions.
|
|
458
422
|
const ROLES = {TABLE: 'Table', TR: 'TR', TD: 'TD', TH: 'TH', UL: 'L', OL: 'L', LI: 'LI', BLOCKQUOTE: 'BlockQuote', P: 'P', PRE: 'Code', FIGCAPTION: 'Caption',
|
|
459
423
|
H1: 'H1', H2: 'H2', H3: 'H3', H4: 'H4', H5: 'H5', H6: 'H6'};
|
|
460
424
|
const tags = block => {
|
|
@@ -497,8 +461,6 @@ function _rapierPrintLinkPages(main, width, height, count) {
|
|
|
497
461
|
return {_rapierWillSentinelPlan, _rapierPrintWillAdmit, _rapierPrintWillFont, _rapierPrintWillMetrics, _rapierPrintWillPlace, _rapierPrintWillLost, _rapierPrintWillPageCss, _rapierWritePrintPdf};
|
|
498
462
|
}
|
|
499
463
|
|
|
500
|
-
// Where each scalar of an invisible text run stands, in character slots from the run's left edge: a mark shares its letter's
|
|
501
|
-
// 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).
|
|
502
464
|
function _rapierPrintSlots(text, rtl) {
|
|
503
465
|
const pieces = [];
|
|
504
466
|
let scalars = 0, count = 0;
|
|
@@ -519,8 +481,6 @@ function _rapierPrintSlots(text, rtl) {
|
|
|
519
481
|
return {slots, count};
|
|
520
482
|
}
|
|
521
483
|
|
|
522
|
-
// A standard PDF page tree, image streams, Unicode text and an associated source file.
|
|
523
|
-
// It accepts already-rendered pages, so there is no second document grammar or layout engine.
|
|
524
484
|
async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [], canonical, bom = false, filename, title, font, width, height, margins, pixelWidth, pixelHeight, pageNumbers, maxBytes, check}) {
|
|
525
485
|
const encoder = new TextEncoder(), encode = value => encoder.encode(value);
|
|
526
486
|
const fail = code => { throw Object.assign(new Error(code), {code}); };
|
|
@@ -588,7 +548,6 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
588
548
|
nativeResources.push(entry.name + ' ' + type + ' 0 R');
|
|
589
549
|
}
|
|
590
550
|
const sharedImages = new Map();
|
|
591
|
-
// The structure tree (PDF 1.7, 14.7): one element per tagged DOM element, each run marked with its page's next MCID.
|
|
592
551
|
const structs = new Map(), documentElement = {role: 'Document', kids: []}, parentTree = [];
|
|
593
552
|
const structOf = tags => {
|
|
594
553
|
let parent = documentElement;
|
|
@@ -606,7 +565,6 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
606
565
|
const held = pages[index], flate = !!held?.rgb;
|
|
607
566
|
const picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (flate ? 'FlateDecode' : 'DCTDecode'), flate ? held.rgb : held);
|
|
608
567
|
pages[index] = null;
|
|
609
|
-
// The page's picture is how its tagged text looks, not a figure of its own: an artifact.
|
|
610
568
|
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';
|
|
611
569
|
const pictures = ['/Im0 ' + picture + ' 0 R'], states = [];
|
|
612
570
|
for (const drawing of vectorPages[index] || []) {
|
|
@@ -637,9 +595,6 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
637
595
|
const textAlphas = new Map();
|
|
638
596
|
for (const run of textPages[index]) {
|
|
639
597
|
if (!run.text) continue;
|
|
640
|
-
// Keep one logical Unicode text object, resetting its font before each scalar's direction
|
|
641
|
-
// can change. Readers that ignore ActualText otherwise discard earlier words in mixed RTL/LTR.
|
|
642
|
-
// Express width in the font size: some readers ignore horizontal text scaling when joining.
|
|
643
598
|
const element = structOf(run.tags || []);
|
|
644
599
|
if (element) {element.kids.push({page: index, mcid: marked.length}); commands += `/${element.role} << /MCID ${marked.length} >> BDC\n`; marked.push(element);}
|
|
645
600
|
commands += `q\n/Span << /ActualText <feff${hex(run.text)}> >> BDC\nBT\n100 Tz\n`;
|
|
@@ -682,7 +637,7 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
682
637
|
const parents = add('<< /Nums [' + parentTree.map((marked, index) => index + ' [' + marked.map(element => ids.get(element) + ' 0 R').join(' ') + ']').join(' ') + '] >>');
|
|
683
638
|
put(tree, '<< /Type /StructTreeRoot /K [' + ids.get(documentElement) + ' 0 R] /ParentTree ' + parents + ' 0 R /ParentTreeNextKey ' + parentTree.length + ' >>');
|
|
684
639
|
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] >>');
|
|
685
|
-
const info = add('<< /Producer (Rapier) /Title <feff' + hex(title || filename.replace(/\.[^.]*$/, '')) + '> >>');
|
|
640
|
+
const info = add('<< /Producer (Rapier) /Creator (Rapier) /Title <feff' + hex(title || filename.replace(/\.[^.]*$/, '')) + '> >>');
|
|
686
641
|
const header = encode('%PDF-1.7\n%\u00e2\u00e3\u00cf\u00d3\n'), offsets = new Array(objects.length);
|
|
687
642
|
let cursor = header.length;
|
|
688
643
|
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,17 +75,18 @@ 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
|
-
|
|
92
|
-
|
|
93
|
-
// content, entities and malformed table markup cross the same boundary as a browser's parser.
|
|
94
|
-
function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeRasterDataUrl}) {
|
|
95
|
-
if (![parseFragment, serialize, sanitizeHtml, safeRasterDataUrl].every(port => typeof port === 'function'))
|
|
88
|
+
function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeRasterDataUrl, chromeOwnsId = id => /^rapier-(?!anchor-)/.test(id)}) {
|
|
89
|
+
if (![parseFragment, serialize, sanitizeHtml, safeRasterDataUrl, chromeOwnsId].every(port => typeof port === 'function'))
|
|
96
90
|
throw new TypeError('The inert HTML sanitizer requires every parser and filtering port');
|
|
97
91
|
const uri = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
|
|
98
92
|
const uriSafe = new Set(['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']);
|
|
@@ -101,8 +95,6 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
|
|
|
101
95
|
const svgTags = new Set(RAPIER_SANITIZE_PROFILES.svgTags), mathTags = new Set(RAPIER_SANITIZE_PROFILES.mathTags);
|
|
102
96
|
const mathIntegration = new Set(['mi', 'mo', 'mn', 'ms', 'mtext']), htmlIntegration = new Set(['annotation-xml']);
|
|
103
97
|
const sharedHtmlSvg = new Set(['title', 'style', 'font', 'a', 'script']);
|
|
104
|
-
// DOMPurify's default namespace admission applies in addition to its tag profile. In
|
|
105
|
-
// particular a MathML name parsed as HTML, or HTML inside SVG desc, is not admitted.
|
|
106
98
|
const validNamespace = (node, parent) => {
|
|
107
99
|
const tag = node.tagName.toLowerCase(), parentTag = (parent.tagName || 'template').toLowerCase();
|
|
108
100
|
const namespace = node.namespaceURI, parentNamespace = parent.namespaceURI || htmlNamespace;
|
|
@@ -156,6 +148,7 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
|
|
|
156
148
|
const key = name.toLowerCase();
|
|
157
149
|
const value = name === 'value' ? inputValue : inputValue.trim();
|
|
158
150
|
if (forbiddenAttributes.has(key) || key.startsWith('on')) continue;
|
|
151
|
+
if (key === 'id' && chromeOwnsId(value)) continue;
|
|
159
152
|
if (/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i.test(value)
|
|
160
153
|
|| key === 'attributename' && value.includes('href')) continue;
|
|
161
154
|
const data = /^data-[\-\w.\u00b7-\uffff]+$/.test(key), aria = /^aria-[\-\w]+$/.test(key);
|
|
@@ -177,13 +170,9 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
|
|
|
177
170
|
};
|
|
178
171
|
}
|
|
179
172
|
|
|
180
|
-
// The document's renderer. DOM, codecs and host state are explicit inputs.
|
|
181
173
|
function createRenderSanitizer(runtime) {
|
|
182
174
|
const {CSSStyleSheet, DOMPurify, RAPIER_RASTER_DATA_URL_RE, URL, _rapierCssDeclaration, _rapierDropRemoteDeclarations, _rapierRemoteContent, _rapierRuleDescriptorIsRemote, _rapierSanitizeRuntime, document, globalThis, location} = runtime;
|
|
183
175
|
|
|
184
|
-
// Only the diagram reader and its styled export admit static foreignObject labels. The
|
|
185
|
-
// ordinary SVG reader still rejects HTML subdocuments. The export class selects scope;
|
|
186
|
-
// every descendant is checked again, so a copied class never confers authority.
|
|
187
176
|
function diagramLabelRoot(node) {
|
|
188
177
|
for (let current = node; current?.nodeType === 1; current = current.parentElement) {
|
|
189
178
|
if (current.localName?.toLowerCase() === 'foreignobject') return current;
|
|
@@ -203,7 +192,6 @@ function diagramLabelStyle(property, value) {
|
|
|
203
192
|
declaration.setProperty(property, value);
|
|
204
193
|
const parsed = declaration.getPropertyValue(property);
|
|
205
194
|
if (!parsed || /(?:url|image-set|element|paint|attr)\s*\(/i.test(parsed)) return false;
|
|
206
|
-
// SVG paint accepts resource references; a custom property must not smuggle one in.
|
|
207
195
|
return !/^(?:fill|stroke)$/.test(property) || !/var\s*\(/i.test(parsed);
|
|
208
196
|
}
|
|
209
197
|
|
|
@@ -277,14 +265,6 @@ function _rapierInstallSanitizeHooks() {
|
|
|
277
265
|
&& _rapierImageDataAttributeAllowed(tag, data.attrName.toLowerCase(), compact, _rapierSafeRasterDataUrl);
|
|
278
266
|
if (!rasterAllowed) data.keepAttr = false;
|
|
279
267
|
});
|
|
280
|
-
// Paste, render and export sanitise: a remote <img src> would fetch the instant its markup lands in the page,
|
|
281
|
-
// so the src the hook below is about to strip never survives. Move it to a data-* attribute first
|
|
282
|
-
// (DOMPurify already leaves data-* alone, and the sanitizer's own inert document never fetches it
|
|
283
|
-
// either), so the picture and its alt text still reach Turndown's rapierImage rule as a live
|
|
284
|
-
// pasted-page picture and write out `` -- not vanish -- and an <img> in a document's own
|
|
285
|
-
// HTML (a table kept as HTML) is written again with its URL: the raw profile admits no data-*, so
|
|
286
|
-
// only the three this hook sets on the node it holds back stay. A picture inside a link is the same
|
|
287
|
-
// node with the same fix: the outer <a> rule reads this img's own converted markdown as its content.
|
|
288
268
|
DOMPurify.addHook('uponSanitizeElement', node => {
|
|
289
269
|
if (_rapierRemoteContent.allowed || !/^(?:paste|render|raw|export)$/.test(_rapierSanitizeRuntime.context)) return;
|
|
290
270
|
if (!node || node.nodeName !== 'IMG' || node.hasAttribute('data-rapier-remote-src')) return;
|
|
@@ -306,18 +286,15 @@ function _rapierInstallSanitizeHooks() {
|
|
|
306
286
|
node.setAttribute('data-rapier-remote-src', href);
|
|
307
287
|
}
|
|
308
288
|
});
|
|
309
|
-
// A list from Word is named by the style of its paragraph and of the span that holds its marker (mso-list,
|
|
310
|
-
// spec/html-reading.mjs), and the CSS pass below empties a style of every property the browser does not
|
|
311
|
-
// know: a marker's style is only `mso-list:Ignore`. The paste carries both as data-* first, where they stay,
|
|
312
|
-
// for the conversion that reads them (_rapierPasteWordLists).
|
|
313
289
|
DOMPurify.addHook('uponSanitizeElement', node => {
|
|
314
290
|
if (_rapierSanitizeRuntime.context !== 'paste' || !node || !/^(?:P|LI|H[1-6]|SPAN)$/.test(node.nodeName)) return;
|
|
315
|
-
// Only the style names them: a page cannot bring these attributes of its own.
|
|
316
291
|
node.removeAttribute('data-rapier-word-level');
|
|
317
292
|
node.removeAttribute('data-rapier-word-marker');
|
|
318
293
|
const style = node.getAttribute('style');
|
|
319
294
|
if (!style || !/mso-list/i.test(style)) return;
|
|
320
|
-
const
|
|
295
|
+
const spec = globalThis.RapierMarkdownSpec;
|
|
296
|
+
if (!spec) return;
|
|
297
|
+
const {wordListLevel, wordListIsMarker} = spec;
|
|
321
298
|
const level = /^(?:P|LI|H[1-6])$/.test(node.nodeName) ? wordListLevel(style) : 0;
|
|
322
299
|
if (level) node.setAttribute('data-rapier-word-level', String(level));
|
|
323
300
|
else if (node.nodeName === 'SPAN' && wordListIsMarker(style)) node.setAttribute('data-rapier-word-marker', '1');
|
|
@@ -329,9 +306,7 @@ function _rapierInstallSanitizeHooks() {
|
|
|
329
306
|
data.keepAttr = !!data.attrValue;
|
|
330
307
|
return;
|
|
331
308
|
}
|
|
332
|
-
|
|
333
|
-
// allow is about what the page fetches, and this rule is about what the page's chrome finds.
|
|
334
|
-
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; }
|
|
335
310
|
if (_rapierRemoteContent.allowed || _rapierSanitizeRuntime.context === 'source') return;
|
|
336
311
|
if (String(data.attrName || '').toLowerCase() === 'style') {
|
|
337
312
|
const cleaned = _rapierStyleWithoutRemoteUrls(value);
|
|
@@ -339,7 +314,6 @@ function _rapierInstallSanitizeHooks() {
|
|
|
339
314
|
return;
|
|
340
315
|
}
|
|
341
316
|
const attribute = String(data.attrName || '').toLowerCase();
|
|
342
|
-
// These attributes carry authored text, never resource requests.
|
|
343
317
|
if (attribute === 'alt' || attribute === 'title') return;
|
|
344
318
|
if (/^data-rapier-remote-(?:src|alt|title)$/.test(attribute) && node && node._rapierHeldBack === true) { data.forceKeepAttr = true; return; }
|
|
345
319
|
if (attribute.startsWith('data-')) return;
|
|
@@ -358,9 +332,6 @@ function _rapierInstallSanitizeHooks() {
|
|
|
358
332
|
data.forceKeepAttr = true;
|
|
359
333
|
}
|
|
360
334
|
});
|
|
361
|
-
// A link that opens a new tab never hands that tab a window.opener or a Referer: the editor's own
|
|
362
|
-
// click router already opens with noopener,noreferrer; the sanitized markup that Share and the
|
|
363
|
-
// standalone export carry gets the same rule in its bytes.
|
|
364
335
|
DOMPurify.addHook('afterSanitizeAttributes', node => {
|
|
365
336
|
if (!node || !/^(?:A|AREA)$/i.test(node.nodeName) || !node.hasAttribute('target')) return;
|
|
366
337
|
if (String(node.getAttribute('target') || '').trim().toLowerCase() !== '_blank') return;
|
|
@@ -382,25 +353,17 @@ function sanitizeRapierHtml(value, context = 'render') {
|
|
|
382
353
|
return sanitize(value, context, false);
|
|
383
354
|
}
|
|
384
355
|
|
|
385
|
-
// The same parser, profile and hooks admit both results. A private renderer can
|
|
386
|
-
// adopt the sanitized nodes without serializing and parsing them again.
|
|
387
356
|
function sanitizeRapierFragment(value, context = 'render') {
|
|
388
357
|
return sanitize(value, context, true);
|
|
389
358
|
}
|
|
390
359
|
|
|
391
360
|
function sanitize(value, context, fragment) {
|
|
392
|
-
// 'export' is the render profile under the export hooks; 'raw' is a document's own HTML (initMarkdownIt, html_block):
|
|
393
|
-
// the render profile without data-* attributes, which Rapier's delegated handlers read as their own controls, and
|
|
394
|
-
// (the hook) without an id the chrome owns. 'source' is Markdown source HTML, written and never shown: the render
|
|
395
|
-
// profile's admission, and the block on remote content stays the reader's (a remote picture's URL is the person's words).
|
|
396
361
|
const selected = context === 'raw' || context === 'export' || context === 'source' || RAPIER_SANITIZE_OPTIONS[context] ? context : 'render';
|
|
397
362
|
const labels = selected === 'diagram' || selected === 'export', key = selected + (fragment ? ' fragment' : '');
|
|
398
363
|
let options = purifyOptions.get(key);
|
|
399
364
|
if (!options) {
|
|
400
365
|
options = selected === 'raw' ? { ...RAPIER_SANITIZE_OPTIONS.render, ALLOW_DATA_ATTR: false } : RAPIER_SANITIZE_OPTIONS[selected] || RAPIER_SANITIZE_OPTIONS.render;
|
|
401
366
|
if (selected === 'source') options = {...options, ADD_DATA_URI_TAGS: ['image']};
|
|
402
|
-
// DOMPurify's USE_PROFILES replaces explicit allowlists. Add MathML to the
|
|
403
|
-
// selected profile without discarding its admitted HTML and SVG vocabulary.
|
|
404
367
|
if (labels) options = {...options, ALLOWED_TAGS: [...new Set([...options.ALLOWED_TAGS, ...RAPIER_SANITIZE_PROFILES.mathTags])],
|
|
405
368
|
ALLOWED_ATTR: [...new Set([...options.ALLOWED_ATTR, ...RAPIER_SANITIZE_PROFILES.mathAttributes])], ADD_TAGS: ['foreignobject'], ADD_DATA_URI_TAGS: ['image'],
|
|
406
369
|
HTML_INTEGRATION_POINTS: {foreignobject: true}, FORBID_TAGS: options.FORBID_TAGS.filter(tag => tag !== 'foreignobject')};
|
|
@@ -412,8 +375,6 @@ function sanitize(value, context, fragment) {
|
|
|
412
375
|
DOMPurify.addHook('uponSanitizeAttribute', diagramLabelAttribute);
|
|
413
376
|
DOMPurify.addHook('afterSanitizeAttributes', diagramLabelContained);
|
|
414
377
|
}
|
|
415
|
-
// DOMPurify reparses a per-call configuration on every call (its cache compares its own copy), about 0.45 ms
|
|
416
|
-
// each in Chrome 155. A profile is set once and kept until another profile is asked for; every caller comes here.
|
|
417
378
|
if (purifyConfigured.get(DOMPurify) !== options) { DOMPurify.setConfig(options); purifyConfigured.set(DOMPurify, options); }
|
|
418
379
|
const previous = _rapierSanitizeRuntime.context;
|
|
419
380
|
_rapierSanitizeRuntime.context = selected;
|
|
@@ -439,13 +400,19 @@ function escapeRapierHtmlText(value) {
|
|
|
439
400
|
}
|
|
440
401
|
|
|
441
402
|
function _rapierChromeOwnsId(id) {
|
|
442
|
-
// Every id the shell's own markup carries, read once before any document renders (initMarkdownIt primes it),
|
|
443
|
-
// and every rapier- id: the chrome makes its dialogs, overlays, parts and plug-in list later, under that prefix
|
|
444
|
-
// (rapier-anchor- is the renderer's own name for a repeated document anchor, and stays the document's).
|
|
445
403
|
if (!_rapierSanitizeRuntime.chromeIds) _rapierSanitizeRuntime.chromeIds = new Set(Array.from(typeof document?.querySelectorAll === 'function' ? document.querySelectorAll('[id]') : [], element => element.id));
|
|
446
404
|
return _rapierSanitizeRuntime.chromeIds.has(String(id)) || /^rapier-(?!anchor-)/.test(id);
|
|
447
405
|
}
|
|
448
406
|
|
|
407
|
+
const RAPIER_BIDI_CONTROL = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/;
|
|
408
|
+
function _rapierLinkDestinationToConfirm(href, text = '') {
|
|
409
|
+
let url, address = String(href || '');
|
|
410
|
+
try { url = new URL(address); } catch (_) { return ''; }
|
|
411
|
+
try { address = decodeURIComponent(address); } catch (_) {}
|
|
412
|
+
return RAPIER_BIDI_CONTROL.test(String(text)) || RAPIER_BIDI_CONTROL.test(address) ||
|
|
413
|
+
url.hostname.split('.').some(label => /^xn--/i.test(label)) ? url.href : '';
|
|
414
|
+
}
|
|
415
|
+
|
|
449
416
|
function _rapierSafeRasterDataUrl(value) {
|
|
450
417
|
const match = RAPIER_RASTER_DATA_URL_RE.exec(value);
|
|
451
418
|
if (!match) return false;
|
|
@@ -468,9 +435,6 @@ function _rapierCssPresentationIsRemote(property, value) {
|
|
|
468
435
|
return declaration.length ? _rapierDropRemoteDeclarations(declaration) : false;
|
|
469
436
|
}
|
|
470
437
|
|
|
471
|
-
// 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
|
|
472
|
-
// host whatever its path, so its answer is the base's, found once for each base and page: a long document's render asked
|
|
473
|
-
// this of every token of every attribute, a URL built each time.
|
|
474
438
|
const relativeReference = {base: null, here: null, origin: ''};
|
|
475
439
|
function _rapierRemoteSubresourceOrigin(value) {
|
|
476
440
|
const raw = String(value == null ? '' : value)
|
|
@@ -528,7 +492,7 @@ function _rapierDropRemoteRules(rules, owner) {
|
|
|
528
492
|
}
|
|
529
493
|
|
|
530
494
|
function _rapierVerifyRasterBytes(bytes, mime) {
|
|
531
|
-
if (mime === 'image/jxl') return globalThis.RapierImageAssets
|
|
495
|
+
if (mime === 'image/jxl') return globalThis.RapierImageAssets?.isJxl(bytes) === true;
|
|
532
496
|
|
|
533
497
|
// declaration, doctype or generator/license comment before <svg itself, of whatever length its
|
|
534
498
|
|
|
@@ -542,6 +506,6 @@ function _rapierVerifyRasterBytes(bytes, mime) {
|
|
|
542
506
|
|
|
543
507
|
return {_rapierVerifyRasterBytes, _rapierInstallSanitizeHooks, sanitizeRapierHtml,
|
|
544
508
|
sanitizeRapierFragment: typeof DOMPurify?.sanitize === 'function' ? sanitizeRapierFragment : undefined,
|
|
545
|
-
diagramLabelStyle, escapeRapierHtmlText, _rapierChromeOwnsId, _rapierSafeRasterDataUrl, _rapierCssPresentationIsRemote, _rapierRemoteSubresourceOrigin, _rapierStyleWithoutRemoteUrls, _rapierStylesheetWithoutRemoteUrls, _rapierDropRemoteRules};
|
|
509
|
+
diagramLabelStyle, escapeRapierHtmlText, _rapierChromeOwnsId, _rapierLinkDestinationToConfirm, _rapierSafeRasterDataUrl, _rapierCssPresentationIsRemote, _rapierRemoteSubresourceOrigin, _rapierStyleWithoutRemoteUrls, _rapierStylesheetWithoutRemoteUrls, _rapierDropRemoteRules};
|
|
546
510
|
}
|
|
547
511
|
export {RAPIER_SANITIZE_FORBID_TAGS as forbidTags, RAPIER_SANITIZE_FORBID_ATTR as forbidAttributes, createRenderSanitizer, createInertHtmlSanitizer, RAPIER_SANITIZE_OPTIONS as options, prepareMathSource};
|
|
@@ -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;
|