view mrjunejune/template_renderer.c @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -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;
}