代码高亮

This commit is contained in:
2025-02-25 11:02:04 +08:00
parent 942c1fb59d
commit 9a289d8c39
10 changed files with 56 additions and 353 deletions

25
package-lock.json generated
View File

@@ -11,17 +11,19 @@
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9", "@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.3", "@codemirror/lang-javascript": "^6.2.3",
"@fsegurai/codemirror-theme-github-light": "^6.1.2",
"@vueuse/core": "^12.7.0", "@vueuse/core": "^12.7.0",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"github-markdown-css": "^5.8.1", "github-markdown-css": "^5.8.1",
"highlight.js": "^11.11.1",
"marked": "^15.0.7", "marked": "^15.0.7",
"marked-highlight": "^2.2.1",
"naive-ui": "^2.41.0", "naive-ui": "^2.41.0",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-codemirror": "^6.1.1" "vue-codemirror": "^6.1.1"
}, },
"devDependencies": { "devDependencies": {
"@codemirror/state": "^6.5.2",
"@iconify/vue": "^4.3.0", "@iconify/vue": "^4.3.0",
"@rsbuild/core": "^1.2.11", "@rsbuild/core": "^1.2.11",
"@rsbuild/plugin-vue": "^1.0.6", "@rsbuild/plugin-vue": "^1.0.6",
@@ -226,6 +228,18 @@
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@fsegurai/codemirror-theme-github-light": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@fsegurai/codemirror-theme-github-light/-/codemirror-theme-github-light-6.1.2.tgz",
"integrity": "sha512-B/YvJ4mx1EuY5WvN+RrFd3uHoFhXiTZRDmSVEyctb/pQonKpgkblVN+FvLt9iRfVTJkkyBavjXJzT16ClaZcDg==",
"license": "MIT",
"peerDependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@iconify/types": { "node_modules/@iconify/types": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
@@ -1846,6 +1860,15 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/marked-highlight": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/marked-highlight/-/marked-highlight-2.2.1.tgz",
"integrity": "sha512-SiCIeEiQbs9TxGwle9/OwbOejHCZsohQRaNTY2u8euEXYt2rYUFoiImUirThU3Gd/o6Q1gHGtH9qloHlbJpNIA==",
"license": "MIT",
"peerDependencies": {
"marked": ">=4 <16"
}
},
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",

View File

@@ -12,17 +12,19 @@
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9", "@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.3", "@codemirror/lang-javascript": "^6.2.3",
"@fsegurai/codemirror-theme-github-light": "^6.1.2",
"@vueuse/core": "^12.7.0", "@vueuse/core": "^12.7.0",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"github-markdown-css": "^5.8.1", "github-markdown-css": "^5.8.1",
"highlight.js": "^11.11.1",
"marked": "^15.0.7", "marked": "^15.0.7",
"marked-highlight": "^2.2.1",
"naive-ui": "^2.41.0", "naive-ui": "^2.41.0",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-codemirror": "^6.1.1" "vue-codemirror": "^6.1.1"
}, },
"devDependencies": { "devDependencies": {
"@codemirror/state": "^6.5.2",
"@iconify/vue": "^4.3.0", "@iconify/vue": "^4.3.0",
"@rsbuild/core": "^1.2.11", "@rsbuild/core": "^1.2.11",
"@rsbuild/plugin-vue": "^1.0.6", "@rsbuild/plugin-vue": "^1.0.6",

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { EditorView } from "@codemirror/view" import { EditorView } from "@codemirror/view"
import { Codemirror } from "vue-codemirror" import { Codemirror } from "vue-codemirror"
import { smoothy } from "../themes/smoothy.ts" import { githubLight } from "@fsegurai/codemirror-theme-github-light"
import { css } from "@codemirror/lang-css" import { css } from "@codemirror/lang-css"
import { javascript } from "@codemirror/lang-javascript" import { javascript } from "@codemirror/lang-javascript"
import { html } from "@codemirror/lang-html" import { html } from "@codemirror/lang-html"
@@ -45,7 +45,7 @@ const lang = computed(() => {
<Codemirror <Codemirror
v-model="code" v-model="code"
indentWithTab indentWithTab
:extensions="[styleTheme, lang, smoothy]" :extensions="[styleTheme, lang, githubLight]"
:tabSize="4" :tabSize="4"
:style="{ height: '100%', fontSize: props.fontSize + 'px' }" :style="{ height: '100%', fontSize: props.fontSize + 'px' }"
/> />

View File

@@ -2,7 +2,6 @@
<n-tabs <n-tabs
style="height: 100%" style="height: 100%"
pane-class="pane" pane-class="pane"
size="large"
:default-value="currentTab" :default-value="currentTab"
type="segment" type="segment"
@update:value="changeTab" @update:value="changeTab"
@@ -41,7 +40,7 @@
<span>选项</span> <span>选项</span>
</n-flex> </n-flex>
</template> </template>
<n-flex size="large" vertical class="wrapper"> <n-flex vertical class="wrapper">
<n-flex align="center"> <n-flex align="center">
<span class="label">重置</span> <span class="label">重置</span>
<n-button @click="reset('html')">HTML</n-button> <n-button @click="reset('html')">HTML</n-button>

View File

@@ -46,7 +46,7 @@ onMounted(preview)
</script> </script>
<style scoped> <style scoped>
.title { .title {
height: 46px; height: 40px;
background-color: rgb(247, 247, 250); background-color: rgb(247, 247, 250);
padding: 0 20px; padding: 0 20px;
} }

View File

@@ -12,22 +12,27 @@
</n-button> </n-button>
</n-flex> </n-flex>
</n-flex> </n-flex>
<div class="markdown-body" v-html="marked(content)"></div> <div class="markdown-body" v-html="markdownContent"></div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { step, prev, next, content } from "../store" import { step, prev, next, content } from "../store"
import { onMounted, ref, watch } from "vue" import { onMounted, ref, shallowRef, watch } from "vue"
import { marked } from "marked" import { marked } from "marked"
import { markedHighlight } from "marked-highlight"
import hljs from "highlight.js"
const end = ref(false) const end = ref(false)
const markdownContent = shallowRef("")
async function getContent() { async function getContent() {
const res = await fetch(`/turtorial/${step.value}/README.md`) const res = await fetch(`/turtorial/${step.value}/README.md`)
const data = await res.text() const data = await res.text()
if (!!data) { if (!!data) {
content.value = data content.value = data
const html = await marked.parse(content.value, { async: true })
markdownContent.value = html
end.value = false end.value = false
} else { } else {
end.value = true end.value = true
@@ -35,7 +40,22 @@ async function getContent() {
} }
} }
onMounted(getContent) onMounted(() => {
getContent()
marked.use({
gfm: true,
async: true,
})
marked.use(
markedHighlight({
langPrefix: "hljs language-",
highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : "plaintext"
return hljs.highlight(code, { language }).value
},
}),
)
})
watch(step, getContent) watch(step, getContent)
</script> </script>
<style scoped> <style scoped>
@@ -45,7 +65,7 @@ watch(step, getContent)
height: 100%; height: 100%;
} }
.title { .title {
height: 46px; height: 40px;
background-color: rgb(247, 247, 250); background-color: rgb(247, 247, 250);
padding: 0 20px; padding: 0 20px;
flex-shrink: 0; flex-shrink: 0;

View File

@@ -2,6 +2,7 @@ import { createApp } from "vue"
import { create } from "naive-ui" import { create } from "naive-ui"
import "normalize.css" import "normalize.css"
import "github-markdown-css/github-markdown-light.css" import "github-markdown-css/github-markdown-light.css"
import "highlight.js/styles/github.min.css"
import App from "./App.vue" import App from "./App.vue"
const app = createApp(App) const app = createApp(App)

View File

@@ -1,110 +0,0 @@
import {
HighlightStyle,
type TagStyle,
syntaxHighlighting,
} from "@codemirror/language"
import { type 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-selectionBackgroundm .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
}

View File

@@ -1,149 +0,0 @@
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"
import { type 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),
]

View File

@@ -1,83 +0,0 @@
import { tags as t } from "@lezer/highlight"
import { createTheme } from "./createTheme"
// Author: Kenneth Reitz
export const smoothy = 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",
},
],
})