From 199bbd9a9a3663d02218a06c08f97bb16dafb7cb Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Wed, 14 Aug 2024 03:54:20 +0800 Subject: [PATCH 1/4] perf(plugin-shikiji): improve `highlight` --- plugins/plugin-shikiji/src/node/highlight.ts | 183 ------------------ .../src/node/highlight/getLanguage.ts | 25 +++ .../src/node/highlight/highlight.ts | 87 +++++++++ .../src/node/highlight/index.ts | 1 + .../src/node/highlight/transformers.ts | 89 +++++++++ 5 files changed, 202 insertions(+), 183 deletions(-) delete mode 100644 plugins/plugin-shikiji/src/node/highlight.ts create mode 100644 plugins/plugin-shikiji/src/node/highlight/getLanguage.ts create mode 100644 plugins/plugin-shikiji/src/node/highlight/highlight.ts create mode 100644 plugins/plugin-shikiji/src/node/highlight/index.ts create mode 100644 plugins/plugin-shikiji/src/node/highlight/transformers.ts diff --git a/plugins/plugin-shikiji/src/node/highlight.ts b/plugins/plugin-shikiji/src/node/highlight.ts deleted file mode 100644 index 49e1f69e..00000000 --- a/plugins/plugin-shikiji/src/node/highlight.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { colors as c, logger } from 'vuepress/utils' -import { customAlphabet } from 'nanoid' -import type { ShikiTransformer } from 'shiki' -import { - addClassToHast, - bundledLanguages, - createHighlighter, - isPlainLang, - isSpecialLang, -} from 'shiki' -import { - transformerCompactLineOptions, - transformerNotationDiff, - transformerNotationErrorLevel, - transformerNotationFocus, - transformerNotationHighlight, - transformerNotationWordHighlight, - transformerRemoveNotationEscape, - transformerRenderWhitespace, -} from '@shikijs/transformers' -import type { HighlighterOptions, ThemeOptions } from './types.js' -import { attrsToLines, resolveLanguage } from './utils/index.js' -import { defaultHoverInfoProcessor, transformerTwoslash } from './twoslash/rendererTransformer.js' - -const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10) - -const vueRE = /-vue$/ -const mustacheRE = /\{\{.*?\}\}/g -const decorationsRE = /^\/\/ @decorations:(.*)\n/ - -export async function highlight( - theme: ThemeOptions, - options: HighlighterOptions, -): Promise<(str: string, lang: string, attrs: string) => string> { - const { - defaultHighlightLang: defaultLang = '', - codeTransformers: userTransformers = [], - whitespace = false, - languages = Object.keys(bundledLanguages), - } = options - - const highlighter = await createHighlighter({ - themes: - typeof theme === 'object' && 'light' in theme && 'dark' in theme - ? [theme.light, theme.dark] - : [theme], - langs: languages, - langAlias: options.languageAlias, - }) - - await options?.shikiSetup?.(highlighter) - - const transformers: ShikiTransformer[] = [ - transformerNotationDiff(), - transformerNotationFocus({ - classActiveLine: 'has-focus', - classActivePre: 'has-focused-lines', - }), - transformerNotationHighlight(), - transformerNotationErrorLevel(), - transformerNotationWordHighlight(), - { - name: 'vuepress:add-class', - pre(node) { - addClassToHast(node, 'vp-code') - }, - }, - { - name: 'vuepress:clean-up', - pre(node) { - delete node.properties.tabindex - delete node.properties.style - }, - }, - { - name: 'shiki:inline-decorations', - preprocess(code, options) { - code = code.replace(decorationsRE, (match, decorations) => { - options.decorations ||= [] - options.decorations.push(...JSON.parse(decorations)) - return '' - }) - return code - }, - }, - transformerRemoveNotationEscape(), - ] - - const loadedLanguages = highlighter.getLoadedLanguages() - - return (str: string, language: string, attrs: string) => { - attrs = attrs || '' - let lang = resolveLanguage(language) || defaultLang - const vPre = vueRE.test(lang) ? '' : 'v-pre' - - if (lang) { - const langLoaded = loadedLanguages.includes(lang as any) - if (!langLoaded && !isPlainLang(lang) && !isSpecialLang(lang)) { - logger.warn( - c.yellow( - `\nThe language '${lang}' is not loaded, falling back to '${defaultLang || 'txt' - }' for syntax highlighting.`, - ), - ) - lang = defaultLang - } - } - // const { attrs: attributes, rawAttrs } = resolveAttrs(attrs || '') - const enabledTwoslash = attrs.includes('twoslash') - const mustaches = new Map() - - const removeMustache = (s: string) => { - return s.replace(mustacheRE, (match) => { - let marker = mustaches.get(match) - if (!marker) { - marker = nanoid() - mustaches.set(match, marker) - } - return marker - }) - } - - const restoreMustache = (s: string) => { - mustaches.forEach((marker, match) => { - s = s.replaceAll(marker, match) - }) - - if (enabledTwoslash && options.twoslash) - s = s.replace(/\{/g, '{') - - return `${s}\n` - } - - str = removeMustache(str).trimEnd() - - const inlineTransformers: ShikiTransformer[] = [ - transformerCompactLineOptions(attrsToLines(attrs)), - ] - - if (enabledTwoslash && options.twoslash) { - inlineTransformers.push(transformerTwoslash({ - processHoverInfo(info) { - return defaultHoverInfoProcessor(info) - }, - })) - } - else { - inlineTransformers.push({ - name: 'vuepress:v-pre', - pre(node) { - if (vPre) - node.properties['v-pre'] = '' - }, - }) - } - - if (attrs.includes('whitespace') || whitespace) - inlineTransformers.push(transformerRenderWhitespace({ position: 'boundary' })) - - try { - const highlighted = highlighter.codeToHtml(str, { - lang, - transformers: [ - ...transformers, - ...inlineTransformers, - ...userTransformers, - ], - meta: { __raw: attrs }, - ...(typeof theme === 'object' && 'light' in theme && 'dark' in theme - ? { themes: theme, defaultColor: false } - : { theme }), - }) - - const rendered = restoreMustache(highlighted) - - return rendered - } - catch (e) { - logger.error(e) - return str - } - } -} diff --git a/plugins/plugin-shikiji/src/node/highlight/getLanguage.ts b/plugins/plugin-shikiji/src/node/highlight/getLanguage.ts new file mode 100644 index 00000000..b6819636 --- /dev/null +++ b/plugins/plugin-shikiji/src/node/highlight/getLanguage.ts @@ -0,0 +1,25 @@ +import { isPlainLang, isSpecialLang } from 'shiki' +import { colors as c, logger } from 'vuepress/utils' +import { resolveLanguage } from '../utils/index.js' + +export function getLanguage( + loadedLanguages: string[], + language: string, + defaultLang: string, +): string { + let lang = resolveLanguage(language) || defaultLang + + if (lang) { + const langLoaded = loadedLanguages.includes(lang as any) + if (!langLoaded && !isPlainLang(lang) && !isSpecialLang(lang)) { + logger.warn( + c.yellow( + `\nThe language '${lang}' is not loaded, falling back to '${defaultLang || 'txt' + }' for syntax highlighting.`, + ), + ) + lang = defaultLang + } + } + return lang +} diff --git a/plugins/plugin-shikiji/src/node/highlight/highlight.ts b/plugins/plugin-shikiji/src/node/highlight/highlight.ts new file mode 100644 index 00000000..3796b79e --- /dev/null +++ b/plugins/plugin-shikiji/src/node/highlight/highlight.ts @@ -0,0 +1,87 @@ +import { logger } from 'vuepress/utils' +import { customAlphabet } from 'nanoid' +import { bundledLanguages, createHighlighter } from 'shiki' +import type { HighlighterOptions, ThemeOptions } from '../types.js' +import { baseTransformers, getInlineTransformers } from './transformers.js' +import { getLanguage } from './getLanguage.js' + +const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10) +const mustacheRE = /\{\{.*?\}\}/g + +export async function highlight( + theme: ThemeOptions, + options: HighlighterOptions, +): Promise<(str: string, lang: string, attrs: string) => string> { + const { + defaultHighlightLang: defaultLang = '', + codeTransformers: userTransformers = [], + whitespace = false, + languages = Object.keys(bundledLanguages), + } = options + + const highlighter = await createHighlighter({ + themes: + typeof theme === 'object' && 'light' in theme && 'dark' in theme + ? [theme.light, theme.dark] + : [theme], + langs: languages, + langAlias: options.languageAlias, + }) + + await options.shikiSetup?.(highlighter) + + const loadedLanguages = highlighter.getLoadedLanguages() + const removeMustache = (s: string, mustaches: Map) => { + return s.replace(mustacheRE, (match) => { + let marker = mustaches.get(match) + if (!marker) { + marker = nanoid() + mustaches.set(match, marker) + } + return marker + }) + } + + const restoreMustache = (s: string, mustaches: Map, twoslash: boolean) => { + mustaches.forEach((marker, match) => { + s = s.replaceAll(marker, match) + }) + + if (twoslash) + s = s.replace(/\{/g, '{') + + return `${s}\n` + } + + return (str: string, language: string, attrs: string = '') => { + const lang = getLanguage(loadedLanguages, language, defaultLang) + + const enabledTwoslash = attrs.includes('twoslash') && !!options.twoslash + + const mustaches = new Map() + str = removeMustache(str, mustaches).trimEnd() + + try { + const highlighted = highlighter.codeToHtml(str, { + lang, + transformers: [ + ...baseTransformers, + ...getInlineTransformers({ attrs, lang, enabledTwoslash, whitespace }), + ...userTransformers, + ], + meta: { __raw: attrs }, + ...(typeof theme === 'object' && 'light' in theme && 'dark' in theme + ? { themes: theme, defaultColor: false } + : { theme }), + }) + + const rendered = restoreMustache(highlighted, mustaches, enabledTwoslash) + + return rendered + } + catch (e) { + logger.error(e) + return str + } + } +} diff --git a/plugins/plugin-shikiji/src/node/highlight/index.ts b/plugins/plugin-shikiji/src/node/highlight/index.ts new file mode 100644 index 00000000..80f96a38 --- /dev/null +++ b/plugins/plugin-shikiji/src/node/highlight/index.ts @@ -0,0 +1 @@ +export * from './highlight.js' diff --git a/plugins/plugin-shikiji/src/node/highlight/transformers.ts b/plugins/plugin-shikiji/src/node/highlight/transformers.ts new file mode 100644 index 00000000..d3cf5a84 --- /dev/null +++ b/plugins/plugin-shikiji/src/node/highlight/transformers.ts @@ -0,0 +1,89 @@ +import type { ShikiTransformer } from 'shiki' +import { addClassToHast } from 'shiki' +import { + transformerCompactLineOptions, + transformerNotationDiff, + transformerNotationErrorLevel, + transformerNotationFocus, + transformerNotationHighlight, + transformerNotationWordHighlight, + transformerRemoveNotationEscape, + transformerRenderWhitespace, +} from '@shikijs/transformers' +import type { WhitespacePosition } from '../utils/index.js' +import { attrsToLines, resolveWhitespacePosition } from '../utils/index.js' +import { defaultHoverInfoProcessor, transformerTwoslash } from '../twoslash/rendererTransformer.js' + +const decorationsRE = /^\/\/ @decorations:(.*)\n/ + +export const baseTransformers: ShikiTransformer[] = [ + transformerNotationDiff(), + transformerNotationFocus({ + classActiveLine: 'has-focus', + classActivePre: 'has-focused-lines', + }), + transformerNotationHighlight(), + transformerNotationErrorLevel(), + transformerNotationWordHighlight(), + { + name: 'vuepress:add-class', + pre(node) { + addClassToHast(node, 'vp-code') + }, + }, + { + name: 'vuepress:clean-up', + pre(node) { + delete node.properties.tabindex + delete node.properties.style + }, + }, + { + name: 'shiki:inline-decorations', + preprocess(code, options) { + code = code.replace(decorationsRE, (match, decorations) => { + options.decorations ||= [] + options.decorations.push(...JSON.parse(decorations)) + return '' + }) + return code + }, + }, + transformerRemoveNotationEscape(), +] + +const vueRE = /-vue$/ +export function getInlineTransformers({ attrs, lang, enabledTwoslash, whitespace }: { + attrs: string + lang: string + enabledTwoslash: boolean + whitespace: boolean | WhitespacePosition +}): ShikiTransformer[] { + const vPre = vueRE.test(lang) ? '' : 'v-pre' + const inlineTransformers: ShikiTransformer[] = [ + transformerCompactLineOptions(attrsToLines(attrs)), + ] + + if (enabledTwoslash) { + inlineTransformers.push(transformerTwoslash({ + processHoverInfo(info) { + return defaultHoverInfoProcessor(info) + }, + })) + } + else { + inlineTransformers.push({ + name: 'vuepress:v-pre', + pre(node) { + if (vPre) + node.properties['v-pre'] = '' + }, + }) + } + + const position = resolveWhitespacePosition(attrs, whitespace) + if (position) + inlineTransformers.push(transformerRenderWhitespace({ position })) + + return inlineTransformers +} From 48a659629738399573a451e757652b3c0667528a Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Wed, 14 Aug 2024 03:54:52 +0800 Subject: [PATCH 2/4] perf(plugin-shikiji): improve `highlight whitespace` --- .../src/node/utils/whitespace.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 plugins/plugin-shikiji/src/node/utils/whitespace.ts diff --git a/plugins/plugin-shikiji/src/node/utils/whitespace.ts b/plugins/plugin-shikiji/src/node/utils/whitespace.ts new file mode 100644 index 00000000..558d48f2 --- /dev/null +++ b/plugins/plugin-shikiji/src/node/utils/whitespace.ts @@ -0,0 +1,18 @@ +export const WHITESPACE_REGEXP = /:whitespace(?:=(all|boundary|trailing)?)?\b/ +export const NO_WHITESPACE_REGEXP = /:no-whitespace\b/ + +export type WhitespacePosition = 'all' | 'boundary' | 'trailing' + +export function resolveWhitespacePosition(info: string, defaultPosition?: boolean | WhitespacePosition): WhitespacePosition | false { + if (NO_WHITESPACE_REGEXP.test(info)) { + return false + } + + defaultPosition = defaultPosition === true ? undefined : defaultPosition + + const match = info.match(WHITESPACE_REGEXP) + if (match) { + return (match[1] || defaultPosition || 'all') as WhitespacePosition + } + return defaultPosition ?? false +} From bb4ee6bb2d8523b4264a16bf6ea0066f8f7ea295 Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Wed, 14 Aug 2024 03:55:49 +0800 Subject: [PATCH 3/4] feat(plugin-shikiji): add support for `collapsed lines` --- .../src/client/composables/collapsed-lines.ts | 15 ++++ .../src/node/markdown/preWrapperPlugin.ts | 23 ++++-- .../src/node/prepareClientConfigFile.ts | 2 + .../plugin-shikiji/src/node/shikiPlugin.ts | 13 +-- plugins/plugin-shikiji/src/node/types.ts | 11 ++- .../src/node/utils/collapsedLines.ts | 18 ++++ .../plugin-shikiji/src/node/utils/index.ts | 2 + plugins/plugin-shikiji/tsup.config.ts | 1 + theme/src/client/styles/code.css | 82 +++++++++++++++++++ 9 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 plugins/plugin-shikiji/src/client/composables/collapsed-lines.ts create mode 100644 plugins/plugin-shikiji/src/node/utils/collapsedLines.ts diff --git a/plugins/plugin-shikiji/src/client/composables/collapsed-lines.ts b/plugins/plugin-shikiji/src/client/composables/collapsed-lines.ts new file mode 100644 index 00000000..a551423f --- /dev/null +++ b/plugins/plugin-shikiji/src/client/composables/collapsed-lines.ts @@ -0,0 +1,15 @@ +import { useEventListener } from '@vueuse/core' + +export function useCollapsedLines({ + selector = 'div[class*="language-"] > .collapsed-lines', +}: { selector?: string } = {}): void { + useEventListener('click', (e) => { + const el = e.target as HTMLElement + if (el.matches(selector)) { + const parent = el.parentElement + if (parent?.classList.toggle('collapsed')) { + parent.scrollIntoView({ block: 'center', behavior: 'instant' }) + } + } + }) +} diff --git a/plugins/plugin-shikiji/src/node/markdown/preWrapperPlugin.ts b/plugins/plugin-shikiji/src/node/markdown/preWrapperPlugin.ts index c6f6f455..1a5cff15 100644 --- a/plugins/plugin-shikiji/src/node/markdown/preWrapperPlugin.ts +++ b/plugins/plugin-shikiji/src/node/markdown/preWrapperPlugin.ts @@ -2,9 +2,12 @@ // v-pre block logic is in `../highlight.ts` import type { Markdown } from 'vuepress/markdown' import type { PreWrapperOptions } from '../types.js' -import { resolveAttr, resolveLanguage } from '../utils/index.js' +import { resolveAttr, resolveCollapsedLines, resolveLanguage } from '../utils/index.js' -export function preWrapperPlugin(md: Markdown, { preWrapper = true }: PreWrapperOptions = {}): void { +export function preWrapperPlugin( + md: Markdown, + { preWrapper = true, collapsedLines = false }: PreWrapperOptions = {}, +): void { const rawFence = md.renderer.rules.fence! md.renderer.rules.fence = (...args) => { @@ -16,17 +19,27 @@ export function preWrapperPlugin(md: Markdown, { preWrapper = true }: PreWrapper const lang = resolveLanguage(info) const title = resolveAttr(info, 'title') || lang - const languageClass = `${options.langPrefix}${lang}` + const classes: string[] = [`${options.langPrefix}${lang}`] let result = rawFence(...args) if (!preWrapper) { // remove `` attributes result = result.replace(//, '') - result = `
`
+    }
 
-    return `
${result}
` + return `
${result}
` } } diff --git a/plugins/plugin-shikiji/src/node/prepareClientConfigFile.ts b/plugins/plugin-shikiji/src/node/prepareClientConfigFile.ts index 0ffabaa4..63670e9d 100644 --- a/plugins/plugin-shikiji/src/node/prepareClientConfigFile.ts +++ b/plugins/plugin-shikiji/src/node/prepareClientConfigFile.ts @@ -17,6 +17,7 @@ export async function prepareClientConfigFile(app: App, { `\ ${twoslash ? `import { enhanceTwoslash } from '${CLIENT_FOLDER}composables/twoslash.js'` : ''} ${copyCode ? `import { useCopyCode } from '${CLIENT_FOLDER}composables/copy-code.js'` : ''} +import { useCollapsedLines } from '${CLIENT_FOLDER}composables/collapsed-lines.js' export default { ${twoslash @@ -30,6 +31,7 @@ export default { selector: __CC_SELECTOR__, duration: __CC_DURATION__, }) + useCollapsedLines() },` : ''} } diff --git a/plugins/plugin-shikiji/src/node/shikiPlugin.ts b/plugins/plugin-shikiji/src/node/shikiPlugin.ts index 2d961a9d..5316b59a 100644 --- a/plugins/plugin-shikiji/src/node/shikiPlugin.ts +++ b/plugins/plugin-shikiji/src/node/shikiPlugin.ts @@ -1,7 +1,6 @@ import type { Plugin } from 'vuepress/core' -import { getDirname } from 'vuepress/utils' import { isPlainObject } from 'vuepress/shared' -import { highlight } from './highlight.js' +import { highlight } from './highlight/index.js' import type { CopyCodeOptions, HighlighterOptions, @@ -16,7 +15,8 @@ import { import { copyCodeButtonPlugin } from './copy-code-button/index.js' import { prepareClientConfigFile } from './prepareClientConfigFile.js' -export interface ShikiPluginOptions extends HighlighterOptions, LineNumberOptions, PreWrapperOptions { +export interface ShikiPluginOptions + extends HighlighterOptions, LineNumberOptions, PreWrapperOptions { /** * Add copy code button * @@ -25,12 +25,11 @@ export interface ShikiPluginOptions extends HighlighterOptions, LineNumberOption copyCode?: boolean | CopyCodeOptions } -const __dirname = getDirname(import.meta.url) - export function shikiPlugin({ preWrapper = true, lineNumbers = true, copyCode = true, + collapsedLines = false, ...options }: ShikiPluginOptions = {}): Plugin { const copyCodeOptions: CopyCodeOptions = isPlainObject(copyCode) ? copyCode : {} @@ -54,9 +53,11 @@ export function shikiPlugin({ md.options.highlight = await highlight(theme, options) md.use(highlightLinesPlugin) - md.use(preWrapperPlugin, { + md.use(preWrapperPlugin, { preWrapper, + collapsedLines, }) + if (preWrapper) { copyCodeButtonPlugin(md, app, copyCode) md.use(lineNumberPlugin, { lineNumbers }) diff --git a/plugins/plugin-shikiji/src/node/types.ts b/plugins/plugin-shikiji/src/node/types.ts index 3503f1b0..db434228 100644 --- a/plugins/plugin-shikiji/src/node/types.ts +++ b/plugins/plugin-shikiji/src/node/types.ts @@ -79,7 +79,7 @@ export interface HighlighterOptions { * Enable transformerRenderWhitespace * @default false */ - whitespace?: boolean + whitespace?: boolean | 'all' | 'boundary' | 'trailing' } export interface LineNumberOptions { @@ -99,6 +99,15 @@ export interface PreWrapperOptions { * - Required for title display of default theme */ preWrapper?: boolean + + /** + * Hide extra rows when exceeding a specific number of lines. + * + * `true` is equivalent to `15` . + * + * @default false + */ + collapsedLines?: number | boolean } /** diff --git a/plugins/plugin-shikiji/src/node/utils/collapsedLines.ts b/plugins/plugin-shikiji/src/node/utils/collapsedLines.ts new file mode 100644 index 00000000..72d42f37 --- /dev/null +++ b/plugins/plugin-shikiji/src/node/utils/collapsedLines.ts @@ -0,0 +1,18 @@ +export const COLLAPSED_LINES_REGEXP = /:collapsed-lines(?:=(\d+))?\b/ +export const NO_COLLAPSED_LINES_REGEXP = /:no-collapsed-lines\b/ + +const DEFAULT_LINES = 15 + +export function resolveCollapsedLines(info: string, defaultLines: boolean | number): number | false { + if (NO_COLLAPSED_LINES_REGEXP.test(info)) + return false + + const lines = defaultLines === true ? DEFAULT_LINES : defaultLines + + const match = info.match(COLLAPSED_LINES_REGEXP) + + if (match) { + return Number(match[1]) || lines || DEFAULT_LINES + } + return lines ?? false +} diff --git a/plugins/plugin-shikiji/src/node/utils/index.ts b/plugins/plugin-shikiji/src/node/utils/index.ts index 9c345151..e5a146d2 100644 --- a/plugins/plugin-shikiji/src/node/utils/index.ts +++ b/plugins/plugin-shikiji/src/node/utils/index.ts @@ -2,3 +2,5 @@ export * from './attrsToLines.js' export * from './resolveAttr.js' export * from './resolveLanguage.js' export * from './lru.js' +export * from './whitespace.js' +export * from './collapsedLines.js' diff --git a/plugins/plugin-shikiji/tsup.config.ts b/plugins/plugin-shikiji/tsup.config.ts index d045dc8b..1a328544 100644 --- a/plugins/plugin-shikiji/tsup.config.ts +++ b/plugins/plugin-shikiji/tsup.config.ts @@ -21,6 +21,7 @@ export default defineConfig(() => { entry: [ 'copy-code.ts', 'twoslash.ts', + 'collapsed-lines.ts', ].map(file => `./src/client/composables/${file}`), outDir: './lib/client/composables', external: [/.*\.css$/], diff --git a/theme/src/client/styles/code.css b/theme/src/client/styles/code.css index dcdd388a..d0fe1d1f 100644 --- a/theme/src/client/styles/code.css +++ b/theme/src/client/styles/code.css @@ -310,3 +310,85 @@ html:not(.dark) .vp-code span { /* rtl:ignore */ transform: translateX(calc(-100% - 1px)); } + +/* + Collapsed lines + -------------------------------------------------------------------------- + */ + +.vp-doc div[class*="language-"].has-collapsed.collapsed { + height: calc(var(--vp-collapsed-lines) * var(--vp-code-line-height) * var(--vp-code-font-size) + 62px); + overflow-y: hidden; +} + +@property --vp-code-bg-collapsed-lines { + inherits: false; + initial-value: #fff; + syntax: ""; +} + +.vp-doc div[class*="language-"].has-collapsed .collapsed-lines { + --vp-code-bg-collapsed-lines: var(--vp-code-block-bg); + + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 4; + display: flex; + align-items: center; + justify-content: center; + height: 44px; + cursor: pointer; + background: linear-gradient(to bottom, transparent 0%, var(--vp-code-bg-collapsed-lines) 50%, var(--vp-code-bg-collapsed-lines) 100%); + transition: --vp-code-bg-collapsed-lines var(--t-color); +} + +.vp-doc div[class*="language-"].has-collapsed .collapsed-lines:hover { + --vp-code-bg-collapsed-lines: var(--vp-c-default-soft); +} + +.vp-doc div[class*="language-"].has-collapsed .collapsed-lines::before { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-width='2' d='m18 12l-6 6l-6-6m12-6l-6 6l-6-6'/%3E%3C/svg%3E"); + --trans-rotate: 0deg; + + display: inline-block; + width: 24px; + height: 24px; + pointer-events: none; + content: ""; + background-color: var(--vp-code-block-color); + -webkit-mask-image: var(--icon); + mask-image: var(--icon); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: 50%; + mask-position: 50%; + -webkit-mask-size: 20px; + mask-size: 20px; + animation: code-collapsed-lines 1.2s infinite alternate-reverse ease-in-out; +} + +.vp-doc div[class*="language-"].has-collapsed:not(.collapsed) code { + padding-bottom: 20px; +} + +.vp-doc div[class*="language-"].has-collapsed:not(.collapsed) .collapsed-lines:hover { + --vp-code-bg-collapsed-lines: transparent; +} + +.vp-doc div[class*="language-"].has-collapsed:not(.collapsed) .collapsed-lines::before { + --trans-rotate: 180deg; +} + +@keyframes code-collapsed-lines { + 0% { + opacity: 0.3; + transform: translateY(-2px) rotate(var(--trans-rotate)); + } + + 100% { + opacity: 1; + transform: translateY(2px) rotate(var(--trans-rotate)); + } +} From 1ff7796d57246388619451291c9dcba90a2be5f0 Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Wed, 14 Aug 2024 03:56:05 +0800 Subject: [PATCH 4/4] docs: update docs --- docs/notes/theme/config/plugins/代码高亮.md | 29 ++-- docs/notes/theme/guide/代码/特性支持.md | 157 ++++++++++++++++++ docs/notes/theme/guide/编写文章.md | 4 +- .../notes/theme/snippet/whitespace.snippet.md | 29 +++- 4 files changed, 200 insertions(+), 19 deletions(-) diff --git a/docs/notes/theme/config/plugins/代码高亮.md b/docs/notes/theme/config/plugins/代码高亮.md index 3a0e22d3..f46e0241 100644 --- a/docs/notes/theme/config/plugins/代码高亮.md +++ b/docs/notes/theme/config/plugins/代码高亮.md @@ -20,12 +20,13 @@ Shiki 支持多种编程语言。 [@vuepress/plugin-shiki](https://ecosystem.vuejs.press/zh/plugins/shiki.html) 两个代码高亮插件, 提供了更为丰富的功能支持,包括: -- [代码行高亮](/guide/markdown/extensions/#在代码块中实现行高亮) -- [代码聚焦](/guide/markdown/extensions/#代码块中聚焦) -- [代码对比差异](/guide/markdown/extensions/#代码块中的颜色差异) -- [代码高亮“错误”和“警告”](/guide/markdown/extensions/#高亮-错误-和-警告) -- [代码词高亮](/guide/markdown/extensions/#代码块中-词高亮) -- [twoslash](/guide/markdown/experiment/#twoslash) ,在代码块内提供内联类型提示。 +- [代码行高亮](../../guide/代码/特性支持.md#在代码块中实现行高亮) +- [代码聚焦](../../guide/代码/特性支持.md#代码块中聚焦) +- [代码对比差异](../../guide/代码/特性支持.md#代码块中的颜色差异) +- [代码高亮“错误”和“警告”](../../guide/代码/特性支持.md#高亮-错误-和-警告) +- [代码词高亮](../../guide/代码/特性支持.md#代码块中-词高亮) +- [代码块折叠](../../guide/代码/特性支持.md#折叠代码块) +- [twoslash](../../guide/代码/twoslash.md#twoslash) ,在代码块内提供内联类型提示。 默认配置: @@ -140,16 +141,18 @@ interface CopyCodeOptions { ### whitespace -- 类型: `boolean` +- 类型: `boolean | 'all' | 'boundary' | 'trailing'` - 默认值: `false` 将空白字符(Tab 和空格)渲染为单独的标签(具有 tab 或 space 类名)。 效果: -```ts whitespace -function block() { - space() - table() -} -``` + + +### collapseLines + +- 类型: `boolean | number` +- 默认值: `false` + +将代码块折叠到指定行数。 diff --git a/docs/notes/theme/guide/代码/特性支持.md b/docs/notes/theme/guide/代码/特性支持.md index f174f8fe..ea7ab42a 100644 --- a/docs/notes/theme/guide/代码/特性支持.md +++ b/docs/notes/theme/guide/代码/特性支持.md @@ -309,6 +309,8 @@ console.log(options.foo) // 这个不会被高亮显示 将空白字符(Tab 和空格)渲染为可见状态。 +在 代码块 后面添加 `:whitespace`。 + 还可以在 `theme.plugins.shiki` 中全局启用 `whitespace` 功能: @@ -327,3 +329,158 @@ export default defineUserConfig({ ``` ::: + +全局启用时,可以使用 `:no-whitespace` 来单独为某一代码块禁用 `whitespace` 功能。 + +## 折叠代码块 + +有时候,代码块会很长,对于阅读其它部分的内容时,会显得很麻烦,影响阅读体验,这时候可以折叠代码块。 + +在 代码块 后面添加 `:collapsed-lines`,即可折叠代码块,默认从第 15 行开始折叠。 + +**输入:** + +````txt +```css :collapsed-lines +html { + margin: 0; + background: black; + height: 100%; +} + +... more code +``` +```` + +**输出:** + +```css :collapsed-lines +html { + margin: 0; + background: black; + height: 100%; +} + +body { + margin: 0; + width: 100%; + height: inherit; +} + +/* the three main rows going down the page */ + +body > div { + height: 25%; +} + +.thumb { + float: left; + width: 25%; + height: 100%; + object-fit: cover; +} + +.main { + display: none; +} + +.blowup { + display: block; + position: absolute; + object-fit: contain; + object-position: center; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2000; +} + +.darken { + opacity: 0.4; +} +``` + +还可以指定起始折叠行。`:collapsed-lines=10` 表示从第十行开始折叠。 + +**输入:** + +````txt +```css :collapsed-lines=10 +html { + margin: 0; + background: black; + height: 100%; +} + +... more code +``` +```` + +**输出:** + +```css :collapsed-lines=10 +html { + margin: 0; + background: black; + height: 100%; +} + +body { + margin: 0; + width: 100%; + height: inherit; +} + +/* the three main rows going down the page */ + +body > div { + height: 25%; +} + +.thumb { + float: left; + width: 25%; + height: 100%; + object-fit: cover; +} + +.main { + display: none; +} + +.blowup { + display: block; + position: absolute; + object-fit: contain; + object-position: center; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2000; +} + +.darken { + opacity: 0.4; +} +``` + +还可以在 `theme.plugins.shiki` 中全局启用 `collapsed-lines` 功能: + +::: code-tabs +@tab .vuepress/config.ts + +```ts +export default defineUserConfig({ + theme: plumeTheme({ + plugins: { + shiki: { collapsedLines: true } + } + }) +}) +``` + +::: + +全局启用时,可以使用 `:no-collapsed-lines` 来单独为某一代码块禁用 `collapsed-lines` 功能。 diff --git a/docs/notes/theme/guide/编写文章.md b/docs/notes/theme/guide/编写文章.md index 12a9fc85..b33e6ce4 100644 --- a/docs/notes/theme/guide/编写文章.md +++ b/docs/notes/theme/guide/编写文章.md @@ -24,7 +24,7 @@ tags: - 文件夹的名称将作为 `category` 即 __分类__。 - 允许多级目录,子级目录将作为父目录对应的分类的子项。 -- 如果目录名称 在 [主题配置 notes](/vuepress-theme-plume/theme-config/#notes) 中声明用于 notes 文章管理,则默认不作为 分类目录。 +- 如果目录名称 在 [主题配置 notes](../config/notes配置.md) 中声明用于 notes 文章管理,则默认不作为 分类目录。 ### 文件夹命名约定 @@ -53,4 +53,4 @@ __example:__ ## 文章写作 你可以使用 `markdown` 语法开始在 `sourceDir` 下新建 `Markdown` 文件,编写你自己的文章了, -关于 markdown 扩展的功能支持,请查看 [这个文档](/guide/markdown/extensions/)。 +关于 markdown 扩展的功能支持,请查看 [这个文档](./markdown/扩展.md) diff --git a/docs/notes/theme/snippet/whitespace.snippet.md b/docs/notes/theme/snippet/whitespace.snippet.md index 1b9c945b..c7cdbcb2 100644 --- a/docs/notes/theme/snippet/whitespace.snippet.md +++ b/docs/notes/theme/snippet/whitespace.snippet.md @@ -3,7 +3,7 @@ **输入:** ```` -```xml whitespace +```xml :whitespace Everyday Italian @@ -15,7 +15,7 @@ **输出:** -```xml whitespace +```xml :whitespace :no-line-numbers Everyday Italian @@ -28,7 +28,7 @@ **输入:** ```` -```xml whitespace +```xml :whitespace Everyday Italian @@ -39,10 +39,31 @@ **输出:** -```xml whitespace +```xml :whitespace :no-line-numbers Everyday Italian ``` + +渲染所有的空格: + +**输入:** + +```` +```js :whitespace=all +function foo( ) { + return 'Hello World' +} +``` +``` +```` + +**输出:** + +```js :whitespace=all :no-line-numbers +function foo( ) { + return 'Hello World' +} +```