Author:ptrace Comitter:ptrace Date:2026-01-07 22:04:26 UTC

init

diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..805c007 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ # Jai .build /bin diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..63e85fc --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ MIT License Copyright (c) 2026 dev@ptrace.dev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1eb5a3d --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ # htmltemplate A tiny html template 'engine'. It currently supports: - replacements of values. Via numbered or not-numbered parameters - loops inside the template file ### Example If you want an example with more explainations, consult `examples/02_with_comments.jai`.   For more explaination about the module parameters, consult: `htmltemplate/lib.jai`.   And if you want to know what you else can do with the template syntax,  look at the unit tests  & comments in `test/test.jai`.   HTML template: ```html <div>     <p>{{ :foo1: }}</p>     {{ :foo2: <a href="%" %>%</a> }}     {{ :foo3: <a href="#%1" target="%2">%1</a> }}     <ul> {{ loop:foo4: <li><a href="#%1">%1</a>%2</li> }}     </ul> </div> ``` Code: ```text #import "Basic"; #import "htmltemplate"()(READ_FROM_TEMPLATE_FILE = false); main :: () {     queue_action: [..]Action;     defer array_free(queue_action);     commit("foo1", "My Title");     commit("foo2", "#about", "target=\"_self\"", "About");     commit("foo3", "blog", "_self");     commit("foo4", .[         .["Foo", "Bar"],         .["Fizz", "Buzz"],         .["contact", ""],     ]);     success,     html_string,     exit_code,     error_message := generate(queue_action, TEMPLATE);     defer {         free(html_string);         free(error_message);     }     if !success {         log("%", error_message);         return;     }     log("%", html_string); } ``` Output: ```html <div>     <p>My Title</p>     <a href="#about" target="_self">About</a>     <a href="#blog" target="_self">blog</a>     <ul> <li><a href="#Foo">Foo</a>Bar</li> <li><a href="#Fizz">Fizz</a>Buzz</li> <li><a href="#contact">contact</a></li>     </ul> </div> ``` ## Compiler Version ``` $ jai -version Version: beta 0.2.024, built on 31 December 2025. ``` **Note:** You need at least version `0.2.023`,  since it introduced tagged unions there.   ## Usage Put the library into your project-local `modules` dir,  or where ever you defined your library path.   ## Tests You can run unit tests with: `jai build.jai - run silent`. ## BNF ``` statement      =  "{{" ( replace | loop ) "}}" ; replace        =  ":" identifier ":" ( html_code )? ; loop           =  "loop" ":" identifier ":" html_code ; html_code      =  { UNICODE | replace_marks | escape_marks } ; replace_marks  =  "%" | "%" number ; escape_marks   =  "\%" ; identifier     =  UNICODE ; number         =  [1-9] ; ``` diff --git a/build.jai b/build.jai new file mode 100644 index 0000000..8036650 --- /dev/null +++ b/build.jai @@ -0,0 +1,142 @@ #import "Basic"; #import "Compiler"; #import "File"; #import "Autorun"; build :: () {     args := get_build_options().compile_time_command_line;     // args     args_help               := array_find(args, "help");     args_compiler_silent    := array_find(args, "silent");     args_program_run        := array_find(args, "run");     args_build_release      := array_find(args, "release");     args_memory_debug       := array_find(args, "memory");     // program args     program_args := program_args_collect(args);     defer array_free(program_args);     // -----------------------------------------     w := compiler_create_workspace();     if !w {         log("Workspace creation failed.");         return;     }     set_build_options_dc(.{ do_output = false });     if args_help {         args_help_print();         return;     }     print("The workspace w is %\n", w);     make_directory_if_it_does_not_exist("bin");     target_options := get_build_options(w);     target_options.output_executable_name = "program";     target_options.output_path = "bin";     import_path: [..] string;     array_add(*import_path, ..target_options.import_path);     array_add(*import_path, ".");     target_options.import_path = import_path;     if args_compiler_silent  target_options.text_output_flags = 0;     if args_build_release {         build_release(w, *target_options);     } else {         build_debug(w, *target_options);     }     compiler_begin_intercept(w);     add_build_file(tprint("%test/test.jai", #filepath), w);     add_build_string(tprint("MEMORY_DEBUGGER_ENABLED :: %;", args_memory_debug), w);     compiler_response := message_loop();     compiler_end_intercept(w);     if !compiler_response {         log("Compiler response failed.");         return;     }     if args_program_run  run_build_result(w, program_args); } build_debug :: (w: Workspace, target_options: *Build_Options) {     log("Choosing debug options...");     target_options.backend =.X64;     set_optimization(target_options, Optimization_Type.DEBUG, true);     set_build_options(target_options.*, w); } build_release :: (w: Workspace, target_options: *Build_Options) {     log("Choosing release options...");     target_options.backend = .LLVM;     set_optimization(target_options, Optimization_Type.VERY_OPTIMIZED);     set_build_options(target_options.*, w); } args_help_print :: () {     help_message := #string _END_ Usage:  jai build.jai - [OPTIONS] :: [PROGRAM ARGS] Options:     help        Prints this help menu.     silent      Disables compiler/linker statistics.     run         Runs your program afterwards.     release     Builds with release options. If omitted, it builds a debug build.     memory      Enables the memory leak detector. Passing Args to your Program:     If you want to supply args to your program, pass it like that:         `jai build.jai - run :: my_arg1 foo bar abc ABC`     Everything after the `::` get's forwarded to your program. _END_;     log(help_message); } program_args_collect :: (args: []string, divider: string = "::") -> [..]string {     buf: [..]string;     success, match := array_find(args, divider);     if success  for i: match+1..args.count-1  array_add(*buf, args[i]);     return buf; } message_loop :: () -> success: bool {     while true {         message := compiler_wait_for_message();         if !message break;         if message.kind == {         case .COMPLETE;             message_complete := cast(*Message_Complete) message;             return message_complete.error_code == 0;         }     }     return false; } #placeholder MEMORY_DEBUGGER_ENABLED; main :: () {} #run build(); diff --git a/examples/01_from_readme.jai b/examples/01_from_readme.jai new file mode 100644 index 0000000..db5c16e --- /dev/null +++ b/examples/01_from_readme.jai @@ -0,0 +1,55 @@ #import "Basic"; #import "htmltemplate"()(READ_FROM_TEMPLATE_FILE = false); /**     jai 01_from_readme.jai -quiet && ./01_from_readme */ TEMPLATE :: #string STR_END <div>     <p>{{ :foo1: }}</p>     {{ :foo2: <a href="%" %>%</a> }}     {{ :foo3: <a href="#%1" target="%2">%1</a> }}     <ul> {{ loop:foo4: <li><a href="#%1">%1</a>%2</li> }}     </ul> </div> STR_END; main :: () {     queue_action: [..]Action;     defer array_free(queue_action);     commit("foo1", "My Title");     commit("foo2", "#about", "target=\"_self\"", "About");     commit("foo3", "blog", "_self");     commit("foo4", .[         .["Foo", "Bar"],         .["Fizz", "Buzz"],         .["contact", ""],     ]);     success,     html_string,     exit_code,     error_message := generate(queue_action, TEMPLATE);     defer {         free(html_string);         free(error_message);     }     if !success {         log("%", error_message);         return;     }     log("%", html_string); } diff --git a/examples/02_with_comments.jai b/examples/02_with_comments.jai new file mode 100644 index 0000000..715cfd3 --- /dev/null +++ b/examples/02_with_comments.jai @@ -0,0 +1,161 @@ #import "Basic"; html_template :: #import "htmltemplate"()(     READ_FROM_TEMPLATE_FILE = false,     CREATE_HTML_FILE = false ); /**     jai 01_with_comments.jai -quiet && ./01_with_comments */ /** The module paramaters define, if you want to read and write into a file,     or not. The defaults assume, you always want to read from a file, but     writing is optional. Instead it just returns the rendered html code as     string - it always does that, independend of the settings.     For this example, I disabled any reading and writing from and to files,     instead I provide a function where you can manually decide, when to     store the result in a file. */ /** This below is the template. This would be read from a file normally, but     this is up to you. */ YOUR_TEMPLATE_FILE :: #string STR_END <!DOCTYPE html> <html> <head>     <meta charset="UTF-8">     <meta name="viewport" content="width=device-width, initial-scale=1.0">     <title>{{ :page_title: }}</title>     <style>         body {             background-color: {{ :bg_color: }};             color: {{ :font_color: }};             font-family: Verdana, Geneva, Tahoma, sans-serif;         }     </style> </head> <body>     <h1>{{ :article_title: % % }}</h1>     <p>This demonstrates a {{ :example1: % % }}</p>     <p>{{ :example2: <a href="%" target="%">This</a> demonstrates % % }}</p>     <p>{{ :example3: Or <b>%1</b> numbered <i>%2</i> that can be reused: <b>%1</b> }}</p>     <p>Lets demonstrate looping!</p>     <ul> {{ loop:example4: <li><a href="#%1" %2>%1</a> %3</li> }}     </ul> </body> </html> STR_END; main :: () {     using html_template;     /** Now lets create some data, so we can populate the placeholders with it.         For that, we need an dynamic array, which stores our data: */     queue_action: [..]Action;     /** If you keep this exact name you don't have to pass it later to the         `commit()` proc, since one of its overloads is a macro.         But you can always chose a different name for your array and pass         it directly to the `commit()` proc if you want.          Now lets create the data for the placeholders: */     commit(         id = "page_title",         value = "Example 01",     );     commit(         id = "bg_color",         value = "#323240",     );     commit(         id = "font_color",         value = "#DFDFFF",     );     // You can pass multiple values as static array     commit(         id = "article_title",         value = .[ "Example", "01" ],     );     // Or via variadic procs     commit("example1", "simple", "replacement");     commit("example2",         "#some_path",         "_self",         "positional",         "placeholders"     );     commit("example3",         "even",         "placeholders"     );     // This identifier `example4` is a loop. The amount of iterations     // is defined by the amount of sub-arrays:     commit("example4", .[         .[ "articles", "target=\"_self\"", "to read" ],         .[ "about", "target=\"_self\"", "me" ],         .[ "books", "target=\"_self\"", "" ],         .[ "extern", "", "website" ],     ]);     /** Now we can generate the html file. Since we disabled the file write,         we don't have to specify an output path and we can just omit the         last paramater. */     success,       // If something went wrong, it's indicated here.     html_string,   // Your output as string. (You have to free it).     exit_code,     // More needed for debugging, consult the `lib.jai` for more information.     error_message  // This returns the exact error message if !success. (You have to free it)     := generate(queue_action, YOUR_TEMPLATE_FILE);     defer {         free(html_string);         free(error_message);     }     if !success {         log_error(error_message);         return;     }     log("%", html_string);     #if false {         #import "File";         write_entire_file("01_example.html", html_string);     }     /** If you want to know, what happens if you supply wrong paramaters,         visit `test/test.jai`. It contains several test cases with brief         explainations. */ } diff --git a/examples/modules/htmltemplate b/examples/modules/htmltemplate new file mode 120000 index 0000000..e992cb6 --- /dev/null +++ b/examples/modules/htmltemplate @@ -0,0 +1 @@ ../../htmltemplate \ No newline at end of file diff --git a/htmltemplate/lib.jai b/htmltemplate/lib.jai new file mode 100644 index 0000000..bc406b3 --- /dev/null +++ b/htmltemplate/lib.jai @@ -0,0 +1,615 @@ /*  * MIT License  *  * Copyright (c) 2026 dev@ptrace.dev  *  * Permission is hereby granted, free of charge, to any person obtaining a copy  * of this software and associated documentation files (the "Software"), to deal  * in the Software without restriction, including without limitation the rights  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell  * copies of the Software, and to permit persons to whom the Software is  * furnished to do so, subject to the following conditions:  *  * The above copyright notice and this permission notice shall be included in all  * copies or substantial portions of the Software.  *  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE  * SOFTWARE.  *  */ #import "Basic"; #import "File"; #import "String"; #import "File_Utilities"; #import "Hash_Table"; #import "Sort"; #import "Math"; /** READ_FROM_TEMPLATE_FILE: If false, you can pass the template string,     instead of the filepath to the template file.     CREATE_HTML_FILE: If true, it will create the final rendered html file by     the path you provided.     In any case, it still returns a string. */ #module_parameters () (READ_FROM_TEMPLATE_FILE := true, CREATE_HTML_FILE := false); Exit_Codes :: enum s32 #specified {     OK :: 0;     ERR_FILE        :: 1;     ERR_TOKENIZER   :: 2;     ERR_ACTION      :: 3;     ERR_REPLACE     :: 4; } Action :: struct {     id: string;     using value: Value; } generate :: (     queue_action: [..]Action,     template_fp_or_string: string,     html_fp: string = "" ) -> (success: bool, html: string, exit_code: Exit_Codes, error_message: string) {     return_if_err :: (exit_code: Exit_Codes) #expand {         if had_error then `return false, "", exit_code, error_string;     }     had_error = false;     error_string = "";     line_no = 1;     tokens: [..]Token;     queue_replace: [..]Replace_Token;     defer {         array_free(tokens);         array_free(queue_replace);     }     #if READ_FROM_TEMPLATE_FILE {         source := file_acquire(template_fp_or_string);         return_if_err(.ERR_FILE);     } else {         source := template_fp_or_string;     }     token_success := tokenize(source, *tokens);     return_if_err(.ERR_TOKENIZER);     token_lookup := token_lookup_table_create(tokens);     defer deinit(*token_lookup);     #if false {         log("-- Tokens ----------------------");         for tokens log("%", it);         log("--------------------------------\n\n");     }     template_action(*queue_replace, queue_action, token_lookup);     return_if_err(.ERR_ACTION);     html_new := template_replace(queue_replace, source);     return_if_err(.ERR_REPLACE);     fw_success := file_write(html_new, html_fp);     return_if_err(.ERR_FILE);     return true, html_new, .OK, ""; } commit :: (queue_action: *[..]Action, id: string, value: [][]string) {     action: Action = {         id = id,         nested = value,         kind = .LOOP     };     array_add(queue_action, action); } commit :: (queue_action: *[..]Action, id: string, value: ..string) {     action: Action = {         id = id,         flat = value,         kind = .REPLACE     };     array_add(queue_action, action); } commit :: (queue_action: *[..]Action, id: string, value: []string) {     commit(queue_action, id, ..value); } commit :: (id: string, value: [][]string) #expand {     commit(*`queue_action, id, value); } commit :: (id: string, value: ..string) #expand {     commit(*`queue_action, id, ..value); } // Needed for unit tests commit :: (id: string, value: []string) #expand {     commit(*`queue_action, id, ..value); } #scope_file had_error: bool; error_string: string; line_no: int; ERR_PARSE_INTEGER :: #string STR_END Could not parse integer. Character in question: %1 The HTML snippet: %2 STR_END; ERR_VALUE_OOB :: #string STR_END Cannot access your value, since it's out of bounds. The index: %1 The array: %2 STR_END; ERR_TOO_MANY_PLACEHOLDERS :: #string STR_END There are more placeholders than provided values. The HTML snippet: %1 Your values: %2 STR_END; ERR_REPLACE_WRONG_TYPE :: #string STR_END Wrong value type commited. Expected type: `..string` or `[]string`. STR_END; ERR_LOOP_WRONG_TYPE :: #string STR_END Wrong value type commited. Maybe you tried to pass a single string, or an one-dimensional array instead of a nested list. Expected type: `[][]string`. STR_END; Token_Kind :: enum u8 {     NONE :: 0;     REPLACE;     LOOP; } Token :: struct {     kind: Token_Kind;     id: string;     html: string;     position_start: int;     position_end: int; } Value :: union kind: Token_Kind {     flat: []string;     nested: [][]string; } Replace_Token :: struct {     position_start: int;     position_end: int;     text: string; } template_replace :: (replace_q: [..]Replace_Token, source: string) -> string {     compare_pos_descending :: (a: Replace_Token, b: Replace_Token) -> s64 {         aa := a.position_start;         bb := b.position_start;         return xx compare_floats(xx bb, xx aa);     }     replacemes := quick_sort(replace_q, compare_pos_descending);     idx: int;     buf: String_Builder;     init_string_builder(*buf);     while idx < source.count {         if replacemes.count != 0 && peek(replacemes).position_start == idx {             replaceme := peek(replacemes);             for replaceme.text  append(*buf, it);             idx = replaceme.position_end;             pop(*replacemes);         } else {             append(*buf, source[idx]);             idx += 1;         }     }     out := builder_to_string(*buf);     return out; } template_action :: (     replace_q: *[..]Replace_Token,     action_q: [..]Action,     lookup: Table(string, Token) ) {     assert_kind :: (msg: string) #expand {         if `token.kind != `it.kind {             had_error = true;             error_string = msg;             `return;         }     }     for action_q {         bail();         success, token := table_find(*lookup, it.id);         if !success continue;         rp: Replace_Token;         rp.position_start = token.position_start;         rp.position_end = token.position_end;         if token.kind == {         case .REPLACE;             assert_kind(ERR_REPLACE_WRONG_TYPE);             rp.text = handle_replace(it.flat, token,, temp);         case .LOOP;             assert_kind(ERR_LOOP_WRONG_TYPE);             rp.text = handle_loop(it.nested, token,, temp);         }         array_add(replace_q, rp);     } } handle_loop :: (value: [][]string, token: Token) -> string {     buf: String_Builder;     init_string_builder(*buf);     for value {         replaced := handle_replace(it, token);         bail("");         append(*buf, replaced);         append(*buf, "\n");     }     out := builder_to_string(*buf);     return out; } handle_replace :: (value: []string, token: Token) -> string {     buf: String_Builder;     init_string_builder(*buf);     if token.html {         html_scan(value, token, *buf);         bail("");     } else {         append(*buf, value[0]);     }     out := builder_to_string(*buf);     return out; } html_scan :: (value: []string, token: Token, buffer: *String_Builder) {     idx: int;     iteration_skip := false;     guard_next_char := false;     using token;     for html {         if iteration_skip {             iteration_skip = false;             continue;         }         prev := clamp(it_index-1, 0, html.count);         next := ifx (it_index+1 > html.count-1) then 0 else it_index+1;         // Add the next char without processing         if it == "\\" && html[next] == "%" {             guard_next_char = true;             continue;         }         if !guard_next_char && it == "%" {             // Populate the same value to every placeholder             if value.count == 1 {                 if is_digit(html[next]) then iteration_skip = true;                 append(buffer, value[0]);             // If user used %n, populate the values by index of `value`             } else if is_digit(html[next]) {                 html_arguments_numbered(html, next, value, buffer);                 bail();                 iteration_skip = true;             // Populate the value by position of the placeholders             } else {                 if idx > value.count-1 {                     error_string = sprint(ERR_TOO_MANY_PLACEHOLDERS, html, value);                     had_error = true;                     return;                 }                 append(buffer, value[idx]);                 idx += 1;             }         } else {             guard_next_char = false;             append(buffer, it);         }     } } html_arguments_numbered :: inline (     html: string,     next: int,     value: []string,     buffer: *String_Builder ) {     str := string.{ 1, *html[next] };     _num, success := parse_int(*str);     if !success {         // Note: `string.{ 1, foo }` Does not work with non-ASCII srings         error_string = sprint(ERR_PARSE_INTEGER, string.{ 1, *html[next] }, html);         had_error = true;         return;     }     num := ifx _num == 0 then 1 else _num - 1;     if num > value.count-1 {         error_string = sprint(ERR_VALUE_OOB, num, value);         had_error = true;         return;     }     append(buffer, value[num]); } bail :: (value: $T) #expand {     if had_error `return value; } bail :: () #expand {     if had_error `return; } file_acquire :: (fp: string) -> data: string {     data, success := read_entire_file(fp);     if !success {         error_string = sprint("Could not read file: %", fp);         had_error = true;     }     return data; } file_write :: (data: string, fp: string) {     #if !CREATE_HTML_FILE return;     success := write_entire_file(fp, data);     if !success {         error_string = sprint("Could not write into file: %", fp);         had_error = true;     } } // ------------------------------------ // -- "Tokenizer" --------------------- // ------------------------------------ adv :: () -> u8 #expand {     value := `source[`current];     `current += 1;     return value; } adv :: (current: *int) -> u8 #expand {     value := `source[current.*];     current.* += 1;     return value; } peek :: () -> u8 #expand {     return `source[`current]; } peek :: (current: int) -> u8 #expand {     return `source[current]; } peek_next :: () -> u8 #expand {     if `current + 1 >= `source.count-1 then return "\0";     return `source[`current+1]; } is_at_end :: () -> bool #expand {     return `current >= `source.count-1; } count_newlines :: inline (s: string) {     for s {         if s == "\n" then line_no += 1;     } } consume_till :: (terminator: u8) -> (success: bool, slice: string, pos: int) #expand {     idx := find_index_from_left(`source, terminator, `current.*);     if idx == -1 {         // string.{ 1, *x } will explode if character is non-ASCII         error_string = sprint(             "Line %: Could not find terminator: '%'", line_no, string.{ 1, *terminator }         );         return false, "", 0;     }     offset := idx - `current.*;     out := slice(`source, `current.*, offset);     count_newlines(out);     `current.* = idx + 1;     return true, out, idx; } consume_till :: (terminator: string) -> (success: bool, slice: string, pos: int) #expand {     idx := find_index_from_left(`source, terminator, `current.*);     if idx == -1 {         error_string = sprint(             "Line %: Could not find terminator: '%'", line_no, terminator         );         return false, "", 0;     }     offset := idx - `current.*;     out := slice(`source, `current.*, offset);     count_newlines(out);     `current.* = idx + terminator.count - 1;     return true, out, idx; } tokenize :: (source: string, tokens: *[..]Token) {     current: int;     while !is_at_end() && !had_error {         c := adv();         if c == "\n" { line_no += 1; }         else if c == "{" && peek() == "{" {             position_start := current - 1;             adv();  // consume second '{'             if peek() == " " || peek() == "\t" then adv();             token_kind := token_determine_kind(source, *current);             token_create(tokens, token_kind, source, *current, position_start);         }     } } token_determine_kind :: (source: string, current: *int) -> Token_Kind {     match :: (m: string) -> bool #expand {         temp := `current.*;         for m {             if it != `source[temp] return false;             temp += 1;         }         `current.* = temp;         return true;     }     if match(":")     { return .REPLACE; }     if match("loop:") { return .LOOP; }     had_error = true;     error_string = sprint("Line %: Expected `:` or `loop` here", line_no);     return .NONE; } token_create :: (     tokens: *[..]Token,     kind: Token_Kind,     source: string,     current: *int,     position_start: int ) {     create :: (kind: Token_Kind) #expand {         token := token_consume(`source, `current, kind, position_start);         array_add(`tokens, token);     }     if kind == {     case .REPLACE; create(.REPLACE);     case .LOOP;    create(.LOOP);     } } token_consume :: (     source: string,     current: *int,     kind: Token_Kind,     position_start: int ) -> Token {     term: u8 = ":";     success, id := consume_till(term);     id_trimmed := trim(id);     if !id_trimmed {         error_string = sprint("Line %: Expected an identifier here", line_no);         had_error = true;         return {};     }     if !success {         had_error = true;         return {};     }     terminator_end :: "}}";     success2, html, position_end := consume_till(terminator_end);     html_trimmed := trim(html);     if !success2 {         had_error = true;         return {};     }     return .{         kind = kind,         id = id_trimmed,         html = html_trimmed,         position_start = position_start,         position_end = position_end + terminator_end.count,     }; } token_lookup_table_create :: (tokens: [..]Token) -> Table(string, Token) {     action_map: Table(string, Token);     init(*action_map);     for token: tokens {         table_set(*action_map, token.id, token);     }     return action_map; } diff --git a/htmltemplate/module.jai b/htmltemplate/module.jai new file mode 100644 index 0000000..60423ff --- /dev/null +++ b/htmltemplate/module.jai @@ -0,0 +1 @@ #load "lib.jai"; diff --git a/modules/stringpad/lib.jai b/modules/stringpad/lib.jai new file mode 100644 index 0000000..db48b1d --- /dev/null +++ b/modules/stringpad/lib.jai @@ -0,0 +1,82 @@ /*  * MIT License  *  * Copyright (c) 2026 dev@ptrace.dev  *  * Permission is hereby granted, free of charge, to any person obtaining a copy  * of this software and associated documentation files (the "Software"), to deal  * in the Software without restriction, including without limitation the rights  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell  * copies of the Software, and to permit persons to whom the Software is  * furnished to do so, subject to the following conditions:  *  * The above copyright notice and this permission notice shall be included in all  * copies or substantial portions of the Software.  *  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE  * SOFTWARE.  *  */ #import "Basic"; #import "Math"; string_pad_left :: (s: string, padding: int, char: string = " ") -> string {     code :: #string DONE         for 1..length {             append(*sb, `char);         }         append(*sb, `s);     DONE;     __string_pad(code); } string_pad_right :: (s: string, padding: int, char: string = " ") -> string {     code :: #string DONE         append(*sb, `s);         for 1..length {             append(*sb, `char);         }     DONE;     __string_pad(code); } string_pad_lr :: (s: string, padding: int, char: string = " ") -> string {     code :: #string DONE         half: int = xx floor(xx length / 2.0);         for 1..half {             append(*sb, `char);         }         append(*sb, `s);         for 1..half {             append(*sb, `char);         }     DONE;     __string_pad(code); } #scope_file __string_pad :: ($code: string) #expand {     sb: String_Builder;     init_string_builder(*sb);     length := `padding - `s.count;     #insert code;     `return builder_to_string(*sb); } diff --git a/modules/stringpad/module.jai b/modules/stringpad/module.jai new file mode 100644 index 0000000..60423ff --- /dev/null +++ b/modules/stringpad/module.jai @@ -0,0 +1 @@ #load "lib.jai"; diff --git a/test/test.jai b/test/test.jai new file mode 100644 index 0000000..3751123 --- /dev/null +++ b/test/test.jai @@ -0,0 +1,435 @@ #import "Basic"()(     MEMORY_DEBUGGER = MEMORY_DEBUGGER_ENABLED  // Enable it via `jai build.jai - memory` ); #import "htmltemplate"()(     READ_FROM_TEMPLATE_FILE = false,    // Consult the lib.jai file     CREATE_HTML_FILE = false            // for more information. ); #import "stringpad";  // Not needed for the lib. Only for unit tests /** Omit logs from individual unit tests */ DONT_YAP :: false; test_cases_count: int; tests_passed: bool = true; tests_failed: [..]int; Test_Case :: struct(T: Type, R: Type) {     id: string;     values: T;     template: string;     expected: R; } main :: () {     #if MEMORY_DEBUGGER_ENABLED defer report_memory_leaks();     defer this_allocation_is_not_a_leak(tests_failed.data);     /** Replace a single value */     {         test: Test_Case(string, string);         test.id = "foo";         test.values = "test123";         test.template = "<h1>{{ :foo: }}</h1>";         test.expected = "<h1>test123</h1>";         test_run_and_store_result(test_html);     }     /** If only one value is provided, but more placeholders,         populate the same value.         Note: the final html string is trimmed from both sides! */     {         test: Test_Case(string, string);         test.id = "foo";         test.values = "test123";         test.template = "<h1>{{ :foo: % and % }}</h1>";         test.expected = "<h1>test123 and test123</h1>";         test_run_and_store_result(test_html);     }     /** Explicit positional placeholders.         You can also omit whitespaces. */     {         test: Test_Case([]string, string);         test.id = "foo";         test.values = .[ "test123", "abcdef" ];         test.template = "<h1>{{:foo:%1 and %2}}</h1>";         test.expected = "<h1>test123 and abcdef</h1>";         test_run_and_store_result(test_html);     }     /** Mixing numerated and not-numerated placeholders.         This works, as long there aren't too many of it.         See one test below what happens. */     {         test: Test_Case([]string, string);         test.id = "foo";         test.values = .[ "test123", "abcdef" ];         test.template = "<h1>{{ :foo: %1 and %2 \nand % and % }}</h1>";         test.expected = "<h1>test123 and abcdef \nand test123 and abcdef</h1>";         test_run_and_store_result(test_html);     }     /** More placeholders than values */     {         test: Test_Case([]string, bool);         test.id = "foo";         test.values = .[ "test123", "abcdef" ];         test.template = "<h1>{{ :foo: %1 and %2 and % and % and % }}</h1>";         test.expected = false;         test_run_and_store_result(test_success);     }     /** If you want a plain `%` just escape it -> `\%`.         Keep in mind to escape the `\` too if using it in Jai strings */     {         test: Test_Case([]string, string);         test.id = "foo";         test.values = .[ "test123", "abcdef" ];         test.template = "<h1>{{ :foo: % but escape this -> \\% }}</h1>";         test.expected = "<h1>test123 but escape this -> %</h1>";         test_run_and_store_result(test_html);     }     /** You don't have to use all values. In this case we don't use `123123`.         Also note how we mix numerated and not-numerated placeholders:                      1          2         3      4         values:    [ "test123", "abcdef", "123", "123123" ]                                 1                                  2       3                                 |                                  |       |         template:  <h1>{{ :foo: %,       %1,      %1,      %2,     %,      %   }} </h1>                                 |        |        |        |       |       |         rendered:  <h1>         test123, test123, test123, abcdef, abcdef, 123    </h1> */     {         test: Test_Case([]string, string);         test.id = "foo";         test.values = .[ "test123", "abcdef", "123", "123123" ];         test.template = "<h1>{{ :foo: %, %1, %1, %2, %, % \\%\\% \\%}}</h1>";         test.expected = "<h1>test123, test123, test123, abcdef, abcdef, 123 %% %</h1>";         test_run_and_store_result(test_html);     }     /** Loops are following the same template behavior as above,         but they additionally repeat the template.         The amount of loops is defined by how big the         outer array of the values is. */     {         test: Test_Case([][]string, string);         test.id = "foo";         test.values = .[             .[ "#link1", "target=\"_blank\"", "My Link 1" ],             .[ "#link2", "", "My Link 2" ],         ];         test.template = "{{ loop:foo:<a href=\"%\" %>%</a> }}";         test.expected = #string STR_END <a href="#link1" target="_blank">My Link 1</a> <a href="#link2" >My Link 2</a>             STR_END;         test_run_and_store_result(test_html);     }     /** Reusing identifier will silently fail and only         populate the last placeholder.         Maybe I'll support reusing identifiers in the future.         TODO: @Reminder */     {         test: Test_Case(string, string);         test.id = "foo1";         test.values = "testABC";         test.template = "<h1>{{ :foo1: }}</h1><h1>{{ :foo1: }}</h1><h1>{{ :foo1: }}</h1>";         test.expected = "<h1>{{ :foo1: }}</h1><h1>{{ :foo1: }}</h1><h1>testABC</h1>";         test_run_and_store_result(test_html);     }     /** Missing postfix `:` */     {         test: Test_Case(string, bool);         test.id = "foo";         test.values = "test123";         test.template = "<h1>{{ :foo }}</h1>";         test.expected = false;         test_run_and_store_result(test_success);     }     /** Missing prefix `:` */     {         test: Test_Case(string, bool);         test.id = "foo";         test.values = "test123";         test.template = "<h1>{{ foo: }}</h1>";         test.expected = false;         test_run_and_store_result(test_success);     }     /** Empty identifier */     {         test: Test_Case(string, bool);         test.id = "foo";         test.values = "test123";         test.template = "<h1>{{ :: }}</h1>";         test.expected = false;         test_run_and_store_result(test_success);     }     /** Incomplete terminator */     {         test: Test_Case(string, bool);         test.id = "foo";         test.values = "test123";         test.template = "<h1>{{ :foo: }</h1>";         test.expected = false;         test_run_and_store_result(test_success);     }     /** Incomplete definition. Not an error. */     {         test: Test_Case(string, bool);         test.id = "foo";         test.values = "test123";         test.template = "<h1>{ :foo: }}</h1>";         test.expected = true;         test_run_and_store_result(test_success);     }     /** Test for many newlines and dangling placeholders */     {         test: Test_Case([]string, string);         test.id = "foo";         test.values = .[ "test123", "abcdef" ];         test.template = "<h1>{{ :foo: %1\n\n%1\n\n\n% 1 \n\n }}</h1>";         test.expected = "<h1>test123\n\ntest123\n\n\ntest123 1</h1>";         test_run_and_store_result(test_html);     }     /** Test if numbered placeholders getting correctly stripped */     {         test: Test_Case([][]string, string);         test.id = "foo";         test.values = .[             .["A", "a", "aa", "aaa"],             .["B"],             .["C", "c", "cc", "ccc"],             .["D"],         ];         test.template = "{{loop:foo:<h1><span><span class=\"%1\">%1_%2_%3_%4_\\%_%4_%3_%2_%1</span></span></h1>}}";         test.expected = #string STR_END <h1><span><span class="A">A_a_aa_aaa_%_aaa_aa_a_A</span></span></h1> <h1><span><span class="B">B_B_B_B_%_B_B_B_B</span></span></h1> <h1><span><span class="C">C_c_cc_ccc_%_ccc_cc_c_C</span></span></h1> <h1><span><span class="D">D_D_D_D_%_D_D_D_D</span></span></h1>         STR_END;         test_run_and_store_result(test_html);     }     /** If user declared a loop, but supplied a single string */     {         test_run_and_store_result(test_case_loop_wrong_overload_1);     }     /** If user declared a loop, but supplied a 1D string array */     {         test_run_and_store_result(test_case_loop_wrong_overload_2);     }     /** If user declared a replace, but supplied a 2D string array */     {         test_run_and_store_result(test_case_replace_wrong_overload);     }     log("\n-- Test Run Result ------------------------");     if tests_passed {         log("All % tests passed.", test_cases_count);         #if !MEMORY_DEBUGGER_ENABLED exit(0); else return;     }     log_error("% tests failed:", tests_failed.count);     for tests_failed log_error("  - %", it);     log_error("\n");     #if !MEMORY_DEBUGGER_ENABLED exit(1); else return; } // ------------------------------------ // Individual test cases // --------------------- test_case_loop_wrong_overload_1 :: () -> success: bool {     queue_action: [..]Action;     defer this_allocation_is_not_a_leak(queue_action.data);     commit("foo_loop", "bar");     template := "{{ loop:foo_loop: %1 %2 %3 }}";     success, _, exit_code, error_message := generate(         queue_action, template     ,, temp);     if !success {         dbg("\nMessage from lib:");         dbg("(Exit Code: %) - %", exit_code, error_message);     }     if success == false then return true;     return false; } test_case_loop_wrong_overload_2 :: () -> success: bool {     queue_action: [..]Action;     defer this_allocation_is_not_a_leak(queue_action.data);     commit("foo_loop", .["string"]);     template := "{{ loop:foo_loop: %1 %2 %3 }}";     success, _, exit_code, error_message := generate(         queue_action, template     ,, temp);     if !success {         dbg("\nMessage from lib:");         dbg("(Exit Code: %) - %", exit_code, error_message);     }     if success == false then return true;     return false; } test_case_replace_wrong_overload :: () -> success: bool {     queue_action: [..]Action;     defer this_allocation_is_not_a_leak(queue_action.data);     commit("foo", .[.["foo"]]);     template := "{{ :foo: %1 %2 %3 }}";     success, _, exit_code, error_message := generate(         queue_action, template     ,, temp);     if !success {         dbg("\nMessage from lib:");         dbg("(Exit Code: %) - %", exit_code, error_message);     }     if success == false then return true;     return false; } // ------------------------------------ // Unit test internals // ------------------- __HEADER :: #string STR_END         test_cases_count += 1;         count := string_pad_left(tprint("%", test_cases_count), 2, "0",, temp);         dbg("-- Test % --------------------------------", count);     STR_END; __FOOTER :: #string STR_END         tests_passed &= success;         if !success array_add(*tests_failed, test_cases_count);         result := ifx success then "Test passed |" else "Test failed |";         dbg("\n%\n\n", string_pad_left(result, 56 - result.count,, temp));     STR_END; test_run_and_store_result :: ($proc: () -> bool) #expand {     #insert __HEADER;     success := proc();     #insert __FOOTER; } test_run_and_store_result :: ($proc: (Test_Case) -> bool) #expand {     #insert __HEADER;     success := proc(`test);     #insert __FOOTER; } test_html :: (tc: Test_Case) -> success: bool {     queue_action: [..]Action;     defer this_allocation_is_not_a_leak(queue_action.data);     commit(tc.id, tc.values);     success, html_new, exit_code, error_message := generate(         queue_action, tc.template     ,, temp);     dbg("Values:    %", tc.values);     dbg("Template:  %\n\n", tc.template);     dbg("GIVEN:    »%«", html_new);     dbg("EXPECTED: »%«", tc.expected);     if !success {         dbg("\nMessage from lib:");         dbg("(Exit Code: %) - %", exit_code, error_message);         return false;     }     if html_new == tc.expected then return true;     return false; } test_success :: (tc: Test_Case) -> success: bool {     queue_action: [..]Action;     defer this_allocation_is_not_a_leak(queue_action.data);     commit(tc.id, tc.values);     success, html_new, exit_code, error_message := generate(         queue_action, tc.template     ,, temp);     dbg("Values:   %", tc.values);     dbg("Template: %\n\n", tc.template);     dbg("Given:    %", success);     dbg("Expected: %", tc.expected);     if !success {         dbg("\nMessage from lib:");         dbg("(Exit Code: %) - %", exit_code, error_message);     }     if success == tc.expected then return true;     return false; } dbg :: (s: string, a: ..Any) {     #if !DONT_YAP {         log(s, ..a);     } }