diff --git a/crates/state/src/cursor.rs b/crates/state/src/cursor.rs index f629507a0..eec527220 100644 --- a/crates/state/src/cursor.rs +++ b/crates/state/src/cursor.rs @@ -1,6 +1,9 @@ -use std::sync::{ - Arc, - Mutex, +use std::{ + num::ParseIntError, + sync::{ + Arc, + Mutex, + }, }; use freya_common::{ @@ -72,7 +75,11 @@ impl ParseAttribute for CursorState { AttributeName::CursorIndex => { if let Some(value) = attr.value.as_text() { if value != "none" { - self.position = Some(value.parse().map_err(|_| ParseError)?); + self.position = Some( + value + .parse() + .map_err(|err: ParseIntError| ParseError(err.to_string()))?, + ); } } } @@ -88,7 +95,11 @@ impl ParseAttribute for CursorState { } AttributeName::CursorId => { if let Some(value) = attr.value.as_text() { - self.cursor_id = Some(value.parse().map_err(|_| ParseError)?); + self.cursor_id = Some( + value + .parse() + .map_err(|err: ParseIntError| ParseError(err.to_string()))?, + ); } } AttributeName::Highlights => { diff --git a/crates/state/src/font_style.rs b/crates/state/src/font_style.rs index 18c033488..3807cff76 100644 --- a/crates/state/src/font_style.rs +++ b/crates/state/src/font_style.rs @@ -1,6 +1,12 @@ -use std::sync::{ - Arc, - Mutex, +use std::{ + num::{ + ParseFloatError, + ParseIntError, + }, + sync::{ + Arc, + Mutex, + }, }; use freya_common::CompositorDirtyNodes; @@ -23,11 +29,13 @@ use torin::torin::Torin; use crate::{ CustomAttributeValues, - ExtSplit, Parse, ParseAttribute, + ParseError, TextHeight, + TextHeightBehavior, TextOverflow, + Token, }; #[derive(Debug, Clone, PartialEq, Component)] @@ -123,7 +131,7 @@ impl ParseAttribute for FontStyleState { fn parse_attribute( &mut self, attr: freya_native_core::prelude::OwnedAttributeView, - ) -> Result<(), crate::ParseError> { + ) -> Result<(), ParseError> { match attr.attribute { AttributeName::Color => { if let Some(value) = attr.value.as_text() { @@ -136,15 +144,13 @@ impl ParseAttribute for FontStyleState { } AttributeName::TextShadow => { if let Some(value) = attr.value.as_text() { - self.text_shadows = value - .split_excluding_group(',', '(', ')') - .map(|chunk| TextShadow::parse(chunk).unwrap_or_default()) - .collect(); + self.text_shadows = TextShadow::parse_with_separator(value, &Token::Comma)?; } } AttributeName::FontFamily => { if let Some(value) = attr.value.as_text() { let families = value.split(','); + self.font_family = families .into_iter() .map(|f| f.trim().to_string()) @@ -167,84 +173,67 @@ impl ParseAttribute for FontStyleState { } AttributeName::TextAlign => { if let Some(value) = attr.value.as_text() { - if let Ok(text_align) = TextAlign::parse(value) { - self.text_align = text_align; - } + self.text_align = TextAlign::parse(value)?; } } AttributeName::MaxLines => { if let Some(value) = attr.value.as_text() { - if let Ok(max_lines) = value.parse() { - self.max_lines = Some(max_lines); - } + self.max_lines = Some( + value + .parse() + .map_err(|err: ParseIntError| ParseError(err.to_string()))?, + ); } } AttributeName::TextOverflow => { - let value = attr.value.as_text(); - if let Some(value) = value { - if let Ok(text_overflow) = TextOverflow::parse(value) { - self.text_overflow = text_overflow; - } + if let Some(value) = attr.value.as_text() { + self.text_overflow = TextOverflow::parse(value)?; } } AttributeName::FontStyle => { if let Some(value) = attr.value.as_text() { - if let Ok(font_slant) = Slant::parse(value) { - self.font_slant = font_slant; - } + self.font_slant = Slant::parse(value)?; } } AttributeName::FontWeight => { if let Some(value) = attr.value.as_text() { - if let Ok(font_weight) = Weight::parse(value) { - self.font_weight = font_weight; - } + self.font_weight = Weight::parse(value)?; } } AttributeName::FontWidth => { if let Some(value) = attr.value.as_text() { - if let Ok(font_width) = Width::parse(value) { - self.font_width = font_width; - } + self.font_width = Width::parse(value)?; } } AttributeName::Decoration => { if let Some(value) = attr.value.as_text() { - if let Ok(decoration) = TextDecoration::parse(value) { - self.decoration.ty = decoration; - } + self.decoration.ty = TextDecoration::parse(value)?; } } AttributeName::DecorationStyle => { if let Some(value) = attr.value.as_text() { - if let Ok(style) = TextDecorationStyle::parse(value) { - self.decoration.style = style; - } + self.decoration.style = TextDecorationStyle::parse(value)?; } } AttributeName::DecorationColor => { if let Some(value) = attr.value.as_text() { - if let Ok(new_decoration_color) = Color::parse(value) { - self.decoration.color = new_decoration_color; - } + self.decoration.color = Color::parse(value)?; } else { self.decoration.color = self.color; } } AttributeName::WordSpacing => { - let value = attr.value.as_text(); - if let Some(value) = value { - if let Ok(word_spacing) = value.parse() { - self.word_spacing = word_spacing; - } + if let Some(value) = attr.value.as_text() { + self.word_spacing = value + .parse() + .map_err(|err: ParseFloatError| ParseError(err.to_string()))?; } } AttributeName::LetterSpacing => { - let value = attr.value.as_text(); - if let Some(value) = value { - if let Ok(letter_spacing) = value.parse() { - self.letter_spacing = letter_spacing; - } + if let Some(value) = attr.value.as_text() { + self.letter_spacing = value + .parse() + .map_err(|err: ParseFloatError| ParseError(err.to_string()))?; } } AttributeName::TextHeight => { diff --git a/crates/state/src/layer.rs b/crates/state/src/layer.rs index 24bed3c99..4f52ab58d 100644 --- a/crates/state/src/layer.rs +++ b/crates/state/src/layer.rs @@ -40,7 +40,9 @@ impl ParseAttribute for LayerState { match attr.attribute { AttributeName::Layer => { if let Some(value) = attr.value.as_text() { - let layer = value.parse::().map_err(|_| ParseError)?; + let layer = value + .parse::() + .map_err(|err| ParseError(err.to_string()))?; self.layer -= layer; self.layer_for_children += layer; } diff --git a/crates/state/src/layout.rs b/crates/state/src/layout.rs index 753123534..34307c624 100644 --- a/crates/state/src/layout.rs +++ b/crates/state/src/layout.rs @@ -102,18 +102,32 @@ impl ParseAttribute for LayoutState { if let Some(value) = attr.value.as_text() { self.direction = match value { "horizontal" => DirectionMode::Horizontal, - _ => DirectionMode::Vertical, + "vertical" => DirectionMode::Vertical, + value => { + return Err(ParseError::invalid_ident( + value, + &["horizontal", "vertical"], + )) + } } } } AttributeName::OffsetY => { if let Some(value) = attr.value.as_text() { - self.offset_y = Length::new(value.parse::().map_err(|_| ParseError)?); + self.offset_y = Length::new( + value + .parse::() + .map_err(|err| ParseError(err.to_string()))?, + ); } } AttributeName::OffsetX => { if let Some(value) = attr.value.as_text() { - self.offset_x = Length::new(value.parse::().map_err(|_| ParseError)?); + self.offset_x = Length::new( + value + .parse::() + .map_err(|err| ParseError(err.to_string()))?, + ); } } AttributeName::MainAlign => { @@ -135,26 +149,38 @@ impl ParseAttribute for LayoutState { } AttributeName::PositionTop => { if let Some(value) = attr.value.as_text() { - self.position - .set_top(value.parse::().map_err(|_| ParseError)?); + self.position.set_top( + value + .parse::() + .map_err(|err| ParseError(err.to_string()))?, + ); } } AttributeName::PositionRight => { if let Some(value) = attr.value.as_text() { - self.position - .set_right(value.parse::().map_err(|_| ParseError)?); + self.position.set_right( + value + .parse::() + .map_err(|err| ParseError(err.to_string()))?, + ); } } AttributeName::PositionBottom => { if let Some(value) = attr.value.as_text() { - self.position - .set_bottom(value.parse::().map_err(|_| ParseError)?); + self.position.set_bottom( + value + .parse::() + .map_err(|err| ParseError(err.to_string()))?, + ); } } AttributeName::PositionLeft => { if let Some(value) = attr.value.as_text() { - self.position - .set_left(value.parse::().map_err(|_| ParseError)?); + self.position.set_left( + value + .parse::() + .map_err(|err| ParseError(err.to_string()))?, + ); } } AttributeName::Content => { @@ -171,7 +197,11 @@ impl ParseAttribute for LayoutState { } AttributeName::Spacing => { if let Some(value) = attr.value.as_text() { - self.spacing = Length::new(value.parse::().map_err(|_| ParseError)?); + self.spacing = Length::new( + value + .parse::() + .map_err(|err| ParseError(err.to_string()))?, + ); } } _ => {} diff --git a/crates/state/src/lexing.rs b/crates/state/src/lexing.rs new file mode 100644 index 000000000..8ee788c66 --- /dev/null +++ b/crates/state/src/lexing.rs @@ -0,0 +1,213 @@ +use std::{ + fmt, + iter, +}; + +#[derive(Debug, PartialEq, Clone)] +pub enum Token { + Ident(String), + Float(f32), + Integer(i64), + ParenOpen, + ParenClose, + Minus, + Plus, + Slash, + Star, + Pound, + Percent, + Comma, +} + +impl Token { + pub fn ident>(value: T) -> Self { + Self::Ident(value.into()) + } + + pub fn is_ident(&self) -> bool { + matches!(self, Token::Ident(_)) + } + + pub fn is_f32(&self) -> bool { + matches!(self, Token::Float(_)) + } + + pub fn is_i64(&self) -> bool { + matches!(self, Token::Integer(_)) + } + + pub fn is_i64_or_f32(&self) -> bool { + matches!(self, Token::Integer(_) | Token::Float(_)) + } + + pub fn into_string(self) -> String { + if let Token::Ident(value) = self { + value + } else { + unreachable!() + } + } + + pub fn into_f32(self) -> f32 { + if let Token::Float(value) = self { + value + } else if let Token::Integer(value) = self { + value as f32 + } else { + unreachable!() + } + } + + pub fn into_i64(self) -> i64 { + if let Token::Integer(value) = self { + value + } else { + unreachable!() + } + } + + pub fn as_str(&self) -> &str { + if let Token::Ident(value) = self { + value.as_str() + } else { + unreachable!() + } + } + + pub fn try_as_str(&self) -> Option<&str> { + if let Token::Ident(value) = self { + Some(value.as_str()) + } else { + None + } + } + + pub fn try_as_f32(&self) -> Option { + if let Token::Float(value) = self { + Some(*value) + } else if let Token::Integer(value) = self { + Some(*value as f32) + } else { + None + } + } + + pub fn try_as_i64(&self) -> Option { + if let Token::Integer(value) = self { + Some(*value) + } else { + None + } + } + + pub fn try_as_u8(&self) -> Option { + if let Token::Integer(value) = self { + u8::try_from(*value).ok() + } else { + None + } + } +} + +impl fmt::Display for Token { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Token::Ident(value) => f.write_str(value), + Token::Float(value) => write!(f, "float {value}"), + Token::Integer(value) => write!(f, "integer {value}"), + Token::ParenOpen => write!(f, "\"(\""), + Token::ParenClose => write!(f, "\")\""), + Token::Minus => write!(f, "\"-\""), + Token::Plus => write!(f, "\"+\""), + Token::Slash => write!(f, "\"/\""), + Token::Star => write!(f, "\"*\""), + Token::Pound => write!(f, "\"#\""), + Token::Percent => write!(f, "\"%\""), + Token::Comma => write!(f, "\",\""), + } + } +} + +pub struct Lexer; + +impl Lexer { + pub fn parse>(data: T) -> Vec { + let mut tokens = vec![]; + let mut chars = data.as_ref().chars().peekable(); + + while let Some(character) = chars.next() { + match character { + ' ' => continue, + 'A'..='z' => { + tokens.push(Token::Ident( + iter::once(character) + .chain(iter::from_fn(|| { + chars + .by_ref() + .next_if(|s| s.is_ascii_alphanumeric() || s == &'-') + })) + .collect::() + .parse() + .unwrap(), + )); + } + '0'..='9' => { + let value = iter::once(character) + .chain(iter::from_fn(|| { + chars.by_ref().next_if(|s| s.is_ascii_digit() || s == &'.') + })) + .collect::(); + + if value.contains('.') { + tokens.push(Token::Float(value.parse().unwrap())); + } else { + tokens.push(Token::Integer(value.parse().unwrap())); + } + } + '(' => tokens.push(Token::ParenOpen), + ')' => tokens.push(Token::ParenClose), + '+' => tokens.push(Token::Plus), + '-' => { + if chars.peek().is_some_and(char::is_ascii_digit) { + let value = iter::once(character) + .chain(iter::from_fn(|| { + chars.by_ref().next_if(|s| s.is_ascii_digit() || s == &'.') + })) + .collect::(); + + if value.contains('.') { + tokens.push(Token::Float(value.parse().unwrap())); + } else { + tokens.push(Token::Integer(value.parse().unwrap())); + } + } else { + tokens.push(Token::Minus); + } + } + '*' => tokens.push(Token::Star), + '/' => tokens.push(Token::Slash), + '#' => { + tokens.push(Token::Pound); + + if chars.peek().is_some_and(char::is_ascii_alphanumeric) { + tokens.push(Token::Ident( + iter::from_fn(|| chars.by_ref().next_if(char::is_ascii_alphanumeric)) + .collect::(), + )); + } + } + '%' => tokens.push(Token::Percent), + ',' => tokens.push(Token::Comma), + character => { + if let Some(Token::Ident(data)) = tokens.last_mut() { + data.push(character); + } else { + tokens.push(Token::Ident(character.to_string())); + } + } + } + } + + tokens + } +} diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 4fae5a39a..1e713a84a 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -4,6 +4,7 @@ mod custom_attributes; mod font_style; mod layer; mod layout; +mod lexing; mod parsing; mod references; mod style; @@ -17,6 +18,7 @@ pub use custom_attributes::*; pub use font_style::*; pub use layer::*; pub use layout::*; +pub use lexing::*; pub use parsing::*; pub use references::*; pub use style::*; diff --git a/crates/state/src/parsing.rs b/crates/state/src/parsing.rs index 20c022ac5..1c5ae02ba 100644 --- a/crates/state/src/parsing.rs +++ b/crates/state/src/parsing.rs @@ -1,183 +1,246 @@ -use std::str::CharIndices; +use std::{ + iter::Peekable, + ops::{ + Bound, + RangeBounds, + }, + vec::IntoIter, +}; use freya_native_core::prelude::OwnedAttributeView; -use crate::CustomAttributeValues; +use crate::{ + CustomAttributeValues, + Lexer, + Token, +}; #[derive(Clone, Debug, PartialEq)] -pub struct ParseError; +pub struct ParseError(pub String); + +impl ParseError { + pub fn expected_token(expected: &Token, found: Option<&Token>) -> Self { + if let Some(found) = found { + Self(format!("expected {expected}, found {found}")) + } else { + Self(format!("expected {expected}, found nothing")) + } + } -// FromStr but we own it so we can impl it on torin and skia_safe types. -pub trait Parse: Sized { - fn parse(value: &str) -> Result; -} + pub fn unexpected_token(value: Option<&Token>) -> Self { + if let Some(value) = value { + Self(format!("unexpected {value}")) + } else { + Self("unexpected nothing".into()) + } + } -pub trait ParseAttribute: Sized { - fn parse_attribute( - &mut self, - attr: OwnedAttributeView, - ) -> Result<(), ParseError>; + pub fn invalid_ident(value: &str, expected: &[&str]) -> Self { + let expected = match expected.len() { + 0 => "nothing".into(), + 1 => expected[0].into(), + size => { + let other = expected[0..(size - 1)].join(", "); + let last = expected[size - 1]; - fn parse_safe(&mut self, attr: OwnedAttributeView) { - #[cfg(debug_assertions)] - { - let error_attr = attr.clone(); - if self.parse_attribute(attr).is_err() { - panic!( - "Failed to parse attribute '{:?}' with value '{:?}'", - error_attr.attribute, error_attr.value - ); + format!("{other} or {last}") } - } + }; - #[cfg(not(debug_assertions))] - self.parse_attribute(attr).ok(); + Self(format!("invalid ident {value} (expected {expected})")) + } + + pub fn too_much_tokens(count: usize) -> Self { + Self(format!( + "found more than zero ({count}) tokens after parsing" + )) } } -pub trait ExtSplit { - fn split_excluding_group( - &self, - delimiter: char, - group_start: char, - group_end: char, - ) -> SplitExcludingGroup<'_>; - fn split_ascii_whitespace_excluding_group( - &self, - group_start: char, - group_end: char, - ) -> SplitAsciiWhitespaceExcludingGroup<'_>; +pub struct Parser { + pub(crate) tokens: Peekable>, } -impl ExtSplit for str { - fn split_excluding_group( - &self, - delimiter: char, - group_start: char, - group_end: char, - ) -> SplitExcludingGroup<'_> { - SplitExcludingGroup { - text: self, - chars: self.char_indices(), - delimiter, - group_start, - group_end, - trailing_empty: true, +impl Parser { + pub fn new(tokens: Vec) -> Self { + Self { + tokens: tokens.into_iter().peekable(), } } - fn split_ascii_whitespace_excluding_group( - &self, - group_start: char, - group_end: char, - ) -> SplitAsciiWhitespaceExcludingGroup<'_> { - SplitAsciiWhitespaceExcludingGroup { - text: self, - chars: self.char_indices(), - group_start, - group_end, + /// Consumes the current token only if it exists and is equal to `value`. + pub fn try_consume(&mut self, value: &Token) -> bool { + if self.peek().is_some_and(|v| v == value) { + self.next(); + + true + } else { + false } } -} -#[derive(Clone, Debug)] -pub struct SplitExcludingGroup<'a> { - pub text: &'a str, - pub chars: CharIndices<'a>, - pub delimiter: char, - pub group_start: char, - pub group_end: char, - pub trailing_empty: bool, + /// Checks if the next token exists and it is equal to `value`. + pub fn check(&mut self, value: &Token) -> bool { + self.peek().is_some_and(|v| v == value) + } + + /// Returns the `bool` result of `func` if the next token exists. + pub fn check_if bool>(&mut self, func: F) -> bool { + self.peek().is_some_and(func) + } + + /// Consumes the current token if it exists and is equal to `value`, otherwise returning `ParseError`. + pub fn consume(&mut self, value: &Token) -> Result { + if self.check(value) { + Ok(self.next().unwrap()) + } else { + Err(ParseError::expected_token(value, self.peek())) + } + } + + /// Consumes the current token if it exists and the result of `func` is `true`, otherwise returning `ParseError`. + pub fn consume_if bool>(&mut self, func: F) -> Result { + if self.check_if(func) { + Ok(self.next().unwrap()) + } else { + Err(ParseError::unexpected_token(self.peek())) + } + } + + /// Consumes the current token if it exists and the result of the `func` is `Some(T)`, otherwise returning `ParseError`. + pub fn consume_map Option>(&mut self, func: F) -> Result { + if let Some(value) = self.peek().and_then(func) { + self.next(); + + Ok(value) + } else { + Err(ParseError::unexpected_token(self.peek())) + } + } + + /// Consumes the current token and returns it wrapped in `Some` if it exists, otherwise returning `None`. + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self) -> Option { + self.tokens.next() + } + + /// Peeks the current token and returns a reference to it wrapped in `Some` if it exists, otherwise returning `None`. + pub fn peek(&mut self) -> Option<&Token> { + self.tokens.peek() + } } -impl<'a> Iterator for SplitExcludingGroup<'a> { - type Item = &'a str; +// FromStr but we own it so we can impl it on torin and skia_safe types. +pub trait Parse: Sized { + fn from_parser(parser: &mut Parser) -> Result; + fn from_parser_multiple( + parser: &mut Parser, + separator: &Token, + ) -> Result, ParseError> { + let mut values = vec![Self::from_parser(parser)?]; + + while parser.try_consume(separator) { + values.push(Self::from_parser(parser)?); + } - fn next(&mut self) -> Option<&'a str> { - let first = self.chars.next(); + Ok(values) + } - let (start, mut prev) = match first { - None => { - if self.text.ends_with(self.delimiter) && self.trailing_empty { - self.trailing_empty = false; - return Some(""); - } - return None; - } - Some((_, c)) if c == self.delimiter => return Some(""), - Some(v) => v, - }; + fn parse_with_separator(value: &str, separator: &Token) -> Result, ParseError> { + let mut parser = Parser::new(Lexer::parse(value)); - let mut in_group = false; - let mut nesting = -1; - - loop { - if prev == self.group_start { - if nesting == -1 { - in_group = true; - } - nesting += 1; - } else if prev == self.group_end { - nesting -= 1; - if nesting == -1 { - in_group = false; - } - } + let values = Self::from_parser_multiple(&mut parser, separator)?; - prev = match self.chars.next() { - None => return Some(&self.text[start..]), - Some((end, c)) if c == self.delimiter && !in_group => { - return Some(&self.text[start..end]) - } - Some((_, c)) => c, - } + if parser.tokens.len() > 0 { + Err(ParseError::too_much_tokens(parser.tokens.len())) + } else { + Ok(values) + } + } + + fn parse(value: &str) -> Result { + let mut parser = Parser::new(Lexer::parse(value)); + + let value = Self::from_parser(&mut parser); + + if parser.tokens.len() > 0 { + Err(ParseError::too_much_tokens(parser.tokens.len())) + } else { + value } } } -#[derive(Clone, Debug)] -pub struct SplitAsciiWhitespaceExcludingGroup<'a> { - pub text: &'a str, - pub chars: CharIndices<'a>, - pub group_start: char, - pub group_end: char, +pub fn parse_angle(parser: &mut Parser) -> Result { + let value = parser.consume_map(Token::try_as_i64)?; + + parser.consume(&Token::ident("deg"))?; + + Ok((value % 360) as f32) } -impl<'a> Iterator for SplitAsciiWhitespaceExcludingGroup<'a> { - type Item = &'a str; +pub fn parse_range(parser: &mut Parser, range: impl RangeBounds) -> Result { + let value = parser.consume_map(Token::try_as_i64)?; - fn next(&mut self) -> Option<&'a str> { - let first = self.chars.next(); + if range.contains(&value) { + Ok(value as f32) + } else { + let start = match range.start_bound() { + Bound::Included(value) => Some(format!("greater than or equal to {value}")), + _ => None, + }; - let (start, mut prev) = match first { - None => return None, - Some((_, c)) if c.is_ascii_whitespace() => return self.next(), - Some(v) => v, + let end = match range.end_bound() { + Bound::Included(value) => Some(format!("less than or equal to {value}")), + Bound::Excluded(value) => Some(format!("less than {value}")), + Bound::Unbounded => None, }; - let mut in_group = false; - let mut nesting = -1; - - loop { - if prev == self.group_start { - if nesting == -1 { - in_group = true; - } - nesting += 1; - } else if prev == self.group_end { - nesting -= 1; - if nesting == -1 { - in_group = false; - } - } + Err(match [start, end] { + [Some(start), Some(end)] => ParseError(format!("{value} must be {start} and {end}")), + [Some(start), None] => ParseError(format!("{value} must be {start}")), + [None, Some(end)] => ParseError(format!("{value} must be {end}")), + [None, None] => unreachable!(), + }) + } +} + +pub fn parse_func, F: FnOnce(&mut Parser) -> Result, O>( + parser: &mut Parser, + name: T, + body: F, +) -> Result { + parser.consume(&Token::ident(name.as_ref()))?; + + parser.consume(&Token::ParenOpen)?; - prev = match self.chars.next() { - None => return Some(&self.text[start..]), - Some((end, c)) if c.is_ascii_whitespace() && !in_group => { - return Some(&self.text[start..end]) - } - Some((_, c)) => c, + let value = body(parser)?; + + parser.consume(&Token::ParenClose)?; + + Ok(value) +} + +pub trait ParseAttribute: Sized { + fn parse_attribute( + &mut self, + attr: OwnedAttributeView, + ) -> Result<(), ParseError>; + + fn parse_safe(&mut self, attr: OwnedAttributeView) { + #[cfg(debug_assertions)] + { + let error_attr = attr.clone(); + + if let Err(ParseError(message)) = self.parse_attribute(attr) { + panic!( + "Failed to parse attribute '{:?}' with value '{:?}': {message}", + error_attr.attribute, error_attr.value + ); } } + + #[cfg(not(debug_assertions))] + self.parse_attribute(attr).ok(); } } diff --git a/crates/state/src/style.rs b/crates/state/src/style.rs index 978a57162..0dd24cb3f 100644 --- a/crates/state/src/style.rs +++ b/crates/state/src/style.rs @@ -20,17 +20,18 @@ use freya_native_core::{ use freya_native_core_macro::partial_derive_state; use crate::{ - parsing::ExtSplit, AttributesBytes, Border, CornerRadius, CustomAttributeValues, Fill, + Lexer, OverflowMode, Parse, ParseAttribute, - ParseError, + Parser, Shadow, + Token, }; #[derive(Default, Debug, Clone, PartialEq, Component)] @@ -55,41 +56,38 @@ impl ParseAttribute for StyleState { if value == "none" { return Ok(()); } + self.background = Fill::parse(value)?; } } AttributeName::Border => { if let Some(value) = attr.value.as_text() { - self.borders = value - .split_excluding_group(',', '(', ')') - .map(|chunk| Border::parse(chunk).unwrap_or_default()) - .collect(); + self.borders = Border::parse_with_separator(value, &Token::Comma)?; } } AttributeName::Shadow => { if let Some(value) = attr.value.as_text() { - self.shadows = value - .split_excluding_group(',', '(', ')') - .map(|chunk| Shadow::parse(chunk).unwrap_or_default()) - .collect(); + self.shadows = Shadow::parse_with_separator(value, &Token::Comma)?; } } AttributeName::CornerRadius => { if let Some(value) = attr.value.as_text() { let mut radius = CornerRadius::parse(value)?; + radius.smoothing = self.corner_radius.smoothing; + self.corner_radius = radius; } } AttributeName::CornerSmoothing => { if let Some(value) = attr.value.as_text() { - if value.ends_with('%') { - let smoothing = value - .replacen('%', "", 1) - .parse::() - .map_err(|_| ParseError)?; - self.corner_radius.smoothing = (smoothing / 100.0).clamp(0.0, 1.0); - } + let mut parser = Parser::new(Lexer::parse(value)); + + let smoothing = parser.consume_map(Token::try_as_f32)?; + + parser.consume(&Token::Percent)?; + + self.corner_radius.smoothing = (smoothing / 100.0).clamp(0.0, 1.0); } } AttributeName::ImageData => { diff --git a/crates/state/src/transform.rs b/crates/state/src/transform.rs index 2d0569722..4967fbde5 100644 --- a/crates/state/src/transform.rs +++ b/crates/state/src/transform.rs @@ -41,18 +41,17 @@ impl ParseAttribute for TransformState { match attr.attribute { AttributeName::Rotate => { if let Some(value) = attr.value.as_text() { - if value.ends_with("deg") { - let rotation = value - .replacen("deg", "", 1) - .parse::() - .map_err(|_| ParseError)?; - self.rotations.push((self.node_id, rotation)); - } + let mut parser = crate::Parser::new(crate::Lexer::parse(value)); + + self.rotations + .push((self.node_id, crate::parse_angle(&mut parser)?)); } } AttributeName::Opacity => { if let Some(value) = attr.value.as_text() { - let opacity = value.parse::().map_err(|_| ParseError)?; + let opacity = value + .parse::() + .map_err(|err| ParseError(err.to_string()))?; self.opacities.push(opacity) } } diff --git a/crates/state/src/values/alignment.rs b/crates/state/src/values/alignment.rs index 93e4d5820..066d28727 100644 --- a/crates/state/src/values/alignment.rs +++ b/crates/state/src/values/alignment.rs @@ -3,17 +3,21 @@ use torin::alignment::Alignment; use crate::{ Parse, ParseError, + Parser, }; impl Parse for Alignment { - fn parse(value: &str) -> Result { - Ok(match value { - "center" => Alignment::Center, - "end" => Alignment::End, - "space-between" => Alignment::SpaceBetween, - "space-evenly" => Alignment::SpaceEvenly, - "space-around" => Alignment::SpaceAround, - _ => Alignment::Start, + fn from_parser(parser: &mut Parser) -> Result { + parser.consume_map(|value| { + value.try_as_str().and_then(|value| match value { + "start" => Some(Self::Start), + "center" => Some(Self::Center), + "end" => Some(Self::End), + "space-between" => Some(Self::SpaceBetween), + "space-evenly" => Some(Self::SpaceEvenly), + "space-around" => Some(Self::SpaceAround), + _ => None, + }) }) } } diff --git a/crates/state/src/values/border.rs b/crates/state/src/values/border.rs index 5ed6939d1..89a86e3d2 100644 --- a/crates/state/src/values/border.rs +++ b/crates/state/src/values/border.rs @@ -4,10 +4,11 @@ use freya_engine::prelude::Color; use torin::scaled::Scaled; use crate::{ - ExtSplit, Fill, Parse, ParseError, + Parser, + Token, }; #[derive(Default, Clone, Debug, PartialEq)] @@ -36,6 +37,44 @@ pub struct BorderWidth { pub left: f32, } +impl Parse for BorderWidth { + fn from_parser(parser: &mut Parser) -> Result { + Ok( + match ( + parser.consume_map(Token::try_as_f32)?, + parser.consume_map(Token::try_as_f32).ok(), + parser.consume_map(Token::try_as_f32).ok(), + parser.consume_map(Token::try_as_f32).ok(), + ) { + (top, Some(right), Some(bottom), Some(left)) => Self { + top, + right, + bottom, + left, + }, + (top, Some(horizontal), Some(bottom), _) => Self { + top, + right: horizontal, + bottom, + left: horizontal, + }, + (vertical, Some(horizontal), ..) => Self { + top: vertical, + right: horizontal, + bottom: vertical, + left: horizontal, + }, + (all, ..) => Self { + top: all, + right: all, + bottom: all, + left: all, + }, + }, + ) + } +} + impl Scaled for BorderWidth { fn scale(&mut self, scale_factor: f32) { self.top *= scale_factor; @@ -64,12 +103,14 @@ pub enum BorderAlignment { } impl Parse for BorderAlignment { - fn parse(value: &str) -> Result { - Ok(match value { - "inner" => BorderAlignment::Inner, - "outer" => BorderAlignment::Outer, - "center" => BorderAlignment::Center, - _ => BorderAlignment::default(), + fn from_parser(parser: &mut Parser) -> Result { + parser.consume_map(|value| { + value.try_as_str().and_then(|value| match value { + "inner" => Some(BorderAlignment::Inner), + "outer" => Some(BorderAlignment::Outer), + "center" => Some(BorderAlignment::Center), + _ => None, + }) }) } } @@ -85,126 +126,22 @@ impl fmt::Display for BorderAlignment { } impl Parse for Border { - fn parse(value: &str) -> Result { - if value == "none" { + fn from_parser(parser: &mut Parser) -> Result { + if parser.try_consume(&Token::ident("none")) { return Ok(Self::default()); } - let mut border_values = value.split_ascii_whitespace_excluding_group('(', ')'); - - Ok(match border_values.clone().count() { - //