use cssparser::{Parser, ParserInput, Token};
use crate::color::Color;
use crate::tcss::ast::{Rule, Stylesheet, VariableDefinition};
use crate::tcss::error::TcssError;
use crate::tcss::property::{Declaration, PropertyName};
use crate::tcss::selector::SelectorList;
use crate::tcss::value::{CssValue, Length};
use crate::tcss::variable::VariableMap;
pub fn parse_color(input: &mut Parser<'_, '_>) -> Result<Color, TcssError> {
let token = input
.next()
.map_err(|e| TcssError::Parse(format!("{e:?}")))?
.clone();
match &token {
Token::Ident(name) => {
let name_str = name.to_string();
Color::from_css_name(&name_str).ok_or_else(|| TcssError::InvalidValue {
property: "color".into(),
value: name_str,
})
}
Token::Hash(hash) | Token::IDHash(hash) => {
let hash_str = hash.to_string();
Color::from_hex(&hash_str).map_err(|e| TcssError::Parse(e.to_string()))
}
Token::Function(name) if name.eq_ignore_ascii_case("rgb") => parse_rgb_block(input),
other => Err(TcssError::Parse(format!("expected color, got {other:?}"))),
}
}
fn parse_rgb_block(input: &mut Parser<'_, '_>) -> Result<Color, TcssError> {
let result: Result<(i32, i32, i32), cssparser::ParseError<'_, ()>> =
input.parse_nested_block(|input| {
let r = input.expect_integer()?;
input.expect_comma()?;
let g = input.expect_integer()?;
input.expect_comma()?;
let b = input.expect_integer()?;
Ok((r, g, b))
});
let (r, g, b) = result.map_err(|e| TcssError::Parse(format!("invalid rgb(): {e:?}")))?;
let r = u8::try_from(r).map_err(|_| TcssError::InvalidValue {
property: "color".into(),
value: format!("rgb component {r} out of range"),
})?;
let g = u8::try_from(g).map_err(|_| TcssError::InvalidValue {
property: "color".into(),
value: format!("rgb component {g} out of range"),
})?;
let b = u8::try_from(b).map_err(|_| TcssError::InvalidValue {
property: "color".into(),
value: format!("rgb component {b} out of range"),
})?;
Ok(Color::Rgb { r, g, b })
}
pub fn parse_length(input: &mut Parser<'_, '_>) -> Result<Length, TcssError> {
let token = input
.next()
.map_err(|e| TcssError::Parse(format!("{e:?}")))?
.clone();
match &token {
Token::Ident(name) if name.eq_ignore_ascii_case("auto") => Ok(Length::Auto),
Token::Number {
int_value: Some(v), ..
} => {
let val = u16::try_from(*v).map_err(|_| TcssError::InvalidValue {
property: "length".into(),
value: format!("{v} is out of range for cell count"),
})?;
Ok(Length::Cells(val))
}
Token::Number { value, .. } => {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let val = *value as u16;
Ok(Length::Cells(val))
}
Token::Percentage { unit_value, .. } => Ok(Length::Percent(*unit_value * 100.0)),
other => Err(TcssError::Parse(format!("expected length, got {other:?}"))),
}
}
pub fn parse_integer(input: &mut Parser<'_, '_>) -> Result<i32, TcssError> {
input
.expect_integer()
.map_err(|e| TcssError::Parse(format!("{e:?}")))
}
pub fn parse_float(input: &mut Parser<'_, '_>) -> Result<f32, TcssError> {
input
.expect_number()
.map_err(|e| TcssError::Parse(format!("{e:?}")))
}
pub fn parse_keyword(input: &mut Parser<'_, '_>) -> Result<String, TcssError> {
input
.expect_ident()
.map(|s| s.to_string())
.map_err(|e| TcssError::Parse(format!("{e:?}")))
}
fn try_parse_variable(input: &mut Parser<'_, '_>) -> Option<CssValue> {
input
.try_parse(|p| -> Result<CssValue, cssparser::ParseError<'_, ()>> {
p.expect_delim('$')?;
let name = p.expect_ident()?.to_string();
Ok(CssValue::Variable(name))
})
.ok()
}
fn parse_grid_template(input: &mut Parser<'_, '_>) -> Result<CssValue, TcssError> {
let mut tracks = Vec::new();
while !input.is_exhausted() {
let track = input.try_parse(|p| -> Result<CssValue, cssparser::ParseError<'_, ()>> {
let token = p.next()?.clone();
match &token {
Token::Dimension { value, unit, .. } if unit.eq_ignore_ascii_case("fr") => {
Ok(CssValue::Fr(*value))
}
Token::Number {
int_value: Some(v), ..
} => {
let val = u16::try_from(*v).map_err(|_| p.new_custom_error(()))?;
Ok(CssValue::Length(Length::Cells(val)))
}
Token::Number { value, .. } => {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let val = *value as u16;
Ok(CssValue::Length(Length::Cells(val)))
}
Token::Percentage { unit_value, .. } => {
Ok(CssValue::Length(Length::Percent(*unit_value * 100.0)))
}
Token::Ident(name) if name.eq_ignore_ascii_case("auto") => {
Ok(CssValue::Length(Length::Auto))
}
_ => Err(p.new_custom_error(())),
}
});
match track {
Ok(t) => tracks.push(t),
Err(_) => break,
}
}
match tracks.len() {
0 => Ok(CssValue::Keyword("auto".into())),
1 => {
match tracks.into_iter().next() {
Some(v) => Ok(v),
None => Ok(CssValue::Keyword("auto".into())),
}
}
_ => Ok(CssValue::List(tracks)),
}
}
fn parse_grid_placement(input: &mut Parser<'_, '_>) -> Result<CssValue, TcssError> {
if let Ok(val) = input.try_parse(|p| -> Result<CssValue, cssparser::ParseError<'_, ()>> {
p.expect_ident_matching("span")?;
let n = p.expect_integer()?;
Ok(CssValue::Keyword(format!("span {n}")))
}) {
return Ok(val);
}
if let Ok(val) = input.try_parse(|p| -> Result<CssValue, cssparser::ParseError<'_, ()>> {
let start = p.expect_integer()?;
p.expect_delim('/')?;
let end = p.expect_integer()?;
Ok(CssValue::Keyword(format!("{start} / {end}")))
}) {
return Ok(val);
}
parse_integer(input).map(CssValue::Integer)
}
pub fn parse_property_value(
property: &PropertyName,
input: &mut Parser<'_, '_>,
) -> Result<CssValue, TcssError> {
if let Some(var) = try_parse_variable(input) {
return Ok(var);
}
match property {
PropertyName::Color | PropertyName::Background | PropertyName::BorderColor => {
parse_color(input).map(CssValue::Color)
}
PropertyName::Width
| PropertyName::Height
| PropertyName::MinWidth
| PropertyName::MaxWidth
| PropertyName::MinHeight
| PropertyName::MaxHeight
| PropertyName::FlexBasis
| PropertyName::Gap => parse_length(input).map(CssValue::Length),
PropertyName::Margin
| PropertyName::MarginTop
| PropertyName::MarginRight
| PropertyName::MarginBottom
| PropertyName::MarginLeft
| PropertyName::Padding
| PropertyName::PaddingTop
| PropertyName::PaddingRight
| PropertyName::PaddingBottom
| PropertyName::PaddingLeft => parse_length(input).map(CssValue::Length),
PropertyName::FlexGrow | PropertyName::FlexShrink => {
parse_integer(input).map(CssValue::Integer)
}
PropertyName::Opacity => parse_float(input).map(CssValue::Float),
PropertyName::GridTemplateColumns | PropertyName::GridTemplateRows => {
parse_grid_template(input)
}
PropertyName::GridColumn | PropertyName::GridRow => parse_grid_placement(input),
PropertyName::Display
| PropertyName::FlexDirection
| PropertyName::FlexWrap
| PropertyName::JustifyContent
| PropertyName::AlignItems
| PropertyName::AlignSelf
| PropertyName::Dock
| PropertyName::Overflow
| PropertyName::OverflowX
| PropertyName::OverflowY
| PropertyName::Visibility
| PropertyName::TextAlign
| PropertyName::ContentAlign
| PropertyName::TextStyle
| PropertyName::Border
| PropertyName::BorderTop
| PropertyName::BorderRight
| PropertyName::BorderBottom
| PropertyName::BorderLeft => parse_keyword(input).map(CssValue::Keyword),
}
}
pub fn parse_stylesheet(input: &str) -> Result<Stylesheet, TcssError> {
let mut parser_input = ParserInput::new(input);
let mut parser = Parser::new(&mut parser_input);
let mut stylesheet = Stylesheet::new();
while !parser.is_exhausted() {
match parse_rule(&mut parser) {
Ok(rule) => stylesheet.add_rule(rule),
Err(_) => {
let _ = skip_to_next_rule(&mut parser);
}
}
}
Ok(stylesheet)
}
enum BlockItem {
Declaration(Declaration),
Variable(VariableDefinition),
}
fn parse_rule(input: &mut Parser<'_, '_>) -> Result<Rule, TcssError> {
let selectors = SelectorList::parse_from(input)?;
input
.expect_curly_bracket_block()
.map_err(|e| TcssError::Parse(format!("expected '{{': {e:?}")))?;
let items: Result<Vec<BlockItem>, cssparser::ParseError<'_, ()>> =
input.parse_nested_block(|input| {
let mut items = Vec::new();
while !input.is_exhausted() {
if let Ok(vardef) = try_parse_variable_definition(input) {
items.push(BlockItem::Variable(vardef));
continue;
}
match parse_declaration_inner(input) {
Ok(decl) => items.push(BlockItem::Declaration(decl)),
Err(_) => {
while input.next().is_ok_and(|t| !matches!(t, Token::Semicolon)) {}
}
}
}
Ok(items)
});
let items = items.map_err(|e| TcssError::Parse(format!("{e:?}")))?;
let mut declarations = Vec::new();
let mut variables = Vec::new();
for item in items {
match item {
BlockItem::Declaration(d) => declarations.push(d),
BlockItem::Variable(v) => variables.push(v),
}
}
if variables.is_empty() {
Ok(Rule::new(selectors, declarations))
} else {
Ok(Rule::with_variables(selectors, declarations, variables))
}
}
fn try_parse_variable_definition<'i>(
input: &mut Parser<'i, '_>,
) -> Result<VariableDefinition, cssparser::ParseError<'i, ()>> {
input.try_parse(|p| {
p.expect_delim('$')?;
let name = p.expect_ident()?.to_string();
p.expect_colon()?;
let value = parse_variable_value(p)?;
let _ = p.try_parse(|p| p.expect_semicolon());
Ok(VariableDefinition { name, value })
})
}
fn parse_variable_value<'i>(
input: &mut Parser<'i, '_>,
) -> Result<CssValue, cssparser::ParseError<'i, ()>> {
if let Ok(var) = input.try_parse(|p| -> Result<CssValue, cssparser::ParseError<'_, ()>> {
p.expect_delim('$')?;
let name = p.expect_ident()?.to_string();
Ok(CssValue::Variable(name))
}) {
return Ok(var);
}
let token = input.next()?.clone();
match &token {
Token::Ident(name) => {
let name_str = name.to_string();
if name_str.eq_ignore_ascii_case("auto") {
Ok(CssValue::Length(Length::Auto))
} else if let Some(color) = Color::from_css_name(&name_str) {
Ok(CssValue::Color(color))
} else {
Ok(CssValue::Keyword(name_str))
}
}
Token::Hash(hash) | Token::IDHash(hash) => {
let hash_str = hash.to_string();
Color::from_hex(&hash_str)
.map(CssValue::Color)
.map_err(|_| input.new_custom_error(()))
}
Token::Number {
int_value: Some(v), ..
} => {
let val = u16::try_from(*v).map_err(|_| input.new_custom_error(()))?;
Ok(CssValue::Length(Length::Cells(val)))
}
Token::Number { value, .. } => Ok(CssValue::Float(*value)),
Token::Percentage { unit_value, .. } => {
Ok(CssValue::Length(Length::Percent(*unit_value * 100.0)))
}
Token::Function(name) if name.eq_ignore_ascii_case("rgb") => parse_rgb_block(input)
.map(CssValue::Color)
.map_err(|_| input.new_custom_error(())),
_ => Err(input.new_custom_error(())),
}
}
fn parse_declaration_inner<'i>(
input: &mut Parser<'i, '_>,
) -> Result<Declaration, cssparser::ParseError<'i, ()>> {
let name = input.expect_ident()?.to_string();
input.expect_colon()?;
let property = PropertyName::from_css(&name).ok_or_else(|| input.new_custom_error(()))?;
let value = parse_property_value(&property, input).map_err(|_| input.new_custom_error(()))?;
let important = input
.try_parse(|p| -> Result<(), cssparser::ParseError<'_, ()>> {
p.expect_delim('!')?;
p.expect_ident_matching("important")?;
Ok(())
})
.is_ok();
let _ = input.try_parse(|p| p.expect_semicolon());
Ok(Declaration {
property,
value,
important,
})
}
fn skip_to_next_rule(input: &mut Parser<'_, '_>) -> Result<(), ()> {
if input.expect_curly_bracket_block().is_ok() {
let _ = input.parse_nested_block(|input| -> Result<(), cssparser::ParseError<'_, ()>> {
while input.next().is_ok() {}
Ok(())
});
return Ok(());
}
if input.next().is_ok() {
Ok(())
} else {
Err(())
}
}
pub fn extract_root_variables(stylesheet: &Stylesheet) -> VariableMap {
use crate::tcss::selector::{PseudoClass, SimpleSelector};
let mut vars = VariableMap::new();
for rule in stylesheet.rules() {
let is_root = rule.selectors.selectors.iter().any(|sel| {
sel.chain.is_empty()
&& sel.head.components.len() == 1
&& matches!(
&sel.head.components[0],
SimpleSelector::PseudoClass(PseudoClass::Root)
)
});
if is_root {
for vardef in &rule.variables {
vars.set(&vardef.name, vardef.value.clone());
}
}
}
vars
}
pub fn parse_declaration(input_str: &str) -> Result<Declaration, TcssError> {
let mut parser_input = ParserInput::new(input_str);
let mut parser = Parser::new(&mut parser_input);
let result: Result<Declaration, cssparser::ParseError<'_, ()>> =
parse_declaration_inner(&mut parser);
result.map_err(|e| TcssError::Parse(format!("{e:?}")))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::NamedColor;
fn parse_with<T>(css: &str, f: impl FnOnce(&mut Parser<'_, '_>) -> T) -> T {
let mut input = ParserInput::new(css);
let mut parser = Parser::new(&mut input);
f(&mut parser)
}
#[test]
fn parse_named_color() {
let result = parse_with("red", parse_color);
assert_eq!(result, Ok(Color::Named(NamedColor::Red)));
}
#[test]
fn parse_named_color_blue() {
let result = parse_with("blue", parse_color);
assert_eq!(result, Ok(Color::Named(NamedColor::Blue)));
}
#[test]
fn parse_hex_color_short() {
let result = parse_with("#fff", parse_color);
assert_eq!(
result,
Ok(Color::Rgb {
r: 255,
g: 255,
b: 255
})
);
}
#[test]
fn parse_hex_color_long() {
let result = parse_with("#1e1e2e", parse_color);
assert_eq!(
result,
Ok(Color::Rgb {
r: 30,
g: 30,
b: 46
})
);
}
#[test]
fn parse_rgb_function() {
let result = parse_with("rgb(255, 0, 128)", parse_color);
assert_eq!(
result,
Ok(Color::Rgb {
r: 255,
g: 0,
b: 128
})
);
}
#[test]
fn parse_invalid_color() {
let result = parse_with("notacolor", parse_color);
assert!(result.is_err());
}
#[test]
fn parse_cell_length() {
let result = parse_with("10", parse_length);
assert_eq!(result, Ok(Length::Cells(10)));
}
#[test]
fn parse_percent_length() {
let result = parse_with("50%", parse_length);
assert_eq!(result, Ok(Length::Percent(50.0)));
}
#[test]
fn parse_auto_length() {
let result = parse_with("auto", parse_length);
assert_eq!(result, Ok(Length::Auto));
}
#[test]
fn parse_integer_value() {
let result = parse_with("42", parse_integer);
assert_eq!(result, Ok(42));
}
#[test]
fn parse_float_value() {
let result = parse_with("0.5", parse_float);
assert_eq!(result, Ok(0.5));
}
#[test]
fn parse_keyword_value() {
let result = parse_with("flex", parse_keyword);
assert_eq!(result, Ok("flex".into()));
}
#[test]
fn parse_property_color() {
let result = parse_with("red", |p| parse_property_value(&PropertyName::Color, p));
assert_eq!(result, Ok(CssValue::Color(Color::Named(NamedColor::Red))));
}
#[test]
fn parse_property_width() {
let result = parse_with("20", |p| parse_property_value(&PropertyName::Width, p));
assert_eq!(result, Ok(CssValue::Length(Length::Cells(20))));
}
#[test]
fn parse_property_display() {
let result = parse_with("flex", |p| parse_property_value(&PropertyName::Display, p));
assert_eq!(result, Ok(CssValue::Keyword("flex".into())));
}
#[test]
fn parse_property_flex_grow() {
let result = parse_with("2", |p| parse_property_value(&PropertyName::FlexGrow, p));
assert_eq!(result, Ok(CssValue::Integer(2)));
}
#[test]
fn parse_property_opacity() {
let result = parse_with("0.8", |p| parse_property_value(&PropertyName::Opacity, p));
assert_eq!(result, Ok(CssValue::Float(0.8)));
}
#[test]
fn parse_property_background_hex() {
let result = parse_with("#ff0000", |p| {
parse_property_value(&PropertyName::Background, p)
});
assert_eq!(
result,
Ok(CssValue::Color(Color::Rgb { r: 255, g: 0, b: 0 }))
);
}
#[test]
fn parse_property_margin_percent() {
let result = parse_with("25%", |p| parse_property_value(&PropertyName::Margin, p));
assert_eq!(result, Ok(CssValue::Length(Length::Percent(25.0))));
}
fn parse_sheet(css: &str) -> Stylesheet {
let result = parse_stylesheet(css);
assert!(
result.is_ok(),
"parse_stylesheet failed for input: {result:?}"
);
match result {
Ok(sheet) => sheet,
Err(_) => unreachable!(),
}
}
fn parse_decl(css: &str) -> Declaration {
let result = parse_declaration(css);
assert!(
result.is_ok(),
"parse_declaration failed for input: {result:?}"
);
match result {
Ok(decl) => decl,
Err(_) => unreachable!(),
}
}
#[test]
fn parse_empty_stylesheet() {
let sheet = parse_sheet("");
assert!(sheet.is_empty());
}
#[test]
fn parse_whitespace_only_stylesheet() {
let sheet = parse_sheet(" \n\t ");
assert!(sheet.is_empty());
}
#[test]
fn parse_single_rule() {
let sheet = parse_sheet("Label { color: red; }");
assert_eq!(sheet.len(), 1);
let rule = &sheet.rules()[0];
assert_eq!(rule.declarations.len(), 1);
assert_eq!(rule.declarations[0].property, PropertyName::Color);
assert_eq!(
rule.declarations[0].value,
CssValue::Color(Color::Named(NamedColor::Red))
);
}
#[test]
fn parse_multiple_declarations() {
let sheet = parse_sheet("Label { color: red; background: blue; }");
assert_eq!(sheet.len(), 1);
let rule = &sheet.rules()[0];
assert_eq!(rule.declarations.len(), 2);
assert_eq!(rule.declarations[0].property, PropertyName::Color);
assert_eq!(rule.declarations[1].property, PropertyName::Background);
}
#[test]
fn parse_multiple_rules() {
let sheet = parse_sheet("Label { color: red; } Container { background: blue; }");
assert_eq!(sheet.len(), 2);
}
#[test]
fn parse_important_declaration() {
let sheet = parse_sheet("Label { color: red !important; }");
assert_eq!(sheet.len(), 1);
assert!(sheet.rules()[0].declarations[0].important);
}
#[test]
fn parse_with_comments() {
let sheet = parse_sheet("/* theme */ Label { color: red; }");
assert_eq!(sheet.len(), 1);
}
#[test]
fn parse_complex_selector_rule() {
let sheet = parse_sheet("Container > Label { color: red; }");
assert_eq!(sheet.len(), 1);
let selector = &sheet.rules()[0].selectors.selectors[0];
assert!(!selector.chain.is_empty());
}
#[test]
fn parse_selector_list_rule() {
let sheet = parse_sheet("Label, Container { color: red; }");
assert_eq!(sheet.len(), 1);
assert_eq!(sheet.rules()[0].selectors.selectors.len(), 2);
}
#[test]
fn parse_all_property_types() {
let css = r#"
Label {
color: red;
width: 20;
display: flex;
flex-grow: 2;
opacity: 0.5;
}
"#;
let sheet = parse_sheet(css);
assert_eq!(sheet.len(), 1);
let decls = &sheet.rules()[0].declarations;
assert_eq!(decls.len(), 5);
assert!(matches!(decls[0].value, CssValue::Color(_)));
assert!(matches!(decls[1].value, CssValue::Length(_)));
assert!(matches!(decls[2].value, CssValue::Keyword(_)));
assert!(matches!(decls[3].value, CssValue::Integer(_)));
assert!(matches!(decls[4].value, CssValue::Float(_)));
}
#[test]
fn parse_invalid_rule_skipped() {
let sheet = parse_sheet("!!! invalid { } Label { color: red; }");
assert!(!sheet.is_empty());
}
#[test]
fn parse_real_world_stylesheet() {
let css = r#"
/* Base styles */
Label {
color: white;
}
Container {
background: #1e1e2e;
padding: 1;
}
.error {
color: red;
}
#sidebar {
width: 30;
background: #2e2e3e;
}
Container > Label.title {
color: blue;
text-style: bold;
}
"#;
let sheet = parse_sheet(css);
assert_eq!(sheet.len(), 5);
}
#[test]
fn parse_declaration_standalone() {
let decl = parse_decl("color: red");
assert_eq!(decl.property, PropertyName::Color);
assert_eq!(decl.value, CssValue::Color(Color::Named(NamedColor::Red)));
assert!(!decl.important);
}
#[test]
fn parse_declaration_important_standalone() {
let decl = parse_decl("color: red !important");
assert_eq!(decl.property, PropertyName::Color);
assert!(decl.important);
}
#[test]
fn parse_variable_reference() {
let result = parse_with("$primary", |p| {
parse_property_value(&PropertyName::Color, p)
});
assert_eq!(result, Ok(CssValue::Variable("primary".into())));
}
#[test]
fn parse_variable_in_color() {
let sheet = parse_sheet("Label { color: $fg; }");
assert_eq!(sheet.len(), 1);
assert_eq!(
sheet.rules()[0].declarations[0].value,
CssValue::Variable("fg".into())
);
}
#[test]
fn parse_variable_in_width() {
let sheet = parse_sheet("Label { width: $sidebar-width; }");
assert_eq!(sheet.len(), 1);
assert_eq!(
sheet.rules()[0].declarations[0].value,
CssValue::Variable("sidebar-width".into())
);
}
#[test]
fn parse_variable_hyphenated() {
let result = parse_with("$my-var-name", |p| {
parse_property_value(&PropertyName::Color, p)
});
assert_eq!(result, Ok(CssValue::Variable("my-var-name".into())));
}
#[test]
fn parse_non_variable_still_works() {
let result = parse_with("red", |p| parse_property_value(&PropertyName::Color, p));
assert_eq!(result, Ok(CssValue::Color(Color::Named(NamedColor::Red))));
}
#[test]
fn parse_variable_definition_color() {
let css = ":root { $primary: red; }";
let sheet = parse_sheet(css);
assert_eq!(sheet.len(), 1);
assert_eq!(sheet.rules()[0].variables.len(), 1);
assert_eq!(sheet.rules()[0].variables[0].name, "primary");
assert_eq!(
sheet.rules()[0].variables[0].value,
CssValue::Color(Color::Named(NamedColor::Red))
);
}
#[test]
fn parse_variable_definition_hex() {
let css = ":root { $bg: #1e1e2e; }";
let sheet = parse_sheet(css);
assert_eq!(sheet.rules()[0].variables.len(), 1);
assert_eq!(
sheet.rules()[0].variables[0].value,
CssValue::Color(Color::Rgb {
r: 30,
g: 30,
b: 46
})
);
}
#[test]
fn parse_variable_definition_length() {
let css = ":root { $width: 30; }";
let sheet = parse_sheet(css);
assert_eq!(sheet.rules()[0].variables.len(), 1);
assert_eq!(
sheet.rules()[0].variables[0].value,
CssValue::Length(Length::Cells(30))
);
}
#[test]
fn parse_root_block_multiple() {
let css = ":root { $fg: white; $bg: #1e1e2e; }";
let sheet = parse_sheet(css);
assert_eq!(sheet.len(), 1);
assert_eq!(sheet.rules()[0].variables.len(), 2);
}
#[test]
fn parse_extract_root_variables() {
let css = r#"
:root { $fg: white; $bg: #1e1e2e; }
Label { color: $fg; }
"#;
let sheet = parse_sheet(css);
let vars = extract_root_variables(&sheet);
assert_eq!(vars.len(), 2);
assert!(vars.contains("fg"));
assert!(vars.contains("bg"));
}
#[test]
fn parse_mixed_variables_and_properties() {
let css = "Label { $theme-color: red; color: $theme-color; width: 20; }";
let sheet = parse_sheet(css);
assert_eq!(sheet.rules()[0].variables.len(), 1);
assert_eq!(sheet.rules()[0].declarations.len(), 2);
}
#[test]
fn parse_variable_in_non_root_block() {
let css = ".dark { $fg: white; $bg: #1e1e2e; }";
let sheet = parse_sheet(css);
assert_eq!(sheet.rules()[0].variables.len(), 2);
}
#[test]
fn parse_grid_template_single_fr() {
let result = parse_with("1fr", |p| {
parse_property_value(&PropertyName::GridTemplateColumns, p)
});
assert_eq!(result, Ok(CssValue::Fr(1.0)));
}
#[test]
fn parse_grid_template_multiple_fr() {
let result = parse_with("1fr 2fr 1fr", |p| {
parse_property_value(&PropertyName::GridTemplateColumns, p)
});
assert!(result.is_ok());
let val = match result {
Ok(v) => v,
Err(_) => unreachable!(),
};
assert!(matches!(val, CssValue::List(v) if v.len() == 3));
}
#[test]
fn parse_grid_template_mixed() {
let result = parse_with("1fr 100 2fr", |p| {
parse_property_value(&PropertyName::GridTemplateColumns, p)
});
assert!(result.is_ok());
let val = match result {
Ok(v) => v,
Err(_) => unreachable!(),
};
match val {
CssValue::List(ref items) => {
assert_eq!(items.len(), 3);
assert_eq!(items[0], CssValue::Fr(1.0));
assert_eq!(items[1], CssValue::Length(Length::Cells(100)));
assert_eq!(items[2], CssValue::Fr(2.0));
}
_ => panic!("expected CssValue::List"),
}
}
#[test]
fn parse_grid_template_with_percent() {
let result = parse_with("25% 1fr 25%", |p| {
parse_property_value(&PropertyName::GridTemplateRows, p)
});
assert!(result.is_ok());
let val = match result {
Ok(v) => v,
Err(_) => unreachable!(),
};
assert!(matches!(val, CssValue::List(v) if v.len() == 3));
}
#[test]
fn parse_grid_template_auto() {
let result = parse_with("auto 1fr auto", |p| {
parse_property_value(&PropertyName::GridTemplateColumns, p)
});
assert!(result.is_ok());
let val = match result {
Ok(v) => v,
Err(_) => unreachable!(),
};
match val {
CssValue::List(ref items) => {
assert_eq!(items.len(), 3);
assert_eq!(items[0], CssValue::Length(Length::Auto));
assert_eq!(items[2], CssValue::Length(Length::Auto));
}
_ => panic!("expected CssValue::List"),
}
}
#[test]
fn parse_grid_template_single_cells() {
let result = parse_with("100", |p| {
parse_property_value(&PropertyName::GridTemplateColumns, p)
});
assert_eq!(result, Ok(CssValue::Length(Length::Cells(100))));
}
#[test]
fn parse_grid_placement_integer() {
let result = parse_with("2", |p| parse_property_value(&PropertyName::GridColumn, p));
assert_eq!(result, Ok(CssValue::Integer(2)));
}
#[test]
fn parse_grid_placement_span() {
let result = parse_with("span 3", |p| {
parse_property_value(&PropertyName::GridColumn, p)
});
assert_eq!(result, Ok(CssValue::Keyword("span 3".into())));
}
#[test]
fn parse_grid_placement_range() {
let result = parse_with("1 / 3", |p| parse_property_value(&PropertyName::GridRow, p));
assert_eq!(result, Ok(CssValue::Keyword("1 / 3".into())));
}
#[test]
fn parse_fr_in_stylesheet() {
let css = "Container { grid-template-columns: 1fr 2fr; }";
let sheet = parse_sheet(css);
assert_eq!(sheet.len(), 1);
let val = &sheet.rules()[0].declarations[0].value;
assert!(matches!(val, CssValue::List(v) if v.len() == 2));
}
#[test]
fn parse_multiple_root_blocks_merge() {
let css = r#"
:root { $fg: white; }
:root { $fg: red; $bg: black; }
"#;
let sheet = parse_sheet(css);
let vars = extract_root_variables(&sheet);
assert_eq!(
vars.get("fg"),
Some(&CssValue::Color(Color::Named(NamedColor::Red)))
);
assert!(vars.contains("bg"));
}
}