From c4306b092d0f531c3136622b8feb32e10dfea876 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 19 Oct 2026 22:18:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=B5=81=E7=A8=8B=E5=9B=BE):=20=E8=80=81?= =?UTF-8?q?=E5=B8=88=E7=BB=99=E7=9A=84=E5=8F=82=E8=80=83=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E5=9B=BE=E6=98=AF=E4=B8=80=E5=A0=86=E5=BD=A9=E8=89=B2=E6=96=B9?= =?UTF-8?q?=E6=A1=86=EF=BC=8C=E5=92=8C=E5=AD=A6=E7=94=9F=E7=94=BB=E7=9A=84?= =?UTF-8?q?=E6=A0=87=E5=87=86=E7=AC=A6=E5=8F=B7=E5=AF=B9=E4=B8=8D=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 题面里的 36 张参考图都是 2026-10 之前手写的 Mermaid,一个框的种类都没标,只能按字分彩色。 显示前按写法和字认框(花括号是判断、开始结束是起止、以输入 / 输出 / 打印开头或写着 input( / print( 的是输入输出框,其余是处理框),换成标准形状和配色。数据不动; 学生的老提交都标了种类,不受影响。36 张全部渲染过,框数不变。 Co-Authored-By: Claude Opus 5.5 --- apps/web/src/shared/composables/useMermaid.ts | 5 +- apps/web/src/utils/legacyFlowchart.ts | 113 ++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/utils/legacyFlowchart.ts diff --git a/apps/web/src/shared/composables/useMermaid.ts b/apps/web/src/shared/composables/useMermaid.ts index c7039a0..ddda49a 100644 --- a/apps/web/src/shared/composables/useMermaid.ts +++ b/apps/web/src/shared/composables/useMermaid.ts @@ -6,6 +6,8 @@ * pele 的版本号锁死(不带 ^):项目 2026-10 才开,小版本也在改排版。 */ +import { normalizeLegacyFlowchart } from "utils/legacyFlowchart" + const semanticNodeClasses = [ "startNode", "endNode", @@ -135,7 +137,8 @@ export function useMermaid() { try { // pele/lazy 按图的类型现拉对应的模块,题目里绝大多数是流程图,只拉那一块 const { renderAsync } = await import("pele/lazy") - const { svg } = await renderAsync(mermaidCode, { + // 老师手写的老参考图一个框的种类都没标,先按标准符号重排(见 legacyFlowchart.ts) + const { svg } = await renderAsync(normalizeLegacyFlowchart(mermaidCode), { // 量字宽用页面真实的字体,否则框会比字窄或宽 fontFamily: getComputedStyle(container).fontFamily, // 题目是老师写的、提交是学生写的,图里的链接和外链图片一律不画 diff --git a/apps/web/src/utils/legacyFlowchart.ts b/apps/web/src/utils/legacyFlowchart.ts new file mode 100644 index 0000000..3e1cc24 --- /dev/null +++ b/apps/web/src/utils/legacyFlowchart.ts @@ -0,0 +1,113 @@ +/** + * 老师手写的老参考流程图,显示前按标准符号重排。 + * + * 现在的流程图(学生的画布、出题页的参考图)都由结构树生成,每个框带 `class nX input` 这样的 + * 种类,useMermaid 按种类上标准的形状和颜色。2026-10 之前老师直接手写 Mermaid 的参考图 + * (库里 36 张,全是这种)一个种类都没标:输入写成普通方框 `B[输入n]`、开始写成正圆, + * 显示时只能按框里的字分彩色,和学生图上的标准符号对不上。 + * + * 这里只处理**一个种类都没标**的图(学生的老提交全都标了,不受影响),按写法和字认框: + * 花括号是判断;字是「开始 / 结束」的是起止;以「输入」「输出」「打印」开头(或者直接写着 + * input( / print()的是输入输出框;其余是处理框。认出来就换成标准形状、补上种类。靠字猜的:「a = 输入」会被当成处理框。 + * 数据不动,只改显示。 + */ + +const SEMANTIC_CLASS = + /^\s*class\s+[\w,-]+\s+(startNode|endNode|startEnd|input|output|process|decision|loop)\s*;?\s*$/m + +/** 节点的开口和对应的收口,长的在前(`((` 要先于 `(` 认) */ +const SHAPES: [string, string][] = [ + ["((", "))"], + ["([", "])"], + ["[[", "]]"], + ["[(", ")]"], + ["{{", "}}"], + ["[/", "/]"], + ["[\\", "\\]"], + ["[", "]"], + ["(", ")"], + ["{", "}"], +] + +/** 这些开头的行不是节点:声明、分组、样式 */ +const SKIP_LINE = /^\s*(graph|flowchart|subgraph|end\s*$|class|classDef|style|linkStyle|click|%%)/ + +const START = /^(开始|start)$/i +const END = /^(结束|end)$/i +const INPUT = /^(输入|读入|读取|input)/i +const OUTPUT = /^(输出|打印|显示|print|output)/i + +type Kind = "startNode" | "endNode" | "input" | "output" | "process" | "decision" + +function plain(body: string) { + const text = body.trim() + return text.startsWith('"') && text.endsWith('"') && text.length >= 2 ? text.slice(1, -1) : text +} + +function kindOf(open: string, text: string): Kind { + const word = text.replace(//gi, " ").trim() + if (START.test(word)) return "startNode" + if (END.test(word)) return "endNode" + if (open === "{" || open === "{{") return "decision" + // 有老师直接写代码:「a = float(input())」是读数据 + if (INPUT.test(word) || /\binput\s*\(/.test(word)) return "input" + if (OUTPUT.test(word)) return "output" + return "process" +} + +function shaped(kind: Kind, text: string) { + const label = `"${text.replace(/"/g, "#quot;")}"` + if (kind === "startNode" || kind === "endNode") return `([${label}])` + if (kind === "decision") return `{${label}}` + if (kind === "input" || kind === "output") return `[/${label}/]` + return `[${label}]` +} + +/** 从 start 起找收口,引号里的不算 */ +function closeAt(line: string, start: number, close: string) { + let quoted = false + for (let i = start; i < line.length; i++) { + if (line[i] === '"') quoted = !quoted + else if (!quoted && line.startsWith(close, i)) return i + } + return -1 +} + +function rewriteLine(line: string, kinds: Map) { + let out = "" + let i = 0 + while (i < line.length) { + const id = /^[A-Za-z_][\w-]*/.exec(line.slice(i)) + const prev = i > 0 ? line[i - 1]! : " " + if (!id || /[\w-]/.test(prev)) { + out += line[i] + i++ + continue + } + const after = i + id[0].length + const shape = SHAPES.find(([open]) => line.startsWith(open, after)) + const end = shape ? closeAt(line, after + shape[0].length, shape[1]) : -1 + if (!shape || end < 0) { + out += id[0] + i = after + continue + } + const text = plain(line.slice(after + shape[0].length, end)) + const kind = kindOf(shape[0], text) + kinds.set(id[0], kind) + out += id[0] + shaped(kind, text) + i = end + shape[1].length + } + return out +} + +export function normalizeLegacyFlowchart(code: string) { + if (!/^\s*(graph|flowchart)\b/.test(code) || SEMANTIC_CLASS.test(code) || code.includes(":::")) + return code + const kinds = new Map() + const lines = code + .split("\n") + .map((line) => (SKIP_LINE.test(line) ? line : rewriteLine(line, kinds))) + if (!kinds.size) return code + return [...lines, "", ...[...kinds].map(([id, kind]) => ` class ${id} ${kind}`)].join("\n") +}