diff --git a/demo/yun/pages/posts/code.md b/demo/yun/pages/posts/code.md index 356b92eb1..83d3855da 100644 --- a/demo/yun/pages/posts/code.md +++ b/demo/yun/pages/posts/code.md @@ -1,5 +1,7 @@ --- title: Code Block Test +categories: + - Test --- ```dockerfile diff --git a/demo/yun/valaxy.config.ts b/demo/yun/valaxy.config.ts index 7cc263530..869a79b97 100644 --- a/demo/yun/valaxy.config.ts +++ b/demo/yun/valaxy.config.ts @@ -68,8 +68,10 @@ export default defineValaxyConfig({ markdown: { // default material-theme-palenight theme: { - light: 'material-theme-lighter', - dark: 'material-theme-darker', + // light: 'material-theme-lighter', + light: 'github-light', + // dark: 'material-theme-darker', + dark: 'github-dark', }, blocks: { tip: { diff --git a/packages/valaxy/client/composables/features/copy-code.ts b/packages/valaxy/client/composables/features/copy-code.ts index 2788ba56b..e87968ca9 100644 --- a/packages/valaxy/client/composables/features/copy-code.ts +++ b/packages/valaxy/client/composables/features/copy-code.ts @@ -1,24 +1,43 @@ -import { nextTick, watch } from 'vue' +// ref vitepress copyCode.ts import { isClient } from '@vueuse/core' -import { useRoute } from 'vue-router' export function useCopyCode() { - const route = useRoute() - if (isClient) { - watch( - () => route.path, - () => { - nextTick(() => { - document - .querySelectorAll( - '.markdown-body div[class*="language-"]>span.copy', - ) - .forEach(handleElement) + const timeoutIdMap: WeakMap = new WeakMap() + window.addEventListener('click', (e) => { + const el = e.target as HTMLElement + if (el.matches('div[class*="language-"] > button.copy')) { + const parent = el.parentElement + const sibling = el.nextElementSibling?.nextElementSibling + if (!parent || !sibling) + return + + const isShell = /language-(shellscript|shell|bash|sh|zsh)/.test( + parent.className, + ) + + let text = '' + + sibling + .querySelectorAll('span.line:not(.diff.remove)') + .forEach(node => (text += `${node.textContent || ''}\n`)) + text = text.slice(0, -1) + + if (isShell) + text = text.replace(/^ *(\$|>) /gm, '').trim() + + copyToClipboard(text).then(() => { + el.classList.add('copied') + clearTimeout(timeoutIdMap.get(el)) + const timeoutId = setTimeout(() => { + el.classList.remove('copied') + el.blur() + timeoutIdMap.delete(el) + }, 2000) + timeoutIdMap.set(el, timeoutId) }) - }, - { immediate: true, flush: 'post' }, - ) + } + }) } } @@ -42,7 +61,7 @@ async function copyToClipboard(text: string) { const selection = document.getSelection() const originalRange = selection - ? (selection.rangeCount > 0 && selection.getRangeAt(0)) + ? selection.rangeCount > 0 && selection.getRangeAt(0) : null document.body.appendChild(element) @@ -61,32 +80,8 @@ async function copyToClipboard(text: string) { } // Get the focus back on the previously focused element, if any - if (previouslyFocusedElement) - (previouslyFocusedElement as HTMLElement).focus() - } -} - -function handleElement(el: HTMLElement) { - el.onclick = () => { - const parent = el.parentElement - - if (!parent) - return - - const isShell - = parent.classList.contains('language-sh') - || parent.classList.contains('language-bash') - - let { innerText: text = '' } = parent - - if (isShell) - text = text.replace(/^ *\$ /gm, '') - - copyToClipboard(text).then(() => { - el.classList.add('copied') - setTimeout(() => { - el.classList.remove('copied') - }, 3000) - }) + if (previouslyFocusedElement) { + ;(previouslyFocusedElement as HTMLElement).focus() + } } } diff --git a/packages/valaxy/client/styles/common/code.scss b/packages/valaxy/client/styles/common/code.scss index 52fd0a872..da7741c7a 100644 --- a/packages/valaxy/client/styles/common/code.scss +++ b/packages/valaxy/client/styles/common/code.scss @@ -101,50 +101,66 @@ html:not(.dark) .vp-code-dark { } } - [class*="language-"] > span.copy { + // copy + [class*='language-'] > button.copy { + /*rtl:ignore*/ + direction: ltr; position: absolute; - top: 8px; - right: 8px; - z-index: 2; - display: block; - justify-content: center; - align-items: center; + top: 12px; + /*rtl:ignore*/ + right: 12px; + z-index: 3; + border: 1px solid var(--va-code-copy-code-border-color); border-radius: 4px; width: 40px; height: 40px; - background-color: var(--va-code-block-bg); + background-color: var(--va-code-copy-code-bg); opacity: 0; cursor: pointer; background-image: var(--va-icon-copy); background-position: 50%; background-size: 20px; background-repeat: no-repeat; - transition: opacity 0.25s; - } - - [class*="language-"]:hover > span.copy { + transition: + border-color 0.25s, + background-color 0.25s, + opacity 0.25s; + } + + [class*='language-']:hover > button.copy, + [class*='language-'] > button.copy:focus { opacity: 1; } - - [class*="language-"] > span.copy:hover { + + [class*='language-'] > button.copy:hover, + [class*='language-'] > button.copy.copied { + border-color: var(--va-code-copy-code-hover-border-color); background-color: var(--va-code-copy-code-hover-bg); } - - [class*="language-"] > span.copy.copied, - [class*="language-"] > span.copy:hover.copied { + + [class*='language-'] > button.copy.copied, + [class*='language-'] > button.copy:hover.copied { + /*rtl:ignore*/ border-radius: 0 4px 4px 0; background-color: var(--va-code-copy-code-hover-bg); background-image: var(--va-icon-copied); } - - [class*="language-"] > span.copy.copied::before, - [class*="language-"] > span.copy:hover.copied::before { + + [class*='language-'] > button.copy.copied::before, + [class*='language-'] > button.copy:hover.copied::before { position: relative; - left: -65px; - display: block; + top: -1px; + /*rtl:ignore*/ + transform: translateX(calc(-100% - 1px)); + display: flex; + justify-content: center; + align-items: center; + border: 1px solid var(--va-code-copy-code-hover-border-color); + /*rtl:ignore*/ + border-right: 0; border-radius: 4px 0 0 4px; - padding-top: 8px; - width: 64px; + padding: 0 10px; + width: fit-content; height: 40px; text-align: center; font-size: 12px; @@ -152,109 +168,25 @@ html:not(.dark) .vp-code-dark { color: var(--va-code-copy-code-active-text); background-color: var(--va-code-copy-code-hover-bg); white-space: nowrap; - content: "Copied"; + content: var(--va-code-copy-copied-text-content); } - [class*="language-"]:before { + [class*='language-'] > span.lang { position: absolute; - top: 6px; - right: 12px; + top: 2px; + /*rtl:ignore*/ + right: 8px; z-index: 2; font-size: 12px; font-weight: 500; - color: var(--va-c-text-dark-3); - transition: color 0.5s, opacity 0.5s; - } - - [class*="language-"]:hover:before { + color: var(--va-code-lang-color); + transition: + color 0.4s, + opacity 0.4s; + } + + [class*='language-']:hover > button.copy + span.lang, + [class*='language-'] > button.copy:focus + span.lang { opacity: 0; } - - [class~="language-c"]:before { - content: "c"; - } - [class~="language-css"]:before { - content: "css"; - } - [class~="language-go"]:before { - content: "go"; - } - [class~="language-html"]:before { - content: "html"; - } - [class~="language-java"]:before { - content: "java"; - } - [class~="language-javascript"]:before { - content: "js"; - } - [class~="language-js"]:before { - content: "js"; - } - [class~="language-json"]:before { - content: "json"; - } - [class~="language-jsx"]:before { - content: "jsx"; - } - [class~="language-less"]:before { - content: "less"; - } - [class~="language-markdown"]:before { - content: "md"; - } - [class~="language-md"]:before { - content: "md"; - } - [class~="language-php"]:before { - content: "php"; - } - [class~="language-python"]:before { - content: "py"; - } - [class~="language-py"]:before { - content: "py"; - } - [class~="language-rb"]:before { - content: "rb"; - } - [class~="language-ruby"]:before { - content: "rb"; - } - [class~="language-rust"]:before { - content: "rust"; - } - [class~="language-sass"]:before { - content: "sass"; - } - [class~="language-scss"]:before { - content: "scss"; - } - [class~="language-sh"]:before { - content: "sh"; - } - [class~="language-bash"]:before { - content: "sh"; - } - [class~="language-stylus"]:before { - content: "styl"; - } - [class~="language-vue-html"]:before { - content: "template"; - } - [class~="language-typescript"]:before { - content: "ts"; - } - [class~="language-ts"]:before { - content: "ts"; - } - [class~="language-tsx"]:before { - content: "tsx"; - } - [class~="language-vue"]:before { - content: "vue"; - } - [class~="language-yaml"]:before { - content: "yaml"; - } } diff --git a/packages/valaxy/client/styles/css-vars.scss b/packages/valaxy/client/styles/css-vars.scss index 4cb1e3a6f..8c1c49ea3 100644 --- a/packages/valaxy/client/styles/css-vars.scss +++ b/packages/valaxy/client/styles/css-vars.scss @@ -38,19 +38,47 @@ html.dark { @include set-css-var-from-map(palette.$dark); } +/** + * Colors: Text + * + * - `text-1`: Used for primary text. + * + * - `text-2`: Used for muted texts, such as "inactive menu" or "info texts". + * + * - `text-3`: Used for subtle texts, such as "placeholders" or "caret icon". + * -------------------------------------------------------------------------- */ +:root { + --va-c-text-1: rgba(60, 60, 67); + --va-c-text-2: rgba(60, 60, 67, 0.78); + --va-c-text-3: rgba(60, 60, 67, 0.56); +} +.dark { + --va-c-text-1: rgba(255, 255, 245, 0.86); + --va-c-text-2: rgba(235, 235, 245, 0.6); + --va-c-text-3: rgba(235, 235, 245, 0.38); +} + /* code */ :root { --va-code-line-height: 1.7; --va-code-font-size: 0.875em; - --va-code-block-color: var(--va-c-text-dark-1); - --va-code-block-bg: #282c34; + --va-code-block-color: var(--va-c-text-2); + --va-code-block-bg: var(--va-c-bg-alt); + --va-code-block-divider-color: var(--va-c-gutter); + + --va-code-lang-color: var(--va-c-text-3); --va-code-line-highlight-color: rgba(0, 0, 0, 0.5); --va-code-line-number-color: var(--va-c-text-dark-3); - --va-code-copy-code-hover-bg: rgba(255, 255, 255, 0.05); - --va-code-copy-code-active-text: var(--va-c-text-dark-2); + // copy + --va-code-copy-code-border-color: var(--va-c-divider); + --va-code-copy-code-bg: var(--va-c-bg-soft); + --va-code-copy-code-hover-border-color: var(--va-c-divider); + --va-code-copy-code-hover-bg: var(--va-c-bg); + --va-code-copy-code-active-text: var(--va-c-text-2); + --va-code-copy-copied-text-content: 'Copied'; } /* Icons */ diff --git a/packages/valaxy/node/markdown/index.ts b/packages/valaxy/node/markdown/index.ts index e640d3b2b..0b3013d1e 100644 --- a/packages/valaxy/node/markdown/index.ts +++ b/packages/valaxy/node/markdown/index.ts @@ -25,9 +25,10 @@ import type { MarkdownOptions } from './types' import Katex from './plugins/markdown-it/katex' import { containerPlugin } from './plugins/markdown-it/container' import { highlight } from './plugins/highlight' -import { highlightLinePlugin, preWrapperPlugin } from './plugins/markdown-it/highlightLines' +import { highlightLinePlugin } from './plugins/markdown-it/highlightLines' import { linkPlugin } from './plugins/link' +import { preWrapperPlugin } from './plugins/markdown-it/preWrapper' // import { lineNumberPlugin } from "./plugins/lineNumbers"; @@ -47,9 +48,12 @@ export async function setupMarkdownPlugins( isExcerpt = false, base = '/', ) { + const theme = mdOptions.theme ?? 'material-theme-palenight' + const hasSingleTheme = typeof theme === 'string' || 'name' in theme + // custom plugins md.use(highlightLinePlugin) - .use(preWrapperPlugin) + .use(preWrapperPlugin, { hasSingleTheme }) .use(containerPlugin, mdOptions.blocks) .use(cssI18nContainer, { languages: ['zh-CN', 'en'], @@ -113,6 +117,7 @@ export async function setupMarkdownPlugins( export async function createMarkdownRenderer(mdOptions: MarkdownOptions = {}): Promise { const theme = mdOptions.theme ?? 'material-theme-palenight' + const md = MarkdownIt({ html: true, linkify: true, diff --git a/packages/valaxy/node/markdown/plugins/markdown-it/highlightLines.ts b/packages/valaxy/node/markdown/plugins/markdown-it/highlightLines.ts index 8c72e2b19..4081d7ba9 100644 --- a/packages/valaxy/node/markdown/plugins/markdown-it/highlightLines.ts +++ b/packages/valaxy/node/markdown/plugins/markdown-it/highlightLines.ts @@ -51,24 +51,6 @@ export function highlightLinePlugin(md: MarkdownIt) { } } -// markdown-it plugin for wrapping
 ... 
. -// -// If your plugin was chained before preWrapper, you can add additional element directly. -// If your plugin was chained after preWrapper, you can use these slots: -// 1. -// 2. -// 3. -// 4. -export function preWrapperPlugin(md: MarkdownIt) { - const fence = md.renderer.rules.fence! - md.renderer.rules.fence = (...args) => { - const [tokens, idx] = args - const token = tokens[idx] - const rawCode = fence(...args) - return `
${rawCode}
` - } -} - // markdown-it plugin for generating line numbers. // It depends on preWrapper plugin. export function lineNumberPlugin(md: MarkdownIt) { diff --git a/packages/valaxy/node/markdown/plugins/markdown-it/preWrapper.ts b/packages/valaxy/node/markdown/plugins/markdown-it/preWrapper.ts new file mode 100644 index 000000000..31803e5b5 --- /dev/null +++ b/packages/valaxy/node/markdown/plugins/markdown-it/preWrapper.ts @@ -0,0 +1,43 @@ +// ref vitepress/packages/vitepress/src/node/markdown/plugins/preWrapper.ts +import type MarkdownIt from 'markdown-it' + +export interface Options { + hasSingleTheme: boolean +} + +export function extractLang(info: string) { + return info + .trim() + .replace(/:(no-)?line-numbers({| |$).*/, '') + .replace(/(-vue|{| ).*$/, '') + .replace(/^vue-html$/, 'template') +} + +export function getAdaptiveThemeMarker(options: Options) { + return options.hasSingleTheme ? '' : ' vp-adaptive-theme' +} + +// markdown-it plugin for wrapping
 ... 
. +// +// If your plugin was chained before preWrapper, you can add additional element directly. +// If your plugin was chained after preWrapper, you can use these slots: +// 1. +// 2. +// 3. +// 4. +export function preWrapperPlugin(md: MarkdownIt, options: Options) { + const fence = md.renderer.rules.fence! + md.renderer.rules.fence = (...args) => { + const [tokens, idx] = args + const token = tokens[idx] + // remove title from info + token.info = token.info.replace(/\[.*\]/, '') + + const lang = extractLang(token.info) + const rawCode = fence(...args) + + return `
${lang}${rawCode}
` + } +}