@file-viewer/core 2.0.7 → 2.0.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.
@@ -1,11 +1,12 @@
1
- import { getDocument, GlobalWorkerOptions, PDFWorker as PdfJsWorker, PixelsPerInch, version, } from 'pdfjs-dist/legacy/build/pdf.mjs';
1
+ import { getDocument, GlobalWorkerOptions, PDFWorker as PdfJsWorker, PixelsPerInch, } from 'pdfjs-dist/legacy/build/pdf.mjs';
2
2
  import { EventBus, GenericL10n, PDFFindController, PDFLinkService, PDFViewer, } from 'pdfjs-dist/legacy/web/pdf_viewer.mjs';
3
3
  import { registerFileViewerSearchProvider, registerFileViewerZoomProvider, unregisterFileViewerSearchProvider, unregisterFileViewerZoomProvider, } from '../features/document/dom/index.js';
4
4
  import { createFileViewerZoomChangeEmitter } from '../features/document/zoom.js';
5
5
  import { buildPrintPageStyle, formatCssPixels, } from '../output/printLayout.js';
6
+ import { DEFAULT_FILE_VIEWER_PDF_WORKER_PATH, resolveFileViewerPdfAssetUrls, } from '../platform/assets.js';
6
7
  import { DEFAULT_PDF_RANGE_CHUNK_SIZE } from '../source/loading.js';
7
8
  import { pdfViewerStyle } from './pdfStyles.js';
8
- export const DEFAULT_FILE_VIEWER_PDF_WORKER_URL = `https://npm.onmicrosoft.cn/pdfjs-dist@${version}/legacy/build/pdf.worker.mjs`;
9
+ export const DEFAULT_FILE_VIEWER_PDF_WORKER_URL = DEFAULT_FILE_VIEWER_PDF_WORKER_PATH;
9
10
  const MIN_SCALE = 0.2;
10
11
  const MAX_SCALE = 3;
11
12
  const SCALE_STEP = 0.1;
@@ -21,6 +22,10 @@ const createStyle = (documentRef) => {
21
22
  const style = documentRef.createElement('style');
22
23
  style.textContent = `${normalizedPdfViewerStyle}
23
24
  .pdf-state[hidden],.pdf-nav-pane[hidden]{display:none!important}
25
+ .pdf-page-button--with-thumbnail{grid-template-columns:52px minmax(0,1fr);min-height:74px}
26
+ .pdf-page-thumb--thumbnail{width:46px;height:60px;overflow:hidden;background:#fff}
27
+ .pdf-page-thumb--thumbnail img{display:block;width:100%;height:100%;object-fit:contain}
28
+ .pdf-page-thumb--thumbnail span{display:inline-flex;align-items:center;justify-content:center;width:100%;height:100%}
24
29
  `;
25
30
  return style;
26
31
  };
@@ -71,16 +76,7 @@ const waitForPaint = (view) => new Promise(resolve => {
71
76
  globalThis.setTimeout(resolve, 0);
72
77
  });
73
78
  const resolvePdfWorkerUrl = (options, documentUrl) => {
74
- const configured = options === null || options === void 0 ? void 0 : options.workerUrl;
75
- if (!configured) {
76
- return DEFAULT_FILE_VIEWER_PDF_WORKER_URL;
77
- }
78
- try {
79
- return documentUrl ? new URL(configured, documentUrl).href : configured;
80
- }
81
- catch {
82
- return configured;
83
- }
79
+ return resolveFileViewerPdfAssetUrls(options, documentUrl).workerUrl;
84
80
  };
85
81
  const buildOutlineItems = (items, prefix = 'outline') => items.map((item, index) => {
86
82
  const id = `${prefix}-${index}`;
@@ -105,6 +101,7 @@ export default async function renderPdf(buffer, target, context) {
105
101
  const options = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.pdf;
106
102
  const navigationEnabled = (options === null || options === void 0 ? void 0 : options.navigation) !== false;
107
103
  const toolbarVisible = (options === null || options === void 0 ? void 0 : options.toolbar) !== false;
104
+ const thumbnailsEnabled = (options === null || options === void 0 ? void 0 : options.thumbnails) === true;
108
105
  const zoomEmitter = createFileViewerZoomChangeEmitter();
109
106
  let navVisible = (options === null || options === void 0 ? void 0 : options.navigation) === false ? false : (options === null || options === void 0 ? void 0 : options.defaultNavigationVisible) !== false;
110
107
  let navMode = 'pages';
@@ -117,13 +114,17 @@ export default async function renderPdf(buffer, target, context) {
117
114
  let currentRotation = normalizeRotation((_b = options === null || options === void 0 ? void 0 : options.rotation) !== null && _b !== void 0 ? _b : 0);
118
115
  let outlineItems = [];
119
116
  let resizeObserver = null;
117
+ let thumbnailObserver = null;
120
118
  let fitFrame = 0;
119
+ let pageDimensionFrame = 0;
121
120
  let destroyed = false;
122
121
  let loadVersion = 0;
123
122
  let pdfSearchState = createPdfSearchState();
124
123
  let pdfMatchesCount = { current: 0, total: 0 };
125
124
  let pdfSearchOptions;
126
125
  let pdfSearchWaiters = [];
126
+ const pdfThumbnails = new Map();
127
+ const pendingPdfThumbnails = new Set();
127
128
  const pdfContext = {
128
129
  viewer: null,
129
130
  linkService: null,
@@ -222,11 +223,21 @@ export default async function renderPdf(buffer, target, context) {
222
223
  navList.replaceChildren();
223
224
  navList.className = navMode === 'pages' ? 'pdf-page-list' : 'pdf-outline-list';
224
225
  if (navMode === 'pages') {
226
+ thumbnailObserver === null || thumbnailObserver === void 0 ? void 0 : thumbnailObserver.disconnect();
225
227
  for (let page = 1; page <= pageCount; page += 1) {
226
228
  const button = createElement(documentRef, 'button', 'pdf-page-button');
227
229
  button.type = 'button';
228
230
  button.classList.toggle('pdf-page-button--active', page === currentPage);
229
- button.append(createElement(documentRef, 'span', 'pdf-page-thumb', String(page)), createElement(documentRef, 'span', 'pdf-page-label', `第 ${page} 页`));
231
+ button.classList.toggle('pdf-page-button--with-thumbnail', thumbnailsEnabled);
232
+ const thumb = createElement(documentRef, 'span', 'pdf-page-thumb');
233
+ if (thumbnailsEnabled) {
234
+ thumb.classList.add('pdf-page-thumb--thumbnail');
235
+ queuePdfThumbnail(page, thumb);
236
+ }
237
+ else {
238
+ thumb.textContent = String(page);
239
+ }
240
+ button.append(thumb, createElement(documentRef, 'span', 'pdf-page-label', `第 ${page} 页`));
230
241
  button.addEventListener('click', () => goToPage(page));
231
242
  navList.append(button);
232
243
  }
@@ -253,6 +264,101 @@ export default async function renderPdf(buffer, target, context) {
253
264
  navList.append(createElement(documentRef, 'div', 'pdf-outline-empty', '当前 PDF 没有可用目录'));
254
265
  }
255
266
  };
267
+ const paintPdfThumbnail = (pageNumber, thumb) => {
268
+ const imageUrl = pdfThumbnails.get(pageNumber);
269
+ thumb.dataset.pdfThumbnailPage = String(pageNumber);
270
+ if (!imageUrl) {
271
+ thumb.replaceChildren(createElement(documentRef, 'span', undefined, String(pageNumber)));
272
+ return false;
273
+ }
274
+ const image = documentRef.createElement('img');
275
+ image.src = imageUrl;
276
+ image.alt = `第 ${pageNumber} 页缩略图`;
277
+ image.loading = 'lazy';
278
+ thumb.replaceChildren(image);
279
+ return true;
280
+ };
281
+ const renderPdfThumbnail = async (pageNumber) => {
282
+ var _a, _b;
283
+ const pdfDocument = pdfContext.document;
284
+ if (!pdfDocument || pdfThumbnails.has(pageNumber) || pendingPdfThumbnails.has(pageNumber)) {
285
+ return;
286
+ }
287
+ pendingPdfThumbnails.add(pageNumber);
288
+ try {
289
+ const page = await pdfDocument.getPage(pageNumber);
290
+ if (destroyed || pdfContext.document !== pdfDocument) {
291
+ return;
292
+ }
293
+ const baseViewport = page.getViewport({
294
+ scale: PixelsPerInch.PDF_TO_CSS_UNITS,
295
+ rotation: currentRotation,
296
+ });
297
+ const deviceScale = Math.min(2, Math.max(1, targetWindow.devicePixelRatio || 1));
298
+ const thumbnailWidth = 46;
299
+ const ratio = Math.min(1, thumbnailWidth / Math.max(baseViewport.width, 1));
300
+ const renderViewport = page.getViewport({
301
+ scale: PixelsPerInch.PDF_TO_CSS_UNITS * ratio * deviceScale,
302
+ rotation: currentRotation,
303
+ });
304
+ const canvas = documentRef.createElement('canvas');
305
+ const canvasContext = canvas.getContext('2d');
306
+ if (!canvasContext) {
307
+ return;
308
+ }
309
+ canvas.width = Math.max(1, Math.ceil(renderViewport.width));
310
+ canvas.height = Math.max(1, Math.ceil(renderViewport.height));
311
+ await page.render({ canvas, canvasContext, viewport: renderViewport }).promise;
312
+ if (destroyed || pdfContext.document !== pdfDocument) {
313
+ return;
314
+ }
315
+ pdfThumbnails.set(pageNumber, canvas.toDataURL('image/png'));
316
+ canvas.width = 0;
317
+ canvas.height = 0;
318
+ (_b = (_a = page).cleanup) === null || _b === void 0 ? void 0 : _b.call(_a);
319
+ navList
320
+ .querySelectorAll(`.pdf-page-thumb--thumbnail[data-pdf-thumbnail-page="${pageNumber}"]`)
321
+ .forEach(thumb => paintPdfThumbnail(pageNumber, thumb));
322
+ }
323
+ catch (error) {
324
+ console.warn('[file-viewer] PDF 缩略图渲染失败。', error);
325
+ }
326
+ finally {
327
+ pendingPdfThumbnails.delete(pageNumber);
328
+ }
329
+ };
330
+ const ensureThumbnailObserver = () => {
331
+ if (!thumbnailsEnabled || thumbnailObserver || typeof targetWindow.IntersectionObserver !== 'function') {
332
+ return;
333
+ }
334
+ thumbnailObserver = new targetWindow.IntersectionObserver(entries => {
335
+ entries.forEach(entry => {
336
+ if (!entry.isIntersecting) {
337
+ return;
338
+ }
339
+ const targetElement = entry.target;
340
+ const pageNumber = Number(targetElement.dataset.pdfThumbnailPage || '0');
341
+ thumbnailObserver === null || thumbnailObserver === void 0 ? void 0 : thumbnailObserver.unobserve(targetElement);
342
+ if (pageNumber > 0) {
343
+ void renderPdfThumbnail(pageNumber);
344
+ }
345
+ });
346
+ }, {
347
+ root: navList,
348
+ rootMargin: '96px 0px',
349
+ });
350
+ };
351
+ const queuePdfThumbnail = (pageNumber, thumb) => {
352
+ if (paintPdfThumbnail(pageNumber, thumb)) {
353
+ return;
354
+ }
355
+ ensureThumbnailObserver();
356
+ if (thumbnailObserver) {
357
+ thumbnailObserver.observe(thumb);
358
+ return;
359
+ }
360
+ void renderPdfThumbnail(pageNumber);
361
+ };
256
362
  const syncUi = () => {
257
363
  root.classList.toggle('pdf-shell--nav-hidden', !navigationEnabled || !navVisible);
258
364
  root.classList.toggle('pdf-shell--toolbar-hidden', !toolbarVisible);
@@ -278,6 +384,33 @@ export default async function renderPdf(buffer, target, context) {
278
384
  stateNode.textContent = loadStatus === 'error' ? errorMessage : '正在加载 PDF...';
279
385
  renderNavList();
280
386
  };
387
+ const writeLegacyCompatiblePageDimensions = () => {
388
+ var _a, _b;
389
+ const pdfViewer = pdfContext.viewer;
390
+ if (!pdfViewer) {
391
+ return;
392
+ }
393
+ const totalPages = pageCount || pdfViewer.pagesCount || 0;
394
+ for (let index = 0; index < totalPages; index += 1) {
395
+ const pageView = pdfViewer.getPageView(index);
396
+ const pageElement = (pageView === null || pageView === void 0 ? void 0 : pageView.div) ||
397
+ pdfViewerRoot.querySelector(`.page[data-page-number="${index + 1}"]`);
398
+ const width = (_a = pageView === null || pageView === void 0 ? void 0 : pageView.viewport) === null || _a === void 0 ? void 0 : _a.width;
399
+ const height = (_b = pageView === null || pageView === void 0 ? void 0 : pageView.viewport) === null || _b === void 0 ? void 0 : _b.height;
400
+ if (!pageElement || !Number.isFinite(width) || !Number.isFinite(height)) {
401
+ continue;
402
+ }
403
+ pageElement.style.setProperty('width', `${Math.max(1, Math.round(width || 0))}px`, 'important');
404
+ pageElement.style.setProperty('height', `${Math.max(1, Math.round(height || 0))}px`, 'important');
405
+ }
406
+ };
407
+ const scheduleLegacyPageDimensionPatch = () => {
408
+ targetWindow.cancelAnimationFrame(pageDimensionFrame);
409
+ pageDimensionFrame = targetWindow.requestAnimationFrame(() => {
410
+ writeLegacyCompatiblePageDimensions();
411
+ targetWindow.requestAnimationFrame(writeLegacyCompatiblePageDimensions);
412
+ });
413
+ };
281
414
  const createPdfWorker = () => {
282
415
  if (!(targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.Worker)) {
283
416
  return null;
@@ -435,6 +568,7 @@ export default async function renderPdf(buffer, target, context) {
435
568
  const normalizedScale = clampScale(scale);
436
569
  pdfContext.viewer.currentScale = normalizedScale;
437
570
  currentScale = normalizedScale;
571
+ scheduleLegacyPageDimensionPatch();
438
572
  zoomEmitter.emit();
439
573
  syncUi();
440
574
  };
@@ -489,6 +623,8 @@ export default async function renderPdf(buffer, target, context) {
489
623
  const applyRotation = (rotation) => {
490
624
  const normalized = normalizeRotation(rotation);
491
625
  currentRotation = normalized;
626
+ pdfThumbnails.clear();
627
+ pendingPdfThumbnails.clear();
492
628
  if (!pdfContext.viewer) {
493
629
  syncUi();
494
630
  return;
@@ -501,6 +637,7 @@ export default async function renderPdf(buffer, target, context) {
501
637
  return;
502
638
  }
503
639
  (_a = pdfContext.viewer) === null || _a === void 0 ? void 0 : _a.update();
640
+ scheduleLegacyPageDimensionPatch();
504
641
  syncUi();
505
642
  });
506
643
  };
@@ -669,6 +806,9 @@ export default async function renderPdf(buffer, target, context) {
669
806
  errorMessage = '';
670
807
  pdfContext.document = null;
671
808
  outlineItems = [];
809
+ pdfThumbnails.clear();
810
+ pendingPdfThumbnails.clear();
811
+ thumbnailObserver === null || thumbnailObserver === void 0 ? void 0 : thumbnailObserver.disconnect();
672
812
  (_a = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _a === void 0 ? void 0 : _a.call(context, null);
673
813
  syncUi();
674
814
  let resource = null;
@@ -702,6 +842,7 @@ export default async function renderPdf(buffer, target, context) {
702
842
  var _a;
703
843
  applyRotation(currentRotation);
704
844
  fitToWidth();
845
+ scheduleLegacyPageDimensionPatch();
705
846
  loadStatus = 'ready';
706
847
  syncUi();
707
848
  (_a = context === null || context === void 0 ? void 0 : context.onProgressiveRender) === null || _a === void 0 ? void 0 : _a.call(context);
@@ -715,13 +856,16 @@ export default async function renderPdf(buffer, target, context) {
715
856
  });
716
857
  eventBus.on('scalechanging', ({ scale }) => {
717
858
  currentScale = clampScale(scale);
859
+ scheduleLegacyPageDimensionPatch();
718
860
  zoomEmitter.emit();
719
861
  syncUi();
720
862
  });
863
+ eventBus.on('pagerendered', scheduleLegacyPageDimensionPatch);
721
864
  if (!(context === null || context === void 0 ? void 0 : context.streamUrl) && !buffer.byteLength) {
722
865
  throw new Error('PDF 缺少可读取的数据源');
723
866
  }
724
867
  const worker = createPdfWorker();
868
+ const pdfAssets = resolveFileViewerPdfAssetUrls(options, documentRef.URL || documentRef.baseURI);
725
869
  const source = (context === null || context === void 0 ? void 0 : context.streamUrl)
726
870
  ? {
727
871
  url: context.streamUrl,
@@ -734,8 +878,9 @@ export default async function renderPdf(buffer, target, context) {
734
878
  const loadingTask = getDocument({
735
879
  ...source,
736
880
  worker: worker || undefined,
737
- cMapUrl: `https://npm.onmicrosoft.cn/pdfjs-dist@${version}/cmaps/`,
738
- wasmUrl: `https://npm.onmicrosoft.cn/pdfjs-dist@${version}/wasm/`,
881
+ cMapUrl: pdfAssets.cMapUrl,
882
+ wasmUrl: pdfAssets.wasmUrl,
883
+ standardFontDataUrl: pdfAssets.standardFontDataUrl,
739
884
  useWorkerFetch: true,
740
885
  cMapPacked: true,
741
886
  enableXfa: true,
@@ -831,6 +976,9 @@ export default async function renderPdf(buffer, target, context) {
831
976
  destroyed = true;
832
977
  loadVersion += 1;
833
978
  targetWindow.cancelAnimationFrame(fitFrame);
979
+ targetWindow.cancelAnimationFrame(pageDimensionFrame);
980
+ thumbnailObserver === null || thumbnailObserver === void 0 ? void 0 : thumbnailObserver.disconnect();
981
+ thumbnailObserver = null;
834
982
  resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.disconnect();
835
983
  resizeObserver = null;
836
984
  unregisterFileViewerSearchProvider(root);
@@ -21,11 +21,6 @@ const typstStyle = `
21
21
  .typst-loading p{margin:0;color:#6a778b;font-size:13px}
22
22
  .typst-error{color:#9f1d1d}
23
23
  .typst-error pre{max-height:360px;margin:14px 0 0;overflow:auto;border-radius:10px;background:#fff1f2;color:#9f1d1d;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono',monospace;font-size:12px;line-height:1.7;padding:14px;white-space:pre-wrap}
24
- .typst-source-fallback{box-sizing:border-box;width:min(1040px,calc(100% - 32px));margin:28px auto 44px;border:1px solid rgba(20,35,53,.1);border-radius:16px;background:#fff;box-shadow:0 18px 44px rgba(15,23,42,.14);overflow:hidden}
25
- .typst-source-fallback header{padding:18px 20px;border-bottom:1px solid rgba(120,134,155,.18);background:linear-gradient(135deg,#f0fdf4,#eff6ff)}
26
- .typst-source-fallback strong{display:block;color:#172033;font-size:15px;font-weight:850}
27
- .typst-source-fallback p{margin:6px 0 0;color:#5f6e84;font-size:13px;line-height:1.7}
28
- .typst-source-fallback pre{box-sizing:border-box;max-height:none;margin:0;overflow:auto;background:#0f172a;color:#e2e8f0;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono',monospace;font-size:13px;line-height:1.75;padding:20px;tab-size:2;white-space:pre}
29
24
  .file-viewer[data-viewer-theme='dark'] .typst-viewer{background:#101820;color:#e6edf3}
30
25
  .file-viewer[data-viewer-theme='dark'] .typst-toolbar{border-bottom-color:rgba(139,148,158,.22);background:rgba(15,23,42,.9)}
31
26
  .file-viewer[data-viewer-theme='dark'] .typst-toolbar strong{color:#f8fafc}
@@ -33,19 +28,15 @@ const typstStyle = `
33
28
  .file-viewer[data-viewer-theme='dark'] .typst-page-shell{border-color:rgba(139,148,158,.26);box-shadow:0 24px 56px rgba(0,0,0,.38)}
34
29
  .file-viewer[data-viewer-theme='dark'] .typst-loading,.file-viewer[data-viewer-theme='dark'] .typst-error{border-color:rgba(139,148,158,.22);background:#151b23;box-shadow:0 24px 56px rgba(0,0,0,.32)}
35
30
  .file-viewer[data-viewer-theme='dark'] .typst-loading strong,.file-viewer[data-viewer-theme='dark'] .typst-error strong{color:#f8fafc}
36
- .file-viewer[data-viewer-theme='dark'] .typst-source-fallback{border-color:rgba(139,148,158,.26);background:#151b23;box-shadow:0 24px 56px rgba(0,0,0,.32)}
37
- .file-viewer[data-viewer-theme='dark'] .typst-source-fallback header{border-bottom-color:rgba(139,148,158,.22);background:linear-gradient(135deg,rgba(16,185,129,.18),rgba(59,130,246,.16))}
38
- .file-viewer[data-viewer-theme='dark'] .typst-source-fallback strong{color:#f8fafc}
39
- .file-viewer[data-viewer-theme='dark'] .typst-source-fallback p{color:#9aa7b8}
40
31
  @keyframes typst-spin{to{transform:rotate(360deg)}}
41
32
  @media (max-width:767px){.typst-toolbar{align-items:flex-start;flex-direction:column;gap:4px}.typst-pages{gap:16px;padding:16px 10px 28px}}
42
- @media (prefers-color-scheme:dark){.file-viewer[data-viewer-theme='system'] .typst-viewer{background:#101820;color:#e6edf3}.file-viewer[data-viewer-theme='system'] .typst-toolbar{border-bottom-color:rgba(139,148,158,.22);background:rgba(15,23,42,.9)}.file-viewer[data-viewer-theme='system'] .typst-toolbar strong{color:#f8fafc}.file-viewer[data-viewer-theme='system'] .typst-toolbar span,.file-viewer[data-viewer-theme='system'] .typst-toolbar em{color:#9aa7b8}.file-viewer[data-viewer-theme='system'] .typst-page-shell{border-color:rgba(139,148,158,.26);box-shadow:0 24px 56px rgba(0,0,0,.38)}.file-viewer[data-viewer-theme='system'] .typst-loading,.file-viewer[data-viewer-theme='system'] .typst-error{border-color:rgba(139,148,158,.22);background:#151b23;box-shadow:0 24px 56px rgba(0,0,0,.32)}.file-viewer[data-viewer-theme='system'] .typst-loading strong,.file-viewer[data-viewer-theme='system'] .typst-error strong{color:#f8fafc}.file-viewer[data-viewer-theme='system'] .typst-source-fallback{border-color:rgba(139,148,158,.26);background:#151b23;box-shadow:0 24px 56px rgba(0,0,0,.32)}.file-viewer[data-viewer-theme='system'] .typst-source-fallback header{border-bottom-color:rgba(139,148,158,.22);background:linear-gradient(135deg,rgba(16,185,129,.18),rgba(59,130,246,.16))}.file-viewer[data-viewer-theme='system'] .typst-source-fallback strong{color:#f8fafc}.file-viewer[data-viewer-theme='system'] .typst-source-fallback p{color:#9aa7b8}}
33
+ @media (prefers-color-scheme:dark){.file-viewer[data-viewer-theme='system'] .typst-viewer{background:#101820;color:#e6edf3}.file-viewer[data-viewer-theme='system'] .typst-toolbar{border-bottom-color:rgba(139,148,158,.22);background:rgba(15,23,42,.9)}.file-viewer[data-viewer-theme='system'] .typst-toolbar strong{color:#f8fafc}.file-viewer[data-viewer-theme='system'] .typst-toolbar span,.file-viewer[data-viewer-theme='system'] .typst-toolbar em{color:#9aa7b8}.file-viewer[data-viewer-theme='system'] .typst-page-shell{border-color:rgba(139,148,158,.26);box-shadow:0 24px 56px rgba(0,0,0,.38)}.file-viewer[data-viewer-theme='system'] .typst-loading,.file-viewer[data-viewer-theme='system'] .typst-error{border-color:rgba(139,148,158,.22);background:#151b23;box-shadow:0 24px 56px rgba(0,0,0,.32)}.file-viewer[data-viewer-theme='system'] .typst-loading strong,.file-viewer[data-viewer-theme='system'] .typst-error strong{color:#f8fafc}}
43
34
  `;
44
35
  let typstEngineConfigKey = '';
45
- const DEFAULT_TYPST_RENDER_TIMEOUT_MS = 20000;
36
+ const DEFAULT_TYPST_RENDER_TIMEOUT_MS = 60000;
46
37
  class TypstRenderTimeoutError extends Error {
47
38
  constructor(timeoutMs) {
48
- super(`Typst 编译超过 ${Math.round(timeoutMs / 1000)} 秒,已切换为源码预览`);
39
+ super(`Typst 编译超过 ${Math.round(timeoutMs / 1000)} 秒`);
49
40
  this.name = 'TypstRenderTimeoutError';
50
41
  }
51
42
  }
@@ -70,15 +61,7 @@ const getWindowOverride = (key) => {
70
61
  }
71
62
  return window[key];
72
63
  };
73
- const configureTypstEngine = (context, documentBaseUrl) => {
74
- var _a;
75
- const typstOptions = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.typst;
76
- const compilerWasmUrl = resolveFileViewerTypstCompilerWasmUrl(typstOptions, [
77
- getWindowOverride('__FLYFISH_TYPST_COMPILER_WASM_URL__'),
78
- ], documentBaseUrl);
79
- const rendererWasmUrl = resolveFileViewerTypstRendererWasmUrl(typstOptions, [
80
- getWindowOverride('__FLYFISH_TYPST_RENDERER_WASM_URL__'),
81
- ], documentBaseUrl);
64
+ const configureTypstEngine = (compilerWasmUrl, rendererWasmUrl) => {
82
65
  const configKey = `${compilerWasmUrl}\n${rendererWasmUrl}`;
83
66
  if (typstEngineConfigKey === configKey) {
84
67
  return;
@@ -91,6 +74,69 @@ const configureTypstEngine = (context, documentBaseUrl) => {
91
74
  });
92
75
  typstEngineConfigKey = configKey;
93
76
  };
77
+ const pushUniqueTypstCandidate = (candidates, candidate) => {
78
+ if (candidates.some(item => item.compilerWasmUrl === candidate.compilerWasmUrl &&
79
+ item.rendererWasmUrl === candidate.rendererWasmUrl)) {
80
+ return;
81
+ }
82
+ candidates.push(candidate);
83
+ };
84
+ const resolveTypstEngineCandidates = (context, documentBaseUrl) => {
85
+ var _a;
86
+ const typstOptions = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.typst;
87
+ const compilerOverride = getWindowOverride('__FLYFISH_TYPST_COMPILER_WASM_URL__');
88
+ const rendererOverride = getWindowOverride('__FLYFISH_TYPST_RENDERER_WASM_URL__');
89
+ const compilerWasmUrl = resolveFileViewerTypstCompilerWasmUrl(typstOptions, [
90
+ compilerOverride,
91
+ ], documentBaseUrl);
92
+ const rendererWasmUrl = resolveFileViewerTypstRendererWasmUrl(typstOptions, [
93
+ rendererOverride,
94
+ ], documentBaseUrl);
95
+ const hasConfiguredAsset = Boolean((typstOptions === null || typstOptions === void 0 ? void 0 : typstOptions.compilerWasmUrl) ||
96
+ (typstOptions === null || typstOptions === void 0 ? void 0 : typstOptions.rendererWasmUrl) ||
97
+ compilerOverride ||
98
+ rendererOverride);
99
+ const candidates = [];
100
+ pushUniqueTypstCandidate(candidates, {
101
+ compilerWasmUrl,
102
+ rendererWasmUrl,
103
+ source: hasConfiguredAsset ? 'configured' : 'local',
104
+ preflight: !hasConfiguredAsset,
105
+ });
106
+ return candidates;
107
+ };
108
+ const isHttpUrl = (url) => /^https?:\/\//i.test(url);
109
+ const isKnownMissingWasmUrl = async (url) => {
110
+ if (typeof fetch !== 'function' || !isHttpUrl(url)) {
111
+ return false;
112
+ }
113
+ try {
114
+ const response = await fetch(url, {
115
+ cache: 'force-cache',
116
+ method: 'HEAD',
117
+ });
118
+ return response.status === 404 || response.status === 410;
119
+ }
120
+ catch {
121
+ return false;
122
+ }
123
+ };
124
+ const resolveKnownMissingTypstAsset = async (candidate) => {
125
+ if (await isKnownMissingWasmUrl(candidate.compilerWasmUrl)) {
126
+ return `Typst compiler WASM missing: ${candidate.compilerWasmUrl}`;
127
+ }
128
+ if (await isKnownMissingWasmUrl(candidate.rendererWasmUrl)) {
129
+ return `Typst renderer WASM missing: ${candidate.rendererWasmUrl}`;
130
+ }
131
+ return '';
132
+ };
133
+ const isTypstAssetLoadError = (error) => {
134
+ if (Array.isArray(error)) {
135
+ return false;
136
+ }
137
+ const message = error instanceof Error ? error.message : String(error);
138
+ return /wasm|webassembly|fetch|module|instantiate|compile|network|404|410/i.test(message);
139
+ };
94
140
  const escapeAttribute = (value) => {
95
141
  return value.replace(/[&<>"']/g, char => {
96
142
  const entities = {
@@ -184,6 +230,22 @@ const formatTypstError = (error) => {
184
230
  }
185
231
  return String(error);
186
232
  };
233
+ const formatTypstRuntimeError = (error) => {
234
+ const message = formatTypstError(error);
235
+ if (error instanceof TypstRenderTimeoutError) {
236
+ return [
237
+ message,
238
+ '请检查 Typst 源文件复杂度,或通过 options.typst.renderTimeoutMs 调大浏览器端编译超时。'
239
+ ].join('\n\n');
240
+ }
241
+ if (isTypstAssetLoadError(error)) {
242
+ return [
243
+ message,
244
+ 'Typst 需要本地 compiler / renderer WASM。请运行 file-viewer-copy-assets,或配置 options.typst.compilerWasmUrl / options.typst.rendererWasmUrl,并确认服务器以 application/wasm 返回资源。'
245
+ ].join('\n\n');
246
+ }
247
+ return message;
248
+ };
187
249
  const clampZoom = (value) => {
188
250
  return Math.min(3, Math.max(0.3, Number(value.toFixed(2))));
189
251
  };
@@ -308,7 +370,6 @@ export default async function renderTypst(buffer, target, _type, context) {
308
370
  let state = 'loading';
309
371
  let pages = [];
310
372
  let errorMessage = '';
311
- let sourceFallbackMessage = '';
312
373
  let zoom = 1;
313
374
  let renderToken = 0;
314
375
  let disposed = false;
@@ -374,16 +435,6 @@ export default async function renderTypst(buffer, target, _type, context) {
374
435
  error.append(createElement(documentRef, 'strong', undefined, 'Typst 渲染失败'), createElement(documentRef, 'pre', undefined, errorMessage));
375
436
  body.replaceChildren(error);
376
437
  };
377
- const renderSourceFallback = () => {
378
- const fallback = createElement(documentRef, 'main', 'typst-source-fallback');
379
- fallback.setAttribute('aria-label', 'Typst source preview');
380
- const header = createElement(documentRef, 'header');
381
- header.append(createElement(documentRef, 'strong', undefined, '已切换为 Typst 源码预览'), createElement(documentRef, 'p', undefined, sourceFallbackMessage || '当前浏览器没有在预期时间内完成 Typst 编译,源码仍可完整查看。'));
382
- const pre = createElement(documentRef, 'pre');
383
- pre.textContent = source;
384
- fallback.append(header, pre);
385
- body.replaceChildren(fallback);
386
- };
387
438
  const renderPages = () => {
388
439
  pageShells.clear();
389
440
  const pagesRoot = createElement(documentRef, 'main', 'typst-pages');
@@ -403,49 +454,66 @@ export default async function renderTypst(buffer, target, _type, context) {
403
454
  const syncUi = () => {
404
455
  summary.textContent = state === 'ready'
405
456
  ? getPageSummary(pages)
406
- : state === 'source'
407
- ? 'Typst source preview'
408
- : 'Typst WASM renderer';
457
+ : 'Typst WASM renderer';
409
458
  status.textContent = state === 'loading'
410
459
  ? '正在编译'
411
460
  : state === 'error'
412
461
  ? '编译失败'
413
- : state === 'source'
414
- ? '源码预览'
415
- : '已渲染';
462
+ : '已渲染';
416
463
  if (state === 'loading') {
417
464
  renderLoading();
418
465
  }
419
466
  else if (state === 'error') {
420
467
  renderError();
421
468
  }
422
- else if (state === 'source') {
423
- renderSourceFallback();
424
- }
425
469
  else {
426
470
  renderPages();
427
471
  }
428
472
  };
473
+ const renderTypstSvg = async () => {
474
+ var _a, _b;
475
+ const candidates = resolveTypstEngineCandidates(context, documentRef.baseURI);
476
+ const timeoutMs = normalizeRenderTimeoutMs((_b = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.typst) === null || _b === void 0 ? void 0 : _b.renderTimeoutMs);
477
+ let lastError;
478
+ for (const candidate of candidates) {
479
+ const missingAsset = candidate.preflight
480
+ ? await resolveKnownMissingTypstAsset(candidate)
481
+ : '';
482
+ if (missingAsset) {
483
+ lastError = new Error(missingAsset);
484
+ continue;
485
+ }
486
+ try {
487
+ configureTypstEngine(candidate.compilerWasmUrl, candidate.rendererWasmUrl);
488
+ return await withRenderTimeout($typst.svg({
489
+ mainContent: source,
490
+ data_selection: {
491
+ body: true,
492
+ defs: true,
493
+ css: true,
494
+ js: false,
495
+ },
496
+ }), timeoutMs);
497
+ }
498
+ catch (error) {
499
+ lastError = error;
500
+ if (error instanceof TypstRenderTimeoutError || !isTypstAssetLoadError(error)) {
501
+ throw error;
502
+ }
503
+ }
504
+ }
505
+ throw lastError instanceof Error ? lastError : new Error('Typst WASM 加载失败');
506
+ };
429
507
  const render = async () => {
430
- var _a, _b, _c, _d, _e;
508
+ var _a, _b;
431
509
  const token = ++renderToken;
432
510
  state = 'loading';
433
511
  errorMessage = '';
434
- sourceFallbackMessage = '';
435
512
  pages = [];
436
513
  (_a = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _a === void 0 ? void 0 : _a.call(context, null);
437
514
  syncUi();
438
515
  try {
439
- configureTypstEngine(context, documentRef.baseURI);
440
- const svg = await withRenderTimeout($typst.svg({
441
- mainContent: source,
442
- data_selection: {
443
- body: true,
444
- defs: true,
445
- css: true,
446
- js: false,
447
- },
448
- }), normalizeRenderTimeoutMs((_c = (_b = context === null || context === void 0 ? void 0 : context.options) === null || _b === void 0 ? void 0 : _b.typst) === null || _c === void 0 ? void 0 : _c.renderTimeoutMs));
516
+ const svg = await renderTypstSvg();
449
517
  if (disposed || token !== renderToken) {
450
518
  return;
451
519
  }
@@ -453,20 +521,13 @@ export default async function renderTypst(buffer, target, _type, context) {
453
521
  state = 'ready';
454
522
  syncUi();
455
523
  registerExportAdapter();
456
- (_d = context === null || context === void 0 ? void 0 : context.onProgressiveRender) === null || _d === void 0 ? void 0 : _d.call(context);
524
+ (_b = context === null || context === void 0 ? void 0 : context.onProgressiveRender) === null || _b === void 0 ? void 0 : _b.call(context);
457
525
  }
458
526
  catch (error) {
459
527
  if (disposed || token !== renderToken) {
460
528
  return;
461
529
  }
462
- if (error instanceof TypstRenderTimeoutError) {
463
- sourceFallbackMessage = error.message;
464
- state = 'source';
465
- syncUi();
466
- (_e = context === null || context === void 0 ? void 0 : context.onProgressiveRender) === null || _e === void 0 ? void 0 : _e.call(context);
467
- return;
468
- }
469
- errorMessage = formatTypstError(error);
530
+ errorMessage = formatTypstRuntimeError(error);
470
531
  state = 'error';
471
532
  syncUi();
472
533
  }