Merge upstream main into local search breakpoint fix

pull/5217/head
cyphercodes 1 month ago
commit 8c8c2910db

@ -51,4 +51,46 @@ describe('local search', () => {
await page.setViewportSize({ width: 1280, height: 720 })
}
})
test('navigate results with macOS Ctrl shortcuts', async () => {
await page.evaluate(() => document.documentElement.classList.add('mac'))
await page.locator('.VPNavBarSearchButton').click()
const input = await page.waitForSelector('input#localsearch-input')
await input.type('lorem')
await page.waitForFunction(() => {
return (
document.querySelectorAll('#localsearch-list li[role=option]').length >
1
)
})
expect(await input.getAttribute('aria-activedescendant')).toBe(
'localsearch-item-0'
)
await pressMacCtrl('n')
expect(await input.getAttribute('aria-activedescendant')).toBe(
'localsearch-item-1'
)
await pressMacCtrl('p')
expect(await input.getAttribute('aria-activedescendant')).toBe(
'localsearch-item-0'
)
})
})
function pressMacCtrl(key: string) {
return page.evaluate((key) => {
window.dispatchEvent(
new KeyboardEvent('keydown', {
key,
ctrlKey: true,
bubbles: true,
cancelable: true
})
)
}, key)
}

@ -0,0 +1,69 @@
import { ref } from 'vue'
import type { VitePressData } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { resolveLocaleLink } from 'client/theme-default/composables/langs'
function createData(
themeConfig: DefaultTheme.Config,
relativePath = 'guide/getting-started.md',
cleanUrls = false,
hash = '#install'
) {
return {
site: ref({
cleanUrls,
locales: {
root: { label: 'English', lang: 'en-US' },
fr: { label: 'Français', lang: 'fr-FR', link: '/fr/' }
},
themeConfig
}),
page: ref({ relativePath }),
theme: ref(themeConfig),
hash: ref(hash)
} as unknown as VitePressData<DefaultTheme.Config>
}
describe('client/theme-default/composables/langs', () => {
test('resolves corresponding links with the default router', () => {
expect(resolveLocaleLink(createData({}), 'fr', '/fr/', '/', true)).toBe(
'/fr/guide/getting-started.html#install'
)
})
test('resolves clean index links with the default router', () => {
expect(
resolveLocaleLink(
createData({}, 'en/guide/index.md', true, '#intro'),
'fr',
'/fr/',
'/en/',
true
)
).toBe('/fr/guide/#intro')
})
test('keeps locale root links when i18n routing is disabled', () => {
expect(
resolveLocaleLink(
createData({ i18nRouting: false }),
'fr',
'/fr/',
'/',
true
)
).toBe('/fr/#install')
})
test('uses custom i18n routing functions for corresponding links', () => {
const data = createData({
i18nRouting(data, hash, targetLocale) {
return `${data.site.value.locales[targetLocale].link}mapped/${data.page.value.relativePath}${hash}`
}
})
expect(resolveLocaleLink(data, 'fr', '/fr/', '/', true)).toBe(
'/fr/mapped/guide/getting-started.md#install'
)
})
})

@ -0,0 +1,39 @@
import { mergeConfig } from 'node/config'
import type { MarkdownItAsync } from 'markdown-it-async'
describe('node/config', () => {
test('merges markdown config hooks from extended configs', async () => {
const calls: string[] = []
const md = {} as MarkdownItAsync
const merged = mergeConfig(
{
markdown: {
lineNumbers: true,
config() {
calls.push('base')
}
}
},
{
markdown: {
attrs: {
allowedAttributes: ['id']
},
async config() {
calls.push('extended')
}
}
}
)
expect(merged.markdown?.lineNumbers).toBe(true)
expect(merged.markdown?.attrs).toEqual({
allowedAttributes: ['id']
})
await merged.markdown?.config?.(md)
expect(calls).toEqual(['base', 'extended'])
})
})

@ -48,4 +48,17 @@ describe('node/markdown/plugins/link', () => {
'href="/foo.html?title=Cat&amp;oldid=916388819#:~:text=Claws-,Like%20almost,the%20Felidae%2C,-cats"'
)
})
test('records source line numbers for collected links', async () => {
const env: {
cleanUrls: boolean
links?: string[]
linkLines?: number[]
} = { cleanUrls: false }
await md.renderAsync('Intro\n\n[Missing](./missing.md)\n', env)
expect(env.links).toEqual(['./missing'])
expect(env.linkLines).toEqual([3])
})
})

@ -0,0 +1,69 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { resolveConfig } from 'node/config'
import { createMarkdownToVueRenderFn } from 'node/markdownToVue'
describe('node/markdownToVue', () => {
let root: string | undefined
afterEach(async () => {
if (root) {
await rm(root, { recursive: true, force: true })
root = undefined
}
})
test('records source line numbers for dead links', async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-dead-link-'))
const file = path.join(root, 'index.md')
const src = '# Home\n\nIntro\n\n[Missing](./missing.md)\n'
await writeFile(file, src)
const siteConfig = await resolveConfig(root, 'build', 'production')
const render = await createMarkdownToVueRenderFn(
siteConfig.srcDir,
{ cache: false },
'/',
false,
false,
siteConfig
)
const result = await render(src, file, 'public')
expect(result.deadLinks).toContainEqual({
url: './missing',
file,
line: 5
})
})
test('records source line numbers after frontmatter', async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-dead-link-'))
const file = path.join(root, 'index.md')
const src =
'---\ntitle: Home\n---\n# Home\n\nIntro\n\n[Missing](./missing.md)\n'
await writeFile(file, src)
const siteConfig = await resolveConfig(root, 'build', 'production')
const render = await createMarkdownToVueRenderFn(
siteConfig.srcDir,
{ cache: false },
'/',
false,
false,
siteConfig
)
const result = await render(src, file, 'public')
expect(result.deadLinks).toContainEqual({
url: './missing',
file,
line: 8
})
})
})

@ -166,6 +166,13 @@ Don't enable options like _Auto Minify_ for HTML code. It will remove comments f
with:
node-version: 24
cache: npm # or pnpm / yarn
- name: Cache VitePress
uses: actions/cache@v4
with:
path: docs/.vitepress/cache
key: ${{ runner.os }}-vitepress-${{ hashFiles('docs/**', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb') }}
restore-keys: |
${{ runner.os }}-vitepress-
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies

@ -37,6 +37,10 @@ $ yarn add -D vitepress@next vue
$ bun add -D vitepress@next
```
```sh [deno]
$ deno add -D vitepress@next
```
:::
::: tip NOTE

@ -25,10 +25,28 @@ export default {
## i18nRouting
- Type: `boolean`
- Type: `boolean | ((data: VitePressData<DefaultTheme.Config>, hash: string, targetLocale: string) => string)`
Changing locale to say `zh` will change the URL from `/foo` (or `/en/foo/`) to `/zh/foo`. You can disable this behavior by setting `themeConfig.i18nRouting` to `false`.
Set `themeConfig.i18nRouting` to a function to customize the locale link. The function receives the current VitePress data, the current hash, and the target locale key, and returns the target link.
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
i18nRouting(data, hash, targetLocale) {
const target = data.site.value.locales[targetLocale]
const targetLink =
target.link || (targetLocale === 'root' ? '/' : `/${targetLocale}/`)
return `${targetLink}${data.page.value.relativePath.replace(/\.md$/, '')}${hash}`
}
}
})
```
## logo
- Type: `ThemeableImage`

@ -62,6 +62,8 @@ interface PageData {
}
```
`page.headers` is populated only when [`markdown.headers`](./site-config#markdown) is enabled. Without that option, it remains an empty array. The default theme outline reads rendered headings from the page content, so it can still appear when `page.headers` is empty.
**Example:**
```vue

@ -134,13 +134,43 @@ export default defineConfigWithTheme<ThemeConfig>({
You can configure the underlying [Markdown-It](https://github.com/markdown-it/markdown-it) instance using the [markdown](#markdown) option in your VitePress config.
### Page-Level Overrides
Some settings can be overridden for specific pages using frontmatter.
See [Frontmatter Config](./frontmatter-config) for details.
### Directory-Level Overrides
Some config settings can be overridden at the directory level, allowing all pages in that directory to share settings without needing to repeat them in the frontmatter of each page.
This is achieved by adding a file called `config.ts` (or `.js`, `.mjs`, or `.mts`) in the relevant directory. This file should export a config object using `export default`, similar to the main config file.
Nested directories inherit settings from their parent directory, with configuration overrides being merged accordingly.
The `defineAdditionalConfig` helper can be used to get TypeScript-powered intellisense for the available options, though as with `defineConfig` its use is optional.
For example, for a site with multiple languages we might want a different `description` for each language. We could add `es/config.ts` with the following content:
```ts
import { defineAdditionalConfig } from 'vitepress'
export default defineAdditionalConfig({
description: 'Generador de Sitios Estáticos desarrollado con Vite y Vue.'
})
```
This `description` would then be used for all pages in the `es` directory.
Alternatively, when using the built-in i18n features, the settings for a locale directory can be overridden via the `locales` setting in the main configuration file. See [Internationalization](../guide/i18n) for details.
## Site Metadata
### title
- Type: `string`
- Default: `VitePress`
- Can be overridden per page via [frontmatter](./frontmatter-config#title)
- Can be overridden per page via [frontmatter](./frontmatter-config#title) or at the [directory level](#directory-level-overrides)
Title for the site. When using the default theme, this will be displayed in the nav bar.
@ -161,7 +191,7 @@ The title of the page will be `Hello | My Awesome Site`.
### titleTemplate
- Type: `string | boolean`
- Can be overridden per page via [frontmatter](./frontmatter-config#titletemplate)
- Can be overridden per page via [frontmatter](./frontmatter-config#titletemplate) or at the [directory level](#directory-level-overrides)
Allows customizing each page's title suffix or the entire title. For example:
@ -194,7 +224,7 @@ The option can be set to `false` to disable title suffixes.
- Type: `string`
- Default: `A VitePress site`
- Can be overridden per page via [frontmatter](./frontmatter-config#description)
- Can be overridden per page via [frontmatter](./frontmatter-config#description) or at the [directory level](#directory-level-overrides)
Description for the site. This will render as a `<meta>` tag in the page HTML.
@ -208,7 +238,7 @@ export default {
- Type: `HeadConfig[]`
- Default: `[]`
- Can be appended per page via [frontmatter](./frontmatter-config#head)
- Can be appended per page via [frontmatter](./frontmatter-config#head) or at the [directory level](#directory-level-overrides)
Additional elements to render in the `<head>` tag in the page HTML. The user-added tags are rendered before the closing `head` tag, after VitePress tags.
@ -320,6 +350,7 @@ export default {
- Type: `string`
- Default: `en-US`
- Can be overridden at the [directory level](#directory-level-overrides)
The lang attribute for the site. This will render as a `<html lang="en-US">` tag in the page HTML.
@ -530,6 +561,8 @@ export default {
Check the [type declaration and jsdocs](https://github.com/vuejs/vitepress/blob/main/src/node/markdown/markdown.ts) for all the options available.
Set `markdown.headers` to `true` or pass [`@mdit-vue/plugin-headers`](https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers) options to collect headings into [`useData().page.headers`](./runtime-api#usedata). This option is disabled by default.
### vite
- Type: `import('vite').UserConfig`

@ -16,6 +16,6 @@
"postcss-rtlcss": "^6.0.0",
"vitepress": "workspace:*",
"vitepress-plugin-group-icons": "^1.7.5",
"vitepress-plugin-llms": "^1.12.1"
"vitepress-plugin-llms": "^1.13.2"
}
}

@ -96,31 +96,31 @@
"*": "prettier --experimental-cli --ignore-unknown --write"
},
"dependencies": {
"@docsearch/css": "^4.6.2",
"@docsearch/js": "^4.6.2",
"@docsearch/sidepanel-js": "^4.6.2",
"@iconify-json/simple-icons": "^1.2.78",
"@shikijs/core": "^4.0.2",
"@shikijs/transformers": "^4.0.2",
"@shikijs/types": "^4.0.2",
"@docsearch/css": "^4.6.3",
"@docsearch/js": "^4.6.3",
"@docsearch/sidepanel-js": "^4.6.3",
"@iconify-json/simple-icons": "^1.2.87",
"@shikijs/core": "^4.3.0",
"@shikijs/transformers": "^4.3.0",
"@shikijs/types": "^4.3.0",
"@types/markdown-it": "^14.1.2",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/devtools-api": "^8.1.1",
"@vue/shared": "^3.5.32",
"@vueuse/core": "^14.2.1",
"@vueuse/integrations": "^14.2.1",
"focus-trap": "^8.0.1",
"@vitejs/plugin-vue": "^6.0.7",
"@vue/devtools-api": "^8.1.4",
"@vue/shared": "^3.5.39",
"@vueuse/core": "^14.3.0",
"@vueuse/integrations": "^14.3.0",
"focus-trap": "^8.2.2",
"mark.js": "8.11.1",
"minisearch": "^7.2.0",
"shiki": "^4.0.2",
"vite": "^7.3.2",
"vue": "^3.5.32"
"shiki": "^4.3.0",
"vite": "^7.3.6",
"vue": "^3.5.39"
},
"devDependencies": {
"@clack/prompts": "^1.2.0",
"@emnapi/core": "^1.10.0",
"@emnapi/runtime": "^1.10.0",
"@iconify/utils": "^3.1.0",
"@clack/prompts": "^1.6.0",
"@emnapi/core": "^1.11.1",
"@emnapi/runtime": "^1.11.1",
"@iconify/utils": "^3.1.3",
"@mdit-vue/plugin-component": "^3.0.2",
"@mdit-vue/plugin-frontmatter": "^3.0.2",
"@mdit-vue/plugin-headers": "^3.0.2",
@ -130,7 +130,7 @@
"@mdit-vue/shared": "^3.0.2",
"@polka/compression": "^1.0.0-next.28",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "^29.0.2",
"@rollup/plugin-commonjs": "^29.0.3",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
@ -142,58 +142,57 @@
"@types/markdown-it-container": "^4.0.0",
"@types/markdown-it-emoji": "^3.0.1",
"@types/minimist": "^1.2.5",
"@types/node": "^25.6.0",
"@types/node": "^25.9.4",
"@types/picomatch": "^4.0.3",
"@types/prompts": "^2.4.9",
"chokidar": "^5.0.0",
"conventional-changelog": "^7.2.0",
"conventional-changelog": "^7.2.1",
"conventional-changelog-angular": "^8.3.1",
"cross-spawn": "^7.0.6",
"esbuild": "^0.27.7",
"execa": "^9.6.1",
"fs-extra": "^11.3.4",
"fs-extra": "^11.3.5",
"get-port": "^7.2.0",
"gray-matter": "^4.0.3",
"lint-staged": "^16.4.0",
"lodash.template": "^4.18.1",
"lru-cache": "^11.3.5",
"markdown-it": "^14.1.1",
"lru-cache": "^11.5.1",
"markdown-it": "^14.2.0",
"markdown-it-anchor": "^9.2.0",
"markdown-it-async": "^2.2.0",
"markdown-it-attrs": "^4.3.1",
"markdown-it-attrs": "4.3.1",
"markdown-it-cjk-friendly": "^2.0.2",
"markdown-it-container": "^4.0.0",
"markdown-it-emoji": "^3.0.0",
"markdown-it-mathjax3": "^4.3.2",
"minimist": "^1.2.8",
"nanoid": "^5.1.9",
"obug": "^2.1.1",
"ora": "^9.3.0",
"nanoid": "^5.1.16",
"obug": "^2.1.3",
"ora": "^9.4.1",
"oxc-minify": "^0.98.0",
"p-map": "^7.0.4",
"package-directory": "^8.2.0",
"path-to-regexp": "^6.3.0",
"picocolors": "^1.1.1",
"picomatch": "^4.0.4",
"playwright-chromium": "^1.59.1",
"playwright-chromium": "^1.61.1",
"polka": "^1.0.0-next.28",
"postcss": "^8.5.6",
"postcss-selector-parser": "^7.1.1",
"prettier": "^3.8.3",
"postcss-selector-parser": "^7.1.4",
"prettier": "^3.9.0",
"prompts": "^2.4.2",
"punycode": "^2.3.1",
"rollup": "^4.60.1",
"rollup": "^4.62.2",
"rollup-plugin-dts": "6.1.1",
"rollup-plugin-esbuild": "^6.2.1",
"semver": "^7.7.4",
"semver": "^7.8.5",
"simple-git-hooks": "^2.13.1",
"sirv": "^3.0.2",
"sitemap": "^9.0.1",
"tinyglobby": "^0.2.16",
"typescript": "^6.0.3",
"tinyglobby": "^0.2.17",
"typescript": "^5.9.3",
"vitest": "4.0.0-beta.4",
"vue-tsc": "^3.2.6",
"wait-on": "^9.0.5"
"vue-tsc": "^3.3.5",
"wait-on": "^9.0.10"
},
"peerDependencies": {
"markdown-it-mathjax3": "^4",

File diff suppressed because it is too large Load Diff

@ -1,10 +1,10 @@
import { spawn } from 'node:child_process'
import { readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
import c from 'picocolors'
import prompts from 'prompts'
import { execa } from 'execa'
import semver from 'semver'
const { version: currentVersion } = createRequire(import.meta.url)(
@ -19,7 +19,24 @@ const tags = ['latest', 'next']
const dir = fileURLToPath(new URL('.', import.meta.url))
const inc = (i) => _inc(currentVersion, i)
const run = (bin, args, opts = {}) =>
execa(bin, args, { stdio: 'inherit', ...opts })
new Promise((resolve, reject) => {
const child = spawn(bin, args, {
stdio: 'inherit',
shell: process.platform === 'win32',
...opts
})
child.on('error', reject)
child.on('close', (code, signal) => {
if (code === 0) {
resolve()
} else if (signal) {
reject(new Error(`${bin} exited with signal ${signal}`))
} else {
reject(new Error(`${bin} exited with code ${code}`))
}
})
})
const step = (msg) => console.log(c.cyan(msg))
async function main() {

@ -15,45 +15,13 @@ import {
createTitle,
inBrowser,
resolveSiteDataByRoute,
type PageData,
type SiteData
type SiteData,
type VitePressData
} from '../shared'
import type { Route } from './router'
export const dataSymbol: InjectionKey<VitePressData> = Symbol()
export interface VitePressData<T = any> {
/**
* Site-level metadata
*/
site: Ref<SiteData<T>>
/**
* themeConfig from .vitepress/config.js
*/
theme: Ref<T>
/**
* Page-level metadata
*/
page: Ref<PageData>
/**
* page frontmatter data
*/
frontmatter: Ref<PageData['frontmatter']>
/**
* dynamic route params
*/
params: Ref<PageData['params']>
title: Ref<string>
description: Ref<string>
lang: Ref<string>
dir: Ref<string>
localeIndex: Ref<string>
isDark: Ref<boolean>
/**
* Current location hash
*/
hash: Ref<string>
}
export type { VitePressData } from '../shared'
// site data is a singleton
export const siteDataRef: Ref<SiteData> = shallowRef(

@ -2,7 +2,7 @@
// so the user can do `import { useRoute, useData } from 'vitepress'`
// generic types
export type { VitePressData } from './app/data'
export type { VitePressData } from './shared'
export type { Route, Router } from './app/router'
// theme types

@ -285,7 +285,7 @@ function scrollToSelectedResult() {
})
}
onKeyStroke('ArrowUp', (event) => {
function selectPreviousResult(event: KeyboardEvent) {
event.preventDefault()
selectedIndex.value--
if (selectedIndex.value < 0) {
@ -293,9 +293,9 @@ onKeyStroke('ArrowUp', (event) => {
}
disableMouseOver.value = true
scrollToSelectedResult()
})
}
onKeyStroke('ArrowDown', (event) => {
function selectNextResult(event: KeyboardEvent) {
event.preventDefault()
selectedIndex.value++
if (selectedIndex.value >= results.value.length) {
@ -303,6 +303,32 @@ onKeyStroke('ArrowDown', (event) => {
}
disableMouseOver.value = true
scrollToSelectedResult()
}
function isMacCtrlShortcut(event: KeyboardEvent) {
return (
event.ctrlKey &&
!event.altKey &&
!event.metaKey &&
!event.shiftKey &&
document.documentElement.classList.contains('mac')
)
}
onKeyStroke('ArrowUp', selectPreviousResult)
onKeyStroke('ArrowDown', selectNextResult)
onKeyStroke(['p', 'P'], (event) => {
if (isMacCtrlShortcut(event)) {
selectPreviousResult(event)
}
})
onKeyStroke(['n', 'N'], (event) => {
if (isMacCtrlShortcut(event)) {
selectNextResult(event)
}
})
const router = useRouter()

@ -1,9 +1,12 @@
import { computed } from 'vue'
import type { DefaultTheme } from 'vitepress/theme'
import type { VitePressData } from '../../app/data'
import { ensureStartingSlash } from '../support/utils'
import { useData } from './data'
export function useLangs({ correspondingLink = false } = {}) {
const { site, localeIndex, page, theme, hash } = useData()
const data = useData()
const { site, localeIndex } = data
const currentLang = computed(() => ({
label: site.value.locales[localeIndex.value]?.label,
link:
@ -17,15 +20,13 @@ export function useLangs({ correspondingLink = false } = {}) {
? []
: {
text: value.label,
link:
normalizeLink(
value.link || (key === 'root' ? '/' : `/${key}/`),
theme.value.i18nRouting !== false && correspondingLink,
page.value.relativePath.slice(
currentLang.value.link.length - 1
),
!site.value.cleanUrls
) + hash.value,
link: resolveLocaleLink(
data,
key,
value.link || (key === 'root' ? '/' : `/${key}/`),
currentLang.value.link,
correspondingLink
),
lang: value.lang,
dir: value.dir
}
@ -35,6 +36,30 @@ export function useLangs({ correspondingLink = false } = {}) {
return { localeLinks, currentLang }
}
export function resolveLocaleLink(
data: VitePressData<DefaultTheme.Config>,
targetLocale: string,
targetLink: string,
currentLink: string,
correspondingLink: boolean
) {
const { site, page, theme, hash } = data
const i18nRouting = theme.value.i18nRouting
if (correspondingLink && typeof i18nRouting === 'function') {
return i18nRouting(data, hash.value, targetLocale)
}
return (
normalizeLink(
targetLink,
i18nRouting !== false && correspondingLink,
page.value.relativePath.slice(currentLink.length - 1),
!site.value.cleanUrls
) + hash.value
)
}
function normalizeLink(
link: string,
addPath: boolean,

@ -201,8 +201,7 @@ function buildTree(
const result: DefaultTheme.OutlineItem[] = []
const stack: (
| DefaultTheme.OutlineItem
| { level: number; shouldIgnore: true }
DefaultTheme.OutlineItem | { level: number; shouldIgnore: true }
)[] = []
data.forEach((item) => {

@ -560,26 +560,30 @@
/* prettier-ignore */
:is(.vp-external-link-icon, .vp-doc a[href*='://'], .vp-doc a[target='_blank']):not(:is(.no-icon, svg a, :has(img, svg)))::after {
display: inline-block;
margin-top: -1px;
display: inline;
margin-left: 4px;
width: 11px;
height: 11px;
padding-left: 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);
-webkit-mask-position: center;
mask-position: center;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 11px 11px;
mask-size: 11px 11px;
/*rtl:raw:transform: scaleX(-1);*/
}
.vp-external-link-icon::after {
content: '';
content: '\2060';
}
/* prettier-ignore */
.external-link-icon-enabled :is(.vp-doc a[href*='://'], .vp-doc a[target='_blank']):not(:is(.no-icon, svg a, :has(img, svg)))::after {
content: '';
content: '\2060';
color: currentColor;
}

@ -41,6 +41,10 @@
--docsearch-modal-shadow: none;
}
:is(.DocSearch-Container, .DocSearch-Sidepanel) svg {
overflow: visible;
}
.DocSearch-AskAiScreen-RelatedSources-Item-Link {
padding: 8px 12px 8px 10px;
}

@ -7,7 +7,8 @@ import {
loadConfigFromFile,
mergeConfig as mergeViteConfig,
normalizePath,
type ConfigEnv
type ConfigEnv,
type UserConfig as ViteUserConfig
} from 'vite'
import { DEFAULT_THEME_PATH } from './alias'
import type { DefaultTheme } from './defaultTheme'
@ -22,6 +23,7 @@ import {
type HeadConfig,
type SiteData
} from './shared'
import type { MarkdownOptions } from './markdown/markdown'
import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig'
import { glob } from './utils/glob'
@ -39,8 +41,7 @@ export type UserConfigFn<ThemeConfig> = (
env: ConfigEnv
) => Awaitable<UserConfig<ThemeConfig>>
export type UserConfigExport<ThemeConfig> =
| Awaitable<UserConfig<ThemeConfig>>
| UserConfigFn<ThemeConfig>
Awaitable<UserConfig<ThemeConfig>> | UserConfigFn<ThemeConfig>
/**
* Type config helper
@ -55,8 +56,7 @@ export type AdditionalConfigFn<ThemeConfig> = (
env: ConfigEnv
) => Awaitable<AdditionalConfig<ThemeConfig>>
export type AdditionalConfigExport<ThemeConfig> =
| Awaitable<AdditionalConfig<ThemeConfig>>
| AdditionalConfigFn<ThemeConfig>
Awaitable<AdditionalConfig<ThemeConfig>> | AdditionalConfigFn<ThemeConfig>
/**
* Type config helper for additional/locale-specific config
@ -288,10 +288,14 @@ async function resolveConfigExtends(
return resolved
}
export function mergeConfig(a: UserConfig, b: UserConfig, isRoot = true) {
export function mergeConfig<A extends object, B extends object>(
a: A,
b: B,
isRoot = true
): A & B {
const merged: Record<string, any> = { ...a }
for (const key in b) {
const value = b[key as keyof UserConfig]
const value = b[key]
if (value == null) {
continue
}
@ -302,7 +306,12 @@ export function mergeConfig(a: UserConfig, b: UserConfig, isRoot = true) {
}
if (isObject(existing) && isObject(value)) {
if (isRoot && key === 'vite') {
merged[key] = mergeViteConfig(existing, value)
merged[key] = mergeViteConfig(
existing as ViteUserConfig,
value as ViteUserConfig
)
} else if (isRoot && key === 'markdown') {
merged[key] = mergeMarkdownConfig(existing, value as MarkdownOptions)
} else {
merged[key] = mergeConfig(existing, value, false)
}
@ -310,6 +319,19 @@ export function mergeConfig(a: UserConfig, b: UserConfig, isRoot = true) {
}
merged[key] = value
}
return merged as A & B
}
function mergeMarkdownConfig(a: MarkdownOptions, b: MarkdownOptions) {
const merged = mergeConfig(a, b, false)
const baseConfig = a.config
const extendedConfig = b.config
if (baseConfig && extendedConfig) {
merged.config = async (md) => {
await baseConfig(md)
await extendedConfig(md)
}
}
return merged
}

@ -19,6 +19,20 @@ export const linkPlugin = (
base: string,
slugify: (str: string) => string
) => {
md.core.ruler.after('inline', 'vitepress_link_lines', (state) => {
for (const token of state.tokens) {
if (token.type !== 'inline' || !token.children || !token.map) continue
const line = token.map[0] + 1
for (const child of token.children) {
if (child.type === 'link_open') {
child.meta ??= {}
child.meta.vpLine = line
}
}
}
})
md.renderer.rules.link_open = (
tokens,
idx,
@ -41,7 +55,7 @@ export const linkPlugin = (
})
// catch localhost links as dead link
if (url.replace(EXTERNAL_URL_RE, '').startsWith('//localhost:')) {
pushLink(url, env)
pushLink(url, env, token.meta?.vpLine)
}
hrefAttr[1] = url
} else {
@ -58,7 +72,7 @@ export const linkPlugin = (
// skip links to files (other than html/md)
treatAsHtml(pathname)
) {
normalizeHref(hrefAttr, env)
normalizeHref(hrefAttr, env, token.meta?.vpLine)
} else if (url.startsWith('#')) {
hrefAttr[1] = decodeURI(normalizeHash(hrefAttr[1]))
}
@ -75,7 +89,11 @@ export const linkPlugin = (
return self.renderToken(tokens, idx, options)
}
function normalizeHref(hrefAttr: [string, string], env: MarkdownEnv) {
function normalizeHref(
hrefAttr: [string, string],
env: MarkdownEnv,
line?: number
) {
let url = hrefAttr[1]
const indexMatch = url.match(indexRE)
@ -106,7 +124,7 @@ export const linkPlugin = (
}
// export it for existence check
pushLink(url.replace(/\.html$/, ''), env)
pushLink(url.replace(/\.html$/, ''), env, line)
// markdown-it encodes the uri
hrefAttr[1] = decodeURI(url)
@ -116,8 +134,12 @@ export const linkPlugin = (
return str ? encodeURI('#' + slugify(decodeURI(str).slice(1))) : ''
}
function pushLink(link: string, env: MarkdownEnv) {
function pushLink(link: string, env: MarkdownEnv, line?: number) {
const links = env.links || (env.links = [])
links.push(link)
if (line != null) {
const linkLines = env.linkLines || (env.linkLines = [])
linkLines[links.length - 1] = line
}
}
}

@ -28,7 +28,7 @@ const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 })
export interface MarkdownCompileResult {
vueSrc: string
pageData: PageData
deadLinks: { url: string; file: string }[]
deadLinks: { url: string; file: string; line?: number }[]
includes: string[]
}
@ -151,17 +151,24 @@ export async function createMarkdownToVueRenderFn(
}
const html = await md.renderAsync(src, env)
const {
content,
frontmatter = {},
headers = [],
linkLines = [],
links = [],
sfcBlocks,
title = ''
} = env
const contentLineOffset = countLineBreaks(
content && src.endsWith(content) ? src.slice(0, -content.length) : ''
)
// validate data.links
const deadLinks: MarkdownCompileResult['deadLinks'] = []
const recordDeadLink = (url: string) => {
deadLinks.push({ url, file: fileOrig })
const recordDeadLink = (url: string, line?: number) => {
deadLinks.push(
line == null ? { url, file: fileOrig } : { url, file: fileOrig, line }
)
}
function shouldIgnoreDeadLink(url: string) {
@ -185,7 +192,12 @@ export async function createMarkdownToVueRenderFn(
if (links && siteConfig?.ignoreDeadLinks !== true) {
const dir = path.dirname(file)
for (let url of links) {
for (const [index, rawUrl] of links.entries()) {
let url = rawUrl
const line =
linkLines[index] == null
? undefined
: linkLines[index] + contentLineOffset
const { pathname } = new URL(url, 'http://a.com')
if (!treatAsHtml(pathname)) continue
@ -207,7 +219,7 @@ export async function createMarkdownToVueRenderFn(
!fs.existsSync(path.resolve(dir, publicDir, `${resolved}.html`)) &&
!shouldIgnoreDeadLink(url)
) {
recordDeadLink(url)
recordDeadLink(url, line)
}
}
}
@ -332,6 +344,10 @@ const inferDescription = (frontmatter: Record<string, any>) => {
return (head && getHeadMetaContent(head, 'description')) || ''
}
function countLineBreaks(str: string) {
return str.match(/\r?\n/g)?.length ?? 0
}
const getHeadMetaContent = (head: HeadConfig[], name: string) => {
if (!head || !head.length) {
return undefined

@ -422,14 +422,15 @@ function logDeadLinks(
devMode = false
) {
const logged = new Set<string>()
deadLinks.forEach(({ url, file }, i) => {
const key = `${file}:::${url}`
deadLinks.forEach(({ url, file, line }, i) => {
const location = line == null ? file : `${file}:${line}`
const key = `${location}:::${url}`
if (logged.has(key)) return
logged.add(key)
const prefix = '\n'.repeat(i === 0 ? (devMode ? 1 : 2) : 0)
logger.warn(
c.yellow(
`${prefix}(!) Found dead link ${c.cyan(url)} in file ${c.white(c.dim(file))}`
`${prefix}(!) Found dead link ${c.cyan(url)} in file ${c.white(c.dim(location))}`
)
)
})

@ -190,8 +190,7 @@ export interface UserConfig<
* @experimental
*/
additionalConfig?:
| AdditionalConfigDict<ThemeConfig>
| AdditionalConfigLoader<ThemeConfig>
AdditionalConfigDict<ThemeConfig> | AdditionalConfigLoader<ThemeConfig>
}
export interface SiteConfig<ThemeConfig = any> extends Pick<

@ -16,6 +16,7 @@ export type {
PageData,
PageDataPayload,
SiteData,
VitePressData,
SSGContext,
AdditionalConfig,
AdditionalConfigDict,

@ -1,7 +1,7 @@
import type { Options as _MiniSearchOptions } from 'minisearch'
import type { DocSearchProps } from './docsearch.js'
import type { LocalSearchTranslations } from './local-search.js'
import type { Header, PageData } from './shared.js'
import type { Header, PageData, VitePressData } from './shared.js'
export namespace DefaultTheme {
export interface Config {
@ -136,11 +136,13 @@ export namespace DefaultTheme {
carbonAds?: CarbonAdsOptions
/**
* Changing locale when current url is `/foo` will redirect to `/locale/foo`.
* Changing locale when current url is `/foo` redirects to `/locale/foo`.
* Set to `false` to disable this behavior, or provide a function to
* customize the target locale link.
*
* @default true
*/
i18nRouting?: boolean
i18nRouting?: boolean | I18nRouting
/**
* Show external link icon in Markdown links.
@ -155,6 +157,12 @@ export namespace DefaultTheme {
notFound?: NotFoundOptions
}
export type I18nRouting = (
data: VitePressData<Config>,
hash: string,
targetLocale: string
) => string
// nav -----------------------------------------------------------------------
export type NavItem = NavItemComponent | NavItemWithLink | NavItemWithChildren

41
types/shared.d.ts vendored

@ -1,5 +1,6 @@
// types shared between server and client
import type { UseDarkOptions } from '@vueuse/core'
import type { Ref } from 'vue'
import type { SSRContext } from 'vue/server-renderer'
export type { DefaultTheme } from './default-theme.js'
@ -143,13 +144,44 @@ export interface SiteData<ThemeConfig = any> {
prefetchLinks: boolean
}
additionalConfig?:
| AdditionalConfigDict<ThemeConfig>
| AdditionalConfigLoader<ThemeConfig>
AdditionalConfigDict<ThemeConfig> | AdditionalConfigLoader<ThemeConfig>
}
export interface VitePressData<T = any> {
/**
* Site-level metadata
*/
site: Ref<SiteData<T>>
/**
* themeConfig from .vitepress/config.js
*/
theme: Ref<T>
/**
* Page-level metadata
*/
page: Ref<PageData>
/**
* page frontmatter data
*/
frontmatter: Ref<PageData['frontmatter']>
/**
* dynamic route params
*/
params: Ref<PageData['params']>
title: Ref<string>
description: Ref<string>
lang: Ref<string>
dir: Ref<string>
localeIndex: Ref<string>
isDark: Ref<boolean>
/**
* Current location hash
*/
hash: Ref<string>
}
export type HeadConfig =
| [string, Record<string, string>]
| [string, Record<string, string>, string]
[string, Record<string, string>] | [string, Record<string, string>, string]
export interface PageDataPayload {
path: string
@ -223,6 +255,7 @@ export interface MarkdownEnv {
relativePath: string
cleanUrls: boolean
links?: string[]
linkLines?: number[]
includes?: string[]
realPath?: string
localeIndex?: string

Loading…
Cancel
Save