Logo

index : blog

---

  • summary
  • about
  • tree
  • log
  • branches
<< path: root/public/blog.git/html/src/gen/blog.jai blob: 63c7fc0a13c9a129c0ff6b6e620692b2af596f26 [raw] [clear marker]

        
0
1
2
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.
6*/
7
8// TODO: @Cleanup
9
10
11Navbar_Item :: struct {
12 label: string;
13 url: string;
14 target: string;
15}
16
17Commit :: struct {
18 variable: string;
19 using as: Data;
20
21 Kind :: enum { LOOP; SIMPLE; SINGLE; };
22 Data :: union kind: Kind {
23 .LOOP ,, loop: [][]string;
24 .SIMPLE ,, simple: []string;
25 .SINGLE ,, single: string;
26 };
27}
28
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";
35 commits: []Commit;
36 dont_create_nav_button: bool;
37 store_on_top_level := true;
38}
39
40Page :: struct {
41 fp: string;
42 body: string;
43}
44
45
46page_create_body :: (page_meta: Page_Metadata) {
47 page: Page;
48 page.body = commit_generate_or_exit(page_meta,, temp);
49
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;
53
54 array_add(*pages, page);
55 pages_generated_add(page_meta.file_name, .HTML);
56
57 if !page_meta.dont_create_nav_button
58 then array_add(*nav_bar, { page_meta.name, page_meta.url, page_meta.url_target });
59}
60
61generate_pages :: () {
62 queue: [..]Action;
63 inner: [..]string;
64 nb: [..][]string;
65 defer {
66 this_allocation_is_not_a_leak(queue.data);
67 this_allocation_is_not_a_leak(inner.data);
68 }
69
70 css := template_read_or_exit("index", "css");
71
72 dt_now := current_time_consensus();
73 year := to_calendar(dt_now).year;
74 copyright := tprint("&copy; 2025 - % ptrace.dev", year);
75 copyright_const := string.[ copyright ];
76
77 /** This is soooo ugly, but it works */
78 for nav_bar {
79 defer array_reset_keeping_memory(*inner);
80
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));
85 }
86
87 footer: [][]string;
88 footer = .[
89 copyright_const,
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>" ],
94 ];
95
96 for pages {
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);
105 }
106}
107
108generate_rss :: (rss_channel: string) {
109 file_write_or_exit(RSS_FP, rss_channel);
110 pages_generated_add(RSS_FP);
111}
112
113generate_or_exit :: (queue: [..]Action, file_name: string) {
114 template := tprint("%/index.html", DIR_TEMPLATES);
115 fp := tprint("%.html", file_name);
116
117 success, body, exit_code, error_message := generate(queue, template, .FILE, fp);
118 defer {
119 free(body);
120 free(error_message);
121 }
122
123 if !success {
124 log_error(error_message);
125 exit(xx exit_code);
126 }
127}
128
129commit_generate_or_exit :: (page: Page_Metadata) -> html: string {
130 template := tprint("%/%.html", DIR_TEMPLATES, page.template);
131
132 local_action: [..]Action;
133 defer array_free(local_action);
134
135 for page.commits {
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);
140 }
141 }
142
143 success, body, exit_code, error_message := generate(local_action, template, .FILE);
144 defer {
145 free(body);
146 free(error_message);
147 }
148
149 if !success {
150 log_error(error_message);
151 exit(xx exit_code);
152 }
153
154 return copy_string(body);
155}
156
157commit_make :: (variable: string, data: string) -> Commit {
158 commit: Commit;
159 commit.variable = variable;
160 commit.kind = .SINGLE;
161 commit.single = data;
162 return commit;
163}
164
165commit_make :: (variable: string, data: []string) -> Commit {
166 commit: Commit;
167 commit.variable = variable;
168 commit.kind = .SIMPLE;
169 commit.simple = data;
170 return commit;
171}
172
173commit_make :: (variable: string, data: [][]string) -> Commit {
174 commit: Commit;
175 commit.variable = variable;
176 commit.kind = .LOOP;
177 commit.loop = data;
178 return commit;
179}
180
181rss_make_channel :: (items: string) -> string {
182 /** RSS 2.0 Channel
183 - 1: title
184 - 2: link
185 - 3: description
186 - 4: items
187 */
188
189 rss := tprint(RSS_2_0_TEMPLATE_CHANNEL,
190 RSS_URL,
191 PAGE_TITLE,
192 PAGE_URL,
193 PAGE_DESCRIPTION,
194 items,
195 );
196
197 return rss;
198}
199
200rss_make_items :: (entries: []Entry) -> string {
201 /** RSS 2.0 Item
202 1: title
203 2: link
204 3: description
205 4: date updated (Fri, 19 Mar 2026 10:00:00 GMT)
206 5: GUID
207 */
208 buf: String_Builder;
209 init_string_builder(*buf);
210
211 for entries {
212 cal := to_calendar(it.published);
213 url := tprint(PAGE_URL_POST, it.uri);
214
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);
218
219 /** Fri, Mar 10:00:00 GMT */
220 date_pattern := "%1, %2 %3 %4 %5:%6:%7 GMT";
221 /** 19 2026 */
222
223 date := tprint(date_pattern,
224 short_day,
225 cal.day_of_month_starting_at_0 + 1,
226 short_month,
227 cal.year,
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")
231 );
232
233 append(*buf,
234 tprint(RSS_2_0_TEMPLATE_ITEM, it.title, url, short_post, date)
235 );
236 }
237
238 return builder_to_string(*buf);
239}
240
241
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];
246 }
247}
248
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) {
251 item: string;
252 file_name := path_filename(file_path);
253
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);
258 }
259
260 array_add(*pages_generated, item);
261}
262
263posts_open :: (directory: string) -> [..]Markdown_File {
264
265 visitor :: (info: *File_Visit_Info, posts: *[..]Markdown_File) {
266 using info;
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);
272 }
273 }
274
275 posts: [..]Markdown_File;
276
277 ok := visit_files(directory, true, *posts, visitor, follow_directory_symlinks = false);
278 assert(ok);
279
280 return posts;
281}
282
283generic_open_and_to_entry :: (fn: string) -> Entry {
284 file := file_open_or_exit(tprint("%/%.md", DIR_GENERIC, fn));
285
286 as_entry := entry_make(file);
287 entry_annotations_filter(*as_entry);
288
289 md := as_entry.post;
290 as_entry.post = markdown_to_html(md, .UNSAFE);
291
292 return as_entry;
293}
294
295generic_page_make :: (
296 md_fn: string, url: string, fn: string, dont_add_to_navbar: bool
297)
298 -> *Page_Metadata
299{
300 generic := generic_open_and_to_entry(md_fn);
301
302 updated_cal := to_calendar(generic.updated);
303 updated_date := make_date(updated_cal);
304
305 title := commit_make("title", generic.title);
306 content := commit_make("content", generic.post);
307 updated := commit_make("updated", updated_date);
308
309 page := New(Page_Metadata);
310 page.name = generic.title;
311 page.template = "generic_page";
312 page.url = url;
313 page.file_name = fn;
314 page.commits = .[ title, content, updated ];
315 page.dont_create_nav_button = dont_add_to_navbar;
316
317 return page;
318}
319
320blank_page_make :: (
321 title: string,
322 url: string,
323 fn: string,
324 template: string,
325 dont_add_to_navbar: bool
326)
327 -> *Page_Metadata
328{
329 page := New(Page_Metadata);
330 page.name = title;
331 page.template = template;
332 page.url = url;
333 page.file_name = fn;
334 page.dont_create_nav_button = dont_add_to_navbar;
335
336 return page;
337}
338
339copy_files_from_to :: (files: []Copy_File) {
340 for files {
341 copy_file_or_exit(it.src, it.dest);
342 log("> Copied to: %", it.dest);
343 }
344}
345
346markdown_generate :: (entries: *[..]Entry) {
347 for * entries.* {
348 md := it.post;
349
350 // Note: Unsafe allows raw html nodes, which we need for the annotation filter.
351 it.post = markdown_to_html(md, .UNSAFE);
352 }
353}
354
355entry_annotations_filter :: (entry: *Entry) {
356
357 annotation :: (pattern: string, class: string) -> bool #expand {
358 if !begins_with(`line, pattern) return false;
359
360 `i += 1;
361
362 append(*`buf, "<blockquote class=\"");
363 append(*`buf, class);
364 append(*`buf, "\"><p>\n");
365
366 for `i..`post.count-1 {
367 if !begins_with(`post[`i], "> ") then break;
368 line := slice(`post[`i], 2, `post[`i].count);
369 append(*`buf, line);
370 append(*`buf, "\n");
371 `i += 1;
372 }
373
374 append(*`buf, "</p></blockquote>\n\n");
375 return true;
376 }
377
378
379 buf: String_Builder;
380 init_string_builder(*buf);
381
382 post := split(entry.post, "\n");
383
384 i: int;
385 while i <= post.count-1 {
386 defer i += 1;
387
388 line := post[i];
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");
394 if skip continue;
395
396 append(*buf, line);
397 append(*buf, "\n");
398 }
399
400 entry.post = builder_to_string(*buf);
401}
402
403annotations_filter :: (entries: *[..]Entry) {
404 for * entries.* {
405 entry_annotations_filter(it);
406 }
407}
408
409/** Note(adam): It does not handle nested tags. */
410replace_tags :: (uri: string, post: string, dont_show_images: bool) -> string {
411
412 is_at_end :: () -> bool #expand {
413 return parser_is_at_end(`i, `post);
414 }
415
416 pop :: () -> u8 #expand {
417 return parser_pop(*`i, `post);
418 }
419
420 buf: String_Builder;
421 init_string_builder(*buf);
422
423 i: int;
424 while !is_at_end() {
425 char := pop();
426 new: string;
427
428 if char == {
429 case "<"; new = replace_html_tag(*buf, char, *i, post, uri, dont_show_images);
430 case "["; new = replace_reference(*buf, *i, post, uri);
431 }
432
433 if new then append(*buf, new); else append(*buf, char);
434 }
435
436 return builder_to_string(*buf);
437}
438
439make_url :: (published: Apollo_Time) -> string {
440 ms := to_milliseconds(published);
441 as_hex := format_hex(ms);
442 return tprint("%", as_hex);
443}
444
445is_file :: (path: string) -> bool, success: bool {
446 stats: stat_t;
447
448 p := to_c_string(path);
449 ret := stat(p, *stats);
450 this_allocation_is_not_a_leak(p);
451
452 if ret != 0 return false, false;
453 return S_ISREG(stats.st_mode), true;
454}
455
456normalize_cr_and_wincringe :: (s: string) -> string {
457 cr := replace(s, "\r", "\n");
458 wincringe := replace(cr, "\r\n", "\n");
459 return wincringe;
460}
461
462normalize_string :: (s: string) -> string {
463 patterns := " °^!\"§$%&/()=?`+#.,{}[]\\´*~'_:;";
464 new := copy_string(s);
465 replace_chars(new, patterns, "-");
466 return new;
467}
468
469report_and_exit :: (message: string, args: ..Any, exit_code := 1, loc := #caller_location) {
470 log_error(message, ..args);
471 exit(xx exit_code);
472}
473
474create_project_directories :: () {
475 for DIRECTORIES_TO_CREATE
476 make_directory_if_it_does_not_exist(it);
477}
478
479copy_file_or_exit :: (src: string, dest: string) {
480 ok := copy_file(src, dest);
481 if !ok exit(1);
482}
483
484
485#scope_file
486
487
488#load "blog.h";
489
490
491DAYS :: string.[
492 "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
493];
494
495MONTHS :: string.[
496 "Dec", "Jan", "Feb", "Mar",
497 "Apr", "Mai", "Jun", "Jul",
498 "Aug", "Sep", "Okt", "Nov"
499];
500
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">
504 <channel>
505 <atom:link href="%1" rel="self" type="application/rss+xml" />
506 <title>%2</title>
507 <link>%3</link>
508 <description>%4</description>
509
510%5
511 </channel>
512</rss>
513STR_END;
514
515RSS_2_0_TEMPLATE_ITEM :: #string STR_END
516 <item>
517 <title>%1</title>
518 <link>%2</link>
519 <description>%3</description>
520 <pubDate>%4</pubDate>
521 <guid>%2</guid>
522 </item>
523STR_END;
524
525
526format_hex :: #bake_arguments formatInt(base = 16);
527
528
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 {
532 buf: String_Builder;
533 init_string_builder(*buf);
534
535 i: int;
536 while i <= text.count-1 && i <= limit-1 {
537 defer i += 1;
538 char := text[i];
539
540 if char == {
541 case "\t";
542 #through;
543 case "\r";
544 #through;
545 case "\n";
546 append(*buf, " ");
547 continue;
548 case "<";
549 idx := find_index_from_left(text, ">", i);
550 if idx == -1 { continue; }
551 else {
552 diff := idx-i+1;
553 i = idx;
554 limit += diff;
555 continue;
556 }
557 }
558
559 append(*buf, text[i]);
560 }
561
562 if limit < text.count then append(*buf, "...");
563
564 return builder_to_string(*buf);
565}
566
567
Copyright 2026  E766CB298A6D1E64 | Git-Thing heavily inspired by cgit