@coffic/cosy-ui 0.9.4 → 0.9.6

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.
Files changed (33) hide show
  1. package/dist/index-astro.ts +2 -19
  2. package/dist/index-collection.ts +1 -105
  3. package/dist/src-astro/collection/entities/BaseDoc.ts +28 -0
  4. package/dist/src-astro/collection/entities/BlogDoc.ts +221 -0
  5. package/dist/src-astro/collection/entities/CourseDoc.ts +254 -0
  6. package/dist/src-astro/collection/entities/ExperimentDoc.ts +169 -0
  7. package/dist/src-astro/collection/entities/LessonDoc.ts +203 -0
  8. package/dist/src-astro/collection/entities/MetaDoc.ts +115 -0
  9. package/dist/src-astro/{entities → collection/entities}/SidebarItem.ts +17 -17
  10. package/dist/src-astro/collection/entities/Tag.ts +42 -0
  11. package/dist/src-astro/collection/index.ts +11 -0
  12. package/dist/src-astro/collection/repos/BaseRepo.ts +276 -0
  13. package/dist/src-astro/collection/repos/BlogRepo.ts +226 -0
  14. package/dist/src-astro/collection/repos/CourseRepo.ts +137 -0
  15. package/dist/src-astro/collection/repos/ExperimentRepo.ts +121 -0
  16. package/dist/src-astro/collection/repos/LessonRepo.ts +128 -0
  17. package/dist/src-astro/collection/repos/MetaRepo.ts +89 -0
  18. package/package.json +1 -1
  19. package/dist/src-astro/database/BaseDB.ts +0 -264
  20. package/dist/src-astro/database/BlogDB.ts +0 -198
  21. package/dist/src-astro/database/CourseDB.ts +0 -90
  22. package/dist/src-astro/database/ExperimentDB.ts +0 -106
  23. package/dist/src-astro/database/LessonDB.ts +0 -106
  24. package/dist/src-astro/database/MetaDB.ts +0 -74
  25. package/dist/src-astro/database/index.ts +0 -3
  26. package/dist/src-astro/entities/BaseDoc.ts +0 -207
  27. package/dist/src-astro/entities/BlogDoc.ts +0 -107
  28. package/dist/src-astro/entities/CourseDoc.ts +0 -102
  29. package/dist/src-astro/entities/ExperimentDoc.ts +0 -119
  30. package/dist/src-astro/entities/LessonDoc.ts +0 -153
  31. package/dist/src-astro/entities/MetaDoc.ts +0 -93
  32. package/dist/src-astro/entities/Tag.ts +0 -42
  33. /package/dist/src-astro/{entities → collection/entities}/Feature.ts +0 -0
@@ -63,22 +63,8 @@ export * from './src-astro/types/sidebar';
63
63
  export * from './src-astro/types/static-path';
64
64
 
65
65
  // 实体类 (按字母顺序)
66
- export * from './src-astro/entities/BaseDoc';
67
- export * from './src-astro/entities/BlogDoc';
68
- export * from './src-astro/entities/CourseDoc';
69
- export * from './src-astro/entities/ExperimentDoc';
70
- export * from './src-astro/entities/LessonDoc';
71
- export * from './src-astro/entities/MetaDoc';
72
- export * from './src-astro/entities/SidebarItem';
73
- export * from './src-astro/entities/Tag';
74
-
75
- // 数据库类 (按字母顺序)
76
- export * from './src-astro/database/BaseDB';
77
- export * from './src-astro/database/BlogDB';
78
- export * from './src-astro/database/CourseDB';
79
- export * from './src-astro/database/ExperimentDB';
80
- export * from './src-astro/database/LessonDB';
81
- export * from './src-astro/database/MetaDB';
66
+ export * from './src-astro/collection/entities/SidebarItem';
67
+ export * from './src-astro/collection/entities/Tag';
82
68
 
83
69
  // 工具类型
84
70
  export type { ImageProvider, ImageOptions } from './src/utils/image';
@@ -93,6 +79,3 @@ export * from './src/utils/link';
93
79
 
94
80
  // 图标组件
95
81
  export * from './src-astro/icons';
96
-
97
- // 数据库
98
- export * from './src-astro/database/index';
@@ -1,105 +1 @@
1
- import { defineCollection, z } from 'astro:content';
2
- import { glob } from 'astro/loaders';
3
-
4
- export const makeArticleCollection = (base: string) => {
5
- return defineCollection({
6
- loader: glob({
7
- pattern: '**/*.{md,mdx}',
8
- base,
9
- }),
10
- schema: z.object({
11
- title: z.string().optional(),
12
- folder: z.boolean().optional(),
13
- order: z.number().optional(),
14
- description: z.string().optional(),
15
- }),
16
- });
17
- };
18
-
19
- export const makeBlogCollection = (base: string) => {
20
- return defineCollection({
21
- loader: glob({
22
- pattern: '**/*.{md,mdx}',
23
- base,
24
- }),
25
- schema: z.object({
26
- title: z.string(),
27
- description: z.string().optional(),
28
- tags: z.array(z.string()).optional(),
29
- date: z.date().optional(),
30
- authors: z
31
- .array(
32
- z.object({
33
- name: z.string(),
34
- picture: z.string().optional(),
35
- url: z.string().optional(),
36
- })
37
- )
38
- .optional(),
39
- }),
40
- });
41
- };
42
-
43
- export const makeCourseCollection = (base: string) => {
44
- return defineCollection({
45
- loader: glob({
46
- pattern: '**/*.{md,mdx}',
47
- base,
48
- }),
49
- schema: z.object({
50
- title: z.string().optional(),
51
- description: z.string().optional(),
52
- folder: z.boolean().optional(),
53
- order: z.number().optional(),
54
- badge: z.string().optional(),
55
- }),
56
- });
57
- };
58
-
59
- export const makeExperimentCollection = (base: string) => {
60
- return defineCollection({
61
- loader: glob({
62
- pattern: '**/*.{md,mdx}',
63
- base,
64
- }),
65
- schema: z.object({
66
- title: z.string().optional(),
67
- description: z.string().optional(),
68
- pubDate: z.date().optional(),
69
- }),
70
- });
71
- };
72
-
73
- export const makeLessonCollection = (base: string) => {
74
- return defineCollection({
75
- loader: glob({
76
- pattern: '**/*.{md,mdx}',
77
- base,
78
- }),
79
- schema: z.object({
80
- title: z.string().optional(),
81
- description: z.string().optional(),
82
- authors: z
83
- .array(
84
- z.object({
85
- name: z.string(),
86
- picture: z.string().optional(),
87
- })
88
- )
89
- .optional(),
90
- }),
91
- });
92
- };
93
-
94
- export const makeMetaCollection = (base: string) => {
95
- return defineCollection({
96
- loader: glob({
97
- pattern: '**/*.{md,mdx}',
98
- base,
99
- }),
100
- schema: z.object({
101
- title: z.string().optional(),
102
- description: z.string().optional(),
103
- }),
104
- });
105
- };
1
+ export * from './src-astro/collection';
@@ -0,0 +1,28 @@
1
+ import type { CollectionEntry } from 'astro:content';
2
+
3
+ /**
4
+ * 文档基类,提供所有文档类型共享的基本功能
5
+ */
6
+ export abstract class BaseDoc {
7
+ abstract entry: CollectionEntry<string>;
8
+
9
+ getId(): string {
10
+ return this.entry.id as string;
11
+ }
12
+
13
+ getLevel(): number {
14
+ return this.getId().split('/').length;
15
+ }
16
+
17
+ getLang(): string {
18
+ return this.entry.id.split('/')[0];
19
+ }
20
+
21
+ getTitle(): string {
22
+ return this.entry.data.title as string;
23
+ }
24
+
25
+ getDescription(): string {
26
+ return this.entry.data.description as string;
27
+ }
28
+ }
@@ -0,0 +1,221 @@
1
+ import { LinkUtil } from '../../../src/utils/link';
2
+ import Tag from './Tag';
3
+ import { blogRepo, type BlogEntry } from '..';
4
+ import { SidebarItemEntity, type SidebarProvider } from './SidebarItem';
5
+ import { cosyLogger, ERROR_PREFIX } from '../../cosy';
6
+ import { render } from 'astro:content';
7
+ import { BaseDoc } from './BaseDoc';
8
+
9
+ /**
10
+ * 博客文档类,配合 BlogRepo 使用
11
+ *
12
+ * 目录结构:
13
+ * ```
14
+ * blogs/
15
+ * ├── zh-cn/ # 中文博客目录
16
+ * │ ├── typescript-intro.md # 文章内容
17
+ * │ ├── images/ # 文章相关图片
18
+ * │ └── web-performance.md
19
+ * └── en/ # 英文博客目录
20
+ * ├── typescript-intro.md
21
+ * ├── images/
22
+ * └── web-performance.md
23
+ * ```
24
+ *
25
+ * 说明:
26
+ * - 每个语言(如 zh-cn, en)是一个独立的目录,存放该语言下的所有博客文章
27
+ * - 每篇博客为一个 markdown 文件,文件名即为 slug
28
+ * - 每个语言目录下可包含 images 文件夹,存放该语言博客相关图片
29
+ * - 支持多语言博客内容,便于国际化
30
+ * - 博客目录可作为 git 子模块独立管理
31
+ *
32
+ * 相关:
33
+ * - BlogRepo:博客仓库管理类,负责博客文档的增删查改
34
+ * - BlogDoc:博客文档实体类,封装单篇博客的相关操作
35
+ *
36
+ * 用法示例:
37
+ * ```typescript
38
+ * import BlogDoc from '../entities/BlogDoc';
39
+ * import { blogRepo } from '../BlogRepo';
40
+ *
41
+ * // 获取所有中文博客
42
+ * const blogs = await blogRepo.allBlogsByLang('zh-cn');
43
+ *
44
+ * // 获取博客链接
45
+ * const link = blogs[0].getLink();
46
+ * ```
47
+ */
48
+
49
+ export default class BlogDoc extends BaseDoc implements SidebarProvider {
50
+ entry: BlogEntry;
51
+
52
+ private constructor(entry: BlogEntry) {
53
+ super();
54
+ this.entry = entry;
55
+ }
56
+
57
+ static fromEntry(entry: BlogEntry) {
58
+ return new BlogDoc(entry);
59
+ }
60
+
61
+ getAncestorIds(): string[] {
62
+ const parts = this.entry.id.split('/');
63
+ return parts
64
+ .slice(0, -1)
65
+ .map((_part: string, index: number) => parts.slice(0, index + 1).join('/'));
66
+ }
67
+
68
+ getAncestorId(level: number): string {
69
+ const ancestorIds = this.getAncestorIds();
70
+
71
+ if (level < 1) {
72
+ cosyLogger.error('Level must be greater than 0');
73
+ throw new Error(ERROR_PREFIX + 'Level must be greater than 0');
74
+ }
75
+
76
+ if (level >= this.getLevel()) {
77
+ cosyLogger.debug('当前文档的ID:' + this.entry.id);
78
+ cosyLogger.debug('当前文档的Level:' + this.getLevel());
79
+ cosyLogger.debug('正在获取祖先ID,level:' + level);
80
+
81
+ throw new Error(
82
+ ERROR_PREFIX + 'Level must be less than the document level'
83
+ );
84
+ }
85
+
86
+ if (ancestorIds.length < level) {
87
+ cosyLogger.debug('当前文档的ID\n' + this.entry.id);
88
+ cosyLogger.array('当前文档的祖先IDs', ancestorIds);
89
+ cosyLogger.debug('正在获取祖先ID,level:\n' + level);
90
+
91
+ throw new Error(ERROR_PREFIX + 'Level must be greater than 0');
92
+ }
93
+
94
+ return ancestorIds[level - 1];
95
+ }
96
+
97
+ getParentId(): string | null {
98
+ return this.getAncestorId(this.getLevel() - 1);
99
+ }
100
+
101
+ getTopDocId(): string {
102
+ const level = this.getLevel();
103
+ if (level <= 2) {
104
+ return this.entry.id;
105
+ }
106
+ return this.getAncestorId(2);
107
+ }
108
+
109
+ getSlug(): string {
110
+ return this.getId().split('/').slice(1).join('/');
111
+ }
112
+
113
+ getDescription(): string {
114
+ return this.entry.data.description as string;
115
+ }
116
+
117
+ async getTopDoc(): Promise<BlogDoc | null> {
118
+ const id = this.getTopDocId();
119
+ const doc = await blogRepo.find(id);
120
+ return doc;
121
+ }
122
+
123
+ async getChildren(): Promise<BlogDoc[]> {
124
+ return await blogRepo.getChildren(this.entry.id);
125
+ }
126
+
127
+ getLink(): string {
128
+ return LinkUtil.getBlogLink(this.entry.id, this.getLang());
129
+ }
130
+
131
+ getTags(): Tag[] {
132
+ const tags = this.entry.data.tags as string[];
133
+ if (!tags || tags.length === 0) {
134
+ return [];
135
+ }
136
+ return tags.map((tag) => new Tag(tag, 0, this.getLang()));
137
+ }
138
+
139
+ getDate(): Date {
140
+ return new Date(this.entry.data.date as Date);
141
+ }
142
+
143
+ getDateForDisplay() {
144
+ try {
145
+ const dateObj = new Date(this.entry.data.date as Date);
146
+ if (isNaN(dateObj.getTime())) {
147
+ console.warn(`Invalid date format: ${this.entry.data.date}`);
148
+ return 'Date unavailable: ' + this.getTitle() + ' ' + this.getLink();
149
+ }
150
+ return dateObj.toLocaleDateString('zh-CN', {
151
+ year: 'numeric',
152
+ month: 'long',
153
+ day: 'numeric',
154
+ });
155
+ } catch (error) {
156
+ console.error(`Error formatting date: ${this.entry.data.date}`, error);
157
+ return 'Date unavailable';
158
+ }
159
+ }
160
+
161
+ getBadge(): string {
162
+ return this.entry.data.badge ?? '';
163
+ }
164
+
165
+ hasTag(tag: string): boolean {
166
+ return this.getTags().some((t) => t.name === tag);
167
+ }
168
+
169
+ hasBadge(): boolean {
170
+ return this.getBadge() !== '';
171
+ }
172
+
173
+ isDraft(): boolean {
174
+ return this.entry.data.draft ?? false;
175
+ }
176
+
177
+ isHidden(): boolean {
178
+ return this.entry.data.hidden ?? false;
179
+ }
180
+
181
+ isNotHidden(): boolean {
182
+ return !this.isHidden();
183
+ }
184
+
185
+ isFamous(): boolean {
186
+ return this.entry.data.famous ?? false;
187
+ }
188
+
189
+ async getTopSidebarItem(): Promise<SidebarItemEntity> {
190
+ const topDoc = await this.getTopDoc();
191
+ if (topDoc) {
192
+ return await topDoc.toSidebarItem();
193
+ }
194
+ return new SidebarItemEntity({
195
+ text: this.getTitle(),
196
+ items: [],
197
+ link: this.getLink(),
198
+ });
199
+ }
200
+
201
+ async render(): Promise<any> {
202
+ return await render(this.entry);
203
+ }
204
+
205
+ async toSidebarItem(): Promise<SidebarItemEntity> {
206
+ const debug = false;
207
+ const children = await this.getChildren();
208
+ const childItems = await Promise.all(
209
+ children.map((child) => child.toSidebarItem())
210
+ );
211
+ if (debug) {
212
+ cosyLogger.info(`${this.entry.id} 的侧边栏项目`);
213
+ console.log(childItems);
214
+ }
215
+ return new SidebarItemEntity({
216
+ text: this.getTitle(),
217
+ items: childItems,
218
+ link: this.getLink(),
219
+ });
220
+ }
221
+ }
@@ -0,0 +1,254 @@
1
+ import { cosyLogger } from '../../cosy';
2
+ import { SidebarItemEntity, type SidebarProvider } from './SidebarItem';
3
+ import { LinkUtil } from '../../../src/utils/link';
4
+ import { render, type CollectionEntry } from 'astro:content';
5
+ import { BaseDoc } from './BaseDoc';
6
+ import { courseRepo } from '../repos/CourseRepo';
7
+
8
+ export const COLLECTION_COURSE = 'courses' as const;
9
+ export type CourseEntry = CollectionEntry<typeof COLLECTION_COURSE>;
10
+
11
+ /**
12
+ * 课程文档类,配合 CourseRepo 使用
13
+ *
14
+ * 目录结构:
15
+ * ```
16
+ * courses/
17
+ * ├── zh-cn/ # 中文版本
18
+ * │ ├── web-development/ # 课程1
19
+ * │ │ ├── index.md # 课程文档
20
+ * │ │ ├── chapter1
21
+ * │ │ │ ├── index.md
22
+ * │ │ │ ├── content.md
23
+ * │ │ │ └── ...
24
+ * │ │ └── chapter2
25
+ * │ │ ├── index.md
26
+ * │ │ ├── content.md
27
+ * │ │ └── ...
28
+ * │ └── mobile-dev/ # 课程2
29
+ * │ ├── index.md
30
+ * │ └── ...
31
+ * └── en/ # 英文版本
32
+ * └── ...
33
+ * ```
34
+ */
35
+ export default class CourseDoc extends BaseDoc implements SidebarProvider {
36
+ entry: CourseEntry;
37
+
38
+ constructor(entry: CourseEntry) {
39
+ super();
40
+ this.entry = entry;
41
+ }
42
+
43
+ static fromEntry(entry: CourseEntry) {
44
+ return new CourseDoc(entry);
45
+ }
46
+
47
+ getAncestorIds(): string[] {
48
+ const parts = this.entry.id.split('/');
49
+ return parts
50
+ .slice(0, -1)
51
+ .map((_part: string, index: number) => parts.slice(0, index + 1).join('/'));
52
+ }
53
+
54
+ getAncestorId(level: number): string {
55
+ const ancestorIds = this.getAncestorIds();
56
+
57
+ if (level < 1) {
58
+ cosyLogger.error('Level must be greater than 0');
59
+ throw new Error('Level must be greater than 0');
60
+ }
61
+
62
+ if (level >= this.getLevel()) {
63
+ cosyLogger.debug('当前文档的ID:' + this.entry.id);
64
+ cosyLogger.debug('当前文档的Level:' + this.getLevel());
65
+ cosyLogger.debug('正在获取祖先ID,level:' + level);
66
+
67
+ throw new Error('Level must be less than the document level');
68
+ }
69
+
70
+ if (ancestorIds.length < level) {
71
+ cosyLogger.debug('当前文档的ID\n' + this.entry.id);
72
+ cosyLogger.array('当前文档的祖先IDs', ancestorIds);
73
+ cosyLogger.debug('正在获取祖先ID,level:\n' + level);
74
+
75
+ throw new Error('Level must be greater than 0');
76
+ }
77
+
78
+ return ancestorIds[level - 1];
79
+ }
80
+
81
+ getParentId(): string | null {
82
+ return this.getAncestorId(this.getLevel() - 1);
83
+ }
84
+
85
+ getTopDocId(): string {
86
+ const level = this.getLevel();
87
+ if (level <= 2) {
88
+ return this.entry.id;
89
+ }
90
+ return this.getAncestorId(2);
91
+ }
92
+
93
+ getSlug(): string {
94
+ return this.getId().split('/').slice(1).join('/');
95
+ }
96
+
97
+ getDescription(): string {
98
+ return this.entry.data.description as string;
99
+ }
100
+
101
+ getOrder(): number {
102
+ return this.entry.data.order ?? 0;
103
+ }
104
+
105
+ async getTopDoc(): Promise<CourseDoc | null> {
106
+ const id = this.getTopDocId();
107
+ const doc = await courseRepo.find(id);
108
+ return doc;
109
+ }
110
+
111
+ async getAncestor(level: number): Promise<CourseDoc | null> {
112
+ const debug = false;
113
+ if (debug) {
114
+ cosyLogger.info(`获取 ${this.entry.id} 的祖先文档,level: ${level}`);
115
+ }
116
+
117
+ if (level >= this.getLevel()) {
118
+ if (debug) {
119
+ cosyLogger.info(`祖先文档为自身`);
120
+ }
121
+ return this;
122
+ }
123
+
124
+ const id = this.getAncestorId(level);
125
+ const doc = await courseRepo.find(id);
126
+ return doc;
127
+ }
128
+
129
+ async getChildren(): Promise<CourseDoc[]> {
130
+ const debug = false;
131
+ const children = (await courseRepo.getChildren(this.entry.id)).sort(
132
+ (a, b) => a.getOrder() - b.getOrder()
133
+ );
134
+ if (debug && children.length > 0) {
135
+ cosyLogger.array(
136
+ `${this.entry.id} 的子文档(${children.length})`,
137
+ children.map((child) => `#${child.getOrder()} ${child.entry.id}`)
138
+ );
139
+ }
140
+ return children;
141
+ }
142
+
143
+ getBadge(): string {
144
+ return this.entry.data.badge ?? '';
145
+ }
146
+
147
+ getCategory(): string {
148
+ return this.entry.data.category ?? '';
149
+ }
150
+
151
+ getTags(): string[] {
152
+ return this.entry.data.tags ?? [];
153
+ }
154
+
155
+ getLink(): string {
156
+ return LinkUtil.getCourseLink(this.entry.id);
157
+ }
158
+
159
+ getDate(): Date {
160
+ return new Date(this.entry.data.date as Date);
161
+ }
162
+
163
+ getDateForDisplay() {
164
+ try {
165
+ const dateObj = new Date(this.entry.data.date as Date);
166
+ if (isNaN(dateObj.getTime())) {
167
+ console.warn(`Invalid date format: ${this.entry.data.date}`);
168
+ return 'Date unavailable: ' + this.getTitle() + ' ' + this.getLink();
169
+ }
170
+ return dateObj.toLocaleDateString('zh-CN', {
171
+ year: 'numeric',
172
+ month: 'long',
173
+ day: 'numeric',
174
+ });
175
+ } catch (error) {
176
+ console.error(`Error formatting date: ${this.entry.data.date}`, error);
177
+ return 'Date unavailable';
178
+ }
179
+ }
180
+
181
+ async render(): Promise<any> {
182
+ return await render(this.entry);
183
+ }
184
+
185
+ async getTopSidebarItem(): Promise<SidebarItemEntity> {
186
+ const topDoc = await this.getTopDoc();
187
+ if (topDoc) {
188
+ return await topDoc.toSidebarItem();
189
+ }
190
+ return new SidebarItemEntity({
191
+ text: this.getTitle(),
192
+ items: [],
193
+ link: this.getLink(),
194
+ });
195
+ }
196
+
197
+ hasTag(tag: string): boolean {
198
+ return this.getTags().includes(tag);
199
+ }
200
+
201
+ hasBadge(): boolean {
202
+ return this.getBadge() !== '';
203
+ }
204
+
205
+ isBook(): boolean {
206
+ return this.entry.id.split('/').length === 2;
207
+ }
208
+
209
+ isDraft(): boolean {
210
+ return this.entry.data.draft ?? false;
211
+ }
212
+
213
+ isHidden(): boolean {
214
+ return this.entry.data.hidden ?? false;
215
+ }
216
+
217
+ isNotHidden(): boolean {
218
+ return !this.isHidden();
219
+ }
220
+
221
+ isFolder(): boolean {
222
+ return this.entry.data.folder ?? false;
223
+ }
224
+
225
+ isFamous(): boolean {
226
+ return this.entry.data.famous ?? false;
227
+ }
228
+
229
+ async toSidebarItem(): Promise<SidebarItemEntity> {
230
+ const debug = false;
231
+ const children = await this.getChildren();
232
+ let childItems = await Promise.all(
233
+ children
234
+ .filter((child) => child.isNotHidden())
235
+ .map((child) => child.toSidebarItem())
236
+ );
237
+
238
+ if (this.isBook()) {
239
+ childItems = [...childItems];
240
+ }
241
+
242
+ if (debug) {
243
+ cosyLogger.info(`${this.entry.id} 的侧边栏项目`);
244
+ console.log(childItems);
245
+ }
246
+
247
+ return new SidebarItemEntity({
248
+ text: this.getTitle(),
249
+ items: childItems,
250
+ link: this.getLink(),
251
+ badge: this.entry.data.badge,
252
+ });
253
+ }
254
+ }