Mercurial
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/template_renderer.c Fri Aug 07 07:34:12 2026 -0700 @@ -0,0 +1,175 @@ +/* + * template_renderer.c — site-owned HTML template renderer for mrjunejune. + * + * Expands one level of {{/path}} include tokens found in HTML templates. + * File I/O is done with standard C fopen/fread so that tests can point + * the renderer at any root without depending on Seobeo server state. + */ + +#include "mrjunejune/template_renderer.h" + +#include "dowa/dowa.h" +#include "seobeo/seobeo.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/* Default matches the path Seobeo_Web_Server_Start_On receives in main.c. */ +static char g_src_root[512] = "mrjunejune/src"; + +void Mjj_Template_Renderer_Init(const char *src_root) +{ + if (!src_root || src_root[0] == '\0') + return; + strncpy(g_src_root, src_root, sizeof(g_src_root) - 1); + g_src_root[sizeof(g_src_root) - 1] = '\0'; +} + +/* Load a file using the renderer's own root; caller must free() result. */ +static char *renderer_load_file(const char *rel_path, size_t *p_size) +{ + if (!rel_path) + return NULL; + + char full_path[1024]; + /* rel_path already starts with '/' when produced by canonical markers + (e.g. "/parts/base_head.html"), so concatenate directly. */ + if (rel_path[0] == '/') + snprintf(full_path, sizeof(full_path), "%s%s", g_src_root, rel_path); + else + snprintf(full_path, sizeof(full_path), "%s/%s", g_src_root, rel_path); + + FILE *f = fopen(full_path, "rb"); + if (!f) + return NULL; + + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + if (sz < 0) + { + fclose(f); + return NULL; + } + + char *buf = (char *)malloc((size_t)sz + 1); + if (!buf) + { + fclose(f); + return NULL; + } + + size_t n = fread(buf, 1, (size_t)sz, f); + fclose(f); + buf[n] = '\0'; + + if (p_size) + *p_size = n; + return buf; +} + +/* Write `len` bytes of `src` to out starting at *p_offset, respecting cap. + Returns FALSE and does a best-effort NUL-terminate on overflow. */ +static boolean safe_append( + char *out, size_t cap, size_t *p_offset, + const char *src, size_t len) +{ + if (len == 0) + return TRUE; + if (*p_offset + len >= cap) + { + /* Partial write then NUL-terminate. */ + size_t avail = (cap > *p_offset + 1) ? (cap - *p_offset - 1) : 0; + if (avail > 0) + memcpy(out + *p_offset, src, avail); + out[cap - 1] = '\0'; + return FALSE; + } + memcpy(out + *p_offset, src, len); + *p_offset += len; + return TRUE; +} + +boolean Mjj_Template_Render( + char *out, size_t cap, const char *tmpl, Dowa_Arena *arena) +{ + if (!out || cap == 0) + return FALSE; + out[0] = '\0'; + if (!tmpl) + return FALSE; + + size_t offset = 0; + const char *cursor = tmpl; + + while (1) + { + const char *open = strstr(cursor, "{{"); + if (!open) + break; + + const char *close = strstr(open + 2, "}}"); + if (!close) + break; + + /* Copy text before the token. */ + size_t lead = (size_t)(open - cursor); + if (!safe_append(out, cap, &offset, cursor, lead)) + return FALSE; + + /* Extract include path between {{ and }}. */ + size_t name_len = (size_t)(close - (open + 2)); + char *include_name = (char *)Dowa_Arena_Allocate(arena, name_len + 1); + if (!include_name) + return FALSE; + memcpy(include_name, open + 2, name_len); + include_name[name_len] = '\0'; + + /* Load and inline the included file; skip silently if missing. */ + size_t inc_size = 0; + char *inc = renderer_load_file(include_name, &inc_size); + Seobeo_Log(SEOBEO_DEBUG, + "[TEMPLATE] include '%s' -> %s (size=%zu)\n", + include_name, inc ? "OK" : "MISSING", inc_size); + if (inc) + { + boolean ok = safe_append(out, cap, &offset, inc, inc_size); + free(inc); + if (!ok) + return FALSE; + } + + cursor = close + 2; + } + + /* Copy the tail (after the last token, or the whole string if no tokens). */ + size_t tail = strlen(cursor); + if (!safe_append(out, cap, &offset, cursor, tail)) + return FALSE; + + out[offset] = '\0'; + return TRUE; +} + +boolean Mjj_Template_Render_File( + char *out, size_t cap, const char *path, Dowa_Arena *arena) +{ + if (!out || cap == 0) + return FALSE; + out[0] = '\0'; + + Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] loading file '%s'\n", path); + + size_t file_size = 0; + char *tmpl = renderer_load_file(path, &file_size); + if (!tmpl) + { + Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] file '%s' not found\n", path); + return FALSE; + } + + boolean ok = Mjj_Template_Render(out, cap, tmpl, arena); + free(tmpl); + return ok; +}