From f789932ffc79723a90b3b19a59d6f277d9edaaa9 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sat, 23 Jul 2022 20:22:16 +0530 Subject: [PATCH] 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 --- .github/contributing.md | 8 +-- .prettierignore | 1 - docs/guide/getting-started.md | 10 ++-- docs/guide/markdown.md | 29 ++++------ docs/guide/using-vue.md | 2 +- .../theme-default/composables/copy-code.ts | 16 +++--- src/client/theme-default/styles/base.css | 8 +-- .../styles/components/vp-doc.css | 42 ++------------ .../styles/components/vp-sponsor.css | 55 ++++++++++++++----- src/client/theme-default/styles/vars.css | 8 +-- src/node/markdown/plugins/highlight.ts | 10 ++-- src/node/markdown/plugins/preWrapper.ts | 6 +- src/node/markdown/plugins/snippet.ts | 11 ++-- 13 files changed, 100 insertions(+), 106 deletions(-) diff --git a/.github/contributing.md b/.github/contributing.md index ae77e6be001..98ef42bb052 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -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 ``` diff --git a/.prettierignore b/.prettierignore index 3b6e8917970..5ad6f264cdb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,5 @@ /docs /examples -*.css *.md *.vue dist diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 5b653a8113f..9172035c7e4 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -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 ``` diff --git a/docs/guide/markdown.md b/docs/guide/markdown.md index 11c60ff7cb4..b86366fdb98 100644 --- a/docs/guide/markdown.md +++ b/docs/guide/markdown.md @@ -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** - - <<< @/snippets/snippet.js - - **Output** - - <<< @/snippets/snippet.js{2} - - ::: 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** - - <<< @/snippets/snippet-with-region.js - - **Output** - - <<< @/snippets/snippet-with-region.js#snippet{1} - +You can also specify the language inside the braces (`{}`) like this: + +```md +<<< @/snippets/snippet.cs{c#} + + +<<< @/snippets/snippet.cs{1,2,4-6 c#} +``` + +This is helpful if source language cannot be inferred from your file extension. ## Markdown File Inclusion diff --git a/docs/guide/using-vue.md b/docs/guide/using-vue.md index 998d3aa8638..0ae94833755 100644 --- a/docs/guide/using-vue.md +++ b/docs/guide/using-vue.md @@ -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** diff --git a/src/client/theme-default/composables/copy-code.ts b/src/client/theme-default/composables/copy-code.ts index 35aca1c1057..75f9f1c9688 100644 --- a/src/client/theme-default/composables/copy-code.ts +++ b/src/client/theme-default/composables/copy-code.ts @@ -11,7 +11,7 @@ export function useCopyCode() { nextTick(() => { document .querySelectorAll( - '.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(() => { diff --git a/src/client/theme-default/styles/base.css b/src/client/theme-default/styles/base.css index 9b6044ed2b5..c7092571ac7 100644 --- a/src/client/theme-default/styles/base.css +++ b/src/client/theme-default/styles/base.css @@ -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; } diff --git a/src/client/theme-default/styles/components/vp-doc.css b/src/client/theme-default/styles/components/vp-doc.css index c8c64a94faa..904523365ed 100644 --- a/src/client/theme-default/styles/components/vp-doc.css +++ b/src/client/theme-default/styles/components/vp-doc.css @@ -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 * -------------------------------------------------------------------------- */ diff --git a/src/client/theme-default/styles/components/vp-sponsor.css b/src/client/theme-default/styles/components/vp-sponsor.css index 09fb6cb148e..32da1de9580 100644 --- a/src/client/theme-default/styles/components/vp-sponsor.css +++ b/src/client/theme-default/styles/components/vp-sponsor.css @@ -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); } diff --git a/src/client/theme-default/styles/vars.css b/src/client/theme-default/styles/vars.css index f6d5248cad4..5b09364b911 100644 --- a/src/client/theme-default/styles/vars.css +++ b/src/client/theme-default/styles/vars.css @@ -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"); } /** diff --git a/src/node/markdown/plugins/highlight.ts b/src/node/markdown/plugins/highlight.ts index 5b1d0e144d6..8e34e813ee5 100644 --- a/src/node/markdown/plugins/highlight.ts +++ b/src/node/markdown/plugins/highlight.ts @@ -10,23 +10,25 @@ export async function highlight(theme: ThemeOptions = 'material-palenight') { themes: hasSingleTheme ? [theme] : [theme.dark, theme.light] }) const preRE = /^/ + 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, '
')
+        .replace(preRE, `
`)
     }
 
     const dark = highlighter
       .codeToHtml(str, { lang, theme: getThemeName(theme.dark) })
-      .replace(preRE, '
')
+      .replace(preRE, `
`)
 
     const light = highlighter
       .codeToHtml(str, { lang, theme: getThemeName(theme.light) })
-      .replace(preRE, '
')
+      .replace(preRE, `
`)
 
     return dark + light
   }
diff --git a/src/node/markdown/plugins/preWrapper.ts b/src/node/markdown/plugins/preWrapper.ts
index ced0ef56580..9d51874d2ce 100644
--- a/src/node/markdown/plugins/preWrapper.ts
+++ b/src/node/markdown/plugins/preWrapper.ts
@@ -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 `
${rawCode}
` + return `
${ + lang === 'vue-html' ? 'template' : lang + }${rawCode}
` } } diff --git a/src/node/markdown/plugins/snippet.ts b/src/node/markdown/plugins/snippet.ts index c8e521534eb..5410c3384c9 100644 --- a/src/node/markdown/plugins/snippet.ts +++ b/src/node/markdown/plugins/snippet.ts @@ -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