Deploy / deploy (push) Canceled after 0s
中职学生看不懂英文报错。原来提交前用浏览器里的 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>
95 lines
3.4 KiB
TypeScript
95 lines
3.4 KiB
TypeScript
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
|
||
}
|