212 lines
5.4 KiB
TypeScript
212 lines
5.4 KiB
TypeScript
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 `<div class="codeblock"><pre><code class="hljs language-${language}">${created}</code></pre></div>`;
|
|
},
|
|
link: (href: string, title: string, text: string): string => {
|
|
return `<a href="${href}" rel="nofollow" target="_blank"${
|
|
title ? ` title="${title}"` : ''
|
|
}>${text}</a>`;
|
|
},
|
|
},
|
|
});
|
|
|
|
async function readBlogPosts() {
|
|
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', ''));
|
|
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<BlogPost[]> {
|
|
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('<!-- more -->')
|
|
|
|
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('<!-- more -->', ''),
|
|
};
|
|
|
|
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<BlogPost[]> => {
|
|
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<BlogPost> => {
|
|
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<BlogPostTag[]> {
|
|
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 = {};
|
|
|
|
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,
|
|
};
|
|
}
|