feat(plugin-shikiji): add support for collapsed lines

This commit is contained in:
pengzhanbo 2024-08-14 03:55:49 +08:00
parent 48a6596297
commit bb4ee6bb2d
9 changed files with 155 additions and 12 deletions

View File

@ -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' })
}
}
})
}

View File

@ -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 `<code>` attributes
result = result.replace(/<code[\s\S]*?>/, '<code>')
result = `<pre class="${languageClass}"${result.slice('<pre'.length)}`
result = `<pre class="${classes.join(' ')}"${result.slice('<pre'.length)}`
return result
}
const attrs: string[] = [
`data-ext="${lang}"`,
`data-title="${title}"`,
]
const collapsed = resolveCollapsedLines(info, collapsedLines)
if (collapsed) {
classes.push('has-collapsed', 'collapsed')
attrs.push(`style="--vp-collapsed-lines:${collapsed}"`)
result += `<div class="collapsed-lines"></div>`
}
return `<div class="${languageClass}" data-ext="${lang}" data-title="${title}">${result}</div>`
return `<div class="${classes.join(' ')}" ${attrs.join(' ')}>${result}</div>`
}
}

View File

@ -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()
},`
: ''}
}

View File

@ -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<PreWrapperOptions>(preWrapperPlugin, {
md.use(preWrapperPlugin, {
preWrapper,
collapsedLines,
})
if (preWrapper) {
copyCodeButtonPlugin(md, app, copyCode)
md.use<LineNumberOptions>(lineNumberPlugin, { lineNumbers })

View File

@ -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
}
/**

View File

@ -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
}

View File

@ -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'

View File

@ -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$/],

View File

@ -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: "<color>";
}
.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));
}
}