Merge pull request #37 from pengzhanbo/RC-14

RC-14
This commit is contained in:
pengzhanbo 2024-01-05 01:46:43 +08:00 committed by GitHub
commit b6783d1198
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 766 additions and 522 deletions

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint"
]
}

31
.vscode/settings.json vendored
View File

@ -11,6 +11,36 @@
"css.validate": false,
"scss.validate": false,
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.experimental.useFlatConfig": true,
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off" },
{ "rule": "*-indent", "severity": "off" },
{ "rule": "*-spacing", "severity": "off" },
{ "rule": "*-spaces", "severity": "off" },
{ "rule": "*-order", "severity": "off" },
{ "rule": "*-dangle", "severity": "off" },
{ "rule": "*-newline", "severity": "off" },
{ "rule": "*quotes", "severity": "off" },
{ "rule": "*semi", "severity": "off" }
],
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"editor.formatOnPaste": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
],
"cSpell.words": [
"bumpp",
"caniuse",
@ -23,6 +53,7 @@
"gsap",
"iarna",
"iconify",
"katex",
"leancloud",
"nprogress",
"pnpm",

View File

@ -3,8 +3,7 @@ import process from 'node:process'
import { viteBundler } from '@vuepress/bundler-vite'
import { webpackBundler } from '@vuepress/bundler-webpack'
import { defineUserConfig } from '@vuepress/cli'
import themePlume from 'vuepress-theme-plume'
import { enNotes, zhNotes } from './notes.js'
import { theme } from './theme.js'
export default defineUserConfig({
base: '/',
@ -14,117 +13,9 @@ export default defineUserConfig({
source: path.resolve(__dirname, '../'),
public: path.resolve(__dirname, 'public'),
locales: {
'/': {
title: 'Plume主题',
description: '',
lang: 'zh-CN',
},
'/en/': {
title: 'Plume Theme',
description: '',
lang: 'en',
},
'/': { title: 'Plume主题', description: '', lang: 'zh-CN' },
'/en/': { title: 'Plume Theme', description: '', lang: 'en' },
},
bundler:
process.env.DOCS_BUNDLER === 'webpack' ? webpackBundler() : viteBundler(),
theme: themePlume({
logo: 'https://pengzhanbo.cn/g.gif',
hostname: 'https://pengzhanbo.cn',
repo: 'https://github.com/pengzhanbo/vuepress-theme-plume',
docsDir: 'docs',
editLink: true,
editLinkText: 'Edit this page on GitHub',
appearance: true,
avatar: {
url: '/images/blogger.jpg',
name: 'Plume Theme',
description: 'The Theme for Vuepress 2.0',
},
social: [{ icon: 'github', link: 'https://github.com/pengzhanbo' }],
notes: zhNotes,
navbar: [
{ text: '首页', link: '/', icon: 'material-symbols:home-outline' },
{
text: '博客',
link: '/blog/',
icon: 'material-symbols:article-outline',
},
{
text: 'VuePress',
icon: 'vscode-icons:file-type-vue',
items: [
{
text: 'theme-plume',
link: '/note/vuepress-theme-plume/',
icon: 'icon-park-outline:theme',
},
{
text: '插件',
icon: 'mingcute:plugin-2-line',
items: [
{
text: 'caniuse',
link: '/note/vuepress-plugin/caniuse/',
icon: 'tabler:brand-css3',
},
{
text: 'netlify-functions',
link: '/note/vuepress-plugin/netlify-functions/',
icon: 'teenyicons:netlify-outline',
},
],
},
],
},
{ text: '友情链接', link: '/friends/', icon: 'emojione-monotone:roller-coaster' },
],
footer: {
copyright: 'Copyright © 2022-present pengzhanbo',
},
themePlugins: {
markdownEnhance: { katex: true },
search: {
locales: {
'/': {
placeholder: '搜索',
},
},
},
},
locales: {
'/': { selectLanguageName: '简体中文', selectLanguageText: '选择语言' },
'/en/': {
selectLanguageName: 'English',
selectLanguageText: 'Language',
notes: enNotes,
navbar: [
{ text: 'Home', link: '/en/', icon: 'material-symbols:home-outline' },
{
text: 'Blog',
link: '/en/blog/',
icon: 'material-symbols:article-outline',
},
{
text: 'VuePress',
icon: 'vscode-icons:file-type-vue',
items: [
{
text: 'Plugin',
icon: 'mingcute:plugin-2-line',
items: [
{
text: 'caniuse',
link: '/en/note/vuepress-plugin/caniuse/',
icon: 'tabler:brand-css3',
},
],
},
],
},
],
},
},
}),
bundler: process.env.DOCS_BUNDLER === 'webpack' ? webpackBundler() : viteBundler(),
theme,
})

64
docs/.vuepress/navbar.ts Normal file
View File

@ -0,0 +1,64 @@
import type { NavItem } from 'vuepress-theme-plume'
export const zhNavbar = [
{ text: '首页', link: '/', icon: 'material-symbols:home-outline' },
{
text: '博客',
link: '/blog/',
icon: 'material-symbols:article-outline',
},
{
text: 'VuePress',
icon: 'vscode-icons:file-type-vue',
items: [
{
text: 'theme-plume',
link: '/note/vuepress-theme-plume/',
icon: 'icon-park-outline:theme',
},
{
text: '插件',
icon: 'mingcute:plugin-2-line',
items: [
{
text: 'caniuse',
link: '/note/vuepress-plugin/caniuse/',
icon: 'tabler:brand-css3',
},
{
text: 'netlify-functions',
link: '/note/vuepress-plugin/netlify-functions/',
icon: 'teenyicons:netlify-outline',
},
],
},
],
},
{ text: '友情链接', link: '/friends/', icon: 'emojione-monotone:roller-coaster' },
] as NavItem[]
export const enNavbar = [
{ text: 'Home', link: '/en/', icon: 'material-symbols:home-outline' },
{
text: 'Blog',
link: '/en/blog/',
icon: 'material-symbols:article-outline',
},
{
text: 'VuePress',
icon: 'vscode-icons:file-type-vue',
items: [
{
text: 'Plugin',
icon: 'mingcute:plugin-2-line',
items: [
{
text: 'caniuse',
link: '/en/note/vuepress-plugin/caniuse/',
icon: 'tabler:brand-css3',
},
],
},
],
},
] as NavItem[]

View File

@ -5,7 +5,6 @@ export const zhNotes = definePlumeNotesConfig({
link: '/note',
notes: [
{
text: '',
dir: 'vuepress-theme-plume',
link: '/vuepress-theme-plume/',
sidebar: [
@ -34,7 +33,6 @@ export const zhNotes = definePlumeNotesConfig({
},
{
dir: 'vuepress-plugin',
text: '',
link: '/vuepress-plugin/',
sidebar: [
'caniuse/README',
@ -50,12 +48,11 @@ export const zhNotes = definePlumeNotesConfig({
})
export const enNotes = definePlumeNotesConfig({
dir: 'notes',
link: '/note',
dir: 'en/notes',
link: '/en/note',
notes: [
{
dir: 'vuepress-plugin',
text: '',
link: '/vuepress-plugin/',
sidebar: ['caniuse/README'],
},

49
docs/.vuepress/theme.ts Normal file
View File

@ -0,0 +1,49 @@
import themePlume from 'vuepress-theme-plume'
import { enNotes, zhNotes } from './notes.js'
import { enNavbar, zhNavbar } from './navbar.js'
export const theme = themePlume({
logo: 'https://pengzhanbo.cn/g.gif',
hostname: 'https://pengzhanbo.cn',
repo: 'https://github.com/pengzhanbo/vuepress-theme-plume',
docsDir: 'docs',
editLink: true,
editLinkText: '在 GitHub 编辑此页',
appearance: true,
avatar: {
url: '/images/blogger.jpg',
name: 'Plume Theme',
description: 'The Theme for Vuepress 2.0',
},
social: [{ icon: 'github', link: 'https://github.com/pengzhanbo' }],
footer: { copyright: 'Copyright © 2022-present pengzhanbo' },
locales: {
'/': {
selectLanguageName: '简体中文',
selectLanguageText: '选择语言',
notes: zhNotes,
navbar: zhNavbar,
},
'/en/': {
selectLanguageName: 'English',
selectLanguageText: 'Language',
editLinkText: 'Edit this page on GitHub',
notes: enNotes,
navbar: enNavbar,
},
},
plugins: {
markdownEnhance: { katex: true },
search: {
locales: {
'/': {
placeholder: '搜索',
},
'/en/': {
placeholder: 'Search',
},
},
},
},
})

View File

@ -17,9 +17,9 @@
"anywhere": "^1.6.0",
"katex": "^0.16.9",
"leancloud-storage": "^4.15.2",
"sass": "^1.69.6",
"sass": "^1.69.7",
"sass-loader": "^13.3.3",
"vue": "^3.4.3",
"vue": "^3.4.5",
"vuepress-theme-plume": "workspace:*"
}
}

View File

@ -3,7 +3,7 @@
"type": "module",
"version": "1.0.0-rc.13",
"private": true,
"packageManager": "pnpm@8.13.1",
"packageManager": "pnpm@8.14.0",
"author": "pengzhanbo",
"license": "MIT",
"keywords": [
@ -19,7 +19,7 @@
},
"scripts": {
"build": "pnpm run build:package",
"build:package": "pnpm --filter=!vuepress-theme-plume-monorepo --filter=!docs run -r --stream build",
"build:package": "pnpm --filter=!vuepress-theme-plume-monorepo --filter=!docs --filter=!plugin-page-collection run -r --stream build",
"commit": "cz",
"dev": "concurrently \"pnpm run dev:package\" \"pnpm run docs\"",
"dev:package": "pnpm --filter=!vuepress-theme-plume-monorepo --filter=!docs --parallel dev",
@ -38,8 +38,8 @@
"release:version": "bumpp package.json plugins/*/package.json theme/package.json --execute=\"pnpm release:changelog\" --commit \"build: publish v%s\" --all --tag --push"
},
"devDependencies": {
"@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3",
"@commitlint/cli": "^18.4.4",
"@commitlint/config-conventional": "^18.4.4",
"@pengzhanbo/eslint-config-vue": "^1.5.1",
"@types/minimist": "^1.2.5",
"@types/node": "20.9.1",
@ -60,7 +60,7 @@
"lint-staged": "^15.2.0",
"minimist": "^1.2.8",
"ora": "^8.0.1",
"pnpm": "^8.13.1",
"pnpm": "^8.14.0",
"rimraf": "^5.0.5",
"sort-package-json": "^2.6.0",
"taze": "^0.13.1",

View File

@ -42,7 +42,7 @@
"@vuepress/utils": "2.0.0-rc.0",
"chokidar": "^3.5.3",
"create-filter": "^1.0.1",
"vue": "^3.4.3"
"vue": "^3.4.5"
},
"publishConfig": {
"access": "public"

View File

@ -39,7 +39,7 @@
"@vuepress/client": "2.0.0-rc.0",
"@vuepress/core": "2.0.0-rc.0",
"@vuepress/utils": "2.0.0-rc.0",
"vue": "^3.4.3",
"vue": "^3.4.5",
"vue-router": "4.2.5"
},
"publishConfig": {

View File

@ -41,7 +41,7 @@
"@vuepress/core": "2.0.0-rc.0",
"@vuepress/shared": "2.0.0-rc.0",
"@vuepress/utils": "2.0.0-rc.0",
"vue": "^3.4.3"
"vue": "^3.4.5"
},
"publishConfig": {
"access": "public"

View File

@ -41,7 +41,7 @@
"@vuepress/core": "2.0.0-rc.0",
"@vuepress/shared": "2.0.0-rc.0",
"@vuepress/utils": "2.0.0-rc.0",
"vue": "^3.4.3"
"vue": "^3.4.5"
},
"publishConfig": {
"access": "public"

View File

@ -51,7 +51,7 @@
"dotenv": "^16.3.1",
"esbuild": "^0.19.11",
"execa": "^8.0.1",
"netlify-cli": "^17.10.2",
"netlify-cli": "^17.11.0",
"portfinder": "^1.0.32"
},
"devDependencies": {

View File

@ -43,7 +43,7 @@
"@vuepress/utils": "2.0.0-rc.0",
"chokidar": "^3.5.3",
"create-filter": "^1.0.1",
"vue": "^3.4.3"
"vue": "^3.4.5"
},
"publishConfig": {
"access": "public"

View File

@ -3,7 +3,7 @@ import { getDirname, path } from '@vuepress/utils'
import type { NotesDataOptions } from '../shared/index.js'
import { prepareNotesData, watchNotesData } from './prepareNotesData.js'
export function notesDataPlugin(options: NotesDataOptions): Plugin {
export function notesDataPlugin(options: NotesDataOptions | NotesDataOptions[]): Plugin {
return (app: App) => {
return {
name: '@vuepress-plume/plugin-notes-data',

View File

@ -5,7 +5,7 @@ import { createFilter } from 'create-filter'
import type {
NotesData,
NotesDataOptions,
NotesItem,
NotesItemOptions,
NotesSidebar,
NotesSidebarItem,
} from '../shared/index.js'
@ -32,10 +32,11 @@ interface NotePage {
link: string
}
export async function prepareNotesData(app: App, { include, exclude, notes, dir, link }: NotesDataOptions) {
function resolvedNotesData(app: App, options: NotesDataOptions, result: NotesData) {
const { include, exclude, notes, dir: _dir, link } = options
if (!notes || notes.length === 0)
return
dir = normalizePath(dir)
const dir = normalizePath(_dir)
const filter = createFilter(ensureArray(include), ensureArray(exclude), {
resolve: false,
})
@ -47,23 +48,27 @@ export async function prepareNotesData(app: App, { include, exclude, notes, dir,
&& page.filePathRelative.startsWith(dir)
&& filter(page.filePathRelative),
)
.map((page) => {
return {
relativePath: page.filePathRelative?.replace(DIR_PATTERN, '') || '',
title: page.title,
link: page.path,
}
})
const notesData: NotesData = {}
.map(page => ({
relativePath: page.filePathRelative?.replace(DIR_PATTERN, '') || '',
title: page.title,
link: page.path,
}))
notes.forEach((note) => {
notesData[normalizePath(path.join('/', link, note.link))] = initSidebar(
result[normalizePath(path.join('/', link, note.link))] = initSidebar(
note,
notesPageList.filter(page =>
page.relativePath.startsWith(note.dir.trim().replace(/^\/|\/$/g, '')),
),
)
})
}
export async function prepareNotesData(app: App, options: NotesDataOptions | NotesDataOptions[]) {
const notesData: NotesData = {}
const allOptions = ensureArray<NotesDataOptions>(options)
allOptions.forEach(option => resolvedNotesData(app, option, notesData))
let content = `
export const notesData = ${JSON.stringify(notesData, null, 2)}
`
@ -73,22 +78,25 @@ export const notesData = ${JSON.stringify(notesData, null, 2)}
await app.writeTemp('internal/notesData.js', content)
}
export function watchNotesData(app: App, watchers: any[], options: NotesDataOptions): void {
if (!options.notes || options.notes.length === 0 || !options.dir)
return
const dir = path.join('pages', options.dir, '**/*')
export function watchNotesData(app: App, watchers: any[], options: NotesDataOptions | NotesDataOptions[]): void {
const allOptions = ensureArray<NotesDataOptions>(options)
const [firstLink, ...links] = allOptions.map(option => option.link)
const dir = path.join('pages', firstLink, '**/*')
const watcher = chokidar.watch(dir, {
cwd: app.dir.temp(),
ignoreInitial: true,
})
links.length && watcher.add(links.map(link => path.join('pages', link, '**/*')))
watcher.on('add', () => prepareNotesData(app, options))
watcher.on('change', () => prepareNotesData(app, options))
watcher.on('unlink', () => prepareNotesData(app, options))
watchers.push(watcher)
}
function initSidebar(note: NotesItem, pages: NotePage[]): NotesSidebarItem[] {
function initSidebar(note: NotesItemOptions, pages: NotePage[]): NotesSidebarItem[] {
if (!note.sidebar)
return []
if (note.sidebar === 'auto')
@ -97,7 +105,7 @@ function initSidebar(note: NotesItem, pages: NotePage[]): NotesSidebarItem[] {
}
function initSidebarByAuto(
note: NotesItem,
note: NotesItemOptions,
pages: NotePage[],
): NotesSidebarItem[] {
pages = pages.sort((prev, next) => {
@ -135,7 +143,7 @@ function initSidebarByAuto(
}
function initSidebarByConfig(
{ text, dir, sidebar }: NotesItem,
{ text, dir, sidebar }: NotesItemOptions,
pages: NotePage[],
): NotesSidebarItem[] {
return (sidebar as NotesSidebar).map((item) => {

View File

@ -3,9 +3,11 @@ export interface NotesDataOptions {
link: string
include?: string | string[]
exclude?: string | string[]
notes: NotesItem[]
notes: NotesItemOptions[]
}
export type NotesItemOptions = (Omit<NotesItem, 'text'> & { text?: string })
export interface NotesItem {
dir: string
link: string

View File

@ -37,7 +37,7 @@
"@vuepress/shared": "2.0.0-rc.0",
"@vuepress/utils": "2.0.0-rc.0",
"leancloud-storage": "^4.15.2",
"vue": "^3.4.3",
"vue": "^3.4.5",
"vue-router": "4.2.5",
"vuepress-plugin-netlify-functions": "workspace:*"
},

View File

@ -36,8 +36,8 @@
"@vuepress/utils": "2.0.0-rc.0",
"nanoid": "^5.0.4",
"picocolors": "^1.0.0",
"shikiji": "^0.9.16",
"shikiji-transformers": "^0.9.16"
"shikiji": "^0.9.17",
"shikiji-transformers": "^0.9.17"
},
"publishConfig": {
"access": "public"

558
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "vuepress-theme-plume",
"type": "module",
"version": "1.0.0-rc.13",
"description": "A Blog Theme for VuePress 2.0",
"description": "A Blog&Document Theme for VuePress 2.0",
"author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
"license": "MIT",
"homepage": "https://pengzhanbo.cn/note/vuepress-theme-plume",
@ -15,7 +15,7 @@
},
"keywords": [
"VuePress",
"Theme",
"theme",
"plume",
"vuepress-theme",
"vuepress-theme-plume",
@ -79,10 +79,10 @@
"@vuepress/utils": "2.0.0-rc.0",
"@vueuse/core": "^10.7.1",
"date-fns": "^3.0.6",
"katex": "^0.16.9",
"lodash.merge": "^4.6.2",
"nanoid": "^5.0.4",
"ts-debounce": "^4.0.0",
"vue": "^3.4.3",
"vue": "^3.4.5",
"vue-router": "4.2.5",
"vuepress-plugin-comment2": "2.0.0-rc.10",
"vuepress-plugin-md-enhance": "2.0.0-rc.10",

View File

@ -14,7 +14,7 @@ const { currentLang, localeLinks } = useLangs()
v-if="localeLinks.length && currentLang.label"
class="navbar-translations"
:icon="IconLanguages"
:label="theme.selectLanguageText || 'change language'"
:label="theme.selectLanguageText || 'Change Language'"
>
<div class="items">
<p class="title">

View File

@ -1,16 +1,20 @@
import { usePageLang } from '@vuepress/client'
import { useBlogPostData } from '@vuepress-plume/plugin-blog-data/client'
import { computed, ref } from 'vue'
import type { Ref } from 'vue'
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
import { useLocaleLink, useThemeLocaleData } from '../composables/index.js'
import { getRandomColor, toArray } from '../utils/index.js'
export function usePostListControl() {
export function useLocalePostList() {
const locale = usePageLang()
const list = useBlogPostData()
return computed(() => list.value.filter(item => item.lang === locale.value))
}
export function usePostListControl() {
const themeData = useThemeLocaleData()
const list = useBlogPostData() as unknown as Ref<PlumeThemeBlogPostItem[]>
const list = useLocalePostList()
const blog = computed(() => themeData.value.blog || {})
const pagination = computed(() => blog.value.pagination || {})
@ -29,7 +33,7 @@ export function usePostListControl() {
return next.sticky > prev.sticky ? 1 : -1
}),
...otherList,
].filter(item => item.lang === locale.value)
]
})
const page = ref(1)
@ -109,15 +113,10 @@ export function useBlogExtract() {
export type ShortPostItem = Pick<PlumeThemeBlogPostItem, 'title' | 'path' | 'createTime'>
export function useTags() {
const locale = usePageLang()
const list = useBlogPostData() as unknown as Ref<PlumeThemeBlogPostItem[]>
const filteredList = computed(() =>
list.value.filter(item => item.lang === locale.value),
)
const list = useLocalePostList()
const tags = computed(() => {
const tagMap: Record<string, number> = {}
filteredList.value.forEach((item) => {
list.value.forEach((item) => {
if (item.tags) {
toArray(item.tags).forEach((tag) => {
if (tagMap[tag])
@ -139,7 +138,7 @@ export function useTags() {
const handleTagClick = (tag: string) => {
currentTag.value = tag
postList.value = filteredList.value.filter((item) => {
postList.value = list.value.filter((item) => {
if (item.tags)
return toArray(item.tags).includes(tag)
@ -160,15 +159,11 @@ export function useTags() {
}
export function useArchives() {
const locale = usePageLang()
const list = useBlogPostData() as unknown as Ref<PlumeThemeBlogPostItem[]>
const filteredList = computed(() =>
list.value.filter(item => item.lang === locale.value),
)
const list = useLocalePostList()
const archives = computed(() => {
const archives: { label: string, list: ShortPostItem[] }[] = []
filteredList.value.forEach((item) => {
list.value.forEach((item) => {
const createTime = item.createTime.split(' ')[0]
const year = createTime.split('/')[0]
let current = archives.find(archive => archive.label === year)
@ -186,7 +181,5 @@ export function useArchives() {
return archives
})
return {
archives,
}
return { archives }
}

View File

@ -3,6 +3,7 @@ import { computed } from 'vue'
import type { PlumeThemePageData } from '../../shared/index.js'
import { ensureStartingSlash } from '../utils/index.js'
import { useThemeData } from './themeData.js'
import { normalizePath } from './sidebar.js'
export function useLangs({
removeCurrent = true,
@ -20,18 +21,34 @@ export function useLangs({
}
})
const addPath = computed(() => {
if (page.value.frontmatter.home || (page.value.type && page.value.type !== 'friends'))
return true
return correspondingLink
})
const getBlogLink = (locale: string) => {
const blog = theme.value.locales?.[`/${locale}/`]?.blog
const defaultBlog = theme.value.locales?.['/']?.blog ?? theme.value.blog
const link = blog?.link ? blog.link : normalizePath(`${locale}${defaultBlog?.link || 'blog/'}`)
return 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,
),
link: page.value.isBlogPost
? getBlogLink(key)
: normalizeLink(
key,
addPath.value,
page.value.path.slice(currentLang.value.link.length - 1),
true,
),
},
),
)

View File

@ -13,6 +13,8 @@ import type {
PlumeThemePluginOptions,
} from '../shared/index.js'
import { getCurrentDirname, getPackage, nanoid, pathJoin } from './utils.js'
import { resolveNotesList } from './resolveNotesList.js'
import { resolveLocaleOptions } from './resolveLocaleOptions.js'
export default function autoFrontmatter(
app: App,
@ -21,18 +23,12 @@ export default function autoFrontmatter(
): AutoFrontmatterOptions {
const sourceDir = app.dir.source()
const pkg = getPackage()
const { locales = {}, avatar, article: articlePrefix = '/article/' } = localeOption
const { locales = {}, article: articlePrefix = '/article/' } = localeOption
const { frontmatter } = options
const localesNotesDirs = Object.keys(app.siteData.locales || {})
.map((locale) => {
// fixed: #15
const notes = locales[locale]?.notes
if (!notes)
return ''
return notes.dir ? pathJoin(locale, notes.dir).replace(/^\//, '') : ''
})
const avatar = resolveLocaleOptions(localeOption, 'avatar')
const notesList = resolveNotesList(localeOption)
const localesNotesDirs = notesList
.map(notes => notes.dir?.replace(/^\//, ''))
.filter(Boolean)
const baseFrontmatter: FrontmatterObject = {
@ -58,7 +54,7 @@ export default function autoFrontmatter(
return resolveLocalePath(localeOption.locales!, file)
}
const notesByLocale = (locale: string) => {
const notes = locales[locale]?.notes || localeOption.notes
const notes = resolveLocaleOptions(localeOption, 'notes', locale)
if (notes === false)
return undefined
return notes
@ -162,7 +158,13 @@ export default function autoFrontmatter(
if (permalink)
return permalink
const locale = resolveLocale(filepath)
return pathJoin(locale, articlePrefix, nanoid(), '/')
const prefix = resolveLocaleOptions(localeOption, 'article', locale, false)
const args: string[] = []
prefix
? args.push(prefix)
: args.push(locale, articlePrefix)
return pathJoin(...args, nanoid(), '/')
},
},
},

View File

@ -1,12 +1,23 @@
import type {
NotesDataOptions,
NotesItem,
NotesItemOptions,
} from '@vuepress-plume/plugin-notes-data'
import type { NavItem } from '../shared/index.js'
export function definePlumeNotesConfig(notes: NotesDataOptions): NotesDataOptions {
return notes
}
export const definePlumeNotesItemConfig = (item: NotesItem): NotesItem => item
export function definePlumeNotesItemConfig(item: NotesItemOptions): NotesItemOptions {
return item
}
export type { NotesDataOptions, NotesItem }
export function defineNavbar(navbar: NavItem[]): NavItem[] {
return navbar
}
export type {
NotesDataOptions,
NotesItemOptions,
NotesItemOptions as NotesItem,
}

View File

@ -1,4 +1,3 @@
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'
@ -28,62 +27,56 @@ import type {
PlumeThemePluginOptions,
} from '../shared/index.js'
import autoFrontmatter from './autoFrontmatter.js'
import { resolveLocaleOptions } from './resolveLocaleOptions.js'
import { pathJoin } from './utils.js'
import { resolveNotesList } from './resolveNotesList.js'
export function setupPlugins(app: App, options: PlumeThemePluginOptions, localeOptions: PlumeThemeLocaleOptions): PluginConfig {
export function setupPlugins(
app: App,
options: PlumeThemePluginOptions,
localeOptions: PlumeThemeLocaleOptions,
): PluginConfig {
const isProd = !app.env.isDev
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(/\\+/g, '/').replace(/^\//, '')
: ''
})
const notesList = resolveNotesList(localeOptions)
const notesDirList = notesList
.map(notes => notes.dir && pathJoin(notes.dir, '**').replace(/^\//, ''))
.filter(Boolean)
const blog = resolveLocaleOptions(localeOptions, 'blog')
const plugins: PluginConfig = [
palettePlugin({ preset: 'sass' }),
themeDataPlugin({
themeData: {
...localeOptions,
notes: localeOptions.notes
? { dir: localeOptions.notes.dir, link: localeOptions.notes.link }
: undefined,
} as any,
}),
themeDataPlugin({ themeData: localeOptions }),
autoFrontmatterPlugin(autoFrontmatter(app, options, localeOptions)),
blogDataPlugin({
include: localeOptions.blog?.include ?? ['**/*.md'],
include: blog?.include ?? ['**/*.md'],
exclude: [
'**/{README,readme,index}.md',
'.vuepress/',
'node_modules/',
...(localeOptions.blog?.exclude ?? []),
...localeNotesDirs,
...(blog?.exclude ?? []),
...notesDirList,
].filter(Boolean),
sortBy: 'createTime',
excerpt: true,
pageFilter(page: any) {
if (page.frontmatter.article !== undefined)
return !!page.frontmatter.article
return true
},
extendBlogData(page: any) {
return {
categoryList: page.data.categoryList,
tags: page.frontmatter.tags,
sticky: page.frontmatter.sticky,
createTime: page.data.frontmatter.createTime,
lang: page.lang,
}
},
pageFilter: (page: any) => page.frontmatter.article !== undefined
? !!page.frontmatter.article
: true,
extendBlogData: (page: any) => ({
categoryList: page.data.categoryList,
tags: page.frontmatter.tags,
sticky: page.frontmatter.sticky,
createTime: page.data.frontmatter.createTime,
lang: page.lang,
}),
}),
notesDataPlugin(notesList),
iconifyPlugin(),
contentUpdatePlugin(),
@ -99,37 +92,27 @@ export function setupPlugins(app: App, options: PlumeThemePluginOptions, localeO
if (options.readingTime !== false)
plugins.push(readingTimePlugin(options.readingTime || {}))
if (localeOptions.notes)
plugins.push(notesDataPlugin(localeOptions.notes))
if (options.nprogress !== false)
plugins.push(nprogressPlugin())
if (options.git !== false) {
plugins.push(gitPlugin({
createdTime: false,
updatedTime: localeOptions.lastUpdated !== false,
contributors: localeOptions.contributors !== false,
updatedTime: resolveLocaleOptions(localeOptions, 'lastUpdated') !== false,
contributors: resolveLocaleOptions(localeOptions, 'contributors') !== false,
}))
}
if (options.mediumZoom !== false) {
plugins.push(mediumZoomPlugin({
selector: '.plume-content > img, .plume-content :not(a) > img',
zoomOptions: {
background: 'var(--vp-c-bg)',
},
zoomOptions: { background: 'var(--vp-c-bg)' },
delay: 300,
}))
}
if (options.caniuse !== false) {
plugins.push(caniusePlugin(
options.caniuse || {
mode: 'embed',
},
))
}
if (options.caniuse !== false)
plugins.push(caniusePlugin(options.caniuse || { mode: 'embed' }))
if (options.externalLinkIcon !== false) {
plugins.push(externalLinkIconPlugin({
@ -150,14 +133,11 @@ export function setupPlugins(app: App, options: PlumeThemePluginOptions, localeO
plugins.push(searchPlugin(options.search))
if (options.docsearch !== false && !options.search) {
if (options.docsearch?.appId && options.docsearch?.apiKey) {
if (options.docsearch?.appId && options.docsearch?.apiKey)
plugins.push(docsearchPlugin(options.docsearch))
}
else {
console.error(
'docsearch plugin: appId and apiKey are both required',
)
}
else
console.error('docsearch plugin: appId and apiKey are both required')
}
if (options.shikiji !== false) {
@ -180,7 +160,6 @@ export function setupPlugins(app: App, options: PlumeThemePluginOptions, localeO
{
hint: true, // info note tip warning danger details
codetabs: true,
tabs: true,
align: true,
mark: true,
tasklist: true,
@ -188,6 +167,7 @@ export function setupPlugins(app: App, options: PlumeThemePluginOptions, localeO
attrs: true,
sup: true,
sub: true,
katex: true,
} as MarkdownEnhanceOptions,
options.markdownEnhance || {},
),
@ -200,16 +180,15 @@ export function setupPlugins(app: App, options: PlumeThemePluginOptions, localeO
if (options.baiduTongji !== false && options.baiduTongji?.key)
plugins.push(baiduTongjiPlugin(options.baiduTongji))
if (options.sitemap !== false && localeOptions.hostname && isProd) {
plugins.push(sitemapPlugin({
hostname: localeOptions.hostname,
}))
}
const hostname = resolveLocaleOptions(localeOptions, 'hostname')
if (options.seo !== false && localeOptions.hostname && isProd) {
if (options.sitemap !== false && hostname && isProd)
plugins.push(sitemapPlugin({ hostname }))
if (options.seo !== false && hostname && isProd) {
plugins.push(seoPlugin({
hostname: localeOptions.hostname || '',
author: localeOptions.avatar?.name,
hostname,
author: resolveLocaleOptions(localeOptions, 'avatar')?.name,
}))
}

View File

@ -0,0 +1,26 @@
import type { PlumeThemeLocaleOptions } from '../shared/index.js'
import { normalizePath } from './utils.js'
export function resolveLocaleOptions<
T extends PlumeThemeLocaleOptions = PlumeThemeLocaleOptions,
K extends Exclude<keyof T, 'locales'> = Exclude<keyof T, 'locales'>,
>(options: T, key: K, locale = '', fallback = true): T[K] | undefined {
const locales = options.locales
if (!locales)
return options[key]
locale = !locale || locale === '/' ? '/' : normalizePath(`/${locale}/`)
const localeOptions = locales[locale]
const fallbackLocaleOptions = locales['/']
if (!localeOptions)
return fallback ? options[key] : undefined
const _key = key as keyof typeof localeOptions
const fallbackData = (fallbackLocaleOptions[_key] ?? options[key]) as T[K]
const value = localeOptions[_key] as T[K]
return value ?? (fallback ? fallbackData : undefined)
}

View File

@ -0,0 +1,15 @@
import type { NotesDataOptions } from '@vuepress-plume/plugin-notes-data'
import type { PlumeThemeLocaleOptions } from '../shared/index.js'
import { resolveLocaleOptions } from './resolveLocaleOptions.js'
export function resolveNotesList(options: PlumeThemeLocaleOptions) {
const locales = options.locales || {}
const notesList: NotesDataOptions[] = []
for (const locale of Object.keys(locales)) {
const notes = resolveLocaleOptions(options, 'notes', locale, false)
notes && notesList.push(notes)
}
return notesList
}

View File

@ -7,15 +7,16 @@ import type {
PlumeThemePageData,
} from '../shared/index.js'
import { pathJoin } from './utils.js'
import { resolveLocaleOptions } from './resolveLocaleOptions.js'
export async function setupPage(
app: App,
localeOption: PlumeThemeLocaleOptions,
) {
const locales = Object.keys(app.siteData.locales || {})
const defaultBlog = resolveLocaleOptions(localeOption, 'blog')
for (const [, locale] of locales.entries()) {
const blog = localeOption.locales?.[locale]?.blog
const defaultBlog = localeOption.blog
const blog = resolveLocaleOptions(localeOption, 'blog', locale, false)
const link = blog?.link
? blog.link
: pathJoin('/', locale, defaultBlog?.link || '/blog/')

View File

@ -1,47 +1,35 @@
import type { App, Page, Theme } from '@vuepress/core'
import { fs, getDirname, path, templateRenderer } from '@vuepress/utils'
import type { Page, Theme } from '@vuepress/core'
import { templateRenderer } from '@vuepress/utils'
import type { PlumeThemeOptions, PlumeThemePageData } from '../shared/index.js'
import { mergeLocaleOptions } from './defaultOptions.js'
import { setupPlugins } from './plugins.js'
import { extendsPageData, setupPage } from './setupPages.js'
import { getThemePackage, resolve, templates } from './utils.js'
const __dirname = getDirname(import.meta.url)
const name = 'vuepress-theme-plume'
const resolve = (...args: string[]) => path.resolve(__dirname, '../', ...args)
const templates = (url: string) => resolve('../templates', url)
function getThemePackage() {
let pkg = {} as any
try {
const content = fs.readFileSync(resolve('../package.json'), 'utf-8')
pkg = JSON.parse(content)
}
catch {}
return pkg
}
const THEME_NAME = 'vuepress-theme-plume'
export function plumeTheme({
themePlugins = {},
themePlugins,
plugins,
...localeOptions
}: PlumeThemeOptions = {}): Theme {
localeOptions = mergeLocaleOptions(localeOptions)
const pluginsOptions = plugins ?? themePlugins ?? {}
const pkg = getThemePackage()
return (app: App) => {
return {
name,
templateBuild: templates('build.html'),
clientConfigFile: resolve('client/config.js'),
plugins: setupPlugins(app, themePlugins, localeOptions),
onInitialized: app => setupPage(app, localeOptions),
extendsPage: (page: Page<PlumeThemePageData>) =>
extendsPageData(app, page, localeOptions),
templateBuildRenderer(template, context) {
template = template
.replace('{{ themeVersion }}', pkg.version || '')
.replace(/^\s+|\s+$/gm, '')
.replace(/\n/g, '')
return templateRenderer(template, context)
},
}
}
return app => ({
name: THEME_NAME,
templateBuild: templates('build.html'),
clientConfigFile: resolve('client/config.js'),
plugins: setupPlugins(app, pluginsOptions, localeOptions),
onInitialized: app => setupPage(app, localeOptions),
extendsPage: page => extendsPageData(app, page as Page<PlumeThemePageData>, localeOptions),
templateBuildRenderer(template, context) {
template = template
.replace('{{ themeVersion }}', pkg.version || '')
.replace(/^\s+|\s+$/gm, '')
.replace(/\n/g, '')
return templateRenderer(template, context)
},
})
}

View File

@ -2,6 +2,12 @@ import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { customAlphabet } from 'nanoid'
import { getDirname } from '@vuepress/utils'
const __dirname = getDirname(import.meta.url)
export const resolve = (...args: string[]) => path.resolve(__dirname, '../', ...args)
export const templates = (url: string) => resolve('../templates', url)
export const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 8)
@ -15,7 +21,17 @@ export function getPackage() {
return pkg
}
const RE_SLASH = /\\+/g
export function getThemePackage() {
let pkg = {} as any
try {
const content = fs.readFileSync(resolve('../package.json'), 'utf-8')
pkg = JSON.parse(content)
}
catch {}
return pkg
}
const RE_SLASH = /(\\|\/)+/g
export function normalizePath(dir: string) {
return dir.replace(RE_SLASH, '/')
}

View File

@ -5,8 +5,15 @@ import type { PlumeThemePluginOptions } from './plugins.js'
export interface PlumeThemeOptions extends PlumeThemeLocaleOptions {
/**
* 使
* @deprecated `plugins`
*/
themePlugins?: PlumeThemePluginOptions
/**
* 使
*/
plugins?: PlumeThemePluginOptions
}
export type PlumeThemeLocaleOptions = PlumeThemeData

View File

@ -30,8 +30,14 @@ export interface PlumeThemePluginOptions {
*/
docsearch?: false | DocsearchOptions
/**
*
*/
shikiji?: false | ShikijiPluginOptions
/**
* git
*/
git?: false
nprogress?: false