149 lines
5.7 KiB
Python
149 lines
5.7 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""页面文案守卫·口语化词 (2026-09-14 页面专业化包, 台账 015)。静态扫描, 不连库、不起浏览器。
|
|||
|
|
|
|||
|
|
## 为什么要有它
|
|||
|
|
|
|||
|
|
用户的第一条意见是「页面文案口语化,不像交易系统」。板块标题写成「等我拍板」「我的持仓」,
|
|||
|
|
处置词写成「出手」「休假」,第一二人称满页都是。改完之后要有一道机器检查, 免得日后又飘回口语。
|
|||
|
|
|
|||
|
|
## 它检查什么
|
|||
|
|
|
|||
|
|
一份禁用词清单, 分三类: 第一二人称、拟人与口语、生造动词。**只查会显示给人看的文本** ——
|
|||
|
|
先剥掉三种注释 (HTML 注释、JS 块注释、JS 行注释), 注释里为解释代码用到这些词不算。命中就打
|
|||
|
|
行号、词、片段, 退出码 1。
|
|||
|
|
|
|||
|
|
剥行注释时跳过字符串内容与网址里的双斜线 (`https://`), 免得把字符串里的 `//` 当注释起点。
|
|||
|
|
|
|||
|
|
## 豁免
|
|||
|
|
|
|||
|
|
确实要在显示文本里用某个词 (极少), 用成对标记圈起来并写原因:
|
|||
|
|
模板里: <!-- copy-guard:off 原因... --> ... <!-- copy-guard:on -->
|
|||
|
|
脚本里: // copy-guard:off 原因... ... // copy-guard:on
|
|||
|
|
缺原因或不成对, 判红 (不许无声豁免)。
|
|||
|
|
|
|||
|
|
跑法: python3 scripts/test_page_copy_guard.py
|
|||
|
|
"""
|
|||
|
|
import os
|
|||
|
|
import re
|
|||
|
|
import sys
|
|||
|
|
|
|||
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|||
|
|
PAGE = os.path.join(os.path.dirname(HERE), "app", "web", "static", "index.html")
|
|||
|
|
|
|||
|
|
# 禁用词。命中即报 (在显示文本里)。分三类只为可读, 检查时一视同仁。
|
|||
|
|
BANNED = [
|
|||
|
|
# 第一二人称
|
|||
|
|
"我的", "等我", "等你", "要你", "给你", "替你", "让你", "问过你", "你自己",
|
|||
|
|
"你设", "你改", "你点", "你来", "你现在", "你还没", "你做过", "你来定", "交你定",
|
|||
|
|
# 拟人与口语
|
|||
|
|
"拍板", "点头", "盯着", "在盯", "盯的", "拿不准", "记一笔", "挤掉", "平安无事",
|
|||
|
|
# 生造动词与口语动词
|
|||
|
|
"出手", "休假", "一跳", "发生了什么",
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
OFF, ON = "copy-guard:off", "copy-guard:on"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _strip_line_comment(line: str) -> str:
|
|||
|
|
"""去掉一行里的 JS 行注释 (// 到行尾), 但跳过字符串内的 // 与网址里的 ://。
|
|||
|
|
走一遍字符, 记录当前是否在 ' " ` 里; 不在字符串里遇到 // 且前一个字符不是 : 就截断。"""
|
|||
|
|
q = None # 当前字符串引号, None 表示不在字符串里
|
|||
|
|
i, n = 0, len(line)
|
|||
|
|
while i < n:
|
|||
|
|
ch = line[i]
|
|||
|
|
if q:
|
|||
|
|
if ch == "\\":
|
|||
|
|
i += 2
|
|||
|
|
continue
|
|||
|
|
if ch == q:
|
|||
|
|
q = None
|
|||
|
|
else:
|
|||
|
|
if ch in "'\"`":
|
|||
|
|
q = ch
|
|||
|
|
elif ch == "/" and i + 1 < n and line[i + 1] == "/":
|
|||
|
|
if i > 0 and line[i - 1] == ":": # 网址 :// 不是注释
|
|||
|
|
i += 2
|
|||
|
|
continue
|
|||
|
|
return line[:i]
|
|||
|
|
i += 1
|
|||
|
|
return line
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _exempt_ranges(raw: str):
|
|||
|
|
"""在原始文本上找成对豁免区。返回 (豁免行号集合, 报错列表)。缺原因或不成对判红。"""
|
|||
|
|
bad = []
|
|||
|
|
lines = raw.split("\n")
|
|||
|
|
ranges = set()
|
|||
|
|
stack = []
|
|||
|
|
for i, line in enumerate(lines, 1):
|
|||
|
|
if OFF in line:
|
|||
|
|
after = line.split(OFF, 1)[1]
|
|||
|
|
reason = re.sub(r"[-#>*/\s]+$", "", after).strip(" -#>*/")
|
|||
|
|
if not reason:
|
|||
|
|
bad.append(f"第 {i} 行:copy-guard:off 缺原因")
|
|||
|
|
stack.append(i)
|
|||
|
|
if ON in line:
|
|||
|
|
if not stack:
|
|||
|
|
bad.append(f"第 {i} 行:copy-guard:on 没有对应的 off")
|
|||
|
|
else:
|
|||
|
|
start = stack.pop()
|
|||
|
|
for k in range(start, i + 1):
|
|||
|
|
ranges.add(k)
|
|||
|
|
for s in stack:
|
|||
|
|
bad.append(f"第 {s} 行:copy-guard:off 没有对应的 on")
|
|||
|
|
return ranges, bad
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
raw = open(PAGE, encoding="utf-8").read()
|
|||
|
|
exempt, bad = _exempt_ranges(raw)
|
|||
|
|
|
|||
|
|
# 剥注释 (保行号)
|
|||
|
|
def _blank(m):
|
|||
|
|
return "\n" * m.group(0).count("\n")
|
|||
|
|
src = re.sub(r"<!--.*?-->", _blank, raw, flags=re.S)
|
|||
|
|
src = re.sub(r"/\*.*?\*/", _blank, src, flags=re.S)
|
|||
|
|
lines = src.split("\n")
|
|||
|
|
|
|||
|
|
for i, line in enumerate(lines, 1):
|
|||
|
|
if i in exempt:
|
|||
|
|
continue
|
|||
|
|
text = _strip_line_comment(line)
|
|||
|
|
for w in BANNED:
|
|||
|
|
idx = text.find(w)
|
|||
|
|
if idx >= 0:
|
|||
|
|
seg = text.strip()[:100]
|
|||
|
|
bad.append(f"第 {i} 行:口语化词「{w}」出现在显示文本里。\n {seg}")
|
|||
|
|
|
|||
|
|
# 内置自检: 只在注释里出现不算, 在字符串里出现要算
|
|||
|
|
_self_ok(bad)
|
|||
|
|
|
|||
|
|
if bad:
|
|||
|
|
print(f"页面文案守卫·口语化:发现 {len(bad)} 处")
|
|||
|
|
for m in bad:
|
|||
|
|
print(" FAIL", m)
|
|||
|
|
sys.exit(1)
|
|||
|
|
print(f"ALL OK — 页面文案守卫·口语化:{len(BANNED)} 个禁用词、注释剥离、成对豁免 全部通过")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _self_ok(bad):
|
|||
|
|
"""自检样例: 注释里的禁用词不该报, 字符串里的该报。任一不符往 bad 里加一条。"""
|
|||
|
|
only_comment = " // 这里解释一下等我拍板的逻辑\n const x = 1;"
|
|||
|
|
in_string = " const t = '等我拍板';"
|
|||
|
|
# 注释行剥掉后不含禁用词
|
|||
|
|
c = _strip_line_comment(only_comment.split("\n")[0])
|
|||
|
|
if any(w in c for w in BANNED):
|
|||
|
|
bad.append("自检失败:行注释里的禁用词没有被剥掉")
|
|||
|
|
# 字符串行剥不掉 (没有 //), 仍含禁用词
|
|||
|
|
s = _strip_line_comment(in_string)
|
|||
|
|
if not any(w in s for w in BANNED):
|
|||
|
|
bad.append("自检失败:字符串里的禁用词被误剥")
|
|||
|
|
# 网址不被当注释截断
|
|||
|
|
u = _strip_line_comment(" const url = 'https://a.b/c'; const y = 2;")
|
|||
|
|
if "const y = 2" not in u:
|
|||
|
|
bad.append("自检失败:网址里的 // 被当成注释起点")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|