vitepress 1.0.0-rc.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +3 -5
  2. package/dist/client/app/components/Content.js +2 -1
  3. package/dist/client/app/composables/copyCode.js +7 -5
  4. package/dist/client/app/composables/head.js +34 -9
  5. package/dist/client/app/data.js +13 -18
  6. package/dist/client/app/devtools.js +3 -2
  7. package/dist/client/app/index.js +24 -15
  8. package/dist/client/app/router.js +32 -60
  9. package/dist/client/app/utils.js +34 -0
  10. package/dist/client/index.d.ts +4 -3
  11. package/dist/client/index.js +2 -2
  12. package/dist/client/shared.js +34 -5
  13. package/dist/client/theme-default/Layout.vue +4 -1
  14. package/dist/client/theme-default/components/VPAlgoliaSearchBox.vue +14 -15
  15. package/dist/client/theme-default/components/VPBadge.vue +12 -1
  16. package/dist/client/theme-default/components/VPButton.vue +4 -2
  17. package/dist/client/theme-default/components/VPContent.vue +3 -0
  18. package/dist/client/theme-default/components/VPDoc.vue +3 -20
  19. package/dist/client/theme-default/components/VPDocAsideOutline.vue +3 -4
  20. package/dist/client/theme-default/components/VPDocFooter.vue +5 -10
  21. package/dist/client/theme-default/components/VPDocFooterLastUpdated.vue +2 -2
  22. package/dist/client/theme-default/components/VPDocOutlineItem.vue +5 -3
  23. package/dist/client/theme-default/components/VPFeature.vue +20 -9
  24. package/dist/client/theme-default/components/VPFeatures.vue +2 -0
  25. package/dist/client/theme-default/components/VPFlyout.vue +7 -15
  26. package/dist/client/theme-default/components/VPFooter.vue +10 -0
  27. package/dist/client/theme-default/components/VPHero.vue +7 -0
  28. package/dist/client/theme-default/components/VPHome.vue +13 -8
  29. package/dist/client/theme-default/components/VPHomeContent.vue +52 -0
  30. package/dist/client/theme-default/components/VPHomeHero.vue +3 -0
  31. package/dist/client/theme-default/components/VPHomeSponsors.vue +33 -9
  32. package/dist/client/theme-default/components/VPLocalNav.vue +61 -33
  33. package/dist/client/theme-default/components/VPLocalNavOutlineDropdown.vue +45 -17
  34. package/dist/client/theme-default/components/VPLocalSearchBox.vue +69 -135
  35. package/dist/client/theme-default/components/VPNav.vue +15 -2
  36. package/dist/client/theme-default/components/VPNavBar.vue +81 -47
  37. package/dist/client/theme-default/components/VPNavBarAppearance.vue +1 -1
  38. package/dist/client/theme-default/components/VPNavBarExtra.vue +1 -1
  39. package/dist/client/theme-default/components/VPNavBarMenuGroup.vue +17 -2
  40. package/dist/client/theme-default/components/VPNavBarMenuLink.vue +1 -0
  41. package/dist/client/theme-default/components/VPNavBarSearch.vue +4 -28
  42. package/dist/client/theme-default/components/VPNavBarSearchButton.vue +25 -25
  43. package/dist/client/theme-default/components/VPNavBarTitle.vue +27 -3
  44. package/dist/client/theme-default/components/VPNavBarTranslations.vue +1 -2
  45. package/dist/client/theme-default/components/VPNavScreenAppearance.vue +1 -1
  46. package/dist/client/theme-default/components/VPNavScreenMenuGroup.vue +3 -7
  47. package/dist/client/theme-default/components/VPNavScreenTranslations.vue +3 -7
  48. package/dist/client/theme-default/components/VPSidebar.vue +0 -2
  49. package/dist/client/theme-default/components/VPSidebarItem.vue +3 -6
  50. package/dist/client/theme-default/components/VPSocialLink.vue +4 -5
  51. package/dist/client/theme-default/components/VPSwitch.vue +4 -4
  52. package/dist/client/theme-default/components/VPSwitchAppearance.vue +11 -7
  53. package/dist/client/theme-default/components/VPTeamMembersItem.vue +2 -5
  54. package/dist/client/theme-default/components/VPTeamPage.vue +7 -2
  55. package/dist/client/theme-default/composables/hash.js +9 -0
  56. package/dist/client/theme-default/composables/langs.js +3 -2
  57. package/dist/client/theme-default/composables/local-nav.js +18 -0
  58. package/dist/client/theme-default/composables/outline.js +55 -37
  59. package/dist/client/theme-default/composables/prev-next.js +10 -1
  60. package/dist/client/theme-default/composables/sidebar.js +2 -7
  61. package/dist/client/theme-default/styles/base.css +10 -1
  62. package/dist/client/theme-default/styles/components/custom-block.css +87 -1
  63. package/dist/client/theme-default/styles/components/vp-code.css +4 -4
  64. package/dist/client/theme-default/styles/components/vp-doc.css +11 -0
  65. package/dist/client/theme-default/styles/components/vp-sponsor.css +3 -3
  66. package/dist/client/theme-default/styles/icons.css +123 -0
  67. package/dist/client/theme-default/styles/vars.css +79 -29
  68. package/dist/client/theme-default/support/lru.js +33 -0
  69. package/dist/client/theme-default/support/translation.js +3 -2
  70. package/dist/client/theme-default/support/utils.js +6 -3
  71. package/dist/client/theme-default/without-fonts.js +3 -3
  72. package/dist/node/cli.js +14 -15
  73. package/dist/node/index.d.ts +345 -3386
  74. package/dist/node/index.js +22 -23
  75. package/dist/node/{serve-05aeef7d.js → serve-fltVOIdt.js} +44576 -64359
  76. package/package.json +88 -72
  77. package/template/.vitepress/theme/index.js +13 -9
  78. package/template/.vitepress/theme/style.css +2 -2
  79. package/template/markdown-examples.md +1 -1
  80. package/theme.d.ts +19 -15
  81. package/types/default-theme.d.ts +73 -9
  82. package/types/docsearch.d.ts +1 -0
  83. package/types/local-search.d.ts +9 -3
  84. package/types/shared.d.ts +61 -1
  85. package/dist/client/theme-default/components/VPDocOutlineDropdown.vue +0 -85
  86. package/dist/client/theme-default/support/socialIcons.js +0 -13
  87. package/dist/node/serve-59fe4482.js +0 -2819
package/README.md CHANGED
@@ -1,14 +1,12 @@
1
- # VitePress (RC: release candidate) 📝💨
1
+ # VitePress 📝💨
2
2
 
3
- [![Test](https://github.com/vuejs/vitepress/workflows/Test/badge.svg)](https://github.com/vuejs/vitepress/actions)
3
+ [![test](https://github.com/vuejs/vitepress/workflows/Test/badge.svg)](https://github.com/vuejs/vitepress/actions)
4
4
  [![npm](https://img.shields.io/npm/v/vitepress)](https://www.npmjs.com/package/vitepress)
5
5
  [![chat](https://img.shields.io/badge/chat-discord-blue?logo=discord)](https://chat.vuejs.org)
6
6
 
7
7
  ---
8
8
 
9
- VitePress is [VuePress](https://vuepress.vuejs.org)' spiritual successor, built on top of [vite](https://github.com/vitejs/vite).
10
-
11
- Currently, it is in the `release candidate` stage. It is already suitable for out-of-the-box documentation use. We do not plan to introduce any breaking changes from here on until the stable release.
9
+ VitePress is a Vue-powered static site generator and a spiritual successor to [VuePress](https://vuepress.vuejs.org), built on top of [Vite](https://github.com/vitejs/vite).
12
10
 
13
11
  ## Documentation
14
12
 
@@ -14,7 +14,8 @@ export const Content = defineComponent({
14
14
  route.component
15
15
  ? h(route.component, {
16
16
  onVnodeMounted: runCbs,
17
- onVnodeUpdated: runCbs
17
+ onVnodeUpdated: runCbs,
18
+ onVnodeUnmounted: runCbs
18
19
  })
19
20
  : '404 Page Not Found'
20
21
  ]);
@@ -11,11 +11,13 @@ export function useCopyCode() {
11
11
  return;
12
12
  }
13
13
  const isShell = /language-(shellscript|shell|bash|sh|zsh)/.test(parent.className);
14
- let text = '';
15
- sibling
16
- .querySelectorAll('span.line:not(.diff.remove)')
17
- .forEach((node) => (text += (node.textContent || '') + '\n'));
18
- text = text.slice(0, -1);
14
+ const ignoredNodes = ['.vp-copy-ignore', '.diff.remove'];
15
+ // Clone the node and remove the ignored nodes
16
+ const clone = sibling.cloneNode(true);
17
+ clone
18
+ .querySelectorAll(ignoredNodes.join(','))
19
+ .forEach((node) => node.remove());
20
+ let text = clone.textContent || '';
19
21
  if (isShell) {
20
22
  text = text.replace(/^ *(\$|>) /gm, '').trim();
21
23
  }
@@ -1,22 +1,37 @@
1
1
  import { watchEffect } from 'vue';
2
2
  import { createTitle, mergeHead } from '../../shared';
3
3
  export function useUpdateHead(route, siteDataByRouteRef) {
4
- let managedHeadTags = [];
5
4
  let isFirstUpdate = true;
5
+ let managedHeadElements = [];
6
6
  const updateHeadTags = (newTags) => {
7
7
  if (import.meta.env.PROD && isFirstUpdate) {
8
8
  // in production, the initial meta tags are already pre-rendered so we
9
9
  // skip the first update.
10
10
  isFirstUpdate = false;
11
+ newTags.forEach((tag) => {
12
+ const headEl = createHeadElement(tag);
13
+ for (const el of document.head.children) {
14
+ if (el.isEqualNode(headEl)) {
15
+ managedHeadElements.push(el);
16
+ return;
17
+ }
18
+ }
19
+ });
11
20
  return;
12
21
  }
13
- managedHeadTags.forEach((el) => document.head.removeChild(el));
14
- managedHeadTags = [];
15
- newTags.forEach((headConfig) => {
16
- const el = createHeadElement(headConfig);
17
- document.head.appendChild(el);
18
- managedHeadTags.push(el);
22
+ const newElements = newTags.map(createHeadElement);
23
+ managedHeadElements.forEach((oldEl, oldIndex) => {
24
+ const matchedIndex = newElements.findIndex((newEl) => newEl?.isEqualNode(oldEl ?? null));
25
+ if (matchedIndex !== -1) {
26
+ delete newElements[matchedIndex];
27
+ }
28
+ else {
29
+ oldEl?.remove();
30
+ delete managedHeadElements[oldIndex];
31
+ }
19
32
  });
33
+ newElements.forEach((el) => el && document.head.appendChild(el));
34
+ managedHeadElements = [...managedHeadElements, ...newElements].filter(Boolean);
20
35
  };
21
36
  watchEffect(() => {
22
37
  const pageData = route.data;
@@ -24,11 +39,16 @@ export function useUpdateHead(route, siteDataByRouteRef) {
24
39
  const pageDescription = pageData && pageData.description;
25
40
  const frontmatterHead = (pageData && pageData.frontmatter.head) || [];
26
41
  // update title and description
27
- document.title = createTitle(siteData, pageData);
42
+ const title = createTitle(siteData, pageData);
43
+ if (title !== document.title) {
44
+ document.title = title;
45
+ }
28
46
  const description = pageDescription || siteData.description;
29
47
  let metaDescriptionElement = document.querySelector(`meta[name=description]`);
30
48
  if (metaDescriptionElement) {
31
- metaDescriptionElement.setAttribute('content', description);
49
+ if (metaDescriptionElement.getAttribute('content') !== description) {
50
+ metaDescriptionElement.setAttribute('content', description);
51
+ }
32
52
  }
33
53
  else {
34
54
  createHeadElement(['meta', { name: 'description', content: description }]);
@@ -44,6 +64,11 @@ function createHeadElement([tag, attrs, innerHTML]) {
44
64
  if (innerHTML) {
45
65
  el.innerHTML = innerHTML;
46
66
  }
67
+ if (tag === 'script' && !attrs.async) {
68
+ // async is true by default for dynamically created scripts
69
+ ;
70
+ el.async = false;
71
+ }
47
72
  return el;
48
73
  }
49
74
  function isMetaDescription(headConfig) {
@@ -16,17 +16,16 @@ if (import.meta.hot) {
16
16
  // per-app data
17
17
  export function initData(route) {
18
18
  const site = computed(() => resolveSiteDataByRoute(siteDataRef.value, route.data.relativePath));
19
- const isDark = site.value.appearance
20
- ? useDark({
21
- storageKey: APPEARANCE_KEY,
22
- initialValue: () => typeof site.value.appearance === 'string'
23
- ? site.value.appearance
24
- : 'auto',
25
- ...(typeof site.value.appearance === 'object'
26
- ? site.value.appearance
27
- : {})
28
- })
29
- : ref(false);
19
+ const appearance = site.value.appearance; // fine with reactivity being lost here, config change triggers a restart
20
+ const isDark = appearance === 'force-dark'
21
+ ? ref(true)
22
+ : appearance
23
+ ? useDark({
24
+ storageKey: APPEARANCE_KEY,
25
+ initialValue: () => typeof appearance === 'string' ? appearance : 'auto',
26
+ ...(typeof appearance === 'object' ? appearance : {})
27
+ })
28
+ : ref(false);
30
29
  return {
31
30
  site,
32
31
  theme: computed(() => site.value.themeConfig),
@@ -34,14 +33,10 @@ export function initData(route) {
34
33
  frontmatter: computed(() => route.data.frontmatter),
35
34
  params: computed(() => route.data.params),
36
35
  lang: computed(() => site.value.lang),
37
- dir: computed(() => site.value.dir),
36
+ dir: computed(() => route.data.frontmatter.dir || site.value.dir),
38
37
  localeIndex: computed(() => site.value.localeIndex || 'root'),
39
- title: computed(() => {
40
- return createTitle(site.value, route.data);
41
- }),
42
- description: computed(() => {
43
- return route.data.description || site.value.description;
44
- }),
38
+ title: computed(() => createTitle(site.value, route.data)),
39
+ description: computed(() => route.data.description || site.value.description),
45
40
  isDark
46
41
  };
47
42
  }
@@ -1,7 +1,7 @@
1
- import { setupDevtoolsPlugin } from '@vue/devtools-api';
1
+ import { setupDevToolsPlugin } from '@vue/devtools-api';
2
2
  const COMPONENT_STATE_TYPE = 'VitePress';
3
3
  export const setupDevtools = (app, router, data) => {
4
- setupDevtoolsPlugin({
4
+ setupDevToolsPlugin({
5
5
  // fix recursive reference
6
6
  app: app,
7
7
  id: 'org.vuejs.vitepress',
@@ -10,6 +10,7 @@ export const setupDevtools = (app, router, data) => {
10
10
  homepage: 'https://vitepress.dev',
11
11
  componentStateTypes: [COMPONENT_STATE_TYPE]
12
12
  }, (api) => {
13
+ // TODO: remove any
13
14
  api.on.inspectComponent((payload) => {
14
15
  payload.instanceData.state.push({
15
16
  type: COMPONENT_STATE_TYPE,
@@ -29,15 +29,15 @@ const Theme = resolveThemeExtends(RawTheme);
29
29
  const VitePressApp = defineComponent({
30
30
  name: 'VitePressApp',
31
31
  setup() {
32
- const { site } = useData();
32
+ const { site, lang, dir } = useData();
33
33
  // change the language on the HTML element based on the current lang
34
34
  onMounted(() => {
35
35
  watchEffect(() => {
36
- document.documentElement.lang = site.value.lang;
37
- document.documentElement.dir = site.value.dir;
36
+ document.documentElement.lang = lang.value;
37
+ document.documentElement.dir = dir.value;
38
38
  });
39
39
  });
40
- if (import.meta.env.PROD) {
40
+ if (import.meta.env.PROD && site.value.router.prefetchLinks) {
41
41
  // in prod mode, enable intersectionObserver based pre-fetch
42
42
  usePrefetch();
43
43
  }
@@ -51,6 +51,8 @@ const VitePressApp = defineComponent({
51
51
  }
52
52
  });
53
53
  export async function createApp() {
54
+ ;
55
+ globalThis.__VITEPRESS__ = true;
54
56
  const router = newRouter();
55
57
  const app = newApp();
56
58
  app.provide(RouterSymbol, router);
@@ -95,21 +97,28 @@ function newRouter() {
95
97
  let initialPath;
96
98
  return createRouter((path) => {
97
99
  let pageFilePath = pathToFile(path);
98
- if (!pageFilePath)
99
- return null;
100
- if (isInitialPageLoad) {
101
- initialPath = pageFilePath;
102
- }
103
- // use lean build if this is the initial page load or navigating back
104
- // to the initial loaded path (the static vnodes already adopted the
105
- // static content on that load so no need to re-fetch the page)
106
- if (isInitialPageLoad || initialPath === pageFilePath) {
107
- pageFilePath = pageFilePath.replace(/\.js$/, '.lean.js');
100
+ let pageModule = null;
101
+ if (pageFilePath) {
102
+ if (isInitialPageLoad) {
103
+ initialPath = pageFilePath;
104
+ }
105
+ // use lean build if this is the initial page load or navigating back
106
+ // to the initial loaded path (the static vnodes already adopted the
107
+ // static content on that load so no need to re-fetch the page)
108
+ if (isInitialPageLoad || initialPath === pageFilePath) {
109
+ pageFilePath = pageFilePath.replace(/\.js$/, '.lean.js');
110
+ }
111
+ if (import.meta.env.SSR) {
112
+ pageModule = import(/*@vite-ignore*/ pageFilePath + '?t=' + Date.now());
113
+ }
114
+ else {
115
+ pageModule = import(/*@vite-ignore*/ pageFilePath);
116
+ }
108
117
  }
109
118
  if (inBrowser) {
110
119
  isInitialPageLoad = false;
111
120
  }
112
- return import(/*@vite-ignore*/ pageFilePath);
121
+ return pageModule;
113
122
  }, Theme.NotFound);
114
123
  }
115
124
  if (inBrowser) {
@@ -1,7 +1,7 @@
1
- import { reactive, inject, markRaw, nextTick, readonly } from 'vue';
2
- import { notFoundPageData } from '../shared';
3
- import { inBrowser, withBase } from './utils';
1
+ import { inject, markRaw, nextTick, reactive, readonly } from 'vue';
2
+ import { notFoundPageData, treatAsHtml } from '../shared';
4
3
  import { siteDataRef } from './data';
4
+ import { getScrollOffset, inBrowser, withBase } from './utils';
5
5
  export const RouterSymbol = Symbol();
6
6
  // we are just using URL to parse the pathname and hash - the base doesn't
7
7
  // matter and is only passed to support same-host hrefs.
@@ -18,18 +18,20 @@ export function createRouter(loadPageModule, fallbackComponent) {
18
18
  go
19
19
  };
20
20
  async function go(href = inBrowser ? location.href : '/') {
21
+ href = normalizeHref(href);
21
22
  if ((await router.onBeforeRouteChange?.(href)) === false)
22
23
  return;
23
- const url = new URL(href, fakeHost);
24
- if (!siteDataRef.value.cleanUrls) {
25
- // ensure correct deep link so page refresh lands on correct files.
26
- // if cleanUrls is enabled, the server should handle this
27
- if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) {
28
- url.pathname += '.html';
29
- href = url.pathname + url.search + url.hash;
24
+ if (inBrowser) {
25
+ const currentUrl = new URL(location.href);
26
+ if (href !== normalizeHref(currentUrl.href)) {
27
+ // save scroll position before changing url
28
+ history.replaceState({ scrollPosition: window.scrollY }, document.title);
29
+ history.pushState(null, '', href);
30
+ if (new URL(href, fakeHost).hash !== currentUrl.hash) {
31
+ window.dispatchEvent(new Event('hashchange'));
32
+ }
30
33
  }
31
34
  }
32
- updateHistory(href);
33
35
  await loadPage(href);
34
36
  await router.onAfterRouteChanged?.(href);
35
37
  }
@@ -124,24 +126,22 @@ export function createRouter(loadPageModule, fallbackComponent) {
124
126
  const { href, origin, pathname, hash, search } = new URL(link.href instanceof SVGAnimatedString
125
127
  ? link.href.animVal
126
128
  : link.href, link.baseURI);
127
- const currentUrl = window.location;
128
- const extMatch = pathname.match(/\.\w+$/);
129
- // only intercept inbound links
129
+ const currentUrl = new URL(location.href); // copy to keep old data
130
+ // only intercept inbound html links
130
131
  if (!e.ctrlKey &&
131
132
  !e.shiftKey &&
132
133
  !e.altKey &&
133
134
  !e.metaKey &&
134
135
  !target &&
135
136
  origin === currentUrl.origin &&
136
- // don't intercept if non-html extension is present
137
- !(extMatch && extMatch[0] !== '.html')) {
137
+ treatAsHtml(pathname)) {
138
138
  e.preventDefault();
139
139
  if (pathname === currentUrl.pathname &&
140
140
  search === currentUrl.search) {
141
141
  // scroll between hash anchors in the same page
142
142
  // avoid duplicate history entries when the hash is same
143
143
  if (hash !== currentUrl.hash) {
144
- history.pushState(null, '', hash);
144
+ history.pushState(null, '', href);
145
145
  // still emit the event so we can listen to it in themes
146
146
  window.dispatchEvent(new Event('hashchange'));
147
147
  }
@@ -150,7 +150,6 @@ export function createRouter(loadPageModule, fallbackComponent) {
150
150
  scrollTo(link, hash, link.classList.contains('header-anchor'));
151
151
  }
152
152
  else {
153
- updateHistory(href);
154
153
  window.scrollTo(0, 0);
155
154
  }
156
155
  }
@@ -160,8 +159,9 @@ export function createRouter(loadPageModule, fallbackComponent) {
160
159
  }
161
160
  }
162
161
  }, { capture: true });
163
- window.addEventListener('popstate', (e) => {
164
- loadPage(location.href, (e.state && e.state.scrollPosition) || 0);
162
+ window.addEventListener('popstate', async (e) => {
163
+ await loadPage(normalizeHref(location.href), (e.state && e.state.scrollPosition) || 0);
164
+ router.onAfterRouteChanged?.(location.href);
165
165
  });
166
166
  window.addEventListener('hashchange', (e) => {
167
167
  e.preventDefault();
@@ -191,32 +191,10 @@ export function scrollTo(el, hash, smooth = false) {
191
191
  console.warn(e);
192
192
  }
193
193
  if (target) {
194
- let scrollOffset = siteDataRef.value.scrollOffset;
195
- let offset = 0;
196
- let padding = 24;
197
- if (typeof scrollOffset === 'object' && 'padding' in scrollOffset) {
198
- padding = scrollOffset.padding;
199
- scrollOffset = scrollOffset.selector;
200
- }
201
- if (typeof scrollOffset === 'number') {
202
- offset = scrollOffset;
203
- }
204
- else if (typeof scrollOffset === 'string') {
205
- offset = tryOffsetSelector(scrollOffset, padding);
206
- }
207
- else if (Array.isArray(scrollOffset)) {
208
- for (const selector of scrollOffset) {
209
- const res = tryOffsetSelector(selector, padding);
210
- if (res) {
211
- offset = res;
212
- break;
213
- }
214
- }
215
- }
216
194
  const targetPadding = parseInt(window.getComputedStyle(target).paddingTop, 10);
217
195
  const targetTop = window.scrollY +
218
196
  target.getBoundingClientRect().top -
219
- offset +
197
+ getScrollOffset() +
220
198
  targetPadding;
221
199
  function scrollToTarget() {
222
200
  // only smooth scroll if distance is smaller than screen height.
@@ -228,15 +206,6 @@ export function scrollTo(el, hash, smooth = false) {
228
206
  requestAnimationFrame(scrollToTarget);
229
207
  }
230
208
  }
231
- function tryOffsetSelector(selector, padding) {
232
- const el = document.querySelector(selector);
233
- if (!el)
234
- return 0;
235
- const bot = el.getBoundingClientRect().bottom;
236
- if (bot < 0)
237
- return 0;
238
- return bot + padding;
239
- }
240
209
  function handleHMR(route) {
241
210
  // update route.data on HMR updates of active page
242
211
  if (import.meta.hot) {
@@ -249,16 +218,19 @@ function handleHMR(route) {
249
218
  }
250
219
  }
251
220
  function shouldHotReload(payload) {
252
- const payloadPath = payload.path.replace(/(\bindex)?\.md$/, '');
221
+ const payloadPath = payload.path.replace(/(?:(^|\/)index)?\.md$/, '$1');
253
222
  const locationPath = location.pathname
254
- .replace(/(\bindex)?\.html$/, '')
223
+ .replace(/(?:(^|\/)index)?\.html$/, '')
255
224
  .slice(siteDataRef.value.base.length - 1);
256
225
  return payloadPath === locationPath;
257
226
  }
258
- function updateHistory(href) {
259
- if (inBrowser && href !== location.href) {
260
- // save scroll position before changing url
261
- history.replaceState({ scrollPosition: window.scrollY }, document.title);
262
- history.pushState(null, '', href);
263
- }
227
+ function normalizeHref(href) {
228
+ const url = new URL(href, fakeHost);
229
+ url.pathname = url.pathname.replace(/(^|\/)index(\.html)?$/, '$1');
230
+ // ensure correct deep link so page refresh lands on correct files.
231
+ if (siteDataRef.value.cleanUrls)
232
+ url.pathname = url.pathname.replace(/\.html$/, '');
233
+ else if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html'))
234
+ url.pathname += '.html';
235
+ return url.pathname + url.search + url.hash;
264
236
  }
@@ -83,3 +83,37 @@ export function defineClientComponent(loader, args, cb) {
83
83
  }
84
84
  };
85
85
  }
86
+ export function getScrollOffset() {
87
+ let scrollOffset = siteDataRef.value.scrollOffset;
88
+ let offset = 0;
89
+ let padding = 24;
90
+ if (typeof scrollOffset === 'object' && 'padding' in scrollOffset) {
91
+ padding = scrollOffset.padding;
92
+ scrollOffset = scrollOffset.selector;
93
+ }
94
+ if (typeof scrollOffset === 'number') {
95
+ offset = scrollOffset;
96
+ }
97
+ else if (typeof scrollOffset === 'string') {
98
+ offset = tryOffsetSelector(scrollOffset, padding);
99
+ }
100
+ else if (Array.isArray(scrollOffset)) {
101
+ for (const selector of scrollOffset) {
102
+ const res = tryOffsetSelector(selector, padding);
103
+ if (res) {
104
+ offset = res;
105
+ break;
106
+ }
107
+ }
108
+ }
109
+ return offset;
110
+ }
111
+ function tryOffsetSelector(selector, padding) {
112
+ const el = document.querySelector(selector);
113
+ if (!el)
114
+ return 0;
115
+ const bot = el.getBoundingClientRect().bottom;
116
+ if (bot < 0)
117
+ return 0;
118
+ return bot + padding;
119
+ }
@@ -61,9 +61,9 @@ interface VitePressData<T = any> {
61
61
  title: Ref<string>;
62
62
  description: Ref<string>;
63
63
  lang: Ref<string>;
64
- isDark: Ref<boolean>;
65
64
  dir: Ref<string>;
66
65
  localeIndex: Ref<string>;
66
+ isDark: Ref<boolean>;
67
67
  }
68
68
  declare function useData<T = any>(): VitePressData<T>;
69
69
 
@@ -100,6 +100,7 @@ declare function defineClientComponent(loader: AsyncComponentLoader, args?: any[
100
100
  [key: string]: any;
101
101
  }> | null;
102
102
  };
103
+ declare function getScrollOffset(): number;
103
104
 
104
105
  declare const Content: vue.DefineComponent<{
105
106
  as: {
@@ -108,7 +109,7 @@ declare const Content: vue.DefineComponent<{
108
109
  };
109
110
  }, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
110
111
  [key: string]: any;
111
- }>, unknown, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.VNodeProps & vue.AllowedComponentProps & vue.ComponentCustomProps, Readonly<vue.ExtractPropTypes<{
112
+ }>, unknown, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
112
113
  as: {
113
114
  type: (ObjectConstructor | StringConstructor)[];
114
115
  default: string;
@@ -117,4 +118,4 @@ declare const Content: vue.DefineComponent<{
117
118
  as: string | Record<string, any>;
118
119
  }, {}>;
119
120
 
120
- export { Content, type EnhanceAppContext, type Route, type Router, type Theme, type VitePressData, dataSymbol, defineClientComponent, inBrowser, onContentUpdated, useData, useRoute, useRouter, withBase };
121
+ export { Content, type EnhanceAppContext, type Route, type Router, type Theme, type VitePressData, dataSymbol, defineClientComponent, getScrollOffset, inBrowser, onContentUpdated, useData, useRoute, useRouter, withBase };
@@ -1,9 +1,9 @@
1
1
  // exports in this file are exposed to themes and md files via 'vitepress'
2
- // so the user can do `import { useRoute, useSiteData } from 'vitepress'`
2
+ // so the user can do `import { useRoute, useData } from 'vitepress'`
3
3
  // composables
4
4
  export { useData, dataSymbol } from './app/data';
5
5
  export { useRoute, useRouter } from './app/router';
6
6
  // utilities
7
- export { inBrowser, onContentUpdated, defineClientComponent, withBase } from './app/utils';
7
+ export { inBrowser, onContentUpdated, defineClientComponent, withBase, getScrollOffset } from './app/utils';
8
8
  // components
9
9
  export { Content } from './app/components/Content';
@@ -1,7 +1,8 @@
1
- export const EXTERNAL_URL_RE = /^[a-z]+:/i;
1
+ export const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i;
2
2
  export const APPEARANCE_KEY = 'vitepress-theme-appearance';
3
- export const HASH_RE = /#.*$/;
4
- export const EXT_RE = /(index)?\.(md|html)$/;
3
+ const HASH_RE = /#.*$/;
4
+ const HASH_OR_QUERY_RE = /[?#].*$/;
5
+ const INDEX_OR_EXT_RE = /(?:(^|\/)index)?\.(?:md|html)$/;
5
6
  export const inBrowser = typeof document !== 'undefined';
6
7
  export const notFoundPageData = {
7
8
  relativePath: '',
@@ -30,8 +31,10 @@ export function isActive(currentPath, matchPath, asRegex = false) {
30
31
  }
31
32
  return true;
32
33
  }
33
- export function normalize(path) {
34
- return decodeURI(path).replace(HASH_RE, '').replace(EXT_RE, '');
34
+ function normalize(path) {
35
+ return decodeURI(path)
36
+ .replace(HASH_OR_QUERY_RE, '')
37
+ .replace(INDEX_OR_EXT_RE, '$1');
35
38
  }
36
39
  export function isExternal(path) {
37
40
  return EXTERNAL_URL_RE.test(path);
@@ -67,6 +70,9 @@ export function createTitle(siteData, pageData) {
67
70
  return template.replace(/:title/g, title);
68
71
  }
69
72
  const templateString = createTitleTemplate(siteData.title, template);
73
+ if (title === templateString.slice(3)) {
74
+ return title;
75
+ }
70
76
  return `${title}${templateString}`;
71
77
  }
72
78
  function createTitleTemplate(siteTitle, template) {
@@ -108,3 +114,26 @@ export function sanitizeFileName(name) {
108
114
  export function slash(p) {
109
115
  return p.replace(/\\/g, '/');
110
116
  }
117
+ const KNOWN_EXTENSIONS = new Set();
118
+ export function treatAsHtml(filename) {
119
+ if (KNOWN_EXTENSIONS.size === 0) {
120
+ const extraExts = (typeof process === 'object' && process.env?.VITE_EXTRA_EXTENSIONS) ||
121
+ import.meta.env?.VITE_EXTRA_EXTENSIONS ||
122
+ '';
123
+ ('3g2,3gp,aac,ai,apng,au,avif,bin,bmp,cer,class,conf,crl,css,csv,dll,' +
124
+ 'doc,eps,epub,exe,gif,gz,ics,ief,jar,jpe,jpeg,jpg,js,json,jsonld,m4a,' +
125
+ 'man,mid,midi,mjs,mov,mp2,mp3,mp4,mpe,mpeg,mpg,mpp,oga,ogg,ogv,ogx,' +
126
+ 'opus,otf,p10,p7c,p7m,p7s,pdf,png,ps,qt,roff,rtf,rtx,ser,svg,t,tif,' +
127
+ 'tiff,tr,ts,tsv,ttf,txt,vtt,wav,weba,webm,webp,woff,woff2,xhtml,xml,' +
128
+ 'yaml,yml,zip' +
129
+ (extraExts && typeof extraExts === 'string' ? ',' + extraExts : ''))
130
+ .split(',')
131
+ .forEach((ext) => KNOWN_EXTENSIONS.add(ext));
132
+ }
133
+ const ext = filename.split('.').pop();
134
+ return ext == null || !KNOWN_EXTENSIONS.has(ext.toLowerCase());
135
+ }
136
+ // https://github.com/sindresorhus/escape-string-regexp/blob/ba9a4473850cb367936417e97f1f2191b7cc67dd/index.js
137
+ export function escapeRegExp(str) {
138
+ return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d');
139
+ }
@@ -35,7 +35,7 @@ provide('hero-image-slot-exists', heroImageSlotExists)
35
35
  <slot name="layout-top" />
36
36
  <VPSkipLink />
37
37
  <VPBackdrop class="backdrop" :show="isSidebarOpen" @click="closeSidebar" />
38
- <VPNav v-if="frontmatter.navbar !== false">
38
+ <VPNav>
39
39
  <template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
40
40
  <template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
41
41
  <template #nav-bar-content-before><slot name="nav-bar-content-before" /></template>
@@ -56,7 +56,10 @@ provide('hero-image-slot-exists', heroImageSlotExists)
56
56
 
57
57
  <template #not-found><slot name="not-found" /></template>
58
58
  <template #home-hero-before><slot name="home-hero-before" /></template>
59
+ <template #home-hero-info-before><slot name="home-hero-info-before" /></template>
59
60
  <template #home-hero-info><slot name="home-hero-info" /></template>
61
+ <template #home-hero-info-after><slot name="home-hero-info-after" /></template>
62
+ <template #home-hero-actions-after><slot name="home-hero-actions-after" /></template>
60
63
  <template #home-hero-image><slot name="home-hero-image" /></template>
61
64
  <template #home-hero-after><slot name="home-hero-after" /></template>
62
65
  <template #home-features-before><slot name="home-features-before" /></template>