lunasqu.ee-nuxt/lib/blog/read-posts.ts
2024-11-11 18:00:38 +02:00

211 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 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 `<div class="codeblock"><pre><code class="hljs language-${language}">${created}</code></pre></div>`;
},
link: ({ href, title, text }): string =>
`<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: 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<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 | undefined> => {
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: 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,
};
}