cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
template.c
Go to the documentation of this file.
3#include <stdio.h>
4#include <stdlib.h>
5#include <string.h>
6#include <ctype.h>
7#include <stdbool.h>
8
15static cwist_sstring *render_internal(const char **template_str, const cJSON *context, int depth);
16
18#define CWIST_TEMPLATE_MAX_DEPTH 32
19
23static void html_escape(const char *value, char *out, size_t out_len) {
24 if (!value || out_len == 0) return;
25 size_t i, j;
26 for (i = 0, j = 0; value[i] && j < out_len - 1; i++) {
27 const char *repl = NULL;
28 switch (value[i]) {
29 case '&': repl = "&amp;"; break;
30 case '<': repl = "&lt;"; break;
31 case '>': repl = "&gt;"; break;
32 case '"': repl = "&quot;"; break;
33 case '\'': repl = "&#39;"; break;
34 default: break;
35 }
36 if (repl) {
37 size_t rlen = strlen(repl);
38 if (j + rlen >= out_len) break;
39 memcpy(out + j, repl, rlen);
40 j += rlen;
41 } else {
42 out[j++] = value[i];
43 }
44 }
45 out[j] = '\0';
46}
47
56static void apply_filter(const char *value, const char *filter, char *out, size_t out_len) {
57 if (!filter || out_len == 0) return;
58 if (strcmp(filter, "upper") == 0) {
59 size_t i;
60 for (i = 0; i < out_len - 1 && value && value[i]; i++)
61 out[i] = (char)toupper((unsigned char)value[i]);
62 out[i] = '\0';
63 } else if (strcmp(filter, "lower") == 0) {
64 size_t i;
65 for (i = 0; i < out_len - 1 && value && value[i]; i++)
66 out[i] = (char)tolower((unsigned char)value[i]);
67 out[i] = '\0';
68 } else if (strcmp(filter, "escape") == 0 || strcmp(filter, "e") == 0) {
69 html_escape(value, out, out_len);
70 } else if (strcmp(filter, "trim") == 0) {
71 if (!value || value[0] == '\0') {
72 out[0] = '\0';
73 return;
74 }
75 const char *s = value;
76 while (isspace((unsigned char)*s)) s++;
77 /* Guard against all-whitespace input: s may have advanced past the
78 * last character, making e = value + vlen - 1 < s. */
79 size_t vlen = strlen(value);
80 const char *e = value + vlen - 1;
81 while (e > s && isspace((unsigned char)*e)) e--;
82 size_t len = (s > e) ? 0 : (size_t)(e - s + 1);
83 if (len >= out_len) len = out_len - 1;
84 memcpy(out, s, len);
85 out[len] = '\0';
86 } else if (strcmp(filter, "length") == 0 || strcmp(filter, "len") == 0) {
87 snprintf(out, out_len, "%zu", value ? strlen(value) : 0);
88 } else if (strncmp(filter, "default", 7) == 0) {
89 const char *arg = NULL;
90 const char *lp = strchr(filter, '(');
91 const char *rp = strrchr(filter, ')');
92 if (lp && rp && rp > lp) {
93 arg = lp + 1;
94 }
95 if (value && value[0]) {
96 snprintf(out, out_len, "%s", value);
97 } else if (arg) {
98 size_t arg_len = (size_t)(rp - arg);
99 if (arg_len >= out_len) arg_len = out_len - 1;
100 memcpy(out, arg, arg_len);
101 out[arg_len] = '\0';
102 } else {
103 out[0] = '\0';
104 }
105 } else {
106 if (value) {
107 snprintf(out, out_len, "%s", value);
108 } else {
109 out[0] = '\0';
110 }
111 }
112}
113
120static const cJSON *get_value_from_context(const cJSON *context, const char *key) {
121 if (!context || !key) return NULL;
122
123 // Handle the special case of "." referring to the current context in a loop
124 if (strcmp(key, ".") == 0) {
125 return context;
126 }
127
128 char *key_copy = cwist_strdup(key);
129 if (!key_copy) return NULL;
130 char *ptr_to_free = key_copy;
131 char *saveptr = NULL;
132 char *token = strtok_r(key_copy, ".", &saveptr);
133 const cJSON *current = context;
134
135 while (token != NULL) {
136 if (!cJSON_IsObject(current)) {
137 current = NULL;
138 break;
139 }
141 if (!current) break;
142 token = strtok_r(NULL, ".", &saveptr);
143 }
144
146 return current;
147}
148
155static cwist_sstring *render_internal(const char **template_str, const cJSON *context, int depth) {
157
159 if (!output) return NULL;
160 const char *p = *template_str;
161 const char *start = p;
162
163 while (*p) {
164 if (p[0] == '{' && p[1] == '#') {
165 /* Comment: {# ... #} -- skip silently. */
167 p += 2;
168 while (*p && !(p[0] == '#' && p[1] == '}')) p++;
169 if (p[0] == '#' && p[1] == '}') p += 2;
170 start = p;
171 continue;
172 }
173 if (p[0] == '{' && (p[1] == '{' || p[1] == '%')) {
174 /* Append text since last tag */
176
177 if (p[1] == '{') { /* Variable: {{ key [| filter] }} */
178 p += 2;
179 const char *var_start = p;
180 while (*p && (p[0] != '}' || p[1] != '}')) p++;
181
182 char var_buf[256] = {0};
183 size_t var_len = (size_t)(p - var_start);
184 if (var_len >= sizeof(var_buf)) var_len = sizeof(var_buf) - 1;
186
187 char *trimmed_var = var_buf;
188 while (*trimmed_var == ' ') trimmed_var++;
189 char *end = trimmed_var + strlen(trimmed_var) - 1;
190 while (end > trimmed_var && *end == ' ') *end-- = '\0';
191
192 char *filter = NULL;
193 char *pipe = strstr(trimmed_var, "|");
194 if (pipe) {
195 *pipe = '\0';
196 char *ve = pipe - 1;
197 while (ve >= trimmed_var && isspace((unsigned char)*ve)) *ve-- = '\0';
198 filter = pipe + 1;
199 while (*filter == ' ') filter++;
200 char *fe = filter + strlen(filter) - 1;
201 while (fe > filter && *fe == ' ') *fe-- = '\0';
202 }
203
204 const cJSON *value = get_value_from_context(context, trimmed_var);
205 if (value) {
206 char num_str[64] = {0};
207 const char *raw = NULL;
208 if (cJSON_IsString(value)) {
209 raw = value->valuestring;
210 } else if (cJSON_IsNumber(value)) {
211 snprintf(num_str, sizeof(num_str), "%g", value->valuedouble);
212 raw = num_str;
213 } else if (cJSON_IsTrue(value)) {
214 raw = "true";
215 } else if (cJSON_IsFalse(value)) {
216 raw = "false";
217 }
218 if (raw) {
219 if (filter) {
220 char filtered[512] = {0};
221 apply_filter(raw, filter, filtered, sizeof(filtered));
223 } else {
225 }
226 }
227 }
228 p += 2;
229 start = p;
230
231 } else if (p[1] == '%') { /* Tag: {% ... %} */
232 p += 2;
233 const char *tag_start = p;
234 while (*p && (p[0] != '%' || p[1] != '}')) p++;
235
236 char tag[256] = {0};
237 size_t tag_len = (size_t)(p - tag_start);
238 if (tag_len >= sizeof(tag)) tag_len = sizeof(tag) - 1;
239 memcpy(tag, tag_start, tag_len);
240
241 char *tag_saveptr = NULL;
242 char *cmd = strtok_r(tag, " \t\n", &tag_saveptr);
243 if (!cmd) {
244 p += 2;
245 start = p;
246 continue;
247 }
248
249 if (strcmp(cmd, "if") == 0) {
250 char *tok = strtok_r(NULL, " \t\n", &tag_saveptr);
251 bool negate = false;
252 if (tok && strcmp(tok, "not") == 0) {
253 negate = true;
254 tok = strtok_r(NULL, " \t\n", &tag_saveptr);
255 }
256 const cJSON *val = tok ? get_value_from_context(context, tok) : NULL;
257 bool cond = false;
258 if (val) {
259 cond = cJSON_IsTrue(val) ||
260 (cJSON_IsString(val) && strlen(val->valuestring) > 0) ||
263 (cJSON_IsNumber(val) && val->valuedouble != 0);
264 }
265 if (negate) cond = !cond;
266
267 const char *block_start = p + 2;
268 const char *else_pos = NULL;
269 const char *block_end = NULL;
270 const char *scan = block_start;
271 int nested = 0;
272 while (*scan) {
273 if (scan[0] == '{' && scan[1] == '%') {
274 const char *inner = scan + 2;
275 while (*inner == ' ' || *inner == '\t' || *inner == '\n') inner++;
276 if (strncmp(inner, "if ", 3) == 0 || strncmp(inner, "if\t", 3) == 0 ||
277 strncmp(inner, "if\n", 3) == 0) {
278 nested++;
279 } else if (strncmp(inner, "endif", 5) == 0) {
280 if (nested == 0) {
281 block_end = scan;
282 break;
283 }
284 nested--;
285 } else if (!else_pos && nested == 0 && strncmp(inner, "else", 4) == 0 &&
286 (inner[4] == ' ' || inner[4] == '\t' || inner[4] == '\n' ||
287 inner[4] == '%')) {
288 else_pos = scan;
289 }
290 }
291 scan++;
292 }
293
294 if (cond) {
295 const char *block_p = block_start;
297 render_internal(&block_p, context, depth + 1);
298 if (rendered_block) {
301 }
302 } else if (else_pos) {
303 const char *else_close = strstr(else_pos, "%}");
304 const char *else_p =
305 else_close ? else_close + 2 : else_pos + strlen("{% else %}");
307 render_internal(&else_p, context, depth + 1);
308 if (rendered_block) {
311 }
312 }
313
314 if (block_end) {
315 const char *endif_close = strstr(block_end, "%}");
316 p = endif_close ? endif_close + 2 : block_end + strlen("{% endif %}");
317 } else {
318 p += 2;
319 }
320 start = p;
321
322 } else if (strcmp(cmd, "for") == 0) {
323 char *item_name = strtok_r(NULL, " \t\n", &tag_saveptr);
324 strtok_r(NULL, " \t\n", &tag_saveptr); /* "in" */
325 char *array_name = strtok_r(NULL, " \t\n", &tag_saveptr);
326
327 const cJSON *array = (array_name && context)
328 ? get_value_from_context(context, array_name)
329 : NULL;
330
331 const char *block_start = p + 2;
332 /* Scan for the matching endfor, counting nested for tags
333 * so that {% for %}...{% for %}...{% endfor %}...{% endfor %}
334 * is handled correctly. */
335 const char *block_end = NULL;
336 {
337 const char *scan = block_start;
338 int depth = 0;
339 while (*scan) {
340 if (scan[0] == '{' && scan[1] == '%') {
341 const char *tag = scan + 2;
342 while (*tag == ' ' || *tag == '\t' || *tag == '\n') tag++;
343 if (strncmp(tag, "for ", 4) == 0 ||
344 strncmp(tag, "for\t", 4) == 0 ||
345 strncmp(tag, "for\n", 4) == 0) {
346 depth++;
347 } else if (strncmp(tag, "endfor", 6) == 0) {
348 if (depth == 0) {
349 block_end = scan;
350 break;
351 }
352 depth--;
353 }
354 }
355 scan++;
356 }
357 }
358
359 if (item_name && cJSON_IsArray(array)) {
360 cJSON *item;
363 if (!loop_context) continue;
366
367 const char *loop_p = block_start;
369 render_internal(&loop_p, loop_context, depth + 1);
370 if (rendered_block) {
373 }
375 }
376 }
377 if (block_end) {
378 const char *endfor_close = strstr(block_end, "%}");
379 p = endfor_close ? endfor_close + 2 : block_end + strlen("{% endfor %}");
380 } else {
381 p += 2;
382 }
383 start = p;
384
385 } else if (strcmp(cmd, "include") == 0) {
386 char *inc_cmd = strstr(tag, "include");
387 char *path = inc_cmd ? inc_cmd + strlen("include") : NULL;
388 if (path) {
389 while (*path == ' ' || *path == '\t' || *path == '\n') path++;
390 char *end = path + strlen(path) - 1;
391 while (end > path &&
392 (*end == ' ' || *end == '\t' || *end == '\n' || *end == '"'))
393 *end-- = '\0';
394 if (path[0] == '"') path++;
395 if (path[0]) {
397 if (included) {
400 }
401 }
402 }
403 p += 2;
404 start = p;
405
406 } else if (strcmp(cmd, "endif") == 0 || strcmp(cmd, "endfor") == 0 ||
407 strcmp(cmd, "else") == 0) {
408 *template_str = p + 2;
409 return output;
410 } else {
411 p += 2;
412 start = p;
413 }
414 }
415 } else {
416 p++;
417 }
418 }
419
421 *template_str = p;
422 return output;
423}
424
432 const char *p = template_str;
433 return render_internal(&p, context, 0);
434}
435
443 if (!file_path) return NULL;
444 FILE *f = fopen(file_path, "rb");
445 if (!f) {
446 perror("Failed to open template file");
447 return NULL;
448 }
449
450 fseek(f, 0, SEEK_END);
451 long len = ftell(f);
452 fseek(f, 0, SEEK_SET);
453 if (len < 0) {
454 fclose(f);
455 return NULL;
456 }
457
458 char *template_str CWIST_DEFER_FREE = cwist_alloc((size_t)len + 1);
459 if (!template_str) {
460 fclose(f);
461 return NULL;
462 }
463
464 size_t read_len = fread(template_str, 1, (size_t)len, f);
465 template_str[read_len] = '\0';
466 fclose(f);
467
469}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
char * cwist_strdup(const char *src)
Duplicate a string using libttak-backed storage.
Definition alloc.c:32
#define CWIST_DEFER_FREE
Attach to a pointer variable's declaration to cwist_free() it automatically when that variable's encl...
Definition alloc.h:101
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
cwist_error_t cwist_sstring_append_len(cwist_sstring *str, const char *data, size_t len)
Append raw bytes with length.
Definition sstring.c:212
cwist_sstring * cwist_sstring_create(void)
Create a new sstring instance.
Definition sstring.c:802
cwist_error_t cwist_sstring_append(cwist_sstring *str, const char *data)
Append a C string.
Definition sstring.c:559
void cwist_sstring_destroy(cwist_sstring *str)
Destroy an sstring instance.
Definition sstring.c:823
size_t cap
Definition otel.c:56
cwist_sstring * cwist_template_render(const char *template_str, const cJSON *context)
Render an in-memory template string against a JSON context object.
Definition template.c:431
cwist_sstring * cwist_template_render_file(const char *file_path, const cJSON *context)
Load a template file from disk and render it against a JSON context.
Definition template.c:442
#define CWIST_TEMPLATE_MAX_DEPTH
Maximum nesting depth for includes/control structures.
Definition template.c:18
Simple template engine.