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 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 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`] = ` "
Raw text
 
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.use(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 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 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`] = ` "
Raw text
Raw text
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 = {}): Promise => { 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}
`) + .replace(/"(language-[^"]*?)"/, '"$1 has-collapsed-lines collapsed"') + .replace(/^]*>/, (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"] }