| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- // 将"方案计划文档管理系统技术方案.md"输出为 docx
- // 正文:仿宋_GB2312、小四(12pt)、固定值28磅行距、首行缩进2字符
- // 标题:黑体、小四(12pt)、加粗
- // 列表:仿宋小四、悬挂缩进、与正文同字号行距
- const fs = require("fs");
- const path = require("path");
- const docx = require(path.join("C:/nvm4w/nodejs/node_modules/docx"));
- const {
- Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
- HeadingLevel, AlignmentType, BorderStyle, WidthType, ShadingType,
- PageBreak, LevelFormat, LineRuleType,
- } = docx;
- // ===== 格式常量 =====
- const FONT_BODY = "仿宋_GB2312";
- const FONT_HEAD = "黑体";
- const SIZE = 24; // 小四 = 12pt = 24 half-points
- const LINE = 560; // 固定值 28磅 = 28*20 = 560 twips
- const INDENT_FIRST = 480; // 首行缩进 2字符 ≈ 12pt*2*20 = 480
- // ===== 行内格式解析(**bold** 与 `code`)=====
- function parseInline(text) {
- const runs = [];
- const regex = /(\*\*[^*]+\*\*|`[^`]+`)/g;
- let last = 0, m;
- while ((m = regex.exec(text)) !== null) {
- if (m.index > last) runs.push({ text: text.slice(last, m.index) });
- const tok = m[0];
- if (tok.startsWith("**")) runs.push({ text: tok.slice(2, -2), bold: true });
- else if (tok.startsWith("`")) runs.push({ text: tok.slice(1, -1), code: true });
- last = m.index + tok.length;
- }
- if (last < text.length) runs.push({ text: text.slice(last) });
- return runs;
- }
- function makeRuns(text, { heading = false } = {}) {
- const font = heading ? FONT_HEAD : FONT_BODY;
- const items = parseInline(text);
- return items.map(r => new TextRun({
- text: r.text,
- bold: !!r.bold || heading,
- font: { name: r.code ? "Consolas" : font, eastAsia: font },
- size: SIZE,
- color: "000000",
- }));
- }
- // ===== 段落构造 =====
- function bodyPara(text) {
- return new Paragraph({
- spacing: { line: LINE, lineRule: LineRuleType.EXACT, before: 0, after: 0 },
- indent: { firstLine: INDENT_FIRST },
- alignment: AlignmentType.JUSTIFIED,
- children: makeRuns(text),
- });
- }
- function headingPara(text) {
- return new Paragraph({
- spacing: { line: LINE, lineRule: LineRuleType.EXACT, before: 120, after: 60 },
- indent: { firstLine: 0 },
- children: makeRuns(text, { heading: true }),
- });
- }
- function quotePara(text) {
- return new Paragraph({
- spacing: { line: LINE, lineRule: LineRuleType.EXACT, before: 0, after: 0 },
- indent: { left: 480, firstLine: 0 },
- alignment: AlignmentType.LEFT,
- children: makeRuns(text),
- });
- }
- function bulletItem(text) {
- // 无序列表项按用户要求:不加黑点、与普通正文段落一致(首行缩进2字符)
- return new Paragraph({
- spacing: { line: LINE, lineRule: LineRuleType.EXACT, before: 0, after: 0 },
- indent: { firstLine: INDENT_FIRST },
- alignment: AlignmentType.JUSTIFIED,
- children: makeRuns(text),
- });
- }
- function orderedItem(text, ref) {
- return new Paragraph({
- numbering: { reference: ref, level: 0 },
- spacing: { line: LINE, lineRule: LineRuleType.EXACT, before: 0, after: 0 },
- alignment: AlignmentType.JUSTIFIED,
- children: makeRuns(text),
- });
- }
- function separator() {
- // 横线:底边框的空段
- return new Paragraph({
- spacing: { line: LINE, lineRule: LineRuleType.EXACT, before: 0, after: 0 },
- border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: "888888", space: 1 } },
- children: [new TextRun({ text: "" })],
- });
- }
- // ===== Markdown 解析为段落对象 =====
- const SRC = path.join(__dirname, "方案计划文档管理系统技术方案.md");
- const OUT = path.join(__dirname, "方案计划文档管理系统技术方案.docx");
- const md = fs.readFileSync(SRC, "utf-8");
- const rawLines = md.split(/\r?\n/);
- const blocks = [];
- let orderCounter = 0;
- for (let i = 0; i < rawLines.length; i++) {
- const line = rawLines[i];
- if (!line.trim()) continue;
- if (/^-{3,}\s*$/.test(line)) {
- blocks.push({ type: "hr" });
- continue;
- }
- const hm = /^(#{1,6})\s+(.+)$/.exec(line);
- if (hm) {
- blocks.push({ type: "heading", text: hm[2].trim() });
- continue;
- }
- if (/^>\s?/.test(line)) {
- blocks.push({ type: "quote", text: line.replace(/^>\s?/, "") });
- continue;
- }
- if (/^[-*+]\s+/.test(line)) {
- blocks.push({ type: "bullet", text: line.replace(/^[-*+]\s+/, "") });
- continue;
- }
- const om = /^(\d+)\.\s+(.*)$/.exec(line);
- if (om) {
- const prev = rawLines[i - 1] || "";
- if (!/^\d+\.\s+/.test(prev)) orderCounter += 1;
- blocks.push({ type: "ordered", text: om[2], group: orderCounter });
- continue;
- }
- blocks.push({ type: "body", text: line });
- }
- // ===== 生成 docx children =====
- const children = [];
- for (const b of blocks) {
- switch (b.type) {
- case "heading":
- children.push(headingPara(b.text));
- break;
- case "quote":
- children.push(quotePara(b.text));
- break;
- case "body":
- children.push(bodyPara(b.text));
- break;
- case "bullet":
- children.push(bulletItem(b.text));
- break;
- case "ordered":
- children.push(orderedItem(b.text, `order-${b.group}`));
- break;
- case "hr":
- children.push(separator());
- break;
- }
- }
- // ===== 构造 numbering(仅用于有序列表;无序列表已改为普通段落样式)=====
- const numbering = {
- config: [
- ...Array.from({ length: orderCounter }, (_, k) => ({
- reference: `order-${k + 1}`,
- levels: [{
- level: 0,
- format: LevelFormat.DECIMAL,
- text: "%1.",
- alignment: AlignmentType.LEFT,
- style: { paragraph: { indent: { left: 720, hanging: 360 } } },
- }],
- })),
- ],
- };
- const doc = new Document({
- creator: "DMS",
- title: "方案计划文档管理系统技术方案",
- styles: {
- default: {
- document: {
- run: { font: { name: FONT_BODY, eastAsia: FONT_BODY }, size: SIZE, color: "000000" },
- paragraph: { spacing: { line: LINE, lineRule: LineRuleType.EXACT } },
- },
- },
- },
- numbering,
- sections: [{
- properties: {
- page: {
- size: { width: 11906, height: 16838 }, // A4
- margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 },
- },
- },
- children,
- }],
- });
- Packer.toBuffer(doc).then((buf) => {
- fs.writeFileSync(OUT, buf);
- console.log(`Generated: ${OUT}`);
- console.log(`Size: ${buf.length} bytes`);
- console.log(`Blocks: ${blocks.length}, ordered groups: ${orderCounter}`);
- }).catch((e) => {
- console.error("ERROR:", e);
- process.exit(1);
- });
|