588 lines
14 KiB
Odin
588 lines
14 KiB
Odin
package www
|
|
|
|
import "core:sort"
|
|
import "base:runtime"
|
|
import "core:bufio"
|
|
import "core:c"
|
|
import "core:encoding/json"
|
|
import "core:fmt"
|
|
import "core:os"
|
|
import "core:strings"
|
|
import md "vendor:commonmark"
|
|
|
|
SITE_NAME :: "PaulW.XYZ"
|
|
|
|
COMMON_STYLESHEETS :: []string {
|
|
"/assets/global.css",
|
|
"https://cdn.jsdelivr.net/npm/prismjs@1.30.0/themes/prism-tomorrow.min.css",
|
|
}
|
|
COMMON_SCRIPTS :: []string {
|
|
"https://cdn.jsdelivr.net/npm/prismjs@1.30.0/components/prism-core.min.js",
|
|
"https://cdn.jsdelivr.net/npm/prismjs@1.30.0/plugins/autoloader/prism-autoloader.min.js",
|
|
}
|
|
|
|
OUTPUT_ROOT_NAME :: "build"
|
|
|
|
markdown_paths :: []string{"notes", "posts"}
|
|
|
|
main :: proc() {
|
|
create_output_directories(markdown_paths)
|
|
copy_public_assets()
|
|
|
|
home_page := generate_sitemap()
|
|
dump_page(home_page)
|
|
|
|
sb := strings.builder_make()
|
|
|
|
process_markdown(&sb, home_page)
|
|
generate_home(&sb, home_page)
|
|
}
|
|
|
|
Page :: struct {
|
|
title: string,
|
|
pages: map[string]Page,
|
|
}
|
|
|
|
add_page :: proc(parent: ^Page, key: string, value: Page) {
|
|
parent.pages[key] = value
|
|
}
|
|
|
|
dump_page :: proc(page: Page, indent_level := 0) {
|
|
for i in 0 ..< indent_level {
|
|
fmt.print("\t")
|
|
}
|
|
fmt.println(page.title)
|
|
for name, page in page.pages {
|
|
for i in 0 ..= indent_level {
|
|
fmt.print("\t")
|
|
}
|
|
fmt.println("/", name)
|
|
dump_page(page, indent_level + 1)
|
|
}
|
|
}
|
|
|
|
generate_sitemap :: proc() -> Page {
|
|
home_page: Page = {
|
|
title = SITE_NAME,
|
|
pages = make(map[string]Page),
|
|
}
|
|
|
|
process_markdown_parent(&home_page, "notes", "Notes")
|
|
process_markdown_parent(&home_page, "posts", "Posts")
|
|
|
|
return home_page
|
|
}
|
|
|
|
process_markdown_parent :: proc(parent: ^Page, dir: string, title: string) {
|
|
page := Page {
|
|
title = title,
|
|
pages = make(map[string]Page),
|
|
}
|
|
|
|
files, os_err := os.read_directory_by_path(dir, 0, context.temp_allocator)
|
|
if os_err != nil {
|
|
fmt.eprintfln("failed to read files from directory '%s' (%s)", dir, os_err)
|
|
return
|
|
}
|
|
for file_info in files {
|
|
if len(file_info.name) < 1 || file_info.name[0] == '.' do continue
|
|
|
|
file, os_err := os.open(file_info.fullpath, {.Read})
|
|
if os_err != nil {
|
|
panic("something funny happened lol; TODO of course!!!")
|
|
}
|
|
defer os.close(file)
|
|
|
|
reader: bufio.Reader
|
|
bufio.reader_init(&reader, os.to_reader(file))
|
|
defer bufio.reader_destroy(&reader)
|
|
|
|
line := bufio.reader_read_string(&reader, '\n', context.temp_allocator) or_continue
|
|
trimmed := strings.trim(line, "\n\r \t")
|
|
|
|
if len(trimmed) < 5 || trimmed[0] != '#' || trimmed[1] != ' ' do continue
|
|
|
|
title := strings.substring_from(trimmed, 2) or_continue
|
|
|
|
ext := os.ext(file_info.name)
|
|
if ext != ".md" do continue
|
|
output_name := strings.trim_suffix(file_info.name, ext)
|
|
|
|
md_page := Page {
|
|
title = strings.clone(title),
|
|
}
|
|
add_page(&page, output_name, md_page)
|
|
|
|
}
|
|
add_page(parent, dir, page)
|
|
}
|
|
|
|
copy_public_assets :: proc() {
|
|
err := os.copy_directory_all(OUTPUT_ROOT_NAME, "public")
|
|
if err != nil {
|
|
fmt.eprintln("error copying public assets", err)
|
|
}
|
|
}
|
|
|
|
create_output_directories :: proc(dir_names: []string) {
|
|
os_err := os.make_directory(OUTPUT_ROOT_NAME)
|
|
if os_err != nil && os_err != .Exist {
|
|
fmt.eprintfln("cannot create output root directory '%s': %s", OUTPUT_ROOT_NAME, os_err)
|
|
return
|
|
}
|
|
|
|
for dir_name in dir_names {
|
|
dir_path, alloc_err := os.join_path({OUTPUT_ROOT_NAME, dir_name}, context.temp_allocator)
|
|
if alloc_err != nil {
|
|
fmt.eprintln("allocator error:", alloc_err)
|
|
continue
|
|
}
|
|
|
|
os_err = os.make_directory(dir_path)
|
|
if os_err != nil && os_err != .Exist {
|
|
fmt.eprintln("failed to create directory:", dir_path)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
remove_title :: proc(root: ^md.Node) {
|
|
heading_state: enum {
|
|
None,
|
|
Entered,
|
|
//Copied,
|
|
Exited,
|
|
} = .None
|
|
//title: string
|
|
iter := md.iter_new(root)
|
|
defer md.iter_free(iter)
|
|
for md.iter_next(iter) != .Done {
|
|
event_type := md.iter_get_event_type(iter)
|
|
node := md.iter_get_node(iter)
|
|
|
|
#partial switch node.type {
|
|
case .Heading:
|
|
level := md.node_get_heading_level(node)
|
|
if level == 1 && heading_state != .Exited {
|
|
if event_type == .Enter {
|
|
heading_state = .Entered
|
|
} else if event_type == .Exit {
|
|
heading_state = .Exited
|
|
md.node_unlink(node)
|
|
md.node_free(node)
|
|
}
|
|
}
|
|
// case .Text:
|
|
// if heading_state == .Entered {
|
|
// heading_state = .Copied
|
|
// title = strings.clone_from_cstring(md.node_get_literal(node))
|
|
// }
|
|
}
|
|
}
|
|
//return title
|
|
}
|
|
|
|
// must free
|
|
parse_json :: proc(file_path: string) -> json.Value {
|
|
json_file_content, os_err := os.read_entire_file_from_path(file_path, context.temp_allocator)
|
|
if os_err != nil {
|
|
fmt.eprintfln("failed to read files from directory '%s' (%s)", file_path, os_err)
|
|
return nil
|
|
}
|
|
json_value, json_err := json.parse(
|
|
json_file_content,
|
|
json.DEFAULT_SPECIFICATION,
|
|
true,
|
|
context.allocator,
|
|
)
|
|
if json_err != .None {
|
|
fmt.eprintfln("error parsing json file '%s' (%s)", file_path, json_err)
|
|
return nil
|
|
}
|
|
return json_value
|
|
}
|
|
|
|
generate_home :: proc(sb: ^strings.Builder, site: Page) {
|
|
strings.builder_reset(sb)
|
|
EXTERNAL_JSON_FILE :: "external.json"
|
|
|
|
external_json := parse_json(EXTERNAL_JSON_FILE)
|
|
defer json.destroy_value(external_json)
|
|
|
|
output_file_path, alloc_err := os.join_path(
|
|
{OUTPUT_ROOT_NAME, "index.html"},
|
|
context.temp_allocator,
|
|
)
|
|
if alloc_err != nil {
|
|
fmt.eprintln("allocator error:", alloc_err)
|
|
return
|
|
}
|
|
|
|
output_file, os_err2 := os.open(output_file_path, {.Write, .Create, .Trunc})
|
|
if os_err2 != nil {
|
|
fmt.eprintfln("failed to open output file '%s' (%s)", output_file_path, os_err2)
|
|
return
|
|
}
|
|
defer os.close(output_file)
|
|
|
|
{
|
|
html_begin(sb)
|
|
defer html_end(sb, {})
|
|
|
|
heading_title(sb, SITE_NAME)
|
|
|
|
container_begin(sb)
|
|
defer container_end(sb)
|
|
|
|
{
|
|
block_begin(sb)
|
|
defer block_end(sb)
|
|
|
|
obj := external_json.(json.Object)
|
|
for text, href in obj {
|
|
fmt.sbprintf(
|
|
sb,
|
|
"<a class='button blue link extern' href='%s'>%s</a>",
|
|
href.(json.String),
|
|
text,
|
|
)
|
|
}
|
|
}
|
|
|
|
{
|
|
block_begin(sb)
|
|
defer block_end(sb)
|
|
|
|
fmt.sbprintf(sb, "<h2>%s</h2>", "Navigation")
|
|
|
|
for name, page in site.pages {
|
|
fmt.sbprintf(sb, "<a class='button green link' href='%s'>%s</a>", name, page.title)
|
|
}
|
|
}
|
|
}
|
|
|
|
os.write_string(output_file, strings.to_string(sb^))
|
|
}
|
|
|
|
set_pages_to_process :: proc(page: Page, pages: ^map[string]^Page, parent_path := "") {
|
|
for name, &page in page.pages {
|
|
current_path := strings.trim(fmt.tprintf("%s/%s", parent_path, name), "/")
|
|
for path in markdown_paths {
|
|
if strings.trim(path, "/") == current_path do map_insert(pages, path, &page)
|
|
}
|
|
set_pages_to_process(page, pages, current_path)
|
|
}
|
|
}
|
|
|
|
process_markdown :: proc(sb: ^strings.Builder, site: Page) {
|
|
pages_to_process := make(map[string]^Page)
|
|
defer delete(pages_to_process)
|
|
set_pages_to_process(site, &pages_to_process)
|
|
|
|
for markdown_dir, page in pages_to_process {
|
|
markdown_dir_toc := make([dynamic][3]string) // path, title, date
|
|
defer delete(markdown_dir_toc)
|
|
defer create_toc_page(page.title, markdown_dir, &markdown_dir_toc, site)
|
|
|
|
for md_name, md_page in page.pages {
|
|
current_path := fmt.tprintf("/%s/%s", markdown_dir, md_name)
|
|
|
|
input_file_name := fmt.tprint(md_name, ".md", sep = "")
|
|
input_file_path, alloc_err := os.join_path(
|
|
{markdown_dir, input_file_name},
|
|
context.temp_allocator,
|
|
)
|
|
|
|
git_state, git_out, git_err, os_err := os.process_exec(
|
|
os.Process_Desc {
|
|
command = {
|
|
"git",
|
|
"log",
|
|
"-1",
|
|
"--date=format-local:%Y-%m-%dT%H:%M:%SZ",
|
|
"--format=%ad",
|
|
"--",
|
|
input_file_path,
|
|
},
|
|
env = {"TZ=UTC"},
|
|
},
|
|
context.temp_allocator,
|
|
)
|
|
if os_err != nil {
|
|
fmt.eprintln("failed to start git process", os_err, git_err)
|
|
return
|
|
}
|
|
date := strings.trim(string(git_out), " \t\n\r")
|
|
|
|
// @leak
|
|
append(&markdown_dir_toc, [3]string{strings.clone(current_path), md_page.title, date})
|
|
|
|
file, os_err1 := os.read_entire_file(input_file_path, context.temp_allocator)
|
|
|
|
strings.builder_reset(sb)
|
|
cmark_root := md.parse_document(
|
|
cast([^]u8)raw_data(file),
|
|
len(file),
|
|
{.Validate_UTF8, .Unsafe},
|
|
)
|
|
remove_title(cmark_root)
|
|
html := md.render_html(cmark_root, {.Unsafe})
|
|
|
|
output_file_name := fmt.tprint(md_name, ".html", sep = "")
|
|
output_file_path, alloc_err2 := os.join_path(
|
|
{OUTPUT_ROOT_NAME, markdown_dir, output_file_name},
|
|
context.temp_allocator,
|
|
)
|
|
|
|
out_file, os_err2 := os.open(output_file_path, {.Write, .Create, .Trunc})
|
|
if os_err2 != nil {
|
|
fmt.eprintfln("failed to open output file '%s' (%s)", output_file_path, os_err2)
|
|
continue
|
|
}
|
|
defer os.close(out_file)
|
|
|
|
{
|
|
html_begin(sb, md_page.title)
|
|
defer html_end(sb, site, current_path)
|
|
|
|
heading_title(sb, md_page.title)
|
|
|
|
|
|
container_begin(sb)
|
|
defer container_end(sb)
|
|
|
|
last_updated_date(sb, date)
|
|
|
|
block_begin(sb)
|
|
defer block_end(sb)
|
|
|
|
fmt.sbprint(sb, html)
|
|
}
|
|
|
|
write_len: int
|
|
write_len, os_err = os.write_strings(out_file, strings.to_string(sb^))
|
|
if os_err != nil {
|
|
fmt.eprintfln("failed to write to output file '%s' (%s)", output_file_path, os_err)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
create_toc_page :: proc(title, toc_path: string, toc_info: ^[dynamic][3]string, site: Page) {
|
|
_sb: strings.Builder = strings.builder_make()
|
|
sb := &_sb
|
|
defer strings.builder_destroy(sb)
|
|
toc_file_path, alloc_err := os.join_path(
|
|
{OUTPUT_ROOT_NAME, toc_path, "index.html"},
|
|
context.temp_allocator,
|
|
)
|
|
if alloc_err != nil {
|
|
fmt.eprintln("allocator error:", alloc_err)
|
|
return
|
|
}
|
|
|
|
sort_toc :: proc(a: [3]string, b: [3]string) -> int {
|
|
return -strings.compare(a[2], b[2])
|
|
}
|
|
|
|
sort.quick_sort_proc(toc_info[:], sort_toc)
|
|
|
|
fmt.println(toc_file_path)
|
|
|
|
{
|
|
html_begin(sb, title)
|
|
defer html_end(sb, site, toc_path)
|
|
|
|
heading_title(sb, title)
|
|
|
|
container_begin(sb)
|
|
defer container_end(sb)
|
|
|
|
block_begin(sb)
|
|
defer block_end(sb)
|
|
|
|
if len(toc_info) > 0 {
|
|
table_begin(sb)
|
|
defer table_end(sb)
|
|
|
|
for item in toc_info {
|
|
tr_begin(sb)
|
|
fmt.sbprintf(
|
|
sb,
|
|
"<td><a href='%s.html'>%s</a></td><td><span class='table-last-updated' data-value='%s'>aarfsdf</span></td>",
|
|
item[0],
|
|
item[1],
|
|
item[2],
|
|
)
|
|
tr_end(sb)
|
|
}
|
|
|
|
fmt.sbprint(sb,
|
|
`<style>
|
|
.table-last-updated {font-style: italic;margin-right:0}
|
|
</style>
|
|
<script>
|
|
document.querySelectorAll('.table-last-updated')
|
|
.forEach(i => {
|
|
const date = new Date(i.attributes['data-value'].value);
|
|
i.innerText = new Intl.DateTimeFormat(undefined, { dateStyle: 'long', timeStyle: 'short'}).format(date)
|
|
})
|
|
</script>`
|
|
)
|
|
} else {
|
|
fmt.sbprint(
|
|
sb,
|
|
"<div class='text center'>",
|
|
"<p>**crickets**</p>",
|
|
"<p>Nothing to list...</p>",
|
|
"<a class='link button green back' href='/'>Go Home</a>",
|
|
"</div>",
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
out_file, os_err2 := os.open(toc_file_path, {.Write, .Create, .Trunc})
|
|
if os_err2 != nil {
|
|
fmt.eprintfln("failed to open output file '%s' (%s)", toc_file_path, os_err2)
|
|
return
|
|
}
|
|
defer os.close(out_file)
|
|
write_len, os_err := os.write_strings(out_file, strings.to_string(_sb))
|
|
if os_err != nil {
|
|
fmt.eprintfln("failed to write to output file '%s' (%s)", toc_file_path, os_err)
|
|
return
|
|
}
|
|
}
|
|
|
|
last_updated_date :: proc(sb: ^strings.Builder, date: string) {
|
|
fmt.sbprint(sb, "<span class='last-updated'></span>")
|
|
fmt.sbprint(
|
|
sb,
|
|
"<script>",
|
|
"const date = new Date('",
|
|
date,
|
|
"');",
|
|
"document.querySelector('.last-updated').innerText = 'Last updated: ' + new Intl.DateTimeFormat(undefined, { dateStyle: 'long', timeStyle: 'short'}).format(date)",
|
|
"</script>",
|
|
sep = "",
|
|
)
|
|
|
|
|
|
}
|
|
|
|
nav :: proc(sb: ^strings.Builder, current_path: string, site: Page) {
|
|
fmt.sbprint(sb, "<nav>")
|
|
current_page := site
|
|
if current_path == "/" {
|
|
fmt.sbprint(sb, SITE_NAME)
|
|
} else {
|
|
fmt.sbprintf(sb, "<a href='%s'>%s</a>", "/", SITE_NAME)
|
|
res, ok := strings.split(current_path, "/", context.temp_allocator)
|
|
|
|
temp_sb := strings.builder_make(context.temp_allocator)
|
|
for i in 1 ..< (len(res) - 1) {
|
|
current_page = current_page.pages[res[i]]
|
|
fmt.sbprintf(&temp_sb, "/%s", res[i])
|
|
fmt.sbprintf(
|
|
sb,
|
|
" / <a href='%s'>%s</a>",
|
|
strings.to_string(temp_sb),
|
|
current_page.title,
|
|
)
|
|
}
|
|
|
|
if len(res) > 0 {
|
|
fmt.sbprint(sb, " /", current_page.pages[res[len(res) - 1]].title)
|
|
}
|
|
}
|
|
|
|
fmt.sbprint(sb, "</nav>")
|
|
}
|
|
|
|
has_called_html_begin := 0
|
|
html_begin :: proc(sb: ^strings.Builder, title: string = "") {
|
|
assert(has_called_html_begin >= 0)
|
|
has_called_html_begin += 1
|
|
fmt.sbprint(sb, "<!DOCTYPE html>")
|
|
fmt.sbprint(sb, "<html lang='en-US'>")
|
|
fmt.sbprint(sb, "<head>")
|
|
fmt.sbprint(sb, "<meta charset='utf-8'>")
|
|
if title == "" do fmt.sbprintf(sb, "<title>%s</title>", SITE_NAME)
|
|
else do fmt.sbprintf(sb, "<title>%s | %s</title>", title, SITE_NAME)
|
|
for s in COMMON_STYLESHEETS {
|
|
fmt.sbprintf(sb, "<link rel='stylesheet' href='%s' />", s)
|
|
}
|
|
for s in COMMON_SCRIPTS {
|
|
fmt.sbprintf(sb, "<script type='text/javascript' src='%s'></script>", s)
|
|
}
|
|
fmt.sbprint(sb, "</head>")
|
|
fmt.sbprint(sb, "<body>")
|
|
}
|
|
|
|
html_end :: proc(sb: ^strings.Builder, site: Page, current_path := "/") {
|
|
assert(has_called_html_begin > 0)
|
|
has_called_html_begin -= 1
|
|
nav(sb, current_path, site)
|
|
fmt.sbprint(sb, "</body>")
|
|
fmt.sbprint(sb, "</html>")
|
|
}
|
|
|
|
has_called_container_begin := 0
|
|
container_begin :: proc(sb: ^strings.Builder, element := "main") {
|
|
assert(has_called_container_begin >= 0)
|
|
has_called_container_begin += 1
|
|
fmt.sbprintf(sb, "<%s class='container'>", element)
|
|
}
|
|
|
|
container_end :: proc(sb: ^strings.Builder, element := "main") {
|
|
assert(has_called_container_begin > 0)
|
|
has_called_container_begin -= 1
|
|
fmt.sbprintf(sb, "</%s>", element)
|
|
}
|
|
|
|
has_called_block_begin := 0
|
|
block_begin :: proc(sb: ^strings.Builder, element := "section") {
|
|
assert(has_called_block_begin >= 0)
|
|
has_called_block_begin += 1
|
|
fmt.sbprintf(sb, "<%s class='block'>", element)
|
|
}
|
|
|
|
block_end :: proc(sb: ^strings.Builder, element := "section") {
|
|
assert(has_called_block_begin > 0)
|
|
has_called_block_begin -= 1
|
|
fmt.sbprintf(sb, "</%s>", element)
|
|
}
|
|
|
|
heading_title :: proc(sb: ^strings.Builder, title: string) {
|
|
fmt.sbprintf(sb, "<heading class='container'><h1 class='title'>%s</h1></heading>", title)
|
|
}
|
|
|
|
has_called_table_begin := 0
|
|
table_begin :: proc(sb: ^strings.Builder) {
|
|
assert(has_called_table_begin >= 0)
|
|
has_called_table_begin += 1
|
|
fmt.sbprint(sb, "<table>")
|
|
}
|
|
|
|
table_end :: proc(sb: ^strings.Builder) {
|
|
assert(has_called_table_begin > 0)
|
|
has_called_table_begin -= 1
|
|
fmt.sbprintf(sb, "</table>")
|
|
}
|
|
|
|
|
|
has_called_tr_begin := 0
|
|
tr_begin :: proc(sb: ^strings.Builder) {
|
|
assert(has_called_tr_begin >= 0)
|
|
has_called_tr_begin += 1
|
|
fmt.sbprint(sb, "<tr>")
|
|
}
|
|
|
|
tr_end :: proc(sb: ^strings.Builder) {
|
|
assert(has_called_tr_begin > 0)
|
|
has_called_tr_begin -= 1
|
|
fmt.sbprintf(sb, "</tr>")
|
|
}
|