Mercurial
view mrjunejune/template_renderer.c @ 273:e02e2036ef84 default tip
add Layer 2 JRPG component system
Add reusable content and window modals, an isolated component sandbox, shared cyberpunk scroll areas, production-safe cache freshness, and server-rendered JRPG panel state.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sat, 08 Aug 2026 02:08:08 -0700 |
| parents | 04fee26ecce0 |
| children |
line wrap: on
line source
/* * 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; }