diff --git a/el-compiler/runtime/el_runtime.js b/el-compiler/runtime/el_runtime.js index a223fdb..eeb814f 100644 --- a/el-compiler/runtime/el_runtime.js +++ b/el-compiler/runtime/el_runtime.js @@ -128,6 +128,22 @@ function str_pad_right(s, width, pad) { return String(s).padEnd(width, String(pad)); } +// ── 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). + +function html_escape(s) { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function html_raw(s) { return s; } + // ── Math ──────────────────────────────────────────────────────────────────── function el_abs(n) { return Math.abs(n); } @@ -1017,6 +1033,8 @@ export { fs_read, fs_write, fs_list, json_parse, json_stringify, json_get, json_get_string, json_get_int, time_now, time_now_utc, sleep_ms, + // HTML template helpers + html_escape, html_raw, bool_to_str, exit_program, args, env, state_set, state_get, state_del, state_keys, el_cgi_init, diff --git a/el-compiler/src/codegen-js.el b/el-compiler/src/codegen-js.el index 876bf9a..7e44b41 100644 --- a/el-compiler/src/codegen-js.el +++ b/el-compiler/src/codegen-js.el @@ -191,6 +191,138 @@ fn js_is_int_call(call_expr: Map) -> Bool { return false } +// ── HTML template codegen (JS) ──────────────────────────────────────────────── +// +// HTML template expressions compile to a JS IIFE that builds the HTML string +// using string concatenation. Interpolated values go through html_escape(); +// raw() bypasses escaping. {#each} blocks compile to Array.forEach or a +// for-loop that pushes fragments into a parts array. +// +// Entry point: js_cg_html_template(expr) → JS expression string. + +fn js_next_html_id() -> String { + let csv: String = state_get("__js_html_counter") + let n = 0 + if !str_eq(csv, "") { + let n = str_to_int(csv) + } + let n = n + 1 + state_set("__js_html_counter", native_int_to_str(n)) + native_int_to_str(n) +} + +fn js_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 + " += " + js_str_lit(text) + "; " + } + if str_eq(html_kind, "Doctype") { + let out = out + acc_var + " += \"\"; " + } + if str_eq(html_kind, "Interp") { + let val_node = child["value"] + let val_c: String = js_cg_expr(val_node) + let out = out + acc_var + " += html_escape(" + val_c + "); " + } + if str_eq(html_kind, "Raw") { + let val_node = child["value"] + let val_c: String = js_cg_expr(val_node) + let out = out + acc_var + " += html_raw(" + val_c + "); " + } + if str_eq(html_kind, "Element") { + let elem_c: String = js_cg_html_element_str(child, acc_var) + let out = out + elem_c + } + if str_eq(html_kind, "Each") { + let each_c: String = js_cg_html_each(child, acc_var) + let out = out + each_c + } + let i = i + 1 + } + out +} + +fn js_cg_html_attrs_str(attrs: [Map], acc_var: String) -> String { + let n: Int = native_list_len(attrs) + let i = 0 + let out = "" + while i < n { + let attr: Map = native_list_get(attrs, i) + let attr_name: String = attr["name"] + let kind: String = attr["kind"] + // open-attr snippet: " name=\"" + let open_val: String = " " + attr_name + "=\"" + if str_eq(kind, "static") { + let sv: String = attr["value"] + let out = out + acc_var + " += " + js_str_lit(open_val) + "; " + let out = out + acc_var + " += " + js_str_lit(sv) + "; " + let out = out + acc_var + " += " + js_str_lit("\"") + "; " + } else { + if str_eq(kind, "dynamic") { + let val_node = attr["value"] + let val_c: String = js_cg_expr(val_node) + let out = out + acc_var + " += " + js_str_lit(open_val) + "; " + let out = out + acc_var + " += html_escape(" + val_c + "); " + let out = out + acc_var + " += " + js_str_lit("\"") + "; " + } else { + // Boolean attribute + let out = out + acc_var + " += " + js_str_lit(" " + attr_name) + "; " + } + } + let i = i + 1 + } + out +} + +fn js_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"] + let out = acc_var + " += " + js_str_lit("<" + tag) + "; " + let out = out + js_cg_html_attrs_str(attrs, acc_var) + if self_closing { + let out = out + acc_var + " += \"/>\"" + "; " + } else { + let out = out + acc_var + " += \">\"; " + let out = out + js_cg_html_parts(children, acc_var) + let out = out + acc_var + " += " + js_str_lit("") + "; " + } + out +} + +fn js_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 = js_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 = js_cg_expr(list_expr) + let inner_c: String = js_cg_html_parts(body_children, acc_var) + "{ const " + list_var + " = " + list_c + "; const " + len_var + " = el_list_len(" + list_var + "); for (let " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { const " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } " +} + +fn js_cg_html_template(expr: Map) -> String { + let root = expr["root"] + let id: String = js_next_html_id() + let acc: String = "_html_" + id + let doctype_flag: Bool = root["doctype"] + let doctype_prefix: String = "" + if doctype_flag { + let doctype_prefix = acc + " += \"\"; " + } + let body: String = js_cg_html_element_str(root, acc) + "(() => { let " + acc + " = \"\"; " + doctype_prefix + body + "return " + acc + "; })()" +} + // ── Expression codegen ──────────────────────────────────────────────────────── // // js_cg_expr returns a JS expression string (not a statement). @@ -569,6 +701,10 @@ fn js_cg_expr(expr: Map) -> String { return js_cg_lambda(expr) } + if kind == "HtmlTemplate" { + return js_cg_html_template(expr) + } + "null" }