use cssparser::{Parser, ParserInput, Token, ParseError};
use crate::css::{
CssValue, Length, LengthUnit, Selector, SelectorPart, PseudoClass,
StyleProperty, StyleRule, StyleSheet,
};
use crate::geometry::Color;
use std::collections::HashMap;
pub struct CssParser;
impl CssParser {
pub fn parse_stylesheet(css: &str) -> Result<StyleSheet, CssParseError> {
let mut input = ParserInput::new(css);
let mut parser = Parser::new(&mut input);
let mut rules = Vec::new();
while !parser.is_exhausted() {
let _ = parser.try_parse::<_, _, ParseError<'_, ()>>(|p| {
p.skip_whitespace();
Ok(())
});
if parser.is_exhausted() {
break;
}
match Self::parse_rule(&mut parser) {
Ok(rule) => rules.push(rule),
Err(_) => {
let _ = parser.next();
}
}
}
Ok(StyleSheet::new(rules))
}
fn parse_rule<'i>(parser: &mut Parser<'i, '_>) -> Result<StyleRule, CssParseError> {
let selector = Self::parse_selector(parser)?;
let declarations = parser.parse_nested_block(|p| {
Self::parse_declarations(p)
}).map_err(|_| CssParseError::InvalidDeclaration)?;
Ok(StyleRule::new(selector, declarations))
}
fn parse_selector<'i>(parser: &mut Parser<'i, '_>) -> Result<Selector, CssParseError> {
let mut parts = Vec::new();
loop {
parser.skip_whitespace();
let token = match parser.next() {
Ok(t) => t.clone(),
Err(_) => break,
};
match token {
Token::Ident(name) => {
parts.push(SelectorPart::Type(name.to_string()));
}
Token::IDHash(name) => {
parts.push(SelectorPart::Id(name.to_string()));
}
Token::Delim('.') => {
if let Ok(Token::Ident(name)) = parser.next() {
parts.push(SelectorPart::Class(name.to_string()));
}
}
Token::Delim('*') => {
parts.push(SelectorPart::Universal);
}
Token::Colon => {
if let Ok(Token::Ident(name)) = parser.next() {
if let Some(pseudo) = PseudoClass::from_name(name) {
parts.push(SelectorPart::PseudoClass(pseudo));
}
}
}
Token::CurlyBracketBlock => {
break;
}
_ => {
break;
}
}
}
if parts.is_empty() {
return Err(CssParseError::InvalidSelector);
}
Ok(Selector::new(parts))
}
fn parse_declarations<'i>(
parser: &mut Parser<'i, '_>,
) -> Result<HashMap<StyleProperty, CssValue>, ParseError<'i, ()>> {
let mut declarations = HashMap::new();
loop {
parser.skip_whitespace();
if parser.is_exhausted() {
break;
}
let property_name = match parser.next() {
Ok(Token::Ident(name)) => name.to_string(),
Ok(Token::Semicolon) => continue,
_ => continue,
};
parser.skip_whitespace();
if parser.expect_colon().is_err() {
continue;
}
parser.skip_whitespace();
if let Ok(value) = Self::parse_value(parser) {
let property = StyleProperty::from_name(&property_name);
declarations.insert(property, value);
}
let _ = parser.try_parse::<_, _, ParseError<'_, ()>>(|p| {
p.expect_semicolon()?;
Ok(())
});
}
Ok(declarations)
}
fn parse_value<'i>(parser: &mut Parser<'i, '_>) -> Result<CssValue, CssParseError> {
let token = parser.next().map_err(|_| CssParseError::InvalidValue)?;
match token {
Token::Ident(name) => {
let name = name.to_string();
if let Some(color) = Self::color_from_keyword(&name) {
return Ok(CssValue::Color(color));
}
Ok(CssValue::Keyword(name))
}
Token::Hash(hash) | Token::IDHash(hash) => {
if let Some(color) = Color::from_hex(hash) {
Ok(CssValue::Color(color))
} else {
Err(CssParseError::InvalidValue)
}
}
Token::Number { value, .. } => {
Ok(CssValue::Number(*value))
}
Token::Percentage { unit_value, .. } => {
Ok(CssValue::Percentage(*unit_value * 100.0))
}
Token::Dimension { value, unit, .. } => {
if let Some(length_unit) = LengthUnit::parse(unit) {
Ok(CssValue::Length(Length::new(*value, length_unit)))
} else {
Err(CssParseError::InvalidValue)
}
}
Token::QuotedString(s) => {
Ok(CssValue::String(s.to_string()))
}
Token::Function(name) => {
let name = name.to_string();
parser.parse_nested_block(|p| {
Self::parse_function(&name, p)
}).map_err(|_| CssParseError::InvalidValue)
}
_ => Err(CssParseError::InvalidValue),
}
}
fn parse_function<'i>(
name: &str,
parser: &mut Parser<'i, '_>,
) -> Result<CssValue, ParseError<'i, ()>> {
match name {
"rgb" | "rgba" => Self::parse_rgb_function(parser),
"hsl" | "hsla" => Self::parse_hsl_function(parser),
"var" => Self::parse_var_function(parser),
"calc" => Ok(CssValue::Keyword("calc(...)".to_string())), _ => Ok(CssValue::Keyword(format!("{}(...)", name))),
}
}
fn parse_rgb_function<'i>(parser: &mut Parser<'i, '_>) -> Result<CssValue, ParseError<'i, ()>> {
let mut values = Vec::new();
loop {
parser.skip_whitespace();
match parser.next() {
Ok(Token::Number { value, .. }) => values.push(*value),
Ok(Token::Percentage { unit_value, .. }) => values.push(*unit_value * 255.0),
Ok(Token::Comma) => continue,
Ok(Token::Delim('/')) => continue, Err(_) => break,
_ => continue,
}
}
match values.len() {
3 => {
let color = Color::from_rgb8(
values[0] as u8,
values[1] as u8,
values[2] as u8,
);
Ok(CssValue::Color(color))
}
4 => {
let alpha = if values[3] <= 1.0 {
(values[3] * 255.0) as u8
} else {
values[3] as u8
};
let color = Color::from_rgba8(
values[0] as u8,
values[1] as u8,
values[2] as u8,
alpha,
);
Ok(CssValue::Color(color))
}
_ => Ok(CssValue::Keyword("rgb()".to_string())),
}
}
fn parse_hsl_function<'i>(parser: &mut Parser<'i, '_>) -> Result<CssValue, ParseError<'i, ()>> {
let mut values = Vec::new();
loop {
parser.skip_whitespace();
match parser.next() {
Ok(Token::Number { value, .. }) => values.push(*value),
Ok(Token::Percentage { unit_value, .. }) => values.push(*unit_value * 100.0),
Ok(Token::Comma) => continue,
Ok(Token::Delim('/')) => continue,
Err(_) => break,
_ => continue,
}
}
if values.len() >= 3 {
let color = Color::from_hsl(values[0], values[1], values[2]);
if values.len() >= 4 {
let alpha = if values[3] <= 1.0 { values[3] } else { values[3] / 100.0 };
Ok(CssValue::Color(color.with_alpha(alpha)))
} else {
Ok(CssValue::Color(color))
}
} else {
Ok(CssValue::Keyword("hsl()".to_string()))
}
}
fn parse_var_function<'i>(parser: &mut Parser<'i, '_>) -> Result<CssValue, ParseError<'i, ()>> {
parser.skip_whitespace();
let var_name = match parser.next() {
Ok(Token::Ident(name)) => name.to_string(),
_ => return Ok(CssValue::Keyword("var()".to_string())),
};
parser.skip_whitespace();
let fallback = if let Ok(Token::Comma) = parser.next() {
parser.skip_whitespace();
CssParser::parse_value(parser).ok().map(Box::new)
} else {
None
};
Ok(CssValue::Var(var_name, fallback))
}
pub fn parse_inline_style(style: &str) -> HashMap<StyleProperty, CssValue> {
let declarations = HashMap::new();
let css = format!("x {{ {} }}", style);
if let Ok(stylesheet) = Self::parse_stylesheet(&css) {
if let Some(rule) = stylesheet.rules.first() {
return rule.declarations.clone();
}
}
declarations
}
fn color_from_keyword(keyword: &str) -> Option<Color> {
match keyword.to_lowercase().as_str() {
"transparent" => Some(Color::TRANSPARENT),
"black" => Some(Color::BLACK),
"white" => Some(Color::WHITE),
"red" => Some(Color::from_rgb8(255, 0, 0)),
"green" => Some(Color::from_rgb8(0, 128, 0)),
"blue" => Some(Color::from_rgb8(0, 0, 255)),
"yellow" => Some(Color::from_rgb8(255, 255, 0)),
"cyan" | "aqua" => Some(Color::from_rgb8(0, 255, 255)),
"magenta" | "fuchsia" => Some(Color::from_rgb8(255, 0, 255)),
"gray" | "grey" => Some(Color::from_rgb8(128, 128, 128)),
"silver" => Some(Color::from_rgb8(192, 192, 192)),
"maroon" => Some(Color::from_rgb8(128, 0, 0)),
"olive" => Some(Color::from_rgb8(128, 128, 0)),
"lime" => Some(Color::from_rgb8(0, 255, 0)),
"teal" => Some(Color::from_rgb8(0, 128, 128)),
"navy" => Some(Color::from_rgb8(0, 0, 128)),
"purple" => Some(Color::from_rgb8(128, 0, 128)),
"orange" => Some(Color::from_rgb8(255, 165, 0)),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub enum CssParseError {
InvalidSelector,
InvalidDeclaration,
InvalidValue,
UnexpectedToken,
}
impl std::fmt::Display for CssParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CssParseError::InvalidSelector => write!(f, "Invalid CSS selector"),
CssParseError::InvalidDeclaration => write!(f, "Invalid CSS declaration"),
CssParseError::InvalidValue => write!(f, "Invalid CSS value"),
CssParseError::UnexpectedToken => write!(f, "Unexpected token"),
}
}
}
impl std::error::Error for CssParseError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_rule() {
let css = ".button { background-color: red; padding: 10px; }";
let stylesheet = CssParser::parse_stylesheet(css).unwrap();
assert_eq!(stylesheet.rules.len(), 1);
}
#[test]
fn test_parse_hex_color() {
let css = ".test { color: #ff0000; }";
let stylesheet = CssParser::parse_stylesheet(css).unwrap();
assert_eq!(stylesheet.rules.len(), 1);
}
#[test]
fn test_parse_inline_style() {
let style = "color: blue; font-size: 16px;";
let declarations = CssParser::parse_inline_style(style);
assert!(declarations.contains_key(&StyleProperty::Color));
assert!(declarations.contains_key(&StyleProperty::FontSize));
}
}