Compare commits

...

4 Commits

Author SHA1 Message Date
Will Anderson 32f0cf7b5d Add html-page.el example and rebuild elc binary
examples/html-page.el demonstrates HTML template syntax:
- <!doctype html> prefix handling
- Attribute values (static and interpolated)
- {#each list as item} iteration
- Auto-escaped interpolation via {expr}
- Self-closing void elements (meta, br, etc.)

Rebuilt dist/platform/elc from modified compiler source. The new
binary is self-hosted from the HTML-capable compiler source and
passes the standard identity check.
2026-05-04 13:02:54 -05:00
Will Anderson 65e26cd7a5 Add HTML template codegen and runtime for JS backend
JS codegen (codegen-js.el):
- js_cg_html_template: emits an IIFE that builds HTML via += concat
- js_cg_html_element_str / js_cg_html_parts / js_cg_html_attrs_str:
  mirror the C codegen structure using JS string accumulator
- js_cg_html_each: {#each} compiles to a JS for-loop
- Reuses existing js_str_lit / js_escape from the file header

Runtime (el_runtime.js):
- html_escape(s): replaces & < > " ' using regex chains
- html_raw(s): identity function
- Both exported from the runtime module exports object
2026-05-04 13:02:50 -05:00
Will Anderson 1fd7cd5545 Add HTML template codegen and runtime for C backend
C codegen (codegen.el):
- cg_html_template: emits a GCC/Clang statement-expression that
  builds the HTML string via el_str_concat chains
- cg_html_element_str / cg_html_parts / cg_html_attrs_str: recursive
  element and attribute emitters
- cg_html_each: {#each} compiles to a C for-loop with el_list_get
- __html_counter state tracks unique accumulator variable names
- Handles both 'static' (raw string) and 'dynamic' (expr node) attrs
  matching the parser's attribute kind convention

Runtime (el_runtime.c / el_runtime.h):
- html_escape(s): escapes & < > " ' for safe interpolation
- html_raw(s): identity function for raw() bypass
- Both use the existing html_buf_t infrastructure from el_html_sanitize
2026-05-04 13:02:44 -05:00
Will Anderson 71689520b6 Add HTML template syntax to El parser
Adds native HTML template literals to the El parser. Templates are
detected in value position when Lt is followed by a known HTML tag
name (is_html_tag_name) or by '!' for <!doctype html>.

New parser helpers:
- is_html_tag_name / is_void_element: classify tag names
- parse_html_text_tokens: collect intertoken text content
- parse_html_attrs: parse name, name="val", name={expr} attributes
- parse_html_children: recursive children with {expr}, {#each} support
- parse_html_element / parse_html_template: entry points

Adds Hash token kind ('#') to lexer for {#each} block syntax.

AST nodes: HtmlTemplate, html:Element, html:Text, html:Interp,
html:Raw, html:Each, html:Doctype. Doctype flag is stored on the
root element node rather than as a separate AST layer.

HTML templates parse correctly after 'return' and as the sole
expression in a function body. The 'return' keyword is required when
other let bindings precede the template, as El has no newline-as-
statement-terminator and '<' would otherwise be parsed as comparison.
2026-05-04 13:02:36 -05:00
9 changed files with 882 additions and 2 deletions
BIN
View File
Binary file not shown.
+39
View File
@@ -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, "&amp;"); break;
case '<': html_buf_puts(&out, "&lt;"); break;
case '>': html_buf_puts(&out, "&gt;"); break;
case '"': html_buf_puts(&out, "&quot;"); break;
case '\'': html_buf_puts(&out, "&#39;"); 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
+7
View File
@@ -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);
+18
View File
@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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,
+136
View File
@@ -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"
}
+165
View File
@@ -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"
}
+7 -2
View File
@@ -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
}
}
}
}
+470
View File
@@ -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)
+40
View File
@@ -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)
}