改进 Markdown 渲染部分
This commit is contained in:
parent
66290ea1d0
commit
5e5f7cd76e
@ -11,8 +11,8 @@
|
||||
"gen": "openapi-generator-cli generate -i ./api/swagger.yaml -g typescript-axios -o ./src/api"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iktakahiro/markdown-it-katex": "^4.0.1",
|
||||
"@notable/html2markdown": "^2.0.3",
|
||||
"@traptitech/markdown-it-katex": "^3.6.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.7.7",
|
||||
"highlight.js": "^11.10.0",
|
||||
@ -48,6 +48,7 @@
|
||||
"naive-ui": "^2.39.0",
|
||||
"postcss": "^8.4.45",
|
||||
"prettier": "^3.3.3",
|
||||
"sass-embedded": "^1.79.5",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.5.2",
|
||||
"unplugin-auto-import": "^0.18.2",
|
||||
|
1
src/components.d.ts
vendored
1
src/components.d.ts
vendored
@ -17,6 +17,7 @@ declare module 'vue' {
|
||||
LeftSettings: typeof import('./components/settings/LeftSettings.vue')['default']
|
||||
LibrarySettings: typeof import('./components/settings/LibrarySettings.vue')['default']
|
||||
Lottie: typeof import('./components/Lottie.vue')['default']
|
||||
Markdown: typeof import('./components/markdown/index.vue')['default']
|
||||
Mask: typeof import('./components/Mask.vue')['default']
|
||||
MemorySettings: typeof import('./components/settings/MemorySettings.vue')['default']
|
||||
Menu: typeof import('./components/Menu.vue')['default']
|
||||
|
@ -35,7 +35,14 @@
|
||||
</div>
|
||||
</n-flex>
|
||||
</div>
|
||||
<div v-else-if="(message.role === 'user' || message.role === 'user_hide' || message.role === 'user_later') && message.content">
|
||||
<div
|
||||
v-else-if="
|
||||
(message.role === 'user' ||
|
||||
message.role === 'user_hide' ||
|
||||
message.role === 'user_later') &&
|
||||
message.content
|
||||
"
|
||||
>
|
||||
<!-- 用户消息 -->
|
||||
<n-flex justify="end">
|
||||
<div class="flex items-center flex-nowrap">
|
||||
@ -51,11 +58,14 @@
|
||||
{{ userStore.user.name }}
|
||||
</n-divider>
|
||||
</div>
|
||||
<div
|
||||
<!-- <div
|
||||
v-if="mdInited"
|
||||
class="break-all break-words markdown-body"
|
||||
v-html="mdIt.render(message.content)"
|
||||
></div>
|
||||
></div> -->
|
||||
<div>
|
||||
<Markdown :typing="false" :content="message.content" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative h-full">
|
||||
@ -103,11 +113,10 @@
|
||||
{{ message.assistant?.name }}
|
||||
</n-divider>
|
||||
</div>
|
||||
<div
|
||||
v-if="mdInited"
|
||||
class="break-all break-words markdown-body"
|
||||
v-html="mdIt.render(message.content)"
|
||||
></div>
|
||||
|
||||
<div>
|
||||
<Markdown :typing="false" :content="message.content" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div v-html="mdIt.render(message.content)"></div> -->
|
||||
|
||||
@ -126,50 +135,9 @@ import { Ref } from "vue";
|
||||
import { EntityChatMessage } from "@/api";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import leaflowPng from "@/assets/images/leaflow.png";
|
||||
import markdownKatex from "@traptitech/markdown-it-katex";
|
||||
import markdownIt from "markdown-it";
|
||||
// highlightjs
|
||||
import hljs from "highlight.js";
|
||||
import Markdown from "@/components/markdown/index.vue";
|
||||
import config from "@/config/config";
|
||||
|
||||
const mdIt = markdownIt();
|
||||
const mdInited = ref(true);
|
||||
|
||||
const unsupportedLanguages = ["assembly", "blade", "vue"];
|
||||
mdIt.options.highlight = function (str: string, lang: string) {
|
||||
// TODO: 前面的区域以后再来探索吧
|
||||
lang = "text"
|
||||
if (!lang || unsupportedLanguages.includes(lang)) {
|
||||
// return str;
|
||||
lang = "text"
|
||||
}
|
||||
|
||||
return hljs.highlight(str, { language: lang }).value;
|
||||
};
|
||||
mdIt.use(markdownKatex, {
|
||||
throwOnError: false,
|
||||
errorColor: "#cc0000",
|
||||
output: "html",
|
||||
});
|
||||
|
||||
// async function initMD() {
|
||||
// mdIt.use(
|
||||
// await Shiki({
|
||||
// themes: {
|
||||
// light: "vitesse-light",
|
||||
// dark: "vitesse-dark",
|
||||
// },
|
||||
// })
|
||||
// );
|
||||
|
||||
// mdIt.use(markdownKatex, {
|
||||
// throwOnError: false,
|
||||
// errorColor: "#cc0000",
|
||||
// output: "html",
|
||||
// });
|
||||
|
||||
// mdInited.value = true;
|
||||
// }
|
||||
const userStore = useUserStore();
|
||||
|
||||
const props = defineProps({
|
||||
@ -182,7 +150,4 @@ const props = defineProps({
|
||||
const chat_messages = toRef(props, "chat_messages") as Ref<EntityChatMessage[]>;
|
||||
const fileBaseUrl = config.backend + "/api/v1/files";
|
||||
|
||||
onMounted(() => {
|
||||
// initMD();
|
||||
});
|
||||
</script>
|
||||
|
17
src/components/markdown/aPlugin/index.ts
Normal file
17
src/components/markdown/aPlugin/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type { PluginWithOptions } from 'markdown-it'
|
||||
|
||||
export interface APluginOptions {}
|
||||
|
||||
export const aPlugin: PluginWithOptions<APluginOptions> = (
|
||||
md,
|
||||
options = {}
|
||||
): void => {
|
||||
md.renderer.rules.link_open = (tokens, idx, options, env, slf) => {
|
||||
const aIndex = tokens[idx].attrIndex('href')
|
||||
|
||||
if (aIndex !== -1) {
|
||||
tokens[idx].attrs.push(['target', '_blank'])
|
||||
}
|
||||
return slf.renderToken(tokens, idx, options)
|
||||
}
|
||||
}
|
52
src/components/markdown/codePlugin/index.ts
Normal file
52
src/components/markdown/codePlugin/index.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import type { PluginWithOptions } from 'markdown-it'
|
||||
|
||||
import { resolveLanguage } from './resolveLanguage.js'
|
||||
import { resolveLineNumbers } from './resolveLineNumbers.js'
|
||||
|
||||
export interface CodePluginOptions {
|
||||
lineNumbers?: boolean | number
|
||||
}
|
||||
|
||||
export const codePlugin: PluginWithOptions<CodePluginOptions> = (
|
||||
md,
|
||||
{ lineNumbers = true } = {}
|
||||
): void => {
|
||||
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
|
||||
const token = tokens[idx]
|
||||
const info = token.info ? md.utils.unescapeAll(token.info).trim() : ''
|
||||
const language = resolveLanguage(info)
|
||||
const languageClass = `${options.langPrefix}${language.name}`
|
||||
|
||||
const code =
|
||||
options.highlight?.(token.content, language.name, '') ||
|
||||
md.utils.escapeHtml(token.content)
|
||||
|
||||
token.attrJoin('class', languageClass)
|
||||
let result = code.startsWith('<pre')
|
||||
? code
|
||||
: `<pre${slf.renderAttrs(token)}><code>${code}</code></pre>`
|
||||
result = `<div data-ext="${language.ext}" v-pre class="code-copy-line">
|
||||
<div class="code-copy-btn">复制代码</div>
|
||||
</div>${result}`
|
||||
const lines = code.split('\n').slice(0, -1)
|
||||
|
||||
const useLineNumbers =
|
||||
resolveLineNumbers(info) ??
|
||||
(typeof lineNumbers === 'number'
|
||||
? lines.length >= lineNumbers
|
||||
: lineNumbers)
|
||||
if (useLineNumbers) {
|
||||
const lineNumbersCode = lines
|
||||
.map(() => `<div class="line-number"></div>`)
|
||||
.join('')
|
||||
|
||||
result = `<div class="line-numbers" aria-hidden="true">${lineNumbersCode}</div>${result}`
|
||||
}
|
||||
|
||||
result = `<div><div class="${languageClass}${
|
||||
useLineNumbers ? ' line-numbers-mode' : ''
|
||||
}"><div class="pre-code-scroll">${result}</div></div></div>`
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
105
src/components/markdown/codePlugin/languages.ts
Normal file
105
src/components/markdown/codePlugin/languages.ts
Normal file
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Language type for syntax highlight
|
||||
*/
|
||||
export interface HighlightLanguage {
|
||||
/**
|
||||
* Name of the language
|
||||
*
|
||||
* The name to be used for the class name,
|
||||
* e.g. `class="language-typescript"`
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* Extension of the language
|
||||
*
|
||||
* The file extension, which will be used for the
|
||||
* class name, e.g. `class="ext-ts"`
|
||||
*/
|
||||
ext: string
|
||||
|
||||
/**
|
||||
* Aliases that point to this language
|
||||
*
|
||||
* Do not conflict with other languages
|
||||
*/
|
||||
aliases: string[]
|
||||
}
|
||||
|
||||
export const languageBash: HighlightLanguage = {
|
||||
name: 'bash',
|
||||
ext: 'sh',
|
||||
aliases: ['bash', 'sh', 'shell', 'zsh']
|
||||
}
|
||||
|
||||
export const languageCsharp: HighlightLanguage = {
|
||||
name: 'csharp',
|
||||
ext: 'cs',
|
||||
aliases: ['cs', 'csharp']
|
||||
}
|
||||
|
||||
export const languageDocker: HighlightLanguage = {
|
||||
name: 'docker',
|
||||
ext: 'docker',
|
||||
aliases: ['docker', 'dockerfile']
|
||||
}
|
||||
|
||||
export const languageFsharp: HighlightLanguage = {
|
||||
name: 'fsharp',
|
||||
ext: 'fs',
|
||||
aliases: ['fs', 'fsharp']
|
||||
}
|
||||
|
||||
export const languageJavascript: HighlightLanguage = {
|
||||
name: 'javascript',
|
||||
ext: 'js',
|
||||
aliases: ['javascript', 'js']
|
||||
}
|
||||
|
||||
export const languageKotlin: HighlightLanguage = {
|
||||
name: 'kotlin',
|
||||
ext: 'kt',
|
||||
aliases: ['kotlin', 'kt']
|
||||
}
|
||||
|
||||
export const languageMarkdown: HighlightLanguage = {
|
||||
name: 'markdown',
|
||||
ext: 'md',
|
||||
aliases: ['markdown', 'md']
|
||||
}
|
||||
|
||||
export const languagePython: HighlightLanguage = {
|
||||
name: 'python',
|
||||
ext: 'py',
|
||||
aliases: ['py', 'python']
|
||||
}
|
||||
|
||||
export const languageRuby: HighlightLanguage = {
|
||||
name: 'ruby',
|
||||
ext: 'rb',
|
||||
aliases: ['rb', 'ruby']
|
||||
}
|
||||
|
||||
export const languageRust: HighlightLanguage = {
|
||||
name: 'rust',
|
||||
ext: 'rs',
|
||||
aliases: ['rs', 'rust']
|
||||
}
|
||||
|
||||
export const languageStylus: HighlightLanguage = {
|
||||
name: 'stylus',
|
||||
ext: 'styl',
|
||||
aliases: ['styl', 'stylus']
|
||||
}
|
||||
|
||||
export const languageTypescript: HighlightLanguage = {
|
||||
name: 'typescript',
|
||||
ext: 'ts',
|
||||
aliases: ['ts', 'typescript']
|
||||
}
|
||||
|
||||
export const languageYaml: HighlightLanguage = {
|
||||
name: 'yaml',
|
||||
ext: 'yml',
|
||||
aliases: ['yaml', 'yml']
|
||||
}
|
46
src/components/markdown/codePlugin/resolveLanguage.ts
Normal file
46
src/components/markdown/codePlugin/resolveLanguage.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import * as languages from './languages.js'
|
||||
import type { HighlightLanguage } from './languages.js'
|
||||
|
||||
type LanguagesMap = Record<string, HighlightLanguage>
|
||||
|
||||
/**
|
||||
* A key-value map to get language info from alias
|
||||
*
|
||||
* - key: alias
|
||||
* - value: language
|
||||
*/
|
||||
let languagesMap: LanguagesMap
|
||||
|
||||
/**
|
||||
* Lazy generate languages map
|
||||
*/
|
||||
const getLanguagesMap = (): LanguagesMap => {
|
||||
if (!languagesMap) {
|
||||
languagesMap = Object.values(languages).reduce((result, item) => {
|
||||
item.aliases.forEach((alias) => {
|
||||
result[alias] = item
|
||||
})
|
||||
return result
|
||||
}, {} as LanguagesMap)
|
||||
}
|
||||
|
||||
return languagesMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve language for highlight from token info
|
||||
*/
|
||||
export const resolveLanguage = (info: string): HighlightLanguage => {
|
||||
// get user-defined language alias
|
||||
const alias = info.match(/^([^ :[{]+)/)?.[1] || ''
|
||||
|
||||
// if the alias does not have a match in the map
|
||||
// fallback to the alias itself
|
||||
return (
|
||||
getLanguagesMap()[alias] ?? {
|
||||
name: alias,
|
||||
ext: alias,
|
||||
aliases: [alias]
|
||||
}
|
||||
)
|
||||
}
|
14
src/components/markdown/codePlugin/resolveLineNumbers.ts
Normal file
14
src/components/markdown/codePlugin/resolveLineNumbers.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Resolve the `:line-numbers` / `:no-line-numbers` mark from token info
|
||||
*/
|
||||
export const resolveLineNumbers = (info: string): boolean | null => {
|
||||
if (/:line-numbers\b/.test(info)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (/:no-line-numbers\b/.test(info)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
76
src/components/markdown/customLink/index.ts
Normal file
76
src/components/markdown/customLink/index.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import type { PluginWithOptions } from 'markdown-it'
|
||||
|
||||
const defaultMarker = '#'
|
||||
interface CustomLinkPluginOptions {
|
||||
marker: string
|
||||
}
|
||||
export const customLinkPlugin: PluginWithOptions<
|
||||
Partial<CustomLinkPluginOptions>
|
||||
> = (md, options = {}): void => {
|
||||
const marker = options.marker || defaultMarker
|
||||
|
||||
md.block.ruler.before(
|
||||
'paragraph',
|
||||
'custom_link',
|
||||
function (state, startLine, endLine, silent) {
|
||||
const pos = state.bMarks[startLine] + state.tShift[startLine]
|
||||
const max = state.eMarks[startLine]
|
||||
|
||||
if (pos >= max) {
|
||||
return false
|
||||
}
|
||||
if (silent) {
|
||||
return true
|
||||
}
|
||||
const text = state.src.substring(pos, max)
|
||||
const start = text.indexOf(marker)
|
||||
const end = text.lastIndexOf(marker)
|
||||
if (start < 0 || end < 0 || start == end) {
|
||||
return false
|
||||
}
|
||||
//#...#规则前面的内容
|
||||
const startContent = text.substring(0, start)
|
||||
//#...#规则后面的内容
|
||||
const endContent = text.substring(end + 1)
|
||||
//#...#规则中间的内容
|
||||
const content = text.substring(start + 1, end)
|
||||
|
||||
//插入<div>
|
||||
const token_div_o = state.push('div_open', 'div', 1)
|
||||
token_div_o.map = [startLine, state.line]
|
||||
|
||||
//插入#...#规则前面的内容
|
||||
const token_s = state.push('inline', '', 0)
|
||||
token_s.content = startContent
|
||||
token_s.children = []
|
||||
|
||||
//插入<a class="markdown-custom-link">
|
||||
const token_a_o = state.push('link_open', 'a', 1)
|
||||
token_a_o.attrs = [['class', 'markdown-custom-link']]
|
||||
token_a_o.map = [startLine, state.line]
|
||||
|
||||
//插入#...#规则中间的内容
|
||||
const token = state.push('inline', '', 0)
|
||||
token.content = content
|
||||
token.children = []
|
||||
|
||||
//闭合a标签
|
||||
state.push('link_close', 'a', -1)
|
||||
const token_e = state.push('inline', '', 0)
|
||||
|
||||
//插入#...#规则后面的内容
|
||||
token_e.content = endContent
|
||||
token_e.children = []
|
||||
|
||||
//闭合div标签
|
||||
state.push('div_close', 'div', -1)
|
||||
|
||||
state.line = startLine + 1
|
||||
|
||||
return true
|
||||
},
|
||||
{
|
||||
alt: ['paragraph', 'reference', 'blockquote']
|
||||
}
|
||||
)
|
||||
}
|
1102
src/components/markdown/github-markdown.css
Normal file
1102
src/components/markdown/github-markdown.css
Normal file
File diff suppressed because it is too large
Load Diff
168
src/components/markdown/index.vue
Normal file
168
src/components/markdown/index.vue
Normal file
@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div class="markdown-it-container">
|
||||
<div
|
||||
ref="markdownBodyRef"
|
||||
class="markdown-body"
|
||||
@click="handleClick($event)"
|
||||
v-html="result"
|
||||
/>
|
||||
<div v-if="typing" class="markdown-typing" :style="cursorPosition"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import hljs from 'highlight.js'
|
||||
// import 'highlight.js/styles/atom-one-dark.css'
|
||||
// import './github-markdown.css'
|
||||
// @ts-ignore
|
||||
import markdownItMath from '@iktakahiro/markdown-it-katex'
|
||||
import type {CodePluginOptions} from './codePlugin'
|
||||
import {codePlugin} from './codePlugin'
|
||||
import {customLinkPlugin} from './customLink'
|
||||
import {aPlugin} from './aPlugin'
|
||||
|
||||
interface Options extends Partial<MarkdownIt.Options> {
|
||||
lineNumbers?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
content: string
|
||||
html?: boolean
|
||||
breaks?: boolean
|
||||
linkify?: boolean
|
||||
typographer?: boolean
|
||||
// 是否显示代码行
|
||||
lineNumbers?: boolean
|
||||
// 是否显示打字效果
|
||||
typing: boolean
|
||||
}>(),
|
||||
{
|
||||
content: '',
|
||||
html: true,
|
||||
breaks: true,
|
||||
typographer: true,
|
||||
linkify: true,
|
||||
lineNumbers: true,
|
||||
typing: false
|
||||
}
|
||||
)
|
||||
const emit = defineEmits<{
|
||||
(event: 'click-custom-link', value: string): void
|
||||
}>()
|
||||
const result = ref('')
|
||||
const markdownBodyRef = shallowRef<HTMLDivElement>()
|
||||
const createMarkdown = (options: Options) => {
|
||||
const md = new MarkdownIt({
|
||||
...options,
|
||||
langPrefix: 'language-',
|
||||
highlight(str: any, lang: any) {
|
||||
try {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
return hljs.highlight(lang, str, true).value
|
||||
}
|
||||
return hljs.highlightAuto(str).value
|
||||
} catch (error) {
|
||||
return str
|
||||
}
|
||||
}
|
||||
})
|
||||
md.use<CodePluginOptions>(codePlugin, {
|
||||
lineNumbers: options.lineNumbers
|
||||
})
|
||||
md.use(markdownItMath, {
|
||||
output: 'mathml'
|
||||
})
|
||||
md.use(aPlugin)
|
||||
md.use(customLinkPlugin)
|
||||
|
||||
return md
|
||||
}
|
||||
let md: MarkdownIt
|
||||
|
||||
const findLastTextNode = (parent: Node): Node | undefined => {
|
||||
const children = parent.childNodes
|
||||
for (let i = children.length - 1; i >= 0; i--) {
|
||||
const node = children[i]
|
||||
if (node.nodeType === Node.TEXT_NODE && /\S/.test(node.nodeValue!)) {
|
||||
node.nodeValue?.replace(/\S+$/, '')
|
||||
return node
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const lastNode = findLastTextNode(node)
|
||||
if (lastNode) {
|
||||
return lastNode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const cursorPosition = reactive({
|
||||
top: 'auto',
|
||||
left: 'auto'
|
||||
})
|
||||
const updateCursor = () => {
|
||||
if (markdownBodyRef.value) {
|
||||
const lastTextNode = findLastTextNode(markdownBodyRef.value)
|
||||
const textNode = document.createTextNode('\u200B')
|
||||
if (lastTextNode) {
|
||||
lastTextNode.parentElement?.appendChild(textNode)
|
||||
} else {
|
||||
markdownBodyRef.value?.appendChild(textNode)
|
||||
}
|
||||
const range = document.createRange()
|
||||
range.setStart(textNode, 0)
|
||||
range.setEnd(textNode, 0)
|
||||
const textRect = range.getBoundingClientRect()
|
||||
const markdownBodyRect = markdownBodyRef.value?.getBoundingClientRect()
|
||||
cursorPosition.left = `${textRect.left - markdownBodyRect.left}px`
|
||||
cursorPosition.top = `${textRect.top - markdownBodyRect.top}px`
|
||||
textNode.remove()
|
||||
}
|
||||
}
|
||||
|
||||
const preprocessContent = (content: string) => {
|
||||
return content.replace(/\n(#.*#)/g, '\n\n$1')
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
md = createMarkdown({
|
||||
html: props.html,
|
||||
breaks: props.breaks,
|
||||
typographer: props.typographer,
|
||||
linkify: props.linkify,
|
||||
lineNumbers: props.lineNumbers
|
||||
})
|
||||
result.value = md.render(preprocessContent(props.content))
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.content,
|
||||
async (value) => {
|
||||
result.value = md?.render(preprocessContent(value))
|
||||
await nextTick()
|
||||
updateCursor()
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
// const {copy} = useCopy()
|
||||
|
||||
const handleClick = async (e: any) => {
|
||||
console.log(e.target)
|
||||
const target: HTMLElement = e.target
|
||||
if (target.className === 'code-copy-btn') {
|
||||
const text = e.target.parentElement.nextElementSibling.textContent
|
||||
// await copy(text)
|
||||
// copy
|
||||
}
|
||||
if (target.className === 'markdown-custom-link') {
|
||||
emit('click-custom-link', target.textContent!)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use 'markdown.scss';
|
||||
</style>
|
212
src/components/markdown/markdown.scss
Normal file
212
src/components/markdown/markdown.scss
Normal file
@ -0,0 +1,212 @@
|
||||
$line-height: 1.375;
|
||||
:root {
|
||||
--code-bg-color: #35373f;
|
||||
--code-hl-bg-color: rgba(0, 0, 0, 0.66);
|
||||
--code-ln-color: #6e6e7f;
|
||||
--code-ln-wrapper-width: 3.5rem;
|
||||
}
|
||||
@keyframes blink {
|
||||
from,
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.markdown-it-container {
|
||||
.markdown-body {
|
||||
background-color: transparent;
|
||||
font-size: 15px;
|
||||
.markdown-custom-link {
|
||||
color: var(--el-color-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
position: relative;
|
||||
.markdown-typing {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 14px;
|
||||
transform: translate(4px, 2px) scaleY(1.3);
|
||||
color: #1a202c;
|
||||
background-color: currentColor;
|
||||
animation: blink 0.6s infinite;
|
||||
}
|
||||
ul {
|
||||
list-style: disc;
|
||||
}
|
||||
ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
code {
|
||||
padding: 0.25rem;
|
||||
margin: 0;
|
||||
font-size: 0.85em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
color: #ccc;
|
||||
background: none;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*='language-'] {
|
||||
padding: 20px 0;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
font-size: 0.85em;
|
||||
background: #35373f;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*='language-'] {
|
||||
border-radius: 0.3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
pre,
|
||||
pre[class*='language-'] {
|
||||
line-height: $line-height;
|
||||
border-radius: 6px;
|
||||
overflow: visible;
|
||||
display: inline-block;
|
||||
padding: 20px;
|
||||
code {
|
||||
color: #fff;
|
||||
padding: 0;
|
||||
background-color: transparent !important;
|
||||
border-radius: 0;
|
||||
overflow-wrap: unset;
|
||||
-webkit-font-smoothing: auto;
|
||||
-moz-osx-font-smoothing: auto;
|
||||
}
|
||||
}
|
||||
|
||||
div[class*='language-'] {
|
||||
position: relative;
|
||||
background-color: var(--code-bg-color);
|
||||
border-radius: 6px;
|
||||
padding-top: 32px;
|
||||
margin: 0.85rem 0;
|
||||
overflow: hidden;
|
||||
.code-copy-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: #595b63;
|
||||
color: #e0e0e0;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
font-size: 12px;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding: 0 12px;
|
||||
|
||||
&::before {
|
||||
content: attr(data-ext);
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 0;
|
||||
font-size: 0.75rem;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.code-copy-btn {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 0;
|
||||
right: 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
pre,
|
||||
pre[class*='language-'] {
|
||||
// force override the background color to be compatible with shiki
|
||||
background: transparent !important;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.pre-code-scroll {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&:not(.line-numbers-mode) {
|
||||
.line-numbers {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.line-numbers-mode {
|
||||
padding-left: var(--code-ln-wrapper-width);
|
||||
pre {
|
||||
vertical-align: middle;
|
||||
padding: 20px 20px 20px 0;
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: var(--code-ln-wrapper-width);
|
||||
text-align: center;
|
||||
color: var(--code-ln-color);
|
||||
padding-top: 52px;
|
||||
line-height: $line-height;
|
||||
counter-reset: line-number;
|
||||
|
||||
.line-number {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
user-select: none;
|
||||
height: #{$line-height - 0.2}em;
|
||||
&::before {
|
||||
display: block;
|
||||
counter-increment: line-number;
|
||||
content: counter(line-number);
|
||||
font-size: 0.8em;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// &::after {
|
||||
// content: "";
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
// width: var(--code-ln-wrapper-width);
|
||||
// height: 100%;
|
||||
// border-radius: 6px 0 0 6px;
|
||||
// border-right: 1px solid var(--code-hl-bg-color);
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user