use crate::{Error, token::Lexer};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum Tag<'a> {
Tag {
name: &'a str,
attrs: HashMap<&'a str, &'a str>,
kind: TagKind,
},
Text(&'a str),
Declaration {
name: &'a str,
attrs: HashMap<&'a str, &'a str>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TagKind {
Opening,
Closing,
SelfClosing,
}
impl TagKind {
pub fn is_opening(self) -> bool {
self == TagKind::Opening
}
pub fn is_closing(self) -> bool {
self == TagKind::Closing
}
pub fn is_self_closing(self) -> bool {
self == TagKind::SelfClosing
}
}
impl<'a> Tag<'a> {
pub fn name(&self) -> Option<&str> {
if let Tag::Tag { name, .. } = *self {
Some(name)
} else {
None
}
}
pub fn attr(&self, key: &str) -> Option<&str> {
if let Tag::Tag { ref attrs, .. } = *self {
attrs.get(key).map(|s| s.as_ref())
} else {
None
}
}
pub fn content(&self) -> Option<&str> {
if let Tag::Text(text) = *self {
Some(text)
} else {
None
}
}
pub fn is_opening(&self) -> bool {
if let Tag::Tag { kind, .. } = self {
kind.is_opening()
} else {
false
}
}
pub fn is_closing(&self) -> bool {
if let Tag::Tag { kind, .. } = self {
kind.is_closing()
} else {
false
}
}
pub fn is_self_closing(&self) -> bool {
if let Tag::Tag { kind, .. } = self {
kind.is_self_closing()
} else {
false
}
}
pub fn is_text(&self) -> bool {
if let Tag::Text(..) = self {
true
} else {
false
}
}
}
#[derive(Debug, Clone)]
pub struct Tags<'a> {
pub(crate) lexer: Lexer<'a>,
pub(crate) diags: Vec<Error>,
peek: Option<<Self as Iterator>::Item>,
}
impl<'a> Tags<'a> {
pub fn new(text: &str) -> Tags {
Tags {
lexer: Lexer::new(text),
diags: Vec::new(),
peek: None,
}
}
pub fn peek(&mut self) -> Option<<Self as Iterator>::Item> {
if self.peek.is_none() {
self.peek = self.next();
}
self.peek.clone()
}
pub fn diags(&mut self) -> &[Error] {
&self.diags
}
pub fn report(&self) -> (usize, usize) {
self.lexer.report()
}
fn recover(&mut self, token: String) {
self.diags.push(Error::Syntax {
token,
span: self.lexer.report(),
});
eprintln!("recovered from an error: {}", self.diags.last().unwrap());
while let Some(token) = self.lexer.peek() {
match token {
(_, "open") | (_, "text") => break,
_ => {
self.lexer.next();
}
}
}
}
}
impl<'a> Iterator for Tags<'a> {
type Item = Tag<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.peek.is_some() {
return self.peek.take();
}
if self.lexer.peek()?.1 == "text" {
let text = self.lexer.next()?.0;
return Some(Tag::Text(text));
}
let open = match self.lexer.peek()? {
(open, "open") => open,
(t, _) => {
self.recover(t.to_owned());
return self.next();
}
};
self.lexer.next();
let name = match self.lexer.peek()? {
(name, "name") => name,
(t, _) => {
self.recover(t.to_owned());
return self.next();
}
};
self.lexer.next();
let mut attrs = HashMap::new();
loop {
let name = match self.lexer.peek()? {
(name, "name") => name,
(_, "close") => break,
(t, _) => {
self.recover(t.to_owned());
return self.next();
}
};
self.lexer.next();
let value = if let Some((_eq, "eq")) = self.lexer.peek() {
let _eq = self.lexer.next();
let value = match self.lexer.next()? {
(value, "value") => value,
(t, _) => {
self.recover(t.to_owned());
return self.next();
}
};
value
} else {
""
};
attrs.insert(name, value);
}
let close = match self.lexer.peek()? {
(close, "close") => close,
(t, _) => {
self.recover(t.to_owned());
return self.next();
}
};
self.lexer.next();
let kind = match (open, close) {
("</", _) => TagKind::Closing,
(_, "/>") => TagKind::SelfClosing,
_ => TagKind::Opening,
};
match (open, close) {
("<", ">") | ("<", "/>") | ("</", ">") => Some(Tag::Tag { name, attrs, kind }),
("<?", "?>") => Some(Tag::Declaration { name, attrs }),
_ => {
self.recover(close.to_owned());
return self.next();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple() {
let text = r#"<a b="c"><d /><e>"#;
let mut tags = Tags::new(text);
let a = tags.next().unwrap();
eprintln!("{a:?}");
assert!(matches!(a, Tag::Tag { name: "a", .. }));
assert!(a.attr("b") == Some("\"c\""));
assert!(matches!(
tags.next().unwrap(),
Tag::Tag {
name: "d",
kind: TagKind::SelfClosing,
..
}
));
assert!(matches!(tags.next().unwrap(), Tag::Tag { name: "e", .. }));
assert!(tags.diags().is_empty());
}
#[test]
fn recover() {
let text = r#"<a <b /><c />"#;
let mut tags = Tags::new(text);
assert!(matches!(tags.next(), Some(Tag::Tag { name: "b", .. })));
assert!(matches!(tags.next(), Some(Tag::Tag { name: "c", .. })));
assert!(!tags.diags().is_empty());
}
}