Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32f0cf7b5d | |||
| 65e26cd7a5 | |||
| 1fd7cd5545 | |||
| 71689520b6 |
Vendored
BIN
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, '"')
|
||||
.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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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"]
|
||||
|
||||
@@ -787,6 +948,10 @@ fn cg_expr(expr: Map<String, Any>) -> String {
|
||||
return cg_match(expr)
|
||||
}
|
||||
|
||||
if kind == "HtmlTemplate" {
|
||||
return cg_html_template(expr)
|
||||
}
|
||||
|
||||
"EL_NULL"
|
||||
}
|
||||
|
||||
|
||||
@@ -713,8 +713,13 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
// unknown char — skip
|
||||
let i = i + 1
|
||||
if ch == "#" {
|
||||
let tokens = native_list_append(tokens, make_tok("Hash", "#"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
// unknown char — skip
|
||||
let i = i + 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)
|
||||
|
||||
@@ -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