master
  1import { t } from './i18n';
  2import { buildConfig } from '@/config';
  3import type { BlogPostData } from '@/types/data';
  4import type { BlogPost } from '@/types/data';
  5import { type CollectionEntry, getCollection, render } from 'astro:content';
  6import dayjs from 'dayjs';
  7
  8export async function getPosts() {
  9  const posts = await getCollection('posts');
 10  const onDev = import.meta.env.DEV;
 11  if (onDev && buildConfig.showDraftsOnDev) {
 12    const drafts = (await getCollection('drafts')) as unknown as CollectionEntry<'posts'>[];
 13    drafts.forEach(async (draft) => {
 14      if (draft.data.published !== undefined) return;
 15      const { remarkPluginFrontmatter } = await render(draft);
 16      const published = dayjs(remarkPluginFrontmatter.createAt as string).toDate();
 17      draft.data.published = published;
 18      draft.data.draft = true;
 19    });
 20    return [...posts, ...drafts];
 21  }
 22  return posts;
 23}
 24
 25export async function getSortedPosts(): Promise<BlogPost[]> {
 26  const allBlogPosts = (await getPosts()) as unknown as BlogPost[];
 27  const sortedBlogPosts = allBlogPosts.sort(
 28    (a: { data: BlogPostData }, b: { data: BlogPostData }) => {
 29      const dateA = new Date(a.data.published);
 30      const dateB = new Date(b.data.published);
 31      return dateA > dateB ? -1 : 1;
 32    }
 33  );
 34  return sortedBlogPosts;
 35}
 36
 37export async function getPostsCount(): Promise<number> {
 38  const allBlogPosts = (await getCollection('posts')) as unknown as {
 39    body: string;
 40    data: BlogPostData;
 41  }[];
 42  return allBlogPosts.length;
 43}
 44
 45export async function getCategories(): Promise<Map<string, number>> {
 46  const allBlogPosts = await getSortedPosts();
 47  const categoryMap = new Map<string, number>();
 48
 49  allBlogPosts.forEach((post) => {
 50    const category = post.data.category || t.meta.unCategorized();
 51    categoryMap.set(category, (categoryMap.get(category) || 0) + 1);
 52  });
 53
 54  return categoryMap;
 55}
 56
 57export async function getTags(): Promise<Map<string, number>> {
 58  const allBlogPosts = await getSortedPosts();
 59  const tagMap = new Map<string, number>();
 60
 61  allBlogPosts.forEach((post) => {
 62    const tags = post.data.tags || [];
 63    tags.forEach((tag) => {
 64      tagMap.set(tag, (tagMap.get(tag) || 0) + 1);
 65    });
 66  });
 67
 68  return tagMap;
 69}
 70
 71export function getCategoryUrl(category: string | undefined) {
 72  return category
 73    ? `/archives/categories/${category.replaceAll(/[\\/]/g, '-')}/1/`
 74    : `/archives/categories/uncategorized/1/`;
 75}
 76
 77export function getTagUrl(tag: string) {
 78  return tag === t.meta.unTagged()
 79    ? `/archives/tags/untagged/1`
 80    : `/archives/tags/${tag.replaceAll(/[\\/]/g, '-')}/1`;
 81}
 82
 83/**
 84 * 获取所有的引用,返回一个数组
 85 */
 86export async function getAllReferences() {
 87  type frontmatterRef = {
 88    /** 引用的文章,即 [[ref|alias]] 中的 ref 部分 */
 89    reference: string;
 90    /** 引用的上下文,即整个 xxxx[[ref|alias]]xxxx 的内容,截取前后 20 个字符 */
 91    context: string;
 92    /** 引用的偏移量,即 [[ref|alias]] 在上下文字符串中的起始和结束位置 */
 93    offset: [number, number];
 94    /** 引用应该被分配的 id,用于通过 #id 跳转 */
 95    id: string;
 96  };
 97  type Article = {
 98    /** 文章的标题 */
 99    title: string;
100    /** 文章的集合,posts 或 spec,drafts 会被处理为 posts */
101    collection: 'posts' | 'spec';
102    /** 文章的 id,对 posts 来说是 slug 参数,对 spec 来说是文件名 */
103    id: string;
104  };
105
106  const posts = await getPosts();
107  const specs = await getCollection('spec');
108
109  const pathMap = [
110    ...posts.map((it) => ({
111      id: it.id,
112      title: it.data.title,
113      collection: it.collection,
114      filePath: it.filePath,
115    })),
116    ...specs.map((it) => ({
117      id: it.id,
118      title: it.data.title,
119      collection: it.collection,
120      filePath: it.filePath,
121    })),
122  ].reduce(
123    (acc, it) => {
124      const path = it.filePath!.replace('src/content/', '').split('.').slice(0, -1).join('.');
125      acc[path] = {
126        id: it.id,
127        title: it.title || path.split('/').slice(-1)[-1],
128        collection: it.collection,
129      };
130      return acc;
131    },
132    {} as Record<
133      string,
134      {
135        id: string;
136        title: string;
137        collection: 'posts' | 'spec';
138      }
139    >
140  );
141
142  const postsRefData = posts.map(async (post) => {
143    const { remarkPluginFrontmatter } = await render(post);
144    const references: frontmatterRef[] = remarkPluginFrontmatter.references || [];
145    return {
146      title: post.data.title,
147      colletion: 'posts',
148      id: post.id,
149      references: references.map((ref) => ({
150        reference: ref.reference.split('#')[0],
151        context: ref.context,
152        offset: ref.offset,
153        id: ref.id,
154      })),
155    };
156  });
157  const specRefData = specs.map(async (spec) => {
158    const { remarkPluginFrontmatter } = await render(spec);
159    const references: frontmatterRef[] = remarkPluginFrontmatter.references || [];
160    return {
161      title: spec.data.title || spec.filePath?.split('/').slice(-1)[0],
162      colletion: 'spec',
163      id: spec.id,
164      references,
165    };
166  });
167
168  const getArticle = (refPath: string): Article | null => {
169    let collection = refPath.split('/')[0];
170    if (!['posts', 'drafts', 'spec'].includes(collection)) {
171      collection = 'posts';
172      refPath = `posts/${refPath}`;
173    }
174    const data = pathMap[refPath];
175    if (!data) return null;
176    const { id, title } = data;
177    if (collection === 'spec') {
178      const article = specs.find((it) => it.id === id);
179      if (article) return { title, collection, id };
180    } else {
181      const article = posts.find((it) => it.id === id);
182      if (article) return { title, collection: 'posts', id };
183    }
184    return null;
185  };
186
187  const references: {
188    refBy: Article;
189    refTo: Article;
190    /** 引用的上下文,即整个 xxxx[[ref|alias]]xxxx 的内容,截取前后 20 个字符 */
191    context: string;
192    /** 引用的偏移量,即 [[ref|alias]] 在上下文字符串中的起始和结束位置 */
193    offset: [number, number];
194    /** 引用应该被分配的 id,用于通过 #id 跳转 */
195    id: string;
196  }[] = [
197    ...(await Promise.all(postsRefData)).flatMap((data) => {
198      const article: Article = {
199        title: data.title,
200        collection: data.colletion as 'posts' | 'spec',
201        id: data.id,
202      };
203      return data.references
204        .map((ref) => {
205          const { reference, context, offset, id } = ref;
206          const refTo = getArticle(reference);
207          if (refTo) return { refBy: article, refTo, context, offset, id };
208          return null;
209        })
210        .filter((it) => it !== null);
211    }),
212    ...(await Promise.all(specRefData)).flatMap((data) => {
213      const article: Article = {
214        title: data.title || data.id,
215        collection: data.colletion as 'posts' | 'spec',
216        id: data.id,
217      };
218      return data.references
219        .map((ref) => {
220          const { reference, context, offset, id } = ref;
221          const refTo = getArticle(reference);
222          if (refTo) return { refBy: article, refTo, context, offset, id };
223          return null;
224        })
225        .filter((it) => it !== null);
226    }),
227  ];
228  return references;
229}