use std::path::{Path, PathBuf};
use crepuscularity_core::ast::{IncludeNode, Node};
use crepuscularity_core::context::TemplateContext;
use crepuscularity_core::eval::eval_expr;
use crepuscularity_core::parser::{parse_component_file, parse_template};
pub(crate) fn read_file(ctx: &TemplateContext, path: &Path) -> Result<String, String> {
let key = path.to_string_lossy();
if let Some(content) = ctx.virtual_files.get(key.as_ref()) {
return Ok(content.clone());
}
for (vkey, content) in &ctx.virtual_files {
if vkey.ends_with(key.as_ref()) || key.ends_with(vkey.as_str()) {
return Ok(content.clone());
}
}
if cfg!(not(target_arch = "wasm32")) {
std::fs::read_to_string(path).map_err(|e| format!("include error: {:?}: {}", path, e))
} else {
Err(format!(
"include error: file not in virtual bundle: {}",
key
))
}
}
pub(crate) fn resolve_include_path(base_dir: Option<&Path>, path: &str) -> Result<PathBuf, String> {
let requested = Path::new(path);
if requested.is_absolute()
|| requested
.components()
.any(|component| matches!(component, std::path::Component::ParentDir))
{
return Err(format!("include path outside base dir: {path}"));
}
let candidate = if let Some(base) = base_dir {
base.join(requested)
} else {
requested.to_path_buf()
};
if cfg!(not(target_arch = "wasm32")) {
let resolved = std::fs::canonicalize(&candidate).unwrap_or(candidate);
if let Some(base) = base_dir {
if let Ok(base) = std::fs::canonicalize(base) {
if !resolved.starts_with(&base) {
return Err(format!("include path outside base dir: {path}"));
}
}
}
Ok(resolved)
} else {
Ok(candidate)
}
}
pub(crate) fn expand_include(
inc: &IncludeNode,
ctx: &TemplateContext,
) -> Result<(Vec<Node>, TemplateContext), String> {
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| 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), String> {
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| format!("component file parse error: {e}"))?;
let comp = comp_file
.components
.get(comp_name)
.ok_or_else(|| 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 {
child_ctx
.vars
.entry(key.clone())
.or_insert_with(|| 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))
}