edit_xml/document/node.rs
1use std::{borrow::Cow, fmt::Debug};
2#[cfg(feature = "document-breakdown")]
3mod breakdown;
4#[cfg(feature = "document-breakdown")]
5pub use breakdown::*;
6use tracing::instrument;
7
8use crate::{element::ElementDebug, Document, Element};
9
10/// Represents an XML node.
11#[derive(Debug, Clone, PartialEq)]
12pub enum Node {
13 /// XML Element
14 Element(Element),
15 /// XML Character Data ([specification](https://www.w3.org/TR/xml/#syntax))
16 Text(String),
17 /// Comments ([specification](https://www.w3.org/TR/xml/#sec-comments))
18 Comment(String),
19 /// CDATA ([specification](https://www.w3.org/TR/xml/#sec-cdata-sect))
20 CData(String),
21 /// Processing Instruction ([specification](https://www.w3.org/TR/xml/#sec-pi))
22 PI(String),
23 /// Document Type Declaration ([specification](https://www.w3.org/TR/xml/#sec-prolog-dtd))
24 DocType(String),
25}
26impl From<Element> for Node {
27 fn from(elem: Element) -> Self {
28 Node::Element(elem)
29 }
30}
31impl From<&Element> for Node {
32 fn from(elem: &Element) -> Self {
33 Node::Element(*elem)
34 }
35}
36macro_rules! enum_is {
37 [
38 $(
39 $(#[$docs:meta])*
40 $fn_name:ident => $name:ident
41 ),*
42 ] => {
43 $(
44 $(#[$docs])*
45 pub fn $fn_name(&self) -> bool {
46 matches!(self, Node::$name(_))
47 }
48 )*
49 };
50}
51impl Node {
52 /// Useful to use inside `filter_map`.
53 ///
54 /// ```
55 /// use edit_xml::{Document, Element};
56 ///
57 /// let mut doc = Document::parse_str(r#"<?xml version="1.0" encoding="UTF-8"?>
58 /// <config>
59 /// Random Text
60 /// <max>1</max>
61 /// </config>
62 /// "#).unwrap();
63 ///
64 /// let elems: Vec<Element> = doc
65 /// .root_element()
66 /// .unwrap()
67 /// .children(&doc)
68 /// .iter()
69 /// .filter_map(|n| n.as_element())
70 /// .collect();
71 /// ```
72 pub fn as_element(&self) -> Option<Element> {
73 match self {
74 Self::Element(elem) => Some(*elem),
75 _ => None,
76 }
77 }
78 #[instrument]
79 pub(crate) fn build_text_content<'a>(&self, doc: &'a Document, buf: &'a mut String) {
80 match self {
81 Node::Element(elem) => elem.build_text_content(doc, buf),
82 Node::Text(text) => buf.push_str(text),
83 Node::CData(text) => buf.push_str(text),
84 Node::PI(text) => buf.push_str(text),
85 _ => {}
86 }
87 }
88
89 /// Returns content if node is `Text`, `CData`, or `PI`.
90 /// If node is `Element`, return [Element::text_content()]
91 ///
92 /// Implementation of [Node.textContent](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)
93 pub fn text_content(&self, doc: &Document) -> String {
94 let mut buf = String::new();
95 self.build_text_content(doc, &mut buf);
96 buf
97 }
98 /// Returns content if node is `Text`, `CData`, or `PI`.
99 ///
100 /// If node is `Element` Cow will be owned.
101 /// Otherwise, Cow will be borrowed.
102 ///
103 /// If None is returned it is a comment or doctype
104 pub fn possible_borrowed_text(&self) -> Option<Cow<'_, str>> {
105 match self {
106 Node::Text(text) => Some(Cow::Borrowed(text)),
107 Node::CData(text) => Some(Cow::Borrowed(text)),
108 Node::PI(text) => Some(Cow::Borrowed(text)),
109 Node::Element(element) => Some(Cow::Owned(element.text_content(&Document::new()))),
110 _ => None,
111 }
112 }
113 /// Debug the node
114 pub fn debug<'node, 'doc>(&'node self, doc: &'doc Document) -> NodeDebug<'node, 'doc> {
115 NodeDebug::new(self, doc)
116 }
117 enum_is![
118 /// Returns true if the node is a text node
119 /// ```
120 /// use edit_xml::Node;
121 /// let node = Node::Text("Hello".to_string());
122 /// assert!(node.is_text());
123 /// ```
124 is_text => Text,
125 /// Returns true if the node is a comment node
126 /// ```
127 /// use edit_xml::Node;
128 /// let node = Node::Comment("Hello".to_string());
129 /// assert!(node.is_comment());
130 /// ```
131 is_comment => Comment,
132 /// Returns true if the node is a CDATA node
133 /// ```
134 /// use edit_xml::Node;
135 /// let node = Node::CData("Hello".to_string());
136 /// assert!(node.is_cdata());
137 /// ```
138 is_cdata => CData,
139 /// Returns true if the node is a Processing Instruction node
140 /// ```
141 /// use edit_xml::Node;
142 /// let node = Node::PI("Hello".to_string());
143 /// assert!(node.is_pi());
144 /// ```
145 is_pi => PI,
146 /// Returns true if the node is a Document Type Declaration
147 /// ```
148 /// use edit_xml::Node;
149 /// let node = Node::DocType("Hello".to_string());
150 /// assert!(node.is_doctype());
151 /// ```
152 is_doctype => DocType,
153 /// Returns true if the node is an element
154 is_element => Element
155 ];
156}
157pub enum NodeDebug<'node, 'doc> {
158 /// Uses ElementDebug to debug the element
159 Element(ElementDebug<'node, 'doc>),
160 /// Text node
161 Text(&'node str),
162 /// Comment node
163 Comment(&'node str),
164 /// CDATA node
165 CData(&'node str),
166 /// Processing Instruction node
167 PI(&'node str),
168 /// Document Type Declaration node
169 DocType(&'node str),
170}
171impl<'node, 'doc> NodeDebug<'node, 'doc> {
172 pub fn new(node: &'node Node, doc: &'doc Document) -> Self {
173 match node {
174 Node::Element(elem) => NodeDebug::Element(ElementDebug::new(elem, doc)),
175 Node::Text(text) => NodeDebug::Text(text),
176 Node::Comment(text) => NodeDebug::Comment(text),
177 Node::CData(text) => NodeDebug::CData(text),
178 Node::PI(text) => NodeDebug::PI(text),
179 Node::DocType(text) => NodeDebug::DocType(text),
180 }
181 }
182}
183
184impl Debug for NodeDebug<'_, '_> {
185 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
186 match self {
187 NodeDebug::Element(element) => element.fmt(f),
188 NodeDebug::Text(text_content) => Debug::fmt(text_content, f),
189 NodeDebug::Comment(f0) => f.debug_tuple("Comment").field(&f0).finish(),
190 NodeDebug::CData(f0) => f.debug_tuple("CData").field(&f0).finish(),
191 NodeDebug::PI(f0) => f.debug_tuple("PI").field(&f0).finish(),
192 NodeDebug::DocType(f0) => f.debug_tuple("DocType").field(&f0).finish(),
193 }
194 }
195}