|
| 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