<<
path:
root/public/blog.git/html/src/gen/blog.jai
blob: 63c7fc0a13c9a129c0ff6b6e620692b2af596f26
[raw]
[clear marker]
3/** This - unfortunately - is a wild collection of procs.
4 It was basically the first file which grew over time. Some names are not that good,
5 the file name itself is not good, but bothering with it would drag progress.
11Navbar_Item :: struct {
21 Kind :: enum { LOOP; SIMPLE; SINGLE; };
22 Data :: union kind: Kind {
23 .LOOP ,, loop: [][]string;
24 .SIMPLE ,, simple: []string;
25 .SINGLE ,, single: string;
29Page_Metadata :: struct {
30 name: string; // navbar name
31 template: string; // from `templates/`
32 file_name: string; // for `www/`
33 url: string; // relative to `./` (except you're routing via nginx)
34 url_target: string = "self";
36 dont_create_nav_button: bool;
37 store_on_top_level := true;
46page_create_body :: (page_meta: Page_Metadata) {
48 page.body = commit_generate_or_exit(page_meta,, temp);
50 if page_meta.store_on_top_level
51 then page.fp = tprint("%/%", DIR_WWW, page_meta.file_name);
52 else page.fp = page_meta.file_name;
54 array_add(*pages, page);
55 pages_generated_add(page_meta.file_name, .HTML);
57 if !page_meta.dont_create_nav_button
58 then array_add(*nav_bar, { page_meta.name, page_meta.url, page_meta.url_target });
66 this_allocation_is_not_a_leak(queue.data);
67 this_allocation_is_not_a_leak(inner.data);
70 css := template_read_or_exit("index", "css");
72 dt_now := current_time_consensus();
73 year := to_calendar(dt_now).year;
74 copyright := tprint("© 2025 - % ptrace.dev", year);
75 copyright_const := string.[ copyright ];
77 /** This is soooo ugly, but it works */
79 defer array_reset_keeping_memory(*inner);
81 array_add(*inner, it.url);
82 array_add(*inner, it.target);
83 array_add(*inner, it.label);
84 array_add(*nb, array_copy(inner));
90 .[ "blog [at] ptrace [dot] dev" ],
91 .[ "<a href=\"https://key.ptrace.dev\" target=\"_blank\">gpg key</a>" ],
92 .[ "<a href=\"/privacy\">Privacy Policy</a>" ],
93 .[ "<a href=\"/rss\">rss</a>" ],
97 commit(*queue, "css", css);
98 commit(*queue, "page_title", PAGE_TITLE);
99 commit(*queue, "header_title", PAGE_TITLE);
100 commit(*queue, "nav_bar", nb);
101 commit(*queue, "body_content", it.body);
102 commit(*queue, "footer_text", footer);
103 generate_or_exit(queue, it.fp,, temp);
104 array_reset_keeping_memory(*queue);
108generate_rss :: (rss_channel: string) {
109 file_write_or_exit(RSS_FP, rss_channel);
110 pages_generated_add(RSS_FP);
113generate_or_exit :: (queue: [..]Action, file_name: string) {
114 template := tprint("%/index.html", DIR_TEMPLATES);
115 fp := tprint("%.html", file_name);
117 success, body, exit_code, error_message := generate(queue, template, .FILE, fp);
124 log_error(error_message);
129commit_generate_or_exit :: (page: Page_Metadata) -> html: string {
130 template := tprint("%/%.html", DIR_TEMPLATES, page.template);
132 local_action: [..]Action;
133 defer array_free(local_action);
136 if #complete it.kind == {
137 case .LOOP; commit(*local_action, it.variable, it.loop);
138 case .SIMPLE; commit(*local_action, it.variable, it.simple);
139 case .SINGLE; commit(*local_action, it.variable, it.single);
143 success, body, exit_code, error_message := generate(local_action, template, .FILE);
150 log_error(error_message);
154 return copy_string(body);
157commit_make :: (variable: string, data: string) -> Commit {
159 commit.variable = variable;
160 commit.kind = .SINGLE;
161 commit.single = data;
165commit_make :: (variable: string, data: []string) -> Commit {
167 commit.variable = variable;
168 commit.kind = .SIMPLE;
169 commit.simple = data;
173commit_make :: (variable: string, data: [][]string) -> Commit {
175 commit.variable = variable;
181rss_make_channel :: (items: string) -> string {
189 rss := tprint(RSS_2_0_TEMPLATE_CHANNEL,
200rss_make_items :: (entries: []Entry) -> string {
205 4: date updated (Fri, 19 Mar 2026 10:00:00 GMT)
209 init_string_builder(*buf);
212 cal := to_calendar(it.published);
213 url := tprint(PAGE_URL_POST, it.uri);
215 short_post := truncate_text_and_remove_html_tags(it.post, 250);
216 short_day := rss_calendar_get(cal, .DAY);
217 short_month := rss_calendar_get(cal, .MONTH);
219 /** Fri, Mar 10:00:00 GMT */
220 date_pattern := "%1, %2 %3 %4 %5:%6:%7 GMT";
223 date := tprint(date_pattern,
225 cal.day_of_month_starting_at_0 + 1,
228 string_pad_left(tprint("%", cal.hour), 2, "0"),
229 string_pad_left(tprint("%", cal.minute), 2, "0"),
230 string_pad_left(tprint("%", cal.second), 2, "0")
234 tprint(RSS_2_0_TEMPLATE_ITEM, it.title, url, short_post, date)
238 return builder_to_string(*buf);
242rss_calendar_get :: (cal: Calendar_Time, kind: enum { DAY; MONTH; }) -> string {
243 if #complete kind == {
244 case .DAY; return DAYS[cal.day_of_week_starting_at_0];
245 case .MONTH; return MONTHS[cal.month_starting_at_0];
249/** This prints only which pages where generated, so basically it's a "NOOP" */
250pages_generated_add :: (file_path: string, extension: enum { NONE; HTML; RSS; } = .NONE) {
252 file_name := path_filename(file_path);
254 if #complete extension == {
255 case .NONE; item = file_name;
256 case .HTML; item = tprint("%.html", file_name);
257 case .RSS; item = tprint("%.rss", file_name);
260 array_add(*pages_generated, item);
263posts_open :: (directory: string) -> [..]Markdown_File {
265 visitor :: (info: *File_Visit_Info, posts: *[..]Markdown_File) {
267 if is_file(full_name) && ends_with(short_name, ".md") {
268 md_file: Markdown_File;
269 md_file.fn = full_name;
270 md_file.content = file_open_or_exit(full_name);
271 array_add(posts, md_file);
275 posts: [..]Markdown_File;
277 ok := visit_files(directory, true, *posts, visitor, follow_directory_symlinks = false);
283generic_open_and_to_entry :: (fn: string) -> Entry {
284 file := file_open_or_exit(tprint("%/%.md", DIR_GENERIC, fn));
286 as_entry := entry_make(file);
287 entry_annotations_filter(*as_entry);
290 as_entry.post = markdown_to_html(md, .UNSAFE);
295generic_page_make :: (
296 md_fn: string, url: string, fn: string, dont_add_to_navbar: bool
300 generic := generic_open_and_to_entry(md_fn);
302 updated_cal := to_calendar(generic.updated);
303 updated_date := make_date(updated_cal);
305 title := commit_make("title", generic.title);
306 content := commit_make("content", generic.post);
307 updated := commit_make("updated", updated_date);
309 page := New(Page_Metadata);
310 page.name = generic.title;
311 page.template = "generic_page";
314 page.commits = .[ title, content, updated ];
315 page.dont_create_nav_button = dont_add_to_navbar;
325 dont_add_to_navbar: bool
329 page := New(Page_Metadata);
331 page.template = template;
334 page.dont_create_nav_button = dont_add_to_navbar;
339copy_files_from_to :: (files: []Copy_File) {
341 copy_file_or_exit(it.src, it.dest);
342 log("> Copied to: %", it.dest);
346markdown_generate :: (entries: *[..]Entry) {
350 // Note: Unsafe allows raw html nodes, which we need for the annotation filter.
351 it.post = markdown_to_html(md, .UNSAFE);
355entry_annotations_filter :: (entry: *Entry) {
357 annotation :: (pattern: string, class: string) -> bool #expand {
358 if !begins_with(`line, pattern) return false;
362 append(*`buf, "<blockquote class=\"");
363 append(*`buf, class);
364 append(*`buf, "\"><p>\n");
366 for `i..`post.count-1 {
367 if !begins_with(`post[`i], "> ") then break;
368 line := slice(`post[`i], 2, `post[`i].count);
374 append(*`buf, "</p></blockquote>\n\n");
380 init_string_builder(*buf);
382 post := split(entry.post, "\n");
385 while i <= post.count-1 {
389 skip := annotation("> [!NOTE]", "bq-note");
390 skip |= annotation("> [!TIP]", "bq-tip");
391 skip |= annotation("> [!IMPORTANT]", "bq-important");
392 skip |= annotation("> [!WARNING]", "bq-warning");
393 skip |= annotation("> [!CAUTION]", "bq-caution");
400 entry.post = builder_to_string(*buf);
403annotations_filter :: (entries: *[..]Entry) {
405 entry_annotations_filter(it);
409/** Note(adam): It does not handle nested tags. */
410replace_tags :: (uri: string, post: string, dont_show_images: bool) -> string {
412 is_at_end :: () -> bool #expand {
413 return parser_is_at_end(`i, `post);
416 pop :: () -> u8 #expand {
417 return parser_pop(*`i, `post);
421 init_string_builder(*buf);
429 case "<"; new = replace_html_tag(*buf, char, *i, post, uri, dont_show_images);
430 case "["; new = replace_reference(*buf, *i, post, uri);
433 if new then append(*buf, new); else append(*buf, char);
436 return builder_to_string(*buf);
439make_url :: (published: Apollo_Time) -> string {
440 ms := to_milliseconds(published);
441 as_hex := format_hex(ms);
442 return tprint("%", as_hex);
445is_file :: (path: string) -> bool, success: bool {
448 p := to_c_string(path);
449 ret := stat(p, *stats);
450 this_allocation_is_not_a_leak(p);
452 if ret != 0 return false, false;
453 return S_ISREG(stats.st_mode), true;
456normalize_cr_and_wincringe :: (s: string) -> string {
457 cr := replace(s, "\r", "\n");
458 wincringe := replace(cr, "\r\n", "\n");
462normalize_string :: (s: string) -> string {
463 patterns := " °^!\"§$%&/()=?`+#.,{}[]\\´*~'_:;";
464 new := copy_string(s);
465 replace_chars(new, patterns, "-");
469report_and_exit :: (message: string, args: ..Any, exit_code := 1, loc := #caller_location) {
470 log_error(message, ..args);
474create_project_directories :: () {
475 for DIRECTORIES_TO_CREATE
476 make_directory_if_it_does_not_exist(it);
479copy_file_or_exit :: (src: string, dest: string) {
480 ok := copy_file(src, dest);
492 "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
496 "Dec", "Jan", "Feb", "Mar",
497 "Apr", "Mai", "Jun", "Jul",
498 "Aug", "Sep", "Okt", "Nov"
501RSS_2_0_TEMPLATE_CHANNEL :: #string STR_END
502<?xml version="1.0" encoding="UTF-8"?>
503<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
505 <atom:link href="%1" rel="self" type="application/rss+xml" />
508 <description>%4</description>
515RSS_2_0_TEMPLATE_ITEM :: #string STR_END
519 <description>%3</description>
520 <pubDate>%4</pubDate>
526format_hex :: #bake_arguments formatInt(base = 16);
529/** This is a very lazy approach since it will also eat tags inside code blocks
530 or exit early inside tag. */
531truncate_text_and_remove_html_tags :: (text: string, limit: int = 100) -> string {
533 init_string_builder(*buf);
536 while i <= text.count-1 && i <= limit-1 {
549 idx := find_index_from_left(text, ">", i);
550 if idx == -1 { continue; }
559 append(*buf, text[i]);
562 if limit < text.count then append(*buf, "...");
564 return builder_to_string(*buf);