feat(代码配色): 代码里的全角标点(print(“你好”)、!=、全角空格)标红加波浪线;Python 的 int / str / list 一律青绿色,print 一律是函数色,"%.f" 也认了
Deploy / deploy (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
12 万条 Python 提交里,代码里带全角标点的 1947 条全部编译出错,占 Python 编译错误的五分之一, 原来的配色下 “你好” 被认成变量名,看着和正常代码一样。 int 原来是 int(input()) 里棕色、map(int, …) 里深蓝;print 碰上 print"yes"、print(c) 会变成 关键字蓝。现在都照 VS Code。C 的解析器碰到全角标点会把后半行当成没有引号的「字符串」, 这种不算字符串,里面的全角标点照样标。 补的这几种标记合在 shared/codeMarks.ts 一处(原 formatPlaceholder.ts),编辑器和静态代码共用; Python 解析器的补丁挪到 extensions/pythonLanguage.ts,免得 language → codeMarks → highlight 绕圈。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3b04afc275
commit
60e96f311e
8 files changed
+262
-111
No files matched your search
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* 在 lezer 分好的词上再补的几种标记。lezer 给不了、但学生的代码里很常见,所以另外扫一遍。
|
||||||
|
* 静态代码在 shared/highlight.ts 里拆 span,编辑器在 shared/extensions/codeMarks.ts 里挂装饰,
|
||||||
|
* 两边都只调 extraMarks(),颜色才一样。
|
||||||
|
*
|
||||||
|
* - 字符串里的格式占位符:printf 的 %d、%.2f、%%,Python 的 "%d 个" % n。
|
||||||
|
* 正则和颜色照 VS Code 的语法文件:C 是 constant.other.placeholder(变量色),
|
||||||
|
* Python 是 constant.character.format.placeholder(关键字色),所以两种语言类名不同。
|
||||||
|
* 写错的(%,2f、%。1f、%S)故意不认,不变色正好提醒学生。
|
||||||
|
* - 字符串和注释之外的全角标点(`print(“你好”)`、`!=`、全角空格):标红加波浪线。
|
||||||
|
* 12 万条 Python 提交里有 1947 条是这样,全部编译出错,占 Python 编译错误的五分之一,
|
||||||
|
* 不标的话 `“你好”` 会被认成变量名,看着和正常代码一样。
|
||||||
|
* - Python 的内置类型(int、str、list…):照 VS Code 一律是类型色。lezer 只按位置分,
|
||||||
|
* `int(input())` 里是函数、`map(int, …)` 里是变量,同一行两个颜色。
|
||||||
|
*/
|
||||||
|
import type { syntaxTree } from "@codemirror/language"
|
||||||
|
|
||||||
|
type Tree = ReturnType<typeof syntaxTree>
|
||||||
|
|
||||||
|
export type CodeFamily = "c" | "python"
|
||||||
|
|
||||||
|
export interface Mark {
|
||||||
|
from: number
|
||||||
|
to: number
|
||||||
|
cls: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FORMAT: Record<CodeFamily, RegExp> = {
|
||||||
|
c: /%(\d+\$)?[#0\- +']*(-?\d+|\*)?(\.(-?\d+|\*)?)?(hh|h|ll|l|j|t|z|L)?[diouxXeEfFgGaAcspn%]/g,
|
||||||
|
python: /%(\([\w\s]*\))?[-+#0 ]*(\d+|\*)?(\.(\d+|\*)?)?[hlL]?[diouxXeEfFgGcrsa%]/g,
|
||||||
|
}
|
||||||
|
|
||||||
|
const FORMAT_CLASS: Record<CodeFamily, string> = {
|
||||||
|
c: "tok-format",
|
||||||
|
python: "tok-format-py",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全角的 ASCII(!(),:;?a1…)、全角空格、、。〈〉《》「」【】、中文引号
|
||||||
|
const FULL_WIDTH = /[!-~ -。〈-】‘’“”]/g
|
||||||
|
|
||||||
|
// VS Code(MagicPython)的 builtin-types
|
||||||
|
const PY_BUILTIN_TYPES = new Set([
|
||||||
|
"bool",
|
||||||
|
"bytearray",
|
||||||
|
"bytes",
|
||||||
|
"classmethod",
|
||||||
|
"complex",
|
||||||
|
"dict",
|
||||||
|
"float",
|
||||||
|
"frozenset",
|
||||||
|
"int",
|
||||||
|
"list",
|
||||||
|
"object",
|
||||||
|
"property",
|
||||||
|
"set",
|
||||||
|
"slice",
|
||||||
|
"staticmethod",
|
||||||
|
"str",
|
||||||
|
"super",
|
||||||
|
"tuple",
|
||||||
|
"type",
|
||||||
|
])
|
||||||
|
|
||||||
|
function matches(text: string, re: RegExp, cls: string): Mark[] {
|
||||||
|
return [...text.matchAll(re)].map((m) => ({ from: m.index, to: m.index + m[0].length, cls }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一个词(lezer 吐出的一段文字和它的类名)上要补的标记,位置相对 text 开头。
|
||||||
|
* prev 是这段文字前面那个字符,用来排除 `obj.int` 这种属性
|
||||||
|
*/
|
||||||
|
export function extraMarks(
|
||||||
|
text: string,
|
||||||
|
classes: string,
|
||||||
|
family: CodeFamily,
|
||||||
|
prev: string,
|
||||||
|
): Mark[] {
|
||||||
|
if (classes.includes("tok-string")) return matches(text, FORMAT[family], FORMAT_CLASS[family])
|
||||||
|
if (classes.includes("tok-comment")) return []
|
||||||
|
if (
|
||||||
|
family === "python" &&
|
||||||
|
prev !== "." &&
|
||||||
|
(classes === "tok-function" || classes === "tok-variable") &&
|
||||||
|
PY_BUILTIN_TYPES.has(text)
|
||||||
|
) {
|
||||||
|
return [{ from: 0, to: text.length, cls: "tok-builtin-type" }]
|
||||||
|
}
|
||||||
|
return matches(text, FULL_WIDTH, "tok-fullwidth")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不是字符串的「字符串」:lezer 的 C 解析器碰到全角标点会把后面一截当成没有引号的 String 节点
|
||||||
|
* (`printf("%d\\n", a);` 里的 `, a);`),整截染成字符串色、全角标点也就不标了。
|
||||||
|
* 开头不是引号(前面可以有 u8 / L / R / f / b 这类前缀)的 String 一律不算字符串
|
||||||
|
*/
|
||||||
|
export function fakeStrings(
|
||||||
|
tree: Tree,
|
||||||
|
read: (from: number, to: number) => string,
|
||||||
|
from = 0,
|
||||||
|
to?: number,
|
||||||
|
): { from: number; to: number }[] {
|
||||||
|
const out: { from: number; to: number }[] = []
|
||||||
|
tree.iterate({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
enter: (n) => {
|
||||||
|
if (n.name === "String" && !/^[uUlLrRbBfF8]*["']/.test(read(n.from, n.from + 4))) {
|
||||||
|
out.push({ from: n.from, to: n.to })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按 extraMarks 把一段文字拆开,每段带上补的类名 */
|
||||||
|
export function splitMarks(
|
||||||
|
text: string,
|
||||||
|
classes: string,
|
||||||
|
/** 判断补什么标记用的类名,平时就是 classes;假字符串(见 fakeStrings)传空 */
|
||||||
|
marksFor: string,
|
||||||
|
family: CodeFamily,
|
||||||
|
prev: string,
|
||||||
|
put: (text: string, classes: string) => void,
|
||||||
|
) {
|
||||||
|
let at = 0
|
||||||
|
for (const m of extraMarks(text, marksFor, family, prev)) {
|
||||||
|
if (m.from > at) put(text.slice(at, m.from), classes)
|
||||||
|
put(text.slice(m.from, m.to), classes ? `${classes} ${m.cls}` : m.cls)
|
||||||
|
at = m.to
|
||||||
|
}
|
||||||
|
if (at < text.length) put(text.slice(at), classes)
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { syntaxTree } from "@codemirror/language"
|
||||||
|
import type { Extension, Range } from "@codemirror/state"
|
||||||
|
import { Decoration, type DecorationSet, type EditorView, ViewPlugin } from "@codemirror/view"
|
||||||
|
import { highlightTree } from "@lezer/highlight"
|
||||||
|
import { type CodeFamily, extraMarks, fakeStrings } from "../codeMarks"
|
||||||
|
import { codeHighlighter } from "../highlight"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑器里补的标记(%d、全角标点、Python 内置类型),规则见 shared/codeMarks.ts。
|
||||||
|
* 和静态代码一样按 codeHighlighter 分出来的词逐个过 extraMarks();只扫看得见的部分。
|
||||||
|
*/
|
||||||
|
export function codeMarks(family: CodeFamily): Extension {
|
||||||
|
const decos = new Map<string, Decoration>()
|
||||||
|
const deco = (cls: string) => {
|
||||||
|
let d = decos.get(cls)
|
||||||
|
if (!d) decos.set(cls, (d = Decoration.mark({ class: cls })))
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
function build(view: EditorView): DecorationSet {
|
||||||
|
const tree = syntaxTree(view.state)
|
||||||
|
const ranges: Range<Decoration>[] = []
|
||||||
|
for (const { from, to } of view.visibleRanges) {
|
||||||
|
const text = view.state.doc.sliceString(from, to)
|
||||||
|
const fake = fakeStrings(tree, (a, b) => view.state.doc.sliceString(a, b), from, to)
|
||||||
|
const prev = (pos: number) => (pos > from ? text[pos - from - 1] : "")
|
||||||
|
const add = (start: number, end: number, classes: string) => {
|
||||||
|
if (fake.some((r) => start >= r.from && start < r.to)) classes = ""
|
||||||
|
for (const m of extraMarks(
|
||||||
|
text.slice(start - from, end - from),
|
||||||
|
classes,
|
||||||
|
family,
|
||||||
|
prev(start),
|
||||||
|
)) {
|
||||||
|
ranges.push(deco(m.cls).range(start + m.from, start + m.to))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// highlightTree 只吐有类名的词,中间没类名的空档(全角标点多半在这里)另外补
|
||||||
|
let at = from
|
||||||
|
highlightTree(
|
||||||
|
tree,
|
||||||
|
codeHighlighter,
|
||||||
|
(start, end, classes) => {
|
||||||
|
if (start > at) add(at, start, "")
|
||||||
|
add(start, end, classes)
|
||||||
|
at = end
|
||||||
|
},
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
)
|
||||||
|
if (to > at) add(at, to, "")
|
||||||
|
}
|
||||||
|
return Decoration.set(ranges, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ViewPlugin.define(
|
||||||
|
(view) => ({
|
||||||
|
decorations: build(view),
|
||||||
|
update(u) {
|
||||||
|
if (u.docChanged || u.viewportChanged || syntaxTree(u.startState) !== syntaxTree(u.state)) {
|
||||||
|
this.decorations = build(u.view)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ decorations: (v) => v.decorations },
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { syntaxTree } from "@codemirror/language"
|
|
||||||
import { RangeSetBuilder, type Extension } from "@codemirror/state"
|
|
||||||
import { Decoration, type DecorationSet, type EditorView, ViewPlugin } from "@codemirror/view"
|
|
||||||
import { FORMAT_CLASS, type FormatFamily, formatSpans } from "../formatPlaceholder"
|
|
||||||
|
|
||||||
/** 编辑器里给字符串中的 %d 这类占位符上色,正则见 shared/formatPlaceholder.ts。只扫看得见的部分 */
|
|
||||||
export function formatPlaceholders(family: FormatFamily): Extension {
|
|
||||||
const mark = Decoration.mark({ class: FORMAT_CLASS[family] })
|
|
||||||
|
|
||||||
function build(view: EditorView): DecorationSet {
|
|
||||||
const builder = new RangeSetBuilder<Decoration>()
|
|
||||||
let last = 0
|
|
||||||
for (const { from, to } of view.visibleRanges) {
|
|
||||||
syntaxTree(view.state).iterate({
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
enter: (node) => {
|
|
||||||
if (node.name !== "String" && node.name !== "FormatString") return
|
|
||||||
// 同一个字符串跨两段可见区(中间折叠了)时只加一次,builder 要求从前往后
|
|
||||||
if (node.from >= last) {
|
|
||||||
const text = view.state.doc.sliceString(node.from, node.to)
|
|
||||||
for (const s of formatSpans(text, family)) {
|
|
||||||
builder.add(node.from + s.from, node.from + s.to, mark)
|
|
||||||
}
|
|
||||||
last = node.to
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return builder.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
return ViewPlugin.define(
|
|
||||||
(view) => ({
|
|
||||||
decorations: build(view),
|
|
||||||
update(u) {
|
|
||||||
if (u.docChanged || u.viewportChanged || syntaxTree(u.startState) !== syntaxTree(u.state)) {
|
|
||||||
this.decorations = build(u.view)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{ decorations: (v) => v.decorations },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,11 @@
|
|||||||
import { cpp } from "@codemirror/lang-cpp"
|
import { cpp } from "@codemirror/lang-cpp"
|
||||||
import { python, pythonLanguage } from "@codemirror/lang-python"
|
import { python } from "@codemirror/lang-python"
|
||||||
import { sql, SQLite } from "@codemirror/lang-sql"
|
import { sql, SQLite } from "@codemirror/lang-sql"
|
||||||
import { LanguageSupport } from "@codemirror/language"
|
import { LanguageSupport } from "@codemirror/language"
|
||||||
import type { Extension } from "@codemirror/state"
|
import type { Extension } from "@codemirror/state"
|
||||||
import { styleTags, tags as t } from "@lezer/highlight"
|
|
||||||
import type { LANGUAGE } from "utils/types"
|
import type { LANGUAGE } from "utils/types"
|
||||||
import { formatPlaceholders } from "./formatPlaceholder"
|
import { codeMarks } from "./codeMarks"
|
||||||
|
import { ojPythonLanguage } from "./pythonLanguage"
|
||||||
/**
|
|
||||||
* Python 的解析器在上色上补几处,向 VS Code 看齐:`from` / `as` / `with` 和 `if`、`import` 一样算管流程
|
|
||||||
* 的词(lezer 把 `from` 和 `def` 归成一类);f-string 的 `{ }` 和 `:.2f` 一样是格式占位符的颜色。
|
|
||||||
* 静态代码(shared/highlight.ts)也用这一份,编辑器和题面的颜色才一样。
|
|
||||||
*/
|
|
||||||
export const ojPythonLanguage = pythonLanguage.configure({
|
|
||||||
props: [
|
|
||||||
styleTags({
|
|
||||||
"from as with": t.controlKeyword,
|
|
||||||
"FormatReplacement/{ FormatReplacement/} FormatSpec/:": t.modifier,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 语言对应的高亮扩展。学生端和教师端共用同一份(两边都是题目页上的 SyncCodeEditor) ——
|
* 语言对应的高亮扩展。学生端和教师端共用同一份(两边都是题目页上的 SyncCodeEditor) ——
|
||||||
@@ -30,7 +16,7 @@ export const ojPythonLanguage = pythonLanguage.configure({
|
|||||||
export function languageExtension(language: LANGUAGE): Extension {
|
export function languageExtension(language: LANGUAGE): Extension {
|
||||||
if (language === "SQL") return sql({ dialect: SQLite, upperCaseKeywords: true })
|
if (language === "SQL") return sql({ dialect: SQLite, upperCaseKeywords: true })
|
||||||
if (language === "Python") {
|
if (language === "Python") {
|
||||||
return [new LanguageSupport(ojPythonLanguage, python().support), formatPlaceholders("python")]
|
return [new LanguageSupport(ojPythonLanguage, python().support), codeMarks("python")]
|
||||||
}
|
}
|
||||||
return [cpp(), formatPlaceholders("c")]
|
return [cpp(), codeMarks("c")]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { pythonLanguage } from "@codemirror/lang-python"
|
||||||
|
import { styleTags, tags as t } from "@lezer/highlight"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Python 的解析器在上色上补几处,向 VS Code 看齐。编辑器(extensions/language.ts)和
|
||||||
|
* 静态代码(shared/highlight.ts)都用这一份,两边的颜色才一样。
|
||||||
|
*
|
||||||
|
* - `from` / `as` / `with` 和 `if`、`import` 一样算管流程的词(lezer 把 `from` 和 `def` 归成一类)
|
||||||
|
* - f-string 的 `{ }` 和 `:.2f` 一样是格式占位符的颜色
|
||||||
|
* - `print` 一律是函数:lezer 把不带括号的 `print`(Python 2 的写法)当关键字,`print"yes"`、
|
||||||
|
* 全角括号的 `print(c)` 就变成关键字色,和旁边的 `print(...)` 不一样
|
||||||
|
*
|
||||||
|
* 单独放一个文件是为了不绕圈:language.ts → codeMarks.ts → highlight.ts 要用到它。
|
||||||
|
*/
|
||||||
|
export const ojPythonLanguage = pythonLanguage.configure({
|
||||||
|
props: [
|
||||||
|
styleTags({
|
||||||
|
"from as with": t.controlKeyword,
|
||||||
|
"FormatReplacement/{ FormatReplacement/} FormatSpec/:": t.modifier,
|
||||||
|
print: t.function(t.variableName),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
/**
|
|
||||||
* 字符串里的格式占位符:printf 的 %d、%.2f、%%,Python 的 "%d 个" % n。
|
|
||||||
* lezer 把整个字符串当一个词,不分出占位符,所以另外扫一遍。静态代码在 shared/highlight.ts 里拆,
|
|
||||||
* 编辑器在 shared/extensions/formatPlaceholder.ts 里挂装饰,两边用这一份正则。
|
|
||||||
*
|
|
||||||
* 正则和颜色照 VS Code 的语法文件:C 是 constant.other.placeholder(变量色),
|
|
||||||
* Python 是 constant.character.format.placeholder(关键字色),所以两种语言类名不同。
|
|
||||||
*/
|
|
||||||
export type FormatFamily = "c" | "python"
|
|
||||||
|
|
||||||
const PATTERNS: Record<FormatFamily, RegExp> = {
|
|
||||||
c: /%(\d+\$)?[#0\- +']*(-?\d+|\*)?(\.(-?\d+|\*)?)?(hh|h|ll|l|j|t|z|L)?[diouxXeEfFgGaAcspn%]/g,
|
|
||||||
python: /%(\([\w\s]*\))?[-+#0 ]*(\d+|\*)?(\.(\d+|\*))?[hlL]?[diouxXeEfFgGcrsa%]/g,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FORMAT_CLASS: Record<FormatFamily, string> = {
|
|
||||||
c: "tok-format",
|
|
||||||
python: "tok-format-py",
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 字符串里每个占位符的位置(相对 text 开头) */
|
|
||||||
export function formatSpans(text: string, family: FormatFamily): { from: number; to: number }[] {
|
|
||||||
return [...text.matchAll(PATTERNS[family])].map((m) => ({
|
|
||||||
from: m.index,
|
|
||||||
to: m.index + m[0].length,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
--code-type: #267f99;
|
--code-type: #267f99;
|
||||||
--code-comment: #008000;
|
--code-comment: #008000;
|
||||||
--code-invalid: #cd3131;
|
--code-invalid: #cd3131;
|
||||||
|
--code-invalid-bg: rgba(205, 49, 49, 0.12);
|
||||||
--code-heading: #800000;
|
--code-heading: #800000;
|
||||||
--code-raw: #800000;
|
--code-raw: #800000;
|
||||||
--code-bold: #000080;
|
--code-bold: #000080;
|
||||||
@@ -31,6 +32,7 @@ html.dark {
|
|||||||
--code-type: #4ec9b0;
|
--code-type: #4ec9b0;
|
||||||
--code-comment: #6a9955;
|
--code-comment: #6a9955;
|
||||||
--code-invalid: #f44747;
|
--code-invalid: #f44747;
|
||||||
|
--code-invalid-bg: rgba(244, 71, 71, 0.18);
|
||||||
--code-heading: #569cd6;
|
--code-heading: #569cd6;
|
||||||
--code-raw: #ce9178;
|
--code-raw: #ce9178;
|
||||||
--code-bold: #569cd6;
|
--code-bold: #569cd6;
|
||||||
@@ -67,18 +69,28 @@ html.dark {
|
|||||||
color: var(--code-invalid);
|
color: var(--code-invalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 字符串里的 %d,见 shared/formatPlaceholder.ts。静态代码是同一个 span 上的两个类名,
|
/* shared/codeMarks.ts 补的几种标记。静态代码是同一个 span 上的两个类名,编辑器里是两层 span、
|
||||||
编辑器里是两层 span、谁包谁不一定,所以三种写法都要 */
|
谁包谁不一定,所以内外两种写法都要,并且要写在上面那些 tok-* 后面 */
|
||||||
.tok-format,
|
.tok-format,
|
||||||
.tok-format .tok-string,
|
.tok-format * {
|
||||||
.tok-string .tok-format {
|
|
||||||
color: var(--code-variable);
|
color: var(--code-variable);
|
||||||
}
|
}
|
||||||
.tok-format-py,
|
.tok-format-py,
|
||||||
.tok-format-py .tok-string,
|
.tok-format-py * {
|
||||||
.tok-string .tok-format-py {
|
|
||||||
color: var(--code-keyword);
|
color: var(--code-keyword);
|
||||||
}
|
}
|
||||||
|
.tok-builtin-type,
|
||||||
|
.tok-builtin-type * {
|
||||||
|
color: var(--code-type);
|
||||||
|
}
|
||||||
|
.tok-fullwidth,
|
||||||
|
.tok-fullwidth * {
|
||||||
|
color: var(--code-invalid);
|
||||||
|
background-color: var(--code-invalid-bg);
|
||||||
|
text-decoration: wavy underline var(--code-invalid);
|
||||||
|
text-decoration-skip-ink: none;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 以下只有 Markdown 编辑器用得到 */
|
/* 以下只有 Markdown 编辑器用得到 */
|
||||||
.tok-heading {
|
.tok-heading {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { cppLanguage } from "@codemirror/lang-cpp"
|
|||||||
import { SQLite } from "@codemirror/lang-sql"
|
import { SQLite } from "@codemirror/lang-sql"
|
||||||
import { highlightCode, tagHighlighter, tags as t } from "@lezer/highlight"
|
import { highlightCode, tagHighlighter, tags as t } from "@lezer/highlight"
|
||||||
import type { ConfigProviderProps } from "naive-ui"
|
import type { ConfigProviderProps } from "naive-ui"
|
||||||
import { ojPythonLanguage } from "./extensions/language"
|
import { type CodeFamily, fakeStrings, splitMarks } from "./codeMarks"
|
||||||
import { FORMAT_CLASS, type FormatFamily, formatSpans } from "./formatPlaceholder"
|
import { ojPythonLanguage } from "./extensions/pythonLanguage"
|
||||||
|
|
||||||
/** 词的种类 → 类名。编辑器在 shared/themes/codeTheme.ts 里挂的也是它 */
|
/** 词的种类 → 类名。编辑器在 shared/themes/codeTheme.ts 里挂的也是它 */
|
||||||
export const codeHighlighter = tagHighlighter([
|
export const codeHighlighter = tagHighlighter([
|
||||||
@@ -55,7 +55,7 @@ function parserFor(lang?: string): Parser | null {
|
|||||||
return PARSERS[lang?.toLowerCase() ?? ""] ?? null
|
return PARSERS[lang?.toLowerCase() ?? ""] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFamilyFor(lang?: string): FormatFamily | null {
|
function familyFor(lang?: string): CodeFamily | null {
|
||||||
const parser = parserFor(lang)
|
const parser = parserFor(lang)
|
||||||
if (parser === cppLanguage.parser) return "c"
|
if (parser === cppLanguage.parser) return "c"
|
||||||
if (parser === ojPythonLanguage.parser) return "python"
|
if (parser === ojPythonLanguage.parser) return "python"
|
||||||
@@ -82,19 +82,22 @@ function walk(
|
|||||||
put(code, "")
|
put(code, "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 字符串里的 %d 这类占位符再拆出来,见 shared/formatPlaceholder.ts
|
// 再补 %d、全角标点、Python 内置类型这几种标记,见 shared/codeMarks.ts
|
||||||
const family = formatFamilyFor(lang)
|
const tree = parser.parse(code)
|
||||||
|
const family = familyFor(lang)
|
||||||
|
const fake = family ? fakeStrings(tree, (from, to) => code.slice(from, to)) : []
|
||||||
|
let pos = 0
|
||||||
const emit = (text: string, classes: string) => {
|
const emit = (text: string, classes: string) => {
|
||||||
if (!family || !classes.includes("tok-string")) return put(text, classes)
|
if (family) {
|
||||||
let at = 0
|
const marks = fake.some((r) => pos >= r.from && pos < r.to) ? "" : classes
|
||||||
for (const s of formatSpans(text, family)) {
|
splitMarks(text, classes, marks, family, code[pos - 1] ?? "", put)
|
||||||
if (s.from > at) put(text.slice(at, s.from), classes)
|
} else put(text, classes)
|
||||||
put(text.slice(s.from, s.to), `${classes} ${FORMAT_CLASS[family]}`)
|
pos += text.length
|
||||||
at = s.to
|
|
||||||
}
|
}
|
||||||
if (at < text.length) put(text.slice(at), classes)
|
highlightCode(code, tree, codeHighlighter, emit, () => {
|
||||||
}
|
put("\n", "")
|
||||||
highlightCode(code, parser.parse(code), codeHighlighter, emit, () => put("\n", ""))
|
pos++
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 逐行高亮,返回每行的 HTML,每行的标签自己闭合 */
|
/** 逐行高亮,返回每行的 HTML,每行的标签自己闭合 */
|
||||||
|
|||||||
Reference in new issue
Block a user