magnesium/
attributes.rs

1use super::*;
2
3/// The output of a [`TagAttributeIterator`].
4///
5/// Attributes within an XML tag are key-value pairs. Only `Start` and `Empty`
6/// tags have attributes.
7///
8/// Each key is expected to only appear once in a given tag. The order of the
9/// keys is not usually significant.
10#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
11#[allow(missing_docs)]
12pub struct TagAttribute<'s> {
13  pub key: &'s str,
14  pub value: &'s str,
15}
16
17/// Iterator to walk through a `Start` or `Empty` tag's attribute string.
18///
19/// Supports both `'` and `"` quoting around the attribute values.
20///
21/// The parsing is a little simplistic, and if the iterator gets confused by bad
22/// input it will just end the iteration.
23#[derive(Debug, Clone, Default)]
24pub struct TagAttributeIterator<'s> {
25  attrs: &'s str,
26}
27impl<'s> TagAttributeIterator<'s> {
28  /// Makes a new iterator over the attribute string.
29  #[inline]
30  #[must_use]
31  pub fn new(attrs: &'s str) -> Self {
32    Self { attrs: attrs.trim() }
33  }
34
35  /// Gets the `value` of the `key` given, if the key is present.
36  ///
37  /// ```rust
38  /// # use magnesium::TagAttributeIterator;
39  /// let attrs = r#"namespace="Graphics" group="Polygon""#;
40  /// let iter = TagAttributeIterator::new(attrs);
41  /// assert_eq!(iter.find_by_key("namespace"), Some("Graphics"));
42  /// assert_eq!(iter.find_by_key("ferris"), None);
43  /// assert_eq!(iter.find_by_key("group"), Some("Polygon"));
44  /// ```
45  #[inline]
46  #[must_use]
47  pub fn find_by_key(&self, key: &str) -> Option<&'s str> {
48    self.clone().find(|ta| ta.key == key).map(|ta| ta.value)
49  }
50}
51impl<'s> Iterator for TagAttributeIterator<'s> {
52  type Item = TagAttribute<'s>;
53
54  #[inline]
55  #[must_use]
56  fn next(&mut self) -> Option<Self::Item> {
57    debug_assert_eq!(self.attrs, self.attrs.trim());
58    if self.attrs.is_empty() {
59      return None;
60    }
61    #[allow(clippy::never_loop)]
62    'clear_and_return_none: loop {
63      // break on `=`
64      let (key, rest) = match break_on_first_char(self.attrs, '=') {
65        Some((key, rest)) => (key, rest),
66        None => break 'clear_and_return_none,
67      };
68      self.attrs = rest;
69      // support both `"` and `'` since it's easy to do
70      let quote_marker = match self.attrs.chars().next() {
71        Some(q) if q == '\'' || q == '\"' => {
72          self.attrs = &self.attrs[1..];
73          q
74        }
75        _ => break 'clear_and_return_none,
76      };
77      // break on the end of the quote
78      let (value, rest) = match break_on_first_char(self.attrs, quote_marker) {
79        Some((key, rest)) => (key, rest),
80        None => break 'clear_and_return_none,
81      };
82      self.attrs = rest.trim_start();
83      return Some(TagAttribute { key, value });
84    }
85    self.attrs = "";
86    None
87  }
88}
89impl<'s> core::iter::FusedIterator for TagAttributeIterator<'s> {}