/** * One-off: convert 企业端操作流程说明.md -> .docx */ const fs = require('fs') const path = require('path') const { Document, Packer, Paragraph, TextRun, HeadingLevel, Table, TableRow, TableCell, WidthType, BorderStyle, ImageRun, AlignmentType, ShadingType } = require('docx') const DOC_DIR = path.resolve(__dirname, '..') const MD_PATH = path.join(DOC_DIR, '企业端操作流程说明.md') const OUT_PATH = path.join(DOC_DIR, '企业端操作流程说明.docx') function parseInline(text) { const runs = [] const re = /(\*\*[^*]+\*\*|`[^`]+`)/g let last = 0 let m while ((m = re.exec(text)) !== null) { if (m.index > last) { runs.push(new TextRun({ text: text.slice(last, m.index), size: 21 })) } const token = m[0] if (token.startsWith('**')) { runs.push(new TextRun({ text: token.slice(2, -2), bold: true, size: 21 })) } else { runs.push(new TextRun({ text: token.slice(1, -1), font: 'Consolas', size: 18 })) } last = m.index + token.length } if (last < text.length) { runs.push(new TextRun({ text: text.slice(last), size: 21 })) } if (!runs.length) runs.push(new TextRun({ text: text || '', size: 21 })) return runs } function para(text, opts = {}) { return new Paragraph({ spacing: { after: 120, line: 360 }, ...opts, children: typeof text === 'string' ? parseInline(text) : text }) } function heading(text, level) { const map = { 1: HeadingLevel.HEADING_1, 2: HeadingLevel.HEADING_2, 3: HeadingLevel.HEADING_3, 4: HeadingLevel.HEADING_4 } return new Paragraph({ heading: map[level] || HeadingLevel.HEADING_2, spacing: { before: 240, after: 160 }, children: [new TextRun({ text, bold: true, size: level === 1 ? 32 : level === 2 ? 28 : 24 })] }) } function quotePara(text) { return new Paragraph({ spacing: { after: 100 }, indent: { left: 240 }, border: { left: { style: BorderStyle.SINGLE, size: 18, color: '93C5FD', space: 8 } }, children: parseInline(text.replace(/^>\s?/, '')) }) } function codePara(line) { return new Paragraph({ spacing: { after: 40 }, shading: { type: ShadingType.CLEAR, fill: 'F3F4F6' }, children: [new TextRun({ text: line || ' ', font: 'Consolas', size: 18 })] }) } function cell(text, opts = {}) { const isHeader = !!opts.header return new TableCell({ width: { size: opts.width || 3000, type: WidthType.DXA }, shading: isHeader ? { type: ShadingType.CLEAR, fill: 'DBEAFE' } : undefined, borders: { top: { style: BorderStyle.SINGLE, size: 4, color: 'CBD5E1' }, bottom: { style: BorderStyle.SINGLE, size: 4, color: 'CBD5E1' }, left: { style: BorderStyle.SINGLE, size: 4, color: 'CBD5E1' }, right: { style: BorderStyle.SINGLE, size: 4, color: 'CBD5E1' } }, children: [ new Paragraph({ spacing: { after: 40 }, children: parseInline(String(text || '').trim()).map((r) => { if (isHeader) { return new TextRun({ text: r.text || '', bold: true, size: 20 }) } return r }) }) ] }) } function parseTable(lines) { const rows = lines .filter((l) => !/^\|?\s*-+/.test(l.replace(/\|/g, '').trim() === '' ? 'x' : l) && !/^\|[\s\-:|]+\|$/.test(l)) .filter((l) => l.includes('|')) .map((l) => l .replace(/^\|/, '') .replace(/\|$/, '') .split('|') .map((c) => c.trim()) ) .filter((cols) => !cols.every((c) => /^:?-+:?$/.test(c))) if (!rows.length) return null const colCount = Math.max(...rows.map((r) => r.length)) const width = Math.floor(9000 / colCount) return new Table({ width: { size: 9000, type: WidthType.DXA }, rows: rows.map((cols, idx) => { while (cols.length < colCount) cols.push('') return new TableRow({ children: cols.map((c) => cell(c, { header: idx === 0, width })) }) }) }) } async function main() { const md = fs.readFileSync(MD_PATH, 'utf8') const lines = md.replace(/\r\n/g, '\n').split('\n') const children = [] let i = 0 while (i < lines.length) { const line = lines[i] const trimmed = line.trim() if (!trimmed) { i++ continue } if (trimmed === '---') { children.push( new Paragraph({ spacing: { before: 120, after: 120 }, border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: 'E5E7EB', space: 1 } }, children: [] }) ) i++ continue } if (trimmed.startsWith('```')) { i++ while (i < lines.length && !lines[i].trim().startsWith('```')) { children.push(codePara(lines[i])) i++ } i++ // closing ``` children.push(new Paragraph({ spacing: { after: 120 }, children: [] })) continue } if (trimmed.startsWith('|')) { const tableLines = [] while (i < lines.length && lines[i].trim().startsWith('|')) { tableLines.push(lines[i].trim()) i++ } const table = parseTable(tableLines) if (table) { children.push(table) children.push(new Paragraph({ spacing: { after: 160 }, children: [] })) } continue } const img = trimmed.match(/^!\[([^\]]*)\]\(([^)]+)\)/) if (img) { const imgPath = path.resolve(DOC_DIR, img[2]) if (fs.existsSync(imgPath)) { const buf = fs.readFileSync(imgPath) children.push( new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 160, after: 160 }, children: [ new ImageRun({ data: buf, transformation: { width: 420, height: 560 }, type: 'png' }) ] }) ) children.push( new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 200 }, children: [new TextRun({ text: img[1] || '流程图', italics: true, size: 18, color: '6B7280' })] }) ) } else { children.push(para(`[图片缺失] ${img[1]} (${img[2]})`)) } i++ continue } if (trimmed.startsWith('# ')) { children.push(heading(trimmed.slice(2), 1)) i++ continue } if (trimmed.startsWith('## ')) { children.push(heading(trimmed.slice(3), 2)) i++ continue } if (trimmed.startsWith('### ')) { children.push(heading(trimmed.slice(4), 3)) i++ continue } if (trimmed.startsWith('#### ')) { children.push(heading(trimmed.slice(5), 4)) i++ continue } if (trimmed.startsWith('>')) { while (i < lines.length && lines[i].trim().startsWith('>')) { children.push(quotePara(lines[i].trim())) i++ } continue } if (/^[-*]\s+/.test(trimmed)) { children.push( new Paragraph({ spacing: { after: 80 }, indent: { left: 360 }, children: [ new TextRun({ text: '• ', size: 21 }), ...parseInline(trimmed.replace(/^[-*]\s+/, '')) ] }) ) i++ continue } if (/^\d+\.\s+/.test(trimmed)) { const num = trimmed.match(/^(\d+)\.\s+/)[1] children.push( new Paragraph({ spacing: { after: 80 }, indent: { left: 240 }, children: [ new TextRun({ text: `${num}. `, size: 21 }), ...parseInline(trimmed.replace(/^\d+\.\s+/, '')) ] }) ) i++ continue } if (trimmed.startsWith('*') && trimmed.endsWith('*') && !trimmed.startsWith('**')) { children.push( new Paragraph({ spacing: { after: 100 }, children: [new TextRun({ text: trimmed.replace(/^\*|\*$/g, ''), italics: true, size: 18, color: '6B7280' })] }) ) i++ continue } children.push(para(trimmed)) i++ } const doc = new Document({ styles: { default: { document: { styles: [ { id: 'Normal', run: { font: 'Microsoft YaHei', size: 21 } } ] } } }, sections: [ { properties: { page: { margin: { top: 720, right: 720, bottom: 720, left: 720 } } }, children } ] }) const buffer = await Packer.toBuffer(doc) fs.writeFileSync(OUT_PATH, buffer) console.log('OK ->', OUT_PATH) console.log('bytes', buffer.length) } main().catch((e) => { console.error(e) process.exit(1) })