use crate::ast::{
GeneralEnclosed, MediaCondition, MediaConditionWithoutOr, MediaFeature, MediaInParens,
MediaModifier, MediaQuery, MediaType, MfComparison, MfName, MfRange, MfRangeDirection, MfValue,
};
use crate::tokenizer::{NumericType, Token, tokenize};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum ParseError {
UnbalancedParens,
EmptyMediaQuery,
TrailingTokens(Token),
ExpectedMediaType(Option<Token>),
ReservedMediaType(String),
ExpectedMediaInParens(Option<Token>),
ExpectedMfName(Option<Token>),
ExpectedColon(Option<Token>),
ExpectedMfValue(Option<Token>),
InvalidRatio,
ExpectedMfComparison(Option<Token>),
InvalidMfRangeInterval,
ExpectedKeyword {
keyword: &'static str,
found: Option<Token>,
},
}
fn matching_close(token: &Token) -> Option<Token> {
match token {
Token::OpenParen | Token::Function(_) => Some(Token::CloseParen),
Token::OpenSquare => Some(Token::CloseSquare),
Token::OpenCurly => Some(Token::CloseCurly),
_ => None,
}
}
fn is_ident_ci(token: Option<&Token>, keyword: &str) -> bool {
matches!(token, Some(Token::Ident(s)) if s.eq_ignore_ascii_case(keyword))
}
type PositionedToken = (Token, bool);
struct Parser<'a> {
tokens: &'a [PositionedToken],
pos: usize,
}
impl<'a> Parser<'a> {
fn new(tokens: &'a [PositionedToken]) -> Self {
Self { tokens, pos: 0 }
}
fn peek(&self) -> Option<&Token> {
self.tokens.get(self.pos).map(|(token, _)| token)
}
fn peek_at(&self, offset: usize) -> Option<&Token> {
self.tokens.get(self.pos + offset).map(|(token, _)| token)
}
fn advance(&mut self) -> Option<Token> {
let token = self.peek().cloned();
if token.is_some() {
self.pos += 1;
}
token
}
fn is_at_end(&self) -> bool {
self.pos == self.tokens.len()
}
fn current_preceded_by_whitespace(&self) -> bool {
self.tokens
.get(self.pos)
.map(|(_, preceded_by_ws)| *preceded_by_ws)
.unwrap_or(false)
}
fn consume_block(&mut self) -> Result<Vec<PositionedToken>, ParseError> {
let opener = self.tokens[self.pos].0.clone();
let mut closers = vec![matching_close(&opener).expect("caller checked an opener")];
self.pos += 1;
let mut inner = Vec::new();
loop {
match self.tokens.get(self.pos).cloned() {
None => return Err(ParseError::UnbalancedParens),
Some((token, preceded_by_ws)) => {
let closed_outermost = track_bracket_depth(&token, &mut closers);
self.pos += 1;
if closed_outermost {
return Ok(inner);
}
inner.push((token, preceded_by_ws));
}
}
}
}
}
fn track_bracket_depth(token: &Token, closers: &mut Vec<Token>) -> bool {
if Some(token) == closers.last() {
closers.pop();
closers.is_empty()
} else {
if let Some(closer) = matching_close(token) {
closers.push(closer);
}
false
}
}
fn parse_fully<T>(
tokens: &[PositionedToken],
production: impl Fn(&mut Parser) -> Result<T, ParseError>,
) -> Result<T, ParseError> {
let mut parser = Parser::new(tokens);
let result = production(&mut parser)?;
if parser.is_at_end() {
Ok(result)
} else {
Err(ParseError::TrailingTokens(
parser.peek().expect("not at end").clone(),
))
}
}
fn expect_end(parser: &Parser) -> Result<(), ParseError> {
match parser.peek() {
None => Ok(()),
Some(token) => Err(ParseError::TrailingTokens(token.clone())),
}
}
fn consume_keyword(parser: &mut Parser, keyword: &'static str) -> Result<(), ParseError> {
match parser.advance() {
Some(Token::Ident(s)) if s.eq_ignore_ascii_case(keyword) => Ok(()),
found => Err(ParseError::ExpectedKeyword { keyword, found }),
}
}
fn media_not(parser: &mut Parser) -> Result<MediaInParens, ParseError> {
consume_keyword(parser, "not")?;
media_in_parens(parser)
}
fn media_and(parser: &mut Parser) -> Result<MediaInParens, ParseError> {
consume_keyword(parser, "and")?;
media_in_parens(parser)
}
fn media_or(parser: &mut Parser) -> Result<MediaInParens, ParseError> {
consume_keyword(parser, "or")?;
media_in_parens(parser)
}
fn in_parens_chain(
parser: &mut Parser,
first: MediaInParens,
keyword: &'static str,
next: impl Fn(&mut Parser) -> Result<MediaInParens, ParseError>,
) -> Result<Vec<MediaInParens>, ParseError> {
let mut items = vec![first];
while is_ident_ci(parser.peek(), keyword) {
items.push(next(parser)?);
}
Ok(items)
}
fn media_not_or_first_in_parens(parser: &mut Parser) -> Result<(bool, MediaInParens), ParseError> {
if is_ident_ci(parser.peek(), "not") {
return Ok((true, media_not(parser)?));
}
Ok((false, media_in_parens(parser)?))
}
fn media_condition(parser: &mut Parser) -> Result<MediaCondition, ParseError> {
let (is_not, first) = media_not_or_first_in_parens(parser)?;
if is_not {
return Ok(MediaCondition::Not(first));
}
if is_ident_ci(parser.peek(), "or") {
return Ok(MediaCondition::Or(in_parens_chain(
parser, first, "or", media_or,
)?));
}
Ok(MediaCondition::And(in_parens_chain(
parser, first, "and", media_and,
)?))
}
fn media_condition_without_or(parser: &mut Parser) -> Result<MediaConditionWithoutOr, ParseError> {
let (is_not, first) = media_not_or_first_in_parens(parser)?;
if is_not {
return Ok(MediaConditionWithoutOr::Not(first));
}
Ok(MediaConditionWithoutOr::And(in_parens_chain(
parser, first, "and", media_and,
)?))
}
fn media_in_parens(parser: &mut Parser) -> Result<MediaInParens, ParseError> {
match parser.peek() {
Some(Token::OpenParen) => {
let inner = parser.consume_block()?;
if let Ok(condition) = parse_fully(&inner, media_condition) {
return Ok(MediaInParens::Condition(Box::new(condition)));
}
if let Ok(feature) = parse_fully(&inner, media_feature) {
return Ok(MediaInParens::Feature(feature));
}
Ok(MediaInParens::GeneralEnclosed(GeneralEnclosed {
tokens: inner.into_iter().map(|(token, _)| token).collect(),
}))
}
Some(Token::Function(_)) => {
let function_token = parser.tokens[parser.pos].0.clone();
let inner = parser.consume_block()?;
let mut tokens = vec![function_token];
tokens.extend(inner.into_iter().map(|(token, _)| token));
Ok(MediaInParens::GeneralEnclosed(GeneralEnclosed { tokens }))
}
other => Err(ParseError::ExpectedMediaInParens(other.cloned())),
}
}
fn mf_name(parser: &mut Parser) -> Result<MfName, ParseError> {
match parser.advance() {
Some(Token::Ident(name)) => Ok(MfName(name)),
other => Err(ParseError::ExpectedMfName(other)),
}
}
fn mf_value(parser: &mut Parser) -> Result<MfValue, ParseError> {
match parser.advance() {
Some(Token::Number { value, type_flag }) => {
if matches!(parser.peek(), Some(Token::Delim('/'))) {
parser.advance();
match parser.advance() {
Some(Token::Number {
value: denominator,
type_flag: NumericType::Integer,
}) if type_flag == NumericType::Integer
&& value >= 0.0
&& denominator >= 0.0
&& value <= u32::MAX as f64
&& denominator <= u32::MAX as f64 =>
{
Ok(MfValue::Ratio {
numerator: value as u32,
denominator: denominator as u32,
})
}
_ => Err(ParseError::InvalidRatio),
}
} else {
Ok(MfValue::Number(value))
}
}
Some(Token::Dimension { value, unit, .. }) => Ok(MfValue::Dimension { value, unit }),
Some(Token::Ident(name)) => Ok(MfValue::Ident(name)),
other => Err(ParseError::ExpectedMfValue(other)),
}
}
fn starts_mf_comparison(token: Option<&Token>) -> bool {
matches!(token, Some(Token::Delim('<' | '>' | '=')))
}
fn mf_comparison(parser: &mut Parser) -> Result<MfComparison, ParseError> {
match parser.advance() {
Some(Token::Delim('<')) => {
if parser.peek() == Some(&Token::Delim('=')) && !parser.current_preceded_by_whitespace()
{
parser.advance();
Ok(MfComparison::Le)
} else {
Ok(MfComparison::Lt)
}
}
Some(Token::Delim('>')) => {
if parser.peek() == Some(&Token::Delim('=')) && !parser.current_preceded_by_whitespace()
{
parser.advance();
Ok(MfComparison::Ge)
} else {
Ok(MfComparison::Gt)
}
}
Some(Token::Delim('=')) => Ok(MfComparison::Eq),
other => Err(ParseError::ExpectedMfComparison(other)),
}
}
fn media_feature(parser: &mut Parser) -> Result<MediaFeature, ParseError> {
if matches!(parser.peek(), Some(Token::Ident(_))) {
if matches!(parser.peek_at(1), Some(Token::Colon)) {
let name = mf_name(parser)?;
parser.advance();
return Ok(MediaFeature::Plain {
name,
value: mf_value(parser)?,
});
}
if parser.peek_at(1).is_none() {
return Ok(MediaFeature::Boolean(mf_name(parser)?));
}
if starts_mf_comparison(parser.peek_at(1)) {
let name = mf_name(parser)?;
let operator = mf_comparison(parser)?;
let value = mf_value(parser)?;
return Ok(MediaFeature::Range(MfRange::NameFirst {
name,
operator,
value,
}));
}
}
let value = mf_value(parser)?;
let operator = mf_comparison(parser)?;
let name = mf_name(parser)?;
if parser.is_at_end() {
return Ok(MediaFeature::Range(MfRange::ValueFirst {
value,
operator,
name,
}));
}
let (direction, lower_inclusive) = match operator {
MfComparison::Lt => (MfRangeDirection::Ascending, false),
MfComparison::Le => (MfRangeDirection::Ascending, true),
MfComparison::Gt => (MfRangeDirection::Descending, false),
MfComparison::Ge => (MfRangeDirection::Descending, true),
MfComparison::Eq => return Err(ParseError::InvalidMfRangeInterval),
};
let upper_inclusive = match (direction, mf_comparison(parser)?) {
(MfRangeDirection::Ascending, MfComparison::Lt) => false,
(MfRangeDirection::Ascending, MfComparison::Le) => true,
(MfRangeDirection::Descending, MfComparison::Gt) => false,
(MfRangeDirection::Descending, MfComparison::Ge) => true,
_ => return Err(ParseError::InvalidMfRangeInterval),
};
let upper = mf_value(parser)?;
Ok(MediaFeature::Range(MfRange::Interval {
lower: value,
lower_inclusive,
name,
upper_inclusive,
upper,
direction,
}))
}
fn media_type(parser: &mut Parser) -> Result<MediaType, ParseError> {
match parser.advance() {
Some(Token::Ident(name)) => {
let lower = name.to_ascii_lowercase();
if matches!(lower.as_str(), "not" | "and" | "or" | "only" | "layer") {
Err(ParseError::ReservedMediaType(name))
} else {
Ok(MediaType(name))
}
}
other => Err(ParseError::ExpectedMediaType(other)),
}
}
fn media_query_type_branch(parser: &mut Parser) -> Result<MediaQuery, ParseError> {
let modifier = match parser.peek() {
Some(Token::Ident(s)) if s.eq_ignore_ascii_case("not") => {
parser.advance();
Some(MediaModifier::Not)
}
Some(Token::Ident(s)) if s.eq_ignore_ascii_case("only") => {
parser.advance();
Some(MediaModifier::Only)
}
_ => None,
};
let media_type = media_type(parser)?;
let condition = if is_ident_ci(parser.peek(), "and") {
parser.advance();
Some(media_condition_without_or(parser)?)
} else {
None
};
Ok(MediaQuery::TypeQuery {
modifier,
media_type,
condition,
})
}
fn media_query(parser: &mut Parser) -> Result<MediaQuery, ParseError> {
let starts_condition = matches!(
parser.peek(),
Some(Token::OpenParen) | Some(Token::Function(_))
) || (is_ident_ci(parser.peek(), "not")
&& matches!(
parser.peek_at(1),
Some(Token::OpenParen) | Some(Token::Function(_))
));
if starts_condition {
Ok(MediaQuery::Condition(media_condition(parser)?))
} else {
media_query_type_branch(parser)
}
}
fn prepare_tokens(input: &str) -> Vec<PositionedToken> {
let mut result = Vec::new();
let mut preceded_by_whitespace = false;
for token in tokenize(input) {
match token {
Token::Whitespace => preceded_by_whitespace = true,
Token::Eof => {}
other => {
result.push((other, preceded_by_whitespace));
preceded_by_whitespace = false;
}
}
}
result
}
fn split_top_level_commas(tokens: &[PositionedToken]) -> Vec<Vec<PositionedToken>> {
let mut segments = Vec::new();
let mut current = Vec::new();
let mut closers: Vec<Token> = Vec::new();
for (token, preceded_by_whitespace) in tokens {
if closers.is_empty() && *token == Token::Comma {
segments.push(std::mem::take(&mut current));
continue;
}
track_bracket_depth(token, &mut closers);
current.push((token.clone(), *preceded_by_whitespace));
}
segments.push(current);
segments
}
fn parse_media_query_tokens(tokens: &[PositionedToken]) -> Result<MediaQuery, ParseError> {
if tokens.is_empty() {
return Err(ParseError::EmptyMediaQuery);
}
let mut parser = Parser::new(tokens);
let query = media_query(&mut parser)?;
expect_end(&parser)?;
Ok(query)
}
pub fn parse_media_query(input: &str) -> Result<MediaQuery, ParseError> {
parse_media_query_tokens(&prepare_tokens(input))
}
pub fn parse_media_query_list(input: &str) -> Vec<Result<MediaQuery, ParseError>> {
let tokens = prepare_tokens(input);
split_top_level_commas(&tokens)
.into_iter()
.map(|segment| parse_media_query_tokens(&segment))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::MediaCondition::*;
use crate::ast::MediaInParens as InParens;
fn feature_boolean(name: &str) -> InParens {
InParens::Feature(MediaFeature::Boolean(MfName(name.into())))
}
fn feature_plain(name: &str, value: MfValue) -> InParens {
InParens::Feature(MediaFeature::Plain {
name: MfName(name.into()),
value,
})
}
fn feature_range(range: MfRange) -> InParens {
InParens::Feature(MediaFeature::Range(range))
}
fn dim(value: f64, unit: &str) -> MfValue {
MfValue::Dimension {
value,
unit: unit.into(),
}
}
#[test]
fn media_query_list_multiple_entries() {
let results = parse_media_query_list("screen, print");
assert_eq!(results.len(), 2);
assert_eq!(
results[0],
Ok(MediaQuery::TypeQuery {
modifier: None,
media_type: MediaType("screen".into()),
condition: None,
})
);
assert_eq!(
results[1],
Ok(MediaQuery::TypeQuery {
modifier: None,
media_type: MediaType("print".into()),
condition: None,
})
);
}
#[test]
fn media_query_type_bare() {
assert_eq!(
parse_media_query("screen"),
Ok(MediaQuery::TypeQuery {
modifier: None,
media_type: MediaType("screen".into()),
condition: None,
})
);
}
#[test]
fn media_query_type_with_not_modifier() {
assert_eq!(
parse_media_query("not screen"),
Ok(MediaQuery::TypeQuery {
modifier: Some(MediaModifier::Not),
media_type: MediaType("screen".into()),
condition: None,
})
);
}
#[test]
fn media_query_type_with_only_modifier() {
assert_eq!(
parse_media_query("only screen"),
Ok(MediaQuery::TypeQuery {
modifier: Some(MediaModifier::Only),
media_type: MediaType("screen".into()),
condition: None,
})
);
}
#[test]
fn media_query_type_with_and_condition() {
assert_eq!(
parse_media_query("screen and (color)"),
Ok(MediaQuery::TypeQuery {
modifier: None,
media_type: MediaType("screen".into()),
condition: Some(MediaConditionWithoutOr::And(vec![feature_boolean("color")])),
})
);
}
#[test]
fn media_query_type_with_and_chain_condition() {
assert_eq!(
parse_media_query("screen and (color) and (monochrome)"),
Ok(MediaQuery::TypeQuery {
modifier: None,
media_type: MediaType("screen".into()),
condition: Some(MediaConditionWithoutOr::And(vec![
feature_boolean("color"),
feature_boolean("monochrome"),
])),
})
);
}
#[test]
fn media_condition_without_or_rejects_or() {
assert_eq!(
parse_media_query("screen and (color) or (monochrome)"),
Err(ParseError::TrailingTokens(Token::Ident("or".into())))
);
}
#[test]
fn media_query_condition_shorthand() {
assert_eq!(
parse_media_query("(color)"),
Ok(MediaQuery::Condition(And(vec![feature_boolean("color")])))
);
}
#[test]
fn media_condition_not() {
assert_eq!(
parse_media_query("not (color)"),
Ok(MediaQuery::Condition(Not(feature_boolean("color"))))
);
}
#[test]
fn media_condition_and_chain() {
assert_eq!(
parse_media_query("(color) and (monochrome)"),
Ok(MediaQuery::Condition(And(vec![
feature_boolean("color"),
feature_boolean("monochrome"),
])))
);
}
#[test]
fn media_condition_or_chain() {
assert_eq!(
parse_media_query("(color) or (monochrome)"),
Ok(MediaQuery::Condition(Or(vec![
feature_boolean("color"),
feature_boolean("monochrome"),
])))
);
}
#[test]
fn media_in_parens_condition() {
assert_eq!(
parse_media_query("((color) and (monochrome))"),
Ok(MediaQuery::Condition(And(vec![InParens::Condition(
Box::new(And(vec![
feature_boolean("color"),
feature_boolean("monochrome"),
]))
)])))
);
}
#[test]
fn media_in_parens_feature() {
assert_eq!(
parse_media_query("(width: 400px)"),
Ok(MediaQuery::Condition(And(vec![feature_plain(
"width",
MfValue::Dimension {
value: 400.0,
unit: "px".into()
}
)])))
);
}
#[test]
fn media_in_parens_general_enclosed() {
let Ok(MediaQuery::Condition(And(items))) = parse_media_query("(3 + 5)") else {
panic!("expected a bare condition with one <media-in-parens>");
};
assert_eq!(items.len(), 1);
assert!(matches!(items[0], InParens::GeneralEnclosed(_)));
}
#[test]
fn general_enclosed_function_token() {
let Ok(MediaQuery::Condition(And(items))) = parse_media_query("foo(bar)") else {
panic!("expected a bare condition with one <media-in-parens>");
};
assert_eq!(
items,
vec![InParens::GeneralEnclosed(GeneralEnclosed {
tokens: vec![Token::Function("foo".into()), Token::Ident("bar".into())],
})]
);
}
#[test]
fn mf_boolean() {
assert_eq!(
parse_media_query("(color)"),
Ok(MediaQuery::Condition(And(vec![feature_boolean("color")])))
);
}
#[test]
fn mf_plain_number() {
assert_eq!(
parse_media_query("(color-index: 2)"),
Ok(MediaQuery::Condition(And(vec![feature_plain(
"color-index",
MfValue::Number(2.0)
)])))
);
}
#[test]
fn mf_plain_dimension() {
assert_eq!(
parse_media_query("(width: 400px)"),
Ok(MediaQuery::Condition(And(vec![feature_plain(
"width",
MfValue::Dimension {
value: 400.0,
unit: "px".into()
}
)])))
);
}
#[test]
fn mf_plain_ident() {
assert_eq!(
parse_media_query("(orientation: landscape)"),
Ok(MediaQuery::Condition(And(vec![feature_plain(
"orientation",
MfValue::Ident("landscape".into())
)])))
);
}
#[test]
fn mf_plain_ratio() {
assert_eq!(
parse_media_query("(aspect-ratio: 16/9)"),
Ok(MediaQuery::Condition(And(vec![feature_plain(
"aspect-ratio",
MfValue::Ratio {
numerator: 16,
denominator: 9
}
)])))
);
}
#[test]
fn mf_plain_ratio_out_of_u32_range_is_invalid() {
let tokens = prepare_tokens("aspect-ratio: 99999999999999/1");
assert_eq!(
parse_fully(&tokens, media_feature),
Err(ParseError::InvalidRatio)
);
}
#[test]
fn media_type_collision_rule() {
assert_eq!(
parse_media_query("layer"),
Err(ParseError::ReservedMediaType("layer".into()))
);
assert_eq!(
parse_media_query("and"),
Err(ParseError::ReservedMediaType("and".into()))
);
}
#[test]
fn parse_error_on_unbalanced_parens() {
assert_eq!(
parse_media_query("(color"),
Err(ParseError::UnbalancedParens)
);
}
#[test]
fn parse_media_query_list_surfaces_per_entry_errors() {
let results = parse_media_query_list("screen, layer, (color");
assert_eq!(results.len(), 3);
assert!(results[0].is_ok());
assert_eq!(
results[1],
Err(ParseError::ReservedMediaType("layer".into()))
);
assert_eq!(results[2], Err(ParseError::UnbalancedParens));
}
#[test]
fn mf_comparison_lt() {
assert_eq!(
parse_fully(&prepare_tokens("<"), mf_comparison),
Ok(MfComparison::Lt)
);
}
#[test]
fn mf_comparison_gt() {
assert_eq!(
parse_fully(&prepare_tokens(">"), mf_comparison),
Ok(MfComparison::Gt)
);
}
#[test]
fn mf_comparison_eq() {
assert_eq!(
parse_fully(&prepare_tokens("="), mf_comparison),
Ok(MfComparison::Eq)
);
}
#[test]
fn mf_comparison_le_no_whitespace() {
assert_eq!(
parse_fully(&prepare_tokens("<="), mf_comparison),
Ok(MfComparison::Le)
);
}
#[test]
fn mf_comparison_ge_no_whitespace() {
assert_eq!(
parse_fully(&prepare_tokens(">="), mf_comparison),
Ok(MfComparison::Ge)
);
}
#[test]
fn mf_comparison_lt_then_eq_with_whitespace_is_not_le() {
assert_eq!(
parse_fully(&prepare_tokens("< ="), mf_comparison),
Err(ParseError::TrailingTokens(Token::Delim('=')))
);
}
#[test]
fn mf_comparison_gt_then_eq_with_whitespace_is_not_ge() {
assert_eq!(
parse_fully(&prepare_tokens("> ="), mf_comparison),
Err(ParseError::TrailingTokens(Token::Delim('=')))
);
}
#[test]
fn mf_range_name_first_lt() {
assert_eq!(
parse_media_query("(width < 400px)"),
Ok(MediaQuery::Condition(And(vec![feature_range(
MfRange::NameFirst {
name: MfName("width".into()),
operator: MfComparison::Lt,
value: dim(400.0, "px"),
}
)])))
);
}
#[test]
fn mf_range_name_first_le() {
assert_eq!(
parse_media_query("(width <= 400px)"),
Ok(MediaQuery::Condition(And(vec![feature_range(
MfRange::NameFirst {
name: MfName("width".into()),
operator: MfComparison::Le,
value: dim(400.0, "px"),
}
)])))
);
}
#[test]
fn mf_range_name_first_gt() {
assert_eq!(
parse_media_query("(width > 400px)"),
Ok(MediaQuery::Condition(And(vec![feature_range(
MfRange::NameFirst {
name: MfName("width".into()),
operator: MfComparison::Gt,
value: dim(400.0, "px"),
}
)])))
);
}
#[test]
fn mf_range_name_first_ge() {
assert_eq!(
parse_media_query("(width >= 400px)"),
Ok(MediaQuery::Condition(And(vec![feature_range(
MfRange::NameFirst {
name: MfName("width".into()),
operator: MfComparison::Ge,
value: dim(400.0, "px"),
}
)])))
);
}
#[test]
fn mf_range_name_first_eq_with_ident_value() {
assert_eq!(
parse_media_query("(orientation = landscape)"),
Ok(MediaQuery::Condition(And(vec![feature_range(
MfRange::NameFirst {
name: MfName("orientation".into()),
operator: MfComparison::Eq,
value: MfValue::Ident("landscape".into()),
}
)])))
);
}
#[test]
fn mf_range_name_first_number_value() {
assert_eq!(
parse_media_query("(color-index >= 2)"),
Ok(MediaQuery::Condition(And(vec![feature_range(
MfRange::NameFirst {
name: MfName("color-index".into()),
operator: MfComparison::Ge,
value: MfValue::Number(2.0),
}
)])))
);
}
#[test]
fn mf_range_name_first_ratio_value() {
assert_eq!(
parse_media_query("(aspect-ratio >= 16/9)"),
Ok(MediaQuery::Condition(And(vec![feature_range(
MfRange::NameFirst {
name: MfName("aspect-ratio".into()),
operator: MfComparison::Ge,
value: MfValue::Ratio {
numerator: 16,
denominator: 9,
},
}
)])))
);
}
#[test]
fn mf_range_value_first_dimension() {
assert_eq!(
parse_media_query("(400px <= width)"),
Ok(MediaQuery::Condition(And(vec![feature_range(
MfRange::ValueFirst {
value: dim(400.0, "px"),
operator: MfComparison::Le,
name: MfName("width".into()),
}
)])))
);
}
#[test]
fn mf_range_value_first_number() {
assert_eq!(
parse_media_query("(2 < color-index)"),
Ok(MediaQuery::Condition(And(vec![feature_range(
MfRange::ValueFirst {
value: MfValue::Number(2.0),
operator: MfComparison::Lt,
name: MfName("color-index".into()),
}
)])))
);
}
#[test]
fn mf_range_value_first_ratio() {
assert_eq!(
parse_media_query("(16/9 <= aspect-ratio)"),
Ok(MediaQuery::Condition(And(vec![feature_range(
MfRange::ValueFirst {
value: MfValue::Ratio {
numerator: 16,
denominator: 9,
},
operator: MfComparison::Le,
name: MfName("aspect-ratio".into()),
}
)])))
);
}
#[test]
fn mf_range_interval_ascending_inclusive() {
assert_eq!(
parse_media_query("(400px <= width <= 700px)"),
Ok(MediaQuery::Condition(And(vec![feature_range(
MfRange::Interval {
lower: dim(400.0, "px"),
lower_inclusive: true,
name: MfName("width".into()),
upper_inclusive: true,
upper: dim(700.0, "px"),
direction: MfRangeDirection::Ascending,
}
)])))
);
}
#[test]
fn mf_range_interval_ascending_exclusive() {
assert_eq!(
parse_media_query("(400px < width < 700px)"),
Ok(MediaQuery::Condition(And(vec![feature_range(
MfRange::Interval {
lower: dim(400.0, "px"),
lower_inclusive: false,
name: MfName("width".into()),
upper_inclusive: false,
upper: dim(700.0, "px"),
direction: MfRangeDirection::Ascending,
}
)])))
);
}
#[test]
fn mf_range_interval_descending_inclusive() {
assert_eq!(
parse_media_query("(700px >= width >= 400px)"),
Ok(MediaQuery::Condition(And(vec![feature_range(
MfRange::Interval {
lower: dim(700.0, "px"),
lower_inclusive: true,
name: MfName("width".into()),
upper_inclusive: true,
upper: dim(400.0, "px"),
direction: MfRangeDirection::Descending,
}
)])))
);
}
#[test]
fn mf_range_interval_descending_exclusive() {
assert_eq!(
parse_media_query("(700px > width > 400px)"),
Ok(MediaQuery::Condition(And(vec![feature_range(
MfRange::Interval {
lower: dim(700.0, "px"),
lower_inclusive: false,
name: MfName("width".into()),
upper_inclusive: false,
upper: dim(400.0, "px"),
direction: MfRangeDirection::Descending,
}
)])))
);
}
#[test]
fn mf_range_interval_rejects_mixed_family() {
let tokens = prepare_tokens("400px <= width > 700px");
assert_eq!(
parse_fully(&tokens, media_feature),
Err(ParseError::InvalidMfRangeInterval)
);
}
#[test]
fn mf_range_interval_rejects_eq_as_first_operator() {
let tokens = prepare_tokens("400px = width < 700px");
assert_eq!(
parse_fully(&tokens, media_feature),
Err(ParseError::InvalidMfRangeInterval)
);
}
#[test]
fn mf_range_incomplete_name_first_is_invalid() {
let tokens = prepare_tokens("width <");
assert_eq!(
parse_fully(&tokens, media_feature),
Err(ParseError::ExpectedMfValue(None))
);
}
#[test]
fn mf_plain_and_boolean_regression_after_range_support() {
assert_eq!(
parse_media_query("(orientation: landscape)"),
Ok(MediaQuery::Condition(And(vec![feature_plain(
"orientation",
MfValue::Ident("landscape".into())
)])))
);
assert_eq!(
parse_media_query("(color)"),
Ok(MediaQuery::Condition(And(vec![feature_boolean("color")])))
);
}
}