diff --git a/__tests__/unit/client/theme-default/composables/langs.test.ts b/__tests__/unit/client/theme-default/composables/langs.test.ts index cf9760c1..39fe021c 100644 --- a/__tests__/unit/client/theme-default/composables/langs.test.ts +++ b/__tests__/unit/client/theme-default/composables/langs.test.ts @@ -3,13 +3,34 @@ import type { Route, VitePressData } from 'vitepress' import type { DefaultTheme } from 'vitepress/theme' import { ref } from 'vue' -function createData( - themeConfig: DefaultTheme.Config, - relativePath = 'guide/getting-started.md', - cleanUrls = false, - hash = '#install' +// `currentPage` is the current page's relative path (like +// `route.data.relativePath`, but with a leading slash), plus any query and +// hash of the current URL. +function resolve( + currentPage: string, + { + themeConfig = {}, + cleanUrls = false, + targetLocale = 'fr', + targetLocaleLink = '/fr/', + currentLocaleLink = '/', + linkToCorrespondingPage = true + }: { + themeConfig?: DefaultTheme.Config + cleanUrls?: boolean + targetLocale?: string + targetLocaleLink?: string + currentLocaleLink?: string + linkToCorrespondingPage?: boolean + } = {} ) { - return { + const { pathname, search, hash } = new URL(currentPage, 'http://a.com') + const route = { + data: { relativePath: pathname.slice(1) }, + query: search, + hash + } as Route + const data = { site: ref({ cleanUrls, locales: { @@ -18,61 +39,108 @@ function createData( }, themeConfig }), - page: ref({ relativePath }), - theme: ref(themeConfig), - hash: ref(hash) + theme: ref(themeConfig) } as unknown as VitePressData -} -function createRoute(query = '', hash = '#install') { - return { - query, - hash - } as unknown as Route + return resolveLocaleLink(data, route, { + targetLocale, + targetLocaleLink, + currentLocaleLink, + linkToCorrespondingPage + }) } describe('client/theme-default/composables/langs', () => { - test('resolves corresponding links with the default router', () => { - expect( - resolveLocaleLink(createData({}), createRoute(), 'fr', '/fr/', '/', true) - ).toBe('/fr/guide/getting-started.html#install') - }) - - test('resolves clean index links with the default router', () => { - expect( - resolveLocaleLink( - createData({}, 'en/guide/index.md', true, '#intro'), - createRoute('?query', '#intro'), - 'fr', - '/fr/', - '/en/', - true - ) - ).toBe('/fr/guide/?query#intro') - }) + describe('resolveLocaleLink', () => { + describe('locale home links (linkToCorrespondingPage: false)', () => { + test('links to the target locale home', () => { + expect( + resolve('/guide/getting-started.md', { + linkToCorrespondingPage: false + }) + ).toBe('/fr/') + }) - test('keeps locale root links when i18n routing is disabled', () => { - expect( - resolveLocaleLink( - createData({ i18nRouting: false }), - createRoute(), - 'fr', - '/fr/', - '/', - true - ) - ).toBe('/fr/#install') - }) + test('preserves query and hash', () => { + expect( + resolve('/guide/getting-started.md?a=1#install', { + linkToCorrespondingPage: false + }) + ).toBe('/fr/?a=1#install') + }) - test('uses custom i18n routing functions for corresponding links', () => { - const data = createData({ - i18nRouting(data, hash, targetLocale) { - return `${data.site.value.locales[targetLocale].link}mapped/${data.page.value.relativePath}${hash}` - } + test('ignores custom i18n routing functions', () => { + expect( + resolve('/guide/getting-started.md', { + linkToCorrespondingPage: false, + themeConfig: { i18nRouting: () => '/custom/' } + }) + ).toBe('/fr/') + }) }) - expect( - resolveLocaleLink(data, createRoute(), 'fr', '/fr/', '/', true) - ).toBe('/fr/mapped/guide/getting-started.md#install') + describe('corresponding page links (linkToCorrespondingPage: true)', () => { + test('rewrites the current page path into the target locale', () => { + expect(resolve('/guide/getting-started.md#install')).toBe( + '/fr/guide/getting-started.html#install' + ) + }) + + test('drops the .html extension when clean URLs are enabled', () => { + expect( + resolve('/guide/getting-started.md#install', { cleanUrls: true }) + ).toBe('/fr/guide/getting-started#install') + }) + + test('resolves index pages to directory links', () => { + expect(resolve('/guide/index.md')).toBe('/fr/guide/') + expect(resolve('/guide/index.md', { cleanUrls: true })).toBe( + '/fr/guide/' + ) + }) + + test('resolves the site root page to the target locale home', () => { + expect(resolve('/index.md')).toBe('/fr/') + }) + + test('strips the current locale prefix before rewriting', () => { + expect( + resolve('/en/guide/index.md?query#intro', { + currentLocaleLink: '/en/', + cleanUrls: true + }) + ).toBe('/fr/guide/?query#intro') + }) + + test('rewrites into the root locale', () => { + expect( + resolve('/fr/guide/getting-started.md#install', { + targetLocale: 'root', + targetLocaleLink: '/', + currentLocaleLink: '/fr/' + }) + ).toBe('/guide/getting-started.html#install') + }) + + test('links to the target locale home when i18n routing is disabled', () => { + expect( + resolve('/guide/getting-started.md#install', { + themeConfig: { i18nRouting: false } + }) + ).toBe('/fr/#install') + }) + + test('delegates to custom i18n routing functions', () => { + expect( + resolve('/guide/getting-started.md#install', { + themeConfig: { + i18nRouting(data, route, targetLocale) { + return `${data.site.value.locales[targetLocale].link}mapped/${route.data.relativePath}${route.hash}` + } + } + }) + ).toBe('/fr/mapped/guide/getting-started.md#install') + }) + }) }) }) diff --git a/__tests__/unit/client/theme-default/support/sidebar.test.ts b/__tests__/unit/client/theme-default/support/sidebar.test.ts index 7232757c..c5a4d44f 100644 --- a/__tests__/unit/client/theme-default/support/sidebar.test.ts +++ b/__tests__/unit/client/theme-default/support/sidebar.test.ts @@ -188,8 +188,8 @@ describe('client/theme-default/support/sidebar', () => { ] } - expect(hasActiveLink('active-1', item)).toBe(true) - expect(hasActiveLink('inactive', item)).toBe(false) + expect(hasActiveLink('active-1', '', item)).toBe(true) + expect(hasActiveLink('inactive', '', item)).toBe(false) }) test('checks `SidebarItem[]`', () => { @@ -210,9 +210,9 @@ describe('client/theme-default/support/sidebar', () => { } ] - expect(hasActiveLink('active-1', item)).toBe(true) - expect(hasActiveLink('active-3', item)).toBe(true) - expect(hasActiveLink('inactive', item)).toBe(false) + expect(hasActiveLink('active-1', '', item)).toBe(true) + expect(hasActiveLink('active-3', '', item)).toBe(true) + expect(hasActiveLink('inactive', '', item)).toBe(false) }) }) }) diff --git a/docs/en/reference/default-theme-config.md b/docs/en/reference/default-theme-config.md index cc4ffd0e..02c221ec 100644 --- a/docs/en/reference/default-theme-config.md +++ b/docs/en/reference/default-theme-config.md @@ -25,23 +25,23 @@ export default { ## i18nRouting -- Type: `boolean | ((data: VitePressData, hash: string, targetLocale: string) => string)` +- Type: `boolean | ((data: VitePressData, route: Route, targetLocale: string) => string)` Changing locale to say `zh` will change the URL from `/foo` (or `/en/foo/`) to `/zh/foo`. You can disable this behavior by setting `themeConfig.i18nRouting` to `false`. -Set `themeConfig.i18nRouting` to a function to customize the locale link. The function receives the current VitePress data, the current hash, and the target locale key, and returns the target link. +Set `themeConfig.i18nRouting` to a function to customize the locale link. The function receives the current VitePress data, the current route, and the target locale key, and returns the target link. ```ts import { defineConfig } from 'vitepress' export default defineConfig({ themeConfig: { - i18nRouting(data, hash, targetLocale) { + i18nRouting(data, route, targetLocale) { const target = data.site.value.locales[targetLocale] const targetLink = target.link || (targetLocale === 'root' ? '/' : `/${targetLocale}/`) - return `${targetLink}${data.page.value.relativePath.replace(/\.md$/, '')}${hash}` + return `${targetLink}${route.data.relativePath.replace(/\.md$/, '')}${route.hash}` } } }) diff --git a/docs/ru/reference/default-theme-config.md b/docs/ru/reference/default-theme-config.md index 796b5bb4..afd17954 100644 --- a/docs/ru/reference/default-theme-config.md +++ b/docs/ru/reference/default-theme-config.md @@ -25,23 +25,23 @@ export default { ## i18nRouting -- Тип: `boolean | ((data: VitePressData, hash: string, targetLocale: string) => string)` +- Тип: `boolean | ((data: VitePressData, route: Route, targetLocale: string) => string)` При смене локали на `ru` URL изменится с `/foo` (или `/en/foo/`) на `/ru/foo`. Вы можете отключить это поведение, установив для параметра `themeConfig.i18nRouting` значение `false`. -Установите для `themeConfig.i18nRouting` функцию, чтобы настроить ссылки для переключения локали. Эта функция получает текущие данные VitePress, текущий хеш и ключ целевой локали, а затем возвращает ссылку для перехода на неё. +Установите для `themeConfig.i18nRouting` функцию, чтобы настроить ссылку локали. Эта функция получает текущие данные VitePress, текущий маршрут и ключ целевой локали, а затем возвращает целевую ссылку. ```ts import { defineConfig } from 'vitepress' export default defineConfig({ themeConfig: { - i18nRouting(data, hash, targetLocale) { + i18nRouting(data, route, targetLocale) { const target = data.site.value.locales[targetLocale] const targetLink = target.link || (targetLocale === 'root' ? '/' : `/${targetLocale}/`) - return `${targetLink}${data.page.value.relativePath.replace(/\.md$/, '')}${hash}` + return `${targetLink}${route.data.relativePath.replace(/\.md$/, '')}${route.hash}` } } }) diff --git a/src/client/app/components/Content.ts b/src/client/app/components/Content.ts index 12456451..d21ed2aa 100644 --- a/src/client/app/components/Content.ts +++ b/src/client/app/components/Content.ts @@ -10,8 +10,8 @@ export const Content = defineComponent({ as: { type: [Object, String], default: 'div' } }, setup(props) { - const route = useRoute() const { frontmatter, site } = useData() + const route = useRoute() watch(frontmatter, runCbs, { deep: true, flush: 'post' }) return () => h( diff --git a/src/client/app/composables/head.ts b/src/client/app/composables/head.ts index 13fb9bb0..2b63d5cf 100644 --- a/src/client/app/composables/head.ts +++ b/src/client/app/composables/head.ts @@ -3,9 +3,9 @@ import { createTitle, mergeHead, type HeadConfig, + type Route, type SiteData } from '../../shared' -import type { Route } from '../router' export function useUpdateHead(route: Route, siteDataByRouteRef: Ref) { let isFirstUpdate = true diff --git a/src/client/app/data.ts b/src/client/app/data.ts index 89297dba..c612dbb7 100644 --- a/src/client/app/data.ts +++ b/src/client/app/data.ts @@ -6,19 +6,17 @@ import { readonly, ref, shallowRef, - watch, type InjectionKey, type Ref } from 'vue' import { APPEARANCE_KEY, createTitle, - inBrowser, resolveSiteDataByRoute, + type Route, type SiteData, type VitePressData } from '../shared' -import type { Route } from './router' export const dataSymbol: InjectionKey = Symbol() export type { VitePressData } from '../shared' @@ -48,21 +46,6 @@ export function initData(route: Route): VitePressData { }) : ref(false) - const hashRef = ref(inBrowser ? location.hash : '') - - if (inBrowser) { - window.addEventListener('hashchange', () => { - hashRef.value = location.hash - }) - } - - watch( - () => route.data, - () => { - hashRef.value = inBrowser ? location.hash : '' - } - ) - return { site, theme: computed(() => site.value.themeConfig), @@ -76,8 +59,7 @@ export function initData(route: Route): VitePressData { description: computed( () => route.data.description || site.value.description ), - isDark, - hash: computed(() => hashRef.value) + isDark } } diff --git a/src/client/app/router.ts b/src/client/app/router.ts index 5d3d86f1..818f46fb 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -1,18 +1,10 @@ import type { Component, InjectionKey } from 'vue' import { inject, markRaw, nextTick, reactive, readonly } from 'vue' -import type { Awaitable, PageData, PageDataPayload } from '../shared' +import type { Awaitable, PageData, PageDataPayload, Route } from '../shared' import { notFoundPageData, treatAsHtml } from '../shared' import { siteDataRef } from './data' import { inBrowser, withBase } from './utils' -export interface Route { - path: string - hash: string - query: string - data: PageData - component: Component | null -} - export interface Router { /** * Current route. diff --git a/src/client/index.ts b/src/client/index.ts index 47074c43..8b0bc15e 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -2,8 +2,8 @@ // so the user can do `import { useRoute, useData } from 'vitepress'` // generic types -export type { Route, Router } from './app/router' -export type { VitePressData } from './shared' +export type { Router } from './app/router' +export type { Route, VitePressData } from './shared' // theme types export type { EnhanceAppContext, Theme } from './app/theme' diff --git a/src/client/theme-default/components/VPCarbonAds.vue b/src/client/theme-default/components/VPCarbonAds.vue index b8fc77a2..ca034ccb 100644 --- a/src/client/theme-default/components/VPCarbonAds.vue +++ b/src/client/theme-default/components/VPCarbonAds.vue @@ -1,10 +1,10 @@ diff --git a/src/client/theme-default/components/VPNavBarExtra.vue b/src/client/theme-default/components/VPNavBarExtra.vue index 7201623b..1ddde9b9 100644 --- a/src/client/theme-default/components/VPNavBarExtra.vue +++ b/src/client/theme-default/components/VPNavBarExtra.vue @@ -8,7 +8,9 @@ import VPSocialLinks from './VPSocialLinks.vue' import VPSwitchAppearance from './VPSwitchAppearance.vue' const { site, theme } = useData() -const { localeLinks, currentLang } = useLangs({ correspondingLink: true }) +const { localeLinks, currentLang } = useLangs({ + linkToCorrespondingPage: true +}) const hasExtraContent = computed( () => @@ -38,6 +40,7 @@ const hasExtraContent = computed( :hreflang="locale.lang" rel="alternate" :dir="locale.dir" + data-allow-mismatch="attribute" /> diff --git a/src/client/theme-default/components/VPNavBarMenuGroup.vue b/src/client/theme-default/components/VPNavBarMenuGroup.vue index 4d014a49..61004f3f 100644 --- a/src/client/theme-default/components/VPNavBarMenuGroup.vue +++ b/src/client/theme-default/components/VPNavBarMenuGroup.vue @@ -1,19 +1,24 @@