generate_docx.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. // 将"方案计划文档管理系统技术方案.md"输出为 docx
  2. // 正文:仿宋_GB2312、小四(12pt)、固定值28磅行距、首行缩进2字符
  3. // 标题:黑体、小四(12pt)、加粗
  4. // 列表:仿宋小四、悬挂缩进、与正文同字号行距
  5. const fs = require("fs");
  6. const path = require("path");
  7. const docx = require(path.join("C:/nvm4w/nodejs/node_modules/docx"));
  8. const {
  9. Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
  10. HeadingLevel, AlignmentType, BorderStyle, WidthType, ShadingType,
  11. PageBreak, LevelFormat, LineRuleType,
  12. } = docx;
  13. // ===== 格式常量 =====
  14. const FONT_BODY = "仿宋_GB2312";
  15. const FONT_HEAD = "黑体";
  16. const SIZE = 24; // 小四 = 12pt = 24 half-points
  17. const LINE = 560; // 固定值 28磅 = 28*20 = 560 twips
  18. const INDENT_FIRST = 480; // 首行缩进 2字符 ≈ 12pt*2*20 = 480
  19. // ===== 行内格式解析(**bold** 与 `code`)=====
  20. function parseInline(text) {
  21. const runs = [];
  22. const regex = /(\*\*[^*]+\*\*|`[^`]+`)/g;
  23. let last = 0, m;
  24. while ((m = regex.exec(text)) !== null) {
  25. if (m.index > last) runs.push({ text: text.slice(last, m.index) });
  26. const tok = m[0];
  27. if (tok.startsWith("**")) runs.push({ text: tok.slice(2, -2), bold: true });
  28. else if (tok.startsWith("`")) runs.push({ text: tok.slice(1, -1), code: true });
  29. last = m.index + tok.length;
  30. }
  31. if (last < text.length) runs.push({ text: text.slice(last) });
  32. return runs;
  33. }
  34. function makeRuns(text, { heading = false } = {}) {
  35. const font = heading ? FONT_HEAD : FONT_BODY;
  36. const items = parseInline(text);
  37. return items.map(r => new TextRun({
  38. text: r.text,
  39. bold: !!r.bold || heading,
  40. font: { name: r.code ? "Consolas" : font, eastAsia: font },
  41. size: SIZE,
  42. color: "000000",
  43. }));
  44. }
  45. // ===== 段落构造 =====
  46. function bodyPara(text) {
  47. return new Paragraph({
  48. spacing: { line: LINE, lineRule: LineRuleType.EXACT, before: 0, after: 0 },
  49. indent: { firstLine: INDENT_FIRST },
  50. alignment: AlignmentType.JUSTIFIED,
  51. children: makeRuns(text),
  52. });
  53. }
  54. function headingPara(text) {
  55. return new Paragraph({
  56. spacing: { line: LINE, lineRule: LineRuleType.EXACT, before: 120, after: 60 },
  57. indent: { firstLine: 0 },
  58. children: makeRuns(text, { heading: true }),
  59. });
  60. }
  61. function quotePara(text) {
  62. return new Paragraph({
  63. spacing: { line: LINE, lineRule: LineRuleType.EXACT, before: 0, after: 0 },
  64. indent: { left: 480, firstLine: 0 },
  65. alignment: AlignmentType.LEFT,
  66. children: makeRuns(text),
  67. });
  68. }
  69. function bulletItem(text) {
  70. // 无序列表项按用户要求:不加黑点、与普通正文段落一致(首行缩进2字符)
  71. return new Paragraph({
  72. spacing: { line: LINE, lineRule: LineRuleType.EXACT, before: 0, after: 0 },
  73. indent: { firstLine: INDENT_FIRST },
  74. alignment: AlignmentType.JUSTIFIED,
  75. children: makeRuns(text),
  76. });
  77. }
  78. function orderedItem(text, ref) {
  79. return new Paragraph({
  80. numbering: { reference: ref, level: 0 },
  81. spacing: { line: LINE, lineRule: LineRuleType.EXACT, before: 0, after: 0 },
  82. alignment: AlignmentType.JUSTIFIED,
  83. children: makeRuns(text),
  84. });
  85. }
  86. function separator() {
  87. // 横线:底边框的空段
  88. return new Paragraph({
  89. spacing: { line: LINE, lineRule: LineRuleType.EXACT, before: 0, after: 0 },
  90. border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: "888888", space: 1 } },
  91. children: [new TextRun({ text: "" })],
  92. });
  93. }
  94. // ===== Markdown 解析为段落对象 =====
  95. const SRC = path.join(__dirname, "方案计划文档管理系统技术方案.md");
  96. const OUT = path.join(__dirname, "方案计划文档管理系统技术方案.docx");
  97. const md = fs.readFileSync(SRC, "utf-8");
  98. const rawLines = md.split(/\r?\n/);
  99. const blocks = [];
  100. let orderCounter = 0;
  101. for (let i = 0; i < rawLines.length; i++) {
  102. const line = rawLines[i];
  103. if (!line.trim()) continue;
  104. if (/^-{3,}\s*$/.test(line)) {
  105. blocks.push({ type: "hr" });
  106. continue;
  107. }
  108. const hm = /^(#{1,6})\s+(.+)$/.exec(line);
  109. if (hm) {
  110. blocks.push({ type: "heading", text: hm[2].trim() });
  111. continue;
  112. }
  113. if (/^>\s?/.test(line)) {
  114. blocks.push({ type: "quote", text: line.replace(/^>\s?/, "") });
  115. continue;
  116. }
  117. if (/^[-*+]\s+/.test(line)) {
  118. blocks.push({ type: "bullet", text: line.replace(/^[-*+]\s+/, "") });
  119. continue;
  120. }
  121. const om = /^(\d+)\.\s+(.*)$/.exec(line);
  122. if (om) {
  123. const prev = rawLines[i - 1] || "";
  124. if (!/^\d+\.\s+/.test(prev)) orderCounter += 1;
  125. blocks.push({ type: "ordered", text: om[2], group: orderCounter });
  126. continue;
  127. }
  128. blocks.push({ type: "body", text: line });
  129. }
  130. // ===== 生成 docx children =====
  131. const children = [];
  132. for (const b of blocks) {
  133. switch (b.type) {
  134. case "heading":
  135. children.push(headingPara(b.text));
  136. break;
  137. case "quote":
  138. children.push(quotePara(b.text));
  139. break;
  140. case "body":
  141. children.push(bodyPara(b.text));
  142. break;
  143. case "bullet":
  144. children.push(bulletItem(b.text));
  145. break;
  146. case "ordered":
  147. children.push(orderedItem(b.text, `order-${b.group}`));
  148. break;
  149. case "hr":
  150. children.push(separator());
  151. break;
  152. }
  153. }
  154. // ===== 构造 numbering(仅用于有序列表;无序列表已改为普通段落样式)=====
  155. const numbering = {
  156. config: [
  157. ...Array.from({ length: orderCounter }, (_, k) => ({
  158. reference: `order-${k + 1}`,
  159. levels: [{
  160. level: 0,
  161. format: LevelFormat.DECIMAL,
  162. text: "%1.",
  163. alignment: AlignmentType.LEFT,
  164. style: { paragraph: { indent: { left: 720, hanging: 360 } } },
  165. }],
  166. })),
  167. ],
  168. };
  169. const doc = new Document({
  170. creator: "DMS",
  171. title: "方案计划文档管理系统技术方案",
  172. styles: {
  173. default: {
  174. document: {
  175. run: { font: { name: FONT_BODY, eastAsia: FONT_BODY }, size: SIZE, color: "000000" },
  176. paragraph: { spacing: { line: LINE, lineRule: LineRuleType.EXACT } },
  177. },
  178. },
  179. },
  180. numbering,
  181. sections: [{
  182. properties: {
  183. page: {
  184. size: { width: 11906, height: 16838 }, // A4
  185. margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 },
  186. },
  187. },
  188. children,
  189. }],
  190. });
  191. Packer.toBuffer(doc).then((buf) => {
  192. fs.writeFileSync(OUT, buf);
  193. console.log(`Generated: ${OUT}`);
  194. console.log(`Size: ${buf.length} bytes`);
  195. console.log(`Blocks: ${blocks.length}, ordered groups: ${orderCounter}`);
  196. }).catch((e) => {
  197. console.error("ERROR:", e);
  198. process.exit(1);
  199. });