| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- /**
- * 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)
- })
|