Merge branch 'main' into SimonEast/patch-81677

pull/2377/head
Simon East 2 years ago committed by GitHub
commit a27a1b4524
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -30,6 +30,8 @@ After cloning the repo, run:
```sh ```sh
# install the dependencies of the project # install the dependencies of the project
$ pnpm install $ pnpm install
# setup git hooks
$ pnpm simple-git-hooks
``` ```
### Setup VitePress Dev Environment ### Setup VitePress Dev Environment

@ -14,6 +14,7 @@ concurrency:
jobs: jobs:
action: action:
if: github.repository == 'vuejs/vitepress'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v4 - uses: dessant/lock-threads@v4

@ -7,6 +7,7 @@ on:
jobs: jobs:
release: release:
if: github.repository == 'vuejs/vitepress'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

@ -5,3 +5,5 @@ pnpm-lock.yaml
cache cache
template template
temp temp
!CHANGELOG.md
.temp

File diff suppressed because it is too large Load Diff

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

@ -87,6 +87,14 @@ export default defineConfig({
title: 'Example', title: 'Example',
description: 'An example app using VitePress.', description: 'An example app using VitePress.',
themeConfig: { themeConfig: {
sidebar sidebar,
search: {
provider: 'local',
options: {
exclude(relativePath) {
return relativePath.startsWith('local-search/excluded')
}
}
}
} }
}) })

@ -0,0 +1 @@
# Local search config excluded

@ -0,0 +1,5 @@
---
search: false
---
# Local search frontmatter excluded

@ -0,0 +1 @@
# Local search included

@ -0,0 +1,34 @@
describe('local search', () => {
beforeEach(async () => {
await goto('/')
// FIXME: remove this when optimizeDeps.include is fixed
await page.locator('#local-search button').click()
await goto('/')
})
test('exclude content from search results', async () => {
await page.locator('#local-search button').click()
const input = await page.waitForSelector('input#localsearch-input')
await input.type('local')
await page.waitForSelector('ul#localsearch-list', { state: 'visible' })
const searchResults = page.locator('#localsearch-list')
expect(await searchResults.locator('li[role=option]').count()).toBe(1)
expect(
await searchResults.filter({ hasText: 'Local search included' }).count()
).toBe(1)
expect(
await searchResults.filter({ hasText: 'Local search excluded' }).count()
).toBe(0)
expect(
await searchResults
.filter({ hasText: 'Local search frontmatter excluded' })
.count()
).toBe(0)
})
})

@ -1,7 +1,11 @@
# Foo # Foo
This is before region
<!-- #region snippet --> <!-- #region snippet -->
## Region ## Region
this is region This is a region
<!-- #endregion snippet --> <!-- #endregion snippet -->
This is after region

@ -180,3 +180,19 @@ export default config
## Markdown At File Inclusion ## Markdown At File Inclusion
<!--@include: @/markdown-extensions/bar.md--> <!--@include: @/markdown-extensions/bar.md-->
## Markdown Nested File Inclusion
<!--@include: ./nested-include.md-->
## Markdown File Inclusion with Range
<!--@include: ./foo.md{6,8}-->
## Markdown File Inclusion with Range without Start
<!--@include: ./foo.md{,8}-->
## Markdown File Inclusion with Range without End
<!--@include: ./foo.md{6,}-->

@ -5,6 +5,8 @@ const getClassList = async (locator: Locator) => {
return className?.split(' ').filter(Boolean) ?? [] return className?.split(' ').filter(Boolean) ?? []
} }
const trim = (str?: string | null) => str?.replace(/\u200B/g, '').trim()
beforeEach(async () => { beforeEach(async () => {
await goto('/markdown-extensions/') await goto('/markdown-extensions/')
}) })
@ -63,7 +65,7 @@ describe('Table of Contents', () => {
test('render toc', async () => { test('render toc', async () => {
const items = page.locator('#table-of-contents + nav ul li') const items = page.locator('#table-of-contents + nav ul li')
const count = await items.count() const count = await items.count()
expect(count).toBe(24) expect(count).toBe(35)
}) })
}) })
@ -161,7 +163,7 @@ describe('Line Numbers', () => {
describe('Import Code Snippets', () => { describe('Import Code Snippets', () => {
test('basic', async () => { test('basic', async () => {
const lines = page.locator('#basic-code-snippet + div code > span') const lines = page.locator('#basic-code-snippet + div code > span')
expect(await lines.count()).toBe(7) expect(await lines.count()).toBe(11)
}) })
test('specify region', async () => { test('specify region', async () => {
@ -214,7 +216,7 @@ describe('Code Groups', () => {
// blocks // blocks
const blocks = div.locator('.blocks > div') const blocks = div.locator('.blocks > div')
expect(await blocks.nth(0).locator('code > span').count()).toBe(7) expect(await blocks.nth(0).locator('code > span').count()).toBe(11)
expect(await getClassList(blocks.nth(1))).toContain('line-numbers-mode') expect(await getClassList(blocks.nth(1))).toContain('line-numbers-mode')
expect(await getClassList(blocks.nth(1))).toContain('language-ts') expect(await getClassList(blocks.nth(1))).toContain('language-ts')
expect(await blocks.nth(1).locator('code > span').count()).toBe(3) expect(await blocks.nth(1).locator('code > span').count()).toBe(3)
@ -229,8 +231,47 @@ describe('Markdown File Inclusion', () => {
const h1 = page.locator('#markdown-file-inclusion + h1') const h1 = page.locator('#markdown-file-inclusion + h1')
expect(await h1.getAttribute('id')).toBe('foo') expect(await h1.getAttribute('id')).toBe('foo')
}) })
test('render markdown using @', async () => { test('render markdown using @', async () => {
const h1 = page.locator('#markdown-at-file-inclusion + h1') const h1 = page.locator('#markdown-at-file-inclusion + h1')
expect(await h1.getAttribute('id')).toBe('bar') expect(await h1.getAttribute('id')).toBe('bar')
}) })
test('render markdown using nested inclusion', async () => {
const h1 = page.locator('#markdown-nested-file-inclusion + h1')
expect(await h1.getAttribute('id')).toBe('foo-1')
})
test('render markdown using nested inclusion inside sub folder', async () => {
const h1 = page.locator('#after-foo + h1')
expect(await h1.getAttribute('id')).toBe('inside-sub-folder')
const h2 = page.locator('#after-foo + h1 + h2')
expect(await h2.getAttribute('id')).toBe('sub-sub')
const h3 = page.locator('#after-foo + h1 + h2 + h3')
expect(await h3.getAttribute('id')).toBe('sub-sub-sub')
})
test('support selecting range', async () => {
const h2 = page.locator('#markdown-file-inclusion-with-range + h2')
expect(trim(await h2.textContent())).toBe('Region')
const p = page.locator('#markdown-file-inclusion-with-range + h2 + p')
expect(trim(await p.textContent())).toBe('This is a region')
})
test('support selecting range without specifying start', async () => {
const p = page.locator(
'#markdown-file-inclusion-with-range-without-start ~ p'
)
expect(trim(await p.nth(0).textContent())).toBe('This is before region')
expect(trim(await p.nth(1).textContent())).toBe('This is a region')
})
test('support selecting range without specifying end', async () => {
const p = page.locator(
'#markdown-file-inclusion-with-range-without-end ~ p'
)
expect(trim(await p.nth(0).textContent())).toBe('This is a region')
expect(trim(await p.nth(1).textContent())).toBe('This is after region')
})
}) })

@ -0,0 +1,5 @@
<!--@include: ./foo.md-->
### After Foo
<!--@include: ./subfolder/inside-subfolder.md-->

@ -0,0 +1,3 @@
# Inside sub folder
<!--@include: ./subsub/subsub.md-->

@ -0,0 +1,3 @@
## Sub sub
<!--@include: ./subsubsub/subsubsub.md-->

@ -1,6 +1,14 @@
{ {
"name": "tests-e2e",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": {
"test": "vitest run",
"watch": "DEBUG=1 vitest",
"site:dev": "vitepress",
"site:build": "vitepress build",
"site:preview": "vitepress preview"
},
"devDependencies": { "devDependencies": {
"vitepress": "workspace:*" "vitepress": "workspace:*"
} }

@ -5,7 +5,7 @@ const timeout = 60_000
export default defineConfig({ export default defineConfig({
test: { test: {
setupFiles: ['vitestSetup.ts'], setupFiles: ['vitestSetup.ts'],
globalSetup: ['__tests__/e2e/vitestGlobalSetup.ts'], globalSetup: ['vitestGlobalSetup.ts'],
testTimeout: timeout, testTimeout: timeout,
hookTimeout: timeout, hookTimeout: timeout,
teardownTimeout: timeout, teardownTimeout: timeout,

@ -7,7 +7,7 @@ import type { Server } from 'net'
let browserServer: BrowserServer let browserServer: BrowserServer
let server: ViteDevServer | Server let server: ViteDevServer | Server
const root = '__tests__/e2e' const root = '.'
export async function setup() { export async function setup() {
browserServer = await chromium.launchServer({ browserServer = await chromium.launchServer({

@ -1,57 +1,50 @@
import { chromium, type Browser, type Page } from 'playwright-chromium'
import { fileURLToPath } from 'url'
import path from 'path'
import fs from 'fs-extra' import fs from 'fs-extra'
import {
scaffold,
build,
createServer,
serve,
ScaffoldThemeType,
type ScaffoldOptions
} from 'vitepress'
import type { ViteDevServer } from 'vite'
import type { Server } from 'net'
import getPort from 'get-port' import getPort from 'get-port'
import { chromium } from 'playwright-chromium'
import { fileURLToPath, URL } from 'url'
import { createServer, scaffold, ScaffoldThemeType } from 'vitepress'
let browser: Browser const root = fileURLToPath(new URL('./.temp', import.meta.url))
let page: Page
beforeAll(async () => { const browser = await chromium.launch({
browser = await chromium.connect(process.env['WS_ENDPOINT']!) headless: !process.env.DEBUG,
page = await browser.newPage() args: process.env.CI
? ['--no-sandbox', '--disable-setuid-sandbox']
: undefined
}) })
const page = await browser.newPage()
const themes = [
ScaffoldThemeType.Default,
ScaffoldThemeType.DefaultCustom,
ScaffoldThemeType.Custom
]
const usingTs = [false, true]
const variations = themes.flatMap((theme) =>
usingTs.map(
(useTs) => [`${theme}${useTs ? ' + ts' : ''}`, { theme, useTs }] as const
)
)
afterAll(async () => { afterAll(async () => {
await page.close() await page.close()
await browser.close() await browser.close()
}) })
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'temp') test.each(variations)('init %s', async (_, { theme, useTs }) => {
await fs.remove(root)
async function testVariation(options: ScaffoldOptions) { scaffold({ root, theme, useTs, injectNpmScripts: false })
fs.removeSync(root)
scaffold({
...options,
root
})
let server: ViteDevServer | Server
const port = await getPort() const port = await getPort()
const server = await createServer(root, { port })
await server.listen()
async function goto(path: string) { async function goto(path: string) {
await page.goto(`http://localhost:${port}${path}`) await page.goto(`http://localhost:${port}${path}`)
await page.waitForSelector('#app div') await page.waitForSelector('#app div')
} }
if (process.env['VITE_TEST_BUILD']) {
await build(root)
server = (await serve({ root, port })).server
} else {
server = await createServer(root, { port })
await server!.listen()
}
try { try {
await goto('/') await goto('/')
expect(await page.textContent('h1')).toMatch('My Awesome Project') expect(await page.textContent('h1')).toMatch('My Awesome Project')
@ -66,33 +59,10 @@ async function testVariation(options: ScaffoldOptions) {
await page.click('a[href="/api-examples.html"]') await page.click('a[href="/api-examples.html"]')
await page.waitForSelector('pre code') await page.waitForSelector('pre code')
expect(await page.textContent('h1')).toMatch('Runtime API Examples') expect(await page.textContent('h1')).toMatch('Runtime API Examples')
// teardown
} finally { } finally {
fs.removeSync(root) await fs.remove(root)
if ('ws' in server) {
await server.close() await server.close()
} else {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()))
})
}
}
}
const themes = [
ScaffoldThemeType.Default,
ScaffoldThemeType.DefaultCustom,
ScaffoldThemeType.Custom
]
const usingTs = [false, true]
for (const theme of themes) {
for (const useTs of usingTs) {
test(`${theme}${useTs ? ` + TypeScript` : ``}`, () =>
testVariation({
root: '.',
theme,
useTs,
injectNpmScripts: false
}))
}
} }
})

@ -1,6 +1,11 @@
{ {
"name": "tests-init",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": {
"test": "vitest run",
"watch": "DEBUG=1 vitest"
},
"devDependencies": { "devDependencies": {
"vitepress": "workspace:*" "vitepress": "workspace:*"
} }

@ -1,20 +1,9 @@
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vitest/config' import { defineConfig } from 'vitest/config'
const dir = dirname(fileURLToPath(import.meta.url))
const timeout = 60_000 const timeout = 60_000
export default defineConfig({ export default defineConfig({
resolve: {
alias: {
node: resolve(dir, '../../src/node')
}
},
test: { test: {
watchExclude: ['**/node_modules/**', '**/temp/**'],
globalSetup: ['__tests__/init/vitestGlobalSetup.ts'],
testTimeout: timeout, testTimeout: timeout,
hookTimeout: timeout, hookTimeout: timeout,
teardownTimeout: timeout, teardownTimeout: timeout,

@ -1,17 +0,0 @@
import { chromium, type BrowserServer } from 'playwright-chromium'
let browserServer: BrowserServer
export async function setup() {
browserServer = await chromium.launchServer({
headless: !process.env.DEBUG,
args: process.env.CI
? ['--no-sandbox', '--disable-setuid-sandbox']
: undefined
})
process.env['WS_ENDPOINT'] = browserServer.wsEndpoint()
}
export async function teardown() {
await browserServer.close()
}

@ -1,17 +1,26 @@
import { dirname, resolve } from 'path' import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { defineConfig } from 'vitest/config' import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
const dir = dirname(fileURLToPath(import.meta.url)) const dir = dirname(fileURLToPath(import.meta.url))
export default defineConfig({ export default defineConfig({
plugins: [vue()],
resolve: { resolve: {
alias: { alias: [
'@siteData': resolve(dir, './shims.ts'), { find: '@siteData', replacement: resolve(dir, './shims.ts') },
client: resolve(dir, '../../src/client'), { find: 'client', replacement: resolve(dir, '../../src/client') },
node: resolve(dir, '../../src/node'), { find: 'node', replacement: resolve(dir, '../../src/node') },
vitepress: resolve(dir, '../../src/client') {
find: /^vitepress$/,
replacement: resolve(dir, '../../src/client/index.js')
},
{
find: /^vitepress\/theme$/,
replacement: resolve(dir, '../../src/client/theme-default/index.js')
} }
]
}, },
test: { test: {
globals: true globals: true

@ -18,7 +18,7 @@ All **static** path references, including absolute paths, should be based on you
## The Public Directory ## The Public Directory
Sometimes you may need to provide static assets that are not directly referenced in any of your Markdown or theme components, or you may want to serve certain files with the original filename. Examples of such files include `robot.txt`, favicons, and PWA icons. Sometimes you may need to provide static assets that are not directly referenced in any of your Markdown or theme components, or you may want to serve certain files with the original filename. Examples of such files include `robots.txt`, favicons, and PWA icons.
You can place these files in the `public` directory under the [source directory](./routing#source-directory). For example, if your project root is `./docs` and using default source directory location, then your public directory will be `./docs/public`. You can place these files in the `public` directory under the [source directory](./routing#source-directory). For example, if your project root is `./docs` and using default source directory location, then your public directory will be `./docs/public`.
@ -31,6 +31,20 @@ There is one exception to this: if you have an HTML page in `public` and link to
- [/pure.html](/pure.html) - [/pure.html](/pure.html)
- <pathname:///pure.html> - <pathname:///pure.html>
Note that `pathname://` is only supported in Markdown links. Also, `pathname://` will open the link in a new tab by default. You can use `target="_self"` instead to open it in the same tab:
**Input**
```md
[Link to pure.html](/pure.html){target="_self"}
<!-- there is no need to specify pathname:// if the target is explicitly specified -->
```
**Output**
[Link to pure.html](/pure.html){target="_self"}
## Base URL ## Base URL
If your site is deployed to a non-root URL, you will need to set the `base` option in `.vitepress/config.js`. For example, if you plan to deploy your site to `https://foo.github.io/bar/`, then `base` should be set to `'/bar/'` (it should always start and end with a slash). If your site is deployed to a non-root URL, you will need to set the `base` option in `.vitepress/config.js`. For example, if you plan to deploy your site to `https://foo.github.io/bar/`, then `base` should be set to `'/bar/'` (it should always start and end with a slash).

@ -119,65 +119,87 @@ Don't enable options like _Auto Minify_ for HTML code. It will remove comments f
### GitHub Pages ### GitHub Pages
1. In your theme config file, `docs/.vitepress/config.js`, set the `base` property to the name of your GitHub repository. If you plan to deploy your site to `https://foo.github.io/bar/`, then you should set base to `'/bar/'`. It should always start and end with a slash. 1. Create a file named `deploy.yml` inside `.github/workflows` directory of your project with some content like this:
2. Create a file named `deploy.yml` inside `.github/workflows` directory of your project with the following content:
```yaml ```yaml
name: Deploy # Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: Deploy VitePress site to Pages
on: on:
workflow_dispatch: {} # Runs on pushes targeting the `main` branch. Change this to `master` if you're
# using the `master` branch as the default branch.
push: push:
branches: branches: [main]
- main
jobs: # Allows you to run this workflow manually from the Actions tab
deploy: workflow_dispatch:
runs-on: ubuntu-latest
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions: permissions:
contents: read
pages: write pages: write
id-token: write id-token: write
environment:
name: github-pages # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
url: ${{ steps.deployment.outputs.page_url }} # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - name: Checkout
uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0 # Not needed if lastUpdated is not enabled
- uses: actions/setup-node@v3 # - uses: pnpm/action-setup@v2 # Uncomment this if you're using pnpm
- name: Setup Node
uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 18
cache: npm cache: npm # or pnpm / yarn
- run: npm ci - name: Setup Pages
- name: Build uses: actions/configure-pages@v3
run: npm run docs:build - name: Install dependencies
- uses: actions/configure-pages@v2 run: npm ci # or pnpm install / yarn install
- uses: actions/upload-pages-artifact@v1 - name: Build with VitePress
run: npm run docs:build # or pnpm docs:build / yarn docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with: with:
path: docs/.vitepress/dist path: docs/.vitepress/dist
- name: Deploy
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@v1 uses: actions/deploy-pages@v2
``` ```
::: tip ::: warning
Please replace the corresponding branch name. For example, if the branch you want to build is `master`, then you should replace `main` with `master` in the above file. Make sure the `base` option in your VitePress is properly configured. See [Setting a Public Base Path](#setting-a-public-base-path) for more details.
::: :::
3. In your repository's Settings under Pages menu item, select `GitHub Actions` in Build and deployment's Source. 2. In your repository's settings under "Pages" menu item, select "GitHub Actions" in "Build and deployment > Source".
4. Now commit your code and push it to the `main` branch. 3. Push your changes to the `main` branch and wait for the GitHub Actions workflow to complete. You should see your site deployed to `https://<username>.github.io/[repository]/` or `https://<custom-domain>/` depending on your settings. Your site will automatically be deployed on every push to the `main` branch.
5. Wait for actions to complete.
6. In your repository's Settings under Pages menu item, click `Visit site`, then you can see your site. Your docs will automatically deploy each time you push.
### GitLab Pages ### GitLab Pages
1. Set `outDir` in `docs/.vitepress/config.js` to `../public`. 1. Set `outDir` in VitePress config to `../public`. Configure `base` option to `'/<repository>/'` if you want to deploy to `https://<username>.gitlab.io/<repository>/`.
2. Still in your config file, `docs/.vitepress/config.js`, set the `base` property to the name of your GitLab repository. If you plan to deploy your site to `https://foo.gitlab.io/bar/`, then you should set base to `'/bar/'`. It should always start and end with a slash.
3. Create a file called `.gitlab-ci.yml` in the root of your project with the content below. This will build and deploy your site whenever you make changes to your content: 2. Create a file named `.gitlab-ci.yml` in the root of your project with the content below. This will build and deploy your site whenever you make changes to your content:
```yaml ```yaml
image: node:16 image: node:16
@ -186,25 +208,7 @@ Don't enable options like _Auto Minify_ for HTML code. It will remove comments f
paths: paths:
- node_modules/ - node_modules/
script: script:
- npm install # - apk add git # Uncomment this if you're using small docker images like alpine and have lastUpdated enabled
- npm run docs:build
artifacts:
paths:
- public
only:
- main
```
4. Alternatively, if you want to use an _alpine_ version of node, you have to install `git` manually. In that case, the code above modifies to this:
```yaml
image: node:16-alpine
pages:
cache:
paths:
- node_modules/
before_script:
- apk add git
script:
- npm install - npm install
- npm run docs:build - npm run docs:build
artifacts: artifacts:

@ -22,7 +22,7 @@ $ npm install -D vitepress
``` ```
```sh [pnpm] ```sh [pnpm]
$ pnpm add -D vitepress $ pnpm add -D vitepress@latest
``` ```
```sh [yarn] ```sh [yarn]
@ -38,7 +38,8 @@ If using PNPM, you will notice a missing peer warning for `@docsearch/js`. This
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {
"ignoreMissing": [ "ignoreMissing": [
"@algolia/client-search" "@algolia/client-search",
"search-insights"
] ]
} }
} }
@ -57,7 +58,7 @@ $ npx vitepress init
``` ```
```sh [pnpm] ```sh [pnpm]
$ pnpm exec vitepress init $ pnpm dlx vitepress init
``` ```
::: :::

@ -566,7 +566,12 @@ It also supports [line highlighting](#line-highlighting-in-code-blocks):
<<< @/snippets/snippet.js{2} <<< @/snippets/snippet.js{2}
::: tip ::: tip
The value of `@` corresponds to the source root. By default it's the VitePress project root, unless `srcDir` is configured. The value of `@` corresponds to the source root. By default it's the VitePress project root, unless `srcDir` is configured. Alternatively, you can also import from relative paths:
```md
<<< ../snippets/snippet.js
```
::: :::
You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/codebasics#_folding) to only include the corresponding part of the code file. You can provide a custom region name after a `#` following the filepath: You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/codebasics#_folding) to only include the corresponding part of the code file. You can provide a custom region name after a `#` following the filepath:
@ -691,10 +696,10 @@ You can also [import snippets](#import-code-snippets) in code groups:
## Markdown File Inclusion ## Markdown File Inclusion
You can include a markdown file in another markdown file. You can include a markdown file in another markdown file, even nested.
::: tip ::: 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. 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: For example, you can include a relative markdown file using this:
@ -733,6 +738,42 @@ Some getting started stuff.
Can be created using `.foorc.json`. Can be created using `.foorc.json`.
``` ```
It also supports selecting a line range:
**Input**
```md
# Docs
## Basics
<!--@include: ./parts/basics.md{3,}-->
```
**Part file** (`parts/basics.md`)
```md
Some getting started stuff.
### Configuration
Can be created using `.foorc.json`.
```
**Equivalent code**
```md
# Docs
## Basics
### Configuration
Can be created using `.foorc.json`.
```
The format of the selected line range can be: `{3,}`, `{,10}`, `{1,10}`
::: warning ::: warning
Note that this does not throw errors if your file is not present. Hence, when using this feature make sure that the contents are being rendered as expected. Note that this does not throw errors if your file is not present. Hence, when using this feature make sure that the contents are being rendered as expected.
::: :::

@ -327,7 +327,6 @@ Instead, you can pass such content to each page using the `content` property on
```js ```js
export default { export default {
paths() {
async paths() { async paths() {
const posts = await (await fetch('https://my-cms.com/blog-posts')).json() const posts = await (await fetch('https://my-cms.com/blog-posts')).json()
@ -339,7 +338,6 @@ export default {
}) })
} }
} }
}
``` ```
Then, use the following special syntax to render the content as part of the Markdown file itself: Then, use the following special syntax to render the content as part of the Markdown file itself:

@ -246,6 +246,7 @@ Vitepress currently has SSG support for teleports to body only. For other target
<script setup> <script setup>
import ModalDemo from '../components/ModalDemo.vue' import ModalDemo from '../components/ModalDemo.vue'
import ComponentInHeader from '../components/ComponentInHeader.vue'
</script> </script>
<style> <style>

@ -12,7 +12,7 @@ Just want to try it out? Skip to the [Quickstart](./getting-started).
- **Documentation** - **Documentation**
VitePress ships with a default theme designed for technical documentation, especially those that need to embed interactive demos. It powers this page you are reading right now, along with the documentation for [Vite](https://vitejs.dev/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Rollup](https://rollupjs.org/), [Mermaid](https://mermaid.js.org/), [Wikimedia Codex](https://doc.wikimedia.org/codex/latest/), and many more. VitePress ships with a default theme designed for technical documentation. It powers this page you are reading right now, along with the documentation for [Vite](https://vitejs.dev/), [Rollup](https://rollupjs.org/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Vitest](https://vitest.dev/), [D3](https://d3js.org/), [UnoCSS](https://unocss.dev/), [Iconify](https://iconify.design/) and [many more](https://www.vuetelescope.com/explore?framework.slug=vitepress).
The [official Vue.js documentation](https://vuejs.org/) is also based on VitePress, but uses a custom theme shared between multiple translations. The [official Vue.js documentation](https://vuejs.org/) is also based on VitePress, but uses a custom theme shared between multiple translations.

@ -11,7 +11,7 @@ hero:
actions: actions:
- theme: brand - theme: brand
text: Get Started text: Get Started
link: /guide/what-is-vitepress link: /guide/getting-started
- theme: alt - theme: alt
text: View on GitHub text: View on GitHub
link: https://github.com/vuejs/vitepress link: https://github.com/vuejs/vitepress
@ -20,10 +20,10 @@ features:
- icon: 📝 - icon: 📝
title: Focus on Your Content title: Focus on Your Content
details: Effortlessly create beautiful documentation sites with just markdown. details: Effortlessly create beautiful documentation sites with just markdown.
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><g fill="none"><path fill="url(#a)" d="m29.884 6.146-13.142 23.5a.714.714 0 0 1-1.244.005L2.096 6.148a.714.714 0 0 1 .746-1.057l13.156 2.352a.714.714 0 0 0 .253 0l12.881-2.348a.714.714 0 0 1 .752 1.05z"/><path fill="url(#b)" d="M22.264 2.007 12.54 3.912a.357.357 0 0 0-.288.33l-.598 10.104a.357.357 0 0 0 .437.369l2.707-.625a.357.357 0 0 1 .43.42l-.804 3.939a.357.357 0 0 0 .454.413l1.672-.508a.357.357 0 0 1 .454.414l-1.279 6.187c-.08.387.435.598.65.267l.143-.222 7.925-15.815a.357.357 0 0 0-.387-.51l-2.787.537a.357.357 0 0 1-.41-.45l1.818-6.306a.357.357 0 0 0-.412-.45z"/><defs><linearGradient id="a" x1="6" x2="235" y1="33" y2="344" gradientTransform="translate(1.34 1.894) scale(.07142)" gradientUnits="userSpaceOnUse"><stop stop-color="#41D1FF"/><stop offset="1" stop-color="#BD34FE"/></linearGradient><linearGradient id="b" x1="194.651" x2="236.076" y1="8.818" y2="292.989" gradientTransform="translate(1.34 1.894) scale(.07142)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFEA83"/><stop offset=".083" stop-color="#FFDD35"/><stop offset="1" stop-color="#FFA800"/></linearGradient></defs></g></svg> - icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 256.32"><defs><linearGradient id="a" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"/><stop offset="100%" stop-color="#BD34FE"/></linearGradient><linearGradient id="b" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"/><stop offset="8.333%" stop-color="#FFDD35"/><stop offset="100%" stop-color="#FFA800"/></linearGradient></defs><path fill="url(#a)" d="M255.153 37.938 134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"/><path fill="url(#b)" d="M185.432.063 96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028 72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"/></svg>
title: Enjoy the Vite DX title: Enjoy the Vite DX
details: Instant server start, lightning fast hot updates, and leverage Vite ecosystem plugins. details: Instant server start, lightning fast hot updates, and leverage Vite ecosystem plugins.
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path fill="#41b883" d="M24.4 3.925H30l-14 24.15L2 3.925h10.71l3.29 5.6 3.22-5.6Z"/><path fill="#41b883" d="m2 3.925 14 24.15 14-24.15h-5.6L16 18.415 7.53 3.925Z"/><path fill="#35495e" d="M7.53 3.925 16 18.485l8.4-14.56h-5.18L16 9.525l-3.29-5.6Z"/></svg> - icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 220.8"><path fill="#41B883" d="M204.8 0H256L128 220.8 0 0h97.92L128 51.2 157.44 0h47.36Z"/><path fill="#41B883" d="m0 0 128 220.8L256 0h-51.2L128 132.48 50.56 0H0Z"/><path fill="#35495E" d="M50.56 0 128 133.12 204.8 0h-47.36L128 51.2 97.92 0H50.56Z"/></svg>
title: Customize with Vue title: Customize with Vue
details: Use Vue syntax and components directly in markdown, or build custom themes with Vue. details: Use Vue syntax and components directly in markdown, or build custom themes with Vue.
- icon: 🚀 - icon: 🚀

@ -1,6 +1,12 @@
{ {
"name": "docs",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": {
"dev": "vitepress",
"build": "vitepress build",
"preview": "vitepress preview"
},
"devDependencies": { "devDependencies": {
"vitepress": "workspace:*" "vitepress": "workspace:*"
} }

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<title>Plain HTML page | VitePress</title> <title>Plain HTML page | VitePress</title>

@ -216,7 +216,9 @@ export default {
icon: { icon: {
svg: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Dribbble</title><path d="M12...6.38z"/></svg>' svg: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Dribbble</title><path d="M12...6.38z"/></svg>'
}, },
link: '...' link: '...',
// You can include a custom label for accessibility too (optional but recommended):
ariaLabel: 'cool link'
} }
] ]
} }
@ -227,6 +229,7 @@ export default {
interface SocialLink { interface SocialLink {
icon: SocialLinkIcon icon: SocialLinkIcon
link: string link: string
ariaLabel?: string
} }
type SocialLinkIcon = type SocialLinkIcon =
@ -245,6 +248,7 @@ type SocialLinkIcon =
## footer ## footer
- Type: `Footer` - Type: `Footer`
- Can be overridden per page via [frontmatter](./frontmatter-config#footer)
Footer configuration. You can add a message or copyright text on the footer, however, it will only be displayed when the page doesn't contain a sidebar. This is due to design concerns. Footer configuration. You can add a message or copyright text on the footer, however, it will only be displayed when the page doesn't contain a sidebar. This is due to design concerns.
@ -291,21 +295,41 @@ export interface EditLink {
} }
``` ```
## lastUpdatedText ## lastUpdated
- Type: `string` - Type: `LastUpdatedOptions`
- Default: `Last updated`
The prefix text showing right before the last updated time. Allows customization for the last updated text and date format.
```ts ```ts
export default { export default {
themeConfig: { themeConfig: {
lastUpdatedText: 'Updated Date' lastUpdated: {
text: 'Updated at',
formatOptions: {
dateStyle: 'full',
timeStyle: 'medium'
}
}
} }
} }
``` ```
```ts
export interface LastUpdatedOptions {
/**
* @default 'Last updated'
*/
text?: string
/**
* @default
* { dateStyle: 'short', timeStyle: 'short' }
*/
formatOptions?: Intl.DateTimeFormatOptions
}
```
## algolia ## algolia
- Type: `AlgoliaSearch` - Type: `AlgoliaSearch`
@ -350,7 +374,7 @@ Learn more in [Default Theme: Carbon Ads](./default-theme-carbon-ads)
- Type: `DocFooter` - Type: `DocFooter`
Can be used to customize text appearing above previous and next links. Helpful if not writing docs in English. Can be used to customize text appearing above previous and next links. Helpful if not writing docs in English. Also can be used to disable prev/next links globally. If you want to selectively enable/disable prev/next links, you can use [frontmatter](./default-theme-prev-next-links).
```js ```js
export default { export default {
@ -365,8 +389,8 @@ export default {
```ts ```ts
export interface DocFooter { export interface DocFooter {
prev?: string prev?: string | false
next?: string next?: string | false
} }
``` ```
@ -397,3 +421,10 @@ Can be used to customize the label of the return to top button. This label is on
- Default: `Change language` - Default: `Change language`
Can be used to customize the aria-label of the language toggle button in navbar. This is only used if you're using [i18n](../guide/i18n). Can be used to customize the aria-label of the language toggle button in navbar. This is only used if you're using [i18n](../guide/i18n).
## externalLinkIcon
- Type: `boolean`
- Default: `false`
Whether to show an external link icon next to external links in markdown.

@ -41,3 +41,13 @@ Only inline elements can be used in `message` and `copyright` as they are render
::: :::
Note that footer will not be displayed when the [SideBar](./default-theme-sidebar) is visible. Note that footer will not be displayed when the [SideBar](./default-theme-sidebar) is visible.
## Frontmatter Config
This can be disabled per-page using the `footer` option on frontmatter:
```yaml
---
footer: false
---
```

@ -2,6 +2,10 @@
The update time of the last content will be displayed in the lower right corner of the page. To enable it, add `lastUpdated` options to your config. The update time of the last content will be displayed in the lower right corner of the page. To enable it, add `lastUpdated` options to your config.
::: tip
You need to commit the markdown file to see the updated time.
:::
## Site-Level Config ## Site-Level Config
```js ```js
@ -20,3 +24,4 @@ lastUpdated: false
--- ---
``` ```
Also refer [Default Theme: Last Updated](./default-theme-last-updated#last-updated) for more details. Any truthy value at theme-level will also enable the feature unless explicitly disabled at site or page level.

@ -1,3 +1,7 @@
---
outline: deep
---
# Search # Search
## Local Search ## Local Search
@ -58,6 +62,60 @@ export default defineConfig({
}) })
``` ```
### miniSearch options
You can configure MiniSearch like this:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'local',
options: {
miniSearch: {
/**
* @type {Pick<import('minisearch').Options, 'extractField' | 'tokenize' | 'processTerm'>}
*/
options: {
/* ... */
},
/**
* @type {import('minisearch').SearchOptions}
* @default
* { fuzzy: 0.2, prefix: true, boost: { title: 4, text: 2, titles: 1 } }
*/
searchOptions: {
/* ... */
}
}
}
}
}
})
```
Learn more in [MiniSearch docs](https://lucaong.github.io/minisearch/classes/_minisearch_.minisearch.html).
### Excluding pages from search
You can exclude pages from search by adding `search: false` to the frontmatter of the page. Alternatively, you can also pass `exclude` function to `themeConfig.search.options` to exclude pages based on their path relative to `srcDir`:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
options: {
exclude: (path) => path.startsWith('/some/path')
}
}
}
})
```
## Algolia Search ## 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: 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:
@ -145,6 +203,114 @@ 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. [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.
### Crawler Config
Here is an example config based on what this site uses:
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: '',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})
```
<style> <style>
img[src="/search.png"] { img[src="/search.png"] {
width: 100%; width: 100%;

@ -180,3 +180,36 @@ export default {
} }
} }
``` ```
## `useSidebar` <Badge type="info" text="composable" />
Returns sidebar-related data. The returned object has the following type:
```ts
export interface DocSidebar {
isOpen: Ref<boolean>
sidebar: ComputedRef<DefaultTheme.SidebarItem[]>
sidebarGroups: ComputedRef<DefaultTheme.SidebarItem[]>
hasSidebar: ComputedRef<boolean>
hasAside: ComputedRef<boolean>
leftAside: ComputedRef<boolean>
isSidebarEnabled: ComputedRef<boolean>
open: () => void
close: () => void
toggle: () => void
}
```
**Example:**
```vue
<script setup>
import { useSidebar } from 'vitepress/theme'
const { hasSidebar } = useSidebar()
</script>
<template>
<div v-if="hasSidebar">Only show when sidebar exists</div>
</template>
```

@ -111,6 +111,32 @@ Defines contents of home hero section when `layout` is set to `home`. More detai
Defines items to display in features section when `layout` is set to `home`. More details in [Default Theme: Home Page](./default-theme-home-page). Defines items to display in features section when `layout` is set to `home`. More details in [Default Theme: Home Page](./default-theme-home-page).
### navbar <Badge type="info" text="default theme only" />
- Type: `boolean`
- Default: `true`
Whether to display [navbar](./default-theme-nav).
```yaml
---
navbar: false
---
```
### sidebar <Badge type="info" text="default theme only" />
- Type: `boolean`
- Default: `true`
Whether to display [sidebar](./default-theme-sidebar).
```yaml
---
sidebar: false
---
```
### aside <Badge type="info" text="default theme only" /> ### aside <Badge type="info" text="default theme only" />
- Type: `boolean | 'left'` - Type: `boolean | 'left'`
@ -140,7 +166,7 @@ The levels of header in the outline to display for the page. It's same as [confi
- Type: `boolean` - Type: `boolean`
- Default: `true` - Default: `true`
Whether to display [Last Updated](./default-theme-last-updated) text in the footer of the current page. Whether to display [last updated](./default-theme-last-updated) text in the footer of the current page.
```yaml ```yaml
--- ---
@ -153,10 +179,23 @@ lastUpdated: false
- Type: `boolean` - Type: `boolean`
- Default: `true` - Default: `true`
Whether to display [Edit Link](./default-theme-edit-link) in the footer of the current page. Whether to display [edit link](./default-theme-edit-link) in the footer of the current page.
```yaml ```yaml
--- ---
editLink: false editLink: false
--- ---
``` ```
### footer <Badge type="info" text="default theme only" />
- Type: `boolean`
- Default: `true`
Whether to display [footer](./default-theme-footer).
```yaml
---
footer: false
---
```

@ -86,8 +86,27 @@ Returns the VitePress router instance so you can programmatically navigate to an
```ts ```ts
interface Router { interface Router {
/**
* Current route.
*/
route: Route route: Route
go: (href?: string) => Promise<void> /**
* Navigate to a new URL.
*/
go: (to?: string) => Promise<void>
/**
* Called before the route changes. Return `false` to cancel the navigation.
*/
onBeforeRouteChange?: (to: string) => Awaitable<void | boolean>
/**
* Called before the page component is loaded (after the history state is
* updated). Return `false` to cancel the navigation.
*/
onBeforePageLoad?: (to: string) => Awaitable<void | boolean>
/**
* Called after the route changes.
*/
onAfterRouteChanged?: (to: string) => Awaitable<void>
} }
``` ```

@ -290,6 +290,19 @@ export default {
} }
``` ```
### assetsDir
- Type: `string`
- Default: `assets`
The directory for assets files. See also: [assetsDir](https://vitejs.dev/config/build-options.html#build-assetsdir).
```ts
export default {
assetsDir: 'static'
}
```
### cacheDir ### cacheDir
- Type: `string` - Type: `string`
@ -384,7 +397,7 @@ export default {
// adjust how header anchors are generated, // adjust how header anchors are generated,
// useful for integrating with tools that use different conventions // useful for integrating with tools that use different conventions
anchors: { anchor: {
slugify(str) { slugify(str) {
return encodeURIComponent(str) return encodeURIComponent(str)
} }

@ -3,4 +3,4 @@
[build] [build]
publish = "docs/.vitepress/dist" publish = "docs/.vitepress/dist"
command = "pnpm docs-build" command = "pnpm docs:build"

@ -1,9 +1,9 @@
{ {
"name": "vitepress", "name": "vitepress",
"version": "1.0.0-alpha.75", "version": "1.0.0-beta.6",
"description": "Vite & Vue powered static site generator", "description": "Vite & Vue powered static site generator",
"type": "module", "type": "module",
"packageManager": "pnpm@8.3.1", "packageManager": "pnpm@8.6.9",
"main": "dist/node/index.js", "main": "dist/node/index.js",
"types": "types/index.d.ts", "types": "types/index.d.ts",
"exports": { "exports": {
@ -23,7 +23,7 @@
"default": "./dist/client/theme-default/index.js" "default": "./dist/client/theme-default/index.js"
}, },
"./theme-without-fonts": { "./theme-without-fonts": {
"types": "./theme.d.ts", "types": "./theme-without-fonts.d.ts",
"default": "./dist/client/theme-default/without-fonts.js" "default": "./dist/client/theme-default/without-fonts.js"
} }
}, },
@ -36,7 +36,8 @@
"types", "types",
"template", "template",
"client.d.ts", "client.d.ts",
"theme.d.ts" "theme.d.ts",
"theme-without-fonts.d.ts"
], ],
"repository": { "repository": {
"type": "git", "type": "git",
@ -54,52 +55,55 @@
"url": "https://github.com/vuejs/vitepress/issues" "url": "https://github.com/vuejs/vitepress/issues"
}, },
"scripts": { "scripts": {
"dev": "rimraf dist && run-s dev-shared dev-start", "dev": "rimraf dist && run-s dev:shared dev:start",
"dev-start": "run-p dev-client dev-node dev-watch", "dev:start": "run-p dev:client dev:node dev:watch",
"dev-client": "tsc --sourcemap -w -p src/client", "dev:client": "tsc --sourcemap -w -p src/client",
"dev-node": "DEV=true pnpm build-node -w", "dev:node": "DEV=true pnpm build:node -w",
"dev-shared": "node scripts/copyShared", "dev:shared": "node scripts/copyShared",
"dev-watch": "node scripts/watchAndCopy", "dev:watch": "node scripts/watchAndCopy",
"build": "run-s build-prepare build-client build-node", "build": "run-s build:prepare build:client build:node",
"build-prepare": "rimraf dist && node scripts/copyShared", "build:prepare": "rimraf dist && node scripts/copyShared",
"build-client": "vue-tsc --noEmit -p src/client && tsc -p src/client && node scripts/copyClient", "build:client": "vue-tsc --noEmit -p src/client && tsc -p src/client && node scripts/copyClient",
"build-node": "tsc -p src/node --noEmit && rollup --config rollup.config.ts --configPlugin esbuild", "build:node": "tsc -p src/node --noEmit && rollup --config rollup.config.ts --configPlugin esbuild",
"test": "run-p --aggregate-output test:unit test:e2e test:init",
"test:unit": "vitest run -r __tests__/unit",
"test:unit:watch": "vitest -r __tests__/unit",
"test:e2e": "run-s test:e2e-dev test:e2e-build",
"test:e2e:site:dev": "pnpm -F=tests-e2e site:dev",
"test:e2e:site:build": "pnpm -F=tests-e2e site:build",
"test:e2e:site:preview": "pnpm -F=tests-e2e site:preview",
"test:e2e-dev": "pnpm -F=tests-e2e test",
"test:e2e-dev:watch": "pnpm -F=tests-e2e watch",
"test:e2e-build": "VITE_TEST_BUILD=1 pnpm test:e2e-dev",
"test:e2e-build:watch": "VITE_TEST_BUILD=1 pnpm test:e2e-dev:watch",
"test:init": "pnpm -F=tests-init test",
"test:init:watch": "pnpm -F=tests-init watch",
"docs": "run-p dev docs:dev",
"docs:dev": "wait-on -d 100 dist/node/cli.js && pnpm -F=docs dev",
"docs:debug": "NODE_OPTIONS='--inspect-brk' pnpm docs:dev",
"docs:build": "run-s build docs:build:only",
"docs:build:only": "pnpm -F=docs build",
"docs:preview": "pnpm -F=docs preview",
"format": "prettier --check --write .", "format": "prettier --check --write .",
"format-fail": "prettier --check .", "format:fail": "prettier --check .",
"check": "run-s format-fail build test", "check": "run-s format:fail build test",
"test": "run-s test-unit test-e2e test-e2e-build",
"test-unit": "vitest run -r __tests__/unit",
"test-e2e": "vitest run -r __tests__/e2e",
"test-e2e-build": "VITE_TEST_BUILD=1 pnpm test-e2e",
"test-init": "vitest run -r __tests__/init",
"test-init-build": "VITE_TEST_BUILD=1 pnpm test-init",
"debug-e2e": "DEBUG=1 vitest -r __tests__/e2e",
"debug-e2e-build": "VITE_TEST_BUILD=1 pnpm debug-e2e",
"unit-dev": "vitest -r __tests__/unit",
"e2e-dev": "wait-on -d 100 dist/node/cli.js && node ./bin/vitepress dev __tests__/e2e",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"release": "node scripts/release.js", "release": "node scripts/release.js"
"docs": "run-p dev docs-dev",
"docs-dev": "wait-on -d 100 dist/node/cli.js && node ./bin/vitepress dev docs",
"docs-debug": "node --inspect-brk ./bin/vitepress dev docs",
"docs-build": "run-s build docs-build-only",
"docs-build-only": "node ./bin/vitepress build docs",
"docs-preview": "node ./bin/vitepress preview docs"
}, },
"dependencies": { "dependencies": {
"@docsearch/css": "^3.3.4", "@docsearch/css": "^3.5.1",
"@docsearch/js": "^3.3.4", "@docsearch/js": "^3.5.1",
"@vitejs/plugin-vue": "^4.2.1", "@vitejs/plugin-vue": "^4.2.3",
"@vue/devtools-api": "^6.5.0", "@vue/devtools-api": "^6.5.0",
"@vueuse/core": "^10.1.0", "@vueuse/core": "^10.2.1",
"@vueuse/integrations": "^10.1.0", "@vueuse/integrations": "^10.2.1",
"body-scroll-lock": "4.0.0-beta.0", "body-scroll-lock": "4.0.0-beta.0",
"focus-trap": "^7.4.0", "focus-trap": "^7.5.2",
"mark.js": "8.11.1", "mark.js": "8.11.1",
"minisearch": "^6.0.1", "minisearch": "^6.1.0",
"shiki": "^0.14.2", "shiki": "^0.14.3",
"vite": "^4.3.3", "vite": "^4.4.6",
"vue": "^3.3.2" "vue": "^3.3.4"
}, },
"devDependencies": { "devDependencies": {
"@clack/prompts": "^0.6.3", "@clack/prompts": "^0.6.3",
@ -111,14 +115,14 @@
"@mdit-vue/plugin-toc": "^0.12.0", "@mdit-vue/plugin-toc": "^0.12.0",
"@mdit-vue/shared": "^0.12.0", "@mdit-vue/shared": "^0.12.0",
"@rollup/plugin-alias": "^5.0.0", "@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-commonjs": "^24.1.0", "@rollup/plugin-commonjs": "^25.0.3",
"@rollup/plugin-json": "^6.0.0", "@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.2", "@rollup/plugin-node-resolve": "^15.1.0",
"@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-replace": "^5.0.2",
"@types/body-scroll-lock": "^3.1.0", "@types/body-scroll-lock": "^3.1.0",
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
"@types/cross-spawn": "^6.0.2", "@types/cross-spawn": "^6.0.2",
"@types/debug": "^4.1.7", "@types/debug": "^4.1.8",
"@types/escape-html": "^1.0.2", "@types/escape-html": "^1.0.2",
"@types/fs-extra": "^11.0.1", "@types/fs-extra": "^11.0.1",
"@types/mark.js": "^8.11.8", "@types/mark.js": "^8.11.8",
@ -128,24 +132,24 @@
"@types/markdown-it-emoji": "^2.0.2", "@types/markdown-it-emoji": "^2.0.2",
"@types/micromatch": "^4.0.2", "@types/micromatch": "^4.0.2",
"@types/minimist": "^1.2.2", "@types/minimist": "^1.2.2",
"@types/node": "^18.16.3", "@types/node": "^20.4.3",
"@types/prompts": "^2.4.4", "@types/prompts": "^2.4.4",
"@vue/shared": "^3.3.4",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"compression": "^1.7.4", "compression": "^1.7.4",
"conventional-changelog-cli": "^2.2.2", "conventional-changelog-cli": "^2",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"debug": "^4.3.4", "debug": "^4.3.4",
"enquirer": "^2.3.6", "esbuild": "^0.18.15",
"esbuild": "^0.17.18",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"execa": "^7.1.1", "execa": "^7.1.1",
"fast-glob": "^3.2.12", "fast-glob": "^3.3.1",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"get-port": "^6.1.2", "get-port": "^7.0.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"lint-staged": "^13.2.2", "lint-staged": "^13.2.3",
"lodash.template": "^4.5.0", "lodash.template": "^4.5.0",
"lru-cache": "^9.1.1", "lru-cache": "^10.0.0",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"markdown-it-anchor": "^8.6.7", "markdown-it-anchor": "^8.6.7",
"markdown-it-attrs": "^4.1.6", "markdown-it-attrs": "^4.1.6",
@ -155,36 +159,29 @@
"minimist": "^1.2.8", "minimist": "^1.2.8",
"nanoid": "^4.0.2", "nanoid": "^4.0.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"ora": "^6.3.0", "ora": "^6.3.1",
"path-to-regexp": "^6.2.1", "path-to-regexp": "^6.2.1",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"pkg-dir": "^7.0.0", "pkg-dir": "^7.0.0",
"playwright-chromium": "^1.33.0", "playwright-chromium": "^1.36.1",
"polka": "1.0.0-next.22", "polka": "1.0.0-next.22",
"prettier": "^2.8.8", "prettier": "^3.0.0",
"prompts": "^2.4.2", "prompts": "^2.4.2",
"punycode": "^2.3.0", "punycode": "^2.3.0",
"rimraf": "^5.0.0", "rimraf": "^5.0.1",
"rollup": "^3.21.1", "rollup": "^3.26.3",
"rollup-plugin-dts": "^5.3.0", "rollup-plugin-dts": "^5.3.0",
"rollup-plugin-esbuild": "^5.0.0", "rollup-plugin-esbuild": "^5.0.0",
"semver": "^7.5.0", "semver": "^7.5.4",
"shiki-processor": "^0.1.3", "shiki-processor": "^0.1.3",
"simple-git-hooks": "^2.8.1", "simple-git-hooks": "^2.8.1",
"sirv": "^2.0.3", "sirv": "^2.0.3",
"supports-color": "^9.3.1", "supports-color": "^9.4.0",
"typescript": "^5.0.4", "typescript": "^5.1.6",
"vitest": "^0.30.1", "vitest": "^0.33.0",
"vue-tsc": "^1.6.1", "vue-tsc": "^1.8.6",
"wait-on": "^7.0.1" "wait-on": "^7.0.1"
}, },
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"@algolia/client-search"
]
}
},
"simple-git-hooks": { "simple-git-hooks": {
"pre-commit": "pnpm lint-staged" "pre-commit": "pnpm lint-staged"
}, },

File diff suppressed because it is too large Load Diff

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

@ -2,13 +2,12 @@ import { inBrowser } from 'vitepress'
export function useCopyCode() { export function useCopyCode() {
if (inBrowser) { if (inBrowser) {
const timeoutIdMap: Map<HTMLElement, NodeJS.Timeout> = new Map() const timeoutIdMap: WeakMap<HTMLElement, NodeJS.Timeout> = new WeakMap()
window.addEventListener('click', (e) => { window.addEventListener('click', (e) => {
const el = e.target as HTMLElement const el = e.target as HTMLElement
if (el.matches('div[class*="language-"] > button.copy')) { if (el.matches('div[class*="language-"] > button.copy')) {
const parent = el.parentElement const parent = el.parentElement
const sibling = el.nextElementSibling const sibling = el.nextElementSibling?.nextElementSibling
?.nextElementSibling as HTMLPreElement | null
if (!parent || !sibling) { if (!parent || !sibling) {
return return
} }

@ -37,9 +37,15 @@ export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) {
// update title and description // update title and description
document.title = createTitle(siteData, pageData) document.title = createTitle(siteData, pageData)
document const description = pageDescription || siteData.description
.querySelector(`meta[name=description]`)! let metaDescriptionElement = document.querySelector(
.setAttribute('content', pageDescription || siteData.description) `meta[name=description]`
)
if (metaDescriptionElement) {
metaDescriptionElement.setAttribute('content', description)
} else {
createHeadElement(['meta', { name: 'description', content: description }])
}
updateHeadTags( updateHeadTags(
mergeHead(siteData.head, filterOutHeadDescription(frontmatterHead)) mergeHead(siteData.head, filterOutHeadDescription(frontmatterHead))

@ -66,7 +66,7 @@ export function usePrefetch() {
if (!hasFetched.has(pathname)) { if (!hasFetched.has(pathname)) {
hasFetched.add(pathname) hasFetched.add(pathname)
const pageChunkPath = pathToFile(pathname) const pageChunkPath = pathToFile(pathname)
doFetch(pageChunkPath) if (pageChunkPath) doFetch(pageChunkPath)
} }
} }
}) })
@ -76,7 +76,6 @@ export function usePrefetch() {
document document
.querySelectorAll<HTMLAnchorElement | SVGAElement>('#app a') .querySelectorAll<HTMLAnchorElement | SVGAElement>('#app a')
.forEach((link) => { .forEach((link) => {
const { target } = link
const { hostname, pathname } = new URL( const { hostname, pathname } = new URL(
link.href instanceof SVGAnimatedString link.href instanceof SVGAnimatedString
? link.href.animVal ? link.href.animVal
@ -91,7 +90,7 @@ export function usePrefetch() {
if ( if (
// only prefetch same tab navigation, since a new tab will load // only prefetch same tab navigation, since a new tab will load
// the lean js chunk instead. // the lean js chunk instead.
target !== `_blank` && link.target !== '_blank' &&
// only prefetch inbound links // only prefetch inbound links
hostname === location.hostname hostname === location.hostname
) { ) {

@ -1,23 +1,22 @@
import RawTheme from '@theme/index'
import { import {
type App,
createApp as createClientApp, createApp as createClientApp,
createSSRApp, createSSRApp,
defineComponent, defineComponent,
h, h,
onMounted, onMounted,
watchEffect watchEffect,
type App
} from 'vue' } from 'vue'
import RawTheme from '@theme/index'
import { inBrowser, pathToFile } from './utils'
import { type Router, RouterSymbol, createRouter, scrollTo } from './router'
import { siteDataRef, useData } from './data'
import { useUpdateHead } from './composables/head'
import { usePrefetch } from './composables/preFetch'
import { dataSymbol, initData } from './data'
import { Content } from './components/Content'
import { ClientOnly } from './components/ClientOnly' import { ClientOnly } from './components/ClientOnly'
import { useCopyCode } from './composables/copyCode' import { Content } from './components/Content'
import { useCodeGroups } from './composables/codeGroups' import { useCodeGroups } from './composables/codeGroups'
import { useCopyCode } from './composables/copyCode'
import { useUpdateHead } from './composables/head'
import { usePrefetch } from './composables/preFetch'
import { dataSymbol, initData, siteDataRef, useData } from './data'
import { RouterSymbol, createRouter, scrollTo, type Router } from './router'
import { inBrowser, pathToFile } from './utils'
function resolveThemeExtends(theme: typeof RawTheme): typeof RawTheme { function resolveThemeExtends(theme: typeof RawTheme): typeof RawTheme {
if (theme.extends) { if (theme.extends) {
@ -123,6 +122,8 @@ function newRouter(): Router {
return createRouter((path) => { return createRouter((path) => {
let pageFilePath = pathToFile(path) let pageFilePath = pathToFile(path)
if (!pageFilePath) return null
if (isInitialPageLoad) { if (isInitialPageLoad) {
initialPath = pageFilePath initialPath = pageFilePath
} }
@ -152,7 +153,9 @@ if (inBrowser) {
// scroll to hash on new tab during dev // scroll to hash on new tab during dev
if (import.meta.env.DEV && location.hash) { if (import.meta.env.DEV && location.hash) {
const target = document.querySelector(decodeURIComponent(location.hash)) const target = document.getElementById(
decodeURIComponent(location.hash).slice(1)
)
if (target) { if (target) {
scrollTo(target, location.hash) scrollTo(target, location.hash)
} }

@ -12,9 +12,26 @@ export interface Route {
} }
export interface Router { export interface Router {
/**
* Current route.
*/
route: Route route: Route
go: (href?: string) => Promise<void> /**
onBeforeRouteChange?: (to: string) => Awaitable<void> * Navigate to a new URL.
*/
go: (to?: string) => Promise<void>
/**
* Called before the route changes. Return `false` to cancel the navigation.
*/
onBeforeRouteChange?: (to: string) => Awaitable<void | boolean>
/**
* Called before the page component is loaded (after the history state is
* updated). Return `false` to cancel the navigation.
*/
onBeforePageLoad?: (to: string) => Awaitable<void | boolean>
/**
* Called after the route changes.
*/
onAfterRouteChanged?: (to: string) => Awaitable<void> onAfterRouteChanged?: (to: string) => Awaitable<void>
} }
@ -22,7 +39,7 @@ export const RouterSymbol: InjectionKey<Router> = Symbol()
// we are just using URL to parse the pathname and hash - the base doesn't // we are just using URL to parse the pathname and hash - the base doesn't
// matter and is only passed to support same-host hrefs. // matter and is only passed to support same-host hrefs.
const fakeHost = `http://a.com` const fakeHost = 'http://a.com'
const getDefaultRoute = (): Route => ({ const getDefaultRoute = (): Route => ({
path: '/', path: '/',
@ -36,7 +53,7 @@ interface PageModule {
} }
export function createRouter( export function createRouter(
loadPageModule: (path: string) => Promise<PageModule>, loadPageModule: (path: string) => Awaitable<PageModule | null>,
fallbackComponent?: Component fallbackComponent?: Component
): Router { ): Router {
const route = reactive(getDefaultRoute()) const route = reactive(getDefaultRoute())
@ -47,7 +64,7 @@ export function createRouter(
} }
async function go(href: string = inBrowser ? location.href : '/') { async function go(href: string = inBrowser ? location.href : '/') {
await router.onBeforeRouteChange?.(href) if ((await router.onBeforeRouteChange?.(href)) === false) return
const url = new URL(href, fakeHost) const url = new URL(href, fakeHost)
if (!siteDataRef.value.cleanUrls) { if (!siteDataRef.value.cleanUrls) {
// ensure correct deep link so page refresh lands on correct files. // ensure correct deep link so page refresh lands on correct files.
@ -69,10 +86,14 @@ export function createRouter(
let latestPendingPath: string | null = null let latestPendingPath: string | null = null
async function loadPage(href: string, scrollPosition = 0, isRetry = false) { async function loadPage(href: string, scrollPosition = 0, isRetry = false) {
if ((await router.onBeforePageLoad?.(href)) === false) return
const targetLoc = new URL(href, fakeHost) const targetLoc = new URL(href, fakeHost)
const pendingPath = (latestPendingPath = targetLoc.pathname) const pendingPath = (latestPendingPath = targetLoc.pathname)
try { try {
let page = await loadPageModule(pendingPath) let page = await loadPageModule(pendingPath)
if (!page) {
throw new Error(`Page not found: ${pendingPath}`)
}
if (latestPendingPath === pendingPath) { if (latestPendingPath === pendingPath) {
latestPendingPath = null latestPendingPath = null
@ -104,8 +125,8 @@ export function createRouter(
if (targetLoc.hash && !scrollPosition) { if (targetLoc.hash && !scrollPosition) {
let target: HTMLElement | null = null let target: HTMLElement | null = null
try { try {
target = document.querySelector( target = document.getElementById(
decodeURIComponent(targetLoc.hash) decodeURIComponent(targetLoc.hash).slice(1)
) )
} catch (e) { } catch (e) {
console.warn(e) console.warn(e)
@ -120,7 +141,10 @@ export function createRouter(
} }
} }
} catch (err: any) { } catch (err: any) {
if (!/fetch/.test(err.message) && !/^\/404(\.html|\/)?$/.test(href)) { if (
!/fetch|Page not found/.test(err.message) &&
!/^\/404(\.html|\/)?$/.test(href)
) {
console.error(err) console.error(err)
} }
@ -176,7 +200,7 @@ export function createRouter(
!e.shiftKey && !e.shiftKey &&
!e.altKey && !e.altKey &&
!e.metaKey && !e.metaKey &&
target !== `_blank` && !target &&
origin === currentUrl.origin && origin === currentUrl.origin &&
// don't intercept if non-html extension is present // don't intercept if non-html extension is present
!(extMatch && extMatch[0] !== '.html') !(extMatch && extMatch[0] !== '.html')
@ -238,7 +262,7 @@ export function scrollTo(el: Element, hash: string, smooth = false) {
try { try {
target = el.classList.contains('header-anchor') target = el.classList.contains('header-anchor')
? el ? el
: document.querySelector(decodeURIComponent(hash)) : document.getElementById(decodeURIComponent(hash).slice(1))
} catch (e) { } catch (e) {
console.warn(e) console.warn(e)
} }
@ -269,7 +293,11 @@ export function scrollTo(el: Element, hash: string, smooth = false) {
offset + offset +
targetPadding targetPadding
// only smooth scroll if distance is smaller than screen height. // only smooth scroll if distance is smaller than screen height.
if (!smooth || Math.abs(targetTop - window.scrollY) > window.innerHeight) { function scrollToTarget() {
if (
!smooth ||
Math.abs(targetTop - window.scrollY) > window.innerHeight
) {
window.scrollTo(0, targetTop) window.scrollTo(0, targetTop)
} else { } else {
window.scrollTo({ window.scrollTo({
@ -279,6 +307,8 @@ export function scrollTo(el: Element, hash: string, smooth = false) {
}) })
} }
} }
requestAnimationFrame(scrollToTarget)
}
} }
function tryOffsetSelector(selector: string): number { function tryOffsetSelector(selector: string): number {
@ -303,6 +333,8 @@ function handleHMR(route: Route): void {
function shouldHotReload(payload: PageDataPayload): boolean { function shouldHotReload(payload: PageDataPayload): boolean {
const payloadPath = payload.path.replace(/(\bindex)?\.md$/, '') const payloadPath = payload.path.replace(/(\bindex)?\.md$/, '')
const locationPath = location.pathname.replace(/(\bindex)?\.html$/, '') const locationPath = location.pathname
.replace(/(\bindex)?\.html$/, '')
.slice(siteDataRef.value.base.length - 1)
return payloadPath === locationPath return payloadPath === locationPath
} }

@ -18,7 +18,7 @@ export { inBrowser } from '../shared'
/** /**
* Join two paths by resolving the slash collision. * Join two paths by resolving the slash collision.
*/ */
export function joinPath(base: string, path: string): string { export function joinPath(base: string, path: string) {
return `${base}${path}`.replace(/\/+/g, '/') return `${base}${path}`.replace(/\/+/g, '/')
} }
@ -31,7 +31,7 @@ export function withBase(path: string) {
/** /**
* Converts a url path to the corresponding js chunk filename. * Converts a url path to the corresponding js chunk filename.
*/ */
export function pathToFile(path: string): string { export function pathToFile(path: string) {
let pagePath = path.replace(/\.html$/, '') let pagePath = path.replace(/\.html$/, '')
pagePath = decodeURIComponent(pagePath) pagePath = decodeURIComponent(pagePath)
pagePath = pagePath.replace(/\/$/, '/index') // /foo/ -> /foo/index pagePath = pagePath.replace(/\/$/, '/index') // /foo/ -> /foo/index
@ -57,7 +57,11 @@ export function pathToFile(path: string): string {
: pagePath.slice(0, -3) + '_index.md' : pagePath.slice(0, -3) + '_index.md'
pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()] pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()]
} }
pagePath = `${base}assets/${pagePath}.${pageHash}.js` if (!pageHash) return null
pagePath = `${base}${__ASSETS_DIR__.replace(
/"(.+)"/,
'$1'
)}/${pagePath}.${pageHash}.js`
} else { } else {
// ssr build uses much simpler name mapping // ssr build uses much simpler name mapping
pagePath = `./${sanitizeFileName( pagePath = `./${sanitizeFileName(

@ -12,7 +12,7 @@ export type { EnhanceAppContext, Theme } from './app/theme'
export type { HeadConfig, Header, PageData, SiteData } from '../../types/shared' export type { HeadConfig, Header, PageData, SiteData } from '../../types/shared'
// composables // composables
export { useData } from './app/data' export { useData, dataSymbol } from './app/data'
export { useRoute, useRouter } from './app/router' export { useRoute, useRouter } from './app/router'
// utilities // utilities

@ -3,6 +3,7 @@ declare const __VP_LOCAL_SEARCH__: boolean
declare const __ALGOLIA__: boolean declare const __ALGOLIA__: boolean
declare const __CARBON__: boolean declare const __CARBON__: boolean
declare const __VUE_PROD_DEVTOOLS__: boolean declare const __VUE_PROD_DEVTOOLS__: boolean
declare const __ASSETS_DIR__: string
declare module '*.vue' { declare module '*.vue' {
import type { DefineComponent } from 'vue' import type { DefineComponent } from 'vue'

@ -1,15 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, provide, useSlots, watch } from 'vue'
import { useRoute } from 'vitepress' import { useRoute } from 'vitepress'
import { useData } from './composables/data' import { computed, provide, useSlots, watch } from 'vue'
import { useSidebar, useCloseSidebarOnEscape } from './composables/sidebar'
import VPSkipLink from './components/VPSkipLink.vue'
import VPBackdrop from './components/VPBackdrop.vue' import VPBackdrop from './components/VPBackdrop.vue'
import VPNav from './components/VPNav.vue'
import VPLocalNav from './components/VPLocalNav.vue'
import VPSidebar from './components/VPSidebar.vue'
import VPContent from './components/VPContent.vue' import VPContent from './components/VPContent.vue'
import VPFooter from './components/VPFooter.vue' import VPFooter from './components/VPFooter.vue'
import VPLocalNav from './components/VPLocalNav.vue'
import VPNav from './components/VPNav.vue'
import VPSidebar from './components/VPSidebar.vue'
import VPSkipLink from './components/VPSkipLink.vue'
import { useData } from './composables/data'
import { useCloseSidebarOnEscape, useSidebar } from './composables/sidebar'
const { const {
isOpen: isSidebarOpen, isOpen: isSidebarOpen,
@ -38,7 +38,7 @@ provide('hero-image-slot-exists', heroImageSlotExists)
<slot name="layout-top" /> <slot name="layout-top" />
<VPSkipLink /> <VPSkipLink />
<VPBackdrop class="backdrop" :show="isSidebarOpen" @click="closeSidebar" /> <VPBackdrop class="backdrop" :show="isSidebarOpen" @click="closeSidebar" />
<VPNav> <VPNav v-if="frontmatter.navbar !== false">
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template> <template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template> <template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
<template #nav-bar-content-before><slot name="nav-bar-content-before" /></template> <template #nav-bar-content-before><slot name="nav-bar-content-before" /></template>

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import NotFound from '../NotFound.vue'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar'
import VPPage from './VPPage.vue'
import VPHome from './VPHome.vue'
import VPDoc from './VPDoc.vue' import VPDoc from './VPDoc.vue'
import NotFound from '../NotFound.vue' import VPHome from './VPHome.vue'
import VPPage from './VPPage.vue'
const { page, frontmatter } = useData() const { page, frontmatter } = useData()
const { hasSidebar } = useSidebar() const { hasSidebar } = useSidebar()

@ -1,11 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from 'vitepress' import { useRoute } from 'vitepress'
import { computed } from 'vue' import { computed } from 'vue'
import { useData } from '../composables/data'
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar'
import VPDocAside from './VPDocAside.vue' import VPDocAside from './VPDocAside.vue'
import VPDocFooter from './VPDocFooter.vue' import VPDocFooter from './VPDocFooter.vue'
import VPDocOutlineDropdown from './VPDocOutlineDropdown.vue' import VPDocOutlineDropdown from './VPDocOutlineDropdown.vue'
const { theme } = useData()
const route = useRoute() const route = useRoute()
const { hasSidebar, hasAside, leftAside } = useSidebar() const { hasSidebar, hasAside, leftAside } = useSidebar()
@ -42,7 +45,13 @@ const pageName = computed(() =>
<slot name="doc-before" /> <slot name="doc-before" />
<VPDocOutlineDropdown /> <VPDocOutlineDropdown />
<main class="main"> <main class="main">
<Content class="vp-doc" :class="pageName" /> <Content
class="vp-doc"
:class="[
pageName,
theme.externalLinkIcon && 'external-link-icon-enabled'
]"
/>
</main> </main>
<VPDocFooter> <VPDocFooter>
<template #doc-footer-before><slot name="doc-footer-before" /></template> <template #doc-footer-before><slot name="doc-footer-before" /></template>
@ -65,7 +74,7 @@ const pageName = computed(() =>
display: none; display: none;
} }
@media (min-width: 960px) and (max-width: 1280px) { @media (min-width: 960px) and (max-width: 1279px) {
.VPDoc .VPDocOutlineDropdown { .VPDoc .VPDocOutlineDropdown {
display: block; display: block;
} }
@ -193,4 +202,9 @@ const pageName = computed(() =>
.VPDoc.has-aside .content-container { .VPDoc.has-aside .content-container {
max-width: 688px; max-width: 688px;
} }
.external-link-icon-enabled :is(.vp-doc a[href*='://'], .vp-doc a[target='_blank'])::after {
content: '';
color: currentColor;
}
</style> </style>

@ -41,20 +41,20 @@ const showFooter = computed(() => {
</div> </div>
</div> </div>
<div v-if="control.prev?.link || control.next?.link" class="prev-next"> <nav v-if="control.prev?.link || control.next?.link" class="prev-next">
<div class="pager"> <div class="pager">
<a v-if="control.prev?.link" class="pager-link prev" :href="normalizeLink(control.prev.link)"> <a v-if="control.prev?.link" class="pager-link prev" :href="normalizeLink(control.prev.link)">
<span class="desc" v-html="theme.docFooter?.prev || 'Previous page'"></span> <span class="desc" v-html="theme.docFooter?.prev || 'Previous page'"></span>
<span class="title" v-html="control.prev.text"></span> <span class="title" v-html="control.prev.text"></span>
</a> </a>
</div> </div>
<div class="pager" :class="{ 'has-prev': control.prev?.link }"> <div class="pager">
<a v-if="control.next?.link" class="pager-link next" :href="normalizeLink(control.next.link)"> <a v-if="control.next?.link" class="pager-link next" :href="normalizeLink(control.next.link)">
<span class="desc" v-html="theme.docFooter?.next || 'Next page'"></span> <span class="desc" v-html="theme.docFooter?.next || 'Next page'"></span>
<span class="title" v-html="control.next.text"></span> <span class="title" v-html="control.next.text"></span>
</a> </a>
</div> </div>
</div> </nav>
</footer> </footer>
</template> </template>
@ -101,29 +101,14 @@ const showFooter = computed(() => {
.prev-next { .prev-next {
border-top: 1px solid var(--vp-c-divider); border-top: 1px solid var(--vp-c-divider);
padding-top: 24px; padding-top: 24px;
display: grid;
grid-row-gap: 8px;
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.prev-next { .prev-next {
display: flex; grid-template-columns: repeat(2, 1fr);
} grid-column-gap: 16px;
}
.pager.has-prev {
padding-top: 8px;
}
@media (min-width: 640px) {
.pager {
display: flex;
flex-direction: column;
flex-shrink: 0;
width: 50%;
}
.pager.has-prev {
padding-top: 0;
padding-left: 16px;
} }
} }

@ -2,7 +2,7 @@
import { ref, computed, watchEffect, onMounted } from 'vue' import { ref, computed, watchEffect, onMounted } from 'vue'
import { useData } from '../composables/data' import { useData } from '../composables/data'
const { theme, page, lang } = useData() const { theme, page } = useData()
const date = computed(() => new Date(page.value.lastUpdated!)) const date = computed(() => new Date(page.value.lastUpdated!))
const isoDatetime = computed(() => date.value.toISOString()) const isoDatetime = computed(() => date.value.toISOString())
@ -12,14 +12,20 @@ const datetime = ref('')
// potential differences in timezones of the server and clients // potential differences in timezones of the server and clients
onMounted(() => { onMounted(() => {
watchEffect(() => { watchEffect(() => {
datetime.value = date.value.toLocaleString(lang.value) datetime.value = new Intl.DateTimeFormat(
undefined,
theme.value.lastUpdated?.formatOptions ?? {
dateStyle: 'short',
timeStyle: 'short'
}
).format(date.value)
}) })
}) })
</script> </script>
<template> <template>
<p class="VPLastUpdated"> <p class="VPLastUpdated">
{{ theme.lastUpdatedText || 'Last updated' }}: {{ theme.lastUpdated?.text || theme.lastUpdatedText || 'Last updated' }}:
<time :datetime="isoDatetime">{{ datetime }}</time> <time :datetime="isoDatetime">{{ datetime }}</time>
</p> </p>
</template> </template>

@ -7,10 +7,8 @@ defineProps<{
}>() }>()
function onClick({ target: el }: Event) { function onClick({ target: el }: Event) {
const id = '#' + (el as HTMLAnchorElement).href!.split('#')[1] const id = (el as HTMLAnchorElement).href!.split('#')[1]
const heading = document.querySelector<HTMLAnchorElement>( const heading = document.getElementById(decodeURIComponent(id))
decodeURIComponent(id)
)
heading?.focus() heading?.focus()
} }
</script> </script>

@ -14,7 +14,7 @@ defineProps<{
</script> </script>
<template> <template>
<VPLink class="VPFeature" :href="link" :no-icon="true"> <VPLink class="VPFeature" :href="link" :no-icon="true" :tag="link ? 'a' : 'div'">
<article class="box"> <article class="box">
<VPImage <VPImage
v-if="typeof icon === 'object'" v-if="typeof icon === 'object'"

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

@ -2,12 +2,12 @@
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar'
const { theme } = useData() const { theme, frontmatter } = useData()
const { hasSidebar } = useSidebar() const { hasSidebar } = useSidebar()
</script> </script>
<template> <template>
<footer v-if="theme.footer" class="VPFooter" :class="{ 'has-sidebar': hasSidebar }"> <footer v-if="theme.footer && frontmatter.footer !== false" class="VPFooter" :class="{ 'has-sidebar': hasSidebar }">
<div class="container"> <div class="container">
<p v-if="theme.footer.message" class="message" v-html="theme.footer.message"></p> <p v-if="theme.footer.message" class="message" v-html="theme.footer.message"></p>
<p v-if="theme.footer.copyright" class="copyright" v-html="theme.footer.copyright"></p> <p v-if="theme.footer.copyright" class="copyright" v-html="theme.footer.copyright"></p>
@ -47,7 +47,4 @@ const { hasSidebar } = useSidebar()
font-weight: 500; font-weight: 500;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
} }
.message { order: 2; }
.copyright { order: 1; }
</style> </style>

@ -27,10 +27,10 @@ const heroImageSlotExists = inject('hero-image-slot-exists') as Ref<boolean>
<div class="main"> <div class="main">
<slot name="home-hero-info"> <slot name="home-hero-info">
<h1 v-if="name" class="name"> <h1 v-if="name" class="name">
<span class="clip">{{ name }}</span> <span v-html="name" class="clip"></span>
</h1> </h1>
<p v-if="text" class="text">{{ text }}</p> <p v-if="text" v-html="text" class="text"></p>
<p v-if="tagline" class="tagline">{{ tagline }}</p> <p v-if="tagline" v-html="tagline" class="tagline"></p>
</slot> </slot>
<div v-if="actions" class="actions"> <div v-if="actions" class="actions">

@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed } from 'vue'
import { normalizeLink } from '../support/utils' import { normalizeLink } from '../support/utils'
import VPIconExternalLink from './icons/VPIconExternalLink.vue'
import { EXTERNAL_URL_RE } from '../../shared' import { EXTERNAL_URL_RE } from '../../shared'
const props = defineProps<{ const props = defineProps<{
@ -20,25 +19,15 @@ const isExternal = computed(() => props.href && EXTERNAL_URL_RE.test(props.href)
<component <component
:is="tag" :is="tag"
class="VPLink" class="VPLink"
:class="{ link: href }" :class="{
link: href,
'vp-external-link-icon': isExternal,
'no-icon': noIcon
}"
:href="href ? normalizeLink(href) : undefined" :href="href ? normalizeLink(href) : undefined"
:target="target || (isExternal ? '_blank' : undefined)" :target="target || (isExternal ? '_blank' : undefined)"
:rel="rel || (isExternal ? 'noreferrer' : undefined)" :rel="rel || (isExternal ? 'noreferrer' : undefined)"
> >
<slot /> <slot />
<VPIconExternalLink v-if="isExternal && !noIcon" class="icon" />
</component> </component>
</template> </template>
<style scoped>
.icon {
display: inline-block;
margin-top: -1px;
margin-left: 4px;
width: 11px;
height: 11px;
fill: var(--vp-c-text-3);
transition: fill 0.25s;
flex-shrink: 0;
}
</style>

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

@ -1,12 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick, shallowRef } from 'vue' import { onContentUpdated } from 'vitepress'
import { nextTick, ref } from 'vue'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { getHeaders, resolveTitle, type MenuItem } from '../composables/outline' import { resolveTitle, type MenuItem } from '../composables/outline'
import VPDocOutlineItem from './VPDocOutlineItem.vue' import VPDocOutlineItem from './VPDocOutlineItem.vue'
import { onContentUpdated } from 'vitepress'
import VPIconChevronRight from './icons/VPIconChevronRight.vue' import VPIconChevronRight from './icons/VPIconChevronRight.vue'
const { frontmatter, theme } = useData() const props = defineProps<{
headers: MenuItem[]
navHeight: number
}>()
const { theme } = useData()
const open = ref(false) const open = ref(false)
const vh = ref(0) const vh = ref(0)
const items = ref<HTMLDivElement>() const items = ref<HTMLDivElement>()
@ -17,7 +22,7 @@ onContentUpdated(() => {
function toggle() { function toggle() {
open.value = !open.value open.value = !open.value
vh.value = window.innerHeight + Math.min(window.scrollY - 64, 0) vh.value = window.innerHeight + Math.min(window.scrollY - props.navHeight, 0)
} }
function onItemClick(e: Event) { function onItemClick(e: Event) {
@ -36,14 +41,6 @@ function scrollToTop() {
open.value = false open.value = false
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
} }
const headers = shallowRef<MenuItem[]>([])
onContentUpdated(() => {
headers.value = getHeaders(
frontmatter.value.outline ?? theme.value.outline
)
})
</script> </script>
<template> <template>

@ -12,7 +12,7 @@ import {
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap' import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
import Mark from 'mark.js/src/vanilla.js' import Mark from 'mark.js/src/vanilla.js'
import MiniSearch, { type SearchResult } from 'minisearch' import MiniSearch, { type SearchResult } from 'minisearch'
import { useRouter } from 'vitepress' import { useRouter, dataSymbol } from 'vitepress'
import { import {
computed, computed,
createApp, createApp,
@ -27,7 +27,6 @@ import {
type Ref type Ref
} from 'vue' } from 'vue'
import type { ModalTranslations } from '../../../../types/local-search' import type { ModalTranslations } from '../../../../types/local-search'
import { dataSymbol } from '../../app/data'
import { pathToFile } from '../../app/utils' import { pathToFile } from '../../app/utils'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { createTranslate } from '../support/translation' import { createTranslate } from '../support/translation'
@ -81,8 +80,12 @@ const searchIndex = computedAsync(async () =>
searchOptions: { searchOptions: {
fuzzy: 0.2, fuzzy: 0.2,
prefix: true, prefix: true,
boost: { title: 4, text: 2, titles: 1 } boost: { title: 4, text: 2, titles: 1 },
} ...(theme.value.search?.provider === 'local' &&
theme.value.search.options?.miniSearch?.searchOptions)
},
...(theme.value.search?.provider === 'local' &&
theme.value.search.options?.miniSearch?.options)
} }
) )
) )
@ -119,8 +122,6 @@ watchEffect(() => {
const results: Ref<(SearchResult & Result)[]> = shallowRef([]) const results: Ref<(SearchResult & Result)[]> = shallowRef([])
const headingRegex = /<h(\d*).*?>.*?<a.*? href="#(.*?)".*?>.*?<\/a><\/h\1>/gi
const enableNoResults = ref(false) const enableNoResults = ref(false)
watch(filterText, () => { watch(filterText, () => {
@ -155,28 +156,42 @@ debouncedWatch(
if (canceled) return if (canceled) return
const c = new Map<string, Map<string, string>>() const c = new Map<string, Map<string, string>>()
for (const { id, mod } of mods) { for (const { id, mod } of mods) {
const mapId = id.slice(0, id.indexOf('#'))
let map = c.get(mapId)
if (map) continue
map = new Map()
c.set(mapId, map)
const comp = mod.default ?? mod const comp = mod.default ?? mod
if (comp?.render) { if (comp?.render || comp?.setup) {
const app = createApp(comp) const app = createApp(comp)
// Silence warnings about missing components // Silence warnings about missing components
app.config.warnHandler = () => {} app.config.warnHandler = () => {}
app.provide(dataSymbol, vitePressData) app.provide(dataSymbol, vitePressData)
Object.defineProperties(app.config.globalProperties, {
$frontmatter: {
get() {
return vitePressData.frontmatter.value
}
},
$params: {
get() {
return vitePressData.page.value.params
}
}
})
const div = document.createElement('div') const div = document.createElement('div')
app.mount(div) app.mount(div)
const sections = div.innerHTML.split(headingRegex) const headings = div.querySelectorAll('h1, h2, h3, h4, h5, h6')
headings.forEach((el) => {
const href = el.querySelector('a')?.getAttribute('href')
const anchor = href?.startsWith('#') && href.slice(1)
if (!anchor) return
let html = ''
while ((el = el.nextElementSibling!) && !/^h[1-6]$/i.test(el.tagName))
html += el.outerHTML
map!.set(anchor, html)
})
app.unmount() app.unmount()
sections.shift()
const mapId = id.slice(0, id.indexOf('#'))
let map = c.get(mapId)
if (!map) {
map = new Map()
c.set(mapId, map)
}
for (let i = 0; i < sections.length; i += 3) {
const anchor = sections[i + 1]
const html = sections[i + 2]
map.set(anchor, html)
}
} }
if (canceled) return if (canceled) return
} }
@ -219,6 +234,7 @@ debouncedWatch(
async function fetchExcerpt(id: string) { async function fetchExcerpt(id: string) {
const file = pathToFile(id.slice(0, id.indexOf('#'))) const file = pathToFile(id.slice(0, id.indexOf('#')))
try { try {
if (!file) throw new Error(`Cannot find file for id: ${id}`)
return { id, mod: await import(/*@vite-ignore*/ file) } return { id, mod: await import(/*@vite-ignore*/ file) }
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@ -229,10 +245,12 @@ async function fetchExcerpt(id: string) {
/* Search input focus */ /* Search input focus */
const searchInput = ref<HTMLInputElement>() const searchInput = ref<HTMLInputElement>()
const disableReset = computed(() => {
function focusSearchInput() { return filterText.value?.length <= 0
})
function focusSearchInput(select = true) {
searchInput.value?.focus() searchInput.value?.focus()
searchInput.value?.select() select && searchInput.value?.select()
} }
onMounted(() => { onMounted(() => {
@ -247,11 +265,11 @@ function onSearchBarClick(event: PointerEvent) {
/* Search keyboard selection */ /* Search keyboard selection */
const selectedIndex = ref(0) const selectedIndex = ref(-1)
const disableMouseOver = ref(false) const disableMouseOver = ref(false)
watch(results, () => { watch(results, (r) => {
selectedIndex.value = 0 selectedIndex.value = r.length ? 0 : -1
scrollToSelectedResult() scrollToSelectedResult()
}) })
@ -348,6 +366,11 @@ onBeforeUnmount(() => {
isLocked.value = false isLocked.value = false
}) })
function resetSearch() {
filterText.value = ''
nextTick().then(() => focusSearchInput(false))
}
function formMarkRegex(terms: Set<string>) { function formMarkRegex(terms: Set<string>) {
return new RegExp( return new RegExp(
[...terms] [...terms]
@ -365,11 +388,28 @@ function formMarkRegex(terms: Set<string>) {
<template> <template>
<Teleport to="body"> <Teleport to="body">
<div ref="el" class="VPLocalSearchBox" aria-modal="true"> <div
ref="el"
role="button"
:aria-owns="results?.length ? 'localsearch-list' : undefined"
aria-expanded="true"
aria-haspopup="listbox"
aria-labelledby="localsearch-label"
class="VPLocalSearchBox"
>
<div class="backdrop" @click="$emit('close')" /> <div class="backdrop" @click="$emit('close')" />
<div class="shell"> <div class="shell">
<div class="search-bar" @pointerup="onSearchBarClick($event)"> <form
class="search-bar"
@pointerup="onSearchBarClick($event)"
@submit.prevent=""
>
<label
:title="placeholder"
id="localsearch-label"
for="localsearch-input"
>
<svg <svg
class="search-icon" class="search-icon"
width="18" width="18"
@ -388,6 +428,7 @@ function formMarkRegex(terms: Set<string>) {
<path d="m21 21l-4.35-4.35" /> <path d="m21 21l-4.35-4.35" />
</g> </g>
</svg> </svg>
</label>
<div class="search-actions before"> <div class="search-actions before">
<button <button
class="back-button" class="back-button"
@ -415,6 +456,8 @@ function formMarkRegex(terms: Set<string>) {
ref="searchInput" ref="searchInput"
v-model="filterText" v-model="filterText"
:placeholder="placeholder" :placeholder="placeholder"
id="localsearch-input"
aria-labelledby="localsearch-label"
class="search-input" class="search-input"
/> />
<div class="search-actions"> <div class="search-actions">
@ -423,7 +466,9 @@ function formMarkRegex(terms: Set<string>) {
class="toggle-layout-button" class="toggle-layout-button"
:class="{ 'detailed-list': showDetailedList }" :class="{ 'detailed-list': showDetailedList }"
:title="$t('modal.displayDetails')" :title="$t('modal.displayDetails')"
@click="showDetailedList = !showDetailedList" @click="
selectedIndex > -1 && (showDetailedList = !showDetailedList)
"
> >
<svg <svg
width="18" width="18"
@ -444,8 +489,10 @@ function formMarkRegex(terms: Set<string>) {
<button <button
class="clear-button" class="clear-button"
type="reset"
:disabled="disableReset"
:title="$t('modal.resetButtonTitle')" :title="$t('modal.resetButtonTitle')"
@click="filterText = ''" @click="resetSearch"
> >
<svg <svg
width="18" width="18"
@ -464,16 +511,23 @@ function formMarkRegex(terms: Set<string>) {
</svg> </svg>
</button> </button>
</div> </div>
</div> </form>
<div <ul
ref="resultsEl" ref="resultsEl"
:id="results?.length ? 'localsearch-list' : undefined"
:role="results?.length ? 'listbox' : undefined"
:aria-labelledby="results?.length ? 'localsearch-label' : undefined"
class="results" class="results"
@mousemove="disableMouseOver = false" @mousemove="disableMouseOver = false"
> >
<a <li
v-for="(p, index) in results" v-for="(p, index) in results"
:key="p.id" :key="p.id"
role="option"
:aria-selected="selectedIndex === index ? 'true' : 'false'"
>
<a
:href="p.id" :href="p.id"
class="result" class="result"
:class="{ :class="{
@ -487,7 +541,11 @@ function formMarkRegex(terms: Set<string>) {
<div> <div>
<div class="titles"> <div class="titles">
<span class="title-icon">#</span> <span class="title-icon">#</span>
<span v-for="(t, index) in p.titles" :key="index" class="title"> <span
v-for="(t, index) in p.titles"
:key="index"
class="title"
>
<span class="text" v-html="t" /> <span class="text" v-html="t" />
<svg width="18" height="18" viewBox="0 0 24 24"> <svg width="18" height="18" viewBox="0 0 24 24">
<path <path
@ -514,15 +572,15 @@ function formMarkRegex(terms: Set<string>) {
</div> </div>
</div> </div>
</a> </a>
</li>
<div <li
v-if="filterText && !results.length && enableNoResults" v-if="filterText && !results.length && enableNoResults"
class="no-results" class="no-results"
> >
{{ $t('modal.noResultsText') }} "<strong>{{ filterText }}</strong {{ $t('modal.noResultsText') }} "<strong>{{ filterText }}</strong
>" >"
</div> </li>
</div> </ul>
<div class="search-keyboard-shortcuts"> <div class="search-keyboard-shortcuts">
<span> <span>
@ -680,11 +738,15 @@ function formMarkRegex(terms: Set<string>) {
padding: 8px; padding: 8px;
} }
.search-actions button:hover, .search-actions button:not([disabled]):hover,
.toggle-layout-button.detailed-list { .toggle-layout-button.detailed-list {
color: var(--vp-c-brand); color: var(--vp-c-brand);
} }
.search-actions button.clear-button:disabled {
opacity: 0.37;
}
.search-keyboard-shortcuts { .search-keyboard-shortcuts {
font-size: 0.8rem; font-size: 0.8rem;
opacity: 75%; opacity: 75%;

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

@ -1,15 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'
import { useWindowScroll } from '@vueuse/core' import { useWindowScroll } from '@vueuse/core'
import { computed } from 'vue'
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar'
import VPNavBarTitle from './VPNavBarTitle.vue'
import VPNavBarSearch from './VPNavBarSearch.vue'
import VPNavBarMenu from './VPNavBarMenu.vue'
import VPNavBarTranslations from './VPNavBarTranslations.vue'
import VPNavBarAppearance from './VPNavBarAppearance.vue' import VPNavBarAppearance from './VPNavBarAppearance.vue'
import VPNavBarSocialLinks from './VPNavBarSocialLinks.vue'
import VPNavBarExtra from './VPNavBarExtra.vue' import VPNavBarExtra from './VPNavBarExtra.vue'
import VPNavBarHamburger from './VPNavBarHamburger.vue' import VPNavBarHamburger from './VPNavBarHamburger.vue'
import VPNavBarMenu from './VPNavBarMenu.vue'
import VPNavBarSearch from './VPNavBarSearch.vue'
import VPNavBarSocialLinks from './VPNavBarSocialLinks.vue'
import VPNavBarTitle from './VPNavBarTitle.vue'
import VPNavBarTranslations from './VPNavBarTranslations.vue'
defineProps<{ defineProps<{
isScreenOpen: boolean isScreenOpen: boolean
@ -62,15 +62,10 @@ const classes = computed(() => ({
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
padding: 0 8px 0 24px; padding: 0 8px 0 24px;
height: var(--vp-nav-height); height: var(--vp-nav-height);
transition: border-color 0.5s, background-color 0.5s;
pointer-events: none; pointer-events: none;
white-space: nowrap; white-space: nowrap;
} }
.VPNavBar.has-sidebar {
border-bottom-color: var(--vp-c-gutter);
}
@media (min-width: 768px) { @media (min-width: 768px) {
.VPNavBar { .VPNavBar {
padding: 0 32px; padding: 0 32px;
@ -79,7 +74,6 @@ const classes = computed(() => ({
@media (min-width: 960px) { @media (min-width: 960px) {
.VPNavBar.has-sidebar { .VPNavBar.has-sidebar {
border-bottom-color: transparent;
padding: 0; padding: 0;
} }

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

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

@ -26,6 +26,7 @@ const { theme, localeIndex } = useData()
// payload), we delay initializing it until the user has actually clicked or // payload), we delay initializing it until the user has actually clicked or
// hit the hotkey to invoke it. // hit the hotkey to invoke it.
const loaded = ref(false) const loaded = ref(false)
const actuallyLoaded = ref(false)
const buttonText = computed(() => { const buttonText = computed(() => {
const options = theme.value.search?.options ?? theme.value.algolia const options = theme.value.search?.options ?? theme.value.algolia
@ -169,9 +170,10 @@ const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : ''
<VPAlgoliaSearchBox <VPAlgoliaSearchBox
v-if="loaded" v-if="loaded"
:algolia="theme.search?.options ?? theme.algolia" :algolia="theme.search?.options ?? theme.algolia"
@vue:beforeMount="actuallyLoaded = true"
/> />
<div v-else id="docsearch"> <div v-if="!actuallyLoaded" id="docsearch">
<VPNavBarSearchButton :placeholder="buttonText" @click="load" /> <VPNavBarSearchButton :placeholder="buttonText" @click="load" />
</div> </div>
</template> </template>

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { useSidebar } from '../composables/sidebar'
import { useLangs } from '../composables/langs' import { useLangs } from '../composables/langs'
import { useSidebar } from '../composables/sidebar'
import { normalizeLink } from '../support/utils' import { normalizeLink } from '../support/utils'
import VPImage from './VPImage.vue' import VPImage from './VPImage.vue'
@ -47,6 +47,6 @@ const { currentLang } = useLangs()
:deep(.logo) { :deep(.logo) {
margin-right: 8px; margin-right: 8px;
height: 24px; height: var(--vp-nav-logo-height);
} }
</style> </style>

@ -27,7 +27,7 @@ function unlockBodyScroll() {
@enter="lockBodyScroll" @enter="lockBodyScroll"
@after-leave="unlockBodyScroll" @after-leave="unlockBodyScroll"
> >
<div v-if="open" class="VPNavScreen" ref="screen"> <div v-if="open" class="VPNavScreen" ref="screen" id="VPNavScreen">
<div class="container"> <div class="container">
<slot name="nav-screen-content-before" /> <slot name="nav-screen-content-before" />
<VPNavScreenMenu class="menu" /> <VPNavScreenMenu class="menu" />

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

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

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

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

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

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock'
import { ref, watchPostEffect } from 'vue' import { ref, watchPostEffect } from 'vue'
import { disableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock'
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar'
import VPSidebarItem from './VPSidebarItem.vue' import VPSidebarItem from './VPSidebarItem.vue'

@ -193,6 +193,7 @@ function onCaretClick() {
color: var(--vp-c-text-3); color: var(--vp-c-text-3);
cursor: pointer; cursor: pointer;
transition: color 0.25s; transition: color 0.25s;
flex-shrink: 0;
} }
.item:hover .caret { .item:hover .caret {

@ -8,8 +8,8 @@ const backToTop = ref()
watch(() => route.path, () => backToTop.value.focus()) watch(() => route.path, () => backToTop.value.focus())
function focusOnTargetAnchor({ target }: Event) { function focusOnTargetAnchor({ target }: Event) {
const el = document.querySelector<HTMLAnchorElement>( const el = document.getElementById(
decodeURIComponent((target as HTMLAnchorElement).hash) decodeURIComponent((target as HTMLAnchorElement).hash).slice(1)
) )
if (el) { if (el) {
@ -59,10 +59,6 @@ function focusOnTargetAnchor({ target }: Event) {
clip-path: none; clip-path: none;
} }
.dark .VPSkipLink {
color: var(--vp-c-green);
}
@media (min-width: 1280px) { @media (min-width: 1280px) {
.VPSkipLink { .VPSkipLink {
top: 14px; top: 14px;

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

@ -10,10 +10,11 @@ defineProps<{
<template> <template>
<div class="VPSocialLinks"> <div class="VPSocialLinks">
<VPSocialLink <VPSocialLink
v-for="{ link, icon } in links" v-for="{ link, icon, ariaLabel } in links"
:key="link" :key="link"
:icon="icon" :icon="icon"
:link="link" :link="link"
:ariaLabel="ariaLabel"
/> />
</div> </div>
</template> </template>

@ -1,14 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, watch } from 'vue'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { APPEARANCE_KEY } from '../../shared' import { inBrowser, APPEARANCE_KEY } from '../../shared'
import VPSwitch from './VPSwitch.vue' import VPSwitch from './VPSwitch.vue'
import VPIconSun from './icons/VPIconSun.vue' import VPIconSun from './icons/VPIconSun.vue'
import VPIconMoon from './icons/VPIconMoon.vue' import VPIconMoon from './icons/VPIconMoon.vue'
const { site, isDark } = useData() const { site, isDark } = useData()
const checked = ref(false) const checked = ref(false)
const toggle = typeof localStorage !== 'undefined' ? useAppearance() : () => {} const toggle = inBrowser ? useAppearance() : () => {}
onMounted(() => { onMounted(() => {
checked.value = document.documentElement.classList.contains('dark') checked.value = document.documentElement.classList.contains('dark')

@ -1,13 +0,0 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
height="24px"
viewBox="0 0 24 24"
width="24px"
>
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z" />
</svg>
</template>

@ -1,5 +1,5 @@
import { computed } from 'vue'
import { useMediaQuery } from '@vueuse/core' import { useMediaQuery } from '@vueuse/core'
import { computed } from 'vue'
import { useSidebar } from './sidebar' import { useSidebar } from './sidebar'
export function useAside() { export function useAside() {

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

@ -1,6 +1,6 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useData } from './data' import { useData } from './data'
import { isActive } from '../support/utils' import { isActive } from '../../shared'
import { getSidebar, getFlatSideBarLinks } from '../support/sidebar' import { getSidebar, getFlatSideBarLinks } from '../support/sidebar'
export function usePrevNext() { export function usePrevNext() {
@ -14,9 +14,16 @@ export function usePrevNext() {
return isActive(page.value.relativePath, link.link) return isActive(page.value.relativePath, link.link)
}) })
return { const hidePrev =
prev: (theme.value.docFooter?.prev === false && !frontmatter.value.prev) ||
frontmatter.value.prev === false frontmatter.value.prev === false
const hideNext =
(theme.value.docFooter?.next === false && !frontmatter.value.next) ||
frontmatter.value.next === false
return {
prev: hidePrev
? undefined ? undefined
: { : {
text: text:
@ -30,8 +37,7 @@ export function usePrevNext() {
? frontmatter.value.prev.link ? frontmatter.value.prev.link
: undefined) ?? candidates[index - 1]?.link : undefined) ?? candidates[index - 1]?.link
}, },
next: next: hideNext
frontmatter.value.next === false
? undefined ? undefined
: { : {
text: text:

@ -10,7 +10,7 @@ import {
import { useMediaQuery } from '@vueuse/core' import { useMediaQuery } from '@vueuse/core'
import { useRoute } from 'vitepress' import { useRoute } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { isActive } from '../support/utils' import { isActive } from '../../shared'
import { import {
hasActiveLink as containsActiveLink, hasActiveLink as containsActiveLink,
getSidebar, getSidebar,

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

@ -42,7 +42,9 @@
font-weight: 500; font-weight: 500;
user-select: none; user-select: none;
opacity: 0; opacity: 0;
transition: color 0.25s, opacity 0.25s; transition:
color 0.25s,
opacity 0.25s;
} }
.vp-doc .header-anchor:before { .vp-doc .header-anchor:before {
@ -224,6 +226,7 @@
.vp-doc .custom-block div[class*='language-'] { .vp-doc .custom-block div[class*='language-'] {
margin: 8px 0; margin: 8px 0;
border-radius: 8px;
} }
.vp-doc .custom-block div[class*='language-'] code { .vp-doc .custom-block div[class*='language-'] code {
@ -231,6 +234,11 @@
background-color: transparent; background-color: transparent;
} }
.vp-doc .custom-block .vp-code-group .tabs {
margin: 0;
border-radius: 8px 8px 0 0;
}
/** /**
* Code * Code
* -------------------------------------------------------------------------- */ * -------------------------------------------------------------------------- */
@ -245,7 +253,9 @@
padding: 3px 6px; padding: 3px 6px;
color: var(--vp-c-text-code); color: var(--vp-c-text-code);
background-color: var(--vp-c-mute); background-color: var(--vp-c-mute);
transition: color 0.5s, background-color 0.5s; transition:
color 0.5s,
background-color 0.5s;
} }
.vp-doc h1 > code, .vp-doc h1 > code,
@ -362,12 +372,16 @@
.vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) { .vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) {
filter: blur(0.095rem); filter: blur(0.095rem);
opacity: 0.4; opacity: 0.4;
transition: filter 0.35s, opacity 0.35s; transition:
filter 0.35s,
opacity 0.35s;
} }
.vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) { .vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) {
opacity: 0.7; opacity: 0.7;
transition: filter 0.35s, opacity 0.35s; transition:
filter 0.35s,
opacity 0.35s;
} }
.vp-doc [class*='language-']:hover .has-focused-lines .line:not(.has-focus) { .vp-doc [class*='language-']:hover .has-focused-lines .line:not(.has-focus) {
@ -415,7 +429,9 @@
line-height: var(--vp-code-line-height); line-height: var(--vp-code-line-height);
font-size: var(--vp-code-font-size); font-size: var(--vp-code-font-size);
color: var(--vp-code-line-number-color); color: var(--vp-code-line-number-color);
transition: border-color 0.5s, color 0.5s; transition:
border-color 0.5s,
color 0.5s;
} }
.vp-doc [class*='language-'] > button.copy { .vp-doc [class*='language-'] > button.copy {
@ -437,7 +453,10 @@
background-position: 50%; background-position: 50%;
background-size: 20px; background-size: 20px;
background-repeat: no-repeat; background-repeat: no-repeat;
transition: border-color 0.25s, background-color 0.25s, opacity 0.25s; transition:
border-color 0.25s,
background-color 0.25s,
opacity 0.25s;
} }
.vp-doc [class*='language-']:hover > button.copy, .vp-doc [class*='language-']:hover > button.copy,
@ -492,7 +511,9 @@
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: var(--vp-c-code-dimm); color: var(--vp-c-code-dimm);
transition: color 0.4s, opacity 0.4s; transition:
color 0.4s,
opacity 0.4s;
} }
.vp-doc [class*='language-']:hover > button.copy + span.lang, .vp-doc [class*='language-']:hover > button.copy + span.lang,
@ -522,3 +543,22 @@
margin: 0 !important; margin: 0 !important;
max-width: calc((100% - 24px) / 2) !important; max-width: calc((100% - 24px) / 2) !important;
} }
/* prettier-ignore */
:is(.vp-external-link-icon, .vp-doc a[href*='://'], .vp-doc a[target='_blank']):not(.no-icon)::after {
display: inline-block;
margin-top: -1px;
margin-left: 4px;
width: 11px;
height: 11px;
background: currentColor;
color: var(--vp-c-text-3);
flex-shrink: 0;
--icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E");
-webkit-mask-image: var(--icon);
mask-image: var(--icon);
}
.vp-external-link-icon::after {
content: '';
}

@ -361,6 +361,7 @@
--vp-nav-height: 64px; --vp-nav-height: 64px;
--vp-nav-bg-color: var(--vp-c-bg); --vp-nav-bg-color: var(--vp-c-bg);
--vp-nav-screen-bg-color: var(--vp-c-bg); --vp-nav-screen-bg-color: var(--vp-c-bg);
--vp-nav-logo-height: 24px;
} }
/** /**

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

@ -2,8 +2,6 @@ import { withBase } from 'vitepress'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { isExternal, PATHNAME_PROTOCOL_RE } from '../../shared' import { isExternal, PATHNAME_PROTOCOL_RE } from '../../shared'
export { isExternal, isActive } from '../../shared'
export function throttleAndDebounce(fn: () => void, delay: number): () => void { export function throttleAndDebounce(fn: () => void, delay: number): () => void {
let timeoutId: NodeJS.Timeout let timeoutId: NodeJS.Timeout
let called = false let called = false
@ -35,7 +33,7 @@ export function normalizeLink(url: string): string {
} }
const { site } = useData() const { site } = useData()
const { pathname, search, hash } = new URL(url, 'http://example.com') const { pathname, search, hash } = new URL(url, 'http://a.com')
const normalizedPath = const normalizedPath =
pathname.endsWith('/') || pathname.endsWith('.html') pathname.endsWith('/') || pathname.endsWith('.html')

@ -23,6 +23,8 @@ export { default as VPTeamPageTitle } from './components/VPTeamPageTitle.vue'
export { default as VPTeamPageSection } from './components/VPTeamPageSection.vue' export { default as VPTeamPageSection } from './components/VPTeamPageSection.vue'
export { default as VPTeamMembers } from './components/VPTeamMembers.vue' export { default as VPTeamMembers } from './components/VPTeamMembers.vue'
export { useSidebar } from './composables/sidebar'
const theme: Theme = { const theme: Theme = {
Layout, Layout,
enhanceApp: ({ app }) => { enhanceApp: ({ app }) => {

@ -1,16 +1,18 @@
import { createHash } from 'crypto'
import fs from 'fs-extra' import fs from 'fs-extra'
import path from 'path' import { createRequire } from 'module'
import ora from 'ora' import ora from 'ora'
import path from 'path'
import { packageDirectorySync } from 'pkg-dir'
import { rimraf } from 'rimraf'
import type { OutputAsset, OutputChunk } from 'rollup'
import { pathToFileURL } from 'url'
import type { BuildOptions } from 'vite' import type { BuildOptions } from 'vite'
import type { OutputChunk, OutputAsset } from 'rollup' import { resolveConfig, type SiteConfig } from '../config'
import { resolveConfig } from '../config' import { slash, type HeadConfig } from '../shared'
import { deserializeFunctions, serializeFunctions } from '../utils/fnSerialize'
import { bundle, failMark, okMark } from './bundle'
import { renderPage } from './render' import { renderPage } from './render'
import { bundle, okMark, failMark } from './bundle'
import { createRequire } from 'module'
import { pathToFileURL } from 'url'
import { packageDirectorySync } from 'pkg-dir'
import { serializeFunctions } from '../utils/fnSerialize'
import type { HeadConfig } from '../shared'
export async function build( export async function build(
root?: string, root?: string,
@ -41,7 +43,7 @@ export async function build(
const entryPath = path.join(siteConfig.tempDir, 'app.js') const entryPath = path.join(siteConfig.tempDir, 'app.js')
const { render } = await import(pathToFileURL(entryPath).toString()) const { render } = await import(pathToFileURL(entryPath).toString())
const spinner = ora() const spinner = ora({ discardStdin: false })
spinner.start('rendering pages...') spinner.start('rendering pages...')
try { try {
@ -55,12 +57,12 @@ export async function build(
) as OutputChunk) ) as OutputChunk)
const cssChunk = ( const cssChunk = (
siteConfig.mpa ? serverResult : clientResult siteConfig.mpa ? serverResult : clientResult!
).output.find( ).output.find(
(chunk) => chunk.type === 'asset' && chunk.fileName.endsWith('.css') (chunk) => chunk.type === 'asset' && chunk.fileName.endsWith('.css')
) as OutputAsset ) as OutputAsset
const assets = (siteConfig.mpa ? serverResult : clientResult).output const assets = (siteConfig.mpa ? serverResult : clientResult!).output
.filter( .filter(
(chunk) => chunk.type === 'asset' && !chunk.fileName.endsWith('.css') (chunk) => chunk.type === 'asset' && !chunk.fileName.endsWith('.css')
) )
@ -78,6 +80,8 @@ export async function build(
chunk.moduleIds.some((id) => id.includes('client/theme-default')) chunk.moduleIds.some((id) => id.includes('client/theme-default'))
) )
const metadataScript = generateMetadataScript(pageToHashMap, siteConfig)
if (isDefaultTheme) { if (isDefaultTheme) {
const fontURL = assets.find((file) => const fontURL = assets.find((file) =>
/inter-roman-latin\.\w+\.woff2/.test(file) /inter-roman-latin\.\w+\.woff2/.test(file)
@ -96,15 +100,6 @@ export async function build(
} }
} }
// We embed the hash map and site config strings into each page directly
// so that it doesn't alter the main chunk's hash on every build.
// It's also embedded as a string and JSON.parsed from the client because
// it's faster than embedding as JS object literal.
const hashMapString = JSON.stringify(JSON.stringify(pageToHashMap))
const siteDataString = JSON.stringify(
JSON.stringify(serializeFunctions({ ...siteConfig.site, head: [] }))
)
await Promise.all( await Promise.all(
['404.md', ...siteConfig.pages] ['404.md', ...siteConfig.pages]
.map((page) => siteConfig.rewrites.map[page] || page) .map((page) => siteConfig.rewrites.map[page] || page)
@ -118,8 +113,7 @@ export async function build(
cssChunk, cssChunk,
assets, assets,
pageToHashMap, pageToHashMap,
hashMapString, metadataScript,
siteDataString,
additionalHeadTags additionalHeadTags
) )
) )
@ -142,8 +136,7 @@ export async function build(
) )
} finally { } finally {
unlinkVue() unlinkVue()
if (!process.env.DEBUG) if (!process.env.DEBUG) await rimraf(siteConfig.tempDir)
fs.rmSync(siteConfig.tempDir, { recursive: true, force: true })
} }
await siteConfig.buildEnd?.(siteConfig) await siteConfig.buildEnd?.(siteConfig)
@ -168,3 +161,51 @@ function linkVue() {
} }
return () => {} return () => {}
} }
function generateMetadataScript(
pageToHashMap: Record<string, string>,
config: SiteConfig
) {
if (config.mpa) {
return { html: '', inHead: false }
}
// We embed the hash map and site config strings into each page directly
// so that it doesn't alter the main chunk's hash on every build.
// It's also embedded as a string and JSON.parsed from the client because
// it's faster than embedding as JS object literal.
const hashMapString = JSON.stringify(JSON.stringify(pageToHashMap))
const siteDataString = JSON.stringify(
JSON.stringify(serializeFunctions({ ...config.site, head: [] }))
)
const metadataContent = `window.__VP_HASH_MAP__=JSON.parse(${hashMapString});${
siteDataString.includes('_vp-fn_')
? `${deserializeFunctions.toString()};window.__VP_SITE_DATA__=deserializeFunctions(JSON.parse(${siteDataString}));`
: `window.__VP_SITE_DATA__=JSON.parse(${siteDataString});`
}`
if (!config.metaChunk) {
return { html: `<script>${metadataContent}</script>`, inHead: false }
}
const metadataFile = path.join(
config.assetsDir,
'chunks',
`metadata.${createHash('sha256')
.update(metadataContent)
.digest('hex')
.slice(0, 8)}.js`
)
const resolvedMetadataFile = path.join(config.outDir, metadataFile)
const metadataFileURL = slash(`${config.site.base}${metadataFile}`)
fs.ensureDirSync(path.dirname(resolvedMetadataFile))
fs.writeFileSync(resolvedMetadataFile, metadataContent)
return {
html: `<script type="module" src="${metadataFileURL}"></script>`,
inHead: true
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save