Merge branch 'main' into main

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

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

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

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

@ -1,4 +1,4 @@
# VitePress (alpha) 📝💨
# VitePress (beta) 📝💨
[![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)
@ -8,7 +8,7 @@
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

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

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

@ -18,7 +18,7 @@ All **static** path references, including absolute paths, should be based on you
## 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`.

@ -566,7 +566,12 @@ It also supports [line highlighting](#line-highlighting-in-code-blocks):
<<< @/snippets/snippet.js{2}
::: tip
The value of `@` corresponds to the source root. By default it's the VitePress project root, unless `srcDir` is configured.
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:
@ -691,7 +696,13 @@ You can also [import snippets](#import-code-snippets) in code groups:
## Markdown File Inclusion
You can include a markdown file in another markdown file like this:
You can include a markdown file in another markdown file.
::: tip
You can also prefix the markdown path with `@`, it will act as the source root. By default it's the VitePress project root, unless `srcDir` is configured.
:::
For example, you can include a relative markdown file using this:
**Input**

@ -214,7 +214,9 @@ export default {
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>'
},
link: '...'
link: '...',
// You can include a custom label for accessibility too (optional but recommended):
ariaLabel: 'cool link'
}
]
}
@ -225,6 +227,7 @@ export default {
interface SocialLink {
icon: SocialLinkIcon
link: string
ariaLabel?: string
}
type SocialLinkIcon =

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

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

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

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

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

File diff suppressed because it is too large Load Diff

@ -1,13 +1,15 @@
import { readFileSync, writeFileSync } from 'fs'
import { resolve } from 'path'
import { fileURLToPath } from 'url'
import { createRequire } from 'module'
import c from 'picocolors'
import prompts from 'prompts'
import { execa } from 'execa'
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 versionIncrements = ['patch', 'minor', 'major']

@ -303,6 +303,8 @@ function handleHMR(route: Route): void {
function shouldHotReload(payload: PageDataPayload): boolean {
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
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1
types/shared.d.ts vendored

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

Loading…
Cancel
Save