comparison mrjunejune/template_renderer.c @ 264:04fee26ecce0

add authenticated JRPG conversation platform Add reusable auth/session storage, owned conversation recovery, guest quotas, admin workflows, URL-routed conversation UI, mobile frame support, and parallel browser acceptance. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 07:34:12 -0700
parents
children
comparison
equal deleted inserted replaced
263:ee04e4e69fed 264:04fee26ecce0
1 /*
2 * template_renderer.c — site-owned HTML template renderer for mrjunejune.
3 *
4 * Expands one level of {{/path}} include tokens found in HTML templates.
5 * File I/O is done with standard C fopen/fread so that tests can point
6 * the renderer at any root without depending on Seobeo server state.
7 */
8
9 #include "mrjunejune/template_renderer.h"
10
11 #include "dowa/dowa.h"
12 #include "seobeo/seobeo.h"
13
14 #include <stdio.h>
15 #include <stdlib.h>
16 #include <string.h>
17
18 /* Default matches the path Seobeo_Web_Server_Start_On receives in main.c. */
19 static char g_src_root[512] = "mrjunejune/src";
20
21 void Mjj_Template_Renderer_Init(const char *src_root)
22 {
23 if (!src_root || src_root[0] == '\0')
24 return;
25 strncpy(g_src_root, src_root, sizeof(g_src_root) - 1);
26 g_src_root[sizeof(g_src_root) - 1] = '\0';
27 }
28
29 /* Load a file using the renderer's own root; caller must free() result. */
30 static char *renderer_load_file(const char *rel_path, size_t *p_size)
31 {
32 if (!rel_path)
33 return NULL;
34
35 char full_path[1024];
36 /* rel_path already starts with '/' when produced by canonical markers
37 (e.g. "/parts/base_head.html"), so concatenate directly. */
38 if (rel_path[0] == '/')
39 snprintf(full_path, sizeof(full_path), "%s%s", g_src_root, rel_path);
40 else
41 snprintf(full_path, sizeof(full_path), "%s/%s", g_src_root, rel_path);
42
43 FILE *f = fopen(full_path, "rb");
44 if (!f)
45 return NULL;
46
47 fseek(f, 0, SEEK_END);
48 long sz = ftell(f);
49 fseek(f, 0, SEEK_SET);
50 if (sz < 0)
51 {
52 fclose(f);
53 return NULL;
54 }
55
56 char *buf = (char *)malloc((size_t)sz + 1);
57 if (!buf)
58 {
59 fclose(f);
60 return NULL;
61 }
62
63 size_t n = fread(buf, 1, (size_t)sz, f);
64 fclose(f);
65 buf[n] = '\0';
66
67 if (p_size)
68 *p_size = n;
69 return buf;
70 }
71
72 /* Write `len` bytes of `src` to out starting at *p_offset, respecting cap.
73 Returns FALSE and does a best-effort NUL-terminate on overflow. */
74 static boolean safe_append(
75 char *out, size_t cap, size_t *p_offset,
76 const char *src, size_t len)
77 {
78 if (len == 0)
79 return TRUE;
80 if (*p_offset + len >= cap)
81 {
82 /* Partial write then NUL-terminate. */
83 size_t avail = (cap > *p_offset + 1) ? (cap - *p_offset - 1) : 0;
84 if (avail > 0)
85 memcpy(out + *p_offset, src, avail);
86 out[cap - 1] = '\0';
87 return FALSE;
88 }
89 memcpy(out + *p_offset, src, len);
90 *p_offset += len;
91 return TRUE;
92 }
93
94 boolean Mjj_Template_Render(
95 char *out, size_t cap, const char *tmpl, Dowa_Arena *arena)
96 {
97 if (!out || cap == 0)
98 return FALSE;
99 out[0] = '\0';
100 if (!tmpl)
101 return FALSE;
102
103 size_t offset = 0;
104 const char *cursor = tmpl;
105
106 while (1)
107 {
108 const char *open = strstr(cursor, "{{");
109 if (!open)
110 break;
111
112 const char *close = strstr(open + 2, "}}");
113 if (!close)
114 break;
115
116 /* Copy text before the token. */
117 size_t lead = (size_t)(open - cursor);
118 if (!safe_append(out, cap, &offset, cursor, lead))
119 return FALSE;
120
121 /* Extract include path between {{ and }}. */
122 size_t name_len = (size_t)(close - (open + 2));
123 char *include_name = (char *)Dowa_Arena_Allocate(arena, name_len + 1);
124 if (!include_name)
125 return FALSE;
126 memcpy(include_name, open + 2, name_len);
127 include_name[name_len] = '\0';
128
129 /* Load and inline the included file; skip silently if missing. */
130 size_t inc_size = 0;
131 char *inc = renderer_load_file(include_name, &inc_size);
132 Seobeo_Log(SEOBEO_DEBUG,
133 "[TEMPLATE] include '%s' -> %s (size=%zu)\n",
134 include_name, inc ? "OK" : "MISSING", inc_size);
135 if (inc)
136 {
137 boolean ok = safe_append(out, cap, &offset, inc, inc_size);
138 free(inc);
139 if (!ok)
140 return FALSE;
141 }
142
143 cursor = close + 2;
144 }
145
146 /* Copy the tail (after the last token, or the whole string if no tokens). */
147 size_t tail = strlen(cursor);
148 if (!safe_append(out, cap, &offset, cursor, tail))
149 return FALSE;
150
151 out[offset] = '\0';
152 return TRUE;
153 }
154
155 boolean Mjj_Template_Render_File(
156 char *out, size_t cap, const char *path, Dowa_Arena *arena)
157 {
158 if (!out || cap == 0)
159 return FALSE;
160 out[0] = '\0';
161
162 Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] loading file '%s'\n", path);
163
164 size_t file_size = 0;
165 char *tmpl = renderer_load_file(path, &file_size);
166 if (!tmpl)
167 {
168 Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] file '%s' not found\n", path);
169 return FALSE;
170 }
171
172 boolean ok = Mjj_Template_Render(out, cap, tmpl, arena);
173 free(tmpl);
174 return ok;
175 }