From 04ab0eb6dcacb065e865332580088891bc2df893 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Tue, 25 Oct 2022 16:40:46 +0200 Subject: [PATCH] feat: support focus, colored diffs, error highlights in code blocks (#1534) Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> --- docs/guide/markdown.md | 133 +++++++++++++++++- package.json | 1 + pnpm-lock.yaml | 10 ++ .../styles/components/vp-doc.css | 56 ++++++++ src/client/theme-default/styles/vars.css | 9 ++ src/node/markdown/plugins/highlight.ts | 76 ++++++++-- 6 files changed, 269 insertions(+), 16 deletions(-) diff --git a/docs/guide/markdown.md b/docs/guide/markdown.md index 41f6017e0a4..6c25b1a1b43 100644 --- a/docs/guide/markdown.md +++ b/docs/guide/markdown.md @@ -71,11 +71,11 @@ For more details, see [Frontmatter](./frontmatter). **Input** ``` -| Tables | Are | Cool | -| ------------- |:-------------:| -----:| +| Tables | Are | Cool | +| ------------- | :-----------: | ----: | | col 3 is | right-aligned | $1600 | -| col 2 is | centered | $12 | -| zebra stripes | are neat | $1 | +| col 2 is | centered | $12 | +| zebra stripes | are neat | $1 | ``` **Output** @@ -351,6 +351,131 @@ export default { // Highlighted } ``` +Alternatively, it's possible to highlight directly in the line by using the `// [!code hl]` comment. + +**Input** + +```` +```js +export default { + data () { + return { + msg: 'Highlighted!' // [!codeㅤ hl] + } + } +} +``` +```` + +**Output** + +```js +export default { + data () { + return { + msg: 'Highlighted!' // [!code hl] + } + } +} +``` + +## Focus in Code Blocks + +Adding the `// [!code focus]` comment on a line will focus it and blur the other parts of the code. + +Additionally, you can define a number of lines to focus using `// [!code focus:]`. + +**Input** + +```` +```js +export default { + data () { + return { + msg: 'Focused!' // [!codeㅤ focus] + } + } +} +``` +```` + +**Output** + +```js +export default { + data () { + return { + msg: 'Focused!' // [!code focus] + } + } +} +``` + +## Colored diffs in Code Blocks + +Adding the `// [!code --]` or `// [!code ++]` comments on a line will create a diff of that line, while keeping the colors of the codeblock. + +**Input** + +```` +```js +export default { + data () { + return { + msg: 'Removed' // [!codeㅤ --] + msg: 'Added' // [!codeㅤ ++] + } + } +} +``` +```` + +**Output** + +```js +export default { + data () { + return { + msg: 'Removed' // [!code --] + msg: 'Added' // [!code ++] + } + } +} +``` + +## Errors and warnings + +Adding the `// [!code warning]` or `// [!code error]` comments on a line will color it accordingly. + +**Input** + +```` +```js +export default { + data () { + return { + msg: 'Error', // [!codeㅤ error] + msg: 'Warning' // [!codeㅤ warning] + } + } +} +``` +```` + +**Output** + +```js +export default { + data () { + return { + msg: 'Error', // [!code error] + msg: 'Warning' // [!code warning] + } + } +} +``` + + ## Line Numbers You can enable line numbers for each code blocks via config: diff --git a/package.json b/package.json index dbacbcb0b7e..8d22c194c01 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@vueuse/core": "^9.3.0", "body-scroll-lock": "4.0.0-beta.0", "shiki": "^0.11.1", + "shiki-processor": "^0.1.0", "vite": "^3.1.6", "vue": "^3.2.40" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ecbb995625..56927823b64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,6 +75,7 @@ importers: rollup-plugin-esbuild: ^4.10.1 semver: ^7.3.8 shiki: ^0.11.1 + shiki-processor: ^0.1.0 simple-git-hooks: ^2.8.0 sirv: ^2.0.2 supports-color: ^9.2.3 @@ -92,6 +93,7 @@ importers: '@vueuse/core': 9.3.0_vue@3.2.40 body-scroll-lock: 4.0.0-beta.0 shiki: 0.11.1 + shiki-processor: 0.1.0_shiki@0.11.1 vite: 3.1.6 vue: 3.2.40 devDependencies: @@ -3441,6 +3443,14 @@ packages: resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==} dev: true + /shiki-processor/0.1.0_shiki@0.11.1: + resolution: {integrity: sha512-7ty3VouP7AQMlERKeiobVeyhjUW6rPMM1b+xFcFF/XwhkN4//Fg9Ju6hPfIOvO4ztylkbLqYufbJmLJmw7SfQA==} + peerDependencies: + shiki: ^0.11.1 + dependencies: + shiki: 0.11.1 + dev: false + /shiki/0.11.1: resolution: {integrity: sha512-EugY9VASFuDqOexOgXR18ZV+TbFrQHeCpEYaXamO+SZlsnT/2LxuLBX25GGtIrwaEVFXUAbUQ601SWE2rMwWHA==} dependencies: diff --git a/src/client/theme-default/styles/components/vp-doc.css b/src/client/theme-default/styles/components/vp-doc.css index 909fbc108f7..7220523c738 100644 --- a/src/client/theme-default/styles/components/vp-doc.css +++ b/src/client/theme-default/styles/components/vp-doc.css @@ -329,6 +329,62 @@ display: inline-block; } +.vp-doc [class*='language-'] code .highlighted.error { + background-color: var(--vp-code-line-error-color); +} + +.vp-doc [class*='language-'] code .highlighted.warning { + background-color: var(--vp-code-line-warning-color); +} + +.vp-doc [class*='language-'] code .diff { + transition: background-color 0.5s; + margin: 0 -24px; + padding: 0 24px; + width: calc(100% + 2 * 24px); + display: inline-block; +} + +.vp-doc [class*='language-'] code .diff::before { + position: absolute; + left: 1rem; +} + +.vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) { + filter: blur(0.095rem); + opacity: 0.4; + transition: filter 0.35s, opacity 0.35s; +} + +.vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) { + opacity: 0.7; + transition: filter 0.35s, opacity 0.35s; +} + +.vp-doc [class*='language-']:hover .has-focused-lines .line:not(.has-focus) { + filter: blur(0); + opacity: 1; +} + +.vp-doc [class*='language-'] code .diff.remove { + background-color: var(--vp-code-line-diff-remove-color); + opacity: 0.7; +} + +.vp-doc [class*='language-'] code .diff.remove::before { + content: '-'; + color: var(--vp-code-line-diff-remove-symbol-color); +} + +.vp-doc [class*='language-'] code .diff.add { + background-color: var(--vp-code-line-diff-add-color); +} + +.vp-doc [class*='language-'] code .diff.add::before { + content: '+'; + color: var(--vp-code-line-diff-add-symbol-color); +} + .vp-doc div[class*='language-'].line-numbers-mode { padding-left: 32px; } diff --git a/src/client/theme-default/styles/vars.css b/src/client/theme-default/styles/vars.css index d81c78309fd..f736d93f08a 100644 --- a/src/client/theme-default/styles/vars.css +++ b/src/client/theme-default/styles/vars.css @@ -209,6 +209,15 @@ --vp-code-line-highlight-color: rgba(0, 0, 0, 0.5); --vp-code-line-number-color: var(--vp-c-text-dark-3); + --vp-code-line-diff-add-color: rgba(125, 191, 123, 0.1); + --vp-code-line-diff-add-symbol-color: rgba(125, 191, 123, 0.5); + + --vp-code-line-diff-remove-color: rgba(255, 128, 128, 0.05); + --vp-code-line-diff-remove-symbol-color: rgba(255, 128, 128, 0.5); + + --vp-code-line-error-color: var(--vp-c-red-dimm-2); + --vp-code-line-warning-color: var(--vp-c-yellow-dimm-2); + --vp-code-copy-code-hover-bg: rgba(255, 255, 255, 0.05); --vp-code-copy-code-active-text: var(--vp-c-text-dark-2); } diff --git a/src/node/markdown/plugins/highlight.ts b/src/node/markdown/plugins/highlight.ts index 761aff428f4..aadd433813f 100644 --- a/src/node/markdown/plugins/highlight.ts +++ b/src/node/markdown/plugins/highlight.ts @@ -1,4 +1,14 @@ -import { IThemeRegistration, getHighlighter, HtmlRendererOptions } from 'shiki' +import { IThemeRegistration, HtmlRendererOptions } from 'shiki' +import { + createDiffProcessor, + createFocusProcessor, + createHighlightProcessor, + createRangeProcessor, + getHighlighter, + Processor, + addClass, + defineProcessor +} from 'shiki-processor' import type { ThemeOptions } from '../markdown' /** @@ -32,6 +42,14 @@ const attrsToLines = (attrs: string): HtmlRendererOptions['lineOptions'] => { })) } +const errorLevelProcessor = defineProcessor({ + name: 'error-level', + handler: createRangeProcessor({ + error: ['highlighted', 'error'], + warning: ['highlighted', 'warning'] + }) +}) + export async function highlight( theme: ThemeOptions = 'material-palenight' ): Promise<(str: string, lang: string, attrs: string) => string> { @@ -39,10 +57,20 @@ export async function highlight( const getThemeName = (themeValue: IThemeRegistration) => typeof themeValue === 'string' ? themeValue : themeValue.name + const processors: Processor[] = [ + createFocusProcessor(), + createHighlightProcessor({ hasHighlightClass: 'highlighted' }), + createDiffProcessor(), + errorLevelProcessor + ] + const highlighter = await getHighlighter({ - themes: hasSingleTheme ? [theme] : [theme.dark, theme.light] + themes: hasSingleTheme ? [theme] : [theme.dark, theme.light], + processors }) - const preRE = /^/ + + const styleRE = /
/
+  const preRE = /^/
   const vueRE = /-vue$/
 
   return (str: string, lang: string, attrs: string) => {
@@ -50,20 +78,44 @@ export async function highlight(
     lang = lang.replace(vueRE, '').toLowerCase()
 
     const lineOptions = attrsToLines(attrs)
+    const cleanup = (str: string) =>
+      str
+        .replace(preRE, (_, attributes) => `
`)
+        .replace(styleRE, (_, style) => _.replace(style, ''))
 
     if (hasSingleTheme) {
-      return highlighter
-        .codeToHtml(str, { lang, lineOptions, theme: getThemeName(theme) })
-        .replace(preRE, `
`)
+      return cleanup(
+        highlighter.codeToHtml(str, {
+          lang,
+          lineOptions,
+          theme: getThemeName(theme)
+        })
+      )
     }
 
-    const dark = highlighter
-      .codeToHtml(str, { lang, lineOptions, theme: getThemeName(theme.dark) })
-      .replace(preRE, `
`)
+    const dark = addClass(
+      cleanup(
+        highlighter.codeToHtml(str, {
+          lang,
+          lineOptions,
+          theme: getThemeName(theme.dark)
+        })
+      ),
+      'vp-code-dark',
+      'pre'
+    )
 
-    const light = highlighter
-      .codeToHtml(str, { lang, lineOptions, theme: getThemeName(theme.light) })
-      .replace(preRE, `
`)
+    const light = addClass(
+      cleanup(
+        highlighter.codeToHtml(str, {
+          lang,
+          lineOptions,
+          theme: getThemeName(theme.light)
+        })
+      ),
+      'vp-code-light',
+      'pre'
+    )
 
     return dark + light
   }