diff --git a/TODOS.md b/TODOS.md index ea75b9b..bfaeb74 100644 --- a/TODOS.md +++ b/TODOS.md @@ -3,3 +3,8 @@ - Look at the source for 3 template languages that support mustache syntax, and see how they implement the tempaltes, then pick the best one. One of them should be Hugo / go. +- [ ] Deal with the favicon +- [ ] Icon support. +- [ ] RSS +- [ ] sitemap +- [ ] robots.txt? diff --git a/content.odin b/content.odin index 4ee6bcf..782502c 100644 --- a/content.odin +++ b/content.odin @@ -7,22 +7,24 @@ import "core:os" import "core:strings" Page_Type :: enum { + Home, Post, Standalone, } Page :: struct { - type: Page_Type, - slug: string, - permalink: string, - title: string, - date: string, - draft: bool, - is_starred: bool, - menu: string, - body: string, - body_html: string, - bundle_dir: string, + type: Page_Type, + slug: string, + permalink: string, + title: string, + description: string, + date: string, + draft: bool, + is_starred: bool, + menu: string, + body: string, + body_html: string, + bundle_dir: string, } // walk_content reads the content directory and returns all non-draft pages @@ -30,6 +32,7 @@ Page :: struct { walk_content :: proc(content_path: string, include_drafts: bool) -> []Page { pages: [dynamic]Page + collect_home(&pages, content_path) collect_standalone(&pages, content_path) posts_path := fmt.tprintf("%s/posts", content_path) @@ -51,6 +54,27 @@ walk_content :: proc(content_path: string, include_drafts: bool) -> []Page { return pages[:] } +collect_home :: proc(pages: ^[dynamic]Page, content_path: string) { + html_path := fmt.tprintf("%s/index.html", content_path) + if os.exists(html_path) { + page, ok := load_page(html_path, .Home, "") + if ok { + page.permalink = "/" + append(pages, page) + } + return + } + + md_path := fmt.tprintf("%s/index.md", content_path) + if os.exists(md_path) { + page, ok := load_page(md_path, .Home, "") + if ok { + page.permalink = "/" + append(pages, page) + } + } +} + collect_standalone :: proc(pages: ^[dynamic]Page, content_path: string) { entries, err := os.read_all_directory_by_path(content_path, context.allocator) if err != nil { @@ -60,14 +84,17 @@ collect_standalone :: proc(pages: ^[dynamic]Page, content_path: string) { defer os.file_info_slice_delete(entries, context.allocator) for entry in entries { - if !strings.has_suffix(entry.name, ".md") { + if !is_content_file(entry.name) { + continue + } + if entry.name == "index.md" || entry.name == "index.html" { continue } if entry.type != .Regular { continue } - slug := entry.name[:len(entry.name) - 3] + slug := strip_extension(entry.name) page, ok := load_page(entry.fullpath, .Standalone, slug) if ok { append(pages, page) @@ -86,16 +113,19 @@ collect_posts :: proc(pages: ^[dynamic]Page, posts_path: string) { for entry in entries { switch entry.type { case .Regular: - if !strings.has_suffix(entry.name, ".md") { + if !is_content_file(entry.name) { continue } - slug := entry.name[:len(entry.name) - 3] + slug := strip_extension(entry.name) page, ok := load_page(entry.fullpath, .Post, slug) if ok { append(pages, page) } case .Directory: - index_path := fmt.tprintf("%s/index.md", entry.fullpath) + index_path := fmt.tprintf("%s/index.html", entry.fullpath) + if !os.exists(index_path) { + index_path = fmt.tprintf("%s/index.md", entry.fullpath) + } if !os.exists(index_path) { continue } @@ -105,7 +135,6 @@ collect_posts :: proc(pages: ^[dynamic]Page, posts_path: string) { append(pages, page) } case .Undetermined, .Symlink, .Named_Pipe, .Socket, .Block_Device, .Character_Device: - // Do nothing } } } @@ -131,17 +160,25 @@ load_page :: proc( return } - page.type = page_type - page.slug = slug - page.title = fm.title - page.date = fm.date - page.draft = fm.draft - page.is_starred = fm.isStarred - page.menu = fm.menu - page.body = strings.clone(body) - page.body_html = cm.markdown_to_html_from_string(body, {.Unsafe}) + page.type = page_type + page.slug = slug + page.title = fm.title + page.description = fm.description + page.date = fm.date + page.draft = fm.draft + page.is_starred = fm.isStarred + page.menu = fm.menu + page.body = strings.clone(body) + + if strings.has_suffix(file_path, ".html") { + page.body_html = strings.clone(body) + } else { + page.body_html = cm.markdown_to_html_from_string(body, {.Unsafe}) + } switch page_type { + case .Home: + page.permalink = "/" case .Post: page.permalink = fmt.aprintf("/posts/%s/", slug) case .Standalone: @@ -152,3 +189,37 @@ load_page :: proc( return } +is_content_file :: proc(name: string) -> bool { + return strings.has_suffix(name, ".md") || strings.has_suffix(name, ".html") +} + +strip_extension :: proc(name: string) -> string { + dot := strings.last_index(name, ".") + if dot < 0 { + return name + } + return name[:dot] +} + +// copy_static_assets copies non-content files from the content root to the output directory. +copy_static_assets :: proc(content_path: string, output_dir: string) { + entries, err := os.read_all_directory_by_path(content_path, context.allocator) + if err != nil { + return + } + defer os.file_info_slice_delete(entries, context.allocator) + + for entry in entries { + if entry.type != .Regular { + continue + } + if is_content_file(entry.name) { + continue + } + + dest := fmt.tprintf("%s/%s", output_dir, entry.name) + if err := os.copy_file(dest, entry.fullpath); err != nil { + fmt.eprintfln("thor: cannot copy %s: %v", entry.name, err) + } + } +} diff --git a/frontmatter.odin b/frontmatter.odin index 8b0c4a4..cb7e26f 100644 --- a/frontmatter.odin +++ b/frontmatter.odin @@ -6,6 +6,7 @@ import "core:strings" Frontmatter :: struct { title: string, + description: string, date: string, publishDate: string, draft: bool, @@ -44,8 +45,9 @@ parse_frontmatter :: proc(content: string) -> (fm: Frontmatter, body: string, ok return } - fm.title = json_get_string(obj, "title") - fm.date = json_get_string(obj, "date") + fm.title = json_get_string(obj, "title") + fm.description = json_get_string(obj, "description") + fm.date = json_get_string(obj, "date") fm.publishDate = json_get_string(obj, "publishDate") fm.draft = json_get_bool(obj, "draft") fm.isStarred = json_get_bool(obj, "isStarred") diff --git a/main.odin b/main.odin index 6a5e494..1508938 100644 --- a/main.odin +++ b/main.odin @@ -20,7 +20,7 @@ main :: proc() { pages := walk_content(opt.content_dir, opt.drafts) - render_site(pages, opt.output_dir) + render_site(pages, opt.content_dir, opt.output_dir) } load_default_options :: proc(opt: ^Options) { diff --git a/render.odin b/render.odin index ae90900..0036a6d 100644 --- a/render.odin +++ b/render.odin @@ -4,45 +4,58 @@ import "core:fmt" import "core:os" import "core:strings" -SITE_TITLE :: "One Idiot Developer" -SITE_DESC :: "This is fine...right?" - -@(rodata) MONTHS: [12]string = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", } -HEADER :: ` -
- -
-` - -render_site :: proc(pages: []Page, output_dir: string) { +render_site :: proc(pages: []Page, content_path: string, output_dir: 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 { - html := render_page_html(page) + if page.type == .Home { + home = page + has_home = true + site_title = page.title + break + } + } + + // Copy static assets (avatar, favicon, etc.) + copy_static_assets(content_path, output_dir) + + // 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) } - home_html := render_home_html(pages) - write_file(fmt.tprintf("%s/index.html", output_dir), home_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) + } - posts_html := render_posts_html(pages) + // Render posts list page + posts_html := render_posts_html(pages, site_title) write_page(output_dir, "/posts/", posts_html) - fmt.printfln("Rendered %d pages to %s", len(pages) + 2, 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) -> string { +render_page_html :: proc(page: Page, site_title: string) -> string { body := fmt.aprintf( `
@@ -55,15 +68,18 @@ render_page_html :: proc(page: Page) -> string { render_date(page), page.body_html, ) - title := fmt.tprintf("%s | %s", page.title, SITE_TITLE) + title := fmt.tprintf("%s | %s", page.title, site_title) return render_chrome(title, body) } -render_home_html :: proc(pages: []Page) -> string { +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)) } @@ -72,24 +88,20 @@ render_home_html :: proc(pages: []Page) -> string { body := fmt.aprintf( `
- -

%s

-

%s

-
+%s
    %s
`, - SITE_TITLE, - SITE_DESC, + home.body_html, post_list, ) - return render_chrome(SITE_TITLE, body) + return render_chrome(site_title, body) } -render_posts_html :: proc(pages: []Page) -> string { +render_posts_html :: proc(pages: []Page, site_title: string) -> string { parts: [dynamic]string defer delete(parts) @@ -123,7 +135,7 @@ render_posts_html :: proc(pages: []Page) -> string { append(&parts, "
\n") body := strings.join(parts[:], "") - title := fmt.tprintf("Posts | %s", SITE_TITLE) + title := fmt.tprintf("Posts | %s", site_title) return render_chrome(title, body) } @@ -153,6 +165,18 @@ render_chrome :: proc(page_title: string, body: string) -> string { ) } +HEADER :: ` +
+ +
+` + render_post_item :: proc(page: Page) -> string { date_html := "" if page.date != "" {