diff options
Diffstat (limited to 'src/templates.ts')
-rw-r--r-- | src/templates.ts | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/src/templates.ts b/src/templates.ts new file mode 100644 index 0000000..50c45ab --- /dev/null +++ b/src/templates.ts @@ -0,0 +1,238 @@ +import * as fs from "node:fs/promises"; +import * as cheerio from "cheerio"; +import { matter } from "toml-matter"; +import { Marked } from "marked"; +import log from "loglevel"; + +import config from "./config"; +import { getPost, readPosts, type Post } from "./posts"; + +const marked = new Marked(); +marked.use({ + gfm: true, +}); + +function addMenu( + parent: cheerio.Cheerio<cheerio.AnyNode>, + child: cheerio.Cheerio<cheerio.AnyNode>, +) { + for (const link of config.extra.main_menu) { + parent.append(child.clone().attr("href", link.url).text(link.name)); + } +} + +export async function layout( + $: cheerio.CheerioAPI, + pageTitle: string, +): Promise<cheerio.CheerioAPI> { + $("html").attr("lang", config.default_language); + $("head > link[rel=alternate]").attr("title", config.title); + addMenu($("nav"), $("nav a")); + $(".title").text(config.title); + $("title").text(pageTitle); + $(".p-name").text(pageTitle); + $("head") + .children("style") + .text(await Bun.file("templates/style.css").text()); + return $; +} + +async function render404Page(): Promise<string> { + const $ = await layout( + cheerio.load(await Bun.file("templates/404.html").text()), + "404 Not Found", + ); + return $.html(); +} + +async function renderHomepage(posts: Array<Post>): Promise<string> { + const file = matter(await Bun.file("content/_index.md").text()); + const $ = await layout( + cheerio.load(await Bun.file("templates/index.html").text()), + config.title, + ); + $("#content").html(marked.parse(file.content)); + const $feed = $(".h-feed"); + const $tpl = $(".h-entry").remove(); + + for (const post of posts) { + const $post = $tpl.clone(); + $post.find(".p-name").text(post.title); + $post.find(".u-url").attr("href", post.url); + $post + .find(".dt-published") + .attr("datetime", post.date.toISOString()) + .text(post.date.toISOString().slice(0, 10)); + $post.appendTo($feed); + } + return $.html(); +} + +async function renderPost(file: Post, content: string) { + const $ = await layout( + cheerio.load(await Bun.file("templates/post.html").text()), + file.title, + ); + + $(".h-entry .dt-published") + .attr("datetime", file.date.toISOString()) + .text(file.date.toISOString().slice(0, 10)); + $(".h-entry .e-content").html(content); + const categories = $(".h-entry .p-categories"); + const cat = categories.find(".p-category").parentsUntil(categories); + cat.remove(); + for (const tag of file.taxonomies.tags) { + categories.append( + cat + .clone() + .find(".p-category") + .attr("href", `/tags/${tag}/`) + .text(`#${tag}`) + .parent(), + ); + } + + return $.html(); +} + +async function renderListPage(tag: string, posts: Post[]) { + const $ = await layout( + cheerio.load(await Bun.file("templates/tag.html").text()), + tag || config.title, + ); + const $feed = $(".h-feed"); + const $tpl = $(".h-entry").remove(); + + for (const post of posts) { + const $post = $tpl.clone(); + $post.find(".p-name").text(post.title); + $post.find(".u-url").attr("href", post.url); + $post + .find(".dt-published") + .attr("datetime", post.date.toISOString()) + .text(post.date.toISOString().slice(0, 10)); + $post.appendTo($feed); + } + return $.html(); +} + +async function renderTags(tags: string[]) { + const $ = await layout( + cheerio.load(await Bun.file("templates/tags.html").text()), + config.title, + ); + const $tags = $(".tags"); + const $tpl = $(".h-feed"); + $tpl.remove(); + for (const tag of tags) { + const $tag = $tpl.clone(); + $tag.find("a").attr("href", `/tags/${tag}/`).text(`#${tag}`); + $tag.appendTo($tags); + } + return $.html(); +} + +const makeTagURI = (specific: string) => + `tag:${config.original_domain},${config.domain_start_date}:${specific}`; + +async function renderFeed(title: string, posts: Post[], tag?: string) { + const $ = cheerio.load(await Bun.file("templates/feed.xml").text(), { + xml: true, + }); + const $feed = $("feed"); + $feed.children("title").text(title); + $feed.children("link").attr("href", config.base_url); + $feed.children("id").text(makeTagURI(tag || "feed")); + $feed.children("updated").text(posts[0].date.toISOString()); + + const $tpl = $("feed > entry").remove(); + for (const post of posts) { + const $post = $tpl.clone(); + $post.children("title").text(post.title); + $post + .children("link") + .attr("href", new URL(post.url, config.base_url).href); + $post.children("id").text(makeTagURI(post.basename)); + $post.children("updated").text(post.date.toISOString()); + $post.find("author > name").text(config.title); + $post.children("summary").text(post.description || ""); + const content = marked.parse((await getPost(post.input)).content); + $post.children("content").html(Bun.escapeHTML(await content)); + $post.appendTo($feed); + } + + return $.xml(); +} + +async function renderFeedStyles() { + const $ = cheerio.load(await Bun.file("templates/feed-styles.xsl").text(), { + xml: true, + }); + $("style").text(await Bun.file("templates/style.css").text()); + return $.xml(); +} + +export default async function generateSite() { + const tasks = []; + const { posts, tags } = await readPosts("content", "post", "public"); + await fs.mkdir("public/post", { recursive: true }); + for (const post of posts) { + const content = await marked.parse((await getPost(post.input)).content); + await fs.mkdir(`public/post/${post.basename}`, { recursive: true }); + tasks.push(async () => { + log.debug(`Rendering post ${post.basename} to ${post.output}`); + return Bun.write(post.output, await renderPost(post, content)); + }); + } + await fs.mkdir("public/tags", { recursive: true }); + tasks.push(async () => { + log.debug("Rendering tags page to public/tags/index.html"); + return Bun.write("public/tags/index.html", await renderTags([...tags])); + }); + for (const tag of tags) { + log.debug(`Processing tag ${tag}`); + const matchingPosts = posts.filter((p) => p.taxonomies.tags.includes(tag)); + await fs.mkdir(`public/tags/${tag}`, { recursive: true }); + tasks.push(async () => { + log.debug(`Rendering tag ${tag} to public/tags/${tag}/index.html`); + return Bun.write( + `public/tags/${tag}/index.html`, + await renderListPage(tag, matchingPosts), + ); + }); + + tasks.push(async () => { + log.debug(`Rendering tag ${tag} feed to public/tags/${tag}/atom.xml`); + return Bun.write( + `public/tags/${tag}/atom.xml`, + await renderFeed(`${config.title} - ${tag}`, matchingPosts), + ); + }); + } + tasks.push(async () => { + log.debug("Rendering posts page to public/post/index.html"); + return Bun.write("public/post/index.html", await renderListPage("", posts)); + }); + tasks.push(async () => { + log.debug("Rendering site feed to public/atom.xml"); + return Bun.write("public/atom.xml", await renderFeed(config.title, posts)); + }); + tasks.push(async () => { + log.debug("Rendering feed styles to public/feed-styles.xsl"); + return Bun.write("public/feed-styles.xsl", await renderFeedStyles()); + }); + tasks.push(async () => { + log.debug("Rendering homepage to public/index.html"); + return Bun.write( + "public/index.html", + await renderHomepage(posts.slice(0, 3)), + ); + }); + tasks.push(async () => { + log.debug("Rendering 404 page to public/404.html"); + return Bun.write("public/404.html", await render404Page()); + }); + for (const task of tasks) { + await task(); + } +} |