diff --git a/README.md b/README.md index 772e5f0..5d0de7c 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ node scripts/generate_gongwen_docx.mjs --input tests/fixture.md --output /tmp/go node scripts/verify_gongwen_docx.mjs --input /tmp/gongwen.docx --profile ordinary ``` -需要正式发文时,必须明确选择版式。下面的命令生成预印红头纸套打稿;首页只留出纸上已有红头和红线区域,DOCX 不会重绘它们。`72` 是默认预留高度,应按本单位实际红头纸改为 `37—130` 毫米。 +需要正式发文时,必须明确选择版式。下面的命令生成预印红头纸套打稿;首页只留出纸上已有红头和红线区域,DOCX 不会重绘它们。`--letterhead-reserve-mm` 表示纸张上边缘到预印机关标志区域起点的高度,生成器随后保留两行 28 磅版心空行再排变量文号,标题前再保留红线下两行空行。`72` 是默认值,应按本单位实际红头纸改为 `37—130` 毫米。 ```bash node scripts/generate_gongwen_docx.mjs --input tests/fixture.md --output /tmp/formal.docx --format formal --letterhead preprinted --letterhead-reserve-mm 72 --org "示例单位文件" --doc-no "示例发〔2026〕1号" --title "公文格式回归测试" diff --git a/SKILL.md b/SKILL.md index 087ced1..23b2377 100644 --- a/SKILL.md +++ b/SKILL.md @@ -28,7 +28,7 @@ node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx -- 普通材料使用 `--format ordinary`,默认居中页码,不绘制红头。正式发文须显式使用 `--format formal`;其默认 `--letterhead preprinted`,即首面预留红头纸区域、不在 DOCX 中重绘纸上已有的红色机关标志和红线。`--doc-no` 等本次需打印的黑色变量仍可保留。只有明确要求完整电子版时使用 `--letterhead digital`。完整电子红头的机关标志按标准推荐使用小标宋体,标题按标准使用小标宋体;生成器会在缺失时明确警告并记录实际替代字体,需要硬性阻止替代输出时加 `--require-standard-fonts`。红线下标题按28磅版心网格空二行。 ```bash -# 预印红头纸套打;72mm 是默认机关标志区域高度,可按实际纸样调为 37—130mm +# 预印红头纸套打;72mm 是默认机关标志区域起点,可按实际纸样调为 37—130mm node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx --format formal --letterhead preprinted --letterhead-reserve-mm 72 --org "发文机关名称" --doc-no "单位发〔2026〕1号" --title "文档标题" --sender "落款单位" --date "2026年9月9日" # 完整电子红头版,仅在明确要求时使用 diff --git a/dist/traework-gongwen-skill.zip b/dist/traework-gongwen-skill.zip index ac7dbb7..dcebbe5 100644 Binary files a/dist/traework-gongwen-skill.zip and b/dist/traework-gongwen-skill.zip differ diff --git a/references/assets/visual-audit/all-rules/command-1.png b/references/assets/visual-audit/all-rules/command-1.png index 7b49bb7..cbcc0a7 100644 Binary files a/references/assets/visual-audit/all-rules/command-1.png and b/references/assets/visual-audit/all-rules/command-1.png differ diff --git a/references/assets/visual-audit/all-rules/command-2.png b/references/assets/visual-audit/all-rules/command-2.png index fe9be1f..57b8100 100644 Binary files a/references/assets/visual-audit/all-rules/command-2.png and b/references/assets/visual-audit/all-rules/command-2.png differ diff --git a/references/assets/visual-audit/all-rules/contact-sheet.png b/references/assets/visual-audit/all-rules/contact-sheet.png index 04eb935..fbdfee9 100644 Binary files a/references/assets/visual-audit/all-rules/contact-sheet.png and b/references/assets/visual-audit/all-rules/contact-sheet.png differ diff --git a/references/assets/visual-audit/all-rules/formal-attachments-3.png b/references/assets/visual-audit/all-rules/formal-attachments-3.png index 6bc5647..ea48995 100644 Binary files a/references/assets/visual-audit/all-rules/formal-attachments-3.png and b/references/assets/visual-audit/all-rules/formal-attachments-3.png differ diff --git a/references/assets/visual-audit/all-rules/formal-attachments-4.png b/references/assets/visual-audit/all-rules/formal-attachments-4.png index fb32399..690d4fb 100644 Binary files a/references/assets/visual-audit/all-rules/formal-attachments-4.png and b/references/assets/visual-audit/all-rules/formal-attachments-4.png differ diff --git a/references/assets/visual-audit/all-rules/formal-attachments-detached-3.png b/references/assets/visual-audit/all-rules/formal-attachments-detached-3.png index b0ea401..7b8b9ea 100644 Binary files a/references/assets/visual-audit/all-rules/formal-attachments-detached-3.png and b/references/assets/visual-audit/all-rules/formal-attachments-detached-3.png differ diff --git a/references/assets/visual-audit/all-rules/formal-digital-all-1.png b/references/assets/visual-audit/all-rules/formal-digital-all-1.png index 9f1b9e8..9bc1849 100644 Binary files a/references/assets/visual-audit/all-rules/formal-digital-all-1.png and b/references/assets/visual-audit/all-rules/formal-digital-all-1.png differ diff --git a/references/assets/visual-audit/all-rules/formal-digital-all-2.png b/references/assets/visual-audit/all-rules/formal-digital-all-2.png index be94306..167bdbf 100644 Binary files a/references/assets/visual-audit/all-rules/formal-digital-all-2.png and b/references/assets/visual-audit/all-rules/formal-digital-all-2.png differ diff --git a/references/assets/visual-audit/all-rules/formal-digital-upward-1.png b/references/assets/visual-audit/all-rules/formal-digital-upward-1.png index 2e81be0..3df802c 100644 Binary files a/references/assets/visual-audit/all-rules/formal-digital-upward-1.png and b/references/assets/visual-audit/all-rules/formal-digital-upward-1.png differ diff --git a/references/assets/visual-audit/all-rules/formal-digital-upward-2.png b/references/assets/visual-audit/all-rules/formal-digital-upward-2.png index 43d1519..e92a12a 100644 Binary files a/references/assets/visual-audit/all-rules/formal-digital-upward-2.png and b/references/assets/visual-audit/all-rules/formal-digital-upward-2.png differ diff --git a/references/assets/visual-audit/all-rules/formal-joint-1.png b/references/assets/visual-audit/all-rules/formal-joint-1.png index 8264580..5e1ba38 100644 Binary files a/references/assets/visual-audit/all-rules/formal-joint-1.png and b/references/assets/visual-audit/all-rules/formal-joint-1.png differ diff --git a/references/assets/visual-audit/all-rules/formal-joint-2.png b/references/assets/visual-audit/all-rules/formal-joint-2.png index 3a1a2df..30e58c1 100644 Binary files a/references/assets/visual-audit/all-rules/formal-joint-2.png and b/references/assets/visual-audit/all-rules/formal-joint-2.png differ diff --git a/references/assets/visual-audit/all-rules/formal-long-2.png b/references/assets/visual-audit/all-rules/formal-long-2.png index 4deac30..a32da7f 100644 Binary files a/references/assets/visual-audit/all-rules/formal-long-2.png and b/references/assets/visual-audit/all-rules/formal-long-2.png differ diff --git a/references/assets/visual-audit/all-rules/formal-long-3.png b/references/assets/visual-audit/all-rules/formal-long-3.png index 12a0951..9c36ce1 100644 Binary files a/references/assets/visual-audit/all-rules/formal-long-3.png and b/references/assets/visual-audit/all-rules/formal-long-3.png differ diff --git a/references/assets/visual-audit/all-rules/formal-preprinted-1.png b/references/assets/visual-audit/all-rules/formal-preprinted-1.png index b95974a..be0062a 100644 Binary files a/references/assets/visual-audit/all-rules/formal-preprinted-1.png and b/references/assets/visual-audit/all-rules/formal-preprinted-1.png differ diff --git a/references/assets/visual-audit/all-rules/formal-preprinted-2.png b/references/assets/visual-audit/all-rules/formal-preprinted-2.png index fe9be1f..e92a12a 100644 Binary files a/references/assets/visual-audit/all-rules/formal-preprinted-2.png and b/references/assets/visual-audit/all-rules/formal-preprinted-2.png differ diff --git a/references/assets/visual-audit/all-rules/formal-seal-2.png b/references/assets/visual-audit/all-rules/formal-seal-2.png index d000960..4404e61 100644 Binary files a/references/assets/visual-audit/all-rules/formal-seal-2.png and b/references/assets/visual-audit/all-rules/formal-seal-2.png differ diff --git a/references/assets/visual-audit/all-rules/formal-signed-2.png b/references/assets/visual-audit/all-rules/formal-signed-2.png index 93a16e3..6194d4a 100644 Binary files a/references/assets/visual-audit/all-rules/formal-signed-2.png and b/references/assets/visual-audit/all-rules/formal-signed-2.png differ diff --git a/references/assets/visual-audit/all-rules/formal-unsigned-1.png b/references/assets/visual-audit/all-rules/formal-unsigned-1.png index 23b43eb..678a197 100644 Binary files a/references/assets/visual-audit/all-rules/formal-unsigned-1.png and b/references/assets/visual-audit/all-rules/formal-unsigned-1.png differ diff --git a/references/assets/visual-audit/all-rules/formal-unsigned-2.png b/references/assets/visual-audit/all-rules/formal-unsigned-2.png index c502441..99b3730 100644 Binary files a/references/assets/visual-audit/all-rules/formal-unsigned-2.png and b/references/assets/visual-audit/all-rules/formal-unsigned-2.png differ diff --git a/references/assets/visual-audit/all-rules/horizontal-table-1.png b/references/assets/visual-audit/all-rules/horizontal-table-1.png index 40a88d3..bb16ef7 100644 Binary files a/references/assets/visual-audit/all-rules/horizontal-table-1.png and b/references/assets/visual-audit/all-rules/horizontal-table-1.png differ diff --git a/references/assets/visual-audit/all-rules/letter-1.png b/references/assets/visual-audit/all-rules/letter-1.png index 96e6a20..567fda6 100644 Binary files a/references/assets/visual-audit/all-rules/letter-1.png and b/references/assets/visual-audit/all-rules/letter-1.png differ diff --git a/references/assets/visual-audit/all-rules/letter-fields-1.png b/references/assets/visual-audit/all-rules/letter-fields-1.png index ab6f2ae..599aef8 100644 Binary files a/references/assets/visual-audit/all-rules/letter-fields-1.png and b/references/assets/visual-audit/all-rules/letter-fields-1.png differ diff --git a/references/assets/visual-audit/all-rules/minutes-1.png b/references/assets/visual-audit/all-rules/minutes-1.png index e67a895..fd61b1e 100644 Binary files a/references/assets/visual-audit/all-rules/minutes-1.png and b/references/assets/visual-audit/all-rules/minutes-1.png differ diff --git a/references/assets/visual-audit/all-rules/minutes-2.png b/references/assets/visual-audit/all-rules/minutes-2.png index d031d1e..7e1b83d 100644 Binary files a/references/assets/visual-audit/all-rules/minutes-2.png and b/references/assets/visual-audit/all-rules/minutes-2.png differ diff --git a/references/assets/visual-audit/all-rules/ordinary-1.png b/references/assets/visual-audit/all-rules/ordinary-1.png index 502f9a2..0c13651 100644 Binary files a/references/assets/visual-audit/all-rules/ordinary-1.png and b/references/assets/visual-audit/all-rules/ordinary-1.png differ diff --git a/references/assets/visual-audit/all-rules/ordinary-calibration-1.png b/references/assets/visual-audit/all-rules/ordinary-calibration-1.png index e1a7a00..0c9c8c4 100644 Binary files a/references/assets/visual-audit/all-rules/ordinary-calibration-1.png and b/references/assets/visual-audit/all-rules/ordinary-calibration-1.png differ diff --git a/references/assets/visual-audit/all-rules/table-1.png b/references/assets/visual-audit/all-rules/table-1.png index ca28f7c..43f0b1b 100644 Binary files a/references/assets/visual-audit/all-rules/table-1.png and b/references/assets/visual-audit/all-rules/table-1.png differ diff --git a/references/gbt9704-2012-summary.md b/references/gbt9704-2012-summary.md index 511602d..f74a261 100644 --- a/references/gbt9704-2012-summary.md +++ b/references/gbt9704-2012-summary.md @@ -25,7 +25,7 @@ ### 3. 预印红头纸套打与完整电子版 -预印红头纸套打是常见制作流程:DOCX 首页预留纸上已印制的机关标志、红线等区域,打印时放入对应红头纸;不在文档中重复绘制这些红色元素。标准规定最终版式,不规定只能使用预印纸。完整电子红头仅在用户明确要求时生成。套打区域高度以实际纸样为准,不能将“35mm”误作全部预留高度。电子红头的发文机关标志与标题必须在目标环境中使用可核对的小标宋体;若字体不存在,生成器会明确警告并显示替代字体,`--require-standard-fonts` 可阻止继续生成。红色分隔线后的标题按版心网格空二行,不以视觉贴近为准。 +预印红头纸套打是常见制作流程:DOCX 首页预留纸上已印制的机关标志、红线等区域,打印时放入对应红头纸;不在文档中重复绘制这些红色元素。标准规定最终版式,不规定只能使用预印纸。完整电子红头仅在用户明确要求时生成。套打起点高度以实际纸样为准,不能将“35mm”误作全部预留高度;生成器在预印机关标志区域后保留两行28磅版心空行,再排变量文号,标题前再保留红线下两行空行。电子红头的发文机关标志与标题必须在目标环境中使用可核对的小标宋体;若字体不存在,生成器会明确警告并显示替代字体,`--require-standard-fonts` 可阻止继续生成。红色分隔线后的标题按版心网格空二行,不以视觉贴近为准。 ## 三、纸张、版面和印制 diff --git a/scripts/generate_gongwen_docx.mjs b/scripts/generate_gongwen_docx.mjs index e02907a..6140242 100755 --- a/scripts/generate_gongwen_docx.mjs +++ b/scripts/generate_gongwen_docx.mjs @@ -826,6 +826,10 @@ function buildDocument(blocks, args) { // separator. Its reserve is a paper-coordinate boundary, so optional // copy/secret/urgency fields must not shorten it. body.push(letterheadReserveParagraph(reserve)); + // The preprinted paper supplies the agency mark and separator. Keep the + // variable document number below the reserved mark block by preserving + // the same two 28-point grid lines used under a digital agency mark. + if (args["doc-no"]) body.push(...blankGridLines(2)); } if (args["doc-no"]) args["doc-no"] = normalizeDocumentNumber(args["doc-no"]); if (upward && args["doc-no"] && args.signer) body.push(upwardHeaderParagraph(args["doc-no"], args.signer)); @@ -1045,6 +1049,15 @@ if (args.help || !args.input || !args.output) { usage(); process.exit(args.help ? 0 : 1); } +const supportedFormats = ["ordinary", "formal", "letter", "command", "minutes", "horizontal-table"]; +try { + if (args.format !== undefined && !supportedFormats.includes(args.format)) throw new Error(`--format must be one of ${supportedFormats.join(", ")}`); + if (args.letterhead !== undefined && !["preprinted", "digital"].includes(args.letterhead)) throw new Error("--letterhead must be preprinted or digital"); + if (args["page-number"] !== undefined && !["center", "standard", "none"].includes(args["page-number"])) throw new Error("--page-number must be center, standard, or none"); +} catch (error) { + console.error(`GENERATOR ERROR: ${error.message}`); + process.exit(2); +} try { reportFontStatus(args); } catch (error) { diff --git a/scripts/verify_gongwen_docx.mjs b/scripts/verify_gongwen_docx.mjs index c41caf5..6c2c468 100755 --- a/scripts/verify_gongwen_docx.mjs +++ b/scripts/verify_gongwen_docx.mjs @@ -170,6 +170,9 @@ if (profile === "formal") { const titleXmlStart = firstTitleIndex < 0 ? doc.length : doc.indexOf(preprintedParagraphs[firstTitleIndex]); const preprintedHeaderXml = titleXmlStart < 0 ? doc : doc.slice(0, titleXmlStart); check("No red drawing for preprinted letterhead", !/w:color w:val="FF0000"/.test(preprintedHeaderXml), "preprinted mode does not redraw red letterhead or rule"); + const preprintedDocNoIndex = preprintedParagraphs.findIndex((p) => /〔\d{4}〕[1-9]\d*号/.test(textOf(p))); + const preprintedDocNoGap = preprintedDocNoIndex >= 0 ? preprintedParagraphs.slice(0, preprintedDocNoIndex).slice(-2) : []; + check("Preprinted agency-to-document number two blank lines", preprintedDocNoIndex < 0 || (preprintedDocNoGap.length === 2 && preprintedDocNoGap.every(isExplicitBlankGridLine)), "document number starts below the reserved physical agency mark block"); check("Title spacing below physical red rule", hasExplicitTwoLineTitleGap, "title is preceded by two exact 28-point blank paragraphs below the printed red rule"); } } diff --git a/tests/run_tests.sh b/tests/run_tests.sh index d0d744b..60dfd70 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -26,6 +26,19 @@ unzip -p "$TMP_DIR/layout.docx" word/document.xml | grep -q 'w:pStyle w:val="Hea ! 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:snapToGrid w:val="false"' +python3 - "$TMP_DIR/standard.docx" <<'PY' +import re +import sys +import zipfile + +with zipfile.ZipFile(sys.argv[1]) as archive: + xml = archive.read("word/document.xml").decode() +paragraphs = re.findall(r'[\s\S]*?', xml) +doc_no_index = next(i for i, paragraph in enumerate(paragraphs) if '示例发〔2026〕1号' in paragraph) +gap = paragraphs[doc_no_index - 2:doc_no_index] +assert len(gap) == 2 +assert all('w:line="560"' in paragraph and 'w:lineRule="exact"' in paragraph and 'w:snapToGrid w:val="false"' in paragraph for paragraph in gap) +PY "$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 @@ -84,6 +97,15 @@ if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/bad-attachment.docx" --f fi grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-attachment.out" +for bad_option in format letterhead page-number; do + output="$TMP_DIR/invalid-$bad_option.out" + if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/invalid-$bad_option.docx" --format ordinary --"$bad_option" "invalid-value" >"$output" 2>&1; then + echo "invalid --$bad_option unexpectedly succeeded" >&2 + exit 1 + fi + grep -q -- "--$bad_option" "$output" +done + "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/command.docx" --format command --org "示例机关命令" --doc-no "第1号" --title "命令正文" "$NODE" "$VERIFY" --input "$TMP_DIR/command.docx" --profile command