Initial commit

This commit is contained in:
2025-11-28 00:28:49 +08:00
committed by GitHub
commit f9a7c123cc
96 changed files with 17673 additions and 0 deletions

304
src/lib/data-utils.ts Normal file
View File

@@ -0,0 +1,304 @@
import { getCollection, render, type CollectionEntry } from 'astro:content'
import { readingTime, calculateWordCountFromHtml } from '@/lib/utils'
export async function getAllAuthors(): Promise<CollectionEntry<'authors'>[]> {
return await getCollection('authors')
}
export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> {
const posts = await getCollection('blog')
return posts
.filter((post) => !post.data.draft && !isSubpost(post.id))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
}
export async function getAllPostsAndSubposts(): Promise<
CollectionEntry<'blog'>[]
> {
const posts = await getCollection('blog')
return posts
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
}
export async function getAllProjects(): Promise<CollectionEntry<'projects'>[]> {
const projects = await getCollection('projects')
return projects.sort((a, b) => {
const dateA = a.data.startDate?.getTime() || 0
const dateB = b.data.startDate?.getTime() || 0
return dateB - dateA
})
}
export async function getAllTags(): Promise<Map<string, number>> {
const posts = await getAllPosts()
return posts.reduce((acc, post) => {
post.data.tags?.forEach((tag) => {
acc.set(tag, (acc.get(tag) || 0) + 1)
})
return acc
}, new Map<string, number>())
}
export async function getAdjacentPosts(currentId: string): Promise<{
newer: CollectionEntry<'blog'> | null
older: CollectionEntry<'blog'> | null
parent: CollectionEntry<'blog'> | null
}> {
const allPosts = await getAllPosts()
if (isSubpost(currentId)) {
const parentId = getParentId(currentId)
const allPosts = await getAllPosts()
const parent = allPosts.find((post) => post.id === parentId) || null
const posts = await getCollection('blog')
const subposts = posts
.filter(
(post) =>
isSubpost(post.id) &&
getParentId(post.id) === parentId &&
!post.data.draft,
)
.sort((a, b) => {
const dateDiff = a.data.date.valueOf() - b.data.date.valueOf()
if (dateDiff !== 0) return dateDiff
const orderA = a.data.order ?? 0
const orderB = b.data.order ?? 0
return orderA - orderB
})
const currentIndex = subposts.findIndex((post) => post.id === currentId)
if (currentIndex === -1) {
return { newer: null, older: null, parent }
}
return {
newer:
currentIndex < subposts.length - 1 ? subposts[currentIndex + 1] : null,
older: currentIndex > 0 ? subposts[currentIndex - 1] : null,
parent,
}
}
const parentPosts = allPosts.filter((post) => !isSubpost(post.id))
const currentIndex = parentPosts.findIndex((post) => post.id === currentId)
if (currentIndex === -1) {
return { newer: null, older: null, parent: null }
}
return {
newer: currentIndex > 0 ? parentPosts[currentIndex - 1] : null,
older:
currentIndex < parentPosts.length - 1
? parentPosts[currentIndex + 1]
: null,
parent: null,
}
}
export async function getPostsByAuthor(
authorId: string,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getAllPosts()
return posts.filter((post) => post.data.authors?.includes(authorId))
}
export async function getPostsByTag(
tag: string,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getAllPosts()
return posts.filter((post) => post.data.tags?.includes(tag))
}
export async function getRecentPosts(
count: number,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getAllPosts()
return posts.slice(0, count)
}
export async function getSortedTags(): Promise<
{ tag: string; count: number }[]
> {
const tagCounts = await getAllTags()
return [...tagCounts.entries()]
.map(([tag, count]) => ({ tag, count }))
.sort((a, b) => {
const countDiff = b.count - a.count
return countDiff !== 0 ? countDiff : a.tag.localeCompare(b.tag)
})
}
export function getParentId(subpostId: string): string {
return subpostId.split('/')[0]
}
export async function getSubpostsForParent(
parentId: string,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getCollection('blog')
return posts
.filter(
(post) =>
!post.data.draft &&
isSubpost(post.id) &&
getParentId(post.id) === parentId,
)
.sort((a, b) => {
const dateDiff = a.data.date.valueOf() - b.data.date.valueOf()
if (dateDiff !== 0) return dateDiff
const orderA = a.data.order ?? 0
const orderB = b.data.order ?? 0
return orderA - orderB
})
}
export function groupPostsByYear(
posts: CollectionEntry<'blog'>[],
): Record<string, CollectionEntry<'blog'>[]> {
return posts.reduce(
(acc: Record<string, CollectionEntry<'blog'>[]>, post) => {
const year = post.data.date.getFullYear().toString()
;(acc[year] ??= []).push(post)
return acc
},
{},
)
}
export async function hasSubposts(postId: string): Promise<boolean> {
const subposts = await getSubpostsForParent(postId)
return subposts.length > 0
}
export function isSubpost(postId: string): boolean {
return postId.includes('/')
}
export async function getParentPost(
subpostId: string,
): Promise<CollectionEntry<'blog'> | null> {
if (!isSubpost(subpostId)) {
return null
}
const parentId = getParentId(subpostId)
const allPosts = await getAllPosts()
return allPosts.find((post) => post.id === parentId) || null
}
export async function parseAuthors(authorIds: string[] = []) {
if (!authorIds.length) return []
const allAuthors = await getAllAuthors()
const authorMap = new Map(allAuthors.map((author) => [author.id, author]))
return authorIds.map((id) => {
const author = authorMap.get(id)
return {
id,
name: author?.data?.name || id,
avatar: author?.data?.avatar || '/static/logo.png',
isRegistered: !!author,
}
})
}
export async function getPostById(
postId: string,
): Promise<CollectionEntry<'blog'> | null> {
const allPosts = await getAllPostsAndSubposts()
return allPosts.find((post) => post.id === postId) || null
}
export async function getSubpostCount(parentId: string): Promise<number> {
const subposts = await getSubpostsForParent(parentId)
return subposts.length
}
export async function getCombinedReadingTime(postId: string): Promise<string> {
const post = await getPostById(postId)
if (!post) return readingTime(0)
let totalWords = calculateWordCountFromHtml(post.body)
if (!isSubpost(postId)) {
const subposts = await getSubpostsForParent(postId)
for (const subpost of subposts) {
totalWords += calculateWordCountFromHtml(subpost.body)
}
}
return readingTime(totalWords)
}
export async function getPostReadingTime(postId: string): Promise<string> {
const post = await getPostById(postId)
if (!post) return readingTime(0)
const wordCount = calculateWordCountFromHtml(post.body)
return readingTime(wordCount)
}
export type TOCHeading = {
slug: string
text: string
depth: number
isSubpostTitle?: boolean
}
export type TOCSection = {
type: 'parent' | 'subpost'
title: string
headings: TOCHeading[]
subpostId?: string
}
export async function getTOCSections(postId: string): Promise<TOCSection[]> {
const post = await getPostById(postId)
if (!post) return []
const parentId = isSubpost(postId) ? getParentId(postId) : postId
const parentPost = isSubpost(postId) ? await getPostById(parentId) : post
if (!parentPost) return []
const sections: TOCSection[] = []
const { headings: parentHeadings } = await render(parentPost)
if (parentHeadings.length > 0) {
sections.push({
type: 'parent',
title: 'Overview',
headings: parentHeadings.map((heading) => ({
slug: heading.slug,
text: heading.text,
depth: heading.depth,
})),
})
}
const subposts = await getSubpostsForParent(parentId)
for (const subpost of subposts) {
const { headings: subpostHeadings } = await render(subpost)
if (subpostHeadings.length > 0) {
sections.push({
type: 'subpost',
title: subpost.data.title,
headings: subpostHeadings.map((heading, index) => ({
slug: heading.slug,
text: heading.text,
depth: heading.depth,
isSubpostTitle: index === 0,
})),
subpostId: subpost.id,
})
}
}
return sections
}

37
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,37 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function formatDate(date: Date) {
return Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date)
}
export function calculateWordCountFromHtml(
html: string | null | undefined,
): number {
if (!html) return 0
const textOnly = html.replace(/<[^>]+>/g, '')
return textOnly.split(/\s+/).filter(Boolean).length
}
export function readingTime(wordCount: number): string {
const readingTimeMinutes = Math.max(1, Math.round(wordCount / 200))
return `${readingTimeMinutes} min read`
}
export function getHeadingMargin(depth: number): string {
const margins: Record<number, string> = {
3: 'ml-4',
4: 'ml-8',
5: 'ml-12',
6: 'ml-16',
}
return margins[depth] || ''
}