diff --git a/dist/traework-gongwen-skill.zip b/dist/traework-gongwen-skill.zip index 1a4249b..902f267 100644 Binary files a/dist/traework-gongwen-skill.zip and b/dist/traework-gongwen-skill.zip differ diff --git a/scripts/verify_gongwen_docx.mjs b/scripts/verify_gongwen_docx.mjs index e9e01a5..748bf18 100755 --- a/scripts/verify_gongwen_docx.mjs +++ b/scripts/verify_gongwen_docx.mjs @@ -165,9 +165,23 @@ if (profile === "formal") { 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 { - const preprintedReserveParagraph = /w:line="1" w:lineRule="exact"[\s\S]*?w:snapToGrid w:val="false"[\s\S]*?w:sz w:val="2"/.test(doc); - check("Preprinted letterhead reserve", preprintedReserveParagraph, "first-page top reserve is present"); const preprintedParagraphs = paragraphs(doc); + const reserveCandidate = (p) => /w:jc w:val="left"/.test(p) + && /w:line="1"/.test(p) + && /w:lineRule="exact"/.test(p) + && /w:snapToGrid w:val="false"/.test(p) + && /w:sz w:val="2"/.test(p) + && /]*> <\/w:t>/.test(p); + const reserveIndex = preprintedParagraphs.findIndex(reserveCandidate); + const reserveParagraph = reserveIndex >= 0 ? preprintedParagraphs[reserveIndex] : ""; + const reserveSpacing = reserveParagraph.match(/]*>/)?.[0] ?? ""; + const reserveBefore = Number((reserveSpacing.match(/w:before="(\d+)"/) ?? [])[1]); + const reserveBeforeInRange = Number.isFinite(reserveBefore) && reserveBefore >= 0 && reserveBefore <= Math.round((130 - 37) * 56.692913); + const priorParagraphsAreFloating = reserveIndex >= 0 && preprintedParagraphs.slice(0, reserveIndex).every((p) => /w:framePr\b/.test(p)); + const preprintedReserveParagraph = reserveIndex >= 0 + && priorParagraphsAreFloating + && reserveBeforeInRange; + check("Preprinted letterhead reserve", preprintedReserveParagraph, "first non-floating body paragraph reserves 37-130 mm paper-top agency-mark space"); 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"); diff --git a/tests/run_tests.sh b/tests/run_tests.sh index f813d08..cbeb5da 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -42,6 +42,25 @@ PY "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/preprinted-minimum.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 37 --doc-no "示例发〔2026〕7号" --title "最小预印留白测试" "$NODE" "$VERIFY" --input "$TMP_DIR/preprinted-minimum.docx" --profile formal --letterhead preprinted +python3 - "$TMP_DIR/preprinted-minimum.docx" "$TMP_DIR/preprinted-malformed.docx" <<'PY' +import sys, zipfile +source, target = sys.argv[1:] +with zipfile.ZipFile(source) as src, zipfile.ZipFile(target, 'w', zipfile.ZIP_DEFLATED) as dst: + for item in src.infolist(): + data = src.read(item.filename) + if item.filename == 'word/document.xml': + text = data.decode() + old = '' + assert text.count(old) == 1 + text = text.replace(old, '', 1) + data = text.encode() + dst.writestr(item, data) +PY +if "$NODE" "$VERIFY" --input "$TMP_DIR/preprinted-malformed.docx" --profile formal >"$TMP_DIR/preprinted-malformed.out" 2>&1; then + echo "malformed preprinted reserve unexpectedly passed" >&2 + exit 1 +fi +grep -q 'Preprinted letterhead reserve' "$TMP_DIR/preprinted-malformed.out" "$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