@skyramp/skyramp 1.3.10 → 1.3.11

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,1349 @@
1
+ /**
2
+ * Copyright (c) Skyramp Corporation.
3
+ *
4
+ * Bundled PDF.js viewer for execution-time injection
5
+ * This is a self-contained JavaScript string that can be injected via page.addInitScript()
6
+ *
7
+ * AUTO-GENERATED from Playwright sources by scripts/build-pdf-bundle.js
8
+ * DO NOT EDIT THIS FILE DIRECTLY - edit pdfJsViewer.ts instead and regenerate
9
+ *
10
+ * Source files:
11
+ * - PdfJsViewer class: /Users/chelfim/code/playwright/packages/injected/src/recorder/skyramp/pdfJsViewer.ts
12
+ * - Execution helpers: /Users/chelfim/Desktop/code/skyramp/libs/npm/scripts/pdf-execution-helpers.js
13
+ */
14
+
15
+ /**
16
+ * Self-contained PDF viewer injection script
17
+ * This script will be injected into pages during test execution to replace Chrome's PDF plugin with PDF.js
18
+ */
19
+ const PDF_VIEWER_INJECTION_SCRIPT = `
20
+ (function() {
21
+ 'use strict';
22
+
23
+ console.log('[PW-PDF] PDF viewer injection script initializing...');
24
+
25
+ // =============================================================================
26
+ // PDF.js Viewer Class (auto-generated from pdfJsViewer.ts)
27
+ // =============================================================================
28
+
29
+ class PdfJsViewer {
30
+ constructor(container) {
31
+ this._pdfDoc = null;
32
+ this._canvasElements = [];
33
+ this._thumbnailElements = [];
34
+ this._isRendering = false;
35
+ this._toolbar = null;
36
+ this._sidebar = null;
37
+ this._mainContent = null;
38
+ this._currentPage = 1;
39
+ this._pageDisplay = null;
40
+ this._zoomDisplay = null;
41
+ this._currentZoom = 1.0; // 100%
42
+ this._rotation = 0; // 0, 90, 180, 270 degrees
43
+ this._pdfDataUrl = ''; // Store for download
44
+ this._filename = 'Document.pdf';
45
+ this._moreMenuElement = null;
46
+ this._moreMenuCleanup = null;
47
+ this._twoPageView = false;
48
+ this._annotationsVisible = true;
49
+ this._container = container;
50
+ }
51
+ /**
52
+ * Loads PDF.js library from CDN if not already loaded
53
+ */
54
+ static async loadPdfJs() {
55
+ if (window.pdfjsLib) {
56
+ return; // Already loaded
57
+ }
58
+ console.log('[PDF.js] Loading PDF.js library from CDN...');
59
+ // Load PDF.js from CDN
60
+ const script = document.createElement('script');
61
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
62
+ return new Promise((resolve, reject) => {
63
+ script.onload = () => {
64
+ if (!window.pdfjsLib) {
65
+ reject(new Error('PDF.js loaded but pdfjsLib not available'));
66
+ return;
67
+ }
68
+ // Configure worker
69
+ window.pdfjsLib.GlobalWorkerOptions.workerSrc =
70
+ 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
71
+ console.log('[PDF.js] ✅ PDF.js library loaded successfully');
72
+ resolve();
73
+ };
74
+ script.onerror = () => reject(new Error('Failed to load PDF.js from CDN'));
75
+ document.head.appendChild(script);
76
+ });
77
+ }
78
+ /**
79
+ * Renders a PDF from a data URL
80
+ */
81
+ async renderPdf(config) {
82
+ const { pdfDataUrl, filename, onReady, onError } = config;
83
+ try {
84
+ // Store for later use (download, etc.)
85
+ this._pdfDataUrl = pdfDataUrl;
86
+ this._filename = filename || 'Document.pdf';
87
+ // Ensure PDF.js is loaded
88
+ await PdfJsViewer.loadPdfJs();
89
+ console.log('[PDF.js] Rendering PDF...');
90
+ this._isRendering = true;
91
+ // Load the PDF document
92
+ const loadingTask = window.pdfjsLib.getDocument(pdfDataUrl);
93
+ this._pdfDoc = await loadingTask.promise;
94
+ console.log(\`[PDF.js] PDF loaded: \${this._pdfDoc.numPages} pages\`);
95
+ // Let the body grow with the PDF content so Playwright's fullPage screenshot
96
+ // can capture all pages by scrolling the document (not an inner div).
97
+ if (document.documentElement) {
98
+ document.documentElement.style.height = 'auto';
99
+ }
100
+ if (document.body) {
101
+ document.body.style.margin = '0';
102
+ document.body.style.padding = '0';
103
+ document.body.style.width = '100%';
104
+ document.body.style.height = 'auto';
105
+ document.body.style.overflow = 'auto';
106
+ }
107
+ // Clear container
108
+ this._container.innerHTML = '';
109
+ this._canvasElements = [];
110
+ this._thumbnailElements = [];
111
+ // Container in normal document flow so all pages contribute to scroll height
112
+ this._container.style.cssText = \`
113
+ width: 100%;
114
+ min-height: 100vh;
115
+ display: flex;
116
+ flex-direction: column;
117
+ background-color: #525252;
118
+ margin: 0;
119
+ padding: 0;
120
+ \`;
121
+ // Create toolbar at top (match Chrome PDF viewer toolbar exactly)
122
+ this._toolbar = document.createElement('div');
123
+ this._toolbar.style.cssText = \`
124
+ width: 100%;
125
+ height: 56px;
126
+ background-color: #4a4a4a;
127
+ color: #e8eaed;
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: space-between;
131
+ padding: 0 8px;
132
+ box-sizing: border-box;
133
+ font-family: 'Roboto', Arial, sans-serif;
134
+ font-size: 14px;
135
+ flex-shrink: 0;
136
+ border-bottom: 1px solid #2a2a2a;
137
+ position: sticky;
138
+ top: 0;
139
+ z-index: 10;
140
+ \`;
141
+ // Left section: menu + filename
142
+ const leftSection = document.createElement('div');
143
+ leftSection.style.cssText = 'display: flex; align-items: center; gap: 12px;';
144
+ // Menu button (hamburger)
145
+ const menuBtn = this._createToolbarButton('≡', 'Menu', () => {
146
+ // Toggle sidebar visibility
147
+ if (this._sidebar) {
148
+ const isHidden = this._sidebar.style.display === 'none';
149
+ this._sidebar.style.display = isHidden ? 'block' : 'none';
150
+ }
151
+ });
152
+ menuBtn.style.fontSize = '24px';
153
+ leftSection.appendChild(menuBtn);
154
+ // Filename
155
+ const filenameDisplay = document.createElement('div');
156
+ filenameDisplay.textContent = filename || 'Document.pdf';
157
+ filenameDisplay.style.cssText = \`
158
+ color: #e8eaed;
159
+ font-size: 14px;
160
+ font-weight: 400;
161
+ margin-left: 4px;
162
+ \`;
163
+ leftSection.appendChild(filenameDisplay);
164
+ // Center section: page navigation + zoom controls
165
+ const centerSection = document.createElement('div');
166
+ centerSection.style.cssText = 'display: flex; align-items: center; gap: 12px;';
167
+ // Page navigation
168
+ const pageNav = document.createElement('div');
169
+ pageNav.style.cssText = 'display: flex; align-items: center; gap: 8px;';
170
+ const pageDisplay = document.createElement('span');
171
+ pageDisplay.textContent = \`1 / \${this._pdfDoc.numPages}\`;
172
+ pageDisplay.style.cssText = 'color: #e8eaed; font-size: 13px; min-width: 50px; text-align: center;';
173
+ pageNav.appendChild(pageDisplay);
174
+ centerSection.appendChild(pageNav);
175
+ // Divider
176
+ const divider1 = document.createElement('div');
177
+ divider1.style.cssText = 'width: 1px; height: 24px; background-color: #5f5f5f;';
178
+ centerSection.appendChild(divider1);
179
+ // Zoom controls
180
+ const zoomControls = document.createElement('div');
181
+ zoomControls.style.cssText = 'display: flex; align-items: center; gap: 8px;';
182
+ const zoomOutBtn = this._createToolbarButton('−', 'Zoom out', () => {
183
+ this._zoom(this._currentZoom - 0.1);
184
+ });
185
+ zoomControls.appendChild(zoomOutBtn);
186
+ const zoomDisplay = document.createElement('span');
187
+ zoomDisplay.textContent = '100%';
188
+ zoomDisplay.style.cssText = 'color: #e8eaed; font-size: 13px; min-width: 45px; text-align: center; cursor: pointer;';
189
+ zoomDisplay.title = 'Reset zoom to 100%';
190
+ zoomDisplay.addEventListener('click', () => {
191
+ this._zoom(1.0);
192
+ });
193
+ zoomControls.appendChild(zoomDisplay);
194
+ const zoomInBtn = this._createToolbarButton('+', 'Zoom in', () => {
195
+ this._zoom(this._currentZoom + 0.1);
196
+ });
197
+ zoomControls.appendChild(zoomInBtn);
198
+ centerSection.appendChild(zoomControls);
199
+ // Divider
200
+ const divider2 = document.createElement('div');
201
+ divider2.style.cssText = 'width: 1px; height: 24px; background-color: #5f5f5f;';
202
+ centerSection.appendChild(divider2);
203
+ // Fit to page button
204
+ const fitBtn = this._createToolbarButton('⊡', 'Fit to page', () => {
205
+ this._fitToPage();
206
+ });
207
+ fitBtn.style.fontSize = '18px';
208
+ centerSection.appendChild(fitBtn);
209
+ // Rotate button
210
+ const rotateBtn = this._createToolbarButton('↻', 'Rotate clockwise', () => {
211
+ this._rotate();
212
+ });
213
+ rotateBtn.style.fontSize = '18px';
214
+ centerSection.appendChild(rotateBtn);
215
+ // Right section: download + print + more
216
+ const rightSection = document.createElement('div');
217
+ rightSection.style.cssText = 'display: flex; align-items: center; gap: 8px;';
218
+ // Download button
219
+ const downloadBtn = this._createToolbarButton('⬇', 'Download', () => {
220
+ this._download();
221
+ });
222
+ downloadBtn.style.fontSize = '18px';
223
+ rightSection.appendChild(downloadBtn);
224
+ // Print button
225
+ const printBtn = this._createToolbarButton('🖨', 'Print', () => {
226
+ this._print();
227
+ });
228
+ printBtn.style.fontSize = '16px';
229
+ rightSection.appendChild(printBtn);
230
+ // More options button
231
+ let moreBtn;
232
+ moreBtn = this._createToolbarButton('⋮', 'More options', () => {
233
+ this._toggleMoreMenu(moreBtn);
234
+ });
235
+ moreBtn.style.fontSize = '20px';
236
+ rightSection.appendChild(moreBtn);
237
+ // Assemble toolbar
238
+ this._toolbar.appendChild(leftSection);
239
+ this._toolbar.appendChild(centerSection);
240
+ this._toolbar.appendChild(rightSection);
241
+ // Store references for updates
242
+ this._pageDisplay = pageDisplay;
243
+ this._zoomDisplay = zoomDisplay;
244
+ // Create content wrapper for sidebar and main content
245
+ const contentWrapper = document.createElement('div');
246
+ contentWrapper.style.cssText = \`
247
+ width: 100%;
248
+ flex: 1;
249
+ display: flex;
250
+ \`;
251
+ // Sidebar sticks to the left as the document scrolls
252
+ this._sidebar = document.createElement('div');
253
+ this._sidebar.style.cssText = \`
254
+ width: 294px;
255
+ height: calc(100vh - 56px);
256
+ overflow-y: auto;
257
+ overflow-x: hidden;
258
+ background-color: #3f3f3f;
259
+ border-right: 1px solid #2a2a2a;
260
+ padding: 20px 35px 20px 70px;
261
+ box-sizing: border-box;
262
+ flex-shrink: 0;
263
+ position: sticky;
264
+ top: 56px;
265
+ align-self: flex-start;
266
+ \`;
267
+ // Main content flows in the document — all pages contribute to scroll height
268
+ this._mainContent = document.createElement('div');
269
+ this._mainContent.style.cssText = \`
270
+ flex: 1;
271
+ overflow: visible;
272
+ background-color: #525252;
273
+ position: relative;
274
+ padding: 0;
275
+ box-sizing: border-box;
276
+ \`;
277
+ this._container.appendChild(this._toolbar);
278
+ contentWrapper.appendChild(this._sidebar);
279
+ contentWrapper.appendChild(this._mainContent);
280
+ this._container.appendChild(contentWrapper);
281
+ // Render all pages and thumbnails
282
+ for (let pageNum = 1; pageNum <= this._pdfDoc.numPages; pageNum++) {
283
+ await this._renderPage(pageNum);
284
+ await this._renderThumbnail(pageNum);
285
+ }
286
+ // Setup scroll sync
287
+ this._setupScrollSync();
288
+ this._isRendering = false;
289
+ console.log('[PDF.js] ✅ All pages rendered successfully');
290
+ if (onReady) {
291
+ onReady();
292
+ }
293
+ }
294
+ catch (error) {
295
+ this._isRendering = false;
296
+ console.error('[PDF.js] ❌ Failed to render PDF:', error);
297
+ if (onError) {
298
+ onError(error);
299
+ }
300
+ }
301
+ }
302
+ /**
303
+ * Injects the minimal CSS required by PDF.js renderTextLayer (once per document).
304
+ * PDF.js relies on external CSS for \`position: absolute\` and \`transform-origin\` on
305
+ * text layer spans — without it the spans are in normal flow and misaligned.
306
+ */
307
+ static _injectTextLayerCss() {
308
+ if (document.getElementById('pw-pdf-text-layer-styles'))
309
+ return;
310
+ const style = document.createElement('style');
311
+ style.id = 'pw-pdf-text-layer-styles';
312
+ style.textContent = \`
313
+ div[data-pw-pdf-text-layer] {
314
+ line-height: 1;
315
+ -webkit-text-size-adjust: none;
316
+ -moz-text-size-adjust: none;
317
+ text-size-adjust: none;
318
+ forced-color-adjust: none;
319
+ transform-origin: 0 0;
320
+ }
321
+ div[data-pw-pdf-text-layer] :is(span, br) {
322
+ color: transparent;
323
+ position: absolute;
324
+ white-space: pre;
325
+ cursor: text;
326
+ transform-origin: 0% 0%;
327
+ pointer-events: none;
328
+ }
329
+ div[data-pw-pdf-text-layer] span.markedContent {
330
+ top: 0;
331
+ height: 0;
332
+ }
333
+ \`;
334
+ document.head.appendChild(style);
335
+ }
336
+ /**
337
+ * Renders a single page in the main content area
338
+ */
339
+ async _renderPage(pageNum) {
340
+ if (!this._mainContent)
341
+ return;
342
+ const page = await this._pdfDoc.getPage(pageNum);
343
+ const viewport = page.getViewport({ scale: 1.0 });
344
+ const containerWidth = this._mainContent.clientWidth || 800;
345
+ let scale;
346
+ let wrapperMargin;
347
+ if (this._twoPageView) {
348
+ // Each page gets half the container width
349
+ const availableWidth = (containerWidth / 2 - 32) * 0.95;
350
+ scale = (availableWidth / viewport.width) * this._currentZoom;
351
+ wrapperMargin = '16px auto';
352
+ }
353
+ else {
354
+ const availableWidth = (containerWidth - 80) * 0.89;
355
+ scale = (availableWidth / viewport.width) * this._currentZoom;
356
+ wrapperMargin = '16px 20px 16px 80px';
357
+ }
358
+ const scaledViewport = page.getViewport({ scale });
359
+ // Create canvas wrapper — position:relative so the text layer can overlay it
360
+ const canvasWrapper = document.createElement('div');
361
+ canvasWrapper.setAttribute('data-page-number', pageNum.toString());
362
+ canvasWrapper.style.cssText = \`
363
+ position: relative;
364
+ margin: \${wrapperMargin};
365
+ background: white;
366
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 4px 8px rgba(0, 0, 0, 0.15);
367
+ width: \${scaledViewport.width}px;
368
+ height: \${scaledViewport.height}px;
369
+ box-sizing: border-box;
370
+ \`;
371
+ // Create canvas
372
+ const canvas = document.createElement('canvas');
373
+ canvas.width = scaledViewport.width;
374
+ canvas.height = scaledViewport.height;
375
+ canvas.style.cssText = \`
376
+ display: block;
377
+ width: 100%;
378
+ height: 100%;
379
+ \`;
380
+ canvasWrapper.appendChild(canvas);
381
+ this._mainContent.appendChild(canvasWrapper);
382
+ this._canvasElements.push(canvas);
383
+ // Render page on canvas
384
+ const context = canvas.getContext('2d');
385
+ if (!context) {
386
+ throw new Error('Failed to get canvas 2D context');
387
+ }
388
+ const renderContext = {
389
+ canvasContext: context,
390
+ viewport: scaledViewport
391
+ };
392
+ await page.render(renderContext).promise;
393
+ // Render invisible text layer so Playwright text assertions can find PDF text
394
+ try {
395
+ PdfJsViewer._injectTextLayerCss();
396
+ const textContent = await page.getTextContent();
397
+ const textLayer = document.createElement('div');
398
+ textLayer.setAttribute('data-pw-pdf-text-layer', pageNum.toString());
399
+ // width/height must match the canvas so span coordinates align correctly.
400
+ // opacity:0 hides the layer visually but keeps spans interactive for mouse events.
401
+ textLayer.style.cssText = \`
402
+ position: absolute;
403
+ top: 0;
404
+ left: 0;
405
+ width: \${scaledViewport.width}px;
406
+ height: \${scaledViewport.height}px;
407
+ overflow: hidden;
408
+ opacity: 0;
409
+ \`;
410
+ // --scale-factor is used by PDF.js CSS for font-size calculations
411
+ textLayer.style.setProperty('--scale-factor', String(scale));
412
+ canvasWrapper.appendChild(textLayer);
413
+ const renderTask = window.pdfjsLib.renderTextLayer({
414
+ textContentSource: textContent,
415
+ container: textLayer,
416
+ viewport: scaledViewport,
417
+ textDivs: [],
418
+ });
419
+ await renderTask.promise;
420
+ }
421
+ catch (e) {
422
+ console.warn(\`[PDF.js] Text layer render failed for page \${pageNum}:\`, e);
423
+ }
424
+ console.log(\`[PDF.js] Rendered page \${pageNum}/\${this._pdfDoc.numPages}\`);
425
+ }
426
+ /**
427
+ * Renders a thumbnail for the sidebar
428
+ */
429
+ async _renderThumbnail(pageNum) {
430
+ if (!this._sidebar)
431
+ return;
432
+ const page = await this._pdfDoc.getPage(pageNum);
433
+ // Thumbnail scale (match Chrome PDF viewer: ~118px wide thumbnails)
434
+ const viewport = page.getViewport({ scale: 1.0 });
435
+ const thumbnailWidth = 118;
436
+ const scale = thumbnailWidth / viewport.width;
437
+ const scaledViewport = page.getViewport({ scale });
438
+ // Create thumbnail wrapper (match Chrome PDF viewer style with generous spacing)
439
+ const thumbWrapper = document.createElement('div');
440
+ thumbWrapper.setAttribute('data-page-number', pageNum.toString());
441
+ thumbWrapper.style.cssText = \`
442
+ margin: 18px auto;
443
+ background: white;
444
+ cursor: pointer;
445
+ border: 3px solid transparent;
446
+ box-sizing: border-box;
447
+ transition: border-color 0.15s;
448
+ width: fit-content;
449
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
450
+ \`;
451
+ // Highlight first page (match Chrome's blue highlight)
452
+ if (pageNum === 1) {
453
+ thumbWrapper.style.borderColor = '#1a73e8';
454
+ }
455
+ // Create thumbnail canvas
456
+ const canvas = document.createElement('canvas');
457
+ canvas.width = scaledViewport.width;
458
+ canvas.height = scaledViewport.height;
459
+ canvas.style.cssText = 'display: block; width: 100%; height: auto;';
460
+ // Page number label (match Chrome style)
461
+ const label = document.createElement('div');
462
+ label.textContent = pageNum.toString();
463
+ label.style.cssText = \`
464
+ text-align: center;
465
+ color: #dadce0;
466
+ font-size: 13px;
467
+ padding: 5px;
468
+ background: #3f3f3f;
469
+ font-family: 'Roboto', Arial, sans-serif;
470
+ \`;
471
+ thumbWrapper.appendChild(canvas);
472
+ thumbWrapper.appendChild(label);
473
+ this._sidebar.appendChild(thumbWrapper);
474
+ this._thumbnailElements.push(thumbWrapper);
475
+ // Click handler to jump to page
476
+ thumbWrapper.addEventListener('click', () => {
477
+ this._scrollToPage(pageNum);
478
+ });
479
+ // Render thumbnail
480
+ const context = canvas.getContext('2d');
481
+ if (!context)
482
+ return;
483
+ await page.render({
484
+ canvasContext: context,
485
+ viewport: scaledViewport
486
+ }).promise;
487
+ }
488
+ /**
489
+ * Scrolls to a specific page
490
+ */
491
+ _scrollToPage(pageNum) {
492
+ if (!this._mainContent)
493
+ return;
494
+ const pageElement = this._mainContent.querySelector(\`[data-page-number="\${pageNum}"]\`);
495
+ if (pageElement) {
496
+ const rect = pageElement.getBoundingClientRect();
497
+ // Offset by toolbar height (56px) so the page isn't hidden under the sticky toolbar
498
+ window.scrollBy({ top: rect.top - 56, behavior: 'smooth' });
499
+ this._updateCurrentPage(pageNum);
500
+ }
501
+ }
502
+ /**
503
+ * Updates the current page highlight in sidebar (match Chrome's blue highlight)
504
+ */
505
+ _updateCurrentPage(pageNum) {
506
+ if (this._currentPage === pageNum)
507
+ return;
508
+ // Remove highlight from old page
509
+ if (this._thumbnailElements[this._currentPage - 1]) {
510
+ this._thumbnailElements[this._currentPage - 1].style.borderColor = 'transparent';
511
+ }
512
+ // Add highlight to new page (match Chrome's blue: #1a73e8)
513
+ if (this._thumbnailElements[pageNum - 1]) {
514
+ this._thumbnailElements[pageNum - 1].style.borderColor = '#1a73e8';
515
+ }
516
+ this._currentPage = pageNum;
517
+ // Update toolbar page display
518
+ if (this._pageDisplay) {
519
+ this._pageDisplay.textContent = \`\${pageNum} / \${this._pdfDoc.numPages}\`;
520
+ }
521
+ }
522
+ /**
523
+ * Creates a toolbar button with consistent styling
524
+ */
525
+ _createToolbarButton(icon, title, onClick) {
526
+ const button = document.createElement('button');
527
+ button.textContent = icon;
528
+ button.title = title;
529
+ button.style.cssText = \`
530
+ background: transparent;
531
+ border: none;
532
+ color: #e8eaed;
533
+ cursor: pointer;
534
+ padding: 6px 8px;
535
+ border-radius: 4px;
536
+ font-size: 16px;
537
+ line-height: 1;
538
+ display: flex;
539
+ align-items: center;
540
+ justify-content: center;
541
+ min-width: 32px;
542
+ height: 32px;
543
+ transition: background-color 0.2s;
544
+ \`;
545
+ // Hover effect
546
+ button.addEventListener('mouseenter', () => {
547
+ button.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
548
+ });
549
+ button.addEventListener('mouseleave', () => {
550
+ button.style.backgroundColor = 'transparent';
551
+ });
552
+ // Click handler
553
+ button.addEventListener('click', (e) => {
554
+ e.preventDefault();
555
+ onClick();
556
+ });
557
+ return button;
558
+ }
559
+ /**
560
+ * Zoom to a specific level (1.0 = 100%)
561
+ */
562
+ async _zoom(newZoom) {
563
+ if (!this._pdfDoc || !this._mainContent) {
564
+ console.warn('[PDF.js] Cannot zoom: PDF not loaded');
565
+ return;
566
+ }
567
+ this._currentZoom = Math.max(0.25, Math.min(4.0, newZoom));
568
+ if (this._zoomDisplay) {
569
+ this._zoomDisplay.textContent = \`\${Math.round(this._currentZoom * 100)}%\`;
570
+ }
571
+ console.log(\`[PDF.js] Zooming to \${Math.round(this._currentZoom * 100)}%...\`);
572
+ await this._rerenderPages();
573
+ console.log('[PDF.js] ✅ Zoom complete');
574
+ }
575
+ /**
576
+ * Re-renders all pages (used by zoom and two-page view toggle)
577
+ */
578
+ async _rerenderPages() {
579
+ if (!this._pdfDoc || !this._mainContent)
580
+ return;
581
+ const scrollPercentage = window.scrollY / (document.body.scrollHeight || 1);
582
+ if (this._twoPageView) {
583
+ this._mainContent.style.cssText = \`
584
+ flex: 1;
585
+ overflow: visible;
586
+ background-color: #525252;
587
+ position: relative;
588
+ padding: 0;
589
+ box-sizing: border-box;
590
+ display: grid;
591
+ grid-template-columns: 1fr 1fr;
592
+ align-items: start;
593
+ \`;
594
+ }
595
+ else {
596
+ this._mainContent.style.cssText = \`
597
+ flex: 1;
598
+ overflow: visible;
599
+ background-color: #525252;
600
+ position: relative;
601
+ padding: 0;
602
+ box-sizing: border-box;
603
+ \`;
604
+ }
605
+ this._mainContent.innerHTML = '';
606
+ this._canvasElements = [];
607
+ for (let pageNum = 1; pageNum <= this._pdfDoc.numPages; pageNum++) {
608
+ await this._renderPage(pageNum);
609
+ }
610
+ setTimeout(() => {
611
+ window.scrollTo(0, scrollPercentage * document.body.scrollHeight);
612
+ }, 100);
613
+ }
614
+ /**
615
+ * Fit page to available width
616
+ */
617
+ _fitToPage() {
618
+ if (!this._mainContent)
619
+ return;
620
+ // Reset zoom to fit width (100% is already calculated to fit)
621
+ this._zoom(1.0);
622
+ console.log('[PDF.js] Fit to page');
623
+ }
624
+ /**
625
+ * Rotate PDF pages clockwise by 90 degrees
626
+ */
627
+ _rotate() {
628
+ this._rotation = (this._rotation + 90) % 360;
629
+ // Apply rotation transform to all pages
630
+ const pages = this._mainContent?.querySelectorAll('[data-page-number]');
631
+ if (pages) {
632
+ pages.forEach((page) => {
633
+ page.style.transform = \`rotate(\${this._rotation}deg)\`;
634
+ });
635
+ }
636
+ console.log(\`[PDF.js] Rotated to \${this._rotation} degrees\`);
637
+ }
638
+ /**
639
+ * Download the PDF file
640
+ */
641
+ _download() {
642
+ if (!this._pdfDataUrl) {
643
+ console.error('[PDF.js] No PDF data URL available for download');
644
+ return;
645
+ }
646
+ // Create a temporary download link
647
+ const link = document.createElement('a');
648
+ link.href = this._pdfDataUrl;
649
+ link.download = this._filename;
650
+ link.style.display = 'none';
651
+ document.body.appendChild(link);
652
+ link.click();
653
+ document.body.removeChild(link);
654
+ console.log(\`[PDF.js] Downloaded: \${this._filename}\`);
655
+ }
656
+ /**
657
+ * Print the PDF by rendering all canvas pages into a new window
658
+ */
659
+ _print() {
660
+ if (!this._canvasElements.length) {
661
+ console.error('[PDF.js] No pages rendered to print');
662
+ return;
663
+ }
664
+ const printWindow = window.open('', '_blank');
665
+ if (!printWindow) {
666
+ console.warn('[PDF.js] Print window blocked by browser');
667
+ return;
668
+ }
669
+ const doc = printWindow.document;
670
+ doc.write(\`<!DOCTYPE html><html><head>
671
+ <title>\${this._filename}</title>
672
+ <style>
673
+ * { margin: 0; padding: 0; box-sizing: border-box; }
674
+ body { background: white; }
675
+ img { display: block; width: 100%; page-break-after: always; page-break-inside: avoid; }
676
+ img:last-child { page-break-after: avoid; }
677
+ </style>
678
+ </head><body>\`);
679
+ for (const canvas of this._canvasElements) {
680
+ const dataUrl = canvas.toDataURL('image/png');
681
+ doc.write(\`<img src="\${dataUrl}">\`);
682
+ }
683
+ doc.write('</body></html>');
684
+ doc.close();
685
+ // Trigger print once the window has loaded
686
+ printWindow.onload = () => {
687
+ printWindow.print();
688
+ printWindow.close();
689
+ };
690
+ // Fallback in case onload doesn't fire (e.g. data already loaded synchronously)
691
+ setTimeout(() => {
692
+ if (!printWindow.closed) {
693
+ printWindow.print();
694
+ printWindow.close();
695
+ }
696
+ }, 1500);
697
+ console.log('[PDF.js] Print window opened');
698
+ }
699
+ /**
700
+ * Toggles the "more options" dropdown menu matching Chrome's PDF viewer
701
+ */
702
+ _toggleMoreMenu(anchorElement) {
703
+ if (this._moreMenuElement) {
704
+ this._closeMoreMenu();
705
+ return;
706
+ }
707
+ const menu = document.createElement('div');
708
+ this._moreMenuElement = menu;
709
+ menu.style.cssText = \`
710
+ position: fixed;
711
+ background: #202124;
712
+ border-radius: 4px;
713
+ box-shadow: 0 2px 10px rgba(0,0,0,0.6);
714
+ z-index: 2147483648;
715
+ min-width: 220px;
716
+ padding: 4px 0;
717
+ font-family: 'Roboto', Arial, sans-serif;
718
+ font-size: 14px;
719
+ color: #e8eaed;
720
+ \`;
721
+ const rect = anchorElement.getBoundingClientRect();
722
+ menu.style.top = \`\${rect.bottom + 4}px\`;
723
+ menu.style.right = \`\${window.innerWidth - rect.right}px\`;
724
+ const addMenuItem = (text, checked, onClick) => {
725
+ const item = document.createElement('div');
726
+ item.style.cssText = \`
727
+ padding: 10px 16px 10px 44px;
728
+ cursor: pointer;
729
+ position: relative;
730
+ white-space: nowrap;
731
+ \`;
732
+ if (checked !== null) {
733
+ const checkEl = document.createElement('span');
734
+ checkEl.textContent = checked ? '✓' : '';
735
+ checkEl.style.cssText = \`
736
+ position: absolute;
737
+ left: 16px;
738
+ top: 50%;
739
+ transform: translateY(-50%);
740
+ font-size: 14px;
741
+ \`;
742
+ item.appendChild(checkEl);
743
+ }
744
+ const label = document.createElement('span');
745
+ label.textContent = text;
746
+ item.appendChild(label);
747
+ item.addEventListener('mouseenter', () => {
748
+ item.style.backgroundColor = 'rgba(255,255,255,0.1)';
749
+ });
750
+ item.addEventListener('mouseleave', () => {
751
+ item.style.backgroundColor = 'transparent';
752
+ });
753
+ item.addEventListener('click', () => {
754
+ this._closeMoreMenu();
755
+ onClick();
756
+ });
757
+ menu.appendChild(item);
758
+ return item;
759
+ };
760
+ const addDivider = () => {
761
+ const d = document.createElement('div');
762
+ d.style.cssText = 'height: 1px; background: rgba(255,255,255,0.15); margin: 4px 0;';
763
+ menu.appendChild(d);
764
+ };
765
+ addMenuItem('Two page view', this._twoPageView, () => {
766
+ this._twoPageView = !this._twoPageView;
767
+ this._rerenderPages();
768
+ });
769
+ addMenuItem('Annotations', this._annotationsVisible, () => {
770
+ this._annotationsVisible = !this._annotationsVisible;
771
+ console.log(\`[PDF.js] Annotations \${this._annotationsVisible ? 'shown' : 'hidden'}\`);
772
+ });
773
+ addDivider();
774
+ addMenuItem('Present', null, () => {
775
+ this._present();
776
+ });
777
+ addMenuItem('Document properties', null, () => {
778
+ this._showDocumentProperties();
779
+ });
780
+ // Close on outside click — registered after current event loop to avoid immediately closing
781
+ const onOutsideClick = (e) => {
782
+ if (!menu.contains(e.target) && e.target !== anchorElement) {
783
+ this._closeMoreMenu();
784
+ }
785
+ };
786
+ setTimeout(() => {
787
+ document.addEventListener('mousedown', onOutsideClick, true);
788
+ this._moreMenuCleanup = () => document.removeEventListener('mousedown', onOutsideClick, true);
789
+ }, 0);
790
+ document.body.appendChild(menu);
791
+ }
792
+ /**
793
+ * Closes the more options dropdown menu
794
+ */
795
+ _closeMoreMenu() {
796
+ if (this._moreMenuElement) {
797
+ this._moreMenuElement.remove();
798
+ this._moreMenuElement = null;
799
+ }
800
+ if (this._moreMenuCleanup) {
801
+ this._moreMenuCleanup();
802
+ this._moreMenuCleanup = null;
803
+ }
804
+ }
805
+ /**
806
+ * Present mode — not yet implemented.
807
+ */
808
+ _present() {
809
+ console.log('[PDF.js] Present: not yet implemented');
810
+ }
811
+ /**
812
+ * Shows a dialog with document metadata matching Chrome's "Document properties"
813
+ */
814
+ async _showDocumentProperties() {
815
+ if (!this._pdfDoc)
816
+ return;
817
+ let info = {};
818
+ try {
819
+ const metadata = await this._pdfDoc.getMetadata();
820
+ info = metadata.info || {};
821
+ }
822
+ catch (e) {
823
+ // metadata unavailable
824
+ }
825
+ // Calculate file size from data URL base64
826
+ let fileSize = '-';
827
+ if (this._pdfDataUrl) {
828
+ try {
829
+ const base64 = this._pdfDataUrl.split(',')[1];
830
+ if (base64) {
831
+ const bytes = Math.ceil((base64.length * 3) / 4);
832
+ fileSize = bytes >= 1024 * 1024
833
+ ? \`\${(bytes / (1024 * 1024)).toFixed(1)} MB\`
834
+ : \`\${(bytes / 1024).toFixed(1)} KB\`;
835
+ }
836
+ }
837
+ catch (e) { /* ignore */ }
838
+ }
839
+ // Parse PDF date string (D:YYYYMMDDHHmmss...) to locale format
840
+ const formatPdfDate = (raw) => {
841
+ if (!raw)
842
+ return '-';
843
+ const m = raw.match(/^D:(\\d{4})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})/);
844
+ if (!m)
845
+ return raw;
846
+ const date = new Date(\`\${m[1]}-\${m[2]}-\${m[3]}T\${m[4]}:\${m[5]}:\${m[6]}\`);
847
+ return isNaN(date.getTime()) ? raw : date.toLocaleString();
848
+ };
849
+ // Get page dimensions from first page and convert points → inches
850
+ let pageSize = '-';
851
+ try {
852
+ const firstPage = await this._pdfDoc.getPage(1);
853
+ const vp = firstPage.getViewport({ scale: 1.0 });
854
+ const wIn = (vp.width / 72).toFixed(2);
855
+ const hIn = (vp.height / 72).toFixed(2);
856
+ const orientation = vp.width > vp.height ? 'landscape' : 'portrait';
857
+ pageSize = \`\${wIn} × \${hIn} in (\${orientation})\`;
858
+ }
859
+ catch (e) { /* ignore */ }
860
+ // Grouped sections matching Chrome's layout exactly
861
+ const sections = [
862
+ [
863
+ ['File name:', this._filename],
864
+ ['File size:', fileSize],
865
+ ],
866
+ [
867
+ ['Title:', info['Title'] || '-'],
868
+ ['Author:', info['Author'] || '-'],
869
+ ['Subject:', info['Subject'] || '-'],
870
+ ['Keywords:', info['Keywords'] || '-'],
871
+ ['Created:', formatPdfDate(info['CreationDate'] || '')],
872
+ ['Modified:', formatPdfDate(info['ModDate'] || '')],
873
+ ['Application:', info['Creator'] || '-'],
874
+ ],
875
+ [
876
+ ['PDF producer:', info['Producer'] || '-'],
877
+ ['PDF version:', info['PDFFormatVersion'] || '-'],
878
+ ['Page count:', \`\${this._pdfDoc.numPages}\`],
879
+ ['Page size:', pageSize],
880
+ ],
881
+ [
882
+ ['Fast web view:', 'No'],
883
+ ],
884
+ ];
885
+ const overlay = document.createElement('div');
886
+ overlay.style.cssText = \`
887
+ position: fixed;
888
+ top: 0; left: 0; right: 0; bottom: 0;
889
+ background: rgba(0,0,0,0.5);
890
+ z-index: 2147483649;
891
+ display: flex;
892
+ align-items: center;
893
+ justify-content: center;
894
+ \`;
895
+ const dialog = document.createElement('div');
896
+ dialog.style.cssText = \`
897
+ background: #3c4043;
898
+ border-radius: 12px;
899
+ padding: 24px 24px 16px;
900
+ min-width: 380px;
901
+ max-width: 500px;
902
+ color: #e8eaed;
903
+ font-family: 'Roboto', Arial, sans-serif;
904
+ box-shadow: 0 4px 20px rgba(0,0,0,0.5);
905
+ \`;
906
+ const titleEl = document.createElement('h3');
907
+ titleEl.textContent = 'Document properties';
908
+ titleEl.style.cssText = 'margin: 0 0 16px; font-size: 18px; font-weight: 500;';
909
+ dialog.appendChild(titleEl);
910
+ const addRow = (label, value) => {
911
+ const row = document.createElement('div');
912
+ row.style.cssText = 'display: flex; padding: 7px 0; font-size: 13px;';
913
+ const labelEl = document.createElement('span');
914
+ labelEl.textContent = label;
915
+ labelEl.style.cssText = 'min-width: 140px; flex-shrink: 0;';
916
+ const valueEl = document.createElement('span');
917
+ valueEl.textContent = value;
918
+ valueEl.style.wordBreak = 'break-all';
919
+ row.appendChild(labelEl);
920
+ row.appendChild(valueEl);
921
+ dialog.appendChild(row);
922
+ };
923
+ const addDivider = () => {
924
+ const d = document.createElement('div');
925
+ d.style.cssText = 'height: 1px; background: rgba(255,255,255,0.15); margin: 6px 0;';
926
+ dialog.appendChild(d);
927
+ };
928
+ for (let i = 0; i < sections.length; i++) {
929
+ for (const [label, value] of sections[i]) {
930
+ addRow(label, value);
931
+ }
932
+ if (i < sections.length - 1) {
933
+ addDivider();
934
+ }
935
+ }
936
+ const closeBtn = document.createElement('button');
937
+ closeBtn.textContent = 'Close';
938
+ closeBtn.style.cssText = \`
939
+ display: block;
940
+ margin: 20px 0 0 auto;
941
+ padding: 10px 28px;
942
+ background: #8ab4f8;
943
+ border: none;
944
+ border-radius: 24px;
945
+ color: #202124;
946
+ font-size: 14px;
947
+ font-weight: 500;
948
+ cursor: pointer;
949
+ font-family: 'Roboto', Arial, sans-serif;
950
+ \`;
951
+ closeBtn.addEventListener('click', () => overlay.remove());
952
+ dialog.appendChild(closeBtn);
953
+ overlay.appendChild(dialog);
954
+ overlay.addEventListener('click', (e) => {
955
+ if (e.target === overlay)
956
+ overlay.remove();
957
+ });
958
+ document.body.appendChild(overlay);
959
+ console.log('[PDF.js] Document properties dialog opened');
960
+ }
961
+ /**
962
+ * Sets up scroll synchronization between the document scroll and the sidebar thumbnail highlight.
963
+ * Pages are in the document flow so we listen on window, not on _mainContent.
964
+ */
965
+ _setupScrollSync() {
966
+ if (!this._mainContent)
967
+ return;
968
+ window.addEventListener('scroll', () => {
969
+ if (!this._mainContent)
970
+ return;
971
+ const pages = this._mainContent.querySelectorAll('[data-page-number]');
972
+ const toolbarBottom = 56; // toolbar height
973
+ for (let i = 0; i < pages.length; i++) {
974
+ const pageElement = pages[i];
975
+ const rect = pageElement.getBoundingClientRect();
976
+ if (rect.top <= toolbarBottom + 100 && rect.bottom > toolbarBottom) {
977
+ const pageNum = parseInt(pageElement.getAttribute('data-page-number') || '1');
978
+ this._updateCurrentPage(pageNum);
979
+ break;
980
+ }
981
+ }
982
+ }, { passive: true });
983
+ }
984
+ /**
985
+ * Cleanup resources
986
+ */
987
+ cleanup() {
988
+ if (this._pdfDoc) {
989
+ this._pdfDoc.destroy();
990
+ this._pdfDoc = null;
991
+ }
992
+ this._closeMoreMenu();
993
+ this._canvasElements = [];
994
+ this._thumbnailElements = [];
995
+ this._toolbar = null;
996
+ this._sidebar = null;
997
+ this._mainContent = null;
998
+ this._currentPage = 1;
999
+ this._container.innerHTML = '';
1000
+ }
1001
+ }
1002
+
1003
+ // =============================================================================
1004
+ // Helper Functions (execution-specific)
1005
+ // =============================================================================
1006
+
1007
+ /**
1008
+ * Fetches a PDF from a URL and converts it to a data URL
1009
+ * Uses fetch() which will be intercepted by route handler during execution
1010
+ */
1011
+ async function fetchPdfAsDataUrl(pdfUrl) {
1012
+ try {
1013
+ console.log('[PW-PDF] Fetching PDF:', pdfUrl.substring(0, 100));
1014
+
1015
+ // During execution, this fetch will be intercepted by Playwright's route handler
1016
+ const response = await fetch(pdfUrl);
1017
+
1018
+ if (!response.ok) {
1019
+ throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
1020
+ }
1021
+
1022
+ const arrayBuffer = await response.arrayBuffer();
1023
+ const base64 = btoa(
1024
+ new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), '')
1025
+ );
1026
+
1027
+ const dataUrl = \`data:application/pdf;base64,\${base64}\`;
1028
+ console.log('[PW-PDF] ✅ PDF fetched successfully');
1029
+
1030
+ return dataUrl;
1031
+ } catch (error) {
1032
+ console.error('[PW-PDF] ❌ Failed to fetch PDF:', error);
1033
+ return null;
1034
+ }
1035
+ }
1036
+
1037
+ /**
1038
+ * Checks if the current page itself is a PDF document
1039
+ */
1040
+ function isCurrentPagePdf() {
1041
+ const url = window.location.href;
1042
+
1043
+ // Check URL ends with .pdf
1044
+ if (url.toLowerCase().endsWith('.pdf')) {
1045
+ console.log('[PW-PDF] URL ends with .pdf:', url);
1046
+ return true;
1047
+ }
1048
+
1049
+ // Check for PDF plugin embed that takes up the whole page
1050
+ const fullPageEmbed = document.querySelector('embed[type="application/pdf"]');
1051
+ if (fullPageEmbed && document.body.children.length === 1) {
1052
+ console.log('[PW-PDF] Found full-page PDF embed');
1053
+ return true;
1054
+ }
1055
+
1056
+ // Check if URL contains .pdf (including S3 URLs with .pdf)
1057
+ if (url.toLowerCase().includes('.pdf')) {
1058
+ console.log('[PW-PDF] PDF URL detected:', url);
1059
+ return true;
1060
+ }
1061
+
1062
+ return false;
1063
+ }
1064
+
1065
+ /**
1066
+ * Replaces the entire page with PDF.js viewer when the page itself is a PDF
1067
+ */
1068
+ async function replacePdfPage() {
1069
+ try {
1070
+ // Get PDF URL from current page URL
1071
+ const pdfUrl = window.location.href;
1072
+ console.log('[PW-PDF] Replacing full-page PDF with PDF.js viewer:', pdfUrl.substring(0, 100));
1073
+
1074
+ // Fetch PDF
1075
+ const dataUrl = await fetchPdfAsDataUrl(pdfUrl);
1076
+ if (!dataUrl) {
1077
+ throw new Error('Failed to fetch PDF');
1078
+ }
1079
+
1080
+ // Clear the document body
1081
+ document.body.innerHTML = '';
1082
+
1083
+ // Create container for PDF.js viewer
1084
+ const container = document.createElement('div');
1085
+ container.setAttribute('data-pw-pdf-viewer', 'true');
1086
+ container.id = 'pw-pdf-viewer-container';
1087
+ container.style.cssText = \`
1088
+ position: fixed;
1089
+ top: 0;
1090
+ left: 0;
1091
+ width: 100%;
1092
+ height: 100%;
1093
+ z-index: 2147483647;
1094
+ background: #525252;
1095
+ \`;
1096
+
1097
+ document.body.appendChild(container);
1098
+
1099
+ // Extract filename from URL
1100
+ let filename = 'Document.pdf';
1101
+ try {
1102
+ const url = new URL(pdfUrl);
1103
+ const pathname = url.pathname;
1104
+ const lastSlash = pathname.lastIndexOf('/');
1105
+ if (lastSlash !== -1) {
1106
+ filename = pathname.substring(lastSlash + 1);
1107
+ filename = decodeURIComponent(filename);
1108
+ }
1109
+ } catch (e) {
1110
+ console.warn('[PW-PDF] Failed to extract filename from URL:', e);
1111
+ }
1112
+
1113
+ // Create PDF.js viewer
1114
+ const viewer = new PdfJsViewer(container);
1115
+
1116
+ // Render the PDF
1117
+ await viewer.renderPdf({
1118
+ pdfDataUrl: dataUrl,
1119
+ filename,
1120
+ onReady: () => {
1121
+ console.log('[PW-PDF] ✅ Full-page PDF replaced with PDF.js viewer');
1122
+ },
1123
+ onError: (error) => {
1124
+ console.error('[PW-PDF] ❌ Failed to render full-page PDF:', error);
1125
+ }
1126
+ });
1127
+
1128
+ return true;
1129
+ } catch (error) {
1130
+ console.error('[PW-PDF] Error replacing full-page PDF:', error);
1131
+ return false;
1132
+ }
1133
+ }
1134
+
1135
+ /**
1136
+ * Replaces a single PDF embed with PDF.js viewer
1137
+ */
1138
+ async function replacePdfEmbed(embed) {
1139
+ try {
1140
+ // Get PDF URL
1141
+ let pdfUrl = embed.getAttribute('src');
1142
+
1143
+ // If src is about:blank, use the page URL (the page itself is the PDF)
1144
+ if (!pdfUrl || pdfUrl === 'about:blank') {
1145
+ console.log('[PW-PDF] Embed has src="about:blank", using page URL as PDF URL...');
1146
+ pdfUrl = window.location.href;
1147
+
1148
+ // Verify it looks like a PDF URL
1149
+ if (!pdfUrl.includes('.pdf')) {
1150
+ console.log('[PW-PDF] Page URL does not appear to be a PDF, skipping');
1151
+ return false;
1152
+ }
1153
+
1154
+ console.log('[PW-PDF] Using page URL as PDF:', pdfUrl.substring(0, 100));
1155
+ }
1156
+
1157
+ console.log('[PW-PDF] Replacing PDF embed with PDF.js viewer:', pdfUrl.substring(0, 100));
1158
+
1159
+ // Fetch PDF
1160
+ const dataUrl = await fetchPdfAsDataUrl(pdfUrl);
1161
+ if (!dataUrl) {
1162
+ throw new Error('Failed to fetch PDF');
1163
+ }
1164
+
1165
+ // Create container for PDF.js viewer
1166
+ const container = document.createElement('div');
1167
+ container.setAttribute('data-pw-pdf-viewer', 'true');
1168
+ container.style.cssText = \`
1169
+ position: absolute;
1170
+ top: \${embed.offsetTop}px;
1171
+ left: \${embed.offsetLeft}px;
1172
+ width: \${embed.offsetWidth || 800}px;
1173
+ height: \${embed.offsetHeight || 600}px;
1174
+ z-index: 2147483647;
1175
+ background: #525252;
1176
+ \`;
1177
+
1178
+ // Insert container next to embed
1179
+ const parent = embed.parentNode;
1180
+ if (!parent) return false;
1181
+
1182
+ parent.insertBefore(container, embed);
1183
+
1184
+ // Hide original embed
1185
+ embed.style.display = 'none';
1186
+
1187
+ // Extract filename from URL
1188
+ let filename = 'Document.pdf';
1189
+ try {
1190
+ const url = new URL(pdfUrl);
1191
+ const pathname = url.pathname;
1192
+ const lastSlash = pathname.lastIndexOf('/');
1193
+ if (lastSlash !== -1) {
1194
+ filename = pathname.substring(lastSlash + 1);
1195
+ filename = decodeURIComponent(filename);
1196
+ }
1197
+ } catch (e) {
1198
+ console.warn('[PW-PDF] Failed to extract filename from URL:', e);
1199
+ }
1200
+
1201
+ // Create PDF.js viewer
1202
+ const viewer = new PdfJsViewer(container);
1203
+
1204
+ // Render the PDF
1205
+ await viewer.renderPdf({
1206
+ pdfDataUrl: dataUrl,
1207
+ filename,
1208
+ onReady: () => {
1209
+ console.log('[PW-PDF] ✅ PDF embed replaced successfully');
1210
+ },
1211
+ onError: (error) => {
1212
+ console.error('[PW-PDF] ❌ Failed to render PDF:', error);
1213
+ // Restore original embed on error
1214
+ embed.style.display = '';
1215
+ if (container.parentNode) {
1216
+ container.parentNode.removeChild(container);
1217
+ }
1218
+ }
1219
+ });
1220
+
1221
+ return true;
1222
+ } catch (error) {
1223
+ console.error('[PW-PDF] Error replacing PDF embed:', error);
1224
+ return false;
1225
+ }
1226
+ }
1227
+
1228
+ /**
1229
+ * Detects and replaces PDF embeds with PDF.js viewers
1230
+ */
1231
+ async function detectAndReplacePdfs() {
1232
+ console.log('[PW-PDF] Scanning for PDF embeds...');
1233
+
1234
+ // Check if we've already replaced a full-page PDF (prevent infinite loop)
1235
+ if (window.__pwPdfPageReplaced) {
1236
+ console.log('[PW-PDF] PDF page already replaced, skipping detection');
1237
+ return;
1238
+ }
1239
+
1240
+ // Check if the current page itself is a PDF document
1241
+ const isPdfPage = isCurrentPagePdf();
1242
+ if (isPdfPage) {
1243
+ console.log('[PW-PDF] Current page is a PDF document, replacing with PDF.js viewer...');
1244
+ window.__pwPdfPageReplaced = true;
1245
+ await replacePdfPage();
1246
+ return;
1247
+ }
1248
+
1249
+ // Find all embed and iframe elements
1250
+ const embeds = document.querySelectorAll('embed[type="application/pdf"], iframe[src*=".pdf"]');
1251
+
1252
+ if (embeds.length === 0) {
1253
+ console.log('[PW-PDF] No PDF embeds found');
1254
+ return;
1255
+ }
1256
+
1257
+ console.log(\`[PW-PDF] Found \${embeds.length} PDF embed(s)\`);
1258
+
1259
+ for (const embed of embeds) {
1260
+ await replacePdfEmbed(embed);
1261
+ }
1262
+ }
1263
+
1264
+ // =============================================================================
1265
+ // Window.open() Interception (for headless mode)
1266
+ // =============================================================================
1267
+
1268
+ /**
1269
+ * Sets up window.open() interception to detect PDF URLs
1270
+ */
1271
+ function setupPdfPopupInterception() {
1272
+ const originalOpen = window.open;
1273
+ window.open = function(url, target, features) {
1274
+ // Check if URL is a PDF (ends with .pdf or contains PDF indicators)
1275
+ const isPdf = url && url.toLowerCase().includes('.pdf');
1276
+
1277
+ if (isPdf) {
1278
+ console.log('[PW-PDF] Intercepted window.open() to PDF:', url);
1279
+
1280
+ // Let the popup open normally (to about:blank or target URL)
1281
+ const popup = originalOpen.call(this, url, target, features);
1282
+
1283
+ // If popup opened successfully, store PDF URL for it to access
1284
+ if (popup && !popup.closed) {
1285
+ popup.__pw_pdfUrl = url;
1286
+
1287
+ // The popup will detect __pw_pdfUrl and render with PDF.js
1288
+ setTimeout(() => {
1289
+ if (popup.__pw_detectAndReplacePdfs) {
1290
+ popup.__pw_detectAndReplacePdfs();
1291
+ }
1292
+ }, 100);
1293
+ }
1294
+
1295
+ return popup;
1296
+ }
1297
+
1298
+ // Not a PDF, let window.open() work normally
1299
+ return originalOpen.apply(this, arguments);
1300
+ };
1301
+ }
1302
+
1303
+ // =============================================================================
1304
+ // Expose Functions and Classes Globally
1305
+ // =============================================================================
1306
+
1307
+ window.PdfJsViewer = PdfJsViewer; // Expose class for direct instantiation
1308
+ window.__pw_detectAndReplacePdfs = detectAndReplacePdfs;
1309
+ window.__pw_setupPdfPopupInterception = setupPdfPopupInterception;
1310
+
1311
+ // =============================================================================
1312
+ // Auto-detect PDFs on page load
1313
+ // =============================================================================
1314
+
1315
+ // Auto-detect if current page is a PDF popup that needs rendering
1316
+ if (window.__pw_pdfUrl && !window.__pwPdfRendered) {
1317
+ window.__pwPdfRendered = true;
1318
+
1319
+ // Wait for DOM ready
1320
+ if (document.readyState === 'loading') {
1321
+ document.addEventListener('DOMContentLoaded', () => {
1322
+ window.__pw_detectAndReplacePdfs();
1323
+ });
1324
+ } else {
1325
+ window.__pw_detectAndReplacePdfs();
1326
+ }
1327
+ }
1328
+
1329
+ // Also auto-detect on every page load (for direct navigation to PDFs)
1330
+ // This runs after the page has fully loaded
1331
+ if (document.readyState === 'loading') {
1332
+ document.addEventListener('DOMContentLoaded', () => {
1333
+ console.log('[PW-PDF] DOMContentLoaded - checking for PDFs...');
1334
+ window.__pw_detectAndReplacePdfs();
1335
+ });
1336
+ } else if (document.readyState === 'interactive' || document.readyState === 'complete') {
1337
+ // If DOM is already ready, run detection immediately
1338
+ console.log('[PW-PDF] DOM already ready - checking for PDFs...');
1339
+ setTimeout(() => {
1340
+ window.__pw_detectAndReplacePdfs();
1341
+ }, 100); // Small delay to let browser plugin initialize
1342
+ }
1343
+
1344
+
1345
+ console.log('[PW-PDF] PDF viewer injection script loaded');
1346
+ })();
1347
+ `;
1348
+
1349
+ module.exports = { PDF_VIEWER_INJECTION_SCRIPT };