From b21365a991a861808ead8e6b72a60d97da5d3b91 Mon Sep 17 00:00:00 2001 From: mizzlelover Date: Thu, 10 Sep 2026 20:50:22 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E5=85=AC=E6=96=87=E7=89=B9?= =?UTF-8?q?=E5=AE=9A=E6=A0=BC=E5=BC=8F=E4=B8=8E=E7=89=88=E5=A4=B4=E8=A6=81?= =?UTF-8?q?=E7=B4=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- SKILL.md | 4 +- dist/traework-gongwen-skill.zip | Bin 23952 -> 27791 bytes marketing/x-v2-release.md | 6 + references/formal-checklist.md | 6 +- references/gbt9704-2012-summary.md | 7 +- scripts/generate_gongwen_docx.mjs | 192 ++++++++++++++++++++++------- scripts/verify_gongwen_docx.mjs | 62 +++++++++- tests/run_tests.sh | 18 +++ 9 files changed, 247 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 8031d02..0daa2cc 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,9 @@ node scripts/generate_gongwen_docx.mjs --input tests/fixture.md --output /tmp/fo node scripts/verify_gongwen_docx.mjs --input /tmp/formal.docx --profile formal --letterhead preprinted ``` -只有需要完整电子红头文件时,才加 `--letterhead digital`。红头和标题要使用目标电脑中实际安装的小标宋体;生成器缺少小标宋体或仿宋体时会在终端明确警告并写出替代字体,要求严格阻止替代输出时加 `--require-standard-fonts`。红线下标题会按版心网格预留两行,不会贴着分隔线。打开文档后,Word/WPS 的“引用→目录”可以按“标题1—标题4”生成并更新目录;是否显示第四级,在目录设置中选择。 +只有需要完整电子红头文件时,才加 `--letterhead digital`。红头和标题要使用目标电脑中实际安装的小标宋体;生成器缺少小标宋体或仿宋体时会在终端明确警告并写出替代字体,要求严格阻止替代输出时加 `--require-standard-fonts`。红线下标题会按版心网格预留两行,不会贴着分隔线;上行文的文号和签发人会在同一行编排。打开文档后,Word/WPS 的“引用→目录”可以按“标题1—标题4”生成并更新目录;是否显示第四级,在目录设置中选择。 + +信函、命令(令)和纪要走专用分支:信函生成170mm上粗下细、下页边20mm处上细下粗的两条红色双线,底线放在页脚并关闭页码;命令(令)落实机关标志下空二行、令号下空二行;纪要把“出席”“请假”“列席”标签设为黑体、人员名单设为仿宋。附件可用重复的 `--attachment-file` 另页生成“附件”标签、第三行标题和正文,并保持正式页码连续。 ## 跨平台安装 diff --git a/SKILL.md b/SKILL.md index fab4d04..728913b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -15,7 +15,7 @@ GB/T 9704-2012《党政机关公文格式》现行,2025-05-30 复审继续有 本工具只处理排版,不判断单位身份、授权状态或后续盖章方式。它会根据用户明确选择的版式模式应用对应的排版规则,不因“正式”“国企”“报告”或仅提供机构名称而擅自使用红头。 -生成器可输出 A4、156mm×225mm 版心、三号仿宋正文、小标宋二号标题、四级标题层次、两字缩进,以及居中或单双页页码。四级标题均写入 Word/WPS 标题样式和大纲级别,便于后续插入、更新目录;文章总标题使用独立“公文标题”样式,不进入目录。 +生成器可输出 A4、156mm×225mm 版心、三号仿宋正文、小标宋二号标题、四级标题层次、两字缩进,以及居中或单双页页码。四级标题均写入 Word/WPS 标题样式和大纲级别,便于后续插入、更新目录;文章总标题使用独立“公文标题”样式,不进入目录。上行文的文号和签发人在同一行编排;信函、命令(令)、纪要使用各自专用版式;重复传入 `--attachment-file` 时,附件会另页生成并保持页码连续。 `references/formal-checklist.md` 是可选的版式复核清单。涉及印章或签名章时,使用者可在 Word/WPS 中插入已取得的图片或完成实体盖章;本生成器不内置印章图像。 @@ -37,6 +37,8 @@ node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx -- `letter`、`command`、`minutes` 分别对应信函、命令(令)、纪要格式;需要时显式传入 `--format` 和相应字段。正式发文默认单双页页码;`standard` 为单页右、双页左,`center` 与 `none` 供普通材料选择。 +附件可重复传入 `--attachment-file attachment.md`。附件 Markdown 的首个一级标题会作为附件标题,第一页第一行写“附件1”(后续文件递增),第三行写标题,随后排正文;附件说明仍通过 `--attachment-note` 提供。复杂横排附件、不能与正文一起装订的附件和机关专用模板仍需在目标 Word/WPS 中核对。 + Markdown 标题优先按序数识别:`一、`、`(一)`、`1.`、`(1)`;序数缺失时才按 Markdown 层级映射。正文、四级标题均左空二字,回行顶格。标题字体在运行机器缺失时会明确报告并使用可见替代字体;打开文件时仍须确认目标 Office/WPS 的实际字体,不把普通宋体视作小标宋的等价替代。 ## 验证 diff --git a/dist/traework-gongwen-skill.zip b/dist/traework-gongwen-skill.zip index 730e73bd8d0114992e4d1bbcc992596ee116028c..f629e5d7c328ef49f909b6f053b925623b968c35 100644 GIT binary patch delta 25157 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 delta 21334 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 diff --git a/marketing/x-v2-release.md b/marketing/x-v2-release.md index 1bd8df1..fe82b19 100644 --- a/marketing/x-v2-release.md +++ b/marketing/x-v2-release.md @@ -26,6 +26,12 @@ 另一个以前看上去没问题的缺口也补了。四级标题原先有字体和字号,却没有真正写进 Word 和 WPS 的标题样式。现在标题一到标题四都有样式和大纲级别。后续插目录、更新目录、跳转章节,文件终于能把自己的结构说清楚。 +这次我又拿实际渲染图反看了一遍,发现“有红线”不等于“版式完成”。红线下面必须给标题留出两行版心网格,不能像旧图那样贴在一起。上行文的文号和签发人也不能上下分开,必须在同一行的左右区域排开。信函格式更不能拿普通红头分支凑数:上边缘30毫米、170毫米上粗下细双线、下页边20毫米处上细下粗双线,都写进了专用分支。命令和纪要的空行、字体也分别落到了生成结构里。 + +字体这件事我也不再含糊。小标宋体和仿宋体是公文版式的一部分,不是“差不多的宋体”。当前这台机器没有方正小标宋和标准仿宋,生成器会直接在终端点名提示,严格模式会停止生成。用替代字体做结构和分页验证可以,但不能把替代字体的文件说成已经达到最终打印标准。 + +附件也补上了可执行入口。给 `--attachment-file` 传入 Markdown,Skill 会在版记前另页排“附件”标签、第三行标题和附件正文,正式页码继续连排。横排表格、机关专用模板、印章和真实套打仍要拿到目标 Word/WPS 与打印条件下复核,这些不能靠一段 XML 假装已经完成。 + 这次也重做了校验逻辑。它会根据普通材料、正式发文、信函、命令、纪要分别检查,不再拿一套机械条件去误判所有文档。TraeWork 的安装包也重新打包,里面没有留下之前的审查记录。 我已经验证了普通稿、预印红头纸套打稿和完整电子红头稿的 DOCX 结构与 PDF 渲染,也跑过了 Codex、Claude Code、OpenCode、Trae Code、Kimi、TraeWork、WorkBuddy、ZCode 的安装测试。 diff --git a/references/formal-checklist.md b/references/formal-checklist.md index 4850144..b886832 100644 --- a/references/formal-checklist.md +++ b/references/formal-checklist.md @@ -19,7 +19,7 @@ - [ ] 密级和保密期限、紧急程度适用时均为三号黑体,按份号、密级和期限、紧急程度顺序自上而下排列。 - [ ] 发文机关标志使用全称或规范简称,可加“文件”;居中、红色、推荐小标宋,上边缘距版心上边缘35mm。 - [ ] 发文字号位于机关标志下空二行;年份为全称阿拉伯数字并使用六角括号〔〕,序号不加“第”、不补零、后加“号”。 -- [ ] 上行文文号左空一字;签发人同区右空一字。“签发人:”为仿宋三号,姓名为楷体三号;多签发人按机关顺序编排。 +- [ ] 上行文文号左空一字;签发人与文号在同一行同一区域,右空一字。“签发人:”为仿宋三号,姓名为楷体三号;多签发人按机关顺序编排。 - [ ] 红色分隔线在文号下4mm处,与版心等宽,线高按模板核定(常用约0.35—0.5mm)。 ## 3. 主体 @@ -33,6 +33,7 @@ - [ ] 署名、成文日期和印章已按“加盖印章/不加盖印章/签发人签名章/联合行文”之一完整处理;成文日期年月日用阿拉伯数字全称且月日不补零;没有空白印章或伪造印章。 - [ ] 附注适用时在成文日期下一行左空二字,以圆括号标注。 - [ ] 附件另面编排、置于版记之前;左上第一行为“附件”及序号黑体三号,标题在第三行居中;与正文一起装订的附件页码连续。 +- [ ] 使用 `--attachment-file` 生成的附件已逐个另页,附件标签在第一行、标题在第三行;无法一起装订时已人工补排发文字号和“附件”标识。 ## 4. 版记、页码与特殊格式 @@ -41,4 +42,7 @@ - [ ] 印发机关和日期为四号仿宋,左空一字与右空一字;日期完整且不补零,后加“印发”。 - [ ] 页码为四号半角宋体阿拉伯数字,两侧各一字线,线距版心下边缘7mm;单页右空一字、双页左空一字;空白页、特定版记页和附件连续页码已按规则处理。 - [ ] 横排表格、信函、命令(令)、纪要、联合行文已使用相应专用版式;不能用普通打印稿替代。 +- [ ] 信函格式已核对上边缘30mm、上粗下细双线、下页边20mm处上细下粗双线,双线长度均170mm;首页无页码且版记不画分隔线。 +- [ ] 命令(令)格式已核对发文机关标志上边缘距版心20mm、令号下空二行、正文下空二行。 +- [ ] 纪要格式已核对标志上边缘距版心35mm,出席/请假/列席标签为黑体,人员为仿宋。 - [ ] 计量单位、标点符号、数字分别按 GB 3100、GB 3101、GB 3102、GB/T 15834、GB/T 15835 复核。 diff --git a/references/gbt9704-2012-summary.md b/references/gbt9704-2012-summary.md index 69d1872..a8cebc6 100644 --- a/references/gbt9704-2012-summary.md +++ b/references/gbt9704-2012-summary.md @@ -101,7 +101,7 @@ | 紧急程度 | 3号黑体,与份号、密级按顺序排列 | | 发文机关标志 | 机关全称或规范简称,可加“文件”;红色小标宋,居中 | | 发文字号 | 年份全称、六角括号、序号不加“第”且不补零 | -| 上行文签发人 | “签发人:”仿宋三号,姓名楷体三号;与文号同区编排 | +| 上行文签发人 | “签发人:”仿宋三号,姓名楷体三号;必须与文号同一行同一区域编排 | | 分隔线 | 文号下4mm,红色,与版心等宽;常用线高约0.35—0.5mm | 版头位置和红色要素可按用户提供的字段和模板进一步调整。 @@ -134,7 +134,9 @@ - A4横排表格的页码位置与其他页一致;单双页表头方向按标准处理。 - 信函、命令(令)、纪要均有专门格式,不能套用普通公文首页。 -- 信函首页不显示页码,版记规则也不同。 +- 信函首页不显示页码,版记规则也不同。上边缘距纸边30mm;发文机关标志下4mm处为170mm上粗下细红色双线,距下页边20mm处为170mm上细下粗红色双线;标题与其上最后一个要素相距二行。 +- 命令(令)发文机关标志上边缘距版心20mm,标志下空二行编排令号,令号下空二行编排正文。 +- 纪要标志上边缘距版心35mm;“出席”“请假”“列席”标签用黑体,人员名单用仿宋。 - 特定格式必须使用对应模板或专用生成逻辑,不得以普通报告模板代替。 ## 九、技能能力边界 @@ -150,6 +152,7 @@ | 预印红头纸套打 | 是 | `--format formal --letterhead preprinted`,按纸样设置预留高度 | | 完整电子红头 | 是 | `--format formal --letterhead digital` | | 印章、签名章 | 文字版式之外 | 在 Word/WPS 中插入图片或实体盖章 | +| 附件 | 通过 `--attachment-file` 另页生成标签、标题和正文 | 无法一起装订时的发文字号标注、复杂附件版式仍需人工复核 | | 版记 | 是 | `--cc`、`--print-org`、`--print-date` 适用时生成 | | 信函、命令、纪要 | 是 | `--format letter|command|minutes`,按实际字段生成 | diff --git a/scripts/generate_gongwen_docx.mjs b/scripts/generate_gongwen_docx.mjs index a12c286..89410e4 100755 --- a/scripts/generate_gongwen_docx.mjs +++ b/scripts/generate_gongwen_docx.mjs @@ -20,7 +20,41 @@ function installedFontFamilies() { .filter(Boolean), ); } catch { - return new Set(); + const families = new Set(); + const aliases = { + simfang: "FangSong", + simfangb: "FangSong_GB2312", + "方正小标宋简体": "方正小标宋简体", + "方正小标宋_GBK": "方正小标宋_GBK", + "fzxiaobiaosong-b05s": "FZXiaoBiaoSong-B05S", + "fzxiaobiaosong-b13s": "FZXiaoBiaoSong-B13S", + fangsong: "FangSong", + fangsong_gb2312: "FangSong_GB2312", + }; + const roots = [ + process.env.WINDIR ? path.join(process.env.WINDIR, "Fonts") : "", + process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, "Microsoft", "Windows", "Fonts") : "", + path.join(os.homedir(), "Library", "Fonts"), + "/Library/Fonts", + "/usr/share/fonts", + "/usr/local/share/fonts", + ].filter(Boolean); + const walk = (dir) => { + let entries; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } + for (const entry of entries) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walk(full); + else if (/\.(?:ttf|ttc|otf)$/i.test(entry.name)) { + const stem = path.basename(entry.name, path.extname(entry.name)); + families.add(stem); + const alias = aliases[stem.toLowerCase()]; + if (alias) families.add(alias); + } + } + }; + roots.forEach(walk); + return families; } } @@ -62,7 +96,7 @@ function reportFontStatus(args) { function usage() { console.log(`Usage: - node generate_gongwen_docx.mjs --input source.md --output out.docx [--format ordinary|formal|letter|command|minutes] [--letterhead preprinted|digital] [--org 发文机关] [--doc-no 发文字号] [--title 标题] [--subtitle 副标题] [--to 主送机关] [--sender 落款] [--date 日期] [--page-number center|standard|none] [--require-standard-fonts] + node generate_gongwen_docx.mjs --input source.md --output out.docx [--format ordinary|formal|letter|command|minutes] [--letterhead preprinted|digital] [--org 发文机关] [--doc-no 发文字号] [--title 标题] [--subtitle 副标题] [--to 主送机关] [--sender 落款] [--date 日期] [--attachment-note 附件说明] [--attachment-file attachment.md ...] [--page-number center|standard|none] [--require-standard-fonts] Notes: - Converts Markdown to a GB/T 9704-2012 page-layout DOCX. @@ -71,13 +105,14 @@ Notes: - letter, command and minutes use their dedicated national-standard layout branches. Their required fields must be supplied explicitly. - --org and --doc-no are printed exactly as supplied. The tool formats document content; it does not infer missing information or decide the document's use. - The generator reports when the required small-standard-title or FangSong font is unavailable and uses a visible fallback. Add --require-standard-fonts to refuse generation until the required font is installed. - - Supports headings, paragraphs, ordered/unordered lines, and pipe tables. + - Supports headings, paragraphs, ordered/unordered lines, pipe tables, and repeatable Markdown attachment pages via --attachment-file. - No npm dependencies; requires zip in PATH.`); } function parseArgs(argv) { const args = {}; const flags = new Set(["help", "no-page-number", "no-page-numbers", "require-standard-fonts"]); + const repeatable = new Set(["attachment-file"]); for (let i = 2; i < argv.length; i += 1) { const key = argv[i]; if (!key.startsWith("--")) continue; @@ -86,7 +121,11 @@ function parseArgs(argv) { args[name] = true; continue; } - args[name] = argv[i + 1]; + if (repeatable.has(name)) { + args[name] = [...(args[name] ?? []), argv[i + 1]]; + } else { + args[name] = argv[i + 1]; + } i += 1; } return args; @@ -196,6 +235,11 @@ function documentNumberParagraph(text, upward = false) { }); } +function upwardHeaderParagraph(docNo, signer) { + const tabPosition = CONTENT_W - FIRST_LINE_INDENT; + return `${run(docNo, { fontPreset: "body", size: "32" })}${run("签发人:", { fontPreset: "body", size: "32" })}${run(signer, { fontPreset: "h2", size: "32" })}`; +} + function subtitleParagraph(text) { return paragraph(text, { align: "center", @@ -215,6 +259,11 @@ function mainSendParagraph(text) { }); } +function normalizeMainSend(text) { + const value = String(text ?? "").trim(); + return value && /[::]$/.test(value) ? value : `${value}:`; +} + function h1(text) { return paragraph(text, { align: "left", @@ -272,33 +321,44 @@ function right(text) { } function redRule(thickness = "5") { - const height = thickness === "5" ? "0.5mm" : "0.35mm"; + const height = thickness === "thick" ? "0.35mm" : thickness === "thin" ? "0.25mm" : "0.5mm"; return ``; } +function mmToDxa(mm) { + return Math.round(Number(mm) * 1440 / 25.4); +} + +function redDoubleRule({ upperMm = "0.35", lowerMm = "0.25", widthMm = "170" } = {}) { + const width = mmToDxa(widthMm); + const row = (heightMm) => ` `; + const spacer = ``; + return `${row(upperMm)}${spacer}${row(lowerMm)}`; +} + +function minutesPersonParagraph(label, people, before = "0") { + return `${run(`${label}:`, { fontPreset: "h1", size: "32" })}${run(people, { fontPreset: "body", size: "32" })}`; +} + function headerFieldParagraph(text, fontPreset = "h1", opts = {}) { return paragraph(text, { align: opts.align ?? "left", indent: false, fontPreset, size: "32", after: opts.after ?? "0", left: opts.left, right: opts.right }); } -function signerParagraph(name) { - return `${run("签发人:", { fontPreset: "body", size: "32" })}${run(name, { fontPreset: "h2", size: "32" })}`; -} - function attachmentNote(text) { return paragraph(`附件:${text}`, { align: "left", fontPreset: "body", size: "32", before: "560", firstLine: FIRST_LINE_INDENT, left: "0" }); } function colophon(args) { if (!args["cc"] && !args["print-org"] && !args["print-date"]) return []; - const body = [redRule("5")]; + const body = [redRule("thick")]; if (args.cc) body.push(paragraph(`抄送:${args.cc.replace(/[。.]?$/, "")}。`, { align: "left", indent: false, left: "320", right: "320", fontPreset: "body", size: "28" })); - if (args.cc) body.push(redRule("2")); + if (args.cc) body.push(redRule("thin")); if (args["print-org"] || args["print-date"]) { const left = args["print-org"] ?? ""; const rightValue = args["print-date"] ? `${args["print-date"].replace(/印发$/, "")}印发` : ""; - body.push(`${run(left, { fontPreset: "body", size: "28" })}${run(rightValue, { fontPreset: "body", size: "28" })}`); + body.push(`${run(left, { fontPreset: "body", size: "28" })}${run(rightValue, { fontPreset: "body", size: "28" })}`); } - body.push(redRule("5")); + body.push(redRule("thick")); return body; } @@ -463,6 +523,25 @@ function renderParagraph(block) { return paragraph(block.text); } +function attachmentPage(blocks, index, filePath) { + const attachmentBlocks = [...blocks]; + let attachmentTitle = path.basename(filePath, path.extname(filePath)); + if (attachmentBlocks[0]?.type === "heading") { + attachmentTitle = attachmentBlocks.shift().text; + } + const body = [ + paragraph(`附件${index}`, { align: "left", indent: false, fontPreset: "h1", size: "32", pageBreakBefore: true, keepNext: true }), + emptyLine(), + titleParagraph(attachmentTitle), + ]; + for (const block of attachmentBlocks) { + if (block.type === "heading") body.push(renderHeading(block)); + else if (block.type === "table") body.push(tableXml(block.headers, block.rows), emptyLine()); + else body.push(renderParagraph(block)); + } + return body.join(""); +} + function resolvePageNumberMode(args, format) { if (args["no-page-number"] || args["no-page-numbers"]) return "none"; if (["center", "standard", "none"].includes(args["page-number"])) return args["page-number"]; @@ -491,33 +570,34 @@ function buildDocument(blocks, args) { if (!mainSend && sourceBlocks[0]?.type === "paragraph" && looksLikeMainSend(sourceBlocks[0].text)) { mainSend = sourceBlocks.shift().text; } + if (mainSend) mainSend = normalizeMainSend(mainSend); const upward = args["upward"] === "true" || args["upward"] === true; const formal = format === "formal"; if (formal) { if (letterhead === "digital" && !args.org) throw new Error("formal digital letterhead requires --org"); + const headerFieldCount = [args["copy-no"], args.secret, args.urgent].filter(Boolean).length; + if (args["copy-no"] && !/^\d{1,6}$/.test(String(args["copy-no"]))) throw new Error("--copy-no must contain one to six Arabic digits"); if (args["copy-no"]) body.push(headerFieldParagraph(String(args["copy-no"]).padStart(6, "0"), "body")); if (args.secret) body.push(headerFieldParagraph(args.secret)); if (args.urgent) body.push(headerFieldParagraph(args.urgent)); - if (letterhead === "digital" && args.org) body.push(agencyMarkParagraph(args.org, "1984")); + if (letterhead === "digital" && args.org) body.push(agencyMarkParagraph(args.org, String(Math.max(0, 1984 - headerFieldCount * 560)))); if (letterhead === "preprinted") { const reserve = Number(args["letterhead-reserve-mm"] ?? 72); if (!Number.isFinite(reserve) || reserve < 37 || reserve > 130) throw new Error("--letterhead-reserve-mm must be between 37 and 130"); - body.push(letterheadReserveParagraph(reserve)); - } - if (args["doc-no"]) body.push(documentNumberParagraph(args["doc-no"], upward)); - if (upward && args.signer) { - body.push(signerParagraph(args.signer)); + body.push(letterheadReserveParagraph(Math.max(37, reserve - headerFieldCount * 9.877))); } + if (upward && args["doc-no"] && args.signer) body.push(upwardHeaderParagraph(args["doc-no"], args.signer)); + else if (args["doc-no"]) body.push(documentNumberParagraph(args["doc-no"], upward)); if (letterhead === "digital" || args["preprinted-rule"] === "true") body.push(redRule()); } else if (format === "letter") { if (!args.org) throw new Error("letter format requires --org"); body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "0", after: "227" })); - body.push(redRule()); + body.push(redDoubleRule({ upperMm: "0.35", lowerMm: "0.25" })); if (args["doc-no"]) body.push(headerFieldParagraph(args["doc-no"], "body", { align: "right" })); } else if (format === "command") { if (!args.org) throw new Error("command format requires --org"); - body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "0", after: "560" })); - if (args["doc-no"]) body.push(paragraph(args["doc-no"], { align: "center", indent: false, fontPreset: "body", size: "32", after: "560" })); + body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "0", after: "1120" })); + if (args["doc-no"]) body.push(paragraph(args["doc-no"], { align: "center", indent: false, fontPreset: "body", size: "32", after: "1120" })); } else if (format === "minutes") { if (!args.org) throw new Error("minutes format requires --org (for XXXXX纪要)"); body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "1984", after: "560" })); @@ -555,16 +635,25 @@ function buildDocument(blocks, args) { } } if (args.note) body.push(paragraph(`(${args.note.replace(/^(|)$/g, "")})`, { align: "left", fontPreset: "body", size: "32" })); - if (format === "minutes" && args.attendees) body.push(paragraph(`出席:${args.attendees}`, { align: "left", fontPreset: "body", size: "32", before: "560" })); - if (format === "minutes" && args.absent) body.push(paragraph(`请假:${args.absent}`, { align: "left", fontPreset: "body", size: "32" })); - if (format === "minutes" && args.observers) body.push(paragraph(`列席:${args.observers}`, { align: "left", fontPreset: "body", size: "32" })); + const attachmentFiles = Array.isArray(args["attachment-file"]) ? args["attachment-file"] : args["attachment-file"] ? [args["attachment-file"]] : []; + attachmentFiles.forEach((filePath, index) => { + const absolutePath = path.resolve(filePath); + if (!fs.existsSync(absolutePath)) throw new Error(`attachment file not found: ${filePath}`); + const attachmentBlocks = parseMarkdown(fs.readFileSync(absolutePath, "utf8")); + body.push(attachmentPage(attachmentBlocks, index + 1, absolutePath)); + }); + if (format === "minutes" && args.attendees) body.push(minutesPersonParagraph("出席", args.attendees, "560")); + if (format === "minutes" && args.absent) body.push(minutesPersonParagraph("请假", args.absent)); + if (format === "minutes" && args.observers) body.push(minutesPersonParagraph("列席", args.observers)); if (format !== "letter") body.push(...colophon(args)); - const footerRefs = pageNumberMode === "standard" + const letterFooter = format === "letter" ? '' : ""; + const footerRefs = letterFooter || (pageNumberMode === "standard" ? '' : pageNumberMode === "center" ? '' - : ""; - body.push(`${footerRefs}`); + : ""); + const footerDistance = format === "letter" ? "1134" : "1588"; + body.push(`${footerRefs}`); return `${body.join("")}`; } @@ -618,38 +707,48 @@ function footerXml(align = "center") { return `${sideIndent}${pageRunProps}— ${pageRunProps}1${pageRunProps} —`; } +function letterFooterXml() { + return `${redDoubleRule({ upperMm: "0.25", lowerMm: "0.35" })}`; +} + function settingsXml(pageNumberMode) { const evenOdd = pageNumberMode === "standard" ? "" : ""; return `${evenOdd}`; } -function documentRelsXml(pageNumberMode) { - const footerRels = pageNumberMode === "standard" +function documentRelsXml(pageNumberMode, format) { + const footerRels = format === "letter" + ? '' + : pageNumberMode === "standard" ? '' - : pageNumberMode === "center" - ? '' + : pageNumberMode === "center" + ? '' : ""; return `${footerRels}`; } -function writeDocx(output, documentXml, pageNumberMode) { +function writeDocx(output, documentXml, pageNumberMode, format) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "gongwen-docx-")); fs.mkdirSync(path.join(dir, "_rels"), { recursive: true }); fs.mkdirSync(path.join(dir, "word"), { recursive: true }); fs.mkdirSync(path.join(dir, "word/_rels"), { recursive: true }); - const footerTypes = pageNumberMode === "standard" + const footerTypes = format === "letter" + ? '' + : pageNumberMode === "standard" ? '' - : pageNumberMode === "center" - ? '' + : pageNumberMode === "center" + ? '' : ""; fs.writeFileSync(path.join(dir, "[Content_Types].xml"), `${footerTypes}`); fs.writeFileSync(path.join(dir, "_rels/.rels"), ``); - fs.writeFileSync(path.join(dir, "word/_rels/document.xml.rels"), documentRelsXml(pageNumberMode)); + fs.writeFileSync(path.join(dir, "word/_rels/document.xml.rels"), documentRelsXml(pageNumberMode, format)); fs.writeFileSync(path.join(dir, "word/document.xml"), documentXml); fs.writeFileSync(path.join(dir, "word/styles.xml"), stylesXml()); fs.writeFileSync(path.join(dir, "word/fontTable.xml"), fontTableXml()); fs.writeFileSync(path.join(dir, "word/settings.xml"), settingsXml(pageNumberMode)); - if (pageNumberMode === "standard") { + if (format === "letter") { + fs.writeFileSync(path.join(dir, "word/footerLetter.xml"), letterFooterXml()); + } else if (pageNumberMode === "standard") { fs.writeFileSync(path.join(dir, "word/footerOdd.xml"), footerXml("right")); fs.writeFileSync(path.join(dir, "word/footerEven.xml"), footerXml("left")); } else if (pageNumberMode === "center") { @@ -672,9 +771,14 @@ try { process.exit(2); } const md = fs.readFileSync(args.input, "utf8"); -const blocks = parseMarkdown(md); -const documentXml = buildDocument(blocks, args); -const format = ["ordinary", "formal", "letter", "command", "minutes"].includes(args.format) ? args.format : "ordinary"; -const pageNumberMode = resolvePageNumberMode(args, format); -writeDocx(path.resolve(args.output), documentXml, pageNumberMode); -console.log(`Generated: ${path.resolve(args.output)}`); +try { + const blocks = parseMarkdown(md); + const documentXml = buildDocument(blocks, args); + const format = ["ordinary", "formal", "letter", "command", "minutes"].includes(args.format) ? args.format : "ordinary"; + const pageNumberMode = resolvePageNumberMode(args, format); + writeDocx(path.resolve(args.output), documentXml, pageNumberMode, format); + console.log(`Generated: ${path.resolve(args.output)}`); +} catch (error) { + console.error(`GENERATOR ERROR: ${error.message}`); + process.exit(2); +} diff --git a/scripts/verify_gongwen_docx.mjs b/scripts/verify_gongwen_docx.mjs index f1e26e3..50b05a0 100755 --- a/scripts/verify_gongwen_docx.mjs +++ b/scripts/verify_gongwen_docx.mjs @@ -2,6 +2,8 @@ import { execFileSync } from "node:child_process"; import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; function usage() { console.log("Usage: node verify_gongwen_docx.mjs --input file.docx [--profile ordinary|formal|letter|command|minutes] [--letterhead preprinted|digital] [--require-standard-fonts]"); @@ -24,7 +26,7 @@ function parseArgs(argv) { } function readPart(file, part, optional = false) { - try { return execFileSync("unzip", ["-p", file, part], { encoding: "utf8" }); } + try { return execFileSync("unzip", ["-p", file, part], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); } catch (error) { if (optional) return ""; throw error; } } @@ -40,7 +42,32 @@ const styles = readPart(args.input, "word/styles.xml"); const footerOdd = readPart(args.input, "word/footerOdd.xml", true); const footerEven = readPart(args.input, "word/footerEven.xml", true); const footerCenter = readPart(args.input, "word/footerCenter.xml", true); +const footerLetter = readPart(args.input, "word/footerLetter.xml", true); const requireStandardFonts = args["require-standard-fonts"] === true; +const installedFonts = (() => { + try { + return new Set(execFileSync("fc-list", ["-f", "%{family}\n"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split(/\r?\n/).flatMap((line) => line.split(",")).map((font) => font.trim()).filter(Boolean)); + } catch { + const families = new Set(); + const aliases = { simfang: "FangSong", simfangb: "FangSong_GB2312", fangsong: "FangSong", fangsong_gb2312: "FangSong_GB2312", "fzxiaobiaosong-b05s": "FZXiaoBiaoSong-B05S", "fzxiaobiaosong-b13s": "FZXiaoBiaoSong-B13S" }; + const roots = [process.env.WINDIR ? path.join(process.env.WINDIR, "Fonts") : "", process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, "Microsoft", "Windows", "Fonts") : "", path.join(os.homedir(), "Library", "Fonts"), "/Library/Fonts", "/usr/share/fonts", "/usr/local/share/fonts"].filter(Boolean); + const walk = (dir) => { + let entries; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } + for (const entry of entries) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walk(full); + else if (/\.(?:ttf|ttc|otf)$/i.test(entry.name)) { + const stem = path.basename(entry.name, path.extname(entry.name)); + families.add(stem); + if (aliases[stem.toLowerCase()]) families.add(aliases[stem.toLowerCase()]); + } + } + }; + roots.forEach(walk); + return families; + } +})(); const checks = []; function check(name, ok, detail) { checks.push({ name, ok, detail }); } @@ -51,6 +78,10 @@ check("Document grid", /]*w:linePitch="560"/.test(doc), "28 pt impl const bodyFontStandard = /w:eastAsia="(?:仿宋|仿宋_GB2312|FangSong|FangSong_GB2312)"/.test(styles); const bodyFontAvailable = bodyFontStandard || (!requireStandardFonts && /w:eastAsia="STFangsong"/.test(styles)); check("Default body font", bodyFontAvailable && / installedFonts.has(font)), "target machine must provide a small-standard-title font"); + check("Installed FangSong font", ["仿宋", "仿宋_GB2312", "FangSong", "FangSong_GB2312"].some((font) => installedFonts.has(font)), "target machine must provide a standard FangSong font"); +} check("Heading styles", [1, 2, 3, 4].every((level) => new RegExp(`w:styleId="Heading${level}"`).test(styles)), "Heading 1 through Heading 4 are defined"); const textOf = (xml) => xml.replace(/<[^>]+>/g, ""); @@ -74,7 +105,9 @@ if (profile === "ordinary") { check("No formal red-head drawing", !/w:color w:val="FF0000"/.test(doc), "ordinary output has no red head or red rule"); check("Centered layout footer", /w:jc w:val="center"/.test(footerCenter) && /w:instr="PAGE"/.test(footerCenter), "ordinary output uses centered page number"); } else if (profile === "letter") { - check("Letter first-page page number", !footerCenter && !footerOdd && !footerEven, "letter test output suppresses first-page page number"); + check("Letter first-page page number", !footerCenter && !footerOdd && !footerEven && !/w:instr="PAGE"/.test(footerLetter), "letter output suppresses page numbers"); + check("Letter upper red double rule", (footerLetter.match(/w:fill="FF0000"/g) ?? []).length >= 2 && /w:tblW[^>]*w:w="9638"/.test(doc), "letter output contains 170 mm red double rules"); + check("Letter bottom rule footer", /rIdLetterFooter/.test(doc) && /w:footer="1134"/.test(doc), "bottom red double rule is anchored 20 mm from the paper edge"); } else { check("Odd/even page fields", /w:instr="PAGE"/.test(footerOdd) && /w:instr="PAGE"/.test(footerEven), "odd and even page footers exist"); check("Odd/even page positions", /w:jc w:val="right"/.test(footerOdd) && /w:jc w:val="left"/.test(footerEven), "odd right and even left"); @@ -83,8 +116,16 @@ if (profile === "formal") { if (args.letterhead === "digital") { check("Digital red head", /w:color w:val="FF0000"/.test(doc), "digital formal output contains requested red elements"); check("Red rule thickness", /height:0\.5mm/.test(doc), "header separator uses the recommended 0.35-0.5 mm range"); - check("Digital red-head agency position", /w:before="1984"/.test(doc), "agency mark starts 35 mm below the type-area top"); + check("Red rule width", /style="width:156mm;height:0\.5mm"/.test(doc), "header separator spans the 156 mm type area"); + const docParagraphs = paragraphs(doc); + const agencyIndex = docParagraphs.findIndex((p) => /w:sz w:val="56"/.test(p) && /w:color w:val="FF0000"/.test(p)); + const headerFieldCount = agencyIndex < 0 ? 0 : docParagraphs.slice(0, agencyIndex).filter((p) => /\d{6}|机密|秘密|特急|加急/.test(textOf(p))).length; + const expectedAgencyBefore = 1984 - headerFieldCount * 560; + check("Digital red-head agency position", new RegExp(`w:before="${expectedAgencyBefore}"`).test(doc), `agency mark starts 35 mm below the type-area top after ${headerFieldCount} header field line(s)`); check("Two blank lines below red rule", titleParagraphs.some((p) => /w:before="1120"/.test(p)), "title paragraph reserves two 28-point grid lines below the red rule"); + if (args.upward === "true") { + check("Upward document number and signer share one row", paragraphs(doc).some((p) => /签发人:/.test(textOf(p)) && /〔|\[|文号|发/.test(textOf(p))), "signer is in the same paragraph row as the document number"); + } } else { check("Preprinted letterhead reserve", /w:before="\d{4,}"/.test(doc), "first-page top reserve is present"); check("No red drawing for preprinted letterhead", !/w:color w:val="FF0000"/.test(doc), "preprinted mode does not redraw red letterhead or rule"); @@ -94,6 +135,21 @@ if (profile === "formal") { if (profile === "letter") { check("Letter title spacing", titleParagraphs.some((p) => /w:before="1120"/.test(p)), "title paragraph reserves two 28-point grid lines below the red rule"); } +if (profile === "command") { + check("Command agency-to-order spacing", /w:after="1120"/.test(paragraphs(doc)[0] ?? ""), "command agency mark is followed by two blank lines"); + check("Command order-to-body spacing", paragraphs(doc).slice(1).some((p) => /w:after="1120"/.test(p) && /w:jc w:val="center"/.test(p)), "command number is followed by two blank lines"); +} +if (profile === "minutes") { + const attendance = paragraphs(doc).filter((p) => /(?:出席|请假|列席):/.test(textOf(p))); + check("Minutes attendance label font", attendance.every((p) => /w:eastAsia="(?:黑体|SimHei|Heiti SC|STHeiti)"/.test(p)), attendance.length ? `${attendance.length} attendance paragraph(s)` : "not present in this document"); + check("Minutes attendee font", attendance.every((p) => /w:eastAsia="(?:仿宋|仿宋_GB2312|FangSong|FangSong_GB2312|STFangsong)"/.test(p)), attendance.length ? "people runs use FangSong" : "not present in this document"); +} +const redRules = doc.match(/height:(?:0\.35|0\.25|0\.5)mm/g) ?? []; +if (["formal", "command", "minutes"].includes(profile)) { + const hasColophon = /抄送:|印发/.test(textOf(doc)); + check("Colophon line thickness model", !hasColophon || (redRules.includes("height:0.35mm") && redRules.includes("height:0.25mm")), "版记粗线0.35 mm、细线0.25 mm"); + if (hasColophon) check("Colophon print-row right tab", /w:tab w:val="right"/.test(doc), "印发机关和日期使用右制表位"); +} for (const item of checks) console.log(`${item.ok ? "PASS" : "FAIL"} ${item.name}: ${item.detail}`); const failed = checks.filter((item) => !item.ok); diff --git a/tests/run_tests.sh b/tests/run_tests.sh index d3e8036..f295a9c 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -34,6 +34,24 @@ unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'height:0.5mm' unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'w:before="1984"' unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'w:before="1120"' +cat >"$TMP_DIR/attachment.md" <<'EOF' +# 附件一:测试附件 + +附件正文。 +EOF +"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/upward.docx" --format formal --letterhead digital --org "示例单位文件" --doc-no "示例发〔2026〕1号" --title "上行文测试" --upward true --signer "张三" +"$NODE" "$VERIFY" --input "$TMP_DIR/upward.docx" --profile formal --letterhead digital --upward true +unzip -p "$TMP_DIR/upward.docx" word/document.xml | grep -q '签发人:' + +"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/letter.docx" --format letter --org "示例机关" --doc-no "示例〔2026〕1号" --title "信函测试" +"$NODE" "$VERIFY" --input "$TMP_DIR/letter.docx" --profile letter +unzip -l "$TMP_DIR/letter.docx" | grep -q 'word/footerLetter.xml' +unzip -p "$TMP_DIR/letter.docx" word/footerLetter.xml | grep -q 'w:fill="FF0000"' + +"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/with-attachment.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔2026〕1号" --title "附件测试" --attachment-note "附件一:测试附件" --attachment-file "$TMP_DIR/attachment.md" +unzip -p "$TMP_DIR/with-attachment.docx" word/document.xml | grep -q '附件一:测试附件' +unzip -p "$TMP_DIR/with-attachment.docx" word/document.xml | grep -q 'w:pageBreakBefore' + if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/strict.docx" --format formal --letterhead digital --org "示例单位文件" --title "严格字体测试" --require-standard-fonts >"$TMP_DIR/strict.out" 2>&1; then if ! fc-list -f '%{family}\n' | grep -Eq '方正小标宋简体|方正小标宋_GBK|FZXiaoBiaoSong'; then echo "strict font mode unexpectedly succeeded without a small-standard-title font" >&2