Skip to content

Instantly share code, notes, and snippets.

@cubetastic33
Created January 17, 2019 05:23
Show Gist options
  • Save cubetastic33/864b1fee326233310b297a981378a43d to your computer and use it in GitHub Desktop.
Save cubetastic33/864b1fee326233310b297a981378a43d to your computer and use it in GitHub Desktop.
//! High-level types and functions related to CSS parsing
use std::{
num::ParseIntError,
fmt,
};
pub use simplecss::Error as CssSyntaxError;
use simplecss::Tokenizer;
use css_parser;
pub use css_parser::CssParsingError;
use azul_css::{
Css,
CssDeclaration,
DynamicCssProperty,
DynamicCssPropertyDefault,
CssPropertyType,
CssRuleBlock,
CssPath,
CssPathSelector,
CssPathPseudoSelector::{self, *},
CssNthChildSelector::*,
NodeTypePath,
NodeTypePathParseError,
};
/// Error that can happen during the parsing of a CSS value
#[derive(Debug, Clone, PartialEq)]
pub struct CssParseError<'a> {
pub error: CssParseErrorInner<'a>,
pub location: ErrorLocation,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CssParseErrorInner<'a> {
/// A hard error in the CSS syntax
ParseError(CssSyntaxError),
/// Braces are not balanced properly
UnclosedBlock,
/// Invalid syntax, such as `#div { #div: "my-value" }`
MalformedCss,
/// Error parsing dynamic CSS property, such as
/// `#div { width: {{ my_id }} /* no default case */ }`
DynamicCssParseError(DynamicCssParseError<'a>),
/// Error while parsing a pseudo selector (like `:aldkfja`)
PseudoSelectorParseError(CssPseudoSelectorParseError<'a>),
/// The path has to be either `*`, `div`, `p` or something like that
NodeTypePath(NodeTypePathParseError<'a>),
/// A certain property has an unknown key, for example: `alsdfkj: 500px` = `unknown CSS key "alsdfkj: 500px"`
UnknownPropertyKey(&'a str, &'a str),
}
impl_display!{ CssParseErrorInner<'a>, {
ParseError(e) => format!("Parse Error: {:?}", e),
UnclosedBlock => "Unclosed block",
MalformedCss => "Malformed Css",
DynamicCssParseError(e) => format!("Error parsing dynamic CSS property: {}", e),
PseudoSelectorParseError(e) => format!("Failed to parse pseudo-selector: {}", e),
NodeTypePath(e) => format!("Failed to parse CSS selector path: {}", e),
UnknownPropertyKey(k, v) => format!("Unknown CSS key: \"{}: {}\"", k, v),
}}
impl_from! { DynamicCssParseError<'a>, CssParseErrorInner::DynamicCssParseError }
impl_from! { CssPseudoSelectorParseError<'a>, CssParseErrorInner::PseudoSelectorParseError }
impl_from! { NodeTypePathParseError<'a>, CssParseErrorInner::NodeTypePath }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CssPseudoSelectorParseError<'a> {
UnknownSelector(&'a str),
InvalidNthChild(ParseIntError),
UnclosedBracesNthChild(&'a str),
}
impl<'a> From<ParseIntError> for CssPseudoSelectorParseError<'a> {
fn from(e: ParseIntError) -> Self { CssPseudoSelectorParseError::InvalidNthChild(e) }
}
impl_display! { CssPseudoSelectorParseError<'a>, {
UnknownSelector(e) => format!("Invalid CSS pseudo-selector: ':{}'", e),
InvalidNthChild(e) => format!("Invalid :nth-child pseudo-selector: ':{}'", e),
UnclosedBracesNthChild(e) => format!(":nth-child has unclosed braces: ':{}'", e),
}}
fn pseudo_selector_from_str(data: &str) -> Result<CssPathPseudoSelector, CssPseudoSelectorParseError> {
println!("data = {}", data);
match data {
"first" => Ok(First),
"last" => Ok(Last),
"hover" => Ok(Hover),
"active" => Ok(Active),
"focus" => Ok(Focus),
other => Err(CssPseudoSelectorParseError::UnknownSelector(other)),
}
}
fn parse_nth_child_selector(input: &str) -> Result<CssPathPseudoSelector, CssPseudoSelectorParseError> {
let nth_child_string = input.trim();
// If the value is a number
if let Ok(number) = nth_child_string.parse::<usize>() {
return Ok(NthChild(Number(number)));
}
// If the value is not a number
match nth_child_string {
"even" => Ok(NthChild(Even)),
"odd" => Ok(NthChild(Odd)),
other => {
if !nth_child_string.contains("n") {
return Err(CssPseudoSelectorParseError::UnknownSelector(&format!("nth-child({})", input)));
}
let repeat = nth_child_string.split("n").next()
.ok_or(CssPseudoSelectorParseError::UnknownSelector(&format!("nth-child({})", input)))?
.parse::<usize>()?;
let offset = if nth_child_string.contains("+") {
nth_child_string.split("+")
.collect::<Vec<&str>>()[1]
.trim()
.parse::<usize>()?
} else {
0
};
Ok(NthChild(Pattern { repeat, offset }))
}
}
}
#[test]
fn test_css_pseudo_selector_parse() {
let ok_res = [
("first", First),
("last", Last),
("hover", Hover),
("active", Active),
("focus", Focus),
];
let err = [
("asdf", CssPseudoSelectorParseError::UnknownSelector("asdf")),
("", CssPseudoSelectorParseError::UnknownSelector("")),
// Can't test for ParseIntError because the fields are private.
// This is an example on why you shouldn't use std::error::Error!
];
for (s, a) in &ok_res {
assert_eq!(pseudo_selector_from_str(s), Ok(*a));
}
for (s, e) in &err {
assert_eq!(pseudo_selector_from_str(s), Err(e.clone()));
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ErrorLocation {
pub line: usize,
pub column: usize,
}
impl<'a> fmt::Display for CssParseError<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "CSS error at line {}:{}: {}", self.location.line, self.location.column, self.error)
}
}
pub fn new_from_str<'a>(css_string: &'a str) -> Result<Css, CssParseError<'a>> {
let mut tokenizer = Tokenizer::new(css_string);
match new_from_str_inner(css_string, &mut tokenizer) {
Ok(css) => Ok(css),
Err(e) => {
let error_location = tokenizer.pos().saturating_sub(1);
let line_number: usize = css_string[0..error_location].lines().count();
// Rust doesn't count "\n" as a character, so we have to add the line number count on top
let total_characters: usize = css_string[0..error_location].lines().take(line_number.saturating_sub(1)).map(|line| line.chars().count()).sum();
let total_characters = total_characters + line_number;
/*println!("line_number: {} error location: {}, total characters: {}", line_number,
error_location, total_characters);*/
let characters_in_line = (error_location + 2) - total_characters;
let error_location = ErrorLocation {
line: line_number,
column: characters_in_line,
};
Err(CssParseError {
error: e,
location: error_location,
})
}
}
}
/// Parses a CSS string (single-threaded) and returns the parsed rules in blocks
fn new_from_str_inner<'a>(css_string: &'a str, tokenizer: &mut Tokenizer<'a>) -> Result<Css, CssParseErrorInner<'a>> {
use simplecss::{Token, Combinator};
let mut css_blocks = Vec::new();
// Used for error checking / checking for closed braces
let mut parser_in_block = false;
let mut block_nesting = 0_usize;
// Current css paths (i.e. `div#id, .class, p` are stored here -
// when the block is finished, all `current_rules` gets duplicated with
// one path corresponding to one set of rules each).
let mut current_paths = Vec::new();
// Current CSS declarations
let mut current_rules = Vec::new();
// Keep track of the current path during parsing
let mut last_path = Vec::new();
let css_property_map = azul_css::get_css_key_map();
loop {
let tokenize_result = tokenizer.parse_next();
match tokenize_result {
Ok(token) => {
match token {
Token::BlockStart => {
if parser_in_block {
// multi-nested CSS blocks are currently not supported
return Err(CssParseErrorInner::MalformedCss);
}
parser_in_block = true;
block_nesting += 1;
current_paths.push(last_path.clone());
last_path.clear();
},
Token::Comma => {
current_paths.push(last_path.clone());
last_path.clear();
},
Token::BlockEnd => {
block_nesting -= 1;
if !parser_in_block {
return Err(CssParseErrorInner::MalformedCss);
}
parser_in_block = false;
for path in current_paths.drain(..) {
css_blocks.push(CssRuleBlock {
path: CssPath { selectors: path },
declarations: current_rules.clone(),
})
}
current_rules.clear();
last_path.clear(); // technically unnecessary, but just to be sure
},
// tokens that adjust the last_path
Token::UniversalSelector => {
if parser_in_block {
return Err(CssParseErrorInner::MalformedCss);
}
last_path.push(CssPathSelector::Global);
},
Token::TypeSelector(div_type) => {
if parser_in_block {
return Err(CssParseErrorInner::MalformedCss);
}
last_path.push(CssPathSelector::Type(NodeTypePath::from_str(div_type)?));
},
Token::IdSelector(id) => {
if parser_in_block {
return Err(CssParseErrorInner::MalformedCss);
}
last_path.push(CssPathSelector::Id(id.to_string()));
},
Token::ClassSelector(class) => {
if parser_in_block {
return Err(CssParseErrorInner::MalformedCss);
}
last_path.push(CssPathSelector::Class(class.to_string()));
},
Token::Combinator(Combinator::GreaterThan) => {
if parser_in_block {
return Err(CssParseErrorInner::MalformedCss);
}
last_path.push(CssPathSelector::DirectChildren);
},
Token::Combinator(Combinator::Space) => {
if parser_in_block {
return Err(CssParseErrorInner::MalformedCss);
}
last_path.push(CssPathSelector::Children);
},
Token::PseudoClass(pseudo_class) => {
if parser_in_block {
return Err(CssParseErrorInner::MalformedCss);
}
last_path.push(CssPathSelector::PseudoSelector(pseudo_selector_from_str(pseudo_class)?));
},
Token::NthChildPseudoClass(nth_child) => {
if parser_in_block {
return Err(CssParseErrorInner::MalformedCss);
}
last_path.push(CssPathSelector::PseudoSelector(parse_nth_child_selector(nth_child)?));
},
Token::Declaration(key, val) => {
if !parser_in_block {
return Err(CssParseErrorInner::MalformedCss);
}
let parsed_key = CssPropertyType::from_str(key, &css_property_map)
.ok_or(CssParseErrorInner::UnknownPropertyKey(key, val))?;
current_rules.push(determine_static_or_dynamic_css_property(parsed_key, val)?);
},
Token::EndOfStream => {
break;
},
_ => {
// attributes, lang-attributes and @keyframes are not supported
}
}
},
Err(e) => {
return Err(CssParseErrorInner::ParseError(e));
}
}
}
// non-even number of blocks
if block_nesting != 0 {
return Err(CssParseErrorInner::UnclosedBlock);
}
Ok(css_blocks.into())
}
/// Error that can happen during `css_parser::parse_key_value_pair`
#[derive(Debug, Clone, PartialEq)]
pub enum DynamicCssParseError<'a> {
/// The braces of a dynamic CSS property aren't closed or unbalanced, i.e. ` [[ `
UnclosedBraces,
/// There is a valid dynamic css property, but no default case
NoDefaultCase,
/// The dynamic CSS property has no ID, i.e. `[[ 400px ]]`
NoId,
/// The ID may not start with a number or be a CSS property itself
InvalidId,
/// Dynamic css property braces are empty, i.e. `[[ ]]`
EmptyBraces,
/// Unexpected value when parsing the string
UnexpectedValue(CssParsingError<'a>),
}
impl_display!{ DynamicCssParseError<'a>, {
UnclosedBraces => "The braces of a dynamic CSS property aren't closed or unbalanced, i.e. ` [[ `",
NoDefaultCase => "There is a valid dynamic css property, but no default case",
NoId => "The dynamic CSS property has no ID, i.e. [[ 400px ]]",
InvalidId => "The ID may not start with a number or be a CSS property itself",
EmptyBraces => "Dynamic css property braces are empty, i.e. `[[ ]]`",
UnexpectedValue(e) => format!("Unexpected value: {}", e),
}}
impl<'a> From<CssParsingError<'a>> for DynamicCssParseError<'a> {
fn from(e: CssParsingError<'a>) -> Self {
DynamicCssParseError::UnexpectedValue(e)
}
}
pub const START_BRACE: &str = "[[";
pub const END_BRACE: &str = "]]";
/// Determine if a Css property is static (immutable) or if it can change
/// during the runtime of the program
pub fn determine_static_or_dynamic_css_property<'a>(key: CssPropertyType, value: &'a str)
-> Result<CssDeclaration, DynamicCssParseError<'a>>
{
let value = value.trim();
let is_starting_with_braces = value.starts_with(START_BRACE);
let is_ending_with_braces = value.ends_with(END_BRACE);
match (is_starting_with_braces, is_ending_with_braces) {
(true, false) | (false, true) => {
Err(DynamicCssParseError::UnclosedBraces)
},
(true, true) => {
parse_dynamic_css_property(key, value).and_then(|val| Ok(CssDeclaration::Dynamic(val)))
},
(false, false) => {
Ok(CssDeclaration::Static(css_parser::parse_key_value_pair(key, value)?))
}
}
}
pub fn parse_dynamic_css_property<'a>(key: CssPropertyType, value: &'a str) -> Result<DynamicCssProperty, DynamicCssParseError<'a>> {
use std::char;
// "[[ id | 400px ]]" => "id | 400px"
let value = value.trim_left_matches(START_BRACE);
let value = value.trim_right_matches(END_BRACE);
let value = value.trim();
let mut pipe_split = value.splitn(2, "|");
let dynamic_id = pipe_split.next();
let default_case = pipe_split.next();
// note: dynamic_id will always be Some(), which is why the
let (default_case, dynamic_id) = match (default_case, dynamic_id) {
(Some(default), Some(id)) => (default, id),
(None, Some(id)) => {
if id.trim().is_empty() {
return Err(DynamicCssParseError::EmptyBraces);
} else if css_parser::parse_key_value_pair(key, id).is_ok() {
// if there is an ID, but the ID is a CSS value
return Err(DynamicCssParseError::NoId);
} else {
return Err(DynamicCssParseError::NoDefaultCase);
}
},
(None, None) | (Some(_), None) => unreachable!(), // iterator would be broken if this happened
};
let dynamic_id = dynamic_id.trim();
let default_case = default_case.trim();
match (dynamic_id.is_empty(), default_case.is_empty()) {
(true, true) => return Err(DynamicCssParseError::EmptyBraces),
(true, false) => return Err(DynamicCssParseError::NoId),
(false, true) => return Err(DynamicCssParseError::NoDefaultCase),
(false, false) => { /* everything OK */ }
}
if dynamic_id.starts_with(char::is_numeric) ||
css_parser::parse_key_value_pair(key, dynamic_id).is_ok() {
return Err(DynamicCssParseError::InvalidId);
}
let default_case_parsed = match default_case {
"auto" => DynamicCssPropertyDefault::Auto,
other => DynamicCssPropertyDefault::Exact(css_parser::parse_key_value_pair(key, other)?),
};
Ok(DynamicCssProperty {
property_type: key,
dynamic_id: dynamic_id.to_string(),
default: default_case_parsed,
})
}
#[test]
fn test_detect_static_or_dynamic_property() {
use azul_css::{CssProperty, StyleTextAlignmentHorz};
use css_parser::InvalidValueErr;
assert_eq!(
determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, " center "),
Ok(CssDeclaration::Static(CssProperty::TextAlign(StyleTextAlignmentHorz::Center)))
);
assert_eq!(
determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ 400px ]]"),
Err(DynamicCssParseError::NoDefaultCase)
);
assert_eq!(determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ 400px"),
Err(DynamicCssParseError::UnclosedBraces)
);
assert_eq!(
determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ 400px | center ]]"),
Err(DynamicCssParseError::InvalidId)
);
assert_eq!(
determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ hello | center ]]"),
Ok(CssDeclaration::Dynamic(DynamicCssProperty {
property_type: CssPropertyType::TextAlign,
default: DynamicCssPropertyDefault::Exact(CssProperty::TextAlign(StyleTextAlignmentHorz::Center)),
dynamic_id: String::from("hello"),
}))
);
assert_eq!(
determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ hello | auto ]]"),
Ok(CssDeclaration::Dynamic(DynamicCssProperty {
property_type: CssPropertyType::TextAlign,
default: DynamicCssPropertyDefault::Auto,
dynamic_id: String::from("hello"),
}))
);
assert_eq!(
determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ abc | hello ]]"),
Err(DynamicCssParseError::UnexpectedValue(
CssParsingError::InvalidValueErr(InvalidValueErr("hello"))
))
);
assert_eq!(
determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ ]]"),
Err(DynamicCssParseError::EmptyBraces)
);
assert_eq!(
determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[]]"),
Err(DynamicCssParseError::EmptyBraces)
);
assert_eq!(
determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ center ]]"),
Err(DynamicCssParseError::NoId)
);
assert_eq!(
determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ hello | ]]"),
Err(DynamicCssParseError::NoDefaultCase)
);
// debatable if this is a suitable error for this case:
assert_eq!(
determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ | ]]"),
Err(DynamicCssParseError::EmptyBraces)
);
}
#[test]
fn test_css_parse_1() {
use azul_css::{ColorU, StyleBackgroundColor, NodeTypePath, CssProperty};
let parsed_css = new_from_str("
div#my_id .my_class:first {
background-color: red;
}
").unwrap();
let expected_css_rules = vec![
CssRuleBlock {
path: CssPath {
selectors: vec![
CssPathSelector::Type(NodeTypePath::Div),
CssPathSelector::Id(String::from("my_id")),
CssPathSelector::Children,
// NOTE: This is technically wrong, the space between "#my_id"
// and ".my_class" is important, but gets ignored for now
CssPathSelector::Class(String::from("my_class")),
CssPathSelector::PseudoSelector(First),
],
},
declarations: vec![CssDeclaration::Static(CssProperty::BackgroundColor(StyleBackgroundColor(ColorU { r: 255, g: 0, b: 0, a: 255 })))],
}
];
assert_eq!(parsed_css, expected_css_rules.into());
}
#[test]
fn test_css_simple_selector_parse() {
use self::CssPathSelector::*;
use azul_css::NodeTypePath;
let css = "div#id.my_class > p .new { }";
let parsed = vec![
Type(NodeTypePath::Div),
Id("id".into()),
Class("my_class".into()),
DirectChildren,
Type(NodeTypePath::P),
Children,
Class("new".into())
];
assert_eq!(new_from_str(css).unwrap(), Css {
rules: vec![CssRuleBlock {
path: CssPath { selectors: parsed },
declarations: Vec::new(),
}],
});
}
#[cfg(test)]
mod stylesheet_parse {
use azul_css::*;
use super::*;
fn test_css(css: &str, expected: Vec<CssRuleBlock>) {
let css = new_from_str(css).unwrap();
assert_eq!(css, expected.into());
}
// Tests that an element with a single class always gets the CSS element applied properly
#[test]
fn test_apply_css_pure_class() {
let red = CssProperty::BackgroundColor(StyleBackgroundColor(ColorU { r: 255, g: 0, b: 0, a: 255 }));
let blue = CssProperty::BackgroundColor(StyleBackgroundColor(ColorU { r: 0, g: 0, b: 255, a: 255 }));
let black = CssProperty::BackgroundColor(StyleBackgroundColor(ColorU { r: 0, g: 0, b: 0, a: 255 }));
// Simple example
{
let css_1 = ".my_class { background-color: red; }";
let expected_rules = vec![
CssRuleBlock {
path: CssPath { selectors: vec![CssPathSelector::Class("my_class".into())] },
declarations: vec![
CssDeclaration::Static(red.clone())
],
},
];
test_css(css_1, expected_rules);
}
// Slightly more complex example
{
let css_2 = "#my_id { background-color: red; } .my_class { background-color: blue; }";
let expected_rules = vec![
CssRuleBlock {
path: CssPath { selectors: vec![CssPathSelector::Id("my_id".into())] },
declarations: vec![CssDeclaration::Static(red.clone())]
},
CssRuleBlock {
path: CssPath { selectors: vec![CssPathSelector::Class("my_class".into())] },
declarations: vec![CssDeclaration::Static(blue.clone())]
},
];
test_css(css_2, expected_rules);
}
// Even more complex example
{
let css_3 = "* { background-color: black; } .my_class#my_id { background-color: red; } .my_class { background-color: blue; }";
let expected_rules = vec![
CssRuleBlock {
path: CssPath { selectors: vec![CssPathSelector::Global] },
declarations: vec![CssDeclaration::Static(black.clone())]
},
CssRuleBlock {
path: CssPath { selectors: vec![CssPathSelector::Class("my_class".into()), CssPathSelector::Id("my_id".into())] },
declarations: vec![CssDeclaration::Static(red.clone())]
},
CssRuleBlock {
path: CssPath { selectors: vec![CssPathSelector::Class("my_class".into())] },
declarations: vec![CssDeclaration::Static(blue.clone())]
},
];
test_css(css_3, expected_rules);
}
}
}
// Assert that order of the style rules is correct (in same order as provided in CSS form)
#[test]
fn test_multiple_rules() {
use azul_css::*;
use self::CssPathSelector::*;
let parsed_css = new_from_str("
* { }
* div.my_class#my_id { }
* div#my_id { }
* #my_id { }
div.my_class.specific#my_id { }
").unwrap();
let expected_rules = vec![
// Rules are sorted by order of appearance in source string
CssRuleBlock { path: CssPath { selectors: vec![Global] }, declarations: Vec::new() },
CssRuleBlock { path: CssPath { selectors: vec![Global, Type(NodeTypePath::Div), Class("my_class".into()), Id("my_id".into())] }, declarations: Vec::new() },
CssRuleBlock { path: CssPath { selectors: vec![Global, Type(NodeTypePath::Div), Id("my_id".into())] }, declarations: Vec::new() },
CssRuleBlock { path: CssPath { selectors: vec![Global, Id("my_id".into())] }, declarations: Vec::new() },
CssRuleBlock { path: CssPath { selectors: vec![Type(NodeTypePath::Div), Class("my_class".into()), Class("specific".into()), Id("my_id".into())] }, declarations: Vec::new() },
];
assert_eq!(parsed_css, expected_rules.into());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment