Merge branch 'main' into zh

pull/1593/head
Xavi Lee 3 years ago
commit 63ec5aef99

@ -37,7 +37,7 @@ body:
attributes:
label: System Info
description: Output of `npx envinfo --system --npmPackages vitepress --binaries --browsers`
render: shell
render: sh
placeholder: System, Binaries, Browsers
validations:
required: true
@ -52,6 +52,8 @@ body:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: Check if you're on the [latest VitePress version](https://github.com/vuejs/vitepress/releases/latest).
required: true
- label: Follow our [Code of Conduct](https://vuejs.org/about/coc.html)
required: true
- label: Read the [docs](https://vitepress.dev).

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

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

@ -1,3 +1,152 @@
# [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)
### Bug Fixes
* **build:** allow data-loaders files in packages to be found (closes [#2272](https://github.com/vuejs/vitepress/issues/2272)) ([84cf457](https://github.com/vuejs/vitepress/commit/84cf45772ed59f5eae747c15fbffc375768007b8))
* **router:** scroll back to the hash anchor even if it is already selected ([#2265](https://github.com/vuejs/vitepress/issues/2265)) ([f3d3332](https://github.com/vuejs/vitepress/commit/f3d3332fff72d1df6f70c5893bfc90442b1776fb))
### Features
* allow using html in member description ([#2269](https://github.com/vuejs/vitepress/issues/2269)) ([f744364](https://github.com/vuejs/vitepress/commit/f7443643a4510a6c650f1a1bda977c1d55fddf64))
* **search:** support custom `disableQueryPersistence` in local search ([#2273](https://github.com/vuejs/vitepress/issues/2273)) ([2f0f2d5](https://github.com/vuejs/vitepress/commit/2f0f2d5ac6efcab22bdb452e5c0780e7cd8f1498))
* **theme:** mobile view show outline button after removing sidebar ([#2274](https://github.com/vuejs/vitepress/issues/2274)) ([25b9111](https://github.com/vuejs/vitepress/commit/25b9111222cbd3de008e18cac6554933d4db993e))
# [1.0.0-alpha.73](https://github.com/vuejs/vitepress/compare/v1.0.0-alpha.72...v1.0.0-alpha.73) (2023-04-20)
### Bug Fixes
* **search:** fix highlighting in detailed view ([1f4920c](https://github.com/vuejs/vitepress/commit/1f4920c60dc1be03444781539064be7b3ec9eb08))
* **search:** local search showDetailedList not working in windows ([#2253](https://github.com/vuejs/vitepress/issues/2253)) ([09be057](https://github.com/vuejs/vitepress/commit/09be057ffb767e55d3a86f1a3664ebd0690f2fc5))
### Features
* outline link add title attribute ([#2261](https://github.com/vuejs/vitepress/issues/2261)) ([1f5798e](https://github.com/vuejs/vitepress/commit/1f5798e43771ae1e13921a39319345c89bb2298a))
# [1.0.0-alpha.72](https://github.com/vuejs/vitepress/compare/v1.0.0-alpha.71...v1.0.0-alpha.72) (2023-04-17)
### Bug Fixes
* **search:** don't directly access userConfig ([3e0e9d2](https://github.com/vuejs/vitepress/commit/3e0e9d2b27c02e250c5b350bf83dce9b95e217a8))
* **search:** ready event is not fired on mac ([e37e5cb](https://github.com/vuejs/vitepress/commit/e37e5cb45a6c3507b906b9955897ce4e84adf500))
* **theme:** local search showDetailedList not working in windows ([#2248](https://github.com/vuejs/vitepress/issues/2248)) ([8354f8f](https://github.com/vuejs/vitepress/commit/8354f8fb8649d429b2cb525dd6f35127faba7ae6))
# [1.0.0-alpha.71](https://github.com/vuejs/vitepress/compare/v1.0.0-alpha.70...v1.0.0-alpha.71) (2023-04-16)
### Bug Fixes
* **search:** esm interop mark.js import ([1b0a249](https://github.com/vuejs/vitepress/commit/1b0a249ad66288ff56675e4db905959ff0079726))
* **search:** properly group nested headings ([b1c956c](https://github.com/vuejs/vitepress/commit/b1c956ce99505316842c157c76a4ec051eb7610b)), closes [#2238](https://github.com/vuejs/vitepress/issues/2238)
# [1.0.0-alpha.70](https://github.com/vuejs/vitepress/compare/v1.0.0-alpha.69...v1.0.0-alpha.70) (2023-04-16)
### Bug Fixes
* **a11y:** increase touch target size of search icons ([4449867](https://github.com/vuejs/vitepress/commit/44498675aca3271596b041881d44e1524d744df6))
* **search:** avoid body scroll when using local search ([#2236](https://github.com/vuejs/vitepress/issues/2236)) ([144a7d8](https://github.com/vuejs/vitepress/commit/144a7d8e4ee483475b6956090c267213a1e2f8e1))
* **search:** better highlighting in detailed view ([#2234](https://github.com/vuejs/vitepress/issues/2234)) ([be83524](https://github.com/vuejs/vitepress/commit/be8352441f8b9a8561961c69f3e1794370101de2))
* **search:** fix keyword highlighting and scrolling in excerpts ([ca8db8a](https://github.com/vuejs/vitepress/commit/ca8db8adca028bb982b819553b85ac19fe946e7e))
* **search:** remove double base on importing excepts ([185213c](https://github.com/vuejs/vitepress/commit/185213c6ba4416071025fbf3c5ca7fadf311fdbf)), closes [#2230](https://github.com/vuejs/vitepress/issues/2230)
* **search:** remove extra /index from routes ([9e04b43](https://github.com/vuejs/vitepress/commit/9e04b435671accfcaee795ec8ec2833d8aa358f8))
* **search:** show escape to close on footer ([6d5b4cd](https://github.com/vuejs/vitepress/commit/6d5b4cd784274786ad57cef378646320ba17faf1))
### Features
* **search:** allow force disabling detailed view ([40f1d1b](https://github.com/vuejs/vitepress/commit/40f1d1b6f6f2f6c43fd0d9bcf4b6bc1174ce4831))
* **search:** make styling more configurable, align more with the theme ([#2233](https://github.com/vuejs/vitepress/issues/2233)) ([b2077c7](https://github.com/vuejs/vitepress/commit/b2077c70250d5c390d69ce31111a1c44769dbc78))
# [1.0.0-alpha.69](https://github.com/vuejs/vitepress/compare/v1.0.0-alpha.68...v1.0.0-alpha.69) (2023-04-15)
### Bug Fixes
* **search:** fix errors on empty titles ([6d363ec](https://github.com/vuejs/vitepress/commit/6d363ec9ffd6b27e1c77e5aab853471c6883c7bd))
* **theme:** fix color of blockquote in custom containers ([#2173](https://github.com/vuejs/vitepress/issues/2173)) ([712a57f](https://github.com/vuejs/vitepress/commit/712a57fde74daa27f69319861d95f9dec6bc05ad))
# [1.0.0-alpha.68](https://github.com/vuejs/vitepress/compare/v1.0.0-alpha.67...v1.0.0-alpha.68) (2023-04-15)
### Bug Fixes
* **theme:** fix top of scrollbar being unusable ([#2224](https://github.com/vuejs/vitepress/issues/2224)) ([7178a22](https://github.com/vuejs/vitepress/commit/7178a22c9d317245e5167abf80f7081fbf87e78a))
# [1.0.0-alpha.67](https://github.com/vuejs/vitepress/compare/v1.0.0-alpha.66...v1.0.0-alpha.67) (2023-04-15)
### Bug Fixes
* **search:** avoid double base ([25a1fe9](https://github.com/vuejs/vitepress/commit/25a1fe90bddd021a1ce5e068d8cad455687647bf))
* **theme:** navbar style ([#2202](https://github.com/vuejs/vitepress/issues/2202)) ([8ee6b90](https://github.com/vuejs/vitepress/commit/8ee6b905f5243a036c2dee7688539ef33e164f09))
### Features
* allow passing props and children/slots to defineClientComponent ([#2198](https://github.com/vuejs/vitepress/issues/2198)) ([4c24960](https://github.com/vuejs/vitepress/commit/4c2496043394d9b14376e74a5bf11ccea5e6e7d7))
# [1.0.0-alpha.66](https://github.com/vuejs/vitepress/compare/v1.0.0-alpha.65...v1.0.0-alpha.66) (2023-04-15)
### Bug Fixes
* **search:** properly resolve page link ([609d447](https://github.com/vuejs/vitepress/commit/609d447ab50b2b8fb78a174e7d5aa0ff52411b0e))
* **theme:** fix meta key not showing on search button ([e295160](https://github.com/vuejs/vitepress/commit/e2951604fd61336df9559ea16972d3ea76a49894))
### Features
* offline search ([#2110](https://github.com/vuejs/vitepress/issues/2110)) ([6c92675](https://github.com/vuejs/vitepress/commit/6c92675e33d3276a02b790a34083a68093b58c7f))
# [1.0.0-alpha.65](https://github.com/vuejs/vitepress/compare/v1.0.0-alpha.64...v1.0.0-alpha.65) (2023-04-04)

@ -175,4 +175,8 @@ export default config
## 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 () => {
const items = page.locator('#table-of-contents + nav ul li')
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')
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')
})
})

@ -30,12 +30,6 @@ export default defineConfig({
copyright: 'Copyright © 2019-present Evan You'
},
algolia: {
appId: '8J64VVRP8K',
apiKey: 'a18e2f4cc5665f6602c5631fd868adfd',
indexName: 'vitepress'
},
carbonAds: {
code: 'CEBDT27Y',
placement: 'vuejsorg'

@ -691,7 +691,13 @@ You can also [import snippets](#import-code-snippets) in code groups:
## 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**

@ -306,7 +306,7 @@ You can use the params to pass additional data to each page. The Markdown route
- version: {{ $params.version }}
```
You can also access the current page's params via the `[useData](../reference/runtime-api#usedata)` runtime API. This is available in both Markdown files and Vue components:
You can also access the current page's params via the [`useData`](../reference/runtime-api#usedata) runtime API. This is available in both Markdown files and Vue components:
```vue
<script setup>

@ -81,4 +81,39 @@ const ClientComp = defineClientComponent(() => {
</template>
```
You can also pass props/children/slots to the target component:
```vue
<script setup>
import { ref } from 'vue'
import { defineClientComponent } from 'vitepress'
const clientCompRef = ref(null)
const ClientComp = defineClientComponent(
() => import('component-that-access-window-on-import'),
// args are passed to h() - https://vuejs.org/api/render-function.html#h
[
{
ref: clientCompRef
},
{
default: () => 'default slot',
foo: () => h('div', 'foo'),
bar: () => [h('span', 'one'), h('span', 'two')]
}
],
// callback after the component is loaded, can be async
() => {
console.log(clientCompRef.value)
}
)
</script>
<template>
<ClientComp />
</template>
```
The target component will only be imported in the mounted hook of the wrapper component.

@ -164,7 +164,7 @@ Alternatively, you can wrap the entire paragraph in a `v-pre` custom container:
```md
::: v-pre
{{ This will be displayed as-is }}`
{{ This will be displayed as-is }}
:::
```
@ -196,6 +196,8 @@ Hello {{ 1 + 1 }}
Hello {{ 1 + 1 }}
```
Note that this might prevent certain tokens from being syntax highlighted properly.
## Using CSS Pre-processors
VitePress has [built-in support](https://vitejs.dev/guide/features.html#css-pre-processors) for CSS pre-processors: `.scss`, `.sass`, `.less`, `.styl` and `.stylus` files. There is no need to install Vite-specific plugins for them, but the corresponding pre-processor itself must be installed:

@ -30,7 +30,7 @@ VitePress aims to provide a great Developer Experience (DX) when working with Ma
- **[Built-in Markdown Extensions:](./markdown)** Frontmatter, tables, syntax highlighting... you name it. Specifically, VitePress provides many advanced features for working with code blocks, making it ideal for highly technical documentation.
- **[Vue-Enhanced Markdown](./using-vue):** each Markdown page is also a Vue [Single-File Component](https://vuejs.org/guide/scaling-up/sfc.html), thanks to Vue template's 100% syntax compatibility with HTML. You can embed interactivity in your static content using Vue templating features or imported Vue components.
- **[Vue-Enhanced Markdown:](./using-vue)** each Markdown page is also a Vue [Single-File Component](https://vuejs.org/guide/scaling-up/sfc.html), thanks to Vue template's 100% syntax compatibility with HTML. You can embed interactivity in your static content using Vue templating features or imported Vue components.
## Performance

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

@ -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.
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
export default {
themeConfig: {
editLink: {
pattern: ({ relativePath }) => {
if (relativePath.startsWith('packages/')) {
return `https://github.com/acme/monorepo/edit/main/${relativePath}`
pattern: ({ filePath }) => {
if (filePath.startsWith('packages/')) {
return `https://github.com/acme/monorepo/edit/main/${filePath}`
} 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.

@ -1,24 +1,28 @@
# Search
VitePress supports searching your docs site using [Algolia DocSearch](https://docsearch.algolia.com/docs/what-is-docsearch). Refer their getting started guide. In your `.vitepress/config.ts` you'll need to provide at least the following to make it work:
## Local Search
VitePress supports fuzzy full-text search using a in-browser index thanks to [minisearch](https://github.com/lucaong/minisearch/). To enable this feature, simply set the `themeConfig.search.provider` option to `'local'` in your `.vitepress/config.ts` file:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
algolia: {
appId: '...',
apiKey: '...',
indexName: '...'
search: {
provider: 'local'
}
}
})
```
If you are not eligible for DocSearch, you might wanna use some community plugins like <https://github.com/emersonbottero/vitepress-plugin-search> or explore some custom solutions on [this GitHub thread](https://github.com/vuejs/vitepress/issues/670).
Example result:
![screenshot of the search modal](/search.png)
## i18n
Alternatively, you can use [Algolia DocSearch](#algolia-search) or some community plugins like <https://www.npmjs.com/package/vitepress-plugin-search> or <https://www.npmjs.com/package/vitepress-plugin-pagefind>.
### i18n {#local-search-i18n}
You can use a config like this to use multilingual search:
@ -26,52 +30,109 @@ You can use a config like this to use multilingual search:
import { defineConfig } from 'vitepress'
export default defineConfig({
// ...
themeConfig: {
// ...
algolia: {
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
searchBox: {
resetButtonTitle: '清除查询条件',
resetButtonAriaLabel: '清除查询条件',
cancelButtonText: '取消',
cancelButtonAriaLabel: '取消'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除'
search: {
provider: 'local',
options: {
locales: {
zh: {
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
},
footer: {
selectText: '选择',
navigateText: '切换',
closeText: '关闭',
searchByText: '搜索提供者'
},
noResultsScreen: {
modal: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
resetButtonTitle: '清除查询条件',
footer: {
selectText: '选择',
navigateText: '切换'
}
}
}
}
}
}
}
}
})
```
## Algolia Search
VitePress supports searching your docs site using [Algolia DocSearch](https://docsearch.algolia.com/docs/what-is-docsearch). Refer their getting started guide. In your `.vitepress/config.ts` you'll need to provide at least the following to make it work:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...'
}
}
}
})
```
### i18n {#algolia-search-i18n}
You can use a config like this to use multilingual search:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
searchBox: {
resetButtonTitle: '清除查询条件',
resetButtonAriaLabel: '清除查询条件',
cancelButtonText: '取消',
cancelButtonAriaLabel: '取消'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
},
footer: {
selectText: '选择',
navigateText: '切换',
closeText: '关闭',
searchByText: '搜索提供者'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
}
}
}
}
@ -83,3 +144,10 @@ export default defineConfig({
```
[These options](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) can be overridden. Refer official Algolia docs to learn more about them.
<style>
img[src="/search.png"] {
width: 100%;
aspect-ratio: 1 / 1;
}
</style>

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

@ -67,15 +67,15 @@ export default defineConfigWithTheme<ThemeConfig>({
- **Vite**
You can configure the underlying Vite instance using the [vite](#vite) option in your VitePress config. No need to create a separate Vite config file.
You can configure the underlying Vite instance using the [vite](#vite) option in your VitePress config. No need to create a separate Vite config file.
- **Vue**
VitePress already includes the official Vue plugin for Vite ([@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue)). You can configure its options using the [vue](#vue) option in your VitePress config.
VitePress already includes the official Vue plugin for Vite ([@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue)). You can configure its options using the [vue](#vue) option in your VitePress config.
- **Markdown**
You can configure the underlying [Markdown-It](https://github.com/markdown-it/markdown-it) instance using the [markdown](#markdown) option in your VitePress config.
You can configure the underlying [Markdown-It](https://github.com/markdown-it/markdown-it) instance using the [markdown](#markdown) option in your VitePress config.
## Site Metadata
@ -94,6 +94,7 @@ export default {
title: 'My Awesome Site'
}
```
```md
# Hello
```
@ -113,6 +114,7 @@ export default {
titleTemplate: 'Custom Suffix'
}
```
```md
# Hello
```
@ -159,8 +161,29 @@ export default {
[
'link',
{ 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
export default {
@ -319,7 +342,7 @@ export default {
- Type: `boolean`
- Default: `false`
When set to `true`, the production app will be built in [MAP Mode](../guide/mpa-mode). MPA mode ships 0kb JavaScript by default, at the cost of disabling client-side navigation and requires explicit opt-in for interactivity.
When set to `true`, the production app will be built in [MPA Mode](../guide/mpa-mode). MPA mode ships 0kb JavaScript by default, at the cost of disabling client-side navigation and requires explicit opt-in for interactivity.
## Theming
@ -557,7 +580,7 @@ export default {
`transformPageData` is a hook to transform the `pageData` of each page. You can directly mutate `pageData` or return changed values which will be merged into PageData.
::: warning
Don't mutate anything inside the `ctx`.
Don't mutate anything inside the `ctx`.
:::
```ts

@ -1,9 +1,9 @@
{
"name": "vitepress",
"version": "1.0.0-alpha.65",
"version": "1.0.0-alpha.75",
"description": "Vite & Vue powered static site generator",
"type": "module",
"packageManager": "pnpm@8.1.1",
"packageManager": "pnpm@8.3.1",
"main": "dist/node/index.js",
"types": "types/index.d.ts",
"exports": {
@ -87,15 +87,19 @@
"docs-preview": "node ./bin/vitepress preview docs"
},
"dependencies": {
"@docsearch/css": "^3.3.3",
"@docsearch/js": "^3.3.3",
"@vitejs/plugin-vue": "^4.1.0",
"@docsearch/css": "^3.3.4",
"@docsearch/js": "^3.3.4",
"@vitejs/plugin-vue": "^4.2.1",
"@vue/devtools-api": "^6.5.0",
"@vueuse/core": "^9.13.0",
"@vueuse/core": "^10.1.0",
"@vueuse/integrations": "^10.1.0",
"body-scroll-lock": "4.0.0-beta.0",
"shiki": "^0.14.1",
"vite": "^4.2.1",
"vue": "^3.2.47"
"focus-trap": "^7.4.0",
"mark.js": "8.11.1",
"minisearch": "^6.0.1",
"shiki": "^0.14.2",
"vite": "^4.3.3",
"vue": "^3.3.2"
},
"devDependencies": {
"@clack/prompts": "^0.6.3",
@ -106,10 +110,10 @@
"@mdit-vue/plugin-title": "^0.12.0",
"@mdit-vue/plugin-toc": "^0.12.0",
"@mdit-vue/shared": "^0.12.0",
"@rollup/plugin-alias": "^4.0.3",
"@rollup/plugin-commonjs": "^24.0.1",
"@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-replace": "^5.0.2",
"@types/body-scroll-lock": "^3.1.0",
"@types/compression": "^1.7.2",
@ -117,15 +121,14 @@
"@types/debug": "^4.1.7",
"@types/escape-html": "^1.0.2",
"@types/fs-extra": "^11.0.1",
"@types/koa": "^2.13.6",
"@types/koa-static": "^4.0.2",
"@types/mark.js": "^8.11.8",
"@types/markdown-it": "^12.2.3",
"@types/markdown-it-attrs": "^4.1.0",
"@types/markdown-it-container": "^2.0.5",
"@types/markdown-it-emoji": "^2.0.2",
"@types/micromatch": "^4.0.2",
"@types/minimist": "^1.2.2",
"@types/node": "^18.15.11",
"@types/node": "^18.16.3",
"@types/prompts": "^2.4.4",
"chokidar": "^3.5.3",
"compression": "^1.7.4",
@ -133,16 +136,16 @@
"cross-spawn": "^7.0.3",
"debug": "^4.3.4",
"enquirer": "^2.3.6",
"esbuild": "^0.17.15",
"esbuild": "^0.17.18",
"escape-html": "^1.0.3",
"execa": "^7.1.1",
"fast-glob": "^3.2.12",
"fs-extra": "^11.1.1",
"get-port": "^6.1.2",
"gray-matter": "^4.0.3",
"lint-staged": "^13.2.0",
"lint-staged": "^13.2.2",
"lodash.template": "^4.5.0",
"lru-cache": "^8.0.4",
"lru-cache": "^9.1.1",
"markdown-it": "^13.0.1",
"markdown-it-anchor": "^8.6.7",
"markdown-it-attrs": "^4.1.6",
@ -156,23 +159,23 @@
"path-to-regexp": "^6.2.1",
"picocolors": "^1.0.0",
"pkg-dir": "^7.0.0",
"playwright-chromium": "^1.32.2",
"playwright-chromium": "^1.33.0",
"polka": "1.0.0-next.22",
"prettier": "^2.8.7",
"prettier": "^2.8.8",
"prompts": "^2.4.2",
"punycode": "^2.3.0",
"rimraf": "^4.4.1",
"rollup": "^3.20.2",
"rimraf": "^5.0.0",
"rollup": "^3.21.1",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-esbuild": "^5.0.0",
"semver": "^7.3.8",
"semver": "^7.5.0",
"shiki-processor": "^0.1.3",
"simple-git-hooks": "^2.8.1",
"sirv": "^2.0.2",
"sirv": "^2.0.3",
"supports-color": "^9.3.1",
"typescript": "^5.0.3",
"vitest": "^0.29.8",
"vue-tsc": "^1.3.2",
"typescript": "^5.0.4",
"vitest": "^0.30.1",
"vue-tsc": "^1.6.1",
"wait-on": "^7.0.1"
},
"pnpm": {

File diff suppressed because it is too large Load Diff

@ -187,10 +187,13 @@ export function createRouter(
search === currentUrl.search
) {
// scroll between hash anchors in the same page
if (hash && hash !== currentUrl.hash) {
history.pushState(null, '', hash)
// still emit the event so we can listen to it in themes
window.dispatchEvent(new Event('hashchange'))
if (hash) {
// avoid duplicate history entries when the hash is same
if (hash !== currentUrl.hash) {
history.pushState(null, '', hash)
// still emit the event so we can listen to it in themes
window.dispatchEvent(new Event('hashchange'))
}
// use smooth scroll when clicking on header anchor links
scrollTo(link, hash, link.classList.contains('header-anchor'))
}
@ -290,7 +293,7 @@ function handleHMR(route: Route): void {
// update route.data on HMR updates of active page
if (import.meta.hot) {
// hot reload pageData
import.meta.hot!.on('vitepress:pageData', (payload: PageDataPayload) => {
import.meta.hot.on('vitepress:pageData', (payload: PageDataPayload) => {
if (shouldHotReload(payload)) {
route.data = payload.pageData
}

@ -1,5 +1,10 @@
import { siteDataRef } from './data'
import { inBrowser, EXTERNAL_URL_RE, sanitizeFileName } from '../shared'
import {
inBrowser,
EXTERNAL_URL_RE,
sanitizeFileName,
type Awaitable
} from '../shared'
import {
h,
onMounted,
@ -77,7 +82,11 @@ export function onContentUpdated(fn: () => any) {
})
}
export function defineClientComponent(loader: AsyncComponentLoader) {
export function defineClientComponent(
loader: AsyncComponentLoader,
args?: any[],
cb?: () => Awaitable<void>
) {
return {
setup() {
const comp = shallowRef()
@ -88,8 +97,9 @@ export function defineClientComponent(loader: AsyncComponentLoader) {
res = res.default
}
comp.value = res
await cb?.()
})
return () => (comp.value ? h(comp.value) : null)
return () => (comp.value ? h(comp.value, ...(args ?? [])) : null)
}
}
}

@ -1,4 +1,5 @@
declare const __VP_HASH_MAP__: Record<string, string>
declare const __VP_LOCAL_SEARCH__: boolean
declare const __ALGOLIA__: boolean
declare const __CARBON__: boolean
declare const __VUE_PROD_DEVTOOLS__: boolean
@ -20,3 +21,14 @@ declare module '@theme/index' {
const theme: Theme
export default theme
}
declare module '@localSearchIndex' {
const data: Record<string, () => Promise<{ default: string }>>
export default data
}
declare module 'mark.js/src/vanilla.js' {
import type { Mark } from 'mark.js'
const mark: Mark
export default mark
}

@ -18,7 +18,7 @@ function onClick({ target: el }: Event) {
<template>
<ul :class="root ? 'root' : 'nested'">
<li v-for="{ children, link, title } in headers">
<a class="outline-link" :href="link" @click="onClick">{{ title }}</a>
<a class="outline-link" :href="link" @click="onClick" :title="title">{{ title }}</a>
<template v-if="children?.length">
<VPDocOutlineItem :headers="children" />
</template>

@ -12,13 +12,14 @@ defineEmits<{
(e: 'open-menu'): void
}>()
const { theme } = useData()
const { theme, frontmatter } = useData()
const { hasSidebar } = useSidebar()
</script>
<template>
<div v-if="hasSidebar" class="VPLocalNav">
<div class="VPLocalNav" v-if="frontmatter.layout !== 'home'">
<button
v-if="hasSidebar"
class="menu"
:aria-expanded="open"
aria-controls="VPSidebarNav"

@ -0,0 +1,860 @@
<script lang="ts" setup>
import localSearchIndex from '@localSearchIndex'
import {
computedAsync,
debouncedWatch,
onKeyStroke,
useEventListener,
useLocalStorage,
useScrollLock,
useSessionStorage
} from '@vueuse/core'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
import Mark from 'mark.js/src/vanilla.js'
import MiniSearch, { type SearchResult } from 'minisearch'
import { useRouter } from 'vitepress'
import {
computed,
createApp,
markRaw,
nextTick,
onBeforeUnmount,
onMounted,
ref,
shallowRef,
watch,
watchEffect,
type Ref
} from 'vue'
import type { ModalTranslations } from '../../../../types/local-search'
import { dataSymbol } from '../../app/data'
import { pathToFile } from '../../app/utils'
import { useData } from '../composables/data'
import { createTranslate } from '../support/translation'
defineProps<{
placeholder: string
}>()
const emit = defineEmits<{
(e: 'close'): void
}>()
const el = shallowRef<HTMLElement>()
const resultsEl = shallowRef<HTMLElement>()
const body = shallowRef<HTMLElement>()
/* Search */
const searchIndexData = shallowRef(localSearchIndex)
// hmr
if (import.meta.hot) {
import.meta.hot.accept('/@localSearchIndex', (m) => {
if (m) {
searchIndexData.value = m.default
}
})
}
interface Result {
title: string
titles: string[]
text?: string
}
const vitePressData = useData()
const { activate } = useFocusTrap(el, {
immediate: true,
allowOutsideClick: true,
clickOutsideDeactivates: true,
escapeDeactivates: true
})
const { localeIndex, theme } = vitePressData
const searchIndex = computedAsync(async () =>
markRaw(
MiniSearch.loadJSON<Result>(
(await searchIndexData.value[localeIndex.value]?.())?.default,
{
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles'],
searchOptions: {
fuzzy: 0.2,
prefix: true,
boost: { title: 4, text: 2, titles: 1 }
}
}
)
)
)
const disableQueryPersistence = computed(() => {
return (
theme.value.search?.provider === 'local' &&
theme.value.search.options?.disableQueryPersistence === true
)
})
const filterText = disableQueryPersistence.value
? ref('')
: useSessionStorage('vitepress:local-search-filter', '')
const showDetailedList = useLocalStorage(
'vitepress:local-search-detailed-list',
false
)
const disableDetailedView = computed(() => {
return (
theme.value.search?.provider === 'local' &&
theme.value.search.options?.disableDetailedView === true
)
})
watchEffect(() => {
if (disableDetailedView.value) {
showDetailedList.value = false
}
})
const results: Ref<(SearchResult & Result)[]> = shallowRef([])
const headingRegex = /<h(\d*).*?>.*?<a.*? href="#(.*?)".*?>.*?<\/a><\/h\1>/gi
const enableNoResults = ref(false)
watch(filterText, () => {
enableNoResults.value = false
})
const mark = computedAsync(async () => {
if (!resultsEl.value) return
return markRaw(new Mark(resultsEl.value))
}, null)
debouncedWatch(
() => [searchIndex.value, filterText.value, showDetailedList.value] as const,
async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => {
let canceled = false
onCleanup(() => {
canceled = true
})
if (!index) return
// Search
results.value = index
.search(filterTextValue)
.slice(0, 16) as (SearchResult & Result)[]
enableNoResults.value = true
// Highlighting
const mods = showDetailedListValue
? await Promise.all(results.value.map((r) => fetchExcerpt(r.id)))
: []
if (canceled) return
const c = new Map<string, Map<string, string>>()
for (const { id, mod } of mods) {
const comp = mod.default ?? mod
if (comp?.render) {
const app = createApp(comp)
// Silence warnings about missing components
app.config.warnHandler = () => {}
app.provide(dataSymbol, vitePressData)
const div = document.createElement('div')
app.mount(div)
const sections = div.innerHTML.split(headingRegex)
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
}
const terms = new Set<string>()
results.value = results.value.map((r) => {
const [id, anchor] = r.id.split('#')
const map = c.get(id)
const text = map?.get(anchor) ?? ''
for (const term in r.match) {
terms.add(term)
}
return { ...r, text }
})
await nextTick()
if (canceled) return
await new Promise((r) => {
mark.value?.unmark({
done: () => {
mark.value?.markRegExp(formMarkRegex(terms), { done: r })
}
})
})
const excerpts = el.value?.querySelectorAll('.result .excerpt') ?? []
for (const excerpt of excerpts) {
excerpt
.querySelector('mark[data-markjs="true"]')
?.scrollIntoView({ block: 'center' })
}
// FIXME: without this whole page scrolls to the bottom
resultsEl.value?.firstElementChild?.scrollIntoView({ block: 'start' })
},
{ debounce: 200, immediate: true }
)
async function fetchExcerpt(id: string) {
const file = pathToFile(id.slice(0, id.indexOf('#')))
try {
return { id, mod: await import(/*@vite-ignore*/ file) }
} catch (e) {
console.error(e)
return { id, mod: {} }
}
}
/* Search input focus */
const searchInput = ref<HTMLInputElement>()
function focusSearchInput() {
searchInput.value?.focus()
searchInput.value?.select()
}
onMounted(() => {
focusSearchInput()
})
function onSearchBarClick(event: PointerEvent) {
if (event.pointerType === 'mouse') {
focusSearchInput()
}
}
/* Search keyboard selection */
const selectedIndex = ref(0)
const disableMouseOver = ref(false)
watch(results, () => {
selectedIndex.value = 0
scrollToSelectedResult()
})
function scrollToSelectedResult() {
nextTick(() => {
const selectedEl = document.querySelector('.result.selected')
if (selectedEl) {
selectedEl.scrollIntoView({
block: 'nearest'
})
}
})
}
onKeyStroke('ArrowUp', (event) => {
event.preventDefault()
selectedIndex.value--
if (selectedIndex.value < 0) {
selectedIndex.value = results.value.length - 1
}
disableMouseOver.value = true
scrollToSelectedResult()
})
onKeyStroke('ArrowDown', (event) => {
event.preventDefault()
selectedIndex.value++
if (selectedIndex.value >= results.value.length) {
selectedIndex.value = 0
}
disableMouseOver.value = true
scrollToSelectedResult()
})
const router = useRouter()
onKeyStroke('Enter', () => {
const selectedPackage = results.value[selectedIndex.value]
if (selectedPackage) {
router.go(selectedPackage.id)
emit('close')
}
})
onKeyStroke('Escape', () => {
emit('close')
})
// Translations
const defaultTranslations: { modal: ModalTranslations } = {
modal: {
displayDetails: 'Display detailed list',
resetButtonTitle: 'Reset search',
backButtonTitle: 'Close search',
noResultsText: 'No results for',
footer: {
selectText: 'to select',
selectKeyAriaLabel: 'enter',
navigateText: 'to navigate',
navigateUpKeyAriaLabel: 'up arrow',
navigateDownKeyAriaLabel: 'down arrow',
closeText: 'to close',
closeKeyAriaLabel: 'escape'
}
}
}
const $t = createTranslate(theme.value.search?.options, defaultTranslations)
// Back
onMounted(() => {
// Prevents going to previous site
window.history.pushState(null, '', null)
})
useEventListener('popstate', (event) => {
event.preventDefault()
emit('close')
})
/** Lock body */
const isLocked = useScrollLock(body)
onMounted(() => {
body.value = document.body
nextTick(() => {
isLocked.value = true
nextTick().then(() => activate())
})
})
onBeforeUnmount(() => {
isLocked.value = false
})
function formMarkRegex(terms: Set<string>) {
return new RegExp(
[...terms]
.sort((a, b) => b.length - a.length)
.map((term) => {
return `(${term
.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
.replace(/-/g, '\\x2d')})`
})
.join('|'),
'gi'
)
}
</script>
<template>
<Teleport to="body">
<div ref="el" class="VPLocalSearchBox" aria-modal="true">
<div class="backdrop" @click="$emit('close')" />
<div class="shell">
<div class="search-bar" @pointerup="onSearchBarClick($event)">
<svg
class="search-icon"
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21l-4.35-4.35" />
</g>
</svg>
<div class="search-actions before">
<button
class="back-button"
:title="$t('modal.backButtonTitle')"
@click="$emit('close')"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 12H5m7 7l-7-7l7-7"
/>
</svg>
</button>
</div>
<input
ref="searchInput"
v-model="filterText"
:placeholder="placeholder"
class="search-input"
/>
<div class="search-actions">
<button
v-if="!disableDetailedView"
class="toggle-layout-button"
:class="{ 'detailed-list': showDetailedList }"
:title="$t('modal.displayDetails')"
@click="showDetailedList = !showDetailedList"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 14h7v7H3zM3 3h7v7H3zm11 1h7m-7 5h7m-7 6h7m-7 5h7"
/>
</svg>
</button>
<button
class="clear-button"
:title="$t('modal.resetButtonTitle')"
@click="filterText = ''"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20 5H9l-7 7l7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2Zm-2 4l-6 6m0-6l6 6"
/>
</svg>
</button>
</div>
</div>
<div
ref="resultsEl"
class="results"
@mousemove="disableMouseOver = false"
>
<a
v-for="(p, index) in results"
:key="p.id"
:href="p.id"
class="result"
:class="{
selected: selectedIndex === index
}"
:aria-label="[...p.titles, p.title].join(' > ')"
@mouseenter="!disableMouseOver && (selectedIndex = index)"
@focusin="selectedIndex = index"
@click="$emit('close')"
>
<div>
<div class="titles">
<span class="title-icon">#</span>
<span v-for="(t, index) in p.titles" :key="index" class="title">
<span class="text" v-html="t" />
<svg width="18" height="18" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m9 18l6-6l-6-6"
/>
</svg>
</span>
<span class="title main">
<span class="text" v-html="p.title" />
</span>
</div>
<div v-if="showDetailedList" class="excerpt-wrapper">
<div v-if="p.text" class="excerpt" inert>
<div class="vp-doc" v-html="p.text" />
</div>
<div class="excerpt-gradient-bottom" />
<div class="excerpt-gradient-top" />
</div>
</div>
</a>
<div
v-if="filterText && !results.length && enableNoResults"
class="no-results"
>
{{ $t('modal.noResultsText') }} "<strong>{{ filterText }}</strong
>"
</div>
</div>
<div class="search-keyboard-shortcuts">
<span>
<kbd :aria-label="$t('modal.footer.navigateUpKeyAriaLabel')">
<svg width="14" height="14" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 19V5m-7 7l7-7l7 7"
/>
</svg>
</kbd>
<kbd :aria-label="$t('modal.footer.navigateDownKeyAriaLabel')">
<svg width="14" height="14" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m7-7l-7 7l-7-7"
/>
</svg>
</kbd>
{{ $t('modal.footer.navigateText') }}
</span>
<span>
<kbd :aria-label="$t('modal.footer.selectKeyAriaLabel')">
<svg width="14" height="14" viewBox="0 0 24 24">
<g
fill="none"
stroke="currentcolor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="m9 10l-5 5l5 5" />
<path d="M20 4v7a4 4 0 0 1-4 4H4" />
</g>
</svg>
</kbd>
{{ $t('modal.footer.selectText') }}
</span>
<span>
<kbd :aria-label="$t('modal.footer.closeKeyAriaLabel')">esc</kbd>
{{ $t('modal.footer.closeText') }}
</span>
</div>
</div>
</div>
</Teleport>
</template>
<style scoped>
.VPLocalSearchBox {
position: fixed;
z-index: 100;
inset: 0;
display: flex;
}
.backdrop {
position: absolute;
inset: 0;
background: var(--vp-backdrop-bg-color);
transition: opacity 0.5s;
}
.shell {
position: relative;
padding: 12px;
margin: 64px auto;
display: flex;
flex-direction: column;
gap: 16px;
background: var(--vp-local-search-bg);
width: min(100vw - 60px, 900px);
height: min-content;
max-height: min(100vh - 128px, 900px);
border-radius: 6px;
}
@media (max-width: 768px) {
.shell {
margin: 0;
width: 100vw;
height: 100vh;
max-height: none;
border-radius: 0;
}
}
.search-bar {
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
display: flex;
align-items: center;
padding: 0 12px;
cursor: text;
}
@media (max-width: 768px) {
.search-bar {
padding: 0 8px;
}
}
.search-bar:focus-within {
border-color: var(--vp-c-brand);
}
.search-icon {
margin: 8px;
}
@media (max-width: 768px) {
.search-icon {
display: none;
}
}
.search-input {
padding: 6px 12px;
font-size: inherit;
width: 100%;
}
@media (max-width: 768px) {
.search-input {
padding: 6px 4px;
}
}
.search-actions {
display: flex;
gap: 4px;
}
@media (any-pointer: coarse) {
.search-actions {
gap: 8px;
}
}
@media (min-width: 769px) {
.search-actions.before {
display: none;
}
}
.search-actions button {
padding: 8px;
}
.search-actions button:hover,
.toggle-layout-button.detailed-list {
color: var(--vp-c-brand);
}
.search-keyboard-shortcuts {
font-size: 0.8rem;
opacity: 75%;
display: flex;
flex-wrap: wrap;
gap: 16px;
line-height: 14px;
}
.search-keyboard-shortcuts span {
display: flex;
align-items: center;
gap: 4px;
}
@media (max-width: 768px) {
.search-keyboard-shortcuts {
display: none;
}
}
.search-keyboard-shortcuts kbd {
background: rgba(128, 128, 128, 0.1);
border-radius: 4px;
padding: 3px 6px;
min-width: 24px;
display: inline-block;
text-align: center;
vertical-align: middle;
border: 1px solid rgba(128, 128, 128, 0.15);
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1);
}
.results {
display: flex;
flex-direction: column;
gap: 6px;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
}
.result {
display: flex;
align-items: center;
gap: 8px;
border-radius: 4px;
transition: none;
line-height: 1rem;
border: solid 2px var(--vp-local-search-result-border);
outline: none;
}
.result > div {
margin: 12px;
width: 100%;
overflow: hidden;
}
@media (max-width: 768px) {
.result > div {
margin: 8px;
}
}
.titles {
display: flex;
flex-wrap: wrap;
gap: 4px;
position: relative;
z-index: 1001;
padding: 2px 0;
}
.title {
display: flex;
align-items: center;
gap: 4px;
}
.title.main {
font-weight: 500;
}
.title-icon {
opacity: 0.5;
font-weight: 500;
color: var(--vp-c-brand);
}
.title svg {
opacity: 0.5;
}
.result.selected {
--vp-local-search-result-bg: var(--vp-local-search-result-selected-bg);
border-color: var(--vp-local-search-result-selected-border);
}
.excerpt-wrapper {
position: relative;
}
.excerpt {
opacity: 75%;
pointer-events: none;
max-height: 140px;
overflow: hidden;
position: relative;
opacity: 0.5;
margin-top: 4px;
}
.result.selected .excerpt {
opacity: 1;
}
.excerpt :deep(*) {
font-size: 0.8rem !important;
line-height: 130% !important;
}
.titles :deep(mark),
.excerpt :deep(mark) {
background-color: var(--vp-local-search-highlight-bg);
color: var(--vp-local-search-highlight-text);
border-radius: 2px;
padding: 0 2px;
}
.excerpt :deep(.vp-code-group) .tabs {
display: none;
}
.excerpt :deep(.vp-code-group) div[class*='language-'] {
border-radius: 8px !important;
}
.excerpt-gradient-bottom {
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 8px;
background: linear-gradient(transparent, var(--vp-local-search-result-bg));
z-index: 1000;
}
.excerpt-gradient-top {
position: absolute;
top: -1px;
left: 0;
width: 100%;
height: 8px;
background: linear-gradient(var(--vp-local-search-result-bg), transparent);
z-index: 1000;
}
.result.selected .titles,
.result.selected .title-icon {
color: var(--vp-c-brand) !important;
}
.no-results {
font-size: 0.9rem;
text-align: center;
padding: 12px;
}
svg {
flex: none;
}
</style>

@ -64,6 +64,7 @@ const classes = computed(() => ({
height: var(--vp-nav-height);
transition: border-color 0.5s, background-color 0.5s;
pointer-events: none;
white-space: nowrap;
}
.VPNavBar.has-sidebar {
@ -97,6 +98,11 @@ const classes = computed(() => ({
pointer-events: none;
}
.container > .title,
.container > .content {
pointer-events: none;
}
.container :deep(*) {
pointer-events: auto;
}
@ -169,6 +175,12 @@ const classes = computed(() => ({
}
}
@media (max-width: 768px) {
.content-body {
column-gap: 0.5rem;
}
}
.menu + .translations::before,
.menu + .appearance::before,
.menu + .social-links::before,

@ -1,5 +1,6 @@
<script lang="ts" setup>
import '@docsearch/css'
import { onKeyStroke } from '@vueuse/core'
import {
computed,
defineAsyncComponent,
@ -7,7 +8,13 @@ import {
onUnmounted,
ref
} from 'vue'
import type { DefaultTheme } from '../../shared'
import { useData } from '../composables/data'
import VPNavBarSearchButton from './VPNavBarSearchButton.vue'
const VPLocalSearchBox = __VP_LOCAL_SEARCH__
? defineAsyncComponent(() => import('./VPLocalSearchBox.vue'))
: () => null
const VPAlgoliaSearchBox = __ALGOLIA__
? defineAsyncComponent(() => import('./VPAlgoliaSearchBox.vue'))
@ -20,14 +27,15 @@ const { theme, localeIndex } = useData()
// hit the hotkey to invoke it.
const loaded = ref(false)
const metaKey = ref(`'Meta'`)
const buttonText = computed(
() =>
theme.value.algolia?.locales?.[localeIndex.value]?.translations?.button
?.buttonText ||
theme.value.algolia?.translations?.button?.buttonText ||
const buttonText = computed(() => {
const options = theme.value.search?.options ?? theme.value.algolia
return (
options?.locales?.[localeIndex.value]?.translations?.button?.buttonText ||
options?.translations?.button?.buttonText ||
'Search'
)
)
})
const preconnect = () => {
const id = 'VPAlgoliaPreconnect'
@ -37,27 +45,28 @@ const preconnect = () => {
const preconnect = document.createElement('link')
preconnect.id = id
preconnect.rel = 'preconnect'
preconnect.href = `https://${theme.value.algolia!.appId}-dsn.algolia.net`
preconnect.href = `https://${
((theme.value.search?.options as DefaultTheme.AlgoliaSearchOptions) ??
theme.value.algolia)!.appId
}-dsn.algolia.net`
preconnect.crossOrigin = ''
document.head.appendChild(preconnect)
})
}
onMounted(() => {
if (!theme.value.algolia) {
if (!__ALGOLIA__) {
return
}
preconnect()
// meta key detect (same logic as in @docsearch/js)
metaKey.value = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)
? `'⌘'`
: `'Ctrl'`
const handleSearchHotKey = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
const handleSearchHotKey = (event: KeyboardEvent) => {
if (
(event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) ||
(!isEditingContent(event) && event.key === '/')
) {
event.preventDefault()
load()
remove()
}
@ -94,44 +103,78 @@ function poll() {
}
}, 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
const showSearch = ref(false)
if (__VP_LOCAL_SEARCH__) {
onKeyStroke('k', (event) => {
if (event.ctrlKey || event.metaKey) {
event.preventDefault()
showSearch.value = true
}
})
onKeyStroke('/', (event) => {
if (!isEditingContent(event)) {
event.preventDefault()
showSearch.value = true
}
})
}
const metaKey = ref(`'Meta'`)
onMounted(() => {
// meta key detect (same logic as in @docsearch/js)
metaKey.value = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)
? `'⌘'`
: `'Ctrl'`
})
const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : ''
</script>
<template>
<div v-if="theme.algolia" class="VPNavBarSearch">
<VPAlgoliaSearchBox v-if="loaded" :algolia="theme.algolia" />
<div v-else id="docsearch">
<button
type="button"
class="DocSearch DocSearch-Button"
aria-label="Search"
@click="load"
>
<span class="DocSearch-Button-Container">
<svg
class="DocSearch-Search-Icon"
width="20"
height="20"
viewBox="0 0 20 20"
aria-label="search icon"
>
<path
d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z"
stroke="currentColor"
fill="none"
fill-rule="evenodd"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<span class="DocSearch-Button-Placeholder">{{ buttonText }}</span>
</span>
<span class="DocSearch-Button-Keys">
<kbd class="DocSearch-Button-Key"></kbd>
<kbd class="DocSearch-Button-Key">K</kbd>
</span>
</button>
</div>
<div class="VPNavBarSearch" :style="{ '--vp-meta-key': metaKey }">
<template v-if="provider === 'local'">
<VPLocalSearchBox
v-if="showSearch"
:placeholder="buttonText"
@close="showSearch = false"
/>
<div id="local-search">
<VPNavBarSearchButton
:placeholder="buttonText"
@click="showSearch = true"
/>
</div>
</template>
<template v-else-if="provider === 'algolia'">
<VPAlgoliaSearchBox
v-if="loaded"
:algolia="theme.search?.options ?? theme.algolia"
/>
<div v-else id="docsearch">
<VPNavBarSearchButton :placeholder="buttonText" @click="load" />
</div>
</template>
</div>
</template>
@ -154,178 +197,6 @@ function poll() {
}
}
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand);
--docsearch-highlight-color: var(--docsearch-primary-color);
--docsearch-text-color: var(--vp-c-text-1);
--docsearch-muted-color: var(--vp-c-text-2);
--docsearch-searchbox-shadow: none;
--docsearch-searchbox-focus-background: transparent;
--docsearch-key-gradient: transparent;
--docsearch-key-shadow: none;
--docsearch-modal-background: var(--vp-c-bg-soft);
--docsearch-footer-background: var(--vp-c-bg);
}
.dark .DocSearch {
--docsearch-modal-shadow: none;
--docsearch-footer-shadow: none;
--docsearch-logo-color: var(--vp-c-text-2);
--docsearch-hit-background: var(--vp-c-bg-soft-mute);
--docsearch-hit-color: var(--vp-c-text-2);
--docsearch-hit-shadow: none;
}
.DocSearch-Button {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
padding: 0;
width: 32px;
height: 55px;
background: transparent;
transition: border-color 0.25s;
}
.DocSearch-Button:hover {
background: transparent;
}
.DocSearch-Button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
.DocSearch-Button:focus:not(:focus-visible) {
outline: none !important;
}
@media (min-width: 768px) {
.DocSearch-Button {
justify-content: flex-start;
border: 1px solid transparent;
border-radius: 8px;
padding: 0 10px 0 12px;
width: 100%;
height: 40px;
background-color: var(--vp-c-bg-alt);
}
.DocSearch-Button:hover {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
}
.DocSearch-Button .DocSearch-Button-Container {
display: flex;
align-items: center;
}
.DocSearch-Button .DocSearch-Search-Icon {
position: relative;
width: 16px;
height: 16px;
color: var(--vp-c-text-1);
fill: currentColor;
transition: color 0.5s;
}
.DocSearch-Button:hover .DocSearch-Search-Icon {
color: var(--vp-c-text-1);
}
@media (min-width: 768px) {
.DocSearch-Button .DocSearch-Search-Icon {
top: 1px;
margin-right: 8px;
width: 14px;
height: 14px;
color: var(--vp-c-text-2);
}
}
.DocSearch-Button .DocSearch-Button-Placeholder {
display: none;
margin-top: 2px;
padding: 0 16px 0 0;
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-2);
transition: color 0.5s;
}
.DocSearch-Button:hover .DocSearch-Button-Placeholder {
color: var(--vp-c-text-1);
}
@media (min-width: 768px) {
.DocSearch-Button .DocSearch-Button-Placeholder {
display: inline-block;
}
}
.DocSearch-Button .DocSearch-Button-Keys {
/*rtl:ignore*/
direction: ltr;
display: none;
min-width: auto;
}
@media (min-width: 768px) {
.DocSearch-Button .DocSearch-Button-Keys {
display: flex;
align-items: center;
}
}
.DocSearch-Button .DocSearch-Button-Key {
display: block;
margin: 2px 0 0 0;
border: 1px solid var(--vp-c-divider);
/*rtl:begin:ignore*/
border-right: none;
border-radius: 4px 0 0 4px;
padding-left: 6px;
/*rtl:end:ignore*/
min-width: 0;
width: auto;
height: 22px;
line-height: 22px;
font-family: var(--vp-font-family-base);
font-size: 12px;
font-weight: 500;
transition: color 0.5s, border-color 0.5s;
}
.DocSearch-Button .DocSearch-Button-Key + .DocSearch-Button-Key {
/*rtl:begin:ignore*/
border-right: 1px solid var(--vp-c-divider);
border-left: none;
border-radius: 0 4px 4px 0;
padding-left: 2px;
padding-right: 6px;
/*rtl:end:ignore*/
}
.DocSearch-Button .DocSearch-Button-Key:first-child {
font-size: 1px;
letter-spacing: -12px;
color: transparent;
}
.DocSearch-Button .DocSearch-Button-Key:first-child:after {
content: v-bind(metaKey);
font-size: 12px;
letter-spacing: normal;
color: var(--docsearch-muted-color);
}
.DocSearch-Button .DocSearch-Button-Key:first-child > * {
display: none;
}
.dark .DocSearch-Footer {
border-top: 1px solid var(--vp-c-divider);
}

@ -0,0 +1,207 @@
<script lang="ts" setup>
defineProps<{
placeholder: string
}>()
</script>
<template>
<button type="button" class="DocSearch DocSearch-Button" aria-label="Search">
<span class="DocSearch-Button-Container">
<svg
class="DocSearch-Search-Icon"
width="20"
height="20"
viewBox="0 0 20 20"
aria-label="search icon"
>
<path
d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z"
stroke="currentColor"
fill="none"
fill-rule="evenodd"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<span class="DocSearch-Button-Placeholder">{{ placeholder }}</span>
</span>
<span class="DocSearch-Button-Keys">
<kbd class="DocSearch-Button-Key"></kbd>
<kbd class="DocSearch-Button-Key">K</kbd>
</span>
</button>
</template>
<style>
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand);
--docsearch-highlight-color: var(--docsearch-primary-color);
--docsearch-text-color: var(--vp-c-text-1);
--docsearch-muted-color: var(--vp-c-text-2);
--docsearch-searchbox-shadow: none;
--docsearch-searchbox-focus-background: transparent;
--docsearch-key-gradient: transparent;
--docsearch-key-shadow: none;
--docsearch-modal-background: var(--vp-c-bg-soft);
--docsearch-footer-background: var(--vp-c-bg);
}
.dark .DocSearch {
--docsearch-modal-shadow: none;
--docsearch-footer-shadow: none;
--docsearch-logo-color: var(--vp-c-text-2);
--docsearch-hit-background: var(--vp-c-bg-soft-mute);
--docsearch-hit-color: var(--vp-c-text-2);
--docsearch-hit-shadow: none;
}
.DocSearch-Button {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
padding: 0;
width: 48px;
height: 55px;
background: transparent;
transition: border-color 0.25s;
}
.DocSearch-Button:hover {
background: transparent;
}
.DocSearch-Button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
.DocSearch-Button:focus:not(:focus-visible) {
outline: none !important;
}
@media (min-width: 768px) {
.DocSearch-Button {
justify-content: flex-start;
border: 1px solid transparent;
border-radius: 8px;
padding: 0 10px 0 12px;
width: 100%;
height: 40px;
background-color: var(--vp-c-bg-alt);
}
.DocSearch-Button:hover {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
}
.DocSearch-Button .DocSearch-Button-Container {
display: flex;
align-items: center;
}
.DocSearch-Button .DocSearch-Search-Icon {
position: relative;
width: 16px;
height: 16px;
color: var(--vp-c-text-1);
fill: currentColor;
transition: color 0.5s;
}
.DocSearch-Button:hover .DocSearch-Search-Icon {
color: var(--vp-c-text-1);
}
@media (min-width: 768px) {
.DocSearch-Button .DocSearch-Search-Icon {
top: 1px;
margin-right: 8px;
width: 14px;
height: 14px;
color: var(--vp-c-text-2);
}
}
.DocSearch-Button .DocSearch-Button-Placeholder {
display: none;
margin-top: 2px;
padding: 0 16px 0 0;
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-2);
transition: color 0.5s;
}
.DocSearch-Button:hover .DocSearch-Button-Placeholder {
color: var(--vp-c-text-1);
}
@media (min-width: 768px) {
.DocSearch-Button .DocSearch-Button-Placeholder {
display: inline-block;
}
}
.DocSearch-Button .DocSearch-Button-Keys {
/*rtl:ignore*/
direction: ltr;
display: none;
min-width: auto;
}
@media (min-width: 768px) {
.DocSearch-Button .DocSearch-Button-Keys {
display: flex;
align-items: center;
}
}
.DocSearch-Button .DocSearch-Button-Key {
display: block;
margin: 2px 0 0 0;
border: 1px solid var(--vp-c-divider);
/*rtl:begin:ignore*/
border-right: none;
border-radius: 4px 0 0 4px;
padding-left: 6px;
/*rtl:end:ignore*/
min-width: 0;
width: auto;
height: 22px;
line-height: 22px;
font-family: var(--vp-font-family-base);
font-size: 12px;
font-weight: 500;
transition: color 0.5s, border-color 0.5s;
}
.DocSearch-Button .DocSearch-Button-Key + .DocSearch-Button-Key {
/*rtl:begin:ignore*/
border-right: 1px solid var(--vp-c-divider);
border-left: none;
border-radius: 0 4px 4px 0;
padding-left: 2px;
padding-right: 6px;
/*rtl:end:ignore*/
}
.DocSearch-Button .DocSearch-Button-Key:first-child {
font-size: 1px;
letter-spacing: -12px;
color: transparent;
}
.DocSearch-Button .DocSearch-Button-Key:first-child:after {
content: var(--vp-meta-key);
font-size: 12px;
letter-spacing: normal;
color: var(--docsearch-muted-color);
}
.DocSearch-Button .DocSearch-Button-Key:first-child > * {
display: none;
}
</style>

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

@ -74,6 +74,7 @@ watchPostEffect(async () => {
overflow-y: auto;
transform: translateX(-100%);
transition: opacity 0.5s, transform 0.25s ease;
overscroll-behavior: contain;
}
.VPSidebar.open {

@ -21,7 +21,6 @@ defineProps<{
<style scoped>
.VPSocialLinks {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
</style>

@ -75,16 +75,15 @@ watch(checked, (newIsDark) => {
</script>
<template>
<label title="toggle dark mode">
<VPSwitch
class="VPSwitchAppearance"
:aria-checked="checked"
@click="toggle"
>
<VPIconSun class="sun" />
<VPIconMoon class="moon" />
</VPSwitch>
</label>
<VPSwitch
title="toggle dark mode"
class="VPSwitchAppearance"
:aria-checked="checked"
@click="toggle"
>
<VPIconSun class="sun" />
<VPIconMoon class="moon" />
</VPSwitch>
</template>
<style scoped>

@ -31,9 +31,7 @@ defineProps<{
{{ member.org }}
</VPLink>
</p>
<p v-if="member.desc" class="desc">
{{ member.desc }}
</p>
<p v-if="member.desc" class="desc" v-html="member.desc"/>
<div v-if="member.links" class="links">
<VPSocialLinks :links="member.links" />
</div>
@ -180,6 +178,13 @@ defineProps<{
margin: 0 auto;
}
.desc :deep(a) {
font-weight: 500;
color: var(--vp-c-brand);
text-decoration-style: dotted;
transition: color 0.25s;
}
.links {
display: flex;
justify-content: center;

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

@ -60,8 +60,7 @@ export function useSidebar() {
const hasAside = computed(() => {
if (frontmatter.value.layout === 'home') return false
if (frontmatter.value.aside != null) return !!frontmatter.value.aside
if (theme.value.aside === false) return false
return true
return theme.value.aside !== false
})
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value)

@ -13,8 +13,10 @@
background-color: var(--vp-custom-block-info-bg);
}
.custom-block.info th {
color: var(--vp-custom-block-info-text);
.custom-block.custom-block th,
.custom-block.custom-block blockquote > p {
font-size: var(--vp-custom-block-font-size);
color: inherit;
}
.custom-block.info code {
@ -27,10 +29,6 @@
background-color: var(--vp-custom-block-tip-bg);
}
.custom-block.tip th {
color: var(--vp-custom-block-tip-text);
}
.custom-block.tip code {
background-color: var(--vp-custom-block-tip-code-bg);
}
@ -41,10 +39,6 @@
background-color: var(--vp-custom-block-warning-bg);
}
.custom-block.warning th {
color: var(--vp-custom-block-warning-text);
}
.custom-block.warning code {
background-color: var(--vp-custom-block-warning-code-bg);
}
@ -55,10 +49,6 @@
background-color: var(--vp-custom-block-danger-bg);
}
.custom-block.danger th {
color: var(--vp-custom-block-danger-text);
}
.custom-block.danger code {
background-color: var(--vp-custom-block-danger-code-bg);
}
@ -69,10 +59,6 @@
background-color: var(--vp-custom-block-details-bg);
}
.custom-block.details th {
color: var(--vp-custom-block-details-text);
}
.custom-block.details code {
background-color: var(--vp-custom-block-details-code-bg);
}

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

@ -1,12 +0,0 @@
#nprogress .bar {
background: var(--vp-c-brand);
}
#nprogress .spinner-icon {
border-top-color: var(--vp-c-brand);
border-left-color: var(--vp-c-brand);
}
#nprogress .peg {
box-shadow: 0 0 10px var(--vp-c-brand), 0 0 5px var(--vp-c-brand);
}

@ -208,6 +208,7 @@
:root {
--vp-code-line-height: 1.7;
--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-bg: #292b30;
@ -215,7 +216,7 @@
--vp-code-block-divider-color: #000000;
--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-symbol-color: var(--vp-c-green);
@ -244,6 +245,27 @@
--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
* -------------------------------------------------------------------------- */
@ -401,7 +423,7 @@
}
/**
* Component: CarbonAds
* Component: Carbon Ads
* -------------------------------------------------------------------------- */
:root {
@ -411,3 +433,16 @@
--vp-carbon-ads-hover-text-color: var(--vp-c-brand);
--vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1);
}
/**
* Component: Local Search
* -------------------------------------------------------------------------- */
:root {
--vp-local-search-bg: var(--vp-c-bg);
--vp-local-search-result-bg: var(--vp-c-bg);
--vp-local-search-result-border: var(--vp-c-divider);
--vp-local-search-result-selected-bg: var(--vp-c-bg);
--vp-local-search-result-selected-border: var(--vp-c-brand);
--vp-local-search-highlight-bg: var(--vp-c-green-lighter);
--vp-local-search-highlight-text: var(--vp-c-black);
}

@ -0,0 +1,60 @@
import { useData } from '../composables/data'
/**
* @param themeObject Can be an object with `translations` and `locales` properties
*/
export function createTranslate(
themeObject: any,
defaultTranslations: Record<string, any>
): (key: string) => string {
const { localeIndex } = useData()
function translate(key: string): string {
const keyPath = key.split('.')
const isObject = themeObject && typeof themeObject === 'object'
const locales =
(isObject && themeObject.locales?.[localeIndex.value]?.translations) ||
null
const translations = (isObject && themeObject.translations) || null
let localeResult: Record<string, any> | null = locales
let translationResult: Record<string, any> | null = translations
let defaultResult: Record<string, any> | null = defaultTranslations
const lastKey = keyPath.pop()!
for (const k of keyPath) {
let fallbackResult: Record<string, any> | null = null
const foundInFallback: any = defaultResult?.[k]
if (foundInFallback) {
fallbackResult = defaultResult = foundInFallback
}
const foundInTranslation: any = translationResult?.[k]
if (foundInTranslation) {
fallbackResult = translationResult = foundInTranslation
}
const foundInLocale: any = localeResult?.[k]
if (foundInLocale) {
fallbackResult = localeResult = foundInLocale
}
// Put fallback into unresolved results
if (!foundInFallback) {
defaultResult = fallbackResult
}
if (!foundInTranslation) {
translationResult = fallbackResult
}
if (!foundInLocale) {
localeResult = fallbackResult
}
}
return (
localeResult?.[lastKey] ??
translationResult?.[lastKey] ??
defaultResult?.[lastKey] ??
''
)
}
return translate
}

@ -16,7 +16,7 @@ export async function buildMPAClient(
root: config.srcDir,
cacheDir: config.cacheDir,
base: config.site.base,
logLevel: 'warn',
logLevel: config.vite?.logLevel ?? 'warn',
build: {
emptyOutDir: false,
outDir: config.outDir,

@ -7,11 +7,10 @@ import {
type UserConfig as ViteUserConfig
} from 'vite'
import type { GetModuleInfo, RollupOutput } from 'rollup'
import { slash } from '../utils/slash'
import type { SiteConfig } from '../config'
import { APP_PATH } from '../alias'
import { createVitePressPlugin } from '../plugin'
import { sanitizeFileName } from '../shared'
import { sanitizeFileName, slash } from '../shared'
import { buildMPAClient } from './buildMPAClient'
import { fileURLToPath } from 'url'
import { normalizePath } from 'vite'
@ -21,7 +20,7 @@ export const failMark = '\x1b[31m✖\x1b[0m'
// A list of default theme components that should only be loaded on demand.
const lazyDefaultThemeComponentsRE =
/VP(HomeSponsors|DocAsideSponsors|TeamPage|TeamMembers|AlgoliaSearch|CarbonAds|DocAsideCarbonAds)/
/VP(HomeSponsors|DocAsideSponsors|TeamPage|TeamMembers|LocalSearchBox|AlgoliaSearchBox|CarbonAds|DocAsideCarbonAds)/
const clientDir = normalizePath(
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../client')
@ -58,7 +57,7 @@ export async function bundle(
root: config.srcDir,
cacheDir: config.cacheDir,
base: config.site.base,
logLevel: 'warn',
logLevel: config.vite?.logLevel ?? 'warn',
plugins: await createVitePressPlugin(
config,
ssr,

@ -12,11 +12,11 @@ import {
notFoundPageData,
resolveSiteDataByRoute,
sanitizeFileName,
slash,
type HeadConfig,
type PageData,
type SSGContext
} from '../shared'
import { slash } from '../utils/slash'
import { deserializeFunctions } from '../utils/fnSerialize'
export async function renderPage(

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

@ -1,6 +1,4 @@
import type { Options as VuePluginOptions } from '@vitejs/plugin-vue'
import _debug from 'debug'
import fg from 'fast-glob'
import fs from 'fs-extra'
import path from 'path'
import c from 'picocolors'
@ -8,207 +6,26 @@ import {
createLogger,
loadConfigFromFile,
mergeConfig as mergeViteConfig,
normalizePath,
type Logger,
type UserConfig as ViteConfig
normalizePath
} from 'vite'
import { DEFAULT_THEME_PATH } from './alias'
import type { MarkdownOptions } from './markdown/markdown'
import {
dynamicRouteRE,
resolveDynamicRoutes,
type ResolvedRouteConfig
} from './plugins/dynamicRoutesPlugin'
import { resolveRewrites } from './plugins/rewritesPlugin'
import { resolvePages } from './plugins/dynamicRoutesPlugin'
import {
APPEARANCE_KEY,
type Awaitable,
type DefaultTheme,
type HeadConfig,
type LocaleConfig,
type LocaleSpecificConfig,
type PageData,
type SiteData,
type SSGContext
type SiteData
} from './shared'
import {
type UserConfig,
type RawConfigExports,
type SiteConfig
} from './siteConfig'
const debug = _debug('vitepress:config')
export interface UserConfig<ThemeConfig = any>
extends LocaleSpecificConfig<ThemeConfig> {
extends?: RawConfigExports<ThemeConfig>
base?: string
srcDir?: string
srcExclude?: string[]
outDir?: string
cacheDir?: string
shouldPreload?: (link: string, page: string) => boolean
locales?: LocaleConfig<ThemeConfig>
appearance?: boolean | 'dark'
lastUpdated?: boolean
/**
* MarkdownIt options
*/
markdown?: MarkdownOptions
/**
* Options to pass on to `@vitejs/plugin-vue`
*/
vue?: VuePluginOptions
/**
* Vite config
*/
vite?: ViteConfig
/**
* Configure the scroll offset when the theme has a sticky header.
* Can be a number or a selector element to get the offset from.
* Can also be an array of selectors in case some elements will be
* invisible due to responsive layout. VitePress will fallback to the next
* selector if a selector fails to match, or the matched element is not
* currently visible in viewport.
*/
scrollOffset?: number | string | string[]
/**
* Enable MPA / zero-JS mode.
* @experimental
*/
mpa?: boolean
/**
* Don't fail builds due to dead links.
*
* @default false
*/
ignoreDeadLinks?:
| boolean
| 'localhostLinks'
| (string | RegExp | ((link: string) => boolean))[]
/**
* Don't force `.html` on URLs.
*
* @default false
*/
cleanUrls?: boolean
/**
* Use web fonts instead of emitting font files to dist.
* The used theme should import a file named `fonts.(s)css` for this to work.
* If you are a theme author, to support this, place your web font import
* between `webfont-marker-begin` and `webfont-marker-end` comments.
*
* @default true in webcontainers, else false
*/
useWebFonts?: boolean
/**
* @experimental
*
* source -> destination
*/
rewrites?: Record<string, string>
/**
* Build end hook: called when SSG finish.
* @param siteConfig The resolved configuration.
*/
buildEnd?: (siteConfig: SiteConfig) => Awaitable<void>
/**
* Render end hook: called when SSR rendering is done.
*/
postRender?: (context: SSGContext) => Awaitable<SSGContext | void>
/**
* Head transform hook: runs before writing HTML to dist.
*
* This build hook will allow you to modify the head adding new entries that cannot be statically added.
*/
transformHead?: (context: TransformContext) => Awaitable<HeadConfig[] | void>
/**
* HTML transform hook: runs before writing HTML to dist.
*/
transformHtml?: (
code: string,
id: string,
ctx: TransformContext
) => Awaitable<string | void>
/**
* PageData transform hook: runs when rendering markdown to vue
*/
transformPageData?: (
pageData: PageData,
ctx: TransformPageContext
) => Awaitable<Partial<PageData> | { [key: string]: any } | void>
}
export interface TransformPageContext {
siteConfig: SiteConfig
}
export interface TransformContext {
page: string
siteConfig: SiteConfig
siteData: SiteData
pageData: PageData
title: string
description: string
head: HeadConfig[]
content: string
assets: string[]
}
export type RawConfigExports<ThemeConfig = any> =
| Awaitable<UserConfig<ThemeConfig>>
| (() => Awaitable<UserConfig<ThemeConfig>>)
export * from './siteConfig'
export { resolvePages } from './plugins/dynamicRoutesPlugin'
export interface SiteConfig<ThemeConfig = any>
extends Pick<
UserConfig,
| 'markdown'
| 'vue'
| 'vite'
| 'shouldPreload'
| 'mpa'
| 'lastUpdated'
| 'ignoreDeadLinks'
| 'cleanUrls'
| 'useWebFonts'
| 'postRender'
| 'buildEnd'
| 'transformHead'
| 'transformHtml'
| 'transformPageData'
> {
root: string
srcDir: string
site: SiteData<ThemeConfig>
configPath: string | undefined
configDeps: string[]
themeDir: string
outDir: string
cacheDir: string
tempDir: string
pages: string[]
dynamicRoutes: {
routes: ResolvedRouteConfig[]
fileToModulesMap: Record<string, Set<string>>
}
rewrites: {
map: Record<string, string | undefined>
inv: Record<string, string | undefined>
}
logger: Logger
userConfig: UserConfig
}
const debug = _debug('vitepress:config')
const resolve = (root: string, file: string) =>
normalizePath(path.resolve(root, `.vitepress`, file))
@ -438,33 +255,3 @@ function resolveSiteDataHead(userConfig?: UserConfig): HeadConfig[] {
return head
}
export async function resolvePages(srcDir: string, userConfig: UserConfig) {
// Important: fast-glob doesn't guarantee order of the returned files.
// We must sort the pages so the input list to rollup is stable across
// builds - otherwise different input order could result in different exports
// order in shared chunks which in turns invalidates the hash of every chunk!
// JavaScript built-in sort() is mandated to be stable as of ES2019 and
// supported in Node 12+, which is required by Vite.
const allMarkdownFiles = (
await fg(['**.md'], {
cwd: srcDir,
ignore: ['**/node_modules', ...(userConfig.srcExclude || [])]
})
).sort()
const pages = allMarkdownFiles.filter((p) => !dynamicRouteRE.test(p))
const dynamicRouteFiles = allMarkdownFiles.filter((p) =>
dynamicRouteRE.test(p)
)
const dynamicRoutes = await resolveDynamicRoutes(srcDir, dynamicRouteFiles)
pages.push(...dynamicRoutes.routes.map((r) => r.path))
const rewrites = resolveRewrites(pages, userConfig.rewrites)
return {
pages,
dynamicRoutes,
rewrites
}
}

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

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

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

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

@ -5,7 +5,7 @@
import type MarkdownIt from 'markdown-it'
import type { MarkdownEnv } from '../env'
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
@ -26,8 +26,7 @@ export const linkPlugin = (
if (hrefIndex >= 0) {
const hrefAttr = token.attrs![hrefIndex]
const url = hrefAttr[1]
const isExternal = EXTERNAL_URL_RE.test(url)
if (isExternal) {
if (isExternal(url)) {
Object.entries(externalAttrs).forEach(([key, val]) => {
token.attrSet(key, val)
})

@ -1,6 +1,10 @@
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!
md.renderer.rules.fence = (...args) => {
const [tokens, idx] = args
@ -10,17 +14,21 @@ export function preWrapperPlugin(md: MarkdownIt) {
const lang = extractLang(token.info)
const rawCode = fence(...args)
return `<div class="language-${lang}${
return `<div class="language-${lang}${getAdaptiveThemeMarker(options)}${
/ active( |$)/.test(token.info) ? ' active' : ''
}"><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) {
return info.match(/\[(.*)\]/)?.[1] || extractLang(info) || 'txt'
}
const extractLang = (info: string) => {
function extractLang(info: string) {
return info
.trim()
.replace(/:(no-)?line-numbers({| |$).*/, '')

@ -1,9 +1,8 @@
import { resolveTitleFromToken } from '@mdit-vue/shared'
import _debug from 'debug'
import fs from 'fs'
import LRUCache from 'lru-cache'
import { LRUCache } from 'lru-cache'
import path from 'path'
import c from 'picocolors'
import type { SiteConfig } from './config'
import {
createMarkdownRenderer,
@ -11,9 +10,13 @@ import {
type MarkdownOptions,
type MarkdownRenderer
} from './markdown'
import { EXTERNAL_URL_RE, type HeadConfig, type PageData } from './shared'
import {
EXTERNAL_URL_RE,
slash,
type HeadConfig,
type PageData
} from './shared'
import { getGitTimestamp } from './utils/getGitTimestamp'
import { slash } from './utils/slash'
const debug = _debug('vitepress:md')
const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 })
@ -22,7 +25,7 @@ const includesRE = /<!--\s*@include:\s*(.*?)\s*-->/g
export interface MarkdownCompileResult {
vueSrc: string
pageData: PageData
deadLinks: string[]
deadLinks: { url: string; file: string }[]
includes: string[]
}
@ -84,9 +87,15 @@ export async function createMarkdownToVueRenderFn(
// resolve includes
let includes: string[] = []
src = src.replace(includesRE, (m, m1) => {
if (!m1.length) return m
const atPresent = m1[0] === '@'
try {
const dir = path.dirname(fileOrig) // include paths are strict relative file paths w/o aliases
const includePath = path.join(dir, m1)
const dir = atPresent ? srcDir : path.dirname(fileOrig)
const includePath = path.join(
dir,
atPresent ? m1.slice(m1.length > 1 && m1[1] === '/' ? 2 : 1) : m1
)
const content = fs.readFileSync(includePath, 'utf-8')
includes.push(slash(includePath))
return content
@ -111,18 +120,9 @@ export async function createMarkdownToVueRenderFn(
} = env
// validate data.links
const deadLinks: string[] = []
const deadLinks: MarkdownCompileResult['deadLinks'] = []
const recordDeadLink = (url: string) => {
;(siteConfig?.logger ?? console).warn(
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)
deadLinks.push({ url, file: path.relative(srcDir, fileOrig) })
}
function shouldIgnoreDeadLink(url: string) {
@ -183,7 +183,8 @@ export async function createMarkdownToVueRenderFn(
frontmatter,
headers,
params,
relativePath
relativePath,
filePath: slash(path.relative(srcDir, fileOrig))
}
if (includeLastUpdatedData) {

@ -1,6 +1,5 @@
import path from 'path'
import c from 'picocolors'
import { slash } from './utils/slash'
import type { OutputAsset, OutputChunk } from 'rollup'
import {
mergeConfig,
@ -16,12 +15,17 @@ import {
resolveAliases
} from './alias'
import { resolveUserConfig, resolvePages, type SiteConfig } from './config'
import { clearCache, createMarkdownToVueRenderFn } from './markdownToVue'
import type { PageDataPayload } from './shared'
import {
clearCache,
createMarkdownToVueRenderFn,
type MarkdownCompileResult
} from './markdownToVue'
import { slash, type PageDataPayload } from './shared'
import { staticDataPlugin } from './plugins/staticDataPlugin'
import { webFontsPlugin } from './plugins/webFontsPlugin'
import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin'
import { rewritesPlugin } from './plugins/rewritesPlugin'
import { localSearchPlugin } from './plugins/localSearchPlugin'
import { serializeFunctions, deserializeFunctions } from './utils/fnSerialize'
declare module 'vite' {
@ -94,7 +98,7 @@ export async function createVitePressPlugin(
}
let siteData = site
let hasDeadLinks = false
let allDeadLinks: MarkdownCompileResult['deadLinks'] = []
let config: ResolvedConfig
const vitePressPlugin: Plugin = {
@ -121,8 +125,11 @@ export async function createVitePressPlugin(
alias: resolveAliases(siteConfig, ssr)
},
define: {
__ALGOLIA__: !!site.themeConfig.algolia,
__CARBON__: !!site.themeConfig.carbonAds
__VP_LOCAL_SEARCH__: site.themeConfig?.search?.provider === 'local',
__ALGOLIA__:
site.themeConfig?.search?.provider === 'algolia' ||
!!site.themeConfig?.algolia, // legacy
__CARBON__: !!site.themeConfig?.carbonAds
},
optimizeDeps: {
// force include vue to avoid duplicated copies when linked + optimized
@ -181,9 +188,7 @@ export async function createVitePressPlugin(
id,
config.publicDir
)
if (deadLinks.length) {
hasDeadLinks = true
}
allDeadLinks.push(...deadLinks)
if (includes.length) {
includes.forEach((i) => {
this.addWatchFile(i)
@ -194,8 +199,22 @@ export async function createVitePressPlugin(
},
renderStart() {
if (hasDeadLinks) {
throw new Error(`One or more pages contain dead links.`)
if (allDeadLinks.length > 0) {
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.`)
}
},
@ -360,6 +379,7 @@ export async function createVitePressPlugin(
vuePlugin,
webFontsPlugin(siteConfig.useWebFonts),
...(userViteConfig?.plugins || []),
await localSearchPlugin(siteConfig),
staticDataPlugin,
await dynamicRoutesPlugin(siteConfig)
]

@ -7,10 +7,46 @@ import {
import fs from 'fs-extra'
import c from 'picocolors'
import path from 'path'
import { resolvePages, type SiteConfig } from '../config'
import fg from 'fast-glob'
import { type SiteConfig, type UserConfig } from '../siteConfig'
import { resolveRewrites } from './rewritesPlugin'
export const dynamicRouteRE = /\[(\w+?)\]/g
export async function resolvePages(srcDir: string, userConfig: UserConfig) {
// Important: fast-glob doesn't guarantee order of the returned files.
// We must sort the pages so the input list to rollup is stable across
// builds - otherwise different input order could result in different exports
// order in shared chunks which in turns invalidates the hash of every chunk!
// JavaScript built-in sort() is mandated to be stable as of ES2019 and
// supported in Node 12+, which is required by Vite.
const allMarkdownFiles = (
await fg(['**.md'], {
cwd: srcDir,
ignore: ['**/node_modules', ...(userConfig.srcExclude || [])]
})
).sort()
const pages: string[] = []
const dynamicRouteFiles: string[] = []
allMarkdownFiles.forEach((file) => {
dynamicRouteRE.lastIndex = 0
;(dynamicRouteRE.test(file) ? dynamicRouteFiles : pages).push(file)
})
const dynamicRoutes = await resolveDynamicRoutes(srcDir, dynamicRouteFiles)
pages.push(...dynamicRoutes.routes.map((r) => r.path))
const rewrites = resolveRewrites(pages, userConfig.rewrites)
return {
pages,
dynamicRoutes,
rewrites
}
}
interface UserRouteConfig {
params: Record<string, string>
content?: string

@ -0,0 +1,260 @@
import path from 'node:path'
import type { Plugin, ViteDevServer } from 'vite'
import MiniSearch from 'minisearch'
import fs from 'fs-extra'
import _debug from 'debug'
import type { SiteConfig } from '../config'
import { createMarkdownRenderer } from '../markdown/markdown'
import { resolveSiteDataByRoute, slash } from '../shared'
const debug = _debug('vitepress:local-search')
const LOCAL_SEARCH_INDEX_ID = '@localSearchIndex'
const LOCAL_SEARCH_INDEX_REQUEST_PATH = '/' + LOCAL_SEARCH_INDEX_ID
interface IndexObject {
id: string
text: string
title: string
titles: string[]
}
export async function localSearchPlugin(
siteConfig: SiteConfig
): Promise<Plugin> {
if (siteConfig.site.themeConfig?.search?.provider !== 'local') {
return {
name: 'vitepress:local-search',
resolveId(id) {
if (id.startsWith(LOCAL_SEARCH_INDEX_ID)) {
return `/${id}`
}
},
load(id) {
if (id.startsWith(LOCAL_SEARCH_INDEX_REQUEST_PATH)) {
return `export default '{}'`
}
}
}
}
const md = await createMarkdownRenderer(
siteConfig.srcDir,
siteConfig.markdown,
siteConfig.site.base,
siteConfig.logger
)
const indexByLocales = new Map<string, MiniSearch<IndexObject>>()
function getIndexByLocale(locale: string) {
let index = indexByLocales.get(locale)
if (!index) {
index = new MiniSearch<IndexObject>({
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles']
})
indexByLocales.set(locale, index)
}
return index
}
function getLocaleForPath(file: string) {
const relativePath = slash(path.relative(siteConfig.srcDir, file))
const siteData = resolveSiteDataByRoute(siteConfig.site, relativePath)
return siteData?.localeIndex ?? 'root'
}
function getIndexForPath(file: string) {
const locale = getLocaleForPath(file)
return getIndexByLocale(locale)
}
let server: ViteDevServer | undefined
function onIndexUpdated() {
if (server) {
server.moduleGraph.onFileChange(LOCAL_SEARCH_INDEX_REQUEST_PATH)
// HMR
const mod = server.moduleGraph.getModuleById(
LOCAL_SEARCH_INDEX_REQUEST_PATH
)
if (!mod) return
server.ws.send({
type: 'update',
updates: [
{
acceptedPath: mod.url,
path: mod.url,
timestamp: Date.now(),
type: 'js-update'
}
]
})
}
}
function getDocId(file: string) {
let relFile = slash(path.relative(siteConfig.srcDir, file))
relFile = siteConfig.rewrites.map[relFile] || relFile
let id = slash(path.join(siteConfig.site.base, relFile))
id = id.replace(/\/index\.md$/, '/')
id = id.replace(/\.md$/, siteConfig.cleanUrls ? '' : '.html')
return id
}
async function indexAllFiles(files: string[]) {
const documentsByLocale = new Map<string, IndexObject[]>()
await Promise.all(
files
.filter((file) => fs.existsSync(file))
.map(async (file) => {
const fileId = getDocId(file)
const sections = splitPageIntoSections(
md.render(await fs.readFile(file, 'utf-8'))
)
const locale = getLocaleForPath(file)
let documents = documentsByLocale.get(locale)
if (!documents) {
documents = []
documentsByLocale.set(locale, documents)
}
documents.push(
...sections.map((section) => ({
id: `${fileId}#${section.anchor}`,
text: section.text,
title: section.titles.at(-1)!,
titles: section.titles.slice(0, -1)
}))
)
})
)
for (const [locale, documents] of documentsByLocale) {
const index = getIndexByLocale(locale)
index.removeAll()
await index.addAllAsync(documents)
}
debug(`🔍️ Indexed ${files.length} files`)
}
async function scanForBuild() {
await indexAllFiles(
siteConfig.pages.map((f) => path.join(siteConfig.srcDir, f))
)
}
return {
name: 'vitepress:local-search',
async configureServer(_server) {
server = _server
await scanForBuild()
onIndexUpdated()
},
resolveId(id) {
if (id.startsWith(LOCAL_SEARCH_INDEX_ID)) {
return `/${id}`
}
},
async load(id) {
if (id === LOCAL_SEARCH_INDEX_REQUEST_PATH) {
if (process.env.NODE_ENV === 'production') {
await scanForBuild()
}
let records: string[] = []
for (const [locale] of indexByLocales) {
records.push(
`${JSON.stringify(
locale
)}: () => import('@localSearchIndex${locale}')`
)
}
return `export default {${records.join(',')}}`
} else if (id.startsWith(LOCAL_SEARCH_INDEX_REQUEST_PATH)) {
return `export default ${JSON.stringify(
JSON.stringify(
indexByLocales.get(
id.replace(LOCAL_SEARCH_INDEX_REQUEST_PATH, '')
) ?? {}
)
)}`
}
},
async handleHotUpdate(ctx) {
if (ctx.file.endsWith('.md')) {
const fileId = getDocId(ctx.file)
if (!fs.existsSync(ctx.file)) {
return
}
const index = getIndexForPath(ctx.file)
const sections = splitPageIntoSections(
md.render(await fs.readFile(ctx.file, 'utf-8'))
)
for (const section of sections) {
const id = `${fileId}#${section.anchor}`
if (index.has(id)) {
index.discard(id)
}
index.add({
id,
text: section.text,
title: section.titles.at(-1)!,
titles: section.titles.slice(0, -1)
})
}
debug('🔍️ Updated', ctx.file)
onIndexUpdated()
}
}
}
}
const headingRegex = /<h(\d*).*?>(.*?<a.*? href="#.*?".*?>.*?<\/a>)<\/h\1>/gi
const headingContentRegex = /(.*?)<a.*? href="#(.*?)".*?>.*?<\/a>/i
interface PageSection {
anchor: string
titles: string[]
text: string
}
/**
* Splits HTML into sections based on headings
*/
function splitPageIntoSections(html: string) {
const result = html.split(headingRegex)
result.shift()
let parentTitles: string[] = []
const sections: PageSection[] = []
for (let i = 0; i < result.length; i += 3) {
const level = parseInt(result[i]) - 1
const heading = result[i + 1]
const headingResult = headingContentRegex.exec(heading)
const title = clearHtmlTags(headingResult?.[1] ?? '').trim()
const anchor = headingResult?.[2] ?? ''
const content = result[i + 2]
if (!title || !content) continue
const titles = parentTitles.slice(0, level)
titles[level] = title
sections.push({ anchor, titles, text: getSearchableText(content) })
if (level === 0) {
parentTitles = [title]
} else {
parentTitles[level] = title
}
}
return sections
}
function getSearchableText(content: string) {
content = clearHtmlTags(content)
return content
}
function clearHtmlTags(str: string) {
return str.replace(/<[^>]*>/g, '')
}

@ -1,6 +1,6 @@
import type { Plugin } from 'vite'
import { compile, match } from 'path-to-regexp'
import type { SiteConfig, UserConfig } from '../config'
import type { SiteConfig, UserConfig } from '../siteConfig'
export function resolveRewrites(
pages: string[],

@ -8,7 +8,7 @@ import path, { dirname, resolve } from 'path'
import { isMatch } from 'micromatch'
import glob from 'fast-glob'
const loaderMatch = /\.data\.(j|t)s$/
const loaderMatch = /\.data\.(j|t)s($|\?)/
let server: ViteDevServer
@ -71,7 +71,7 @@ export const staticDataPlugin: Plugin = {
} else {
// use vite's load config util as a away to load Node.js file with
// TS & native ESM support
const res = await loadConfigFromFile({} as any, id)
const res = await loadConfigFromFile({} as any, id.replace(/\?.*$/, ''))
// record deps for hmr
if (server && res) {

@ -0,0 +1,95 @@
import colors from 'picocolors'
import type { ViteDevServer } from 'vite'
export type CLIShortcut = {
key: string
description: string
action(server: ViteDevServer): void | Promise<void>
}
export function bindShortcuts(server: ViteDevServer): void {
if (!server.httpServer || !process.stdin.isTTY || process.env.CI) {
return
}
server.config.logger.info(
colors.dim(colors.green(' ➜')) +
colors.dim(' press ') +
colors.bold('h') +
colors.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(
[
'',
colors.bold(' Shortcuts'),
...SHORTCUTS.map(
(shortcut) =>
colors.dim(' press ') +
colors.bold(shortcut.key) +
colors.dim(` to ${shortcut.description}`)
)
].join('\n')
)
}
const shortcut = SHORTCUTS.find((shortcut) => shortcut.key === input)
if (!shortcut) return
actionRunning = true
await shortcut.action(server)
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: '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())
}
}
]

@ -0,0 +1,208 @@
import {
type Awaitable,
type HeadConfig,
type LocaleConfig,
type LocaleSpecificConfig,
type PageData,
type SiteData,
type SSGContext
} from './shared'
import type { MarkdownOptions } from './markdown/markdown'
import type { Options as VuePluginOptions } from '@vitejs/plugin-vue'
import { type Logger, type UserConfig as ViteConfig } from 'vite'
export type RawConfigExports<ThemeConfig = any> =
| Awaitable<UserConfig<ThemeConfig>>
| (() => Awaitable<UserConfig<ThemeConfig>>)
export interface TransformContext {
page: string
siteConfig: SiteConfig
siteData: SiteData
pageData: PageData
title: string
description: string
head: HeadConfig[]
content: string
assets: string[]
}
interface UserRouteConfig {
params: Record<string, string>
content?: string
}
export type ResolvedRouteConfig = UserRouteConfig & {
/**
* the raw route (relative to src root), e.g. foo/[bar].md
*/
route: string
/**
* the actual path with params resolved (relative to src root), e.g. foo/1.md
*/
path: string
/**
* absolute fs path
*/
fullPath: string
}
export interface TransformPageContext {
siteConfig: SiteConfig
}
export interface UserConfig<ThemeConfig = any>
extends LocaleSpecificConfig<ThemeConfig> {
extends?: RawConfigExports<ThemeConfig>
base?: string
srcDir?: string
srcExclude?: string[]
outDir?: string
cacheDir?: string
shouldPreload?: (link: string, page: string) => boolean
locales?: LocaleConfig<ThemeConfig>
appearance?: boolean | 'dark'
lastUpdated?: boolean
/**
* MarkdownIt options
*/
markdown?: MarkdownOptions
/**
* Options to pass on to `@vitejs/plugin-vue`
*/
vue?: VuePluginOptions
/**
* Vite config
*/
vite?: ViteConfig
/**
* Configure the scroll offset when the theme has a sticky header.
* Can be a number or a selector element to get the offset from.
* Can also be an array of selectors in case some elements will be
* invisible due to responsive layout. VitePress will fallback to the next
* selector if a selector fails to match, or the matched element is not
* currently visible in viewport.
*/
scrollOffset?: number | string | string[]
/**
* Enable MPA / zero-JS mode.
* @experimental
*/
mpa?: boolean
/**
* Don't fail builds due to dead links.
*
* @default false
*/
ignoreDeadLinks?:
| boolean
| 'localhostLinks'
| (string | RegExp | ((link: string) => boolean))[]
/**
* Don't force `.html` on URLs.
*
* @default false
*/
cleanUrls?: boolean
/**
* Use web fonts instead of emitting font files to dist.
* The used theme should import a file named `fonts.(s)css` for this to work.
* If you are a theme author, to support this, place your web font import
* between `webfont-marker-begin` and `webfont-marker-end` comments.
*
* @default true in webcontainers, else false
*/
useWebFonts?: boolean
/**
* @experimental
*
* source -> destination
*/
rewrites?: Record<string, string>
/**
* Build end hook: called when SSG finish.
* @param siteConfig The resolved configuration.
*/
buildEnd?: (siteConfig: SiteConfig) => Awaitable<void>
/**
* Render end hook: called when SSR rendering is done.
*/
postRender?: (context: SSGContext) => Awaitable<SSGContext | void>
/**
* Head transform hook: runs before writing HTML to dist.
*
* This build hook will allow you to modify the head adding new entries that cannot be statically added.
*/
transformHead?: (context: TransformContext) => Awaitable<HeadConfig[] | void>
/**
* HTML transform hook: runs before writing HTML to dist.
*/
transformHtml?: (
code: string,
id: string,
ctx: TransformContext
) => Awaitable<string | void>
/**
* PageData transform hook: runs when rendering markdown to vue
*/
transformPageData?: (
pageData: PageData,
ctx: TransformPageContext
) => Awaitable<Partial<PageData> | { [key: string]: any } | void>
}
export interface SiteConfig<ThemeConfig = any>
extends Pick<
UserConfig,
| 'markdown'
| 'vue'
| 'vite'
| 'shouldPreload'
| 'mpa'
| 'lastUpdated'
| 'ignoreDeadLinks'
| 'cleanUrls'
| 'useWebFonts'
| 'postRender'
| 'buildEnd'
| 'transformHead'
| 'transformHtml'
| 'transformPageData'
> {
root: string
srcDir: string
site: SiteData<ThemeConfig>
configPath: string | undefined
configDeps: string[]
themeDir: string
outDir: string
cacheDir: string
tempDir: string
pages: string[]
dynamicRoutes: {
routes: ResolvedRouteConfig[]
fileToModulesMap: Record<string, Set<string>>
}
rewrites: {
map: Record<string, string | undefined>
inv: Record<string, string | undefined>
}
logger: Logger
userConfig: UserConfig
}

@ -1,3 +0,0 @@
export function slash(p: string): string {
return p.replace(/\\/g, '/')
}

@ -11,7 +11,7 @@ export type {
PageDataPayload,
SiteData,
SSGContext
} from '../../types/shared.js'
} from '../../types/shared'
export const EXTERNAL_URL_RE = /^[a-z]+:/i
export const PATHNAME_PROTOCOL_RE = /^pathname:\/\//
@ -23,6 +23,7 @@ export const inBrowser = typeof document !== 'undefined'
export const notFoundPageData: PageData = {
relativePath: '',
filePath: '',
title: '404',
description: 'Not Found',
headers: [],
@ -165,3 +166,7 @@ export function sanitizeFileName(name: string): string {
.replace(/(^|\/)_+(?=[^/]*$)/, '$1')
)
}
export function slash(p: string): string {
return p.replace(/\\/g, '/')
}

@ -1,4 +1,6 @@
import { DocSearchProps } from './docsearch.js'
import type { DocSearchProps } from './docsearch.js'
import type { LocalSearchTranslations } from './local-search.js'
import type { PageData } from './shared.js'
export namespace DefaultTheme {
export interface Config {
@ -100,8 +102,12 @@ export namespace DefaultTheme {
*/
langMenuLabel?: string
search?:
| { provider: 'local'; options?: LocalSearchOptions }
| { provider: 'algolia'; options: AlgoliaSearchOptions }
/**
* The algolia options. Leave it undefined to disable the search feature.
* @deprecated Use `search` instead.
*/
algolia?: AlgoliaSearchOptions
@ -210,8 +216,9 @@ export namespace DefaultTheme {
* Pattern for edit link.
*
* @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.
@ -285,6 +292,23 @@ export namespace DefaultTheme {
label?: string
}
// local search --------------------------------------------------------------
export interface LocalSearchOptions {
/**
* @default false
*/
disableDetailedView?: boolean
/**
* @default false
*/
disableQueryPersistence?: boolean
translations?: LocalSearchTranslations
locales?: Record<string, Partial<Omit<LocalSearchOptions, 'locales'>>>
}
// algolia -------------------------------------------------------------------
/**

@ -0,0 +1,27 @@
export interface LocalSearchTranslations {
button?: ButtonTranslations
modal?: ModalTranslations
}
interface ButtonTranslations {
buttonText?: string
buttonAriaLabel?: string
}
interface ModalTranslations {
displayDetails?: string
resetButtonTitle?: string
backButtonTitle?: string
noResultsText?: string
footer?: FooterTranslations
}
interface FooterTranslations {
selectText?: string
selectKeyAriaLabel?: string
navigateText?: string
navigateUpKeyAriaLabel?: string
navigateDownKeyAriaLabel?: string
closeText?: string
closeKeyAriaLabel?: string
}

1
types/shared.d.ts vendored

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

Loading…
Cancel
Save