diff --git a/__tests__/client/theme-default/support/sideBar.spec.ts b/__tests__/client/theme-default/support/sideBar.spec.ts new file mode 100644 index 000000000000..a1d0ea825d01 --- /dev/null +++ b/__tests__/client/theme-default/support/sideBar.spec.ts @@ -0,0 +1,123 @@ +import { + getSideBarConfig, + getFlatSideBarLinks +} from 'client/theme-default/support/sideBar' + +describe('client/theme-default/support/sideBar', () => { + it('gets the correct sidebar items', () => { + expect(getSideBarConfig(false, '')).toEqual(false) + expect(getSideBarConfig('auto', '')).toEqual('auto') + + const sidebar = [{ text: 'Title 01', link: 'title-01' }] + const expected = [{ text: 'Title 01', link: 'title-01' }] + + expect(getSideBarConfig(sidebar, '')).toEqual(expected) + }) + + it('gets the correct sidebar items from the given path', () => { + const sidebar = { + '/': [{ text: 'R', link: 'r' }], + '/guide/': [{ text: 'G', link: 'g' }] + } + + expect(getSideBarConfig(sidebar, '/')).toEqual(sidebar['/']) + expect(getSideBarConfig(sidebar, '/guide/')).toEqual(sidebar['/guide/']) + }) + + it('gets the correct sidebar items with various combination', () => { + const s = { + '/guide/': [{ text: 'G', link: 'g' }], + api: [{ text: 'A', link: 'a' }] + } + + expect(getSideBarConfig(s, '/guide/')).toEqual(s['/guide/']) + expect(getSideBarConfig(s, '/guide')).toEqual(s['/guide/']) + expect(getSideBarConfig(s, 'guide/')).toEqual(s['/guide/']) + expect(getSideBarConfig(s, 'guide/nested')).toEqual(s['/guide/']) + expect(getSideBarConfig(s, '/guide/nested')).toEqual(s['/guide/']) + expect(getSideBarConfig(s, 'guide/nested/')).toEqual(s['/guide/']) + expect(getSideBarConfig(s, '/api/')).toEqual(s['api']) + expect(getSideBarConfig(s, '/api')).toEqual(s['api']) + expect(getSideBarConfig(s, 'api/')).toEqual(s['api']) + expect(getSideBarConfig(s, 'api/nested')).toEqual(s['api']) + expect(getSideBarConfig(s, '/api/nested')).toEqual(s['api']) + expect(getSideBarConfig(s, 'api/nested/')).toEqual(s['api']) + expect(getSideBarConfig(s, '/')).toEqual('auto') + }) + + it('creates flat sidebar links', () => { + const sidebar = [ + { text: 'Title 01', link: '/title-01' }, + { text: 'Title 02', link: '/title-02' }, + { text: 'Title 03', link: '/title-03' } + ] + + const expected = [ + { text: 'Title 01', link: '/title-01' }, + { text: 'Title 02', link: '/title-02' }, + { text: 'Title 03', link: '/title-03' } + ] + + expect(getFlatSideBarLinks(sidebar)).toEqual(expected) + }) + + it('creates flat sidebar links with mixed sidebar group', () => { + const sidebar = [ + { + text: 'Title 01', + link: '/title-01', + children: [ + { text: 'Children 01', link: '/children-01' }, + { text: 'Children 02', link: '/children-02' } + ] + }, + { text: 'Title 02', link: '/title-02' }, + { text: 'Title 03', link: '/title-03' } + ] + + const expected = [ + { text: 'Title 01', link: '/title-01' }, + { text: 'Children 01', link: '/children-01' }, + { text: 'Children 02', link: '/children-02' }, + { text: 'Title 02', link: '/title-02' }, + { text: 'Title 03', link: '/title-03' } + ] + + expect(getFlatSideBarLinks(sidebar)).toEqual(expected) + }) + + it('ignores any items with no `link` property', () => { + const sidebar = [ + { + text: 'Title 01', + children: [ + { text: 'Children 01', link: '/children-01' }, + { text: 'Children 02', link: '/children-02' } + ] + }, + { text: 'Title 02', link: '/title-02' } + ] + + const expected = [ + { text: 'Children 01', link: '/children-01' }, + { text: 'Children 02', link: '/children-02' }, + { text: 'Title 02', link: '/title-02' } + ] + + expect(getFlatSideBarLinks(sidebar)).toEqual(expected) + }) + + it('removes `.md` or `.html` extention', () => { + const sidebar = [ + { text: 'Title 01', link: '/title-01.md' }, + { text: 'Title 02', link: '/title-02.html' } + ] + + const expected = [ + { text: 'Title 01', link: '/title-01' }, + { text: 'Title 02', link: '/title-02' } + ] + + expect(getFlatSideBarLinks(sidebar)).toEqual(expected) + }) +}) diff --git a/__tests__/client/theme-default/utils.spec.ts b/__tests__/client/theme-default/utils.spec.ts index 19392ba47e3b..52f01d4ab953 100644 --- a/__tests__/client/theme-default/utils.spec.ts +++ b/__tests__/client/theme-default/utils.spec.ts @@ -19,4 +19,23 @@ describe('client/theme-default/utils', () => { expect(Utils.ensureEndingSlash('path/page.html')).toBe('path/page.html') }) }) + + describe('removeExtention', () => { + it('removes `.md` or `.html` extention from the path', () => { + expect(Utils.removeExtention('/')).toBe('/') + expect(Utils.removeExtention('index')).toBe('/') + expect(Utils.removeExtention('index.md')).toBe('/') + expect(Utils.removeExtention('index.html')).toBe('/') + expect(Utils.removeExtention('/index')).toBe('/') + expect(Utils.removeExtention('/index.md')).toBe('/') + expect(Utils.removeExtention('/index.html')).toBe('/') + expect(Utils.removeExtention('path')).toBe('path') + expect(Utils.removeExtention('path.md')).toBe('path') + expect(Utils.removeExtention('path.html')).toBe('path') + expect(Utils.removeExtention('path/')).toBe('path/') + expect(Utils.removeExtention('path/nested.md')).toBe('path/nested') + expect(Utils.removeExtention('path/nested.html')).toBe('path/nested') + expect(Utils.removeExtention('path/nested/index')).toBe('path/nested/') + }) + }) }) diff --git a/src/client/theme-default/composables/nextAndPrevLinks.ts b/src/client/theme-default/composables/nextAndPrevLinks.ts index ab784e5605f9..0efb7dceabfe 100644 --- a/src/client/theme-default/composables/nextAndPrevLinks.ts +++ b/src/client/theme-default/composables/nextAndPrevLinks.ts @@ -1,44 +1,41 @@ import { computed } from 'vue' import { useSiteDataByRoute, usePageData } from 'vitepress' -import { isArray, getPathDirName, ensureStartingSlash } from '../utils' -import { DefaultTheme } from '../config' +import { isArray, ensureStartingSlash, removeExtention } from '../utils' +import { getSideBarConfig, getFlatSideBarLinks } from '../support/sideBar' export function useNextAndPrevLinks() { const site = useSiteDataByRoute() const page = usePageData() - const candidates = computed(() => { - const path = ensureStartingSlash(page.value.relativePath) - const sidebar = site.value.themeConfig.sidebar - - return getFlatSidebarLinks(path, sidebar) + const path = computed(() => { + return removeExtention(ensureStartingSlash(page.value.relativePath)) }) - const currentPath = computed(() => { - const path = ensureStartingSlash(page.value.relativePath) + const candidates = computed(() => { + const config = getSideBarConfig(site.value.themeConfig.sidebar, path.value) - return path.replace(/(index)?\.(md|html)$/, '') + return isArray(config) ? getFlatSideBarLinks(config) : [] }) - const currentIndex = computed(() => { + const index = computed(() => { return candidates.value.findIndex((item) => { - return item.link === currentPath.value + return item.link === path.value }) }) const next = computed(() => { if ( site.value.themeConfig.nextLinks !== false && - currentIndex.value > -1 && - currentIndex.value < candidates.value.length - 1 + index.value > -1 && + index.value < candidates.value.length - 1 ) { - return candidates.value[currentIndex.value + 1] + return candidates.value[index.value + 1] } }) const prev = computed(() => { - if (site.value.themeConfig.prevLinks !== false && currentIndex.value > 0) { - return candidates.value[currentIndex.value - 1] + if (site.value.themeConfig.prevLinks !== false && index.value > 0) { + return candidates.value[index.value - 1] } }) @@ -50,53 +47,3 @@ export function useNextAndPrevLinks() { hasLinks } } - -function getFlatSidebarLinks( - path: string, - sidebar?: DefaultTheme.SideBarConfig -): DefaultTheme.SideBarLink[] { - if (!sidebar || sidebar === 'auto') { - return [] - } - - return isArray(sidebar) - ? getFlatSidebarLinksFromArray(path, sidebar) - : getFlatSidebarLinksFromObject(path, sidebar) -} - -function getFlatSidebarLinksFromArray( - path: string, - sidebar: DefaultTheme.SideBarItem[] -): DefaultTheme.SideBarLink[] { - return sidebar.reduce((links, item) => { - if (item.link) { - links.push({ text: item.text, link: item.link }) - } - - if (isSideBarGroup(item)) { - links = [...links, ...getFlatSidebarLinks(path, item.children)] - } - - return links - }, []) -} - -function getFlatSidebarLinksFromObject( - path: string, - sidebar: DefaultTheme.MultiSideBarConfig -): DefaultTheme.SideBarLink[] { - const paths = [path, Object.keys(sidebar)[0]] - const item = paths.map((p) => sidebar[getPathDirName(p)]).find(Boolean) - - if (isArray(item)) { - return getFlatSidebarLinksFromArray(path, item) - } - - return [] -} - -function isSideBarGroup( - item: DefaultTheme.SideBarItem -): item is DefaultTheme.SideBarGroup { - return (item as DefaultTheme.SideBarGroup).children !== undefined -} diff --git a/src/client/theme-default/support/sideBar.ts b/src/client/theme-default/support/sideBar.ts new file mode 100644 index 000000000000..d0697181448a --- /dev/null +++ b/src/client/theme-default/support/sideBar.ts @@ -0,0 +1,70 @@ +import { DefaultTheme } from '../config' +import { + isArray, + ensureSlash, + ensureStartingSlash, + removeExtention +} from '../utils' + +export function isSideBarConfig( + sidebar: DefaultTheme.SideBarConfig | DefaultTheme.MultiSideBarConfig +): sidebar is DefaultTheme.SideBarConfig { + return sidebar === false || sidebar === 'auto' || isArray(sidebar) +} + +export function isSideBarGroup( + item: DefaultTheme.SideBarItem +): item is DefaultTheme.SideBarGroup { + return (item as DefaultTheme.SideBarGroup).children !== undefined +} + +/** + * Get the `SideBarConfig` from sidebar option. This method will ensure to get + * correct sidebar config from `MultiSideBarConfig` with various path + * combinations such as matching `guide/` and `/guide/`. If no matching config + * was found, it will return `auto` as a fallback. + */ +export function getSideBarConfig( + sidebar: DefaultTheme.SideBarConfig | DefaultTheme.MultiSideBarConfig, + path: string +): DefaultTheme.SideBarConfig { + if (isSideBarConfig(sidebar)) { + return sidebar + } + + // get the very first segment of the path to compare with nulti sidebar keys + // and make sure it's surrounded by slash + path = ensureStartingSlash(path).split('/')[1] || '/' + path = ensureSlash(path) + + for (const dir in sidebar) { + // make sure the multi sidebar key is surrounded by slash too + if (path === ensureSlash(dir)) { + return sidebar[dir] + } + } + + return 'auto' +} + +/** + * Get flat sidebar links from the sidebar items. This method is useful for + * creating the "next and prev link" feature. It will ignore any items that + * don't have `link` property and removes `.md` or `.html` extension if a + * link contains it. + */ +export function getFlatSideBarLinks( + sidebar: DefaultTheme.SideBarItem[] +): DefaultTheme.SideBarLink[] { + return sidebar.reduce((links, item) => { + if (item.link) { + links.push({ text: item.text, link: removeExtention(item.link) }) + } + + if (isSideBarGroup(item)) { + links = [...links, ...getFlatSideBarLinks(item.children)] + } + + return links + }, []) +} diff --git a/src/client/theme-default/utils.ts b/src/client/theme-default/utils.ts index 2f8b4bd44566..16ccc8241231 100644 --- a/src/client/theme-default/utils.ts +++ b/src/client/theme-default/utils.ts @@ -66,6 +66,10 @@ export function getPathDirName(path: string): string { return ensureEndingSlash(segments.join('/')) } +export function ensureSlash(path: string): string { + return ensureEndingSlash(ensureStartingSlash(path)) +} + export function ensureStartingSlash(path: string): string { return /^\//.test(path) ? path : `/${path}` } @@ -73,3 +77,11 @@ export function ensureStartingSlash(path: string): string { export function ensureEndingSlash(path: string): string { return /(\.html|\/)$/.test(path) ? path : `${path}/` } + +/** + * Remove `.md` or `.html` extention from the given path. It also converts + * `index` to slush. + */ +export function removeExtention(path: string): string { + return path.replace(/(index)?(\.(md|html))?$/, '') || '/' +}