Logo

index : htmltemplate

---

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