use super::ast::Node;
use super::error::{TemplateError, TemplateErrorPhase, TemplateResult};
use std::collections::BTreeMap;
const PREAMBLE_DIRECTIVES: &[&str] = &["data", "sort", "filter", "slice"];
pub type Fetch<'a> = dyn Fn(&str) -> TemplateResult<Vec<Node>> + 'a;
pub fn with_default_layout(nodes: &[Node], layout: Option<&str>, imports: &[String]) -> Vec<Node> {
let Some(layout) = layout else {
return nodes.to_vec();
};
let declares_load = nodes
.iter()
.any(|n| matches!(n, Node::VoidElement { name, .. } if name == "load"));
if declares_load {
return nodes.to_vec();
}
let mut wrapped: Vec<Node> = imports.iter().map(|f| load_node(f)).collect();
wrapped.push(load_node(layout));
let names_a_block = nodes
.iter()
.any(|n| matches!(n, Node::Element { name, .. } if name == "block"));
if names_a_block {
wrapped.extend(nodes.iter().cloned());
} else {
let mut body = Vec::new();
for node in nodes {
match node {
Node::VoidElement { name, .. } if PREAMBLE_DIRECTIVES.contains(&name.as_str()) => {
wrapped.push(node.clone())
}
_ => body.push(node.clone()),
}
}
wrapped.push(content_block(body));
}
wrapped
}
fn content_block(children: Vec<Node>) -> Node {
let mut attrs = BTreeMap::new();
attrs.insert("slot".to_string(), "content".to_string());
Node::Element {
name: "block".to_string(),
attrs,
children,
}
}
fn load_node(file: &str) -> Node {
let mut attrs = BTreeMap::new();
attrs.insert("file".to_string(), file.to_string());
Node::VoidElement {
name: "load".to_string(),
attrs,
}
}
pub fn inject_blocks(nodes: Vec<Node>, blocks: &BTreeMap<String, Vec<Node>>) -> Vec<Node> {
let mut result = Vec::new();
for node in nodes {
match node {
Node::Element { ref name, .. } if name == "component" => result.push(node),
Node::Element {
name,
attrs,
children,
} if name == "slot" => {
if let Some(filled) = attrs.get("id").and_then(|id| blocks.get(id)) {
result.extend(filled.clone());
} else {
result.push(Node::Element {
name,
attrs,
children: inject_blocks(children, blocks),
});
}
}
Node::VoidElement { name, attrs } if name == "slot" => {
if let Some(filled) = attrs.get("id").and_then(|id| blocks.get(id)) {
result.extend(filled.clone());
} else {
result.push(Node::VoidElement { name, attrs });
}
}
Node::Element {
name,
attrs,
children,
} => result.push(Node::Element {
name,
attrs,
children: inject_blocks(children, blocks),
}),
other => result.push(other),
}
}
result
}
pub fn collect_components(nodes: &[Node], out: &mut Vec<Node>) {
for node in nodes {
if let Node::Element { name, children, .. } = node {
if name == "component" {
out.push(node.clone());
} else {
collect_components(children, out);
}
}
}
}
pub fn unresolved_uses(nodes: &[Node]) -> Vec<String> {
fn walk(
nodes: &[Node],
defined: &mut std::collections::BTreeSet<String>,
used: &mut Vec<String>,
) {
for node in nodes {
match node {
Node::Element {
name,
attrs,
children,
} => {
if name == "component" {
if let Some(id) = attrs.get("id") {
defined.insert(id.clone());
}
} else if name == "use" {
if let Some(id) = attrs.get("id") {
used.push(id.clone());
}
}
walk(children, defined, used);
}
Node::VoidElement { name, attrs } if name == "use" => {
if let Some(id) = attrs.get("id") {
used.push(id.clone());
}
}
_ => {}
}
}
}
let mut defined = std::collections::BTreeSet::new();
let mut used = Vec::new();
walk(nodes, &mut defined, &mut used);
let mut missing: Vec<String> = used
.into_iter()
.filter(|id| !defined.contains(id))
.collect();
missing.sort();
missing.dedup();
missing
}
pub fn extract_blocks(nodes: &[Node]) -> BTreeMap<String, Vec<Node>> {
let mut blocks = BTreeMap::new();
for node in nodes {
if let Node::Element {
name,
attrs,
children,
} = node
{
if name == "block" {
if let Some(slot) = attrs.get("slot") {
blocks.insert(slot.clone(), children.clone());
}
}
}
}
blocks
}
pub fn extract_load_targets(nodes: &[Node]) -> Vec<String> {
let mut targets = Vec::new();
for node in nodes {
match node {
Node::VoidElement { name, attrs } if name == "load" => {
if let Some(file) = attrs.get("file") {
targets.push(file.clone());
}
}
Node::Element { children, .. } => targets.extend(extract_load_targets(children)),
_ => {}
}
}
targets
}
pub fn resolve_loads(
nodes: &[Node],
fetch: &Fetch,
visited: &mut Vec<String>,
hoist: bool,
) -> TemplateResult<Vec<Node>> {
let blocks = extract_blocks(nodes);
let mut preamble = Vec::new();
let mut body = Vec::new();
for node in nodes {
match node {
Node::VoidElement { name, attrs } if name == "load" => {
let loaded = fetch_loaded(fetch, attrs, visited)?;
body.extend(inject_blocks(loaded, &blocks));
}
Node::Element { name, .. } if name == "block" => {
}
Node::VoidElement { name, .. }
if hoist && PREAMBLE_DIRECTIVES.contains(&name.as_str()) =>
{
preamble.push(node.clone())
}
Node::Element {
name,
attrs,
children,
} => body.push(Node::Element {
name: name.clone(),
attrs: attrs.clone(),
children: expand_loads(children, fetch, visited, &blocks)?,
}),
other => body.push(other.clone()),
}
}
preamble.extend(body);
Ok(preamble)
}
fn expand_loads(
nodes: &[Node],
fetch: &Fetch,
visited: &mut Vec<String>,
scope_blocks: &BTreeMap<String, Vec<Node>>,
) -> TemplateResult<Vec<Node>> {
let mut out = Vec::new();
for node in nodes {
match node {
Node::VoidElement { name, attrs } if name == "load" => {
let loaded = fetch_loaded(fetch, attrs, visited)?;
out.extend(inject_blocks(loaded, scope_blocks));
}
Node::Element {
name,
attrs,
children,
} => out.push(Node::Element {
name: name.clone(),
attrs: attrs.clone(),
children: expand_loads(children, fetch, visited, scope_blocks)?,
}),
other => out.push(other.clone()),
}
}
Ok(out)
}
fn fetch_loaded(
fetch: &Fetch,
attrs: &BTreeMap<String, String>,
visited: &mut Vec<String>,
) -> TemplateResult<Vec<Node>> {
let file = attrs.get("file").ok_or_else(|| {
TemplateError::code(
TemplateErrorPhase::Resolve,
"Load missing 'file' attribute".to_string(),
)
})?;
if visited.iter().any(|v| v == file) {
return Err(TemplateError::code(
TemplateErrorPhase::Resolve,
format!(
"Circular template dependency detected: {} -> {}",
visited.join(" -> "),
file
),
)
.with_template_path(file)
.with_directive("load"));
}
let loaded = fetch(file)?;
visited.push(file.clone());
let resolved = resolve_loads(&loaded, fetch, visited, false);
visited.pop();
resolved
}