mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
feat: Site pages get listed.
This commit is contained in:
+150
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,8 +1,67 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
import "core:flags"
|
||||||
import "core:fmt"
|
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() {
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user