Skip to content

Commit 4d898fd

Browse files
committed
feat: 添加标点检查工具并针对 PR 自动检查修改文件
1 parent d977718 commit 4d898fd

2 files changed

Lines changed: 231 additions & 0 deletions

File tree

.github/workflows/checker.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Check Punctuation
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, reopened, synchronize]
6+
paths:
7+
- '**/*.md'
8+
9+
permissions:
10+
contents: read
11+
pull-requests: write
12+
13+
concurrency: ci-${{ github.workflow }}-${{ github.ref }}
14+
15+
jobs:
16+
check-punctuation:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0
23+
repository: ${{ github.event.pull_request.head.repo.full_name }}
24+
ref: ${{ github.event.pull_request.head.ref }}
25+
- name: Setup Python
26+
uses: actions/setup-python@v4
27+
with:
28+
python-version: '3.x'
29+
- name: Get changed files
30+
id: changed-files
31+
uses: tj-actions/changed-files@v46
32+
with:
33+
files: |
34+
**/*.md
35+
- name: Check punctuation
36+
if: steps.changed-files.outputs.any_changed == 'true'
37+
id: punctuation-check
38+
env:
39+
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
40+
CI: 1
41+
run: |
42+
python tools/punctuation_checker.py \
43+
"${{ env.CHANGED_FILES }}" \
44+
"${{ github.event.pull_request.head.repo.clone_url }}" \
45+
"${{ github.event.pull_request.head.ref }}"
46+
- name: Comment PR
47+
if: steps.changed-files.outputs.any_changed == 'true'
48+
uses: thollander/actions-comment-pull-request@v3
49+
with:
50+
file-path: results.txt
51+
comment-tag: punctuation-check

tools/punctuation_checker.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import os
2+
import re
3+
import sys
4+
5+
CJK = "\u2e80-\u2eff\u2f00-\u2fdf\u3040-\u309f\u30a0-\u30fa\u30fc-\u30ff\u3100-\u312f\u3200-\u32ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff"
6+
A = "A-Za-z\u0080-\u00ff\u0370-\u03ff"
7+
N = "0-9"
8+
S = "`~!@#\\$%\\^&\\*\\(\\)-_=\\+\\[\\]{}\\\\\\|;:'\",<.>\\/\\?"
9+
EN_BD = ",.;:?!"
10+
BD = r"。.,、:;!‼?⁇·・‧「『(《〈【〖〔[{」』)》〉】〗〕]}"
11+
12+
13+
class ResultLogger:
14+
def __init__(self, github_url: str = "", github_ref: str = ""):
15+
self.github_url = github_url
16+
self.github_ref = github_ref
17+
self.results = []
18+
19+
def info(self, message: str, path: str, line_no: int):
20+
self.results.append(("INFO", message, path, line_no + 1))
21+
22+
def warn(self, message: str, path: str, line_no: int):
23+
self.results.append(("WARNING", message, path, line_no + 1))
24+
25+
def error(self, message: str, path: str, line_no: int):
26+
self.results.append(("ERROR", message, path, line_no + 1))
27+
28+
def export_result_ci(self):
29+
assert self.github_url and self.github_ref, (
30+
"GitHub URL and ref must be provided for CI export."
31+
)
32+
with open("results.txt", "w", encoding="utf-8") as f:
33+
if self.results:
34+
f.write("标点符号使用情况检查结果(可能存在误判,请人工甄别):\n\n")
35+
f.write("|等级|问题原因|文件路径|\n")
36+
f.write("|:--:|:--|:--|\n")
37+
for level, msg, path, line_no in self.results:
38+
url = f"{self.github_url}/blob/{self.github_ref}/{path}?plain=1#L{line_no}"
39+
f.write(f"|{level}|{msg}|[`{path}:{line_no}`]({url})|\n")
40+
else:
41+
f.write("未发现标点符号相关问题 :)\n")
42+
43+
def export_result_console(self):
44+
if self.results:
45+
print("标点符号使用情况检查结果(可能存在误判,请人工甄别):\n")
46+
print("|等级|问题原因|文件路径|行号|")
47+
print("|:--:|:--|:--|:--:|")
48+
for level, msg, path, line_no in self.results:
49+
print(f"|{level}|{msg}|{path}|{line_no}|")
50+
else:
51+
print("未发现标点符号相关问题 :)")
52+
53+
def export_result_rich(self):
54+
from rich.text import Text
55+
from rich.table import Table
56+
from rich.console import Console
57+
58+
console = Console()
59+
for level, msg, path, line_no in self.results:
60+
output = Table.grid(padding=(0, 1))
61+
output.expand = True
62+
output.add_column(style="log.level", width=None)
63+
output.add_column(ratio=1, style="log.message", no_wrap=False)
64+
output.add_column(style="log.path")
65+
row = []
66+
row.append(Text.styled(level.ljust(8), f"logging.level.{level.lower()}"))
67+
row.append(Text(msg))
68+
row.append(Text(f"{path}:{line_no}"))
69+
output.add_row(*row)
70+
console.print(output)
71+
72+
73+
class PunctuationChecker:
74+
def __init__(self, path: str, logger: ResultLogger):
75+
self.path = path
76+
self.logger = logger
77+
with open(path, "r", encoding="utf-8") as file:
78+
self.content = file.read()
79+
self.content = self.pure_text(self.content)
80+
self.check_punctuation()
81+
82+
def pure_text(self, text: str) -> str:
83+
text = re.sub(r":([^ ]+):", r"\1", text) # remove emoji
84+
text = re.sub(r"!\[[^\]]*\]\([^)]*\)", "", text) # remove images
85+
text = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", text) # replace links with text
86+
text = re.sub(r"<!--.*?-->", "", text) # remove single line comments
87+
text = re.sub(r"\{#.*?\}", "", text) # remove class tags
88+
text = re.sub(r"<a[^>]*>(.*?)</a>", r"\1", text) # replace HTML links with text
89+
return text
90+
91+
def check_parentheses(self, line: str, idx: int):
92+
in_en, in_cn, error_cn_in_en = False, False, False
93+
for char in line:
94+
if in_en and re.match(rf"[{CJK}]", char) and not error_cn_in_en:
95+
self.logger.error("英文括号内出现中文字符", self.path, idx)
96+
error_cn_in_en = True
97+
if char == "(":
98+
if in_en or in_cn:
99+
self.logger.warn("存在括号嵌套,请手动检查", self.path, idx)
100+
return
101+
in_en = True
102+
elif char == ")":
103+
if not in_en:
104+
self.logger.error("出现未配对的英文右括号", self.path, idx)
105+
in_en = False
106+
elif char == "(":
107+
if in_en or in_cn:
108+
self.logger.warn("存在括号嵌套,请手动检查", self.path, idx)
109+
return
110+
in_cn = True
111+
elif char == ")":
112+
if not in_cn:
113+
self.logger.error("出现未配对的中文右括号", self.path, idx)
114+
in_cn = False
115+
if in_en:
116+
self.logger.error("出现未配对的英文左括号", self.path, idx)
117+
if in_cn:
118+
self.logger.warn("出现未配对的中文左括号(", self.path, idx)
119+
120+
def check_punctuation(self):
121+
in_comment = False
122+
for idx, line in enumerate(self.content.splitlines()):
123+
if line.strip() == "" or in_comment:
124+
continue
125+
126+
if "<!--" in line: # has made sure it's multi-line
127+
in_comment = True
128+
line = line.split("<!--")[0]
129+
elif "-->" in line and in_comment:
130+
in_comment = False
131+
line = line.split("-->")[-1]
132+
133+
if "\t" in line:
134+
self.logger.warn("出现制表符", self.path, idx)
135+
if re.findall(rf"[{CJK}] +[{CJK}]", line):
136+
self.logger.error("中文字符间出现空格", self.path, idx)
137+
if re.findall(rf"[{A}{N}{CJK}] +[{EN_BD}{BD}]", line):
138+
self.logger.error("中英字符后接标点间出现空格", self.path, idx)
139+
if re.findall(rf"[{CJK}][{EN_BD}]", line):
140+
self.logger.error("中文字符后接英文标点", self.path, idx)
141+
if re.findall(rf"[{EN_BD}][{CJK}]", line):
142+
self.logger.error("英文标点后接中文字符", self.path, idx)
143+
if re.findall(rf"[{BD}] ", line.strip()):
144+
self.logger.error("中文标点后接空格", self.path, idx)
145+
self.check_parentheses(line, idx)
146+
147+
148+
if __name__ == "__main__":
149+
exclude = ["./docs/major/introduction_to_data_visualization/数据可视化导论小测.md"]
150+
if len(sys.argv) == 4:
151+
files = sys.argv[1].split()
152+
github_url = sys.argv[2].replace(".git", "")
153+
github_ref = sys.argv[3]
154+
elif len(sys.argv) == 2:
155+
files = sys.argv[1].split()
156+
github_url, github_ref = "", ""
157+
else:
158+
files = []
159+
github_url, github_ref = "", ""
160+
logger = ResultLogger(github_url, github_ref)
161+
162+
if files:
163+
for file_path in files:
164+
if not os.path.exists(file_path):
165+
logger.info("文件已被删除", file_path, 0)
166+
continue
167+
PunctuationChecker(file_path, logger)
168+
if os.getenv("CI", "0") == "1":
169+
logger.export_result_ci()
170+
else:
171+
logger.export_result_console()
172+
else:
173+
for root, dirs, files in os.walk("."):
174+
for file in files:
175+
if file.endswith(".md"):
176+
file_path = os.path.join(root, file)
177+
if file_path in exclude:
178+
continue
179+
PunctuationChecker(file_path, logger)
180+
logger.export_result_rich()

0 commit comments

Comments
 (0)