diff --git a/package.json b/package.json index 27554d8..2335dec 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components.d.ts b/src/components.d.ts index 57e2021..29d7c11 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -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'] diff --git a/src/components/chat/MessageList.vue b/src/components/chat/MessageList.vue index d053715..d547950 100644 --- a/src/components/chat/MessageList.vue +++ b/src/components/chat/MessageList.vue @@ -35,7 +35,14 @@ -
+
@@ -51,11 +58,14 @@ {{ userStore.user.name }}
-
+ >
--> +
+ +
@@ -103,11 +113,10 @@ {{ message.assistant?.name }}
-
+ +
+ +
@@ -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; const fileBaseUrl = config.backend + "/api/v1/files"; -onMounted(() => { - // initMD(); -}); diff --git a/src/components/markdown/aPlugin/index.ts b/src/components/markdown/aPlugin/index.ts new file mode 100644 index 0000000..63a0815 --- /dev/null +++ b/src/components/markdown/aPlugin/index.ts @@ -0,0 +1,17 @@ +import type { PluginWithOptions } from 'markdown-it' + +export interface APluginOptions {} + +export const aPlugin: PluginWithOptions = ( + 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) + } +} diff --git a/src/components/markdown/codePlugin/index.ts b/src/components/markdown/codePlugin/index.ts new file mode 100644 index 0000000..4529723 --- /dev/null +++ b/src/components/markdown/codePlugin/index.ts @@ -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 = ( + 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('${code}` + result = `
+
复制代码
+
${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(() => `
`) + .join('') + + result = `${result}` + } + + result = `
${result}
` + + return result + } +} diff --git a/src/components/markdown/codePlugin/languages.ts b/src/components/markdown/codePlugin/languages.ts new file mode 100644 index 0000000..ba8bc0b --- /dev/null +++ b/src/components/markdown/codePlugin/languages.ts @@ -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'] +} diff --git a/src/components/markdown/codePlugin/resolveLanguage.ts b/src/components/markdown/codePlugin/resolveLanguage.ts new file mode 100644 index 0000000..8d8d4ae --- /dev/null +++ b/src/components/markdown/codePlugin/resolveLanguage.ts @@ -0,0 +1,46 @@ +import * as languages from './languages.js' +import type { HighlightLanguage } from './languages.js' + +type LanguagesMap = Record + +/** + * 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] + } + ) +} diff --git a/src/components/markdown/codePlugin/resolveLineNumbers.ts b/src/components/markdown/codePlugin/resolveLineNumbers.ts new file mode 100644 index 0000000..73cd43a --- /dev/null +++ b/src/components/markdown/codePlugin/resolveLineNumbers.ts @@ -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 +} diff --git a/src/components/markdown/customLink/index.ts b/src/components/markdown/customLink/index.ts new file mode 100644 index 0000000..eaa39d6 --- /dev/null +++ b/src/components/markdown/customLink/index.ts @@ -0,0 +1,76 @@ +import type { PluginWithOptions } from 'markdown-it' + +const defaultMarker = '#' +interface CustomLinkPluginOptions { + marker: string +} +export const customLinkPlugin: PluginWithOptions< + Partial +> = (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) + + //插入
+ 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 = [] + + //插入 + 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'] + } + ) +} diff --git a/src/components/markdown/github-markdown.css b/src/components/markdown/github-markdown.css new file mode 100644 index 0000000..d6f5b60 --- /dev/null +++ b/src/components/markdown/github-markdown.css @@ -0,0 +1,1102 @@ + +:root.dark .markdown-body { + color-scheme: dark; + --color-prettylights-syntax-comment: #8b949e; + --color-prettylights-syntax-constant: #79c0ff; + --color-prettylights-syntax-entity: #d2a8ff; + --color-prettylights-syntax-storage-modifier-import: #c9d1d9; + --color-prettylights-syntax-entity-tag: #7ee787; + --color-prettylights-syntax-keyword: #ff7b72; + --color-prettylights-syntax-string: #a5d6ff; + --color-prettylights-syntax-variable: #ffa657; + --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; + --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; + --color-prettylights-syntax-invalid-illegal-bg: #8e1519; + --color-prettylights-syntax-carriage-return-text: #f0f6fc; + --color-prettylights-syntax-carriage-return-bg: #b62324; + --color-prettylights-syntax-string-regexp: #7ee787; + --color-prettylights-syntax-markup-list: #f2cc60; + --color-prettylights-syntax-markup-heading: #1f6feb; + --color-prettylights-syntax-markup-italic: #c9d1d9; + --color-prettylights-syntax-markup-bold: #c9d1d9; + --color-prettylights-syntax-markup-deleted-text: #ffdcd7; + --color-prettylights-syntax-markup-deleted-bg: #67060c; + --color-prettylights-syntax-markup-inserted-text: #aff5b4; + --color-prettylights-syntax-markup-inserted-bg: #033a16; + --color-prettylights-syntax-markup-changed-text: #ffdfb6; + --color-prettylights-syntax-markup-changed-bg: #5a1e02; + --color-prettylights-syntax-markup-ignored-text: #c9d1d9; + --color-prettylights-syntax-markup-ignored-bg: #1158c7; + --color-prettylights-syntax-meta-diff-range: #d2a8ff; + --color-prettylights-syntax-brackethighlighter-angle: #8b949e; + --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; + --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; + --color-fg-default: #c9d1d9; + --color-fg-muted: #8b949e; + --color-fg-subtle: #6e7681; + --color-canvas-default: #0d1117; + --color-canvas-subtle: #161b22; + --color-border-default: #30363d; + --color-border-muted: #21262d; + --color-neutral-muted: rgba(110,118,129,0.4); + --color-accent-fg: #58a6ff; + --color-accent-emphasis: #1f6feb; + --color-attention-subtle: rgba(187,128,9,0.15); + --color-danger-fg: #f85149; +} + + + +:root .markdown-body { + color-scheme: light; + --color-prettylights-syntax-comment: #6e7781; + --color-prettylights-syntax-constant: #0550ae; + --color-prettylights-syntax-entity: #8250df; + --color-prettylights-syntax-storage-modifier-import: #24292f; + --color-prettylights-syntax-entity-tag: #116329; + --color-prettylights-syntax-keyword: #cf222e; + --color-prettylights-syntax-string: #0a3069; + --color-prettylights-syntax-variable: #953800; + --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; + --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; + --color-prettylights-syntax-invalid-illegal-bg: #82071e; + --color-prettylights-syntax-carriage-return-text: #f6f8fa; + --color-prettylights-syntax-carriage-return-bg: #cf222e; + --color-prettylights-syntax-string-regexp: #116329; + --color-prettylights-syntax-markup-list: #3b2300; + --color-prettylights-syntax-markup-heading: #0550ae; + --color-prettylights-syntax-markup-italic: #24292f; + --color-prettylights-syntax-markup-bold: #24292f; + --color-prettylights-syntax-markup-deleted-text: #82071e; + --color-prettylights-syntax-markup-deleted-bg: #ffebe9; + --color-prettylights-syntax-markup-inserted-text: #116329; + --color-prettylights-syntax-markup-inserted-bg: #dafbe1; + --color-prettylights-syntax-markup-changed-text: #953800; + --color-prettylights-syntax-markup-changed-bg: #ffd8b5; + --color-prettylights-syntax-markup-ignored-text: #eaeef2; + --color-prettylights-syntax-markup-ignored-bg: #0550ae; + --color-prettylights-syntax-meta-diff-range: #8250df; + --color-prettylights-syntax-brackethighlighter-angle: #57606a; + --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; + --color-prettylights-syntax-constant-other-reference-link: #0a3069; + --color-fg-default: #24292f; + --color-fg-muted: #57606a; + --color-fg-subtle: #6e7781; + --color-canvas-default: #ffffff; + --color-canvas-subtle: #f6f8fa; + --color-border-default: #d0d7de; + --color-border-muted: hsla(210,18%,87%,1); + --color-neutral-muted: rgba(175,184,193,0.2); + --color-accent-fg: #0969da; + --color-accent-emphasis: #0969da; + --color-attention-subtle: #fff8c5; + --color-danger-fg: #cf222e; + } + + +.markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + margin: 0; + color: var(--color-fg-default); + background-color: var(--color-canvas-default); + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; +} + +.markdown-body .octicon { + display: inline-block; + fill: currentColor; + vertical-align: text-bottom; +} + +.markdown-body h1:hover .anchor .octicon-link:before, +.markdown-body h2:hover .anchor .octicon-link:before, +.markdown-body h3:hover .anchor .octicon-link:before, +.markdown-body h4:hover .anchor .octicon-link:before, +.markdown-body h5:hover .anchor .octicon-link:before, +.markdown-body h6:hover .anchor .octicon-link:before { + width: 16px; + height: 16px; + content: ' '; + display: inline-block; + background-color: currentColor; + -webkit-mask-image: url("data:image/svg+xml,"); + mask-image: url("data:image/svg+xml,"); +} + +.markdown-body details, +.markdown-body figcaption, +.markdown-body figure { + display: block; +} + +.markdown-body summary { + display: list-item; +} + +.markdown-body [hidden] { + display: none !important; +} + +.markdown-body a { + background-color: transparent; + color: var(--color-accent-fg); + text-decoration: none; +} + +.markdown-body abbr[title] { + border-bottom: none; + text-decoration: underline dotted; +} + +.markdown-body b, +.markdown-body strong { + font-weight: var(--base-text-weight-semibold, 600); +} + +.markdown-body dfn { + font-style: italic; +} + +.markdown-body h1 { + margin: .67em 0; + font-weight: var(--base-text-weight-semibold, 600); + padding-bottom: .3em; + font-size: 2em; + border-bottom: 1px solid var(--color-border-muted); +} + +.markdown-body mark { + background-color: var(--color-attention-subtle); + color: var(--color-fg-default); +} + +.markdown-body small { + font-size: 90%; +} + +.markdown-body sub, +.markdown-body sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +.markdown-body sub { + bottom: -0.25em; +} + +.markdown-body sup { + top: -0.5em; +} + +.markdown-body img { + border-style: none; + max-width: 100%; + box-sizing: content-box; + background-color: var(--color-canvas-default); +} + +.markdown-body code, +.markdown-body kbd, +.markdown-body pre, +.markdown-body samp { + font-family: monospace; + font-size: 1em; +} + +.markdown-body figure { + margin: 1em 40px; +} + +.markdown-body hr { + box-sizing: content-box; + overflow: hidden; + background: transparent; + border-bottom: 1px solid var(--color-border-muted); + height: .25em; + padding: 0; + margin: 24px 0; + background-color: var(--color-border-default); + border: 0; +} + +.markdown-body input { + font: inherit; + margin: 0; + overflow: visible; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.markdown-body [type=button], +.markdown-body [type=reset], +.markdown-body [type=submit] { + -webkit-appearance: button; +} + +.markdown-body [type=checkbox], +.markdown-body [type=radio] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body [type=number]::-webkit-inner-spin-button, +.markdown-body [type=number]::-webkit-outer-spin-button { + height: auto; +} + +.markdown-body [type=search]::-webkit-search-cancel-button, +.markdown-body [type=search]::-webkit-search-decoration { + -webkit-appearance: none; +} + +.markdown-body ::-webkit-input-placeholder { + color: inherit; + opacity: .54; +} + +.markdown-body ::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body ::placeholder { + color: var(--color-fg-subtle); + opacity: 1; +} + +.markdown-body hr::before { + display: table; + content: ""; +} + +.markdown-body hr::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body table { + border-spacing: 0; + border-collapse: collapse; + display: block; + width: max-content; + max-width: 100%; + overflow: auto; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body details summary { + cursor: pointer; +} + +.markdown-body details:not([open])>*:not(summary) { + display: none !important; +} + +.markdown-body a:focus, +.markdown-body [role=button]:focus, +.markdown-body input[type=radio]:focus, +.markdown-body input[type=checkbox]:focus { + outline: 2px solid var(--color-accent-fg); + outline-offset: -2px; + box-shadow: none; +} + +.markdown-body a:focus:not(:focus-visible), +.markdown-body [role=button]:focus:not(:focus-visible), +.markdown-body input[type=radio]:focus:not(:focus-visible), +.markdown-body input[type=checkbox]:focus:not(:focus-visible) { + outline: solid 1px transparent; +} + +.markdown-body a:focus-visible, +.markdown-body [role=button]:focus-visible, +.markdown-body input[type=radio]:focus-visible, +.markdown-body input[type=checkbox]:focus-visible { + outline: 2px solid var(--color-accent-fg); + outline-offset: -2px; + box-shadow: none; +} + +.markdown-body a:not([class]):focus, +.markdown-body a:not([class]):focus-visible, +.markdown-body input[type=radio]:focus, +.markdown-body input[type=radio]:focus-visible, +.markdown-body input[type=checkbox]:focus, +.markdown-body input[type=checkbox]:focus-visible { + outline-offset: 0; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; + line-height: 10px; + color: var(--color-fg-default); + vertical-align: middle; + background-color: var(--color-canvas-subtle); + border: solid 1px var(--color-neutral-muted); + border-bottom-color: var(--color-neutral-muted); + border-radius: 6px; + box-shadow: inset 0 -1px 0 var(--color-neutral-muted); +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: var(--base-text-weight-semibold, 600); + line-height: 1.25; +} + +.markdown-body h2 { + font-weight: var(--base-text-weight-semibold, 600); + padding-bottom: .3em; + font-size: 1.5em; + border-bottom: 1px solid var(--color-border-muted); +} + +.markdown-body h3 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: 1.25em; +} + +.markdown-body h4 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: 1em; +} + +.markdown-body h5 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: .875em; +} + +.markdown-body h6 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: .85em; + color: var(--color-fg-muted); +} + +.markdown-body p { + margin-top: 0; + margin-bottom: 10px; +} + +.markdown-body blockquote { + margin: 0; + padding: 0 1em; + color: var(--color-fg-muted); + border-left: .25em solid var(--color-border-default); +} + +.markdown-body ul, +.markdown-body ol { + margin-top: 0; + margin-bottom: 0; + padding-left: 2em; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ul ul ol, +.markdown-body ul ol ol, +.markdown-body ol ul ol, +.markdown-body ol ol ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body tt, +.markdown-body code, +.markdown-body samp { + font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; + font-size: 12px; +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; + font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; + font-size: 12px; + word-wrap: normal; +} + +.markdown-body .octicon { + display: inline-block; + overflow: visible !important; + vertical-align: text-bottom; + fill: currentColor; +} + +.markdown-body input::-webkit-outer-spin-button, +.markdown-body input::-webkit-inner-spin-button { + margin: 0; + -webkit-appearance: none; + appearance: none; +} + +.markdown-body::before { + display: table; + content: ""; +} + +.markdown-body::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body>*:first-child { + margin-top: 0 !important; +} + +.markdown-body>*:last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body .absent { + color: var(--color-danger-fg); +} + +.markdown-body .anchor { + float: left; + padding-right: 4px; + margin-left: -20px; + line-height: 1; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre, +.markdown-body details { + margin-top: 0; + margin-bottom: 16px; +} + +.markdown-body blockquote>:first-child { + margin-top: 0; +} + +.markdown-body blockquote>:last-child { + margin-bottom: 0; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: var(--color-fg-default); + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1 tt, +.markdown-body h1 code, +.markdown-body h2 tt, +.markdown-body h2 code, +.markdown-body h3 tt, +.markdown-body h3 code, +.markdown-body h4 tt, +.markdown-body h4 code, +.markdown-body h5 tt, +.markdown-body h5 code, +.markdown-body h6 tt, +.markdown-body h6 code { + padding: 0 .2em; + font-size: inherit; +} + +.markdown-body summary h1, +.markdown-body summary h2, +.markdown-body summary h3, +.markdown-body summary h4, +.markdown-body summary h5, +.markdown-body summary h6 { + display: inline-block; +} + +.markdown-body summary h1 .anchor, +.markdown-body summary h2 .anchor, +.markdown-body summary h3 .anchor, +.markdown-body summary h4 .anchor, +.markdown-body summary h5 .anchor, +.markdown-body summary h6 .anchor { + margin-left: -40px; +} + +.markdown-body summary h1, +.markdown-body summary h2 { + padding-bottom: 0; + border-bottom: 0; +} + +.markdown-body ul.no-list, +.markdown-body ol.no-list { + padding: 0; + list-style-type: none; +} + +.markdown-body ol[type=a] { + list-style-type: lower-alpha; +} + +.markdown-body ol[type=A] { + list-style-type: upper-alpha; +} + +.markdown-body ol[type=i] { + list-style-type: lower-roman; +} + +.markdown-body ol[type=I] { + list-style-type: upper-roman; +} + +.markdown-body ol[type="1"] { + list-style-type: decimal; +} + +.markdown-body div>ol:not([type]) { + list-style-type: decimal; +} + +.markdown-body ul ul, +.markdown-body ul ol, +.markdown-body ol ol, +.markdown-body ol ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li>p { + margin-top: 16px; +} + +.markdown-body li+li { + margin-top: .25em; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 16px; + font-size: 1em; + font-style: italic; + font-weight: var(--base-text-weight-semibold, 600); +} + +.markdown-body dl dd { + padding: 0 16px; + margin-bottom: 16px; +} + +.markdown-body table th { + font-weight: var(--base-text-weight-semibold, 600); +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid var(--color-border-default); +} + +.markdown-body table tr { + background-color: var(--color-canvas-default); + border-top: 1px solid var(--color-border-muted); +} + +.markdown-body table tr:nth-child(2n) { + background-color: var(--color-canvas-subtle); +} + +.markdown-body table img { + background-color: transparent; +} + +.markdown-body img[align=right] { + padding-left: 20px; +} + +.markdown-body img[align=left] { + padding-right: 20px; +} + +.markdown-body .emoji { + max-width: none; + vertical-align: text-top; + background-color: transparent; +} + +.markdown-body span.frame { + display: block; + overflow: hidden; +} + +.markdown-body span.frame>span { + display: block; + float: left; + width: auto; + padding: 7px; + margin: 13px 0 0; + overflow: hidden; + border: 1px solid var(--color-border-default); +} + +.markdown-body span.frame span img { + display: block; + float: left; +} + +.markdown-body span.frame span span { + display: block; + padding: 5px 0 0; + clear: both; + color: var(--color-fg-default); +} + +.markdown-body span.align-center { + display: block; + overflow: hidden; + clear: both; +} + +.markdown-body span.align-center>span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: center; +} + +.markdown-body span.align-center span img { + margin: 0 auto; + text-align: center; +} + +.markdown-body span.align-right { + display: block; + overflow: hidden; + clear: both; +} + +.markdown-body span.align-right>span { + display: block; + margin: 13px 0 0; + overflow: hidden; + text-align: right; +} + +.markdown-body span.align-right span img { + margin: 0; + text-align: right; +} + +.markdown-body span.float-left { + display: block; + float: left; + margin-right: 13px; + overflow: hidden; +} + +.markdown-body span.float-left span { + margin: 13px 0 0; +} + +.markdown-body span.float-right { + display: block; + float: right; + margin-left: 13px; + overflow: hidden; +} + +.markdown-body span.float-right>span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: right; +} + +.markdown-body code, +.markdown-body tt { + padding: .2em .4em; + margin: 0; + font-size: 85%; + white-space: break-spaces; + background-color: var(--color-neutral-muted); + border-radius: 6px; +} + +.markdown-body code br, +.markdown-body tt br { + display: none; +} + +.markdown-body del code { + text-decoration: inherit; +} + +.markdown-body samp { + font-size: 85%; +} + +.markdown-body pre code { + font-size: 100%; +} + +.markdown-body pre>code { + padding: 0; + margin: 0; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 16px; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: var(--color-canvas-subtle); + border-radius: 6px; +} + +.markdown-body pre code, +.markdown-body pre tt { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; +} + +.markdown-body .csv-data td, +.markdown-body .csv-data th { + padding: 5px; + overflow: hidden; + font-size: 12px; + line-height: 1; + text-align: left; + white-space: nowrap; +} + +.markdown-body .csv-data .blob-num { + padding: 10px 8px 9px; + text-align: right; + background: var(--color-canvas-default); + border: 0; +} + +.markdown-body .csv-data tr { + border-top: 0; +} + +.markdown-body .csv-data th { + font-weight: var(--base-text-weight-semibold, 600); + background: var(--color-canvas-subtle); + border-top: 0; +} + +.markdown-body [data-footnote-ref]::before { + content: "["; +} + +.markdown-body [data-footnote-ref]::after { + content: "]"; +} + +.markdown-body .footnotes { + font-size: 12px; + color: var(--color-fg-muted); + border-top: 1px solid var(--color-border-default); +} + +.markdown-body .footnotes ol { + padding-left: 16px; +} + +.markdown-body .footnotes ol ul { + display: inline-block; + padding-left: 16px; + margin-top: 16px; +} + +.markdown-body .footnotes li { + position: relative; +} + +.markdown-body .footnotes li:target::before { + position: absolute; + top: -8px; + right: -8px; + bottom: -8px; + left: -24px; + pointer-events: none; + content: ""; + border: 2px solid var(--color-accent-emphasis); + border-radius: 6px; +} + +.markdown-body .footnotes li:target { + color: var(--color-fg-default); +} + +.markdown-body .footnotes .data-footnote-backref g-emoji { + font-family: monospace; +} + +.markdown-body .pl-c { + color: var(--color-prettylights-syntax-comment); +} + +.markdown-body .pl-c1, +.markdown-body .pl-s .pl-v { + color: var(--color-prettylights-syntax-constant); +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: var(--color-prettylights-syntax-entity); +} + +.markdown-body .pl-smi, +.markdown-body .pl-s .pl-s1 { + color: var(--color-prettylights-syntax-storage-modifier-import); +} + +.markdown-body .pl-ent { + color: var(--color-prettylights-syntax-entity-tag); +} + +.markdown-body .pl-k { + color: var(--color-prettylights-syntax-keyword); +} + +.markdown-body .pl-s, +.markdown-body .pl-pds, +.markdown-body .pl-s .pl-pse .pl-s1, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sre, +.markdown-body .pl-sr .pl-sra { + color: var(--color-prettylights-syntax-string); +} + +.markdown-body .pl-v, +.markdown-body .pl-smw { + color: var(--color-prettylights-syntax-variable); +} + +.markdown-body .pl-bu { + color: var(--color-prettylights-syntax-brackethighlighter-unmatched); +} + +.markdown-body .pl-ii { + color: var(--color-prettylights-syntax-invalid-illegal-text); + background-color: var(--color-prettylights-syntax-invalid-illegal-bg); +} + +.markdown-body .pl-c2 { + color: var(--color-prettylights-syntax-carriage-return-text); + background-color: var(--color-prettylights-syntax-carriage-return-bg); +} + +.markdown-body .pl-sr .pl-cce { + font-weight: bold; + color: var(--color-prettylights-syntax-string-regexp); +} + +.markdown-body .pl-ml { + color: var(--color-prettylights-syntax-markup-list); +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + font-weight: bold; + color: var(--color-prettylights-syntax-markup-heading); +} + +.markdown-body .pl-mi { + font-style: italic; + color: var(--color-prettylights-syntax-markup-italic); +} + +.markdown-body .pl-mb { + font-weight: bold; + color: var(--color-prettylights-syntax-markup-bold); +} + +.markdown-body .pl-md { + color: var(--color-prettylights-syntax-markup-deleted-text); + background-color: var(--color-prettylights-syntax-markup-deleted-bg); +} + +.markdown-body .pl-mi1 { + color: var(--color-prettylights-syntax-markup-inserted-text); + background-color: var(--color-prettylights-syntax-markup-inserted-bg); +} + +.markdown-body .pl-mc { + color: var(--color-prettylights-syntax-markup-changed-text); + background-color: var(--color-prettylights-syntax-markup-changed-bg); +} + +.markdown-body .pl-mi2 { + color: var(--color-prettylights-syntax-markup-ignored-text); + background-color: var(--color-prettylights-syntax-markup-ignored-bg); +} + +.markdown-body .pl-mdr { + font-weight: bold; + color: var(--color-prettylights-syntax-meta-diff-range); +} + +.markdown-body .pl-ba { + color: var(--color-prettylights-syntax-brackethighlighter-angle); +} + +.markdown-body .pl-sg { + color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); +} + +.markdown-body .pl-corl { + text-decoration: underline; + color: var(--color-prettylights-syntax-constant-other-reference-link); +} + +.markdown-body g-emoji { + display: inline-block; + min-width: 1ch; + font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; + font-size: 1em; + font-style: normal !important; + font-weight: var(--base-text-weight-normal, 400); + line-height: 1; + vertical-align: -0.075em; +} + +.markdown-body g-emoji img { + width: 1em; + height: 1em; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item label { + font-weight: var(--base-text-weight-normal, 400); +} + +.markdown-body .task-list-item.enabled label { + cursor: pointer; +} + +.markdown-body .task-list-item+.task-list-item { + margin-top: 4px; +} + +.markdown-body .task-list-item .handle { + display: none; +} + +.markdown-body .task-list-item-checkbox { + margin: 0 .2em .25em -1.4em; + vertical-align: middle; +} + +.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { + margin: 0 -1.6em .25em .2em; +} + +.markdown-body .contains-task-list { + position: relative; +} + +.markdown-body .contains-task-list:hover .task-list-item-convert-container, +.markdown-body .contains-task-list:focus-within .task-list-item-convert-container { + display: block; + width: auto; + height: 24px; + overflow: visible; + clip: auto; +} + +.markdown-body ::-webkit-calendar-picker-indicator { + filter: invert(50%); +} diff --git a/src/components/markdown/index.vue b/src/components/markdown/index.vue new file mode 100644 index 0000000..2b1bdab --- /dev/null +++ b/src/components/markdown/index.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/src/components/markdown/markdown.scss b/src/components/markdown/markdown.scss new file mode 100644 index 0000000..3da4336 --- /dev/null +++ b/src/components/markdown/markdown.scss @@ -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); + // } + } + } +}