From 1fcf4a4462dfcd7cc11dab027c23b2298708ec22 Mon Sep 17 00:00:00 2001 From: Ryo_gk <62658104+ryo-gk@users.noreply.github.com> Date: Thu, 26 May 2022 01:18:43 +0900 Subject: [PATCH] feat: use shiki instead of prismjs for syntax highlight (#627) (#654) close #627 Co-authored-by: Kia Ishii --- docs/config/app-configs.md | 50 ++++++ docs/guide/api.md | 4 +- docs/guide/markdown-extensions.md | 32 ++-- package.json | 2 +- pnpm-lock.yaml | 25 ++- .../styles/components/vp-doc.css | 150 +++++------------- src/client/theme-default/styles/vars.css | 10 +- src/node/build/bundle.ts | 8 +- src/node/markdown/markdown.ts | 8 +- src/node/markdown/plugins/highlight.ts | 54 +------ src/node/markdown/plugins/lineNumbers.ts | 2 +- src/node/markdownToVue.ts | 5 +- src/node/plugin.ts | 8 +- src/node/shims.d.ts | 10 -- 14 files changed, 163 insertions(+), 205 deletions(-) diff --git a/docs/config/app-configs.md b/docs/config/app-configs.md index 51632242..b1cf0d8f 100644 --- a/docs/config/app-configs.md +++ b/docs/config/app-configs.md @@ -81,6 +81,56 @@ export default { } ``` +## markdown + +- Type: `MarkdownOption` + +Configre Markdown parser options. VitePress uses [Markdown-it](https://github.com/markdown-it/markdown-it) as the parser, and [Shiki](https://shiki.matsu.io/) to highlight language syntax. Inside this option, you may pass various Markdown related options to fit your needs. + +```js +export default { + markdown: { + theme: 'material-palenight', + lineNumbers: true + } +} +``` + +Below shows the the full option you may define within this object. + +```ts +interface MarkdownOptions extends MarkdownIt.Options { + // Syntax highlight theme for Shiki. + // See: https://github.com/shikijs/shiki/blob/main/docs/themes.md#all-themes + theme?: Shiki.Theme + + // Enable line numbers in code block. + lineNumbers?: boolean + + // markdown-it-anchor plugin options. + // See: https://github.com/valeriangalliat/markdown-it-anchor + anchor?: { + permalink?: anchor.AnchorOptions['permalink'] + } + + // markdown-it-attrs plugin options. + // See: https://github.com/arve0/markdown-it-attrs + attrs?: { + leftDelimiter?: string + rightDelimiter?: string + allowedAttributes?: string[] + } + + // markdown-it-table-of-contents cplugin options + // https://github.com/Oktavilla/markdown-it-table-of-contents + toc?: any + + // Configure the Markdown-it instance to fully customize + // how it works. + config?: (md: MarkdownIt) => void +} +``` + ## appearance - Type: `boolean` diff --git a/docs/guide/api.md b/docs/guide/api.md index ba427e5a..28eeb0c0 100644 --- a/docs/guide/api.md +++ b/docs/guide/api.md @@ -16,9 +16,9 @@ interface VitePressData { page: Ref theme: Ref // themeConfig from .vitepress/config.js frontmatter: Ref + lang: Ref title: Ref description: Ref - lang: Ref localePath: Ref } ``` @@ -85,7 +85,7 @@ Because VitePress applications are server-rendered in Node.js when generating st If you are using or demoing components that are not SSR-friendly (for example, contain custom directives), you can wrap them inside the `ClientOnly` component. -```html +```vue-html diff --git a/docs/guide/markdown-extensions.md b/docs/guide/markdown-extensions.md index dbeefe11..0e10ba20 100644 --- a/docs/guide/markdown-extensions.md +++ b/docs/guide/markdown-extensions.md @@ -198,7 +198,7 @@ console.log('Hello, VitePress!') ## Syntax Highlighting in Code Blocks -VitePress uses [Prism](https://prismjs.com) to highlight language syntax in Markdown code blocks, using coloured text. Prism supports a wide variety of programming languages. All you need to do is append a valid language alias to the beginning backticks for the code block: +VitePress uses [Shiki](https://shiki.matsu.io/) to highlight language syntax in Markdown code blocks, using coloured text. Shiki supports a wide variety of programming languages. All you need to do is append a valid language alias to the beginning backticks for the code block: **Input** @@ -211,6 +211,16 @@ export default { ``` ```` +```` +```html +
    +
  • + {{ todo.text }} +
  • +
+``` +```` + **Output** ```js @@ -220,9 +230,6 @@ export default { } ``` -**Input** - -```` ```html
  • @@ -230,17 +237,10 @@ export default {
``` -```` - -**Output** -```html -
    -
  • {{ todo.text }}
  • -
-``` +A [list of valid languages](https://github.com/shikijs/shiki/blob/main/docs/languages.md) is available on Shiki’s repository. -A [list of valid languages](https://prismjs.com/#languages-list) is available on Prism’s site. +You may also customize syntax highlight theme in app config. Please see [`markdown` options](../config/app-configs#markdown) for more details. ## Line Highlighting in Code Blocks @@ -287,7 +287,7 @@ export default { // Highlighted This line isn't highlighted, but this and the next 2 are.`, motd: 'VitePress is awesome', - lorem: 'ipsum', + lorem: 'ipsum' } } } @@ -315,13 +315,15 @@ export default { // Highlighted You can enable line numbers for each code blocks via config: ```js -module.exports = { +export default { markdown: { lineNumbers: true } } ``` +Please see [`markdown` options](../config/app-configs#markdown) for more details. + ## Import Code Snippets You can import code snippets from existing files via following syntax: diff --git a/package.json b/package.json index 51adb045..baa25af5 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "@vitejs/plugin-vue": "^2.3.2", "@vueuse/core": "^8.5.0", "body-scroll-lock": "^4.0.0-beta.0", - "prismjs": "^1.25.0", + "shiki": "^0.10.1", "vite": "^2.9.7", "vue": "3.2.33" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 555a2b16..27df0624 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,11 +56,11 @@ importers: ora: ^5.4.0 polka: ^0.5.2 prettier: ^2.3.0 - prismjs: ^1.25.0 rimraf: ^3.0.2 rollup: ^2.56.3 rollup-plugin-esbuild: ^4.8.2 semver: ^7.3.5 + shiki: ^0.10.1 sirv: ^1.0.12 typescript: ^4.6.4 vite: ^2.9.7 @@ -73,7 +73,7 @@ importers: '@vitejs/plugin-vue': 2.3.3_vite@2.9.9+vue@3.2.33 '@vueuse/core': 8.5.0_vue@3.2.33 body-scroll-lock: 4.0.0-beta.0 - prismjs: 1.25.0 + shiki: 0.10.1 vite: 2.9.9 vue: 3.2.33 devDependencies: @@ -2557,7 +2557,6 @@ packages: /jsonc-parser/3.0.0: resolution: {integrity: sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==} - dev: true /jsonfile/4.0.0: resolution: {integrity: sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=} @@ -3191,10 +3190,6 @@ packages: hasBin: true dev: true - /prismjs/1.25.0: - resolution: {integrity: sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==} - dev: false - /process-nextick-args/2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: true @@ -3464,6 +3459,14 @@ packages: resolution: {integrity: sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==} dev: true + /shiki/0.10.1: + resolution: {integrity: sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==} + dependencies: + jsonc-parser: 3.0.0 + vscode-oniguruma: 1.6.2 + vscode-textmate: 5.2.0 + dev: false + /side-channel/1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: @@ -3909,6 +3912,14 @@ packages: - stylus dev: true + /vscode-oniguruma/1.6.2: + resolution: {integrity: sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA==} + dev: false + + /vscode-textmate/5.2.0: + resolution: {integrity: sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==} + dev: false + /vue-demi/0.12.5_vue@3.2.33: resolution: {integrity: sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==} engines: {node: '>=12'} diff --git a/src/client/theme-default/styles/components/vp-doc.css b/src/client/theme-default/styles/components/vp-doc.css index 150d3f94..6e9e4ef0 100644 --- a/src/client/theme-default/styles/components/vp-doc.css +++ b/src/client/theme-default/styles/components/vp-doc.css @@ -327,49 +327,49 @@ top: 0; bottom: 0; left: 0; - padding: 13px 0 11px; + padding-top: 16px; width: 100%; - font-family: var(--vp-font-family-mono); line-height: var(--vp-code-line-height); + font-family: var(--vp-font-family-mono); font-size: var(--vp-code-font-size); user-select: none; overflow: hidden; } .vp-doc .highlight-lines .highlighted { - background-color: rgba(0, 0, 0, 0.3); + background-color: var(--vp-code-line-highlight-color); transition: background-color 0.5s; } -.dark .vp-doc .highlight-lines .highlighted { - background-color: rgba(255, 255, 255, 0.05); -} - .vp-doc div[class*='language-'].line-numbers-mode { padding-left: 32px; } +.vp-doc div[class*='language-'].line-numbers-mode pre { + padding-left: 16px; +} + .vp-doc .line-numbers-wrapper { position: absolute; top: 0; bottom: 0; left: 0; z-index: 3; - border-right: 1px solid var(--vp-c-divider-light); - padding: 13px 0 11px; + border-right: 1px solid var(--vp-c-divider-dark-2); + padding-top: 16px; width: 32px; text-align: center; font-family: var(--vp-font-family-mono); line-height: var(--vp-code-line-height); font-size: var(--vp-code-font-size); - color: var(--vp-c-text-3); + color: var(--vp-code-line-number-color); transition: border-color 0.5s, color 0.5s; } .vp-doc [class*='language-']:before { position: absolute; - top: 4px; - right: 10px; + top: 6px; + right: 12px; z-index: 2; font-size: 12px; font-weight: 500; @@ -377,100 +377,32 @@ transition: color 0.5s; } -.vp-doc [class~='language-vue']:before { content: 'vue'; } -.vp-doc [class~='language-html']:before { content: 'html'; } -.vp-doc [class~='language-vue-html']:before { content: 'template'; } -.vp-doc [class~='language-css']:before { content: 'css'; } -.vp-doc [class~='language-js']:before { content: 'js'; } -.vp-doc [class~='language-jsx']:before { content: 'jsx'; } -.vp-doc [class~='language-ts']:before { content: 'ts'; } -.vp-doc [class~='language-tsx']:before { content: 'tsx'; } -.vp-doc [class~='language-json']:before { content: 'json'; } -.vp-doc [class~='language-yaml']:before { content: 'yaml'; } -.vp-doc [class~='language-yml']:before { content: 'yaml'; } -.vp-doc [class~='language-sh']:before { content: 'sh'; } -.vp-doc [class~='language-bash']:before { content: 'sh'; } - -/** - * Code: Highlight - * - * prism.js tomorrow night eighties theme. - * https://github.com/chriskempson/tomorrow-theme - * - * @author Rose Pritchard - * -------------------------------------------------------------------------- */ - -.token.comment, -.token.block-comment, -.token.prolog, -.token.doctype, -.token.cdata { - color: #999; -} - -.token.punctuation { - color: #ccc; -} - -.token.tag, -.token.attr-name, -.token.namespace, -.token.deleted { - color: #e2777a; -} - -.token.function-name { - color: #6196cc; -} - -.token.boolean, -.token.number, -.token.function { - color: #f08d49; -} - -.token.property, -.token.class-name, -.token.constant, -.token.symbol { - color: #f8c555; -} - -.token.selector, -.token.important, -.token.atrule, -.token.keyword, -.token.builtin { - color: #cc99cd; -} - -.token.string, -.token.char, -.token.attr-value, -.token.regex, -.token.variable { - color: #7ec699; -} - -.token.operator, -.token.entity, -.token.url { - color: #67cdcc; -} - -.token.important, -.token.bold { - font-weight: bold; -} - -.token.italic { - font-style: italic; -} - -.token.entity { - cursor: help; -} - -.token.inserted { - color: green; -} +.vp-doc [class~='language-c']:before { content: 'c'; } +.vp-doc [class~='language-css']:before { content: 'css'; } +.vp-doc [class~='language-go']:before { content: 'go'; } +.vp-doc [class~='language-html']:before { content: 'html'; } +.vp-doc [class~='language-java']:before { content: 'java'; } +.vp-doc [class~='language-javascript']:before { content: 'js'; } +.vp-doc [class~='language-js']:before { content: 'js'; } +.vp-doc [class~='language-json']:before { content: 'json'; } +.vp-doc [class~='language-jsx']:before { content: 'jsx'; } +.vp-doc [class~='language-less']:before { content: 'less'; } +.vp-doc [class~='language-markdown']:before { content: 'md'; } +.vp-doc [class~='language-md']:before { content: 'md' } +.vp-doc [class~='language-php']:before { content: 'php'; } +.vp-doc [class~='language-python']:before { content: 'py'; } +.vp-doc [class~='language-py']:before { content: 'py'; } +.vp-doc [class~='language-rb']:before { content: 'rb'; } +.vp-doc [class~='language-ruby']:before { content: 'rb'; } +.vp-doc [class~='language-rust']:before { content: 'rust'; } +.vp-doc [class~='language-sass']:before { content: 'sass'; } +.vp-doc [class~='language-scss']:before { content: 'scss'; } +.vp-doc [class~='language-sh']:before { content: 'sh'; } +.vp-doc [class~='language-bash']:before { content: 'sh'; } +.vp-doc [class~='language-stylus']:before { content: 'styl'; } +.vp-doc [class~='language-vue-html']:before { content: 'template'; } +.vp-doc [class~='language-typescript']:before { content: 'ts'; } +.vp-doc [class~='language-ts']:before { content: 'ts'; } +.vp-doc [class~='language-tsx']:before { content: 'tsx'; } +.vp-doc [class~='language-vue']:before { content: 'vue'; } +.vp-doc [class~='language-yaml']:before { content: 'yaml'; } diff --git a/src/client/theme-default/styles/vars.css b/src/client/theme-default/styles/vars.css index aca3345a..c2fbf70c 100644 --- a/src/client/theme-default/styles/vars.css +++ b/src/client/theme-default/styles/vars.css @@ -188,15 +188,21 @@ * -------------------------------------------------------------------------- */ :root { - --vp-code-line-height: 24px; - --vp-code-font-size: 14px; + --vp-code-line-height: 1.7; + --vp-code-font-size: 0.875em; --vp-code-block-color: var(--vp-c-text-dark-1); --vp-code-block-bg: #292d3e; + + --vp-code-line-highlight-color: rgba(0, 0, 0, 0.5); + + --vp-code-line-number-color: var(--vp-c-text-dark-3); } .dark { --vp-code-block-bg: var(--vp-c-bg-alt); + + --vp-code-line-number-color: var(--vp-c-text-dark-3); } /** diff --git a/src/node/build/bundle.ts b/src/node/build/bundle.ts index 7a5a46f1..26d608fb 100644 --- a/src/node/build/bundle.ts +++ b/src/node/build/bundle.ts @@ -41,11 +41,11 @@ export async function bundle( // resolve options to pass to vite const { rollupOptions } = options - const resolveViteConfig = (ssr: boolean): ViteUserConfig => ({ + const resolveViteConfig = async (ssr: boolean): Promise => ({ root: srcDir, base: config.site.base, logLevel: 'warn', - plugins: createVitePressPlugin( + plugins: await createVitePressPlugin( root, config, ssr, @@ -108,8 +108,8 @@ export async function bundle( spinner.start('building client + server bundles...') try { ;[clientResult, serverResult] = await (Promise.all([ - config.mpa ? null : build(resolveViteConfig(false)), - build(resolveViteConfig(true)) + config.mpa ? null : build(await resolveViteConfig(false)), + build(await resolveViteConfig(true)) ]) as Promise<[RollupOutput, RollupOutput]>) } catch (e) { spinner.stopAndPersist({ diff --git a/src/node/markdown/markdown.ts b/src/node/markdown/markdown.ts index 46cbff35..b4f2d226 100644 --- a/src/node/markdown/markdown.ts +++ b/src/node/markdown/markdown.ts @@ -1,4 +1,5 @@ import MarkdownIt from 'markdown-it' +import { Theme } from 'shiki' import { parseHeader } from '../utils/parseHeader' import { highlight } from './plugins/highlight' import { slugify } from './plugins/slugify' @@ -29,6 +30,7 @@ export interface MarkdownOptions extends MarkdownIt.Options { rightDelimiter?: string allowedAttributes?: string[] } + theme?: Theme // https://github.com/Oktavilla/markdown-it-table-of-contents toc?: any externalLinks?: Record @@ -48,15 +50,15 @@ export interface MarkdownRenderer extends MarkdownIt { export type { Header } -export const createMarkdownRenderer = ( +export const createMarkdownRenderer = async ( srcDir: string, options: MarkdownOptions = {}, base: string -): MarkdownRenderer => { +): Promise => { const md = MarkdownIt({ html: true, linkify: true, - highlight, + highlight: await highlight(options.theme), ...options }) as MarkdownRenderer diff --git a/src/node/markdown/plugins/highlight.ts b/src/node/markdown/plugins/highlight.ts index aab5f996..291267c3 100644 --- a/src/node/markdown/plugins/highlight.ts +++ b/src/node/markdown/plugins/highlight.ts @@ -1,52 +1,14 @@ -import chalk from 'chalk' import escapeHtml from 'escape-html' -import prism from 'prismjs' +import { getHighlighter } from 'shiki' -// prism is listed as actual dep so it's ok to require -const loadLanguages = require('prismjs/components/index') +export const highlight = async (theme = 'material-palenight') => { + const highlighter = await getHighlighter({ theme }) -// required to make embedded highlighting work... -loadLanguages(['markup', 'css', 'javascript']) - -function wrap(code: string, lang: string): string { - if (lang === 'text') { - code = escapeHtml(code) - } - return `
${code}
` -} - -export const highlight = (str: string, lang: string) => { - if (!lang) { - return wrap(str, 'text') - } - lang = lang.toLowerCase() - const rawLang = lang - if (lang === 'vue' || lang === 'html') { - lang = 'markup' - } - if (lang === 'md') { - lang = 'markdown' - } - if (lang === 'ts') { - lang = 'typescript' - } - if (lang === 'py') { - lang = 'python' - } - if (!prism.languages[lang]) { - try { - loadLanguages([lang]) - } catch (e) { - console.warn( - chalk.yellow( - `[vitepress] Syntax highlight for language "${lang}" is not supported.` - ) - ) + return (str: string, lang: string) => { + if (!lang || lang === 'text') { + return `
${escapeHtml(str)}
` } + + return highlighter.codeToHtml(str, lang).replace(/^/, '
')
   }
-  if (prism.languages[lang]) {
-    const code = prism.highlight(str, prism.languages[lang], lang)
-    return wrap(code, rawLang)
-  }
-  return wrap(str, 'text')
 }
diff --git a/src/node/markdown/plugins/lineNumbers.ts b/src/node/markdown/plugins/lineNumbers.ts
index 94a8f5ec..739b0222 100644
--- a/src/node/markdown/plugins/lineNumbers.ts
+++ b/src/node/markdown/plugins/lineNumbers.ts
@@ -21,7 +21,7 @@ export const lineNumberPlugin = (md: MarkdownIt) => {
 
     const finalCode = rawCode
       .replace(/<\/div>$/, `${lineNumbersWrapperCode}`)
-      .replace(/"(language-\w+)"/, '"$1 line-numbers-mode"')
+      .replace(/"(language-\w*)"/, '"$1 line-numbers-mode"')
 
     return finalCode
   }
diff --git a/src/node/markdownToVue.ts b/src/node/markdownToVue.ts
index f3bb2542..a7b71b1d 100644
--- a/src/node/markdownToVue.ts
+++ b/src/node/markdownToVue.ts
@@ -21,7 +21,7 @@ export interface MarkdownCompileResult {
   includes: string[]
 }
 
-export function createMarkdownToVueRenderFn(
+export async function createMarkdownToVueRenderFn(
   srcDir: string,
   options: MarkdownOptions = {},
   pages: string[],
@@ -30,7 +30,8 @@ export function createMarkdownToVueRenderFn(
   base: string,
   includeLastUpdatedData = false
 ) {
-  const md = createMarkdownRenderer(srcDir, options, base)
+  const md = await createMarkdownRenderer(srcDir, options, base)
+
   pages = pages.map((p) => slash(p.replace(/\.md$/, '')))
 
   const userDefineRegex = userDefines
diff --git a/src/node/plugin.ts b/src/node/plugin.ts
index 02ba4661..d30d32b0 100644
--- a/src/node/plugin.ts
+++ b/src/node/plugin.ts
@@ -7,6 +7,8 @@ import { slash } from './utils/slash'
 import { OutputAsset, OutputChunk } from 'rollup'
 import { staticDataPlugin } from './staticDataPlugin'
 
+type Awaited = T extends Promise ? P : never
+
 const hashRE = /\.(\w+)\.js$/
 const staticInjectMarkerRE =
   /\b(const _hoisted_\d+ = \/\*(?:#|@)__PURE__\*\/\s*createStaticVNode)\("(.*)", (\d+)\)/g
@@ -46,7 +48,7 @@ export function createVitePressPlugin(
     pages
   } = siteConfig
 
-  let markdownToVue: ReturnType
+  let markdownToVue: Awaited>
 
   // lazy require plugin-vue to respect NODE_ENV in @vue/compiler-x
   const vuePlugin = require('@vitejs/plugin-vue')({
@@ -70,9 +72,9 @@ export function createVitePressPlugin(
   const vitePressPlugin: Plugin = {
     name: 'vitepress',
 
-    configResolved(resolvedConfig) {
+    async configResolved(resolvedConfig) {
       config = resolvedConfig
-      markdownToVue = createMarkdownToVueRenderFn(
+      markdownToVue = await createMarkdownToVueRenderFn(
         srcDir,
         markdown,
         pages,
diff --git a/src/node/shims.d.ts b/src/node/shims.d.ts
index 097e3cc8..af90ae3e 100644
--- a/src/node/shims.d.ts
+++ b/src/node/shims.d.ts
@@ -23,16 +23,6 @@ declare module 'escape-html' {
   export default def
 }
 
-declare module 'prismjs' {
-  const def: any
-  export default def
-}
-
-declare module 'prismjs/components/index' {
-  const def: any
-  export default def
-}
-
 declare module 'diacritics' {
   export const remove: (str: string) => string
 }