Files

172 lines
4.3 KiB
TypeScript

export type BlogPost = {
id?: string
path?: string
title?: string
description?: string
published?: string
updated?: string | null
image?: string | null
draft?: boolean
pinned?: boolean
tags?: string[]
series?: string | null
seriesOrder?: number | null
difficulty?: string | null
body?: unknown
}
export type ArchiveMonth = {
month: string
label: string
posts: BlogPost[]
}
export type ArchiveYear = {
year: string
months: ArchiveMonth[]
}
export type SeriesSummary = {
name: string
path: string
count: number
posts: BlogPost[]
latestPost: BlogPost
latestTime: number
tags: string[]
}
export function isVisiblePost(post: BlogPost): boolean {
return !post.draft
}
export function sortPostsByPublished(posts: BlogPost[]): BlogPost[] {
return [...posts].sort((a, b) => getPostTime(b.published) - getPostTime(a.published))
}
export function sortPostsForSeries(posts: BlogPost[]): BlogPost[] {
return [...posts].sort((a, b) => {
const orderA = typeof a.seriesOrder === 'number' ? a.seriesOrder : Number.MAX_SAFE_INTEGER
const orderB = typeof b.seriesOrder === 'number' ? b.seriesOrder : Number.MAX_SAFE_INTEGER
if (orderA !== orderB) return orderA - orderB
return getPostTime(a.published) - getPostTime(b.published)
})
}
export function getArchiveGroups(posts: BlogPost[]): ArchiveYear[] {
const years = new Map<string, Map<string, BlogPost[]>>()
for (const post of sortPostsByPublished(posts)) {
const date = getPostDate(post.published)
if (!date) continue
const year = String(date.getFullYear())
const month = String(date.getMonth() + 1).padStart(2, '0')
if (!years.has(year)) {
years.set(year, new Map())
}
const months = years.get(year)!
if (!months.has(month)) {
months.set(month, [])
}
months.get(month)!.push(post)
}
return [...years.entries()]
.sort(([a], [b]) => Number(b) - Number(a))
.map(([year, months]) => ({
year,
months: [...months.entries()]
.sort(([a], [b]) => Number(b) - Number(a))
.map(([month, monthPosts]) => ({
month,
label: `${Number(month)}`,
posts: monthPosts,
})),
}))
}
export function getSeriesSummaries(posts: BlogPost[]): SeriesSummary[] {
const groups = new Map<string, BlogPost[]>()
for (const post of posts) {
const name = normalizeSeriesName(post.series)
if (!name) continue
if (!groups.has(name)) {
groups.set(name, [])
}
groups.get(name)!.push(post)
}
return [...groups.entries()]
.map(([name, items]) => {
const seriesPosts = sortPostsForSeries(items)
const latestPost = sortPostsByPublished(seriesPosts)[0]
const tags = [...new Set(seriesPosts.flatMap(post => post.tags || []))].slice(0, 6)
return {
name,
path: getSeriesPath(name),
count: seriesPosts.length,
posts: seriesPosts,
latestPost,
latestTime: Math.max(...seriesPosts.map(post => getPostTime(post.updated || post.published))),
tags,
}
})
.sort((a, b) => b.latestTime - a.latestTime)
}
export function normalizeSeriesName(series?: string | null): string {
return typeof series === 'string' ? series.trim() : ''
}
export function getSeriesPath(name: string): string {
return `/series/${encodeURIComponent(name)}`
}
export function decodeRouteParam(value: string): string {
try {
return decodeURIComponent(value)
} catch {
return value
}
}
export function formatPostDate(date?: string | null): string {
if (!date) return ''
return new Date(date).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
export function getWordCount(post: BlogPost): number {
const text = JSON.stringify(post.body || '').replace(/<[^>]*>/g, '')
const chinese = text.match(/[一-龥]/g) || []
const english = text.match(/[a-zA-Z]+/g) || []
return chinese.length + english.length
}
export function getReadTime(post: BlogPost): number {
return Math.max(1, Math.ceil(getWordCount(post) / 300))
}
function getPostDate(date?: string | null): Date | null {
if (!date) return null
const value = new Date(date)
if (Number.isNaN(value.getTime())) return null
return value
}
function getPostTime(date?: string | null): number {
return getPostDate(date)?.getTime() || 0
}