1#![warn(missing_docs, clippy::pedantic)]
46
47use std::collections::BTreeMap;
48use std::fmt;
49
50#[derive(Debug, Clone, PartialEq, Eq, Default)]
57pub struct Element {
58 pub name: String,
60 pub attributes: BTreeMap<String, String>,
62 pub text: String,
64 pub children: Vec<Element>,
66}
67
68impl Element {
69 #[must_use]
71 pub fn local_name(&self) -> &str {
72 local_name(&self.name)
73 }
74
75 #[must_use]
77 pub fn attribute(&self, name: &str) -> Option<&str> {
78 self.attributes
79 .iter()
80 .find(|(key, _)| local_name(key) == name)
81 .map(|(_, value)| value.as_str())
82 }
83
84 #[must_use]
91 pub fn text_opt(&self) -> Option<&str> {
92 Some(self.text.as_str()).filter(|text| !text.is_empty())
93 }
94
95 #[must_use]
97 pub fn child<'a>(&'a self, name: &str) -> Option<&'a Element> {
98 self.children
99 .iter()
100 .find(|child| child.local_name() == name)
101 }
102
103 pub fn children_named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a Element> + 'a {
105 self.children
106 .iter()
107 .filter(move |child| child.local_name() == name)
108 }
109
110 #[must_use]
113 pub fn find<'a>(&'a self, name: &str) -> Option<&'a Element> {
114 if self.local_name() == name {
115 return Some(self);
116 }
117 self.children.iter().find_map(|child| child.find(name))
118 }
119
120 #[must_use]
135 pub fn text_at<'a>(&'a self, path: &[&str]) -> Option<&'a str> {
136 let mut level: Vec<&Element> = vec![self];
137 for step in path {
138 let mut next: Vec<&Element> = Vec::new();
139 for element in level {
140 next.extend(
141 element
142 .children
143 .iter()
144 .filter(|child| child.local_name() == *step),
145 );
146 }
147 if next.is_empty() {
148 return None;
149 }
150 level = next;
151 }
152 level
153 .into_iter()
154 .map(|element| element.text.trim())
155 .find(|text| !text.is_empty())
156 }
157}
158
159#[must_use]
161pub fn local_name(name: &str) -> &str {
162 match name.split_once(':') {
163 Some((_, local)) => local,
164 None => name,
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170pub enum Error {
171 NoRootElement,
173 Unclosed(String),
175 Mismatched {
177 open: String,
179 close: String,
181 },
182 Malformed(String, usize),
184}
185
186impl fmt::Display for Error {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 match self {
189 Error::NoRootElement => write!(f, "no root element found"),
190 Error::Unclosed(name) => write!(f, "element <{name}> is never closed"),
191 Error::Mismatched { open, close } => write!(f, "<{open}> is closed by </{close}>"),
192 Error::Malformed(reason, at) => write!(f, "{reason} at byte {at}"),
193 }
194 }
195}
196
197impl std::error::Error for Error {}
198
199pub fn parse(xml: &str) -> Result<Element, Error> {
212 let mut cursor = Cursor::new(xml.strip_prefix('\u{feff}').unwrap_or(xml));
213 cursor.skip_prolog()?;
214 cursor.skip_whitespace();
215 if cursor.at_end() {
216 return Err(Error::NoRootElement);
217 }
218 cursor.parse_element()
219}
220
221#[must_use]
226pub fn escape(text: &str) -> String {
227 let mut out = String::with_capacity(text.len());
228 for c in text.chars() {
229 match c {
230 '&' => out.push_str("&"),
231 '<' => out.push_str("<"),
232 '>' => out.push_str(">"),
233 '"' => out.push_str("""),
234 '\'' => out.push_str("'"),
235 c => out.push(c),
236 }
237 }
238 out
239}
240
241const MAX_DEPTH: usize = 256;
250
251struct Cursor<'a> {
252 text: &'a str,
253 position: usize,
254 depth: usize,
255}
256
257impl<'a> Cursor<'a> {
258 fn new(text: &'a str) -> Cursor<'a> {
259 Cursor {
260 text,
261 position: 0,
262 depth: 0,
263 }
264 }
265
266 fn rest(&self) -> &'a str {
267 &self.text[self.position..]
268 }
269
270 fn at_end(&self) -> bool {
271 self.position >= self.text.len()
272 }
273
274 fn starts_with(&self, pattern: &str) -> bool {
275 self.rest().starts_with(pattern)
276 }
277
278 fn advance(&mut self, bytes: usize) {
279 self.position += bytes;
280 }
281
282 fn skip_whitespace(&mut self) {
283 let trimmed = self.rest().trim_start();
284 self.position = self.text.len() - trimmed.len();
285 }
286
287 fn skip_prolog(&mut self) -> Result<(), Error> {
288 loop {
289 self.skip_whitespace();
290 if self.starts_with("<?") {
291 self.skip_through("?>")?;
292 } else if self.starts_with("<!--") {
293 self.skip_through("-->")?;
294 } else if self.starts_with("<!") {
295 self.skip_through(">")?;
296 } else {
297 return Ok(());
298 }
299 }
300 }
301
302 fn skip_through(&mut self, end: &str) -> Result<(), Error> {
303 match self.rest().find(end) {
304 Some(index) => {
305 self.advance(index + end.len());
306 Ok(())
307 }
308 None => Err(Error::Malformed(
309 format!("unterminated {end:?}"),
310 self.position,
311 )),
312 }
313 }
314
315 fn parse_element(&mut self) -> Result<Element, Error> {
316 if !self.starts_with("<") {
317 return Err(Error::Malformed("expected '<'".into(), self.position));
318 }
319 if self.depth >= MAX_DEPTH {
320 return Err(Error::Malformed(
321 format!("elements nested more than {MAX_DEPTH} deep"),
322 self.position,
323 ));
324 }
325 self.advance(1);
326 let name = self.read_name()?;
327 let (attributes, self_closing) = self.read_attributes()?;
328 if self_closing {
329 return Ok(Element {
330 name,
331 attributes,
332 text: String::new(),
333 children: Vec::new(),
334 });
335 }
336 let (text, children) = self.parse_content(&name)?;
337 Ok(Element {
338 name,
339 attributes,
340 text,
341 children,
342 })
343 }
344
345 fn read_name(&mut self) -> Result<String, Error> {
347 let end = self
348 .rest()
349 .find(|c: char| c.is_whitespace() || c == '/' || c == '>')
350 .ok_or_else(|| Error::Malformed("unterminated tag".into(), self.position))?;
351 if end == 0 {
352 return Err(Error::Malformed("empty element name".into(), self.position));
353 }
354 let name = self.rest()[..end].to_string();
355 self.advance(end);
356 Ok(name)
357 }
358
359 fn read_attributes(&mut self) -> Result<(BTreeMap<String, String>, bool), Error> {
362 let mut attributes = BTreeMap::new();
363 loop {
364 self.skip_whitespace();
365 if self.starts_with("/>") {
366 self.advance(2);
367 return Ok((attributes, true));
368 }
369 if self.starts_with(">") {
370 self.advance(1);
371 return Ok((attributes, false));
372 }
373 if self.at_end() {
374 return Err(Error::Malformed("unterminated tag".into(), self.position));
375 }
376 let name_end = self
377 .rest()
378 .find(|c: char| c.is_whitespace() || c == '=')
379 .ok_or_else(|| Error::Malformed("malformed attribute".into(), self.position))?;
380 let name = self.rest()[..name_end].to_string();
381 self.advance(name_end);
382 self.skip_whitespace();
383 if !self.starts_with("=") {
384 return Err(Error::Malformed(
385 "attribute without a value".into(),
386 self.position,
387 ));
388 }
389 self.advance(1);
390 self.skip_whitespace();
391 let quote = self
392 .rest()
393 .chars()
394 .next()
395 .filter(|&c| c == '"' || c == '\'')
396 .ok_or_else(|| {
397 Error::Malformed("unquoted attribute value".into(), self.position)
398 })?;
399 self.advance(1);
400 let close = self
401 .rest()
402 .find(quote)
403 .ok_or_else(|| Error::Malformed("unterminated attribute".into(), self.position))?;
404 let value = decode(&self.rest()[..close]);
405 self.advance(close + 1);
406 attributes.insert(name, value);
407 }
408 }
409
410 fn parse_content(&mut self, open_name: &str) -> Result<(String, Vec<Element>), Error> {
411 let mut text = String::new();
412 let mut children = Vec::new();
413 loop {
414 let next = self
415 .rest()
416 .find('<')
417 .ok_or_else(|| Error::Unclosed(open_name.to_string()))?;
418 if next > 0 {
419 text.push_str(&decode(&self.rest()[..next]));
420 self.advance(next);
421 }
422 if self.starts_with("</") {
423 self.advance(2);
424 let close_name = self.read_name()?;
425 self.skip_whitespace();
426 if !self.starts_with(">") {
427 return Err(Error::Malformed(
428 "unterminated close tag".into(),
429 self.position,
430 ));
431 }
432 self.advance(1);
433 if close_name != open_name {
434 return Err(Error::Mismatched {
435 open: open_name.to_string(),
436 close: close_name,
437 });
438 }
439 if !children.is_empty() && text.trim().is_empty() {
441 text.clear();
442 }
443 return Ok((text, children));
444 }
445 if self.starts_with("<!--") {
446 self.skip_through("-->")?;
447 continue;
448 }
449 if self.starts_with("<?") {
450 self.skip_through("?>")?;
451 continue;
452 }
453 if self.starts_with("<![CDATA[") {
454 self.advance("<![CDATA[".len());
455 let end = self
456 .rest()
457 .find("]]>")
458 .ok_or_else(|| Error::Malformed("unterminated CDATA".into(), self.position))?;
459 text.push_str(&self.rest()[..end]);
460 self.advance(end + "]]>".len());
461 continue;
462 }
463 self.depth += 1;
464 let child = self.parse_element();
465 self.depth -= 1;
466 children.push(child?);
467 }
468 }
469}
470
471fn decode(text: &str) -> String {
474 if !text.contains('&') {
475 return text.to_string();
476 }
477 let mut out = String::with_capacity(text.len());
478 let mut rest = text;
479 while let Some(index) = rest.find('&') {
480 out.push_str(&rest[..index]);
481 rest = &rest[index..];
482 let Some(end) = rest.find(';') else {
483 out.push_str(rest);
484 return out;
485 };
486 let entity = &rest[1..end];
487 let decoded = match entity {
488 "amp" => Some('&'),
489 "lt" => Some('<'),
490 "gt" => Some('>'),
491 "quot" => Some('"'),
492 "apos" => Some('\''),
493 _ => entity
494 .strip_prefix('#')
495 .and_then(|number| match number.strip_prefix(['x', 'X']) {
496 Some(hex) => u32::from_str_radix(hex, 16).ok(),
497 None => number.parse().ok(),
498 })
499 .and_then(char::from_u32),
500 };
501 match decoded {
502 Some(c) => out.push(c),
503 None => out.push_str(&rest[..=end]),
504 }
505 rest = &rest[end + 1..];
506 }
507 out.push_str(rest);
508 out
509}