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 { BlogPost, BlogPostTag } from '../types/post'; const dir = join('content', 'blog'); marked.use({ renderer: { code: (code, info, escaped): string => { const language = hljs.getLanguage(info) ? info : 'plaintext'; const created = hljs.highlight(code, { language }).value; return `
${created}
`; }, link: (href: string, title: string, text: string): string => { return `${text}`; }, }, }); export async function readBlogPosts( condition?: (body: BlogPost) => boolean, render = true, includeContent = true ) { const files = await fs.readdir(dir); const readMD = files.filter((file) => file.endsWith('.md')); const readFiles = []; for (const file of readMD) { const post = await readBlogPost( file.replace('.md', ''), condition, render, includeContent ); 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; } export async function readBlogPost( slug: string, condition?: (body: BlogPost) => boolean, render = true, includeContent = true ): 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 = render ? await marked(read.substring(headerLength), { async: true }) : undefined; const { year, month, day } = getDateObject(parsedHeader); const content = { ...parsedHeader, file, slug, fullSlug: `${year}/${month}/${day}/${slug}`, markdown: includeContent ? read.substring(headerLength) : undefined, html: renderedMd, }; if (condition) { if (!condition(content)) { return; } } return content; } export async function getTags(): Promise { const posts = await readBlogPosts(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 readBlogPosts(condition, false, false); const obj = {}; 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, }; }