2022-10-16 09:37:13 +00:00
|
|
|
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';
|
2022-10-16 11:04:03 +00:00
|
|
|
import { BlogPost, BlogPostTag } from '../types/post';
|
2022-10-16 09:37:13 +00:00
|
|
|
|
|
|
|
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>`;
|
|
|
|
},
|
2022-10-16 11:04:03 +00:00
|
|
|
link: (href: string, title: string, text: string): string => {
|
|
|
|
return `<a href="${href}" rel="nofollow" target="_blank"${
|
|
|
|
title ? ` title="${title}"` : ''
|
|
|
|
}>${text}</a>`;
|
|
|
|
},
|
2022-10-16 09:37:13 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
export async function readBlogPosts(
|
2022-10-16 11:04:03 +00:00
|
|
|
condition?: (body: BlogPost) => boolean,
|
2022-10-16 09:37:13 +00:00
|
|
|
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,
|
2022-10-16 11:04:03 +00:00
|
|
|
condition?: (body: BlogPost) => boolean,
|
2022-10-16 09:37:13 +00:00
|
|
|
render = true,
|
|
|
|
includeContent = true
|
2022-10-16 11:04:03 +00:00
|
|
|
): Promise<BlogPost[]> {
|
2022-10-16 09:37:13 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-10-16 11:04:03 +00:00
|
|
|
export async function getTags(): Promise<BlogPostTag[]> {
|
2022-10-16 09:37:13 +00:00
|
|
|
const posts = await readBlogPosts(undefined, false, false);
|
2022-10-16 11:04:03 +00:00
|
|
|
const obj: BlogPostTag[] = [];
|
2022-10-16 09:37:13 +00:00
|
|
|
|
|
|
|
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],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
|
2022-10-16 11:04:03 +00:00
|
|
|
export async function getArchiveTree(condition?: (body: BlogPost) => boolean) {
|
|
|
|
const posts = await readBlogPosts(condition, false, false);
|
2022-10-16 09:37:13 +00:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|