mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-26 11:38:15 +08:00
feat: add i18n support
This commit is contained in:
parent
30fe922a0c
commit
249ea11fbb
@ -21,6 +21,7 @@ export type NotesSidebarItem = {
|
||||
dir?: string
|
||||
collapsed?: boolean
|
||||
items?: NotesSidebar
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export type NotesData = Record<string, NotesSidebarItem[]>
|
||||
|
||||
@ -8,6 +8,8 @@ const props = defineProps<{
|
||||
tag?: string
|
||||
href?: string
|
||||
noIcon?: boolean
|
||||
target?: string
|
||||
rel?: string
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
@ -33,8 +35,8 @@ const linkTo = (e: Event) => {
|
||||
class="auto-link"
|
||||
:class="{ link: href }"
|
||||
:href="href ? normalizeLink(href) : undefined"
|
||||
:target="isExternal ? '_blank' : undefined"
|
||||
:rel="isExternal ? 'noreferrer' : undefined"
|
||||
:target="target || (isExternal ? '_blank' : undefined)"
|
||||
:rel="rel || (isExternal ? 'noreferrer' : undefined)"
|
||||
@click="linkTo($event)"
|
||||
>
|
||||
<slot />
|
||||
|
||||
@ -9,6 +9,7 @@ import NavBarMenu from './NavBarMenu.vue'
|
||||
import NavBarSearch from './NavBarSearch.vue'
|
||||
import NavBarSocialLinks from './NavBarSocialLinks.vue'
|
||||
import NavBarTitle from './NavBarTitle.vue'
|
||||
import NavBarTranslations from './NavBarTranslations.vue'
|
||||
|
||||
defineProps<{
|
||||
isScreenOpen: boolean
|
||||
@ -38,6 +39,7 @@ const classes = computed(() => ({
|
||||
<div class="content-body">
|
||||
<NavBarSearch class="search" />
|
||||
<NavBarMenu class="menu" />
|
||||
<NavBarTranslations class="translations" />
|
||||
<NavBarAppearance class="appearance" />
|
||||
<NavBarSocialLinks class="social-links" />
|
||||
<NavBarExtra class="extra" />
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useThemeLocaleData } from '../../composables/index.js'
|
||||
import { useLangs } from '../../composables/langs.js'
|
||||
import Flyout from '../Flyout/index.vue'
|
||||
import MenuLink from '../Flyout/MenuLink.vue'
|
||||
import SocialLinks from '../SocialLinks.vue'
|
||||
import SwitchAppearance from '../SwitchAppearance.vue'
|
||||
|
||||
const theme = useThemeLocaleData()
|
||||
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
|
||||
|
||||
const hasExtraContent = computed(
|
||||
() => theme.value.appearance || theme.value.social
|
||||
@ -14,6 +17,17 @@ const hasExtraContent = computed(
|
||||
|
||||
<template>
|
||||
<Flyout v-if="hasExtraContent" class="navbar-extra" label="extra navigation">
|
||||
<div
|
||||
v-if="localeLinks.length && currentLang.label"
|
||||
class="group translations"
|
||||
>
|
||||
<p class="trans-title">{{ currentLang.label }}</p>
|
||||
|
||||
<template v-for="locale in localeLinks" :key="locale.link">
|
||||
<MenuLink :item="locale" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="theme.appearance" class="group">
|
||||
<div class="item appearance">
|
||||
<p class="label">Appearance</p>
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
<script lang="ts" setup>
|
||||
import { useLangs } from '../../composables/langs.js'
|
||||
import { useThemeLocaleData } from '../../composables/themeData.js'
|
||||
import Flyout from '../Flyout/index.vue'
|
||||
import MenuLink from '../Flyout/MenuLink.vue'
|
||||
import IconLanguages from '../icons/IconLanguages.vue'
|
||||
|
||||
const theme = useThemeLocaleData()
|
||||
const { currentLang, localeLinks } = useLangs()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Flyout
|
||||
v-if="localeLinks.length && currentLang.label"
|
||||
class="navbar-translations"
|
||||
:icon="IconLanguages"
|
||||
:label="theme.selectLanguageText || 'change language'"
|
||||
>
|
||||
<div class="items">
|
||||
<p class="title">{{ currentLang.label }}</p>
|
||||
|
||||
<template v-for="locale in localeLinks" :key="locale.link">
|
||||
<MenuLink :item="locale" />
|
||||
</template>
|
||||
</div>
|
||||
</Flyout>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar-translations {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.navbar-translations {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 0 24px 0 12px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
@ -4,6 +4,7 @@ import { ref } from 'vue'
|
||||
import NavScreenAppearance from './NavScreenAppearance.vue'
|
||||
import NavScreenMenu from './NavScreenMenu.vue'
|
||||
import NavScreenSocialLinks from './NavScreenSocialLinks.vue'
|
||||
import NavScreenTranslates from './NavScreenTranslations.vue'
|
||||
|
||||
defineProps<{
|
||||
open: boolean
|
||||
@ -29,6 +30,7 @@ function unlockBodyScroll() {
|
||||
<div v-if="open" ref="screen" class="nav-screen">
|
||||
<div class="container">
|
||||
<NavScreenMenu class="menu" />
|
||||
<NavScreenTranslates class="translations" />
|
||||
<NavScreenAppearance class="appearance" />
|
||||
<NavScreenSocialLinks class="social-links" />
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useLangs } from '../../composables/langs.js'
|
||||
import AutoLink from '../AutoLink.vue'
|
||||
import IconChevronDown from '../icons/IconChevronDown.vue'
|
||||
import IconLanguages from '../icons/IconLanguages.vue'
|
||||
|
||||
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
|
||||
const isOpen = ref(false)
|
||||
|
||||
function toggle() {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="localeLinks.length && currentLang.label"
|
||||
class="nav-screen-translations"
|
||||
:class="{ open: isOpen }"
|
||||
>
|
||||
<button class="title" @click="toggle">
|
||||
<IconLanguages class="icon lang" />
|
||||
{{ currentLang.label }}
|
||||
<IconChevronDown class="icon chevron" />
|
||||
</button>
|
||||
|
||||
<ul class="list">
|
||||
<li v-for="locale in localeLinks" :key="locale.link" class="item">
|
||||
<AutoLink class="link" :href="locale.link">{{ locale.text }}</AutoLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav-screen-translations {
|
||||
height: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-screen-translations.open {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.icon.lang {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.icon.chevron {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 4px 0 0 24px;
|
||||
}
|
||||
|
||||
.link {
|
||||
line-height: 32px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
@ -11,7 +11,7 @@ const page = usePageData<PlumeThemePageData>()
|
||||
const { isScreenOpen, closeScreen, toggleScreen } = useNav()
|
||||
|
||||
const fixed = computed(() => {
|
||||
return page.value.isBlogPost || page.value.type === 'blog'
|
||||
return page.value.isBlogPost || page.value.frontmatter.type === 'blog'
|
||||
})
|
||||
|
||||
provide('close-screen', closeScreen)
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { usePageLang } from '@vuepress/client'
|
||||
import { useBlogPostData } from '@vuepress-plume/vuepress-plugin-blog-data/client'
|
||||
import { computed } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
|
||||
import PostItem from './PostItem.vue'
|
||||
|
||||
const locale = usePageLang()
|
||||
|
||||
const list = useBlogPostData() as unknown as Ref<PlumeThemeBlogPostItem[]>
|
||||
|
||||
const postList = computed(() => {
|
||||
@ -21,7 +24,7 @@ const postList = computed(() => {
|
||||
return next.sticky > prev.sticky ? 1 : -1
|
||||
}),
|
||||
...otherList,
|
||||
]
|
||||
].filter((item) => item.lang === locale.value)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
|
||||
56
packages/theme/src/client/composables/langs.ts
Normal file
56
packages/theme/src/client/composables/langs.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { usePageData, usePageLang, useSiteData } from '@vuepress/client'
|
||||
import { computed } from 'vue'
|
||||
import type { PlumeThemePageData } from '../../shared/index.js'
|
||||
import { ensureStartingSlash } from '../utils/index.js'
|
||||
import { useThemeData } from './themeData.js'
|
||||
|
||||
export function useLangs({
|
||||
removeCurrent = true,
|
||||
correspondingLink = false,
|
||||
} = {}) {
|
||||
const page = usePageData<PlumeThemePageData>()
|
||||
const site = useSiteData()
|
||||
const theme = useThemeData()
|
||||
const locale = usePageLang()
|
||||
const currentLang = computed(() => {
|
||||
const link = locale.value === site.value.lang ? '/' : `/${locale.value}/`
|
||||
return {
|
||||
label: theme.value.locales?.[link]?.selectLanguageName,
|
||||
link,
|
||||
}
|
||||
})
|
||||
|
||||
const localeLinks = computed(() =>
|
||||
Object.entries(theme.value.locales || {}).flatMap(([key, value]) =>
|
||||
removeCurrent && currentLang.value.label === value.selectLanguageName
|
||||
? []
|
||||
: {
|
||||
text: value.selectLanguageName,
|
||||
link: normalizeLink(
|
||||
key,
|
||||
correspondingLink,
|
||||
page.value.path.slice(currentLang.value.link.length - 1),
|
||||
true
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return { localeLinks, currentLang }
|
||||
}
|
||||
|
||||
function normalizeLink(
|
||||
link: string,
|
||||
addPath: boolean,
|
||||
path: string,
|
||||
addExt: boolean
|
||||
) {
|
||||
return addPath
|
||||
? link.replace(/\/$/, '') +
|
||||
ensureStartingSlash(
|
||||
path
|
||||
.replace(/(^|\/)?index.md$/, '$1')
|
||||
.replace(/\.md$/, addExt ? '.html' : '')
|
||||
)
|
||||
: link
|
||||
}
|
||||
@ -21,6 +21,8 @@ import {
|
||||
|
||||
const page = usePageData<PlumeThemePageData>()
|
||||
|
||||
console.log(page)
|
||||
|
||||
const {
|
||||
isOpen: isSidebarOpen,
|
||||
open: openSidebar,
|
||||
@ -49,7 +51,7 @@ provide('is-sidebar-open', isSidebarOpen)
|
||||
<Sidebar :open="isSidebarOpen" />
|
||||
<LayoutContent>
|
||||
<Home v-if="page.frontmatter.home" />
|
||||
<Blog v-else-if="page.type === 'blog'" />
|
||||
<Blog v-else-if="page.frontmatter.type === 'blog'" />
|
||||
<Page v-else />
|
||||
<VFooter />
|
||||
</LayoutContent>
|
||||
|
||||
@ -277,6 +277,10 @@
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
.plume-content .vp-code-tabs-nav {
|
||||
margin: 0.85rem 0 0;
|
||||
}
|
||||
|
||||
// .plume-content div[class*='language-'] {
|
||||
// position: relative;
|
||||
// margin: 16px -24px;
|
||||
|
||||
@ -63,3 +63,7 @@ export function throttleAndDebounce(fn: () => void, delay: number): () => void {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureStartingSlash(path: string): string {
|
||||
return /^\//.test(path) ? path : `/${path}`
|
||||
}
|
||||
|
||||
@ -1,38 +1,49 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import path from 'node:path'
|
||||
import type { App } from '@vuepress/core'
|
||||
import { resolveLocalePath } from '@vuepress/shared'
|
||||
import type {
|
||||
AutoFrontmatterOptions,
|
||||
FormatterArray,
|
||||
FormatterObject,
|
||||
} from '@vuepress-plume/vuepress-plugin-auto-frontmatter'
|
||||
import type {
|
||||
NotesDataOptions,
|
||||
NotesItem,
|
||||
} from '@vuepress-plume/vuepress-plugin-notes-data'
|
||||
import type { NotesItem } from '@vuepress-plume/vuepress-plugin-notes-data'
|
||||
import { format } from 'date-fns'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import type { PlumeThemeLocaleOptions } from '../shared/index.js'
|
||||
|
||||
const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 8)
|
||||
const require = createRequire(process.cwd())
|
||||
const getPackage = () => {
|
||||
let pkg = {} as any
|
||||
try {
|
||||
pkg = require('package.json') || {}
|
||||
} catch {}
|
||||
return pkg
|
||||
}
|
||||
|
||||
export default function (
|
||||
export default function autoFrontmatter(
|
||||
app: App,
|
||||
localeOption: PlumeThemeLocaleOptions
|
||||
): AutoFrontmatterOptions {
|
||||
const sourceDir = app.dir.source()
|
||||
const require = createRequire(process.cwd())
|
||||
let pkg = {} as any
|
||||
try {
|
||||
pkg = require(path.join(process.cwd(), './package.json')) || {}
|
||||
} catch {}
|
||||
const pkg = getPackage()
|
||||
const articlePrefix = localeOption.article || '/article/'
|
||||
const {
|
||||
dir,
|
||||
link: notesLink,
|
||||
notes: notesList,
|
||||
} = localeOption.notes as NotesDataOptions
|
||||
const notesDir = dir.replace(/^\//, '')
|
||||
const baseFormatter = {
|
||||
|
||||
const locales = (app.siteData.locales || {}) as PlumeThemeLocaleOptions
|
||||
const localesNotesDirs = Object.keys(locales)
|
||||
.map((locale) => {
|
||||
const notes = localeOption.locales?.[locale].notes
|
||||
if (!notes) return ''
|
||||
const dir = notes.dir
|
||||
console.log(locale, dir)
|
||||
return dir ? path.join(locale, dir).replace(/^\//, '') : ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
console.log('locales notes dirs', Object.keys(locales), localesNotesDirs)
|
||||
|
||||
const baseFormatter: FormatterObject = {
|
||||
author(author: string) {
|
||||
if (author) return author
|
||||
return localeOption.avatar?.name || pkg.author || ''
|
||||
@ -42,59 +53,92 @@ export default function (
|
||||
return format(new Date(createTime), 'yyyy/MM/dd hh:mm:ss')
|
||||
},
|
||||
}
|
||||
|
||||
const resolveLocale = (filepath: string) => {
|
||||
const file = path.join('/', path.relative(sourceDir, filepath))
|
||||
return resolveLocalePath(localeOption.locales!, file)
|
||||
}
|
||||
const notesByLocale = (locale: string) => {
|
||||
const notes = localeOption.locales![locale].notes || localeOption.notes
|
||||
if (notes === false) return undefined
|
||||
return notes
|
||||
}
|
||||
const findNote = (filepath: string) => {
|
||||
const file = path.relative(sourceDir, filepath)
|
||||
const file = path.join('/', path.relative(sourceDir, filepath))
|
||||
const locale = resolveLocalePath(localeOption.locales!, file)
|
||||
const notes = notesByLocale(locale)
|
||||
if (!notes) return undefined
|
||||
const notesList = notes?.notes || []
|
||||
const notesDir = notes?.dir || ''
|
||||
return notesList.find((note) =>
|
||||
file.startsWith(path.join(notesDir.replace(/^\//, ''), note.dir))
|
||||
file.startsWith(path.join(locale, notesDir, note.dir))
|
||||
)
|
||||
}
|
||||
|
||||
const getCurrentDirname = (note: NotesItem | undefined, filepath: string) => {
|
||||
const dirList =
|
||||
(note?.dir || path.dirname(filepath))
|
||||
.replace(/^\/|\/$/g, '')
|
||||
.split('/') || []
|
||||
const dirList = (note?.dir || path.dirname(filepath))
|
||||
.replace(/^\/|\/$/g, '')
|
||||
.split('/')
|
||||
return dirList.length > 0 ? dirList[dirList.length - 1] : ''
|
||||
}
|
||||
return {
|
||||
include: ['**/*.md'],
|
||||
formatter: [
|
||||
{
|
||||
// note 首页链接
|
||||
include: path.join(notesDir, `**/{readme,README,index}.md`),
|
||||
formatter: {
|
||||
title(title: string, { filepath }) {
|
||||
if (title) return title
|
||||
const note = findNote(filepath)
|
||||
if (note?.text) return note.text
|
||||
return getCurrentDirname(note, filepath) || ''
|
||||
},
|
||||
...baseFormatter,
|
||||
permalink(permalink: string, { filepath }) {
|
||||
if (permalink) return permalink
|
||||
const note = findNote(filepath)
|
||||
const dirname = getCurrentDirname(note, filepath)
|
||||
return path.join(notesLink, note?.link || dirname, '/')
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
include: path.join(notesDir, '**/*.md'),
|
||||
formatter: {
|
||||
title(title: string, { filepath }) {
|
||||
if (title) return title
|
||||
const basename = path.basename(filepath, '.md')
|
||||
return basename
|
||||
},
|
||||
...baseFormatter,
|
||||
permalink(permalink: string, { filepath }) {
|
||||
if (permalink) return permalink
|
||||
const note = findNote(filepath)
|
||||
const dirname = getCurrentDirname(note, filepath)
|
||||
return path.join(notesLink, note?.link || dirname, nanoid(), '/')
|
||||
},
|
||||
},
|
||||
},
|
||||
localesNotesDirs.length
|
||||
? {
|
||||
// note 首页链接
|
||||
include: localesNotesDirs.map((dir) =>
|
||||
path.join(dir, '**/{readme,README,index}.md')
|
||||
),
|
||||
formatter: {
|
||||
title(title: string, { filepath }) {
|
||||
if (title) return title
|
||||
const note = findNote(filepath)
|
||||
if (note?.text) return note.text
|
||||
return getCurrentDirname(note, filepath) || ''
|
||||
},
|
||||
...baseFormatter,
|
||||
permalink(permalink: string, { filepath }) {
|
||||
if (permalink) return permalink
|
||||
const locale = resolveLocale(filepath)
|
||||
const notes = notesByLocale(locale)
|
||||
const note = findNote(filepath)
|
||||
return path.join(
|
||||
locale,
|
||||
notes?.link || '',
|
||||
note?.link || getCurrentDirname(note, filepath),
|
||||
'/'
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
: '',
|
||||
localesNotesDirs.length
|
||||
? {
|
||||
include: localesNotesDirs.map((dir) => path.join(dir, '**/**.md')),
|
||||
formatter: {
|
||||
title(title: string, { filepath }) {
|
||||
if (title) return title
|
||||
const basename = path.basename(filepath, '.md')
|
||||
return basename
|
||||
},
|
||||
...baseFormatter,
|
||||
permalink(permalink: string, { filepath }) {
|
||||
if (permalink) return permalink
|
||||
const locale = resolveLocale(filepath)
|
||||
const note = findNote(filepath)
|
||||
const notes = notesByLocale(locale)
|
||||
return path.join(
|
||||
locale,
|
||||
notes?.link || '',
|
||||
note?.link || getCurrentDirname(note, filepath),
|
||||
nanoid(),
|
||||
'/'
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
: '',
|
||||
{
|
||||
include: '**/{readme,README,index}.md',
|
||||
formatter: {},
|
||||
@ -108,12 +152,13 @@ export default function (
|
||||
return basename
|
||||
},
|
||||
...baseFormatter,
|
||||
permalink(permalink: string) {
|
||||
permalink(permalink: string, { filepath }) {
|
||||
if (permalink) return permalink
|
||||
return path.join(articlePrefix, nanoid(), '/')
|
||||
const locale = resolveLocale(filepath)
|
||||
return path.join(locale, articlePrefix, nanoid(), '/')
|
||||
},
|
||||
},
|
||||
},
|
||||
] as FormatterArray,
|
||||
].filter(Boolean) as FormatterArray,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import merge from 'lodash.merge'
|
||||
import type { PlumeThemeLocaleOptions } from '../shared/index.js'
|
||||
|
||||
export const defaultLocaleOption: Partial<PlumeThemeLocaleOptions> = {
|
||||
@ -13,8 +12,23 @@ export const defaultLocaleOption: Partial<PlumeThemeLocaleOptions> = {
|
||||
message:
|
||||
'Power by <a target="_blank" href="https://v2.vuepress.vuejs.org/">Vuepress</a> & <a target="_blank" href="https://github.com/pengzhanbo/vuepress-theme-plume">vuepress-theme-plume</a>',
|
||||
},
|
||||
appearance: true,
|
||||
}
|
||||
|
||||
export const mergeLocaleOptions = (options: PlumeThemeLocaleOptions) => {
|
||||
return merge(defaultLocaleOption, options)
|
||||
if (!options.locales) {
|
||||
options.locales = {}
|
||||
}
|
||||
if (!options.locales['/']) {
|
||||
options.locales['/'] = {}
|
||||
}
|
||||
Object.assign(options, {
|
||||
...defaultLocaleOption,
|
||||
...options,
|
||||
})
|
||||
Object.assign(options.locales['/'], {
|
||||
...defaultLocaleOption,
|
||||
...options.locales['/'],
|
||||
})
|
||||
return options
|
||||
}
|
||||
|
||||
@ -8,3 +8,5 @@ export const definePlumeNotesConfig = (
|
||||
): NotesDataOptions => notes
|
||||
|
||||
export const definePlumeNotesItemConfig = (item: NotesItem): NotesItem => item
|
||||
|
||||
export type { NotesDataOptions, NotesItem }
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import path from 'node:path'
|
||||
import type { App, PluginConfig } from '@vuepress/core'
|
||||
import { activeHeaderLinksPlugin } from '@vuepress/plugin-active-header-links'
|
||||
import { docsearchPlugin } from '@vuepress/plugin-docsearch'
|
||||
@ -34,10 +35,13 @@ export const setupPlugins = (
|
||||
): PluginConfig => {
|
||||
const isProd = !app.env.isDev
|
||||
|
||||
let notesDir: string | undefined
|
||||
if (localeOptions.notes !== false) {
|
||||
notesDir = localeOptions.notes?.dir
|
||||
}
|
||||
const locales = (localeOptions.locales || {}) as PlumeThemeLocaleOptions
|
||||
const localeNotesDirs = Object.keys(locales)
|
||||
.map((locale) => {
|
||||
const dir = locales[locale].notes?.dir || ''
|
||||
return dir ? path.join(locale, dir, '**').replace(/^\//, '') : ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
return [
|
||||
palettePlugin({ preset: 'sass' }),
|
||||
@ -56,7 +60,7 @@ export const setupPlugins = (
|
||||
'**/{README,readme,index}.md',
|
||||
'.vuepress/',
|
||||
...(localeOptions.blog?.exclude || []),
|
||||
notesDir ? `${notesDir}/**` : '',
|
||||
...localeNotesDirs,
|
||||
].filter(Boolean),
|
||||
sortBy: 'createTime',
|
||||
excerpt: true,
|
||||
@ -72,6 +76,7 @@ export const setupPlugins = (
|
||||
tags: page.frontmatter.tags,
|
||||
sticky: page.frontmatter.sticky,
|
||||
createTime: page.data.frontmatter.createTime,
|
||||
lang: page.lang,
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import path from 'node:path'
|
||||
import type { App, Page } from '@vuepress/core'
|
||||
import { createPage } from '@vuepress/core'
|
||||
import type {
|
||||
@ -10,30 +11,51 @@ export async function setupPage(
|
||||
app: App,
|
||||
localeOption: PlumeThemeLocaleOptions
|
||||
) {
|
||||
const blogPage = await createPage(app, {
|
||||
path: localeOption.blog?.link,
|
||||
})
|
||||
const locales = Object.keys(app.siteData.locales || {})
|
||||
for (const [, locale] of locales.entries()) {
|
||||
const blog = localeOption.locales?.[locale]?.blog
|
||||
const blogPage = await createPage(app, {
|
||||
path: blog?.link
|
||||
? blog.link
|
||||
: path.join('/', locale, localeOption.blog?.link || ''),
|
||||
frontmatter: {
|
||||
lang: locale.replace(/^\/|\/$/g, '') || app.siteData.lang,
|
||||
type: 'blog',
|
||||
},
|
||||
})
|
||||
|
||||
app.pages.push(blogPage)
|
||||
app.pages.push(blogPage)
|
||||
}
|
||||
}
|
||||
|
||||
let uuid = 10000
|
||||
const cache: Record<string, number> = {}
|
||||
const RE_CATEGORY = /^(\d+)?(?:\.?)([^]+)$/
|
||||
|
||||
export function autoCategory(
|
||||
app: App,
|
||||
page: Page<PlumeThemePageData>,
|
||||
options: PlumeThemeLocaleOptions
|
||||
) {
|
||||
const pagePath = page.filePathRelative
|
||||
if (page.data.type || !pagePath) return
|
||||
|
||||
const { notes } = options
|
||||
if (notes && notes.link && page.path.startsWith(notes.link)) return
|
||||
const categoryList: PageCategoryData[] = pagePath
|
||||
if (page.frontmatter.type || !pagePath) return
|
||||
const locales = Object.keys(app.siteData.locales)
|
||||
const notesLinks: string[] = []
|
||||
for (const [, locale] of locales.entries()) {
|
||||
const notes = options.locales?.[locale]?.notes
|
||||
if (notes && notes.link) notesLinks.push(path.join(locale, notes.link))
|
||||
}
|
||||
if (notesLinks.some((link) => page.path.startsWith(link))) return
|
||||
const RE_LOCALE = new RegExp(
|
||||
`^(${locales.filter((l) => l !== '/').join('|')})`
|
||||
)
|
||||
const categoryList: PageCategoryData[] = `/${pagePath}`
|
||||
.replace(RE_LOCALE, '')
|
||||
.replace(/^\//, '')
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.map((category) => {
|
||||
const match = category.match(/^(\d+)?(?:\.?)([^]+)$/) || []
|
||||
const match = category.match(RE_CATEGORY) || []
|
||||
!cache[match[2]] && !match[1] && (cache[match[2]] = uuid++)
|
||||
return {
|
||||
type: Number(match[1] || cache[match[2]]),
|
||||
|
||||
@ -6,6 +6,9 @@ import { setupPlugins } from './plugins.js'
|
||||
import { autoCategory, pageContentRendered, setupPage } from './setupPages.js'
|
||||
|
||||
const __dirname = getDirname(import.meta.url)
|
||||
const name = '@vuepress-plume/theme-plume'
|
||||
const resolve = (...args: string[]) => path.resolve(__dirname, '../', ...args)
|
||||
const templates = (url: string) => resolve('../templates', url)
|
||||
|
||||
export const plumeTheme = ({
|
||||
themePlugins = {},
|
||||
@ -14,32 +17,24 @@ export const plumeTheme = ({
|
||||
localeOptions = mergeLocaleOptions(localeOptions)
|
||||
return (app: App) => {
|
||||
return {
|
||||
name: '@vuepress-plume/theme-plume',
|
||||
templateBuild: path.resolve(__dirname, '../../templates/build.html'),
|
||||
name,
|
||||
templateBuild: templates('build.html'),
|
||||
clientConfigFile: resolve('client/config.js'),
|
||||
alias: {
|
||||
...Object.fromEntries(
|
||||
fs
|
||||
.readdirSync(path.resolve(__dirname, '../client/components'))
|
||||
.readdirSync(resolve('client/components'))
|
||||
.filter((file) => file.endsWith('.vue'))
|
||||
.map((file) => [
|
||||
`@theme/${file}`,
|
||||
path.resolve(__dirname, '../client/components', file),
|
||||
resolve('client/components', file),
|
||||
])
|
||||
),
|
||||
},
|
||||
clientConfigFile: path.resolve(__dirname, '../client/config.js'),
|
||||
plugins: setupPlugins(app, themePlugins, localeOptions),
|
||||
onInitialized: async (app) => {
|
||||
await setupPage(app, localeOptions)
|
||||
},
|
||||
onInitialized: async (app) => await setupPage(app, localeOptions),
|
||||
extendsPage: (page: Page<PlumeThemePageData>) => {
|
||||
if (
|
||||
localeOptions.blog?.link &&
|
||||
page.path.startsWith(localeOptions.blog.link)
|
||||
) {
|
||||
page.data.type = 'blog'
|
||||
}
|
||||
autoCategory(page, localeOptions)
|
||||
autoCategory(app, page, localeOptions)
|
||||
pageContentRendered(page)
|
||||
},
|
||||
}
|
||||
|
||||
@ -148,15 +148,15 @@ export interface PlumeThemeLocaleData extends LocaleData {
|
||||
/**
|
||||
* language text
|
||||
*/
|
||||
// selectLanguageText?: string
|
||||
selectLanguageText?: string
|
||||
/**
|
||||
* language aria label
|
||||
*/
|
||||
// selectLanguageAriaLabel?: string
|
||||
selectLanguageAriaLabel?: string
|
||||
/**
|
||||
* language name
|
||||
*/
|
||||
// selectLanguageName?: string
|
||||
selectLanguageName?: string
|
||||
/**
|
||||
* repository of navbar
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user