Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dbf2c659d9 | |||
| 2b8062c55f | |||
| 2ed6b26dde | |||
| d8e9fd12f4 | |||
| 8fa9c4ba20 | |||
| 9c7bde47dc | |||
| c0553459e1 | |||
| fd208583fe | |||
| 3e29fc43ab | |||
| 979a5677d5 | |||
| 17b1aa0736 | |||
| f0c731d2db | |||
| e7e0f7d3e5 |
@@ -252,51 +252,4 @@ jobs:
|
|||||||
--source=el-compiler/runtime/el_runtime.js
|
--source=el-compiler/runtime/el_runtime.js
|
||||||
|
|
||||||
echo "Published El SDK version=${VERSION} to foundation-dev"
|
echo "Published El SDK version=${VERSION} to foundation-dev"
|
||||||
# Keep key alive for the ci-base rebuild step below
|
|
||||||
# (deleted in that step after docker push)
|
|
||||||
|
|
||||||
- name: Rebuild ci-base with fresh El SDK (dev)
|
|
||||||
# Patches ci-base:dev in-place: pulls the existing image (which has all
|
|
||||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
|
||||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
env:
|
|
||||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
|
|
||||||
SHA="${GITHUB_SHA:0:8}"
|
|
||||||
|
|
||||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
|
||||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
|
||||||
gcloud config set project neuron-785695
|
|
||||||
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
|
||||||
|
|
||||||
# Pull existing ci-base:dev (system deps stay cached in the base layer)
|
|
||||||
docker pull "${CI_BASE}:dev" || docker pull "${CI_BASE}:latest"
|
|
||||||
|
|
||||||
# Inline Dockerfile — only replaces the El SDK layer
|
|
||||||
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
|
|
||||||
ARG BASE
|
|
||||||
FROM ${BASE}
|
|
||||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
|
||||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
|
||||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
|
||||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
|
||||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
|
||||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
|
||||||
EOF
|
|
||||||
|
|
||||||
docker build \
|
|
||||||
--build-arg BASE="${CI_BASE}:dev" \
|
|
||||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
|
||||||
-f /tmp/Dockerfile.ci-base-patch \
|
|
||||||
-t "${CI_BASE}:dev" \
|
|
||||||
-t "${CI_BASE}:dev-${SHA}" \
|
|
||||||
.
|
|
||||||
|
|
||||||
docker push "${CI_BASE}:dev"
|
|
||||||
docker push "${CI_BASE}:dev-${SHA}"
|
|
||||||
|
|
||||||
echo "ci-base rebuilt: ${CI_BASE}:dev (${SHA})"
|
|
||||||
rm -f /tmp/gcp-key.json
|
rm -f /tmp/gcp-key.json
|
||||||
|
|||||||
@@ -246,51 +246,4 @@ jobs:
|
|||||||
--source=el-compiler/runtime/el_runtime.h
|
--source=el-compiler/runtime/el_runtime.h
|
||||||
|
|
||||||
echo "Published El SDK version=${VERSION} to foundation-stage"
|
echo "Published El SDK version=${VERSION} to foundation-stage"
|
||||||
# Keep key alive for the ci-base rebuild step below
|
|
||||||
# (deleted in that step after docker push)
|
|
||||||
|
|
||||||
- name: Rebuild ci-base with fresh El SDK (stage)
|
|
||||||
# Patches ci-base:stage in-place: pulls the existing image (which has all
|
|
||||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
|
||||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
env:
|
|
||||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
|
|
||||||
SHA="${GITHUB_SHA:0:8}"
|
|
||||||
|
|
||||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
|
||||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
|
||||||
gcloud config set project neuron-785695
|
|
||||||
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
|
||||||
|
|
||||||
# Pull existing ci-base:stage (system deps stay cached in the base layer)
|
|
||||||
docker pull "${CI_BASE}:stage" || docker pull "${CI_BASE}:latest"
|
|
||||||
|
|
||||||
# Inline Dockerfile — only replaces the El SDK layer
|
|
||||||
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
|
|
||||||
ARG BASE
|
|
||||||
FROM ${BASE}
|
|
||||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
|
||||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
|
||||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
|
||||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
|
||||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
|
||||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
|
||||||
EOF
|
|
||||||
|
|
||||||
docker build \
|
|
||||||
--build-arg BASE="${CI_BASE}:stage" \
|
|
||||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
|
||||||
-f /tmp/Dockerfile.ci-base-patch \
|
|
||||||
-t "${CI_BASE}:stage" \
|
|
||||||
-t "${CI_BASE}:stage-${SHA}" \
|
|
||||||
.
|
|
||||||
|
|
||||||
docker push "${CI_BASE}:stage"
|
|
||||||
docker push "${CI_BASE}:stage-${SHA}"
|
|
||||||
|
|
||||||
echo "ci-base rebuilt: ${CI_BASE}:stage (${SHA})"
|
|
||||||
rm -f /tmp/gcp-key.json
|
rm -f /tmp/gcp-key.json
|
||||||
|
|||||||
@@ -3135,23 +3135,49 @@ static void jb_puts(JsonBuf* b, const char* s) {
|
|||||||
|
|
||||||
static void jb_emit_escaped(JsonBuf* b, const char* s) {
|
static void jb_emit_escaped(JsonBuf* b, const char* s) {
|
||||||
jb_putc(b, '"');
|
jb_putc(b, '"');
|
||||||
for (; *s; s++) {
|
const unsigned char* p = (const unsigned char*)s;
|
||||||
unsigned char c = (unsigned char)*s;
|
while (*p) {
|
||||||
|
unsigned char c = *p;
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case '"': jb_puts(b, "\\\""); break;
|
case '"': jb_puts(b, "\\\""); p++; break;
|
||||||
case '\\': jb_puts(b, "\\\\"); break;
|
case '\\': jb_puts(b, "\\\\"); p++; break;
|
||||||
case '\b': jb_puts(b, "\\b"); break;
|
case '\b': jb_puts(b, "\\b"); p++; break;
|
||||||
case '\f': jb_puts(b, "\\f"); break;
|
case '\f': jb_puts(b, "\\f"); p++; break;
|
||||||
case '\n': jb_puts(b, "\\n"); break;
|
case '\n': jb_puts(b, "\\n"); p++; break;
|
||||||
case '\r': jb_puts(b, "\\r"); break;
|
case '\r': jb_puts(b, "\\r"); p++; break;
|
||||||
case '\t': jb_puts(b, "\\t"); break;
|
case '\t': jb_puts(b, "\\t"); p++; break;
|
||||||
default:
|
default:
|
||||||
if (c < 0x20) {
|
if (c < 0x20) {
|
||||||
char tmp[8];
|
char tmp[8];
|
||||||
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
|
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
|
||||||
jb_puts(b, tmp);
|
jb_puts(b, tmp);
|
||||||
} else {
|
p++;
|
||||||
|
} else if (c < 0x80) {
|
||||||
jb_putc(b, (char)c);
|
jb_putc(b, (char)c);
|
||||||
|
p++;
|
||||||
|
} else {
|
||||||
|
/* Multi-byte UTF-8: validate sequence, pass through if valid,
|
||||||
|
* escape as \u00xx if the start byte is invalid/orphaned. */
|
||||||
|
int seq_len = 0;
|
||||||
|
if ((c & 0xE0) == 0xC0) seq_len = 2;
|
||||||
|
else if ((c & 0xF0) == 0xE0) seq_len = 3;
|
||||||
|
else if ((c & 0xF8) == 0xF0) seq_len = 4;
|
||||||
|
if (seq_len >= 2) {
|
||||||
|
int valid = 1;
|
||||||
|
for (int i = 1; i < seq_len; i++) {
|
||||||
|
if ((p[i] & 0xC0) != 0x80) { valid = 0; break; }
|
||||||
|
}
|
||||||
|
if (valid) {
|
||||||
|
for (int i = 0; i < seq_len; i++) jb_putc(b, (char)p[i]);
|
||||||
|
p += seq_len;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Invalid start byte or truncated sequence — escape it */
|
||||||
|
char tmp[8];
|
||||||
|
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
|
||||||
|
jb_puts(b, tmp);
|
||||||
|
p++;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -8447,7 +8473,7 @@ static el_val_t llm_provider_request(const char* url, const char* key,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static el_val_t llm_chain_call(const char* system_str, const char* user_str) {
|
static el_val_t llm_chain_call(const char* model_pref, const char* system_str, const char* user_str) {
|
||||||
char url_key[64], key_key[64], fmt_key[64], model_key[64];
|
char url_key[64], key_key[64], fmt_key[64], model_key[64];
|
||||||
for (int i = 0; i < LLM_MAX_PROVIDERS; i++) {
|
for (int i = 0; i < LLM_MAX_PROVIDERS; i++) {
|
||||||
snprintf(url_key, sizeof(url_key), "NEURON_LLM_%d_URL", i);
|
snprintf(url_key, sizeof(url_key), "NEURON_LLM_%d_URL", i);
|
||||||
@@ -8460,6 +8486,7 @@ static el_val_t llm_chain_call(const char* system_str, const char* user_str) {
|
|||||||
const char* fmt_s = getenv(fmt_key);
|
const char* fmt_s = getenv(fmt_key);
|
||||||
int fmt = (fmt_s && strcmp(fmt_s, "anthropic") == 0) ? 1 : 0;
|
int fmt = (fmt_s && strcmp(fmt_s, "anthropic") == 0) ? 1 : 0;
|
||||||
const char* model = getenv(model_key);
|
const char* model = getenv(model_key);
|
||||||
|
if (!model || !*model) model = model_pref; /* fall back to the caller-requested model */
|
||||||
fprintf(stderr, "[llm] trying provider %d (%s)\n", i, url);
|
fprintf(stderr, "[llm] trying provider %d (%s)\n", i, url);
|
||||||
el_val_t result = llm_provider_request(url, key, fmt, model, system_str, user_str);
|
el_val_t result = llm_provider_request(url, key, fmt, model, system_str, user_str);
|
||||||
const char* t = EL_CSTR(result);
|
const char* t = EL_CSTR(result);
|
||||||
@@ -8470,7 +8497,7 @@ static el_val_t llm_chain_call(const char* system_str, const char* user_str) {
|
|||||||
const char* api_key = getenv("ANTHROPIC_API_KEY");
|
const char* api_key = getenv("ANTHROPIC_API_KEY");
|
||||||
if (!api_key || !*api_key) return http_error_json("no LLM providers configured");
|
if (!api_key || !*api_key) return http_error_json("no LLM providers configured");
|
||||||
fprintf(stderr, "[llm] using legacy ANTHROPIC_API_KEY fallback\n");
|
fprintf(stderr, "[llm] using legacy ANTHROPIC_API_KEY fallback\n");
|
||||||
return llm_provider_request(LLM_API_URL, api_key, 1, NULL, system_str, user_str);
|
return llm_provider_request(LLM_API_URL, api_key, 1, model_pref, system_str, user_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Legacy llm_request — kept for backward compat with agentic loop internals */
|
/* Legacy llm_request — kept for backward compat with agentic loop internals */
|
||||||
@@ -8534,14 +8561,16 @@ static el_val_t llm_extract_text(el_val_t resp_val) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
el_val_t llm_call(el_val_t model, el_val_t prompt) {
|
el_val_t llm_call(el_val_t model, el_val_t prompt) {
|
||||||
|
const char* m = EL_CSTR(model);
|
||||||
const char* u = EL_CSTR(prompt); if (!u) u = "";
|
const char* u = EL_CSTR(prompt); if (!u) u = "";
|
||||||
return llm_chain_call(NULL, u);
|
return llm_chain_call(m, NULL, u);
|
||||||
}
|
}
|
||||||
|
|
||||||
el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt) {
|
el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt) {
|
||||||
|
const char* m = EL_CSTR(model);
|
||||||
const char* s = EL_CSTR(system_prompt); if (!s) s = "";
|
const char* s = EL_CSTR(system_prompt); if (!s) s = "";
|
||||||
const char* u = EL_CSTR(user_prompt); if (!u) u = "";
|
const char* u = EL_CSTR(user_prompt); if (!u) u = "";
|
||||||
return llm_chain_call(s, u);
|
return llm_chain_call(m, s, u);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Tool registry for llm_call_agentic ─────────────────────────────────── */
|
/* ── Tool registry for llm_call_agentic ─────────────────────────────────── */
|
||||||
|
|||||||
@@ -324,10 +324,6 @@ fn cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
|
|||||||
let each_c: String = cg_html_each(child, acc_var)
|
let each_c: String = cg_html_each(child, acc_var)
|
||||||
let parts = native_list_append(parts, each_c)
|
let parts = native_list_append(parts, each_c)
|
||||||
}
|
}
|
||||||
if str_eq(html_kind, "HtmlIf") {
|
|
||||||
let if_c: String = cg_html_if(child, acc_var)
|
|
||||||
let parts = native_list_append(parts, if_c)
|
|
||||||
}
|
|
||||||
let i = i + 1
|
let i = i + 1
|
||||||
}
|
}
|
||||||
str_join(parts, "")
|
str_join(parts, "")
|
||||||
@@ -417,17 +413,6 @@ fn cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
|
|||||||
"{ 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 + "} } "
|
"{ 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 + "} } "
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate code for {#if cond} ... {/if} (with optional {#else}).
|
|
||||||
fn cg_html_if(node: Map<String, Any>, acc_var: String) -> String {
|
|
||||||
let cond_expr = node["cond"]
|
|
||||||
let then_children: [Map<String, Any>] = node["then"]
|
|
||||||
let else_children: [Map<String, Any>] = node["else"]
|
|
||||||
let cond_c: String = cg_expr(cond_expr)
|
|
||||||
let then_c: String = cg_html_parts(then_children, acc_var)
|
|
||||||
let else_c: String = cg_html_parts(else_children, acc_var)
|
|
||||||
"if (" + cond_c + ") { " + then_c + " } else { " + else_c + " } "
|
|
||||||
}
|
|
||||||
|
|
||||||
// Top-level HTML template codegen — returns a C statement-expression string.
|
// Top-level HTML template codegen — returns a C statement-expression string.
|
||||||
fn cg_html_template(expr: Map<String, Any>) -> String {
|
fn cg_html_template(expr: Map<String, Any>) -> String {
|
||||||
let root = expr["root"]
|
let root = expr["root"]
|
||||||
|
|||||||
+93
-283
@@ -293,48 +293,6 @@ fn is_void_element(name: String) -> Bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all tokens as raw text until </tag_name> is encountered.
|
|
||||||
// Used for <style> and <script> elements to avoid parsing CSS/JS as El.
|
|
||||||
// Returns { "text": "...", "pos": p_after_closing_tag }
|
|
||||||
fn parse_raw_text_content(tokens: [Any], pos: Int, tag_name: String) -> 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 k2 = tok_kind(tokens, p + 1)
|
|
||||||
if str_eq(k2, "Slash") {
|
|
||||||
// Check if this is </tag_name>
|
|
||||||
let close_name = tok_value(tokens, p + 2)
|
|
||||||
if str_eq(close_name, tag_name) {
|
|
||||||
// consume </tag_name>
|
|
||||||
let p = p + 3
|
|
||||||
let p = expect(tokens, p, "Gt")
|
|
||||||
let running = false
|
|
||||||
} else {
|
|
||||||
let v = tok_value(tokens, p)
|
|
||||||
let parts = native_list_append(parts, v)
|
|
||||||
let p = p + 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let v = tok_value(tokens, p)
|
|
||||||
let parts = native_list_append(parts, v)
|
|
||||||
let p = p + 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let v = tok_value(tokens, p)
|
|
||||||
let parts = native_list_append(parts, v)
|
|
||||||
let p = p + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{ "text": str_join(parts, " "), "pos": p }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect tokens as text content until we hit Lt, LBrace, Eof, or a
|
// Collect tokens as text content until we hit Lt, LBrace, Eof, or a
|
||||||
// closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p }
|
// closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p }
|
||||||
fn parse_html_text_tokens(tokens: [Any], pos: Int) -> Map<String, Any> {
|
fn parse_html_text_tokens(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||||
@@ -477,125 +435,77 @@ fn parse_html_children(tokens: [Any], pos: Int, parent_tag: String) -> Map<Strin
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if str_eq(k, "LBrace") {
|
if str_eq(k, "LBrace") {
|
||||||
// Interpolation: {expr}, {#each ...}, {#if ...}, {#else}, {/each}, {/if}
|
// Interpolation: {expr} or {#each ...} or {/each}
|
||||||
// Note: '#' (ASCII 35) is skipped by the lexer, so {#each} lexes as
|
|
||||||
// LBrace Ident:"each" ... and {#if} lexes as LBrace If ... and
|
|
||||||
// {#else} lexes as LBrace Else RBrace.
|
|
||||||
let k2 = tok_kind(tokens, p + 1)
|
let k2 = tok_kind(tokens, p + 1)
|
||||||
if str_eq(k2, "Slash") {
|
if str_eq(k2, "Hash") {
|
||||||
// {/each} or {/if} — end of block, stop
|
// {#each list as item}
|
||||||
// skip { /
|
let k3_v = tok_value(tokens, p + 2)
|
||||||
let p = p + 2
|
if str_eq(k3_v, "each") {
|
||||||
// skip the close-tag name token (each, if, etc.)
|
let p = p + 3
|
||||||
let p = p + 1
|
// parse list expr up to "as" keyword
|
||||||
// skip }
|
|
||||||
let p = expect(tokens, p, "RBrace")
|
|
||||||
let running = false
|
|
||||||
} else {
|
|
||||||
if str_eq(k2, "If") {
|
|
||||||
// {#if condition} ... {/if}
|
|
||||||
// Skip { if (2 tokens; '#' was silently skipped by lexer)
|
|
||||||
let p = p + 2
|
|
||||||
// Parse condition expression (no block expr)
|
|
||||||
let prev_no_block: String = state_get("__no_block_expr")
|
let prev_no_block: String = state_get("__no_block_expr")
|
||||||
state_set("__no_block_expr", "1")
|
state_set("__no_block_expr", "1")
|
||||||
let r_cond = parse_expr(tokens, p)
|
let r_list = parse_expr(tokens, p)
|
||||||
state_set("__no_block_expr", prev_no_block)
|
state_set("__no_block_expr", prev_no_block)
|
||||||
let cond_expr = r_cond["node"]
|
let list_expr = r_list["node"]
|
||||||
let p = r_cond["pos"]
|
let p = r_list["pos"]
|
||||||
el_release(r_cond)
|
// r_list result map fully consumed — release to free peak heap.
|
||||||
|
el_release(r_list)
|
||||||
|
// expect "as"
|
||||||
|
let p = expect(tokens, p, "As")
|
||||||
|
// item variable name
|
||||||
|
let item_name = tok_value(tokens, p)
|
||||||
|
let p = p + 1
|
||||||
// consume closing }
|
// consume closing }
|
||||||
let p = expect(tokens, p, "RBrace")
|
let p = expect(tokens, p, "RBrace")
|
||||||
// parse then-children until {#else} or {/if}
|
// parse body until {/each}
|
||||||
let r_then = parse_html_children(tokens, p, "__if_then__")
|
let r_body = parse_html_each_body(tokens, p)
|
||||||
let then_children = r_then["children"]
|
let body_children = r_body["children"]
|
||||||
let p = r_then["pos"]
|
let p = r_body["pos"]
|
||||||
el_release(r_then)
|
// r_body result map fully consumed — release to free peak heap.
|
||||||
// check for {#else} — lexed as LBrace Else RBrace
|
el_release(r_body)
|
||||||
let else_children: [Map<String, Any>] = native_list_empty()
|
let each_node: Map<String, Any> = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children }
|
||||||
let ck = tok_kind(tokens, p)
|
let children = native_list_append(children, each_node)
|
||||||
if str_eq(ck, "LBrace") {
|
} else {
|
||||||
let ck2 = tok_kind(tokens, p + 1)
|
let p = p + 1
|
||||||
if str_eq(ck2, "Else") {
|
}
|
||||||
// consume { else }
|
} else {
|
||||||
let p = p + 2
|
if str_eq(k2, "Slash") {
|
||||||
let p = expect(tokens, p, "RBrace")
|
// {/each} — end of each block, stop
|
||||||
// parse else-children until {/if}
|
// skip {/each}
|
||||||
let r_else = parse_html_children(tokens, p, "__if_else__")
|
let p = p + 2
|
||||||
let else_children = r_else["children"]
|
// skip "each"
|
||||||
let p = r_else["pos"]
|
let p = p + 1
|
||||||
el_release(r_else)
|
// 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"]
|
||||||
|
// r result map fully consumed — release to free peak heap.
|
||||||
|
el_release(r)
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let if_node: Map<String, Any> = { "html": "HtmlIf", "cond": cond_expr, "then": then_children, "else": else_children }
|
if is_raw_call {
|
||||||
let children = native_list_append(children, if_node)
|
let raw_args = interp_val["args"]
|
||||||
} else {
|
let raw_inner = native_list_get(raw_args, 0)
|
||||||
if str_eq(k2, "Else") {
|
let children = native_list_append(children, { "html": "Raw", "value": raw_inner })
|
||||||
// {#else} sentinel — lexed as LBrace Else RBrace
|
|
||||||
// Do NOT consume — leave position for caller ({#if} handler checks for it)
|
|
||||||
let running = false
|
|
||||||
} else {
|
} else {
|
||||||
// Check for {#each list as item} — lexed as LBrace Ident:"each" ...
|
let children = native_list_append(children, { "html": "Interp", "value": interp_val })
|
||||||
let k2_v = tok_value(tokens, p + 1)
|
|
||||||
if str_eq(k2_v, "each") {
|
|
||||||
let p = p + 2
|
|
||||||
// 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"]
|
|
||||||
// r_list result map fully consumed — release to free peak heap.
|
|
||||||
el_release(r_list)
|
|
||||||
// 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"]
|
|
||||||
// r_body result map fully consumed — release to free peak heap.
|
|
||||||
el_release(r_body)
|
|
||||||
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 {
|
|
||||||
// regular {expr} — disable map-literal parsing so {fn(a,b)}
|
|
||||||
// does not trigger the LBrace→map path inside parse_primary
|
|
||||||
let prev_no_block: String = state_get("__no_block_expr")
|
|
||||||
state_set("__no_block_expr", "1")
|
|
||||||
let r = parse_expr(tokens, p + 1)
|
|
||||||
state_set("__no_block_expr", prev_no_block)
|
|
||||||
let interp_val = r["node"]
|
|
||||||
let p = r["pos"]
|
|
||||||
// r result map fully consumed — release to free peak heap.
|
|
||||||
el_release(r)
|
|
||||||
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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -655,27 +565,6 @@ fn parse_html_element(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
if is_void_element(tag_name) {
|
if is_void_element(tag_name) {
|
||||||
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
|
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
|
||||||
}
|
}
|
||||||
// raw-text mode for style/script — collect content as plain text without parsing CSS/JS as El
|
|
||||||
if str_eq(tag_name, "style") {
|
|
||||||
let r_raw = parse_raw_text_content(tokens, p, "style")
|
|
||||||
let raw_text: String = r_raw["text"]
|
|
||||||
let p = r_raw["pos"]
|
|
||||||
el_release(r_raw)
|
|
||||||
let raw_child: Map<String, Any> = { "html": "Text", "text": raw_text }
|
|
||||||
let raw_children: [Map<String, Any>] = native_list_empty()
|
|
||||||
let raw_children = native_list_append(raw_children, raw_child)
|
|
||||||
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": raw_children, "self_closing": false }, p)
|
|
||||||
}
|
|
||||||
if str_eq(tag_name, "script") {
|
|
||||||
let r_raw = parse_raw_text_content(tokens, p, "script")
|
|
||||||
let raw_text: String = r_raw["text"]
|
|
||||||
let p = r_raw["pos"]
|
|
||||||
el_release(r_raw)
|
|
||||||
let raw_child: Map<String, Any> = { "html": "Text", "text": raw_text }
|
|
||||||
let raw_children: [Map<String, Any>] = native_list_empty()
|
|
||||||
let raw_children = native_list_append(raw_children, raw_child)
|
|
||||||
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": raw_children, "self_closing": false }, p)
|
|
||||||
}
|
|
||||||
// parse children
|
// parse children
|
||||||
let r_children = parse_html_children(tokens, p, tag_name)
|
let r_children = parse_html_children(tokens, p, tag_name)
|
||||||
let children = r_children["children"]
|
let children = r_children["children"]
|
||||||
@@ -829,123 +718,44 @@ fn parse_primary(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
// as the start of the block they're expecting.
|
// as the start of the block they're expecting.
|
||||||
return make_result({ "expr": "Nil" }, pos)
|
return make_result({ "expr": "Nil" }, pos)
|
||||||
}
|
}
|
||||||
// Distinguish map literal from interpolation chain.
|
let p = pos + 1
|
||||||
// A map literal requires { key: value } — the second token inside { must be Colon.
|
let pairs: [Map<String, Any>] = native_list_empty()
|
||||||
// An empty {} is a map literal. Everything else is an interpolation chain.
|
let running = true
|
||||||
let first_k: String = tok_kind(tokens, pos + 1)
|
while running {
|
||||||
let second_k: String = tok_kind(tokens, pos + 2)
|
let k2 = tok_kind(tokens, p)
|
||||||
if str_eq(first_k, "RBrace") {
|
if k2 == "RBrace" {
|
||||||
// Empty map literal {}
|
let running = false
|
||||||
return make_result({ "expr": "Map", "pairs": native_list_empty() }, pos + 2)
|
} else {
|
||||||
}
|
if k2 == "Eof" {
|
||||||
if str_eq(second_k, "Colon") {
|
|
||||||
// MAP LITERAL: { key: value, ... }
|
|
||||||
let p = pos + 1
|
|
||||||
let pairs: [Map<String, Any>] = native_list_empty()
|
|
||||||
let running = true
|
|
||||||
while running {
|
|
||||||
let k2 = tok_kind(tokens, p)
|
|
||||||
if k2 == "RBrace" {
|
|
||||||
let running = false
|
let running = false
|
||||||
} else {
|
} else {
|
||||||
if k2 == "Eof" {
|
// key: Str token
|
||||||
let running = false
|
let key = tok_value(tokens, p)
|
||||||
|
let new_p: Int = p + 1
|
||||||
|
let new_p = expect(tokens, new_p, "Colon")
|
||||||
|
let r = parse_expr(tokens, new_p)
|
||||||
|
let val_node = r["node"]
|
||||||
|
let new_p = r["pos"]
|
||||||
|
// r result map fully consumed — release to free peak heap.
|
||||||
|
el_release(r)
|
||||||
|
let pair = { "key": key, "value": val_node }
|
||||||
|
let pairs = native_list_append(pairs, pair)
|
||||||
|
let k3 = tok_kind(tokens, new_p)
|
||||||
|
if k3 == "Comma" {
|
||||||
|
let new_p = new_p + 1
|
||||||
|
}
|
||||||
|
// Non-progress guard: malformed map content can leave
|
||||||
|
// parse_expr returning the same pos. Force advance.
|
||||||
|
if new_p <= p {
|
||||||
|
let p = p + 1
|
||||||
} else {
|
} else {
|
||||||
// key: Str or Ident token
|
let p = new_p
|
||||||
let key = tok_value(tokens, p)
|
|
||||||
let new_p: Int = p + 1
|
|
||||||
let new_p = expect(tokens, new_p, "Colon")
|
|
||||||
let r = parse_expr(tokens, new_p)
|
|
||||||
let val_node = r["node"]
|
|
||||||
let new_p = r["pos"]
|
|
||||||
// r result map fully consumed — release to free peak heap.
|
|
||||||
el_release(r)
|
|
||||||
let pair = { "key": key, "value": val_node }
|
|
||||||
let pairs = native_list_append(pairs, pair)
|
|
||||||
let k3 = tok_kind(tokens, new_p)
|
|
||||||
if k3 == "Comma" {
|
|
||||||
let new_p = new_p + 1
|
|
||||||
}
|
|
||||||
// Non-progress guard: malformed map content can leave
|
|
||||||
// parse_expr returning the same pos. Force advance.
|
|
||||||
if new_p <= p {
|
|
||||||
let p = p + 1
|
|
||||||
} else {
|
|
||||||
let p = new_p
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let p = expect(tokens, p, "RBrace")
|
|
||||||
return make_result({ "expr": "Map", "pairs": pairs }, p)
|
|
||||||
}
|
}
|
||||||
// INTERPOLATION CHAIN: {expr}, {expr}{expr}, {expr}<html>, etc.
|
let p = expect(tokens, p, "RBrace")
|
||||||
// Build a BinOp(Plus, ...) concatenation chain.
|
return make_result({ "expr": "Map", "pairs": pairs }, p)
|
||||||
let p = pos
|
|
||||||
let chain_node: Map<String, Any> = { "expr": "Nil" }
|
|
||||||
let chain_started = false
|
|
||||||
let chain_running = true
|
|
||||||
while chain_running {
|
|
||||||
let ck: String = tok_kind(tokens, p)
|
|
||||||
if str_eq(ck, "LBrace") {
|
|
||||||
let prev_no_block: String = state_get("__no_block_expr")
|
|
||||||
state_set("__no_block_expr", "1")
|
|
||||||
let r = parse_expr(tokens, p + 1)
|
|
||||||
state_set("__no_block_expr", prev_no_block)
|
|
||||||
let part = r["node"]
|
|
||||||
let p = r["pos"]
|
|
||||||
// r result map fully consumed — release to free peak heap.
|
|
||||||
el_release(r)
|
|
||||||
let p = expect(tokens, p, "RBrace")
|
|
||||||
if !chain_started {
|
|
||||||
let chain_node = part
|
|
||||||
let chain_started = true
|
|
||||||
} else {
|
|
||||||
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if str_eq(ck, "Lt") {
|
|
||||||
let ck2: String = tok_kind(tokens, p + 1)
|
|
||||||
if str_eq(ck2, "Not") {
|
|
||||||
let r = parse_html_template(tokens, p)
|
|
||||||
let part = r["node"]
|
|
||||||
let p = r["pos"]
|
|
||||||
// r result map fully consumed — release to free peak heap.
|
|
||||||
el_release(r)
|
|
||||||
if !chain_started {
|
|
||||||
let chain_node = part
|
|
||||||
let chain_started = true
|
|
||||||
} else {
|
|
||||||
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if str_eq(ck2, "Ident") {
|
|
||||||
let tag_candidate: String = tok_value(tokens, p + 1)
|
|
||||||
if is_html_tag_name(tag_candidate) {
|
|
||||||
let r = parse_html_template(tokens, p)
|
|
||||||
let part = r["node"]
|
|
||||||
let p = r["pos"]
|
|
||||||
// r result map fully consumed — release to free peak heap.
|
|
||||||
el_release(r)
|
|
||||||
if !chain_started {
|
|
||||||
let chain_node = part
|
|
||||||
let chain_started = true
|
|
||||||
} else {
|
|
||||||
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let chain_running = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let chain_running = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let chain_running = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return make_result(chain_node, p)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if expression
|
// if expression
|
||||||
|
|||||||
Reference in New Issue
Block a user