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
@@ -34,7 +34,6 @@
|
||||
"copy-text-to-clipboard": "^3.2.2",
|
||||
"date-fns": "^4.4.0",
|
||||
"fflate": "^0.8.3",
|
||||
"highlight.js": "^11.12.0",
|
||||
"lib0": "0.2.119",
|
||||
"markdown-it": "^14.3.0",
|
||||
"mermaid": "^12.1.0",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { darkTheme, dateZhCN, zhCN } from "naive-ui"
|
||||
import "normalize.css"
|
||||
import "./index.css"
|
||||
import "shared/highlight.css"
|
||||
import { useConfigStore } from "shared/store/config"
|
||||
import { useConfigUpdate } from "shared/composables/configUpdate"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
@@ -40,7 +41,8 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// n-code(提交详情、看代码的弹框)用的 highlight.js,空闲时再加载,不挡首屏
|
||||
// n-code(提交详情、看代码的弹框)的高亮,空闲时再加载,不挡首屏。
|
||||
// 它只认 highlight.js 的接口,shared/highlight 给了一个同名的壳,里面是和编辑器同一套解析器
|
||||
const hljsInstance = shallowRef<typeof import("shared/highlight").hljs>()
|
||||
onMounted(() => {
|
||||
const load = async () => {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import ExerciseResult from "./ExerciseResult.vue"
|
||||
import type { Exercise, ExerciseDebugData } from "utils/types"
|
||||
import { highlightLines } from "shared/highlight"
|
||||
import "shared/highlight.css"
|
||||
|
||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import ExerciseResult from "./ExerciseResult.vue"
|
||||
import type { Exercise, ExerciseFillData } from "utils/types"
|
||||
import { highlight } from "shared/highlight"
|
||||
import "shared/highlight.css"
|
||||
import { highlightWithHoles } from "shared/highlight"
|
||||
|
||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
const emit = defineEmits<{
|
||||
@@ -16,25 +15,26 @@ type BlankSeg = { type: "blank"; answers: string[]; index: number }
|
||||
type Segment = CodeSeg | BlankSeg
|
||||
|
||||
const segments = computed<Segment[]>(() => {
|
||||
// 空里先填上第一个答案再解析,免得空把语法搅乱;答案本身不输出,见 highlightWithHoles
|
||||
const blanks: string[][] = []
|
||||
const markedCode = data.value.code.replace(/\{\{([^}]+)\}\}/g, (_, inner) => {
|
||||
blanks.push(inner.split("|"))
|
||||
return `____${blanks.length - 1}____`
|
||||
})
|
||||
|
||||
const highlighted = highlight(markedCode, props.lang)
|
||||
|
||||
const parts = highlighted.split(/____(\d+)____/)
|
||||
const result: Segment[] = []
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i % 2 === 0) {
|
||||
if (parts[i]) result.push({ type: "code", html: parts[i] })
|
||||
} else {
|
||||
const idx = parseInt(parts[i])
|
||||
result.push({ type: "blank", answers: blanks[idx], index: idx })
|
||||
const holes: { from: number; to: number }[] = []
|
||||
let code = ""
|
||||
let last = 0
|
||||
for (const m of data.value.code.matchAll(/\{\{([^}]+)\}\}/g)) {
|
||||
const answers = m[1].split("|")
|
||||
code += data.value.code.slice(last, m.index)
|
||||
holes.push({ from: code.length, to: code.length + answers[0].length })
|
||||
code += answers[0]
|
||||
blanks.push(answers)
|
||||
last = m.index + m[0].length
|
||||
}
|
||||
}
|
||||
return result
|
||||
code += data.value.code.slice(last)
|
||||
|
||||
return highlightWithHoles(code, holes, props.lang).map((part) =>
|
||||
typeof part === "string"
|
||||
? { type: "code", html: part }
|
||||
: { type: "blank", answers: blanks[part], index: part },
|
||||
)
|
||||
})
|
||||
|
||||
const blankCount = computed(() => segments.value.filter((s) => s.type === "blank").length)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import ExerciseResult from "./ExerciseResult.vue"
|
||||
import type { Exercise, ExercisePredictData } from "utils/types"
|
||||
import { highlight } from "shared/highlight"
|
||||
import "shared/highlight.css"
|
||||
|
||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -3,7 +3,6 @@ import ExerciseResult from "./ExerciseResult.vue"
|
||||
import type { Exercise, ExerciseSortData } from "utils/types"
|
||||
import { shuffle } from "../composables/useShuffle"
|
||||
import { highlightLines } from "shared/highlight"
|
||||
import "shared/highlight.css"
|
||||
|
||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useDark } from "@vueuse/core"
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
code: string
|
||||
/** highlight.js 的语言名(LANGUAGE_FORMAT_VALUE) */
|
||||
/** 高亮用的语言名(LANGUAGE_FORMAT_VALUE),见 shared/highlight.ts */
|
||||
language: string
|
||||
/** 要标红的行,1 起 */
|
||||
markLine?: number | null
|
||||
|
||||
@@ -7,8 +7,7 @@ import CodeMirror from "./CodeMirror.vue"
|
||||
import type { Extension } from "@codemirror/state"
|
||||
import { autocompletion, closeBrackets, completeAnyWord } from "@codemirror/autocomplete"
|
||||
import type { LANGUAGE } from "utils/types"
|
||||
import { oneDark } from "../themes/oneDark"
|
||||
import { smoothy } from "../themes/smoothy"
|
||||
import { darkCodeTheme, lightCodeTheme } from "../themes/codeTheme"
|
||||
import { styleTheme } from "shared/extensions/baseTheme"
|
||||
import { enhanceCompletion } from "shared/extensions/autocompletion"
|
||||
|
||||
@@ -47,7 +46,7 @@ const extensions = computed(() => [
|
||||
autocompletion({
|
||||
override: [enhanceCompletion(language), completeAnyWord],
|
||||
}),
|
||||
isDark.value ? oneDark : smoothy,
|
||||
isDark.value ? darkCodeTheme : lightCodeTheme,
|
||||
...extraExtensions,
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { renderMarkdown } from "shared/markdown"
|
||||
import "shared/highlight.css"
|
||||
|
||||
/**
|
||||
* 显示一段 Markdown / HTML 正文:题面、教程、AI 的提示和报告。
|
||||
|
||||
@@ -6,8 +6,7 @@ import { Icon } from "@iconify/vue"
|
||||
import CodeMirror from "./CodeMirror.vue"
|
||||
import { uploadImage } from "../../admin/api"
|
||||
import { styleTheme } from "../extensions/baseTheme"
|
||||
import { oneDark } from "../themes/oneDark"
|
||||
import { smoothy } from "../themes/smoothy"
|
||||
import { darkCodeTheme, lightCodeTheme } from "../themes/codeTheme"
|
||||
|
||||
/**
|
||||
* 后台教程正文的 Markdown 编辑器:左边写,右边由教程编辑页用学生端同一套组件预览。
|
||||
@@ -145,7 +144,7 @@ const extensions = computed(() => [
|
||||
EditorView.lineWrapping,
|
||||
imageEvents,
|
||||
shortcuts,
|
||||
isDark.value ? oneDark : smoothy,
|
||||
isDark.value ? darkCodeTheme : lightCodeTheme,
|
||||
])
|
||||
|
||||
const tools = computed(() => [
|
||||
|
||||
@@ -5,8 +5,7 @@ import { autocompletion, closeBrackets, completeAnyWord } from "@codemirror/auto
|
||||
import type { EditorView } from "@codemirror/view"
|
||||
import type { Extension } from "@codemirror/state"
|
||||
import type { LANGUAGE } from "utils/types"
|
||||
import { oneDark } from "../themes/oneDark"
|
||||
import { smoothy } from "../themes/smoothy"
|
||||
import { darkCodeTheme, lightCodeTheme } from "../themes/codeTheme"
|
||||
import { styleTheme } from "shared/extensions/baseTheme"
|
||||
import { enhanceCompletion } from "shared/extensions/autocompletion"
|
||||
import { languageExtension } from "shared/extensions/language"
|
||||
@@ -60,7 +59,7 @@ const extensions = computed(() => [
|
||||
langExtension.value,
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
isDark.value ? oneDark : smoothy,
|
||||
isDark.value ? darkCodeTheme : lightCodeTheme,
|
||||
autocompletion({
|
||||
override: [enhanceCompletion(language), completeAnyWord],
|
||||
}),
|
||||
|
||||
@@ -1,50 +1,74 @@
|
||||
/* 代码高亮配色(明 / 暗):练一练的题型组件和 Markdown.vue 的代码块共用,见 shared/highlight.ts */
|
||||
.hljs-keyword,
|
||||
.hljs-operator,
|
||||
.hljs-selector-tag {
|
||||
color: #d73a49;
|
||||
}
|
||||
.hljs-string,
|
||||
.hljs-regexp,
|
||||
.hljs-template-literal {
|
||||
color: #032f62;
|
||||
}
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #6a737d;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-number,
|
||||
.hljs-literal {
|
||||
color: #005cc5;
|
||||
}
|
||||
.hljs-built_in,
|
||||
.hljs-title.function_,
|
||||
.hljs-class .hljs-title {
|
||||
color: #6f42c1;
|
||||
/* 全站代码配色:亮色 Alucard、暗色 Dracula(https://draculatheme.com/spec,Alucard 是它的官方浅色版)。
|
||||
编辑器(CodeMirror)、正文代码块、练一练、n-code 都用 shared/highlight.ts 的 codeHighlighter
|
||||
给词打 tok-* 类名,颜色只在这里写一份。App.vue 全局引入。 */
|
||||
:root {
|
||||
--code-fg: #1f1f1f;
|
||||
--code-comment: #6c664b;
|
||||
--code-cyan: #036a96;
|
||||
--code-green: #14710a;
|
||||
--code-orange: #a34d14;
|
||||
--code-pink: #a3144d;
|
||||
--code-purple: #644ac9;
|
||||
--code-red: #cb3a2a;
|
||||
--code-yellow: #846e15;
|
||||
}
|
||||
|
||||
.dark .hljs-keyword,
|
||||
.dark .hljs-operator,
|
||||
.dark .hljs-selector-tag {
|
||||
color: #c678dd;
|
||||
html.dark {
|
||||
--code-fg: #f8f8f2;
|
||||
--code-comment: #6272a4;
|
||||
--code-cyan: #8be9fd;
|
||||
--code-green: #50fa7b;
|
||||
--code-orange: #ffb86c;
|
||||
--code-pink: #ff79c6;
|
||||
--code-purple: #bd93f9;
|
||||
--code-red: #ff5555;
|
||||
--code-yellow: #f1fa8c;
|
||||
}
|
||||
.dark .hljs-string,
|
||||
.dark .hljs-regexp,
|
||||
.dark .hljs-template-literal {
|
||||
color: #98c379;
|
||||
|
||||
.tok-keyword {
|
||||
color: var(--code-pink);
|
||||
}
|
||||
.dark .hljs-comment,
|
||||
.dark .hljs-quote {
|
||||
color: #7f848e;
|
||||
.tok-function {
|
||||
color: var(--code-green);
|
||||
}
|
||||
.tok-string {
|
||||
color: var(--code-yellow);
|
||||
}
|
||||
.tok-number {
|
||||
color: var(--code-purple);
|
||||
}
|
||||
.tok-type {
|
||||
color: var(--code-cyan);
|
||||
}
|
||||
.tok-comment {
|
||||
color: var(--code-comment);
|
||||
}
|
||||
.tok-invalid {
|
||||
color: var(--code-red);
|
||||
}
|
||||
|
||||
/* 以下只有 Markdown 编辑器用得到 */
|
||||
.tok-heading {
|
||||
color: var(--code-purple);
|
||||
font-weight: bold;
|
||||
}
|
||||
.tok-strong {
|
||||
color: var(--code-orange);
|
||||
font-weight: bold;
|
||||
}
|
||||
.tok-emphasis {
|
||||
color: var(--code-yellow);
|
||||
font-style: italic;
|
||||
}
|
||||
.dark .hljs-number,
|
||||
.dark .hljs-literal {
|
||||
color: #e5c07b;
|
||||
.tok-link {
|
||||
color: var(--code-cyan);
|
||||
}
|
||||
.dark .hljs-built_in,
|
||||
.dark .hljs-title.function_,
|
||||
.dark .hljs-class .hljs-title {
|
||||
color: #61afef;
|
||||
.tok-monospace {
|
||||
color: var(--code-green);
|
||||
}
|
||||
.tok-quote {
|
||||
color: var(--code-yellow);
|
||||
}
|
||||
.tok-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
@@ -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"]>
|
||||
@@ -0,0 +1,107 @@
|
||||
import { syntaxHighlighting } from "@codemirror/language"
|
||||
import type { Extension } from "@codemirror/state"
|
||||
import { EditorView } from "@codemirror/view"
|
||||
import { codeHighlighter } from "../highlight"
|
||||
|
||||
/**
|
||||
* 编辑器的外观:亮 / 暗两套只管底色、光标、选区、行号这些「框」,
|
||||
* 代码的颜色两套都是 codeHighlighter 打的 tok-* 类名,跟着 html.dark 在 highlight.css 里切。
|
||||
* 框沿用原来的 smoothy(亮)和 One Dark(暗),只把代码配色换成了 Alucard / Dracula。
|
||||
*/
|
||||
const highlighting = syntaxHighlighting(codeHighlighter)
|
||||
|
||||
const light = EditorView.theme({
|
||||
"&": {
|
||||
backgroundColor: "#ffffff",
|
||||
color: "var(--code-fg)",
|
||||
},
|
||||
".cm-content": { caretColor: "#000000" },
|
||||
".cm-cursor, .cm-dropCursor": { borderLeftColor: "#000000" },
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||
backgroundColor: "#FFFD0054",
|
||||
},
|
||||
".cm-activeLine": { backgroundColor: "#00000008" },
|
||||
".cm-gutters": {
|
||||
backgroundColor: "#ffffff",
|
||||
borderRight: "none",
|
||||
color: "#00000070",
|
||||
},
|
||||
".cm-activeLineGutter": { backgroundColor: "#00000008" },
|
||||
".cm-tooltip": {
|
||||
border: "1px solid #e0e0e0",
|
||||
backgroundColor: "#f8f8f8",
|
||||
},
|
||||
".cm-tooltip-autocomplete": {
|
||||
"& > ul > li[aria-selected]": {
|
||||
backgroundColor: "#e8e8e8",
|
||||
color: "#000000",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const darkBackground = "#26262a",
|
||||
highlightBackground = "#2c313a",
|
||||
background = "#101014", // naive-ui
|
||||
tooltipBackground = "#353a42",
|
||||
stone = "#7d8799",
|
||||
cursor = "#528bff"
|
||||
|
||||
const dark = EditorView.theme(
|
||||
{
|
||||
"&": {
|
||||
color: "var(--code-fg)",
|
||||
backgroundColor: background,
|
||||
},
|
||||
".cm-content": { caretColor: cursor },
|
||||
".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor },
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||
backgroundColor: "#3E4451",
|
||||
},
|
||||
".cm-panels": { backgroundColor: darkBackground, color: "var(--code-fg)" },
|
||||
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
|
||||
".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
|
||||
".cm-searchMatch": {
|
||||
backgroundColor: "#72a1ff59",
|
||||
outline: "1px solid #457dff",
|
||||
},
|
||||
".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: "#6199ff2f" },
|
||||
".cm-activeLine": { backgroundColor: "#6699ff0b" },
|
||||
".cm-selectionMatch": { backgroundColor: "#aafe661a" },
|
||||
"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": {
|
||||
backgroundColor: "#bad0f847",
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: background,
|
||||
color: stone,
|
||||
border: "none",
|
||||
},
|
||||
".cm-activeLineGutter": { backgroundColor: highlightBackground },
|
||||
".cm-foldPlaceholder": {
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
color: "#ddd",
|
||||
},
|
||||
".cm-tooltip": {
|
||||
border: "none",
|
||||
backgroundColor: tooltipBackground,
|
||||
},
|
||||
".cm-tooltip .cm-tooltip-arrow:before": {
|
||||
borderTopColor: "transparent",
|
||||
borderBottomColor: "transparent",
|
||||
},
|
||||
".cm-tooltip .cm-tooltip-arrow:after": {
|
||||
borderTopColor: tooltipBackground,
|
||||
borderBottomColor: tooltipBackground,
|
||||
},
|
||||
".cm-tooltip-autocomplete": {
|
||||
"& > ul > li[aria-selected]": {
|
||||
backgroundColor: highlightBackground,
|
||||
color: "var(--code-fg)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{ dark: true },
|
||||
)
|
||||
|
||||
export const lightCodeTheme: Extension = [light, highlighting]
|
||||
export const darkCodeTheme: Extension = [dark, highlighting]
|
||||
@@ -1,101 +0,0 @@
|
||||
import { HighlightStyle, TagStyle, syntaxHighlighting } from "@codemirror/language"
|
||||
import { Extension } from "@codemirror/state"
|
||||
import { EditorView } from "@codemirror/view"
|
||||
|
||||
interface Options {
|
||||
/**
|
||||
* Theme variant. Determines which styles CodeMirror will apply by default.
|
||||
*/
|
||||
variant: Variant
|
||||
|
||||
/**
|
||||
* Settings to customize the look of the editor, like background, gutter, selection and others.
|
||||
*/
|
||||
settings: Settings
|
||||
|
||||
/**
|
||||
* Syntax highlighting styles.
|
||||
*/
|
||||
styles: TagStyle[]
|
||||
}
|
||||
|
||||
type Variant = "light" | "dark"
|
||||
|
||||
interface Settings {
|
||||
/**
|
||||
* Editor background.
|
||||
*/
|
||||
background: string
|
||||
|
||||
/**
|
||||
* Default text color.
|
||||
*/
|
||||
foreground: string
|
||||
|
||||
/**
|
||||
* Caret color.
|
||||
*/
|
||||
caret: string
|
||||
|
||||
/**
|
||||
* Selection background.
|
||||
*/
|
||||
selection: string
|
||||
|
||||
/**
|
||||
* Background of highlighted lines.
|
||||
*/
|
||||
lineHighlight: string
|
||||
|
||||
/**
|
||||
* Gutter background.
|
||||
*/
|
||||
gutterBackground: string
|
||||
|
||||
/**
|
||||
* Text color inside gutter.
|
||||
*/
|
||||
gutterForeground: string
|
||||
|
||||
gutterBorderRight: string
|
||||
}
|
||||
|
||||
export const createTheme = ({ variant, settings, styles }: Options): Extension => {
|
||||
const theme = EditorView.theme(
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"&": {
|
||||
backgroundColor: settings.background,
|
||||
color: settings.foreground,
|
||||
},
|
||||
".cm-content": {
|
||||
caretColor: settings.caret,
|
||||
},
|
||||
".cm-cursor, .cm-dropCursor": {
|
||||
borderLeftColor: settings.caret,
|
||||
},
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||
backgroundColor: settings.selection,
|
||||
},
|
||||
".cm-activeLine": {
|
||||
backgroundColor: settings.lineHighlight,
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: settings.gutterBackground,
|
||||
borderRight: settings.gutterBorderRight,
|
||||
color: settings.gutterForeground,
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: settings.lineHighlight,
|
||||
},
|
||||
},
|
||||
{
|
||||
dark: variant === "dark",
|
||||
},
|
||||
)
|
||||
|
||||
const highlightStyle = HighlightStyle.define(styles)
|
||||
const extension = [theme, syntaxHighlighting(highlightStyle)]
|
||||
|
||||
return extension
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"
|
||||
import { Extension } from "@codemirror/state"
|
||||
import { EditorView } from "@codemirror/view"
|
||||
import { tags as t } from "@lezer/highlight"
|
||||
|
||||
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
|
||||
|
||||
const chalky = "#e5c07b",
|
||||
coral = "#e06c75",
|
||||
cyan = "#56b6c2",
|
||||
invalid = "#ffffff",
|
||||
ivory = "#abb2bf",
|
||||
stone = "#7d8799", // Brightened compared to original to increase contrast
|
||||
malibu = "#61afef",
|
||||
sage = "#98c379",
|
||||
whiskey = "#d19a66",
|
||||
violet = "#c678dd",
|
||||
darkBackground = "#26262a",
|
||||
highlightBackground = "#2c313a",
|
||||
background = "#101014", // naive-ui
|
||||
tooltipBackground = "#353a42",
|
||||
selection = "#3E4451",
|
||||
cursor = "#528bff"
|
||||
|
||||
/// The editor theme styles for One Dark.
|
||||
const oneDarkTheme = EditorView.theme(
|
||||
{
|
||||
"&": {
|
||||
color: ivory,
|
||||
backgroundColor: background,
|
||||
},
|
||||
|
||||
".cm-content": {
|
||||
caretColor: cursor,
|
||||
},
|
||||
|
||||
".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor },
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||
backgroundColor: selection,
|
||||
},
|
||||
|
||||
".cm-panels": { backgroundColor: darkBackground, color: ivory },
|
||||
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
|
||||
".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
|
||||
|
||||
".cm-searchMatch": {
|
||||
backgroundColor: "#72a1ff59",
|
||||
outline: "1px solid #457dff",
|
||||
},
|
||||
".cm-searchMatch.cm-searchMatch-selected": {
|
||||
backgroundColor: "#6199ff2f",
|
||||
},
|
||||
|
||||
".cm-activeLine": { backgroundColor: "#6699ff0b" },
|
||||
".cm-selectionMatch": { backgroundColor: "#aafe661a" },
|
||||
|
||||
"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": {
|
||||
backgroundColor: "#bad0f847",
|
||||
},
|
||||
|
||||
".cm-gutters": {
|
||||
backgroundColor: background,
|
||||
color: stone,
|
||||
border: "none",
|
||||
},
|
||||
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: highlightBackground,
|
||||
},
|
||||
|
||||
".cm-foldPlaceholder": {
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
color: "#ddd",
|
||||
},
|
||||
|
||||
".cm-tooltip": {
|
||||
border: "none",
|
||||
backgroundColor: tooltipBackground,
|
||||
},
|
||||
".cm-tooltip .cm-tooltip-arrow:before": {
|
||||
borderTopColor: "transparent",
|
||||
borderBottomColor: "transparent",
|
||||
},
|
||||
".cm-tooltip .cm-tooltip-arrow:after": {
|
||||
borderTopColor: tooltipBackground,
|
||||
borderBottomColor: tooltipBackground,
|
||||
},
|
||||
".cm-tooltip-autocomplete": {
|
||||
"& > ul > li[aria-selected]": {
|
||||
backgroundColor: highlightBackground,
|
||||
color: ivory,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ dark: true },
|
||||
)
|
||||
|
||||
/// The highlighting style for code in the One Dark theme.
|
||||
const oneDarkHighlightStyle = HighlightStyle.define([
|
||||
{ tag: t.keyword, color: violet },
|
||||
{
|
||||
tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
|
||||
color: coral,
|
||||
},
|
||||
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
|
||||
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
|
||||
{ tag: [t.definition(t.name), t.separator], color: ivory },
|
||||
{
|
||||
tag: [
|
||||
t.typeName,
|
||||
t.className,
|
||||
t.number,
|
||||
t.changed,
|
||||
t.annotation,
|
||||
t.modifier,
|
||||
t.self,
|
||||
t.namespace,
|
||||
],
|
||||
color: chalky,
|
||||
},
|
||||
{
|
||||
tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)],
|
||||
color: cyan,
|
||||
},
|
||||
{ tag: [t.meta, t.comment], color: stone },
|
||||
{ tag: t.strong, fontWeight: "bold" },
|
||||
{ tag: t.emphasis, fontStyle: "italic" },
|
||||
{ tag: t.strikethrough, textDecoration: "line-through" },
|
||||
{ tag: t.link, color: stone, textDecoration: "underline" },
|
||||
{ tag: t.heading, fontWeight: "bold", color: coral },
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
|
||||
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
|
||||
{ tag: t.invalid, color: invalid },
|
||||
])
|
||||
|
||||
/// Extension to enable the One Dark theme (both the editor theme and
|
||||
/// the highlight style).
|
||||
export const oneDark: Extension = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)]
|
||||
@@ -1,100 +0,0 @@
|
||||
import { tags as t } from "@lezer/highlight"
|
||||
import { EditorView } from "@codemirror/view"
|
||||
import type { Extension } from "@codemirror/state"
|
||||
import { createTheme } from "./createTheme"
|
||||
|
||||
const tooltipTheme = EditorView.theme({
|
||||
".cm-tooltip": {
|
||||
border: "1px solid #e0e0e0",
|
||||
backgroundColor: "#f8f8f8",
|
||||
},
|
||||
".cm-tooltip-autocomplete": {
|
||||
"& > ul > li[aria-selected]": {
|
||||
backgroundColor: "#e8e8e8",
|
||||
color: "#000000",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Author: Kenneth Reitz
|
||||
const smoothyBase = createTheme({
|
||||
variant: "light",
|
||||
settings: {
|
||||
background: "#FFFFFF",
|
||||
foreground: "#000000",
|
||||
caret: "#000000",
|
||||
selection: "#FFFD0054",
|
||||
gutterBackground: "#FFFFFF",
|
||||
gutterForeground: "#00000070",
|
||||
gutterBorderRight: "none",
|
||||
lineHighlight: "#00000008",
|
||||
},
|
||||
styles: [
|
||||
{
|
||||
tag: t.comment,
|
||||
color: "#CFCFCF",
|
||||
},
|
||||
{
|
||||
tag: [t.number, t.bool, t.null],
|
||||
color: "#E66C29",
|
||||
},
|
||||
{
|
||||
tag: [
|
||||
t.className,
|
||||
t.definition(t.propertyName),
|
||||
t.function(t.variableName),
|
||||
t.labelName,
|
||||
t.definition(t.typeName),
|
||||
],
|
||||
color: "#2EB43B",
|
||||
},
|
||||
{
|
||||
tag: t.keyword,
|
||||
color: "#D8B229",
|
||||
},
|
||||
{
|
||||
tag: t.operator,
|
||||
color: "#4EA44E",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
{
|
||||
tag: [t.definitionKeyword, t.modifier],
|
||||
color: "#925A47",
|
||||
},
|
||||
{
|
||||
tag: t.string,
|
||||
color: "#704D3D",
|
||||
},
|
||||
{
|
||||
tag: t.typeName,
|
||||
color: "#2F8996",
|
||||
},
|
||||
{
|
||||
tag: [t.variableName, t.propertyName],
|
||||
color: "#77ACB0",
|
||||
},
|
||||
{
|
||||
tag: t.self,
|
||||
color: "#77ACB0",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
{
|
||||
tag: t.regexp,
|
||||
color: "#E3965E",
|
||||
},
|
||||
{
|
||||
tag: [t.tagName, t.angleBracket],
|
||||
color: "#BAA827",
|
||||
},
|
||||
{
|
||||
tag: t.attributeName,
|
||||
color: "#B06520",
|
||||
},
|
||||
{
|
||||
tag: t.derefOperator,
|
||||
color: "#000",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export const smoothy: Extension = [smoothyBase, tooltipTheme]
|
||||
Reference in new issue
Block a user