merge integrate/el-html-templates: add HTML template parser and codegen
This commit is contained in:
@@ -191,6 +191,138 @@ fn js_is_int_call(call_expr: Map<String, Any>) -> 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<String, Any>], acc_var: String) -> String {
|
||||
let n: Int = native_list_len(children)
|
||||
let i = 0
|
||||
let out = ""
|
||||
while i < n {
|
||||
let child: Map<String, Any> = 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 + " += \"<!doctype html>\"; "
|
||||
}
|
||||
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<String, Any>], acc_var: String) -> String {
|
||||
let n: Int = native_list_len(attrs)
|
||||
let i = 0
|
||||
let out = ""
|
||||
while i < n {
|
||||
let attr: Map<String, Any> = 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<String, Any>, acc_var: String) -> String {
|
||||
let tag: String = elem["tag"]
|
||||
let attrs: [Map<String, Any>] = elem["attrs"]
|
||||
let children: [Map<String, Any>] = 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("</" + tag + ">") + "; "
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn js_cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
|
||||
let list_expr = node["list"]
|
||||
let item_name: String = node["item"]
|
||||
let body_children: [Map<String, Any>] = 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, Any>) -> 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 + " += \"<!doctype html>\"; "
|
||||
}
|
||||
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, Any>) -> String {
|
||||
return js_cg_lambda(expr)
|
||||
}
|
||||
|
||||
if kind == "HtmlTemplate" {
|
||||
return js_cg_html_template(expr)
|
||||
}
|
||||
|
||||
"null"
|
||||
}
|
||||
|
||||
|
||||
+194
-3
@@ -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<String, Any>], acc_var: String) -> String {
|
||||
let n: Int = native_list_len(children)
|
||||
let i = 0
|
||||
let out = ""
|
||||
while i < n {
|
||||
let child: Map<String, Any> = 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(\"<!doctype html>\")); "
|
||||
}
|
||||
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<String, Any>], 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<String, Any> = 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<String, Any>, acc_var: String) -> String {
|
||||
let tag: String = elem["tag"]
|
||||
let attrs: [Map<String, Any>] = elem["attrs"]
|
||||
let children: [Map<String, Any>] = elem["children"]
|
||||
let self_closing: Bool = elem["self_closing"]
|
||||
// Open tag: <tagname
|
||||
let out = acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<" + tag + "\")); "
|
||||
let out = out + cg_html_attrs_str(attrs, acc_var)
|
||||
if self_closing {
|
||||
// Self-closing void element: />
|
||||
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(\"</" + tag + ">\")); "
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// Generate code for {#each list as item} ... {/each}.
|
||||
fn cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
|
||||
let list_expr = node["list"]
|
||||
let item_name: String = node["item"]
|
||||
let body_children: [Map<String, Any>] = 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, Any>) -> 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 <!doctype html>
|
||||
let doctype_flag: Bool = root["doctype"]
|
||||
let doctype_prefix: String = ""
|
||||
if doctype_flag {
|
||||
let doctype_prefix = acc + " = el_str_concat(" + acc + ", EL_STR(\"<!doctype html>\")); "
|
||||
}
|
||||
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, Any>) -> String {
|
||||
let kind: String = expr["expr"]
|
||||
|
||||
@@ -788,7 +949,16 @@ fn cg_expr(expr: Map<String, Any>) -> 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, Any>) -> 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<String, Any>, 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 }
|
||||
|
||||
+541
-1
@@ -142,6 +142,460 @@ fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
|
||||
// -- Expression parsing --------------------------------------------------------
|
||||
|
||||
// ── HTML template parser ──────────────────────────────────────────────────────
|
||||
//
|
||||
// HTML templates are written as unquoted HTML in expression position:
|
||||
// return <div class="x"><h1>{title}</h1></div>
|
||||
//
|
||||
// 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
|
||||
// <!doctype html>). 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<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
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 </: Lt already stops us, but Slash alone
|
||||
// (after consuming whitespace) also stops text.
|
||||
// Anything else is text content.
|
||||
let v = tok_value(tokens, p)
|
||||
let parts = native_list_append(parts, v)
|
||||
let p = p + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, " "), "pos": p }
|
||||
}
|
||||
|
||||
// Parse an attribute list: (attrname | attrname="val" | attrname={expr})*
|
||||
// Stops at Gt or Slash (for self-closing />).
|
||||
fn parse_html_attrs(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let attrs: [Map<String, Any>] = 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 </tag>
|
||||
// or EOF. Returns { "children": [...], "pos": p_after_closing_tag }
|
||||
fn parse_html_children(tokens: [Map<String, Any>], pos: Int, parent_tag: String) -> Map<String, Any> {
|
||||
let children: [Map<String, Any>] = 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: </
|
||||
let k2 = tok_kind(tokens, p + 1)
|
||||
if str_eq(k2, "Slash") {
|
||||
// </tagname> — 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 <!doctype html>
|
||||
let k3_v = tok_value(tokens, p + 2)
|
||||
if str_eq(k3_v, "doctype") {
|
||||
// consume <!doctype html>
|
||||
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<String, Any> = { "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<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
parse_html_children(tokens, pos, "__each__")
|
||||
}
|
||||
|
||||
// Parse a single HTML element: <tag attrs> children </tag>
|
||||
// or self-closing: <tag attrs/>
|
||||
// Pos points to the Lt token.
|
||||
fn parse_html_element(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
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 <!doctype>).
|
||||
// May parse an optional <!doctype html> prefix followed by the root element.
|
||||
fn parse_html_template(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let p = pos
|
||||
// Check for <!doctype html>
|
||||
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 <!doctype html>
|
||||
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<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let k = tok_kind(tokens, pos)
|
||||
let v = tok_value(tokens, pos)
|
||||
@@ -166,6 +620,22 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
return make_result({ "expr": "Bool", "value": v }, pos + 1)
|
||||
}
|
||||
|
||||
// HTML template literal: <tagname ...>...</tagname> or <!doctype html>...
|
||||
// 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<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
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<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
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<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
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<String, Any>] = native_list_empty()
|
||||
let running = true
|
||||
@@ -914,6 +1420,40 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
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
|
||||
|
||||
@@ -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 <li class="item">{item}</li>
|
||||
}
|
||||
|
||||
fn render_page(title: String, items: [String]) -> String {
|
||||
return <!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{title}</h1>
|
||||
<ul>
|
||||
{#each items as item}
|
||||
<li class="item">{item}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<p>Built with El HTML templates</p>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
let items: [String] = ["Lexer", "Parser", "Codegen", "Runtime"]
|
||||
let page: String = render_page("El Compiler Stages", items)
|
||||
println(page)
|
||||
}
|
||||
Reference in New Issue
Block a user