Logo

index : blog

---

  • summary
  • about
  • tree
  • log
  • branches
<< path: root/public/blog.git/html/modules/htmltemplate/lib.jai blob: cb8407e6f1bdf7c64aaefbf3cdabf05f5a147ac1 [raw] [clear marker]

        
0/*
1 * MIT License
2 *
3 * Copyright (c) 2026 dev@ptrace.dev
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to deal
7 * in the Software without restriction, including without limitation the rights
8 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 * copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be included in all
13 * copies or substantial portions of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 * SOFTWARE.
22 *
23 */
24
25#import "Basic";
26#import "File";
27#import "String";
28#import "File_Utilities";
29#import "Hash_Table";
30#import "Sort";
31#import "Math";
32
33
34Exit_Codes :: enum s32 #specified {
35 OK :: 0;
36
37 ERR_FILE :: 1;
38 ERR_TOKENIZER :: 2;
39 ERR_ACTION :: 3;
40 ERR_REPLACE :: 4;
41}
42
43Action :: struct {
44 id: string;
45 using value: Value;
46}
47
48
49generate :: (
50 queue_action: [..]Action,
51 template_fp_or_string: string,
52 input_kind: enum { STRING; FILE; },
53 html_fp: string = ""
54) -> (success: bool, html: string, exit_code: Exit_Codes, error_message: string)
55{
56 return_if_err :: (exit_code: Exit_Codes) #expand {
57 if had_error then `return false, "", exit_code, error_string;
58 }
59
60 had_error = false;
61 error_string = "";
62 line_no = 1;
63
64 tokens: [..]Token;
65 source: string;
66 queue_replace: [..]Replace_Token;
67
68 defer {
69 array_free(tokens);
70 array_free(queue_replace);
71 }
72
73 if #complete input_kind == {
74 case .STRING;
75 source = template_fp_or_string;
76 case .FILE;
77 source = file_acquire(template_fp_or_string);
78 return_if_err(.ERR_FILE);
79 }
80
81 token_success := tokenize(source, *tokens);
82 return_if_err(.ERR_TOKENIZER);
83
84 token_lookup := token_lookup_table_create(tokens);
85 defer deinit(*token_lookup);
86
87 #if false {
88 log("-- Tokens ----------------------");
89 for tokens log("%", it);
90 log("--------------------------------\n\n");
91 }
92
93 template_action(*queue_replace, queue_action, token_lookup);
94 return_if_err(.ERR_ACTION);
95
96 html_new := template_replace(queue_replace, source);
97 return_if_err(.ERR_REPLACE);
98
99 if html_fp {
100 fw_success := file_write(html_new, html_fp);
101 return_if_err(.ERR_FILE);
102 }
103
104 return true, html_new, .OK, "";
105}
106
107commit :: (queue_action: *[..]Action, id: string, value: [][]string) {
108 action: Action = {
109 id = copy_string(id),
110 nested = array_copy(value),
111 kind = .LOOP
112 };
113 array_add(queue_action, action);
114}
115
116commit :: (queue_action: *[..]Action, id: string, value: []string) {
117 action: Action = {
118 id = copy_string(id),
119 flat = array_copy(value),
120 kind = .REPLACE
121 };
122 array_add(queue_action, action);
123}
124
125/** TODO(adam, 5): If not inlined it will result in bogus data in `queue_action`.
126 This is quite janky. Investigate.
127*/
128commit :: (queue_action: *[..]Action, id: string, value: string) {
129 commit(queue_action, id, .[value]);
130}
131
132commit :: (queue_action: *[..]Action, id: string, value: ..string) {
133 commit(queue_action, id, value);
134}
135
136commit :: (id: string, value: [][]string) #expand {
137 commit(*`queue_action, id, value);
138}
139
140commit :: (id: string, value: ..string) #expand {
141 commit(*`queue_action, id, ..value);
142}
143
144// Needed for unit tests
145commit :: (id: string, value: []string) #expand {
146 commit(*`queue_action, id, ..value);
147}
148
149
150#scope_file
151
152
153had_error: bool;
154error_string: string;
155line_no: int;
156
157
158ERR_PARSE_INTEGER :: #string STR_END
159Could not parse integer. Character in question: %1
160The HTML snippet: %2
161STR_END;
162
163ERR_VALUE_OOB :: #string STR_END
164Cannot access your value, since it's out of bounds.
165The index: %1
166The array: %2
167STR_END;
168
169ERR_TOO_MANY_PLACEHOLDERS :: #string STR_END
170There are more placeholders than provided values.
171The HTML snippet: %1
172Your values: %2
173STR_END;
174
175ERR_REPLACE_WRONG_TYPE :: #string STR_END
176Wrong value type commited. Expected type: `..string` or `[]string`.
177STR_END;
178
179ERR_LOOP_WRONG_TYPE :: #string STR_END
180Wrong value type commited. Maybe you tried to pass a single string,
181or an one-dimensional array instead of a nested list.
182Expected type: `[][]string`.
183STR_END;
184
185
186/** Note(adam): To reduce confusion why Token_Kind is set on two places:
187 When using `commit()` we set a Token_Kind.
188 When creating a token, we also set a Token_Kind.
189
190 The reason is, that we're able to check the syntax in the template
191 versus the supplied data structure in `commit()`.
192
193 If it does not match, we bail. See `assert_kind()`.
194*/
195
196Token_Kind :: enum u8 {
197 NONE :: 0;
198
199 REPLACE;
200 LOOP;
201}
202
203Token :: struct {
204 kind: Token_Kind;
205 id: string;
206 html: string;
207 position_start: int;
208 position_end: int;
209}
210
211Value :: union kind: Token_Kind {
212 .REPLACE ,, flat: []string;
213 .LOOP ,, nested: [][]string;
214}
215
216Replace_Token :: struct {
217 position_start: int;
218 position_end: int;
219 text: string;
220}
221
222
223template_replace :: (replace_q: [..]Replace_Token, source: string) -> string {
224
225 compare_pos_descending :: (a: Replace_Token, b: Replace_Token) -> s64 {
226 aa := a.position_start;
227 bb := b.position_start;
228 return xx compare_floats(xx bb, xx aa);
229 }
230
231 replacemes := quick_sort(replace_q, compare_pos_descending);
232
233 idx: int;
234 buf: String_Builder;
235 init_string_builder(*buf);
236
237 while idx < source.count {
238 if replacemes.count != 0 && peek(replacemes).position_start == idx {
239 replaceme := peek(replacemes);
240
241 for replaceme.text append(*buf, it);
242 idx = replaceme.position_end;
243
244 pop(*replacemes);
245 } else {
246 append(*buf, source[idx]);
247 idx += 1;
248 }
249 }
250
251 out := builder_to_string(*buf);
252 return out;
253}
254
255template_action :: (
256 replace_q: *[..]Replace_Token,
257 action_q: [..]Action,
258 lookup: Table(string, Token)
259) {
260 assert_kind :: (msg: string) #expand {
261 if `token.kind != `it.kind {
262 had_error = true;
263 error_string = msg;
264 `return;
265 }
266 }
267
268 for action_q {
269 bail();
270 success, token := table_find(*lookup, it.id);
271 if !success continue;
272
273 rp: Replace_Token;
274 rp.position_start = token.position_start;
275 rp.position_end = token.position_end;
276
277 if token.kind == {
278 case .REPLACE;
279 assert_kind(ERR_REPLACE_WRONG_TYPE);
280 rp.text = handle_replace(it.flat, token,, temp);
281 case .LOOP;
282 assert_kind(ERR_LOOP_WRONG_TYPE);
283 rp.text = handle_loop(it.nested, token,, temp);
284 }
285
286 array_add(replace_q, rp);
287 }
288}
289
290handle_loop :: (value: [][]string, token: Token) -> string {
291 buf: String_Builder;
292 init_string_builder(*buf);
293
294 for value {
295 replaced := handle_replace(it, token);
296 bail("");
297 append(*buf, replaced);
298
299 if it_index != value.count-1 then append(*buf, "\n");
300 }
301
302 out := builder_to_string(*buf);
303 return out;
304}
305
306handle_replace :: (value: []string, token: Token) -> string {
307 buf: String_Builder;
308 init_string_builder(*buf);
309
310 if token.html {
311 html_scan(value, token, *buf);
312 bail("");
313 } else {
314 append(*buf, value[0]);
315 }
316
317 out := builder_to_string(*buf);
318 return out;
319}
320
321html_scan :: (value: []string, token: Token, buffer: *String_Builder) {
322 idx: int;
323 iteration_skip := false;
324 guard_next_char := false;
325
326 using token;
327
328 for html {
329 if iteration_skip {
330 iteration_skip = false;
331 continue;
332 }
333
334 prev := clamp(it_index-1, 0, html.count);
335 next := ifx (it_index+1 > html.count-1) then 0 else it_index+1;
336
337 // Add the next char without processing
338 if it == "\\" && html[next] == "%" {
339 guard_next_char = true;
340 continue;
341 }
342
343 if !guard_next_char && it == "%" {
344
345 // Populate the same value to every placeholder
346 if value.count == 1 {
347 if is_digit(html[next]) then iteration_skip = true;
348 append(buffer, value[0]);
349
350 // If user used %n, populate the values by index of `value`
351 } else if is_digit(html[next]) {
352 html_arguments_numbered(html, next, value, buffer);
353 bail();
354 iteration_skip = true;
355
356 // Populate the value by position of the placeholders
357 } else {
358 if idx > value.count-1 {
359 error_string = sprint(ERR_TOO_MANY_PLACEHOLDERS, html, value);
360 had_error = true;
361 return;
362 }
363
364 append(buffer, value[idx]);
365 idx += 1;
366 }
367 } else {
368 guard_next_char = false;
369 append(buffer, it);
370 }
371 }
372}
373
374
375
376html_arguments_numbered :: inline (
377 html: string,
378 next: int,
379 value: []string,
380 buffer: *String_Builder
381) {
382 str := string.{ 1, *html[next] };
383 _num, success := parse_int(*str);
384
385 if !success {
386 // Note: `string.{ 1, foo }` Does not work with non-ASCII srings
387 error_string = sprint(ERR_PARSE_INTEGER, string.{ 1, *html[next] }, html);
388 had_error = true;
389 return;
390 }
391
392 num := ifx _num == 0 then 1 else _num - 1;
393
394 if num > value.count-1 {
395 error_string = sprint(ERR_VALUE_OOB, num, value);
396 had_error = true;
397 return;
398 }
399
400 append(buffer, value[num]);
401}
402
403
404bail :: (value: $T) #expand {
405 if had_error `return value;
406}
407
408bail :: () #expand {
409 if had_error `return;
410}
411
412
413file_acquire :: (fp: string) -> data: string {
414 data, success := read_entire_file(fp);
415 if !success {
416 error_string = sprint("Could not read file: %", fp);
417 had_error = true;
418 }
419 return data;
420}
421
422file_write :: (data: string, fp: string) {
423 success := write_entire_file(fp, data);
424
425 if !success {
426 error_string = sprint("Could not write into file: %", fp);
427 had_error = true;
428 }
429}
430
431
432// ------------------------------------
433// -- "Tokenizer" ---------------------
434// ------------------------------------
435
436
437adv :: () -> u8 #expand {
438 value := `source[`current];
439 `current += 1;
440 return value;
441}
442
443adv :: (current: *int) -> u8 #expand {
444 value := `source[current.*];
445 current.* += 1;
446 return value;
447}
448
449peek :: () -> u8 #expand {
450 return `source[`current];
451}
452
453peek :: (current: int) -> u8 #expand {
454 return `source[current];
455}
456
457peek_next :: () -> u8 #expand {
458 if `current + 1 >= `source.count-1 then return "\0";
459 return `source[`current+1];
460}
461
462is_at_end :: () -> bool #expand {
463 return `current >= `source.count-1;
464}
465
466count_newlines :: inline (s: string) {
467 for s {
468 if s == "\n" then line_no += 1;
469 }
470}
471
472consume_till :: (terminator: u8) -> (success: bool, slice: string, pos: int) #expand {
473 idx := find_index_from_left(`source, terminator, `current.*);
474
475 if idx == -1 {
476 // string.{ 1, *x } will explode if character is non-ASCII
477 error_string = sprint(
478 "Line %: Could not find terminator: '%'", line_no, string.{ 1, *terminator }
479 );
480 return false, "", 0;
481 }
482
483 offset := idx - `current.*;
484 out := slice(`source, `current.*, offset);
485
486 count_newlines(out);
487
488 `current.* = idx + 1;
489
490 return true, out, idx;
491}
492
493consume_till :: (terminator: string) -> (success: bool, slice: string, pos: int) #expand {
494 idx := find_index_from_left(`source, terminator, `current.*);
495
496 if idx == -1 {
497 error_string = sprint(
498 "Line %: Could not find terminator: '%'", line_no, terminator
499 );
500 return false, "", 0;
501 }
502
503 offset := idx - `current.*;
504 out := slice(`source, `current.*, offset);
505
506 count_newlines(out);
507
508 `current.* = idx + terminator.count - 1;
509
510 return true, out, idx;
511}
512
513tokenize :: (source: string, tokens: *[..]Token) {
514 current: int;
515
516 while !is_at_end() && !had_error {
517 c := adv();
518
519 if c == "\n" { line_no += 1; }
520 else if c == "{" && peek() == "{" {
521 position_start := current - 1;
522 adv(); // consume second '{'
523
524 if peek() == " " || peek() == "\t" then adv();
525
526 token_kind := token_determine_kind(source, *current);
527 token_create(tokens, token_kind, source, *current, position_start);
528 }
529 }
530}
531
532token_determine_kind :: (source: string, current: *int) -> Token_Kind {
533
534 match :: (m: string) -> bool #expand {
535 temp := `current.*;
536
537 for m {
538 if it != `source[temp] return false;
539 temp += 1;
540 }
541
542 `current.* = temp;
543 return true;
544 }
545
546 if match(":") { return .REPLACE; }
547 if match("loop:") { return .LOOP; }
548
549 had_error = true;
550 error_string = sprint("Line %: Expected `:` or `loop` here", line_no);
551
552 return .NONE;
553}
554
555token_create :: (
556 tokens: *[..]Token,
557 kind: Token_Kind,
558 source: string,
559 current: *int,
560 position_start: int
561) {
562 create :: (kind: Token_Kind) #expand {
563 token := token_consume(`source, `current, kind, position_start);
564 array_add(`tokens, token);
565 }
566
567 if kind == {
568 case .REPLACE; create(.REPLACE);
569 case .LOOP; create(.LOOP);
570 }
571}
572
573token_consume :: (
574 source: string,
575 current: *int,
576 kind: Token_Kind,
577 position_start: int
578) -> Token
579{
580 term: u8 = ":";
581 success, id := consume_till(term);
582 id_trimmed := trim(id);
583
584 if !id_trimmed {
585 error_string = sprint("Line %: Expected an identifier here", line_no);
586 had_error = true;
587 return {};
588 }
589
590 if !success {
591 had_error = true;
592 return {};
593 }
594
595 terminator_end :: "}}";
596
597 success2, html, position_end := consume_till(terminator_end);
598 html_trimmed := trim(html);
599
600 if !success2 {
601 had_error = true;
602 return {};
603 }
604
605 return .{
606 kind = kind,
607 id = id_trimmed,
608 html = html_trimmed,
609 position_start = position_start,
610 position_end = position_end + terminator_end.count,
611 };
612}
613
614token_lookup_table_create :: (tokens: [..]Token) -> Table(string, Token) {
615 action_map: Table(string, Token);
616
617 for token: tokens {
618 table_set(*action_map, token.id, token);
619 }
620
621 return action_map;
622}
623
624
625
Copyright 2026  E766CB298A6D1E64 | Git-Thing heavily inspired by cgit