feat: add PageFooter
This commit is contained in:
parent
b9ffac184a
commit
918fba3e5e
@ -3,6 +3,7 @@ import { usePageData } from '@vuepress/client'
|
|||||||
import type { PlumeThemePageData } from '../../shared/index.js'
|
import type { PlumeThemePageData } from '../../shared/index.js'
|
||||||
import { useDarkMode, useSidebar } from '../composables/index.js'
|
import { useDarkMode, useSidebar } from '../composables/index.js'
|
||||||
import PageAside from './PageAside.vue'
|
import PageAside from './PageAside.vue'
|
||||||
|
import PageFooter from './PageFooter.vue'
|
||||||
import PageMeta from './PageMeta.vue'
|
import PageMeta from './PageMeta.vue'
|
||||||
|
|
||||||
const { hasSidebar, hasAside } = useSidebar()
|
const { hasSidebar, hasAside } = useSidebar()
|
||||||
@ -32,6 +33,7 @@ const page = usePageData<PlumeThemePageData>()
|
|||||||
<main class="main">
|
<main class="main">
|
||||||
<PageMeta />
|
<PageMeta />
|
||||||
<Content class="plume-content" />
|
<Content class="plume-content" />
|
||||||
|
<PageFooter />
|
||||||
<PageComment :darkmode="isDark" />
|
<PageComment :darkmode="isDark" />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
190
theme/src/client/components/PageFooter.vue
Normal file
190
theme/src/client/components/PageFooter.vue
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useContributors, useEditNavLink, useLastUpdated, usePageNav, useThemeLocaleData } from '../composables/index.js'
|
||||||
|
import AutoLink from './AutoLink.vue'
|
||||||
|
import IconEdit from './icons/IconEdit.vue'
|
||||||
|
|
||||||
|
const themeLocale = useThemeLocaleData()
|
||||||
|
const editNavLink = useEditNavLink()
|
||||||
|
const lastUpdated = useLastUpdated()
|
||||||
|
const contributors = useContributors()
|
||||||
|
const { prev, next } = usePageNav()
|
||||||
|
|
||||||
|
const showFooter = computed(() => {
|
||||||
|
return editNavLink.value || lastUpdated.value || contributors.value || prev.value || next.value
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<footer v-if="showFooter" class="page-footer">
|
||||||
|
<div v-if="editNavLink || lastUpdated" class="edit-info">
|
||||||
|
<div v-if="editNavLink" class="edit-link">
|
||||||
|
<AutoLink class="edit-link-button" :href="editNavLink.link" :no-icon="true">
|
||||||
|
<IconEdit class="edit-link-icon" aria-label="edit icon"/>
|
||||||
|
{{ editNavLink.text }}
|
||||||
|
</AutoLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="lastUpdated" class="last-updated">
|
||||||
|
<p class="last-updated-text">
|
||||||
|
{{ themeLocale.lastUpdatedText || 'Last updated' }}:
|
||||||
|
<time :datetime="lastUpdated" class="last-updated-time">{{ lastUpdated }}</time>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="contributors && contributors.length" class="contributors">
|
||||||
|
<span class="contributors-label">{{ themeLocale.contributorsText || 'Contributors' }}:</span>
|
||||||
|
<span class="contributors-info">
|
||||||
|
<template v-for="(contributor, index) in contributors" :key="contributor">
|
||||||
|
<span class="contributor" :title="`email: ${contributor.email}`">
|
||||||
|
{{ contributor.name }}
|
||||||
|
</span>
|
||||||
|
<template v-if="index !== contributors.length - 1">, </template>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav v-if="prev?.link || next?.link" class="prev-next">
|
||||||
|
<div class="pager">
|
||||||
|
<AutoLink v-if="prev?.link" class="pager-link prev" :href="prev.link">
|
||||||
|
<!--eslint-disable-next-line vue/no-v-html-->
|
||||||
|
<span class="desc" v-html="themeLocale.prevPageLabel || 'Previous page'"></span>
|
||||||
|
<!--eslint-disable-next-line vue/no-v-html-->
|
||||||
|
<span class="title" v-html="prev.text"></span>
|
||||||
|
</AutoLink>
|
||||||
|
</div>
|
||||||
|
<div class="pager">
|
||||||
|
<AutoLink v-if="next?.link" class="pager-link next" :href="next.link">
|
||||||
|
<!--eslint-disable-next-line vue/no-v-html-->
|
||||||
|
<span class="desc" v-html="themeLocale.nextPageLabel || 'Next page'"></span>
|
||||||
|
<!--eslint-disable-next-line vue/no-v-html-->
|
||||||
|
<span class="title" v-html="next.text"></span>
|
||||||
|
</AutoLink>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-footer {
|
||||||
|
margin-top: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.edit-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-link-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 0;
|
||||||
|
line-height: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
transition: color 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-link-button:hover {
|
||||||
|
color: var(--vp-c-brand-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-link-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-updated-text {
|
||||||
|
line-height: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.last-updated-text {
|
||||||
|
line-height: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contributors {
|
||||||
|
padding-bottom: 6px;
|
||||||
|
line-height: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contributors-label {
|
||||||
|
padding-right: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contributors-info {
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
.contributor {
|
||||||
|
color: var(--vp-c-brand-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-next {
|
||||||
|
border-top: 1px solid var(--vp-c-divider);
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 24px;
|
||||||
|
display: grid;
|
||||||
|
grid-row-gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.prev-next {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-column-gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager-link {
|
||||||
|
display: block;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 11px 16px 13px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager-link:hover {
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager-link.next {
|
||||||
|
margin-left: auto;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
display: block;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: block;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
transition: color 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -4,3 +4,4 @@ export * from './themeData.js'
|
|||||||
export * from './useResolveRouteWithRedirect.js'
|
export * from './useResolveRouteWithRedirect.js'
|
||||||
export * from './sidebar.js'
|
export * from './sidebar.js'
|
||||||
export * from './aside.js'
|
export * from './aside.js'
|
||||||
|
export * from './page.js'
|
||||||
|
|||||||
215
theme/src/client/composables/page.ts
Normal file
215
theme/src/client/composables/page.ts
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import { usePageData, usePageFrontmatter, usePageLang } from '@vuepress/client'
|
||||||
|
import { isArray, isPlainObject, isString } from '@vuepress/shared'
|
||||||
|
import { useBlogPostData } from '@vuepress-plume/plugin-blog-data/client'
|
||||||
|
import type { NotesSidebarItem } from '@vuepress-plume/plugin-notes-data'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { ComputedRef, Ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import type {
|
||||||
|
NavItemWithLink,
|
||||||
|
PlumeThemeBlogPostItem,
|
||||||
|
PlumeThemePageData,
|
||||||
|
PlumeThemePageFrontmatter,
|
||||||
|
} from '../../shared/index.js'
|
||||||
|
import { useNavLink, useSidebar, useThemeLocaleData } from '../composables/index.js'
|
||||||
|
import { resolveEditLink } from '../utils/index.js'
|
||||||
|
|
||||||
|
export const useEditNavLink = (): ComputedRef<null | NavItemWithLink> => {
|
||||||
|
const themeLocale = useThemeLocaleData()
|
||||||
|
const page = usePageData<PlumeThemePageData>()
|
||||||
|
const frontmatter = usePageFrontmatter<PlumeThemePageFrontmatter>()
|
||||||
|
|
||||||
|
return computed(() => {
|
||||||
|
const showEditLink =
|
||||||
|
frontmatter.value.editLink ?? themeLocale.value.editLink ?? true
|
||||||
|
if (!showEditLink) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
repo,
|
||||||
|
docsRepo = repo,
|
||||||
|
docsBranch = 'main',
|
||||||
|
docsDir = '',
|
||||||
|
editLinkText,
|
||||||
|
} = themeLocale.value
|
||||||
|
|
||||||
|
if (!docsRepo) return null
|
||||||
|
|
||||||
|
const editLink = resolveEditLink({
|
||||||
|
docsRepo,
|
||||||
|
docsBranch,
|
||||||
|
docsDir,
|
||||||
|
filePathRelative: page.value.filePathRelative,
|
||||||
|
editLinkPattern:
|
||||||
|
frontmatter.value.editLinkPattern ?? themeLocale.value.editLinkPattern,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!editLink) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: editLinkText ?? 'Edit this page',
|
||||||
|
link: editLink,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLastUpdated = (): ComputedRef<null | string> => {
|
||||||
|
const themeLocale = useThemeLocaleData()
|
||||||
|
const page = usePageData<PlumeThemePageData>()
|
||||||
|
const frontmatter = usePageFrontmatter<PlumeThemePageFrontmatter>()
|
||||||
|
|
||||||
|
return computed(() => {
|
||||||
|
const showLastUpdated =
|
||||||
|
frontmatter.value.lastUpdated ?? themeLocale.value.lastUpdated ?? true
|
||||||
|
|
||||||
|
if (!showLastUpdated) return null
|
||||||
|
|
||||||
|
if (!page.value.git?.updatedTime) return null
|
||||||
|
|
||||||
|
const updatedDate = new Date(page.value.git?.updatedTime)
|
||||||
|
|
||||||
|
return updatedDate.toLocaleString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useContributors = (): ComputedRef<
|
||||||
|
null | Required<PlumeThemePageData['git']>['contributors']
|
||||||
|
> => {
|
||||||
|
const themeLocale = useThemeLocaleData()
|
||||||
|
const page = usePageData<PlumeThemePageData>()
|
||||||
|
const frontmatter = usePageFrontmatter<PlumeThemePageFrontmatter>()
|
||||||
|
|
||||||
|
return computed(() => {
|
||||||
|
const showContributors =
|
||||||
|
frontmatter.value.contributors ?? themeLocale.value.contributors ?? true
|
||||||
|
|
||||||
|
if (!showContributors) return null
|
||||||
|
|
||||||
|
return page.value.git?.contributors ?? null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve `prev` or `next` config from frontmatter
|
||||||
|
*/
|
||||||
|
const resolveFromFrontmatterConfig = (
|
||||||
|
conf: unknown,
|
||||||
|
): null | false | NavItemWithLink => {
|
||||||
|
if (conf === false) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isString(conf)) {
|
||||||
|
return useNavLink(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject<NavItemWithLink>(conf)) {
|
||||||
|
return conf
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const flatSidebar = (
|
||||||
|
sidebar: NotesSidebarItem[],
|
||||||
|
res: NavItemWithLink[] = []
|
||||||
|
): NavItemWithLink[] => {
|
||||||
|
for (const item of sidebar) {
|
||||||
|
if (item.link) {
|
||||||
|
res.push({ link: item.link, text: item.text || item.dir || '' })
|
||||||
|
}
|
||||||
|
if (isArray(item.items) && item.items.length) {
|
||||||
|
flatSidebar(item.items as NotesSidebarItem[], res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve `prev` or `next` config from sidebar items
|
||||||
|
*/
|
||||||
|
const resolveFromSidebarItems = (
|
||||||
|
sidebarItems: NavItemWithLink[],
|
||||||
|
currentPath: string,
|
||||||
|
offset: number,
|
||||||
|
): null | NavItemWithLink => {
|
||||||
|
const index = sidebarItems.findIndex((item) => item.link === currentPath)
|
||||||
|
if (index !== -1) {
|
||||||
|
const targetItem = sidebarItems[index + offset]
|
||||||
|
if (targetItem?.link) {
|
||||||
|
return {
|
||||||
|
link: targetItem.link,
|
||||||
|
text: targetItem.text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveFromBlogPostData = (
|
||||||
|
postList: PlumeThemeBlogPostItem[],
|
||||||
|
currentPath: string,
|
||||||
|
offset: number,
|
||||||
|
): null | NavItemWithLink => {
|
||||||
|
const index = postList.findIndex((item) => item.path === currentPath)
|
||||||
|
if (index !== -1) {
|
||||||
|
const targetItem = postList[index + offset]
|
||||||
|
if (!targetItem?.path) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
link: targetItem.path,
|
||||||
|
text: targetItem.title,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePageNav = () => {
|
||||||
|
const route = useRoute()
|
||||||
|
const page = usePageData<PlumeThemePageData>()
|
||||||
|
const frontmatter = usePageFrontmatter<PlumeThemePageFrontmatter>()
|
||||||
|
const { sidebar } = useSidebar()
|
||||||
|
const postList = useBlogPostData() as unknown as Ref<PlumeThemeBlogPostItem[]>
|
||||||
|
const locale = usePageLang()
|
||||||
|
|
||||||
|
const prevNavList = computed(() => {
|
||||||
|
const prevConfig = resolveFromFrontmatterConfig(frontmatter.value.prev)
|
||||||
|
if (prevConfig !== false) {
|
||||||
|
return prevConfig
|
||||||
|
}
|
||||||
|
if (page.value.isBlogPost) {
|
||||||
|
return resolveFromBlogPostData(
|
||||||
|
postList.value.filter(item => item.lang === locale.value),
|
||||||
|
route.path,
|
||||||
|
-1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return resolveFromSidebarItems(flatSidebar(sidebar.value), route.path, -1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const nextNavList = computed(() => {
|
||||||
|
const nextConfig = resolveFromFrontmatterConfig(frontmatter.value.next)
|
||||||
|
if (nextConfig !== false) {
|
||||||
|
return nextConfig
|
||||||
|
}
|
||||||
|
if (page.value.isBlogPost) {
|
||||||
|
return resolveFromBlogPostData(
|
||||||
|
postList.value.filter(item => item.lang === locale.value),
|
||||||
|
route.path,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return resolveFromSidebarItems(flatSidebar(sidebar.value), route.path, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
prev: prevNavList,
|
||||||
|
next: nextNavList,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,13 @@
|
|||||||
import { isFunction, isString } from '@vuepress/shared'
|
import { isFunction, isString } from '@vuepress/shared'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import type { Router } from 'vue-router'
|
import type { Router } from 'vue-router'
|
||||||
|
import type { NavItemWithLink } from '../../shared/index.js'
|
||||||
|
|
||||||
|
declare module 'vue-router' {
|
||||||
|
interface RouteMeta {
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a route with redirection
|
* Resolve a route with redirection
|
||||||
@ -26,3 +33,21 @@ export const useResolveRouteWithRedirect = (
|
|||||||
...resolvedRedirectObj,
|
...resolvedRedirectObj,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve NavLink props from string
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* - Input: '/README.md'
|
||||||
|
* - Output: { text: 'Home', link: '/' }
|
||||||
|
*/
|
||||||
|
export const useNavLink = (item: string): NavItemWithLink => {
|
||||||
|
// the route path of vue-router is url-encoded, and we expect users are using
|
||||||
|
// non-url-encoded string in theme config, so we need to url-encode it first to
|
||||||
|
// resolve the route correctly
|
||||||
|
const resolved = useResolveRouteWithRedirect(encodeURI(item))
|
||||||
|
return {
|
||||||
|
text: resolved.meta.title || item,
|
||||||
|
link: resolved.name === '404' ? item : resolved.fullPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -2,3 +2,5 @@ export * from './shared.js'
|
|||||||
export * from './normalizeLink.js'
|
export * from './normalizeLink.js'
|
||||||
export * from './socialIcons.js'
|
export * from './socialIcons.js'
|
||||||
export * from './dom.js'
|
export * from './dom.js'
|
||||||
|
export * from './resolveEditLink.js'
|
||||||
|
export * from './resolveRepoType.js'
|
||||||
|
|||||||
64
theme/src/client/utils/resolveEditLink.ts
Normal file
64
theme/src/client/utils/resolveEditLink.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
isLinkHttp,
|
||||||
|
removeEndingSlash,
|
||||||
|
removeLeadingSlash,
|
||||||
|
} from '@vuepress/shared'
|
||||||
|
import { resolveRepoType } from './resolveRepoType.js'
|
||||||
|
import type { RepoType } from './resolveRepoType.js'
|
||||||
|
|
||||||
|
export const editLinkPatterns: Record<Exclude<RepoType, null>, string> = {
|
||||||
|
GitHub: ':repo/edit/:branch/:path',
|
||||||
|
GitLab: ':repo/-/edit/:branch/:path',
|
||||||
|
Gitee: ':repo/edit/:branch/:path',
|
||||||
|
Bitbucket:
|
||||||
|
':repo/src/:branch/:path?mode=edit&spa=0&at=:branch&fileviewer=file-view-default',
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveEditLinkPatterns = ({
|
||||||
|
docsRepo,
|
||||||
|
editLinkPattern,
|
||||||
|
}: {
|
||||||
|
docsRepo: string
|
||||||
|
editLinkPattern?: string
|
||||||
|
}): string | null => {
|
||||||
|
if (editLinkPattern) {
|
||||||
|
return editLinkPattern
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoType = resolveRepoType(docsRepo)
|
||||||
|
if (repoType !== null) {
|
||||||
|
return editLinkPatterns[repoType]
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolveEditLink = ({
|
||||||
|
docsRepo,
|
||||||
|
docsBranch,
|
||||||
|
docsDir,
|
||||||
|
filePathRelative,
|
||||||
|
editLinkPattern,
|
||||||
|
}: {
|
||||||
|
docsRepo: string
|
||||||
|
docsBranch: string
|
||||||
|
docsDir: string
|
||||||
|
filePathRelative: string | null
|
||||||
|
editLinkPattern?: string
|
||||||
|
}): string | null => {
|
||||||
|
if (!filePathRelative) return null
|
||||||
|
|
||||||
|
const pattern = resolveEditLinkPatterns({ docsRepo, editLinkPattern })
|
||||||
|
if (!pattern) return null
|
||||||
|
|
||||||
|
return pattern
|
||||||
|
.replace(
|
||||||
|
/:repo/,
|
||||||
|
isLinkHttp(docsRepo) ? docsRepo : `https://github.com/${docsRepo}`,
|
||||||
|
)
|
||||||
|
.replace(/:branch/, docsBranch)
|
||||||
|
.replace(
|
||||||
|
/:path/,
|
||||||
|
removeLeadingSlash(`${removeEndingSlash(docsDir)}/${filePathRelative}`),
|
||||||
|
)
|
||||||
|
}
|
||||||
11
theme/src/client/utils/resolveRepoType.ts
Normal file
11
theme/src/client/utils/resolveRepoType.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { isLinkHttp } from '@vuepress/shared'
|
||||||
|
|
||||||
|
export type RepoType = 'GitHub' | 'GitLab' | 'Gitee' | 'Bitbucket' | null
|
||||||
|
|
||||||
|
export const resolveRepoType = (repo: string): RepoType => {
|
||||||
|
if (!isLinkHttp(repo) || /github\.com/.test(repo)) return 'GitHub'
|
||||||
|
if (/bitbucket\.org/.test(repo)) return 'Bitbucket'
|
||||||
|
if (/gitlab\.com/.test(repo)) return 'GitLab'
|
||||||
|
if (/gitee\.com/.test(repo)) return 'Gitee'
|
||||||
|
return null
|
||||||
|
}
|
||||||
@ -97,9 +97,9 @@ export const setupPlugins = (
|
|||||||
|
|
||||||
options.git !== false
|
options.git !== false
|
||||||
? gitPlugin({
|
? gitPlugin({
|
||||||
createdTime: true,
|
createdTime: false,
|
||||||
updatedTime: true,
|
updatedTime: localeOptions.lastUpdated !== false,
|
||||||
contributors: false,
|
contributors: localeOptions.contributors !== false,
|
||||||
})
|
})
|
||||||
: [],
|
: [],
|
||||||
|
|
||||||
@ -141,7 +141,10 @@ export const setupPlugins = (
|
|||||||
? docsearchPlugin(options.docsearch!)
|
? docsearchPlugin(options.docsearch!)
|
||||||
: [],
|
: [],
|
||||||
|
|
||||||
options.shikiji !== false ? shikijiPlugin(options.shikiji) : [],
|
options.shikiji !== false ? shikijiPlugin({
|
||||||
|
theme: { light: 'vitesse-light', dark: 'vitesse-dark' },
|
||||||
|
...(options.shikiji ?? {}),
|
||||||
|
}) : [],
|
||||||
|
|
||||||
options.copyCode !== false
|
options.copyCode !== false
|
||||||
? copyCodePlugin({
|
? copyCodePlugin({
|
||||||
@ -154,7 +157,7 @@ export const setupPlugins = (
|
|||||||
? mdEnhancePlugin(
|
? mdEnhancePlugin(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
container: true, // info note tip warning danger details
|
hint: true, // info note tip warning danger details d
|
||||||
codetabs: true,
|
codetabs: true,
|
||||||
tabs: true,
|
tabs: true,
|
||||||
align: true,
|
align: true,
|
||||||
|
|||||||
@ -34,6 +34,8 @@ export const plumeTheme = ({
|
|||||||
plugins: setupPlugins(app, themePlugins, localeOptions),
|
plugins: setupPlugins(app, themePlugins, localeOptions),
|
||||||
onInitialized: async (app) => await setupPage(app, localeOptions),
|
onInitialized: async (app) => await setupPage(app, localeOptions),
|
||||||
extendsPage: (page: Page<PlumeThemePageData>) => {
|
extendsPage: (page: Page<PlumeThemePageData>) => {
|
||||||
|
page.data.filePathRelative = page.filePathRelative
|
||||||
|
page.routeMeta.title = page.title
|
||||||
autoCategory(app, page, localeOptions)
|
autoCategory(app, page, localeOptions)
|
||||||
pageContentRendered(page)
|
pageContentRendered(page)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import type { NavItemWithLink } from ".";
|
||||||
|
|
||||||
export interface PlumeThemeHomeFrontmatter {
|
export interface PlumeThemeHomeFrontmatter {
|
||||||
home?: true
|
home?: true
|
||||||
banner?: string
|
banner?: string
|
||||||
@ -15,7 +17,17 @@ export interface PlumeThemeHeroAction {
|
|||||||
text: string
|
text: string
|
||||||
link?: string
|
link?: string
|
||||||
}
|
}
|
||||||
export interface PlumeThemePostFrontmatter {
|
|
||||||
|
export interface PlumeThemePageFrontmatter {
|
||||||
|
editLink?: boolean
|
||||||
|
editLinkPattern?: string
|
||||||
|
lastUpdated?: boolean
|
||||||
|
contributors?: boolean
|
||||||
|
prev?: string | NavItemWithLink
|
||||||
|
next?: string | NavItemWithLink
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlumeThemePostFrontmatter extends PlumeThemePageFrontmatter {
|
||||||
createTime?: string
|
createTime?: string
|
||||||
author?: string
|
author?: string
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
@ -24,6 +36,6 @@ export interface PlumeThemePostFrontmatter {
|
|||||||
banner?: string
|
banner?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlumeThemeNoteFrontmatter {
|
export interface PlumeThemeNoteFrontmatter extends PlumeThemePageFrontmatter {
|
||||||
createTime?: string
|
createTime?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -160,11 +160,11 @@ export interface PlumeThemeLocaleData extends LocaleData {
|
|||||||
/**
|
/**
|
||||||
* repository of navbar
|
* repository of navbar
|
||||||
*/
|
*/
|
||||||
// repo?: null | string
|
repo?: null | string
|
||||||
/**
|
/**
|
||||||
* repository text of navbar
|
* repository text of navbar
|
||||||
*/
|
*/
|
||||||
// repoLabel?: string
|
repoLabel?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navbar config
|
* Navbar config
|
||||||
@ -172,6 +172,76 @@ export interface PlumeThemeLocaleData extends LocaleData {
|
|||||||
* Set to `false` to disable navbar in current locale
|
* Set to `false` to disable navbar in current locale
|
||||||
*/
|
*/
|
||||||
navbar?: false | NavItem[]
|
navbar?: false | NavItem[]
|
||||||
|
/**
|
||||||
|
* Page meta - edit link config
|
||||||
|
*
|
||||||
|
* Whether to show "Edit this page" or not
|
||||||
|
*/
|
||||||
|
editLink?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page meta - edit link config
|
||||||
|
*
|
||||||
|
* The text to replace the default "Edit this page"
|
||||||
|
*/
|
||||||
|
editLinkText?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page meta - edit link config
|
||||||
|
*
|
||||||
|
* Pattern of edit link
|
||||||
|
*
|
||||||
|
* @example ':repo/edit/:branch/:path'
|
||||||
|
*/
|
||||||
|
editLinkPattern?: string
|
||||||
|
/**
|
||||||
|
* Page meta - edit link config
|
||||||
|
*
|
||||||
|
* Use `repo` config by default
|
||||||
|
*
|
||||||
|
* Set this config if your docs is placed in a different repo
|
||||||
|
*/
|
||||||
|
docsRepo?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page meta - edit link config
|
||||||
|
*
|
||||||
|
* Set this config if the branch of your docs is not 'main'
|
||||||
|
*/
|
||||||
|
docsBranch?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page meta - edit link config
|
||||||
|
*
|
||||||
|
* Set this config if your docs is placed in sub dir of your `docsRepo`
|
||||||
|
*/
|
||||||
|
docsDir?: string
|
||||||
|
/**
|
||||||
|
* Page meta - last updated config
|
||||||
|
*
|
||||||
|
* Whether to show "Last Updated" or not
|
||||||
|
*/
|
||||||
|
lastUpdated?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page meta - last updated config
|
||||||
|
*
|
||||||
|
* The text to replace the default "Last Updated"
|
||||||
|
*/
|
||||||
|
lastUpdatedText?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page meta - contributors config
|
||||||
|
*
|
||||||
|
* Whether to show "Contributors" or not
|
||||||
|
*/
|
||||||
|
contributors?: boolean
|
||||||
|
/**
|
||||||
|
* Page meta - contributors config
|
||||||
|
*
|
||||||
|
* The text to replace the default "Contributors"
|
||||||
|
*/
|
||||||
|
contributorsText?: string
|
||||||
/**
|
/**
|
||||||
* 外部链接打开方式
|
* 外部链接打开方式
|
||||||
*/
|
*/
|
||||||
@ -191,6 +261,10 @@ export interface PlumeThemeLocaleData extends LocaleData {
|
|||||||
|
|
||||||
outlineLabel?: string
|
outlineLabel?: string
|
||||||
|
|
||||||
|
prevPageLabel?: string
|
||||||
|
|
||||||
|
nextPageLabel?: string
|
||||||
|
|
||||||
footer?:
|
footer?:
|
||||||
| false
|
| false
|
||||||
| {
|
| {
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
export interface PlumeThemePageData {
|
import type { GitPluginPageData } from '@vuepress/plugin-git'
|
||||||
git: {
|
|
||||||
createTime: number
|
export interface PlumeThemePageData extends GitPluginPageData {
|
||||||
updateTime: number
|
|
||||||
}
|
|
||||||
isBlogPost: boolean
|
isBlogPost: boolean
|
||||||
type: 'blog' | 'product'
|
type: 'blog' | 'product'
|
||||||
categoryList?: PageCategoryData[]
|
categoryList?: PageCategoryData[]
|
||||||
|
filePathRelative: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageCategoryData {
|
export interface PageCategoryData {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user