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} `; function escapeHtml(text) { return String(text ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function previewDocument(title, bodyHtml, { downloadUrl = null, extraHead = '' } = {}) { 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'", "script-src 'none'", ].join('; '); const downloadLink = downloadUrl ? `

下载原文件

` : ''; return `${escapeHtml(title)}${extraHead}

${escapeHtml(title)}

${downloadLink}${bodyHtml}`; } 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, `${escapeHtml(title)}`, { downloadUrl }, ); } 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' }); }