From e9e20e7e79f8177ed197a0ec2517ca40e287aa9c Mon Sep 17 00:00:00 2001 From: root Date: Wed, 7 Oct 2026 02:10:19 +0800 Subject: [PATCH] =?UTF-8?q?chore(copy):=20check:copy=20=E6=8B=A6=E3=80=8C?= =?UTF-8?q?=E5=85=B6=E5=AE=83=E3=80=8D,=E5=B9=B6=E6=9F=A5=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E6=A8=A1=E6=9D=BF=E6=8F=92=E5=80=BC=E7=89=87=E6=AE=B5?= =?UTF-8?q?=E5=BC=80=E5=A4=B4=E7=9A=84=E5=8D=8A=E8=A7=92=E6=A0=87=E7=82=B9?= =?UTF-8?q?=E4=B8=8E=20${a}(${b})=20=E5=8D=8A=E8=A7=92=E6=8B=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei --- scripts/check-copy-punctuation.mjs | 31 +++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/scripts/check-copy-punctuation.mjs b/scripts/check-copy-punctuation.mjs index 572c4863..2558eef1 100644 --- a/scripts/check-copy-punctuation.mjs +++ b/scripts/check-copy-punctuation.mjs @@ -45,6 +45,8 @@ function findIssues(text) { push("括号内有中文应用全角括号", new RegExp(`\\([^()]*[${CJK}][^()]*\\)`, "g")); push("引号应为「」", /[“”]/g); if (termScope) push("处理状态写「整理」不写「入库」(见 glossary)", TERM_RUKU); + // 插值紧邻的半角括号:`${date}(${rel})` 在中文句子里,括号里多半是中文 + push("插值旁的半角括号应为全角(括号内有中文时)", new RegExp(`${PH}\\(${PH}\\)`, "g")); for (const m of text.matchAll(BANNED_WORDS)) out.push({ rule: `用词「${m[0]}」(见 human-copy)`, index: m.index }); return out; } @@ -68,6 +70,7 @@ const BANNED_WORDS = new RegExp( "释放潜能", "尽在掌握", "成功地", + "其它", "恭喜", "太棒了", "哎呀", @@ -197,6 +200,8 @@ function collect(ast, src) { const parts = node.quasis.map((q) => ({ start: q.start, end: q.end, text: src.slice(q.start, q.end) })); if (!parts.some((p) => looksLikeRegex(p.text))) groups.push(parts); } + if (node.quasis.some((q) => CJK_RE.test(q.value.raw))) + for (const e of node.expressions) for (const f of fragmentsOf(e)) checkFragment(f, src); for (const e of node.expressions) walk(e, node, "expressions"); return; } @@ -225,6 +230,24 @@ function collect(ast, src) { return groups; } +/** 插值表达式里会被拼进外层中文模板的字面量(`${x ? `,…` : ""}` 里的 `,…`) */ +function fragmentsOf(e) { + if (!e) return []; + if (e.type === "StringLiteral" || e.type === "TemplateLiteral") return [e]; + if (e.type === "ConditionalExpression") return [...fragmentsOf(e.consequent), ...fragmentsOf(e.alternate)]; + if (e.type === "LogicalExpression") return [...fragmentsOf(e.left), ...fragmentsOf(e.right)]; + if (e.type === "TSAsExpression" || e.type === "ParenthesizedExpression") return fragmentsOf(e.expression); + return []; +} + +/** 外层是中文模板时,插值片段开头的半角 , ; : 会紧贴中文 */ +let fragIssues = []; +function checkFragment(f, src) { + const text = f.type === "StringLiteral" ? src.slice(f.start + 1, f.end - 1) : src.slice(f.quasis[0].start, f.quasis[0].end); + const m = /^\s*[,;:](?!\/\/)/.exec(text); + if (m) fragIssues.push({ start: f.start + 1, rule: "中文模板的插值片段以半角标点开头", excerpt: text.slice(0, 20) }); +} + function lineOf(src, idx) { let line = 1; for (let i = 0; i < idx; i++) if (src.charCodeAt(i) === 10) line++; @@ -252,7 +275,13 @@ function processFile(file) { const lines = src.split("\n"); const issues = []; const edits = []; - for (const g of collect(ast, src)) { + fragIssues = []; + const groups = collect(ast, src); + for (const fi of fragIssues) { + const line = lineOf(src, fi.start); + if (!allowed(lines, line)) issues.push({ line, rule: fi.rule, excerpt: fi.excerpt }); + } + for (const g of groups) { const texts = g.filter(Boolean); if (texts.length === 0) continue; const composed = joinWithPH(g);