/** This - unfortunately - is a wild collection of procs. It was basically the first file which grew over time. Some names are not that good, the file name itself is not good, but bothering with it would drag progress. */ // TODO: @Cleanup Navbar_Item :: struct { label: string; url: string; target: string; } Commit :: struct { variable: string; using as: Data; Kind :: enum { LOOP; SIMPLE; SINGLE; }; Data :: union kind: Kind { .LOOP ,, loop: [][]string; .SIMPLE ,, simple: []string; .SINGLE ,, single: string; }; } Page_Metadata :: struct { name: string; // navbar name template: string; // from `templates/` file_name: string; // for `www/` url: string; // relative to `./` (except you're routing via nginx) url_target: string = "self"; commits: []Commit; dont_create_nav_button: bool; store_on_top_level := true; } Page :: struct { fp: string; body: string; } page_create_body :: (page_meta: Page_Metadata) { page: Page; page.body = commit_generate_or_exit(page_meta,, temp); if page_meta.store_on_top_level then page.fp = tprint("%/%", DIR_WWW, page_meta.file_name); else page.fp = page_meta.file_name; array_add(*pages, page); pages_generated_add(page_meta.file_name, .HTML); if !page_meta.dont_create_nav_button then array_add(*nav_bar, { page_meta.name, page_meta.url, page_meta.url_target }); } generate_pages :: () { queue: [..]Action; inner: [..]string; nb: [..][]string; defer { this_allocation_is_not_a_leak(queue.data); this_allocation_is_not_a_leak(inner.data); } css := template_read_or_exit("index", "css"); dt_now := current_time_consensus(); year := to_calendar(dt_now).year; copyright := tprint("© 2025 - % ptrace.dev", year); copyright_const := string.[ copyright ]; /** This is soooo ugly, but it works */ for nav_bar { defer array_reset_keeping_memory(*inner); array_add(*inner, it.url); array_add(*inner, it.target); array_add(*inner, it.label); array_add(*nb, array_copy(inner)); } footer: [][]string; footer = .[ copyright_const, .[ "blog [at] ptrace [dot] dev" ], .[ "gpg key" ], .[ "Privacy Policy" ], .[ "rss" ], ]; for pages { commit(*queue, "css", css); commit(*queue, "page_title", PAGE_TITLE); commit(*queue, "header_title", PAGE_TITLE); commit(*queue, "nav_bar", nb); commit(*queue, "body_content", it.body); commit(*queue, "footer_text", footer); generate_or_exit(queue, it.fp,, temp); array_reset_keeping_memory(*queue); } } generate_rss :: (rss_channel: string) { file_write_or_exit(RSS_FP, rss_channel); pages_generated_add(RSS_FP); } generate_or_exit :: (queue: [..]Action, file_name: string) { template := tprint("%/index.html", DIR_TEMPLATES); fp := tprint("%.html", file_name); success, body, exit_code, error_message := generate(queue, template, .FILE, fp); defer { free(body); free(error_message); } if !success { log_error(error_message); exit(xx exit_code); } } commit_generate_or_exit :: (page: Page_Metadata) -> html: string { template := tprint("%/%.html", DIR_TEMPLATES, page.template); local_action: [..]Action; defer array_free(local_action); for page.commits { if #complete it.kind == { case .LOOP; commit(*local_action, it.variable, it.loop); case .SIMPLE; commit(*local_action, it.variable, it.simple); case .SINGLE; commit(*local_action, it.variable, it.single); } } success, body, exit_code, error_message := generate(local_action, template, .FILE); defer { free(body); free(error_message); } if !success { log_error(error_message); exit(xx exit_code); } return copy_string(body); } commit_make :: (variable: string, data: string) -> Commit { commit: Commit; commit.variable = variable; commit.kind = .SINGLE; commit.single = data; return commit; } commit_make :: (variable: string, data: []string) -> Commit { commit: Commit; commit.variable = variable; commit.kind = .SIMPLE; commit.simple = data; return commit; } commit_make :: (variable: string, data: [][]string) -> Commit { commit: Commit; commit.variable = variable; commit.kind = .LOOP; commit.loop = data; return commit; } rss_make_channel :: (items: string) -> string { /** RSS 2.0 Channel - 1: title - 2: link - 3: description - 4: items */ rss := tprint(RSS_2_0_TEMPLATE_CHANNEL, RSS_URL, PAGE_TITLE, PAGE_URL, PAGE_DESCRIPTION, items, ); return rss; } rss_make_items :: (entries: []Entry) -> string { /** RSS 2.0 Item 1: title 2: link 3: description 4: date updated (Fri, 19 Mar 2026 10:00:00 GMT) 5: GUID */ buf: String_Builder; init_string_builder(*buf); for entries { cal := to_calendar(it.published); url := tprint(PAGE_URL_POST, it.uri); short_post := truncate_text_and_remove_html_tags(it.post, 250); short_day := rss_calendar_get(cal, .DAY); short_month := rss_calendar_get(cal, .MONTH); /** Fri, Mar 10:00:00 GMT */ date_pattern := "%1, %2 %3 %4 %5:%6:%7 GMT"; /** 19 2026 */ date := tprint(date_pattern, short_day, cal.day_of_month_starting_at_0 + 1, short_month, cal.year, string_pad_left(tprint("%", cal.hour), 2, "0"), string_pad_left(tprint("%", cal.minute), 2, "0"), string_pad_left(tprint("%", cal.second), 2, "0") ); append(*buf, tprint(RSS_2_0_TEMPLATE_ITEM, it.title, url, short_post, date) ); } return builder_to_string(*buf); } rss_calendar_get :: (cal: Calendar_Time, kind: enum { DAY; MONTH; }) -> string { if #complete kind == { case .DAY; return DAYS[cal.day_of_week_starting_at_0]; case .MONTH; return MONTHS[cal.month_starting_at_0]; } } /** This prints only which pages where generated, so basically it's a "NOOP" */ pages_generated_add :: (file_path: string, extension: enum { NONE; HTML; RSS; } = .NONE) { item: string; file_name := path_filename(file_path); if #complete extension == { case .NONE; item = file_name; case .HTML; item = tprint("%.html", file_name); case .RSS; item = tprint("%.rss", file_name); } array_add(*pages_generated, item); } posts_open :: (directory: string) -> [..]Markdown_File { visitor :: (info: *File_Visit_Info, posts: *[..]Markdown_File) { using info; if is_file(full_name) && ends_with(short_name, ".md") { md_file: Markdown_File; md_file.fn = full_name; md_file.content = file_open_or_exit(full_name); array_add(posts, md_file); } } posts: [..]Markdown_File; ok := visit_files(directory, true, *posts, visitor, follow_directory_symlinks = false); assert(ok); return posts; } generic_open_and_to_entry :: (fn: string) -> Entry { file := file_open_or_exit(tprint("%/%.md", DIR_GENERIC, fn)); as_entry := entry_make(file); entry_annotations_filter(*as_entry); md := as_entry.post; as_entry.post = markdown_to_html(md, .UNSAFE); return as_entry; } generic_page_make :: ( md_fn: string, url: string, fn: string, dont_add_to_navbar: bool ) -> *Page_Metadata { generic := generic_open_and_to_entry(md_fn); updated_cal := to_calendar(generic.updated); updated_date := make_date(updated_cal); title := commit_make("title", generic.title); content := commit_make("content", generic.post); updated := commit_make("updated", updated_date); page := New(Page_Metadata); page.name = generic.title; page.template = "generic_page"; page.url = url; page.file_name = fn; page.commits = .[ title, content, updated ]; page.dont_create_nav_button = dont_add_to_navbar; return page; } blank_page_make :: ( title: string, url: string, fn: string, template: string, dont_add_to_navbar: bool ) -> *Page_Metadata { page := New(Page_Metadata); page.name = title; page.template = template; page.url = url; page.file_name = fn; page.dont_create_nav_button = dont_add_to_navbar; return page; } copy_files_from_to :: (files: []Copy_File) { for files { copy_file_or_exit(it.src, it.dest); log("> Copied to: %", it.dest); } } markdown_generate :: (entries: *[..]Entry) { for * entries.* { md := it.post; // Note: Unsafe allows raw html nodes, which we need for the annotation filter. it.post = markdown_to_html(md, .UNSAFE); } } entry_annotations_filter :: (entry: *Entry) { annotation :: (pattern: string, class: string) -> bool #expand { if !begins_with(`line, pattern) return false; `i += 1; append(*`buf, "

\n"); for `i..`post.count-1 { if !begins_with(`post[`i], "> ") then break; line := slice(`post[`i], 2, `post[`i].count); append(*`buf, line); append(*`buf, "\n"); `i += 1; } append(*`buf, "

\n\n"); return true; } buf: String_Builder; init_string_builder(*buf); post := split(entry.post, "\n"); i: int; while i <= post.count-1 { defer i += 1; line := post[i]; skip := annotation("> [!NOTE]", "bq-note"); skip |= annotation("> [!TIP]", "bq-tip"); skip |= annotation("> [!IMPORTANT]", "bq-important"); skip |= annotation("> [!WARNING]", "bq-warning"); skip |= annotation("> [!CAUTION]", "bq-caution"); if skip continue; append(*buf, line); append(*buf, "\n"); } entry.post = builder_to_string(*buf); } annotations_filter :: (entries: *[..]Entry) { for * entries.* { entry_annotations_filter(it); } } /** Note(adam): It does not handle nested tags. */ replace_tags :: (uri: string, post: string, dont_show_images: bool) -> string { is_at_end :: () -> bool #expand { return parser_is_at_end(`i, `post); } pop :: () -> u8 #expand { return parser_pop(*`i, `post); } buf: String_Builder; init_string_builder(*buf); i: int; while !is_at_end() { char := pop(); new: string; if char == { case "<"; new = replace_html_tag(*buf, char, *i, post, uri, dont_show_images); case "["; new = replace_reference(*buf, *i, post, uri); } if new then append(*buf, new); else append(*buf, char); } return builder_to_string(*buf); } make_url :: (published: Apollo_Time) -> string { ms := to_milliseconds(published); as_hex := format_hex(ms); return tprint("%", as_hex); } is_file :: (path: string) -> bool, success: bool { stats: stat_t; p := to_c_string(path); ret := stat(p, *stats); this_allocation_is_not_a_leak(p); if ret != 0 return false, false; return S_ISREG(stats.st_mode), true; } normalize_cr_and_wincringe :: (s: string) -> string { cr := replace(s, "\r", "\n"); wincringe := replace(cr, "\r\n", "\n"); return wincringe; } normalize_string :: (s: string) -> string { patterns := " °^!\"§$%&/()=?`+#.,{}[]\\´*~'_:;"; new := copy_string(s); replace_chars(new, patterns, "-"); return new; } report_and_exit :: (message: string, args: ..Any, exit_code := 1, loc := #caller_location) { log_error(message, ..args); exit(xx exit_code); } create_project_directories :: () { for DIRECTORIES_TO_CREATE make_directory_if_it_does_not_exist(it); } copy_file_or_exit :: (src: string, dest: string) { ok := copy_file(src, dest); if !ok exit(1); } #scope_file #load "blog.h"; DAYS :: string.[ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ]; MONTHS :: string.[ "Dec", "Jan", "Feb", "Mar", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov" ]; RSS_2_0_TEMPLATE_CHANNEL :: #string STR_END %2 %3 %4 %5 STR_END; RSS_2_0_TEMPLATE_ITEM :: #string STR_END %1 %2 %3 %4 %2 STR_END; format_hex :: #bake_arguments formatInt(base = 16); /** This is a very lazy approach since it will also eat tags inside code blocks or exit early inside tag. */ truncate_text_and_remove_html_tags :: (text: string, limit: int = 100) -> string { buf: String_Builder; init_string_builder(*buf); i: int; while i <= text.count-1 && i <= limit-1 { defer i += 1; char := text[i]; if char == { case "\t"; #through; case "\r"; #through; case "\n"; append(*buf, " "); continue; case "<"; idx := find_index_from_left(text, ">", i); if idx == -1 { continue; } else { diff := idx-i+1; i = idx; limit += diff; continue; } } append(*buf, text[i]); } if limit < text.count then append(*buf, "..."); return builder_to_string(*buf); }