const W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'; const R_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'; const CONTENT_TYPES = ` `; const ROOT_RELS = ` `; const DOCUMENT_RELS = ` `; function escapeXml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function decodeHtmlEntities(value) { return String(value ?? '') .replace(/ /gi, ' ') .replace(/&/gi, '&') .replace(/</gi, '<') .replace(/>/gi, '>') .replace(/"/gi, '"') .replace(/'/gi, "'") .replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code))) .replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(Number.parseInt(code, 16))); } export function extractPlainTextFromHtml(html) { return decodeHtmlEntities( String(html ?? '') .replace(//gi, ' ') .replace(//gi, ' ') .replace(/<\/(?:p|div|section|article|header|footer|h[1-6]|li|tr|blockquote)>/gi, '\n') .replace(//gi, '\n') .replace(/<[^>]+>/g, ' ') .replace(/[ \t\r\f\v]+/g, ' ') .replace(/\n[ \t]+/g, '\n') .replace(/[ \t]+\n/g, '\n') .replace(/\n{3,}/g, '\n\n') .trim(), ); } function normalizeParagraphs(content) { return String(content ?? '') .split(/\n{1,}/) .map((line) => line.replace(/^#{1,6}\s+/, '').replace(/[*_`~]/g, '').trim()) .filter(Boolean) .slice(0, 500); } function paragraphXml(text, { heading = false } = {}) { const style = heading ? '' : ''; const runStyle = heading ? '' : ''; return `${style}${runStyle}${escapeXml(text)}`; } function documentXml({ title, paragraphs }) { const body = [ title ? paragraphXml(title, { heading: true }) : '', ...paragraphs.map((paragraph) => paragraphXml(paragraph)), ].join(''); return ` ${body} `; } const CRC_TABLE = (() => { const table = new Uint32Array(256); for (let n = 0; n < 256; n += 1) { let c = n; for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; table[n] = c >>> 0; } return table; })(); function crc32(buffer) { let crc = 0xffffffff; for (const byte of buffer) crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8); return (crc ^ 0xffffffff) >>> 0; } function dosDateTime(date = new Date()) { const year = Math.max(1980, date.getFullYear()); const dosTime = (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2); const dosDate = ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(); return { dosTime, dosDate }; } function writeZipEntry(name, content, offset) { const nameBuffer = Buffer.from(name); const data = Buffer.isBuffer(content) ? content : Buffer.from(String(content), 'utf8'); const checksum = crc32(data); const { dosTime, dosDate } = dosDateTime(); const local = Buffer.alloc(30 + nameBuffer.length); local.writeUInt32LE(0x04034b50, 0); local.writeUInt16LE(20, 4); local.writeUInt16LE(0, 6); local.writeUInt16LE(0, 8); local.writeUInt16LE(dosTime, 10); local.writeUInt16LE(dosDate, 12); local.writeUInt32LE(checksum, 14); local.writeUInt32LE(data.length, 18); local.writeUInt32LE(data.length, 22); local.writeUInt16LE(nameBuffer.length, 26); nameBuffer.copy(local, 30); const central = Buffer.alloc(46 + nameBuffer.length); central.writeUInt32LE(0x02014b50, 0); central.writeUInt16LE(20, 4); central.writeUInt16LE(20, 6); central.writeUInt16LE(0, 8); central.writeUInt16LE(0, 10); central.writeUInt16LE(dosTime, 12); central.writeUInt16LE(dosDate, 14); central.writeUInt32LE(checksum, 16); central.writeUInt32LE(data.length, 20); central.writeUInt32LE(data.length, 24); central.writeUInt16LE(nameBuffer.length, 28); central.writeUInt32LE(offset, 42); nameBuffer.copy(central, 46); return { local, data, central }; } function zip(entries) { let offset = 0; const locals = []; const centrals = []; for (const [name, content] of entries) { const entry = writeZipEntry(name, content, offset); locals.push(entry.local, entry.data); centrals.push(entry.central); offset += entry.local.length + entry.data.length; } const centralSize = centrals.reduce((sum, item) => sum + item.length, 0); const end = Buffer.alloc(22); end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(entries.length, 8); end.writeUInt16LE(entries.length, 10); end.writeUInt32LE(centralSize, 12); end.writeUInt32LE(offset, 16); return Buffer.concat([...locals, ...centrals, end]); } export function generateDocxBuffer({ title = '文档', content = '', contentFormat = 'markdown' } = {}) { const plain = contentFormat === 'html' ? extractPlainTextFromHtml(content) : String(content ?? ''); const paragraphs = normalizeParagraphs(plain); return zip([ ['[Content_Types].xml', CONTENT_TYPES], ['_rels/.rels', ROOT_RELS], ['word/document.xml', documentXml({ title, paragraphs: paragraphs.length > 0 ? paragraphs : [''] })], ['word/_rels/document.xml.rels', DOCUMENT_RELS], ]); }