#![allow(clippy::too_many_arguments)]
use std::collections::HashMap;
use crate::parser::{self, AttrValue, Document, Element, Node};
use crate::semantic::PropField;
pub fn generate(doc: &Document) -> String {
generate_with_id(doc, 0)
}
pub fn generate_with_id(doc: &Document, file_id: usize) -> String {
generate_with_imports(doc, file_id, &[], None, None, None, &[], None, &[], None, None)
}
pub type ResolvedImport<'a> = (&'a str, &'a str);
pub fn generate_with_imports(
doc: &Document,
file_id: usize,
imports: &[ResolvedImport],
component_name: Option<&str>,
resolved_state_type: Option<&str>,
props_type: Option<&str>,
import_props: &[(&str, Option<&str>)],
prop_fields: Option<&HashMap<String, Vec<PropField>>>,
import_has_slots: &[(&str, bool)],
style_cascade: Option<&fncc_styles::Stylesheet>,
style_theme: Option<&str>,
) -> String {
generate_with_imports_and_route_params(
doc,
file_id,
imports,
component_name,
resolved_state_type,
props_type,
import_props,
prop_fields,
import_has_slots,
style_cascade,
style_theme,
&[],
)
}
fn count_router_outlets(el: &Element) -> usize {
let mut count = if el.name == "RouterOutlet" { 1 } else { 0 };
for child in &el.children {
if let Node::Element(child_el) = child {
count += count_router_outlets(child_el);
}
}
count
}
fn format_interpolation(e: &str) -> String {
if e.contains(".render(") || e.contains("render_") || e.contains("children") {
e.to_string()
} else {
format!("{}.to_string()", e)
}
}
pub fn generate_with_imports_and_route_params(
doc: &Document,
file_id: usize,
imports: &[ResolvedImport],
component_name: Option<&str>,
resolved_state_type: Option<&str>,
props_type: Option<&str>,
import_props: &[(&str, Option<&str>)],
prop_fields: Option<&HashMap<String, Vec<PropField>>>,
import_has_slots: &[(&str, bool)],
style_cascade: Option<&fncc_styles::Stylesheet>,
style_theme: Option<&str>,
route_params: &[String],
) -> String {
let router_outlet_count = count_router_outlets(&doc.root);
if router_outlet_count > 1 {
panic!(
"duplicate RouterOutlet usage is not allowed: found {} RouterOutlet elements",
router_outlet_count
);
}
let mut out = String::new();
let state_type = resolved_state_type.or(doc.state_type.as_deref());
let has_state = state_type.is_some();
let has_slot = parser::has_slot(&doc.root);
if let Some(ref fm) = doc.frontmatter {
out.push_str(fm);
out.push('\n');
}
let commands = parser::collect_commands(&doc.root);
if !commands.is_empty() {
out.push_str("#[allow(unused)]\n");
out.push_str(&format!("fn _fncc_validate_{file_id}() {{\n"));
for cmd in &commands {
let trampoline = format!("__fncc_cmd_{cmd}");
out.push_str(&format!(" let _ = {trampoline};\n"));
}
out.push_str("}\n\n");
}
if has_state {
generate_stateful(
doc,
&mut out,
imports,
state_type,
import_props,
prop_fields,
import_has_slots,
style_cascade,
style_theme,
);
} else {
generate_stateless(
doc,
&mut out,
imports,
component_name,
props_type,
import_props,
prop_fields,
import_has_slots,
has_slot,
style_cascade,
style_theme,
route_params,
);
}
out
}
fn generate_stateless(
doc: &Document,
out: &mut String,
imports: &[ResolvedImport],
component_name: Option<&str>,
props_type: Option<&str>,
import_props: &[(&str, Option<&str>)],
prop_fields: Option<&HashMap<String, Vec<PropField>>>,
import_has_slots: &[(&str, bool)],
has_slot: bool,
style_cascade: Option<&fncc_styles::Stylesheet>,
style_theme: Option<&str>,
route_params: &[String],
) {
let name = component_name.unwrap_or(&doc.root.name);
let fn_name = format!("render_{}", to_snake_case(name));
if !route_params.is_empty()
&& let Some(pt) = props_type
{
panic!(
"routes combining dynamic parameters and props are not supported: screen '{}' has params {:?} and props type '{}'",
name, route_params, pt
);
}
let mut param_parts = Vec::new();
for p in route_params {
param_parts.push(format!("{p}: &str"));
}
if let Some(pt) = props_type {
param_parts.push(format!("props: &{pt}"));
}
if has_slot {
param_parts.push("children: impl IntoElement".to_string());
}
let params = param_parts.join(", ");
let sig = if params.is_empty() {
format!("pub fn {fn_name}() -> impl IntoElement {{\n")
} else {
format!("pub fn {fn_name}({params}) -> impl IntoElement {{\n")
};
out.push_str(&sig);
out.push_str(&generate_element(
&doc.root,
1,
false,
imports,
import_props,
prop_fields,
import_has_slots,
style_cascade,
style_theme,
));
out.push('\n');
out.push_str("}\n");
}
fn generate_stateful(
doc: &Document,
out: &mut String,
imports: &[ResolvedImport],
state_type: Option<&str>,
import_props: &[(&str, Option<&str>)],
prop_fields: Option<&HashMap<String, Vec<PropField>>>,
_import_has_slots: &[(&str, bool)],
style_cascade: Option<&fncc_styles::Stylesheet>,
style_theme: Option<&str>,
) {
let state_type = state_type.unwrap_or("Self");
out.push_str(&format!("impl Render for {state_type} {{\n"));
out.push_str(" fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n");
out.push_str(" let handle = cx.entity().downgrade();\n");
out.push_str(&generate_element(
&doc.root,
2,
true,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
));
out.push_str("\n }\n");
out.push_str("}\n");
}
fn collect_if_chain(nodes: &[Node], start: usize) -> Vec<&Element> {
let mut branches = Vec::new();
for node in &nodes[start..] {
match node {
Node::Element(el) if el.name == "If" => {
if branches.is_empty() {
branches.push(el);
} else {
break; }
}
Node::Element(el) if el.name == "ElseIf" || el.name == "Else" => {
if branches.is_empty() {
break; }
branches.push(el);
}
_ => break,
}
}
branches
}
fn gen_if_expr(
branches: &[&Element],
stateful: bool,
imports: &[ResolvedImport],
import_props: &[(&str, Option<&str>)],
prop_fields: Option<&HashMap<String, Vec<PropField>>>,
_import_has_slots: &[(&str, bool)],
style_cascade: Option<&fncc_styles::Stylesheet>,
style_theme: Option<&str>,
) -> String {
let mut expr = String::new();
for (i, el) in branches.iter().enumerate() {
if i == 0 {
expr.push_str("if ");
} else if el.name == "Else" {
expr.push_str(" else ");
} else {
expr.push_str(" else if ");
}
if el.name != "Else" {
if let Some(cond) = parser::get_condition_attr(el) {
let e = interpolation_expr(cond, stateful);
expr.push_str(&e);
expr.push(' ');
} else {
expr.push_str("compile_error!(\"<");
expr.push_str(&el.name);
expr.push_str("> element is missing `condition` attribute\") ");
}
}
expr.push_str("{ ");
match &el.children[..] {
[] => expr.push_str("div()"),
[single] => match single {
Node::Text(t) => expr.push_str(&format!("div().child({:?})", t)),
Node::Interpolation(interp) => {
let e = interpolation_expr(interp, stateful);
let formatted = format_interpolation(&e);
expr.push_str(&format!("div().child({formatted})"));
}
Node::Element(child_el) => {
let child_code = generate_element(
child_el,
0,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
);
expr.push_str(&format!("div().child({})", clean_inline(&child_code)));
}
},
children => {
expr.push_str("div()");
for child in children {
match child {
Node::Text(t) => expr.push_str(&format!(".child({:?})", t)),
Node::Interpolation(interp) => {
let e = interpolation_expr(interp, stateful);
let formatted = format_interpolation(&e);
expr.push_str(&format!(".child({formatted})"));
}
Node::Element(child_el) => {
let child_code = generate_element(
child_el,
0,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
);
expr.push_str(&format!(".child({})", clean_inline(&child_code)));
}
}
}
}
}
expr.push_str(" }");
}
if branches.last().map(|b| b.name.as_str()) != Some("Else") {
expr.push_str(" else { div() }");
}
expr
}
fn gen_for_expr(
el: &Element,
stateful: bool,
imports: &[ResolvedImport],
import_props: &[(&str, Option<&str>)],
prop_fields: Option<&HashMap<String, Vec<PropField>>>,
_import_has_slots: &[(&str, bool)],
style_cascade: Option<&fncc_styles::Stylesheet>,
style_theme: Option<&str>,
) -> String {
let each = match parser::get_each_attr(el) {
Some(e) => e,
None => {
return "compile_error!(\"<For> element is missing required `each` attribute\")".to_string();
}
};
let let_var = parser::get_let_attr(el).unwrap_or("item");
let index_var = parser::get_index_attr(el);
let iter_expr = interpolation_expr(each, stateful);
let mut body = String::new();
match &el.children[..] {
[] => body.push_str("div()"),
[single] => match single {
Node::Text(t) => body.push_str(&format!("div().child({:?})", t)),
Node::Interpolation(interp) => {
let e = interpolation_expr(interp, stateful);
let formatted = format_interpolation(&e);
body.push_str(&format!("div().child({formatted})"));
}
Node::Element(child_el) => {
let child_code = generate_element(
child_el,
1,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
);
body.push_str(&child_code);
}
},
children => {
body.push_str("div()");
for child in children {
match child {
Node::Text(t) => body.push_str(&format!(".child({:?})", t)),
Node::Interpolation(interp) => {
let e = interpolation_expr(interp, stateful);
let formatted = format_interpolation(&e);
body.push_str(&format!(".child({formatted})"));
}
Node::Element(child_el) => {
let child_code = generate_element(
child_el,
1,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
);
body.push_str(&format!(".child({})", clean_inline(&child_code)));
}
}
}
}
}
let var_names: Vec<&str> = index_var.iter().chain(std::iter::once(&let_var)).copied().collect();
for var in &var_names {
body = replace_self_prefix(&body, var);
}
if let Some(index) = index_var {
format!("{iter_expr}.iter().enumerate().map(|({index}, {let_var})| {{\n {body}\n}})")
} else {
format!("{iter_expr}.iter().map(|{let_var}| {{\n {body}\n}})")
}
}
fn generate_element(
el: &Element,
depth: usize,
stateful: bool,
imports: &[ResolvedImport],
import_props: &[(&str, Option<&str>)],
prop_fields: Option<&HashMap<String, Vec<PropField>>>,
_import_has_slots: &[(&str, bool)],
style_cascade: Option<&fncc_styles::Stylesheet>,
style_theme: Option<&str>,
) -> String {
let indent = " ".repeat(depth);
let code = match el.name.as_str() {
"Stack" => gen_stack(
el,
&indent,
depth,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
),
"Text" => gen_text(
el,
&indent,
depth,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
),
"Button" => gen_button(
el,
&indent,
depth,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
),
"Fragment" => gen_fragment(
el,
&indent,
depth,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
),
"For" => format!(
"div().children({})",
gen_for_expr(
el,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme
)
),
"If" | "ElseIf" | "Else" => {
let branches = [el];
gen_if_expr(
&branches,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
)
}
"RouterOutlet" | "Slot" => "children".to_string(),
_ => {
if let Some(render_fn) = imports
.iter()
.find(|(name, fn_name)| name == &el.name && !fn_name.is_empty())
.map(|(_, fn_name)| *fn_name)
{
gen_import_call(
el,
&indent,
render_fn,
import_props,
prop_fields,
_import_has_slots,
stateful,
imports,
depth,
style_cascade,
style_theme,
)
} else {
gen_fallback(
el,
&indent,
depth,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
)
}
}
};
match inject_element_styles(&code, el, style_cascade, style_theme) {
Ok(code) => code,
Err(e) => format!("compile_error!({e:?})"),
}
}
fn gen_import_call(
el: &Element,
indent: &str,
render_fn: &str,
import_props: &[(&str, Option<&str>)],
prop_fields: Option<&HashMap<String, Vec<PropField>>>,
_import_has_slots: &[(&str, bool)],
stateful: bool,
imports: &[ResolvedImport],
depth: usize,
style_cascade: Option<&fncc_styles::Stylesheet>,
style_theme: Option<&str>,
) -> String {
let props_type = import_props.iter().find(|(n, _)| n == &el.name).and_then(|(_, p)| *p);
let has_slot = _import_has_slots
.iter()
.find(|(n, _)| n == &el.name)
.map(|(_, h)| *h)
.unwrap_or(false);
let children_expr = if has_slot {
if el.children.is_empty() {
"div()".to_string()
} else {
let child_code = generate_children_code(
&el.children,
indent,
depth + 1,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
);
clean_inline(&format!("div(){child_code}"))
}
} else {
String::new()
};
if let Some(pt) = props_type {
let mut struct_fields = String::new();
if let Some(fields) = prop_fields.and_then(|m| m.get(pt)) {
for f in fields {
if let Some((_, attr_val)) = el.attrs.iter().find(|(n, _)| n == &f.name) {
let v = match attr_val {
AttrValue::String(s) => format!("{:?}.into()", s),
AttrValue::Interpolation(expr) => format!("{}.into()", interpolation_expr(expr, stateful)),
};
struct_fields.push_str(&format!("\n{indent} {}: {},", f.name, v));
} else if f.is_optional {
struct_fields.push_str(&format!("\n{indent} {}: None,", f.name));
}
}
} else {
for (attr_name, attr_val) in &el.attrs {
if attr_name == "class" || attr_name == "style" {
continue;
}
let v = match attr_val {
AttrValue::String(s) => format!("{:?}.into()", s),
AttrValue::Interpolation(expr) => format!("{}.into()", interpolation_expr(expr, stateful)),
};
struct_fields.push_str(&format!("\n{indent} {attr_name}: {v},"));
}
}
if has_slot {
format!("{indent}{render_fn}(&{pt} {{ {struct_fields}\n{indent} }}, {children_expr})")
} else {
format!("{indent}{render_fn}(&{pt} {{ {struct_fields}\n{indent} }})")
}
} else if has_slot {
format!("{indent}{render_fn}({children_expr})")
} else {
format!("{indent}{render_fn}()")
}
}
fn inject_element_styles(
code: &str,
el: &Element,
cascade: Option<&fncc_styles::Stylesheet>,
theme: Option<&str>,
) -> Result<String, String> {
let cascade = match cascade {
Some(c) => c,
None => return Ok(code.to_string()),
};
let classes: Vec<String> = el
.attrs
.iter()
.filter(|(name, _)| name == "class")
.filter_map(|(_, val)| match val {
AttrValue::String(s) => Some(s.split_whitespace().map(|s| s.to_string()).collect::<Vec<_>>()),
AttrValue::Interpolation(_) => None,
})
.flatten()
.collect();
let inline_style = el
.attrs
.iter()
.find(|(name, _)| name == "style")
.and_then(|(_, val)| match val {
AttrValue::String(s) => Some(s.clone()),
AttrValue::Interpolation(_) => None,
});
if classes.is_empty() && inline_style.is_none() {
return Ok(code.to_string());
}
let calls = fncc_styles::resolve(&classes, inline_style.as_deref(), cascade, theme)?;
if calls.is_empty() {
return Ok(code.to_string());
}
let indent = code
.lines()
.rev()
.find(|l| !l.trim().is_empty())
.map(|l| l.len() - l.trim_start().len())
.unwrap_or(4);
let indent_str = " ".repeat(indent);
let style_code: String = calls.iter().map(|c| format!("\n{indent_str}.{}", c.code)).collect();
let mut insert_pos = None;
for (i, line) in code.lines().enumerate() {
let trimmed = line.trim_start();
if trimmed.starts_with(".child(") || trimmed.starts_with(".children(") {
insert_pos = Some(
code.lines()
.take(i)
.map(|l| l.len() + 1)
.sum::<usize>()
.saturating_sub(1),
);
break;
}
}
if let Some(pos) = insert_pos {
Ok(format!("{}{}{}", &code[..pos], style_code, &code[pos..]))
} else {
Ok(format!("{}{}", code.trim_end(), style_code))
}
}
fn gen_fragment(
el: &Element,
indent: &str,
depth: usize,
stateful: bool,
imports: &[ResolvedImport],
import_props: &[(&str, Option<&str>)],
prop_fields: Option<&HashMap<String, Vec<PropField>>>,
_import_has_slots: &[(&str, bool)],
style_cascade: Option<&fncc_styles::Stylesheet>,
style_theme: Option<&str>,
) -> String {
let children_code = generate_children_code(
&el.children,
indent,
depth,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
);
format!("{indent}div()\n{children_code}").trim_end().to_string()
}
fn generate_children_code(
children: &[Node],
indent: &str,
depth: usize,
stateful: bool,
imports: &[ResolvedImport],
import_props: &[(&str, Option<&str>)],
prop_fields: Option<&HashMap<String, Vec<PropField>>>,
_import_has_slots: &[(&str, bool)],
style_cascade: Option<&fncc_styles::Stylesheet>,
style_theme: Option<&str>,
) -> String {
let mut out = String::new();
let mut i = 0;
while i < children.len() {
match &children[i] {
Node::Element(el) if el.name == "If" => {
let chain = collect_if_chain(children, i);
let expr = gen_if_expr(
&chain,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
);
out.push_str(&format!("{indent} .child(\n"));
out.push_str(&format!("{indent} {expr}\n"));
out.push_str(&format!("{indent} )\n"));
i += chain.len();
}
Node::Element(el) if el.name == "For" => {
let for_code = gen_for_expr(
el,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
);
out.push_str(&format!("{indent} .children(\n"));
out.push_str(&format!("{indent} {for_code}\n"));
out.push_str(&format!("{indent} )\n"));
i += 1;
}
Node::Element(el) if el.name == "Fragment" => {
let frag_code = generate_children_code(
&el.children,
indent,
depth,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
);
out.push_str(&frag_code);
i += 1;
}
_ => {
out.push_str(&format!("{indent} .child(\n"));
match &children[i] {
Node::Element(child_el) => {
out.push_str(&generate_element(
child_el,
depth + 2,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
));
}
Node::Text(t) => {
out.push_str(&format!("{indent} {:?}", t));
}
Node::Interpolation(expr) => {
let e = interpolation_expr(expr, stateful);
let formatted = format_interpolation(&e);
out.push_str(&format!("{indent} {formatted}"));
}
}
out.push('\n');
out.push_str(&format!("{indent} )\n"));
i += 1;
}
}
}
out
}
fn gen_stack(
el: &Element,
indent: &str,
depth: usize,
stateful: bool,
imports: &[ResolvedImport],
import_props: &[(&str, Option<&str>)],
prop_fields: Option<&HashMap<String, Vec<PropField>>>,
_import_has_slots: &[(&str, bool)],
style_cascade: Option<&fncc_styles::Stylesheet>,
style_theme: Option<&str>,
) -> String {
let mut out = format!("{indent}div()\n");
let mut is_vertical = false;
for (key, val) in &el.attrs {
match key.as_str() {
"direction" if val.as_str() == "vertical" => is_vertical = true,
"gap" => {
let v = val.as_str();
if let Ok(n) = v.parse::<f64>() {
out.push_str(&format!("{indent} .gap(px({}))\n", float_lit(n)));
}
}
_ => {}
}
}
if is_vertical {
out.push_str(&format!("{indent} .flex()\n{indent} .flex_col()\n"));
} else {
out.push_str(&format!("{indent} .flex()\n"));
}
out.push_str(&generate_children_code(
&el.children,
indent,
depth,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
));
out.trim_end().to_string()
}
fn gen_text(
el: &Element,
indent: &str,
depth: usize,
stateful: bool,
imports: &[ResolvedImport],
import_props: &[(&str, Option<&str>)],
prop_fields: Option<&HashMap<String, Vec<PropField>>>,
_import_has_slots: &[(&str, bool)],
style_cascade: Option<&fncc_styles::Stylesheet>,
style_theme: Option<&str>,
) -> String {
let mut out = format!("{indent}div()\n");
for (key, val) in &el.attrs {
if key.as_str() == "size" {
let v = val.as_str();
let ts = match v {
"xs" => "text_xs()",
"sm" => "text_sm()",
"base" => "text_base()",
"lg" => "text_lg()",
"xl" => "text_xl()",
"2xl" | "xxl" => "text_2xl()",
"3xl" => "text_3xl()",
_ => "text_base()",
};
out.push_str(&format!("{indent} .{ts}\n"));
}
}
match &el.children[..] {
[Node::Text(t)] => {
out.push_str(&format!("{indent} .child({:?})", t));
}
[Node::Interpolation(expr)] => {
let e = interpolation_expr(expr, stateful);
let formatted = format_interpolation(&e);
out.push_str(&format!("{indent} .child({formatted})"));
}
children => {
out.push_str(&generate_children_code(
children,
indent,
depth,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
));
}
}
out.trim_end().to_string()
}
fn gen_button(
el: &Element,
indent: &str,
depth: usize,
stateful: bool,
imports: &[ResolvedImport],
import_props: &[(&str, Option<&str>)],
prop_fields: Option<&HashMap<String, Vec<PropField>>>,
_import_has_slots: &[(&str, bool)],
style_cascade: Option<&fncc_styles::Stylesheet>,
style_theme: Option<&str>,
) -> String {
let mut out = format!("{indent}div()\n");
let btn_id = match &el.children[..] {
[Node::Text(t)] => t.clone(),
_ => format!("button_{depth}"),
};
out.push_str(&format!("{indent} .id({:?})\n", btn_id));
out.push_str(&format!("{indent} .cursor_pointer()\n"));
for (key, val) in &el.attrs {
if key.as_str() == "onclick" {
let handler = val.as_str();
let trampoline = format!("__fncc_cmd_{handler}");
if stateful {
out.push_str(&format!("{indent} .on_click({{\n"));
out.push_str(&format!("{indent} let handle = handle.clone();\n"));
out.push_str(&format!("{indent} move |_, _, cx| {{\n"));
out.push_str(&format!("{indent} handle.update(cx, |this, cx| {{\n"));
out.push_str(&format!("{indent} {trampoline}(this, cx);\n"));
out.push_str(&format!("{indent} }}).ok();\n"));
out.push_str(&format!("{indent} }}\n"));
out.push_str(&format!("{indent} }})\n"));
} else {
out.push_str(&format!("{indent} .on_click({trampoline})\n"));
}
}
}
match &el.children[..] {
[Node::Text(t)] => out.push_str(&format!("{indent} .child({:?})", t)),
[Node::Interpolation(expr)] => {
let e = interpolation_expr(expr, stateful);
let formatted = format_interpolation(&e);
out.push_str(&format!("{indent} .child({formatted})"));
}
children => {
out.push_str(&generate_children_code(
children,
indent,
depth,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
));
}
}
out.trim_end().to_string()
}
fn gen_fallback(
el: &Element,
indent: &str,
depth: usize,
stateful: bool,
imports: &[ResolvedImport],
import_props: &[(&str, Option<&str>)],
prop_fields: Option<&HashMap<String, Vec<PropField>>>,
_import_has_slots: &[(&str, bool)],
style_cascade: Option<&fncc_styles::Stylesheet>,
style_theme: Option<&str>,
) -> String {
let mut out = format!("{indent}div()\n");
for (key, val) in &el.attrs {
if key == "class" || key == "style" {
continue;
}
let v = val.as_str();
out.push_str(&format!("{indent} .attr({:?}, {:?})\n", key, v));
}
out.push_str(&generate_children_code(
&el.children,
indent,
depth,
stateful,
imports,
import_props,
prop_fields,
_import_has_slots,
style_cascade,
style_theme,
));
out.trim_end().to_string()
}
fn clean_inline(s: &str) -> String {
let collapsed: String = s.split_whitespace().collect::<Vec<_>>().join(" ");
collapsed.replace(") .", ").")
}
fn float_lit(n: f64) -> String {
if n.fract() == 0.0 {
format!("{n}.")
} else {
format!("{n}")
}
}
fn interpolation_expr(expr: &str, stateful: bool) -> String {
let trimmed = expr.trim();
if let Some(field) = trimmed.strip_prefix("state.") {
if stateful {
format!("self.{field}")
} else {
field.to_string()
}
} else if trimmed.starts_with("props.") {
trimmed.to_string()
} else if stateful {
format!("self.{trimmed}")
} else {
trimmed.to_string()
}
}
fn replace_self_prefix(s: &str, var: &str) -> String {
let pattern = format!("self.{var}");
let mut out = String::new();
let mut last = 0;
for (idx, _) in s.match_indices(&pattern) {
out.push_str(&s[last..idx]);
let end = idx + pattern.len();
let keep = if end < s.len() {
let b = s.as_bytes()[end];
b.is_ascii_alphanumeric() || b == b'_'
} else {
false
};
if keep {
out.push_str(&pattern);
last = idx + pattern.len();
} else {
out.push_str(var);
last = end;
}
}
out.push_str(&s[last..]);
out
}
pub(crate) fn to_snake_case(name: &str) -> String {
let mut result = String::new();
for (i, ch) in name.chars().enumerate() {
if ch.is_uppercase() {
if i > 0 {
result.push('_');
}
for c in ch.to_lowercase() {
result.push(c);
}
} else {
result.push(ch);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
fn cascade_none() -> Option<fncc_styles::Stylesheet> {
None
}
fn generate_from(source: &str) -> String {
let doc = parse(source).unwrap();
generate(&doc)
}
#[test]
fn test_generates_stateless_render_function() {
let out = generate_from("<MyComp></MyComp>");
assert!(out.contains("pub fn render_my_comp() -> impl IntoElement {"));
assert!(out.contains("div()"));
}
#[test]
fn test_generates_stateful_render_impl() {
let src = "---\n@state CounterState\n---\n<App></App>";
let out = generate_from(src);
assert!(out.contains("impl Render for CounterState {"));
assert!(
out.contains("fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {")
);
}
#[test]
fn test_frontmatter_is_preserved_in_output() {
let src = "---\nuse crate::prelude::*;\n---\n<App></App>";
let out = generate_from(src);
assert!(out.contains("use crate::prelude::*;"));
}
#[test]
fn test_stack_with_direction_vertical_generates_flex_col() {
let out = generate_from("<Stack direction=\"vertical\"></Stack>");
assert!(out.contains(".flex()"));
assert!(out.contains(".flex_col()"));
}
#[test]
fn test_stack_with_direction_horizontal_generates_flex_only() {
let out = generate_from("<Stack direction=\"horizontal\"></Stack>");
assert!(out.contains(".flex()"));
assert!(!out.contains(".flex_col()"));
}
#[test]
fn test_stack_with_gap_generates_px_value() {
let out = generate_from("<Stack gap=\"12\"></Stack>");
assert!(out.contains(".gap(px(12.))"));
}
#[test]
fn test_text_with_size_xl() {
let out = generate_from("<Text size=\"xl\">hello</Text>");
assert!(out.contains(".text_xl()"));
assert!(out.contains(".child(\"hello\")"));
}
#[test]
fn test_text_with_size_unknown_falls_back_to_base() {
let out = generate_from("<Text size=\"huge\">text</Text>");
assert!(out.contains(".text_base()"));
}
#[test]
fn test_button_with_text_child() {
let out = generate_from("<Button onclick=\"handle_click\">Click</Button>");
assert!(out.contains(".id(\"Click\")"));
assert!(out.contains(".cursor_pointer()"));
assert!(out.contains(".child(\"Click\")"));
}
#[test]
fn test_stateful_button_with_onclick_generates_entity_pattern() {
let src = "---\n@state AppState\n---\n<Button onclick=\"inc\">+1</Button>";
let out = generate_from(src);
assert!(out.contains("let handle = handle.clone();"));
assert!(out.contains("handle.update(cx, |this, cx| {"));
assert!(out.contains("__fncc_cmd_inc(this, cx);"));
}
#[test]
fn test_stateless_button_with_onclick_generates_direct_call() {
let out = generate_from("<Button onclick=\"log_click\">Go</Button>");
assert!(out.contains(".on_click(__fncc_cmd_log_click)"));
}
#[test]
fn test_empty_element_children() {
let out = generate_from("<Div></Div>");
assert!(out.contains("div()"));
}
#[test]
fn test_unknown_element_falls_back_to_div_with_attrs() {
let out = generate_from("<CustomEl foo=\"bar\">content</CustomEl>");
assert!(out.contains(".attr(\"foo\", \"bar\")"));
assert!(out.contains(".child("));
assert!(out.contains("\"content\""));
}
#[test]
fn test_interpolation_in_text_content_generates_format() {
let out = generate_from("---\n@state S\n---\n<Text>{state.msg}</Text>");
assert!(out.contains("self.msg.to_string()"));
assert!(!out.contains("self.state.msg"));
}
#[test]
fn test_interpolation_strips_state_prefix() {
let out = generate_from("<Text>{state.count}</Text>");
assert!(out.contains("count.to_string()"));
}
#[test]
fn test_multiple_commands_collected_in_validation_fn() {
let src = "<Stack><Button onclick=\"a\">A</Button><Button onclick=\"b\">B</Button></Stack>";
let out = generate_from(src);
assert!(out.contains("fn _fncc_validate_"));
assert!(out.contains("__fncc_cmd_a"));
assert!(out.contains("__fncc_cmd_b"));
}
#[test]
fn test_generated_code_contains_no_markdown_or_template_leftovers() {
let out = generate_from("<Text>hello</Text>");
assert!(!out.contains("{{"));
assert!(!out.contains("{state."));
assert!(!out.contains("__fncc_cmd_") || out.contains("__fncc_cmd_"));
}
#[test]
fn test_generated_function_name_follows_snake_case() {
let out = generate_from("<HTMLParser></HTMLParser>");
assert!(out.contains("render_h_t_m_l_parser") || out.contains("render_html_parser"));
}
#[test]
fn test_regression_gap_with_decimal_does_not_produce_invalid_syntax() {
let out = generate_from("<Stack gap=\"12.5\"></Stack>");
assert!(out.contains(".gap(px(12.5))"), "got: {out}");
}
#[test]
fn test_regression_duplicate_button_ids_at_same_depth() {
let src = "<Stack><Button>OK</Button><Button>OK</Button></Stack>";
let out = generate_from(src);
let id_count = out.matches(".id(\"OK\")").count();
assert!(
id_count <= 2,
"expected at most 2 .id(\"OK\") occurrences, got {id_count}"
);
}
#[test]
fn test_regression_multiple_calls_have_unique_validation_fn_names() {
let doc = parse("<Button onclick=\"x\">X</Button>").unwrap();
let a = generate_with_id(&doc, 0);
let b = generate_with_id(&doc, 1);
assert_ne!(a, b);
assert!(a.contains("__fncc_cmd_x"));
assert!(b.contains("__fncc_cmd_x"));
}
#[test]
fn test_regression_empty_gap_does_not_panic() {
let out = generate_from("<Stack gap=\"\"></Stack>");
assert!(!out.contains(".gap(") || out.contains(".gap(px(0.))"));
}
#[test]
fn test_regression_interpolation_without_state_prefix() {
let out = generate_from("---\n@state S\n---\n<Text>{custom_expr}</Text>");
assert!(out.contains("custom_expr") || out.contains("self.custom_expr"));
}
#[test]
fn test_regression_many_calls_do_not_panic() {
for _ in 0..100 {
generate_from("<Button onclick=\"f\">F</Button>");
}
}
#[test]
fn test_imported_element_generates_render_call() {
let doc = parse("<Stack><Header /></Stack>").unwrap();
let imports: &[(&str, &str)] = &[("Header", "render_header")];
let out = generate_with_imports(
&doc,
0,
imports,
None,
None,
None,
&[],
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains("render_header()"));
}
#[test]
fn test_imported_element_in_stateful_component() {
let src = "---\n@state AppState\n---\n<Stack><Footer /></Stack>";
let doc = parse(src).unwrap();
let imports: &[(&str, &str)] = &[("Footer", "render_footer")];
let out = generate_with_imports(
&doc,
0,
imports,
None,
None,
None,
&[],
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains("render_footer()"));
}
#[test]
fn test_gpui_import_falls_back_to_div() {
let doc = parse("<Stack><TextInput /></Stack>").unwrap();
let imports: &[(&str, &str)] = &[("TextInput", "")];
let out = generate_with_imports(
&doc,
0,
imports,
None,
None,
None,
&[],
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains("div()"));
}
#[test]
fn test_builtin_takes_precedence_over_import() {
let doc = parse("<Text>hello</Text>").unwrap();
let imports: &[(&str, &str)] = &[("Text", "render_text")];
let out = generate_with_imports(
&doc,
0,
imports,
None,
None,
None,
&[],
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains(".child(\"hello\")"));
}
#[test]
fn test_imported_element_with_custom_component_name() {
let doc = parse("<Stack><MyHeader /></Stack>").unwrap();
let imports: &[(&str, &str)] = &[("MyHeader", "render_header")];
let out = generate_with_imports(
&doc,
0,
imports,
None,
None,
None,
&[],
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains("render_header()"));
}
#[test]
fn test_render_fn_name_uses_component_name_arg() {
let doc = parse("<Text>hello</Text>").unwrap();
let out = generate_with_imports(
&doc,
0,
&[],
Some("CustomWidget"),
None,
None,
&[],
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains("pub fn render_custom_widget()"));
assert!(!out.contains("pub fn render_text()"));
}
#[test]
fn test_props_stateless_component_with_props_signature() {
let doc = parse("<Text>{props.title}</Text>").unwrap();
let out = generate_with_imports(
&doc,
0,
&[],
Some("Header"),
None,
Some("HeaderProps"),
&[],
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains("pub fn render_header(props: &HeaderProps) -> impl IntoElement {"));
assert!(out.contains("props.title"));
}
#[test]
fn test_props_caller_generates_struct_construction() {
let doc = parse("<Header title=\"Welcome\" />").unwrap();
let imports: &[(&str, &str)] = &[("Header", "render_header")];
let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
let out = generate_with_imports(
&doc,
0,
imports,
None,
None,
None,
import_props,
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains("render_header(&HeaderProps {"));
assert!(out.contains("title: \"Welcome\".into(),"));
assert!(out.contains("})"));
}
#[test]
fn test_props_caller_multiple_attributes() {
let doc = parse("<Header title=\"Hi\" subtitle=\"World\" />").unwrap();
let imports: &[(&str, &str)] = &[("Header", "render_header")];
let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
let out = generate_with_imports(
&doc,
0,
imports,
None,
None,
None,
import_props,
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains("title: \"Hi\".into(),"));
assert!(out.contains("subtitle: \"World\".into(),"));
}
#[test]
fn test_props_caller_option_field() {
let doc = parse("<Header title=\"Hi\" />").unwrap();
let imports: &[(&str, &str)] = &[("Header", "render_header")];
let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
let out = generate_with_imports(
&doc,
0,
imports,
None,
None,
None,
import_props,
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains("title: \"Hi\".into(),"));
}
#[test]
fn test_props_nested_element_with_props() {
let doc = parse("<Stack><Header title=\"Nested\" /><Text>ok</Text></Stack>").unwrap();
let imports: &[(&str, &str)] = &[("Header", "render_header")];
let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
let out = generate_with_imports(
&doc,
0,
imports,
None,
None,
None,
import_props,
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains("render_header(&HeaderProps {"));
assert!(out.contains("title: \"Nested\".into(),"));
}
#[test]
fn test_props_mixed_components_with_and_without_props() {
let doc = parse("<Stack><Header title=\"A\" /><Footer /></Stack>").unwrap();
let imports: &[(&str, &str)] = &[("Header", "render_header"), ("Footer", "render_footer")];
let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps")), ("Footer", None)];
let out = generate_with_imports(
&doc,
0,
imports,
None,
None,
None,
import_props,
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains("render_header(&HeaderProps {"));
assert!(out.contains("title: \"A\".into(),"));
assert!(out.contains("render_footer()"));
}
#[test]
fn test_props_element_without_props_still_calls_directly() {
let doc = parse("<Footer />").unwrap();
let imports: &[(&str, &str)] = &[("Footer", "render_footer")];
let import_props: &[(&str, Option<&str>)] = &[("Footer", None)];
let out = generate_with_imports(
&doc,
0,
imports,
None,
None,
None,
import_props,
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains("render_footer()"));
assert!(!out.contains("&"));
}
#[test]
fn test_props_self_closing_with_props() {
let doc = parse("<Header title=\"SelfClose\" subtitle=\"X\" />").unwrap();
let imports: &[(&str, &str)] = &[("Header", "render_header")];
let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
let out = generate_with_imports(
&doc,
0,
imports,
None,
None,
None,
import_props,
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains("title: \"SelfClose\".into(),"));
assert!(out.contains("subtitle: \"X\".into(),"));
}
#[test]
fn test_if_generates_if_expression() {
let out = generate_from("<If condition=\"{state.show}\"><Text>Hi</Text></If>");
assert!(out.contains("if show {"));
assert!(out.contains("div().child(div().child(\"Hi\"))"));
assert!(out.contains("} else {"), "else branch should be present");
assert!(out.contains("div() }"));
}
#[test]
fn test_if_else_chain() {
let src = "<Stack><If condition=\"{state.a}\"><Text>A</Text></If><Else><Text>B</Text></Else></Stack>";
let out = generate_from(src);
assert!(out.contains("if a {"));
assert!(out.contains("} else {"));
assert!(out.contains("div().child(\"B\")"));
}
#[test]
fn test_if_elseif_else_chain() {
let src = "<Stack><If condition=\"{state.x}\"><Text>X</Text></If><ElseIf condition=\"{state.y}\"><Text>Y</Text></ElseIf><Else><Text>Z</Text></Else></Stack>";
let out = generate_from(src);
assert!(out.contains("if x {"));
assert!(out.contains("else if y {"));
assert!(out.contains("else {"));
}
#[test]
fn test_for_generates_iteration() {
let out = generate_from("<For each=\"{state.items}\" let=\"item\"><Text>{item.name}</Text></For>");
assert!(out.contains("items.iter().map(|item| {"));
assert!(out.contains("item.name"));
}
#[test]
fn test_for_with_index() {
let out = generate_from("<For each=\"{state.items}\" let=\"item\" index=\"i\"><Text>{item.name}</Text></For>");
assert!(out.contains(".enumerate()"));
assert!(out.contains("|(i, item)|"));
}
#[test]
fn test_fragment_generates_div() {
let out = generate_from("<Fragment><Text>A</Text><Text>B</Text></Fragment>");
assert!(out.contains("div()"));
}
#[test]
fn test_slot_in_stateless_component() {
let doc = parse("<Stack><Slot /></Stack>").unwrap();
let out = generate_with_imports(
&doc,
0,
&[],
Some("Card"),
None,
None,
&[],
None,
&[],
cascade_none().as_ref(),
None,
);
assert!(out.contains("children: impl IntoElement"));
assert!(out.contains("children"));
}
#[test]
fn test_if_in_stack() {
let src = "<Stack><If condition=\"{state.flag}\"><Text>Yes</Text></If><Text>Always</Text></Stack>";
let out = generate_from(src);
assert!(out.contains("if flag {"));
assert!(out.contains("\"Always\""));
}
#[test]
fn test_for_in_stack() {
let src = "<Stack><For each=\"{state.items}\" let=\"item\"><Text>{item}</Text></For></Stack>";
let out = generate_from(src);
assert!(out.contains("items.iter().map(|item| {"));
assert!(out.contains(".children("));
}
#[test]
fn test_fragment_nested_in_stack() {
let src = "<Stack><Fragment><Text>A</Text><Text>B</Text></Fragment></Stack>";
let out = generate_from(src);
assert!(out.contains("div()"));
assert!(out.contains(".child(\"A\")"));
assert!(out.contains(".child(\"B\")"));
}
#[test]
fn test_style_class_injects_method_calls() {
let doc = parse("<Text class=\"heading\">Title</Text>").unwrap();
let mut ss = fncc_styles::Stylesheet::default();
ss.rules.insert(
"heading".into(),
vec![("color".into(), "red".into()), ("font-size".into(), "24px".into())],
);
let out = generate_with_imports(&doc, 0, &[], None, None, None, &[], None, &[], Some(&ss), None);
assert!(
out.contains(".text_color(rgba(0xff0000ff))"),
"missing text_color: {out}"
);
assert!(out.contains(".text_2xl()"), "missing text_2xl: {out}");
}
#[test]
fn test_style_inline_injects_method_calls() {
let doc = parse("<Text style=\"color: blue; padding: 8px\">Hi</Text>").unwrap();
let ss = fncc_styles::Stylesheet::default();
let out = generate_with_imports(&doc, 0, &[], None, None, None, &[], None, &[], Some(&ss), None);
assert!(out.contains(".text_color(rgba(0x0000ffff))"));
assert!(out.contains(".p(px(8.))"));
}
#[test]
fn test_style_class_unknown_skipped() {
let doc = parse("<Text class=\"nonexistent\">Hi</Text>").unwrap();
let ss = fncc_styles::Stylesheet::default();
let out = generate_with_imports(&doc, 0, &[], None, None, None, &[], None, &[], Some(&ss), None);
assert!(out.contains(".child(\"Hi\")"));
}
#[test]
fn test_style_class_and_inline_merged() {
let doc = parse("<Button class=\"big\" style=\"color: white\">Go</Button>").unwrap();
let mut ss = fncc_styles::Stylesheet::default();
ss.rules.insert(
"big".into(),
vec![("padding".into(), "16px".into()), ("font-size".into(), "20px".into())],
);
let out = generate_with_imports(&doc, 0, &[], None, None, None, &[], None, &[], Some(&ss), None);
assert!(out.contains(".p(px(16.))"));
assert!(out.contains(".text_xl()"));
assert!(out.contains(".text_color(rgba(0xffffffff))"));
}
}