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;
+}