use std::collections::HashMap;
use crepuscularity_core::ast::*;
use crepuscularity_core::context::{value_to_str, TemplateContext, TemplateValue};
use crepuscularity_core::eval::eval_expr;
use crepuscularity_core::parser::{parse_component_file, parse_template};
use crepuscularity_core::preprocess::slot_rotate_child_phrases;
use crate::include_expand;
use crate::ir::{StackAxis, ViewIr, ViewNode, IR_VERSION};
use crate::style;
pub fn render_from_files(
files: &HashMap<String, String>,
entry: &str,
ctx: &TemplateContext,
) -> Result<ViewIr, String> {
let mut ctx = ctx.clone();
ctx.virtual_files = files.clone();
if let Some((file_part, comp_name)) = entry.split_once('#') {
let content = files
.get(file_part)
.ok_or_else(|| format!("file not found in virtual fs: {file_part}"))?;
return render_component_file_to_ir(content, comp_name, &ctx);
}
let content = files
.get(entry)
.ok_or_else(|| format!("file not found in virtual fs: {entry}"))?;
render_template_to_ir(content, &ctx)
}
pub fn render_template_to_ir(template: &str, ctx: &TemplateContext) -> Result<ViewIr, String> {
let nodes = parse_template(template)?;
render_nodes_to_ir(&nodes, ctx)
}
pub fn render_component_file_to_ir(
content: &str,
component_name: &str,
ctx: &TemplateContext,
) -> Result<ViewIr, String> {
let file = parse_component_file(content)?;
let component = file
.components
.get(component_name)
.ok_or_else(|| format!("component not found: {component_name}"))?;
let mut child_ctx = ctx.clone();
for (key, expr) in &component.meta.defaults {
child_ctx
.vars
.entry(key.clone())
.or_insert_with(|| eval_expr(expr, &TemplateContext::new()));
}
render_nodes_to_ir(&component.nodes, &child_ctx)
}
pub fn render_nodes_to_ir(nodes: &[Node], ctx: &TemplateContext) -> Result<ViewIr, String> {
let root = render_nodes_list(nodes, ctx)?;
Ok(ViewIr {
version: IR_VERSION,
root,
})
}
pub(crate) fn render_nodes_list(
nodes: &[Node],
ctx: &TemplateContext,
) -> Result<Vec<ViewNode>, String> {
let mut ctx = ctx.clone();
let mut out = Vec::new();
for node in nodes {
if let Node::LetDecl(decl) = node {
if decl.is_default && ctx.vars.contains_key(&decl.name) {
continue;
}
let val = eval_expr(&decl.expr, &ctx);
ctx.vars.insert(decl.name.clone(), val);
continue;
}
if let Node::Include(inc) = node {
let (inner_nodes, inner_ctx) = include_expand::expand_include(inc, &ctx)?;
out.extend(render_nodes_list(&inner_nodes, &inner_ctx)?);
continue;
}
out.push(render_node(node, &ctx)?);
}
Ok(out)
}
fn render_node(node: &Node, ctx: &TemplateContext) -> Result<ViewNode, String> {
match node {
Node::Element(el) => render_element(el, ctx),
Node::Text(parts) => Ok(ViewNode::Text {
content: render_text_inline(parts, ctx)?,
style: None,
}),
Node::If(block) => render_if(block, ctx),
Node::For(block) => render_for(block, ctx),
Node::Match(block) => render_match(block, ctx),
Node::LetDecl(_) => Ok(stack_column_raw(vec![])),
Node::Include(_) => {
Err("internal error: include should be expanded in render_nodes_list".into())
}
Node::RawText(expr) => Ok(ViewNode::Text {
content: value_to_str(&eval_expr(expr, ctx)),
style: None,
}),
}
}
fn stack_column_raw(children: Vec<ViewNode>) -> ViewNode {
ViewNode::Stack {
axis: StackAxis::Column,
spacing: None,
align_items: None,
justify_content: None,
style: None,
children,
}
}
fn render_element(el: &Element, ctx: &TemplateContext) -> Result<ViewNode, String> {
if el.tag == "slot" {
return if let Some((slot_nodes, slot_ctx)) = &ctx.slot {
let children = render_nodes_list(slot_nodes, slot_ctx)?;
Ok(stack_column_raw(children))
} else {
let children = render_nodes_list(&el.children, ctx)?;
Ok(stack_column_raw(children))
};
}
if el.tag == "slot-rotate" {
let phrases = slot_rotate_child_phrases(&el.children)?;
let mut interval_ms = 3200u64;
for b in &el.bindings {
if b.prop == "interval" {
let v = value_to_str(&eval_expr(&b.value, ctx));
let v = v.trim_matches('"').trim();
interval_ms = v.parse().unwrap_or(3200);
}
}
let classes = active_classes(el, ctx);
let style = style::extract_stack_hints(&classes, Some(ctx)).style;
return Ok(ViewNode::SlotRotate {
phrases,
interval_ms,
style: style.opt(),
});
}
let classes = active_classes(el, ctx);
if el.tag == "button" {
let label = collect_primary_text(&el.children, ctx)?;
let on_click = el
.event_handlers
.iter()
.find(|e| e.event == "click")
.map(|e| e.handler.clone());
let hints = style::extract_stack_hints(&classes, Some(ctx));
return Ok(ViewNode::Button {
label,
on_click,
style: hints.style.opt(),
});
}
if el.tag == "img" {
let src = el
.bindings
.iter()
.find(|b| b.prop == "src")
.map(|b| value_to_str(&eval_expr(&b.value, ctx)))
.unwrap_or_default();
let alt = el
.bindings
.iter()
.find(|b| b.prop == "alt")
.map(|b| value_to_str(&eval_expr(&b.value, ctx)));
let hints = style::extract_stack_hints(&classes, Some(ctx));
return Ok(ViewNode::Image {
src,
alt,
style: hints.style.opt(),
});
}
if el.tag == "span" && el.children.len() == 1 {
if let Node::Text(parts) = &el.children[0] {
let txt = render_text_inline(parts, ctx)?;
let st = style::extract_text_style(&classes, Some(ctx)).opt();
return Ok(ViewNode::Text {
content: txt,
style: st,
});
}
}
let axis = stack_axis(&classes);
let spacing = parse_gap_spacing(&classes);
let scroll = style::is_scroll_container(&classes);
let hints = style::extract_stack_hints(&classes, Some(ctx));
let children = render_nodes_list(&el.children, ctx)?;
if scroll {
return Ok(ViewNode::Scroll {
axis,
style: hints.style.opt(),
children,
});
}
Ok(ViewNode::Stack {
axis,
spacing,
align_items: hints.align_items,
justify_content: hints.justify_content,
style: hints.style.opt(),
children,
})
}
fn collect_primary_text(children: &[Node], ctx: &TemplateContext) -> Result<String, String> {
for c in children {
match c {
Node::Text(parts) => return render_text_inline(parts, ctx),
Node::Element(inner) => {
let s = collect_primary_text(&inner.children, ctx)?;
if !s.is_empty() {
return Ok(s);
}
}
_ => {}
}
}
Ok(String::new())
}
fn active_classes(el: &Element, ctx: &TemplateContext) -> Vec<String> {
let mut expanded = Vec::new();
for c in el.classes.iter() {
expanded.push(ctx.interpolate(c));
}
for cc in &el.conditional_classes {
if ctx.eval_condition(&cc.condition) {
expanded.push(ctx.interpolate(&cc.class));
}
}
expanded
}
fn stack_axis(classes: &[String]) -> StackAxis {
let set: std::collections::HashSet<&str> = classes.iter().map(|s| s.as_str()).collect();
if set.contains("flex-col") {
StackAxis::Column
} else if set.contains("flex-row") || set.contains("flex") {
StackAxis::Row
} else {
StackAxis::Column
}
}
fn parse_gap_spacing(classes: &[String]) -> Option<f32> {
for c in classes {
if let Some(rest) = c.strip_prefix("gap-") {
if let Ok(n) = rest.parse::<u32>() {
return Some((n as f32) * 4.0);
}
}
}
None
}
fn render_text_inline(parts: &[TextPart], ctx: &TemplateContext) -> Result<String, String> {
let mut result = String::new();
for part in parts {
match part {
TextPart::Literal(text) => result.push_str(text),
TextPart::Expr(expr) => result.push_str(&value_to_str(&eval_expr(expr, ctx))),
}
}
Ok(result)
}
fn render_if(block: &IfBlock, ctx: &TemplateContext) -> Result<ViewNode, String> {
let body = if ctx.eval_condition(&block.condition) {
&block.then_children
} else if let Some(else_children) = &block.else_children {
else_children
} else {
return Ok(stack_column_raw(vec![]));
};
let children = render_nodes_list(body, ctx)?;
Ok(stack_column_raw(children))
}
fn render_for(block: &ForBlock, ctx: &TemplateContext) -> Result<ViewNode, String> {
let items = ctx.get_list(&block.iterator);
let mut children = Vec::new();
for item_ctx in items {
let mut child_ctx = ctx.clone();
for (k, v) in &item_ctx.vars {
child_ctx.vars.insert(k.clone(), v.clone());
}
let pattern = block.pattern.trim();
if !pattern.is_empty() {
let item_str = item_ctx.get_str("value");
if !item_str.is_empty() {
child_ctx
.vars
.insert(pattern.to_string(), TemplateValue::Str(item_str));
}
}
children.push(render_nodes_list(&block.body, &child_ctx)?);
}
let flattened: Vec<ViewNode> = children
.into_iter()
.flat_map(|v| {
if v.len() == 1 {
v
} else if v.is_empty() {
vec![]
} else {
vec![stack_column_raw(v)]
}
})
.collect();
Ok(stack_column_raw(flattened))
}
fn render_match(block: &MatchBlock, ctx: &TemplateContext) -> Result<ViewNode, String> {
let val = eval_expr(&block.expr, ctx);
let value = value_to_str(&val);
for arm in &block.arms {
let pattern = arm.pattern.trim();
if pattern == "_" {
let children = render_nodes_list(&arm.body, ctx)?;
return Ok(stack_column_raw(children));
}
if pattern.starts_with('"') && pattern.ends_with('"') {
let lit = &pattern[1..pattern.len() - 1];
if value == lit {
let children = render_nodes_list(&arm.body, ctx)?;
return Ok(stack_column_raw(children));
}
}
if value == pattern {
let children = render_nodes_list(&arm.body, ctx)?;
return Ok(stack_column_raw(children));
}
}
Ok(stack_column_raw(vec![]))
}