feat: use shiki instead of prismjs for syntax highlight (#627) (#654)

close #627

Co-authored-by: Kia Ishii <kia.king.08@gmail.com>
pull/660/head
Ryo_gk 3 years ago committed by GitHub
parent 8bc14e6366
commit 1fcf4a4462
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

@ -16,9 +16,9 @@ interface VitePressData {
page: Ref<PageData>
theme: Ref<any> // themeConfig from .vitepress/config.js
frontmatter: Ref<PageData['frontmatter']>
lang: Ref<string>
title: Ref<string>
description: Ref<string>
lang: Ref<string>
localePath: Ref<string>
}
```
@ -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
<ClientOnly>
<NonSSRFriendlyComponent />
</ClientOnly>

@ -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
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
</li>
</ul>
```
````
**Output**
```js
@ -220,9 +230,6 @@ export default {
}
```
**Input**
````
```html
<ul>
<li v-for="todo in todos" :key="todo.id">
@ -230,17 +237,10 @@ export default {
</li>
</ul>
```
````
**Output**
```html
<ul>
<li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
</ul>
```
A [list of valid languages](https://github.com/shikijs/shiki/blob/main/docs/languages.md) is available on Shikis repository.
A [list of valid languages](https://prismjs.com/#languages-list) is available on Prisms 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:

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

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

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

@ -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);
}
/**

@ -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<ViteUserConfig> => ({
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({

@ -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<string, string>
@ -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<MarkdownRenderer> => {
const md = MarkdownIt({
html: true,
linkify: true,
highlight,
highlight: await highlight(options.theme),
...options
}) as MarkdownRenderer

@ -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 `<pre v-pre><code>${code}</code></pre>`
}
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 `<pre v-pre><code>${escapeHtml(str)}</code></pre>`
}
return highlighter.codeToHtml(str, lang).replace(/^<pre.*?>/, '<pre v-pre>')
}
if (prism.languages[lang]) {
const code = prism.highlight(str, prism.languages[lang], lang)
return wrap(code, rawLang)
}
return wrap(str, 'text')
}

@ -21,7 +21,7 @@ export const lineNumberPlugin = (md: MarkdownIt) => {
const finalCode = rawCode
.replace(/<\/div>$/, `${lineNumbersWrapperCode}</div>`)
.replace(/"(language-\w+)"/, '"$1 line-numbers-mode"')
.replace(/"(language-\w*)"/, '"$1 line-numbers-mode"')
return finalCode
}

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

@ -7,6 +7,8 @@ import { slash } from './utils/slash'
import { OutputAsset, OutputChunk } from 'rollup'
import { staticDataPlugin } from './staticDataPlugin'
type Awaited<T> = T extends Promise<infer P> ? 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<typeof createMarkdownToVueRenderFn>
let markdownToVue: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>>
// 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,

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

Loading…
Cancel
Save