diff options
| author | Mica White <botahamec@outlook.com> | 2026-07-12 19:49:14 -0400 |
|---|---|---|
| committer | Mica White <botahamec@outlook.com> | 2026-07-12 19:49:14 -0400 |
| commit | 8399983fd45bc0f11eac9def280c0833ffb93c04 (patch) | |
| tree | f84173ca30a3c62db7abb92a0d869ff5d5199c51 /src/parse.rs | |
Diffstat (limited to 'src/parse.rs')
| -rw-r--r-- | src/parse.rs | 296 |
1 files changed, 296 insertions, 0 deletions
diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..b2e5116 --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,296 @@ +use std::iter::Peekable; + +use bumpalo::Bump; + +use crate::tokenize::{AttributeToken, AttributeTokenType, Token, TokenType}; + +#[derive(Debug, Clone, Copy)] +pub struct Document<'a> { + pub children: &'a [Node<'a>], +} + +#[derive(Debug, Clone, Copy)] +pub enum Node<'a> { + Element(Element<'a>), + CharacterData(Token<'a>), + Comment(Token<'a>), + StrayEndTag(Token<'a>), +} + +#[derive(Debug, Clone, Copy)] +pub struct Element<'a> { + pub start_tag: Token<'a>, + pub attributes: &'a [Attribute<'a>], + pub children: &'a [Node<'a>], + pub end_tag: Option<Token<'a>>, + pub end_tag_attributes: &'a [Attribute<'a>], +} + +#[derive(Debug, Clone, Copy)] +pub enum Attribute<'a> { + NameOnly { + name: AttributeToken<'a>, + }, + NamedArgument { + name: AttributeToken<'a>, + equal_sign: AttributeToken<'a>, + value: Option<AttributeToken<'a>>, + }, + PositionalArgument { + value: AttributeToken<'a>, + }, + StrayEqualSign(AttributeToken<'a>), + NamedArgumentWithTooManyEqualSigns { + name: AttributeToken<'a>, + equal_signs: &'a [AttributeToken<'a>], + value: Option<AttributeToken<'a>>, + }, +} + +impl<'a> Element<'a> { + pub fn is_self_closing(&self) -> bool { + matches!(self.start_tag.ty, TokenType::Tag { self_closing, .. } if self_closing) + } + + pub fn tag_name_lowercase(&self) -> Option<&str> { + let TokenType::Tag { + lowercase_tag_name, .. + } = self.start_tag.ty + else { + return None; + }; + Some(lowercase_tag_name) + } + + pub fn attribute(&self, name: &str) -> Option<&Attribute<'a>> { + self.attributes + .iter() + .find(|attribute| attribute.name_lowercase().is_some_and(|n| n == name)) + } +} + +impl<'a> Attribute<'a> { + pub fn name_token(&self) -> Option<&AttributeToken<'_>> { + match self { + Self::NamedArgument { name, .. } + | Attribute::NameOnly { name } + | Attribute::NamedArgumentWithTooManyEqualSigns { name, .. } => Some(name), + Self::PositionalArgument { .. } | Self::StrayEqualSign(_) => None, + } + } + + pub fn name_lowercase(&self) -> Option<&str> { + let name = self.name_token()?; + let AttributeTokenType::Identifier { lowercase_name, .. } = name.ty else { + return None; + }; + Some(lowercase_name) + } + + pub fn value_token(&self) -> Option<&AttributeToken<'a>> { + match self { + Self::NamedArgument { value, .. } + | Self::NamedArgumentWithTooManyEqualSigns { value, .. } => value.as_ref(), + Self::PositionalArgument { value } => Some(value), + Self::NameOnly { .. } | Self::StrayEqualSign(..) => None, + } + } + + pub fn value_str(&self) -> Option<&str> { + let value = self.value_token()?; + let AttributeTokenType::String { value, .. } = value.ty else { + return None; + }; + Some(value) + } +} + +fn parse_named_attribute<'a>( + attribute_tokens: &mut Peekable<impl Iterator<Item = &'a AttributeToken<'a>>>, + name: AttributeToken<'a>, + equal_sign: AttributeToken<'a>, + arena: &'a Bump, +) -> Attribute<'a> { + if let Some(equal_sign_2) = attribute_tokens + .next_if(|t| matches!(t.ty, AttributeTokenType::EqualSign)) + .copied() + { + let mut equal_signs = vec![equal_sign, equal_sign_2]; + while let Some(equal_sign) = attribute_tokens + .next_if(|t| matches!(t.ty, AttributeTokenType::EqualSign)) + .copied() + { + equal_signs.push(equal_sign); + } + let equal_signs = arena.alloc_slice_copy(&equal_signs); + + let value = attribute_tokens.next().copied(); + Attribute::NamedArgumentWithTooManyEqualSigns { + name, + equal_signs, + value, + } + } else { + let value = attribute_tokens.next().copied(); + Attribute::NamedArgument { + name, + equal_sign, + value, + } + } +} + +fn parse_attribute<'a>( + attribute_tokens: &mut Peekable<impl Iterator<Item = &'a AttributeToken<'a>>>, + arena: &'a Bump, +) -> Option<Attribute<'a>> { + let attribute_token = *attribute_tokens.next()?; + match attribute_token.ty { + AttributeTokenType::Identifier { .. } => { + let name = attribute_token; + if let Some(equal_sign) = attribute_tokens + .next_if(|t| matches!(t.ty, AttributeTokenType::EqualSign)) + .copied() + { + parse_named_attribute(attribute_tokens, name, equal_sign, arena) + } else { + Attribute::NameOnly { name } + } + } + AttributeTokenType::String { .. } => Attribute::PositionalArgument { + value: attribute_token, + }, + AttributeTokenType::EqualSign => Attribute::StrayEqualSign(attribute_token), + } + .into() +} + +fn parse_attributes<'a>( + attribute_tokens: &'a [AttributeToken<'a>], + arena: &'a Bump, +) -> &'a [Attribute<'a>] { + let mut attributes = Vec::new(); + let mut attribute_tokens = attribute_tokens.iter().peekable(); + while let Some(attribute) = parse_attribute(&mut attribute_tokens, arena) { + attributes.push(attribute); + } + arena.alloc_slice_copy(&attributes) +} + +fn parse_element<'a>( + tokens: &mut Peekable<impl Iterator<Item = &'a Token<'a>>>, + stack: &[&'a str], + arena: &'a Bump, +) -> Option<Node<'a>> { + let start_tag = *tokens.next()?; + let TokenType::Tag { + is_end, + lowercase_tag_name, + original_tag_name: _, + self_closing, + attribute_tokens, + greater_than_exists: _, + } = start_tag.ty + else { + return None; + }; + + if is_end { + return Some(Node::StrayEndTag(start_tag)); + } + + let attributes = parse_attributes(attribute_tokens, arena); + + if self_closing { + return Some(Node::Element(Element { + start_tag, + attributes, + children: &[], + end_tag: None, + end_tag_attributes: &[], + })); + } + + let mut children = Vec::new(); + let mut stack = Vec::from(stack); + stack.push(lowercase_tag_name); + + while let Some(token) = tokens.peek() { + match token.ty { + TokenType::Comment { .. } => { + let token = tokens.next().expect("a token"); + children.push(Node::Comment(*token)) + } + TokenType::Data(_) => { + let token = tokens.next().expect("a token"); + children.push(Node::CharacterData(*token)) + } + TokenType::Tag { + is_end, + lowercase_tag_name: end_tag_name, + attribute_tokens, + .. + } if is_end && end_tag_name == lowercase_tag_name => { + let token = tokens.next().expect("a token"); + let children = arena.alloc_slice_copy(&children); + let end_tag_attributes = parse_attributes(attribute_tokens, arena); + return Some(Node::Element(Element { + start_tag, + attributes, + children, + end_tag: Some(*token), + end_tag_attributes, + })); + } + TokenType::Tag { + is_end, + lowercase_tag_name, + .. + } if is_end && stack.contains(&lowercase_tag_name) => { + tokens.next().expect("a token"); + break; + } + TokenType::Tag { is_end, .. } if is_end => { + let token = tokens.next().expect("a token"); + children.push(Node::StrayEndTag(*token)) + } + TokenType::Tag { .. } => { + children.push(parse_element(tokens, &[], arena).expect("an element")); + } + } + } + + let children = arena.alloc_slice_copy(&children); + + Some(Node::Element(Element { + start_tag, + attributes, + children, + end_tag: None, + end_tag_attributes: &[], + })) +} + +pub fn parse_document<'a>(tokens: &'a [Token<'a>], arena: &'a Bump) -> Document<'a> { + let mut children = Vec::new(); + let mut tokens = tokens.iter().peekable(); + while let Some(token) = tokens.peek() { + match token.ty { + TokenType::Comment { .. } => { + let token = tokens.next().expect("we just saw a token from peeking"); + children.push(Node::Comment(*token)) + } + TokenType::Data(_) => { + let token = tokens.next().expect("we just saw a token from peeking"); + children.push(Node::CharacterData(*token)) + } + TokenType::Tag { .. } => { + children.push(parse_element(&mut tokens, &[], arena).expect("an element")); + } + } + } + + Document { + children: arena.alloc_slice_copy(&children), + } +} |
