@jet-w/astro-blog 0.1.1 → 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.
@@ -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 DividerConfig, type FooterConfig, type FooterLink, type ManualConfig, type MixedConfig, type PathMatchConfig, type ScanConfig, type SidebarConfig, type SidebarGroup, type SidebarItem, type SocialLink, defaultFooterConfig, defaultIcons, defaultMenu, defaultSEO, defaultSidebarConfig, defaultSiteConfig, defaultSocialLinks, defineFooterConfig, defineMenu, defineSidebarConfig, defineSiteConfig, defineSocialLinks, footerConfig, menu, sidebarConfig, siteConfig, socialLinks };
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 { SidebarConfig, FooterConfig, SocialLink } from './config/index.js';
2
- export { DividerConfig, FooterLink, ManualConfig, MixedConfig, PathMatchConfig, ScanConfig, SidebarGroup, SidebarItem, defaultFooterConfig, defaultIcons, defaultMenu, defaultSEO, defaultSidebarConfig, defaultSiteConfig, defaultSocialLinks, defineFooterConfig, defineMenu, defineSidebarConfig, defineSiteConfig, defineSocialLinks, footerConfig, menu, sidebarConfig, siteConfig, socialLinks } from './config/index.js';
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.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/*": "./src/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": {
@@ -16,7 +16,7 @@ import {
16
16
  socialLinks as configSocialLinks,
17
17
  defaultIcons,
18
18
  type SocialLink
19
- } from '../../config';
19
+ } from '@jet-w/astro-blog/config';
20
20
 
21
21
  export interface Props {
22
22
  links?: SocialLink[];
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { siteConfig } from '../../config/site';
2
+ import { siteConfig } from '@jet-w/astro-blog/config';
3
3
  ---
4
4
 
5
5
  <section class="py-16 mb-16">
@@ -1,6 +1,5 @@
1
1
  ---
2
- import { siteConfig } from '../../config/site';
3
- import { footerConfig, defaultIcons } from '../../config';
2
+ import { siteConfig, footerConfig, defaultIcons } from '@jet-w/astro-blog/config';
4
3
 
5
4
  const currentYear = new Date().getFullYear();
6
5
 
@@ -1,8 +1,8 @@
1
1
  ---
2
- import { siteConfig } from '../../config/site';
3
- import ThemeToggle from '../../components/ui/ThemeToggle.vue';
4
- import SearchBox from '../../components/ui/SearchBox.vue';
5
- import MobileMenu from '../../components/ui/MobileMenu.vue';
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 '../../components/ui/Icon.astro';
4
- import { sidebarConfig } from '../../config/sidebar';
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 '../../utils/sidebar';
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 '../../types'
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 '../../types'
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 '../types';
3
- import { siteConfig, defaultSEO } from '../config/site';
4
- import '../styles/global.css';
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
 
@@ -1,6 +1,6 @@
1
1
  ---
2
- import { siteConfig } from '../config/site';
3
- import '../styles/slides.css';
2
+ import { siteConfig } from '@jet-w/astro-blog/config';
3
+ import '@jet-w/astro-blog/styles/slides.css';
4
4
 
5
5
  export interface Props {
6
6
  title: string;
@@ -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
- }