use super::feature::{Evaluator, QueryFeatureDescription};
use super::feature::{FeatureFlags, KeywordDiscriminant};
use crate::parser::{Parse, ParserContext};
use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase};
use crate::values::computed::{self, Ratio, ToComputedValue};
use crate::values::specified::{Integer, Length, Number, Resolution};
use crate::values::{AtomString, CSSFloat};
use crate::{Atom, Zero};
use cssparser::{Parser, Token};
use selectors::kleene_value::KleeneValue;
use std::cmp::Ordering;
use std::fmt::{self, Write};
use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
pub enum FeatureType {
Media,
Container,
}
impl FeatureType {
fn features(&self) -> &'static [QueryFeatureDescription] {
#[cfg(feature = "gecko")]
use crate::gecko::media_features::MEDIA_FEATURES;
#[cfg(feature = "servo")]
use crate::servo::media_queries::MEDIA_FEATURES;
use crate::stylesheets::container_rule::CONTAINER_FEATURES;
match *self {
FeatureType::Media => &MEDIA_FEATURES,
FeatureType::Container => &CONTAINER_FEATURES,
}
}
fn find_feature(&self, name: &Atom) -> Option<(usize, &'static QueryFeatureDescription)> {
self.features()
.iter()
.enumerate()
.find(|(_, f)| f.name == *name)
}
}
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
enum LegacyRange {
Min,
Max,
}
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
enum Operator {
Equal,
GreaterThan,
GreaterThanEqual,
LessThan,
LessThanEqual,
}
impl ToCss for Operator {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
W: fmt::Write,
{
dest.write_str(match *self {
Self::Equal => "=",
Self::LessThan => "<",
Self::LessThanEqual => "<=",
Self::GreaterThan => ">",
Self::GreaterThanEqual => ">=",
})
}
}
impl Operator {
fn is_compatible_with(self, right_op: Self) -> bool {
match self {
Self::Equal => false,
Self::GreaterThan | Self::GreaterThanEqual => {
matches!(right_op, Self::GreaterThan | Self::GreaterThanEqual)
},
Self::LessThan | Self::LessThanEqual => {
matches!(right_op, Self::LessThan | Self::LessThanEqual)
},
}
}
fn evaluate(&self, cmp: Ordering) -> bool {
match *self {
Self::Equal => cmp == Ordering::Equal,
Self::GreaterThan => cmp == Ordering::Greater,
Self::GreaterThanEqual => cmp == Ordering::Equal || cmp == Ordering::Greater,
Self::LessThan => cmp == Ordering::Less,
Self::LessThanEqual => cmp == Ordering::Equal || cmp == Ordering::Less,
}
}
fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
let location = input.current_source_location();
let operator = match *input.next()? {
Token::Delim('=') => return Ok(Operator::Equal),
Token::Delim('>') => Operator::GreaterThan,
Token::Delim('<') => Operator::LessThan,
ref t => return Err(location.new_unexpected_token_error(t.clone())),
};
let parsed_equal = input
.try_parse(|i| {
let t = i.next_including_whitespace().map_err(|_| ())?;
if !matches!(t, Token::Delim('=')) {
return Err(());
}
Ok(())
})
.is_ok();
if !parsed_equal {
return Ok(operator);
}
Ok(match operator {
Operator::GreaterThan => Operator::GreaterThanEqual,
Operator::LessThan => Operator::LessThanEqual,
_ => unreachable!(),
})
}
}
#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
enum QueryFeatureExpressionKind {
Empty,
Single(QueryExpressionValue),
LegacyRange(LegacyRange, QueryExpressionValue),
Range {
left: Option<(Operator, QueryExpressionValue)>,
right: Option<(Operator, QueryExpressionValue)>,
},
}
impl QueryFeatureExpressionKind {
fn evaluate<T>(
&self,
context_value: T,
mut compute: impl FnMut(&QueryExpressionValue) -> T,
) -> bool
where
T: PartialOrd + Zero,
{
match *self {
Self::Empty => return !context_value.is_zero(),
Self::Single(ref value) => {
let value = compute(value);
let cmp = match context_value.partial_cmp(&value) {
Some(c) => c,
None => return false,
};
cmp == Ordering::Equal
},
Self::LegacyRange(ref range, ref value) => {
let value = compute(value);
let cmp = match context_value.partial_cmp(&value) {
Some(c) => c,
None => return false,
};
cmp == Ordering::Equal ||
match range {
LegacyRange::Min => cmp == Ordering::Greater,
LegacyRange::Max => cmp == Ordering::Less,
}
},
Self::Range {
ref left,
ref right,
} => {
debug_assert!(left.is_some() || right.is_some());
if let Some((ref op, ref value)) = left {
let value = compute(value);
let cmp = match value.partial_cmp(&context_value) {
Some(c) => c,
None => return false,
};
if !op.evaluate(cmp) {
return false;
}
}
if let Some((ref op, ref value)) = right {
let value = compute(value);
let cmp = match context_value.partial_cmp(&value) {
Some(c) => c,
None => return false,
};
if !op.evaluate(cmp) {
return false;
}
}
true
},
}
}
fn non_ranged_value(&self) -> Option<&QueryExpressionValue> {
match *self {
Self::Empty => None,
Self::Single(ref v) => Some(v),
Self::LegacyRange(..) | Self::Range { .. } => {
debug_assert!(false, "Unexpected ranged value in non-ranged feature!");
None
},
}
}
}
#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
pub struct QueryFeatureExpression {
feature_type: FeatureType,
feature_index: usize,
kind: QueryFeatureExpressionKind,
}
impl ToCss for QueryFeatureExpression {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
W: fmt::Write,
{
dest.write_char('(')?;
match self.kind {
QueryFeatureExpressionKind::Empty => self.write_name(dest)?,
QueryFeatureExpressionKind::Single(ref v) |
QueryFeatureExpressionKind::LegacyRange(_, ref v) => {
self.write_name(dest)?;
dest.write_str(": ")?;
v.to_css(dest, self)?;
},
QueryFeatureExpressionKind::Range {
ref left,
ref right,
} => {
if let Some((ref op, ref val)) = left {
val.to_css(dest, self)?;
dest.write_char(' ')?;
op.to_css(dest)?;
dest.write_char(' ')?;
}
self.write_name(dest)?;
if let Some((ref op, ref val)) = right {
dest.write_char(' ')?;
op.to_css(dest)?;
dest.write_char(' ')?;
val.to_css(dest, self)?;
}
},
}
dest.write_char(')')
}
}
fn consume_operation_or_colon<'i>(
input: &mut Parser<'i, '_>,
) -> Result<Option<Operator>, ParseError<'i>> {
if input.try_parse(|input| input.expect_colon()).is_ok() {
return Ok(None);
}
Operator::parse(input).map(|op| Some(op))
}
#[allow(unused_variables)]
fn disabled_by_pref(feature: &Atom, context: &ParserContext) -> bool {
#[cfg(feature = "gecko")]
{
if *feature == atom!("prefers-reduced-transparency") {
return !context.chrome_rules_enabled() &&
!static_prefs::pref!("layout.css.prefers-reduced-transparency.enabled");
}
if *feature == atom!("inverted-colors") {
return !context.chrome_rules_enabled() &&
!static_prefs::pref!("layout.css.inverted-colors.enabled");
}
}
false
}
impl QueryFeatureExpression {
fn new(
feature_type: FeatureType,
feature_index: usize,
kind: QueryFeatureExpressionKind,
) -> Self {
debug_assert!(feature_index < feature_type.features().len());
Self {
feature_type,
feature_index,
kind,
}
}
fn write_name<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
W: fmt::Write,
{
let feature = self.feature();
if feature.flags.contains(FeatureFlags::WEBKIT_PREFIX) {
dest.write_str("-webkit-")?;
}
if let QueryFeatureExpressionKind::LegacyRange(range, _) = self.kind {
match range {
LegacyRange::Min => dest.write_str("min-")?,
LegacyRange::Max => dest.write_str("max-")?,
}
}
write!(dest, "{}", feature.name)?;
Ok(())
}
fn feature(&self) -> &'static QueryFeatureDescription {
&self.feature_type.features()[self.feature_index]
}
pub fn feature_flags(&self) -> FeatureFlags {
self.feature().flags
}
pub fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
feature_type: FeatureType,
) -> Result<Self, ParseError<'i>> {
input.expect_parenthesis_block()?;
input.parse_nested_block(|input| {
Self::parse_in_parenthesis_block(context, input, feature_type)
})
}
fn parse_feature_name<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
feature_type: FeatureType,
) -> Result<(usize, Option<LegacyRange>), ParseError<'i>> {
let mut flags = FeatureFlags::empty();
let location = input.current_source_location();
let ident = input.expect_ident()?;
if context.chrome_rules_enabled() {
flags.insert(FeatureFlags::CHROME_AND_UA_ONLY);
}
let mut feature_name = &**ident;
if starts_with_ignore_ascii_case(feature_name, "-webkit-") {
feature_name = &feature_name[8..];
flags.insert(FeatureFlags::WEBKIT_PREFIX);
}
let range = if starts_with_ignore_ascii_case(feature_name, "min-") {
feature_name = &feature_name[4..];
Some(LegacyRange::Min)
} else if starts_with_ignore_ascii_case(feature_name, "max-") {
feature_name = &feature_name[4..];
Some(LegacyRange::Max)
} else {
None
};
let atom = Atom::from(string_as_ascii_lowercase(feature_name));
let (feature_index, feature) = match feature_type.find_feature(&atom) {
Some((i, f)) => (i, f),
None => {
return Err(location.new_custom_error(
StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()),
))
},
};
if disabled_by_pref(&feature.name, context) ||
!flags.contains(feature.flags.parsing_requirements()) ||
(range.is_some() && !feature.allows_ranges())
{
return Err(location.new_custom_error(
StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()),
));
}
Ok((feature_index, range))
}
fn parse_multi_range_syntax<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
feature_type: FeatureType,
) -> Result<Self, ParseError<'i>> {
let start = input.state();
let feature_index = loop {
if let Ok((index, range)) = Self::parse_feature_name(context, input, feature_type) {
if range.is_some() {
return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
break index;
}
if input.is_exhausted() {
return Err(start
.source_location()
.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
};
input.reset(&start);
let feature = &feature_type.features()[feature_index];
let left_val = QueryExpressionValue::parse(feature, context, input)?;
let left_op = Operator::parse(input)?;
{
let (parsed_index, _) = Self::parse_feature_name(context, input, feature_type)?;
debug_assert_eq!(
parsed_index, feature_index,
"How did we find a different feature?"
);
}
let right_op = input.try_parse(Operator::parse).ok();
let right = match right_op {
Some(op) => {
if !left_op.is_compatible_with(op) {
return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
Some((op, QueryExpressionValue::parse(feature, context, input)?))
},
None => None,
};
Ok(Self::new(
feature_type,
feature_index,
QueryFeatureExpressionKind::Range {
left: Some((left_op, left_val)),
right,
},
))
}
pub fn parse_in_parenthesis_block<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
feature_type: FeatureType,
) -> Result<Self, ParseError<'i>> {
let (feature_index, range) =
match input.try_parse(|input| Self::parse_feature_name(context, input, feature_type)) {
Ok(v) => v,
Err(e) => {
if let Ok(expr) = Self::parse_multi_range_syntax(context, input, feature_type) {
return Ok(expr);
}
return Err(e);
},
};
let operator = input.try_parse(consume_operation_or_colon);
let operator = match operator {
Err(..) => {
if range.is_some() {
return Err(
input.new_custom_error(StyleParseErrorKind::RangedExpressionWithNoValue)
);
}
return Ok(Self::new(
feature_type,
feature_index,
QueryFeatureExpressionKind::Empty,
));
},
Ok(operator) => operator,
};
let feature = &feature_type.features()[feature_index];
let value = QueryExpressionValue::parse(feature, context, input).map_err(|err| {
err.location
.new_custom_error(StyleParseErrorKind::MediaQueryExpectedFeatureValue)
})?;
let kind = match range {
Some(range) => {
if operator.is_some() {
return Err(
input.new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator)
);
}
QueryFeatureExpressionKind::LegacyRange(range, value)
},
None => match operator {
Some(operator) => {
if !feature.allows_ranges() {
return Err(input
.new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator));
}
QueryFeatureExpressionKind::Range {
left: None,
right: Some((operator, value)),
}
},
None => QueryFeatureExpressionKind::Single(value),
},
};
Ok(Self::new(feature_type, feature_index, kind))
}
pub fn matches(&self, context: &computed::Context) -> KleeneValue {
macro_rules! expect {
($variant:ident, $v:expr) => {
match *$v {
QueryExpressionValue::$variant(ref v) => v,
_ => unreachable!("Unexpected QueryExpressionValue"),
}
};
}
KleeneValue::from(match self.feature().evaluator {
Evaluator::Length(eval) => {
let v = eval(context);
self.kind
.evaluate(v, |v| expect!(Length, v).to_computed_value(context))
},
Evaluator::OptionalLength(eval) => {
let v = match eval(context) {
Some(v) => v,
None => return KleeneValue::Unknown,
};
self.kind
.evaluate(v, |v| expect!(Length, v).to_computed_value(context))
},
Evaluator::Integer(eval) => {
let v = eval(context);
self.kind.evaluate(v, |v| *expect!(Integer, v))
},
Evaluator::Float(eval) => {
let v = eval(context);
self.kind.evaluate(v, |v| *expect!(Float, v))
},
Evaluator::NumberRatio(eval) => {
let ratio = eval(context);
self.kind
.evaluate(ratio, |v| expect!(NumberRatio, v).used_value())
},
Evaluator::OptionalNumberRatio(eval) => {
let ratio = match eval(context) {
Some(v) => v,
None => return KleeneValue::Unknown,
};
self.kind
.evaluate(ratio, |v| expect!(NumberRatio, v).used_value())
},
Evaluator::Resolution(eval) => {
let v = eval(context).dppx();
self.kind.evaluate(v, |v| {
expect!(Resolution, v).to_computed_value(context).dppx()
})
},
Evaluator::Enumerated { evaluator, .. } => {
let computed = self
.kind
.non_ranged_value()
.map(|v| *expect!(Enumerated, v));
return evaluator(context, computed);
},
Evaluator::String(evaluator) => {
let string = self.kind.non_ranged_value().map(|v| expect!(String, v));
return evaluator(context, string);
},
Evaluator::BoolInteger(eval) => {
let computed = self
.kind
.non_ranged_value()
.map(|v| *expect!(BoolInteger, v));
let boolean = eval(context);
computed.map_or(boolean, |v| v == boolean)
},
})
}
}
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
pub enum QueryExpressionValue {
Length(Length),
Integer(i32),
Float(CSSFloat),
BoolInteger(bool),
NumberRatio(Ratio),
Resolution(Resolution),
Enumerated(KeywordDiscriminant),
String(AtomString),
}
impl QueryExpressionValue {
fn to_css<W>(&self, dest: &mut CssWriter<W>, for_expr: &QueryFeatureExpression) -> fmt::Result
where
W: fmt::Write,
{
match *self {
QueryExpressionValue::Length(ref l) => l.to_css(dest),
QueryExpressionValue::Integer(v) => v.to_css(dest),
QueryExpressionValue::Float(v) => v.to_css(dest),
QueryExpressionValue::BoolInteger(v) => dest.write_str(if v { "1" } else { "0" }),
QueryExpressionValue::NumberRatio(ratio) => ratio.to_css(dest),
QueryExpressionValue::Resolution(ref r) => r.to_css(dest),
QueryExpressionValue::Enumerated(value) => match for_expr.feature().evaluator {
Evaluator::Enumerated { serializer, .. } => dest.write_str(&*serializer(value)),
_ => unreachable!(),
},
QueryExpressionValue::String(ref s) => s.to_css(dest),
}
}
fn parse<'i, 't>(
for_feature: &QueryFeatureDescription,
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<QueryExpressionValue, ParseError<'i>> {
Ok(match for_feature.evaluator {
Evaluator::OptionalLength(..) | Evaluator::Length(..) => {
let length = Length::parse(context, input)?;
QueryExpressionValue::Length(length)
},
Evaluator::Integer(..) => {
let integer = Integer::parse(context, input)?;
QueryExpressionValue::Integer(integer.value())
},
Evaluator::BoolInteger(..) => {
let integer = Integer::parse_non_negative(context, input)?;
let value = integer.value();
if value > 1 {
return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
QueryExpressionValue::BoolInteger(value == 1)
},
Evaluator::Float(..) => {
let number = Number::parse(context, input)?;
QueryExpressionValue::Float(number.get())
},
Evaluator::OptionalNumberRatio(..) | Evaluator::NumberRatio(..) => {
use crate::values::specified::Ratio as SpecifiedRatio;
let ratio = SpecifiedRatio::parse(context, input)?;
QueryExpressionValue::NumberRatio(Ratio::new(ratio.0.get(), ratio.1.get()))
},
Evaluator::Resolution(..) => {
QueryExpressionValue::Resolution(Resolution::parse(context, input)?)
},
Evaluator::String(..) => {
QueryExpressionValue::String(input.expect_string()?.as_ref().into())
},
Evaluator::Enumerated { parser, .. } => {
QueryExpressionValue::Enumerated(parser(context, input)?)
},
})
}
}