Files
OJ2/apps/web/src/oj/problem/utils/chinesePunctuation.ts
T
xuyueandClaude Opus 5.5 2a4405d2ea
Deploy / deploy (push) Canceled after 0s
feat(题目): Python 语法错误改成中文说明,在编辑器里标出位置,中文标点可以一键换成英文
中职学生看不懂英文报错。原来提交前用浏览器里的 Skulpt 拦语法错误,只弹一句
「第 N 行存在语法错误」——Skulpt 九成报的是 bad input / bad token,没有列号。

- 提交前的语法检查挪到服务端:/code/format 先用 CPython 编译一遍(只编译不执行),
  api 镜像加 python3-minimal(3.13,与判题机同版本,+17MB),前端删掉 Skulpt
- 报错按规则表翻成中文(pythonError.ts),生产库 2695 条 Python 编译错误覆盖 99.7%;
  提交前拦下的和判题回来的编译错误共用同一张卡片
- 编辑器里标红出错行和 ^ 指的位置
- 中文标点(历史编译错误的 22%)一键换成英文,跳过字符串和注释、处理 f 字符串花括号;
  594 条里修好 593 条,经 tokenize 核对没有改到字符串内容;走编辑器事务,可 Ctrl+Z

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:47:01 +08:00

95 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { PUNCTUATION_MAP } from "./pythonError"
export interface PunctuationFix {
from: number
to: number
insert: string
}
/**
* 找出 Python 代码里**字符串和注释之外**的中文标点,给出换成英文的改动。
*
* 字符串里的中文标点是合法的,`print("你好,世界")` 里那个逗号动了就改了输出,
* 所以不能整篇替换。中文引号「“ ”」另算:它们出现在代码里,本身就是没切输入法
* 打出来的字符串引号,要换成英文引号,而**它们中间的内容原样保留** —— 否则
* `print(“你好,世界”)` 会被改成 `print("你好,世界")`,输出跟着变了。
*
* f 字符串花括号里的是代码不是文字(`f"{s:.2f}"`、`f"{len(a)}"`),照样要换;
* `{{` 是转义出来的字面花括号,不算。拿生产库 591 条中文标点的编译错误核过:
* 修完之后报错点名的那个字符还在的只剩 1 条,是学生自己的引号就是乱的。
*
* 不是完整的词法分析:只认引号、三引号、字符串前缀、反斜杠转义和 `#` 注释,
* 这对学生代码够用。单引号字符串遇到换行就当它结束,免得一个没配对的引号把
* 后面整篇都当成字符串。
*/
export function findChinesePunctuation(code: string): PunctuationFix[] {
const fixes: PunctuationFix[] = []
function fix(at: number) {
const english = PUNCTUATION_MAP[code[at]!]
if (english) fixes.push({ from: at, to: at + 1, insert: english })
}
/** 从 start 往后扫一个字符串的内容,返回结束位置(收尾引号之后) */
function skipString(start: number, triple: boolean, fstring: boolean, closers: string[]) {
let depth = 0
let j = start
while (j < code.length) {
const ch = code[j]!
if (ch === "\\") {
j += 2
continue
}
if (!triple && ch === "\n") return j
if (fstring) {
if (ch === "{" && depth === 0 && code[j + 1] === "{") {
j += 2
continue
}
if (ch === "{") depth++
else if (ch === "}" && depth > 0) depth--
else if (depth > 0) fix(j)
if (depth > 0 || ch === "{" || ch === "}") {
j++
continue
}
}
const closer = closers.find((c) => code.startsWith(c, j))
if (closer) {
// 中文收尾引号也要换成英文
if (closer.length === 1) fix(j)
return j + closer.length
}
j++
}
return j
}
let i = 0
while (i < code.length) {
const ch = code[i]!
if (ch === "#") {
const end = code.indexOf("\n", i)
i = end === -1 ? code.length : end
continue
}
if (ch === '"' || ch === "'") {
// 前缀是紧挨着引号的 f / r / b / u 组合,前面不能再连着别的标识符字符
const prefix = code.slice(0, i).match(/(?<![\w])[rRbBfFuU]{1,2}$/)?.[0] ?? ""
const quote = code.startsWith(ch.repeat(3), i) ? ch.repeat(3) : ch
i = skipString(i + quote.length, quote.length === 3, /f/i.test(prefix), [quote])
continue
}
if (ch === "“" || ch === "”" || ch === "‘" || ch === "’") {
const double = ch === "“" || ch === "”"
fix(i)
// 收尾可能打成中文也可能打成英文,两种都认
i = skipString(i + 1, false, false, double ? ["”", "“", '"'] : ["’", "‘", "'"])
continue
}
fix(i)
i++
}
return fixes
}