feat(build): improve code blocks and snippets (#875)

* refactor: don't hardcode language names

* docs: fix typo

* feat: support specifying language while importing code snippets

* feat: support interpolation inside code blocks

* docs: update v-pre escaping

* fix: ignore starting `>` in case of shell commands

fixes #861, fixes #471, fixes #884
pull/1055/head
Divyansh Singh 2 years ago committed by GitHub
parent d1a2c76f33
commit f789932ffc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -27,7 +27,7 @@ You will need [pnpm](https://pnpm.io)
After cloning the repo, run: After cloning the repo, run:
```bash ```sh
# install the dependencies of the project # install the dependencies of the project
$ pnpm install $ pnpm install
``` ```
@ -36,7 +36,7 @@ $ pnpm install
At first, execute the `pnpm run build` command. At first, execute the `pnpm run build` command.
```bash ```sh
$ pnpm run build $ pnpm run build
``` ```
@ -44,7 +44,7 @@ You only need to do this once for your fresh project. It copies required files a
The easiest way to start testing out VitePress is to tweak the VitePress docs. You may run `pnpm run docs` to boot up VitePress documentation site locally, with live reloading of the source code. The easiest way to start testing out VitePress is to tweak the VitePress docs. You may run `pnpm run docs` to boot up VitePress documentation site locally, with live reloading of the source code.
```bash ```sh
$ pnpm run docs $ pnpm run docs
``` ```
@ -52,6 +52,6 @@ After executing the above command, visit http://localhost:3000 and try modifying
If you don't need docs site up and running, you may start VitePress local dev environment with `pnpm run dev`. If you don't need docs site up and running, you may start VitePress local dev environment with `pnpm run dev`.
```bash ```sh
$ pnpm run dev $ pnpm run dev
``` ```

@ -1,6 +1,5 @@
/docs /docs
/examples /examples
*.css
*.md *.md
*.vue *.vue
dist dist

@ -10,13 +10,13 @@ VitePress is currently in `alpha` status. It is already suitable for out-of-the-
Create and change into a new directory. Create and change into a new directory.
```bash ```sh
$ mkdir vitepress-starter && cd vitepress-starter $ mkdir vitepress-starter && cd vitepress-starter
``` ```
Then, initialize with your preferred package manager. Then, initialize with your preferred package manager.
```bash ```sh
$ yarn init $ yarn init
``` ```
@ -24,7 +24,7 @@ $ yarn init
Add VitePress and Vue as dev dependencies for the project. Add VitePress and Vue as dev dependencies for the project.
```bash ```sh
$ yarn add --dev vitepress vue $ yarn add --dev vitepress vue
``` ```
@ -64,7 +64,7 @@ On PNPM, add this in your `package.json`:
Create your first document. Create your first document.
```bash ```sh
$ mkdir docs && echo '# Hello VitePress' > docs/index.md $ mkdir docs && echo '# Hello VitePress' > docs/index.md
``` ```
@ -86,7 +86,7 @@ Add some scripts to `package.json`.
Serve the documentation site in the local server. Serve the documentation site in the local server.
```bash ```sh
$ yarn docs:dev $ yarn docs:dev
``` ```

@ -279,7 +279,7 @@ In addition to a single line, you can also specify multiple single lines, ranges
**Input** **Input**
```` ````
```js{1,4,6-7} ```js{1,4,6-8}
export default { // Highlighted export default { // Highlighted
data () { data () {
return { return {
@ -296,7 +296,7 @@ export default { // Highlighted
**Output** **Output**
```js{1,4,6-7} ```js{1,4,6-8}
export default { // Highlighted export default { // Highlighted
data () { data () {
return { return {
@ -346,20 +346,12 @@ It also supports [line highlighting](#line-highlighting-in-code-blocks):
**Code file** **Code file**
<!--lint disable strong-marker-->
<<< @/snippets/snippet.js <<< @/snippets/snippet.js
<!--lint enable strong-marker-->
**Output** **Output**
<!--lint disable strong-marker-->
<<< @/snippets/snippet.js{2} <<< @/snippets/snippet.js{2}
<!--lint enable strong-marker-->
::: tip ::: tip
The value of `@` corresponds to the source root. By default it's the VitePress project root, unless `srcDir` is configured. The value of `@` corresponds to the source root. By default it's the VitePress project root, unless `srcDir` is configured.
::: :::
@ -374,19 +366,22 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co
**Code file** **Code file**
<!--lint disable strong-marker-->
<<< @/snippets/snippet-with-region.js <<< @/snippets/snippet-with-region.js
<!--lint enable strong-marker-->
**Output** **Output**
<!--lint disable strong-marker-->
<<< @/snippets/snippet-with-region.js#snippet{1} <<< @/snippets/snippet-with-region.js#snippet{1}
<!--lint enable strong-marker--> You can also specify the language inside the braces (`{}`) like this:
```md
<<< @/snippets/snippet.cs{c#}
<!-- with line highlighting: -->
<<< @/snippets/snippet.cs{1,2,4-6 c#}
```
This is helpful if source language cannot be inferred from your file extension.
## Markdown File Inclusion ## Markdown File Inclusion

@ -62,7 +62,7 @@ const { page } = useData()
## Escaping ## Escaping
By default, fenced code blocks are automatically wrapped with `v-pre`. To display raw mustaches or Vue-specific syntax inside inline code snippets or plain text, you need to wrap a paragraph with the `v-pre` custom container: By default, fenced code blocks are automatically wrapped with `v-pre`, unless you have set some language with `-vue` suffix like `js-vue` (in that case you can use Vue-style interpolation inside fences). To display raw mustaches or Vue-specific syntax inside inline code snippets or plain text, you need to wrap a paragraph with the `v-pre` custom container:
**Input** **Input**

@ -11,7 +11,7 @@ export function useCopyCode() {
nextTick(() => { nextTick(() => {
document document
.querySelectorAll<HTMLSpanElement>( .querySelectorAll<HTMLSpanElement>(
'.vp-doc div[class*="language-"]>span.copy' '.vp-doc div[class*="language-"] > span.copy'
) )
.forEach(handleElement) .forEach(handleElement)
}) })
@ -67,19 +67,19 @@ async function copyToClipboard(text: string) {
function handleElement(el: HTMLElement) { function handleElement(el: HTMLElement) {
el.onclick = () => { el.onclick = () => {
const parent = el.parentElement const parent = el.parentElement
const sibling = el.nextElementSibling as HTMLPreElement | null
if (!parent) { if (!parent || !sibling) {
return return
} }
const isShell = const isShell = /language-(shellscript|shell|bash|sh|zsh)/.test(
parent.classList.contains('language-sh') || parent.classList.toString()
parent.classList.contains('language-bash') )
let { innerText: text = '' } = parent let { innerText: text = '' } = sibling
if (isShell) { if (isShell) {
text = text.replace(/^ *\$ /gm, '') text = text.replace(/^ *(\$|>) /gm, '')
} }
copyToClipboard(text).then(() => { copyToClipboard(text).then(() => {

@ -75,7 +75,7 @@ b {
a, a,
area, area,
button, button,
[role="button"], [role='button'],
input, input,
label, label,
select, select,
@ -142,13 +142,13 @@ textarea {
button { button {
padding: 0; padding: 0;
font-family: inherit;; font-family: inherit;
background-color: transparent; background-color: transparent;
background-image: none; background-image: none;
} }
button, button,
[role="button"] { [role='button'] {
cursor: pointer; cursor: pointer;
} }
@ -197,7 +197,7 @@ input::-webkit-inner-spin-button {
margin: 0; margin: 0;
} }
input[type="number"] { input[type='number'] {
-moz-appearance: textfield; -moz-appearance: textfield;
} }

@ -203,7 +203,7 @@
color: inherit; color: inherit;
font-weight: 600; font-weight: 600;
text-decoration: underline; text-decoration: underline;
transition: opacity .25s; transition: opacity 0.25s;
} }
.vp-doc .custom-block a:hover { .vp-doc .custom-block a:hover {
@ -381,7 +381,7 @@
background-position: 50%; background-position: 50%;
background-size: 20px; background-size: 20px;
background-repeat: no-repeat; background-repeat: no-repeat;
transition: opacity 0.25s; transition: opacity 0.4s;
} }
.vp-doc [class*='language-']:hover > span.copy { .vp-doc [class*='language-']:hover > span.copy {
@ -414,10 +414,10 @@
color: var(--vp-code-copy-code-active-text); color: var(--vp-code-copy-code-active-text);
background-color: var(--vp-code-copy-code-hover-bg); background-color: var(--vp-code-copy-code-hover-bg);
white-space: nowrap; white-space: nowrap;
content: "Copied"; content: 'Copied';
} }
.vp-doc [class*='language-']:before { .vp-doc [class*='language-'] > span.lang {
position: absolute; position: absolute;
top: 6px; top: 6px;
right: 12px; right: 12px;
@ -425,43 +425,13 @@
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: var(--vp-c-text-dark-3); color: var(--vp-c-text-dark-3);
transition: color 0.5s, opacity 0.5s; transition: color 0.4s, opacity 0.4s;
} }
.vp-doc [class*='language-']:hover:before { .vp-doc [class*='language-']:hover > span.lang {
opacity: 0; opacity: 0;
} }
.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'; }
/** /**
* Component: Team * Component: Team
* -------------------------------------------------------------------------- */ * -------------------------------------------------------------------------- */

@ -46,38 +46,63 @@
gap: 4px; gap: 4px;
} }
.vp-sponsor-grid.xmini .vp-sponsor-grid-link { height: 64px; } .vp-sponsor-grid.xmini .vp-sponsor-grid-link {
.vp-sponsor-grid.xmini .vp-sponsor-grid-image { max-width: 64px; max-height: 22px } height: 64px;
}
.vp-sponsor-grid.xmini .vp-sponsor-grid-image {
max-width: 64px;
max-height: 22px;
}
.vp-sponsor-grid.mini .vp-sponsor-grid-link { height: 72px; } .vp-sponsor-grid.mini .vp-sponsor-grid-link {
.vp-sponsor-grid.mini .vp-sponsor-grid-image { max-width: 96px; max-height: 24px } height: 72px;
}
.vp-sponsor-grid.mini .vp-sponsor-grid-image {
max-width: 96px;
max-height: 24px;
}
.vp-sponsor-grid.small .vp-sponsor-grid-link { height: 96px; } .vp-sponsor-grid.small .vp-sponsor-grid-link {
.vp-sponsor-grid.small .vp-sponsor-grid-image { max-width: 96px; max-height: 24px } height: 96px;
}
.vp-sponsor-grid.small .vp-sponsor-grid-image {
max-width: 96px;
max-height: 24px;
}
.vp-sponsor-grid.medium .vp-sponsor-grid-link { height: 112px; } .vp-sponsor-grid.medium .vp-sponsor-grid-link {
.vp-sponsor-grid.medium .vp-sponsor-grid-image { max-width: 120px; max-height: 36px } height: 112px;
}
.vp-sponsor-grid.medium .vp-sponsor-grid-image {
max-width: 120px;
max-height: 36px;
}
.vp-sponsor-grid.big .vp-sponsor-grid-link { height: 184px; } .vp-sponsor-grid.big .vp-sponsor-grid-link {
.vp-sponsor-grid.big .vp-sponsor-grid-image { max-width: 192px; max-height: 56px } height: 184px;
}
.vp-sponsor-grid.big .vp-sponsor-grid-image {
max-width: 192px;
max-height: 56px;
}
.vp-sponsor-grid[data-vp-grid="2"] .vp-sponsor-grid-item { .vp-sponsor-grid[data-vp-grid='2'] .vp-sponsor-grid-item {
width: calc((100% - 4px) / 2); width: calc((100% - 4px) / 2);
} }
.vp-sponsor-grid[data-vp-grid="3"] .vp-sponsor-grid-item { .vp-sponsor-grid[data-vp-grid='3'] .vp-sponsor-grid-item {
width: calc((100% - 4px * 2) / 3); width: calc((100% - 4px * 2) / 3);
} }
.vp-sponsor-grid[data-vp-grid="4"] .vp-sponsor-grid-item { .vp-sponsor-grid[data-vp-grid='4'] .vp-sponsor-grid-item {
width: calc((100% - 4px * 3) / 4); width: calc((100% - 4px * 3) / 4);
} }
.vp-sponsor-grid[data-vp-grid="5"] .vp-sponsor-grid-item { .vp-sponsor-grid[data-vp-grid='5'] .vp-sponsor-grid-item {
width: calc((100% - 4px * 4) / 5); width: calc((100% - 4px * 4) / 5);
} }
.vp-sponsor-grid[data-vp-grid="6"] .vp-sponsor-grid-item { .vp-sponsor-grid[data-vp-grid='6'] .vp-sponsor-grid-item {
width: calc((100% - 4px * 5) / 6); width: calc((100% - 4px * 5) / 6);
} }

@ -34,12 +34,12 @@
--vp-c-divider-dark-2: rgba(84, 84, 84, 0.48); --vp-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vp-c-text-light-1: var(--vp-c-indigo); --vp-c-text-light-1: var(--vp-c-indigo);
--vp-c-text-light-2: rgba(60, 60, 60, 0.70); --vp-c-text-light-2: rgba(60, 60, 60, 0.7);
--vp-c-text-light-3: rgba(60, 60, 60, 0.33); --vp-c-text-light-3: rgba(60, 60, 60, 0.33);
--vp-c-text-light-4: rgba(60, 60, 60, 0.18); --vp-c-text-light-4: rgba(60, 60, 60, 0.18);
--vp-c-text-dark-1: rgba(255, 255, 255, 0.87); --vp-c-text-dark-1: rgba(255, 255, 255, 0.87);
--vp-c-text-dark-2: rgba(235, 235, 235, 0.60); --vp-c-text-dark-2: rgba(235, 235, 235, 0.6);
--vp-c-text-dark-3: rgba(235, 235, 235, 0.38); --vp-c-text-dark-3: rgba(235, 235, 235, 0.38);
--vp-c-text-dark-4: rgba(235, 235, 235, 0.18); --vp-c-text-dark-4: rgba(235, 235, 235, 0.18);
@ -180,8 +180,8 @@
* -------------------------------------------------------------------------- */ * -------------------------------------------------------------------------- */
:root { :root {
--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' class='h-6 w-6' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2'/%3E%3C/svg%3E"); --vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2'/%3E%3C/svg%3E");
--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' class='h-6 w-6' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m-6 9 2 2 4-4'/%3E%3C/svg%3E"); --vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m-6 9 2 2 4-4'/%3E%3C/svg%3E");
} }
/** /**

@ -10,23 +10,25 @@ export async function highlight(theme: ThemeOptions = 'material-palenight') {
themes: hasSingleTheme ? [theme] : [theme.dark, theme.light] themes: hasSingleTheme ? [theme] : [theme.dark, theme.light]
}) })
const preRE = /^<pre.*?>/ const preRE = /^<pre.*?>/
const vueRE = /-vue$/
return (str: string, lang: string) => { return (str: string, lang: string) => {
lang = lang || 'text' const vPre = vueRE.test(lang) ? '' : 'v-pre'
lang = lang.replace(vueRE, '')
if (hasSingleTheme) { if (hasSingleTheme) {
return highlighter return highlighter
.codeToHtml(str, { lang, theme: getThemeName(theme) }) .codeToHtml(str, { lang, theme: getThemeName(theme) })
.replace(preRE, '<pre v-pre>') .replace(preRE, `<pre ${vPre}>`)
} }
const dark = highlighter const dark = highlighter
.codeToHtml(str, { lang, theme: getThemeName(theme.dark) }) .codeToHtml(str, { lang, theme: getThemeName(theme.dark) })
.replace(preRE, '<pre v-pre class="vp-code-dark">') .replace(preRE, `<pre ${vPre} class="vp-code-dark">`)
const light = highlighter const light = highlighter
.codeToHtml(str, { lang, theme: getThemeName(theme.light) }) .codeToHtml(str, { lang, theme: getThemeName(theme.light) })
.replace(preRE, '<pre v-pre class="vp-code-light">') .replace(preRE, `<pre ${vPre} class="vp-code-light">`)
return dark + light return dark + light
} }

@ -13,8 +13,10 @@ export const preWrapperPlugin = (md: MarkdownIt) => {
const fence = md.renderer.rules.fence! const fence = md.renderer.rules.fence!
md.renderer.rules.fence = (...args) => { md.renderer.rules.fence = (...args) => {
const [tokens, idx] = args const [tokens, idx] = args
const token = tokens[idx] const lang = tokens[idx].info.trim().replace(/-vue$/, '')
const rawCode = fence(...args) const rawCode = fence(...args)
return `<div class="language-${token.info.trim()}"><span class="copy"></span>${rawCode}</div>` return `<div class="language-${lang}"><span class="lang">${
lang === 'vue-html' ? 'template' : lang
}</span><span class="copy"></span>${rawCode}</div>`
} }
} }

@ -104,25 +104,26 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => {
/** /**
* raw path format: "/path/to/file.extension#region {meta}" * raw path format: "/path/to/file.extension#region {meta}"
* where #region and {meta} are optional * where #region and {meta} are optional
* and meta can be like '1,2,4-6 lang', 'lang' or '1,2,4-6'
* *
* captures: ['/path/to/file.extension', 'extension', '#region', '{meta}'] * captures: ['/path/to/file.extension', 'extension', '#region', '{meta}']
*/ */
const rawPathRegexp = const rawPathRegexp =
/^(.+(?:\.([a-z]+)))(?:(#[\w-]+))?(?: ?({\d+(?:[,-]\d+)*}))?$/ /^(.+(?:\.([a-z]+)))(?:(#[\w-]+))?(?: ?(?:{(\d+(?:[,-]\d+)*)? ?(\S+)?}))?$/
const rawPath = state.src const rawPath = state.src
.slice(start, end) .slice(start, end)
.trim() .trim()
.replace(/^@/, srcDir) .replace(/^@/, srcDir)
.trim() .trim()
const [filename = '', extension = '', region = '', meta = ''] = (
rawPathRegexp.exec(rawPath) || [] const [filename = '', extension = '', region = '', lines = '', lang = ''] =
).slice(1) (rawPathRegexp.exec(rawPath) || []).slice(1)
state.line = startLine + 1 state.line = startLine + 1
const token = state.push('fence', 'code', 0) const token = state.push('fence', 'code', 0)
token.info = extension + meta token.info = `${lang || extension}${lines ? `{${lines}}` : ''}`
// @ts-ignore // @ts-ignore
token.src = path.resolve(filename) + region token.src = path.resolve(filename) + region

Loading…
Cancel
Save