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:
```bash
```sh
# install the dependencies of the project
$ pnpm install
```
@ -36,7 +36,7 @@ $ pnpm install
At first, execute the `pnpm run build` command.
```bash
```sh
$ 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.
```bash
```sh
$ 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`.
```bash
```sh
$ pnpm run dev
```

@ -1,6 +1,5 @@
/docs
/examples
*.css
*.md
*.vue
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.
```bash
```sh
$ mkdir vitepress-starter && cd vitepress-starter
```
Then, initialize with your preferred package manager.
```bash
```sh
$ yarn init
```
@ -24,7 +24,7 @@ $ yarn init
Add VitePress and Vue as dev dependencies for the project.
```bash
```sh
$ yarn add --dev vitepress vue
```
@ -64,7 +64,7 @@ On PNPM, add this in your `package.json`:
Create your first document.
```bash
```sh
$ 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.
```bash
```sh
$ yarn docs:dev
```

@ -279,7 +279,7 @@ In addition to a single line, you can also specify multiple single lines, ranges
**Input**
````
```js{1,4,6-7}
```js{1,4,6-8}
export default { // Highlighted
data () {
return {
@ -296,7 +296,7 @@ export default { // Highlighted
**Output**
```js{1,4,6-7}
```js{1,4,6-8}
export default { // Highlighted
data () {
return {
@ -346,20 +346,12 @@ It also supports [line highlighting](#line-highlighting-in-code-blocks):
**Code file**
<!--lint disable strong-marker-->
<<< @/snippets/snippet.js
<!--lint enable strong-marker-->
**Output**
<!--lint disable strong-marker-->
<<< @/snippets/snippet.js{2}
<!--lint enable strong-marker-->
::: tip
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**
<!--lint disable strong-marker-->
<<< @/snippets/snippet-with-region.js
<!--lint enable strong-marker-->
**Output**
<!--lint disable strong-marker-->
<<< @/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

@ -62,7 +62,7 @@ const { page } = useData()
## 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**

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

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

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

@ -46,38 +46,63 @@
gap: 4px;
}
.vp-sponsor-grid.xmini .vp-sponsor-grid-link { height: 64px; }
.vp-sponsor-grid.xmini .vp-sponsor-grid-image { max-width: 64px; max-height: 22px }
.vp-sponsor-grid.xmini .vp-sponsor-grid-link {
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-image { max-width: 96px; max-height: 24px }
.vp-sponsor-grid.mini .vp-sponsor-grid-link {
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-image { max-width: 96px; max-height: 24px }
.vp-sponsor-grid.small .vp-sponsor-grid-link {
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-image { max-width: 120px; max-height: 36px }
.vp-sponsor-grid.medium .vp-sponsor-grid-link {
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-image { max-width: 192px; max-height: 56px }
.vp-sponsor-grid.big .vp-sponsor-grid-link {
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);
}
.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);
}
.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);
}
.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);
}
.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);
}

@ -34,12 +34,12 @@
--vp-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--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-4: rgba(60, 60, 60, 0.18);
--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-4: rgba(235, 235, 235, 0.18);
@ -180,8 +180,8 @@
* -------------------------------------------------------------------------- */
: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-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-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' 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]
})
const preRE = /^<pre.*?>/
const vueRE = /-vue$/
return (str: string, lang: string) => {
lang = lang || 'text'
const vPre = vueRE.test(lang) ? '' : 'v-pre'
lang = lang.replace(vueRE, '')
if (hasSingleTheme) {
return highlighter
.codeToHtml(str, { lang, theme: getThemeName(theme) })
.replace(preRE, '<pre v-pre>')
.replace(preRE, `<pre ${vPre}>`)
}
const dark = highlighter
.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
.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
}

@ -13,8 +13,10 @@ export const preWrapperPlugin = (md: MarkdownIt) => {
const fence = md.renderer.rules.fence!
md.renderer.rules.fence = (...args) => {
const [tokens, idx] = args
const token = tokens[idx]
const lang = tokens[idx].info.trim().replace(/-vue$/, '')
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}"
* 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}']
*/
const rawPathRegexp =
/^(.+(?:\.([a-z]+)))(?:(#[\w-]+))?(?: ?({\d+(?:[,-]\d+)*}))?$/
/^(.+(?:\.([a-z]+)))(?:(#[\w-]+))?(?: ?(?:{(\d+(?:[,-]\d+)*)? ?(\S+)?}))?$/
const rawPath = state.src
.slice(start, end)
.trim()
.replace(/^@/, srcDir)
.trim()
const [filename = '', extension = '', region = '', meta = ''] = (
rawPathRegexp.exec(rawPath) || []
).slice(1)
const [filename = '', extension = '', region = '', lines = '', lang = ''] =
(rawPathRegexp.exec(rawPath) || []).slice(1)
state.line = startLine + 1
const token = state.push('fence', 'code', 0)
token.info = extension + meta
token.info = `${lang || extension}${lines ? `{${lines}}` : ''}`
// @ts-ignore
token.src = path.resolve(filename) + region

Loading…
Cancel
Save