@jet-w/astro-blog 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/index.d.ts +2 -92
- package/dist/index.d.ts +4 -2
- package/dist/sidebar-DNdiCKBw.d.ts +92 -0
- package/dist/utils/sidebar.d.ts +98 -0
- package/dist/utils/sidebar.js +305 -0
- package/package.json +5 -3
- package/src/components/about/SocialLinks.astro +1 -1
- package/src/components/blog/Hero.astro +1 -1
- package/src/components/layout/Footer.astro +1 -2
- package/src/components/layout/Header.astro +4 -4
- package/src/components/layout/Sidebar.astro +3 -3
- package/src/components/ui/MobileMenu.vue +1 -1
- package/src/components/ui/SearchInterface.vue +1 -1
- package/src/layouts/BaseLayout.astro +3 -3
- package/src/layouts/SlidesLayout.astro +2 -2
- package/src/utils/sidebar.ts +0 -492
package/dist/config/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SiteConfig, NavigationItem } from '../types/index.js';
|
|
2
|
+
export { D as DividerConfig, M as ManualConfig, f as MixedConfig, P as PathMatchConfig, e as ScanConfig, S as SidebarConfig, b as SidebarGroup, c as SidebarItem, d as defaultSidebarConfig, a as defineSidebarConfig, s as sidebarConfig } from '../sidebar-DNdiCKBw.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Default site configuration
|
|
@@ -27,97 +28,6 @@ declare const menu: NavigationItem[];
|
|
|
27
28
|
declare function defineMenu(items: NavigationItem[]): NavigationItem[];
|
|
28
29
|
declare const defaultMenu: NavigationItem[];
|
|
29
30
|
|
|
30
|
-
/**
|
|
31
|
-
* 侧边栏配置系统
|
|
32
|
-
*
|
|
33
|
-
* 支持三种配置类型:
|
|
34
|
-
* 1. scan - 扫描指定文件夹,自动生成树形结构
|
|
35
|
-
* 2. manual - 手动配置显示特定内容
|
|
36
|
-
* 3. mixed - 混合使用以上两种方式
|
|
37
|
-
*/
|
|
38
|
-
interface PathMatchConfig {
|
|
39
|
-
pattern: string;
|
|
40
|
-
exact?: boolean;
|
|
41
|
-
}
|
|
42
|
-
interface SidebarItem {
|
|
43
|
-
title: string;
|
|
44
|
-
slug?: string;
|
|
45
|
-
link?: string;
|
|
46
|
-
icon?: string;
|
|
47
|
-
badge?: string;
|
|
48
|
-
badgeType?: 'info' | 'success' | 'warning' | 'error';
|
|
49
|
-
children?: SidebarItem[];
|
|
50
|
-
collapsed?: boolean;
|
|
51
|
-
}
|
|
52
|
-
interface ScanConfig {
|
|
53
|
-
type: 'scan';
|
|
54
|
-
title: string;
|
|
55
|
-
icon?: string;
|
|
56
|
-
scanPath: string;
|
|
57
|
-
collapsed?: boolean;
|
|
58
|
-
maxDepth?: number;
|
|
59
|
-
exclude?: string[];
|
|
60
|
-
include?: string[];
|
|
61
|
-
sortBy?: 'name' | 'date' | 'title' | 'custom';
|
|
62
|
-
sortOrder?: 'asc' | 'desc';
|
|
63
|
-
showForPaths?: string[];
|
|
64
|
-
hideForPaths?: string[];
|
|
65
|
-
}
|
|
66
|
-
interface ManualConfig {
|
|
67
|
-
type: 'manual';
|
|
68
|
-
title: string;
|
|
69
|
-
icon?: string;
|
|
70
|
-
collapsed?: boolean;
|
|
71
|
-
items: SidebarItem[];
|
|
72
|
-
showForPaths?: string[];
|
|
73
|
-
hideForPaths?: string[];
|
|
74
|
-
}
|
|
75
|
-
interface MixedConfig {
|
|
76
|
-
type: 'mixed';
|
|
77
|
-
title: string;
|
|
78
|
-
icon?: string;
|
|
79
|
-
collapsed?: boolean;
|
|
80
|
-
sections: (ScanConfig | ManualConfig)[];
|
|
81
|
-
showForPaths?: string[];
|
|
82
|
-
hideForPaths?: string[];
|
|
83
|
-
}
|
|
84
|
-
interface DividerConfig {
|
|
85
|
-
type: 'divider';
|
|
86
|
-
title?: string;
|
|
87
|
-
showForPaths?: string[];
|
|
88
|
-
hideForPaths?: string[];
|
|
89
|
-
}
|
|
90
|
-
type SidebarGroup = ScanConfig | ManualConfig | MixedConfig | DividerConfig;
|
|
91
|
-
interface SidebarConfig {
|
|
92
|
-
enabled: boolean;
|
|
93
|
-
width?: string;
|
|
94
|
-
position?: 'left' | 'right';
|
|
95
|
-
showSearch?: boolean;
|
|
96
|
-
showRecentPosts?: boolean;
|
|
97
|
-
recentPostsCount?: number;
|
|
98
|
-
showPopularTags?: boolean;
|
|
99
|
-
popularTagsCount?: number;
|
|
100
|
-
showArchives?: boolean;
|
|
101
|
-
archivesCount?: number;
|
|
102
|
-
showFriendLinks?: boolean;
|
|
103
|
-
friendLinks?: Array<{
|
|
104
|
-
title: string;
|
|
105
|
-
url: string;
|
|
106
|
-
icon?: string;
|
|
107
|
-
description?: string;
|
|
108
|
-
}>;
|
|
109
|
-
groups: SidebarGroup[];
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* 默认侧边栏配置
|
|
113
|
-
*/
|
|
114
|
-
declare const sidebarConfig: SidebarConfig;
|
|
115
|
-
/**
|
|
116
|
-
* Define sidebar configuration
|
|
117
|
-
*/
|
|
118
|
-
declare function defineSidebarConfig(config: Partial<SidebarConfig>): SidebarConfig;
|
|
119
|
-
declare const defaultSidebarConfig: SidebarConfig;
|
|
120
|
-
|
|
121
31
|
/**
|
|
122
32
|
* 社交链接配置
|
|
123
33
|
*/
|
|
@@ -163,4 +73,4 @@ declare const footerConfig: FooterConfig;
|
|
|
163
73
|
declare function defineFooterConfig(config: Partial<FooterConfig>): FooterConfig;
|
|
164
74
|
declare const defaultFooterConfig: FooterConfig;
|
|
165
75
|
|
|
166
|
-
export { type
|
|
76
|
+
export { type FooterConfig, type FooterLink, type SocialLink, defaultFooterConfig, defaultIcons, defaultMenu, defaultSEO, defaultSiteConfig, defaultSocialLinks, defineFooterConfig, defineMenu, defineSiteConfig, defineSocialLinks, footerConfig, menu, siteConfig, socialLinks };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { FooterConfig, SocialLink } from './config/index.js';
|
|
2
|
+
export { FooterLink, defaultFooterConfig, defaultIcons, defaultMenu, defaultSEO, defaultSiteConfig, defaultSocialLinks, defineFooterConfig, defineMenu, defineSiteConfig, defineSocialLinks, footerConfig, menu, siteConfig, socialLinks } from './config/index.js';
|
|
3
|
+
import { S as SidebarConfig } from './sidebar-DNdiCKBw.js';
|
|
4
|
+
export { D as DividerConfig, M as ManualConfig, f as MixedConfig, P as PathMatchConfig, e as ScanConfig, b as SidebarGroup, c as SidebarItem, d as defaultSidebarConfig, a as defineSidebarConfig, s as sidebarConfig } from './sidebar-DNdiCKBw.js';
|
|
3
5
|
import { SiteConfig } from './types/index.js';
|
|
4
6
|
export { BlogPost, Category, NavigationItem, PostFrontmatter, SEOProps, SearchResult, Tag } from './types/index.js';
|
|
5
7
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 侧边栏配置系统
|
|
3
|
+
*
|
|
4
|
+
* 支持三种配置类型:
|
|
5
|
+
* 1. scan - 扫描指定文件夹,自动生成树形结构
|
|
6
|
+
* 2. manual - 手动配置显示特定内容
|
|
7
|
+
* 3. mixed - 混合使用以上两种方式
|
|
8
|
+
*/
|
|
9
|
+
interface PathMatchConfig {
|
|
10
|
+
pattern: string;
|
|
11
|
+
exact?: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface SidebarItem {
|
|
14
|
+
title: string;
|
|
15
|
+
slug?: string;
|
|
16
|
+
link?: string;
|
|
17
|
+
icon?: string;
|
|
18
|
+
badge?: string;
|
|
19
|
+
badgeType?: 'info' | 'success' | 'warning' | 'error';
|
|
20
|
+
children?: SidebarItem[];
|
|
21
|
+
collapsed?: boolean;
|
|
22
|
+
}
|
|
23
|
+
interface ScanConfig {
|
|
24
|
+
type: 'scan';
|
|
25
|
+
title: string;
|
|
26
|
+
icon?: string;
|
|
27
|
+
scanPath: string;
|
|
28
|
+
collapsed?: boolean;
|
|
29
|
+
maxDepth?: number;
|
|
30
|
+
exclude?: string[];
|
|
31
|
+
include?: string[];
|
|
32
|
+
sortBy?: 'name' | 'date' | 'title' | 'custom';
|
|
33
|
+
sortOrder?: 'asc' | 'desc';
|
|
34
|
+
showForPaths?: string[];
|
|
35
|
+
hideForPaths?: string[];
|
|
36
|
+
}
|
|
37
|
+
interface ManualConfig {
|
|
38
|
+
type: 'manual';
|
|
39
|
+
title: string;
|
|
40
|
+
icon?: string;
|
|
41
|
+
collapsed?: boolean;
|
|
42
|
+
items: SidebarItem[];
|
|
43
|
+
showForPaths?: string[];
|
|
44
|
+
hideForPaths?: string[];
|
|
45
|
+
}
|
|
46
|
+
interface MixedConfig {
|
|
47
|
+
type: 'mixed';
|
|
48
|
+
title: string;
|
|
49
|
+
icon?: string;
|
|
50
|
+
collapsed?: boolean;
|
|
51
|
+
sections: (ScanConfig | ManualConfig)[];
|
|
52
|
+
showForPaths?: string[];
|
|
53
|
+
hideForPaths?: string[];
|
|
54
|
+
}
|
|
55
|
+
interface DividerConfig {
|
|
56
|
+
type: 'divider';
|
|
57
|
+
title?: string;
|
|
58
|
+
showForPaths?: string[];
|
|
59
|
+
hideForPaths?: string[];
|
|
60
|
+
}
|
|
61
|
+
type SidebarGroup = ScanConfig | ManualConfig | MixedConfig | DividerConfig;
|
|
62
|
+
interface SidebarConfig {
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
width?: string;
|
|
65
|
+
position?: 'left' | 'right';
|
|
66
|
+
showSearch?: boolean;
|
|
67
|
+
showRecentPosts?: boolean;
|
|
68
|
+
recentPostsCount?: number;
|
|
69
|
+
showPopularTags?: boolean;
|
|
70
|
+
popularTagsCount?: number;
|
|
71
|
+
showArchives?: boolean;
|
|
72
|
+
archivesCount?: number;
|
|
73
|
+
showFriendLinks?: boolean;
|
|
74
|
+
friendLinks?: Array<{
|
|
75
|
+
title: string;
|
|
76
|
+
url: string;
|
|
77
|
+
icon?: string;
|
|
78
|
+
description?: string;
|
|
79
|
+
}>;
|
|
80
|
+
groups: SidebarGroup[];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 默认侧边栏配置
|
|
84
|
+
*/
|
|
85
|
+
declare const sidebarConfig: SidebarConfig;
|
|
86
|
+
/**
|
|
87
|
+
* Define sidebar configuration
|
|
88
|
+
*/
|
|
89
|
+
declare function defineSidebarConfig(config: Partial<SidebarConfig>): SidebarConfig;
|
|
90
|
+
declare const defaultSidebarConfig: SidebarConfig;
|
|
91
|
+
|
|
92
|
+
export { type DividerConfig as D, type ManualConfig as M, type PathMatchConfig as P, type SidebarConfig as S, defineSidebarConfig as a, type SidebarGroup as b, type SidebarItem as c, defaultSidebarConfig as d, type ScanConfig as e, type MixedConfig as f, sidebarConfig as s };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { c as SidebarItem, b as SidebarGroup, S as SidebarConfig } from '../sidebar-DNdiCKBw.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 侧边栏工具函数
|
|
5
|
+
* 处理配置解析和树形结构生成
|
|
6
|
+
*/
|
|
7
|
+
interface PostEntry {
|
|
8
|
+
id: string;
|
|
9
|
+
data: {
|
|
10
|
+
title?: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
pubDate?: Date;
|
|
13
|
+
tags?: string[];
|
|
14
|
+
draft?: boolean;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface TreeNode {
|
|
19
|
+
name: string;
|
|
20
|
+
slug?: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
displayName?: string;
|
|
23
|
+
icon?: string;
|
|
24
|
+
badge?: string;
|
|
25
|
+
badgeType?: 'info' | 'success' | 'warning' | 'error';
|
|
26
|
+
children: TreeNode[];
|
|
27
|
+
isFolder: boolean;
|
|
28
|
+
isReadme?: boolean;
|
|
29
|
+
link?: string;
|
|
30
|
+
collapsed?: boolean;
|
|
31
|
+
}
|
|
32
|
+
interface ProcessedGroup {
|
|
33
|
+
type: 'tree' | 'items' | 'divider';
|
|
34
|
+
title: string;
|
|
35
|
+
icon?: string;
|
|
36
|
+
collapsed?: boolean;
|
|
37
|
+
tree?: TreeNode[];
|
|
38
|
+
items?: SidebarItem[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 从文章集合构建树形结构
|
|
42
|
+
*/
|
|
43
|
+
declare function buildTreeFromPosts(posts: PostEntry[], scanPath?: string, options?: {
|
|
44
|
+
maxDepth?: number;
|
|
45
|
+
exclude?: string[];
|
|
46
|
+
include?: string[];
|
|
47
|
+
sortBy?: 'name' | 'date' | 'title' | 'custom';
|
|
48
|
+
sortOrder?: 'asc' | 'desc';
|
|
49
|
+
}): TreeNode[];
|
|
50
|
+
/**
|
|
51
|
+
* 路径 glob 模式匹配
|
|
52
|
+
* 支持:
|
|
53
|
+
* - /posts/tech/** 匹配 /posts/tech 及其所有子路径
|
|
54
|
+
* - /posts/tech/* 匹配 /posts/tech 的直接子路径
|
|
55
|
+
* - /posts/tech 精确匹配
|
|
56
|
+
*/
|
|
57
|
+
declare function matchPathPattern(currentPath: string, pattern: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* 检查侧边栏组是否应该在当前路径显示
|
|
60
|
+
*/
|
|
61
|
+
declare function shouldShowGroup(group: {
|
|
62
|
+
showForPaths?: string[];
|
|
63
|
+
hideForPaths?: string[];
|
|
64
|
+
}, currentPath: string): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* 根据当前路径过滤侧边栏组
|
|
67
|
+
*/
|
|
68
|
+
declare function filterGroupsByPath(groups: SidebarGroup[], currentPath: string): SidebarGroup[];
|
|
69
|
+
/**
|
|
70
|
+
* 将手动配置的项目转换为树节点
|
|
71
|
+
*/
|
|
72
|
+
declare function manualItemsToTree(items: SidebarItem[]): TreeNode[];
|
|
73
|
+
/**
|
|
74
|
+
* 处理侧边栏配置,生成可渲染的数据结构
|
|
75
|
+
*/
|
|
76
|
+
declare function processSidebarConfig(config: SidebarConfig, posts: PostEntry[]): Promise<ProcessedGroup[]>;
|
|
77
|
+
/**
|
|
78
|
+
* 获取最新文章
|
|
79
|
+
*/
|
|
80
|
+
declare function getRecentPosts(posts: PostEntry[], count?: number): PostEntry[];
|
|
81
|
+
/**
|
|
82
|
+
* 获取热门标签
|
|
83
|
+
*/
|
|
84
|
+
declare function getPopularTags(posts: PostEntry[], count?: number): Array<{
|
|
85
|
+
name: string;
|
|
86
|
+
count: number;
|
|
87
|
+
slug: string;
|
|
88
|
+
}>;
|
|
89
|
+
/**
|
|
90
|
+
* 获取归档数据
|
|
91
|
+
*/
|
|
92
|
+
declare function getArchives(posts: PostEntry[], count?: number): Array<{
|
|
93
|
+
year: number;
|
|
94
|
+
month: number;
|
|
95
|
+
count: number;
|
|
96
|
+
}>;
|
|
97
|
+
|
|
98
|
+
export { type ProcessedGroup, type TreeNode, buildTreeFromPosts, filterGroupsByPath, getArchives, getPopularTags, getRecentPosts, manualItemsToTree, matchPathPattern, processSidebarConfig, shouldShowGroup };
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// src/utils/sidebar.ts
|
|
2
|
+
function buildTreeFromPosts(posts, scanPath = "", options = {}) {
|
|
3
|
+
const { maxDepth, exclude = [], include = [], sortBy = "name", sortOrder = "asc" } = options;
|
|
4
|
+
const filteredPosts = posts.filter((post) => {
|
|
5
|
+
const postPath = post.id.toLowerCase();
|
|
6
|
+
const targetPath = scanPath.toLowerCase();
|
|
7
|
+
if (targetPath && !postPath.startsWith(targetPath + "/") && postPath !== targetPath) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const pathParts = post.id.split("/");
|
|
11
|
+
for (const part of pathParts) {
|
|
12
|
+
if (exclude.some((pattern) => matchPattern(part, pattern))) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (include.length > 0) {
|
|
17
|
+
const matchesInclude = pathParts.some(
|
|
18
|
+
(part) => include.some((pattern) => matchPattern(part, pattern))
|
|
19
|
+
);
|
|
20
|
+
if (!matchesInclude) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
26
|
+
const folderTitles = {};
|
|
27
|
+
const folderIcons = {};
|
|
28
|
+
filteredPosts.forEach((post) => {
|
|
29
|
+
const pathParts = post.id.split("/");
|
|
30
|
+
const fileName = pathParts[pathParts.length - 1].toLowerCase();
|
|
31
|
+
if (fileName === "readme" || fileName === "readme.md") {
|
|
32
|
+
const folderPath = pathParts.slice(0, -1).join("/");
|
|
33
|
+
if (folderPath) {
|
|
34
|
+
if (post.data.title) {
|
|
35
|
+
folderTitles[folderPath] = post.data.title;
|
|
36
|
+
}
|
|
37
|
+
if (post.data.icon) {
|
|
38
|
+
folderIcons[folderPath] = post.data.icon;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
const tree = [];
|
|
44
|
+
filteredPosts.forEach((post) => {
|
|
45
|
+
let relativePath = post.id;
|
|
46
|
+
if (scanPath) {
|
|
47
|
+
const scanPathLower = scanPath.toLowerCase();
|
|
48
|
+
const postIdLower = post.id.toLowerCase();
|
|
49
|
+
if (postIdLower.startsWith(scanPathLower + "/")) {
|
|
50
|
+
relativePath = post.id.slice(scanPath.length + 1);
|
|
51
|
+
} else if (postIdLower === scanPathLower) {
|
|
52
|
+
relativePath = post.id.split("/").pop() || post.id;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const pathParts = relativePath.split("/");
|
|
56
|
+
if (maxDepth !== void 0 && pathParts.length > maxDepth) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let currentLevel = tree;
|
|
60
|
+
let currentPath = scanPath;
|
|
61
|
+
pathParts.forEach((part, index) => {
|
|
62
|
+
const isLast = index === pathParts.length - 1;
|
|
63
|
+
const existing = currentLevel.find((n) => n.name.toLowerCase() === part.toLowerCase());
|
|
64
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
65
|
+
const isReadme = isLast && (part.toLowerCase() === "readme" || part.toLowerCase() === "readme.md");
|
|
66
|
+
if (existing) {
|
|
67
|
+
if (isLast) {
|
|
68
|
+
existing.slug = post.id;
|
|
69
|
+
existing.title = post.data.title;
|
|
70
|
+
existing.icon = post.data.icon;
|
|
71
|
+
existing.isReadme = isReadme;
|
|
72
|
+
} else {
|
|
73
|
+
const folderPath = scanPath ? `${scanPath}/${pathParts.slice(0, index + 1).join("/")}` : pathParts.slice(0, index + 1).join("/");
|
|
74
|
+
if (folderTitles[folderPath]) {
|
|
75
|
+
existing.displayName = folderTitles[folderPath];
|
|
76
|
+
}
|
|
77
|
+
if (folderIcons[folderPath]) {
|
|
78
|
+
existing.icon = folderIcons[folderPath];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
currentLevel = existing.children;
|
|
82
|
+
} else {
|
|
83
|
+
const folderPath = scanPath ? `${scanPath}/${pathParts.slice(0, index + 1).join("/")}` : pathParts.slice(0, index + 1).join("/");
|
|
84
|
+
const newNode = {
|
|
85
|
+
name: part,
|
|
86
|
+
slug: isLast ? post.id : void 0,
|
|
87
|
+
title: isLast ? post.data.title : void 0,
|
|
88
|
+
displayName: isLast ? post.data.title : folderTitles[folderPath],
|
|
89
|
+
icon: isLast ? post.data.icon : folderIcons[folderPath],
|
|
90
|
+
children: [],
|
|
91
|
+
isFolder: !isLast,
|
|
92
|
+
isReadme
|
|
93
|
+
};
|
|
94
|
+
currentLevel.push(newNode);
|
|
95
|
+
currentLevel = newNode.children;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
return sortTree(tree, sortBy, sortOrder);
|
|
100
|
+
}
|
|
101
|
+
function sortTree(nodes, sortBy = "name", sortOrder = "asc") {
|
|
102
|
+
const filtered = nodes.filter((node) => !node.isReadme);
|
|
103
|
+
const sorted = filtered.sort((a, b) => {
|
|
104
|
+
if (a.isFolder && !b.isFolder) return -1;
|
|
105
|
+
if (!a.isFolder && b.isFolder) return 1;
|
|
106
|
+
let comparison = 0;
|
|
107
|
+
switch (sortBy) {
|
|
108
|
+
case "title":
|
|
109
|
+
comparison = (a.displayName || a.title || a.name).localeCompare(
|
|
110
|
+
b.displayName || b.title || b.name,
|
|
111
|
+
"zh-CN"
|
|
112
|
+
);
|
|
113
|
+
break;
|
|
114
|
+
case "name":
|
|
115
|
+
default:
|
|
116
|
+
comparison = a.name.localeCompare(b.name, "zh-CN", { numeric: true });
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
return sortOrder === "desc" ? -comparison : comparison;
|
|
120
|
+
});
|
|
121
|
+
return sorted.map((node) => ({
|
|
122
|
+
...node,
|
|
123
|
+
children: sortTree(node.children, sortBy, sortOrder)
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
function matchPattern(str, pattern) {
|
|
127
|
+
if (pattern === "*") return true;
|
|
128
|
+
if (pattern.includes("*")) {
|
|
129
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$", "i");
|
|
130
|
+
return regex.test(str);
|
|
131
|
+
}
|
|
132
|
+
return str.toLowerCase() === pattern.toLowerCase();
|
|
133
|
+
}
|
|
134
|
+
function matchPathPattern(currentPath, pattern) {
|
|
135
|
+
const normalizedPath = currentPath.replace(/\/$/, "").toLowerCase();
|
|
136
|
+
const normalizedPattern = pattern.replace(/\/$/, "").toLowerCase();
|
|
137
|
+
if (normalizedPattern.endsWith("/**")) {
|
|
138
|
+
const basePath = normalizedPattern.slice(0, -3);
|
|
139
|
+
return normalizedPath === basePath || normalizedPath.startsWith(basePath + "/");
|
|
140
|
+
}
|
|
141
|
+
if (normalizedPattern.endsWith("/*")) {
|
|
142
|
+
const basePath = normalizedPattern.slice(0, -2);
|
|
143
|
+
if (normalizedPath === basePath) return true;
|
|
144
|
+
if (normalizedPath.startsWith(basePath + "/")) {
|
|
145
|
+
const remaining = normalizedPath.slice(basePath.length + 1);
|
|
146
|
+
return !remaining.includes("/");
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return normalizedPath === normalizedPattern;
|
|
151
|
+
}
|
|
152
|
+
function shouldShowGroup(group, currentPath) {
|
|
153
|
+
if (!group.showForPaths && !group.hideForPaths) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if (group.hideForPaths && group.hideForPaths.length > 0) {
|
|
157
|
+
for (const pattern of group.hideForPaths) {
|
|
158
|
+
if (matchPathPattern(currentPath, pattern)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (group.showForPaths && group.showForPaths.length > 0) {
|
|
164
|
+
for (const pattern of group.showForPaths) {
|
|
165
|
+
if (matchPathPattern(currentPath, pattern)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
function filterGroupsByPath(groups, currentPath) {
|
|
174
|
+
return groups.filter((group) => shouldShowGroup(group, currentPath));
|
|
175
|
+
}
|
|
176
|
+
function manualItemsToTree(items) {
|
|
177
|
+
return items.map((item) => ({
|
|
178
|
+
name: item.title,
|
|
179
|
+
slug: item.slug,
|
|
180
|
+
title: item.title,
|
|
181
|
+
displayName: item.title,
|
|
182
|
+
icon: item.icon,
|
|
183
|
+
badge: item.badge,
|
|
184
|
+
badgeType: item.badgeType,
|
|
185
|
+
link: item.link,
|
|
186
|
+
children: item.children ? manualItemsToTree(item.children) : [],
|
|
187
|
+
isFolder: !!(item.children && item.children.length > 0),
|
|
188
|
+
collapsed: item.collapsed
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
async function processSidebarConfig(config, posts) {
|
|
192
|
+
const processedGroups = [];
|
|
193
|
+
for (const group of config.groups) {
|
|
194
|
+
const processed = await processGroup(group, posts);
|
|
195
|
+
if (processed) {
|
|
196
|
+
processedGroups.push(processed);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return processedGroups;
|
|
200
|
+
}
|
|
201
|
+
async function processGroup(group, posts) {
|
|
202
|
+
switch (group.type) {
|
|
203
|
+
case "scan": {
|
|
204
|
+
const scanConfig = group;
|
|
205
|
+
const tree = buildTreeFromPosts(posts, scanConfig.scanPath, {
|
|
206
|
+
maxDepth: scanConfig.maxDepth,
|
|
207
|
+
exclude: scanConfig.exclude,
|
|
208
|
+
include: scanConfig.include,
|
|
209
|
+
sortBy: scanConfig.sortBy,
|
|
210
|
+
sortOrder: scanConfig.sortOrder
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
type: "tree",
|
|
214
|
+
title: scanConfig.title,
|
|
215
|
+
icon: scanConfig.icon,
|
|
216
|
+
collapsed: scanConfig.collapsed,
|
|
217
|
+
tree
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
case "manual": {
|
|
221
|
+
const manualConfig = group;
|
|
222
|
+
const tree = manualItemsToTree(manualConfig.items);
|
|
223
|
+
return {
|
|
224
|
+
type: "tree",
|
|
225
|
+
title: manualConfig.title,
|
|
226
|
+
icon: manualConfig.icon,
|
|
227
|
+
collapsed: manualConfig.collapsed,
|
|
228
|
+
tree
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
case "mixed": {
|
|
232
|
+
const mixedConfig = group;
|
|
233
|
+
const combinedTree = [];
|
|
234
|
+
for (const section of mixedConfig.sections) {
|
|
235
|
+
const processed = await processGroup(section, posts);
|
|
236
|
+
if (processed && processed.tree) {
|
|
237
|
+
combinedTree.push({
|
|
238
|
+
name: section.title,
|
|
239
|
+
displayName: section.title,
|
|
240
|
+
icon: section.icon,
|
|
241
|
+
children: processed.tree,
|
|
242
|
+
isFolder: true,
|
|
243
|
+
collapsed: section.collapsed
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
type: "tree",
|
|
249
|
+
title: mixedConfig.title,
|
|
250
|
+
icon: mixedConfig.icon,
|
|
251
|
+
collapsed: mixedConfig.collapsed,
|
|
252
|
+
tree: combinedTree
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
case "divider": {
|
|
256
|
+
return {
|
|
257
|
+
type: "divider",
|
|
258
|
+
title: group.title || ""
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
default:
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function getRecentPosts(posts, count = 5) {
|
|
266
|
+
return posts.filter((p) => p.data.pubDate).sort((a, b) => (b.data.pubDate?.getTime() ?? 0) - (a.data.pubDate?.getTime() ?? 0)).slice(0, count);
|
|
267
|
+
}
|
|
268
|
+
function getPopularTags(posts, count = 8) {
|
|
269
|
+
const tagCounts = {};
|
|
270
|
+
posts.forEach((post) => {
|
|
271
|
+
(post.data.tags || []).forEach((tag) => {
|
|
272
|
+
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
return Object.entries(tagCounts).sort((a, b) => b[1] - a[1]).slice(0, count).map(([name, count2]) => ({
|
|
276
|
+
name,
|
|
277
|
+
count: count2,
|
|
278
|
+
slug: name.toLowerCase().replace(/\s+/g, "-")
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
function getArchives(posts, count = 6) {
|
|
282
|
+
const archiveMap = {};
|
|
283
|
+
posts.forEach((post) => {
|
|
284
|
+
if (post.data.pubDate) {
|
|
285
|
+
const date = new Date(post.data.pubDate);
|
|
286
|
+
const key = `${date.getFullYear()}-${date.getMonth() + 1}`;
|
|
287
|
+
archiveMap[key] = (archiveMap[key] || 0) + 1;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
return Object.entries(archiveMap).sort((a, b) => b[0].localeCompare(a[0])).slice(0, count).map(([key, count2]) => {
|
|
291
|
+
const [year, month] = key.split("-").map(Number);
|
|
292
|
+
return { year, month, count: count2 };
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
export {
|
|
296
|
+
buildTreeFromPosts,
|
|
297
|
+
filterGroupsByPath,
|
|
298
|
+
getArchives,
|
|
299
|
+
getPopularTags,
|
|
300
|
+
getRecentPosts,
|
|
301
|
+
manualItemsToTree,
|
|
302
|
+
matchPathPattern,
|
|
303
|
+
processSidebarConfig,
|
|
304
|
+
shouldShowGroup
|
|
305
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jet-w/astro-blog",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A modern Astro blog theme with Vue and Tailwind CSS support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
"./styles/*": "./src/styles/*",
|
|
21
21
|
"./components/*": "./src/components/*",
|
|
22
22
|
"./layouts/*": "./src/layouts/*",
|
|
23
|
-
"./utils
|
|
23
|
+
"./utils/sidebar": {
|
|
24
|
+
"types": "./dist/utils/sidebar.d.ts",
|
|
25
|
+
"import": "./dist/utils/sidebar.js"
|
|
26
|
+
}
|
|
24
27
|
},
|
|
25
28
|
"files": [
|
|
26
29
|
"dist",
|
|
@@ -28,7 +31,6 @@
|
|
|
28
31
|
"src/styles",
|
|
29
32
|
"src/components",
|
|
30
33
|
"src/layouts",
|
|
31
|
-
"src/utils",
|
|
32
34
|
"templates"
|
|
33
35
|
],
|
|
34
36
|
"scripts": {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { siteConfig } from '
|
|
3
|
-
import ThemeToggle from '
|
|
4
|
-
import SearchBox from '
|
|
5
|
-
import MobileMenu from '
|
|
2
|
+
import { siteConfig } from '@jet-w/astro-blog/config';
|
|
3
|
+
import ThemeToggle from '../ui/ThemeToggle.vue';
|
|
4
|
+
import SearchBox from '../ui/SearchBox.vue';
|
|
5
|
+
import MobileMenu from '../ui/MobileMenu.vue';
|
|
6
6
|
|
|
7
7
|
const currentPath = Astro.url.pathname;
|
|
8
8
|
---
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection } from 'astro:content';
|
|
3
|
-
import Icon from '
|
|
4
|
-
import { sidebarConfig } from '
|
|
3
|
+
import Icon from '../ui/Icon.astro';
|
|
4
|
+
import { sidebarConfig } from '@jet-w/astro-blog/config';
|
|
5
5
|
import {
|
|
6
6
|
processSidebarConfig,
|
|
7
7
|
getRecentPosts,
|
|
8
8
|
getPopularTags,
|
|
9
9
|
getArchives,
|
|
10
10
|
filterGroupsByPath,
|
|
11
|
-
} from '
|
|
11
|
+
} from '@jet-w/astro-blog/utils/sidebar';
|
|
12
12
|
|
|
13
13
|
interface Props {
|
|
14
14
|
currentPath?: string;
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
|
|
100
100
|
<script setup lang="ts">
|
|
101
101
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
102
|
-
import type { NavigationItem } from '
|
|
102
|
+
import type { NavigationItem } from '@jet-w/astro-blog/types'
|
|
103
103
|
import SearchBox from './SearchBox.vue'
|
|
104
104
|
import ThemeToggle from './ThemeToggle.vue'
|
|
105
105
|
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
|
|
197
197
|
<script setup lang="ts">
|
|
198
198
|
import { ref, computed, onMounted } from 'vue'
|
|
199
|
-
import type { SearchResult } from '
|
|
199
|
+
import type { SearchResult } from '@jet-w/astro-blog/types'
|
|
200
200
|
|
|
201
201
|
const searchQuery = ref('')
|
|
202
202
|
const selectedTag = ref('')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
import type { SEOProps } from '
|
|
3
|
-
import { siteConfig, defaultSEO } from '
|
|
4
|
-
import '
|
|
2
|
+
import type { SEOProps } from '@jet-w/astro-blog/types';
|
|
3
|
+
import { siteConfig, defaultSEO } from '@jet-w/astro-blog/config';
|
|
4
|
+
import '@jet-w/astro-blog/styles/global.css';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
|
package/src/utils/sidebar.ts
DELETED
|
@@ -1,492 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 侧边栏工具函数
|
|
3
|
-
* 处理配置解析和树形结构生成
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { CollectionEntry } from 'astro:content';
|
|
7
|
-
import type {
|
|
8
|
-
SidebarConfig,
|
|
9
|
-
SidebarGroup,
|
|
10
|
-
SidebarItem,
|
|
11
|
-
ScanConfig,
|
|
12
|
-
ManualConfig,
|
|
13
|
-
MixedConfig,
|
|
14
|
-
} from '../config/sidebar';
|
|
15
|
-
|
|
16
|
-
// 树节点类型
|
|
17
|
-
export interface TreeNode {
|
|
18
|
-
name: string;
|
|
19
|
-
slug?: string;
|
|
20
|
-
title?: string;
|
|
21
|
-
displayName?: string;
|
|
22
|
-
icon?: string;
|
|
23
|
-
badge?: string;
|
|
24
|
-
badgeType?: 'info' | 'success' | 'warning' | 'error';
|
|
25
|
-
children: TreeNode[];
|
|
26
|
-
isFolder: boolean;
|
|
27
|
-
isReadme?: boolean;
|
|
28
|
-
link?: string;
|
|
29
|
-
collapsed?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// 处理后的侧边栏组
|
|
33
|
-
export interface ProcessedGroup {
|
|
34
|
-
type: 'tree' | 'items' | 'divider';
|
|
35
|
-
title: string;
|
|
36
|
-
icon?: string;
|
|
37
|
-
collapsed?: boolean;
|
|
38
|
-
tree?: TreeNode[];
|
|
39
|
-
items?: SidebarItem[];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 从文章集合构建树形结构
|
|
44
|
-
*/
|
|
45
|
-
export function buildTreeFromPosts(
|
|
46
|
-
posts: CollectionEntry<'posts'>[],
|
|
47
|
-
scanPath: string = '',
|
|
48
|
-
options: {
|
|
49
|
-
maxDepth?: number;
|
|
50
|
-
exclude?: string[];
|
|
51
|
-
include?: string[];
|
|
52
|
-
sortBy?: 'name' | 'date' | 'title' | 'custom';
|
|
53
|
-
sortOrder?: 'asc' | 'desc';
|
|
54
|
-
} = {}
|
|
55
|
-
): TreeNode[] {
|
|
56
|
-
const { maxDepth, exclude = [], include = [], sortBy = 'name', sortOrder = 'asc' } = options;
|
|
57
|
-
|
|
58
|
-
// 过滤出指定路径下的文章
|
|
59
|
-
const filteredPosts = posts.filter(post => {
|
|
60
|
-
const postPath = post.id.toLowerCase();
|
|
61
|
-
const targetPath = scanPath.toLowerCase();
|
|
62
|
-
|
|
63
|
-
// 检查是否在指定路径下
|
|
64
|
-
if (targetPath && !postPath.startsWith(targetPath + '/') && postPath !== targetPath) {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// 检查排除规则
|
|
69
|
-
const pathParts = post.id.split('/');
|
|
70
|
-
for (const part of pathParts) {
|
|
71
|
-
if (exclude.some(pattern => matchPattern(part, pattern))) {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// 检查包含规则
|
|
77
|
-
if (include.length > 0) {
|
|
78
|
-
const matchesInclude = pathParts.some(part =>
|
|
79
|
-
include.some(pattern => matchPattern(part, pattern))
|
|
80
|
-
);
|
|
81
|
-
if (!matchesInclude) {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return true;
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// 收集文件夹的 README 标题和图标
|
|
90
|
-
const folderTitles: Record<string, string> = {};
|
|
91
|
-
const folderIcons: Record<string, string> = {};
|
|
92
|
-
|
|
93
|
-
filteredPosts.forEach(post => {
|
|
94
|
-
const pathParts = post.id.split('/');
|
|
95
|
-
const fileName = pathParts[pathParts.length - 1].toLowerCase();
|
|
96
|
-
|
|
97
|
-
if (fileName === 'readme' || fileName === 'readme.md') {
|
|
98
|
-
const folderPath = pathParts.slice(0, -1).join('/');
|
|
99
|
-
if (folderPath) {
|
|
100
|
-
if (post.data.title) {
|
|
101
|
-
folderTitles[folderPath] = post.data.title;
|
|
102
|
-
}
|
|
103
|
-
if (post.data.icon) {
|
|
104
|
-
folderIcons[folderPath] = post.data.icon;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// 构建树
|
|
111
|
-
const tree: TreeNode[] = [];
|
|
112
|
-
|
|
113
|
-
filteredPosts.forEach(post => {
|
|
114
|
-
// 移除 scanPath 前缀
|
|
115
|
-
let relativePath = post.id;
|
|
116
|
-
if (scanPath) {
|
|
117
|
-
const scanPathLower = scanPath.toLowerCase();
|
|
118
|
-
const postIdLower = post.id.toLowerCase();
|
|
119
|
-
if (postIdLower.startsWith(scanPathLower + '/')) {
|
|
120
|
-
relativePath = post.id.slice(scanPath.length + 1);
|
|
121
|
-
} else if (postIdLower === scanPathLower) {
|
|
122
|
-
relativePath = post.id.split('/').pop() || post.id;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const pathParts = relativePath.split('/');
|
|
127
|
-
|
|
128
|
-
// 检查深度限制
|
|
129
|
-
if (maxDepth !== undefined && pathParts.length > maxDepth) {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let currentLevel = tree;
|
|
134
|
-
let currentPath = scanPath;
|
|
135
|
-
|
|
136
|
-
pathParts.forEach((part, index) => {
|
|
137
|
-
const isLast = index === pathParts.length - 1;
|
|
138
|
-
const existing = currentLevel.find(n => n.name.toLowerCase() === part.toLowerCase());
|
|
139
|
-
|
|
140
|
-
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
141
|
-
const isReadme = isLast && (part.toLowerCase() === 'readme' || part.toLowerCase() === 'readme.md');
|
|
142
|
-
|
|
143
|
-
if (existing) {
|
|
144
|
-
if (isLast) {
|
|
145
|
-
existing.slug = post.id;
|
|
146
|
-
existing.title = post.data.title;
|
|
147
|
-
existing.icon = post.data.icon;
|
|
148
|
-
existing.isReadme = isReadme;
|
|
149
|
-
} else {
|
|
150
|
-
// 更新文件夹信息
|
|
151
|
-
const folderPath = scanPath ? `${scanPath}/${pathParts.slice(0, index + 1).join('/')}` : pathParts.slice(0, index + 1).join('/');
|
|
152
|
-
if (folderTitles[folderPath]) {
|
|
153
|
-
existing.displayName = folderTitles[folderPath];
|
|
154
|
-
}
|
|
155
|
-
if (folderIcons[folderPath]) {
|
|
156
|
-
existing.icon = folderIcons[folderPath];
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
currentLevel = existing.children;
|
|
160
|
-
} else {
|
|
161
|
-
const folderPath = scanPath ? `${scanPath}/${pathParts.slice(0, index + 1).join('/')}` : pathParts.slice(0, index + 1).join('/');
|
|
162
|
-
const newNode: TreeNode = {
|
|
163
|
-
name: part,
|
|
164
|
-
slug: isLast ? post.id : undefined,
|
|
165
|
-
title: isLast ? post.data.title : undefined,
|
|
166
|
-
displayName: isLast ? post.data.title : folderTitles[folderPath],
|
|
167
|
-
icon: isLast ? post.data.icon : folderIcons[folderPath],
|
|
168
|
-
children: [],
|
|
169
|
-
isFolder: !isLast,
|
|
170
|
-
isReadme: isReadme,
|
|
171
|
-
};
|
|
172
|
-
currentLevel.push(newNode);
|
|
173
|
-
currentLevel = newNode.children;
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// 排序并过滤 README
|
|
179
|
-
return sortTree(tree, sortBy, sortOrder);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* 排序树并过滤 README 文件
|
|
184
|
-
*/
|
|
185
|
-
function sortTree(
|
|
186
|
-
nodes: TreeNode[],
|
|
187
|
-
sortBy: 'name' | 'date' | 'title' | 'custom' = 'name',
|
|
188
|
-
sortOrder: 'asc' | 'desc' = 'asc'
|
|
189
|
-
): TreeNode[] {
|
|
190
|
-
const filtered = nodes.filter(node => !node.isReadme);
|
|
191
|
-
|
|
192
|
-
const sorted = filtered.sort((a, b) => {
|
|
193
|
-
// 文件夹优先
|
|
194
|
-
if (a.isFolder && !b.isFolder) return -1;
|
|
195
|
-
if (!a.isFolder && b.isFolder) return 1;
|
|
196
|
-
|
|
197
|
-
let comparison = 0;
|
|
198
|
-
switch (sortBy) {
|
|
199
|
-
case 'title':
|
|
200
|
-
comparison = (a.displayName || a.title || a.name).localeCompare(
|
|
201
|
-
b.displayName || b.title || b.name,
|
|
202
|
-
'zh-CN'
|
|
203
|
-
);
|
|
204
|
-
break;
|
|
205
|
-
case 'name':
|
|
206
|
-
default:
|
|
207
|
-
comparison = a.name.localeCompare(b.name, 'zh-CN', { numeric: true });
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return sortOrder === 'desc' ? -comparison : comparison;
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
return sorted.map(node => ({
|
|
215
|
-
...node,
|
|
216
|
-
children: sortTree(node.children, sortBy, sortOrder),
|
|
217
|
-
}));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* 简单的模式匹配(支持 * 通配符)
|
|
222
|
-
*/
|
|
223
|
-
function matchPattern(str: string, pattern: string): boolean {
|
|
224
|
-
if (pattern === '*') return true;
|
|
225
|
-
if (pattern.includes('*')) {
|
|
226
|
-
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i');
|
|
227
|
-
return regex.test(str);
|
|
228
|
-
}
|
|
229
|
-
return str.toLowerCase() === pattern.toLowerCase();
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* 路径 glob 模式匹配
|
|
234
|
-
* 支持:
|
|
235
|
-
* - /posts/tech/** 匹配 /posts/tech 及其所有子路径
|
|
236
|
-
* - /posts/tech/* 匹配 /posts/tech 的直接子路径
|
|
237
|
-
* - /posts/tech 精确匹配
|
|
238
|
-
*/
|
|
239
|
-
export function matchPathPattern(currentPath: string, pattern: string): boolean {
|
|
240
|
-
// 规范化路径
|
|
241
|
-
const normalizedPath = currentPath.replace(/\/$/, '').toLowerCase();
|
|
242
|
-
const normalizedPattern = pattern.replace(/\/$/, '').toLowerCase();
|
|
243
|
-
|
|
244
|
-
// ** 匹配任意深度
|
|
245
|
-
if (normalizedPattern.endsWith('/**')) {
|
|
246
|
-
const basePath = normalizedPattern.slice(0, -3);
|
|
247
|
-
return normalizedPath === basePath || normalizedPath.startsWith(basePath + '/');
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// * 匹配单层
|
|
251
|
-
if (normalizedPattern.endsWith('/*')) {
|
|
252
|
-
const basePath = normalizedPattern.slice(0, -2);
|
|
253
|
-
if (normalizedPath === basePath) return true;
|
|
254
|
-
// 检查是否是直接子路径
|
|
255
|
-
if (normalizedPath.startsWith(basePath + '/')) {
|
|
256
|
-
const remaining = normalizedPath.slice(basePath.length + 1);
|
|
257
|
-
return !remaining.includes('/');
|
|
258
|
-
}
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// 精确匹配
|
|
263
|
-
return normalizedPath === normalizedPattern;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* 检查侧边栏组是否应该在当前路径显示
|
|
268
|
-
*/
|
|
269
|
-
export function shouldShowGroup(
|
|
270
|
-
group: { showForPaths?: string[]; hideForPaths?: string[] },
|
|
271
|
-
currentPath: string
|
|
272
|
-
): boolean {
|
|
273
|
-
// 如果没有配置路径规则,默认显示
|
|
274
|
-
if (!group.showForPaths && !group.hideForPaths) {
|
|
275
|
-
return true;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// 检查隐藏规则
|
|
279
|
-
if (group.hideForPaths && group.hideForPaths.length > 0) {
|
|
280
|
-
for (const pattern of group.hideForPaths) {
|
|
281
|
-
if (matchPathPattern(currentPath, pattern)) {
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// 检查显示规则
|
|
288
|
-
if (group.showForPaths && group.showForPaths.length > 0) {
|
|
289
|
-
for (const pattern of group.showForPaths) {
|
|
290
|
-
if (matchPathPattern(currentPath, pattern)) {
|
|
291
|
-
return true;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
// 配置了 showForPaths 但没有匹配,则不显示
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// 默认显示
|
|
299
|
-
return true;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* 根据当前路径过滤侧边栏组
|
|
304
|
-
*/
|
|
305
|
-
export function filterGroupsByPath(
|
|
306
|
-
groups: SidebarGroup[],
|
|
307
|
-
currentPath: string
|
|
308
|
-
): SidebarGroup[] {
|
|
309
|
-
return groups.filter(group => shouldShowGroup(group, currentPath));
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* 将手动配置的项目转换为树节点
|
|
314
|
-
*/
|
|
315
|
-
export function manualItemsToTree(items: SidebarItem[]): TreeNode[] {
|
|
316
|
-
return items.map(item => ({
|
|
317
|
-
name: item.title,
|
|
318
|
-
slug: item.slug,
|
|
319
|
-
title: item.title,
|
|
320
|
-
displayName: item.title,
|
|
321
|
-
icon: item.icon,
|
|
322
|
-
badge: item.badge,
|
|
323
|
-
badgeType: item.badgeType,
|
|
324
|
-
link: item.link,
|
|
325
|
-
children: item.children ? manualItemsToTree(item.children) : [],
|
|
326
|
-
isFolder: !!(item.children && item.children.length > 0),
|
|
327
|
-
collapsed: item.collapsed,
|
|
328
|
-
}));
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* 处理侧边栏配置,生成可渲染的数据结构
|
|
333
|
-
*/
|
|
334
|
-
export async function processSidebarConfig(
|
|
335
|
-
config: SidebarConfig,
|
|
336
|
-
posts: CollectionEntry<'posts'>[]
|
|
337
|
-
): Promise<ProcessedGroup[]> {
|
|
338
|
-
const processedGroups: ProcessedGroup[] = [];
|
|
339
|
-
|
|
340
|
-
for (const group of config.groups) {
|
|
341
|
-
const processed = await processGroup(group, posts);
|
|
342
|
-
if (processed) {
|
|
343
|
-
processedGroups.push(processed);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return processedGroups;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* 处理单个侧边栏组
|
|
352
|
-
*/
|
|
353
|
-
async function processGroup(
|
|
354
|
-
group: SidebarGroup,
|
|
355
|
-
posts: CollectionEntry<'posts'>[]
|
|
356
|
-
): Promise<ProcessedGroup | null> {
|
|
357
|
-
switch (group.type) {
|
|
358
|
-
case 'scan': {
|
|
359
|
-
const scanConfig = group as ScanConfig;
|
|
360
|
-
const tree = buildTreeFromPosts(posts, scanConfig.scanPath, {
|
|
361
|
-
maxDepth: scanConfig.maxDepth,
|
|
362
|
-
exclude: scanConfig.exclude,
|
|
363
|
-
include: scanConfig.include,
|
|
364
|
-
sortBy: scanConfig.sortBy,
|
|
365
|
-
sortOrder: scanConfig.sortOrder,
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
return {
|
|
369
|
-
type: 'tree',
|
|
370
|
-
title: scanConfig.title,
|
|
371
|
-
icon: scanConfig.icon,
|
|
372
|
-
collapsed: scanConfig.collapsed,
|
|
373
|
-
tree,
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
case 'manual': {
|
|
378
|
-
const manualConfig = group as ManualConfig;
|
|
379
|
-
const tree = manualItemsToTree(manualConfig.items);
|
|
380
|
-
|
|
381
|
-
return {
|
|
382
|
-
type: 'tree',
|
|
383
|
-
title: manualConfig.title,
|
|
384
|
-
icon: manualConfig.icon,
|
|
385
|
-
collapsed: manualConfig.collapsed,
|
|
386
|
-
tree,
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
case 'mixed': {
|
|
391
|
-
const mixedConfig = group as MixedConfig;
|
|
392
|
-
const combinedTree: TreeNode[] = [];
|
|
393
|
-
|
|
394
|
-
for (const section of mixedConfig.sections) {
|
|
395
|
-
const processed = await processGroup(section, posts);
|
|
396
|
-
if (processed && processed.tree) {
|
|
397
|
-
// 将每个子部分作为一个文件夹节点
|
|
398
|
-
combinedTree.push({
|
|
399
|
-
name: section.title,
|
|
400
|
-
displayName: section.title,
|
|
401
|
-
icon: section.icon,
|
|
402
|
-
children: processed.tree,
|
|
403
|
-
isFolder: true,
|
|
404
|
-
collapsed: section.collapsed,
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
return {
|
|
410
|
-
type: 'tree',
|
|
411
|
-
title: mixedConfig.title,
|
|
412
|
-
icon: mixedConfig.icon,
|
|
413
|
-
collapsed: mixedConfig.collapsed,
|
|
414
|
-
tree: combinedTree,
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
case 'divider': {
|
|
419
|
-
return {
|
|
420
|
-
type: 'divider',
|
|
421
|
-
title: group.title || '',
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
default:
|
|
426
|
-
return null;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* 获取最新文章
|
|
432
|
-
*/
|
|
433
|
-
export function getRecentPosts(
|
|
434
|
-
posts: CollectionEntry<'posts'>[],
|
|
435
|
-
count: number = 5
|
|
436
|
-
): CollectionEntry<'posts'>[] {
|
|
437
|
-
return posts
|
|
438
|
-
.filter(p => p.data.pubDate)
|
|
439
|
-
.sort((a, b) => (b.data.pubDate?.getTime() ?? 0) - (a.data.pubDate?.getTime() ?? 0))
|
|
440
|
-
.slice(0, count);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* 获取热门标签
|
|
445
|
-
*/
|
|
446
|
-
export function getPopularTags(
|
|
447
|
-
posts: CollectionEntry<'posts'>[],
|
|
448
|
-
count: number = 8
|
|
449
|
-
): Array<{ name: string; count: number; slug: string }> {
|
|
450
|
-
const tagCounts: Record<string, number> = {};
|
|
451
|
-
|
|
452
|
-
posts.forEach(post => {
|
|
453
|
-
(post.data.tags || []).forEach(tag => {
|
|
454
|
-
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
455
|
-
});
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
return Object.entries(tagCounts)
|
|
459
|
-
.sort((a, b) => b[1] - a[1])
|
|
460
|
-
.slice(0, count)
|
|
461
|
-
.map(([name, count]) => ({
|
|
462
|
-
name,
|
|
463
|
-
count,
|
|
464
|
-
slug: name.toLowerCase().replace(/\s+/g, '-'),
|
|
465
|
-
}));
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* 获取归档数据
|
|
470
|
-
*/
|
|
471
|
-
export function getArchives(
|
|
472
|
-
posts: CollectionEntry<'posts'>[],
|
|
473
|
-
count: number = 6
|
|
474
|
-
): Array<{ year: number; month: number; count: number }> {
|
|
475
|
-
const archiveMap: Record<string, number> = {};
|
|
476
|
-
|
|
477
|
-
posts.forEach(post => {
|
|
478
|
-
if (post.data.pubDate) {
|
|
479
|
-
const date = new Date(post.data.pubDate);
|
|
480
|
-
const key = `${date.getFullYear()}-${date.getMonth() + 1}`;
|
|
481
|
-
archiveMap[key] = (archiveMap[key] || 0) + 1;
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
return Object.entries(archiveMap)
|
|
486
|
-
.sort((a, b) => b[0].localeCompare(a[0]))
|
|
487
|
-
.slice(0, count)
|
|
488
|
-
.map(([key, count]) => {
|
|
489
|
-
const [year, month] = key.split('-').map(Number);
|
|
490
|
-
return { year, month, count };
|
|
491
|
-
});
|
|
492
|
-
}
|