From 1fd7cd55458a29f781ff619033ad88fb221b7716 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Mon, 4 May 2026 13:02:44 -0500 Subject: [PATCH] Add HTML template codegen and runtime for C backend C codegen (codegen.el): - cg_html_template: emits a GCC/Clang statement-expression that builds the HTML string via el_str_concat chains - cg_html_element_str / cg_html_parts / cg_html_attrs_str: recursive element and attribute emitters - cg_html_each: {#each} compiles to a C for-loop with el_list_get - __html_counter state tracks unique accumulator variable names - Handles both 'static' (raw string) and 'dynamic' (expr node) attrs matching the parser's attribute kind convention Runtime (el_runtime.c / el_runtime.h): - html_escape(s): escapes & < > " ' for safe interpolation - html_raw(s): identity function for raw() bypass - Both use the existing html_buf_t infrastructure from el_html_sanitize --- el-compiler/runtime/el_runtime.c | 39 ++++++++ el-compiler/runtime/el_runtime.h | 7 ++ el-compiler/src/codegen.el | 165 +++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+) diff --git a/el-compiler/runtime/el_runtime.c b/el-compiler/runtime/el_runtime.c index d599395..e5e3884 100644 --- a/el-compiler/runtime/el_runtime.c +++ b/el-compiler/runtime/el_runtime.c @@ -2602,6 +2602,45 @@ el_val_t el_html_sanitize(el_val_t input_v, el_val_t allowlist_v) { return el_wrap_str(result); } +/* ── html_escape / html_raw ──────────────────────────────────────────────── */ +/* + * html_escape(s) — escape a user-supplied string for safe inline interpolation + * in HTML text content or attribute values. Escapes: & < > " ' + * + * html_raw(s) — identity function; used by the `raw()` escape hatch in El HTML + * templates to explicitly opt out of escaping. + */ + +el_val_t html_escape(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return EL_STR(""); + html_buf_t out; + html_buf_init(&out); + for (const char* p = s; *p; p++) { + unsigned char c = (unsigned char)*p; + switch (c) { + case '&': html_buf_puts(&out, "&"); break; + case '<': html_buf_puts(&out, "<"); break; + case '>': html_buf_puts(&out, ">"); break; + case '"': html_buf_puts(&out, """); break; + case '\'': html_buf_puts(&out, "'"); break; + default: html_buf_putc(&out, (char)c); break; + } + } + char* result = el_strbuf(out.len); + memcpy(result, out.data, out.len); + result[out.len] = '\0'; + html_buf_free(&out); + return el_wrap_str(result); +} + +el_val_t html_raw(el_val_t sv) { + /* Identity — returns the value unchanged. The name exists so generated + * code can call html_raw(expr) instead of expr directly, making it clear + * at the call site that escaping is intentionally bypassed. */ + return sv; +} + /* ── JSON ────────────────────────────────────────────────────────────────── */ /* True iff the segment is non-empty and every byte is an ASCII digit. We treat diff --git a/el-compiler/runtime/el_runtime.h b/el-compiler/runtime/el_runtime.h index a9b9b44..892c922 100644 --- a/el-compiler/runtime/el_runtime.h +++ b/el-compiler/runtime/el_runtime.h @@ -214,6 +214,13 @@ el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */ * where each value is the array of attribute names allowed for that tag. */ el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json); +/* ── HTML template helpers ─────────────────────────────────────────────────── + * Used by compiled El HTML template expressions. + * html_escape(s) — escape & < > " ' for safe inline interpolation. + * html_raw(s) — identity; explicit opt-out from escaping (`raw()` form). */ +el_val_t html_escape(el_val_t s); +el_val_t html_raw(el_val_t s); + /* ── Filesystem ──────────────────────────────────────────────────────────── */ el_val_t fs_read(el_val_t path); diff --git a/el-compiler/src/codegen.el b/el-compiler/src/codegen.el index 532a43b..e0bba78 100644 --- a/el-compiler/src/codegen.el +++ b/el-compiler/src/codegen.el @@ -175,6 +175,167 @@ fn duration_unit_nanos(unit: String) -> String { "1LL" } +// ── HTML template codegen ───────────────────────────────────────────────── +// +// cg_html_template(expr) emits a C statement-expression `({ ... })` that +// builds the HTML string by chaining el_str_concat calls. +// +// Interpolated values are passed through html_escape(); the raw() form +// bypasses escaping. {#each} blocks compile to C for-loops that index into +// the list with el_list_get / el_list_len. +// +// A per-template accumulator variable `_html_N` holds the growing string. +// A global counter stored in state keeps names unique. + +fn next_html_id() -> String { + let csv: String = state_get("__html_counter") + let n = 0 + if !str_eq(csv, "") { + let n = str_to_int(csv) + } + let n = n + 1 + state_set("__html_counter", native_int_to_str(n)) + native_int_to_str(n) +} + +// Emit children nodes into a flat list of C fragment strings (parts). +// Each part is either a static string fragment (already C-literal form) or +// a dynamic expression that produces an el_val_t string. +// We build them all into parts, then the caller wraps with concat chain. + +fn cg_html_parts(children: [Map], acc_var: String) -> String { + let n: Int = native_list_len(children) + let i = 0 + let out = "" + while i < n { + let child: Map = native_list_get(children, i) + let html_kind: String = child["html"] + if str_eq(html_kind, "Text") { + let text: String = child["text"] + let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(text) + ")); " + } + if str_eq(html_kind, "Doctype") { + let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"\")); " + } + if str_eq(html_kind, "Interp") { + let val_node = child["value"] + let val_c: String = cg_expr(val_node) + let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); " + } + if str_eq(html_kind, "Raw") { + let val_node = child["value"] + let val_c: String = cg_expr(val_node) + let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_raw(" + val_c + ")); " + } + if str_eq(html_kind, "Element") { + let elem_c: String = cg_html_element_str(child, acc_var) + let out = out + elem_c + } + if str_eq(html_kind, "Each") { + let each_c: String = cg_html_each(child, acc_var) + let out = out + each_c + } + let i = i + 1 + } + out +} + +// Generate open-tag attribute fragments inline. +// Parser stores attrs with "kind": "static" | "dynamic" | "bool". +// Static: "value" is the raw string value (not an expr node). +// Dynamic: "value" is an expr node. +// Bool: no "value" field. +fn cg_html_attrs_str(attrs: [Map], acc_var: String) -> String { + let n: Int = native_list_len(attrs) + let i = 0 + let out = "" + // Closing-quote snippet: EL_STR("\"") in C text. + let close_q: String = "EL_STR(" + c_str_lit("\"") + ")" + while i < n { + let attr: Map = native_list_get(attrs, i) + let attr_name: String = attr["name"] + let kind: String = attr["kind"] + // Build: EL_STR(" name=\"") + let open_val: String = " " + attr_name + "=\"" + let open_attr: String = "EL_STR(" + c_str_lit(open_val) + ")" + if str_eq(kind, "static") { + // Static attribute: value is a raw string. + let sv: String = attr["value"] + let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); " + let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(sv) + ")); " + let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); " + } else { + if str_eq(kind, "dynamic") { + // Dynamic attribute: value is an expr node — html_escape it. + let val_node = attr["value"] + let val_c: String = cg_expr(val_node) + let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); " + let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); " + let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); " + } else { + // Boolean attribute (no value): emit " name" + let bool_attr: String = "EL_STR(" + c_str_lit(" " + attr_name) + ")" + let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + bool_attr + "); " + } + } + let i = i + 1 + } + out +} + +// Generate code for a single element, appending into acc_var. +fn cg_html_element_str(elem: Map, acc_var: String) -> String { + let tag: String = elem["tag"] + let attrs: [Map] = elem["attrs"] + let children: [Map] = elem["children"] + let self_closing: Bool = elem["self_closing"] + // Open tag: + let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"/>\")); " + } else { + // Close open tag: > + let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\">\")); " + let out = out + cg_html_parts(children, acc_var) + let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"\")); " + } + out +} + +// Generate code for {#each list as item} ... {/each}. +fn cg_html_each(node: Map, acc_var: String) -> String { + let list_expr = node["list"] + let item_name: String = node["item"] + let body_children: [Map] = node["body"] + let id: String = next_html_id() + let list_var: String = "_html_list_" + id + let len_var: String = "_html_len_" + id + let idx_var: String = "_html_i_" + id + let list_c: String = cg_expr(list_expr) + let inner_c: String = cg_html_parts(body_children, acc_var) + // Emit: { el_val_t _list = expr; int _len = el_list_len(_list); + // for (int _i = 0; _i < _len; _i++) { + // el_val_t item = el_list_get(_list, _i); inner_c } } + "{ el_val_t " + list_var + " = (" + list_c + "); el_val_t " + len_var + " = el_list_len(" + list_var + "); for (el_val_t " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { el_val_t " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } " +} + +// Top-level HTML template codegen — returns a C statement-expression string. +fn cg_html_template(expr: Map) -> String { + let root = expr["root"] + let id: String = next_html_id() + let acc: String = "_html_" + id + // If the root element has doctype:true the parser tagged it from + let doctype_flag: Bool = root["doctype"] + let doctype_prefix: String = "" + if doctype_flag { + let doctype_prefix = acc + " = el_str_concat(" + acc + ", EL_STR(\"\")); " + } + let body: String = cg_html_element_str(root, acc) + "({ el_val_t " + acc + " = EL_STR(\"\"); " + doctype_prefix + body + acc + "; })" +} + fn cg_expr(expr: Map) -> String { let kind: String = expr["expr"] @@ -787,6 +948,10 @@ fn cg_expr(expr: Map) -> String { return cg_match(expr) } + if kind == "HtmlTemplate" { + return cg_html_template(expr) + } + "EL_NULL" }