修正预印红头留白并严格校验参数
@@ -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
|
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
|
```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 "公文格式回归测试"
|
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 "公文格式回归测试"
|
||||||
|
|||||||
@@ -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磅版心网格空二行。
|
普通材料使用 `--format ordinary`,默认居中页码,不绘制红头。正式发文须显式使用 `--format formal`;其默认 `--letterhead preprinted`,即首面预留红头纸区域、不在 DOCX 中重绘纸上已有的红色机关标志和红线。`--doc-no` 等本次需打印的黑色变量仍可保留。只有明确要求完整电子版时使用 `--letterhead digital`。完整电子红头的机关标志按标准推荐使用小标宋体,标题按标准使用小标宋体;生成器会在缺失时明确警告并记录实际替代字体,需要硬性阻止替代输出时加 `--require-standard-fonts`。红线下标题按28磅版心网格空二行。
|
||||||
|
|
||||||
```bash
|
```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日"
|
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日"
|
||||||
|
|
||||||
# 完整电子红头版,仅在明确要求时使用
|
# 完整电子红头版,仅在明确要求时使用
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 331 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 328 KiB After Width: | Height: | Size: 359 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 271 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 50 KiB |
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
### 3. 预印红头纸套打与完整电子版
|
### 3. 预印红头纸套打与完整电子版
|
||||||
|
|
||||||
预印红头纸套打是常见制作流程:DOCX 首页预留纸上已印制的机关标志、红线等区域,打印时放入对应红头纸;不在文档中重复绘制这些红色元素。标准规定最终版式,不规定只能使用预印纸。完整电子红头仅在用户明确要求时生成。套打区域高度以实际纸样为准,不能将“35mm”误作全部预留高度。电子红头的发文机关标志与标题必须在目标环境中使用可核对的小标宋体;若字体不存在,生成器会明确警告并显示替代字体,`--require-standard-fonts` 可阻止继续生成。红色分隔线后的标题按版心网格空二行,不以视觉贴近为准。
|
预印红头纸套打是常见制作流程:DOCX 首页预留纸上已印制的机关标志、红线等区域,打印时放入对应红头纸;不在文档中重复绘制这些红色元素。标准规定最终版式,不规定只能使用预印纸。完整电子红头仅在用户明确要求时生成。套打起点高度以实际纸样为准,不能将“35mm”误作全部预留高度;生成器在预印机关标志区域后保留两行28磅版心空行,再排变量文号,标题前再保留红线下两行空行。电子红头的发文机关标志与标题必须在目标环境中使用可核对的小标宋体;若字体不存在,生成器会明确警告并显示替代字体,`--require-standard-fonts` 可阻止继续生成。红色分隔线后的标题按版心网格空二行,不以视觉贴近为准。
|
||||||
|
|
||||||
## 三、纸张、版面和印制
|
## 三、纸张、版面和印制
|
||||||
|
|
||||||
|
|||||||
@@ -826,6 +826,10 @@ function buildDocument(blocks, args) {
|
|||||||
// separator. Its reserve is a paper-coordinate boundary, so optional
|
// separator. Its reserve is a paper-coordinate boundary, so optional
|
||||||
// copy/secret/urgency fields must not shorten it.
|
// copy/secret/urgency fields must not shorten it.
|
||||||
body.push(letterheadReserveParagraph(reserve));
|
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 (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));
|
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();
|
usage();
|
||||||
process.exit(args.help ? 0 : 1);
|
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 {
|
try {
|
||||||
reportFontStatus(args);
|
reportFontStatus(args);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ if (profile === "formal") {
|
|||||||
const titleXmlStart = firstTitleIndex < 0 ? doc.length : doc.indexOf(preprintedParagraphs[firstTitleIndex]);
|
const titleXmlStart = firstTitleIndex < 0 ? doc.length : doc.indexOf(preprintedParagraphs[firstTitleIndex]);
|
||||||
const preprintedHeaderXml = titleXmlStart < 0 ? doc : doc.slice(0, titleXmlStart);
|
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");
|
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");
|
check("Title spacing below physical red rule", hasExplicitTwoLineTitleGap, "title is preceded by two exact 28-point blank paragraphs below the printed red rule");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 '示例单位文件'
|
||||||
unzip -p "$TMP_DIR/standard.docx" word/document.xml | grep -q '示例发〔2026〕1号'
|
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"'
|
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'<w:p>[\s\S]*?</w:p>', 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" "$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
|
"$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
|
fi
|
||||||
grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-attachment.out"
|
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" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/command.docx" --format command --org "示例机关命令" --doc-no "第1号" --title "命令正文"
|
||||||
"$NODE" "$VERIFY" --input "$TMP_DIR/command.docx" --profile command
|
"$NODE" "$VERIFY" --input "$TMP_DIR/command.docx" --profile command
|
||||||
|
|
||||||
|
|||||||