Files
thor/render.odin
T
2026-07-11 10:37:23 -04:00

283 lines
6.3 KiB
Odin

package main
import "core:fmt"
import "core:os"
import "core:strings"
MONTHS: [12]string = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
}
render_site :: proc(pages: []Page, content_path: string, output_dir: string, base_url: string) {
sort_pages_by_date(pages)
// Find home page and extract site title
home: Page
has_home := false
site_title := "Site"
for page in pages {
if page.type == .Home {
home = page
has_home = true
site_title = page.title
break
}
}
// Render individual content pages (skip home)
for page in pages {
if page.type == .Home {
continue
}
html := render_page_html(page, site_title)
write_page(output_dir, page.permalink, html)
}
// Render home page
if has_home {
home_html := render_home_html(home, pages, site_title)
write_file(fmt.tprintf("%s/index.html", output_dir), home_html)
}
// Render posts list page
posts_html := render_posts_html(pages, site_title)
write_page(output_dir, "/posts/", posts_html)
// Generate RSS feed
if has_home {
rss := generate_rss(pages, site_title, home.description, base_url)
write_file(fmt.tprintf("%s/index.xml", output_dir), rss)
}
// Generate sitemap
sitemap := generate_sitemap(pages, base_url)
write_file(fmt.tprintf("%s/sitemap.xml", output_dir), sitemap)
// Copy static assets (avatar, favicon, etc.)
copy_static_assets(content_path, output_dir)
total := len(pages) + 1
if !has_home {
total += 1
}
fmt.printfln("Rendered %d pages to %s", total, output_dir)
}
render_page_html :: proc(page: Page, site_title: string) -> string {
body := fmt.aprintf(
`<main>
<article class="prose">
<h1>%s</h1>
%s %s
</article>
</main>
`,
page.title,
render_date(page),
page.body_html,
)
title := fmt.tprintf("%s | %s", page.title, site_title)
return render_chrome(title, body)
}
render_home_html :: proc(home: Page, pages: []Page, site_title: string) -> string {
items: [dynamic]string
defer delete(items)
for page in pages {
if page.type == .Home {
continue
}
append(&items, render_post_item(page))
}
post_list := strings.join(items[:], "\n")
body := fmt.aprintf(
`<main>
<header class="text-center mb-28">
%s </header>
<ul>
%s
</ul>
</main>
`,
home.body_html,
post_list,
)
return render_chrome(site_title, body)
}
render_posts_html :: proc(pages: []Page, site_title: string) -> string {
parts: [dynamic]string
defer delete(parts)
append(&parts, "<main>\n")
append(&parts, ` <h1 class="text-center">Posts</h1>` + "\n")
current_year := ""
open := false
for page in pages {
if page.type != .Post {
continue
}
year := get_year(page.date)
if year != current_year {
if open {
append(&parts, " </ul>\n </section>\n")
}
open = true
current_year = year
append(
&parts,
fmt.aprintf(" <section>\n <h2>%s</h2>\n <hr class=\"text-slate-800 mb-1\">\n <ul>\n", year),
)
}
append(&parts, render_post_item(page))
append(&parts, "\n")
}
if open {
append(&parts, " </ul>\n </section>\n")
}
append(&parts, "</main>\n")
body := strings.join(parts[:], "")
title := fmt.tprintf("Posts | %s", site_title)
return render_chrome(title, body)
}
render_chrome :: proc(page_title: string, body: string) -> string {
header := fmt.aprintf(HEADER, ICON_HOME)
return fmt.aprintf(
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>%s</title>
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
<script src="/js/main.js" defer></script>
</head>
<body>
%s%s<footer>
<a class="goto-top opacity-0" href="#">%s</a>
<a href="https://github.com/sbrow" target="_blank" rel="noopener noreferrer me" title="Github">%s</a>
<a href="/index.xml" target="_blank" rel="noopener noreferrer me" title="Rss">%s</a>
<p class="pt-5 prose">Proudly built with <a href="https://odin-lang.org/">Odin</a> and <a href="https://tailwindcss.com/">Tailwindcss</a></p>
<p><small>&copy;</small> 2026</p>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</body>
</html>
`,
page_title,
header,
body,
ICON_CHEVRON_UP,
ICON_GITHUB,
ICON_RSS,
)
}
HEADER :: `
<header>
<nav>
<ul>
<li class="mr-auto"><a href="/">%s</a></li>
<li><a href="/ideas/">Ideas</a></li>
<li><a href="/posts/">Posts</a></li>
</ul>
</nav>
</header>
`
render_post_item :: proc(page: Page) -> string {
date_html := ""
if page.date != "" {
date_html = fmt.aprintf(
"<time datetime=\"%s\">%s</time>",
page.date,
format_date(page.date),
)
}
star := ""
if page.is_starred {
star = ICON_STAR
}
return fmt.aprintf(
` <li class="flex justify-between"><a href="%s">%s</a><span>%s%s</span></li>`,
page.permalink,
page.title,
star,
date_html,
)
}
render_date :: proc(page: Page) -> string {
if page.date == "" {
return ""
}
return fmt.aprintf(` <time datetime="%s">%s</time>` + "\n", page.date, format_date(page.date))
}
format_date :: proc(iso: string) -> string {
if len(iso) < 10 {
return iso
}
year := iso[:4]
month_num := (int(iso[5]) - 0x30) * 10 + (int(iso[6]) - 0x30)
day_num := (int(iso[8]) - 0x30) * 10 + (int(iso[9]) - 0x30)
if month_num < 1 || month_num > 12 {
return iso[:10]
}
return fmt.aprintf("%d %s %s", day_num, MONTHS[month_num - 1], year)
}
get_year :: proc(iso: string) -> string {
if len(iso) < 4 {
return ""
}
return iso[:4]
}
sort_pages_by_date :: proc(pages: []Page) {
for i in 1..<len(pages) {
key := pages[i]
j := i - 1
for j >= 0 && pages[j].date < key.date {
pages[j + 1] = pages[j]
j -= 1
}
pages[j + 1] = key
}
}
write_page :: proc(output_dir: string, permalink: string, html: string) {
rel := permalink
if len(rel) > 0 && rel[0] == '/' {
rel = rel[1:]
}
dir := fmt.tprintf("%s/%s", output_dir, rel)
if err := os.make_directory_all(dir); err != nil && err != .Exist {
fmt.eprintfln("thor: cannot create %s: %v", dir, err)
return
}
file_path := fmt.tprintf("%s/index.html", dir)
write_file(file_path, html)
}
write_file :: proc(path: string, html: string) {
if err := os.write_entire_file_from_string(path, html); err != nil {
fmt.eprintfln("thor: cannot write %s: %v", path, err)
}
}