musicxml 1.1.2

MusicXML parsing, manipulation, and export library
Documentation
use alloc::{string::String, vec::Vec};
use musicxml_internal::XmlElement;

enum TagType {
  Opening(XmlElement),
  Closing(XmlElement),
  SelfClosing(XmlElement),
  Ignored,
  Done,
}

fn read_tag_str(str: &mut core::str::Chars) -> TagType {
  let mut tag = XmlElement {
    name: String::new(),
    attributes: Vec::new(),
    elements: Vec::new(),
    text: String::new(),
  };
  let (mut is_closing, mut is_self_closing, mut in_attribute, mut in_string, mut in_tag, mut ignore) =
    (false, false, true, false, true, false);
  let (mut attribute, mut value) = (String::new(), String::new());
  for (i, c) in str.enumerate() {
    match c {
      '>' => {
        if !in_attribute {
          tag.attributes.push((attribute.clone(), value.clone()));
        }
        return if ignore {
          TagType::Ignored
        } else if is_closing {
          TagType::Closing(tag)
        } else if is_self_closing {
          TagType::SelfClosing(tag)
        } else {
          TagType::Opening(tag)
        };
      }
      '\r' | '\n' => (),
      '/' => {
        if in_string {
          value.push(c);
        } else if i == 0 {
          is_closing = true;
        } else {
          is_self_closing = true;
        }
      }
      '?' | '!' => {
        if i == 0 {
          ignore = true;
        }
      }
      ' ' => {
        if in_tag {
          in_tag = false;
        } else if in_string {
          value.push(c);
        } else {
          tag.attributes.push((attribute.clone(), value.clone()));
          in_attribute = true;
          attribute.clear();
          value.clear();
        }
      }
      '"' => in_string = !in_string,
      '=' => in_attribute = false,
      _ => {
        if in_tag {
          tag.name.push(c);
        } else if in_attribute {
          attribute.push(c);
        } else {
          value.push(c);
        }
      }
    }
  }
  TagType::Done
}

pub fn parse_to_string(xml: &XmlElement, depth: i16) -> String {
  let mut xml_str = String::new();
  if depth > 0 {
    xml_str += "\n";
  }
  for _ in 0..depth {
    xml_str += "  ";
  }
  xml_str += ["<", &xml.name].concat().as_str();
  for (key, value) in &xml.attributes {
    xml_str += [" ", key, "=\"", value, "\""].concat().as_str();
  }
  if xml.elements.is_empty() && xml.text.is_empty() {
    xml_str += "/>";
  } else {
    xml_str += ">";
    for element in &xml.elements {
      xml_str += parse_to_string(element, if depth >= 0 { depth + 1 } else { depth }).as_str();
    }
    if xml.text.is_empty() && depth >= 0 {
      xml_str += "\n";
      for _ in 0..depth {
        xml_str += "  ";
      }
    } else {
      xml_str += xml.text.as_str();
    }
    xml_str += ["</", &xml.name, ">"].concat().as_str();
  }
  xml_str
}

pub fn parse_from_string(str: &str) -> Result<XmlElement, String> {
  let mut it = str.chars();
  let mut open_tags: Vec<XmlElement> = Vec::new();
  while let Some(ch) = it.next() {
    if ch == '<' {
      match read_tag_str(&mut it) {
        TagType::Ignored => (),
        TagType::Opening(tag) => open_tags.push(tag),
        TagType::SelfClosing(tag) => match open_tags.last_mut() {
          Some(last_open_tag) => last_open_tag.elements.push(tag),
          None => return Err(String::from("Root tag cannot be self-closing")),
        },
        TagType::Closing(tag) => {
          if let Some(mut element) = open_tags.pop() {
            element.text.truncate(element.text.trim().len());
            if tag.name != element.name {
              return Err(format!(
                "Mismatched closing tag...expected '{}' but found '{}'",
                element.name, tag.name
              ));
            }
            if let Some(last_open_tag) = open_tags.last_mut() {
              last_open_tag.elements.push(element);
            } else {
              return Ok(element);
            }
          }
        }
        TagType::Done => break,
      }
    } else if ch != '\r' && ch != '\n' && ch != '\t' {
      if let Some(item) = open_tags.last_mut() {
        if !item.text.is_empty() || ch != ' ' {
          item.text.push(ch);
        }
      }
    }
  }
  Err(String::from("Missing one or more matched tags"))
}

#[cfg(test)]
mod xml_parser_tests {
  use super::*;

  #[test]
  fn serialize_valid_str() {
    let test_xml_str = "<outer2 attr=\"Test Attr\">
  <inner>Inner Test1</inner>
  <inner test=\"More Attr\">Inner Test2</inner>
  <inner2>
    <val1>123</val1>
    <val2>567</val2>
  </inner2>
  <self-close/>
</outer2>";
    let test_xml = XmlElement {
      name: String::from("outer2"),
      attributes: vec![(String::from("attr"), String::from("Test Attr"))],
      elements: vec![
        XmlElement {
          name: String::from("inner"),
          attributes: vec![],
          elements: vec![],
          text: String::from("Inner Test1"),
        },
        XmlElement {
          name: String::from("inner"),
          attributes: vec![(String::from("test"), String::from("More Attr"))],
          elements: vec![],
          text: String::from("Inner Test2"),
        },
        XmlElement {
          name: String::from("inner2"),
          attributes: vec![],
          elements: vec![
            XmlElement {
              name: String::from("val1"),
              attributes: vec![],
              elements: vec![],
              text: String::from("123"),
            },
            XmlElement {
              name: String::from("val2"),
              attributes: vec![],
              elements: vec![],
              text: String::from("567"),
            },
          ],
          text: String::new(),
        },
        XmlElement {
          name: String::from("self-close"),
          attributes: vec![],
          elements: vec![],
          text: String::new(),
        },
      ],
      text: String::new(),
    };
    let result = parse_to_string(&test_xml, 0);
    assert_eq!(result.as_str(), test_xml_str);
  }

  #[test]
  fn deserialize_valid_str() {
    let test_xml = "
    <outer2 attr=\"Test Attr\">
      <inner>Inner Test1</inner>
      <inner test=\"More Attr\">Inner Test2</inner>
      <inner2>
        <val1>123</val1>
        <val2>567</val2>
      </inner2>
    </outer2>";
    let result = parse_from_string(test_xml);
    assert!(result.is_ok());
    assert_eq!(
      result.unwrap(),
      XmlElement {
        name: String::from("outer2"),
        attributes: vec![(String::from("attr"), String::from("Test Attr"))],
        elements: vec![
          XmlElement {
            name: String::from("inner"),
            attributes: vec![],
            elements: vec![],
            text: String::from("Inner Test1")
          },
          XmlElement {
            name: String::from("inner"),
            attributes: vec![(String::from("test"), String::from("More Attr"))],
            elements: vec![],
            text: String::from("Inner Test2")
          },
          XmlElement {
            name: String::from("inner2"),
            attributes: vec![],
            elements: vec![
              XmlElement {
                name: String::from("val1"),
                attributes: vec![],
                elements: vec![],
                text: String::from("123")
              },
              XmlElement {
                name: String::from("val2"),
                attributes: vec![],
                elements: vec![],
                text: String::from("567")
              },
            ],
            text: String::new()
          },
        ],
        text: String::new()
      }
    );
  }

  #[test]
  fn serialize_valid_unicode_str() {
    let test_xml_str = "<element><test1>Waltz in E♭ Major</test1><test2>Frédéric François Chopin</test2></element>";
    let test_xml = XmlElement {
      name: String::from("element"),
      attributes: vec![],
      elements: vec![
        XmlElement {
          name: String::from("test1"),
          attributes: vec![],
          elements: vec![],
          text: String::from("Waltz in E♭ Major"),
        },
        XmlElement {
          name: String::from("test2"),
          attributes: vec![],
          elements: vec![],
          text: String::from("Frédéric François Chopin"),
        },
      ],
      text: String::new(),
    };
    let result = parse_to_string(&test_xml, -1);
    assert_eq!(result.as_str(), test_xml_str);
  }

  #[test]
  fn deserialize_valid_unicode_str() {
    let test_xml = "<element><test1>Waltz in E♭ Major</test1><test2>Frédéric François Chopin</test2></element>";
    let result = parse_from_string(test_xml);
    assert!(result.is_ok());
    assert_eq!(
      result.unwrap(),
      XmlElement {
        name: String::from("element"),
        attributes: vec![],
        elements: vec![
          XmlElement {
            name: String::from("test1"),
            attributes: vec![],
            elements: vec![],
            text: String::from("Waltz in E♭ Major")
          },
          XmlElement {
            name: String::from("test2"),
            attributes: vec![],
            elements: vec![],
            text: String::from("Frédéric François Chopin")
          },
        ],
        text: String::new()
      }
    );
  }
}