diff --git a/packages/api/src/files/documents/html.spec.ts b/packages/api/src/files/documents/html.spec.ts index 441c6e1fa3..5b460b2e27 100644 --- a/packages/api/src/files/documents/html.spec.ts +++ b/packages/api/src/files/documents/html.spec.ts @@ -8,6 +8,7 @@ import { csvToHtml, excelSheetToHtml, officeHtmlBucket, + pptxToHtml, pptxToSlideListHtml, sanitizeOfficeHtml, wordDocToHtml, @@ -306,6 +307,120 @@ describe('Office HTML producers', () => { }); }); + describe('pptxToHtml dispatcher', () => { + /** Mirror the buildPptx helper from the slide-list block. */ + const buildPptx = async ( + slides: Array<{ title: string; body?: string[] }>, + ): Promise => { + const zip = new JSZip(); + const slideXml = (title: string, body: string[] = []) => { + const titleP = `${title}`; + const bodyPs = body.map((b) => `${b}`).join(''); + return ` + + ${titleP}${bodyPs} +`; + }; + slides.forEach((s, i) => { + zip.file(`ppt/slides/slide${i + 1}.xml`, slideXml(s.title, s.body)); + }); + zip.file('docProps/core.xml', ''); + return zip.generateAsync({ type: 'nodebuffer' }); + }; + + test('routes a small pptx (≤ cap) through the CDN-rendered path', async () => { + const pptx = await buildPptx([{ title: 'Hello', body: ['First slide'] }]); + const html = await pptxToHtml(pptx); + expect(html).toMatch(/^/); + expect(html).toContain('id="lc-doc-data"'); + expect(html).toContain('pptxPreview.init'); + expect(html).toContain('cdn.jsdelivr.net/npm/pptx-preview@'); + // Slide-list signature must NOT appear (those are
    ). + expect(html).not.toContain('class="lc-pptx-list"'); + }); + + describe('CDN-rendered path', () => { + test('embeds the binary as base64 that round-trips to the original bytes', async () => { + const original = await buildPptx([{ title: 'Test' }]); + const html = await _internal.pptxToHtmlViaCdn(original); + const match = html.match( + / + + +
    Loading preview…
    + + + + +`; +} + +async function pptxToHtmlViaCdn(buffer: Buffer): Promise { + return buildPptxCdnDocument(buffer.toString('base64')); +} + +/** + * Convert a `.pptx` buffer to a sandboxed HTML document. Two render + * paths, chosen by file size and the `OFFICE_PREVIEW_DISABLE_CDN` + * escape hatch: + * + * 1. **CDN-rendered (default for files ≤ 350 KB binary)**: embeds + * the binary as base64 and lets `pptx-preview` render it inside + * the Sandpack iframe. High visual fidelity. + * 2. **Slide-list (fallback)**: server-side text-only extraction + * (each slide → card with title + bullets). Lower fidelity but + * compact, network-independent, and proven against arbitrary + * decks. Triggered when the binary exceeds the embed cap, when + * `OFFICE_PREVIEW_DISABLE_CDN=true` is set (air-gapped / + * filtered networks), or when the CDN script fails to load + * (handled iframe-side). + * + * Pre-flights through `assertSafeZipSize` so a zip-bomb PPTX is + * rejected before either renderer touches it. + */ +export async function pptxToHtml(buffer: Buffer): Promise { + await assertSafeZipSize(buffer, { name: 'pptx' }); + if (isOfficePreviewCdnDisabled()) { + return pptxToSlideListHtmlInternal(buffer); + } + if (buffer.length <= MAX_PPTX_CDN_BINARY_BYTES) { + return pptxToHtmlViaCdn(buffer); + } + return pptxToSlideListHtmlInternal(buffer); +} + +/** + * Slide-list path that skips its own `assertSafeZipSize` call — used + * by `pptxToHtml` after the dispatcher has already validated the + * buffer. `pptxToSlideListHtml` (public, validating) stays the entry + * point for tests and direct callers. + */ +async function pptxToSlideListHtmlInternal(buffer: Buffer): Promise { + const rawSlides = await extractPptxSlideXml(buffer); + const slides: PptxSlide[] = rawSlides.map(({ number, xml }) => { + const { title, body } = extractSlideText(xml); + return { number, title, body }; + }); + return renderPptxSlidesHtml(slides); +} + +/** + * Test-only re-exports. Each path has distinct visual-fidelity and + * size characteristics that warrant direct testing rather than + * round-tripping through the size-based dispatcher with synthetically + * padded fixtures. Not part of the public API — callers in production + * code should always go through `wordDocToHtml` / `pptxToHtml`. + */ +export const _internal = { + wordDocToHtmlViaCdn, + wordDocToHtmlViaMammoth, + MAX_DOCX_CDN_BINARY_BYTES, + DOCX_PREVIEW_CDN, + pptxToHtmlViaCdn, + MAX_PPTX_CDN_BINARY_BYTES, + PPTX_PREVIEW_CDN, +}; + /* ============================================================================= * Dispatcher * ============================================================================= */ @@ -1026,7 +1200,7 @@ export async function bufferToOfficeHtml( case 'spreadsheet': return excelSheetToHtml(buffer); case 'pptx': - return pptxToSlideListHtml(buffer); + return pptxToHtml(buffer); default: return null; }