style(代码配色): 全站代码换成 Dracula 配色(亮色用官方浅色版 Alucard);编辑器、题面和教程的代码块、练一练、提交详情的颜色一模一样了
Deploy / deploy (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
原来三套配色各管一块:编辑器是 smoothy / One Dark,正文代码块和练一练是仿 GitHub, 提交详情是 Naive 自带的,同一段 printf 在一课里就有两种颜色。现在静态代码也走编辑器 那套 lezer 解析器,颜色只在 shared/highlight.css 写一份,highlight.js 删掉。 填空题顺手修好:空会把语法搅乱,(humidity ____ 40) 被认成类型声明、humidity 染成类型色, 现在空里先填上答案再解析,答案不输出。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
27210eac1b
commit
df3b4f0dfd
18 files changed
+345
-462
No files matched your search
@@ -1,62 +1,161 @@
|
||||
/**
|
||||
* 代码高亮:自学的练一练、Markdown.vue 里的代码块、以及 App.vue 交给 n-code 的都是这一份
|
||||
* (配色在 highlight.css)。
|
||||
* 只注册判题机认的几种语言,别的语言和不标语言的代码块按原文显示。
|
||||
* 代码高亮:全站一套。编辑器(CodeMirror)和静态的代码(Markdown.vue 的代码块、自学的练一练、
|
||||
* App.vue 交给 n-code 的提交代码)都用同一个解析器(lezer)、同一张类名表,同一段代码在哪儿颜色都一样。
|
||||
* 颜色在 highlight.css。
|
||||
*
|
||||
* 原来静态代码走 highlight.js,它和编辑器的分词不一样(`int` 一边是类型一边是基本类型,
|
||||
* `printf(` 一边是内置函数一边是函数调用),配色表写得再一致,染出来也对不上。
|
||||
*
|
||||
* 只认判题机认的几种语言,别的语言和不标语言的代码块按原文显示。
|
||||
*/
|
||||
import hljs from "highlight.js/lib/core"
|
||||
import c from "highlight.js/lib/languages/c"
|
||||
import cpp from "highlight.js/lib/languages/cpp"
|
||||
import python from "highlight.js/lib/languages/python"
|
||||
import sql from "highlight.js/lib/languages/sql"
|
||||
import { cppLanguage } from "@codemirror/lang-cpp"
|
||||
import { pythonLanguage } from "@codemirror/lang-python"
|
||||
import { SQLite } from "@codemirror/lang-sql"
|
||||
import { highlightCode, tagHighlighter, tags as t } from "@lezer/highlight"
|
||||
import type { ConfigProviderProps } from "naive-ui"
|
||||
|
||||
hljs.registerLanguage("python", python)
|
||||
hljs.registerLanguage("c", c)
|
||||
hljs.registerLanguage("cpp", cpp)
|
||||
hljs.registerLanguage("sql", sql)
|
||||
/** 词的种类 → 类名。编辑器在 shared/themes/codeTheme.ts 里挂的也是它 */
|
||||
export const codeHighlighter = tagHighlighter([
|
||||
// `int` 这类基本类型和关键字一个颜色(Dracula 的 storage.type),自定义的类型才是青色
|
||||
{
|
||||
tag: [
|
||||
t.keyword,
|
||||
t.modifier,
|
||||
t.operator,
|
||||
t.processingInstruction,
|
||||
t.escape,
|
||||
t.standard(t.typeName),
|
||||
],
|
||||
class: "tok-keyword",
|
||||
},
|
||||
{ tag: [t.function(t.variableName), t.function(t.propertyName)], class: "tok-function" },
|
||||
{ tag: t.string, class: "tok-string" },
|
||||
{ tag: [t.number, t.bool, t.null, t.atom, t.self], class: "tok-number" },
|
||||
{ tag: [t.typeName, t.className, t.namespace, t.standard(t.name)], class: "tok-type" },
|
||||
{ tag: t.comment, class: "tok-comment" },
|
||||
{ tag: t.invalid, class: "tok-invalid" },
|
||||
{ tag: t.heading, class: "tok-heading" },
|
||||
{ tag: t.strong, class: "tok-strong" },
|
||||
{ tag: t.emphasis, class: "tok-emphasis" },
|
||||
{ tag: [t.link, t.url], class: "tok-link" },
|
||||
{ tag: t.monospace, class: "tok-monospace" },
|
||||
{ tag: t.quote, class: "tok-quote" },
|
||||
{ tag: t.strikethrough, class: "tok-strikethrough" },
|
||||
])
|
||||
|
||||
export { hljs }
|
||||
type Parser = typeof cppLanguage.parser
|
||||
|
||||
const LANGUAGE_ALIASES: Record<string, string> = {
|
||||
python: "python",
|
||||
py: "python",
|
||||
python3: "python",
|
||||
c: "c",
|
||||
cpp: "cpp",
|
||||
"c++": "cpp",
|
||||
sql: "sql",
|
||||
// C 和编辑器一样用 C++ 的解析器
|
||||
const PARSERS: Record<string, Parser> = {
|
||||
python: pythonLanguage.parser,
|
||||
py: pythonLanguage.parser,
|
||||
python3: pythonLanguage.parser,
|
||||
c: cppLanguage.parser,
|
||||
cpp: cppLanguage.parser,
|
||||
"c++": cppLanguage.parser,
|
||||
sql: SQLite.language.parser,
|
||||
}
|
||||
|
||||
function parserFor(lang?: string): Parser | null {
|
||||
return PARSERS[lang?.toLowerCase() ?? ""] ?? null
|
||||
}
|
||||
|
||||
export function escapeHtml(text: string): string {
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
||||
}
|
||||
|
||||
function normalizeLang(lang?: string): string | null {
|
||||
return LANGUAGE_ALIASES[lang?.toLowerCase() ?? ""] ?? null
|
||||
function span(text: string, classes: string): string {
|
||||
const html = escapeHtml(text)
|
||||
return classes ? `<span class="${classes}">${html}</span>` : html
|
||||
}
|
||||
|
||||
// 把整段代码高亮为 HTML;不支持的语言或异常时回退为转义文本
|
||||
/** 按顺序吐出每一段文字和它的类名,换行单独吐一个 "\n"。跨行的词(块注释、三引号字符串)在换行处断开 */
|
||||
function walk(
|
||||
code: string,
|
||||
lang: string | undefined,
|
||||
put: (text: string, classes: string) => void,
|
||||
) {
|
||||
const parser = parserFor(lang)
|
||||
if (!parser) {
|
||||
put(code, "")
|
||||
return
|
||||
}
|
||||
highlightCode(code, parser.parse(code), codeHighlighter, put, () => put("\n", ""))
|
||||
}
|
||||
|
||||
/** 逐行高亮,返回每行的 HTML,每行的标签自己闭合 */
|
||||
function toLines(code: string, lang?: string): string[] {
|
||||
const lines = [""]
|
||||
walk(code, lang, (text, classes) => {
|
||||
text.split("\n").forEach((part, i) => {
|
||||
if (i > 0) lines.push("")
|
||||
lines[lines.length - 1] += span(part, classes)
|
||||
})
|
||||
})
|
||||
return lines
|
||||
}
|
||||
|
||||
/** 把整段代码高亮为 HTML;不支持的语言按转义后的原文 */
|
||||
export function highlight(code: string, lang?: string): string {
|
||||
const language = normalizeLang(lang)
|
||||
if (language) {
|
||||
try {
|
||||
return hljs.highlight(code, { language }).value
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
return escapeHtml(code)
|
||||
return toLines(code, lang).join("\n")
|
||||
}
|
||||
|
||||
// 按行高亮:整体高亮后再按行切分,保证跨行 token 着色正确,返回逐行 HTML 数组
|
||||
/** 按行高亮:整段一起解析再切行,跨行的词也能染对 */
|
||||
export function highlightLines(lines: string[], lang?: string): string[] {
|
||||
const language = normalizeLang(lang)
|
||||
if (language) {
|
||||
try {
|
||||
const html = hljs.highlight(lines.join("\n"), { language }).value
|
||||
return html.split("\n")
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
return lines.map((line) => escapeHtml(line))
|
||||
return toLines(lines.join("\n"), lang)
|
||||
}
|
||||
|
||||
/**
|
||||
* 填空题用:代码按「空里已经填上答案」的样子解析,但答案不输出,每个空的位置吐一个空的序号。
|
||||
* 直接拿挖了空的代码去解析不行:语法被空搅乱,`(humidity ____ 40)` 会被认成类型声明,
|
||||
* humidity 染成类型的颜色。holes 按位置从前往后排、互不重叠。
|
||||
*/
|
||||
export function highlightWithHoles(
|
||||
code: string,
|
||||
holes: { from: number; to: number }[],
|
||||
lang?: string,
|
||||
): (string | number)[] {
|
||||
const out: (string | number)[] = []
|
||||
let html = ""
|
||||
let pos = 0
|
||||
let hole = 0
|
||||
const flush = () => {
|
||||
if (html) out.push(html)
|
||||
html = ""
|
||||
}
|
||||
walk(code, lang, (text, classes) => {
|
||||
const end = pos + text.length
|
||||
let at = pos
|
||||
while (at < end) {
|
||||
const h = holes[hole]
|
||||
if (h && at >= h.from) {
|
||||
if (at === h.from) {
|
||||
flush()
|
||||
out.push(hole)
|
||||
}
|
||||
at = Math.min(end, h.to)
|
||||
if (at === h.to) hole++
|
||||
continue
|
||||
}
|
||||
const stop = h ? Math.min(end, h.from) : end
|
||||
html += span(text.slice(at - pos, stop - pos), classes)
|
||||
at = stop
|
||||
}
|
||||
pos = end
|
||||
})
|
||||
flush()
|
||||
// 落在代码末尾、长度为 0 的空
|
||||
for (; hole < holes.length; hole++) out.push(hole)
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* 给 n-code 的:它只认 highlight.js 的接口。类型照 highlight.js 写(结果里还要 relevance 之类),
|
||||
* 但 n-code 只调 getLanguage() 和读 highlight().value,所以只给这两样
|
||||
*/
|
||||
export const hljs = {
|
||||
getLanguage: (lang: string) => (parserFor(lang) ? {} : undefined),
|
||||
highlight: (code: string, { language }: { language: string }) => ({
|
||||
value: highlight(code, language),
|
||||
}),
|
||||
} as unknown as NonNullable<ConfigProviderProps["hljs"]>
|
||||
Reference in new issue
Block a user