1(() => { 2 'use strict'; 3 4 let ooxmlEnginePromise = null; 5 let legacyEnginePromise = null; 6 let activeState = null; 7 const maxLegacySlides = 500; 8 const maxLegacyTopLevelElements = 50000; 9 const maxLegacySvgCharacters = 40 * 1024 * 1024; 10 11 function loadOoxmlEngine() { 12 ooxmlEnginePromise ??= import('../lib/pptx-renderer/aiden0z-pptx-renderer.browser.es.js'); 13 return ooxmlEnginePromise; 14 } 15 16 function loadLegacyEngine() { 17 legacyEnginePromise ??= import('../lib/web-ppt/core.js'); 18 return legacyEnginePromise; 19 } 20 21 function sanitizeLinks(container) { 22 container.querySelectorAll('a').forEach(link => { 23 const href = link.getAttribute('href') || link.getAttribute('xlink:href') || ''; 24 if (href && !href.startsWith('#')) { 25 try { 26 const protocol = new URL(href, window.location.href).protocol; 27 if (!['http:', 'https:', 'mailto:', 'tel:'].includes(protocol)) { 28 link.removeAttribute('href'); 29 link.removeAttribute('xlink:href'); 30 } 31 } catch { 32 link.removeAttribute('href'); 33 link.removeAttribute('xlink:href'); 34 } 35 } 36 link.target = '_blank'; 37 link.rel = 'noopener noreferrer nofollow'; 38 }); 39 } 40 41 function containsExternalCssUrl(value) { 42 return /@import/i.test(value) || /url\s*\(\s*['"]?(?!#|data:image\/|blob:)/i.test(value); 43 } 44 45 function sanitizeLegacySvg(svgMarkup, idPrefix) { 46 const parsed = new DOMParser().parseFromString(svgMarkup, 'image/svg+xml'); 47 if (parsed.querySelector('parsererror') || parsed.documentElement.localName !== 'svg') { 48 throw new Error('The legacy presentation renderer produced invalid SVG.'); 49 } 50 51 const root = parsed.documentElement; 52 root.querySelectorAll('script, iframe, object, embed, foreignObject').forEach(element => element.remove()); 53 root.querySelectorAll('style').forEach(style => { 54 if (containsExternalCssUrl(style.textContent || '')) style.remove(); 55 }); 56 57 const nodes = [root, ...root.querySelectorAll('*')]; 58 nodes.forEach(node => { 59 for (const attribute of [...node.attributes]) { 60 const name = attribute.name.toLowerCase(); 61 if (name.startsWith('on') || (name === 'style' && containsExternalCssUrl(attribute.value))) { 62 node.removeAttribute(attribute.name); 63 continue; 64 } 65 if (!['src', 'href', 'xlink:href'].includes(name) || node.localName === 'a') continue; 66 const value = attribute.value.trim(); 67 if (!value.startsWith('#') && !value.startsWith('blob:') && !value.startsWith('data:image/')) { 68 node.removeAttribute(attribute.name); 69 } 70 } 71 }); 72 73 const ids = new Map(); 74 root.querySelectorAll('[id]').forEach(node => { 75 const original = node.id; 76 const replacement = `${idPrefix}${original.replace(/[^a-zA-Z0-9_-]/g, '-')}`; 77 ids.set(original, replacement); 78 node.id = replacement; 79 }); 80 nodes.forEach(node => { 81 for (const attribute of [...node.attributes]) { 82 let value = attribute.value; 83 ids.forEach((replacement, original) => { 84 value = value.replaceAll(`url(#${original})`, `url(#${replacement})`); 85 if (value === `#${original}`) value = `#${replacement}`; 86 }); 87 if (value !== attribute.value) node.setAttribute(attribute.name, value); 88 } 89 }); 90 91 root.setAttribute('width', '100%'); 92 root.removeAttribute('height'); 93 root.setAttribute('preserveAspectRatio', 'xMidYMid meet'); 94 root.classList.add('pptx-legacy-slide'); 95 return document.importNode(root, true); 96 } 97 98 function createLegacyViewer(engine, presentation, mount) { 99 let destroyed = false;
100 let currentSlideIndex = 0; 101 let renderSequence = 0; 102 103 function renderInto(index, container, kind) { 104 if (destroyed) return; 105 const markup = engine.renderSlideToSvg(presentation, presentation.slides[index], { 106 includeNotes: false, 107 media: 'badge', 108 showComments: false, 109 textMode: 'svg' 110 }); 111 if (markup.length > maxLegacySvgCharacters) { 112 throw new Error('This slide is too complex for a safe browser preview.'); 113 } 114 const prefix = `pdfle-${kind}-${index}-${renderSequence += 1}-`; 115 container.replaceChildren(sanitizeLegacySvg(markup, prefix)); 116 sanitizeLinks(container); 117 } 118 119 return { 120 get currentSlideIndex() { return currentSlideIndex; }, 121 slideCount: presentation.slides.length, 122 async renderSlide(index) { 123 renderInto(index, mount, 'slide'); 124 currentSlideIndex = index; 125 }, 126 renderThumbnailToContainer(index, container) { 127 let disposed = false; 128 const ready = Promise.resolve().then(() => { 129 if (!disposed) renderInto(index, container, 'thumbnail'); 130 }); 131 return { 132 ready, 133 dispose() { 134 disposed = true; 135 container.replaceChildren(); 136 } 137 }; 138 }, 139 destroy() { 140 if (destroyed) return; 141 destroyed = true; 142 presentation.dispose?.(); 143 } 144 }; 145 } 146 147 async function openLegacyViewer(file, mount, state, isCurrent) { 148 const engine = await loadLegacyEngine(); 149 const presentation = await engine.parse(file, { lazy: false }); 150 if (activeState !== state || !isCurrent()) { 151 presentation.dispose?.(); 152 return null; 153 } 154 if (!presentation.slides.length) { 155 presentation.dispose?.(); 156 throw new Error('This presentation does not contain any slides.'); 157 } 158 const elementCount = presentation.slides.reduce((total, slide) => total + slide.elements.length, 0); 159 if (presentation.slides.length > maxLegacySlides || elementCount > maxLegacyTopLevelElements) { 160 presentation.dispose?.(); 161 throw new Error('This presentation is too complex for a safe browser preview.'); 162 } 163 const viewer = createLegacyViewer(engine, presentation, mount); 164 await viewer.renderSlide(0); 165 return viewer; 166 } 167 168 function disposeState(state) { 169 if (!state) return; 170 state.abortController.abort(); 171 state.thumbnailObserver?.disconnect(); 172 state.thumbnailHandles.forEach(handle => handle?.dispose()); 173 state.thumbnailHandles.clear(); 174 state.viewer?.destroy(); 175 state.elements.stage.onkeydown = null; 176 state.elements.previous.onclick = null; 177 state.elements.next.onclick = null; 178 state.elements.mount.replaceChildren(); 179 state.elements.thumbnails.replaceChildren(); 180 state.elements.counter.textContent = ''; 181 state.elements.host.classList.add('d-none'); 182 } 183 184 function reset() { 185 disposeState(activeState); 186 activeState = null; 187 } 188 189 function updateNavigation(state, index, scrollThumbnail = false) { 190 if (activeState !== state) return; 191 state.currentIndex = index; 192 state.elements.previous.disabled = index <= 0; 193 state.elements.next.disabled = index >= state.viewer.slideCount - 1; 194 state.elements.counter.textContent = `Slide ${index + 1} of ${state.viewer.slideCount}`; 195 196 state.elements.thumbnails.querySelectorAll('.pptx-thumbnail-button').forEach(button => { 197 const selected = Number(button.dataset.slideIndex) === index; 198 button.classList.toggle('is-active', selected); 199 button.setAttribute('aria-current', selected ? 'true' : 'false');
200 if (selected && scrollThumbnail) button.scrollIntoView({ block: 'nearest' }); 201 }); 202 } 203 204 async function showSlide(state, index, scrollThumbnail = true) { 205 if (activeState !== state || !state.viewer || index < 0 || index >= state.viewer.slideCount) return; 206 await state.viewer.renderSlide(index); 207 if (activeState !== state) return; 208 sanitizeLinks(state.elements.mount); 209 updateNavigation(state, index, scrollThumbnail); 210 } 211 212 function renderThumbnail(state, index, container) { 213 if (activeState !== state || state.thumbnailHandles.has(index)) return; 214 try { 215 const handle = state.viewer.renderThumbnailToContainer(index, container, { width: 150 }); 216 state.thumbnailHandles.set(index, handle); 217 void handle?.ready.then(() => { 218 if (activeState === state) sanitizeLinks(container); 219 }).catch(() => { 220 container.classList.add('pptx-thumbnail-preview--error'); 221 }); 222 } catch { 223 container.classList.add('pptx-thumbnail-preview--error'); 224 } 225 } 226 227 function buildThumbnails(state) { 228 if ('IntersectionObserver' in window) { 229 state.thumbnailObserver = new IntersectionObserver(entries => { 230 entries.forEach(entry => { 231 if (!entry.isIntersecting) return; 232 const preview = entry.target; 233 state.thumbnailObserver.unobserve(preview); 234 renderThumbnail(state, Number(preview.dataset.slideIndex), preview); 235 }); 236 }, { root: state.elements.thumbnails, rootMargin: '180px 0px' }); 237 } 238 239 const fragment = document.createDocumentFragment(); 240 for (let index = 0; index < state.viewer.slideCount; index += 1) { 241 const button = document.createElement('button'); 242 button.className = 'pptx-thumbnail-button'; 243 button.type = 'button'; 244 button.dataset.slideIndex = String(index); 245 button.setAttribute('aria-label', `Show slide ${index + 1}`); 246 button.setAttribute('aria-current', index === 0 ? 'true' : 'false'); 247 248 const preview = document.createElement('span'); 249 preview.className = 'pptx-thumbnail-preview'; 250 preview.dataset.slideIndex = String(index); 251 const label = document.createElement('span'); 252 label.className = 'pptx-thumbnail-label'; 253 label.textContent = String(index + 1); 254 button.append(preview, label); 255 button.addEventListener('click', () => void showSlide(state, index, false)); 256 fragment.append(button); 257 258 if (state.thumbnailObserver) state.thumbnailObserver.observe(preview); 259 else renderThumbnail(state, index, preview); 260 } 261 state.elements.thumbnails.append(fragment); 262 } 263 264 async function render(file, extension, elements, isCurrent) { 265 reset(); 266 const state = { 267 abortController: new AbortController(), 268 currentIndex: 0, 269 elements, 270 nodeErrorCount: 0, 271 thumbnailHandles: new Map(), 272 thumbnailObserver: null, 273 viewer: null 274 }; 275 activeState = state; 276 elements.host.classList.remove('d-none'); 277 278 try { 279 if (extension === 'ppt' || extension === 'pps') { 280 state.viewer = await openLegacyViewer(file, elements.mount, state, isCurrent); 281 if (!state.viewer) return null; 282 } else { 283 const { PptxViewer, RECOMMENDED_ZIP_LIMITS } = await loadOoxmlEngine(); 284 if (activeState !== state || !isCurrent()) return null; 285 286 state.viewer = await PptxViewer.open(file, elements.mount, { 287 renderMode: 'slide', 288 fitMode: 'contain', 289 lazyMedia: true, 290 lazySlides: true, 291 pdfjs: false, 292 signal: state.abortController.signal, 293 zipLimits: RECOMMENDED_ZIP_LIMITS, 294 onNodeError: () => { state.nodeErrorCount += 1; }, 295 onSlideRendered: (_index, element) => sanitizeLinks(element), 296 onSlideChange: index => { 297 if (state.viewer) updateNavigation(state, index); 298 } 299 }); 300 } 301 if (activeState !== state || !isCurrent()) { 302 disposeState(state); 303 return null; 304 } 305 if (!state.viewer.slideCount) throw new Error('This presentation does not contain any slides.'); 306 307 buildThumbnails(state); 308 updateNavigation(state, state.viewer.currentSlideIndex || 0); 309 elements.previous.onclick = () => void showSlide(state, state.currentIndex - 1); 310 elements.next.onclick = () => void showSlide(state, state.currentIndex + 1); 311 elements.stage.onkeydown = event => { 312 if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return; 313 event.preventDefault(); 314 const offset = event.key === 'ArrowLeft' ? -1 : 1; 315 void showSlide(state, state.currentIndex + offset); 316 };
317 sanitizeLinks(elements.mount); 318 return { slideCount: state.viewer.slideCount, warningCount: state.nodeErrorCount }; 319 } catch (error) { 320 if (activeState === state) reset(); 321 throw error; 322 } 323 } 324 325 const api = Object.freeze({ render, reset }); 326 window.PdflePresentationViewer = api; 327 window.PdflePptxViewer = api; 328})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.