Skip to main content

xacro_rs/
processor.rs

1use crate::{
2    error::{XacroError, IMPLEMENTED_FEATURES, UNIMPLEMENTED_FEATURES},
3    expander::{expand_node, XacroContext},
4    extensions::ExtensionHandler,
5    parse::xml::{extract_xacro_namespace, is_known_xacro_uri},
6};
7use xmltree::XMLNode;
8
9use ::core::{cell::RefCell, str::FromStr};
10use std::{collections::HashMap, path::PathBuf, rc::Rc};
11use thiserror::Error;
12
13/// Error type for invalid compatibility mode strings
14#[derive(Debug, Error)]
15pub enum CompatModeParseError {
16    #[error("Compatibility mode cannot be empty (valid: all, namespace, duplicate_params)")]
17    Empty,
18    #[error("Unknown compatibility mode: '{0}' (valid: all, namespace, duplicate_params)")]
19    Unknown(String),
20}
21
22/// Python xacro compatibility modes
23#[derive(Debug, Clone, Copy, Default)]
24pub struct CompatMode {
25    /// Accept duplicate macro parameters (last declaration wins)
26    pub duplicate_params: bool,
27    /// Accept namespace URIs that don't match known xacro URIs
28    pub namespace: bool,
29}
30
31impl CompatMode {
32    /// No compatibility mode (strict validation)
33    pub fn none() -> Self {
34        Self {
35            duplicate_params: false,
36            namespace: false,
37        }
38    }
39
40    /// All compatibility modes enabled
41    pub fn all() -> Self {
42        Self {
43            duplicate_params: true,
44            namespace: true,
45        }
46    }
47}
48
49impl FromStr for CompatMode {
50    type Err = CompatModeParseError;
51
52    /// Parse compatibility mode from string
53    ///
54    /// Supported formats:
55    /// - "all" → all modes enabled
56    /// - "namespace" → only namespace mode
57    /// - "duplicate_params" → only duplicate params mode
58    /// - "namespace,duplicate_params" → multiple modes (comma-separated)
59    fn from_str(s: &str) -> Result<Self, Self::Err> {
60        // Reject empty or whitespace-only strings to prevent silent misconfigurations
61        let s = s.trim();
62        if s.is_empty() {
63            return Err(CompatModeParseError::Empty);
64        }
65
66        let mut mode = Self::none();
67
68        for part in s.split(',') {
69            let part = part.trim();
70            if part.is_empty() {
71                continue;
72            }
73            match part {
74                "all" => return Ok(Self::all()),
75                "namespace" => mode.namespace = true,
76                "duplicate_params" => mode.duplicate_params = true,
77                _ => return Err(CompatModeParseError::Unknown(part.to_string())),
78            }
79        }
80
81        Ok(mode)
82    }
83}
84
85pub struct XacroProcessor {
86    /// Maximum recursion depth for macro expansion and insert_block
87    /// Default: 50 (set conservatively to prevent stack overflow)
88    max_recursion_depth: usize,
89    /// CLI arguments passed to the processor (for xacro:arg support)
90    /// These take precedence over XML defaults
91    args: HashMap<String, String>,
92    /// Python xacro compatibility modes
93    compat_mode: CompatMode,
94    /// Extension handlers for $(command args...) resolution
95    extensions: Rc<Vec<Box<dyn ExtensionHandler>>>,
96    /// YAML tag handlers for custom tags (e.g., !degrees, !millimeters)
97    #[cfg(feature = "yaml")]
98    yaml_tag_handlers: Rc<crate::eval::yaml_tag_handler::YamlTagHandlerRegistry>,
99}
100
101/// Builder for configuring XacroProcessor with custom settings.
102///
103/// Provides a fluent API for setting optional parameters without constructor explosion.
104///
105/// # Example
106/// ```
107/// use xacro_rs::XacroProcessor;
108///
109/// let processor = XacroProcessor::builder()
110///     .with_arg("robot_name", "my_robot")
111///     .with_arg("use_lidar", "true")
112///     .with_max_depth(100)
113///     .build();
114/// ```
115pub struct XacroBuilder {
116    args: HashMap<String, String>,
117    max_recursion_depth: usize,
118    compat_mode: CompatMode,
119    extensions: Option<Vec<Box<dyn ExtensionHandler>>>,
120    #[cfg(feature = "yaml")]
121    yaml_tag_handlers: Option<crate::eval::yaml_tag_handler::YamlTagHandlerRegistry>,
122}
123
124impl XacroBuilder {
125    /// Create a new builder with default settings.
126    fn new() -> Self {
127        Self {
128            args: HashMap::new(),
129            max_recursion_depth: XacroContext::DEFAULT_MAX_DEPTH,
130            compat_mode: CompatMode::none(),
131            extensions: None, // None = use default extensions
132            #[cfg(feature = "yaml")]
133            yaml_tag_handlers: None, // None = empty registry (no default handlers)
134        }
135    }
136
137    /// Add a single CLI argument.
138    ///
139    /// # Example
140    /// ```
141    /// use xacro_rs::XacroProcessor;
142    ///
143    /// let processor = XacroProcessor::builder()
144    ///     .with_arg("scale", "0.5")
145    ///     .build();
146    /// ```
147    pub fn with_arg(
148        mut self,
149        name: impl Into<String>,
150        value: impl Into<String>,
151    ) -> Self {
152        self.args.insert(name.into(), value.into());
153        self
154    }
155
156    /// Add multiple CLI arguments from a HashMap.
157    ///
158    /// # Example
159    /// ```
160    /// use xacro_rs::XacroProcessor;
161    /// use std::collections::HashMap;
162    ///
163    /// let mut args = HashMap::new();
164    /// args.insert("scale".to_string(), "0.5".to_string());
165    /// args.insert("prefix".to_string(), "robot_".to_string());
166    ///
167    /// let processor = XacroProcessor::builder()
168    ///     .with_args(args)
169    ///     .build();
170    /// ```
171    pub fn with_args(
172        mut self,
173        args: HashMap<String, String>,
174    ) -> Self {
175        self.args.extend(args);
176        self
177    }
178
179    /// Set the maximum recursion depth.
180    ///
181    /// # Example
182    /// ```
183    /// use xacro_rs::XacroProcessor;
184    ///
185    /// let processor = XacroProcessor::builder()
186    ///     .with_max_depth(100)
187    ///     .build();
188    /// ```
189    pub fn with_max_depth(
190        mut self,
191        max_depth: usize,
192    ) -> Self {
193        self.max_recursion_depth = max_depth;
194        self
195    }
196
197    /// Enable all compatibility modes.
198    ///
199    /// # Example
200    /// ```
201    /// use xacro_rs::XacroProcessor;
202    ///
203    /// let processor = XacroProcessor::builder()
204    ///     .with_compat_all()
205    ///     .build();
206    /// ```
207    pub fn with_compat_all(mut self) -> Self {
208        self.compat_mode = CompatMode::all();
209        self
210    }
211
212    /// Set a specific compatibility mode.
213    ///
214    /// # Example
215    /// ```
216    /// use xacro_rs::{XacroProcessor, CompatMode};
217    ///
218    /// let compat = "namespace,duplicate_params".parse().unwrap();
219    /// let processor = XacroProcessor::builder()
220    ///     .with_compat_mode(compat)
221    ///     .build();
222    /// ```
223    pub fn with_compat_mode(
224        mut self,
225        mode: CompatMode,
226    ) -> Self {
227        self.compat_mode = mode;
228        self
229    }
230
231    /// Add a custom extension handler.
232    ///
233    /// By default, the processor includes CwdExtension and EnvExtension.
234    /// Note: $(arg ...) is handled specially, not via an extension handler.
235    /// This method allows adding additional custom extensions without replacing the defaults.
236    ///
237    /// # Example
238    /// ```ignore
239    /// use xacro_rs::XacroProcessor;
240    /// use xacro_rs::extensions::ExtensionHandler;
241    ///
242    /// struct MyExtension;
243    /// impl ExtensionHandler for MyExtension {
244    ///     fn resolve(&self, command: &str, args_raw: &str)
245    ///         -> Result<Option<String>, Box<dyn std::error::Error>> {
246    ///         if command == "my_ext" {
247    ///             Ok(Some(format!("Custom: {}", args_raw)))
248    ///         } else {
249    ///             Ok(None)
250    ///         }
251    ///     }
252    /// }
253    ///
254    /// let processor = XacroProcessor::builder()
255    ///     .with_extension(Box::new(MyExtension))
256    ///     .build();
257    /// ```
258    pub fn with_extension(
259        mut self,
260        handler: Box<dyn ExtensionHandler>,
261    ) -> Self {
262        self.extensions
263            .get_or_insert_with(Self::default_extensions)
264            .push(handler);
265        self
266    }
267
268    /// Clear all extensions (including defaults).
269    ///
270    /// Use this if you want to provide a completely custom extension list
271    /// without any of the built-in extensions.
272    ///
273    /// # Example
274    /// ```ignore
275    /// use xacro_rs::XacroProcessor;
276    ///
277    /// let processor = XacroProcessor::builder()
278    ///     .clear_extensions()
279    ///     .with_extension(Box::new(MyExtension))
280    ///     .build();
281    /// ```
282    pub fn clear_extensions(mut self) -> Self {
283        self.extensions = Some(Vec::new());
284        self
285    }
286
287    /// Register a YAML tag handler
288    ///
289    /// Handlers are tried in registration order. Register more specific handlers
290    /// before more general ones.
291    ///
292    /// # Example
293    /// ```ignore
294    /// use xacro_rs::XacroProcessor;
295    ///
296    /// let processor = XacroProcessor::builder()
297    ///     .with_yaml_tag_handler(Box::new(MyHandler))
298    ///     .build();
299    /// ```
300    #[cfg(feature = "yaml")]
301    pub fn with_yaml_tag_handler(
302        mut self,
303        handler: crate::extensions::DynYamlTagHandler,
304    ) -> Self {
305        self.yaml_tag_handlers
306            .get_or_insert_with(crate::eval::yaml_tag_handler::YamlTagHandlerRegistry::new)
307            .register(handler);
308        self
309    }
310
311    /// Enable ROS unit conversions (degrees, radians, millimeters, etc.)
312    ///
313    /// This is a convenience method for ROS users. Registers the RosUnitTagHandler
314    /// which handles standard ROS unit tags like !degrees, !millimeters, etc.
315    ///
316    /// # Example
317    /// ```ignore
318    /// use xacro_rs::XacroProcessor;
319    ///
320    /// let processor = XacroProcessor::builder()
321    ///     .with_ros_yaml_units()
322    ///     .build();
323    /// ```
324    #[cfg(feature = "yaml")]
325    pub fn with_ros_yaml_units(self) -> Self {
326        self.with_yaml_tag_handler(Box::new(
327            crate::extensions::ros_yaml_handlers::RosUnitTagHandler::new(),
328        ))
329    }
330
331    /// Build the XacroProcessor with the configured settings.
332    pub fn build(self) -> XacroProcessor {
333        let extensions = Rc::new(self.extensions.unwrap_or_else(Self::default_extensions));
334
335        XacroProcessor {
336            max_recursion_depth: self.max_recursion_depth,
337            args: self.args,
338            compat_mode: self.compat_mode,
339            extensions,
340            #[cfg(feature = "yaml")]
341            yaml_tag_handlers: Rc::new(self.yaml_tag_handlers.unwrap_or_default()),
342        }
343    }
344
345    /// Create default extension handlers (CwdExtension, EnvExtension).
346    ///
347    /// This delegates to the centralized `extensions::core::default_extensions()`.
348    /// ROS extensions (FindExtension, OptEnvExtension) are NOT included by default.
349    /// Library users should explicitly add them via builder pattern if needed.
350    /// The CLI binary adds ROS extensions automatically for user convenience.
351    ///
352    /// Note: $(arg ...) is handled specially in `EvalContext::resolve_extension()`
353    /// to ensure correct interaction with the shared arguments map.
354    fn default_extensions() -> Vec<Box<dyn ExtensionHandler>> {
355        crate::extensions::core::default_extensions()
356    }
357}
358
359impl Default for XacroProcessor {
360    fn default() -> Self {
361        Self::builder().build()
362    }
363}
364
365impl XacroProcessor {
366    /// Create a new builder for configuring the processor.
367    ///
368    /// # Example
369    /// ```
370    /// use xacro_rs::XacroProcessor;
371    ///
372    /// let processor = XacroProcessor::builder()
373    ///     .with_arg("robot_name", "my_robot")
374    ///     .with_max_depth(100)
375    ///     .build();
376    /// ```
377    pub fn builder() -> XacroBuilder {
378        XacroBuilder::new()
379    }
380
381    /// Create a new xacro processor with default settings.
382    ///
383    /// For custom configuration, use [`XacroProcessor::builder()`].
384    ///
385    /// # Example
386    /// ```
387    /// use xacro_rs::XacroProcessor;
388    ///
389    /// let processor = XacroProcessor::new();
390    /// let input = r#"<?xml version="1.0"?>
391    /// <robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="test">
392    ///   <xacro:property name="value" value="42"/>
393    ///   <link name="base"><inertial><mass value="${value}"/></inertial></link>
394    /// </robot>"#;
395    /// let output = processor.run_from_string(input)?;
396    /// assert!(output.contains("mass value=\"42\""));
397    /// # Ok::<(), xacro_rs::XacroError>(())
398    /// ```
399    pub fn new() -> Self {
400        Self::default()
401    }
402
403    /// Get a reference to the registered extension handlers.
404    ///
405    /// This allows callers to inspect or downcast extensions for
406    /// observability purposes (e.g., extracting package resolution info
407    /// from FindExtension).
408    ///
409    /// # Example
410    /// ```ignore
411    /// use xacro_rs::{XacroProcessor, FindExtension};
412    ///
413    /// let processor = XacroProcessor::new();
414    /// processor.run("robot.xacro")?;
415    ///
416    /// // Extract package info from FindExtension
417    /// for ext in processor.extensions().iter() {
418    ///     if let Some(find_ext) = ext.as_any().downcast_ref::<FindExtension>() {
419    ///         let packages = find_ext.get_found_packages();
420    ///         println!("Found packages: {:?}", packages);
421    ///     }
422    /// }
423    /// # Ok::<(), xacro_rs::XacroError>(())
424    /// ```
425    pub fn extensions(&self) -> &[Box<dyn ExtensionHandler>] {
426        &self.extensions
427    }
428
429    /// Process xacro content from a file path
430    pub fn run<P: AsRef<std::path::Path>>(
431        &self,
432        path: P,
433    ) -> Result<String, XacroError> {
434        // Thin wrapper over run_with_deps that discards dependency list
435        self.run_with_deps(path).map(|(output, _)| output)
436    }
437
438    /// Process xacro content from a file path and return included files
439    ///
440    /// Returns a tuple of (processed_output, included_files).
441    /// The included_files list contains paths to all files that were included
442    /// during processing via `<xacro:include>` directives.
443    pub fn run_with_deps<P: AsRef<std::path::Path>>(
444        &self,
445        path: P,
446    ) -> Result<(String, Vec<PathBuf>), XacroError> {
447        let doc = XacroProcessor::parse_file(&path)?;
448        let file_path = path.as_ref();
449        let base_dir = file_path
450            .parent()
451            .unwrap_or_else(|| std::path::Path::new("."));
452        self.run_impl(doc, base_dir, Some(file_path))
453    }
454
455    /// Process xacro content from a string
456    ///
457    /// # Note
458    /// Any `<xacro:include>` directives with relative paths will be resolved
459    /// relative to the current working directory.
460    pub fn run_from_string(
461        &self,
462        content: &str,
463    ) -> Result<String, XacroError> {
464        // Thin wrapper over run_from_string_with_deps that discards dependency list
465        self.run_from_string_with_deps(content)
466            .map(|(output, _)| output)
467    }
468
469    /// Process xacro content from a string and return included files
470    ///
471    /// Returns a tuple of (processed_output, included_files).
472    /// The included_files list contains paths to all files that were included
473    /// during processing via `<xacro:include>` directives.
474    ///
475    /// # Note
476    /// Any `<xacro:include>` directives with relative paths will be resolved
477    /// relative to the current working directory.
478    pub fn run_from_string_with_deps(
479        &self,
480        content: &str,
481    ) -> Result<(String, Vec<PathBuf>), XacroError> {
482        let doc = crate::parse::document::XacroDocument::parse(content.as_bytes())?;
483        // Use current directory as base path for any includes in test content
484        self.run_impl(doc, std::path::Path::new("."), None)
485    }
486
487    /// Notify all extension handlers of file context change
488    ///
489    /// Calls the lifecycle hook on_file_change() for all registered extensions.
490    /// Extensions that need file context (e.g., FindExtension for ancestor package
491    /// detection) can implement this hook to update their state.
492    ///
493    /// Pass Some(path) when entering a file context, None when clearing.
494    fn notify_file_change(
495        extensions: &Rc<Vec<Box<dyn ExtensionHandler>>>,
496        file_path: Option<&std::path::Path>,
497    ) {
498        for handler in extensions.iter() {
499            handler.on_file_change(file_path);
500        }
501    }
502
503    /// Internal implementation
504    fn run_impl(
505        &self,
506        mut doc: crate::parse::document::XacroDocument,
507        base_path: &std::path::Path,
508        file_path: Option<&std::path::Path>,
509    ) -> Result<(String, Vec<PathBuf>), XacroError> {
510        // Extract xacro namespace from document root (if present)
511        // Strategy:
512        // 1. Try standard "xacro" prefix (e.g., xmlns:xacro="...")
513        // 2. Fallback: search for any prefix bound to a known xacro URI
514        // 3. If not found, use empty string (lazy checking - only error if xacro elements actually used)
515        //
516        // Documents with NO xacro elements don't need xacro namespace declaration.
517        // Only error during finalize_tree if xacro elements are found.
518        //
519        // When in namespace compat mode, skip URI validation to accept files with
520        // "typo" URIs like xmlns:xacro="...#interface" that Python xacro accepts
521        let xacro_ns = extract_xacro_namespace(&doc.root, self.compat_mode.namespace)?;
522
523        // Create expansion context with CLI arguments, compat mode, and extensions
524        // Math constants (pi, e, tau, etc.) are automatically initialized by EvalContext::new()
525        // CLI args are passed to the context and take precedence over XML defaults
526        //
527        // Note: $(arg ...) is handled specially in resolve_extension using the shared
528        // args map, so ArgExtension is not included in the extensions list
529        let args_rc = Rc::new(RefCell::new(self.args.clone()));
530
531        let mut ctx = XacroContext::new_with_extensions(
532            base_path.to_path_buf(),
533            xacro_ns.clone(),
534            args_rc,
535            self.compat_mode,
536            self.extensions.clone(),
537            #[cfg(feature = "yaml")]
538            self.yaml_tag_handlers.clone(), // Rc clone is cheap
539        );
540        ctx.set_max_recursion_depth(self.max_recursion_depth);
541
542        // Set actual source file path if provided (replaces directory in namespace_stack)
543        // Always notify extensions to prevent stale file context
544        if let Some(file) = file_path {
545            ctx.set_source_file(file.to_path_buf());
546            Self::notify_file_change(&self.extensions, Some(file));
547        } else {
548            // For run_from_string, clear file context in extensions
549            Self::notify_file_change(&self.extensions, None);
550        }
551
552        // Expand the root element itself. This will handle attributes on the root
553        // and any xacro directives at the root level (though unlikely).
554        let expanded_nodes = expand_node(XMLNode::Element(doc.root), &ctx)?;
555
556        // The expansion of the root must result in a single root element.
557        if expanded_nodes.len() != 1 {
558            return Err(XacroError::InvalidRoot(format!(
559                "Root element expanded to {} nodes, expected 1",
560                expanded_nodes.len()
561            )));
562        }
563
564        doc.root = match expanded_nodes.into_iter().next().unwrap() {
565            XMLNode::Element(elem) => elem,
566            _ => {
567                return Err(XacroError::InvalidRoot(
568                    "Root element expanded to a non-element node (e.g., text or comment)"
569                        .to_string(),
570                ))
571            }
572        };
573
574        // Final cleanup: check for unprocessed xacro elements and remove namespace
575        Self::finalize_tree(&mut doc.root, &xacro_ns, &self.compat_mode)?;
576
577        let output = XacroProcessor::serialize(&doc)?;
578        let includes = ctx.get_all_includes();
579        Ok((output, includes))
580    }
581
582    fn finalize_tree_children(
583        element: &mut xmltree::Element,
584        xacro_ns: &str,
585        compat_mode: &CompatMode,
586    ) -> Result<(), XacroError> {
587        for child in &mut element.children {
588            if let Some(child_elem) = child.as_mut_element() {
589                Self::finalize_tree(child_elem, xacro_ns, compat_mode)?;
590            }
591        }
592        Ok(())
593    }
594
595    fn finalize_tree(
596        element: &mut xmltree::Element,
597        xacro_ns: &str,
598        compat_mode: &CompatMode,
599    ) -> Result<(), XacroError> {
600        // Check if this element is in the xacro namespace (indicates unprocessed feature)
601        // Must check namespace URI, not prefix, to handle namespace aliasing (e.g., xmlns:x="...")
602
603        // Remove xacro namespace declarations from all elements first
604        // Strategy: Remove prefixes that are:
605        // 1. Literally named "xacro" (regardless of URI)
606        // 2. Bound to known standard xacro URIs (defense-in-depth)
607        // This handles both standard URIs and non-standard URIs accepted in compat mode.
608        // Do this BEFORE checking for unprocessed features to ensure cleanup happens regardless.
609        //
610        // Note: We DON'T remove non-"xacro" prefixes bound to non-standard URIs, even if
611        // that URI happens to be used as xacro_ns in compat mode. Those prefixes may be
612        // actively used in the document (namespace collision case).
613        if !xacro_ns.is_empty() {
614            if let Some(ref mut ns) = element.namespaces {
615                // Find prefixes to remove
616                let prefixes_to_remove: Vec<String> =
617                    ns.0.iter()
618                        .filter(|(prefix, uri)| {
619                            // Always remove "xacro" prefix
620                            if prefix.as_str() == "xacro" {
621                                return true;
622                            }
623                            // Remove other prefixes only if bound to known standard URIs
624                            is_known_xacro_uri(uri.as_str())
625                        })
626                        .map(|(prefix, _)| prefix.clone())
627                        .collect();
628
629                // Remove all found prefixes
630                for prefix in prefixes_to_remove {
631                    ns.0.remove(&prefix);
632                }
633            }
634        }
635
636        // Case 1: Element has namespace and matches declared xacro namespace
637        if !xacro_ns.is_empty() && element.namespace.as_deref() == Some(xacro_ns) {
638            // Compat mode: handle namespace collision (same URI bound to multiple prefixes)
639            // If the element uses a non-"xacro" prefix, Python xacro ignores it based on prefix string check.
640            // In strict mode, this is a hard error (poor XML practice).
641            if compat_mode.namespace {
642                let prefix = element.prefix.as_deref().unwrap_or("");
643                if prefix != "xacro" {
644                    let element_display = if prefix.is_empty() {
645                        format!("<{}>", element.name)
646                    } else {
647                        format!("<{}:{}>", prefix, element.name)
648                    };
649                    log::warn!(
650                        "Namespace collision: {} uses xacro namespace URI but different prefix (compat mode)",
651                        element_display
652                    );
653                    // Pass through - recursively finalize children but don't error
654                    Self::finalize_tree_children(element, xacro_ns, compat_mode)?;
655                    return Ok(());
656                }
657            }
658
659            // Use centralized feature lists for consistent error messages
660            return Err(XacroError::UnimplementedFeature(format!(
661                "<xacro:{}>\n\
662                     This element was not processed. Either:\n\
663                     1. The feature is not implemented yet (known unimplemented: {})\n\
664                     2. There's a bug in the processor\n\
665                     \n\
666                     Currently implemented: {}",
667                element.name,
668                UNIMPLEMENTED_FEATURES.join(", "),
669                IMPLEMENTED_FEATURES.join(", ")
670            )));
671        }
672
673        // Case 2: Element has a known xacro namespace but no namespace was declared in root
674        // This is the lazy checking: only error if xacro elements are actually used
675        if xacro_ns.is_empty() {
676            if let Some(elem_ns) = element.namespace.as_deref() {
677                if is_known_xacro_uri(elem_ns) {
678                    return Err(XacroError::MissingNamespace(format!(
679                        "Found xacro element <{}> with namespace '{}', but no xacro namespace declared in document root. \
680                         Please add xmlns:xacro=\"{}\" to your root element.",
681                        element.name, elem_ns, elem_ns
682                    )));
683                }
684            }
685        }
686
687        // Recursively process children
688        Self::finalize_tree_children(element, xacro_ns, compat_mode)?;
689
690        Ok(())
691    }
692
693    /// Parse a xacro document from a file path
694    pub(crate) fn parse_file<P: AsRef<std::path::Path>>(
695        path: P
696    ) -> Result<crate::parse::XacroDocument, XacroError> {
697        let file = std::fs::File::open(path)?;
698        crate::parse::XacroDocument::parse(file)
699    }
700
701    /// Serialize a xacro document to a string
702    pub(crate) fn serialize(doc: &crate::parse::XacroDocument) -> Result<String, XacroError> {
703        let mut writer = Vec::new();
704        doc.write(&mut writer)?;
705        Ok(String::from_utf8(writer)?)
706    }
707}