From 3e7d316c65661aea4a6617a198276f2c59309dc3 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Tue, 5 May 2026 00:17:59 -0500 Subject: [PATCH] feat: extract HTML template parser/codegen from feat/el-html-templates Parser additions (parser.el, no existing features removed): - HTML template parser functions: is_html_tag_name, is_void_element, parse_html_text_tokens, parse_html_attrs, parse_html_children, parse_html_each_body, parse_html_element, parse_html_template - HtmlTemplate detection in parse_primary ( and ) - Lambda fn literal expression node (parse_primary) - Enum::Variant pattern matching (parse_pattern) - type definition optional = before { - try/catch statement (TryCatch AST node) Codegen additions (codegen.el, no existing features removed): - HTML template C codegen: cg_html_template, cg_html_parts, cg_html_attrs_str, cg_html_element_str, cg_html_each, next_html_id - HtmlTemplate and Lambda dispatch in cg_expr - Variant pattern support in cg_match - TryCatch lowering in cg_stmt (C: runs try body, ignores catch) - builtin_arity entries: getpid_now, stdout_to_file, stdout_restore JS codegen additions (codegen-js.el, pure additions only): - JS HTML template codegen: js_cg_html_template and helpers - HtmlTemplate dispatch in js_cg_expr Example: examples/html-page.el --- el-compiler/src/codegen-js.el | 136 +++++++++ el-compiler/src/codegen.el | 197 +++++++++++- el-compiler/src/parser.el | 542 +++++++++++++++++++++++++++++++++- examples/html-page.el | 40 +++ 4 files changed, 911 insertions(+), 4 deletions(-) create mode 100644 examples/html-page.el 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" } diff --git a/el-compiler/src/codegen.el b/el-compiler/src/codegen.el index 9375843..6ac69ce 100644 --- a/el-compiler/src/codegen.el +++ b/el-compiler/src/codegen.el @@ -176,6 +176,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"] @@ -788,7 +949,16 @@ fn cg_expr(expr: Map) -> String { return cg_match(expr) } - "EL_NULL" + if kind == "HtmlTemplate" { + return cg_html_template(expr) + } + + if kind == "Lambda" { + // Lambda expressions are JS-target only. In the C target, emit EL_NULL. + "EL_NULL" + } else { + "EL_NULL" + } } // -- Match codegen ------------------------------------------------------------- @@ -849,8 +1019,15 @@ fn cg_match(expr: Map) -> String { } let parts = native_list_append(parts, "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ") } else { - // unknown pattern -> wildcard - let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ") + if str_eq(pkind, "Variant") { + // Enum::Variant pattern — match against the variant name + // string (El enums compile to plain strings). + let variant: String = pat["variant"] + let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", EL_STR(" + c_str_lit(variant) + "))) { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ") + } else { + // unknown pattern -> wildcard + let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ") + } } } } @@ -1264,6 +1441,15 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin if kind == "Import" { return declared } if kind == "ExternFn" { return declared } if kind == "CgiBlock" { return declared } + // TryCatch: browser-only control flow. In the C target, emit a comment + // noting that the try body runs unconditionally; error handling is a no-op. + // Programs that rely on catching JS exceptions should compile with --target=js. + if kind == "TryCatch" { + let try_body = stmt["try_body"] + emit_line(indent + "/* try (C target: exception handling not supported) */") + cg_stmts(try_body, indent, native_list_clone(declared)) + return declared + } declared } @@ -2264,6 +2450,11 @@ fn builtin_arity(name: String) -> Int { if str_eq(name, "bool_to_str") { return 1 } // Process if str_eq(name, "exit_program") { return 1 } + // Process info + if str_eq(name, "getpid_now") { return 0 } + // stdout redirect (used by elc post-processing) + if str_eq(name, "stdout_to_file") { return 1 } + if str_eq(name, "stdout_restore") { return 0 } // Subprocess execution if str_eq(name, "exec_command") { return 1 } if str_eq(name, "exec_capture") { return 1 } diff --git a/el-compiler/src/parser.el b/el-compiler/src/parser.el index 0e5375a..62fda8c 100644 --- a/el-compiler/src/parser.el +++ b/el-compiler/src/parser.el @@ -142,6 +142,460 @@ fn parse_params(tokens: [Map], pos: Int) -> Map { // -- Expression parsing -------------------------------------------------------- +// ── HTML template parser ────────────────────────────────────────────────────── +// +// HTML templates are written as unquoted HTML in expression position: +// return

{title}

+// +// The parser detects an HTML template when parse_primary sees Lt followed +// by a lowercase ident (a known or assumed HTML element name) or `!` (for +// ). It then recursively parses the HTML into an AST. +// +// AST nodes produced: +// { "expr": "HtmlTemplate", "root": child_node } +// { "html": "Element", "tag": "div", "attrs": [...], "children": [...], "self_closing": bool } +// { "html": "Text", "text": "..." } +// { "html": "Interp", "value": expr_node } +// { "html": "Each", "list": expr_node, "item": "name", "body": [...] } +// { "html": "Doctype" } +// { "html": "Raw", "value": expr_node } + +fn is_html_tag_name(name: String) -> Bool { + if str_eq(name, "a") { return true } + if str_eq(name, "abbr") { return true } + if str_eq(name, "address") { return true } + if str_eq(name, "area") { return true } + if str_eq(name, "article") { return true } + if str_eq(name, "aside") { return true } + if str_eq(name, "audio") { return true } + if str_eq(name, "b") { return true } + if str_eq(name, "base") { return true } + if str_eq(name, "blockquote") { return true } + if str_eq(name, "body") { return true } + if str_eq(name, "br") { return true } + if str_eq(name, "button") { return true } + if str_eq(name, "canvas") { return true } + if str_eq(name, "caption") { return true } + if str_eq(name, "cite") { return true } + if str_eq(name, "code") { return true } + if str_eq(name, "col") { return true } + if str_eq(name, "colgroup") { return true } + if str_eq(name, "data") { return true } + if str_eq(name, "datalist") { return true } + if str_eq(name, "dd") { return true } + if str_eq(name, "del") { return true } + if str_eq(name, "details") { return true } + if str_eq(name, "dfn") { return true } + if str_eq(name, "dialog") { return true } + if str_eq(name, "div") { return true } + if str_eq(name, "dl") { return true } + if str_eq(name, "dt") { return true } + if str_eq(name, "em") { return true } + if str_eq(name, "embed") { return true } + if str_eq(name, "fieldset") { return true } + if str_eq(name, "figcaption") { return true } + if str_eq(name, "figure") { return true } + if str_eq(name, "footer") { return true } + if str_eq(name, "form") { return true } + if str_eq(name, "h1") { return true } + if str_eq(name, "h2") { return true } + if str_eq(name, "h3") { return true } + if str_eq(name, "h4") { return true } + if str_eq(name, "h5") { return true } + if str_eq(name, "h6") { return true } + if str_eq(name, "head") { return true } + if str_eq(name, "header") { return true } + if str_eq(name, "hr") { return true } + if str_eq(name, "html") { return true } + if str_eq(name, "i") { return true } + if str_eq(name, "iframe") { return true } + if str_eq(name, "img") { return true } + if str_eq(name, "input") { return true } + if str_eq(name, "ins") { return true } + if str_eq(name, "kbd") { return true } + if str_eq(name, "label") { return true } + if str_eq(name, "legend") { return true } + if str_eq(name, "li") { return true } + if str_eq(name, "link") { return true } + if str_eq(name, "main") { return true } + if str_eq(name, "map") { return true } + if str_eq(name, "mark") { return true } + if str_eq(name, "menu") { return true } + if str_eq(name, "meta") { return true } + if str_eq(name, "meter") { return true } + if str_eq(name, "nav") { return true } + if str_eq(name, "noscript") { return true } + if str_eq(name, "object") { return true } + if str_eq(name, "ol") { return true } + if str_eq(name, "optgroup") { return true } + if str_eq(name, "option") { return true } + if str_eq(name, "output") { return true } + if str_eq(name, "p") { return true } + if str_eq(name, "param") { return true } + if str_eq(name, "picture") { return true } + if str_eq(name, "pre") { return true } + if str_eq(name, "progress") { return true } + if str_eq(name, "q") { return true } + if str_eq(name, "rp") { return true } + if str_eq(name, "rt") { return true } + if str_eq(name, "ruby") { return true } + if str_eq(name, "s") { return true } + if str_eq(name, "samp") { return true } + if str_eq(name, "script") { return true } + if str_eq(name, "section") { return true } + if str_eq(name, "select") { return true } + if str_eq(name, "small") { return true } + if str_eq(name, "source") { return true } + if str_eq(name, "span") { return true } + if str_eq(name, "strong") { return true } + if str_eq(name, "style") { return true } + if str_eq(name, "sub") { return true } + if str_eq(name, "summary") { return true } + if str_eq(name, "sup") { return true } + if str_eq(name, "table") { return true } + if str_eq(name, "tbody") { return true } + if str_eq(name, "td") { return true } + if str_eq(name, "template") { return true } + if str_eq(name, "textarea") { return true } + if str_eq(name, "tfoot") { return true } + if str_eq(name, "th") { return true } + if str_eq(name, "thead") { return true } + if str_eq(name, "time") { return true } + if str_eq(name, "title") { return true } + if str_eq(name, "tr") { return true } + if str_eq(name, "track") { return true } + if str_eq(name, "u") { return true } + if str_eq(name, "ul") { return true } + if str_eq(name, "var") { return true } + if str_eq(name, "video") { return true } + if str_eq(name, "wbr") { return true } + false +} + +fn is_void_element(name: String) -> Bool { + if str_eq(name, "area") { return true } + if str_eq(name, "base") { return true } + if str_eq(name, "br") { return true } + if str_eq(name, "col") { return true } + if str_eq(name, "embed") { return true } + if str_eq(name, "hr") { return true } + if str_eq(name, "img") { return true } + if str_eq(name, "input") { return true } + if str_eq(name, "link") { return true } + if str_eq(name, "meta") { return true } + if str_eq(name, "param") { return true } + if str_eq(name, "source") { return true } + if str_eq(name, "track") { return true } + if str_eq(name, "wbr") { return true } + false +} + +// Collect tokens as text content until we hit Lt, LBrace, Eof, or a +// closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p } +fn parse_html_text_tokens(tokens: [Map], pos: Int) -> Map { + let parts: [String] = native_list_empty() + let p = pos + let running = true + while running { + let k = tok_kind(tokens, p) + if str_eq(k, "Eof") { + let running = false + } else { + if str_eq(k, "Lt") { + let running = false + } else { + if str_eq(k, "LBrace") { + let running = false + } else { + // Check for ). +fn parse_html_attrs(tokens: [Map], pos: Int) -> Map { + let attrs: [Map] = native_list_empty() + let p = pos + let running = true + while running { + let k = tok_kind(tokens, p) + if str_eq(k, "Gt") { + let running = false + } else { + if str_eq(k, "Slash") { + let running = false + } else { + if str_eq(k, "Eof") { + let running = false + } else { + // Attribute name: could be Ident or keyword used as attr name + let attr_name = tok_value(tokens, p) + let p = p + 1 + let k2 = tok_kind(tokens, p) + if str_eq(k2, "Eq") { + let p = p + 1 + let k3 = tok_kind(tokens, p) + if str_eq(k3, "Str") { + // static: attr="value" + let attr_val = tok_value(tokens, p) + let p = p + 1 + let attrs = native_list_append(attrs, { "name": attr_name, "kind": "static", "value": attr_val }) + } else { + if str_eq(k3, "LBrace") { + // dynamic: attr={expr} + let r = parse_expr(tokens, p + 1) + let val_node = r["node"] + let p = r["pos"] + let p = expect(tokens, p, "RBrace") + let attrs = native_list_append(attrs, { "name": attr_name, "kind": "dynamic", "value": val_node }) + } else { + // malformed, skip + } + } + } else { + // boolean attribute + let attrs = native_list_append(attrs, { "name": attr_name, "kind": "bool" }) + } + } + } + } + } + { "attrs": attrs, "pos": p } +} + +// Parse the children of an HTML element until we see the closing tag +// or EOF. Returns { "children": [...], "pos": p_after_closing_tag } +fn parse_html_children(tokens: [Map], pos: Int, parent_tag: String) -> Map { + let children: [Map] = native_list_empty() + let p = pos + let running = true + while running { + let k = tok_kind(tokens, p) + if str_eq(k, "Eof") { + let running = false + } else { + if str_eq(k, "Lt") { + // Check for closing tag: — consume and stop + let p = p + 2 + // skip tag name + let close_name = tok_value(tokens, p) + let p = p + 1 + // consume > + let p = expect(tokens, p, "Gt") + let running = false + } else { + if str_eq(k2, "Not") { + // Possible + let k3_v = tok_value(tokens, p + 2) + if str_eq(k3_v, "doctype") { + // consume + let p = p + 2 + // skip until > + let scanning = true + while scanning { + let ck = tok_kind(tokens, p) + if str_eq(ck, "Eof") { let scanning = false } + if str_eq(ck, "Gt") { + let p = p + 1 + let scanning = false + } else { + let p = p + 1 + } + } + let children = native_list_append(children, { "html": "Doctype" }) + } else { + let p = p + 1 + } + } else { + // nested element + let r = parse_html_element(tokens, p) + let child = r["node"] + let p = r["pos"] + let children = native_list_append(children, child) + } + } + } else { + if str_eq(k, "LBrace") { + // Interpolation: {expr} or {#each ...} or {/each} + let k2 = tok_kind(tokens, p + 1) + if str_eq(k2, "Hash") { + // {#each list as item} + let k3_v = tok_value(tokens, p + 2) + if str_eq(k3_v, "each") { + let p = p + 3 + // parse list expr up to "as" keyword + let prev_no_block: String = state_get("__no_block_expr") + state_set("__no_block_expr", "1") + let r_list = parse_expr(tokens, p) + state_set("__no_block_expr", prev_no_block) + let list_expr = r_list["node"] + let p = r_list["pos"] + // expect "as" + let p = expect(tokens, p, "As") + // item variable name + let item_name = tok_value(tokens, p) + let p = p + 1 + // consume closing } + let p = expect(tokens, p, "RBrace") + // parse body until {/each} + let r_body = parse_html_each_body(tokens, p) + let body_children = r_body["children"] + let p = r_body["pos"] + let each_node: Map = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children } + let children = native_list_append(children, each_node) + } else { + let p = p + 1 + } + } else { + if str_eq(k2, "Slash") { + // {/each} — end of each block, stop + // skip {/each} + let p = p + 2 + // skip "each" + let p = p + 1 + // skip } + let p = expect(tokens, p, "RBrace") + let running = false + } else { + // regular {expr} + let r = parse_expr(tokens, p + 1) + let interp_val = r["node"] + let p = r["pos"] + let p = expect(tokens, p, "RBrace") + // Check if the expr is a call to raw() + let is_raw_call = false + let interp_kind: String = interp_val["expr"] + if str_eq(interp_kind, "Call") { + let fn_node = interp_val["func"] + let fn_kind: String = fn_node["expr"] + if str_eq(fn_kind, "Ident") { + let fn_name_v: String = fn_node["name"] + if str_eq(fn_name_v, "raw") { + let is_raw_call = true + } + } + } + if is_raw_call { + let raw_args = interp_val["args"] + let raw_inner = native_list_get(raw_args, 0) + let children = native_list_append(children, { "html": "Raw", "value": raw_inner }) + } else { + let children = native_list_append(children, { "html": "Interp", "value": interp_val }) + } + } + } + } else { + // Text tokens — collect run of non-special tokens + let r_text = parse_html_text_tokens(tokens, p) + let text_str: String = r_text["text"] + let p = r_text["pos"] + let text_trimmed: String = str_trim(text_str) + if !str_eq(text_trimmed, "") { + let children = native_list_append(children, { "html": "Text", "text": text_trimmed }) + } + } + } + } + } + { "children": children, "pos": p } +} + +// Parse body of {#each} until {/each}. Mirrors parse_html_children but +// stops at the {/each} sentinel rather than a closing element tag. +fn parse_html_each_body(tokens: [Map], pos: Int) -> Map { + parse_html_children(tokens, pos, "__each__") +} + +// Parse a single HTML element: children +// or self-closing: +// Pos points to the Lt token. +fn parse_html_element(tokens: [Map], pos: Int) -> Map { + let p = pos + // consume < + let p = expect(tokens, p, "Lt") + // tag name + let tag_name = tok_value(tokens, p) + let p = p + 1 + // parse attributes + let r_attrs = parse_html_attrs(tokens, p) + let attrs = r_attrs["attrs"] + let p = r_attrs["pos"] + // check for self-closing /> or void element + let k = tok_kind(tokens, p) + let self_closing = false + if str_eq(k, "Slash") { + // /> + let p = p + 1 + let p = expect(tokens, p, "Gt") + let self_closing = true + return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p) + } + // consume > + let p = expect(tokens, p, "Gt") + // void elements have no children, no closing tag + if is_void_element(tag_name) { + return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p) + } + // parse children + let r_children = parse_html_children(tokens, p, tag_name) + let children = r_children["children"] + let p = r_children["pos"] + make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": children, "self_closing": false }, p) +} + +// Entry point for HTML template parsing. +// Pos points to Lt (or Lt Not for ). +// May parse an optional prefix followed by the root element. +fn parse_html_template(tokens: [Map], pos: Int) -> Map { + let p = pos + // Check for + let doctype = false + let k = tok_kind(tokens, p) + let k2 = tok_kind(tokens, p + 1) + if str_eq(k, "Lt") { + if str_eq(k2, "Not") { + let k3_v = tok_value(tokens, p + 2) + if str_eq(k3_v, "doctype") { + let doctype = true + // consume + let p = p + 2 + let scanning = true + while scanning { + let ck = tok_kind(tokens, p) + if str_eq(ck, "Eof") { let scanning = false } + if str_eq(ck, "Gt") { + let p = p + 1 + let scanning = false + } else { + let p = p + 1 + } + } + } + } + } + // Parse root element + let r = parse_html_element(tokens, p) + let root = r["node"] + let p = r["pos"] + let root_with_doctype = root + if doctype { + let root_with_doctype = { "html": root["html"], "tag": root["tag"], "attrs": root["attrs"], "children": root["children"], "self_closing": root["self_closing"], "doctype": true } + } + make_result({ "expr": "HtmlTemplate", "root": root_with_doctype }, p) +} + fn parse_primary(tokens: [Map], pos: Int) -> Map { let k = tok_kind(tokens, pos) let v = tok_value(tokens, pos) @@ -166,6 +620,22 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { return make_result({ "expr": "Bool", "value": v }, pos + 1) } + // HTML template literal: ... or ... + // Detected in value position only; `<` in comparison position is already + // consumed by parse_binop before parse_primary is reached. + if k == "Lt" { + let k2 = tok_kind(tokens, pos + 1) + if str_eq(k2, "Not") { + return parse_html_template(tokens, pos) + } + if str_eq(k2, "Ident") { + let tag_candidate = tok_value(tokens, pos + 1) + if is_html_tag_name(tag_candidate) { + return parse_html_template(tokens, pos) + } + } + } + // Identifier if k == "Ident" { return make_result({ "expr": "Ident", "name": v }, pos + 1) @@ -279,6 +749,30 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { return r } + // Anonymous function literal (lambda): fn(params) -> RetType { body } + // Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... }) + // Produces a Lambda expression node (distinct from a named FnDef statement). + if k == "Fn" { + let p = pos + 1 + let r = parse_params(tokens, p) + let params = r["params"] + let p = r["pos"] + let ret_type = "" + let k2 = tok_kind(tokens, p) + if k2 == "Arrow" { + let p = p + 1 + let kt = tok_kind(tokens, p) + if kt == "Ident" { + let ret_type = tok_value(tokens, p) + } + let p = skip_type(tokens, p) + } + let r2 = parse_block(tokens, p) + let body = r2["stmts"] + let p = r2["pos"] + return make_result({ "expr": "Lambda", "params": params, "body": body, "ret_type": ret_type }, p) + } + // Unary not if k == "Not" { let r = parse_primary(tokens, pos + 1) @@ -408,6 +902,13 @@ fn parse_pattern(tokens: [Map], pos: Int) -> Map { if v == "_" { return make_result({ "pattern": "Wildcard" }, pos + 1) } + // Check for Enum::Variant pattern (Color::Red, Status::Ok, etc.) + // Lexed as: Ident ColonColon Ident + let next_k = tok_kind(tokens, pos + 1) + if next_k == "ColonColon" { + let variant_name = tok_value(tokens, pos + 2) + return make_result({ "pattern": "Variant", "enum_name": v, "variant": variant_name }, pos + 3) + } return make_result({ "pattern": "Binding", "name": v }, pos + 1) } if k == "Int" { @@ -739,11 +1240,16 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body, "ret_type": ret_type }, p) } - // type definition + // type definition: `type Name = { field: Type, ... }` + // The `=` between the name and the brace is optional in the spec but + // present in practice. Skip it if present before consuming the LBrace. if k == "Type" { let p = pos + 1 let name = tok_value(tokens, p) let p = p + 1 + // Consume optional `=` before the opening brace + let pk = tok_kind(tokens, p) + if pk == "Eq" { let p = p + 1 } let p = expect(tokens, p, "LBrace") let fields: [Map] = native_list_empty() let running = true @@ -914,6 +1420,40 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p) } + // try/catch statement + // try { body } catch (name: Type) { handler } + // The catch variable name and type are both captured; type is skipped. + if k == "Try" { + let p = pos + 1 + let r_try = parse_block(tokens, p) + let try_body = r_try["stmts"] + let p = r_try["pos"] + let catch_name = "err" + let k2 = tok_kind(tokens, p) + if str_eq(k2, "Catch") { + let p = p + 1 + let p = expect(tokens, p, "LParen") + // catch variable name + let kn = tok_kind(tokens, p) + if str_eq(kn, "Ident") { + let catch_name = tok_value(tokens, p) + let p = p + 1 + } + // optional type annotation: : Type + let k3 = tok_kind(tokens, p) + if str_eq(k3, "Colon") { + let p = p + 1 + let p = skip_type(tokens, p) + } + let p = expect(tokens, p, "RParen") + let r_catch = parse_block(tokens, p) + let catch_body = r_catch["stmts"] + let p = r_catch["pos"] + return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": catch_body }, p) + } + return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": native_list_empty() }, p) + } + // @decorator - capture decorator name and attach to following stmt if k == "At" { let p = pos + 1 diff --git a/examples/html-page.el b/examples/html-page.el new file mode 100644 index 0000000..95019ff --- /dev/null +++ b/examples/html-page.el @@ -0,0 +1,40 @@ +// html-page.el — Example of native HTML template syntax in El. +// +// El HTML templates let you write HTML directly in expression position. +// Interpolated values are automatically HTML-escaped. +// Use raw(expr) to bypass escaping when you know the content is safe. +// +// Compile and run: +// ./dist/platform/elc examples/html-page.el > /tmp/html-page.c +// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ +// -o /tmp/html-page /tmp/html-page.c el-compiler/runtime/el_runtime.c +// /tmp/html-page + +fn render_item(item: String) -> String { + return
  • {item}
  • +} + +fn render_page(title: String, items: [String]) -> String { + return + + + + {title} + + +

    {title}

    +
      + {#each items as item} +
    • {item}
    • + {/each} +
    +

    Built with El HTML templates

    + + +} + +fn main() -> Void { + let items: [String] = ["Lexer", "Parser", "Codegen", "Runtime"] + let page: String = render_page("El Compiler Stages", items) + println(page) +}