crepuscularity-native 0.5.4

View intermediate representation (JSON) for native SwiftUI / Compose shells (UNSTABLE).
Documentation
//! Expand `include` directives into parsed child nodes + context (no cycles with IR lowering).

use std::path::Path;

use crepuscularity_core::ast::{IncludeNode, Node};
use crepuscularity_core::context::TemplateContext;
use crepuscularity_core::eval::eval_expr;
use crepuscularity_core::include_paths::resolve_include_path;
use crepuscularity_core::parser::{parse_component_file, parse_template};
use crepuscularity_core::virtual_files::lookup_virtual_file;
use crepuscularity_core::CrepusError;

pub(crate) fn read_file(ctx: &TemplateContext, path: &Path) -> Result<String, CrepusError> {
    if let Some(content) = lookup_virtual_file(ctx, path) {
        return Ok(content);
    }
    if cfg!(not(target_arch = "wasm32")) {
        std::fs::read_to_string(path)
            .map_err(|e| CrepusError::render(format!("include error: {:?}: {}", path, e)))
    } else {
        Err(CrepusError::render(format!(
            "include error: file not in virtual bundle: {}",
            path.to_string_lossy()
        )))
    }
}

/// Parsed nodes to render and the child context to use (base_dir, virtual_files, vars, slot).
pub(crate) fn expand_include(
    inc: &IncludeNode,
    ctx: &TemplateContext,
) -> Result<(Vec<Node>, TemplateContext), CrepusError> {
    if let Some((file_part, comp_name)) = inc.path.split_once('#') {
        return expand_named_component(inc, ctx, file_part, comp_name);
    }

    let file_path = resolve_include_path(ctx.base_dir.as_deref(), &inc.path)?;
    let content = read_file(ctx, &file_path)?;
    let nodes = parse_template(&content)
        .map_err(|e| CrepusError::render(format!("include parse error: {e}")))?;

    let mut child_ctx = TemplateContext::new();
    child_ctx.base_dir = file_path.parent().map(|p| p.to_path_buf());
    child_ctx.virtual_files = ctx.virtual_files.clone();
    for (key, expr) in &inc.props {
        child_ctx.vars.insert(key.clone(), eval_expr(expr, ctx)?);
    }
    if !inc.slot.is_empty() {
        child_ctx.slot = Some((inc.slot.clone(), Box::new(ctx.clone())));
    }

    Ok((nodes, child_ctx))
}

fn expand_named_component(
    inc: &IncludeNode,
    ctx: &TemplateContext,
    file_part: &str,
    comp_name: &str,
) -> Result<(Vec<Node>, TemplateContext), CrepusError> {
    let file_path = resolve_include_path(ctx.base_dir.as_deref(), file_part)?;
    let content = read_file(ctx, &file_path)?;
    let comp_file = parse_component_file(&content)
        .map_err(|e| CrepusError::render(format!("component file parse error: {e}")))?;
    let comp = comp_file.components.get(comp_name).ok_or_else(|| {
        CrepusError::render(format!("component '{comp_name}' not found in {file_part}"))
    })?;

    let mut child_ctx = TemplateContext::new();
    child_ctx.base_dir = file_path.parent().map(|p| p.to_path_buf());
    child_ctx.virtual_files = ctx.virtual_files.clone();
    for (key, expr) in &comp.meta.defaults {
        if !child_ctx.vars.contains_key(key) {
            child_ctx
                .vars
                .insert(key.clone(), eval_expr(expr, &TemplateContext::new())?);
        }
    }
    for (key, expr) in &inc.props {
        child_ctx.vars.insert(key.clone(), eval_expr(expr, ctx)?);
    }
    if !inc.slot.is_empty() {
        child_ctx.slot = Some((inc.slot.clone(), Box::new(ctx.clone())));
    }

    Ok((comp.nodes.clone(), child_ctx))
}