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 `
`;
},
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,
};
}