import { join } from 'path'; import fs from 'fs/promises'; import yaml from 'yaml'; import { marked } from 'marked'; import hljs from 'highlight.js'; import { getDateObject } from '../utils/date-object'; import type { ArchiveDto, BlogPost, BlogPostTag } from '../types/post'; const dir = join('content', 'blog'); marked.use({ renderer: { code: ({ text, lang }): string => { const language = lang || 'plaintext'; const created = hljs.highlight(text, { language }).value; return `
${created}
`; }, link: ({ href, title, text }): string => `${text}`, }, }); async function readBlogPosts() { const files = await fs.readdir(dir); const readMD = files.filter((file) => file.endsWith('.md')); const readFiles: BlogPost[] = []; for (const file of readMD) { const post = await readBlogPost(file.replace('.md', '')); if (!post) continue; readFiles.push(post); } readFiles.sort((a, b) => new Date(b.date) .toISOString() .localeCompare(new Date(a.date).toISOString(), 'en', { numeric: true }), ); return readFiles; } async function readBlogPost(slug: string): Promise { const decoded = decodeURIComponent(slug); if (!slug || decoded.includes('/') || decoded.includes('.')) throw new Error('Invalid post slug'); const file = `${slug}.md`; const mdpath = join(dir, file); const read = await fs.readFile(mdpath, { encoding: 'utf-8' }); const { header: parsedHeader, length: headerLength } = await readHeader(read); const renderedMd = await marked(read.substring(headerLength), { async: true, }); const boundary = renderedMd.indexOf(''); const { year, month, day } = getDateObject(parsedHeader); const content = { ...parsedHeader, file, slug, summary: renderedMd.substring(0, boundary > -1 ? boundary : 240), fullSlug: `${year}/${month}/${day}/${slug}`, markdown: read.substring(headerLength), html: renderedMd.replace('', ''), }; return content; } let blogPostCache: BlogPost[] = []; export const blogPostList = async () => { if (!blogPostCache.length) { blogPostCache = await readBlogPosts(); } return blogPostCache; }; export const getFilteredBlogPosts = async ( condition?: (body: BlogPost) => boolean, htmlContent = true, includeMarkdown = true, ): Promise => { const posts = await blogPostList(); return posts .filter((item) => (condition ? condition(item) : true)) .map((item) => ({ ...item, html: htmlContent ? item.html : undefined, markdown: includeMarkdown ? item.markdown : undefined, })); }; export const getBlogPost = async ( slug: string, htmlContent = true, includeMarkdown = true, ): Promise => { const posts = await blogPostList(); const item = posts.find((item) => item.slug === slug); if (!item) return; const index = posts.indexOf(item); const next = index > 0 ? posts[index - 1] : undefined; const prev = posts[index + 1]; return { ...item, html: htmlContent ? item.html : undefined, markdown: includeMarkdown ? item.markdown : undefined, next: next ? { title: next.title, slug: next.slug, fullSlug: next.fullSlug } : undefined, prev: prev ? { title: prev.title, slug: prev.slug, fullSlug: prev.fullSlug } : undefined, }; }; export async function getTags(): Promise { const posts = await getFilteredBlogPosts(undefined, false, false); const obj: BlogPostTag[] = []; for (const post of posts) { for (const tag of post.tags || []) { const find = obj.find((item) => item.name === tag); if (find) { find.count++; find.posts.push(post.slug); continue; } obj.push({ name: tag, count: 1, posts: [post.slug], }); } } obj.sort((a, b) => a.name.localeCompare(b.name, 'en')); return obj; } export async function getArchiveTree(condition?: (body: BlogPost) => boolean) { const posts = await getFilteredBlogPosts(condition, false, false); const obj: ArchiveDto = {}; for (const post of posts) { const { year, month, day } = getDateObject(post); if (!obj[year]) { obj[year] = {}; } if (!obj[year][month]) { obj[year][month] = {}; } if (!obj[year][month][day]) { obj[year][month][day] = []; } obj[year][month][day].push(post.slug); } return obj; } export async function generatePaths(prefix = '/') { const tree = await getArchiveTree(); const paths = []; for (const year of Object.keys(tree)) { paths.push(`${prefix}archive/${year}`); for (const month of Object.keys(tree[year])) { paths.push(`${prefix}archive/${year}/${month}`); for (const day of Object.keys(tree[year][month])) { paths.push(`${prefix}archive/${year}/${month}/${day}`); } } } paths.sort((a, b) => b.localeCompare(a, 'en', { numeric: true })); return paths; } async function readHeader(fileContents: string) { const splitter = fileContents.split('\n'); let header = ''; for (const line of splitter) { if (line === '---') { if (!header.length) continue; break; } header += line + '\n'; } return { header: yaml.parse(header), length: header.length + 9, }; }