Merge branch 'main' into main

pull/2317/head
Divyansh Singh 3 years ago committed by GitHub
commit b225d41b14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -23,7 +23,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node_version: [16, 18] node_version: [16, 18, 20]
steps: steps:
- name: Checkout - name: Checkout

@ -1 +1,2 @@
shell-emulator=true shell-emulator=true
resolution-mode=highest

@ -1,3 +1,60 @@
# [1.0.0-beta.1](https://github.com/vuejs/vitepress/compare/v1.0.0-alpha.76...v1.0.0-beta.1) (2023-05-22)
### Bug Fixes
* **config:** set scrollOffset to 0 is not effect ([#2395](https://github.com/vuejs/vitepress/issues/2395)) ([8153f23](https://github.com/vuejs/vitepress/commit/8153f23c901a6200661813e65f0d8eb602ad46da))
* **theme:** features section layout is not consistent ([#2382](https://github.com/vuejs/vitepress/issues/2382)) ([26f21d9](https://github.com/vuejs/vitepress/commit/26f21d95dfbd671477d425e6b8ac5b0172a846ac))
* **theme:** some global properties in localSearch are missing ([#2396](https://github.com/vuejs/vitepress/issues/2396)) ([4896811](https://github.com/vuejs/vitepress/commit/489681117f46a803704b6ec80546a5e787e19df2))
* **theme:** custom target and rel in navbar links for mobile is missing ([#2400](https://github.com/vuejs/vitepress/issues/2400)) ([f364a5d](https://github.com/vuejs/vitepress/commit/f364a5d1d3c066c9728beb5d07576d6cb4b0640d))
# [1.0.0-alpha.76](https://github.com/vuejs/vitepress/compare/v1.0.0-alpha.75...v1.0.0-alpha.76) (2023-05-18)
### Bug Fixes
* **a11y:** mobile and theme switcher ([#2354](https://github.com/vuejs/vitepress/issues/2354)) ([d6c0985](https://github.com/vuejs/vitepress/commit/d6c0985002ee792b1e8e052f71cdd6bd72c315ad))
* **build:** uniform handling of windows slash in localSearchPlugin ([#2358](https://github.com/vuejs/vitepress/issues/2358)) ([b31933f](https://github.com/vuejs/vitepress/commit/b31933fbdd7aabfe080234407153aefa8f6a3f30))
* hmr when `base` is set ([#2375](https://github.com/vuejs/vitepress/issues/2375)) ([484ff5d](https://github.com/vuejs/vitepress/commit/484ff5dd4bd2e5c2d5168437895d400a39f2bfa8))
* **theme:** don't update opacity on hover ([#2326](https://github.com/vuejs/vitepress/issues/2326)) ([35f8b89](https://github.com/vuejs/vitepress/commit/35f8b896372e75e62882df613a49e8945e7bc832))
### Features
* **cli:** add shortcuts ([#2353](https://github.com/vuejs/vitepress/issues/2353)) ([97065ce](https://github.com/vuejs/vitepress/commit/97065cefc22e4772c0295c5ad23a87eea286f46b))
* **theme:** add focus trap to local search dialog ([#2324](https://github.com/vuejs/vitepress/issues/2324)) ([2f482af](https://github.com/vuejs/vitepress/commit/2f482afaabdb4206b87e2453d0099257693c4653))
* **theme:** open search box on pressing slash too ([#2328](https://github.com/vuejs/vitepress/issues/2328)) ([c20bd28](https://github.com/vuejs/vitepress/commit/c20bd283319158135e2d850485970dfc5fe82812))
# [1.0.0-alpha.75](https://github.com/vuejs/vitepress/compare/v1.0.0-alpha.74...v1.0.0-alpha.75) (2023-04-30)
### Bug Fixes
* **build:** reset regex lastIndex before testing ([188893c](https://github.com/vuejs/vitepress/commit/188893c2c1569e332e8776581cfa40b4c5f1168e))
* **cli/init:** remove trailing slash from npm scripts ([64ecedc](https://github.com/vuejs/vitepress/commit/64ecedc73f3e7010de85381d946af1c95404820e))
* **theme:** hide local nav on home page ([f07587a](https://github.com/vuejs/vitepress/commit/f07587af8a51f92a5ec491e5789dd088e28067b5)), closes [#2312](https://github.com/vuejs/vitepress/issues/2312)
* **theme:** local search get 404 on build when use route rewrites in windows ([#2301](https://github.com/vuejs/vitepress/issues/2301)) ([494c634](https://github.com/vuejs/vitepress/commit/494c634eb1d77963e555a736fa057dcb23700989))
* **theme:** vitepress data not properly injected in app when use localSearch ([#2299](https://github.com/vuejs/vitepress/issues/2299)) ([69c7646](https://github.com/vuejs/vitepress/commit/69c7646dafe7a774e0717e032f697b008d9cf7aa))
### Features
* add `filePath` to `PageData` ([#2140](https://github.com/vuejs/vitepress/issues/2140)) ([b24acc6](https://github.com/vuejs/vitepress/commit/b24acc6991570aa054a99b8d3977b8b4d0255418))
* **build:** allow using `@` prefix with `@include` ([#2292](https://github.com/vuejs/vitepress/issues/2292)) ([a3b38d1](https://github.com/vuejs/vitepress/commit/a3b38d18824343fd5b571a7a9a5d2c4ccf29e8e1))
* preserve user log level ([#2310](https://github.com/vuejs/vitepress/issues/2310)) ([a647cd3](https://github.com/vuejs/vitepress/commit/a647cd384320101f6df31e03960dd2c40808c49c))
* **theme:** support light shiki themes ([#2319](https://github.com/vuejs/vitepress/issues/2319)) ([d0f0012](https://github.com/vuejs/vitepress/commit/d0f0012aea4cc71fb28f60f2dd649c23aae146b8))
### BREAKING CHANGES
* **theme:** Styling for code blocks might break, especially if you were earlier overriding it for light theme. Those workarounds are no longer required. VitePress will now show code blocks and groups in light mode too if a light shiki theme is specified.
# [1.0.0-alpha.74](https://github.com/vuejs/vitepress/compare/v1.0.0-alpha.73...v1.0.0-alpha.74) (2023-04-24) # [1.0.0-alpha.74](https://github.com/vuejs/vitepress/compare/v1.0.0-alpha.73...v1.0.0-alpha.74) (2023-04-24)

@ -1,4 +1,4 @@
# VitePress (alpha) 📝💨 # VitePress (beta) 📝💨
[![Test](https://github.com/vuejs/vitepress/workflows/Test/badge.svg)](https://github.com/vuejs/vitepress/actions) [![Test](https://github.com/vuejs/vitepress/workflows/Test/badge.svg)](https://github.com/vuejs/vitepress/actions)
[![npm](https://img.shields.io/npm/v/vitepress)](https://www.npmjs.com/package/vitepress) [![npm](https://img.shields.io/npm/v/vitepress)](https://www.npmjs.com/package/vitepress)
@ -8,7 +8,7 @@
VitePress is [VuePress](https://vuepress.vuejs.org)' spiritual successor, built on top of [vite](https://github.com/vitejs/vite). VitePress is [VuePress](https://vuepress.vuejs.org)' spiritual successor, built on top of [vite](https://github.com/vitejs/vite).
Currently, it's in the `alpha` stage. It is already suitable for out-of-the-box documentation use, but the config and theming API may still change between minor releases. Currently, it is in the `beta` stage. It is already suitable for out-of-the-box documentation use. There still might be issues, however, we do not plan to introduce any breaking changes from here on until the stable release.
## Documentation ## Documentation

@ -176,3 +176,7 @@ export default config
## Markdown File Inclusion ## Markdown File Inclusion
<!--@include: ./foo.md--> <!--@include: ./foo.md-->
## Markdown At File Inclusion
<!--@include: @/markdown-extensions/bar.md-->

@ -63,7 +63,7 @@ describe('Table of Contents', () => {
test('render toc', async () => { test('render toc', async () => {
const items = page.locator('#table-of-contents + nav ul li') const items = page.locator('#table-of-contents + nav ul li')
const count = await items.count() const count = await items.count()
expect(count).toBe(23) expect(count).toBe(24)
}) })
}) })
@ -229,4 +229,8 @@ describe('Markdown File Inclusion', () => {
const h1 = page.locator('#markdown-file-inclusion + h1') const h1 = page.locator('#markdown-file-inclusion + h1')
expect(await h1.getAttribute('id')).toBe('foo') expect(await h1.getAttribute('id')).toBe('foo')
}) })
test('render markdown using @', async () => {
const h1 = page.locator('#markdown-at-file-inclusion + h1')
expect(await h1.getAttribute('id')).toBe('bar')
})
}) })

@ -18,7 +18,7 @@ All **static** path references, including absolute paths, should be based on you
## The Public Directory ## The Public Directory
Sometimes you may need to provide static assets that are not directly referenced in any of your Markdown or theme components, or you may want to serve certain files with the original filename. Examples of such files include `robot.txt`, favicons, and PWA icons. Sometimes you may need to provide static assets that are not directly referenced in any of your Markdown or theme components, or you may want to serve certain files with the original filename. Examples of such files include `robots.txt`, favicons, and PWA icons.
You can place these files in the `public` directory under the [source directory](./routing#source-directory). For example, if your project root is `./docs` and using default source directory location, then your public directory will be `./docs/public`. You can place these files in the `public` directory under the [source directory](./routing#source-directory). For example, if your project root is `./docs` and using default source directory location, then your public directory will be `./docs/public`.

@ -566,7 +566,12 @@ It also supports [line highlighting](#line-highlighting-in-code-blocks):
<<< @/snippets/snippet.js{2} <<< @/snippets/snippet.js{2}
::: 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. Alternatively, you can also import from relative paths:
```md
<<< ../snippets/snippet.js
```
::: :::
You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/codebasics#_folding) to only include the corresponding part of the code file. You can provide a custom region name after a `#` following the filepath: You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/codebasics#_folding) to only include the corresponding part of the code file. You can provide a custom region name after a `#` following the filepath:
@ -691,7 +696,13 @@ You can also [import snippets](#import-code-snippets) in code groups:
## Markdown File Inclusion ## Markdown File Inclusion
You can include a markdown file in another markdown file like this: You can include a markdown file in another markdown file.
::: tip
You can also prefix the markdown path with `@`, it will act as the source root. By default it's the VitePress project root, unless `srcDir` is configured.
:::
For example, you can include a relative markdown file using this:
**Input** **Input**

@ -214,7 +214,9 @@ export default {
icon: { icon: {
svg: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Dribbble</title><path d="M12...6.38z"/></svg>' svg: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Dribbble</title><path d="M12...6.38z"/></svg>'
}, },
link: '...' link: '...',
// You can include a custom label for accessibility too (optional but recommended):
ariaLabel: 'cool link'
} }
] ]
} }
@ -225,6 +227,7 @@ export default {
interface SocialLink { interface SocialLink {
icon: SocialLinkIcon icon: SocialLinkIcon
link: string link: string
ariaLabel?: string
} }
type SocialLinkIcon = type SocialLinkIcon =

@ -16,17 +16,17 @@ export default {
The `pattern` option defines the URL structure for the link, and `:path` is going to be replaced with the page path. The `pattern` option defines the URL structure for the link, and `:path` is going to be replaced with the page path.
You can also put a pure function that accepts `relativePath` as the argument and returns the URL string. You can also put a pure function that accepts [`PageData`](./runtime-api#usedata) as the argument and returns the URL string.
```js ```js
export default { export default {
themeConfig: { themeConfig: {
editLink: { editLink: {
pattern: ({ relativePath }) => { pattern: ({ filePath }) => {
if (relativePath.startsWith('packages/')) { if (filePath.startsWith('packages/')) {
return `https://github.com/acme/monorepo/edit/main/${relativePath}` return `https://github.com/acme/monorepo/edit/main/${filePath}`
} else { } else {
return `https://github.com/acme/monorepo/edit/main/docs/${relativePath}` return `https://github.com/acme/monorepo/edit/main/docs/${filePath}`
} }
} }
} }

@ -36,4 +36,8 @@ export default {
} }
``` ```
::: warning
Only inline elements can be used in `message` and `copyright` as they are rendered inside a `<p>` element. If you want to add block elements, consider using [`layout-bottom`](../guide/extending-default-theme#layout-slots) slot instead.
:::
Note that footer will not be displayed when the [SideBar](./default-theme-sidebar) is visible. Note that footer will not be displayed when the [SideBar](./default-theme-sidebar) is visible.

@ -45,6 +45,7 @@ interface PageData {
titleTemplate?: string | boolean titleTemplate?: string | boolean
description: string description: string
relativePath: string relativePath: string
filePath: string,
headers: Header[] headers: Header[]
frontmatter: Record<string, any> frontmatter: Record<string, any>
params?: Record<string, any> params?: Record<string, any>

@ -94,6 +94,7 @@ export default {
title: 'My Awesome Site' title: 'My Awesome Site'
} }
``` ```
```md ```md
# Hello # Hello
``` ```
@ -113,6 +114,7 @@ export default {
titleTemplate: 'Custom Suffix' titleTemplate: 'Custom Suffix'
} }
``` ```
```md ```md
# Hello # Hello
``` ```
@ -159,8 +161,29 @@ export default {
[ [
'link', 'link',
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' } { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }
// would render:
//
// <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
],
[
'script',
{ id: 'register-sw' },
`;(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
}
})()`
// would render:
//
// <script id="register-sw">
// ;(() => {
// if ('serviceWorker' in navigator) {
// navigator.serviceWorker.register('/sw.js')
// }
// })()
// </script>
] ]
// would render: <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
] ]
} }
``` ```
@ -295,7 +318,7 @@ export default {
} }
``` ```
It can also be an array of extact url string, regex patterns, or custom filter functions. It can also be an array of exact url string, regex patterns, or custom filter functions.
```ts ```ts
export default { export default {
@ -361,7 +384,7 @@ export default {
// adjust how header anchors are generated, // adjust how header anchors are generated,
// useful for integrating with tools that use different conventions // useful for integrating with tools that use different conventions
anchors: { anchor: {
slugify(str) { slugify(str) {
return encodeURIComponent(str) return encodeURIComponent(str)
} }

@ -1,9 +1,9 @@
{ {
"name": "vitepress", "name": "vitepress",
"version": "1.0.0-alpha.74", "version": "1.0.0-beta.1",
"description": "Vite & Vue powered static site generator", "description": "Vite & Vue powered static site generator",
"type": "module", "type": "module",
"packageManager": "pnpm@8.3.1", "packageManager": "pnpm@8.6.1",
"main": "dist/node/index.js", "main": "dist/node/index.js",
"types": "types/index.d.ts", "types": "types/index.d.ts",
"exports": { "exports": {
@ -23,7 +23,7 @@
"default": "./dist/client/theme-default/index.js" "default": "./dist/client/theme-default/index.js"
}, },
"./theme-without-fonts": { "./theme-without-fonts": {
"types": "./theme.d.ts", "types": "./theme-without-fonts.d.ts",
"default": "./dist/client/theme-default/without-fonts.js" "default": "./dist/client/theme-default/without-fonts.js"
} }
}, },
@ -36,7 +36,8 @@
"types", "types",
"template", "template",
"client.d.ts", "client.d.ts",
"theme.d.ts" "theme.d.ts",
"theme-without-fonts.d.ts"
], ],
"repository": { "repository": {
"type": "git", "type": "git",
@ -87,17 +88,19 @@
"docs-preview": "node ./bin/vitepress preview docs" "docs-preview": "node ./bin/vitepress preview docs"
}, },
"dependencies": { "dependencies": {
"@docsearch/css": "^3.3.3", "@docsearch/css": "^3.4.0",
"@docsearch/js": "^3.3.3", "@docsearch/js": "^3.4.0",
"@vitejs/plugin-vue": "^4.1.0", "@vitejs/plugin-vue": "^4.2.3",
"@vue/devtools-api": "^6.5.0", "@vue/devtools-api": "^6.5.0",
"@vueuse/core": "^10.1.0", "@vueuse/core": "^10.1.2",
"@vueuse/integrations": "^10.1.2",
"body-scroll-lock": "4.0.0-beta.0", "body-scroll-lock": "4.0.0-beta.0",
"focus-trap": "^7.4.3",
"mark.js": "8.11.1", "mark.js": "8.11.1",
"minisearch": "^6.0.1", "minisearch": "^6.1.0",
"shiki": "^0.14.1", "shiki": "^0.14.2",
"vite": "^4.3.1", "vite": "^4.3.9",
"vue": "^3.2.47" "vue": "^3.3.4"
}, },
"devDependencies": { "devDependencies": {
"@clack/prompts": "^0.6.3", "@clack/prompts": "^0.6.3",
@ -109,14 +112,14 @@
"@mdit-vue/plugin-toc": "^0.12.0", "@mdit-vue/plugin-toc": "^0.12.0",
"@mdit-vue/shared": "^0.12.0", "@mdit-vue/shared": "^0.12.0",
"@rollup/plugin-alias": "^5.0.0", "@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-commonjs": "^24.1.0", "@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-json": "^6.0.0", "@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.2", "@rollup/plugin-node-resolve": "^15.1.0",
"@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-replace": "^5.0.2",
"@types/body-scroll-lock": "^3.1.0", "@types/body-scroll-lock": "^3.1.0",
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
"@types/cross-spawn": "^6.0.2", "@types/cross-spawn": "^6.0.2",
"@types/debug": "^4.1.7", "@types/debug": "^4.1.8",
"@types/escape-html": "^1.0.2", "@types/escape-html": "^1.0.2",
"@types/fs-extra": "^11.0.1", "@types/fs-extra": "^11.0.1",
"@types/mark.js": "^8.11.8", "@types/mark.js": "^8.11.8",
@ -126,24 +129,23 @@
"@types/markdown-it-emoji": "^2.0.2", "@types/markdown-it-emoji": "^2.0.2",
"@types/micromatch": "^4.0.2", "@types/micromatch": "^4.0.2",
"@types/minimist": "^1.2.2", "@types/minimist": "^1.2.2",
"@types/node": "^18.16.0", "@types/node": "^20.2.5",
"@types/prompts": "^2.4.4", "@types/prompts": "^2.4.4",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"compression": "^1.7.4", "compression": "^1.7.4",
"conventional-changelog-cli": "^2.2.2", "conventional-changelog-cli": "^3.0.0",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"debug": "^4.3.4", "debug": "^4.3.4",
"enquirer": "^2.3.6", "esbuild": "^0.17.19",
"esbuild": "^0.17.18",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"execa": "^7.1.1", "execa": "^7.1.1",
"fast-glob": "^3.2.12", "fast-glob": "^3.2.12",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"get-port": "^6.1.2", "get-port": "^7.0.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"lint-staged": "^13.2.1", "lint-staged": "^13.2.2",
"lodash.template": "^4.5.0", "lodash.template": "^4.5.0",
"lru-cache": "^9.1.1", "lru-cache": "^9.1.2",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"markdown-it-anchor": "^8.6.7", "markdown-it-anchor": "^8.6.7",
"markdown-it-attrs": "^4.1.6", "markdown-it-attrs": "^4.1.6",
@ -153,27 +155,27 @@
"minimist": "^1.2.8", "minimist": "^1.2.8",
"nanoid": "^4.0.2", "nanoid": "^4.0.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"ora": "^6.3.0", "ora": "^6.3.1",
"path-to-regexp": "^6.2.1", "path-to-regexp": "^6.2.1",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"pkg-dir": "^7.0.0", "pkg-dir": "^7.0.0",
"playwright-chromium": "^1.32.3", "playwright-chromium": "^1.34.3",
"polka": "1.0.0-next.22", "polka": "1.0.0-next.22",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"prompts": "^2.4.2", "prompts": "^2.4.2",
"punycode": "^2.3.0", "punycode": "^2.3.0",
"rimraf": "^5.0.0", "rimraf": "^5.0.1",
"rollup": "^3.21.0", "rollup": "^3.24.0",
"rollup-plugin-dts": "^5.3.0", "rollup-plugin-dts": "^5.3.0",
"rollup-plugin-esbuild": "^5.0.0", "rollup-plugin-esbuild": "^5.0.0",
"semver": "^7.5.0", "semver": "^7.5.1",
"shiki-processor": "^0.1.3", "shiki-processor": "^0.1.3",
"simple-git-hooks": "^2.8.1", "simple-git-hooks": "^2.8.1",
"sirv": "^2.0.2", "sirv": "^2.0.3",
"supports-color": "^9.3.1", "supports-color": "^9.3.1",
"typescript": "^5.0.4", "typescript": "^5.1.3",
"vitest": "^0.30.1", "vitest": "^0.32.0",
"vue-tsc": "^1.4.4", "vue-tsc": "^1.6.5",
"wait-on": "^7.0.1" "wait-on": "^7.0.1"
}, },
"pnpm": { "pnpm": {

File diff suppressed because it is too large Load Diff

@ -1,13 +1,15 @@
import { readFileSync, writeFileSync } from 'fs' import { readFileSync, writeFileSync } from 'fs'
import { resolve } from 'path' import { resolve } from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { createRequire } from 'module'
import c from 'picocolors' import c from 'picocolors'
import prompts from 'prompts' import prompts from 'prompts'
import { execa } from 'execa' import { execa } from 'execa'
import semver from 'semver' import semver from 'semver'
import pkg from '../package.json' assert { type: 'json' }
const { version: currentVersion } = pkg const { version: currentVersion } = createRequire(import.meta.url)(
'../package.json'
)
const { inc: _inc, valid } = semver const { inc: _inc, valid } = semver
const versionIncrements = ['patch', 'minor', 'major'] const versionIncrements = ['patch', 'minor', 'major']

@ -303,6 +303,8 @@ function handleHMR(route: Route): void {
function shouldHotReload(payload: PageDataPayload): boolean { function shouldHotReload(payload: PageDataPayload): boolean {
const payloadPath = payload.path.replace(/(\bindex)?\.md$/, '') const payloadPath = payload.path.replace(/(\bindex)?\.md$/, '')
const locationPath = location.pathname.replace(/(\bindex)?\.html$/, '') const locationPath = location.pathname
.replace(/(\bindex)?\.html$/, '')
.slice(siteDataRef.value.base.length - 1)
return payloadPath === locationPath return payloadPath === locationPath
} }

@ -26,7 +26,7 @@ const grid = computed(() => {
return 'grid-3' return 'grid-3'
} else if (length % 3 === 0) { } else if (length % 3 === 0) {
return 'grid-6' return 'grid-6'
} else if (length % 2 === 0) { } else if (length > 3) {
return 'grid-4' return 'grid-4'
} }
}) })

@ -47,7 +47,4 @@ const { hasSidebar } = useSidebar()
font-weight: 500; font-weight: 500;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
} }
.message { order: 2; }
.copyright { order: 1; }
</style> </style>

@ -1,8 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core'
import { computed, shallowRef } from 'vue'
import { onContentUpdated } from 'vitepress'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { getHeaders, type MenuItem } from '../composables/outline'
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar'
import VPIconAlignLeft from './icons/VPIconAlignLeft.vue'
import VPLocalNavOutlineDropdown from './VPLocalNavOutlineDropdown.vue' import VPLocalNavOutlineDropdown from './VPLocalNavOutlineDropdown.vue'
import VPIconAlignLeft from './icons/VPIconAlignLeft.vue'
defineProps<{ defineProps<{
open: boolean open: boolean
@ -12,12 +16,34 @@ defineEmits<{
(e: 'open-menu'): void (e: 'open-menu'): void
}>() }>()
const { theme } = useData() const { theme, frontmatter } = useData()
const { hasSidebar } = useSidebar() const { hasSidebar } = useSidebar()
const { y } = useWindowScroll()
const headers = shallowRef<MenuItem[]>([])
onContentUpdated(() => {
headers.value = getHeaders(frontmatter.value.outline ?? theme.value.outline)
})
const empty = computed(() => {
return headers.value.length === 0 && !hasSidebar.value
})
const classes = computed(() => {
return {
VPLocalNav: true,
fixed: empty.value,
'reached-top': y.value >= 64
}
})
</script> </script>
<template> <template>
<div class="VPLocalNav"> <div
v-if="frontmatter.layout !== 'home' && (!empty || y >= 64)"
:class="classes"
>
<button <button
v-if="hasSidebar" v-if="hasSidebar"
class="menu" class="menu"
@ -31,7 +57,7 @@ const { hasSidebar } = useSidebar()
</span> </span>
</button> </button>
<VPLocalNavOutlineDropdown /> <VPLocalNavOutlineDropdown :headers="headers" />
</div> </div>
</template> </template>
@ -45,11 +71,19 @@ const { hasSidebar } = useSidebar()
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
border-top: 1px solid var(--vp-c-gutter);
border-bottom: 1px solid var(--vp-c-gutter); border-bottom: 1px solid var(--vp-c-gutter);
padding-top: var(--vp-layout-top-height, 0px); padding-top: var(--vp-layout-top-height, 0px);
width: 100%; width: 100%;
background-color: var(--vp-local-nav-bg-color); background-color: var(--vp-local-nav-bg-color);
transition: border-color 0.5s, background-color 0.5s; }
.VPLocalNav.fixed {
position: fixed;
}
.VPLocalNav.reached-top {
border-top-color: transparent;
} }
@media (min-width: 960px) { @media (min-width: 960px) {

@ -1,12 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick, shallowRef } from 'vue' import { onContentUpdated } from 'vitepress'
import { nextTick, ref } from 'vue'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { getHeaders, resolveTitle, type MenuItem } from '../composables/outline' import { resolveTitle, type MenuItem } from '../composables/outline'
import VPDocOutlineItem from './VPDocOutlineItem.vue' import VPDocOutlineItem from './VPDocOutlineItem.vue'
import { onContentUpdated } from 'vitepress'
import VPIconChevronRight from './icons/VPIconChevronRight.vue' import VPIconChevronRight from './icons/VPIconChevronRight.vue'
const { frontmatter, theme } = useData() defineProps<{
headers: MenuItem[]
}>()
const { theme } = useData()
const open = ref(false) const open = ref(false)
const vh = ref(0) const vh = ref(0)
const items = ref<HTMLDivElement>() const items = ref<HTMLDivElement>()
@ -36,14 +40,6 @@ function scrollToTop() {
open.value = false open.value = false
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
} }
const headers = shallowRef<MenuItem[]>([])
onContentUpdated(() => {
headers.value = getHeaders(
frontmatter.value.outline ?? theme.value.outline
)
})
</script> </script>
<template> <template>

@ -9,6 +9,7 @@ import {
useScrollLock, useScrollLock,
useSessionStorage useSessionStorage
} from '@vueuse/core' } from '@vueuse/core'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
import Mark from 'mark.js/src/vanilla.js' import Mark from 'mark.js/src/vanilla.js'
import MiniSearch, { type SearchResult } from 'minisearch' import MiniSearch, { type SearchResult } from 'minisearch'
import { useRouter } from 'vitepress' import { useRouter } from 'vitepress'
@ -26,11 +27,10 @@ import {
type Ref type Ref
} from 'vue' } from 'vue'
import type { ModalTranslations } from '../../../../types/local-search' import type { ModalTranslations } from '../../../../types/local-search'
import { dataSymbol } from '../../app/data'
import { pathToFile } from '../../app/utils' import { pathToFile } from '../../app/utils'
import { slash } from '../../shared'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { createTranslate } from '../support/translation' import { createTranslate } from '../support/translation'
import { dataSymbol } from '../../app/data'
defineProps<{ defineProps<{
placeholder: string placeholder: string
@ -64,6 +64,12 @@ interface Result {
} }
const vitePressData = useData() const vitePressData = useData()
const { activate } = useFocusTrap(el, {
immediate: true,
allowOutsideClick: true,
clickOutsideDeactivates: true,
escapeDeactivates: true
})
const { localeIndex, theme } = vitePressData const { localeIndex, theme } = vitePressData
const searchIndex = computedAsync(async () => const searchIndex = computedAsync(async () =>
markRaw( markRaw(
@ -113,8 +119,6 @@ watchEffect(() => {
const results: Ref<(SearchResult & Result)[]> = shallowRef([]) const results: Ref<(SearchResult & Result)[]> = shallowRef([])
const headingRegex = /<h(\d*).*?>.*?<a.*? href="#(.*?)".*?>.*?<\/a><\/h\1>/gi
const enableNoResults = ref(false) const enableNoResults = ref(false)
watch(filterText, () => { watch(filterText, () => {
@ -149,28 +153,42 @@ debouncedWatch(
if (canceled) return if (canceled) return
const c = new Map<string, Map<string, string>>() const c = new Map<string, Map<string, string>>()
for (const { id, mod } of mods) { for (const { id, mod } of mods) {
const mapId = id.slice(0, id.indexOf('#'))
let map = c.get(mapId)
if (map) continue
map = new Map()
c.set(mapId, map)
const comp = mod.default ?? mod const comp = mod.default ?? mod
if (comp?.render) { if (comp?.render || comp?.setup) {
const app = createApp(comp) const app = createApp(comp)
// Silence warnings about missing components // Silence warnings about missing components
app.config.warnHandler = () => {} app.config.warnHandler = () => {}
app.provide(dataSymbol, vitePressData) app.provide(dataSymbol, vitePressData)
Object.defineProperties(app.config.globalProperties, {
$frontmatter: {
get() {
return vitePressData.frontmatter.value
}
},
$params: {
get() {
return vitePressData.page.value.params
}
}
})
const div = document.createElement('div') const div = document.createElement('div')
app.mount(div) app.mount(div)
const sections = div.innerHTML.split(headingRegex) const headings = div.querySelectorAll('h1, h2, h3, h4, h5, h6')
headings.forEach((el) => {
const href = el.querySelector('a')?.getAttribute('href')
const anchor = href?.startsWith('#') && href.slice(1)
if (!anchor) return
let html = ''
while ((el = el.nextElementSibling!) && !/^h[1-6]$/i.test(el.tagName))
html += el.outerHTML
map!.set(anchor, html)
})
app.unmount() app.unmount()
sections.shift()
const mapId = id.slice(0, id.indexOf('#'))
let map = c.get(mapId)
if (!map) {
map = new Map()
c.set(mapId, map)
}
for (let i = 0; i < sections.length; i += 3) {
const anchor = sections[i + 1]
const html = sections[i + 2]
map.set(anchor, html)
}
} }
if (canceled) return if (canceled) return
} }
@ -211,7 +229,7 @@ debouncedWatch(
) )
async function fetchExcerpt(id: string) { async function fetchExcerpt(id: string) {
const file = pathToFile(slash(id.slice(0, id.indexOf('#')))) const file = pathToFile(id.slice(0, id.indexOf('#')))
try { try {
return { id, mod: await import(/*@vite-ignore*/ file) } return { id, mod: await import(/*@vite-ignore*/ file) }
} catch (e) { } catch (e) {
@ -334,6 +352,7 @@ onMounted(() => {
body.value = document.body body.value = document.body
nextTick(() => { nextTick(() => {
isLocked.value = true isLocked.value = true
nextTick().then(() => activate())
}) })
}) })
@ -474,6 +493,7 @@ function formMarkRegex(terms: Set<string>) {
}" }"
:aria-label="[...p.titles, p.title].join(' > ')" :aria-label="[...p.titles, p.title].join(' > ')"
@mouseenter="!disableMouseOver && (selectedIndex = index)" @mouseenter="!disableMouseOver && (selectedIndex = index)"
@focusin="selectedIndex = index"
@click="$emit('close')" @click="$emit('close')"
> >
<div> <div>
@ -498,7 +518,7 @@ function formMarkRegex(terms: Set<string>) {
</div> </div>
<div v-if="showDetailedList" class="excerpt-wrapper"> <div v-if="showDetailedList" class="excerpt-wrapper">
<div v-if="p.text" class="excerpt"> <div v-if="p.text" class="excerpt" inert>
<div class="vp-doc" v-html="p.text" /> <div class="vp-doc" v-html="p.text" />
</div> </div>
<div class="excerpt-gradient-bottom" /> <div class="excerpt-gradient-bottom" />
@ -727,6 +747,7 @@ function formMarkRegex(terms: Set<string>) {
transition: none; transition: none;
line-height: 1rem; line-height: 1rem;
border: solid 2px var(--vp-local-search-result-border); border: solid 2px var(--vp-local-search-result-border);
outline: none;
} }
.result > div { .result > div {

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { isActive } from '../support/utils' import { isActive } from '../../shared'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'
defineProps<{ defineProps<{

@ -62,15 +62,10 @@ const classes = computed(() => ({
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
padding: 0 8px 0 24px; padding: 0 8px 0 24px;
height: var(--vp-nav-height); height: var(--vp-nav-height);
transition: border-color 0.5s, background-color 0.5s;
pointer-events: none; pointer-events: none;
white-space: nowrap; white-space: nowrap;
} }
.VPNavBar.has-sidebar {
border-bottom-color: var(--vp-c-gutter);
}
@media (min-width: 768px) { @media (min-width: 768px) {
.VPNavBar { .VPNavBar {
padding: 0 32px; padding: 0 32px;
@ -79,7 +74,6 @@ const classes = computed(() => ({
@media (min-width: 960px) { @media (min-width: 960px) {
.VPNavBar.has-sidebar { .VPNavBar.has-sidebar {
border-bottom-color: transparent;
padding: 0; padding: 0;
} }
@ -175,6 +169,12 @@ const classes = computed(() => ({
} }
} }
@media (max-width: 768px) {
.content-body {
column-gap: 0.5rem;
}
}
.menu + .translations::before, .menu + .translations::before,
.menu + .appearance::before, .menu + .appearance::before,
.menu + .social-links::before, .menu + .social-links::before,

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { isActive } from '../support/utils' import { isActive } from '../../shared'
import VPFlyout from './VPFlyout.vue' import VPFlyout from './VPFlyout.vue'
defineProps<{ defineProps<{

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { isActive } from '../support/utils' import { isActive } from '../../shared'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'
defineProps<{ defineProps<{

@ -61,9 +61,12 @@ onMounted(() => {
preconnect() preconnect()
const handleSearchHotKey = (e: KeyboardEvent) => { const handleSearchHotKey = (event: KeyboardEvent) => {
if (e.key === 'k' && (e.ctrlKey || e.metaKey)) { if (
e.preventDefault() (event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) ||
(!isEditingContent(event) && event.key === '/')
) {
event.preventDefault()
load() load()
remove() remove()
} }
@ -101,6 +104,18 @@ function poll() {
}, 16) }, 16)
} }
function isEditingContent(event: KeyboardEvent): boolean {
const element = event.target as HTMLElement
const tagName = element.tagName
return (
element.isContentEditable ||
tagName === 'INPUT' ||
tagName === 'SELECT' ||
tagName === 'TEXTAREA'
)
}
// Local search // Local search
const showSearch = ref(false) const showSearch = ref(false)
@ -112,6 +127,13 @@ if (__VP_LOCAL_SEARCH__) {
showSearch.value = true showSearch.value = true
} }
}) })
onKeyStroke('/', (event) => {
if (!isEditingContent(event)) {
event.preventDefault()
showSearch.value = true
}
})
} }
const metaKey = ref(`'Meta'`) const metaKey = ref(`'Meta'`)

@ -61,7 +61,7 @@ defineProps<{
align-items: center; align-items: center;
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 32px; width: 48px;
height: 55px; height: 55px;
background: transparent; background: transparent;
transition: border-color 0.25s; transition: border-color 0.25s;

@ -35,10 +35,6 @@ const { currentLang } = useLangs()
transition: opacity 0.25s; transition: opacity 0.25s;
} }
.title:hover {
opacity: 0.6;
}
@media (min-width: 960px) { @media (min-width: 960px) {
.title { .title {
flex-shrink: 0; flex-shrink: 0;

@ -11,8 +11,7 @@ const { theme } = useData()
<template v-for="item in theme.nav" :key="item.text"> <template v-for="item in theme.nav" :key="item.text">
<VPNavScreenMenuLink <VPNavScreenMenuLink
v-if="'link' in item" v-if="'link' in item"
:text="item.text" :item="item"
:link="item.link"
/> />
<VPNavScreenMenuGroup <VPNavScreenMenuGroup
v-else v-else

@ -35,10 +35,7 @@ function toggle() {
<div :id="groupId" class="items"> <div :id="groupId" class="items">
<template v-for="item in items" :key="item.text"> <template v-for="item in items" :key="item.text">
<div v-if="'link' in item" :key="item.text" class="item"> <div v-if="'link' in item" :key="item.text" class="item">
<VPNavScreenMenuGroupLink <VPNavScreenMenuGroupLink :item="item" />
:text="item.text"
:link="item.link"
/>
</div> </div>
<div v-else class="group"> <div v-else class="group">

@ -1,18 +1,24 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { inject } from 'vue' import { inject } from 'vue'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'
defineProps<{ defineProps<{
text: string item: DefaultTheme.NavItemWithLink
link: string
}>() }>()
const closeScreen = inject('close-screen') as () => void const closeScreen = inject('close-screen') as () => void
</script> </script>
<template> <template>
<VPLink class="VPNavScreenMenuGroupLink" :href="link" @click="closeScreen"> <VPLink
{{ text }} class="VPNavScreenMenuGroupLink"
:href="item.link"
:target="item.target"
:rel="item.rel"
@click="closeScreen"
>
{{ item.text }}
</VPLink> </VPLink>
</template> </template>

@ -14,8 +14,7 @@ defineProps<{
<VPNavScreenMenuGroupLink <VPNavScreenMenuGroupLink
v-for="item in items" v-for="item in items"
:key="item.text" :key="item.text"
:text="item.text" :item="item"
:link="item.link"
/> />
</div> </div>
</template> </template>

@ -1,18 +1,24 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { inject } from 'vue' import { inject } from 'vue'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'
defineProps<{ defineProps<{
text: string item: DefaultTheme.NavItemWithLink
link: string
}>() }>()
const closeScreen = inject('close-screen') as () => void const closeScreen = inject('close-screen') as () => void
</script> </script>
<template> <template>
<VPLink class="VPNavScreenMenuLink" :href="link" @click="closeScreen"> <VPLink
{{ text }} class="VPNavScreenMenuLink"
:href="item.link"
:target="item.target"
:rel="item.rel"
@click="closeScreen"
>
{{ item.text }}
</VPLink> </VPLink>
</template> </template>

@ -59,10 +59,6 @@ function focusOnTargetAnchor({ target }: Event) {
clip-path: none; clip-path: none;
} }
.dark .VPSkipLink {
color: var(--vp-c-green);
}
@media (min-width: 1280px) { @media (min-width: 1280px) {
.VPSkipLink { .VPSkipLink {
top: 14px; top: 14px;

@ -6,6 +6,7 @@ import { icons } from '../support/socialIcons'
const props = defineProps<{ const props = defineProps<{
icon: DefaultTheme.SocialLinkIcon icon: DefaultTheme.SocialLinkIcon
link: string link: string
ariaLabel?: string
}>() }>()
const svg = computed(() => { const svg = computed(() => {
@ -18,7 +19,7 @@ const svg = computed(() => {
<a <a
class="VPSocialLink" class="VPSocialLink"
:href="link" :href="link"
:aria-label="typeof icon === 'string' ? icon : ''" :aria-label="ariaLabel ?? (typeof icon === 'string' ? icon : '')"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
v-html="svg" v-html="svg"

@ -10,10 +10,11 @@ defineProps<{
<template> <template>
<div class="VPSocialLinks"> <div class="VPSocialLinks">
<VPSocialLink <VPSocialLink
v-for="{ link, icon } in links" v-for="{ link, icon, ariaLabel } in links"
:key="link" :key="link"
:icon="icon" :icon="icon"
:link="link" :link="link"
:ariaLabel="ariaLabel"
/> />
</div> </div>
</template> </template>
@ -21,7 +22,6 @@ defineProps<{
<style scoped> <style scoped>
.VPSocialLinks { .VPSocialLinks {
display: flex; display: flex;
flex-wrap: wrap;
justify-content: center; justify-content: center;
} }
</style> </style>

@ -1,14 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, watch } from 'vue'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { APPEARANCE_KEY } from '../../shared' import { inBrowser, APPEARANCE_KEY } from '../../shared'
import VPSwitch from './VPSwitch.vue' import VPSwitch from './VPSwitch.vue'
import VPIconSun from './icons/VPIconSun.vue' import VPIconSun from './icons/VPIconSun.vue'
import VPIconMoon from './icons/VPIconMoon.vue' import VPIconMoon from './icons/VPIconMoon.vue'
const { site, isDark } = useData() const { site, isDark } = useData()
const checked = ref(false) const checked = ref(false)
const toggle = typeof localStorage !== 'undefined' ? useAppearance() : () => {} const toggle = inBrowser ? useAppearance() : () => {}
onMounted(() => { onMounted(() => {
checked.value = document.documentElement.classList.contains('dark') checked.value = document.documentElement.classList.contains('dark')
@ -75,8 +75,8 @@ watch(checked, (newIsDark) => {
</script> </script>
<template> <template>
<label title="toggle dark mode">
<VPSwitch <VPSwitch
title="toggle dark mode"
class="VPSwitchAppearance" class="VPSwitchAppearance"
:aria-checked="checked" :aria-checked="checked"
@click="toggle" @click="toggle"
@ -84,7 +84,6 @@ watch(checked, (newIsDark) => {
<VPIconSun class="sun" /> <VPIconSun class="sun" />
<VPIconMoon class="moon" /> <VPIconMoon class="moon" />
</VPSwitch> </VPSwitch>
</label>
</template> </template>
<style scoped> <style scoped>

@ -6,12 +6,11 @@ export function useEditLink() {
return computed(() => { return computed(() => {
const { text = 'Edit this page', pattern = '' } = theme.value.editLink || {} const { text = 'Edit this page', pattern = '' } = theme.value.editLink || {}
const { relativePath } = page.value
let url: string let url: string
if (typeof pattern === 'function') { if (typeof pattern === 'function') {
url = pattern({ relativePath }) url = pattern(page.value)
} else { } else {
url = pattern.replace(/:path/g, relativePath) url = pattern.replace(/:path/g, page.value.filePath)
} }
return { url, text } return { url, text }

@ -1,7 +1,7 @@
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { onMounted, onUnmounted, onUpdated, type Ref } from 'vue' import { onMounted, onUnmounted, onUpdated, type Ref } from 'vue'
import type { Header } from '../../shared' import type { Header } from '../../shared'
import { useAside } from '../composables/aside' import { useAside } from './aside'
import { throttleAndDebounce } from '../support/utils' import { throttleAndDebounce } from '../support/utils'
// magic number to avoid repeated retrieval // magic number to avoid repeated retrieval

@ -1,6 +1,6 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useData } from './data' import { useData } from './data'
import { isActive } from '../support/utils' import { isActive } from '../../shared'
import { getSidebar, getFlatSideBarLinks } from '../support/sidebar' import { getSidebar, getFlatSideBarLinks } from '../support/sidebar'
export function usePrevNext() { export function usePrevNext() {

@ -10,7 +10,7 @@ import {
import { useMediaQuery } from '@vueuse/core' import { useMediaQuery } from '@vueuse/core'
import { useRoute } from 'vitepress' import { useRoute } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { isActive } from '../support/utils' import { isActive } from '../../shared'
import { import {
hasActiveLink as containsActiveLink, hasActiveLink as containsActiveLink,
getSidebar, getSidebar,
@ -60,8 +60,7 @@ export function useSidebar() {
const hasAside = computed(() => { const hasAside = computed(() => {
if (frontmatter.value.layout === 'home') return false if (frontmatter.value.layout === 'home') return false
if (frontmatter.value.aside != null) return !!frontmatter.value.aside if (frontmatter.value.aside != null) return !!frontmatter.value.aside
if (theme.value.aside === false) return false return theme.value.aside !== false
return true
}) })
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value) const isSidebarEnabled = computed(() => hasSidebar.value && is960.value)

@ -56,7 +56,7 @@
right: 8px; right: 8px;
bottom: -1px; bottom: -1px;
left: 8px; left: 8px;
z-index: 10; z-index: 1;
height: 1px; height: 1px;
content: ''; content: '';
background-color: transparent; background-color: transparent;

@ -491,7 +491,7 @@
z-index: 2; z-index: 2;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: var(--vp-c-text-dark-3); color: var(--vp-c-code-dimm);
transition: color 0.4s, opacity 0.4s; transition: color 0.4s, opacity 0.4s;
} }

@ -208,6 +208,7 @@
:root { :root {
--vp-code-line-height: 1.7; --vp-code-line-height: 1.7;
--vp-code-font-size: 0.875em; --vp-code-font-size: 0.875em;
--vp-c-code-dimm: var(--vp-c-text-dark-3);
--vp-code-block-color: var(--vp-c-text-dark-1); --vp-code-block-color: var(--vp-c-text-dark-1);
--vp-code-block-bg: #292b30; --vp-code-block-bg: #292b30;
@ -215,7 +216,7 @@
--vp-code-block-divider-color: #000000; --vp-code-block-divider-color: #000000;
--vp-code-line-highlight-color: rgba(0, 0, 0, 0.5); --vp-code-line-highlight-color: rgba(0, 0, 0, 0.5);
--vp-code-line-number-color: var(--vp-c-text-dark-3); --vp-code-line-number-color: var(--vp-c-code-dimm);
--vp-code-line-diff-add-color: var(--vp-c-green-dimm-2); --vp-code-line-diff-add-color: var(--vp-c-green-dimm-2);
--vp-code-line-diff-add-symbol-color: var(--vp-c-green); --vp-code-line-diff-add-symbol-color: var(--vp-c-green);
@ -244,6 +245,27 @@
--vp-code-block-bg: #161618; --vp-code-block-bg: #161618;
} }
:root:not(.dark) .vp-adaptive-theme {
--vp-c-code-dimm: var(--vp-c-text-2);
--vp-code-block-color: var(--vp-c-text-1);
--vp-code-block-bg: #f8f8f8;
--vp-code-block-divider-color: var(--vp-c-divider);
--vp-code-line-highlight-color: #ececec;
--vp-code-line-number-color: var(--vp-c-code-dimm);
--vp-code-copy-code-bg: #e2e2e2;
--vp-code-copy-code-hover-bg: #dcdcdc;
--vp-code-copy-code-active-text: var(--vp-c-text-2);
--vp-code-tab-divider: var(--vp-c-divider);
--vp-code-tab-text-color: var(--vp-c-text-2);
--vp-code-tab-bg: var(--vp-code-block-bg);
--vp-code-tab-hover-text-color: var(--vp-c-text-1);
--vp-code-tab-active-text-color: var(--vp-c-text-1);
}
/** /**
* Component: Button * Component: Button
* -------------------------------------------------------------------------- */ * -------------------------------------------------------------------------- */

@ -1,5 +1,6 @@
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { ensureStartingSlash, isActive } from './utils' import { ensureStartingSlash } from './utils'
import { isActive } from '../../shared'
export interface SidebarLink { export interface SidebarLink {
text: string text: string

@ -2,8 +2,6 @@ import { withBase } from 'vitepress'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { isExternal, PATHNAME_PROTOCOL_RE } from '../../shared' import { isExternal, PATHNAME_PROTOCOL_RE } from '../../shared'
export { isExternal, isActive } from '../../shared'
export function throttleAndDebounce(fn: () => void, delay: number): () => void { export function throttleAndDebounce(fn: () => void, delay: number): () => void {
let timeoutId: NodeJS.Timeout let timeoutId: NodeJS.Timeout
let called = false let called = false

@ -1,16 +1,17 @@
import fs from 'fs-extra' import fs from 'fs-extra'
import path from 'path' import { createRequire } from 'module'
import ora from 'ora' import ora from 'ora'
import path from 'path'
import { packageDirectorySync } from 'pkg-dir'
import { rimraf } from 'rimraf'
import type { OutputAsset, OutputChunk } from 'rollup'
import { pathToFileURL } from 'url'
import type { BuildOptions } from 'vite' import type { BuildOptions } from 'vite'
import type { OutputChunk, OutputAsset } from 'rollup'
import { resolveConfig } from '../config' import { resolveConfig } from '../config'
import { renderPage } from './render'
import { bundle, okMark, failMark } from './bundle'
import { createRequire } from 'module'
import { pathToFileURL } from 'url'
import { packageDirectorySync } from 'pkg-dir'
import { serializeFunctions } from '../utils/fnSerialize'
import type { HeadConfig } from '../shared' import type { HeadConfig } from '../shared'
import { serializeFunctions } from '../utils/fnSerialize'
import { bundle, failMark, okMark } from './bundle'
import { renderPage } from './render'
export async function build( export async function build(
root?: string, root?: string,
@ -142,8 +143,7 @@ export async function build(
) )
} finally { } finally {
unlinkVue() unlinkVue()
if (!process.env.DEBUG) if (!process.env.DEBUG) await rimraf(siteConfig.tempDir)
fs.rmSync(siteConfig.tempDir, { recursive: true, force: true })
} }
await siteConfig.buildEnd?.(siteConfig) await siteConfig.buildEnd?.(siteConfig)

@ -165,13 +165,15 @@ export async function bundle(
if (config.mpa) { if (config.mpa) {
// in MPA mode, we need to copy over the non-js asset files from the // in MPA mode, we need to copy over the non-js asset files from the
// server build since there is no client-side build. // server build since there is no client-side build.
for (const chunk of serverResult.output) { await Promise.all(
serverResult.output.map(async (chunk) => {
if (!chunk.fileName.endsWith('.js')) { if (!chunk.fileName.endsWith('.js')) {
const tempPath = path.resolve(config.tempDir, chunk.fileName) const tempPath = path.resolve(config.tempDir, chunk.fileName)
const outPath = path.resolve(config.outDir, chunk.fileName) const outPath = path.resolve(config.outDir, chunk.fileName)
await fs.copy(tempPath, outPath) await fs.copy(tempPath, outPath)
} }
} })
)
// also copy over public dir // also copy over public dir
const publicDir = path.resolve(config.srcDir, 'public') const publicDir = path.resolve(config.srcDir, 'public')
if (fs.existsSync(publicDir)) { if (fs.existsSync(publicDir)) {

@ -4,6 +4,7 @@ import { createLogger } from 'vite'
import { build, createServer, serve } from '.' import { build, createServer, serve } from '.'
import { init } from './init/init' import { init } from './init/init'
import { version } from '../../package.json' import { version } from '../../package.json'
import { bindShortcuts } from './shortcuts'
const argv: any = minimist(process.argv.slice(2)) const argv: any = minimist(process.argv.slice(2))
@ -33,6 +34,7 @@ if (!command || command === 'dev') {
await server.listen() await server.listen()
logVersion(server.config.logger) logVersion(server.config.logger)
server.printUrls() server.printUrls()
bindShortcuts(server, createDevServer)
} }
createDevServer().catch((err) => { createDevServer().catch((err) => {
createLogger().error( createLogger().error(

@ -222,7 +222,7 @@ export async function resolveSiteData(
appearance: userConfig.appearance ?? true, appearance: userConfig.appearance ?? true,
themeConfig: userConfig.themeConfig || {}, themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {}, locales: userConfig.locales || {},
scrollOffset: userConfig.scrollOffset || 90, scrollOffset: userConfig.scrollOffset ?? 90,
cleanUrls: !!userConfig.cleanUrls cleanUrls: !!userConfig.cleanUrls
} }
} }

@ -1,4 +1,4 @@
import fs from 'fs' import fs from 'fs-extra'
import path from 'path' import path from 'path'
import glob from 'fast-glob' import glob from 'fast-glob'
import type { SiteConfig } from './config' import type { SiteConfig } from './config'

@ -157,7 +157,8 @@ export function scaffold({
renderFile(file) renderFile(file)
} }
const dir = root === './' ? `` : ` ${root.replace(/^\.\//, '')}` const dir =
root === './' ? `` : ` ${root.replace(/^\.\//, '').replace(/[/\\]$/, '')}`
const pkgPath = path.resolve('package.json') const pkgPath = path.resolve('package.json')
const userPkg = fs.existsSync(pkgPath) const userPkg = fs.existsSync(pkgPath)

@ -60,13 +60,16 @@ export const createMarkdownRenderer = async (
base = '/', base = '/',
logger: Pick<Logger, 'warn'> = console logger: Pick<Logger, 'warn'> = console
): Promise<MarkdownRenderer> => { ): Promise<MarkdownRenderer> => {
const theme = options.theme ?? 'material-theme-palenight'
const hasSingleTheme = typeof theme === 'string' || 'name' in theme
const md = MarkdownIt({ const md = MarkdownIt({
html: true, html: true,
linkify: true, linkify: true,
highlight: highlight:
options.highlight || options.highlight ||
(await highlight( (await highlight(
options.theme, theme,
options.languages, options.languages,
options.defaultHighlightLang, options.defaultHighlightLang,
logger logger
@ -79,9 +82,9 @@ export const createMarkdownRenderer = async (
// custom plugins // custom plugins
md.use(componentPlugin) md.use(componentPlugin)
.use(highlightLinePlugin) .use(highlightLinePlugin)
.use(preWrapperPlugin) .use(preWrapperPlugin, { hasSingleTheme })
.use(snippetPlugin, srcDir) .use(snippetPlugin, srcDir)
.use(containerPlugin) .use(containerPlugin, { hasSingleTheme })
.use(imagePlugin) .use(imagePlugin)
.use( .use(
linkPlugin, linkPlugin,

@ -3,9 +3,13 @@ import type { RenderRule } from 'markdown-it/lib/renderer'
import type Token from 'markdown-it/lib/token' import type Token from 'markdown-it/lib/token'
import container from 'markdown-it-container' import container from 'markdown-it-container'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import { extractTitle } from './preWrapper' import {
extractTitle,
getAdaptiveThemeMarker,
type Options
} from './preWrapper'
export const containerPlugin = (md: MarkdownIt) => { export const containerPlugin = (md: MarkdownIt, options: Options) => {
md.use(...createContainer('tip', 'TIP', md)) md.use(...createContainer('tip', 'TIP', md))
.use(...createContainer('info', 'INFO', md)) .use(...createContainer('info', 'INFO', md))
.use(...createContainer('warning', 'WARNING', md)) .use(...createContainer('warning', 'WARNING', md))
@ -20,7 +24,7 @@ export const containerPlugin = (md: MarkdownIt) => {
render: (tokens: Token[], idx: number) => render: (tokens: Token[], idx: number) =>
tokens[idx].nesting === 1 ? `<div class="vp-raw">\n` : `</div>\n` tokens[idx].nesting === 1 ? `<div class="vp-raw">\n` : `</div>\n`
}) })
.use(...createCodeGroup()) .use(...createCodeGroup(options))
} }
type ContainerArgs = [typeof container, string, { render: RenderRule }] type ContainerArgs = [typeof container, string, { render: RenderRule }]
@ -51,7 +55,7 @@ function createContainer(
] ]
} }
function createCodeGroup(): ContainerArgs { function createCodeGroup(options: Options): ContainerArgs {
return [ return [
container, container,
'code-group', 'code-group',
@ -82,7 +86,9 @@ function createCodeGroup(): ContainerArgs {
} }
} }
return `<div class="vp-code-group"><div class="tabs">${tabs}</div><div class="blocks">\n` return `<div class="vp-code-group${getAdaptiveThemeMarker(
options
)}"><div class="tabs">${tabs}</div><div class="blocks">\n`
} }
return `</div></div>\n` return `</div></div>\n`
} }

@ -62,7 +62,7 @@ const errorLevelProcessor = defineProcessor({
}) })
export async function highlight( export async function highlight(
theme: ThemeOptions = 'material-theme-palenight', theme: ThemeOptions,
languages: ILanguageRegistration[] = [], languages: ILanguageRegistration[] = [],
defaultLang: string = '', defaultLang: string = '',
logger: Pick<Logger, 'warn'> = console logger: Pick<Logger, 'warn'> = console

@ -5,7 +5,7 @@
import type MarkdownIt from 'markdown-it' import type MarkdownIt from 'markdown-it'
import type { MarkdownEnv } from '../env' import type { MarkdownEnv } from '../env'
import { URL } from 'url' import { URL } from 'url'
import { EXTERNAL_URL_RE, PATHNAME_PROTOCOL_RE } from '../../shared' import { EXTERNAL_URL_RE, PATHNAME_PROTOCOL_RE, isExternal } from '../../shared'
const indexRE = /(^|.*\/)index.md(#?.*)$/i const indexRE = /(^|.*\/)index.md(#?.*)$/i
@ -26,8 +26,7 @@ export const linkPlugin = (
if (hrefIndex >= 0) { if (hrefIndex >= 0) {
const hrefAttr = token.attrs![hrefIndex] const hrefAttr = token.attrs![hrefIndex]
const url = hrefAttr[1] const url = hrefAttr[1]
const isExternal = EXTERNAL_URL_RE.test(url) if (isExternal(url)) {
if (isExternal) {
Object.entries(externalAttrs).forEach(([key, val]) => { Object.entries(externalAttrs).forEach(([key, val]) => {
token.attrSet(key, val) token.attrSet(key, val)
}) })

@ -1,6 +1,10 @@
import type MarkdownIt from 'markdown-it' import type MarkdownIt from 'markdown-it'
export function preWrapperPlugin(md: MarkdownIt) { export interface Options {
hasSingleTheme: boolean
}
export function preWrapperPlugin(md: MarkdownIt, options: Options) {
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
@ -10,17 +14,21 @@ export function preWrapperPlugin(md: MarkdownIt) {
const lang = extractLang(token.info) const lang = extractLang(token.info)
const rawCode = fence(...args) const rawCode = fence(...args)
return `<div class="language-${lang}${ return `<div class="language-${lang}${getAdaptiveThemeMarker(options)}${
/ active( |$)/.test(token.info) ? ' active' : '' / active( |$)/.test(token.info) ? ' active' : ''
}"><button title="Copy Code" class="copy"></button><span class="lang">${lang}</span>${rawCode}</div>` }"><button title="Copy Code" class="copy"></button><span class="lang">${lang}</span>${rawCode}</div>`
} }
} }
export function getAdaptiveThemeMarker(options: Options) {
return options.hasSingleTheme ? '' : ' vp-adaptive-theme'
}
export function extractTitle(info: string) { export function extractTitle(info: string) {
return info.match(/\[(.*)\]/)?.[1] || extractLang(info) || 'txt' return info.match(/\[(.*)\]/)?.[1] || extractLang(info) || 'txt'
} }
const extractLang = (info: string) => { function extractLang(info: string) {
return info return info
.trim() .trim()
.replace(/:(no-)?line-numbers({| |$).*/, '') .replace(/:(no-)?line-numbers({| |$).*/, '')

@ -1,7 +1,8 @@
import fs from 'fs' import fs from 'fs-extra'
import path from 'path' import path from 'path'
import type MarkdownIt from 'markdown-it' import type MarkdownIt from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block' import type { RuleBlock } from 'markdown-it/lib/parser_block'
import type { MarkdownEnv } from '../env'
export function dedent(text: string): string { export function dedent(text: string): string {
const lines = text.split('\n') const lines = text.split('\n')
@ -107,7 +108,7 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => {
.trim() .trim()
const [ const [
filename = '', filepath = '',
extension = '', extension = '',
region = '', region = '',
lines = '', lines = '',
@ -115,7 +116,7 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => {
rawTitle = '' rawTitle = ''
] = (rawPathRegexp.exec(rawPath) || []).slice(1) ] = (rawPathRegexp.exec(rawPath) || []).slice(1)
const title = rawTitle || filename.split('/').pop() || '' const title = rawTitle || filepath.split('/').pop() || ''
state.line = startLine + 1 state.line = startLine + 1
@ -124,8 +125,12 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => {
title ? `[${title}]` : '' title ? `[${title}]` : ''
}` }`
const resolvedPath = path.resolve(
path.dirname((state.env as MarkdownEnv).path),
filepath
)
// @ts-ignore // @ts-ignore
token.src = path.resolve(filename) + region token.src = [resolvedPath, region.slice(1)]
token.markup = '```' token.markup = '```'
token.map = [startLine, startLine + 1] token.map = [startLine, startLine + 1]
@ -138,8 +143,7 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => {
const [tokens, idx, , { loader }] = args const [tokens, idx, , { loader }] = args
const token = tokens[idx] const token = tokens[idx]
// @ts-ignore // @ts-ignore
const tokenSrc = token.src const [src, regionName] = token.src ?? []
const [src, regionName] = tokenSrc ? tokenSrc.split('#') : ['']
if (src) { if (src) {
if (loader) { if (loader) {

@ -1,9 +1,8 @@
import { resolveTitleFromToken } from '@mdit-vue/shared' import { resolveTitleFromToken } from '@mdit-vue/shared'
import _debug from 'debug' import _debug from 'debug'
import fs from 'fs' import fs from 'fs-extra'
import { LRUCache } from 'lru-cache' import { LRUCache } from 'lru-cache'
import path from 'path' import path from 'path'
import c from 'picocolors'
import type { SiteConfig } from './config' import type { SiteConfig } from './config'
import { import {
createMarkdownRenderer, createMarkdownRenderer,
@ -26,7 +25,7 @@ const includesRE = /<!--\s*@include:\s*(.*?)\s*-->/g
export interface MarkdownCompileResult { export interface MarkdownCompileResult {
vueSrc: string vueSrc: string
pageData: PageData pageData: PageData
deadLinks: string[] deadLinks: { url: string; file: string }[]
includes: string[] includes: string[]
} }
@ -88,9 +87,15 @@ export async function createMarkdownToVueRenderFn(
// resolve includes // resolve includes
let includes: string[] = [] let includes: string[] = []
src = src.replace(includesRE, (m, m1) => { src = src.replace(includesRE, (m, m1) => {
if (!m1.length) return m
const atPresent = m1[0] === '@'
try { try {
const dir = path.dirname(fileOrig) // include paths are strict relative file paths w/o aliases const dir = atPresent ? srcDir : path.dirname(fileOrig)
const includePath = path.join(dir, m1) const includePath = path.join(
dir,
atPresent ? m1.slice(m1.length > 1 && m1[1] === '/' ? 2 : 1) : m1
)
const content = fs.readFileSync(includePath, 'utf-8') const content = fs.readFileSync(includePath, 'utf-8')
includes.push(slash(includePath)) includes.push(slash(includePath))
return content return content
@ -115,18 +120,9 @@ export async function createMarkdownToVueRenderFn(
} = env } = env
// validate data.links // validate data.links
const deadLinks: string[] = [] const deadLinks: MarkdownCompileResult['deadLinks'] = []
const recordDeadLink = (url: string) => { const recordDeadLink = (url: string) => {
;(siteConfig?.logger ?? console).warn( deadLinks.push({ url, file: path.relative(srcDir, fileOrig) })
c.yellow(
`\n(!) Found dead link ${c.cyan(url)} in file ${c.white(
c.dim(file)
)}\nIf it is intended, you can use:\n ${c.cyan(
`<a href="${url}" target="_blank" rel="noreferrer">${url}</a>`
)}`
)
)
deadLinks.push(url)
} }
function shouldIgnoreDeadLink(url: string) { function shouldIgnoreDeadLink(url: string) {
@ -187,7 +183,8 @@ export async function createMarkdownToVueRenderFn(
frontmatter, frontmatter,
headers, headers,
params, params,
relativePath relativePath,
filePath: slash(path.relative(srcDir, fileOrig))
} }
if (includeLastUpdatedData) { if (includeLastUpdatedData) {

@ -15,7 +15,11 @@ import {
resolveAliases resolveAliases
} from './alias' } from './alias'
import { resolveUserConfig, resolvePages, type SiteConfig } from './config' import { resolveUserConfig, resolvePages, type SiteConfig } from './config'
import { clearCache, createMarkdownToVueRenderFn } from './markdownToVue' import {
clearCache,
createMarkdownToVueRenderFn,
type MarkdownCompileResult
} from './markdownToVue'
import { slash, type PageDataPayload } from './shared' import { slash, type PageDataPayload } from './shared'
import { staticDataPlugin } from './plugins/staticDataPlugin' import { staticDataPlugin } from './plugins/staticDataPlugin'
import { webFontsPlugin } from './plugins/webFontsPlugin' import { webFontsPlugin } from './plugins/webFontsPlugin'
@ -94,7 +98,7 @@ export async function createVitePressPlugin(
} }
let siteData = site let siteData = site
let hasDeadLinks = false let allDeadLinks: MarkdownCompileResult['deadLinks'] = []
let config: ResolvedConfig let config: ResolvedConfig
const vitePressPlugin: Plugin = { const vitePressPlugin: Plugin = {
@ -184,9 +188,7 @@ export async function createVitePressPlugin(
id, id,
config.publicDir config.publicDir
) )
if (deadLinks.length) { allDeadLinks.push(...deadLinks)
hasDeadLinks = true
}
if (includes.length) { if (includes.length) {
includes.forEach((i) => { includes.forEach((i) => {
this.addWatchFile(i) this.addWatchFile(i)
@ -197,8 +199,22 @@ export async function createVitePressPlugin(
}, },
renderStart() { renderStart() {
if (hasDeadLinks) { if (allDeadLinks.length > 0) {
throw new Error(`One or more pages contain dead links.`) allDeadLinks.forEach(({ url, file }, i) => {
siteConfig.logger.warn(
c.yellow(
`${i === 0 ? '\n\n' : ''}(!) Found dead link ${c.cyan(
url
)} in file ${c.white(c.dim(file))}`
)
)
})
siteConfig.logger.info(
c.cyan(
'\nIf this is expected, you can disable this check via config. Refer: https://vitepress.dev/reference/site-config#ignoredeadlinks\n'
)
)
throw new Error(`${allDeadLinks.length} dead link(s) found.`)
} }
}, },

@ -4,7 +4,7 @@ import MiniSearch from 'minisearch'
import fs from 'fs-extra' import fs from 'fs-extra'
import _debug from 'debug' import _debug from 'debug'
import type { SiteConfig } from '../config' import type { SiteConfig } from '../config'
import { createMarkdownRenderer } from '../markdown/markdown' import { createMarkdownRenderer } from '../markdown'
import { resolveSiteDataByRoute, slash } from '../shared' import { resolveSiteDataByRoute, slash } from '../shared'
const debug = _debug('vitepress:local-search') const debug = _debug('vitepress:local-search')
@ -60,7 +60,7 @@ export async function localSearchPlugin(
} }
function getLocaleForPath(file: string) { function getLocaleForPath(file: string) {
const relativePath = path.relative(siteConfig.srcDir, file) const relativePath = slash(path.relative(siteConfig.srcDir, file))
const siteData = resolveSiteDataByRoute(siteConfig.site, relativePath) const siteData = resolveSiteDataByRoute(siteConfig.site, relativePath)
return siteData?.localeIndex ?? 'root' return siteData?.localeIndex ?? 'root'
} }
@ -97,7 +97,7 @@ export async function localSearchPlugin(
function getDocId(file: string) { function getDocId(file: string) {
let relFile = slash(path.relative(siteConfig.srcDir, file)) let relFile = slash(path.relative(siteConfig.srcDir, file))
relFile = siteConfig.rewrites.map[relFile] || relFile relFile = siteConfig.rewrites.map[relFile] || relFile
let id = path.join(siteConfig.site.base, relFile) let id = slash(path.join(siteConfig.site.base, relFile))
id = id.replace(/\/index\.md$/, '/') id = id.replace(/\/index\.md$/, '/')
id = id.replace(/\.md$/, siteConfig.cleanUrls ? '' : '.html') id = id.replace(/\.md$/, siteConfig.cleanUrls ? '' : '.html')
return id return id

@ -1,5 +1,5 @@
import compression from 'compression' import compression from 'compression'
import fs from 'fs' import fs from 'fs-extra'
import path from 'path' import path from 'path'
import polka, { type IOptions } from 'polka' import polka, { type IOptions } from 'polka'
import sirv, { type RequestHandler } from 'sirv' import sirv, { type RequestHandler } from 'sirv'

@ -0,0 +1,117 @@
import type { ViteDevServer } from 'vite'
import c from 'picocolors'
import { clearCache } from './markdownToVue'
type CreateDevServer = () => Promise<void>
export type CLIShortcut = {
key: string
description: string
action(
server: ViteDevServer,
createDevServer: CreateDevServer
): void | Promise<void>
}
export function bindShortcuts(
server: ViteDevServer,
createDevServer: CreateDevServer
): void {
if (!server.httpServer || !process.stdin.isTTY || process.env.CI) {
return
}
server.config.logger.info(
c.dim(c.green(' ➜')) +
c.dim(' press ') +
c.bold('h') +
c.dim(' to show help')
)
let actionRunning = false
const onInput = async (input: string) => {
// ctrl+c or ctrl+d
if (input === '\x03' || input === '\x04') {
await server.close().finally(() => process.exit(1))
return
}
if (actionRunning) return
if (input === 'h') {
server.config.logger.info(
[
'',
c.bold(' Shortcuts'),
...SHORTCUTS.map(
(shortcut) =>
c.dim(' press ') +
c.bold(shortcut.key) +
c.dim(` to ${shortcut.description}`)
)
].join('\n')
)
}
const shortcut = SHORTCUTS.find((shortcut) => shortcut.key === input)
if (!shortcut) return
actionRunning = true
await shortcut.action(server, createDevServer)
actionRunning = false
}
process.stdin.setRawMode(true)
process.stdin.on('data', onInput).setEncoding('utf8').resume()
server.httpServer.on('close', () => {
process.stdin.off('data', onInput).pause()
})
}
const SHORTCUTS: CLIShortcut[] = [
{
key: 'r',
description: 'restart the server',
async action(server, createDevServer) {
server.config.logger.info(c.green(`restarting server...\n`), {
clear: true,
timestamp: true
})
clearCache()
await server.close()
await createDevServer()
}
},
{
key: 'u',
description: 'show server url',
action(server) {
server.config.logger.info('')
server.printUrls()
}
},
{
key: 'o',
description: 'open in browser',
action(server) {
server.openBrowser()
}
},
{
key: 'c',
description: 'clear console',
action(server) {
server.config.logger.clearScreen('error')
}
},
{
key: 'q',
description: 'quit',
async action(server) {
await server.close().finally(() => process.exit())
}
}
]

@ -7,7 +7,7 @@ import {
type SiteData, type SiteData,
type SSGContext type SSGContext
} from './shared' } from './shared'
import type { MarkdownOptions } from './markdown/markdown' import type { MarkdownOptions } from './markdown'
import type { Options as VuePluginOptions } from '@vitejs/plugin-vue' import type { Options as VuePluginOptions } from '@vitejs/plugin-vue'
import { type Logger, type UserConfig as ViteConfig } from 'vite' import { type Logger, type UserConfig as ViteConfig } from 'vite'

@ -23,6 +23,7 @@ export const inBrowser = typeof document !== 'undefined'
export const notFoundPageData: PageData = { export const notFoundPageData: PageData = {
relativePath: '', relativePath: '',
filePath: '',
title: '404', title: '404',
description: 'Not Found', description: 'Not Found',
headers: [], headers: [],

@ -0,0 +1 @@
export * from './theme'

@ -1,5 +1,6 @@
import { DocSearchProps } from './docsearch.js' import type { DocSearchProps } from './docsearch.js'
import { LocalSearchTranslations } from './local-search.js' import type { LocalSearchTranslations } from './local-search.js'
import type { PageData } from './shared.js'
export namespace DefaultTheme { export namespace DefaultTheme {
export interface Config { export interface Config {
@ -215,8 +216,9 @@ export namespace DefaultTheme {
* Pattern for edit link. * Pattern for edit link.
* *
* @example 'https://github.com/vuejs/vitepress/edit/main/docs/:path' * @example 'https://github.com/vuejs/vitepress/edit/main/docs/:path'
* @example ({ filePath }) => { ... }
*/ */
pattern: string | ((payload: { relativePath: string }) => string) pattern: string | ((payload: PageData) => string)
/** /**
* Custom text for edit link. * Custom text for edit link.
@ -249,6 +251,7 @@ export namespace DefaultTheme {
export interface SocialLink { export interface SocialLink {
icon: SocialLinkIcon icon: SocialLinkIcon
link: string link: string
ariaLabel?: string
} }
export type SocialLinkIcon = export type SocialLinkIcon =

1
types/shared.d.ts vendored

@ -6,6 +6,7 @@ export type Awaitable<T> = T | PromiseLike<T>
export interface PageData { export interface PageData {
relativePath: string relativePath: string
filePath: string // differs from relativePath in case of path rewrites
title: string title: string
titleTemplate?: string | boolean titleTemplate?: string | boolean
description: string description: string

Loading…
Cancel
Save