<<
path:
root/public/blog.git/html/modules/htmltemplate/lib.jai
blob: cb8407e6f1bdf7c64aaefbf3cdabf05f5a147ac1
[raw]
[clear marker]
3 * Copyright (c) 2026 dev@ptrace.dev
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:
12 * The above copyright notice and this permission notice shall be included in all
13 * copies or substantial portions of the Software.
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
28#import "File_Utilities";
34Exit_Codes :: enum s32 #specified {
50 queue_action: [..]Action,
51 template_fp_or_string: string,
52 input_kind: enum { STRING; FILE; },
54) -> (success: bool, html: string, exit_code: Exit_Codes, error_message: string)
56 return_if_err :: (exit_code: Exit_Codes) #expand {
57 if had_error then `return false, "", exit_code, error_string;
66 queue_replace: [..]Replace_Token;
70 array_free(queue_replace);
73 if #complete input_kind == {
75 source = template_fp_or_string;
77 source = file_acquire(template_fp_or_string);
78 return_if_err(.ERR_FILE);
81 token_success := tokenize(source, *tokens);
82 return_if_err(.ERR_TOKENIZER);
84 token_lookup := token_lookup_table_create(tokens);
85 defer deinit(*token_lookup);
88 log("-- Tokens ----------------------");
89 for tokens log("%", it);
90 log("--------------------------------\n\n");
93 template_action(*queue_replace, queue_action, token_lookup);
94 return_if_err(.ERR_ACTION);
96 html_new := template_replace(queue_replace, source);
97 return_if_err(.ERR_REPLACE);
100 fw_success := file_write(html_new, html_fp);
101 return_if_err(.ERR_FILE);
104 return true, html_new, .OK, "";
107commit :: (queue_action: *[..]Action, id: string, value: [][]string) {
109 id = copy_string(id),
110 nested = array_copy(value),
113 array_add(queue_action, action);
116commit :: (queue_action: *[..]Action, id: string, value: []string) {
118 id = copy_string(id),
119 flat = array_copy(value),
122 array_add(queue_action, action);
125/** TODO(adam, 5): If not inlined it will result in bogus data in `queue_action`.
126 This is quite janky. Investigate.
128commit :: (queue_action: *[..]Action, id: string, value: string) {
129 commit(queue_action, id, .[value]);
132commit :: (queue_action: *[..]Action, id: string, value: ..string) {
133 commit(queue_action, id, value);
136commit :: (id: string, value: [][]string) #expand {
137 commit(*`queue_action, id, value);
140commit :: (id: string, value: ..string) #expand {
141 commit(*`queue_action, id, ..value);
144// Needed for unit tests
145commit :: (id: string, value: []string) #expand {
146 commit(*`queue_action, id, ..value);
158ERR_PARSE_INTEGER :: #string STR_END
159Could not parse integer. Character in question: %1
163ERR_VALUE_OOB :: #string STR_END
164Cannot access your value, since it's out of bounds.
169ERR_TOO_MANY_PLACEHOLDERS :: #string STR_END
170There are more placeholders than provided values.
175ERR_REPLACE_WRONG_TYPE :: #string STR_END
176Wrong value type commited. Expected type: `..string` or `[]string`.
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`.
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.
190 The reason is, that we're able to check the syntax in the template
191 versus the supplied data structure in `commit()`.
193 If it does not match, we bail. See `assert_kind()`.
196Token_Kind :: enum u8 {
211Value :: union kind: Token_Kind {
212 .REPLACE ,, flat: []string;
213 .LOOP ,, nested: [][]string;
216Replace_Token :: struct {
223template_replace :: (replace_q: [..]Replace_Token, source: string) -> string {
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);
231 replacemes := quick_sort(replace_q, compare_pos_descending);
235 init_string_builder(*buf);
237 while idx < source.count {
238 if replacemes.count != 0 && peek(replacemes).position_start == idx {
239 replaceme := peek(replacemes);
241 for replaceme.text append(*buf, it);
242 idx = replaceme.position_end;
246 append(*buf, source[idx]);
251 out := builder_to_string(*buf);
256 replace_q: *[..]Replace_Token,
257 action_q: [..]Action,
258 lookup: Table(string, Token)
260 assert_kind :: (msg: string) #expand {
261 if `token.kind != `it.kind {
270 success, token := table_find(*lookup, it.id);
271 if !success continue;
274 rp.position_start = token.position_start;
275 rp.position_end = token.position_end;
279 assert_kind(ERR_REPLACE_WRONG_TYPE);
280 rp.text = handle_replace(it.flat, token,, temp);
282 assert_kind(ERR_LOOP_WRONG_TYPE);
283 rp.text = handle_loop(it.nested, token,, temp);
286 array_add(replace_q, rp);
290handle_loop :: (value: [][]string, token: Token) -> string {
292 init_string_builder(*buf);
295 replaced := handle_replace(it, token);
297 append(*buf, replaced);
299 if it_index != value.count-1 then append(*buf, "\n");
302 out := builder_to_string(*buf);
306handle_replace :: (value: []string, token: Token) -> string {
308 init_string_builder(*buf);
311 html_scan(value, token, *buf);
314 append(*buf, value[0]);
317 out := builder_to_string(*buf);
321html_scan :: (value: []string, token: Token, buffer: *String_Builder) {
323 iteration_skip := false;
324 guard_next_char := false;
330 iteration_skip = false;
334 prev := clamp(it_index-1, 0, html.count);
335 next := ifx (it_index+1 > html.count-1) then 0 else it_index+1;
337 // Add the next char without processing
338 if it == "\\" && html[next] == "%" {
339 guard_next_char = true;
343 if !guard_next_char && it == "%" {
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]);
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);
354 iteration_skip = true;
356 // Populate the value by position of the placeholders
358 if idx > value.count-1 {
359 error_string = sprint(ERR_TOO_MANY_PLACEHOLDERS, html, value);
364 append(buffer, value[idx]);
368 guard_next_char = false;
376html_arguments_numbered :: inline (
380 buffer: *String_Builder
382 str := string.{ 1, *html[next] };
383 _num, success := parse_int(*str);
386 // Note: `string.{ 1, foo }` Does not work with non-ASCII srings
387 error_string = sprint(ERR_PARSE_INTEGER, string.{ 1, *html[next] }, html);
392 num := ifx _num == 0 then 1 else _num - 1;
394 if num > value.count-1 {
395 error_string = sprint(ERR_VALUE_OOB, num, value);
400 append(buffer, value[num]);
404bail :: (value: $T) #expand {
405 if had_error `return value;
409 if had_error `return;
413file_acquire :: (fp: string) -> data: string {
414 data, success := read_entire_file(fp);
416 error_string = sprint("Could not read file: %", fp);
422file_write :: (data: string, fp: string) {
423 success := write_entire_file(fp, data);
426 error_string = sprint("Could not write into file: %", fp);
432// ------------------------------------
433// -- "Tokenizer" ---------------------
434// ------------------------------------
437adv :: () -> u8 #expand {
438 value := `source[`current];
443adv :: (current: *int) -> u8 #expand {
444 value := `source[current.*];
449peek :: () -> u8 #expand {
450 return `source[`current];
453peek :: (current: int) -> u8 #expand {
454 return `source[current];
457peek_next :: () -> u8 #expand {
458 if `current + 1 >= `source.count-1 then return "\0";
459 return `source[`current+1];
462is_at_end :: () -> bool #expand {
463 return `current >= `source.count-1;
466count_newlines :: inline (s: string) {
468 if s == "\n" then line_no += 1;
472consume_till :: (terminator: u8) -> (success: bool, slice: string, pos: int) #expand {
473 idx := find_index_from_left(`source, terminator, `current.*);
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 }
483 offset := idx - `current.*;
484 out := slice(`source, `current.*, offset);
488 `current.* = idx + 1;
490 return true, out, idx;
493consume_till :: (terminator: string) -> (success: bool, slice: string, pos: int) #expand {
494 idx := find_index_from_left(`source, terminator, `current.*);
497 error_string = sprint(
498 "Line %: Could not find terminator: '%'", line_no, terminator
503 offset := idx - `current.*;
504 out := slice(`source, `current.*, offset);
508 `current.* = idx + terminator.count - 1;
510 return true, out, idx;
513tokenize :: (source: string, tokens: *[..]Token) {
516 while !is_at_end() && !had_error {
519 if c == "\n" { line_no += 1; }
520 else if c == "{" && peek() == "{" {
521 position_start := current - 1;
522 adv(); // consume second '{'
524 if peek() == " " || peek() == "\t" then adv();
526 token_kind := token_determine_kind(source, *current);
527 token_create(tokens, token_kind, source, *current, position_start);
532token_determine_kind :: (source: string, current: *int) -> Token_Kind {
534 match :: (m: string) -> bool #expand {
538 if it != `source[temp] return false;
546 if match(":") { return .REPLACE; }
547 if match("loop:") { return .LOOP; }
550 error_string = sprint("Line %: Expected `:` or `loop` here", line_no);
562 create :: (kind: Token_Kind) #expand {
563 token := token_consume(`source, `current, kind, position_start);
564 array_add(`tokens, token);
568 case .REPLACE; create(.REPLACE);
569 case .LOOP; create(.LOOP);
581 success, id := consume_till(term);
582 id_trimmed := trim(id);
585 error_string = sprint("Line %: Expected an identifier here", line_no);
595 terminator_end :: "}}";
597 success2, html, position_end := consume_till(terminator_end);
598 html_trimmed := trim(html);
609 position_start = position_start,
610 position_end = position_end + terminator_end.count,
614token_lookup_table_create :: (tokens: [..]Token) -> Table(string, Token) {
615 action_map: Table(string, Token);
618 table_set(*action_map, token.id, token);