From 39360ac6472fe7d57669452131f3e861bf3b419a Mon Sep 17 00:00:00 2001 From: mizzlelover Date: Thu, 10 Sep 2026 20:25:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=BA=A2=E5=A4=B4=E5=AD=97?= =?UTF-8?q?=E4=BD=93=E4=B8=8E=E6=A0=87=E9=A2=98=E9=97=B4=E8=B7=9D=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- SKILL.md | 6 +- dist/traework-gongwen-skill.zip | Bin 22095 -> 23952 bytes references/formal-checklist.md | 3 +- references/gbt9704-2012-summary.md | 4 +- scripts/generate_gongwen_docx.mjs | 129 ++++++++++++++++++++--------- scripts/verify_gongwen_docx.mjs | 30 ++++++- tests/run_tests.sh | 15 ++++ 8 files changed, 144 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 80d4558..8031d02 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ 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`。打开文档后,Word/WPS 的“引用→目录”可以按“标题1—标题4”生成并更新目录;是否显示第四级,在目录设置中选择。 +只有需要完整电子红头文件时,才加 `--letterhead digital`。红头和标题要使用目标电脑中实际安装的小标宋体;生成器缺少小标宋体或仿宋体时会在终端明确警告并写出替代字体,要求严格阻止替代输出时加 `--require-standard-fonts`。红线下标题会按版心网格预留两行,不会贴着分隔线。打开文档后,Word/WPS 的“引用→目录”可以按“标题1—标题4”生成并更新目录;是否显示第四级,在目录设置中选择。 ## 跨平台安装 @@ -140,6 +140,8 @@ Windows: tests/run_tests.sh ``` +如果要把字体替代也视为失败,可在生成器和校验器上同时加 `--require-standard-fonts`;当前电脑没有小标宋体或标准仿宋体时,命令会停止并明确列出缺失字体。 + 这项测试证明生成器、校验器和可打印文件链路可运行。最终文件可按实际用途在目标 Word/WPS 与打印条件中检查。 ## 许可 diff --git a/SKILL.md b/SKILL.md index dec4b6f..fab4d04 100644 --- a/SKILL.md +++ b/SKILL.md @@ -25,7 +25,7 @@ GB/T 9704-2012《党政机关公文格式》现行,2025-05-30 复审继续有 node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx --format ordinary --title "文档标题" ``` -普通材料使用 `--format ordinary`,默认居中页码,不绘制红头。正式发文须显式使用 `--format formal`;其默认 `--letterhead preprinted`,即首面预留红头纸区域、不在 DOCX 中重绘纸上已有的红色机关标志和红线。`--doc-no` 等本次需打印的黑色变量仍可保留。只有明确要求完整电子版时使用 `--letterhead digital`。 +普通材料使用 `--format ordinary`,默认居中页码,不绘制红头。正式发文须显式使用 `--format formal`;其默认 `--letterhead preprinted`,即首面预留红头纸区域、不在 DOCX 中重绘纸上已有的红色机关标志和红线。`--doc-no` 等本次需打印的黑色变量仍可保留。只有明确要求完整电子版时使用 `--letterhead digital`。完整电子红头要求目标环境存在可核对的小标宋体;生成器会在缺失时明确警告并记录实际替代字体,需要硬性阻止替代输出时加 `--require-standard-fonts`。红线下标题按28磅版心网格空二行。 ```bash # 预印红头纸套打;72mm 是默认机关标志区域高度,可按实际纸样调为 37—130mm @@ -37,7 +37,7 @@ node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx -- `letter`、`command`、`minutes` 分别对应信函、命令(令)、纪要格式;需要时显式传入 `--format` 和相应字段。正式发文默认单双页页码;`standard` 为单页右、双页左,`center` 与 `none` 供普通材料选择。 -Markdown 标题优先按序数识别:`一、`、`(一)`、`1.`、`(1)`;序数缺失时才按 Markdown 层级映射。正文、四级标题均左空二字,回行顶格。标题字体在运行机器缺失时,会保留请求的字体名;打开文件时仍须确认 Office 的实际替代字体,不把普通宋体视作小标宋的等价替代。 +Markdown 标题优先按序数识别:`一、`、`(一)`、`1.`、`(1)`;序数缺失时才按 Markdown 层级映射。正文、四级标题均左空二字,回行顶格。标题字体在运行机器缺失时会明确报告并使用可见替代字体;打开文件时仍须确认目标 Office/WPS 的实际字体,不把普通宋体视作小标宋的等价替代。 ## 验证 @@ -45,6 +45,8 @@ Markdown 标题优先按序数识别:`一、`、`(一)`、`1.`、`(1)` unzip -t output.docx soffice --headless --convert-to pdf --outdir /tmp output.docx node scripts/verify_gongwen_docx.mjs --input output.docx --profile formal --letterhead preprinted +# 目标电脑必须装有标准小标宋体和仿宋体时,再加严格字体校验 +node scripts/verify_gongwen_docx.mjs --input output.docx --profile formal --letterhead digital --require-standard-fonts ``` 校验器核验生成器可检查的版式要素,并按 `ordinary`、`formal`、`letter`、`command`、`minutes` 选择校验档案。输出后仍须在目标 Word/WPS 与实际打印条件下检查首页正文、分页、附件、表格、页码、红头纸套打和装订。 diff --git a/dist/traework-gongwen-skill.zip b/dist/traework-gongwen-skill.zip index b5f0ab648685687e6f09af351152ae80d70d7bfb..730e73bd8d0114992e4d1bbcc992596ee116028c 100644 GIT binary patch delta 21336 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 DA+UjX delta 19425 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 diff --git a/references/formal-checklist.md b/references/formal-checklist.md index ab0a867..4850144 100644 --- a/references/formal-checklist.md +++ b/references/formal-checklist.md @@ -10,6 +10,7 @@ - [ ] A4 成品幅面为 210mm×297mm;天头 37mm±1mm、订口 28mm±1mm、版心 156mm×225mm。 - [ ] 无特殊说明时采用三号仿宋;一般每面22行、每行28字并撑满版心。不得把固定28pt当作国标唯一强制值。 - [ ] 文本颜色为黑色;需红色的发文机关标志、分隔线、印章或签名章已按规则处理。 +- [ ] 已检查目标电脑字体:发文机关标志和标题应使用可核对的小标宋体;正文应使用可用的仿宋体。生成器报告缺失时不得把替代字体当作国标字体,必要时使用 `--require-standard-fonts` 阻止生成。 - [ ] 制作环节已确认双面印刷、左侧装订、页码套正、钉位及成品质量要求;这些项目不能从 DOCX 自动推定。 ## 2. 版头 @@ -19,7 +20,7 @@ - [ ] 发文机关标志使用全称或规范简称,可加“文件”;居中、红色、推荐小标宋,上边缘距版心上边缘35mm。 - [ ] 发文字号位于机关标志下空二行;年份为全称阿拉伯数字并使用六角括号〔〕,序号不加“第”、不补零、后加“号”。 - [ ] 上行文文号左空一字;签发人同区右空一字。“签发人:”为仿宋三号,姓名为楷体三号;多签发人按机关顺序编排。 -- [ ] 红色分隔线在文号下4mm处,与版心等宽。 +- [ ] 红色分隔线在文号下4mm处,与版心等宽,线高按模板核定(常用约0.35—0.5mm)。 ## 3. 主体 diff --git a/references/gbt9704-2012-summary.md b/references/gbt9704-2012-summary.md index a1c4e8b..69d1872 100644 --- a/references/gbt9704-2012-summary.md +++ b/references/gbt9704-2012-summary.md @@ -25,7 +25,7 @@ ### 3. 预印红头纸套打与完整电子版 -预印红头纸套打是常见制作流程:DOCX 首页预留纸上已印制的机关标志、红线等区域,打印时放入对应红头纸;不在文档中重复绘制这些红色元素。标准规定最终版式,不规定只能使用预印纸。完整电子红头仅在用户明确要求时生成。套打区域高度以实际纸样为准,不能将“35mm”误作全部预留高度。 +预印红头纸套打是常见制作流程:DOCX 首页预留纸上已印制的机关标志、红线等区域,打印时放入对应红头纸;不在文档中重复绘制这些红色元素。标准规定最终版式,不规定只能使用预印纸。完整电子红头仅在用户明确要求时生成。套打区域高度以实际纸样为准,不能将“35mm”误作全部预留高度。电子红头的发文机关标志与标题必须在目标环境中使用可核对的小标宋体;若字体不存在,生成器会明确警告并显示替代字体,`--require-standard-fonts` 可阻止继续生成。红色分隔线后的标题按版心网格空二行,不以视觉贴近为准。 ## 三、纸张、版面和印制 @@ -102,7 +102,7 @@ | 发文机关标志 | 机关全称或规范简称,可加“文件”;红色小标宋,居中 | | 发文字号 | 年份全称、六角括号、序号不加“第”且不补零 | | 上行文签发人 | “签发人:”仿宋三号,姓名楷体三号;与文号同区编排 | -| 分隔线 | 文号下4mm,红色,与版心等宽 | +| 分隔线 | 文号下4mm,红色,与版心等宽;常用线高约0.35—0.5mm | 版头位置和红色要素可按用户提供的字段和模板进一步调整。 diff --git a/scripts/generate_gongwen_docx.mjs b/scripts/generate_gongwen_docx.mjs index a7973f8..a12c286 100755 --- a/scripts/generate_gongwen_docx.mjs +++ b/scripts/generate_gongwen_docx.mjs @@ -25,22 +25,44 @@ function installedFontFamilies() { } const INSTALLED_FONTS = installedFontFamilies(); -function preferredFont(candidates) { - return candidates.find((font) => INSTALLED_FONTS.has(font)) ?? candidates[0]; +function preferredFont(candidates, fallback) { + return candidates.find((font) => INSTALLED_FONTS.has(font)) ?? fallback ?? candidates[0]; } const FONT = { - title: { eastAsia: preferredFont(["方正小标宋简体", "方正小标宋_GBK", "FZXiaoBiaoSong-B05S"]), ascii: "Times New Roman", cs: "Times New Roman" }, - subtitle: { eastAsia: preferredFont(["仿宋", "仿宋_GB2312", "STFangsong"]), ascii: "Times New Roman", cs: "Times New Roman" }, - body: { eastAsia: preferredFont(["仿宋", "仿宋_GB2312", "FangSong", "FangSong_GB2312", "STFangsong"]), ascii: "Times New Roman", cs: "Times New Roman" }, - h1: { eastAsia: preferredFont(["黑体", "SimHei", "Heiti SC", "STHeiti"]), ascii: "Times New Roman", cs: "Times New Roman" }, - h2: { eastAsia: preferredFont(["楷体", "楷体_GB2312", "KaiTi", "Kaiti SC", "STKaiti"]), ascii: "Times New Roman", cs: "Times New Roman" }, - song: { eastAsia: preferredFont(["宋体", "SimSun"]), ascii: "宋体", cs: "宋体" }, + title: { eastAsia: preferredFont(["方正小标宋简体", "方正小标宋_GBK", "FZXiaoBiaoSong-B05S", "FZXiaoBiaoSong-B13S"], "Songti SC"), ascii: "Times New Roman", cs: "Times New Roman" }, + subtitle: { eastAsia: preferredFont(["仿宋", "仿宋_GB2312", "STFangsong"], "STFangsong"), ascii: "Times New Roman", cs: "Times New Roman" }, + body: { eastAsia: preferredFont(["仿宋", "仿宋_GB2312", "FangSong", "FangSong_GB2312", "STFangsong"], "STFangsong"), ascii: "Times New Roman", cs: "Times New Roman" }, + h1: { eastAsia: preferredFont(["黑体", "SimHei", "Heiti SC", "STHeiti"], "Heiti SC"), ascii: "Times New Roman", cs: "Times New Roman" }, + h2: { eastAsia: preferredFont(["楷体", "楷体_GB2312", "KaiTi", "Kaiti SC", "STKaiti"], "Kaiti SC"), ascii: "Times New Roman", cs: "Times New Roman" }, + song: { eastAsia: preferredFont(["宋体", "SimSun"], "Songti SC"), ascii: "宋体", cs: "宋体" }, }; +const STANDARD_FONT_CANDIDATES = { + title: ["方正小标宋简体", "方正小标宋_GBK", "FZXiaoBiaoSong-B05S", "FZXiaoBiaoSong-B13S"], + body: ["仿宋", "仿宋_GB2312", "FangSong", "FangSong_GB2312"], +}; + +function missingFonts() { + return Object.entries(STANDARD_FONT_CANDIDATES) + .filter(([, candidates]) => !candidates.some((font) => INSTALLED_FONTS.has(font))) + .map(([role]) => role); +} + +function reportFontStatus(args) { + const missing = missingFonts(); + if (!missing.length) return; + const details = []; + if (missing.includes("title")) details.push(`标题和发文机关标志使用“${FONT.title.eastAsia}”替代`); + if (missing.includes("body")) details.push(`正文使用“${FONT.body.eastAsia}”替代`); + const message = `当前环境缺少国标常用字体:${details.join(";")}。请安装方正小标宋简体、方正小标宋_GBK 或 FZXiaoBiaoSong,以及可用的仿宋字体后重新生成。`; + if (args["require-standard-fonts"]) throw new Error(`FONT ERROR: ${message}`); + console.error(`FONT WARNING: ${message}`); +} + 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] + 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] Notes: - Converts Markdown to a GB/T 9704-2012 page-layout DOCX. @@ -48,13 +70,14 @@ Notes: - formal is used only when a formal issuing-document layout is explicitly requested. It defaults to preprinted letterhead: red elements are reserved, not redrawn. Use --letterhead digital only for a complete electronic red-head layout. - 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. - No npm dependencies; requires zip in PATH.`); } function parseArgs(argv) { const args = {}; - const flags = new Set(["help", "no-page-number", "no-page-numbers"]); + const flags = new Set(["help", "no-page-number", "no-page-numbers", "require-standard-fonts"]); for (let i = 2; i < argv.length; i += 1) { const key = argv[i]; if (!key.startsWith("--")) continue; @@ -114,26 +137,29 @@ function paragraph(text, opts = {}) { const keepNext = opts.keepNext ? "" : ""; const before = opts.before ?? "0"; const after = opts.after ?? "0"; + const line = opts.line ?? "560"; + const lineRule = opts.lineRule ?? "exact"; const style = opts.style ? `` : ""; const outline = opts.outlineLevel === undefined ? "" : ``; const pageBreak = opts.pageBreakBefore ? "" : ""; - const pPr = `${style}${keepNext}${pageBreak}${align}${indent}${outline}`; + const snapToGrid = opts.snapToGrid === false ? "" : ""; + const pPr = `${style}${keepNext}${pageBreak}${align}${indent}${outline}${snapToGrid}`; return `${pPr}${run(text, opts)}`; } -function titleParagraph(text) { +function titleParagraph(text, before = "0") { return paragraph(text, { align: "center", indent: false, fontPreset: "title", size: "44", - before: "0", + before, after: "560", style: "GongwenTitle", }); } -function agencyMarkParagraph(text) { +function agencyMarkParagraph(text, before = "0") { return paragraph(text, { align: "center", indent: false, @@ -141,7 +167,21 @@ function agencyMarkParagraph(text) { size: "56", color: "FF0000", after: "560", - before: "5953", + before, + }); +} + +function letterheadReserveParagraph(reserveMm) { + const before = Math.round((reserveMm - 37) * 56.692913); + return paragraph("\u00a0", { + align: "left", + indent: false, + fontPreset: "body", + size: "2", + line: "1", + snapToGrid: false, + before: String(before), + after: "0", }); } @@ -231,21 +271,26 @@ function right(text) { return paragraph(text, { align: "right", indent: false, right: FIRST_LINE_INDENT }); } -function redRule(thickness = "8") { - return ``; +function redRule(thickness = "5") { + const height = thickness === "5" ? "0.5mm" : "0.35mm"; + return ``; } 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("4")]; + const body = [redRule("5")]; 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["print-org"] || args["print-date"]) { @@ -253,7 +298,7 @@ function colophon(args) { 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(redRule("4")); + body.push(redRule("5")); return body; } @@ -418,16 +463,21 @@ function renderParagraph(block) { return paragraph(block.text); } +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"]; + if (format === "letter") return "none"; + if (format === "formal" || format === "command" || format === "minutes") return "standard"; + return "center"; +} + function buildDocument(blocks, args) { let title = args.title; const format = ["ordinary", "formal", "letter", "command", "minutes"].includes(args.format) ? args.format : "ordinary"; const letterhead = args.letterhead === "digital" ? "digital" : "preprinted"; - const pageNumberMode = args["no-page-number"] || args["no-page-numbers"] - ? "none" - : ["center", "standard", "none"].includes(args["page-number"]) - ? args["page-number"] - : format === "formal" || format === "letter" || format === "command" || format === "minutes" ? "standard" : "center"; + const pageNumberMode = resolvePageNumberMode(args, format); const body = []; + const pageMarginTop = format === "letter" ? 1701 : format === "command" ? 1134 : 2098; const sourceBlocks = [...blocks]; if (!title && sourceBlocks[0]?.type === "heading") { title = sourceBlocks.shift().text; @@ -444,34 +494,36 @@ function buildDocument(blocks, args) { 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"); 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)); + if (letterhead === "digital" && args.org) body.push(agencyMarkParagraph(args.org, "1984")); 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(paragraph("", { indent: false, before: String(Math.round((reserve - 37) * 56.7)), after: "0", fontPreset: "body" })); + body.push(letterheadReserveParagraph(reserve)); } if (args["doc-no"]) body.push(documentNumberParagraph(args["doc-no"], upward)); if (upward && args.signer) { - body.push(headerFieldParagraph(`签发人:${args.signer}`, "h2", { align: "right", right: FIRST_LINE_INDENT })); + body.push(signerParagraph(args.signer)); } 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: "4252", after: "227" })); + body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "0", after: "227" })); body.push(redRule()); 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: "3402", after: "560" })); + 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" })); } 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: "5953", after: "560" })); + body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "1984", after: "560" })); } - if (title) body.push(titleParagraph(title)); + const hasRedRule = (formal && (letterhead === "digital" || letterhead === "preprinted" || args["preprinted-rule"] === "true")) || format === "letter"; + if (title) body.push(titleParagraph(title, hasRedRule ? "1120" : "0")); if (args.subtitle) body.push(subtitleParagraph(args.subtitle)); if (mainSend) body.push(mainSendParagraph(mainSend)); @@ -512,7 +564,7 @@ function buildDocument(blocks, args) { : pageNumberMode === "center" ? '' : ""; - body.push(`${footerRefs}`); + body.push(`${footerRefs}`); return `${body.join("")}`; } @@ -613,13 +665,16 @@ if (args.help || !args.input || !args.output) { usage(); process.exit(args.help ? 0 : 1); } +try { + reportFontStatus(args); +} catch (error) { + console.error(error.message); + process.exit(2); +} const md = fs.readFileSync(args.input, "utf8"); const blocks = parseMarkdown(md); const documentXml = buildDocument(blocks, args); -const pageNumberMode = args["no-page-number"] || args["no-page-numbers"] - ? "none" - : ["center", "standard", "none"].includes(args["page-number"]) - ? args["page-number"] - : "center"; +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)}`); diff --git a/scripts/verify_gongwen_docx.mjs b/scripts/verify_gongwen_docx.mjs index 23a5b01..f1e26e3 100755 --- a/scripts/verify_gongwen_docx.mjs +++ b/scripts/verify_gongwen_docx.mjs @@ -4,14 +4,20 @@ import { execFileSync } from "node:child_process"; import fs from "node:fs"; function usage() { - console.log("Usage: node verify_gongwen_docx.mjs --input file.docx [--profile ordinary|formal|letter|command|minutes] [--letterhead preprinted|digital]"); + console.log("Usage: node verify_gongwen_docx.mjs --input file.docx [--profile ordinary|formal|letter|command|minutes] [--letterhead preprinted|digital] [--require-standard-fonts]"); } function parseArgs(argv) { const args = {}; + const flags = new Set(["require-standard-fonts"]); for (let i = 2; i < argv.length; i += 1) { if (!argv[i].startsWith("--")) continue; - args[argv[i].slice(2)] = argv[i + 1]; + const name = argv[i].slice(2); + if (flags.has(name)) { + args[name] = true; + continue; + } + args[name] = argv[i + 1]; i += 1; } return args; @@ -34,13 +40,17 @@ 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 requireStandardFonts = args["require-standard-fonts"] === true; const checks = []; function check(name, ok, detail) { checks.push({ name, ok, detail }); } check("A4 page size", /]*w:w="11906"[^>]*w:h="16838"/.test(doc), "210 mm x 297 mm"); -check("Page margins", /]*w:top="2098"[^>]*w:right="1474"[^>]*w:bottom="1984"[^>]*w:left="1588"/.test(doc), "top 37, bottom 35, left 28, right 26 mm"); +const expectedTopMargin = profile === "letter" ? "1701" : profile === "command" ? "1134" : "2098"; +check("Page margins", new RegExp(`]*w:top="${expectedTopMargin}"[^>]*w:right="1474"[^>]*w:bottom="1984"[^>]*w:left="1588"`).test(doc), `top ${expectedTopMargin === "2098" ? 37 : expectedTopMargin === "1701" ? 30 : 20} mm, bottom 35, left 28, right 26 mm`); check("Document grid", /]*w:linePitch="560"/.test(doc), "28 pt implementation grid"); -check("Default body font", /w:eastAsia="(?:仿宋|仿宋_GB2312|FangSong|FangSong_GB2312|STFangsong)"/.test(styles) && / new RegExp(`w:styleId="Heading${level}"`).test(styles)), "Heading 1 through Heading 4 are defined"); const textOf = (xml) => xml.replace(/<[^>]+>/g, ""); @@ -55,6 +65,11 @@ for (const [pattern, style, font] of headingRules) { check(`${style} applied when present`, matches.every((p) => new RegExp(`w:pStyle w:val="${style}"`).test(p) && /w:firstLine="640"/.test(p) && font.test(p)), matches.length ? `${matches.length} matching paragraph(s)` : "not present in this document"); } +const titleParagraphs = paragraphs(doc).filter((p) => /w:pStyle w:val="GongwenTitle"/.test(p)); +const titleFontStandard = titleParagraphs.every((p) => /w:eastAsia="(?:方正小标宋简体|方正小标宋_GBK|FZXiaoBiaoSong-B05S|FZXiaoBiaoSong-B13S)"/.test(p)); +const titleFontAvailable = titleFontStandard || (!requireStandardFonts && titleParagraphs.every((p) => /w:eastAsia="Songti SC"/.test(p))); +check("Title style and font", titleFontAvailable, requireStandardFonts ? "small-standard-title font is required" : titleParagraphs.length ? `${titleParagraphs.length} title paragraph(s); Songti SC fallback is allowed unless strict mode is enabled` : "not present in this document"); + 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"); @@ -67,11 +82,18 @@ if (profile === "ordinary") { 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("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"); } 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"); + check("Title spacing below physical red rule", titleParagraphs.some((p) => /w:before="1120"/.test(p)), "title paragraph reserves two 28-point grid lines below the printed red rule"); } } +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"); +} 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 8d71c9e..d3e8036 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -24,11 +24,26 @@ unzip -p "$TMP_DIR/layout.docx" word/document.xml | grep -q 'w:pStyle w:val="Hea "$NODE" "$VERIFY" --input "$TMP_DIR/standard.docx" --profile formal --letterhead preprinted ! unzip -p "$TMP_DIR/standard.docx" word/document.xml | grep -q '示例单位文件' unzip -p "$TMP_DIR/standard.docx" word/document.xml | grep -q '示例发〔2026〕1号' +unzip -p "$TMP_DIR/standard.docx" word/document.xml | grep -q 'w:before="1120"' "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/digital.docx" --format formal --letterhead digital --org "示例单位文件" --doc-no "示例发〔2026〕1号" --title "电子红头测试" --page-number standard "$NODE" "$VERIFY" --input "$TMP_DIR/digital.docx" --profile formal --letterhead digital unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q '示例单位文件' unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'FF0000' +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"' + +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 + exit 1 + fi +elif ! grep -q 'FONT ERROR:' "$TMP_DIR/strict.out"; then + cat "$TMP_DIR/strict.out" >&2 + echo "strict font mode failed for an unexpected reason" >&2 + exit 1 +fi if command -v soffice >/dev/null 2>&1; then mkdir -p "$TMP_DIR/pdf"