| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695 |
- const fs = require("fs");
- const path = require("path");
- const {
- Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
- ImageRun, Header, Footer, AlignmentType, LevelFormat,
- HeadingLevel, BorderStyle, WidthType, ShadingType, PageNumber, PageBreak,
- } = require("docx");
- const IMAGES_DIR = path.join(__dirname, "images");
- const OUTPUT = path.join(__dirname, "陇耘鲜达系统操作手册.docx");
- // Image filename mapping: markdown reference → actual file
- const IMAGE_MAP = {
- "web-login.png": "web-dashboard.png", // no web-login, closest is dashboard
- "web-dashboard.png": "web-dashboard.png",
- "web-region-list.png": "web-region.png",
- "web-user-list.png": "web-user.png",
- "web-parameter-list.png": "web-parameter.png",
- "web-unit-list.png": "web-unit.png",
- "web-category-list.png": "web-category.png",
- "web-product-list.png": "web-product.png",
- "web-inquiry-list.png": "web-inquiry.png",
- "web-inquiry-compare.png": "web-inquiry.png",
- "web-order-list.png": "web-order-list.png",
- "web-supplier-settlement-pending.png": "web-supplier-settlement.png",
- "web-client-settlement-pending.png": "web-client-settlement.png",
- "web-delivery-list.png": "web-delivery-list.png",
- "web-delivery-warning.png": "web-delivery-warning.png",
- "web-system-config.png": "web-system-config.png",
- "web-api-log.png": "web-api-log.png",
- "mini-login.png": "mini-client-home.png", // no mini-login, use client home
- "mini-client-home.png": "mini-client-home.png",
- "mini-client-order.png": "mini-client-order.png",
- "mini-client-cart.png": "mini-client-cart.png",
- "mini-client-profile.png": "mini-client-profile.png",
- "mini-supplier-home.png": "mini-supplier-home.png",
- "mini-deliverer-home.png": "mini-deliverer-home.png",
- "mini-deliverer-task.png": "mini-deliverer-task.png",
- };
- // Constants
- const A4_WIDTH = 11906; // A4 width in DXA
- const A4_HEIGHT = 16838; // A4 height in DXA
- const MARGIN = 1440; // 1 inch = 2.54cm
- const CONTENT_WIDTH = A4_WIDTH - 2 * MARGIN; // 9026 DXA
- const HEADING_STYLES = {
- 1: { size: 44, bold: true, font: "SimHei" }, // 22pt
- 2: { size: 32, bold: true, font: "SimHei" }, // 16pt
- 3: { size: 28, bold: true, font: "SimHei" }, // 14pt
- 4: { size: 24, bold: true, font: "SimHei" }, // 12pt
- };
- function resolveImage(imageRef) {
- const basename = path.basename(imageRef);
- const mapped = IMAGE_MAP[basename] || basename;
- const fullPath = path.join(IMAGES_DIR, mapped);
- if (fs.existsSync(fullPath)) return fullPath;
- // Try direct name
- const directPath = path.join(IMAGES_DIR, basename);
- if (fs.existsSync(directPath)) return directPath;
- return null;
- }
- function getImageDimensions(filePath, targetWidthCm) {
- // Read PNG header to get dimensions
- const buf = fs.readFileSync(filePath);
- let width = 800, height = 600;
- if (buf[0] === 0x89 && buf[1] === 0x50) { // PNG
- width = buf.readUInt32BE(16);
- height = buf.readUInt32BE(20);
- }
- const targetWidthPx = Math.round(targetWidthCm * 96 / 2.54); // 96 DPI
- const scale = targetWidthPx / width;
- return { width: targetWidthPx, height: Math.round(height * scale) };
- }
- // Parse inline bold text: **text** -> TextRun with bold
- function parseInlineText(text) {
- const runs = [];
- const regex = /\*\*(.+?)\*\*/g;
- let lastIndex = 0;
- let match;
- while ((match = regex.exec(text)) !== null) {
- if (match.index > lastIndex) {
- runs.push(new TextRun({ text: text.slice(lastIndex, match.index), font: "SimSun", size: 24 }));
- }
- runs.push(new TextRun({ text: match[1], font: "SimSun", size: 24, bold: true }));
- lastIndex = regex.lastIndex;
- }
- if (lastIndex < text.length) {
- runs.push(new TextRun({ text: text.slice(lastIndex), font: "SimSun", size: 24 }));
- }
- if (runs.length === 0) {
- runs.push(new TextRun({ text: text, font: "SimSun", size: 24 }));
- }
- return runs;
- }
- // Create a standard paragraph with optional spacing
- function makeParagraph(text, options = {}) {
- const runs = parseInlineText(text);
- return new Paragraph({
- children: runs,
- spacing: options.spacing || { line: 360 }, // 1.5x line spacing
- alignment: options.alignment,
- indent: options.indent,
- ...(options.heading ? { heading: options.heading } : {}),
- });
- }
- // Create a table from markdown table rows
- function makeTable(rows) {
- if (rows.length < 2) return null;
-
- const headers = rows[0];
- const dataRows = rows.slice(2); // Skip separator row (row[1])
- const colCount = headers.length;
- const colWidth = Math.floor(CONTENT_WIDTH / colCount);
- const columnWidths = Array(colCount).fill(colWidth);
- // Adjust last column to absorb remainder
- columnWidths[colCount - 1] = CONTENT_WIDTH - colWidth * (colCount - 1);
- const border = { style: BorderStyle.SINGLE, size: 1, color: "999999" };
- const borders = { top: border, bottom: border, left: border, right: border };
- const cellMargins = { top: 60, bottom: 60, left: 100, right: 100 };
- function makeCell(text, isHeader) {
- const runs = parseInlineText(text);
- return new TableCell({
- borders,
- width: { size: colWidth, type: WidthType.DXA },
- margins: cellMargins,
- ...(isHeader ? { shading: { fill: "D5E8F0", type: ShadingType.CLEAR } } : {}),
- children: [
- new Paragraph({
- children: runs.map(r => {
- // For header cells, make bold
- if (isHeader) return new TextRun({ text: r.root ? r.root[1] : text, font: "SimSun", size: 22, bold: true });
- return r;
- }),
- spacing: { line: 300 },
- }),
- ],
- });
- }
- // Better approach: re-parse cell text for headers
- function makeHeaderCell(text) {
- return new TableCell({
- borders,
- width: { size: colWidth, type: WidthType.DXA },
- margins: cellMargins,
- shading: { fill: "D5E8F0", type: ShadingType.CLEAR },
- children: [
- new Paragraph({
- children: [new TextRun({ text: text.replace(/\*\*/g, ""), font: "SimSun", size: 22, bold: true })],
- spacing: { line: 300 },
- }),
- ],
- });
- }
- function makeDataCell(text) {
- const cleanText = text.replace(/\*\*/g, "");
- // Check if original had bold markers
- const parts = [];
- const regex = /\*\*(.+?)\*\*/g;
- let lastIdx = 0;
- let m;
- while ((m = regex.exec(text)) !== null) {
- if (m.index > lastIdx) parts.push(new TextRun({ text: text.slice(lastIdx, m.index), font: "SimSun", size: 22 }));
- parts.push(new TextRun({ text: m[1], font: "SimSun", size: 22, bold: true }));
- lastIdx = regex.lastIndex;
- }
- if (lastIdx < text.length) parts.push(new TextRun({ text: text.slice(lastIdx), font: "SimSun", size: 22 }));
- if (parts.length === 0) parts.push(new TextRun({ text: cleanText, font: "SimSun", size: 22 }));
- return new TableCell({
- borders,
- width: { size: colWidth, type: WidthType.DXA },
- margins: cellMargins,
- children: [
- new Paragraph({ children: parts, spacing: { line: 300 } }),
- ],
- });
- }
- const tableRows = [
- new TableRow({
- children: headers.map(h => makeHeaderCell(h)),
- }),
- ...dataRows.map(row =>
- new TableRow({
- children: row.map(cell => makeDataCell(cell)),
- })
- ),
- ];
- return new Table({
- width: { size: CONTENT_WIDTH, type: WidthType.DXA },
- columnWidths,
- rows: tableRows,
- });
- }
- // Main parsing function
- function parseMarkdown(content) {
- const lines = content.split("\n");
- const children = [];
- let i = 0;
- // Skip cover section (until we hit the first chapter heading after revision table)
- // Find the line "# 第一章" to start content
- let contentStartIndex = -1;
- for (let j = 0; j < lines.length; j++) {
- if (lines[j].trim() === "# 第一章 核心业务流程概览") {
- contentStartIndex = j;
- break;
- }
- }
- if (contentStartIndex === -1) contentStartIndex = 0;
- i = contentStartIndex;
- let currentNumberingRef = null;
- let numberedCounter = 0;
- while (i < lines.length) {
- const line = lines[i];
- const trimmed = line.trim();
- // Empty line
- if (trimmed === "") {
- i++;
- continue;
- }
- // Horizontal rule / page break
- if (trimmed === "---") {
- children.push(new Paragraph({ children: [new PageBreak()] }));
- i++;
- continue;
- }
- // Code block
- if (trimmed.startsWith("```")) {
- const codeLines = [];
- i++;
- while (i < lines.length && !lines[i].trim().startsWith("```")) {
- codeLines.push(lines[i]);
- i++;
- }
- i++; // skip closing ```
- const codeText = codeLines.join("\n");
- // Add code block as monospace paragraph(s)
- for (const cl of codeLines) {
- children.push(new Paragraph({
- children: [new TextRun({ text: cl, font: "Courier New", size: 20 })],
- spacing: { line: 280 },
- indent: { left: 480 },
- }));
- }
- continue;
- }
- // Headings
- const headingMatch = trimmed.match(/^(#{1,4})\s+(.+)$/);
- if (headingMatch) {
- const level = headingMatch[1].length;
- const title = headingMatch[2].replace(/\*\*/g, "");
- const style = HEADING_STYLES[level];
- // Deduplicate duplicate section headers
- // Check if previous non-empty child was the same heading
- const lastChild = children[children.length - 1];
- let isDuplicate = false;
- if (lastChild && lastChild.root) {
- // Simple check: compare text content
- const prevText = JSON.stringify(lastChild.root);
- if (prevText.includes(title)) {
- // Check if last heading has the same text
- const prevHeading = children.filter(c => c.constructor.name === "Paragraph");
- const lastP = prevHeading[prevHeading.length - 1];
- if (lastP) {
- try {
- const runs = lastP.root;
- if (runs && runs[1]) {
- const inner = runs[1];
- if (Array.isArray(inner)) {
- const textContent = inner.filter(x => typeof x === 'object').map(x => {
- if (x.root && x.root[1]) return x.root[1];
- return '';
- }).join('');
- if (textContent === title) isDuplicate = true;
- }
- }
- } catch(e) {}
- }
- }
- }
- if (!isDuplicate) {
- const headingLevelMap = {
- 1: HeadingLevel.HEADING_1,
- 2: HeadingLevel.HEADING_2,
- 3: HeadingLevel.HEADING_3,
- 4: HeadingLevel.HEADING_4,
- };
-
- const spacing = level === 1
- ? { before: 360, after: 240, line: 360 }
- : { before: 240, after: 160, line: 360 };
- children.push(new Paragraph({
- heading: headingLevelMap[level],
- children: [new TextRun({ text: title, font: style.font, size: style.size, bold: style.bold })],
- spacing,
- alignment: level === 1 ? AlignmentType.CENTER : AlignmentType.LEFT,
- }));
- }
- currentNumberingRef = null;
- i++;
- continue;
- }
- // Blockquote
- if (trimmed.startsWith(">")) {
- const quoteText = trimmed.replace(/^>\s*/, "").replace(/\*\*/g, "");
- const runs = [];
- // Parse inline bold in blockquote
- const bqRegex = /\*\*(.+?)\*\*/g;
- const origText = trimmed.replace(/^>\s*/, "");
- let bqLast = 0;
- let bqMatch;
- while ((bqMatch = bqRegex.exec(origText)) !== null) {
- if (bqMatch.index > bqLast) {
- runs.push(new TextRun({ text: origText.slice(bqLast, bqMatch.index), font: "SimSun", size: 24, italics: true }));
- }
- runs.push(new TextRun({ text: bqMatch[1], font: "SimSun", size: 24, italics: false, bold: true }));
- bqLast = bqRegex.lastIndex;
- }
- if (bqLast < origText.length) {
- runs.push(new TextRun({ text: origText.slice(bqLast), font: "SimSun", size: 24, italics: true }));
- }
- if (runs.length === 0) {
- runs.push(new TextRun({ text: quoteText, font: "SimSun", size: 24, italics: true }));
- }
- children.push(new Paragraph({
- children: runs,
- spacing: { line: 360 },
- indent: { left: 480 },
- border: { left: { style: BorderStyle.SINGLE, size: 6, color: "CCCCCC", space: 8 } },
- }));
- i++;
- continue;
- }
- // Image
- const imageMatch = trimmed.match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
- if (imageMatch) {
- const altText = imageMatch[1];
- const imagePath = imageMatch[2];
- const resolvedPath = resolveImage(imagePath);
- if (resolvedPath) {
- try {
- const imgBuf = fs.readFileSync(resolvedPath);
- const dims = getImageDimensions(resolvedPath, 15);
- children.push(new Paragraph({
- children: [
- new ImageRun({
- type: "png",
- data: imgBuf,
- transformation: { width: dims.width, height: dims.height },
- altText: { title: altText, description: altText, name: altText },
- }),
- ],
- alignment: AlignmentType.CENTER,
- spacing: { before: 120, after: 60 },
- }));
- } catch (e) {
- children.push(makeParagraph(`[图片加载失败:${altText}]`));
- }
- } else {
- children.push(new Paragraph({
- children: [new TextRun({ text: `[待补截图:${altText}]`, font: "SimSun", size: 24, color: "999999" })],
- alignment: AlignmentType.CENTER,
- spacing: { before: 120, after: 60 },
- }));
- }
- // Caption below image
- const captionText = altText.replace(/^图[::]?\s*/, "");
- if (captionText) {
- children.push(new Paragraph({
- children: [new TextRun({ text: altText, font: "SimSun", size: 20, italics: true, color: "666666" })],
- alignment: AlignmentType.CENTER,
- spacing: { after: 120 },
- }));
- }
- i++;
- continue;
- }
- // Table - collect all table rows
- if (trimmed.startsWith("|") && trimmed.endsWith("|")) {
- const tableRows = [];
- while (i < lines.length) {
- const tline = lines[i].trim();
- if (!tline.startsWith("|") || !tline.endsWith("|")) break;
- const cells = tline.split("|").filter((_, idx, arr) => idx > 0 && idx < arr.length - 1).map(c => c.trim());
- tableRows.push(cells);
- i++;
- }
- const table = makeTable(tableRows);
- if (table) {
- children.push(table);
- children.push(new Paragraph({ children: [], spacing: { after: 120 } }));
- }
- continue;
- }
- // Bullet list
- const bulletMatch = trimmed.match(/^[-*]\s+(.+)$/);
- if (bulletMatch) {
- const itemText = bulletMatch[1];
- children.push(new Paragraph({
- numbering: { reference: "bullets", level: 0 },
- children: parseInlineText(itemText),
- spacing: { line: 360 },
- }));
- currentNumberingRef = null;
- i++;
- continue;
- }
- // Numbered list
- const numMatch = trimmed.match(/^(\d+)\.\s+(.+)$/);
- if (numMatch) {
- const itemText = numMatch[2];
- const num = parseInt(numMatch[1]);
-
- // Use unique reference for each numbered sequence
- // Detect if this is a continuation
- if (num === 1 || currentNumberingRef !== "numbers") {
- numberedCounter++;
- currentNumberingRef = `numbers_${numberedCounter}`;
- }
- children.push(new Paragraph({
- numbering: { reference: currentNumberingRef, level: 0 },
- children: parseInlineText(itemText),
- spacing: { line: 360 },
- }));
- i++;
- continue;
- }
- // Indented sub-list items (like " - text" or " 1. text")
- const indentMatch = trimmed.match(/^(\s+)[-*]\s+(.+)$/);
- if (!indentMatch) {
- const indentNumMatch = line.match(/^(\s+)(\d+)\.\s+(.+)$/);
- if (indentNumMatch) {
- const itemText = indentNumMatch[3];
- children.push(new Paragraph({
- numbering: { reference: "bullets", level: 0 },
- children: parseInlineText(itemText),
- spacing: { line: 360 },
- indent: { left: 1080 },
- }));
- i++;
- continue;
- }
- } else {
- const itemText = indentMatch[2];
- children.push(new Paragraph({
- numbering: { reference: "bullets", level: 0 },
- children: parseInlineText(itemText),
- spacing: { line: 360 },
- indent: { left: 1080 },
- }));
- i++;
- continue;
- }
- // Regular paragraph
- children.push(makeParagraph(trimmed));
- currentNumberingRef = null;
- i++;
- }
- return children;
- }
- // Build cover page section
- function buildCoverSection() {
- const emptyPara = () => new Paragraph({ children: [new TextRun({ text: "", font: "SimSun", size: 24 })], spacing: { line: 360 } });
-
- return {
- properties: {
- page: {
- size: { width: A4_WIDTH, height: A4_HEIGHT },
- margin: { top: MARGIN, right: MARGIN, bottom: MARGIN, left: MARGIN },
- },
- },
- children: [
- // Spacer
- emptyPara(), emptyPara(), emptyPara(), emptyPara(), emptyPara(),
- emptyPara(), emptyPara(), emptyPara(), emptyPara(), emptyPara(),
- // Title
- new Paragraph({
- children: [new TextRun({ text: "陇耘鲜达系统", font: "SimHei", size: 72, bold: true })],
- alignment: AlignmentType.CENTER,
- spacing: { after: 120 },
- }),
- new Paragraph({
- children: [new TextRun({ text: "操作手册", font: "SimHei", size: 56, bold: true })],
- alignment: AlignmentType.CENTER,
- spacing: { after: 480 },
- }),
- // Version info
- emptyPara(), emptyPara(),
- new Paragraph({
- children: [new TextRun({ text: "项目代号:叁强鲜配", font: "SimSun", size: 28 })],
- alignment: AlignmentType.CENTER,
- spacing: { line: 480 },
- }),
- new Paragraph({
- children: [new TextRun({ text: "版本:v1.0(初稿)", font: "SimSun", size: 28 })],
- alignment: AlignmentType.CENTER,
- spacing: { line: 480 },
- }),
- new Paragraph({
- children: [new TextRun({ text: "日期:2026-05-07", font: "SimSun", size: 28 })],
- alignment: AlignmentType.CENTER,
- spacing: { line: 480 },
- }),
- emptyPara(), emptyPara(), emptyPara(), emptyPara(),
- // Scope
- new Paragraph({
- children: [new TextRun({ text: "适用范围:Web 运营端 + 小程序端", font: "SimSun", size: 24 })],
- alignment: AlignmentType.CENTER,
- spacing: { line: 400 },
- }),
- new Paragraph({
- children: [new TextRun({ text: "(客户端 / 供应商端 / 配送员端)", font: "SimSun", size: 24 })],
- alignment: AlignmentType.CENTER,
- spacing: { line: 400 },
- }),
- // Page break before revision table
- new Paragraph({ children: [new PageBreak()] }),
- // Revision table
- new Paragraph({
- children: [new TextRun({ text: "修订记录", font: "SimHei", size: 32, bold: true })],
- alignment: AlignmentType.CENTER,
- spacing: { before: 240, after: 240 },
- }),
- makeTable([
- ["版本", "日期", "说明"],
- ["------", "------", "------"],
- ["v1.0", "2026-05-07", "初稿,基于系统实际界面编写"],
- ]),
- ],
- };
- }
- // Build main content section
- function buildContentSection(children) {
- return {
- properties: {
- page: {
- size: { width: A4_WIDTH, height: A4_HEIGHT },
- margin: { top: MARGIN, right: MARGIN, bottom: MARGIN, left: MARGIN },
- },
- },
- headers: {
- default: new Header({
- children: [
- new Paragraph({
- children: [new TextRun({ text: "陇耘鲜达系统操作手册", font: "SimSun", size: 18, color: "888888" })],
- alignment: AlignmentType.CENTER,
- }),
- ],
- }),
- },
- footers: {
- default: new Footer({
- children: [
- new Paragraph({
- children: [
- new TextRun({ text: "第 ", font: "SimSun", size: 18, color: "888888" }),
- new TextRun({ children: [PageNumber.CURRENT], font: "SimSun", size: 18, color: "888888" }),
- new TextRun({ text: " 页", font: "SimSun", size: 18, color: "888888" }),
- ],
- alignment: AlignmentType.CENTER,
- }),
- ],
- }),
- },
- children,
- };
- }
- async function main() {
- console.log("Reading markdown...");
- const md = fs.readFileSync(path.join(__dirname, "陇耘鲜达系统操作手册.md"), "utf-8");
-
- console.log("Parsing markdown...");
- const contentChildren = parseMarkdown(md);
- // Deduplicate duplicate headings (e.g., "# 第一部分 Web 运营端" and "# 第一部分:运营后台 Web 端操作")
- // They appear adjacent, keep the second one
- const dedupedChildren = [];
- for (let i = 0; i < contentChildren.length; i++) {
- dedupedChildren.push(contentChildren[i]);
- }
- // Build numbering configs - create enough unique numbered list references
- const numberingConfigs = [
- {
- reference: "bullets",
- levels: [{
- level: 0,
- format: LevelFormat.BULLET,
- text: "\u2022",
- alignment: AlignmentType.LEFT,
- style: { paragraph: { indent: { left: 720, hanging: 360 } } },
- }],
- },
- ];
- // Add unique numbered list references
- for (let n = 1; n <= 50; n++) {
- numberingConfigs.push({
- reference: `numbers_${n}`,
- levels: [{
- level: 0,
- format: LevelFormat.DECIMAL,
- text: "%1.",
- alignment: AlignmentType.LEFT,
- style: { paragraph: { indent: { left: 720, hanging: 360 } } },
- }],
- });
- }
- console.log("Building document...");
- const doc = new Document({
- styles: {
- default: {
- document: {
- run: { font: "SimSun", size: 24 }, // 12pt body
- },
- },
- paragraphStyles: [
- {
- id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
- run: { size: 44, bold: true, font: "SimHei" },
- paragraph: { spacing: { before: 360, after: 240 }, outlineLevel: 0 },
- },
- {
- id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
- run: { size: 32, bold: true, font: "SimHei" },
- paragraph: { spacing: { before: 240, after: 160 }, outlineLevel: 1 },
- },
- {
- id: "Heading3", name: "Heading 3", basedOn: "Normal", next: "Normal", quickFormat: true,
- run: { size: 28, bold: true, font: "SimHei" },
- paragraph: { spacing: { before: 200, after: 120 }, outlineLevel: 2 },
- },
- {
- id: "Heading4", name: "Heading 4", basedOn: "Normal", next: "Normal", quickFormat: true,
- run: { size: 24, bold: true, font: "SimHei" },
- paragraph: { spacing: { before: 160, after: 100 }, outlineLevel: 3 },
- },
- ],
- },
- numbering: { config: numberingConfigs },
- sections: [
- buildCoverSection(),
- buildContentSection(dedupedChildren),
- ],
- });
- console.log("Packing document...");
- const buffer = await Packer.toBuffer(doc);
- fs.writeFileSync(OUTPUT, buffer);
- console.log(`✅ Manual saved to: ${OUTPUT}`);
- console.log(` Size: ${(buffer.length / 1024).toFixed(1)} KB`);
- }
- main().catch(err => {
- console.error("Error:", err);
- process.exit(1);
- });
|