xacro 0.1.3

A xml preprocessor for xacro files to generate URDF files
Documentation
use crate::error::XacroError;
use hashbrown::HashMap;
use xmltree::{
    Element,
    XMLNode::{Element as NodeElement, Text as TextElement},
};

pub struct PropertyProcessor {}

impl PropertyProcessor {
    #[allow(clippy::new_without_default)]
    pub fn new() -> Self {
        Self {}
    }

    pub fn process(
        &self,
        mut xml: Element,
    ) -> Result<Element, XacroError> {
        let mut properties = HashMap::new();
        Self::collect_properties(&xml, &mut properties)?;
        Self::substitute_properties(&mut xml, &properties)?;
        Self::remove_property_elements(&mut xml);
        Ok(xml)
    }

    fn collect_properties(
        element: &Element,
        properties: &mut HashMap<String, String>,
    ) -> Result<(), XacroError> {
        if element.name == "property" {
            if let (Some(name), Some(value)) = (
                element.attributes.get("name"),
                element.attributes.get("value"),
            ) {
                properties.insert(name.clone(), value.clone());
            }
        }

        for child in &element.children {
            if let NodeElement(child_elem) = child {
                Self::collect_properties(child_elem, properties)?;
            }
        }

        Ok(())
    }

    fn substitute_properties(
        element: &mut Element,
        properties: &HashMap<String, String>,
    ) -> Result<(), XacroError> {
        for value in element.attributes.values_mut() {
            *value = Self::substitute_in_text(value, properties)?;
        }

        for child in &mut element.children {
            if let NodeElement(child_elem) = child {
                Self::substitute_properties(child_elem, properties)?;
            } else if let TextElement(text) = child {
                *text = Self::substitute_in_text(text, properties)?;
            }
        }

        Ok(())
    }

    fn substitute_in_text(
        text: &str,
        properties: &HashMap<String, String>,
    ) -> Result<String, XacroError> {
        let mut result = text.to_string();

        while let Some(start) = result.find("${") {
            if let Some(end) = result[start..].find('}') {
                let end = start + end + 1;
                let prop_name = &result[start + 2..end - 1];

                if let Some(value) = properties.get(prop_name) {
                    result.replace_range(start..end, value);
                } else {
                    return Err(XacroError::PropertyNotFound(prop_name.to_string()));
                }
            }
        }

        Ok(result)
    }

    fn remove_property_elements(element: &mut Element) {
        element.children.retain_mut(|child| {
            if let NodeElement(child_elem) = child {
                if child_elem.name == "property" {
                    return false;
                }
                Self::remove_property_elements(child_elem);
            }
            true
        });
    }
}

#[cfg(test)]
mod tests;