rapier-markdown-kit 1.1.99 → 1.1.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -2
- package/README.md +42 -8
- package/dist/kit/export-fonts.mjs +403 -0
- package/dist/kit/ledger/data.mjs +1 -1
- package/dist/kit/ledger/format.mjs +12 -5
- package/dist/kit/ledger/hash.mjs +16 -22
- package/dist/kit/ledger/history.mjs +59 -276
- package/dist/kit/ledger/inverse-source.mjs +316 -0
- package/dist/kit/ledger/journal-records.mjs +116 -12
- package/dist/kit/ledger/merge.mjs +7 -4
- package/dist/kit/ledger/transport.mjs +123 -19
- package/dist/kit/pdf-glyphs.mjs +159 -0
- package/dist/kit/pdf-vectors.mjs +204 -0
- package/dist/kit/print-capitals.mjs +85 -0
- package/dist/kit/rapier-ledger.mjs +2 -2
- package/dist/kit/render-entry.mjs +1 -0
- package/dist/kit/render-ink.mjs +132 -0
- package/dist/kit/render-markdown.mjs +71 -17
- package/dist/kit/render-print.mjs +208 -60
- package/dist/kit/render-sanitize.mjs +23 -7
- package/dist/kit/render-styles.mjs +8 -12
- package/dist/kit/render.mjs +365 -133
- package/dist/layout/model.mjs +26 -20
- package/dist/layout/typography.mjs +364 -0
- package/dist/packages/rapier-diff/LICENSE +21 -0
- package/dist/packages/rapier-diff/index.mjs +610 -0
- package/dist/spec/document-fonts.mjs +244 -0
- package/dist/spec/document-settings.mjs +494 -0
- package/dist/spec/frontmatter.mjs +30 -0
- package/dist/spec/html-blocks.mjs +143 -0
- package/dist/spec/md-assets.mjs +77 -5
- package/dist/spec/md-layout.mjs +7 -3
- package/dist/spec/md-marks.mjs +114 -8
- package/package.json +1 -1
- package/style.css +110 -52
- package/dist/agent/diff.mjs +0 -52
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {collectPdfVectors} from './pdf-vectors.mjs';
|
|
3
|
+
import {collectPdfGlyphs} from './pdf-glyphs.mjs';
|
|
4
|
+
import {fontRequests, waitForFonts} from './export-fonts.mjs';
|
|
5
|
+
import {apply as applyTypography} from '../layout/typography.mjs';
|
|
6
|
+
import {documentLanguage} from '../spec/document-settings.mjs';
|
|
7
|
+
import {applyPrintCapitals} from './print-capitals.mjs';
|
|
2
8
|
const RAPIER_WILL_SENTINEL_MARK = '';
|
|
3
9
|
// SPDX-License-Identifier: MIT
|
|
4
10
|
// The document's renderer. DOM, codecs and host state are explicit inputs.
|
|
@@ -64,7 +70,10 @@ function _rapierPrintWillFont(markers) {
|
|
|
64
70
|
const cmap = narrow.length <= 0xffff
|
|
65
71
|
? cat(u16(0), u16(2), u16(3), u16(1), u32(20), u16(3), u16(10), u32(20 + narrow.length), narrow, full)
|
|
66
72
|
: cat(u16(0), u16(1), u16(3), u16(10), u32(12), full);
|
|
67
|
-
|
|
73
|
+
// The collinear contour has zero area and enough bounds for native print at 2px.
|
|
74
|
+
// Keep it below the baseline so a carrier at a page break does not straddle both page clips.
|
|
75
|
+
const bounds = [0, -400, 10, 0];
|
|
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]);
|
|
68
77
|
const utf16 = text => Array.from(text, ch => u16(ch.charCodeAt(0)));
|
|
69
78
|
const names = [[1, 'RapierWill'], [2, 'Regular'], [4, 'RapierWill Regular'], [6, 'RapierWill-Regular']];
|
|
70
79
|
let offset = 0;
|
|
@@ -75,7 +84,7 @@ function _rapierPrintWillFont(markers) {
|
|
|
75
84
|
u16(500), u16(500), u32(1), u32(0), u16(500), u16(700), u16(0), u16(32), u16(0)),
|
|
76
85
|
cmap,
|
|
77
86
|
glyf: Array.from({ length: count }, () => outline).flat(),
|
|
78
|
-
head: cat(u32(0x00010000), u32(0x00010000), u32(0), u32(0x5f0f3cf5), u16(1), u16(1000), new Array(16).fill(0),
|
|
87
|
+
head: cat(u32(0x00010000), u32(0x00010000), u32(0), u32(0x5f0f3cf5), u16(1), u16(1000), new Array(16).fill(0), bounds.map(u16),
|
|
79
88
|
u16(0), u16(8), u16(2), u16(1), u16(0)),
|
|
80
89
|
hhea: cat(u32(0x00010000), u16(500), u16(-500), u16(0), u16(advance), u16(0), u16(advance - 10), u16(10), u16(1), u16(0), u16(0), new Array(8).fill(0), u16(0), u16(count)),
|
|
81
90
|
hmtx: Array.from({ length: count }, () => cat(u16(advance), u16(0))).flat(),
|
|
@@ -96,7 +105,7 @@ function _rapierPrintWillFont(markers) {
|
|
|
96
105
|
const file = cat(u32(0x00010000), u16(tags.length), u16((1 << tableSelect) * 16), u16(tableSelect), u16(tags.length * 16 - (1 << tableSelect) * 16), directory, tags.map(tag => padded(tables[tag])));
|
|
97
106
|
const headAt = 12 + tags.length * 16 + tags.slice(0, tags.indexOf('head')).reduce((total, tag) => total + padded(tables[tag]).length, 0);
|
|
98
107
|
file.splice(headAt + 8, 4, ...u32((0xb1b0afba - sum(file)) >>> 0));
|
|
99
|
-
return { family: 'RapierWillCarrier', bytes: Uint8Array.from(file), size, codes: order };
|
|
108
|
+
return { family: 'RapierWillCarrier', bytes: Uint8Array.from(file), size, codes: order, bounds };
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
function _rapierPrintWillMetrics(markers) {
|
|
@@ -125,7 +134,7 @@ function _rapierPrintWillPlace(root, plan) {
|
|
|
125
134
|
else runs.push([row]);
|
|
126
135
|
}
|
|
127
136
|
const line = 'display:block;position:static;float:none;box-sizing:content-box;width:auto;height:0;min-height:0;margin:0;padding:0;border:0;overflow:visible;'
|
|
128
|
-
+ 'white-space:pre;text-align:left;text-indent:0;text-transform:none;text-shadow:none;letter-spacing:0;word-spacing:0;hyphens:none;'
|
|
137
|
+
+ 'white-space:pre;text-align:left;text-indent:0;text-transform:none;text-shadow:none;text-rendering:geometricPrecision;letter-spacing:0;word-spacing:0;hyphens:none;'
|
|
129
138
|
+ 'font:' + measure.size + 'px/0 RapierWillCarrier,sans-serif;font-variant-ligatures:none;font-kerning:none;font-feature-settings:normal;'
|
|
130
139
|
+ 'direction:ltr;unicode-bidi:normal;color:rgba(0,0,0,.0039);break-after:avoid;break-inside:avoid;user-select:none;pointer-events:none;';
|
|
131
140
|
for (const run of runs) {
|
|
@@ -166,7 +175,7 @@ function _rapierPrintWillPageCss() {
|
|
|
166
175
|
// The capture's own limits (agent/visual.mjs VISUAL_LIMITS): the longest edge and the most pixels one capture returns.
|
|
167
176
|
const CAPTURE_EDGE = 4096, CAPTURE_PIXELS = 4 * 1024 * 1024;
|
|
168
177
|
|
|
169
|
-
async function _rapierWritePrintPdf({root, css, canonical, filename, title, settings}, {capture, signal, current, maxBytes = 8 * 1024 * 1024} = {}) {
|
|
178
|
+
async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings}, {capture, signal, current, maxBytes = 8 * 1024 * 1024} = {}) {
|
|
170
179
|
const refuse = code => { throw Object.assign(new Error(code), {code}); };
|
|
171
180
|
const check = () => {
|
|
172
181
|
if (signal?.aborted) refuse('cancelled');
|
|
@@ -184,6 +193,7 @@ async function _rapierWritePrintPdf({root, css, canonical, filename, title, sett
|
|
|
184
193
|
const bodyHeight = Math.floor((height - margins.top - margins.bottom) * 4 / 3);
|
|
185
194
|
if (bodyWidth < 1 || bodyHeight < 1 || bodyWidth * bodyHeight > 4 * 1024 * 1024) refuse('pdf_page_geometry_invalid');
|
|
186
195
|
const frame = document.createElement('iframe');
|
|
196
|
+
let vectors, nativeGlyphs;
|
|
187
197
|
let rejectAbort;
|
|
188
198
|
const aborted = new Promise((_, reject) => { rejectAbort = reject; });
|
|
189
199
|
aborted.catch(() => {});
|
|
@@ -206,7 +216,11 @@ async function _rapierWritePrintPdf({root, css, canonical, filename, title, sett
|
|
|
206
216
|
check();
|
|
207
217
|
const into = frame.contentDocument;
|
|
208
218
|
if (!into?.body) refuse('pdf_render_unavailable');
|
|
219
|
+
// The capture and native printer use standards layout. An initial blank
|
|
220
|
+
// iframe otherwise measures preformatted inline struts in quirks mode.
|
|
221
|
+
into.open(); into.write('<!doctype html><html><head></head><body></body></html>'); into.close();
|
|
209
222
|
into.documentElement.setAttribute('data-rapier-theme', 'light');
|
|
223
|
+
into.documentElement.setAttribute('lang', documentLanguage(settings, document.defaultView?.navigator?.language));
|
|
210
224
|
into.documentElement.setAttribute('data-highlights', document.documentElement.getAttribute('data-highlights') || 'standard');
|
|
211
225
|
into.body.className = 'light';
|
|
212
226
|
const sheet = into.createElement('style'); sheet.textContent = String(css || ''); into.head.append(sheet);
|
|
@@ -231,6 +245,7 @@ async function _rapierWritePrintPdf({root, css, canonical, filename, title, sett
|
|
|
231
245
|
main.setAttribute('data-md-theme', 'light');
|
|
232
246
|
for (const details of main.querySelectorAll('details')) details.open = true;
|
|
233
247
|
main.getBoundingClientRect();
|
|
248
|
+
if (fontCss) await step(waitForFonts(main, {fontCss, css, settings}));
|
|
234
249
|
await step(into.fonts.ready);
|
|
235
250
|
for (const image of main.querySelectorAll('img')) { image.loading = 'eager'; await step(image.decode()); }
|
|
236
251
|
check();
|
|
@@ -238,6 +253,14 @@ async function _rapierWritePrintPdf({root, css, canonical, filename, title, sett
|
|
|
238
253
|
// pages without a print dialog; its break constraints become column constraints here.
|
|
239
254
|
for (const node of [main, ...main.querySelectorAll('*')]) {
|
|
240
255
|
const style = into.defaultView.getComputedStyle(node);
|
|
256
|
+
// Clipped, out-of-flow source has no page ink. Leaving it in a multicolumn
|
|
257
|
+
// container can still create blank columns and expose its text to PDF search.
|
|
258
|
+
if (!node.closest('.rapier-will-carrier') && ['absolute', 'fixed'].includes(style.position) && /^rect\(/.test(style.clip)) {
|
|
259
|
+
const box = style.clip.slice(5, -1).trim().split(/[,\s]+/).map(Number.parseFloat);
|
|
260
|
+
if (box.length === 4 && box.every(Number.isFinite) && (box[1] <= box[3] || box[2] <= box[0])) {
|
|
261
|
+
node.style.setProperty('display', 'none', 'important'); continue;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
241
264
|
for (const name of ['break-before', 'break-after', 'break-inside']) {
|
|
242
265
|
const value = style.getPropertyValue(name);
|
|
243
266
|
if (value.includes('page')) node.style.setProperty(name, value.replace('page', 'column'), 'important');
|
|
@@ -248,9 +271,23 @@ async function _rapierWritePrintPdf({root, css, canonical, filename, title, sett
|
|
|
248
271
|
'column-width': bodyWidth + 'px', 'column-gap': '0', 'column-count': 'auto', 'column-fill': 'auto'}))
|
|
249
272
|
main.style.setProperty(name, value, 'important');
|
|
250
273
|
globalThis.RapierMath?.fit(main);
|
|
274
|
+
applyPrintCapitals(main);
|
|
275
|
+
applyTypography(main, {all: true});
|
|
251
276
|
const count = Math.max(1, Math.ceil((main.scrollWidth - 1) / bodyWidth));
|
|
252
277
|
if (!Number.isSafeInteger(count)) refuse('pdf_page_geometry_invalid');
|
|
253
|
-
|
|
278
|
+
// Captured document font bytes own a same-name family; factory glyphs must
|
|
279
|
+
// never replace their page paint. This uses the artifact, not a live catalog.
|
|
280
|
+
const customFamilies = new Set(fontCss ? fontRequests(main, {fontCss, css, settings}).map(face => face.family.toLowerCase()) : []);
|
|
281
|
+
nativeGlyphs = await collectPdfGlyphs(main, {width: bodyWidth, check,
|
|
282
|
+
faces: globalThis.RapierDocxFonts?.DOCX_FONTS?.filter(face => !customFamilies.has(face.family.toLowerCase()))});
|
|
283
|
+
const textPages = await _rapierPrintTextPages(main, bodyWidth, count, check, undefined, nativeGlyphs.nodes);
|
|
284
|
+
const linkPages = _rapierPrintLinkPages(main, bodyWidth, bodyHeight, count);
|
|
285
|
+
vectors = await collectPdfVectors(main, {width: bodyWidth, height: bodyHeight, count, capture, signal, current, check, maxBytes,
|
|
286
|
+
sanitizeSvg: globalThis.RapierImageAssets?.sanitizeSvgText});
|
|
287
|
+
for (const root of vectors.textRoots) {
|
|
288
|
+
const held = await _rapierPrintTextPages(root, bodyWidth, count, check, main.getBoundingClientRect());
|
|
289
|
+
for (const [page, runs] of held.entries()) textPages[page].push(...runs);
|
|
290
|
+
}
|
|
254
291
|
const text = textPages.flatMap(page => page.map(run => run.text));
|
|
255
292
|
const glyphs = new Set();
|
|
256
293
|
for (const line of text) for (const character of line) {
|
|
@@ -289,80 +326,92 @@ async function _rapierWritePrintPdf({root, css, canonical, filename, title, sett
|
|
|
289
326
|
if (!context) refuse('pdf_render_unavailable');
|
|
290
327
|
context.fillStyle = '#fff'; context.fillRect(0, 0, bodyWidth, bodyHeight);
|
|
291
328
|
context.drawImage(image, offset * bodyWidth, 0, bodyWidth, bodyHeight, 0, 0, bodyWidth, bodyHeight);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
329
|
+
// Preserve the capture's exact pixels. A JPEG re-encode changes
|
|
330
|
+
// glyph edges, pictures and paint even at its highest quality.
|
|
331
|
+
const rgba = context.getImageData(0, 0, bodyWidth, bodyHeight).data;
|
|
332
|
+
const rgb = new Uint8Array(bodyWidth * bodyHeight * 3);
|
|
333
|
+
for (let at = 0, to = 0; at < rgba.length; at += 4) {rgb[to++] = rgba[at]; rgb[to++] = rgba[at + 1]; rgb[to++] = rgba[at + 2];}
|
|
334
|
+
const packed = new Uint8Array(await step(new Response(new Blob([rgb]).stream().pipeThrough(new CompressionStream('deflate'))).arrayBuffer()));
|
|
335
|
+
imageBytes += packed.byteLength;
|
|
295
336
|
if (imageBytes > maxBytes) refuse('export_too_large');
|
|
296
|
-
pages.push(
|
|
337
|
+
pages.push({rgb: packed});
|
|
297
338
|
} finally { canvas.width = canvas.height = 0; }
|
|
298
339
|
}
|
|
299
340
|
} finally { image.src = ''; }
|
|
300
341
|
first += span;
|
|
301
342
|
}
|
|
302
343
|
check();
|
|
303
|
-
const bytes = await writeRasterPdf({pages, textPages, canonical, filename, title, font, width, height, margins,
|
|
344
|
+
const bytes = await writeRasterPdf({pages, textPages, linkPages, vectorPages: vectors.pages, canonical, bom, filename, title, font, width, height, margins,
|
|
304
345
|
pixelWidth: bodyWidth, pixelHeight: bodyHeight, pageNumbers: settings?.pagestyle === 'plain', maxBytes, check});
|
|
305
346
|
check();
|
|
306
347
|
return {bytes, pages: count};
|
|
307
|
-
} finally { signal?.removeEventListener('abort', abort); frame.remove(); }
|
|
348
|
+
} finally { nativeGlyphs?.restore(); vectors?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
|
|
308
349
|
}
|
|
309
350
|
|
|
310
351
|
// Ranges locate the actual text fragments after column layout. Splitting a text node at
|
|
311
352
|
// its line and page boundaries keeps a PDF search on the page that carries those words.
|
|
312
|
-
async function _rapierPrintTextPages(main, pageWidth, count, check) {
|
|
313
|
-
const into = main.ownerDocument
|
|
314
|
-
const pages = Array.from({length: count}, () => []);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const walker = into.createTreeWalker(main, 5, {acceptNode(node) {
|
|
318
|
-
if (node.parentElement?.closest('.math-rendered svg[aria-label]')) return 2;
|
|
319
|
-
return node.nodeType === 3 || node.matches('.math-rendered svg[aria-label]') ? 1 : 3;
|
|
320
|
-
}});
|
|
353
|
+
async function _rapierPrintTextPages(main, pageWidth, count, check, origin = main.getBoundingClientRect(), nativeNodes = null) {
|
|
354
|
+
const into = main.ownerDocument;
|
|
355
|
+
const pages = Array.from({length: count}, () => []), walker = into.createTreeWalker(main, 5);
|
|
356
|
+
const markerMetrics = _rapierPrintWillMetrics(Array.from(main.querySelectorAll('.rapier-will-carrier'), node => node.textContent));
|
|
357
|
+
const markerLost = () => { throw _rapierPrintWillLost('a marker has no readable position on the page', 'Try another browser'); };
|
|
321
358
|
const range = into.createRange();
|
|
322
|
-
const pageOf = x => Math.max(0, Math.min(count - 1, Math.floor((x - origin.left + .01) / pageWidth)));
|
|
323
359
|
let nodes = 0, operations = 0;
|
|
324
360
|
for (let node; (node = walker.nextNode());) {
|
|
325
361
|
if (++nodes % 128 === 0) { await new Promise(resolve => setTimeout(resolve, 0)); check(); }
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
362
|
+
const element = node.nodeType === 1 ? node : node.parentElement;
|
|
363
|
+
const formula = node.nodeType === 1 && node.matches('.math-rendered[data-math-src]') && node.querySelector('svg');
|
|
364
|
+
if (node.nodeType === 1 && !formula || node.nodeType === 3 && element.closest('.math-rendered[data-math-src]')) continue;
|
|
365
|
+
let value;
|
|
366
|
+
if (formula) {
|
|
367
|
+
// The math owner paints SVG paths and retains the equation's exact TeX alternative.
|
|
368
|
+
// Carry that semantic text at the SVG's measured position for search and copy.
|
|
369
|
+
value = formula.getAttribute('aria-label') || '';
|
|
370
|
+
} else value = node.data;
|
|
371
|
+
const style = into.defaultView.getComputedStyle(element);
|
|
372
|
+
const carrier = element.closest('.rapier-will-carrier');
|
|
373
|
+
if (style.visibility === 'hidden' || style.visibility === 'collapse') { if (carrier) markerLost(); continue; }
|
|
338
374
|
const preserve = /pre|break-spaces/.test(style.whiteSpace);
|
|
339
|
-
const block =
|
|
340
|
-
const
|
|
341
|
-
const
|
|
375
|
+
const block = element.closest('p,h1,h2,h3,h4,h5,h6,li,pre,td,th,figcaption,blockquote,.rapier-will-carrier') || element;
|
|
376
|
+
const native = nativeNodes?.get(node);
|
|
377
|
+
const pageOf = x => Math.max(0, Math.min(count - 1, Math.floor((x - origin.left + .01) / pageWidth)));
|
|
342
378
|
const visit = (start, end) => {
|
|
343
379
|
if (++operations % 128 === 0) check();
|
|
344
|
-
range.setStart(node, start); range.setEnd(node, end);
|
|
345
|
-
|
|
346
|
-
|
|
380
|
+
if (!formula) { range.setStart(node, start); range.setEnd(node, end); }
|
|
381
|
+
// A tiny carrier font can have its advance rounded to zero by a browser. Its
|
|
382
|
+
// measured baseline still locates the marker; the font's authored metrics keep its text width.
|
|
383
|
+
const rects = Array.from(formula ? [formula.getBoundingClientRect()] : range.getClientRects()).filter(rect => rect.height > 0 && (rect.width > 0 || carrier));
|
|
384
|
+
if (!rects.length) { if (carrier) markerLost(); return; }
|
|
347
385
|
const first = rects[0], page = pageOf(first.left);
|
|
348
|
-
const oneLine = rects.every(rect => pageOf(rect.left) === page && pageOf(rect.right - .02) === page && Math.abs(rect.top - first.top) < .5);
|
|
349
|
-
let middle = Math.floor((start + end) / 2);
|
|
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);
|
|
387
|
+
let middle = formula ? start : Math.floor((start + end) / 2);
|
|
350
388
|
if (middle > start && /[\uD800-\uDBFF]/.test(value[middle - 1]) && /[\uDC00-\uDFFF]/.test(value[middle])) middle--;
|
|
389
|
+
const cluster = native?.find(item => item.start < middle && item.end > middle);
|
|
390
|
+
if (cluster) middle = cluster.start > start ? cluster.start : cluster.end;
|
|
351
391
|
if (!oneLine && middle > start && middle < end) { visit(start, middle); visit(middle, end); return; }
|
|
352
392
|
let text = value.slice(start, end);
|
|
353
|
-
text = preserve ? text.replace(/\r\n?|\n/g, '') : text.replace(/[ \t\r\n\f]+/g, ' ');
|
|
393
|
+
if (!formula) text = preserve ? text.replace(/\r\n?|\n/g, '') : text.replace(/[ \t\r\n\f]+/g, ' ');
|
|
354
394
|
if (!text) return;
|
|
355
395
|
const x = Math.min(...rects.map(rect => rect.left)) - origin.left - page * pageWidth;
|
|
356
396
|
const y = Math.min(...rects.map(rect => rect.top)) - origin.top;
|
|
357
|
-
|
|
397
|
+
let right = Math.max(...rects.map(rect => rect.right)) - origin.left - page * pageWidth;
|
|
398
|
+
if (carrier && right === x) right += Array.from(text).length * markerMetrics.advance * markerMetrics.size / 1000;
|
|
358
399
|
const bottom = Math.max(...rects.map(rect => rect.bottom)) - origin.top;
|
|
400
|
+
const parts = native ? native.filter(item => item.start >= start && item.end <= end && item.text).map(item => ({...item}))
|
|
401
|
+
: [{text, x, y, width: right - x, height: bottom - y}];
|
|
359
402
|
const last = pages[page].at(-1);
|
|
360
|
-
if (last && !last.math && last.block === block && last.carrier === carrier && Math.abs(last.y - y) < Math.max(2, Math.min(last.height, bottom - y) * .2)) {
|
|
403
|
+
if (last && !formula && !last.math && last.block === block && last.carrier === carrier && Math.abs(last.y - y) < Math.max(2, Math.min(last.height, bottom - y) * .2)) {
|
|
404
|
+
if (!preserve && last.text.endsWith(' ') && text.startsWith(' ') && parts.length) {
|
|
405
|
+
const first = parts[0];
|
|
406
|
+
if (first.text === ' ') parts.shift();
|
|
407
|
+
else {const advance = first.width / Array.from(first.text).length; first.text = first.text.slice(1); first.x += advance; first.width -= advance;}
|
|
408
|
+
}
|
|
361
409
|
last.text = preserve ? last.text + text : (last.text + text).replace(/[ \t\r\n\f]+/g, ' ');
|
|
410
|
+
last.parts.push(...parts);
|
|
362
411
|
last.width = Math.max(last.x + last.width, right) - Math.min(last.x, x);
|
|
363
412
|
last.height = Math.max(last.y + last.height, bottom) - Math.min(last.y, y);
|
|
364
413
|
last.x = Math.min(last.x, x); last.y = Math.min(last.y, y);
|
|
365
|
-
} else pages[page].push({text, x, y, width: right - x, height: bottom - y, block, carrier});
|
|
414
|
+
} else pages[page].push({text, x, y, width: right - x, height: bottom - y, block, carrier, math: !!formula, parts});
|
|
366
415
|
};
|
|
367
416
|
visit(0, value.length);
|
|
368
417
|
}
|
|
@@ -370,12 +419,40 @@ async function _rapierPrintTextPages(main, pageWidth, count, check) {
|
|
|
370
419
|
return pages.map(page => page.map(({block, carrier, ...run}) => run));
|
|
371
420
|
}
|
|
372
421
|
|
|
422
|
+
function _rapierPrintLinkPages(main, width, height, count) {
|
|
423
|
+
const origin = main.getBoundingClientRect(), pages = Array.from({length: count}, () => []);
|
|
424
|
+
for (const anchor of main.querySelectorAll('a[href]')) {
|
|
425
|
+
const href = anchor.getAttribute('href');
|
|
426
|
+
if (!href || /^(?:javascript|vbscript|data):/i.test(href)) continue;
|
|
427
|
+
let target;
|
|
428
|
+
if (href.startsWith('#')) {
|
|
429
|
+
let id; try { id = decodeURIComponent(href.slice(1)); } catch (_) { continue; }
|
|
430
|
+
const node = main.ownerDocument.getElementById(id);
|
|
431
|
+
if (!node || !main.contains(node)) continue;
|
|
432
|
+
const box = node.getBoundingClientRect(), page = Math.floor((box.left - origin.left + .01) / width);
|
|
433
|
+
if (page < 0 || page >= count) continue;
|
|
434
|
+
target = {page, x: box.left - origin.left - page * width, y: box.top - origin.top};
|
|
435
|
+
} else target = {uri: encodeURI(href).replace(/%25([a-f0-9]{2})/gi, '%$1')};
|
|
436
|
+
for (const box of anchor.getClientRects()) {
|
|
437
|
+
if (!(box.width > 0 && box.height > 0)) continue;
|
|
438
|
+
const first = Math.max(0, Math.floor((box.left - origin.left + .01) / width));
|
|
439
|
+
const last = Math.min(count - 1, Math.floor((box.right - origin.left - .01) / width));
|
|
440
|
+
for (let page = first; page <= last; page++) {
|
|
441
|
+
const x = Math.max(0, box.left - origin.left - page * width), y = Math.max(0, box.top - origin.top);
|
|
442
|
+
const right = Math.min(width, box.right - origin.left - page * width), bottom = Math.min(height, box.bottom - origin.top);
|
|
443
|
+
if (right > x && bottom > y) pages[page].push({x, y, width: right - x, height: bottom - y, target});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return pages;
|
|
448
|
+
}
|
|
449
|
+
|
|
373
450
|
return {_rapierWillSentinelPlan, _rapierPrintWillAdmit, _rapierPrintWillFont, _rapierPrintWillMetrics, _rapierPrintWillPlace, _rapierPrintWillLost, _rapierPrintWillPageCss, _rapierWritePrintPdf};
|
|
374
451
|
}
|
|
375
452
|
|
|
376
453
|
// A standard PDF page tree, image streams, Unicode text and an associated source file.
|
|
377
454
|
// It accepts already-rendered pages, so there is no second document grammar or layout engine.
|
|
378
|
-
async function writeRasterPdf({pages, textPages, canonical, filename, title, font, width, height, margins, pixelWidth, pixelHeight, pageNumbers, maxBytes, check}) {
|
|
455
|
+
async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [], canonical, bom = false, filename, title, font, width, height, margins, pixelWidth, pixelHeight, pageNumbers, maxBytes, check}) {
|
|
379
456
|
const encoder = new TextEncoder(), encode = value => encoder.encode(value);
|
|
380
457
|
const fail = code => { throw Object.assign(new Error(code), {code}); };
|
|
381
458
|
const concat = parts => {
|
|
@@ -397,7 +474,7 @@ async function writeRasterPdf({pages, textPages, canonical, filename, title, fon
|
|
|
397
474
|
const stream = (dict, bytes) => add(concat([encode('<<' + dict + ' /Length ' + bytes.length + '>>\nstream\n'), bytes, encode('\nendstream')]));
|
|
398
475
|
const hex = value => Array.from({length: value.length}, (_, index) => value.charCodeAt(index).toString(16).padStart(4, '0')).join('');
|
|
399
476
|
const literal = value => '(' + String(value).replace(/[\\()\r\n]/g, character => ({'\\': '\\\\', '(': '\\(', ')': '\\)', '\r': '\\r', '\n': '\\n'}[character])) + ')';
|
|
400
|
-
const source = encoder.encode(canonical);
|
|
477
|
+
const source = encoder.encode((bom ? '\uFEFF' : '') + canonical);
|
|
401
478
|
const embedded = stream('/Type /EmbeddedFile /Subtype /text#2Fplain /Params <</Size ' + source.length + '>> /Filter /FlateDecode', await compressed(source));
|
|
402
479
|
const file = add('<< /Type /Filespec /F ' + literal(String(filename).replace(/[^\x20-\x7e]/g, '_')) + ' /UF <feff' + hex(filename) + '> /AFRelationship /Source /EF << /F ' + embedded + ' 0 R /UF ' + embedded + ' 0 R >> >>');
|
|
403
480
|
const codes = new Map(font.codes.map((code, index) => [code, index + 1]));
|
|
@@ -407,27 +484,98 @@ async function writeRasterPdf({pages, textPages, canonical, filename, title, fon
|
|
|
407
484
|
cmap += 'endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend';
|
|
408
485
|
const mapping = stream('/Filter /FlateDecode', await compressed(encode(cmap)));
|
|
409
486
|
const fontFile = stream('/Length1 ' + font.bytes.length + ' /Filter /FlateDecode', await compressed(font.bytes));
|
|
410
|
-
const descriptor = add('<< /Type /FontDescriptor /FontName /RapierText /Flags 4 /FontBBox [
|
|
487
|
+
const descriptor = add('<< /Type /FontDescriptor /FontName /RapierText /Flags 4 /FontBBox [' + font.bounds.join(' ') + '] /ItalicAngle 0 /Ascent 500 /Descent -500 /CapHeight 500 /StemV 80 /FontFile2 ' + fontFile + ' 0 R >>');
|
|
411
488
|
const descendant = add('<< /Type /Font /Subtype /CIDFontType2 /BaseFont /RapierText /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ' + descriptor + ' 0 R /DW 500 /CIDToGIDMap /Identity >>');
|
|
412
489
|
const type = add('<< /Type /Font /Subtype /Type0 /BaseFont /RapierText /Encoding /Identity-H /DescendantFonts [' + descendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
|
|
413
490
|
const numberFont = pageNumbers ? add('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>') : null;
|
|
414
|
-
const kids =
|
|
491
|
+
const kids = pages.map(() => { objects.push(null); return objects.length - 1; }), decimal = value => Number(value.toFixed(4));
|
|
492
|
+
const nativeFonts = new Map();
|
|
493
|
+
const glyphKey = part => part.glyph + ':' + part.text;
|
|
494
|
+
for (const page of textPages) for (const run of page) for (const part of run.parts || []) if (part.font) {
|
|
495
|
+
let entry = nativeFonts.get(part.font);
|
|
496
|
+
if (!entry) {entry = {font: part.font, name: '/Fn' + nativeFonts.size, codes: new Map(), rows: []}; nativeFonts.set(part.font, entry);}
|
|
497
|
+
const key = glyphKey(part);
|
|
498
|
+
if (!entry.codes.has(key)) {const code = entry.rows.length + 1; if (code > 65000) fail('pdf_text_glyph_limit'); entry.codes.set(key, code); entry.rows.push({...part, code});}
|
|
499
|
+
}
|
|
500
|
+
const nativeResources = [];
|
|
501
|
+
for (const entry of nativeFonts.values()) {
|
|
502
|
+
check();
|
|
503
|
+
const face = entry.font, name = face.asset.family.replace(/\s/g, '') + '-' + face.asset.style, scale = 1000 / face.units;
|
|
504
|
+
const file = stream('/Length1 ' + face.bytes.length + ' /Filter /FlateDecode', await compressed(face.bytes));
|
|
505
|
+
const descriptor = add('<< /Type /FontDescriptor /FontName /' + name + ' /Flags 32 /FontBBox [' + face.bbox.map(value => decimal(value * scale)).join(' ') + '] /ItalicAngle 0 /Ascent ' + decimal(face.ascent * scale)
|
|
506
|
+
+ ' /Descent ' + decimal(face.descent * scale) + ' /CapHeight ' + decimal(face.capHeight * scale) + ' /StemV 80 /FontFile2 ' + file + ' 0 R >>');
|
|
507
|
+
const ids = new Uint8Array((entry.rows.length + 1) * 2);
|
|
508
|
+
for (const row of entry.rows) {ids[row.code * 2] = row.glyph >> 8; ids[row.code * 2 + 1] = row.glyph & 255;}
|
|
509
|
+
const glyphMap = stream('/Filter /FlateDecode', await compressed(ids));
|
|
510
|
+
let unicode = '/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n/CMapName /' + name + 'Unicode def\n/CMapType 2 def\n1 begincodespacerange\n<0000> <ffff>\nendcodespacerange\n';
|
|
511
|
+
for (let at = 0; at < entry.rows.length; at += 100) {const rows = entry.rows.slice(at, at + 100); unicode += rows.length + ' beginbfchar\n' + rows.map(row => '<' + row.code.toString(16).padStart(4, '0') + '> <' + hex(row.text) + '>').join('\n') + '\nendbfchar\n';}
|
|
512
|
+
unicode += 'endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend';
|
|
513
|
+
const mapping = stream('/Filter /FlateDecode', await compressed(encode(unicode)));
|
|
514
|
+
const descendant = add('<< /Type /Font /Subtype /CIDFontType2 /BaseFont /' + name + ' /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ' + descriptor + ' 0 R /CIDToGIDMap ' + glyphMap + ' 0 R /W [1 [' + entry.rows.map(row => decimal(face.advance(row.glyph) * scale)).join(' ') + ']] >>');
|
|
515
|
+
const type = add('<< /Type /Font /Subtype /Type0 /BaseFont /' + name + ' /Encoding /Identity-H /DescendantFonts [' + descendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
|
|
516
|
+
nativeResources.push(entry.name + ' ' + type + ' 0 R');
|
|
517
|
+
}
|
|
415
518
|
for (let index = 0; index < pages.length; index++) {
|
|
416
519
|
check();
|
|
417
|
-
const picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode', pages[index]);
|
|
520
|
+
const picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (pages[index].rgb ? 'FlateDecode' : 'DCTDecode'), pages[index].rgb || pages[index]);
|
|
418
521
|
let commands = 'q\n' + [decimal(pixelWidth * .75), 0, 0, decimal(pixelHeight * .75), decimal(margins.left), decimal(height - margins.top - pixelHeight * .75)].join(' ') + ' cm\n/Im0 Do\nQ\n';
|
|
522
|
+
const pictures = ['/Im0 ' + picture + ' 0 R'], states = [], rasterCache = new Map();
|
|
523
|
+
for (const drawing of vectorPages[index] || []) {
|
|
524
|
+
commands += `q\n.75 0 0 -.75 ${decimal(margins.left)} ${decimal(height - margins.top)} cm\n0 0 ${pixelWidth} ${pixelHeight} re W n\n1 0 0 1 ${decimal(drawing.x)} 0 cm\n`;
|
|
525
|
+
commands += [drawing.clip.x, drawing.clip.y, drawing.clip.width, drawing.clip.height].map(decimal).join(' ') + ' re W n\n';
|
|
526
|
+
for (const piece of drawing.sequence) {
|
|
527
|
+
if (piece.image) {
|
|
528
|
+
let name = rasterCache.get(piece.image);
|
|
529
|
+
if (!name) {
|
|
530
|
+
const image = piece.image;
|
|
531
|
+
const mask = stream('/Type /XObject /Subtype /Image /Width ' + image.width + ' /Height ' + image.height + ' /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode', image.alpha);
|
|
532
|
+
const object = stream('/Type /XObject /Subtype /Image /Width ' + image.width + ' /Height ' + image.height + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /SMask ' + mask + ' 0 R /Filter /FlateDecode', image.rgb);
|
|
533
|
+
name = '/Im' + pictures.length; pictures.push(name + ' ' + object + ' 0 R'); rasterCache.set(image, name);
|
|
534
|
+
}
|
|
535
|
+
commands += `q\n${[piece.width, 0, 0, -piece.height, piece.x, piece.y + piece.height].map(decimal).join(' ')} cm\n${name} Do\nQ\n`;
|
|
536
|
+
} else {
|
|
537
|
+
const state = '/Gs' + states.length;
|
|
538
|
+
states.push(state + ' ' + add('<< /Type /ExtGState /ca ' + decimal(piece.fillAlpha) + ' /CA ' + decimal(piece.strokeAlpha) + ' >>') + ' 0 R');
|
|
539
|
+
commands += 'q\n' + state + ' gs\n' + piece.commands + 'Q\n';
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
commands += 'Q\n';
|
|
543
|
+
}
|
|
544
|
+
const textAlphas = new Map();
|
|
419
545
|
for (const run of textPages[index]) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
546
|
+
if (!run.text) continue;
|
|
547
|
+
// Keep one logical Unicode text object, resetting its font before each scalar's direction
|
|
548
|
+
// can change. Readers that ignore ActualText otherwise discard earlier words in mixed RTL/LTR.
|
|
549
|
+
// Express width in the font size: some readers ignore horizontal text scaling when joining.
|
|
550
|
+
commands += `q\n/Span << /ActualText <feff${hex(run.text)}> >> BDC\nBT\n100 Tz\n`;
|
|
551
|
+
for (const part of run.parts || [run]) {
|
|
552
|
+
if (part.font) {
|
|
553
|
+
const entry = nativeFonts.get(part.font), value = entry.codes.get(glyphKey(part)).toString(16).padStart(4, '0'), alpha = decimal(part.rgba[3]);
|
|
554
|
+
let state = textAlphas.get(alpha);
|
|
555
|
+
if (!state) {state = '/Gs' + states.length; states.push(state + ' ' + add('<< /Type /ExtGState /ca ' + alpha + ' >>') + ' 0 R'); textAlphas.set(alpha, state);}
|
|
556
|
+
commands += `${state} gs\n${part.rgba.slice(0, 3).map(decimal).join(' ')} rg\n${entry.name} ${decimal(part.size * .75)} Tf\n0 Tr\n1 0 0 1 ${decimal(margins.left + part.x * .75)} ${decimal(height - margins.top - part.baseline * .75)} Tm\n<${value}> Tj\n`;
|
|
557
|
+
} else {
|
|
558
|
+
const characters = Array.from(part.text); if (!characters.length) continue;
|
|
559
|
+
const advance = part.width * .75 / characters.length, size = Math.max(.1, advance * 2), scaleY = Math.max(.1, part.height * .75) / size;
|
|
560
|
+
const baseline = decimal(height - margins.top - (part.y + part.height * .8) * .75);
|
|
561
|
+
for (let letter = 0; letter < characters.length; letter++) {
|
|
562
|
+
const value = codes.get(characters[letter].codePointAt(0)).toString(16).padStart(4, '0');
|
|
563
|
+
commands += `/F1 ${decimal(size)} Tf\n3 Tr\n1 0 0 ${decimal(scaleY)} ${decimal(margins.left + part.x * .75 + letter * advance)} ${baseline} Tm\n<${value}> Tj\n`;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
commands += 'ET\nEMC\nQ\n';
|
|
427
568
|
}
|
|
428
569
|
if (numberFont) commands += `BT\n/F2 9 Tf\n0 Tr\n1 0 0 1 ${decimal(width / 2 - String(index + 1).length * 2.5)} ${decimal(margins.bottom / 2)} Tm\n(${index + 1}) Tj\nET\n`;
|
|
429
570
|
const content = stream('/Filter /FlateDecode', await compressed(encode(commands)));
|
|
430
|
-
|
|
571
|
+
const annotations = (linkPages[index] || []).map(link => {
|
|
572
|
+
const rect = [margins.left + link.x * .75, height - margins.top - (link.y + link.height) * .75,
|
|
573
|
+
margins.left + (link.x + link.width) * .75, height - margins.top - link.y * .75].map(decimal);
|
|
574
|
+
const target = link.target.uri ? '/A << /S /URI /URI ' + literal(link.target.uri) + ' >>'
|
|
575
|
+
: '/Dest [' + kids[link.target.page] + ' 0 R /XYZ ' + decimal(margins.left + link.target.x * .75) + ' ' + decimal(height - margins.top - link.target.y * .75) + ' null]';
|
|
576
|
+
return add('<< /Type /Annot /Subtype /Link /Border [0 0 0] /Rect [' + rect.join(' ') + '] ' + target + ' >>');
|
|
577
|
+
});
|
|
578
|
+
put(kids[index], '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ' + decimal(width) + ' ' + decimal(height) + '] /Resources << /XObject << ' + pictures.join(' ') + ' >> /ExtGState << ' + states.join(' ') + ' >> /Font << /F1 ' + type + ' 0 R' + (numberFont ? ' /F2 ' + numberFont + ' 0 R' : '') + ' ' + nativeResources.join(' ') + ' >> >> /Contents ' + content + ' 0 R' + (annotations.length ? ' /Annots [' + annotations.map(id => id + ' 0 R').join(' ') + ']' : '') + ' >>');
|
|
431
579
|
}
|
|
432
580
|
put(2, '<< /Type /Pages /Count ' + kids.length + ' /Kids [' + kids.map(id => id + ' 0 R').join(' ') + '] >>');
|
|
433
581
|
put(1, '<< /Type /Catalog /Pages 2 0 R /Names << /EmbeddedFiles << /Names [<feff' + hex(filename) + '> ' + file + ' 0 R] >> >> /AF [' + file + ' 0 R] >>');
|
|
@@ -442,4 +590,4 @@ async function writeRasterPdf({pages, textPages, canonical, filename, title, fon
|
|
|
442
590
|
parts.push(encode('xref\n0 ' + objects.length + '\n0000000000 65535 f \n' + offsets.slice(1).map(at => String(at).padStart(10, '0') + ' 00000 n \n').join('') + 'trailer\n<< /Size ' + objects.length + ' /Root 1 0 R /Info ' + info + ' 0 R >>\nstartxref\n' + xref + '\n%%EOF\n'));
|
|
443
591
|
const bytes = concat(parts); if (bytes.length > maxBytes) fail('export_too_large'); return bytes;
|
|
444
592
|
}
|
|
445
|
-
export {createPrintRenderer, RAPIER_WILL_SENTINEL_MARK as sentinel};
|
|
593
|
+
export {createPrintRenderer, applyPrintCapitals, RAPIER_WILL_SENTINEL_MARK as sentinel};
|
|
@@ -58,6 +58,11 @@ const DIAGRAM_LABEL_SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
|
58
58
|
const DIAGRAM_LABEL_HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
59
59
|
const DIAGRAM_LABEL_MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
|
|
60
60
|
|
|
61
|
+
function _rapierImageDataAttributeAllowed(tag, attribute, value, verify) {
|
|
62
|
+
return (tag === 'img' && attribute === 'src' || tag === 'image' && /^(?:href|xlink:href)$/.test(attribute)
|
|
63
|
+
&& /^data:image\/(?:png|jpeg|webp|jxl);base64,/i.test(value)) && verify(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
// Snapshot current math text before a Markdown writer collapses whitespace. Rendered SVG
|
|
62
67
|
// glyphs have no textContent: put their retained source in this inert copy so the writer's
|
|
63
68
|
// blank-node rule cannot discard an equation or its containing paragraph/list item.
|
|
@@ -140,7 +145,7 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
|
|
|
140
145
|
const cleaned = sanitizeHtml(serialize(tree), {
|
|
141
146
|
allowedTags: [...tags], allowedAttributes: false,
|
|
142
147
|
disallowedTagsMode: 'discard', nonTextTags: RAPIER_SANITIZE_DROP_CONTENTS,
|
|
143
|
-
allowedSchemes: schemes, allowedSchemesByTag: {img: [...schemes, 'data']},
|
|
148
|
+
allowedSchemes: schemes, allowedSchemesByTag: {img: [...schemes, 'data'], image: [...schemes, 'data']},
|
|
144
149
|
allowedSchemesAppliedToAttributes: ['href', 'src', 'xlink:href', 'cite', 'background', 'poster'],
|
|
145
150
|
parseStyleAttributes: false,
|
|
146
151
|
parser: {lowerCaseTags: false, lowerCaseAttributeNames: false, decodeEntities: true},
|
|
@@ -158,7 +163,7 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
|
|
|
158
163
|
const compact = value.replace(/[\u0000-\u0020\u00a0\u1680\u180e\u2000-\u2029\u205f\u3000]/g, '');
|
|
159
164
|
if (!data && !aria && !pageBreak && !uriSafe.has(key) && compact) {
|
|
160
165
|
if (/^data:/i.test(compact)) {
|
|
161
|
-
if (
|
|
166
|
+
if (!_rapierImageDataAttributeAllowed(tag, key, compact, safeRasterDataUrl)) continue;
|
|
162
167
|
} else if (!uri.test(compact)) continue;
|
|
163
168
|
}
|
|
164
169
|
attribs[name] = value;
|
|
@@ -264,10 +269,11 @@ function _rapierInstallSanitizeHooks() {
|
|
|
264
269
|
if (!/^(?:src|href|xlink:href)$/i.test(data.attrName || '')) return;
|
|
265
270
|
const compact = String(data.attrValue || '').replace(/[\u0000-\u0020\u00a0]+/g, '');
|
|
266
271
|
if (!/^data:/i.test(compact)) return;
|
|
267
|
-
const
|
|
268
|
-
&&
|
|
269
|
-
|
|
270
|
-
&&
|
|
272
|
+
const tag = node?.nodeName === 'IMG' ? 'img'
|
|
273
|
+
: node?.namespaceURI === DIAGRAM_LABEL_SVG_NAMESPACE && node.localName === 'image' ? 'image' : '';
|
|
274
|
+
const rasterAllowed = (/^(?:render|export|raw|source)$/.test(_rapierSanitizeRuntime.context)
|
|
275
|
+
|| _rapierSanitizeRuntime.context === 'diagram' && tag === 'image')
|
|
276
|
+
&& _rapierImageDataAttributeAllowed(tag, data.attrName.toLowerCase(), compact, _rapierSafeRasterDataUrl);
|
|
271
277
|
if (!rasterAllowed) data.keepAttr = false;
|
|
272
278
|
});
|
|
273
279
|
// Paste, render and export sanitise: a remote <img src> would fetch the instant its markup lands in the page,
|
|
@@ -290,6 +296,15 @@ function _rapierInstallSanitizeHooks() {
|
|
|
290
296
|
const title = node.getAttribute('title');
|
|
291
297
|
if (title != null) node.setAttribute('data-rapier-remote-title', title);
|
|
292
298
|
});
|
|
299
|
+
DOMPurify.addHook('uponSanitizeElement', node => {
|
|
300
|
+
if (_rapierRemoteContent.allowed || !/^(?:diagram|export)$/.test(_rapierSanitizeRuntime.context) ||
|
|
301
|
+
node?.namespaceURI !== DIAGRAM_LABEL_SVG_NAMESPACE || node.localName?.toLowerCase() !== 'image') return;
|
|
302
|
+
const href = node.getAttribute('href') || node.getAttribute('xlink:href') || '';
|
|
303
|
+
if (href && _rapierRemoteSubresourceOrigin(href)) {
|
|
304
|
+
node._rapierHeldBack = true;
|
|
305
|
+
node.setAttribute('data-rapier-remote-src', href);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
293
308
|
// A list from Word is named by the style of its paragraph and of the span that holds its marker (mso-list,
|
|
294
309
|
// spec/html-reading.mjs), and the CSS pass below empties a style of every property the browser does not
|
|
295
310
|
// know: a marker's style is only `mso-list:Ignore`. The paste carries both as data-* first, where they stay,
|
|
@@ -362,12 +377,13 @@ function sanitizeRapierHtml(value, context = 'render') {
|
|
|
362
377
|
// profile's admission, and the block on remote content stays the reader's (a remote picture's URL is the person's words).
|
|
363
378
|
const selected = context === 'raw' || context === 'export' || context === 'source' || RAPIER_SANITIZE_OPTIONS[context] ? context : 'render';
|
|
364
379
|
let options = selected === 'raw' ? { ...RAPIER_SANITIZE_OPTIONS.render, ALLOW_DATA_ATTR: false } : RAPIER_SANITIZE_OPTIONS[selected] || RAPIER_SANITIZE_OPTIONS.render;
|
|
380
|
+
if (selected === 'source') options = {...options, ADD_DATA_URI_TAGS: ['image']};
|
|
365
381
|
const labels = selected === 'diagram' || selected === 'export';
|
|
366
382
|
if (labels) {
|
|
367
383
|
// DOMPurify's USE_PROFILES replaces explicit allowlists. Add MathML to the
|
|
368
384
|
// selected profile without discarding its admitted HTML and SVG vocabulary.
|
|
369
385
|
options = {...options, ALLOWED_TAGS: [...new Set([...options.ALLOWED_TAGS, ...RAPIER_SANITIZE_PROFILES.mathTags])],
|
|
370
|
-
ALLOWED_ATTR: [...new Set([...options.ALLOWED_ATTR, ...RAPIER_SANITIZE_PROFILES.mathAttributes])], ADD_TAGS: ['foreignobject'],
|
|
386
|
+
ALLOWED_ATTR: [...new Set([...options.ALLOWED_ATTR, ...RAPIER_SANITIZE_PROFILES.mathAttributes])], ADD_TAGS: ['foreignobject'], ADD_DATA_URI_TAGS: ['image'],
|
|
371
387
|
HTML_INTEGRATION_POINTS: {foreignobject: true}, FORBID_TAGS: options.FORBID_TAGS.filter(tag => tag !== 'foreignobject')};
|
|
372
388
|
DOMPurify.addHook('uponSanitizeElement', diagramLabelElement);
|
|
373
389
|
DOMPurify.addHook('uponSanitizeAttribute', diagramLabelAttribute);
|
|
@@ -5,6 +5,8 @@ function createRenderStyles(runtime) {
|
|
|
5
5
|
function _rapierArtifactThemeCss(houseDefaults = false) {
|
|
6
6
|
// Geometry and the selected accent belong to this page; palette and type belong to the
|
|
7
7
|
// 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.
|
|
8
10
|
const accent = houseDefaults ? '' : ':root,.md-render{--md-color-accent:' + _rapierArtifactAccent() + '}';
|
|
9
11
|
return `
|
|
10
12
|
${accent}
|
|
@@ -18,12 +20,14 @@ body{
|
|
|
18
20
|
--color-text-subtle:var(--md-color-text-subtle);--color-accent-text:var(--md-color-accent-text);
|
|
19
21
|
margin:0;background:var(--md-color-bg);color:var(--md-color-text);font-family:var(--md-font-sans);-webkit-font-smoothing:antialiased
|
|
20
22
|
}
|
|
21
|
-
.rapier-page{width:min(100%,calc(var(--md-measure,39.6rem) +
|
|
23
|
+
.rapier-page{width:min(100%,calc(var(--md-measure,39.6rem) + 32px));margin:0 auto;padding:48px 16px 72px}
|
|
22
24
|
.rapier-page [id]{scroll-margin-top:var(--md-space-8)}
|
|
23
25
|
.artifact-flat{margin:0;white-space:pre-wrap;overflow-wrap:anywhere;tab-size:2}
|
|
24
26
|
.artifact-flat--text{font:var(--md-fw-regular) 1rem/1.7 var(--md-font-sans)}
|
|
25
27
|
.artifact-flat--code{font:var(--md-fw-regular) .875rem/1.6 var(--md-font-mono);overflow-x:auto;white-space:pre}
|
|
26
|
-
@media
|
|
28
|
+
@media screen{html{scrollbar-width:none}html::-webkit-scrollbar{width:0;height:0;display:none}}
|
|
29
|
+
@media (max-width:600px){.rapier-page{padding:32px 16px 56px}}
|
|
30
|
+
@media (prefers-color-scheme:light){.rapier-page[data-md-theme="system"]{--md-heading-color:var(--md-color-line);--md-heading-muted-color:var(--md-color-line)}}
|
|
27
31
|
`;
|
|
28
32
|
}
|
|
29
33
|
|
|
@@ -54,6 +58,7 @@ function _rapierDocumentPrintCss(hostSelector) {
|
|
|
54
58
|
${list('.md-render p','.md-render li','.md-render blockquote')}{orphans:3;widows:3}
|
|
55
59
|
${p('.md-render img')}{max-width:100%!important;height:auto!important;break-inside:avoid-page}
|
|
56
60
|
${list('.md-render pre','.artifact-flat')}{white-space:pre-wrap!important;overflow-wrap:anywhere!important;word-break:break-word!important;overflow:visible!important;max-height:none!important}
|
|
61
|
+
${p('.diagram-block[data-diagram-state="ready"] .diagram-source')}{display:none!important}
|
|
57
62
|
${p('.artifact-flat')}{margin:0;font:400 12pt/1.55 var(--md-font-sans);tab-size:2}
|
|
58
63
|
${p('.artifact-flat--code')}{font:400 10pt/1.5 var(--md-font-mono)}
|
|
59
64
|
${list('.table-scroll-wrap','.math-display-wrap')}{overflow:visible!important;padding:0!important}
|
|
@@ -77,16 +82,7 @@ function _rapierArtifactStyles(theme, includeFonts, printMode, houseDefaults = f
|
|
|
77
82
|
else if (includeFonts) parts.push(_rapierStyleText('rapier-font-style'));
|
|
78
83
|
parts.push(_rapierStyleText('rapier-content-style'));
|
|
79
84
|
parts.push(_rapierArtifactThemeCss(houseDefaults));
|
|
80
|
-
//
|
|
81
|
-
// matched to Geist's metrics: one family per system face, src:local() only (fetched by nothing, so the
|
|
82
|
-
// policy is untouched), size-adjust from Geist's weighted average advance over the face's own (Geist 467
|
|
83
|
-
// per 1000, measured from repo/shell/fonts; the system faces from their tables), the ascent and descent
|
|
84
|
-
// overrides divided by that size-adjust so the used metrics are Geist's 1005/295 per 1000, the line gap
|
|
85
|
-
// Geist's zero. Bold weights take the face's own bold; 500 the regular. Proved in Chromium against the
|
|
86
|
-
// installed Liberation Sans: widths 1.057 as computed, regular and bold loaded, the stack resolving to it.
|
|
87
|
-
// The family list walks the platforms: Roboto (Android), Helvetica Neue (Apple), Segoe UI (Windows),
|
|
88
|
-
// Arial, then Linux's Noto Sans, Ubuntu, Liberation Sans, DejaVu Sans; a face that is not installed fails
|
|
89
|
-
// to load and the browser moves to the next family. font-size-adjust was tried and rescaled Geist itself.
|
|
85
|
+
// Local faces keep an offline fallback. The HTML writer may append a small factory Sans subset.
|
|
90
86
|
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%}
|
|
91
87
|
@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%}
|
|
92
88
|
@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%}
|