use crate::{
error::{EnrichError, XacroError},
parse::xml::{is_known_xacro_uri, is_xacro_element},
};
use xmltree::{Element, XMLNode};
mod children;
mod directives;
#[cfg(test)]
mod directives_tests;
mod guards;
mod include;
mod macro_call;
mod utils;
use children::expand_children_list;
use directives::{check_unimplemented_directive, get_directive_registry};
use guards::DepthGuard;
use include::handle_include_directive;
use macro_call::{expand_macro_call, is_macro_call};
use utils::normalize_attribute_whitespace;
pub use crate::expand::XacroContext;
pub(crate) fn expand_node(
node: XMLNode,
ctx: &XacroContext,
) -> Result<Vec<XMLNode>, XacroError> {
if *ctx.recursion_depth.borrow() >= ctx.max_recursion_depth {
return Err(XacroError::MacroRecursionLimit {
depth: *ctx.recursion_depth.borrow(),
limit: ctx.max_recursion_depth,
});
}
let _depth_guard = DepthGuard::new(&ctx.recursion_depth);
match node {
XMLNode::Element(elem) => expand_element(elem, ctx),
XMLNode::Text(text) => {
let loc = ctx.get_location_context();
let resolved = ctx
.properties
.substitute_all(&text, Some(&loc))
.with_loc(&loc)?;
Ok(vec![XMLNode::Text(resolved)])
}
other => Ok(vec![other]), }
}
fn extract_directive_name<'a>(
elem: &'a Element,
xacro_ns: &str,
) -> Option<&'a str> {
let elem_ns = elem.namespace.as_deref();
if xacro_ns.is_empty() {
return None;
}
if elem_ns == Some(xacro_ns) || elem_ns.is_some_and(is_known_xacro_uri) {
Some(&elem.name)
} else {
None
}
}
fn expand_element(
mut elem: Element,
ctx: &XacroContext,
) -> Result<Vec<XMLNode>, XacroError> {
let xacro_ns = ctx.current_xacro_ns();
if let Some(directive_name) = extract_directive_name(&elem, &xacro_ns) {
let registry = get_directive_registry();
if let Some(handler) = registry.get(directive_name) {
return handler.handle(elem, ctx);
}
}
if is_xacro_element(&elem, "include", &xacro_ns) {
return handle_include_directive(elem, ctx);
}
check_unimplemented_directive(&elem, &xacro_ns)?;
if is_macro_call(&elem, &ctx.macros.borrow(), &xacro_ns) {
let loc = ctx.get_location_context();
for attr_value in elem.attributes.values_mut() {
let substituted = ctx
.properties
.substitute_all(attr_value, Some(&loc))
.with_loc(&loc)?;
*attr_value = normalize_attribute_whitespace(&substituted);
}
let parent_scope_depth = ctx.properties.scope_depth();
let expanded_nodes = expand_macro_call(&elem, ctx, parent_scope_depth)?;
return expand_children_list(expanded_nodes, ctx);
}
let loc = ctx.get_location_context();
for attr_value in elem.attributes.values_mut() {
let substituted = ctx
.properties
.substitute_all(attr_value, Some(&loc))
.with_loc(&loc)?;
*attr_value = normalize_attribute_whitespace(&substituted);
}
let expanded_children = expand_children_list(elem.children, ctx)?;
elem.children = expanded_children;
Ok(vec![XMLNode::Element(elem)])
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_context_creation() {
let ctx = XacroContext::new(
PathBuf::from("/test"),
"http://www.ros.org/wiki/xacro".to_string(),
);
assert_eq!(ctx.current_xacro_ns(), "http://www.ros.org/wiki/xacro");
assert_eq!(ctx.include_stack.borrow().len(), 0);
assert_eq!(ctx.block_stack.borrow().len(), 0);
}
#[test]
fn test_expand_text_node() {
let ctx = XacroContext::new(
PathBuf::from("/test"),
"http://www.ros.org/wiki/xacro".to_string(),
);
ctx.properties
.add_raw_property("x".to_string(), "42".to_string());
let text_node = XMLNode::Text("value: ${x}".to_string());
let result = expand_node(text_node, &ctx).unwrap();
assert_eq!(result.len(), 1);
let text = result[0].as_text().expect("Expected text node");
assert_eq!(text, "value: 42");
}
#[test]
fn test_expand_empty_property() {
let ctx = XacroContext::new(
PathBuf::from("/test"),
"http://www.ros.org/wiki/xacro".to_string(),
);
ctx.properties
.add_raw_property("empty".to_string(), "".to_string());
let text_node = XMLNode::Text("prefix_${empty}_suffix".to_string());
let result = expand_node(text_node, &ctx).unwrap();
assert_eq!(result.len(), 1);
let text = result[0].as_text().expect("Expected text node");
assert_eq!(text, "prefix__suffix");
}
#[test]
fn test_expand_nested_expressions() {
let ctx = XacroContext::new(
PathBuf::from("/test"),
"http://www.ros.org/wiki/xacro".to_string(),
);
ctx.properties
.add_raw_property("x".to_string(), "10".to_string());
ctx.properties
.add_raw_property("y".to_string(), "20".to_string());
let text_node = XMLNode::Text("sum: ${x + y}, product: ${x * y}".to_string());
let result = expand_node(text_node, &ctx).unwrap();
assert_eq!(result.len(), 1);
let text = result[0].as_text().expect("Expected text node");
assert_eq!(text, "sum: 30, product: 200");
}
#[test]
fn test_expand_nested_property_reference() {
let ctx = XacroContext::new(
PathBuf::from("/test"),
"http://www.ros.org/wiki/xacro".to_string(),
);
ctx.properties
.add_raw_property("base".to_string(), "value".to_string());
ctx.properties
.add_raw_property("derived".to_string(), "${base}_extended".to_string());
let text_node = XMLNode::Text("result: ${derived}".to_string());
let result = expand_node(text_node, &ctx).unwrap();
assert_eq!(result.len(), 1);
let text = result[0].as_text().expect("Expected text node");
assert_eq!(text, "result: value_extended");
}
#[test]
fn test_expand_error_undefined_property() {
let ctx = XacroContext::new(
PathBuf::from("/test"),
"http://www.ros.org/wiki/xacro".to_string(),
);
let text_node = XMLNode::Text("value: ${undefined_property}".to_string());
let result = expand_node(text_node, &ctx);
let err = result.expect_err("Should error on undefined property reference");
assert!(
matches!(
err,
XacroError::WithContext { ref source, .. }
if matches!(&**source, XacroError::EvalError { ref expr, .. } if expr.contains("undefined_property"))
),
"Expected WithContext wrapping EvalError mentioning 'undefined_property', got: {:?}",
err
);
}
#[test]
fn test_expand_error_malformed_expression() {
let ctx = XacroContext::new(
PathBuf::from("/test"),
"http://www.ros.org/wiki/xacro".to_string(),
);
let text_node = XMLNode::Text("value: ${1 +* 2}".to_string());
let result = expand_node(text_node, &ctx);
assert!(
result.is_err(),
"Should error on malformed expression syntax"
);
}
#[test]
fn test_expand_error_invalid_operation() {
let ctx = XacroContext::new(
PathBuf::from("/test"),
"http://www.ros.org/wiki/xacro".to_string(),
);
ctx.properties
.add_raw_property("text".to_string(), "hello".to_string());
let text_node = XMLNode::Text("value: ${text * 2}".to_string());
let result = expand_node(text_node, &ctx);
assert!(
result.is_err(),
"Should error on invalid operation (multiply string)"
);
}
}