diff --git a/content.odin b/content.odin new file mode 100644 index 0000000..c5f2796 --- /dev/null +++ b/content.odin @@ -0,0 +1,150 @@ +package main + +import "core:fmt" +import "core:os" +import "core:strings" + +Page_Type :: enum { + Post, + Standalone, +} + +Page :: struct { + type: Page_Type, + slug: string, + permalink: string, + title: string, + date: string, + draft: bool, + is_starred: bool, + menu: string, + body: string, + bundle_dir: string, +} + +// walk_content reads the content directory and returns all non-draft pages +// (or all pages if include_drafts is true). +walk_content :: proc(content_path: string, include_drafts: bool) -> []Page { + pages: [dynamic]Page + + collect_standalone(&pages, content_path) + + posts_path := fmt.tprintf("%s/posts", content_path) + if os.exists(posts_path) { + collect_posts(&pages, posts_path) + } + + if !include_drafts { + filtered: [dynamic]Page + for &page in pages { + if !page.draft { + append(&filtered, page) + } + } + delete(pages) + return filtered[:] + } + + return pages[:] +} + +collect_standalone :: proc(pages: ^[dynamic]Page, content_path: string) { + entries, err := os.read_all_directory_by_path(content_path, context.allocator) + if err != nil { + fmt.eprintfln("thor: cannot read %s: %v", content_path, err) + return + } + defer os.file_info_slice_delete(entries, context.allocator) + + for entry in entries { + if !strings.has_suffix(entry.name, ".md") { + continue + } + if entry.type != .Regular { + continue + } + + slug := entry.name[:len(entry.name) - 3] + page, ok := load_page(entry.fullpath, .Standalone, slug) + if ok { + append(pages, page) + } + } +} + +collect_posts :: proc(pages: ^[dynamic]Page, posts_path: string) { + entries, err := os.read_all_directory_by_path(posts_path, context.allocator) + if err != nil { + fmt.eprintfln("thor: cannot read %s: %v", posts_path, err) + return + } + defer os.file_info_slice_delete(entries, context.allocator) + + for entry in entries { + switch entry.type { + case .Regular: + if !strings.has_suffix(entry.name, ".md") { + continue + } + slug := entry.name[:len(entry.name) - 3] + page, ok := load_page(entry.fullpath, .Post, slug) + if ok { + append(pages, page) + } + case .Directory: + index_path := fmt.tprintf("%s/index.md", entry.fullpath) + if !os.exists(index_path) { + continue + } + page, ok := load_page(index_path, .Post, entry.name) + if ok { + page.bundle_dir = entry.fullpath + append(pages, page) + } + case .Undetermined, .Symlink, .Named_Pipe, .Socket, .Block_Device, .Character_Device: + // Do nothing + } + } +} + +load_page :: proc( + file_path: string, + page_type: Page_Type, + slug: string, +) -> ( + page: Page, + ok: bool, +) { + data, err := os.read_entire_file_from_path(file_path, context.allocator) + if err != nil { + fmt.eprintfln("thor: cannot read %s: %v", file_path, err) + return + } + + content := string(data) + fm, body, parsed := parse_frontmatter(content) + if !parsed { + fmt.eprintfln("thor: no frontmatter in %s", file_path) + 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) + + switch page_type { + case .Post: + page.permalink = fmt.aprintf("/posts/%s/", slug) + case .Standalone: + page.permalink = fmt.aprintf("/%s/", slug) + } + + ok = true + return +} + diff --git a/frontmatter.odin b/frontmatter.odin new file mode 100644 index 0000000..8b0c4a4 --- /dev/null +++ b/frontmatter.odin @@ -0,0 +1,75 @@ +package main + +import "core:encoding/json" +import "core:fmt" +import "core:strings" + +Frontmatter :: struct { + title: string, + date: string, + publishDate: string, + draft: bool, + isStarred: bool, + menu: string, +} + +// parse_frontmatter splits raw file content into a Frontmatter struct and the +// remaining markdown body. The frontmatter is a JSON object delimited by { } +// at the start of the file. +parse_frontmatter :: proc(content: string) -> (fm: Frontmatter, body: string, ok: bool) { + body = content + + if !strings.has_prefix(content, "{\n") { + return + } + end := strings.index(content, "\n}\n") + if end < 0 { + return + } else { + end += 2 + } + + json_str := content[:end + 1] + body = strings.trim_left(content[end + 1:], " \t\r\n") + + value, err := json.parse_string(json_str, spec = .JSON) + if err != nil { + fmt.eprintfln("thor: failed to parse frontmatter JSON: %v", err) + return + } + defer json.destroy_value(value) + + obj, ok2 := value.(json.Object) + if !ok2 { + return + } + + fm.title = json_get_string(obj, "title") + 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") + fm.menu = json_get_string(obj, "menu") + + ok = true + return +} + +json_get_string :: proc(obj: json.Object, key: string) -> string { + if v, ok := obj[key]; ok { + if s, ok2 := v.(json.String); ok2 { + return strings.clone(s) + } + } + return "" +} + +json_get_bool :: proc(obj: json.Object, key: string) -> bool { + if v, ok := obj[key]; ok { + if b, ok2 := v.(json.Boolean); ok2 { + return b + } + } + return false +} + diff --git a/main.odin b/main.odin index cbb5428..d9dbf31 100644 --- a/main.odin +++ b/main.odin @@ -1,8 +1,67 @@ package main +import "core:flags" import "core:fmt" +import "core:os" +Options :: struct { + base_url: string `args:"name=base-url"`, + content_dir: string `args:"name=content"`, + output_dir: string `args:"name=output"`, + drafts: bool `args:"name=drafts"`, +} + main :: proc() { - fmt.println("Hellope, World!") + opt: Options + + flags.parse_or_exit(&opt, os.args, .Odin) + + load_default_options(&opt) + + pages := walk_content(opt.content_dir, opt.drafts) + + print_summary(pages) +} + +load_default_options :: proc(opt: ^Options) { + if opt.content_dir == "" { + opt.content_dir = "./content" + } + + if opt.output_dir == "" { + opt.output_dir = "./public" + } +} + +print_summary :: proc(pages: []Page) { + fmt.printfln("Pages: %d\n", len(pages)) + + for page in pages { + type_label := "post" + if page.type == .Standalone { + type_label = "standalone" + } + + date_short := page.date + if len(date_short) > 10 { + date_short = date_short[:10] + } + + badge := "" + if page.draft { + badge = " (draft)" + } else if page.is_starred { + badge = " *" + } + + fmt.printfln( + " [%-11s] %-30s %s %s%s", + type_label, + page.title, + date_short, + page.permalink, + badge, + ) + } } diff --git a/site.odin b/site.odin new file mode 100644 index 0000000..48fb166 --- /dev/null +++ b/site.odin @@ -0,0 +1,13 @@ +package main + +Site :: struct { + base_url: string, + title: string, + socials: []Social_Icon, +} + +Social_Icon :: struct { + name: string, + url: string, +} +