/** * 软著材料 docx 生成脚本 * 1) 源程序:解析已生成的 txt,按显示宽度折行(每行 ≤84 列),每页 60 行, * 页眉左侧"软件全称 版本号"、右侧 Word 自动页码域"第 X 页 共 Y 页"。 * 2) 设计说明书:解析 md,生成封面 + 章节正文 docx。 * 用法:node gen_docx.mjs */ import fs from 'fs'; import path from 'path'; import { Document, Packer, Paragraph, TextRun, Header, AlignmentType, PageNumber, TabStopType, HeadingLevel, } from 'docx'; const DIR = import.meta.dirname; const SRC_TXT = path.join(DIR, '像素大勇者游戏软件_V1.0_源程序.txt'); const MANUAL_MD = path.join(DIR, '像素大勇者游戏软件_V1.0_设计说明书.md'); const OUT_SRC = path.join(DIR, '像素大勇者游戏软件_V1.0_源程序.docx'); const OUT_MANUAL = path.join(DIR, '像素大勇者游戏软件_V1.0_设计说明书.docx'); const SOFTWARE = '像素大勇者游戏软件 V1.0'; const LINES_PER_PAGE = 60; const FOLD_WIDTH = 84; // A4 折行限制:显示列宽(ASCII=1 列,中文=2 列) // A4 纵向,边距:上下 1.27cm,左右 1.5cm const PAGE = { size: { width: 11906, height: 16838 }, margin: { top: 720, bottom: 720, left: 850, right: 850, header: 400 }, }; const TEXT_WIDTH = PAGE.size.width - PAGE.margin.left - PAGE.margin.right; /** 统一页眉:左侧软件名+版本号,右侧自动页码域 */ function makeHeader() { return new Header({ children: [ new Paragraph({ tabStops: [{ type: TabStopType.RIGHT, position: TEXT_WIDTH }], children: [ new TextRun({ text: SOFTWARE, size: 18, font: { ascii: 'Consolas', eastAsia: 'SimSun' } }), new TextRun({ children: ['\t第 ', PageNumber.CURRENT, ' 页 共 ', PageNumber.TOTAL_PAGES, ' 页'], size: 18, font: { ascii: 'Consolas', eastAsia: 'SimSun' }, }), ], }), ], }); } // ============================================================ // 一、源程序 docx // ============================================================ /** 字符显示宽度(ASCII 记 1 列,全角/中文记 2 列) */ const charWidth = (ch) => (ch.charCodeAt(0) > 0xff ? 2 : 1); /** 超宽行按显示列宽折行,续行缩进 4 空格 */ function foldLine(line, width = FOLD_WIDTH) { if ([...line].reduce((w, c) => w + charWidth(c), 0) <= width) return [line]; const out = []; let cur = '', curW = 0; for (const ch of line) { const cw = charWidth(ch); if (curW + cw > width) { out.push(cur); cur = ' ' + ch; // 续行缩进 curW = 4 + cw; } else { cur += ch; curW += cw; } } if (cur) out.push(cur); return out; } /** 解析 txt:剔除页眉/分隔线/页间空行,还原连续代码行 */ function parseSourceTxt() { const raw = fs.readFileSync(SRC_TXT, 'utf8').split('\n'); const codeLines = []; let count = 0; for (const line of raw) { if (line.startsWith('像素大勇者游戏软件')) continue; // 页眉行 if (/^=+$/.test(line.trim()) && line.trim() !== '') continue; // 分隔线 if (count > 0 && count % LINES_PER_PAGE === 0 && line.trim() === '') continue; // 页间空行 codeLines.push(line); count++; } return codeLines; } async function buildSourceDocx() { const codeLines = parseSourceTxt(); // 折行后按每页 50 个显示行分页 const folded = codeLines.flatMap((l) => foldLine(l.replace(/\t/g, ' '))); const totalPages = Math.ceil(folded.length / LINES_PER_PAGE); const foldedPages = Math.ceil(codeLines.length / LINES_PER_PAGE); // 折行前理论页数(对照 txt) const children = folded.map((line, i) => new Paragraph({ pageBreakBefore: i > 0 && i % LINES_PER_PAGE === 0, spacing: { after: 0, line: 230, lineRule: 'auto' }, children: [ new TextRun({ text: line, size: 19, // 9.5pt font: { ascii: 'Consolas', eastAsia: 'SimSun' }, }), ], }) ); const doc = new Document({ sections: [{ properties: { page: PAGE }, headers: { default: makeHeader() }, children, }], }); await Packer.toBuffer(doc).then((buf) => fs.writeFileSync(OUT_SRC, buf)); console.log('=== 源程序 docx 生成完成 ==='); console.log(`代码行: ${codeLines.length},折行后显示行: ${folded.length}`); console.log(`折行前页数(对照txt): ${foldedPages},docx 实际页数: ${totalPages}`); console.log(`输出: ${OUT_SRC}`); } // ============================================================ // 二、设计说明书 docx // ============================================================ const BODY_FONT = { ascii: 'Times New Roman', eastAsia: 'SimSun' }; const HEAD_FONT = { ascii: 'Arial', eastAsia: 'SimHei' }; /** 解析 **粗体** 行内标记 */ function parseRuns(text, base = {}) { return text .split(/(\*\*[^*]+\*\*)/g) .filter(Boolean) .map((p) => p.startsWith('**') ? new TextRun({ text: p.slice(2, -2), bold: true, font: BODY_FONT, ...base }) : new TextRun({ text: p, font: BODY_FONT, ...base }) ); } async function buildManualDocx() { const md = fs.readFileSync(MANUAL_MD, 'utf8').split('\n'); const children = []; let coverDone = false; for (const rawLine of md) { const line = rawLine.trimEnd(); if (!coverDone) { // 封面区:软件名 / 文档名 / 著作权人 / 日期,居中排布,遇 --- 结束封面 if (line.startsWith('# ')) { children.push(new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 240, after: 240 }, children: [new TextRun({ text: line.slice(2), bold: true, size: 52, font: HEAD_FONT })], })); } else if (/^(著作权人|开发完成日期):/.test(line)) { children.push(new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 120, after: 120 }, children: [new TextRun({ text: line, size: 24, font: BODY_FONT })], })); } else if (line.trim() === '---') { coverDone = true; // 封面独立成页 children.push(new Paragraph({ children: [new TextRun({ text: '', size: 2 })], pageBreakBefore: false })); children.push(new Paragraph({ pageBreakBefore: true, children: [] })); } continue; } // 正文区 if (line.trim() === '' || line.trim() === '---') continue; if (line.startsWith('## ')) { children.push(new Paragraph({ heading: HeadingLevel.HEADING_1, spacing: { before: 240, after: 120 }, children: [new TextRun({ text: line.slice(3), bold: true, size: 30, font: HEAD_FONT, color: '000000' })], })); } else if (line.startsWith('### ')) { children.push(new Paragraph({ heading: HeadingLevel.HEADING_2, spacing: { before: 180, after: 100 }, children: [new TextRun({ text: line.slice(4), bold: true, size: 26, font: HEAD_FONT, color: '000000' })], })); } else if (line.startsWith('【建议插入截图')) { children.push(new Paragraph({ spacing: { before: 60, after: 60 }, indent: { left: 420 }, children: [new TextRun({ text: line, italics: true, color: '808080', size: 21, font: BODY_FONT })], })); } else if (/^\d+\.\s/.test(line)) { children.push(new Paragraph({ spacing: { after: 100 }, indent: { left: 420 }, children: parseRuns(line, { size: 21 }), })); } else { children.push(new Paragraph({ spacing: { after: 100, line: 300 }, children: parseRuns(line, { size: 21 }), })); } } const doc = new Document({ styles: { default: { document: { run: { font: BODY_FONT, size: 21 } }, }, }, sections: [{ properties: { page: PAGE }, headers: { default: makeHeader() }, children, }], }); await Packer.toBuffer(doc).then((buf) => fs.writeFileSync(OUT_MANUAL, buf)); console.log('=== 设计说明书 docx 生成完成 ==='); console.log(`输出: ${OUT_MANUAL}`); } await buildSourceDocx(); await buildManualDocx();