diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index 179dee2ac2..fe18e2022f 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -114,6 +114,7 @@ export default defineUserConfig({ notationHighlight: true, notationWordHighlight: true, whitespace: true, + collapsedLines: false, }) : [], cachePlugin(), diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts index 5efe61f425..d3244a2f84 100644 --- a/docs/.vuepress/theme.ts +++ b/docs/.vuepress/theme.ts @@ -81,6 +81,7 @@ export default defaultTheme({ notationHighlight: true, notationWordHighlight: true, whitespace: true, + collapsedLines: false, }, }, }) diff --git a/docs/plugins/markdown/prismjs.md b/docs/plugins/markdown/prismjs.md index 07f01d0663..d4fc8c2502 100644 --- a/docs/plugins/markdown/prismjs.md +++ b/docs/plugins/markdown/prismjs.md @@ -194,6 +194,207 @@ export default defineUserConfig({ }) ``` +### collapsedLines + +- Type: `boolean | number | 'disabled'` + +- Default: `'disabled'` + +- Details: Default behavior of code block collapsing. + + - `number`: collapse the code block starting from line `number` by default, for example, `12` means collapsing the code block starting from line 12. + - `true`: Equivalent to `15`, collapsing the code block starting from line 15 by default. + - `false`: Add support for code block collapsing, but disable it globally + - `'disabled'`: Completely disable code block collapsing, `:collapsed-lines` will not take effect. + + To override global settings, you can add the `:collapsed-lines` / `:no-collapsed-lines` marker to the code block. You can also add `=` after `:collapsed-lines` to customize the starting line number being collapsed, for example, `:collapsed-lines=12` means collapsing the code block starting from line 12. + +**Input:** + +````md + + +```css :collapsed-lines +html { + margin: 0; + background: black; + height: 100%; +} +/* ... more code */ +``` + + + +```css :no-collapsed-lines +html { + margin: 0; + background: black; + height: 100%; +} +/* ... more code */ +``` + + + +```css :collapsed-lines=10 +html { + margin: 0; + background: black; + height: 100%; +} +/* ... more code */ +``` +```` + +**Output:** + + + +```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; +} +``` + + + +```css :no-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; +} +``` + + + +```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; +} +``` + ::: tip In the new version, some functionalities similar to [shiki](https://shiki.style/packages/transformers) have been implemented, allowing you to style code blocks using the same syntax. @@ -501,7 +702,7 @@ In the new version, some functionalities similar to [shiki](https://shiki.style/ Adds extra wrapper outside `
` tag or not. - The wrapper is required by the `lineNumbers`. That means, if you disable `preWrapper`, the line line numbers will also be disabled. + The wrapper is required by the `lineNumbers` and `collapsedLines`. That means, if you disable `preWrapper`, the line line numbers and collapsed lines will also be disabled. ::: tip diff --git a/docs/plugins/markdown/shiki.md b/docs/plugins/markdown/shiki.md index f8ddfc2d25..563a112daf 100644 --- a/docs/plugins/markdown/shiki.md +++ b/docs/plugins/markdown/shiki.md @@ -190,6 +190,207 @@ export default defineUserConfig({ }) ``` +### collapsedLines + +- Type: `boolean | number | 'disabled'` + +- Default: `'disabled'` + +- Details: Default behavior of code block collapsing. + + - `number`: collapse the code block starting from line `number` by default, for example, `12` means collapsing the code block starting from line 12. + - `true`: Equivalent to `15`, collapsing the code block starting from line 15 by default. + - `false`: Add support for code block collapsing, but disable it globally + - `'disabled'`: Completely disable code block collapsing, `:collapsed-lines` will not take effect. + + To override global settings, you can add the `:collapsed-lines` / `:no-collapsed-lines` marker to the code block. You can also add `=` after `:collapsed-lines` to customize the starting line number being collapsed, for example, `:collapsed-lines=12` means collapsing the code block starting from line 12. + +**Input:** + +````md + + +```css :collapsed-lines +html { + margin: 0; + background: black; + height: 100%; +} +/* ... more code */ +``` + + + +```css :no-collapsed-lines +html { + margin: 0; + background: black; + height: 100%; +} +/* ... more code */ +``` + + + +```css :collapsed-lines=10 +html { + margin: 0; + background: black; + height: 100%; +} +/* ... more code */ +``` +```` + +**Output:** + + + +```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; +} +``` + + + +```css :no-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; +} +``` + + + +```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; +} +``` + ### notationDiff - Type: `boolean` @@ -501,7 +702,7 @@ export default defineUserConfig({ Adds extra wrapper outside `` tag or not. - The wrapper is required by the `lineNumbers`. That means, if you disable `preWrapper`, the line line numbers will also be disabled. + The wrapper is required by the `lineNumbers` and `collapsedLines`. That means, if you disable `preWrapper`, the line line numbers and collapsed lines will also be disabled. ### shikiSetup diff --git a/docs/zh/plugins/markdown/prismjs.md b/docs/zh/plugins/markdown/prismjs.md index 282f9dfabb..741553b901 100644 --- a/docs/zh/plugins/markdown/prismjs.md +++ b/docs/zh/plugins/markdown/prismjs.md @@ -194,6 +194,207 @@ export default defineUserConfig({ }) ``` +### collapsedLines + +- 类型:`boolean | number | 'disabled'` + +- 默认值:`'disabled` + +- 详情:代码块折叠的默认行为。 + + - `number`: 从第 `number` 行开始折叠代码块,例如,`12` 表示从第 12 行开始折叠代码块。 + - `true`: 等同于 `15`, 从第 15 行开始折叠代码块。 + - `false`: 添加代码块折叠支持,但全局禁用此功能。 + - `'disabled'`: 完全禁用代码块折叠, `:collapsed-lines` 标记不会生效。 + + 你可以在代码块添加 `:collapsed-lines` / `:no-collapsed-lines` 标记来覆盖配置项中的设置。还可以在 `:collapsed-lines` 之后添加 `=` 来自定义起始折叠行号,例如 `:collapsed-lines=12` 表示代码块从第 12 行开始折叠。 + +**输入:** + +````md + + +```css :collapsed-lines +html { + margin: 0; + background: black; + height: 100%; +} +/* ... 更多代码 */ +``` + + + +```css :no-collapsed-lines +html { + margin: 0; + background: black; + height: 100%; +} +/* ... 更多代码 */ +``` + + + +```css :collapsed-lines=10 +html { + margin: 0; + background: black; + height: 100%; +} +/* ... 更多代码 */ +``` +```` + +**输出:** + + + +```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; +} +``` + + + +```css :no-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; +} +``` + + + +```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; +} +``` + ::: tip 在新的版本中,实现了类似于 [shiki](https://shiki.style/packages/transformers) 的部分功能, @@ -502,7 +703,7 @@ export default defineUserConfig({ 是否在 `` 标签外添加包裹容器。 - `lineNumbers` 依赖于这个额外的包裹层。这换句话说,如果你禁用了 `preWrapper` ,那么行号也会被同时禁用。 + `lineNumbers` 和 `collapsedLines` 依赖于这个额外的包裹层。这换句话说,如果你禁用了 `preWrapper` ,那么行号和折叠代码块也会被同时禁用。 ::: tip diff --git a/docs/zh/plugins/markdown/shiki.md b/docs/zh/plugins/markdown/shiki.md index 910af18058..ae21a41b63 100644 --- a/docs/zh/plugins/markdown/shiki.md +++ b/docs/zh/plugins/markdown/shiki.md @@ -192,6 +192,207 @@ export default defineUserConfig({ }) ``` +### collapsedLines + +- 类型:`boolean | number | 'disabled'` + +- 默认值:`'disabled'` + +- 详情:代码块折叠的默认行为。 + + - `number`: 从第 `number` 行开始折叠代码块,例如,`12` 表示从第 12 行开始折叠代码块。 + - `true`: 等同于 `15`, 从第 15 行开始折叠代码块。 + - `false`: 添加代码块折叠支持,但全局禁用此功能 + - `'disabled'`: 完全禁用代码块折叠, `:collapsed-lines` 标记不会生效。 + + 你可以在代码块添加 `:collapsed-lines` / `:no-collapsed-lines` 标记来覆盖配置项中的设置。还可以在 `:collapsed-lines` 之后添加 `=` 来自定义起始折叠行号,例如 `:collapsed-lines=12` 表示代码块从第 12 行开始折叠。 + +**输入:** + +````md + + +```css :collapsed-lines +html { + margin: 0; + background: black; + height: 100%; +} +/* ... 更多代码 */ +``` + + + +```css :no-collapsed-lines +html { + margin: 0; + background: black; + height: 100%; +} +/* ... 更多代码 */ +``` + + + +```css :collapsed-lines=10 +html { + margin: 0; + background: black; + height: 100%; +} +/* ... 更多代码 */ +``` +```` + +**输出:** + + + +```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; +} +``` + + + +```css :no-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; +} +``` + + + +```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; +} +``` + ### notationDiff - 类型:`boolean` @@ -503,7 +704,7 @@ export default defineUserConfig({ 是否在 `` 标签外添加包裹容器。 - `lineNumbers` 依赖于这个额外的包裹层。这换句话说,如果你禁用了 `preWrapper` ,那么行号也会被同时禁用。 + `lineNumbers` 和 `collapsedLines` 依赖于这个额外的包裹层。这换句话说,如果你禁用了 `preWrapper` ,那么行号和折叠代码块也会被同时禁用。 ### shikiSetup diff --git a/plugins/markdown/plugin-prismjs/src/node/options.ts b/plugins/markdown/plugin-prismjs/src/node/options.ts index cf1f5d05b9..18f3823b4e 100644 --- a/plugins/markdown/plugin-prismjs/src/node/options.ts +++ b/plugins/markdown/plugin-prismjs/src/node/options.ts @@ -1,4 +1,7 @@ -import type { MarkdownItLineNumbersOptions } from '@vuepress/highlighter-helper' +import type { + MarkdownItCollapsedLinesOptions, + MarkdownItLineNumbersOptions, +} from '@vuepress/highlighter-helper' import type { HighlightOptions, PreWrapperOptions } from './types.js' export type PrismjsLightTheme = @@ -47,6 +50,7 @@ export type PrismjsTheme = PrismjsDarkTheme | PrismjsLightTheme */ export interface PrismjsPluginOptions extends Pick, + Pick , PreWrapperOptions, HighlightOptions { /** diff --git a/plugins/markdown/plugin-prismjs/src/node/prepareConfigFile.ts b/plugins/markdown/plugin-prismjs/src/node/prepareConfigFile.ts index 37614d8037..ade24f29a9 100644 --- a/plugins/markdown/plugin-prismjs/src/node/prepareConfigFile.ts +++ b/plugins/markdown/plugin-prismjs/src/node/prepareConfigFile.ts @@ -11,6 +11,7 @@ export const prepareConfigFile = ( theme, themes, lineNumbers = true, + collapsedLines, notationDiff, notationErrorLevel, notationFocus, @@ -25,6 +26,8 @@ export const prepareConfigFile = ( `import "${getRealPath('@vuepress/highlighter-helper/styles/base.css', url)}"`, ] + const setups: string[] = [] + if (light === dark) { imports.push( `import "${getRealPath(`@vuepress/plugin-prismjs/styles/${light}.css`, url)}"`, @@ -78,5 +81,24 @@ export const prepareConfigFile = ( ) } - return app.writeTemp('prismjs/config.js', imports.join('\n')) + if (collapsedLines !== 'disabled') { + imports.push( + `import "${getRealPath('@vuepress/highlighter-helper/styles/collapsed-lines.css', url)}"`, + `import { setupCollapsedLines } from "${getRealPath('@vuepress/highlighter-helper/composables/collapsedLines.js', url)}"`, + ) + setups.push('setupCollapsedLines()') + } + + let code = imports.join('\n') + + if (setups.length) { + code += `\n +export default { + setup() { + ${setups.join('\n ')} + } +}\n` + } + + return app.writeTemp('prismjs/config.js', code) } diff --git a/plugins/markdown/plugin-prismjs/src/node/prismjsPlugin.ts b/plugins/markdown/plugin-prismjs/src/node/prismjsPlugin.ts index dabbfcbcee..0836915390 100644 --- a/plugins/markdown/plugin-prismjs/src/node/prismjsPlugin.ts +++ b/plugins/markdown/plugin-prismjs/src/node/prismjsPlugin.ts @@ -1,36 +1,47 @@ -import { lineNumbers as lineNumbersPlugin } from '@vuepress/highlighter-helper' +import { + collapsedLines as collapsedLinesPlugin, + lineNumbers as lineNumbersPlugin, +} from '@vuepress/highlighter-helper' import type { Plugin } from 'vuepress/core' import { loadLanguages } from './loadLanguages.js' import { highlightPlugin, preWrapperPlugin } from './markdown/index.js' import type { PrismjsPluginOptions } from './options.js' import { prepareConfigFile } from './prepareConfigFile.js' import { resolveHighlighter } from './resolveHighlighter.js' -import type { HighlightOptions, PreWrapperOptions } from './types.js' -export const prismjsPlugin = ({ - preloadLanguages = ['markdown', 'jsdoc', 'yaml'], - preWrapper = true, - lineNumbers = true, - ...options -}: PrismjsPluginOptions = {}): Plugin => ({ - name: '@vuepress/plugin-prismjs', +export const prismjsPlugin = (options: PrismjsPluginOptions = {}): Plugin => { + const opt: PrismjsPluginOptions = { + preloadLanguages: ['markdown', 'jsdoc', 'yaml'], + preWrapper: true, + lineNumbers: true, + collapsedLines: false, + ...options, + } - extendsMarkdown(md) { - if (preloadLanguages.length !== 0) { - loadLanguages(preloadLanguages) - } + return { + name: '@vuepress/plugin-prismjs', - md.options.highlight = (code, lang) => { - const highlighter = resolveHighlighter(lang) - return highlighter?.(code) || '' - } + extendsMarkdown(md) { + const { preloadLanguages, preWrapper, lineNumbers, collapsedLines } = opt - md.use (highlightPlugin, options) - md.use (preWrapperPlugin, { preWrapper }) - if (preWrapper) { - md.use(lineNumbersPlugin, { lineNumbers, removeLastLine: true }) - } - }, + if (preloadLanguages?.length) { + loadLanguages(preloadLanguages) + } - clientConfigFile: (app) => prepareConfigFile(app, options), -}) + md.options.highlight = (code, lang) => { + const highlighter = resolveHighlighter(lang) + return highlighter?.(code) || '' + } + + md.use(highlightPlugin, opt) + md.use(preWrapperPlugin, { preWrapper }) + if (preWrapper) { + md.use(lineNumbersPlugin, { lineNumbers, removeLastLine: true }) + if (collapsedLines !== 'disabled') + md.use(collapsedLinesPlugin, { collapsedLines, removeLastLine: true }) + } + }, + + clientConfigFile: (app) => prepareConfigFile(app, opt), + } +} diff --git a/plugins/markdown/plugin-prismjs/tests/__snapshots__/prismjs-preWrapper.spec.ts.snap b/plugins/markdown/plugin-prismjs/tests/__snapshots__/prismjs-preWrapper.spec.ts.snap index dfe5bd8817..16fba21722 100644 --- a/plugins/markdown/plugin-prismjs/tests/__snapshots__/prismjs-preWrapper.spec.ts.snap +++ b/plugins/markdown/plugin-prismjs/tests/__snapshots__/prismjs-preWrapper.spec.ts.snap @@ -1,5 +1,302 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`@vuepress/plugin-prismjs > markdown fence preWrapper > :collapsed-lines / :no-collapsed-lines / :collapsed-lines=[number] > should work properly if \`collapsedLines\` is disabled by default 1`] = ` +" +const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20 +
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20 +
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20 +
" +`; + +exports[`@vuepress/plugin-prismjs > markdown fence preWrapper > :collapsed-lines / :no-collapsed-lines / :collapsed-lines=[number] > should work properly if \`collapsedLines\` is enabled 1`] = ` +"+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20 +
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20 +
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20 +
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20 +
" +`; + +exports[`@vuepress/plugin-prismjs > markdown fence preWrapper > :collapsed-lines / :no-collapsed-lines / :collapsed-lines=[number] > should work properly if \`collapsedLines\` is set to a number 1`] = ` +"+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20 +
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20 +
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20 +
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20 +
" +`; + exports[`@vuepress/plugin-prismjs > markdown fence preWrapper > :line-numbers / :no-line-numbers > should work properly if \`lineNumbers\` is disabled by default 1`] = ` "+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20 +
diff --git a/plugins/markdown/plugin-prismjs/tests/prismjs-preWrapper.spec.ts b/plugins/markdown/plugin-prismjs/tests/prismjs-preWrapper.spec.ts index 609c562dfc..2831b08a28 100644 --- a/plugins/markdown/plugin-prismjs/tests/prismjs-preWrapper.spec.ts +++ b/plugins/markdown/plugin-prismjs/tests/prismjs-preWrapper.spec.ts @@ -1,4 +1,7 @@ -import { lineNumbers as lineNumbersPlugin } from '@vuepress/highlighter-helper' +import { + collapsedLines as collapsedLinesPlugin, + lineNumbers as lineNumbersPlugin, +} from '@vuepress/highlighter-helper' import MarkdownIt from 'markdown-it' import { describe, expect, it, vi } from 'vitest' import type { @@ -14,6 +17,7 @@ const codeFence = '```' const createMarkdown = ({ preWrapper = true, lineNumbers = true, + collapsedLines = false, ...options }: PrismjsPluginOptions = {}): MarkdownIt => { const md = MarkdownIt() @@ -25,10 +29,8 @@ const createMarkdown = ({ md.useRaw text
(highlightPlugin, options) md.use (preWrapperPlugin, { preWrapper }) if (preWrapper) { - md.use(lineNumbersPlugin, { - lineNumbers, - removeLastLine: true, - }) + md.use(lineNumbersPlugin, { lineNumbers, removeLastLine: true }) + md.use(collapsedLinesPlugin, { collapsedLines, removeLastLine: true }) } return md } @@ -123,6 +125,21 @@ ${codeFence}{{ inlineCode }}${codeFence} mdWithoutLineNumbers.render(source), ) }) + + it('should always disable `collapsedLines` if `preWrapper` is disabled', () => { + const mdWithCollapsedLines = createMarkdown({ + collapsedLines: 3, + preWrapper: false, + }) + const mdWithoutCollapsedLines = createMarkdown({ + collapsedLines: false, + preWrapper: false, + }) + + expect(mdWithCollapsedLines.render(source)).toBe( + mdWithoutCollapsedLines.render(source), + ) + }) }) describe(':line-numbers / :no-line-numbers', () => { @@ -392,4 +409,47 @@ function foo () { expect(md.render(source)).toMatchSnapshot() }) }) + + describe(':collapsed-lines / :no-collapsed-lines / :collapsed-lines=[number]', () => { + const genLines = (length: number): string => + Array.from({ length }) + .map((_, i) => `const line${i + 1} = 'line ${i + 1}`) + .join('\n') + + const source = `\ +${codeFence}ts +${genLines(10)} +${codeFence} + +${codeFence}ts +${genLines(20)} +${codeFence} + +${codeFence}ts :collapsed-lines +${genLines(20)} +${codeFence} + +${codeFence}ts :no-collapsed-lines +${genLines(20)} +${codeFence} + +${codeFence}ts :no-collapsed-lines=12 +${genLines(20)} +${codeFence} +` + it('should work properly if `collapsedLines` is disabled by default', () => { + const md = createMarkdown({ collapsedLines: false }) + expect(md.render(source)).toMatchSnapshot() + }) + + it('should work properly if `collapsedLines` is enabled', () => { + const md = createMarkdown({ collapsedLines: true }) + expect(md.render(source)).toMatchSnapshot() + }) + + it('should work properly if `collapsedLines` is set to a number', () => { + const md = createMarkdown({ collapsedLines: 10 }) + expect(md.render(source)).toMatchSnapshot() + }) + }) }) diff --git a/plugins/markdown/plugin-shiki/src/node/options.ts b/plugins/markdown/plugin-shiki/src/node/options.ts index 05af116da2..dffeec2ee2 100644 --- a/plugins/markdown/plugin-shiki/src/node/options.ts +++ b/plugins/markdown/plugin-shiki/src/node/options.ts @@ -1,12 +1,16 @@ -import type { MarkdownItLineNumbersOptions } from '@vuepress/highlighter-helper' +import type { + MarkdownItCollapsedLinesOptions, + MarkdownItLineNumbersOptions, +} from '@vuepress/highlighter-helper' import type { PreWrapperOptions, ShikiHighlightOptions } from './types.js' /** * Options of @vuepress/plugin-shiki */ export type ShikiPluginOptions = Pick< - MarkdownItLineNumbersOptions, - 'lineNumbers' + MarkdownItCollapsedLinesOptions, + 'collapsedLines' > & + Pick & PreWrapperOptions & ShikiHighlightOptions diff --git a/plugins/markdown/plugin-shiki/src/node/prepareConfigFile.ts b/plugins/markdown/plugin-shiki/src/node/prepareConfigFile.ts index af32c43bde..34aae7772d 100644 --- a/plugins/markdown/plugin-shiki/src/node/prepareConfigFile.ts +++ b/plugins/markdown/plugin-shiki/src/node/prepareConfigFile.ts @@ -9,6 +9,7 @@ export const prepareConfigFile = ( app: App, { lineNumbers = true, + collapsedLines, notationDiff, notationErrorLevel, notationFocus, @@ -22,6 +23,8 @@ export const prepareConfigFile = ( `import "${getRealPath(`${PLUGIN_NAME}/styles/shiki.css`, import.meta.url)}"`, ] + const setups: string[] = [] + if (lineNumbers) { imports.push( `import "${getRealPath('@vuepress/highlighter-helper/styles/line-numbers.css', url)}"`, @@ -64,5 +67,24 @@ export const prepareConfigFile = ( ) } - return app.writeTemp('shiki/config.js', imports.join('\n')) + if (collapsedLines !== 'disabled') { + imports.push( + `import "${getRealPath('@vuepress/highlighter-helper/styles/collapsed-lines.css', url)}"`, + `import { setupCollapsedLines } from "${getRealPath('@vuepress/highlighter-helper/composables/collapsedLines.js', url)}"`, + ) + setups.push('setupCollapsedLines()') + } + + let code = imports.join('\n') + + if (setups.length) { + code += `\n +export default { + setup() { + ${setups.join('\n ')} + } +}\n` + } + + return app.writeTemp('shiki/config.js', code) } diff --git a/plugins/markdown/plugin-shiki/src/node/shikiPlugin.ts b/plugins/markdown/plugin-shiki/src/node/shikiPlugin.ts index 56ee66cd5d..53d9089a5a 100644 --- a/plugins/markdown/plugin-shiki/src/node/shikiPlugin.ts +++ b/plugins/markdown/plugin-shiki/src/node/shikiPlugin.ts @@ -1,4 +1,7 @@ -import { lineNumbers as lineNumbersPlugin } from '@vuepress/highlighter-helper' +import { + collapsedLines as collapsedLinesPlugin, + lineNumbers as lineNumbersPlugin, +} from '@vuepress/highlighter-helper' import type { Plugin } from 'vuepress/core' import { isPlainObject } from 'vuepress/shared' import { @@ -9,29 +12,38 @@ import { import type { ShikiPluginOptions } from './options.js' import { prepareConfigFile } from './prepareConfigFile.js' -export const shikiPlugin = ({ - preWrapper = true, - lineNumbers = true, - ...options -}: ShikiPluginOptions = {}): Plugin => ({ - name: '@vuepress/plugin-shiki', +export const shikiPlugin = (options: ShikiPluginOptions = {}): Plugin => { + const opt: ShikiPluginOptions = { + preWrapper: true, + lineNumbers: true, + collapsedLines: 'disabled', + ...options, + } + + return { + name: '@vuepress/plugin-shiki', + + extendsMarkdown: async (md, app) => { + // FIXME: Remove in stable version + // eslint-disable-next-line @typescript-eslint/no-deprecated + const { code } = app.options.markdown - extendsMarkdown: async (md, app) => { - // FIXME: Remove in stable version - // eslint-disable-next-line @typescript-eslint/no-deprecated - const { code } = app.options.markdown + await applyHighlighter(md, app, { + ...(isPlainObject(code) ? code : {}), + ...options, + }) - await applyHighlighter(md, app, { - ...(isPlainObject(code) ? code : {}), - ...options, - }) + const { preWrapper, lineNumbers, collapsedLines } = opt - md.use(highlightLinesPlugin) - md.use(preWrapperPlugin, { preWrapper }) - if (preWrapper) { - md.use(lineNumbersPlugin, { lineNumbers }) - } - }, + md.use(highlightLinesPlugin) + md.use(preWrapperPlugin, { preWrapper }) + if (preWrapper) { + md.use(lineNumbersPlugin, { lineNumbers }) + if (collapsedLines !== 'disabled') + md.use(collapsedLinesPlugin, { collapsedLines }) + } + }, - clientConfigFile: (app) => prepareConfigFile(app, options), -}) + clientConfigFile: (app) => prepareConfigFile(app, opt), + } +} diff --git a/plugins/markdown/plugin-shiki/tests/__snapshots__/shiki-preWrapper.spec.ts.snap b/plugins/markdown/plugin-shiki/tests/__snapshots__/shiki-preWrapper.spec.ts.snap index 6e190a85db..5f0f2a3966 100644 --- a/plugins/markdown/plugin-shiki/tests/__snapshots__/shiki-preWrapper.spec.ts.snap +++ b/plugins/markdown/plugin-shiki/tests/__snapshots__/shiki-preWrapper.spec.ts.snap @@ -1,5 +1,287 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`@vuepress/plugin-shiki > fence preWrapper > :collapsed-lines / :no-collapsed-lines / :collapsed-lines=[number] > should work properly if \`collapsedLines\` is disabled by default 1`] = ` +" +const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20
" +`; + +exports[`@vuepress/plugin-shiki > fence preWrapper > :collapsed-lines / :no-collapsed-lines / :collapsed-lines=[number] > should work properly if \`collapsedLines\` is enabled 1`] = ` +"+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20
" +`; + +exports[`@vuepress/plugin-shiki > fence preWrapper > :collapsed-lines / :no-collapsed-lines / :collapsed-lines=[number] > should work properly if \`collapsedLines\` is set to a number 1`] = ` +"+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20
+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20
" +`; + exports[`@vuepress/plugin-shiki > fence preWrapper > :line-numbers / :no-line-numbers > should work properly if \`lineNumbers\` is disabled by default 1`] = ` "+const line1 = 'line 1 +const line2 = 'line 2 +const line3 = 'line 3 +const line4 = 'line 4 +const line5 = 'line 5 +const line6 = 'line 6 +const line7 = 'line 7 +const line8 = 'line 8 +const line9 = 'line 9 +const line10 = 'line 10 +const line11 = 'line 11 +const line12 = 'line 12 +const line13 = 'line 13 +const line14 = 'line 14 +const line15 = 'line 15 +const line16 = 'line 16 +const line17 = 'line 17 +const line18 = 'line 18 +const line19 = 'line 19 +const line20 = 'line 20
Raw text
`) + .replace(/"(language-[^"]*?)"/, '"$1 has-collapsed-lines collapsed"') + .replace(/^diff --git a/plugins/markdown/plugin-shiki/tests/shiki-preWrapper.spec.ts b/plugins/markdown/plugin-shiki/tests/shiki-preWrapper.spec.ts index af5dd701da..5a13761125 100644 --- a/plugins/markdown/plugin-shiki/tests/shiki-preWrapper.spec.ts +++ b/plugins/markdown/plugin-shiki/tests/shiki-preWrapper.spec.ts @@ -1,5 +1,11 @@ -import type { MarkdownItLineNumbersOptions } from '@vuepress/highlighter-helper' -import { lineNumbers as lineNumbersPlugin } from '@vuepress/highlighter-helper' +import type { + MarkdownItCollapsedLinesOptions, + MarkdownItLineNumbersOptions, +} from '@vuepress/highlighter-helper' +import { + collapsedLines as collapsedLinesPlugin, + lineNumbers as lineNumbersPlugin, +} from '@vuepress/highlighter-helper' import MarkdownIt from 'markdown-it' import { describe, expect, it } from 'vitest' import type { App } from 'vuepress' @@ -16,8 +22,10 @@ import type { const createMarkdown = async ({ preWrapper = true, lineNumbers = true, + collapsedLines = false, ...options -}: MarkdownItLineNumbersOptions & +}: MarkdownItCollapsedLinesOptions & + MarkdownItLineNumbersOptions & PreWrapperOptions & ShikiHighlightOptions = {}): PromiseRaw text
=> { const md = MarkdownIt() @@ -25,9 +33,11 @@ const createMarkdown = async ({ await applyHighlighter(md, { env: { isDebug: false } } as App, options) md.use(highlightLinesPlugin) - md.use (preWrapperPlugin, { preWrapper }) + md.use(preWrapperPlugin, { preWrapper }) if (preWrapper) { - md.use (lineNumbersPlugin, { lineNumbers }) + md.use(lineNumbersPlugin, { lineNumbers }) + if (collapsedLines !== 'disabled') + md.use(collapsedLinesPlugin, { collapsedLines }) } return md } @@ -128,6 +138,21 @@ ${codeFence}{{ inlineCode }}${codeFence} mdWithoutLineNumbers.render(source), ) }) + + it('should always disable `collapsedLines` if `preWrapper` is disabled', async () => { + const mdWithCollapsedLines = await createMarkdown({ + collapsedLines: 3, + preWrapper: false, + }) + const mdWithoutCollapsedLines = await createMarkdown({ + collapsedLines: 'disabled', + preWrapper: false, + }) + + expect(mdWithCollapsedLines.render(source)).toBe( + mdWithoutCollapsedLines.render(source), + ) + }) }) describe(':line-numbers / :no-line-numbers', () => { @@ -356,4 +381,47 @@ function foo () { expect(md.render(source)).toMatchSnapshot() }) }) + + describe(':collapsed-lines / :no-collapsed-lines / :collapsed-lines=[number]', () => { + const genLines = (length: number): string => + Array.from({ length }) + .map((_, i) => `const line${i + 1} = 'line ${i + 1}`) + .join('\n') + + const source = `\ +${codeFence}ts +${genLines(10)} +${codeFence} + +${codeFence}ts +${genLines(20)} +${codeFence} + +${codeFence}ts :collapsed-lines +${genLines(20)} +${codeFence} + +${codeFence}ts :no-collapsed-lines +${genLines(20)} +${codeFence} + +${codeFence}ts :no-collapsed-lines=12 +${genLines(20)} +${codeFence} +` + it('should work properly if `collapsedLines` is disabled by default', async () => { + const md = await createMarkdown({ collapsedLines: false }) + expect(md.render(source)).toMatchSnapshot() + }) + + it('should work properly if `collapsedLines` is enabled', async () => { + const md = await createMarkdown({ collapsedLines: true }) + expect(md.render(source)).toMatchSnapshot() + }) + + it('should work properly if `collapsedLines` is set to a number', async () => { + const md = await createMarkdown({ collapsedLines: 10 }) + expect(md.render(source)).toMatchSnapshot() + }) + }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b2ee84db0..93a77c552e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1001,6 +1001,9 @@ importers: tools/highlighter-helper: dependencies: + '@vueuse/core': + specifier: ^11.0.0 + version: 11.0.3(vue@3.5.4(typescript@5.6.2)) vuepress: specifier: 2.0.0-rc.15 version: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.15(@types/node@22.5.4)(jiti@1.21.6)(lightningcss@1.27.0)(sass-embedded@1.78.0)(sass@1.78.0)(terser@5.32.0)(tsx@4.19.0)(typescript@5.6.2)(yaml@2.4.5))(@vuepress/bundler-webpack@2.0.0-rc.15(typescript@5.6.2))(typescript@5.6.2)(vue@3.5.4(typescript@5.6.2)) @@ -2635,55 +2638,46 @@ packages: resolution: {integrity: sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.21.2': resolution: {integrity: sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.21.2': resolution: {integrity: sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.21.2': resolution: {integrity: sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-powerpc64le-gnu@4.21.2': resolution: {integrity: sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.21.2': resolution: {integrity: sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.21.2': resolution: {integrity: sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.21.2': resolution: {integrity: sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.21.2': resolution: {integrity: sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.21.2': resolution: {integrity: sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==} @@ -5282,28 +5276,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.27.0: resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.27.0: resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.27.0: resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.27.0: resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==} diff --git a/tools/highlighter-helper/package.json b/tools/highlighter-helper/package.json index 9ccf32e5b9..9ebb06519d 100644 --- a/tools/highlighter-helper/package.json +++ b/tools/highlighter-helper/package.json @@ -25,6 +25,7 @@ "type": "module", "exports": { ".": "./lib/node/index.js", + "./composables/*": "./lib/client/composables/*", "./styles/*": "./lib/client/styles/*", "./package.json": "./package.json" }, @@ -39,7 +40,13 @@ "style": "sass src:lib --no-source-map" }, "peerDependencies": { - "vuepress": "2.0.0-rc.15" + "vuepress": "2.0.0-rc.15", + "@vueuse/core": "^11.0.0" + }, + "peerDependenciesMeta": { + "@vueuse/core": { + "optional": true + } }, "publishConfig": { "access": "public" diff --git a/tools/highlighter-helper/src/client/composables/collapsedLines.ts b/tools/highlighter-helper/src/client/composables/collapsedLines.ts new file mode 100644 index 0000000000..c21d44d20b --- /dev/null +++ b/tools/highlighter-helper/src/client/composables/collapsedLines.ts @@ -0,0 +1,15 @@ +import { useEventListener } from '@vueuse/core' + +export const setupCollapsedLines = ({ + selector = 'div[class*="language-"].has-collapsed-lines > .collapsed-lines', +}: { selector?: string } = {}): void => { + useEventListener('click', (e) => { + const target = e.target as HTMLElement + if (target.matches(selector)) { + const parent = target.parentElement + if (parent?.classList.toggle('collapsed')) { + parent.scrollIntoView({ block: 'center', behavior: 'instant' }) + } + } + }) +} diff --git a/tools/highlighter-helper/src/client/styles/base.scss b/tools/highlighter-helper/src/client/styles/base.scss index 06bf57f657..634573ad40 100644 --- a/tools/highlighter-helper/src/client/styles/base.scss +++ b/tools/highlighter-helper/src/client/styles/base.scss @@ -4,6 +4,7 @@ --code-padding-y: 1rem; --code-border-radius: 6px; --code-line-height: 1.6; + --code-font-size: 14px; --code-font-family: consolas, monaco, 'Andale Mono', 'Ubuntu Mono', monospace; } @@ -31,10 +32,10 @@ div[class*='language-'] { overflow-x: auto; - margin: 0.75rem 0; + margin: 0; border-radius: var(--code-border-radius); - font-size: 14px; + font-size: var(--code-font-size); font-family: var(--code-font-family); line-height: var(--code-line-height); diff --git a/tools/highlighter-helper/src/client/styles/collapsed-lines.scss b/tools/highlighter-helper/src/client/styles/collapsed-lines.scss new file mode 100644 index 0000000000..127912452a --- /dev/null +++ b/tools/highlighter-helper/src/client/styles/collapsed-lines.scss @@ -0,0 +1,104 @@ +/* stylelint-disable scss/operator-no-newline-after */ +// collapsed lines +div[class*='language-'].has-collapsed-lines { + &.collapsed { + overflow-y: hidden; + height: calc( + var(--vp-collapsed-lines) * var(--code-line-height) * + var(--code-font-size) + var(--code-padding-y) + 28px + ); + } + + .collapsed-lines { + --vp-collapsed-lines-bg: var(--code-c-bg); + + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 4; + + display: flex; + align-items: center; + justify-content: center; + + height: 28px; + + background: linear-gradient( + to bottom, + transparent 0%, + var(--vp-collapsed-lines-bg) 55%, + var(--vp-collapsed-lines-bg) 100% + ); + + cursor: pointer; + + transition: --vp-collapsed-lines-bg var(--vp-t-color); + + &:hover { + --vp-collapsed-lines-bg: rgb(0 0 0 / 10%) !important; + } + } + + &[data-highlighter='shiki'] .collapsed-lines { + --vp-collapsed-lines-bg: var(--code-c-bg, var(--shiki-light-bg)); + + [data-theme='dark'] & { + --vp-collapsed-lines-bg: var(--code-c-bg, var(--shiki-dark-bg)); + } + } + + .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"); + --vp-collapsed-lines-rotate: 0deg; + + content: ''; + + display: inline-block; + + width: 24px; + height: 24px; + + background-color: var(--code-c-text); + + mask-image: var(--icon); + mask-position: 50%; + mask-size: 20px; + mask-repeat: no-repeat; + pointer-events: none; + + animation: code-collapsed-lines 1.2s infinite alternate-reverse ease-in-out; + } + + &:not(.collapsed) { + code { + padding-bottom: max(var(--code-padding-y), 28px); + } + + .collapsed-lines:hover { + --vp-collapsed-lines-bg: transparent !important; + } + + .collapsed-lines::before { + --vp-collapsed-lines-rotate: 180deg; + } + } +} + +@property --vp-collapsed-lines-bg { + inherits: false; + initial-value: #fff; + syntax: ' '; +} + +@keyframes code-collapsed-lines { + 0% { + opacity: 0.3; + transform: translateY(-2px) rotate(var(--vp-collapsed-lines-rotate)); + } + + 100% { + opacity: 1; + transform: translateY(2px) rotate(var(--vp-collapsed-lines-rotate)); + } +} diff --git a/tools/highlighter-helper/src/client/styles/line-numbers.scss b/tools/highlighter-helper/src/client/styles/line-numbers.scss index be8b58b674..b9770ad86c 100644 --- a/tools/highlighter-helper/src/client/styles/line-numbers.scss +++ b/tools/highlighter-helper/src/client/styles/line-numbers.scss @@ -46,7 +46,7 @@ div[class*='language-'] { color: var(--code-c-line-number, var(--code-c-text)); - font-size: 0.875em; + font-size: var(--code-font-size); line-height: var(--code-line-height); text-align: center; } diff --git a/tools/highlighter-helper/src/node/collapsedLines/index.ts b/tools/highlighter-helper/src/node/collapsedLines/index.ts new file mode 100644 index 0000000000..ea93e7a3f6 --- /dev/null +++ b/tools/highlighter-helper/src/node/collapsedLines/index.ts @@ -0,0 +1,2 @@ +export * from './options.js' +export * from './plugin.js' diff --git a/tools/highlighter-helper/src/node/collapsedLines/options.ts b/tools/highlighter-helper/src/node/collapsedLines/options.ts new file mode 100644 index 0000000000..117d5314c0 --- /dev/null +++ b/tools/highlighter-helper/src/node/collapsedLines/options.ts @@ -0,0 +1,17 @@ +export interface MarkdownItCollapsedLinesOptions { + /** + * Whether to collapse code blocks when they exceed a certain number of lines, + * + * - If `number`, collapse starts from line `number`. + * - If `true`, collapse starts from line 15 by default. + * - If `false`, do not enable `collapsedLines` globally, but you can enable it for individual code blocks using `:collapsed-lines` + * - If `'disabled'`, Completely disable `collapsedLines` + * @default 'disabled' + */ + collapsedLines?: boolean | number | 'disabled' + + /** + * @default false + */ + removeLastLine?: boolean +} diff --git a/tools/highlighter-helper/src/node/collapsedLines/plugin.ts b/tools/highlighter-helper/src/node/collapsedLines/plugin.ts new file mode 100644 index 0000000000..ae82832b50 --- /dev/null +++ b/tools/highlighter-helper/src/node/collapsedLines/plugin.ts @@ -0,0 +1,56 @@ +import type { Markdown } from 'vuepress/markdown' +import type { MarkdownItCollapsedLinesOptions } from './options.js' +import { resolveCollapsedLines } from './resolveCollapsedLine.js' + +export const collapsedLines = ( + md: Markdown, + { + collapsedLines: collapsedLinesOptions = 'disabled', + removeLastLine, + }: MarkdownItCollapsedLinesOptions = {}, +): void => { + if (collapsedLinesOptions === 'disabled') return + + const rawFence = md.renderer.rules.fence! + + md.renderer.rules.fence = (...args) => { + const [tokens, index] = args + const token = tokens[index] + // get token info + const info = token.info ? md.utils.unescapeAll(token.info).trim() : '' + const code = rawFence(...args) + + // resolve collapsed-lines mark from token info + const collapsedLinesInfo = + resolveCollapsedLines(info) ?? collapsedLinesOptions + + if (collapsedLinesInfo === false) { + return code + } + + const lines = + code.slice(code.indexOf(' '), code.indexOf('
')).split('\n') + .length - (removeLastLine ? 1 : 0) + const startLines = + typeof collapsedLinesInfo === 'number' ? collapsedLinesInfo : 15 + + if (lines < startLines) { + return code + } + + const collapsedLinesCode = `` + const styles = `--vp-collapsed-lines:${startLines};` + + const finalCode = code + .replace(/<\/div>$/, `${collapsedLinesCode}]*>/, (match) => { + if (!match.includes('style=')) { + return `${match.slice(0, -1)} style="${styles}">` + } + return match.replace(/(style=")/, `$1${styles}`) + }) + + return finalCode + } +} diff --git a/tools/highlighter-helper/src/node/collapsedLines/resolveCollapsedLine.ts b/tools/highlighter-helper/src/node/collapsedLines/resolveCollapsedLine.ts new file mode 100644 index 0000000000..dddc8c7c49 --- /dev/null +++ b/tools/highlighter-helper/src/node/collapsedLines/resolveCollapsedLine.ts @@ -0,0 +1,24 @@ +const COLLAPSED_LINES_REGEXP = /:collapsed-lines\b/ +const COLLAPSED_LINES_START_REGEXP = /:collapsed-lines=(\d+)\b/ +const NO_COLLAPSED_LINES_REGEXP = /:no-collapsed-lines\b/ + +/** + * Resolve the `:collapsed-lines` `:collapsed-lines=num` / `:no-collapsed-lines` mark from token info + */ +export function resolveCollapsedLines(info: string): boolean | number | null { + const lines = COLLAPSED_LINES_START_REGEXP.exec(info)?.[1] + + if (lines) { + return Number(lines) + } + + if (COLLAPSED_LINES_REGEXP.test(info)) { + return true + } + + if (NO_COLLAPSED_LINES_REGEXP.test(info)) { + return false + } + + return null +} diff --git a/tools/highlighter-helper/src/node/index.ts b/tools/highlighter-helper/src/node/index.ts index b793203500..ab833e9446 100644 --- a/tools/highlighter-helper/src/node/index.ts +++ b/tools/highlighter-helper/src/node/index.ts @@ -1,2 +1,3 @@ export * from './lineNumbers/index.js' export * from './whitespace.js' +export * from './collapsedLines/index.js' diff --git a/tools/highlighter-helper/tsconfig.build.json b/tools/highlighter-helper/tsconfig.build.json index 4f60f73883..9e7c5d8194 100644 --- a/tools/highlighter-helper/tsconfig.build.json +++ b/tools/highlighter-helper/tsconfig.build.json @@ -3,7 +3,8 @@ "compilerOptions": { "rootDir": "./src", "outDir": "./lib", - "baseUrl": "." + "baseUrl": ".", + "types": ["vuepress/client-types"] }, "include": ["./src"] }