import zlib from 'node:zlib'; const PREVIEWABLE_MIME_TYPES = new Set([ 'text/html', 'text/plain', 'text/markdown', 'text/csv', 'application/pdf', 'image/png', 'image/jpeg', 'image/webp', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', ]); const PREVIEW_SHELL_STYLE = ` html,body{margin:0;padding:0;background:#f5f0e5;color:#1f2937;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} body{padding:24px;box-sizing:border-box;line-height:1.6} pre,code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace} pre{white-space:pre-wrap;word-break:break-word;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:16px} table{border-collapse:collapse;width:100%;background:#fff;border-radius:12px;overflow:hidden} th,td{border:1px solid #e5e7eb;padding:8px 10px;text-align:left;font-size:14px} th{background:#faf7ef} .docx-preview h1{font-size:1.5rem;margin:0 0 16px} .docx-preview p{margin:0 0 12px;text-indent:2em} .docx-preview .meta{color:#6b7280;font-size:13px;margin-bottom:20px;text-indent:0} .pdf-frame,.image-frame{display:block;width:100%;min-height:calc(100vh - 48px);border:0;border-radius:12px;background:#fff} .image-frame{object-fit:contain;max-height:calc(100vh - 48px);width:auto;max-width:100%;margin:0 auto;cursor:zoom-in} .image-viewer{padding:0} .image-viewer h1,.image-viewer .meta{display:none} .image-viewer .image-frame{min-height:100vh;max-height:100vh;margin:0;border-radius:0;background:#0b100e} .image-lightbox[hidden]{display:none} .image-lightbox{position:fixed;inset:0;z-index:9999;display:grid;place-items:center;padding:max(16px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));background:rgba(7,12,10,.9);cursor:zoom-out} .image-lightbox img{display:block;max-width:min(100%,1600px);max-height:calc(100vh - 32px);object-fit:contain;border-radius:8px;box-shadow:0 24px 80px rgba(0,0,0,.35);cursor:default} .image-lightbox-close{position:fixed;top:max(16px,env(safe-area-inset-top));right:max(16px,env(safe-area-inset-right));z-index:10000;display:grid;place-items:center;width:40px;height:40px;padding:0;border:0;border-radius:999px;color:#fffaf0;background:rgba(24,33,29,.72);box-shadow:0 8px 24px rgba(0,0,0,.28);cursor:pointer} .image-lightbox-close svg{display:block;width:18px;height:18px;stroke:currentColor;stroke-width:2;fill:none} .image-lightbox-close:hover{background:rgba(24,33,29,.92)} `; const IMAGE_LIGHTBOX_CLOSE_ICON = ''; const IMAGE_LIGHTBOX_SCRIPT = ``; function escapeHtml(text) { return String(text ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function previewDocument( title, bodyHtml, { downloadUrl = null, extraHead = '', allowScripts = false, bodyClass = '' } = {}, ) { const csp = [ "default-src 'none'", "style-src 'unsafe-inline'", "img-src 'self' data: https:", "font-src 'self' data:", "frame-src 'self'", "object-src 'self'", "base-uri 'none'", "form-action 'none'", allowScripts ? "script-src 'unsafe-inline'" : "script-src 'none'", ].join('; '); const downloadLink = downloadUrl ? `

下载原文件

` : ''; const bodyAttrs = bodyClass ? ` class="${escapeHtml(bodyClass)}"` : ''; return `${escapeHtml(title)}${extraHead}

${escapeHtml(title)}

${downloadLink}${bodyHtml}`; } function renderImageLightboxMarkup({ downloadUrl, title, triggerClass = 'image-frame' }) { const safeUrl = escapeHtml(downloadUrl); const safeTitle = escapeHtml(title); return ( `${safeTitle}` + `${IMAGE_LIGHTBOX_SCRIPT}` ); } export function wantsInlineImageViewer(req) { if (req?.query?.viewer === '1') return true; if (req?.query?.viewer === '0') return false; const fetchDest = String(req?.get?.('sec-fetch-dest') ?? req?.headers?.['sec-fetch-dest'] ?? '').toLowerCase(); if (fetchDest === 'image') return false; if (fetchDest === 'document' || fetchDest === 'iframe') return true; const accept = String(req?.get?.('accept') ?? req?.headers?.accept ?? ''); return /text\/html/i.test(accept); } export function renderImageAssetViewerHtml({ asset, downloadUrl }) { const title = asset.displayName || asset.filename; return previewDocument(title, renderImageLightboxMarkup({ downloadUrl, title }), { downloadUrl, allowScripts: true, bodyClass: 'image-viewer', }); } function renderInlineMarkdown(text) { let html = escapeHtml(text); html = html.replace(/`([^`]+)`/g, '$1'); html = html.replace(/\*\*(.+?)\*\*/g, '$1'); html = html.replace(/\*(.+?)\*/g, '$1'); html = html.replace(/\[([^\]]+)]\(([^)]+)\)/g, '$1'); return html; } function renderMarkdownDocument(text) { const lines = String(text ?? '').split('\n'); const blocks = []; let inCode = false; let code = []; for (const line of lines) { if (line.startsWith('```')) { if (inCode) { blocks.push(`
${escapeHtml(code.join('\n'))}
`); code = []; inCode = false; } else { inCode = true; } continue; } if (inCode) { code.push(line); continue; } if (!line.trim()) continue; const heading = line.match(/^(#{1,6})\s+(.+)$/); if (heading) { const level = heading[1].length; blocks.push(`${renderInlineMarkdown(heading[2])}`); continue; } blocks.push(`

${renderInlineMarkdown(line)}

`); } if (inCode && code.length) { blocks.push(`
${escapeHtml(code.join('\n'))}
`); } return blocks.join('\n'); } function parseCsvRows(text) { const rows = []; let row = []; let cell = ''; let inQuotes = false; for (let i = 0; i < text.length; i += 1) { const ch = text[i]; const next = text[i + 1]; if (inQuotes) { if (ch === '"' && next === '"') { cell += '"'; i += 1; } else if (ch === '"') { inQuotes = false; } else { cell += ch; } continue; } if (ch === '"') { inQuotes = true; continue; } if (ch === ',') { row.push(cell); cell = ''; continue; } if (ch === '\n') { row.push(cell); rows.push(row); row = []; cell = ''; continue; } if (ch === '\r') continue; cell += ch; } row.push(cell); rows.push(row); return rows.filter((item) => item.some((value) => String(value ?? '').trim())); } function renderCsvPreview(text) { const rows = parseCsvRows(String(text ?? '')); if (rows.length === 0) return '
(空文件)
'; const [head, ...body] = rows; const header = `${head.map((cell) => `${escapeHtml(cell)}`).join('')}`; const content = body .slice(0, 200) .map((line) => `${line.map((cell) => `${escapeHtml(cell)}`).join('')}`) .join(''); const tail = body.length > 200 ? `

仅展示前 200 行

` : ''; return `
${tail}${header}${content}
`; } function extractZipEntry(buffer, targetName) { let offset = 0; while (offset + 30 <= buffer.length) { if (buffer.subarray(offset, offset + 2).toString('ascii') !== 'PK') break; const compressionMethod = buffer.readUInt16LE(offset + 8); const compressedSize = buffer.readUInt32LE(offset + 18); const nameLength = buffer.readUInt16LE(offset + 26); const extraLength = buffer.readUInt16LE(offset + 28); const name = buffer.subarray(offset + 30, offset + 30 + nameLength).toString('utf8'); const dataStart = offset + 30 + nameLength + extraLength; if (name === targetName) { const compressed = buffer.subarray(dataStart, dataStart + compressedSize); if (compressionMethod === 0) return compressed; if (compressionMethod === 8) return zlib.inflateRawSync(compressed); return null; } offset = dataStart + compressedSize; } return null; } function extractDocxText(buffer) { const xmlBuffer = extractZipEntry(buffer, 'word/document.xml'); if (!xmlBuffer) return ''; const xml = xmlBuffer.toString('utf8'); const paragraphs = []; for (const block of xml.split('')) { const texts = [...block.matchAll(/]*>([\s\S]*?)<\/w:t>/g)].map((match) => match[1] .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') .replace(/"/g, '"'), ); const line = texts.join(''); if (line.trim()) paragraphs.push(line.trim()); } return paragraphs.join('\n\n'); } export function canPreviewAsset(mimeType) { return PREVIEWABLE_MIME_TYPES.has(mimeType); } export function renderAssetPreviewHtml({ asset, buffer, downloadUrl }) { const title = asset.displayName || asset.filename; const mimeType = asset.mimeType; if (mimeType === 'text/html') { const csp = ''; const html = buffer.toString('utf8'); if (/]*>/i.test(html)) { return html.replace(/]*)>/i, `${csp}`); } return `${csp}${html}`; } if (mimeType === 'application/pdf') { return previewDocument( title, ``, { downloadUrl }, ); } if (mimeType.startsWith('image/')) { return previewDocument(title, renderImageLightboxMarkup({ downloadUrl, title }), { downloadUrl, allowScripts: true, }); } if (mimeType === 'text/csv') { return previewDocument(title, renderCsvPreview(buffer.toString('utf8')), { downloadUrl }); } if (mimeType === 'text/markdown') { return previewDocument(title, renderMarkdownDocument(buffer.toString('utf8')), { downloadUrl }); } if (mimeType === 'text/plain') { return previewDocument(title, `
${escapeHtml(buffer.toString('utf8'))}
`, { downloadUrl }); } if (mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') { const text = extractDocxText(buffer); const body = text ? `
${text .split(/\n{2,}/) .map((paragraph) => `

${escapeHtml(paragraph)}

`) .join('')}
` : '

无法提取正文,请下载原文件查看。

'; return previewDocument(title, body, { downloadUrl }); } throw Object.assign(new Error('该资产不支持预览'), { code: 'preview_not_supported' }); }