完善国标公文全条款核验与特定版式

This commit is contained in:
mizzlelover
2026-09-11 07:51:42 +08:00
parent ac994d4881
commit de1fd6574d
44 changed files with 319 additions and 77 deletions
+43
View File
@@ -0,0 +1,43 @@
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
+9 -5
View File
@@ -20,11 +20,12 @@ function decodeHtml(value) {
function words(pdf) {
const html = execFileSync("pdftotext", ["-f", "1", "-l", "1", "-bbox-layout", pdf, "-"], { encoding: "utf8" });
return [...html.matchAll(/<word\s+[^>]*xMin="([0-9.]+)"[^>]*yMin="([0-9.]+)"[^>]*xMax="([0-9.]+)"[^>]*>([\s\S]*?)<\/word>/g)].map((match) => ({
return [...html.matchAll(/<word\s+[^>]*xMin="([0-9.]+)"[^>]*yMin="([0-9.]+)"[^>]*xMax="([0-9.]+)"[^>]*yMax="([0-9.]+)"[^>]*>([\s\S]*?)<\/word>/g)].map((match) => ({
xMinPt: Number(match[1]),
yMinPt: Number(match[2]),
xMaxPt: Number(match[3]),
text: decodeHtml(match[4]),
yMaxPt: Number(match[4]),
text: decodeHtml(match[5]),
}));
}
@@ -34,9 +35,12 @@ function findText(pdfWords, expected) {
// measurement is independent of that tokenization choice.
const rows = [];
for (const word of [...pdfWords].sort((left, right) => left.yMinPt - right.yMinPt || left.xMinPt - right.xMinPt)) {
const row = rows.find(({ yMinPt }) => Math.abs(yMinPt - word.yMinPt) <= 1.5);
if (row) row.words.push(word);
else rows.push({ yMinPt: word.yMinPt, words: [word] });
const row = rows.find(({ yMinPt, yMaxPt }) => word.yMinPt <= yMaxPt + 1.5 && word.yMaxPt >= yMinPt - 1.5);
if (row) {
row.words.push(word);
row.yMinPt = Math.min(row.yMinPt, word.yMinPt);
row.yMaxPt = Math.max(row.yMaxPt, word.yMaxPt);
} else rows.push({ yMinPt: word.yMinPt, yMaxPt: word.yMaxPt, words: [word] });
}
for (const row of rows) {
const ordered = row.words.sort((left, right) => left.xMinPt - right.xMinPt);
+20
View File
@@ -12,6 +12,7 @@ FIXTURE="$SKILL_DIR/tests/fixture.md"
"$NODE" --check "$GEN"
"$NODE" --check "$VERIFY"
"$NODE" --check "$SKILL_DIR/tests/measure-header-coordinates.mjs"
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/layout.docx" --format ordinary --org "不应变红头的单位" --title "公文格式回归测试" --page-number center
"$NODE" "$VERIFY" --input "$TMP_DIR/layout.docx" --profile ordinary
@@ -74,6 +75,9 @@ unzip -p "$TMP_DIR/letter.docx" word/footerLetter.xml | grep -q 'w:fill="FF0000"
unzip -p "$TMP_DIR/with-attachment.docx" word/document.xml | grep -q '附件:1. 测试附件'
unzip -p "$TMP_DIR/with-attachment.docx" word/document.xml | grep -q 'w:pageBreakBefore'
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/detached-attachment.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔20261号" --title "分离附件测试" --attachment-note "1. 测试附件" --attachment-file "$TMP_DIR/attachment.md" --attachment-detached
unzip -p "$TMP_DIR/detached-attachment.docx" word/document.xml | grep -q '示例发〔20261号 附件1'
if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/bad-attachment.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔20266号" --title "附件一致性测试" --attachment-note "1. 另一份附件" --attachment-file "$TMP_DIR/attachment.md" >"$TMP_DIR/bad-attachment.out" 2>&1; then
echo "mismatched attachment note unexpectedly succeeded" >&2
exit 1
@@ -91,6 +95,10 @@ grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-command.out"
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/minutes.docx" --format minutes --org "示例机关纪要" --title "纪要正文" --attendees "张三、李四" --absent "王五"
"$NODE" "$VERIFY" --input "$TMP_DIR/minutes.docx" --profile minutes
minutes_xml=$(unzip -p "$TMP_DIR/minutes.docx" word/document.xml)
test "${minutes_xml%%出席:*}" != "$minutes_xml"
test "${minutes_xml%%w:sectPr*}" != "$minutes_xml"
test "${minutes_xml%%出席:*}" != "${minutes_xml%%w:sectPr*}"
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/seal-layout.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "签章位置测试" --sender "示例机关" --date "2026年9月10日" --seal-mode seal
unzip -p "$TMP_DIR/seal-layout.docx" word/document.xml | grep -q 'w:right="1280"'
@@ -112,6 +120,18 @@ grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-date.out"
unzip -p "$TMP_DIR/letter-fields.docx" word/document.xml | grep -q '000007'
unzip -p "$TMP_DIR/letter-fields.docx" word/document.xml | grep -q '机密★3年'
"$NODE" "$GEN" --input "$SKILL_DIR/tests/fixtures/visual-horizontal.md" --output "$TMP_DIR/horizontal-table.docx" --format horizontal-table --title "横排表格测试" --page-number standard
"$NODE" "$VERIFY" --input "$TMP_DIR/horizontal-table.docx" --profile horizontal-table
unzip -p "$TMP_DIR/horizontal-table.docx" word/document.xml | grep -q 'w:orient="landscape"'
unzip -p "$TMP_DIR/horizontal-table.docx" word/document.xml | grep -q 'w:tblW w:w="12756"'
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/joint.docx" --format formal --letterhead digital --org "主办机关文件" --joint-org "协办机关" --doc-no "示例发〔20263号" --title "联合行文测试"
"$NODE" "$VERIFY" --input "$TMP_DIR/joint.docx" --profile formal --letterhead digital
unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q '主办机关'
unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q '协办机关'
unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q '文件'
unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q 'w:sz w:val="48"'
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
+9 -2
View File
@@ -15,6 +15,7 @@ long="$skill_dir/tests/fixtures/visual-long.md"
attach_a="$skill_dir/tests/fixtures/visual-attachment-a.md"
attach_b="$skill_dir/tests/fixtures/visual-attachment-b.md"
horizontal="$skill_dir/tests/fixtures/visual-horizontal.md"
calibration="$skill_dir/tests/fixtures/visual-calibration.md"
node "$gen" --input "$all" --output "$out_dir/docx/ordinary.docx" --format ordinary --title "普通材料全要素实测" --page-number center
node "$gen" --input "$all" --output "$out_dir/docx/formal-digital-all.docx" --format formal --letterhead digital --copy-no 7 --secret "机密★3年" --urgent "特急" --org "示例单位文件" --doc-no "示例发〔20267号" --title "正式公文全要素实测" --to "各主送机关" --sender "示例单位" --date "2026年9月10日" --note "此件公开" --attachment-note "1. 数据清单;2. 实施计划。" --cc "办公室、财务部" --print-org "示例印发机关" --print-date "2026年9月10日" --page-number standard
@@ -22,25 +23,31 @@ node "$gen" --input "$all" --output "$out_dir/docx/formal-digital-upward.docx" -
node "$gen" --input "$all" --output "$out_dir/docx/formal-preprinted.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --copy-no 7 --secret "机密★3年" --urgent "特急" --doc-no "示例发〔20269号" --title "预印红头套打实测" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/formal-seal.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "盖章公文实测" --sender "示例单位" --date "2026年9月10日" --seal-mode seal --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/formal-signed.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "签名章公文实测" --sender "示例单位" --date "2026年9月10日" --seal-mode signed --signer-title "主要负责人" --signer "张三" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/formal-unsigned.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "不盖章公文实测" --sender "示例单位" --date "2026年9月10日" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/formal-attachments.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔202610号" --title "多附件实测" --attachment-note "1. 数据清单;2. 实施计划。" --attachment-file "$attach_a" --attachment-file "$attach_b" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/formal-attachments-detached.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔202615号" --title "分离装订附件实测" --attachment-note "1. 数据清单;2. 实施计划。" --attachment-file "$attach_a" --attachment-file "$attach_b" --attachment-detached --page-number standard
node "$gen" --input "$long" --output "$out_dir/docx/formal-long.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔202611号" --title "长文版记与单双页实测" --cc "办公室、财务部、审计部、采购部、法务部、信息中心" --print-org "示例印发机关" --print-date "2026年9月10日" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/formal-joint.docx" --format formal --letterhead digital --org "主办机关文件" --joint-org "协办机关一" --joint-org "协办机关二" --doc-no "示例发〔202616号" --title "联合行文实测" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/letter.docx" --format letter --org "示例机关" --doc-no "示例〔202612号" --title "信函格式实测" --page-number none
node "$gen" --input "$all" --output "$out_dir/docx/letter-fields.docx" --format letter --org "示例机关" --copy-no 7 --secret "机密★3年" --urgent "特急" --doc-no "示例〔202614号" --title "信函版头字段实测" --page-number none
node "$gen" --input "$all" --output "$out_dir/docx/command.docx" --format command --org "示例机关命令" --doc-no "第1号" --title "命令格式实测" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/minutes.docx" --format minutes --org "示例机关纪要" --title "纪要格式实测" --attendees "张三(办公室)、李四(财务部)" --absent "王五(审计部)" --observers "赵六(采购部)" --page-number standard
node "$gen" --input "$horizontal" --output "$out_dir/docx/table.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔202613号" --title "表格版式实测" --page-number standard
node "$gen" --input "$horizontal" --output "$out_dir/docx/horizontal-table.docx" --format horizontal-table --title "横排表格实测" --page-number standard
node "$gen" --input "$calibration" --output "$out_dir/docx/ordinary-calibration.docx" --format ordinary --page-number center
for file in "$out_dir/docx"/*.docx; do
base=$(basename "$file" .docx)
case "$base" in
ordinary) profile=ordinary ;;
ordinary|ordinary-calibration) profile=ordinary ;;
letter|letter-fields) profile=letter ;;
horizontal-table) profile=horizontal-table ;;
command) profile=command ;;
minutes) profile=minutes ;;
*) profile=formal ;;
esac
case "$base" in
formal-digital-*) letterhead=digital ;;
formal-digital-*|formal-joint) letterhead=digital ;;
*) letterhead=preprinted ;;
esac
node "$verify" --input "$file" --profile "$profile" --letterhead "$letterhead" >"$out_dir/logs/$base.verify.log"