package main import "mustache" import "core:encoding/json" import "core:fmt" import "core:log" import "core:os" import "core:strings" import "core:time" import "core:time/datetime" Template_Context :: struct { params: json.Value, now: string, content: string, title: string, description: string, og: Open_Graph, date_format: string, timezone: ^datetime.TZ_Region, page: Page, // Home data pages: [dynamic]Page, // Section Data // TODO: Remove "posts" from the Odin code posts: [dynamic]Page, } load_template :: proc(vfs: ^VFS, virtual_path: string) -> mustache.Template { entry, data, ok := vfs_get_entry(vfs, virtual_path) if !ok { log.fatalf("template %s not found", virtual_path) os.exit(1) } source := string(data) tpl, err := mustache.parse(source, entry.fs_path) if err != nil { b := mustache.body(err) log.errorf( "%s", mustache.format_error( entry.fs_path, source, b.pos, b.msg, colorize = mustache.should_colorize(), ), ) os.exit(1) } return tpl } get_template :: proc( vfs: ^VFS, layout: string, cache: ^map[string]mustache.Template, ) -> mustache.Template { chain: [4]string n := 0 chain[n] = layout; n += 1 if strings.has_suffix(layout, "_index") && layout != "section_index" { chain[n] = "section_index"; n += 1 } if layout != "page" && layout != "base" { chain[n] = "page"; n += 1 } if layout != "base" { chain[n] = "base"; n += 1 } for i in 0 ..< n { candidate := chain[i] if cached, ok := cache[candidate]; ok { return cached } virtual := fmt.tprintf("layouts/%s.html", candidate) if _, ok := vfs_get(vfs, virtual); ok { tpl := load_template(vfs, virtual) cache[candidate] = tpl return tpl } if candidate != chain[n - 1] { log.debugf("template %s not found, falling back", virtual) } } log.errorf("base.html not found in VFS") return mustache.Template{} } capitalize :: proc(s: string) -> string { if len(s) == 0 { return s } if s[0] >= 'a' && s[0] <= 'z' { return fmt.aprintf("%c%s", s[0] - 32, s[1:]) } return s } render_template :: proc( content_tpl: mustache.Template, data: Template_Context, partials: map[string]mustache.Template, ) -> string { result, err := mustache.render(content_tpl, data, partials) if err != nil { log.errorf( "%s", mustache.format_render_error(err, content_tpl, colorize = mustache.should_colorize()), ) return "" } return result } render_site :: proc(site: ^Site) { allocator := site_allocator(site) pages := site.pages[:] sort_pages_by_date(pages) // Load shared resources partials := load_partials(&site.vfs) partials["base"] = load_template(&site.vfs, "layouts/base.html") template_cache: map[string]mustache.Template defer delete(template_cache) offset, ok := mustache.compute_utc_offset(site.tz) assert(ok) now, ok2 := time.time_to_rfc3339(time.now(), offset, false, allocator) assert(ok2) ctx := Template_Context { now = now, params = site.params, description = site.description, og = site.og, date_format = site.date.format, timezone = site.tz, } // Find home page home: Page has_home := false for page in pages { if page.section == "" && page._is_index { home = page has_home = true break } } // Collect sections sections := make(map[string]bool) defer delete(sections) for page in pages { if page.section != "" { sections[page.section] = true } } // Render individual content pages (skip all index pages) for page in pages { if page._is_index { continue } tpl := get_template(&site.vfs, page.layout, &template_cache) html := render_page_html(page, site, tpl, partials, ctx) if .Minify in site.features { html = minify_html(html) } write_page(site.output_dir, page.permalink, html) } // Render section index pages for section in sections { section_index: Page has_section_index := false for page in pages { if page.section == section && page._is_index { section_index = page has_section_index = true break } } layout := fmt.tprintf("%s_index", section) section_tpl := get_template(&site.vfs, layout, &template_cache) html := render_section( site, section, section_index, has_section_index, section_tpl, partials, ctx, ) if .Minify in site.features { html = minify_html(html) } write_page(site.output_dir, fmt.aprintf("/%s/", section), html) } // Render home page if has_home { home_tpl := get_template(&site.vfs, "home", &template_cache) home_html := render_home_html(home, site, home_tpl, partials, ctx) if .Minify in site.features { home_html = minify_html(home_html) } write_file(fmt.tprintf("%s/index.html", site.output_dir), home_html) } // Generate RSS feed rss := generate_rss(site) write_file(fmt.tprintf("%s/index.xml", site.output_dir), rss) // Generate sitemap sitemap := generate_sitemap(site) write_file(fmt.tprintf("%s/sitemap.xml", site.output_dir), sitemap) // Copy and optionally minify assets directory copy_assets_dir(&site.vfs, site.output_dir, site.features) // Generate robots.txt robots := fmt.aprintf("User-agent: *\nAllow: /\nSitemap: %s/sitemap.xml\n", site.base_url) write_file(fmt.tprintf("%s/robots.txt", site.output_dir), robots) total := len(pages) + len(sections) if !has_home { total += 1 } log.infof("Rendered %d pages to %s", total, site.output_dir) } render_page_html :: proc( page: Page, site: ^Site, content_tpl: mustache.Template, partials: map[string]mustache.Template, ctx: Template_Context, ) -> string { ctx := ctx ctx.title = fmt.tprintf("%s | %s", page.title, site.title) ctx.page = page ctx.content = page.content ctx.og = og_for_page(site.og, page) return render_template(content_tpl, ctx, partials) } render_home_html :: proc( home: Page, site: ^Site, content_tpl: mustache.Template, partials: map[string]mustache.Template, ctx: Template_Context, ) -> string { list_pages := make([dynamic]Page, 0, 8, context.temp_allocator) for page in site.pages { if page._is_index { continue } append(&list_pages, page) } ctx := ctx ctx.title = site.title ctx.content = home.content ctx.pages = list_pages ctx.og = og_for_page(site.og, home) return render_template(content_tpl, ctx, partials) } render_section :: proc( site: ^Site, section: string, section_index: Page, has_index: bool, content_tpl: mustache.Template, partials: map[string]mustache.Template, ctx: Template_Context, ) -> string { posts := make([dynamic]Page, 0, len(site.pages) / 2, context.temp_allocator) for page in site.pages { if page.section != section || page._is_index { continue } append(&posts, page) } ctx := ctx if has_index { ctx.content = section_index.content ctx.page = section_index ctx.title = fmt.tprintf("%s | %s", section_index.title, site.title) ctx.og = og_for_page(site.og, section_index) } else { ctx.page = Page { title = capitalize(section), } ctx.title = fmt.tprintf("%s | %s", ctx.page.title, site.title) ctx.og.title = capitalize(section) ctx.og.description = "" ctx.og.url = fmt.tprintf("%s/%s/", site.base_url, section) ctx.og.type = "website" ctx.og.is_article = false } ctx.posts = posts return render_template(content_tpl, ctx, partials) } load_partials :: proc(vfs: ^VFS) -> map[string]mustache.Template { partials: map[string]mustache.Template prefix := "layouts/partials/" for virtual_path, entry in vfs.files { if !strings.has_prefix(virtual_path, prefix) { continue } if !strings.has_suffix(virtual_path, ".html") { continue } stripped := virtual_path[len(prefix):] key := stripped[:len(stripped) - len(".html")] data := vfs_entry_data(entry) or_continue source := string(data) tpl, err := mustache.parse(source, entry.fs_path) if err != nil { b := mustache.body(err) log.errorf( "%s", mustache.format_error( entry.fs_path, source, b.pos, b.msg, colorize = mustache.should_colorize(), ), ) os.exit(1) } partials[key] = tpl } return partials } get_year :: proc(iso: string) -> string { if len(iso) < 4 { return "" } return iso[:4] } sort_pages_by_date :: proc(pages: #soa[]Page) { for i in 1 ..< len(pages) { key := pages[i] j := i - 1 for j >= 0 && pages.date[j] < 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 { log.errorf("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 { log.errorf("cannot write %s: %v", path, err) } }