Merge branch 'main' into doc-zh

pull/2249/head
Xavi Lee 2 years ago committed by GitHub
commit e624a09d4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,3 +1,21 @@
# [1.0.0-rc.34](https://github.com/vuejs/vitepress/compare/v1.0.0-rc.33...v1.0.0-rc.34) (2023-12-30)
### Bug Fixes
- **build:** clear cache after build ([9568fea](https://github.com/vuejs/vitepress/commit/9568fea8fc50e625c8ef27c588eca3dbe5a44e81)), closes [#3363](https://github.com/vuejs/vitepress/issues/3363)
- **default-theme:** remove use of reactify for search i18n ([8687b86](https://github.com/vuejs/vitepress/commit/8687b86e1e00ae39ff9c8173aef04eb8a9cda0a8))
- print errors when importing an invalid dynamic route ([#3201](https://github.com/vuejs/vitepress/issues/3201)) ([6d89a08](https://github.com/vuejs/vitepress/commit/6d89a08cb76674f4d92f54218f8af5624bcf4c47))
- remove double title from home pages ([9f1f04e](https://github.com/vuejs/vitepress/commit/9f1f04e00a9722ec7369941c40d3d8ad86f61d35)), closes [#3375](https://github.com/vuejs/vitepress/issues/3375)
- **theme/i18n:** support customizing dark mode switch titles ([#3311](https://github.com/vuejs/vitepress/issues/3311)) ([50c9758](https://github.com/vuejs/vitepress/commit/50c9758d3fa1b60aad5399a0db890644ac44a522))
### Features
- support custom image lazy loading ([#3346](https://github.com/vuejs/vitepress/issues/3346)) ([55be3f1](https://github.com/vuejs/vitepress/commit/55be3f14d79eb578c9aa2e3bc7a90205c910005d))
- support dir in frontmatter ([#3353](https://github.com/vuejs/vitepress/issues/3353)) ([203446d](https://github.com/vuejs/vitepress/commit/203446d69193483a46e1082bba5fbad0e35966fb))
- **theme/i18n:** allow customizing sponsor link's text ([#3276](https://github.com/vuejs/vitepress/issues/3276)) ([9c20e3b](https://github.com/vuejs/vitepress/commit/9c20e3b5f80e4197c14c20fa751ec3c8c8219e8e))
- **theme:** allow using VPBadge classes in sidebar ([#3391](https://github.com/vuejs/vitepress/issues/3391)) ([50a774e](https://github.com/vuejs/vitepress/commit/50a774ea7c70bb200e12c176d6691ab7144a73f9))
- **theme:** new design for local nav and global header ([#3359](https://github.com/vuejs/vitepress/issues/3359)) ([d10bf42](https://github.com/vuejs/vitepress/commit/d10bf42c2632f1aacb905bc01b36274e9004cbd9))
# [1.0.0-rc.33](https://github.com/vuejs/vitepress/compare/v1.0.0-rc.32...v1.0.0-rc.33) (2023-12-26)
### Features

@ -86,6 +86,11 @@ const sidebar: DefaultTheme.Config['sidebar'] = {
export default defineConfig({
title: 'Example',
description: 'An example app using VitePress.',
markdown: {
image: {
lazyLoading: true
}
},
themeConfig: {
sidebar,
search: {

@ -196,3 +196,7 @@ export default config
## Markdown File Inclusion with Range without End
<!--@include: ./foo.md{6,}-->
## Image Lazy Loading
![vitepress logo](/vitepress.png)

@ -65,7 +65,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(35)
expect(count).toBe(36)
})
})
@ -280,3 +280,10 @@ describe('Markdown File Inclusion', () => {
expect(await p.textContent()).not.toContain('title')
})
})
describe('Image Lazy Loading', () => {
test('render loading="lazy" in the <img> tag', async () => {
const img = page.locator('#image-lazy-loading + p img')
expect(await img.getAttribute('loading')).toBe('lazy')
})
})

@ -847,6 +847,21 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
| $\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} = \vec{\mathbf{0}}$ | curl of $\vec{\mathbf{E}}$ is proportional to the rate of change of $\vec{\mathbf{B}}$ |
| $\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} = \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} = 4 \pi \rho$ | _wha?_ |
## Image Lazy Loading
You can enable lazy loading for each image added via markdown by setting `lazyLoading` to `true` in your config file:
```js
export default {
markdown: {
image: {
// image lazy loading is disabled by default
lazyLoading: true
}
}
}
```
## Advanced Configuration
VitePress uses [markdown-it](https://github.com/markdown-it/markdown-it) as the Markdown renderer. A lot of the extensions above are implemented via custom plugins. You can further customize the `markdown-it` instance using the `markdown` option in `.vitepress/config.js`:

@ -406,6 +406,20 @@ export interface DocFooter {
Can be used to customize the dark mode switch label. This label is only displayed in the mobile view.
## lightModeSwitchTitle
- Type: `string`
- Default: `Switch to light theme`
Can be used to customize the light mode switch title that appears on hovering.
## darkModeSwitchTitle
- Type: `string`
- Default: `Switch to dark theme`
Can be used to customize the dark mode switch title that appears on hovering.
## sidebarMenuLabel
- Type: `string`

@ -24,6 +24,62 @@ export default {
}
```
:::details Dynamic (Async) Config
If you need to dynamically generate the config, you can also default export a function. For example:
```ts
import { defineConfig } from 'vitepress'
export default async () => defineConfig({
const posts = await (await fetch('https://my-cms.com/blog-posts')).json()
return {
// app level config options
lang: 'en-US',
title: 'VitePress',
description: 'Vite & Vue powered static site generator.',
// theme level config options
themeConfig: {
sidebar: [
...posts.map((post) => ({
text: post.name,
link: `/posts/${post.name}`
}))
]
}
}
})
```
You can also use top-level `await`. For example:
```ts
import { defineConfig } from 'vitepress'
const posts = await (await fetch('https://my-cms.com/blog-posts')).json()
export default defineConfig({
// app level config options
lang: 'en-US',
title: 'VitePress',
description: 'Vite & Vue powered static site generator.',
// theme level config options
themeConfig: {
sidebar: [
...posts.map((post) => ({
text: post.name,
link: `/posts/${post.name}`
}))
]
}
})
```
:::
### Config Intellisense
Using the `defineConfig` helper will provide TypeScript-powered intellisense for config options. Assuming your IDE supports it, this should work in both JavaScript and TypeScript.

@ -1,9 +1,9 @@
{
"name": "vitepress",
"version": "1.0.0-rc.33",
"version": "1.0.0-rc.34",
"description": "Vite & Vue powered static site generator",
"type": "module",
"packageManager": "pnpm@8.12.1",
"packageManager": "pnpm@8.13.1",
"main": "dist/node/index.js",
"types": "types/index.d.ts",
"exports": {
@ -93,18 +93,19 @@
"@docsearch/css": "^3.5.2",
"@docsearch/js": "^3.5.2",
"@types/markdown-it": "^13.0.7",
"@vitejs/plugin-vue": "^5.0.0",
"@vitejs/plugin-vue": "^5.0.2",
"@vue/devtools-api": "^6.5.1",
"@vueuse/core": "^10.7.0",
"@vueuse/integrations": "^10.7.0",
"@vueuse/core": "^10.7.1",
"@vueuse/integrations": "^10.7.1",
"focus-trap": "^7.5.4",
"mark.js": "8.11.1",
"minisearch": "^6.3.0",
"mrmime": "^2.0.0",
"shikiji": "^0.9.12",
"shikiji-transformers": "^0.9.12",
"shikiji": "^0.9.15",
"shikiji-core": "^0.9.15",
"shikiji-transformers": "^0.9.15",
"vite": "^5.0.10",
"vue": "^3.4.0-rc.2"
"vue": "^3.4.3"
},
"peerDependencies": {
"markdown-it-mathjax3": "^4.3.2",
@ -144,16 +145,16 @@
"@types/markdown-it-emoji": "^2.0.4",
"@types/micromatch": "^4.0.6",
"@types/minimist": "^1.2.5",
"@types/node": "^20.10.5",
"@types/node": "^20.10.6",
"@types/postcss-prefix-selector": "^1.16.3",
"@types/prompts": "^2.4.9",
"@vue/shared": "^3.3.13",
"@vue/shared": "^3.4.3",
"chokidar": "^3.5.3",
"compression": "^1.7.4",
"conventional-changelog-cli": "^4.1.0",
"cross-spawn": "^7.0.3",
"debug": "^4.3.4",
"esbuild": "^0.19.10",
"esbuild": "^0.19.11",
"escape-html": "^1.0.3",
"execa": "^8.0.1",
"fast-glob": "^3.3.2",
@ -174,7 +175,7 @@
"nanoid": "^5.0.4",
"npm-run-all": "^4.1.5",
"ora": "^8.0.1",
"p-map": "^7.0.0",
"p-map": "^7.0.1",
"path-to-regexp": "^6.2.1",
"picocolors": "^1.0.0",
"pkg-dir": "^8.0.0",
@ -185,18 +186,17 @@
"prompts": "^2.4.2",
"punycode": "^2.3.1",
"rimraf": "^5.0.5",
"rollup": "^4.9.1",
"rollup": "^4.9.2",
"rollup-plugin-dts": "^6.1.0",
"rollup-plugin-esbuild": "^6.1.0",
"semver": "^7.5.4",
"shikiji-core": "^0.9.12",
"simple-git-hooks": "^2.9.0",
"sirv": "^2.0.4",
"sitemap": "^7.1.1",
"supports-color": "^9.4.0",
"typescript": "^5.3.3",
"vitest": "^1.1.0",
"vue-tsc": "^1.8.26",
"vue-tsc": "^1.8.27",
"wait-on": "^7.2.0"
},
"simple-git-hooks": {

File diff suppressed because it is too large Load Diff

@ -89,7 +89,7 @@ export function initData(route: Route): VitePressData {
frontmatter: computed(() => route.data.frontmatter),
params: computed(() => route.data.params),
lang: computed(() => site.value.lang),
dir: computed(() => site.value.dir),
dir: computed(() => route.data.frontmatter.dir || site.value.dir || 'ltr'),
localeIndex: computed(() => site.value.localeIndex || 'root'),
title: computed(() => {
return createTitle(site.value, route.data)

@ -14,7 +14,7 @@ withDefaults(defineProps<Props>(), {
</span>
</template>
<style scoped>
<style>
.VPBadge {
display: inline-block;
margin-left: 2px;
@ -27,6 +27,17 @@ withDefaults(defineProps<Props>(), {
transform: translateY(-2px);
}
.VPBadge.small {
padding: 0 6px;
line-height: 18px;
font-size: 10px;
transform: translateY(-8px);
}
.VPDocFooter .VPBadge {
display: none;
}
.vp-doc h1 > .VPBadge {
margin-top: 4px;
vertical-align: top;

@ -5,7 +5,6 @@ import { useData } from '../composables/data'
import { useSidebar } from '../composables/sidebar'
import VPDocAside from './VPDocAside.vue'
import VPDocFooter from './VPDocFooter.vue'
import VPDocOutlineDropdown from './VPDocOutlineDropdown.vue'
const { theme } = useData()
@ -43,7 +42,6 @@ const pageName = computed(() =>
<div class="content">
<div class="content-container">
<slot name="doc-before" />
<VPDocOutlineDropdown />
<main class="main">
<Content
class="vp-doc"
@ -70,16 +68,6 @@ const pageName = computed(() =>
width: 100%;
}
.VPDoc .VPDocOutlineDropdown {
display: none;
}
@media (min-width: 960px) and (max-width: 1279px) {
.VPDoc .VPDocOutlineDropdown {
display: block;
}
}
@media (min-width: 768px) {
.VPDoc {
padding: 48px 32px 128px;
@ -88,7 +76,7 @@ const pageName = computed(() =>
@media (min-width: 960px) {
.VPDoc {
padding: 32px 32px 0;
padding: 48px 32px 0;
}
.VPDoc:not(.has-sidebar) .container {
@ -147,7 +135,7 @@ const pageName = computed(() =>
.aside-container {
position: fixed;
top: 0;
padding-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 32px);
padding-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 48px);
width: 224px;
height: 100vh;
overflow-x: hidden;
@ -171,7 +159,7 @@ const pageName = computed(() =>
.aside-content {
display: flex;
flex-direction: column;
min-height: calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 32px));
min-height: calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px));
padding-bottom: 32px;
}

@ -80,9 +80,8 @@ useActiveAnchor(container, marker)
}
.outline-title {
letter-spacing: 0.4px;
line-height: 28px;
font-size: 13px;
line-height: 32px;
font-size: 14px;
font-weight: 600;
}
</style>

@ -1,85 +0,0 @@
<script setup lang="ts">
import { ref, shallowRef } from 'vue'
import { useData } from '../composables/data'
import { getHeaders, 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()
const open = ref(false)
onContentUpdated(() => {
open.value = false
})
const headers = shallowRef<MenuItem[]>([])
onContentUpdated(() => {
headers.value = getHeaders(
frontmatter.value.outline ?? theme.value.outline
)
})
</script>
<template>
<div class="VPDocOutlineDropdown" v-if="headers.length > 0">
<button @click="open = !open" :class="{ open }">
{{ resolveTitle(theme) }}
<VPIconChevronRight class="icon" />
</button>
<div class="items" v-if="open">
<VPDocOutlineItem :headers="headers" />
</div>
</div>
</template>
<style scoped>
.VPDocOutlineDropdown {
margin-bottom: 48px;
}
.VPDocOutlineDropdown button {
display: block;
font-size: 14px;
font-weight: 500;
line-height: 24px;
border: 1px solid var(--vp-c-border);
padding: 4px 12px;
color: var(--vp-c-text-2);
background-color: var(--vp-c-default-soft);
border-radius: 8px;
transition: color 0.5s;
}
.VPDocOutlineDropdown button:hover {
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.VPDocOutlineDropdown button.open {
color: var(--vp-c-text-1);
}
.icon {
display: inline-block;
vertical-align: middle;
width: 16px;
height: 16px;
fill: currentColor;
}
:deep(.outline-link) {
font-size: 14px;
font-weight: 400;
}
.open > .icon {
transform: rotate(90deg);
}
.items {
margin-top: 12px;
border-left: 1px solid var(--vp-c-divider);
}
</style>

@ -14,7 +14,7 @@ function onClick({ target: el }: Event) {
</script>
<template>
<ul :class="root ? 'root' : 'nested'">
<ul class="VPDocOutlineItem" :class="root ? 'root' : 'nested'">
<li v-for="{ children, link, title } in headers">
<a class="outline-link" :href="link" @click="onClick" :title="title">{{ title }}</a>
<template v-if="children?.length">
@ -31,18 +31,20 @@ function onClick({ target: el }: Event) {
}
.nested {
padding-right: 16px;
padding-left: 16px;
}
.outline-link {
display: block;
line-height: 28px;
line-height: 32px;
font-size: 14px;
font-weight: 400;
color: var(--vp-c-text-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.5s;
font-weight: 400;
}
.outline-link:hover,

@ -1,9 +1,10 @@
<script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core'
import { onContentUpdated } from 'vitepress'
import { computed, onMounted, ref, shallowRef } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useData } from '../composables/data'
import { getHeaders, type MenuItem } from '../composables/outline'
import { useLocalNav } from '../composables/local-nav'
import { getHeaders } from '../composables/outline'
import { useSidebar } from '../composables/sidebar'
import VPLocalNavOutlineDropdown from './VPLocalNavOutlineDropdown.vue'
import VPIconAlignLeft from './icons/VPIconAlignLeft.vue'
@ -18,9 +19,9 @@ defineEmits<{
const { theme, frontmatter } = useData()
const { hasSidebar } = useSidebar()
const { headers } = useLocalNav()
const { y } = useWindowScroll()
const headers = shallowRef<MenuItem[]>([])
const navHeight = ref(0)
onMounted(() => {
@ -36,37 +37,44 @@ onContentUpdated(() => {
})
const empty = computed(() => {
return headers.value.length === 0 && !hasSidebar.value
return headers.value.length === 0
})
const emptyAndNoSidebar = computed(() => {
return empty.value && !hasSidebar.value
})
const classes = computed(() => {
return {
VPLocalNav: true,
fixed: empty.value,
'reached-top': y.value >= navHeight.value
'has-sidebar': hasSidebar.value,
empty: empty.value,
fixed: emptyAndNoSidebar.value
}
})
</script>
<template>
<div
v-if="frontmatter.layout !== 'home' && (!empty || y >= navHeight)"
v-if="frontmatter.layout !== 'home' && (!emptyAndNoSidebar || y >= navHeight)"
:class="classes"
>
<button
v-if="hasSidebar"
class="menu"
:aria-expanded="open"
aria-controls="VPSidebarNav"
@click="$emit('open-menu')"
>
<VPIconAlignLeft class="menu-icon" />
<span class="menu-text">
{{ theme.sidebarMenuLabel || 'Menu' }}
</span>
</button>
<VPLocalNavOutlineDropdown :headers="headers" :navHeight="navHeight" />
<div class="container">
<button
v-if="hasSidebar"
class="menu"
:aria-expanded="open"
aria-controls="VPSidebarNav"
@click="$emit('open-menu')"
>
<VPIconAlignLeft class="menu-icon" />
<span class="menu-text">
{{ theme.sidebarMenuLabel || 'Menu' }}
</span>
</button>
<VPLocalNavOutlineDropdown :headers="headers" :navHeight="navHeight" />
</div>
</div>
</template>
@ -77,10 +85,6 @@ const classes = computed(() => {
/*rtl:ignore*/
left: 0;
z-index: var(--vp-z-index-local-nav);
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%;
@ -91,16 +95,38 @@ const classes = computed(() => {
position: fixed;
}
.VPLocalNav.reached-top {
border-top-color: transparent;
@media (min-width: 960px) {
.VPLocalNav {
top: var(--vp-nav-height);
}
.VPLocalNav.has-sidebar {
padding-left: var(--vp-sidebar-width);
}
.VPLocalNav.empty {
display: none;
}
}
@media (min-width: 960px) {
@media (min-width: 1280px) {
.VPLocalNav {
display: none;
}
}
@media (min-width: 1440px) {
.VPLocalNav.has-sidebar {
padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
}
}
.container {
display: flex;
justify-content: space-between;
align-items: center;
}
.menu {
display: flex;
align-items: center;
@ -123,6 +149,12 @@ const classes = computed(() => {
}
}
@media (min-width: 960px) {
.menu {
display: none;
}
}
.menu-icon {
margin-right: 8px;
width: 16px;

@ -1,4 +1,5 @@
<script setup lang="ts">
import { onClickOutside, onKeyStroke } from '@vueuse/core'
import { onContentUpdated } from 'vitepress'
import { nextTick, ref } from 'vue'
import { useData } from '../composables/data'
@ -14,8 +15,17 @@ const props = defineProps<{
const { theme } = useData()
const open = ref(false)
const vh = ref(0)
const main = ref<HTMLDivElement>()
const items = ref<HTMLDivElement>()
onClickOutside(main, () => {
open.value = false
})
onKeyStroke('Escape', () => {
open.value = false
})
onContentUpdated(() => {
open.value = false
})
@ -44,7 +54,11 @@ function scrollToTop() {
</script>
<template>
<div class="VPLocalNavOutlineDropdown" :style="{ '--vp-vh': vh + 'px' }">
<div
class="VPLocalNavOutlineDropdown"
:style="{ '--vp-vh': vh + 'px' }"
ref="main"
>
<button @click="toggle" :class="{ open }" v-if="headers.length > 0">
{{ resolveTitle(theme) }}
<VPIconChevronRight class="icon" />
@ -53,11 +67,7 @@ function scrollToTop() {
{{ theme.returnToTopLabel || 'Return to top' }}
</button>
<Transition name="flyout">
<div v-if="open"
ref="items"
class="items"
@click="onItemClick"
>
<div v-if="open" ref="items" class="items" @click="onItemClick">
<div class="header">
<a class="top-link" href="#" @click="scrollToTop">
{{ theme.returnToTopLabel || 'Return to top' }}
@ -76,6 +86,12 @@ function scrollToTop() {
padding: 12px 20px 11px;
}
@media (min-width: 960px) {
.VPLocalNavOutlineDropdown {
padding: 12px 36px 11px;
}
}
.VPLocalNavOutlineDropdown button {
display: block;
font-size: 12px;
@ -95,6 +111,12 @@ function scrollToTop() {
color: var(--vp-c-text-1);
}
@media (min-width: 960px) {
.VPLocalNavOutlineDropdown button {
font-size: 14px;
}
}
.icon {
display: inline-block;
vertical-align: middle;
@ -104,18 +126,13 @@ function scrollToTop() {
fill: currentColor;
}
:deep(.outline-link) {
font-size: 14px;
padding: 2px 0;
}
.open > .icon {
transform: rotate(90deg);
}
.items {
position: absolute;
top: 64px;
top: 40px;
right: 16px;
left: 16px;
display: grid;
@ -128,6 +145,14 @@ function scrollToTop() {
box-shadow: var(--vp-shadow-3);
}
@media (min-width: 960px) {
.items {
right: auto;
left: calc(var(--vp-sidebar-width) + 32px);
width: 320px;
}
}
.header {
background-color: var(--vp-c-bg-soft);
}
@ -147,11 +172,11 @@ function scrollToTop() {
}
.flyout-enter-active {
transition: all .2s ease-out;
transition: all 0.2s ease-out;
}
.flyout-leave-active {
transition: all .15s ease-in;
transition: all 0.15s ease-in;
}
.flyout-enter-from,

@ -2,6 +2,7 @@
import { useWindowScroll } from '@vueuse/core'
import { ref, watchPostEffect } from 'vue'
import { useData } from '../composables/data'
import { useLocalNav } from '../composables/local-nav'
import { useSidebar } from '../composables/sidebar'
import VPNavBarAppearance from './VPNavBarAppearance.vue'
import VPNavBarExtra from './VPNavBarExtra.vue'
@ -22,6 +23,7 @@ defineEmits<{
const { y } = useWindowScroll()
const { hasSidebar } = useSidebar()
const { hasLocalNav } = useLocalNav()
const { frontmatter } = useData()
const classes = ref<Record<string, boolean>>({})
@ -29,6 +31,7 @@ const classes = ref<Record<string, boolean>>({})
watchPostEffect(() => {
classes.value = {
'has-sidebar': hasSidebar.value,
'has-local-nav': hasLocalNav.value,
top: frontmatter.value.layout === 'home' && y.value === 0,
}
})
@ -36,59 +39,76 @@ watchPostEffect(() => {
<template>
<div class="VPNavBar" :class="classes">
<div class="container">
<div class="title">
<VPNavBarTitle>
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
</VPNavBarTitle>
</div>
<div class="wrapper">
<div class="container">
<div class="title">
<VPNavBarTitle>
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
</VPNavBarTitle>
</div>
<div class="content">
<div class="curtain" />
<div class="content-body">
<slot name="nav-bar-content-before" />
<VPNavBarSearch class="search" />
<VPNavBarMenu class="menu" />
<VPNavBarTranslations class="translations" />
<VPNavBarAppearance class="appearance" />
<VPNavBarSocialLinks class="social-links" />
<VPNavBarExtra class="extra" />
<slot name="nav-bar-content-after" />
<VPNavBarHamburger class="hamburger" :active="isScreenOpen" @click="$emit('toggle-screen')" />
<div class="content">
<div class="content-body">
<slot name="nav-bar-content-before" />
<VPNavBarSearch class="search" />
<VPNavBarMenu class="menu" />
<VPNavBarTranslations class="translations" />
<VPNavBarAppearance class="appearance" />
<VPNavBarSocialLinks class="social-links" />
<VPNavBarExtra class="extra" />
<slot name="nav-bar-content-after" />
<VPNavBarHamburger class="hamburger" :active="isScreenOpen" @click="$emit('toggle-screen')" />
</div>
</div>
</div>
</div>
<div class="divider">
<div class="divider-line" />
</div>
</div>
</template>
<style scoped>
.VPNavBar {
position: relative;
border-bottom: 1px solid transparent;
padding: 0 8px 0 24px;
height: var(--vp-nav-height);
pointer-events: none;
white-space: nowrap;
transition: background-color 0.5s;
}
@media (min-width: 768px) {
.VPNavBar {
padding: 0 32px;
}
.VPNavBar.has-local-nav {
background-color: var(--vp-nav-bg-color);
}
@media (min-width: 960px) {
.VPNavBar.has-sidebar {
padding: 0;
.VPNavBar.has-local-nav {
background-color: transparent;
}
.VPNavBar:not(.has-sidebar):not(.top) {
border-bottom-color: var(--vp-c-gutter);
background-color: var(--vp-nav-bg-color);
}
}
.wrapper {
padding: 0 8px 0 24px;
}
@media (min-width: 768px) {
.wrapper {
padding: 0 32px;
}
}
@media (min-width: 960px) {
.VPNavBar.has-sidebar .wrapper {
padding: 0;
}
}
.container {
display: flex;
justify-content: space-between;
@ -163,15 +183,19 @@ watchPostEffect(() => {
display: flex;
justify-content: flex-end;
align-items: center;
height: calc(var(--vp-nav-height) - 1px);
height: var(--vp-nav-height);
transition: background-color 0.5s;
}
@media (min-width: 960px) {
.VPNavBar:not(.top) .content-body{
.VPNavBar:not(.top) .content-body {
position: relative;
background-color: var(--vp-nav-bg-color);
}
.VPNavBar:not(.has-sidebar):not(.top) .content-body {
background-color: transparent;
}
}
@media (max-width: 767px) {
@ -206,27 +230,40 @@ watchPostEffect(() => {
margin-right: -8px;
}
.divider {
width: 100%;
height: 1px;
}
@media (min-width: 960px) {
.VPNavBar.has-sidebar .curtain {
position: absolute;
right: 0;
bottom: -31px;
width: calc(100% - var(--vp-sidebar-width));
height: 32px;
.VPNavBar.has-sidebar .divider {
padding-left: var(--vp-sidebar-width);
}
}
.VPNavBar.has-sidebar .curtain::before {
display: block;
width: 100%;
height: 32px;
background: linear-gradient(var(--vp-c-bg), transparent 70%);
content: "";
@media (min-width: 1440px) {
.VPNavBar.has-sidebar .divider {
padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
}
}
@media (min-width: 1440px) {
.VPNavBar.has-sidebar .curtain {
width: calc(100% - ((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width)));
.divider-line {
width: 100%;
height: 1px;
transition: background-color 0.5s;
}
.VPNavBar.has-local-nav .divider-line {
background-color: var(--vp-c-gutter);
}
@media (min-width: 960px) {
.VPNavBar:not(.top) .divider-line {
background-color: var(--vp-c-gutter);
}
.VPNavBar:not(.has-sidebar):not(.top) .divider {
background-color: var(--vp-c-gutter);
}
}
</style>

@ -87,7 +87,6 @@ watch(
@media (min-width: 960px) {
.VPSidebar {
z-index: 1;
padding-top: var(--vp-nav-height);
width: var(--vp-sidebar-width);
max-width: 100%;

@ -5,14 +5,16 @@ import VPSwitch from './VPSwitch.vue'
import VPIconMoon from './icons/VPIconMoon.vue'
import VPIconSun from './icons/VPIconSun.vue'
const { isDark } = useData()
const { isDark, theme } = useData()
const toggleAppearance = inject('toggle-appearance', () => {
isDark.value = !isDark.value
})
const switchTitle = computed(() => {
return isDark.value ? 'Switch to light theme' : 'Switch to dark theme'
return isDark.value
? theme.value.lightModeSwitchTitle || 'Switch to light theme'
: theme.value.darkModeSwitchTitle || 'Switch to dark theme'
})
</script>

@ -0,0 +1,24 @@
import { onContentUpdated } from 'vitepress'
import { type DefaultTheme } from 'vitepress/theme'
import { computed, shallowRef } from 'vue'
import { getHeaders, type MenuItem } from '../composables/outline'
import { useData } from './data'
export function useLocalNav(): DefaultTheme.DocLocalNav {
const { theme, frontmatter } = useData()
const headers = shallowRef<MenuItem[]>([])
const hasLocalNav = computed(() => {
return headers.value.length > 0
})
onContentUpdated(() => {
headers.value = getHeaders(frontmatter.value.outline ?? theme.value.outline)
})
return {
headers,
hasLocalNav
}
}

@ -181,7 +181,7 @@ export function useActiveAnchor(
if (activeLink) {
activeLink.classList.add('active')
marker.value.style.top = activeLink.offsetTop + 33 + 'px'
marker.value.style.top = activeLink.offsetTop + 39 + 'px'
marker.value.style.opacity = '1'
} else {
marker.value.style.top = '33px'

@ -39,7 +39,6 @@ body {
font-weight: 400;
color: var(--vp-c-text-1);
background-color: var(--vp-c-bg);
direction: ltr;
font-synthesis: style;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;

@ -264,6 +264,12 @@
--vp-z-index-sidebar: 60;
}
@media (min-width: 960px) {
:root {
--vp-z-index-sidebar: 25;
}
}
/**
* Icons
* -------------------------------------------------------------------------- */

@ -27,6 +27,7 @@ export { default as VPTeamPageSection } from './components/VPTeamPageSection.vue
export { default as VPTeamMembers } from './components/VPTeamMembers.vue'
export { useSidebar } from './composables/sidebar'
export { useLocalNav } from './composables/local-nav'
const theme: Theme = {
Layout,

@ -104,7 +104,8 @@ export async function resolveConfig(
const { pages, dynamicRoutes, rewrites } = await resolvePages(
srcDir,
userConfig
userConfig,
logger
)
const config: SiteConfig = {
@ -251,7 +252,7 @@ export async function resolveSiteData(
appearance: userConfig.appearance ?? true,
themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {},
scrollOffset: userConfig.scrollOffset ?? 90,
scrollOffset: userConfig.scrollOffset ?? 134,
cleanUrls: !!userConfig.cleanUrls,
contentProps: userConfig.contentProps
}

@ -19,31 +19,31 @@ import anchorPlugin from 'markdown-it-anchor'
import attrsPlugin from 'markdown-it-attrs'
// @ts-ignore
import { full as emojiPlugin } from 'markdown-it-emoji'
import type {
BuiltinTheme,
Highlighter,
LanguageInput,
ShikijiTransformer,
ThemeRegistrationAny
} from 'shikiji'
import type { Logger } from 'vite'
import { containerPlugin, type ContainerOptions } from './plugins/containers'
import { highlight } from './plugins/highlight'
import { highlightLinePlugin } from './plugins/highlightLines'
import { imagePlugin } from './plugins/image'
import { imagePlugin, type Options as ImageOptions } from './plugins/image'
import { lineNumberPlugin } from './plugins/lineNumbers'
import { linkPlugin } from './plugins/link'
import { preWrapperPlugin } from './plugins/preWrapper'
import { snippetPlugin } from './plugins/snippet'
import type {
ThemeRegistration,
BuiltinTheme,
LanguageInput,
ShikijiTransformer,
Highlighter
} from 'shikiji'
export type { Header } from '../shared'
export type ThemeOptions =
| ThemeRegistration
| ThemeRegistrationAny
| BuiltinTheme
| {
light: ThemeRegistration | BuiltinTheme
dark: ThemeRegistration | BuiltinTheme
light: ThemeRegistrationAny | BuiltinTheme
dark: ThemeRegistrationAny | BuiltinTheme
}
export interface MarkdownOptions extends MarkdownIt.Options {
@ -166,6 +166,7 @@ export interface MarkdownOptions extends MarkdownIt.Options {
* @see https://vitepress.dev/guide/markdown#math-equations
*/
math?: boolean | any
image?: ImageOptions
}
export type MarkdownRenderer = MarkdownIt
@ -198,7 +199,7 @@ export const createMarkdownRenderer = async (
.use(preWrapperPlugin, { hasSingleTheme })
.use(snippetPlugin, srcDir)
.use(containerPlugin, { hasSingleTheme }, options.container)
.use(imagePlugin)
.use(imagePlugin, options.image)
.use(
linkPlugin,
{ target: '_blank', rel: 'noreferrer', ...options.externalLinks },

@ -2,14 +2,12 @@ import { customAlphabet } from 'nanoid'
import c from 'picocolors'
import type { ShikijiTransformer } from 'shikiji'
import {
addClassToHast,
bundledLanguages,
getHighlighter,
addClassToHast,
isPlaintext as isPlainLang,
isSpecialLang
} from 'shikiji'
import type { Logger } from 'vite'
import type { MarkdownOptions, ThemeOptions } from '../markdown'
import {
transformerCompactLineOptions,
transformerNotationDiff,
@ -18,6 +16,8 @@ import {
transformerNotationHighlight,
type TransformerCompactLineOption
} from 'shikiji-transformers'
import type { Logger } from 'vite'
import type { MarkdownOptions, ThemeOptions } from '../markdown'
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
@ -65,9 +65,9 @@ export async function highlight(
const highlighter = await getHighlighter({
themes:
typeof theme === 'string' || 'name' in theme
? [theme]
: [theme.light, theme.dark],
typeof theme === 'object' && 'light' in theme && 'dark' in theme
? [theme.light, theme.dark]
: [theme],
langs: [...Object.keys(bundledLanguages), ...(options.languages || [])],
langAlias: options.languageAlias
})
@ -169,15 +169,10 @@ export async function highlight(
},
...userTransformers
],
meta: {
__raw: attrs
},
...(typeof theme === 'string' || 'name' in theme
? { theme }
: {
themes: theme,
defaultColor: false
})
meta: { __raw: attrs },
...(typeof theme === 'object' && 'light' in theme && 'dark' in theme
? { themes: theme, defaultColor: false }
: { theme })
})
return fillEmptyHighlightedLine(restoreMustache(highlighted))

@ -3,7 +3,15 @@
import type MarkdownIt from 'markdown-it'
import { EXTERNAL_URL_RE } from '../../shared'
export const imagePlugin = (md: MarkdownIt) => {
export interface Options {
/**
* Support native lazy loading for the `<img>` tag.
* @default false
*/
lazyLoading?: boolean
}
export const imagePlugin = (md: MarkdownIt, { lazyLoading }: Options = {}) => {
const imageRule = md.renderer.rules.image!
md.renderer.rules.image = (tokens, idx, options, env, self) => {
const token = tokens[idx]
@ -12,6 +20,9 @@ export const imagePlugin = (md: MarkdownIt) => {
if (!/^\.?\//.test(url)) url = './' + url
token.attrSet('src', decodeURIComponent(url))
}
if (lazyLoading) {
token.attrSet('loading', 'lazy')
}
return imageRule(tokens, idx, options, env, self)
}
}

@ -268,7 +268,11 @@ export async function createVitePressPlugin(
if (file.endsWith('.md')) {
Object.assign(
siteConfig,
await resolvePages(siteConfig.srcDir, siteConfig.userConfig)
await resolvePages(
siteConfig.srcDir,
siteConfig.userConfig,
siteConfig.logger
)
)
}

@ -1,6 +1,7 @@
import {
loadConfigFromFile,
normalizePath,
type Logger,
type Plugin,
type ViteDevServer
} from 'vite'
@ -13,7 +14,11 @@ import { resolveRewrites } from './rewritesPlugin'
export const dynamicRouteRE = /\[(\w+?)\]/g
export async function resolvePages(srcDir: string, userConfig: UserConfig) {
export async function resolvePages(
srcDir: string,
userConfig: UserConfig,
logger: Logger
) {
// Important: fast-glob doesn't guarantee order of the returned files.
// We must sort the pages so the input list to rollup is stable across
// builds - otherwise different input order could result in different exports
@ -39,7 +44,11 @@ export async function resolvePages(srcDir: string, userConfig: UserConfig) {
;(dynamicRouteRE.test(file) ? dynamicRouteFiles : pages).push(file)
})
const dynamicRoutes = await resolveDynamicRoutes(srcDir, dynamicRouteFiles)
const dynamicRoutes = await resolveDynamicRoutes(
srcDir,
dynamicRouteFiles,
logger
)
pages.push(...dynamicRoutes.routes.map((r) => r.path))
const rewrites = resolveRewrites(pages, userConfig.rewrites)
@ -141,7 +150,7 @@ export const dynamicRoutesPlugin = async (
if (!/\.md$/.test(ctx.file)) {
Object.assign(
config,
await resolvePages(config.srcDir, config.userConfig)
await resolvePages(config.srcDir, config.userConfig, config.logger)
)
}
for (const id of mods) {
@ -154,7 +163,8 @@ export const dynamicRoutesPlugin = async (
export async function resolveDynamicRoutes(
srcDir: string,
routes: string[]
routes: string[],
logger: Logger
): Promise<SiteConfig['dynamicRoutes']> {
const pendingResolveRoutes: Promise<ResolvedRouteConfig[]>[] = []
const routeFileToModulesMap: Record<string, Set<string>> = {}
@ -170,7 +180,7 @@ export async function resolveDynamicRoutes(
const pathsFile = paths.find((p) => fs.existsSync(p))
if (pathsFile == null) {
console.warn(
logger.warn(
c.yellow(
`Missing paths file for dynamic route ${route}: ` +
`a corresponding ${paths[0]} (or .ts/.mjs/.mts) file is needed.`
@ -183,15 +193,15 @@ export async function resolveDynamicRoutes(
let mod = routeModuleCache.get(pathsFile)
if (!mod) {
try {
mod = (await loadConfigFromFile({} as any, pathsFile)) as RouteModule
mod = (await loadConfigFromFile(
{} as any,
pathsFile,
undefined,
'silent'
)) as RouteModule
routeModuleCache.set(pathsFile, mod)
} catch (e) {
console.warn(
c.yellow(
`Invalid paths file export in ${pathsFile}. ` +
`Expects default export of an object with a "paths" property.`
)
)
} catch (e: any) {
logger.warn(`${c.yellow(`Failed to load ${pathsFile}:`)}\n${e.stack}`)
continue
}
}
@ -210,7 +220,7 @@ export async function resolveDynamicRoutes(
const loader = mod!.config.paths
if (!loader) {
console.warn(
logger.warn(
c.yellow(
`Invalid paths file export in ${pathsFile}. ` +
`Missing "paths" property from default export.`

@ -1,6 +1,7 @@
import _debug from 'debug'
import fs from 'fs-extra'
import MiniSearch from 'minisearch'
import pMap from 'p-map'
import path from 'path'
import type { Plugin, ViteDevServer } from 'vite'
import type { SiteConfig } from '../config'
@ -53,15 +54,18 @@ export async function localSearchPlugin(
const options = siteConfig.site.themeConfig.search.options || {}
function render(file: string) {
async function render(file: string) {
if (!fs.existsSync(file)) return ''
const { srcDir, cleanUrls = false } = siteConfig
const relativePath = slash(path.relative(srcDir, file))
const env: MarkdownEnv = { path: file, relativePath, cleanUrls }
let src = fs.readFileSync(file, 'utf-8')
src = processIncludes(srcDir, src, file, [])
if (options._render) return options._render(src, env, md)
const html = md.render(src, env)
return env.frontmatter?.search === false ? '' : html
const md_raw = await fs.promises.readFile(file, 'utf-8')
const md_src = processIncludes(srcDir, md_raw, file, [])
if (options._render) return await options._render(md_src, env, md)
else {
const html = md.render(md_src, env)
return env.frontmatter?.search === false ? '' : html
}
}
const indexByLocales = new Map<string, MiniSearch<IndexObject>>()
@ -85,11 +89,6 @@ export async function localSearchPlugin(
return siteData?.localeIndex ?? 'root'
}
function getIndexForPath(file: string) {
const locale = getLocaleForPath(file)
return getIndexByLocale(locale)
}
let server: ViteDevServer | undefined
function onIndexUpdated() {
@ -123,43 +122,39 @@ export async function localSearchPlugin(
return id
}
async function indexAllFiles(files: string[]) {
const documentsByLocale = new Map<string, IndexObject[]>()
await Promise.all(
files
.filter((file) => fs.existsSync(file))
.map(async (file) => {
const fileId = getDocId(file)
const sections = splitPageIntoSections(render(file))
if (sections.length === 0) return
const locale = getLocaleForPath(file)
let documents = documentsByLocale.get(locale)
if (!documents) {
documents = []
documentsByLocale.set(locale, documents)
}
documents.push(
...sections.map((section) => ({
id: `${fileId}#${section.anchor}`,
text: section.text,
title: section.titles.at(-1)!,
titles: section.titles.slice(0, -1)
}))
)
})
)
for (const [locale, documents] of documentsByLocale) {
const index = getIndexByLocale(locale)
index.removeAll()
await index.addAllAsync(documents)
async function indexFile(page: string) {
const file = path.join(siteConfig.srcDir, page)
// get file metadata
const fileId = getDocId(file)
const locale = getLocaleForPath(file)
const index = getIndexByLocale(locale)
// retrieve file and split into "sections"
const html = await render(file)
const sections =
// user provided generator
(await options.miniSearch?._splitIntoSections?.(file, html)) ??
// default implementation
splitPageIntoSections(html)
// add sections to the locale index
for await (const section of sections) {
if (!section || !(section.text || section.titles)) break
const { anchor, text, titles } = section
const id = anchor ? [fileId, anchor].join('#') : fileId
index.add({
id,
text,
title: titles.at(-1)!,
titles: titles.slice(0, -1)
})
}
debug(`🔍️ Indexed ${files.length} files`)
}
async function scanForBuild() {
await indexAllFiles(
siteConfig.pages.map((f) => path.join(siteConfig.srcDir, f))
)
debug('🔍️ Indexing files for search...')
await pMap(siteConfig.pages, indexFile, {
concurrency: siteConfig.buildConcurrency
})
debug('✅ Indexing finished...')
}
return {
@ -214,25 +209,8 @@ export async function localSearchPlugin(
async handleHotUpdate({ file }) {
if (file.endsWith('.md')) {
const fileId = getDocId(file)
if (!fs.existsSync(file)) return
const index = getIndexForPath(file)
const sections = splitPageIntoSections(render(file))
if (sections.length === 0) return
for (const section of sections) {
const id = `${fileId}#${section.anchor}`
if (index.has(id)) {
index.discard(id)
}
index.add({
id,
text: section.text,
title: section.titles.at(-1)!,
titles: section.titles.slice(0, -1)
})
}
await indexFile(file)
debug('🔍️ Updated', file)
onIndexUpdated()
}
}
@ -242,20 +220,13 @@ export async function localSearchPlugin(
const headingRegex = /<h(\d*).*?>(.*?<a.*? href="#.*?".*?>.*?<\/a>)<\/h\1>/gi
const headingContentRegex = /(.*?)<a.*? href="#(.*?)".*?>.*?<\/a>/i
interface PageSection {
anchor: string
titles: string[]
text: string
}
/**
* Splits HTML into sections based on headings
*/
function splitPageIntoSections(html: string) {
function* splitPageIntoSections(html: string) {
const result = html.split(headingRegex)
result.shift()
let parentTitles: string[] = []
const sections: PageSection[] = []
for (let i = 0; i < result.length; i += 3) {
const level = parseInt(result[i]) - 1
const heading = result[i + 1]
@ -266,14 +237,13 @@ function splitPageIntoSections(html: string) {
if (!title || !content) continue
const titles = parentTitles.slice(0, level)
titles[level] = title
sections.push({ anchor, titles, text: getSearchableText(content) })
yield { anchor, titles, text: getSearchableText(content) }
if (level === 0) {
parentTitles = [title]
} else {
parentTitles[level] = title
}
}
return sections
}
function getSearchableText(content: string) {

1
theme.d.ts vendored

@ -13,6 +13,7 @@ declare const theme: {
export default theme
export declare const useSidebar: () => DefaultTheme.DocSidebar
export declare const useLocalNav: () => DefaultTheme.DocLocalNav
// TODO: add props for these
export declare const VPButton: DefineComponent

@ -2,8 +2,11 @@ import type MarkdownIt from 'markdown-it'
import type { Options as MiniSearchOptions } from 'minisearch'
import type { ComputedRef, Ref } from 'vue'
import type { DocSearchProps } from './docsearch.js'
import type { LocalSearchTranslations } from './local-search.js'
import type { MarkdownEnv, PageData } from './shared.js'
import type {
LocalSearchTranslations,
PageSplitSection
} from './local-search.js'
import type { Awaitable, MarkdownEnv, PageData } from './shared.js'
export namespace DefaultTheme {
export interface Config {
@ -96,6 +99,16 @@ export namespace DefaultTheme {
*/
darkModeSwitchLabel?: string
/**
* @default 'Switch to light theme'
*/
lightModeSwitchTitle?: string
/**
* @default 'Switch to dark theme'
*/
darkModeSwitchTitle?: string
/**
* @default 'Menu'
*/
@ -348,6 +361,25 @@ export namespace DefaultTheme {
actionText?: string
}
// local nav -----------------------------------------------------------------
/**
* ReturnType of `useLocalNav`.
*/
export interface DocLocalNav {
/**
* The outline headers of the current page.
*/
headers: ShallowRef<MenuItem[]>
/**
* Whether the current page has a local nav. Local nav is shown when the
* "outline" is present in the page. However, note that the actual
* local nav visibility depends on the screen width as well.
*/
hasLocalNav: ComputedRef<boolean>
}
// outline -------------------------------------------------------------------
export interface Outline {
@ -393,13 +425,34 @@ export namespace DefaultTheme {
* @see https://lucaong.github.io/minisearch/modules/_minisearch_.html#searchoptions-1
*/
searchOptions?: MiniSearchOptions['searchOptions']
}
/**
* Overrides the default regex based page splitter.
* Supports async generator, making it possible to run in true parallel
* (when used along with `node:child_process` or `worker_threads`)
* ---
* This should be especially useful for scalability reasons.
* ---
* @param {string} path - absolute path to the markdown source file
* @param {string} html - document page rendered as html
*/
_splitIntoSections?: (
path: string,
html: string
) =>
| AsyncGenerator<PageSplitSection>
| Generator<PageSplitSection>
| Awaitable<PageSplitSection[]>
}
/**
* Allows transformation of content before indexing (node only)
* Return empty string to skip indexing
*/
_render?: (src: string, env: MarkdownEnv, md: MarkdownIt) => string
_render?: (
src: string,
env: MarkdownEnv,
md: MarkdownIt
) => Awaitable<string>
}
// algolia -------------------------------------------------------------------

@ -25,3 +25,9 @@ export interface FooterTranslations {
closeText?: string
closeKeyAriaLabel?: string
}
export interface PageSplitSection {
anchor?: string
titles: string[]
text: string
}

Loading…
Cancel
Save