Merge pull request #139 from pengzhanbo/code-block-collapsed-lines

Add code block support for collapsed lines
This commit is contained in:
pengzhanbo 2024-08-14 10:28:26 +08:00 committed by GitHub
commit 720cde6573
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 575 additions and 214 deletions

View File

@ -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()
}
```
<!-- @include: ../../snippet/whitespace.snippet.md{18-24} -->
### collapseLines
- 类型: `boolean | number`
- 默认值: `false`
将代码块折叠到指定行数。

View File

@ -309,6 +309,8 @@ console.log(options.foo) // 这个不会被高亮显示
将空白字符Tab 和空格)渲染为可见状态。
在 代码块 后面添加 `:whitespace`
<!-- @include: ../../snippet/whitespace.snippet.md -->
还可以在 `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` 功能。

View File

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

View File

@ -3,7 +3,7 @@
**输入:**
````
```xml whitespace
```xml :whitespace
<catalog>
<book>
<title>Everyday Italian</title>
@ -15,7 +15,7 @@
**输出:**
```xml whitespace
```xml :whitespace :no-line-numbers
<catalog>
<book>
<title>Everyday Italian</title>
@ -28,7 +28,7 @@
**输入:**
````
```xml whitespace
```xml :whitespace
<catalog>
<book>
<title>Everyday Italian</title>
@ -39,10 +39,31 @@
**输出:**
```xml whitespace
```xml :whitespace :no-line-numbers
<catalog>
<book>
<title>Everyday Italian</title>
</book>
</catalog>
```
渲染所有的空格:
**输入:**
````
```js :whitespace=all
function foo( ) {
return 'Hello World'
}
```
```
````
**输出:**
```js :whitespace=all :no-line-numbers
function foo( ) {
return 'Hello World'
}
```

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

@ -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<string, string>()
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, '&#123;')
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
}
}
}

View File

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

View File

@ -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<string, 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: Map<string, string>, twoslash: boolean) => {
mustaches.forEach((marker, match) => {
s = s.replaceAll(marker, match)
})
if (twoslash)
s = s.replace(/\{/g, '&#123;')
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<string, string>()
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
}
}
}

View File

@ -0,0 +1 @@
export * from './highlight.js'

View File

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

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

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

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));
}
}