import Handlebars from 'handlebars'; import { readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import { format } from 'prettier'; import { createScriptPath } from '../../utils/create-script-path'; import { dateDiffInDays, formatDate } from '../../utils/dates'; import { path2emoji } from '../../utils/path2emoji'; import { getFormattedNow, setLastBuild } from '../../utils/set-last-build'; import { mdToHtml } from './marked'; import { apiUrl } from 'web-shared/src/lib/api'; export interface KnownFrontMatter { updatedAt?: string; publishedAt?: string; attribution?: string; alt?: string; image?: string; summary?: string; title?: string; scripts?: string; hidden?: string; } export interface Post { file: { filePath: string; fileName: string; relativeFolder: string; }; emoji: string; frontMatter: KnownFrontMatter; mdContent: string; htmlContent: string; templatedContent: string; } interface TemplateValues { title: string; titleEncoded: string; hidden?: boolean; summary?: string; summaryEncoded: string; image?: string; content: string; lastBuildDate: string; publishDate?: string; createDate?: { daysAgo: number; string: string; }; updateDate?: { daysAgo: number; string: string; }; headScript?: string[]; stylesheet?: string[]; footerScript?: string[]; apiUrl: string; } const rawTemplate = await readFile(resolve('build', 'templates', 'post.hbs'), { encoding: 'utf-8', }); const template = Handlebars.compile(rawTemplate); export const renderTemplate = async (args: { content: string; title: string; hidden?: boolean; summary?: string; image?: string; scriptString?: string; dates: { published?: Date; updated?: Date; }; }): Promise<{ templated: string; html: string }> => { const { content, title, hidden, summary, image, scriptString, dates } = args; const { published, updated } = dates; const htmlContent = await mdToHtml(content); return { html: htmlContent, templated: await format( setLastBuild( template({ title: title, summary: summary, titleEncoded: encodeURIComponent(title), summaryEncoded: encodeURIComponent(summary || ' '), image: image, hidden: hidden, content: htmlContent, headScript: createScriptPaths(scriptString), stylesheet: [`/styles/style.css`], lastBuildDate: getFormattedNow(), publishDate: published ? formatDate(published) : undefined, updateDate: updated ? { daysAgo: dateDiffInDays(new Date(), updated), string: formatDate(updated), } : undefined, apiUrl: apiUrl, }), ), { parser: 'html', htmlWhitespaceSensitivity: 'strict', bracketSameLine: true, }, ), }; }; const createScriptPaths = (scriptsString?: string): string[] => { if (!scriptsString) { return []; } const split = scriptsString.split(' '); return split.map((path) => createScriptPath(path)); }; export const createPostObject = async ( path: string, cwd: string, ): Promise => { const content = await readFile(`${path}`, { encoding: 'utf-8', }); const split = content.split('---'); if (split.length < 3) { throw new Error('Missing metadata'); } const attributes = split[1].split('\n').reduce((acc, val) => { return { // Regex grabs the value on the line after the first occurrence of ":" [val.split(':')[0].trim()]: val.replace(/^.+?:/, '')?.trim(), ...acc, }; }, {} as KnownFrontMatter); const fileName = path.split(/([\\/])/g).pop()!; const { html, templated } = await renderTemplate({ content, title: attributes.title!, hidden: !!attributes.hidden, summary: attributes.summary, image: attributes.image, scriptString: attributes.scripts, dates: { published: attributes.publishedAt ? new Date(parseInt(attributes.publishedAt)) : undefined, updated: attributes.updatedAt ? new Date(parseInt(attributes.updatedAt)) : undefined, }, }); return { file: { filePath: path, fileName: fileName, relativeFolder: path.replace(`${cwd}/`, '').replace(fileName, ''), }, emoji: path2emoji(path), frontMatter: attributes, mdContent: content, htmlContent: html, templatedContent: templated, }; }; export const createPostsObject = ( paths: string[], cwd: string, ): Promise => Promise.all(paths.map((path) => createPostObject(path, cwd)));