@file-viewer/core 2.4.0 → 3.0.0

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.
@@ -0,0 +1,848 @@
1
+ import createDOMPurify from 'dompurify';
2
+ import { applyFileViewerPagePrintMasksToHtml, buildFileViewerPrintMaskOverlayHtml, FILE_VIEWER_PRINT_MASK_STYLE, normalizeFileViewerPrintMaskOptions, } from './features/printMask.js';
3
+ import { inlineFileViewerBlobUrlsInHtml, prepareFileViewerRenderedContentForSnapshot, replaceFileViewerCanvasWithImages, resolveFileViewerPrintStyle, } from './output/export.js';
4
+ const escapeHtmlAttribute = (value) => value
5
+ .replace(/&/g, '&')
6
+ .replace(/"/g, '"')
7
+ .replace(/</g, '&lt;')
8
+ .replace(/>/g, '&gt;');
9
+ const EXPORT_SANITIZER_OPTIONS = {
10
+ WHOLE_DOCUMENT: true,
11
+ USE_PROFILES: { html: true, svg: true, svgFilters: true, mathMl: true },
12
+ ADD_TAGS: ['use'],
13
+ ADD_ATTR: ['target', 'rel', 'download'],
14
+ FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'base', 'form', 'link'],
15
+ FORBID_ATTR: ['srcdoc'],
16
+ };
17
+ const resolveExportDocument = (documentRef) => {
18
+ return documentRef || globalThis.document || null;
19
+ };
20
+ const isCssIdentifierCharacter = (value) => /[A-Za-z0-9_-]/.test(value);
21
+ const decodeCssEscape = (value, slashIndex) => {
22
+ let cursor = slashIndex + 1;
23
+ const first = value[cursor] || '';
24
+ if (!first) {
25
+ return { value: '', nextIndex: cursor };
26
+ }
27
+ if (first === '\r' || first === '\n' || first === '\f') {
28
+ if (first === '\r' && value[cursor + 1] === '\n')
29
+ cursor += 1;
30
+ return { value: '', nextIndex: cursor + 1 };
31
+ }
32
+ let hexadecimal = '';
33
+ while (cursor < value.length && hexadecimal.length < 6 && /[0-9a-f]/i.test(value[cursor] || '')) {
34
+ hexadecimal += value[cursor];
35
+ cursor += 1;
36
+ }
37
+ if (hexadecimal) {
38
+ if (/\s/.test(value[cursor] || ''))
39
+ cursor += 1;
40
+ const codePoint = Number.parseInt(hexadecimal, 16);
41
+ return {
42
+ value: codePoint === 0 || codePoint > 0x10ffff
43
+ ? '\uFFFD'
44
+ : String.fromCodePoint(codePoint),
45
+ nextIndex: cursor,
46
+ };
47
+ }
48
+ return { value: first, nextIndex: cursor + 1 };
49
+ };
50
+ /**
51
+ * Remove CSS comments and decode identifier escapes before looking for URL
52
+ * functions. CSS comments and escapes may otherwise split security-sensitive
53
+ * tokens such as `url` or `@import` while the browser still accepts them.
54
+ */
55
+ const canonicalizeCssForSecurity = (css) => {
56
+ let output = '';
57
+ let quote = '';
58
+ let cursor = 0;
59
+ while (cursor < css.length) {
60
+ const character = css[cursor] || '';
61
+ if (quote) {
62
+ output += character;
63
+ if (character === '\\') {
64
+ output += css[cursor + 1] || '';
65
+ cursor += 2;
66
+ continue;
67
+ }
68
+ if (character === quote)
69
+ quote = '';
70
+ cursor += 1;
71
+ continue;
72
+ }
73
+ if (character === '/' && css[cursor + 1] === '*') {
74
+ const commentEnd = css.indexOf('*/', cursor + 2);
75
+ cursor = commentEnd < 0 ? css.length : commentEnd + 2;
76
+ continue;
77
+ }
78
+ if (character === '"' || character === "'") {
79
+ quote = character;
80
+ output += character;
81
+ cursor += 1;
82
+ continue;
83
+ }
84
+ if (character === '\\') {
85
+ const decoded = decodeCssEscape(css, cursor);
86
+ output += decoded.value;
87
+ cursor = decoded.nextIndex;
88
+ continue;
89
+ }
90
+ const codePoint = character.charCodeAt(0);
91
+ output += codePoint < 0x20 && character !== '\t' && character !== '\n' && character !== '\r'
92
+ ? ' '
93
+ : character;
94
+ cursor += 1;
95
+ }
96
+ return output;
97
+ };
98
+ const SAFE_CSS_DATA_URL_RE = /^data:(?:image\/(?:avif|bmp|gif|jpeg|png|webp|x-icon)|font\/(?:collection|otf|sfnt|ttf|woff2?)|application\/(?:font-sfnt|font-woff|vnd\.ms-fontobject|x-font-opentype|x-font-ttf|x-font-woff));/i;
99
+ const isSafeSvgCssDataUrl = (value) => {
100
+ const match = /^data:image\/svg\+xml(?:;charset=[A-Za-z0-9._-]+)?,([\s\S]*)$/i.exec(value);
101
+ if (!match || match[1].length > 1024 * 1024)
102
+ return false;
103
+ let svg;
104
+ try {
105
+ svg = decodeURIComponent(match[1]);
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ if (!/^\s*<svg(?:\s|>)/i.test(svg))
111
+ return false;
112
+ if (/<\/?(?:script|style|foreignObject|iframe|object|embed|form|link)\b/i.test(svg))
113
+ return false;
114
+ if (/<!\s*(?:doctype|entity)\b/i.test(svg) || /\son[a-z0-9_-]+\s*=/i.test(svg))
115
+ return false;
116
+ if (/@import\b/i.test(svg) || /url\s*\(\s*(?!["']?#)/i.test(svg))
117
+ return false;
118
+ for (const resource of svg.matchAll(/\s(?:href|xlink:href|src)\s*=\s*(["'])([\s\S]*?)\1/gi)) {
119
+ const target = normalizeResourceUrlForSvgData(resource[2] || '');
120
+ if (!/^#[A-Za-z0-9_.:-]+$/.test(target) && !SAFE_CSS_DATA_URL_RE.test(target))
121
+ return false;
122
+ }
123
+ return true;
124
+ };
125
+ const stripUrlControlCharacters = (value) => {
126
+ let normalized = '';
127
+ for (const character of value) {
128
+ const codePoint = character.charCodeAt(0);
129
+ if (codePoint <= 0x20 || (codePoint >= 0x7f && codePoint <= 0x9f))
130
+ continue;
131
+ normalized += character;
132
+ }
133
+ return normalized.trim();
134
+ };
135
+ const normalizeResourceUrlForSvgData = stripUrlControlCharacters;
136
+ const SAFE_EXPORT_EMBEDDED_DATA_URL_RE = /^data:(?:image\/(?:avif|bmp|gif|jpeg|png|webp|x-icon)|audio\/[a-z0-9.+-]+|video\/[a-z0-9.+-]+|text\/vtt)(?:;[^,]*)?,/i;
137
+ const isSafeExportEmbeddedResourceUrl = (value) => {
138
+ const normalized = stripUrlControlCharacters(value);
139
+ if (!normalized)
140
+ return false;
141
+ if (normalized.startsWith('#'))
142
+ return true;
143
+ if (/^blob:/i.test(normalized))
144
+ return true;
145
+ return SAFE_EXPORT_EMBEDDED_DATA_URL_RE.test(normalized);
146
+ };
147
+ const sanitizeExportEmbeddedResourceAttribute = (element, attributeName, options = {}) => {
148
+ if (!element.hasAttribute(attributeName))
149
+ return;
150
+ const normalized = stripUrlControlCharacters(element.getAttribute(attributeName) || '');
151
+ const allowed = options.fragmentOnly
152
+ ? /^#[A-Za-z0-9_.:-]+$/.test(normalized)
153
+ : isSafeExportEmbeddedResourceUrl(normalized);
154
+ if (allowed) {
155
+ element.setAttribute(attributeName, normalized);
156
+ }
157
+ else {
158
+ element.removeAttribute(attributeName);
159
+ }
160
+ };
161
+ const normalizeCssUrl = (value) => {
162
+ let normalized = value.trim();
163
+ const quote = normalized[0];
164
+ if ((quote === '"' || quote === "'") && normalized[normalized.length - 1] === quote) {
165
+ normalized = normalized.slice(1, -1).trim();
166
+ }
167
+ return stripUrlControlCharacters(normalized);
168
+ };
169
+ const isSafeCssUrl = (value) => {
170
+ const normalized = normalizeCssUrl(value);
171
+ if (!normalized)
172
+ return false;
173
+ if (normalized.startsWith('#'))
174
+ return true;
175
+ if (/^blob:/i.test(normalized))
176
+ return true;
177
+ return SAFE_CSS_DATA_URL_RE.test(normalized) || isSafeSvgCssDataUrl(normalized);
178
+ };
179
+ const readCssUrlFunctionEnd = (css, start) => {
180
+ let quote = '';
181
+ for (let cursor = start; cursor < css.length; cursor += 1) {
182
+ const character = css[cursor] || '';
183
+ if (quote) {
184
+ if (character === '\\') {
185
+ cursor += 1;
186
+ }
187
+ else if (character === quote) {
188
+ quote = '';
189
+ }
190
+ continue;
191
+ }
192
+ if (character === '"' || character === "'") {
193
+ quote = character;
194
+ }
195
+ else if (character === ')') {
196
+ return cursor;
197
+ }
198
+ }
199
+ return -1;
200
+ };
201
+ const inspectCssNetworkTokens = (css) => {
202
+ let hasImport = false;
203
+ let hasUrl = false;
204
+ let quote = '';
205
+ let cursor = 0;
206
+ while (cursor < css.length) {
207
+ const character = css[cursor] || '';
208
+ if (quote) {
209
+ if (character === '\\') {
210
+ cursor += 2;
211
+ continue;
212
+ }
213
+ if (character === quote)
214
+ quote = '';
215
+ cursor += 1;
216
+ continue;
217
+ }
218
+ if (character === '/' && css[cursor + 1] === '*') {
219
+ const commentEnd = css.indexOf('*/', cursor + 2);
220
+ cursor = commentEnd < 0 ? css.length : commentEnd + 2;
221
+ continue;
222
+ }
223
+ if (character === '"' || character === "'") {
224
+ quote = character;
225
+ cursor += 1;
226
+ continue;
227
+ }
228
+ if (character === '\\') {
229
+ cursor += 2;
230
+ continue;
231
+ }
232
+ if (character === '@' && css.slice(cursor + 1, cursor + 7).toLowerCase() === 'import') {
233
+ const boundary = css[cursor + 7] || '';
234
+ if (!boundary || !isCssIdentifierCharacter(boundary))
235
+ hasImport = true;
236
+ }
237
+ if (css.slice(cursor, cursor + 3).toLowerCase() === 'url' &&
238
+ !isCssIdentifierCharacter(css[cursor - 1] || '')) {
239
+ let openParenthesis = cursor + 3;
240
+ while (/\s/.test(css[openParenthesis] || ''))
241
+ openParenthesis += 1;
242
+ if (css[openParenthesis] === '(')
243
+ hasUrl = true;
244
+ }
245
+ cursor += 1;
246
+ }
247
+ return { hasImport, hasUrl };
248
+ };
249
+ /**
250
+ * Keep ordinary CSS and local embedded resources, but remove every construct
251
+ * that can make an exported document initiate a new network request. Imported
252
+ * stylesheets fail closed; unsafe url() values become `none` so the rest of a
253
+ * declaration can still render.
254
+ */
255
+ const sanitizeExportCss = (css) => {
256
+ const canonical = canonicalizeCssForSecurity(css);
257
+ const canonicalTokens = inspectCssNetworkTokens(canonical);
258
+ const originalTokens = inspectCssNetworkTokens(css);
259
+ if (canonicalTokens.hasImport)
260
+ return '';
261
+ if (canonicalTokens.hasUrl && !originalTokens.hasUrl)
262
+ return '';
263
+ if (!originalTokens.hasUrl)
264
+ return css;
265
+ let output = '';
266
+ let quote = '';
267
+ let cursor = 0;
268
+ while (cursor < css.length) {
269
+ const character = css[cursor] || '';
270
+ if (quote) {
271
+ output += character;
272
+ if (character === '\\') {
273
+ output += css[cursor + 1] || '';
274
+ cursor += 2;
275
+ continue;
276
+ }
277
+ if (character === quote)
278
+ quote = '';
279
+ cursor += 1;
280
+ continue;
281
+ }
282
+ if (character === '"' || character === "'") {
283
+ quote = character;
284
+ output += character;
285
+ cursor += 1;
286
+ continue;
287
+ }
288
+ if (character === '/' && css[cursor + 1] === '*') {
289
+ const commentEnd = css.indexOf('*/', cursor + 2);
290
+ if (commentEnd < 0) {
291
+ output += css.slice(cursor);
292
+ break;
293
+ }
294
+ output += css.slice(cursor, commentEnd + 2);
295
+ cursor = commentEnd + 2;
296
+ continue;
297
+ }
298
+ if (character === '\\') {
299
+ output += character;
300
+ output += css[cursor + 1] || '';
301
+ cursor += 2;
302
+ continue;
303
+ }
304
+ if (css.slice(cursor, cursor + 3).toLowerCase() === 'url' &&
305
+ !isCssIdentifierCharacter(css[cursor - 1] || '')) {
306
+ let openParenthesis = cursor + 3;
307
+ while (/\s/.test(css[openParenthesis] || ''))
308
+ openParenthesis += 1;
309
+ if (css[openParenthesis] === '(') {
310
+ const closeParenthesis = readCssUrlFunctionEnd(css, openParenthesis + 1);
311
+ if (closeParenthesis < 0)
312
+ return '';
313
+ const urlValue = css.slice(openParenthesis + 1, closeParenthesis);
314
+ output += isSafeCssUrl(urlValue)
315
+ ? css.slice(cursor, closeParenthesis + 1)
316
+ : 'none';
317
+ cursor = closeParenthesis + 1;
318
+ continue;
319
+ }
320
+ }
321
+ output += character;
322
+ cursor += 1;
323
+ }
324
+ return output;
325
+ };
326
+ const getExportPurifier = (documentRef) => {
327
+ const windowRef = documentRef.defaultView;
328
+ if (!windowRef) {
329
+ return null;
330
+ }
331
+ const purifier = createDOMPurify(windowRef);
332
+ if (!purifier.isSupported) {
333
+ return null;
334
+ }
335
+ purifier.addHook('afterSanitizeElements', node => {
336
+ var _a;
337
+ const element = node;
338
+ if (((_a = element.localName) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== 'style')
339
+ return;
340
+ const sanitizedCss = sanitizeExportCss(element.textContent || '');
341
+ if (sanitizedCss) {
342
+ element.textContent = sanitizedCss;
343
+ }
344
+ else {
345
+ element.remove();
346
+ }
347
+ });
348
+ purifier.addHook('afterSanitizeAttributes', node => {
349
+ var _a;
350
+ const element = node;
351
+ const localName = (_a = element.localName) === null || _a === void 0 ? void 0 : _a.toLowerCase();
352
+ if (localName === 'a' && (element.getAttribute('target') || '').trim().toLowerCase() === '_blank') {
353
+ element.setAttribute('rel', 'noopener noreferrer');
354
+ }
355
+ if (localName === 'a' || localName === 'area') {
356
+ // Keep ordinary links, but never preserve document-controlled beacons.
357
+ element.removeAttribute('ping');
358
+ }
359
+ if (element.hasAttribute('srcset')) {
360
+ // Candidate lists have their own comma/descriptor grammar. The export
361
+ // keeps the materialized src and drops candidates that could select a
362
+ // remote resource after the standalone document is opened.
363
+ element.removeAttribute('srcset');
364
+ }
365
+ if (['img', 'audio', 'video', 'source', 'track', 'input'].includes(localName || '')) {
366
+ sanitizeExportEmbeddedResourceAttribute(element, 'src');
367
+ }
368
+ if (localName === 'video') {
369
+ sanitizeExportEmbeddedResourceAttribute(element, 'poster');
370
+ }
371
+ if (element.hasAttribute('background')) {
372
+ sanitizeExportEmbeddedResourceAttribute(element, 'background');
373
+ }
374
+ if (element.namespaceURI === 'http://www.w3.org/2000/svg' && localName !== 'a') {
375
+ const fragmentOnly = localName === 'use' || localName === 'mpath';
376
+ sanitizeExportEmbeddedResourceAttribute(element, 'href', { fragmentOnly });
377
+ sanitizeExportEmbeddedResourceAttribute(element, 'xlink:href', { fragmentOnly });
378
+ }
379
+ if (element.hasAttribute('style')) {
380
+ const sanitizedStyle = sanitizeExportCss(element.getAttribute('style') || '');
381
+ if (sanitizedStyle) {
382
+ element.setAttribute('style', sanitizedStyle);
383
+ }
384
+ else {
385
+ element.removeAttribute('style');
386
+ }
387
+ }
388
+ });
389
+ return purifier;
390
+ };
391
+ const EXPORT_STATIC_META = [
392
+ '<meta charset="utf-8" />',
393
+ '<meta name="viewport" content="width=device-width,initial-scale=1" />',
394
+ ].join('\n ');
395
+ /**
396
+ * Sanitize the complete standalone export document before it is downloaded or
397
+ * mounted into a print window. This is deliberately enforced in core because
398
+ * third-party render adapters and watermark overrides are library inputs.
399
+ */
400
+ export const sanitizeFileViewerExportDocumentHtml = (html, documentRef) => {
401
+ const currentDocument = resolveExportDocument(documentRef);
402
+ const purifier = currentDocument
403
+ ? getExportPurifier(currentDocument)
404
+ : null;
405
+ if (!purifier) {
406
+ // Export is a browser feature. Fail closed rather than return executable
407
+ // markup when called without a usable DOM implementation.
408
+ return '<!doctype html>\n<html lang="en"><head><meta charset="utf-8" /></head><body></body></html>';
409
+ }
410
+ const sanitized = String(purifier.sanitize(html, EXPORT_SANITIZER_OPTIONS));
411
+ const withStaticMeta = sanitized.replace('<head>', `<head>\n ${EXPORT_STATIC_META}`);
412
+ return `<!doctype html>\n${withStaticMeta}`;
413
+ };
414
+ const createEmptyFileViewerExportDocumentRoot = (documentRef) => {
415
+ const root = documentRef.createElement('html');
416
+ root.lang = 'en';
417
+ const head = documentRef.createElement('head');
418
+ const charset = documentRef.createElement('meta');
419
+ charset.setAttribute('charset', 'utf-8');
420
+ head.append(charset);
421
+ root.append(head, documentRef.createElement('body'));
422
+ return root;
423
+ };
424
+ const ensureFileViewerExportDocumentMeta = (root) => {
425
+ const head = root.querySelector(':scope > head');
426
+ if (!head)
427
+ return root;
428
+ if (!head.querySelector('meta[charset]')) {
429
+ const charset = root.ownerDocument.createElement('meta');
430
+ charset.setAttribute('charset', 'utf-8');
431
+ head.prepend(charset);
432
+ }
433
+ if (!head.querySelector('meta[name="viewport"]')) {
434
+ const viewport = root.ownerDocument.createElement('meta');
435
+ viewport.setAttribute('name', 'viewport');
436
+ viewport.setAttribute('content', 'width=device-width,initial-scale=1');
437
+ const charset = head.querySelector('meta[charset]');
438
+ charset === null || charset === void 0 ? void 0 : charset.after(viewport);
439
+ }
440
+ return root;
441
+ };
442
+ /**
443
+ * Produce a sanitized, inert document tree for the print-window mount path.
444
+ * Returning DOM here avoids reinterpreting the sanitized string through a
445
+ * second HTML sink such as document.write or innerHTML.
446
+ */
447
+ export const sanitizeFileViewerExportDocumentDom = (html, documentRef) => {
448
+ const currentDocument = resolveExportDocument(documentRef);
449
+ if (!currentDocument) {
450
+ throw new Error('A browser document is required to build printable DOM.');
451
+ }
452
+ const purifier = getExportPurifier(currentDocument);
453
+ if (!purifier) {
454
+ return createEmptyFileViewerExportDocumentRoot(currentDocument);
455
+ }
456
+ const sanitized = purifier.sanitize(html, {
457
+ ...EXPORT_SANITIZER_OPTIONS,
458
+ RETURN_DOM: true
459
+ });
460
+ if (!sanitized ||
461
+ sanitized.nodeType !== 1 ||
462
+ sanitized.localName.toLowerCase() !== 'html') {
463
+ return createEmptyFileViewerExportDocumentRoot(currentDocument);
464
+ }
465
+ return ensureFileViewerExportDocumentMeta(sanitized);
466
+ };
467
+ const EXPORT_DOCUMENT_STYLE = `
468
+ * { box-sizing: border-box; }
469
+ html, body { margin: 0; min-height: 100%; background: #f2f4f7; color: #172033; font-family: Aptos, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; print-color-adjust: exact; -webkit-print-color-adjust: exact; }
470
+ body { padding: 24px; }
471
+ .viewer-export-shell { position: relative; min-height: calc(100vh - 48px); overflow: visible; background: #f2f4f7; }
472
+ .viewer-export-content { position: relative; z-index: 1; contain: none; width: 100%; min-height: 100%; overflow: visible; }
473
+ .viewer-export-watermark { position: absolute; inset: 0; pointer-events: none; z-index: 20; background-repeat: repeat; print-color-adjust: exact; -webkit-print-color-adjust: exact; }
474
+ .viewer-export-content .file-render,
475
+ .viewer-export-content .file-viewer,
476
+ .viewer-export-content .viewer-stage,
477
+ .viewer-export-content .content,
478
+ .viewer-export-content .pdf-shell,
479
+ .viewer-export-content .pdf-content,
480
+ .viewer-export-content .pdf-viewport,
481
+ .viewer-export-content .pdf-wrapper,
482
+ .viewer-export-content .docx-fit-viewer,
483
+ .viewer-export-content .docx-wrapper,
484
+ .viewer-export-content .msdoc-stage,
485
+ .viewer-export-content .code-viewer,
486
+ .viewer-export-content .markdown-viewer,
487
+ .viewer-export-content .email-shell,
488
+ .viewer-export-content .archive-shell,
489
+ .viewer-export-content .eda-shell,
490
+ .viewer-export-content .ebook-shell,
491
+ .viewer-export-content .umd-shell,
492
+ .viewer-export-content .drawing-shell,
493
+ .viewer-export-content .audio-shell,
494
+ .viewer-export-content .cad-shell,
495
+ .viewer-export-content .cad-body,
496
+ .viewer-export-content .cad-canvas-wrap,
497
+ .viewer-export-content .dwg-preview-frame {
498
+ position: relative !important;
499
+ inset: auto !important;
500
+ contain: none !important;
501
+ width: 100% !important;
502
+ height: auto !important;
503
+ min-height: 0 !important;
504
+ max-height: none !important;
505
+ overflow: visible !important;
506
+ }
507
+ .viewer-export-content .docx-wrapper {
508
+ display: block !important;
509
+ padding: 0 !important;
510
+ background: transparent !important;
511
+ }
512
+ .viewer-export-content .docx-print-document {
513
+ display: block !important;
514
+ width: fit-content !important;
515
+ max-width: 100% !important;
516
+ height: auto !important;
517
+ overflow: visible !important;
518
+ margin: 0 auto !important;
519
+ }
520
+ .viewer-export-content .docx-page-frame {
521
+ position: relative !important;
522
+ width: var(--viewer-print-page-width, fit-content) !important;
523
+ height: var(--viewer-print-page-height, auto) !important;
524
+ min-height: var(--viewer-print-page-height, 0) !important;
525
+ max-width: 100% !important;
526
+ margin: 0 auto 18px !important;
527
+ overflow: hidden !important;
528
+ break-inside: avoid;
529
+ page-break-inside: avoid;
530
+ break-after: page;
531
+ page-break-after: always;
532
+ }
533
+ .viewer-export-content .msdoc-page {
534
+ position: relative !important;
535
+ width: var(--viewer-print-page-width, 794px) !important;
536
+ min-height: var(--viewer-print-page-height, 1123px) !important;
537
+ max-width: 100% !important;
538
+ height: auto !important;
539
+ margin: 0 auto 18px !important;
540
+ overflow: visible !important;
541
+ break-after: page;
542
+ page-break-after: always;
543
+ }
544
+ .viewer-export-content .docx-page-frame:last-child,
545
+ .viewer-export-content .msdoc-page:last-child {
546
+ break-after: auto;
547
+ page-break-after: auto;
548
+ }
549
+ .viewer-export-content .docx-page-frame > section.docx {
550
+ position: relative !important;
551
+ top: auto !important;
552
+ left: auto !important;
553
+ width: var(--viewer-print-page-width, auto) !important;
554
+ min-height: var(--viewer-print-page-height, auto) !important;
555
+ max-width: none !important;
556
+ margin: 0 auto !important;
557
+ overflow: visible !important;
558
+ transform: none !important;
559
+ box-shadow: none !important;
560
+ break-inside: avoid;
561
+ page-break-inside: avoid;
562
+ }
563
+ .viewer-export-content .msdoc-stage {
564
+ display: block !important;
565
+ padding: 0 !important;
566
+ background: transparent !important;
567
+ }
568
+ .viewer-export-content .msdoc-page > .msdoc-root {
569
+ margin: 0 auto !important;
570
+ box-shadow: none !important;
571
+ overflow: visible !important;
572
+ }
573
+ .viewer-export-content .pdf-toolbar,
574
+ .viewer-export-content .pdf-nav-pane,
575
+ .viewer-export-content .viewer-actions,
576
+ .viewer-export-content .code-toolbar,
577
+ .viewer-export-content .umd-toolbar,
578
+ .viewer-export-content .drawing-toolbar,
579
+ .viewer-export-content .cad-toolbar {
580
+ display: none !important;
581
+ }
582
+ .viewer-export-content .pdf-content,
583
+ .viewer-export-content .pdf-shell--nav-hidden .pdf-content,
584
+ .viewer-export-content .cad-body.without-layers {
585
+ display: block !important;
586
+ grid-template-columns: none !important;
587
+ }
588
+ .viewer-export-content .pdfViewer { padding: 0 !important; }
589
+ .viewer-export-content .pdfViewer .page {
590
+ margin: 0 auto 16px !important;
591
+ border: 0 !important;
592
+ box-shadow: none !important;
593
+ break-after: page;
594
+ page-break-after: always;
595
+ }
596
+ .viewer-export-content .pdfViewer .page:last-child {
597
+ break-after: auto;
598
+ page-break-after: auto;
599
+ }
600
+ .viewer-export-content .pdf-export-document {
601
+ display: grid;
602
+ justify-items: center;
603
+ gap: 18px;
604
+ padding: 4px 0;
605
+ }
606
+ .viewer-export-content .pdf-export-page {
607
+ width: var(--viewer-print-page-width, auto);
608
+ height: var(--viewer-print-page-height, auto);
609
+ max-width: 100%;
610
+ overflow: hidden;
611
+ background: #ffffff;
612
+ box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
613
+ break-inside: avoid;
614
+ page-break-inside: avoid;
615
+ break-after: page;
616
+ page-break-after: always;
617
+ }
618
+ .viewer-export-content .pdf-export-page:last-child {
619
+ break-after: auto;
620
+ page-break-after: auto;
621
+ }
622
+ .viewer-export-content .pdf-export-page img {
623
+ display: block;
624
+ width: 100%;
625
+ height: 100%;
626
+ object-fit: contain;
627
+ }
628
+ .viewer-export-content .pptx-wrapper {
629
+ width: 100% !important;
630
+ max-width: 100% !important;
631
+ height: auto !important;
632
+ overflow: visible !important;
633
+ transform: none !important;
634
+ }
635
+ .viewer-export-content .pptx-wrapper .slide {
636
+ margin: 0 auto 18px !important;
637
+ break-inside: avoid;
638
+ page-break-inside: avoid;
639
+ break-after: page;
640
+ page-break-after: always;
641
+ box-shadow: none !important;
642
+ }
643
+ .viewer-export-content .pptx-wrapper .slide:last-child {
644
+ break-after: auto;
645
+ page-break-after: auto;
646
+ }
647
+ .viewer-export-content .ofd-stage {
648
+ padding: 0 !important;
649
+ overflow: visible !important;
650
+ }
651
+ .viewer-export-content .ofd-page,
652
+ .viewer-export-content .drawing-svg,
653
+ .viewer-export-content .cad-canvas-wrap,
654
+ .viewer-export-content .dwg-preview-frame {
655
+ break-inside: avoid;
656
+ page-break-inside: avoid;
657
+ break-after: page;
658
+ page-break-after: always;
659
+ box-shadow: none !important;
660
+ }
661
+ .viewer-export-content .ofd-page:last-child,
662
+ .viewer-export-content .drawing-svg:last-child,
663
+ .viewer-export-content .cad-canvas-wrap:last-child,
664
+ .viewer-export-content .dwg-preview-frame:last-child {
665
+ break-after: auto;
666
+ page-break-after: auto;
667
+ }
668
+ .viewer-export-content .code-area {
669
+ overflow: visible !important;
670
+ white-space: pre-wrap !important;
671
+ word-break: break-word !important;
672
+ }
673
+ .viewer-export-content .umd-body,
674
+ .viewer-export-content .umd-stage-wrap,
675
+ .viewer-export-content .umd-stage {
676
+ display: block !important;
677
+ height: auto !important;
678
+ max-height: none !important;
679
+ overflow: visible !important;
680
+ }
681
+ .viewer-export-content .umd-toc {
682
+ display: none !important;
683
+ }
684
+ img, canvas, svg, video { max-width: 100%; }
685
+ @media print {
686
+ @page { margin: 12mm; }
687
+ html, body { min-height: auto; background: #ffffff; }
688
+ body { padding: 0; }
689
+ .viewer-export-shell,
690
+ .viewer-export-content {
691
+ min-height: 0;
692
+ overflow: visible;
693
+ background: #ffffff;
694
+ }
695
+ .viewer-export-content .pdf-export-document {
696
+ display: block;
697
+ padding: 0;
698
+ }
699
+ .viewer-export-content .pdf-export-page {
700
+ width: var(--viewer-print-page-width, auto) !important;
701
+ height: var(--viewer-print-page-height, auto) !important;
702
+ max-width: none !important;
703
+ margin: 0;
704
+ overflow: hidden;
705
+ box-shadow: none;
706
+ }
707
+ .viewer-export-content .docx-page-frame {
708
+ width: var(--viewer-print-page-width, auto) !important;
709
+ height: var(--viewer-print-page-height, auto) !important;
710
+ min-height: var(--viewer-print-page-height, 0) !important;
711
+ max-width: none !important;
712
+ margin: 0 !important;
713
+ overflow: hidden !important;
714
+ }
715
+ .viewer-export-content .msdoc-page {
716
+ width: var(--viewer-print-page-width, 794px) !important;
717
+ min-height: var(--viewer-print-page-height, 1123px) !important;
718
+ max-width: none !important;
719
+ margin: 0 !important;
720
+ overflow: visible !important;
721
+ }
722
+ .viewer-export-content .docx-page-frame > section.docx,
723
+ .viewer-export-content .msdoc-page > .msdoc-root {
724
+ width: var(--viewer-print-page-width, 100%) !important;
725
+ max-width: none !important;
726
+ border: 0 !important;
727
+ }
728
+ .viewer-export-content .pptx-wrapper .slide,
729
+ .viewer-export-content .ofd-page,
730
+ .viewer-export-content .drawing-svg,
731
+ .viewer-export-content .cad-canvas-wrap,
732
+ .viewer-export-content .dwg-preview-frame {
733
+ box-shadow: none !important;
734
+ }
735
+ }
736
+ `;
737
+ export const collectDocumentStyles = (documentRef) => {
738
+ const currentDocument = resolveExportDocument(documentRef);
739
+ if (!currentDocument) {
740
+ return '';
741
+ }
742
+ return Array.from(currentDocument.querySelectorAll('style, link[rel="stylesheet"]'))
743
+ .map(node => {
744
+ var _a;
745
+ if (node.localName.toLowerCase() === 'style') {
746
+ return `<style>${node.textContent || ''}</style>`;
747
+ }
748
+ const link = node;
749
+ try {
750
+ const cssText = Array.from(((_a = link.sheet) === null || _a === void 0 ? void 0 : _a.cssRules) || [])
751
+ .map(rule => rule.cssText)
752
+ .join('\n');
753
+ return cssText
754
+ ? `<style data-viewer-inlined-stylesheet>${cssText}</style>`
755
+ : '';
756
+ }
757
+ catch {
758
+ // Cross-origin and unreadable stylesheets fail closed. Keeping their
759
+ // link would make a standalone export initiate a second request.
760
+ return '';
761
+ }
762
+ })
763
+ .filter(Boolean)
764
+ .join('\n');
765
+ };
766
+ const buildExportDocumentMarkup = ({ contentHtml, includeDocumentStyles = true, printStyle = '', title, watermarkInlineStyle = '', mask = null, documentRef, }) => {
767
+ var _a, _b;
768
+ const watermark = watermarkInlineStyle
769
+ ? `<div class="viewer-export-watermark" style="${escapeHtmlAttribute(watermarkInlineStyle)}"></div>`
770
+ : '';
771
+ const normalizedMask = normalizeFileViewerPrintMaskOptions(mask);
772
+ const globalMask = normalizedMask
773
+ ? {
774
+ ...normalizedMask,
775
+ regions: (_a = normalizedMask.regions) === null || _a === void 0 ? void 0 : _a.filter(region => region.pageIndex === undefined),
776
+ stamps: (_b = normalizedMask.stamps) === null || _b === void 0 ? void 0 : _b.filter(stamp => stamp.pageIndex === undefined),
777
+ }
778
+ : null;
779
+ const maskHtml = buildFileViewerPrintMaskOverlayHtml(globalMask);
780
+ const maskedContentHtml = applyFileViewerPagePrintMasksToHtml(contentHtml, normalizedMask);
781
+ const styles = includeDocumentStyles ? collectDocumentStyles(documentRef) : '';
782
+ const printOverrideStyle = printStyle ? `<style data-viewer-print-style>${printStyle}</style>` : '';
783
+ const maskStyle = normalizedMask ? `<style data-viewer-print-mask-style>${FILE_VIEWER_PRINT_MASK_STYLE}</style>` : '';
784
+ const safeTitle = escapeHtmlAttribute(title);
785
+ return `<!doctype html>
786
+ <html lang="zh-CN">
787
+ <head>
788
+ <meta charset="utf-8" />
789
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
790
+ <title>${safeTitle}</title>
791
+ ${styles}
792
+ <style>${EXPORT_DOCUMENT_STYLE}</style>
793
+ ${maskStyle}
794
+ </head>
795
+ <body>
796
+ <main class="viewer-export-shell">
797
+ <div class="viewer-export-content">${maskedContentHtml}</div>
798
+ ${maskHtml}
799
+ ${watermark}
800
+ </main>
801
+ ${printOverrideStyle}
802
+ </body>
803
+ </html>`;
804
+ };
805
+ export const buildExportHtmlDocument = (options) => {
806
+ return sanitizeFileViewerExportDocumentHtml(buildExportDocumentMarkup(options), options.documentRef);
807
+ };
808
+ export const buildExportDomDocument = (options) => {
809
+ return sanitizeFileViewerExportDocumentDom(buildExportDocumentMarkup(options), options.documentRef);
810
+ };
811
+ const resolveFileViewerRenderedDocumentOptions = async ({ source, mode = 'export', title, adapter = null, watermarkInlineStyle = '', mask = null, }) => {
812
+ const exportOptions = { mode, title };
813
+ const toHtml = adapter === null || adapter === void 0 ? void 0 : adapter.toHtml;
814
+ const normalizedMask = normalizeFileViewerPrintMaskOptions(mask);
815
+ if (toHtml) {
816
+ await prepareFileViewerRenderedContentForSnapshot(source, adapter);
817
+ const contentHtml = await inlineFileViewerBlobUrlsInHtml(await toHtml(exportOptions));
818
+ const printStyle = await resolveFileViewerPrintStyle(adapter, exportOptions);
819
+ return {
820
+ contentHtml,
821
+ includeDocumentStyles: adapter.includeDocumentStyles !== false,
822
+ printStyle,
823
+ title,
824
+ watermarkInlineStyle,
825
+ mask: normalizedMask,
826
+ documentRef: source.ownerDocument,
827
+ };
828
+ }
829
+ await prepareFileViewerRenderedContentForSnapshot(source, adapter);
830
+ const clone = source.cloneNode(true);
831
+ clone.querySelectorAll('.viewer-watermark').forEach(node => node.remove());
832
+ replaceFileViewerCanvasWithImages(source, clone);
833
+ const printStyle = await resolveFileViewerPrintStyle(adapter, exportOptions);
834
+ return {
835
+ contentHtml: await inlineFileViewerBlobUrlsInHtml(clone.innerHTML),
836
+ printStyle,
837
+ title,
838
+ watermarkInlineStyle,
839
+ mask: normalizedMask,
840
+ documentRef: source.ownerDocument,
841
+ };
842
+ };
843
+ export const buildFileViewerRenderedHtmlDocument = async (options) => {
844
+ return buildExportHtmlDocument(await resolveFileViewerRenderedDocumentOptions(options));
845
+ };
846
+ export const buildFileViewerRenderedDomDocument = async (options) => {
847
+ return buildExportDomDocument(await resolveFileViewerRenderedDocumentOptions(options));
848
+ };