@coffic/cosy-ui 0.5.8 → 0.5.12

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.
@@ -0,0 +1,170 @@
1
+ import { render, type RenderResult, type CollectionEntry, type DataEntryMap } from 'astro:content';
2
+ import { SidebarItemEntity, type SidebarProvider } from './SidebarItem';
3
+ import { logger } from '../utils/logger';
4
+
5
+ /**
6
+ * 文档基类,提供所有文档类型共享的基本功能
7
+ */
8
+ export abstract class BaseDoc<
9
+ Collection extends keyof DataEntryMap,
10
+ T extends CollectionEntry<Collection>,
11
+ > implements SidebarProvider
12
+ {
13
+ protected entry: T;
14
+
15
+ constructor(entry: T) {
16
+ this.entry = entry;
17
+ }
18
+
19
+ /**
20
+ * 获取文档ID
21
+ */
22
+ getId(): string {
23
+ return this.entry.id;
24
+ }
25
+
26
+ /**
27
+ * 获取文档标题
28
+ */
29
+ getTitle(): string {
30
+ return this.entry.data.title as string;
31
+ }
32
+
33
+ /**
34
+ * 获取文档语言
35
+ */
36
+ getLang(): string {
37
+ return this.entry.id.split('/')[0];
38
+ }
39
+
40
+ /**
41
+ * 获取文档slug
42
+ */
43
+ getSlug(): string {
44
+ return this.getId().split('/').slice(1).join('/');
45
+ }
46
+
47
+ /**
48
+ * 获取文档描述
49
+ */
50
+ getDescription(): string {
51
+ return this.entry.data.description as string;
52
+ }
53
+
54
+ /**
55
+ * 获取文档链接
56
+ * 每个子类必须实现此方法以提供正确的链接
57
+ */
58
+ abstract getLink(): string;
59
+
60
+ /**
61
+ * 渲染文档内容
62
+ */
63
+ async render(): Promise<RenderResult> {
64
+ return await render(this.entry);
65
+ }
66
+
67
+ /**
68
+ * 获取文档的层级深度
69
+ * 例如:对于 ID 为 "zh-cn/blog/typescript" 的文档,深度为3
70
+ */
71
+ getLevel(): number {
72
+ return this.entry.id.split('/').length;
73
+ }
74
+
75
+ /**
76
+ * 转换为侧边栏项目
77
+ * 基本实现,只包含当前文档
78
+ */
79
+ async toSidebarItem(): Promise<SidebarItemEntity> {
80
+ return new SidebarItemEntity({
81
+ text: this.getTitle(),
82
+ link: this.getLink(),
83
+ });
84
+ }
85
+ }
86
+
87
+ /**
88
+ * 层级文档基类,为有层级结构的文档类型提供额外功能
89
+ * 例如课程等有父子关系的文档
90
+ */
91
+ export abstract class HierarchicalDoc<
92
+ Collection extends keyof DataEntryMap,
93
+ T extends CollectionEntry<Collection>,
94
+ > extends BaseDoc<Collection, T> {
95
+ /**
96
+ * 获取父文档ID
97
+ * 例如:对于 ID 为 "zh-cn/blog/typescript" 的文档,父ID为 "zh-cn/blog"
98
+ *
99
+ * @returns 父文档ID,如果没有父文档则返回null
100
+ */
101
+ getParentId(): string | null {
102
+ const parts = this.entry.id.split('/');
103
+ return parts.length > 1 ? parts.slice(0, -1).join('/') : null;
104
+ }
105
+
106
+ /**
107
+ * 获取顶级文档的ID
108
+ * 例如:对于 ID 为 "zh-cn/blog/typescript" 的文档,顶级ID为 "zh-cn/blog"
109
+ *
110
+ * 默认实现假设顶级ID是前两部分
111
+ * 子类可以根据需要覆盖此方法
112
+ */
113
+ async getTopDocId(): Promise<string> {
114
+ const id = this.entry.id;
115
+ const parts = id.split('/');
116
+ return parts[0] + '/' + parts[1];
117
+ }
118
+
119
+ /**
120
+ * 获取顶级文档
121
+ * 子类应该实现此方法以提供正确的顶级文档
122
+ */
123
+ abstract getTopDoc(): Promise<HierarchicalDoc<Collection, T> | null>;
124
+
125
+ /**
126
+ * 获取子文档
127
+ * 子类应该实现此方法以提供正确的子文档列表
128
+ */
129
+ abstract getChildren(): Promise<HierarchicalDoc<Collection, T>[]>;
130
+
131
+ /**
132
+ * 转换为侧边栏项目
133
+ * 如果文档有子文档,会包含子文档的侧边栏项目
134
+ */
135
+ override async toSidebarItem(): Promise<SidebarItemEntity> {
136
+ const debug = false;
137
+
138
+ const children = await this.getChildren();
139
+ const childItems = await Promise.all(children.map((child) => child.toSidebarItem()));
140
+
141
+ if (debug) {
142
+ logger.info(`${this.entry.id} 的侧边栏项目`);
143
+ console.log(childItems);
144
+ }
145
+
146
+ return new SidebarItemEntity({
147
+ text: this.getTitle(),
148
+ items: childItems,
149
+ link: this.getLink(),
150
+ });
151
+ }
152
+
153
+ /**
154
+ * 获取顶级侧边栏项目
155
+ * 如果有顶级文档,返回顶级文档的侧边栏项目
156
+ * 否则返回当前文档的侧边栏项目
157
+ */
158
+ async getTopSidebarItem(): Promise<SidebarItemEntity> {
159
+ const topDoc = await this.getTopDoc();
160
+ if (topDoc) {
161
+ return await topDoc.toSidebarItem();
162
+ }
163
+
164
+ return new SidebarItemEntity({
165
+ text: this.getTitle(),
166
+ items: [],
167
+ link: this.getLink(),
168
+ });
169
+ }
170
+ }
@@ -0,0 +1,53 @@
1
+ import type { BlogEntry } from '../database/BlogDB';
2
+ import { LinkUtil } from '../utils/link';
3
+ import Tag from './Tag';
4
+ import { BaseDoc } from './BaseDoc';
5
+ import { COLLECTION_NAME } from '../database/BlogDB';
6
+
7
+ export default class BlogDoc extends BaseDoc<typeof COLLECTION_NAME, BlogEntry> {
8
+ private constructor(entry: BlogEntry) {
9
+ super(entry);
10
+ }
11
+
12
+ static fromEntry(entry: BlogEntry) {
13
+ return new BlogDoc(entry);
14
+ }
15
+
16
+ getLink(): string {
17
+ return LinkUtil.getBlogLink(this.entry.id, this.getLang());
18
+ }
19
+
20
+ getTags(): Tag[] {
21
+ const tags = this.entry.data.tags as string[];
22
+
23
+ if (!tags || tags.length === 0) {
24
+ return [];
25
+ }
26
+
27
+ return tags.map((tag) => new Tag(tag, 0, this.getLang()));
28
+ }
29
+
30
+ getDate(): Date {
31
+ return new Date(this.entry.data.date as Date);
32
+ }
33
+
34
+ getDateForDisplay() {
35
+ try {
36
+ const dateObj = new Date(this.entry.data.date as Date);
37
+
38
+ // Check if date is valid
39
+ if (isNaN(dateObj.getTime())) {
40
+ console.warn(`Invalid date format: ${this.entry.data.date}`);
41
+ return 'Date unavailable: ' + this.getTitle() + ' ' + this.getLink();
42
+ }
43
+ return dateObj.toLocaleDateString('zh-CN', {
44
+ year: 'numeric',
45
+ month: 'long',
46
+ day: 'numeric',
47
+ });
48
+ } catch (error) {
49
+ console.error(`Error formatting date: ${this.entry.data.date}`, error);
50
+ return 'Date unavailable';
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,56 @@
1
+ import { logger } from '@/utils/logger';
2
+ import { SidebarItemEntity } from './SidebarItem';
3
+ import type { CourseEntry } from '../database/CourseDB';
4
+ import courseDB from '../database/CourseDB';
5
+ import { LinkUtil } from '../utils/link';
6
+ import { HierarchicalDoc } from './BaseDoc';
7
+ import { COLLECTION_NAME } from '../database/CourseDB';
8
+
9
+ export default class CourseDoc extends HierarchicalDoc<typeof COLLECTION_NAME, CourseEntry> {
10
+ constructor(entry: CourseEntry) {
11
+ super(entry);
12
+ }
13
+
14
+ static fromEntry(entry: CourseEntry) {
15
+ return new CourseDoc(entry);
16
+ }
17
+
18
+ getLink(): string {
19
+ return LinkUtil.getCourseLink(this.entry.id);
20
+ }
21
+
22
+ async getTopDoc(): Promise<CourseDoc | null> {
23
+ const id = await this.getTopDocId();
24
+ const doc = await courseDB.find(id);
25
+ return doc;
26
+ }
27
+
28
+ async getChildren(): Promise<CourseDoc[]> {
29
+ return await courseDB.getChildren(this.entry.id);
30
+ }
31
+
32
+ override async toSidebarItem(): Promise<SidebarItemEntity> {
33
+ const debug = false;
34
+ const children = await this.getChildren();
35
+ let childItems = await Promise.all(children.map((child) => child.toSidebarItem()));
36
+
37
+ if (this.isBook()) {
38
+ childItems = [...childItems];
39
+ }
40
+
41
+ if (debug) {
42
+ logger.info(`${this.entry.id} 的侧边栏项目`);
43
+ console.log(childItems);
44
+ }
45
+
46
+ return new SidebarItemEntity({
47
+ text: this.getTitle(),
48
+ items: childItems,
49
+ link: this.getLink(),
50
+ });
51
+ }
52
+
53
+ isBook(): boolean {
54
+ return this.entry.id.split('/').length === 2;
55
+ }
56
+ }
@@ -0,0 +1,117 @@
1
+ import type { ExperimentEntry } from '../database/ExperimentDB';
2
+ import experimentDB from '../database/ExperimentDB';
3
+ import { logger } from '../utils/logger';
4
+ import { SidebarItemEntity } from './SidebarItem';
5
+ import type { Heading } from './Heading';
6
+ import { LinkUtil } from '../utils/link';
7
+ import { HierarchicalDoc } from './BaseDoc';
8
+ import { COLLECTION_NAME } from '../database/ExperimentDB';
9
+
10
+ export default class ExperimentDoc extends HierarchicalDoc<
11
+ typeof COLLECTION_NAME,
12
+ ExperimentEntry
13
+ > {
14
+ constructor(entry: ExperimentEntry) {
15
+ super(entry);
16
+ }
17
+
18
+ isBook(): boolean {
19
+ return this.entry.id.split('/').length === 2;
20
+ }
21
+
22
+ async getBookId(): Promise<string> {
23
+ return await this.getTopDocId();
24
+ }
25
+
26
+ async getBook(): Promise<ExperimentDoc | null> {
27
+ const bookId = await this.getBookId();
28
+ return await experimentDB.find(bookId);
29
+ }
30
+
31
+ getLink(): string {
32
+ const debug = false;
33
+ const lang = this.getLang();
34
+ const link = LinkUtil.getExperimentLink(lang, this.getId());
35
+
36
+ if (debug) {
37
+ logger.info(`获取 ${this.entry.id} 的链接: ${link}`);
38
+ }
39
+
40
+ return link;
41
+ }
42
+
43
+ /**
44
+ * 获取文档的语言
45
+ *
46
+ * 文档的 id 格式为 `book-id/zh-cn/chapter-id/lesson-id`
47
+ *
48
+ * @returns 语言
49
+ */
50
+ override getLang(): string {
51
+ const debug = false;
52
+
53
+ const parts = this.entry.id.split('/');
54
+ const lang = parts[1];
55
+
56
+ if (debug) {
57
+ logger.info(`获取 ${this.entry.id} 的语言: ${lang}`);
58
+ }
59
+
60
+ return lang;
61
+ }
62
+
63
+ getHTML(): string {
64
+ const debug = false;
65
+
66
+ if (debug) {
67
+ logger.info(`获取 ${this.entry.id} 的 HTML`);
68
+ }
69
+
70
+ return this.entry.rendered?.html || '';
71
+ }
72
+
73
+ getHeadings(): Heading[] {
74
+ const debug = false;
75
+
76
+ if (debug) {
77
+ logger.info(`获取 ${this.entry.id} 的 headings`);
78
+ }
79
+
80
+ return (this.entry.rendered?.metadata?.headings as Heading[]) || [];
81
+ }
82
+
83
+ async getTopDoc(): Promise<ExperimentDoc | null> {
84
+ const bookId = await this.getBookId();
85
+ return await experimentDB.find(bookId);
86
+ }
87
+
88
+ async getChildren(): Promise<ExperimentDoc[]> {
89
+ return await experimentDB.getChildren(this.entry.id);
90
+ }
91
+
92
+ async getDescendants(): Promise<ExperimentDoc[]> {
93
+ return await experimentDB.getDescendantDocs(this.entry.id);
94
+ }
95
+
96
+ override async toSidebarItem(): Promise<SidebarItemEntity> {
97
+ const debug = false;
98
+
99
+ const children = await this.getChildren();
100
+ let childItems = await Promise.all(children.map((child) => child.toSidebarItem()));
101
+
102
+ if (this.isBook()) {
103
+ childItems = [...childItems];
104
+ }
105
+
106
+ if (debug) {
107
+ logger.info(`${this.entry.id} 的侧边栏项目`);
108
+ console.log(childItems);
109
+ }
110
+
111
+ return new SidebarItemEntity({
112
+ text: this.getTitle(),
113
+ items: childItems,
114
+ link: this.getLink(),
115
+ });
116
+ }
117
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 表示文档中的标题结构
3
+ */
4
+ export interface Heading {
5
+ /** 标题深度,如 h1=1, h2=2, h3=3 等 */
6
+ depth: number;
7
+
8
+ /** 标题的唯一标识符,用于锚点链接 */
9
+ slug: string;
10
+
11
+ /** 标题文本内容 */
12
+ text: string;
13
+ }
@@ -0,0 +1,114 @@
1
+ import type { LessonEntry } from '../database/LessonDB';
2
+ import lessonDB from '../database/LessonDB';
3
+ import { logger } from '../utils/logger';
4
+ import { SidebarItemEntity } from './SidebarItem';
5
+ import type { Heading } from './Heading';
6
+ import { LinkUtil } from '../utils/link';
7
+ import { HierarchicalDoc } from './BaseDoc';
8
+ import { COLLECTION_NAME } from '../database/LessonDB';
9
+
10
+ export default class LessonDoc extends HierarchicalDoc<typeof COLLECTION_NAME, LessonEntry> {
11
+ constructor(entry: LessonEntry) {
12
+ super(entry);
13
+ }
14
+
15
+ isBook(): boolean {
16
+ return this.entry.id.split('/').length === 2;
17
+ }
18
+
19
+ async getBookId(): Promise<string> {
20
+ return await this.getTopDocId();
21
+ }
22
+
23
+ async getBook(): Promise<LessonDoc | null> {
24
+ const bookId = await this.getBookId();
25
+ return await lessonDB.find(bookId);
26
+ }
27
+
28
+ getLink(): string {
29
+ const debug = false;
30
+ const lang = this.getLang();
31
+ const link = LinkUtil.getLessonLink(lang, this.getId());
32
+
33
+ if (debug) {
34
+ logger.info(`获取 ${this.entry.id} 的链接: ${link}`);
35
+ }
36
+
37
+ return link;
38
+ }
39
+
40
+ /**
41
+ * 获取文档的语言
42
+ *
43
+ * 文档的 id 格式为 `book-id/zh-cn/chapter-id/lesson-id`
44
+ *
45
+ * @returns 语言
46
+ */
47
+ override getLang(): string {
48
+ const debug = false;
49
+
50
+ const parts = this.entry.id.split('/');
51
+ const lang = parts[1];
52
+
53
+ if (debug) {
54
+ logger.info(`获取 ${this.entry.id} 的语言: ${lang}`);
55
+ }
56
+
57
+ return lang;
58
+ }
59
+
60
+ getHTML(): string {
61
+ const debug = false;
62
+
63
+ if (debug) {
64
+ logger.info(`获取 ${this.entry.id} 的 HTML`);
65
+ }
66
+
67
+ return this.entry.rendered?.html || '';
68
+ }
69
+
70
+ getHeadings(): Heading[] {
71
+ const debug = false;
72
+
73
+ if (debug) {
74
+ logger.info(`获取 ${this.entry.id} 的 headings`);
75
+ }
76
+
77
+ return (this.entry.rendered?.metadata?.headings as Heading[]) || [];
78
+ }
79
+
80
+ async getTopDoc(): Promise<LessonDoc | null> {
81
+ const bookId = await this.getBookId();
82
+ return await lessonDB.find(bookId);
83
+ }
84
+
85
+ async getChildren(): Promise<LessonDoc[]> {
86
+ return await lessonDB.getChildren(this.entry.id);
87
+ }
88
+
89
+ async getDescendants(): Promise<LessonDoc[]> {
90
+ return await lessonDB.getDescendantDocs(this.entry.id);
91
+ }
92
+
93
+ override async toSidebarItem(): Promise<SidebarItemEntity> {
94
+ const debug = false;
95
+
96
+ const children = await this.getChildren();
97
+ let childItems = await Promise.all(children.map((child) => child.toSidebarItem()));
98
+
99
+ if (this.isBook()) {
100
+ childItems = [...childItems];
101
+ }
102
+
103
+ if (debug) {
104
+ logger.info(`${this.entry.id} 的侧边栏项目`);
105
+ console.log(childItems);
106
+ }
107
+
108
+ return new SidebarItemEntity({
109
+ text: this.getTitle(),
110
+ items: childItems,
111
+ link: this.getLink(),
112
+ });
113
+ }
114
+ }
@@ -0,0 +1,82 @@
1
+ import { SidebarItem } from './SidebarItem';
2
+ import type { MetaEntry } from '../database/MetaDB';
3
+ import { LinkUtil } from '../utils/link';
4
+ import { BaseDoc } from './BaseDoc';
5
+ import metaDB from '../database/MetaDB';
6
+ import { COLLECTION_NAME } from '../database/MetaDB';
7
+
8
+ export default class MetaDoc extends BaseDoc<typeof COLLECTION_NAME, MetaEntry> {
9
+ constructor(entry: MetaEntry) {
10
+ super(entry);
11
+ }
12
+
13
+ static fromEntry(entry: MetaEntry) {
14
+ return new MetaDoc(entry);
15
+ }
16
+
17
+ getLink(): string {
18
+ return LinkUtil.getMetaLink(this.getLang(), this.getSlug());
19
+ }
20
+
21
+ getLang(): string {
22
+ return this.entry.id.split('/')[0];
23
+ }
24
+
25
+ /**
26
+ * 获取元数据页面的 slug
27
+ * 例如:
28
+ * ID: zh-cn/about 的 slug 为 about
29
+ * ID: en/privacy 的 slug 为 privacy
30
+ */
31
+ override getSlug(): string {
32
+ // 从 ID 中获取 slug,即删除以/分割后的第一个元素
33
+ return this.getId().split('/').slice(1).join('/');
34
+ }
35
+
36
+ /**
37
+ * 获取兄弟文档
38
+ * 例如:对于 'zh-cn/about',会返回 'zh-cn' 下的其他文档
39
+ */
40
+ async getSiblingDocs(): Promise<MetaDoc[]> {
41
+ return await metaDB.getSiblings(this.entry.id);
42
+ }
43
+
44
+ /**
45
+ * 获取兄弟文档的侧边栏项目
46
+ */
47
+ async getSiblingSidebarItems(): Promise<SidebarItem[]> {
48
+ const siblings = await this.getSiblingDocs();
49
+ const siblingItems = await Promise.all(
50
+ siblings.map((sibling) => {
51
+ return new SidebarItem({
52
+ label: sibling.getTitle(),
53
+ link: sibling.getLink(),
54
+ });
55
+ })
56
+ );
57
+ return siblingItems;
58
+ }
59
+
60
+ /**
61
+ * 重写侧边栏项目方法
62
+ * 对于元数据页面,我们不显示子项目
63
+ */
64
+ override async toSidebarItem(): Promise<SidebarItem> {
65
+ return new SidebarItem({
66
+ label: this.getTitle(),
67
+ link: this.getLink(),
68
+ });
69
+ }
70
+
71
+ /**
72
+ * 重写顶级侧边栏项目方法
73
+ * 对于元数据页面,我们显示所有兄弟页面作为侧边栏项目
74
+ */
75
+ async getTopSidebarItem(): Promise<SidebarItem> {
76
+ return new SidebarItem({
77
+ label: '了解我们',
78
+ items: await this.getSiblingSidebarItems(),
79
+ link: '',
80
+ });
81
+ }
82
+ }
@@ -0,0 +1,42 @@
1
+ import blogDB from '../database/BlogDB';
2
+ import { SidebarItem } from './SidebarItem';
3
+ import { type TagStaticPath } from '../types/static-path';
4
+ import { LinkUtil } from '../utils/link';
5
+
6
+ export class Tag {
7
+ name: string;
8
+ lang: string;
9
+ count: number;
10
+
11
+ constructor(name: string, count: number, lang: string) {
12
+ this.name = name;
13
+ this.count = count;
14
+ this.lang = lang;
15
+ }
16
+
17
+ toSidebarItem(lang: string): SidebarItem {
18
+ return new SidebarItem({
19
+ label: this.name,
20
+ link: LinkUtil.getTagLink(lang, this.name),
21
+ });
22
+ }
23
+
24
+ toTagPath(): TagStaticPath {
25
+ return {
26
+ params: { slug: this.lang + '/blogs/tag/' + this.name },
27
+ props: { tag: this.name },
28
+ };
29
+ }
30
+
31
+ static async makeRootSidebarItem(lang: string): Promise<SidebarItem> {
32
+ const tags = await blogDB.getTagsByLang(lang);
33
+
34
+ return new SidebarItem({
35
+ label: 'Tags',
36
+ link: LinkUtil.getTagLink(lang, ''),
37
+ items: tags.map((tag: Tag) => tag.toSidebarItem(lang)),
38
+ });
39
+ }
40
+ }
41
+
42
+ export default Tag;
package/dist/index.ts CHANGED
@@ -94,7 +94,7 @@ export * from './types/meta';
94
94
  export type { ImageProvider, ImageOptions } from './utils/image';
95
95
 
96
96
  // Models
97
- export * from './models/BaseDoc';
97
+ export * from './entities/BaseDoc';
98
98
 
99
99
  // Database
100
100
  export * from './database/BaseDB';
@@ -0,0 +1,8 @@
1
+ export interface TagStaticPath {
2
+ params: {
3
+ slug: string;
4
+ };
5
+ props: {
6
+ tag: string;
7
+ };
8
+ }