/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Parsing for query feature expressions, like `(foo: bar)` or //! `(width >= 400px)`.
usesuper::feature::{Evaluator, QueryFeatureDescription}; usesuper::feature::{FeatureFlags, KeywordDiscriminant}; usecrate::context::QuirksMode; usecrate::custom_properties::{ self, ComputedSubstitutionFunctions, VariableValue as CustomVariableValue,
}; usecrate::derives::*; usecrate::dom::AttributeTracker; usecrate::parser::{Parse, ParserContext}; usecrate::properties::{self, CSSWideKeyword}; usecrate::properties_and_values::value::{ComputedValueComponent as Component, ValueInner}; usecrate::selector_map::PrecomputedHashSet; usecrate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase}; usecrate::stylesheets::{CssRuleType, Origin, UrlExtraData}; usecrate::values::computed::{self, CSSPixelLength, ToComputedValue}; usecrate::values::specified::{
Angle, Integer, Length, Number, Percentage, Ratio, Resolution, Time,
}; usecrate::values::DashedIdent; usecrate::{Atom, Zero}; use cssparser::{Parser, ParserInput, Token}; use selectors::kleene_value::KleeneValue; use std::cmp::Ordering; use std::fmt::{self, Write}; use style_traits::{CssWriter, ParseError, ParsingMode, StyleParseErrorKind, ToCss};
/// Whether we're parsing a media or container query feature. #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] pubenum FeatureType { /// We're parsing a media feature.
Media, /// We're parsing a container feature.
Container,
}
/// The kind of matching that should be performed on a feature value. #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] enum LegacyRange { /// At least the specified value.
Min, /// At most the specified value.
Max,
}
/// The operator that was specified in this feature. #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] pubenum Operator { /// =
Equal, /// >
GreaterThan, /// >=
GreaterThanEqual, /// <
LessThan, /// <=
LessThanEqual,
}
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())),
};
// https://drafts.csswg.org/mediaqueries-4/#mq-syntax: // // No whitespace is allowed between the “<” or “>” // <delim-token>s and the following “=” <delim-token>, if it’s // present. // // TODO(emilio): Maybe we should ignore comments as well? // https://github.com/w3c/csswg-drafts/issues/6248 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();
impl QueryFeatureExpressionKind { /// Evaluate a given range given an optional query value and a value from /// the browser. 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 => returnfalse,
};
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 => returnfalse,
};
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()); iflet Some((ref op, ref value)) = left { let value = compute(value); let cmp = match value.partial_cmp(&context_value) {
Some(c) => c,
None => returnfalse,
}; if !op.evaluate(cmp) { returnfalse;
}
} iflet Some((ref op, ref value)) = right { let value = compute(value); let cmp = match context_value.partial_cmp(&value) {
Some(c) => c,
None => returnfalse,
}; if !op.evaluate(cmp) { returnfalse;
}
} true
},
}
}
/// Non-ranged features only need to compare to one value at most. 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
},
}
}
}
/// A feature expression contains a reference to the feature, the value the /// query contained, and the range to evaluate. #[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)] pubstruct 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('(')?;
#[allow(unused_variables)] fn disabled_by_pref(feature: &Atom, context: &ParserContext) -> bool { #[cfg(feature = "gecko")]
{ // prefers-reduced-transparency is always enabled in the ua and chrome. On // the web it is hidden behind a preference (see Bug 1822176). if *feature == atom!("prefers-reduced-transparency") { return !context.chrome_rules_enabled()
&& !static_prefs::pref!("layout.css.prefers-reduced-transparency.enabled");
}
// inverted-colors is always enabled in the ua and chrome. On // the web it is hidden behind a preference. if *feature == atom!("inverted-colors") { return !context.chrome_rules_enabled()
&& !static_prefs::pref!("layout.css.inverted-colors.enabled");
}
} false
}
/// Parses the following range syntax: /// /// (feature-value <operator> feature-name) /// (feature-value <operator> feature-name <operator> feature-value) 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();
// To parse the values, we first need to find the feature name. We rely // on feature values for ranged features not being able to be top-level // <ident>s, which holds. let feature_index = loop { // NOTE: parse_feature_name advances the input. iflet Ok((index, range)) = Self::parse_feature_name(context, input, feature_type) { if range.is_some() { // Ranged names are not allowed here. 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,
},
))
}
/// Parse a feature expression where we've already consumed the parenthesis. pubfn 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) => { iflet 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 there's no colon, this is a query of the form // '(<feature>)', that is, there's no value specified. // // Gecko doesn't allow ranged expressions without a // value, so just reject them here too. if range.is_some() { return Err(
input.new_custom_error(StyleParseErrorKind::RangedExpressionWithNoValue)
);
}
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),
},
};
/// Returns whether this "plain" feature query evaluates to true for the given device. pubfn 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(matchself.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).to_computed_value(context))
},
Evaluator::Float(eval) => { let v = eval(context); self.kind
.evaluate(v, |v| expect!(Float, v).to_computed_value(context))
},
Evaluator::NumberRatio(eval) => { let ratio = eval(context); // A ratio of 0/0 behaves as the ratio 1/0, so we need to call used_value() // to convert it if necessary. // FIXME: we may need to update here once // https://github.com/w3c/csswg-drafts/issues/4954 got resolved. self.kind.evaluate(ratio, |v| {
expect!(NumberRatio, v)
.to_computed_value(context)
.used_value()
})
},
Evaluator::OptionalNumberRatio(eval) => { let ratio = match eval(context) {
Some(v) => v,
None => return KleeneValue::Unknown,
}; // See above for subtleties here. self.kind.evaluate(ratio, |v| {
expect!(NumberRatio, v)
.to_computed_value(context)
.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::BoolInteger(eval) => { let computed = self
.kind
.non_ranged_value()
.map(|v| expect!(BoolInteger, v).to_computed_value(context)); let boolean = eval(context);
computed.map_or(boolean, |v| v == boolean as i32)
},
})
}
}
/// A value found or expected in a expression. /// /// FIXME(emilio): How should calc() serialize in the Number / Integer / /// BoolInteger / NumberRatio case, as computed or as specified value? /// /// If the first, this would need to store the relevant values. /// /// See: https://github.com/w3c/csswg-drafts/issues/1968 #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] pubenum QueryExpressionValue { /// A length.
Length(Length), /// An integer.
Integer(Integer), /// A floating point value.
Float(Number), /// A boolean value, specified as an integer (i.e., either 0 or 1).
BoolInteger(Integer), /// A single non-negative number or two non-negative numbers separated by '/', /// with optional whitespace on either side of the '/'.
NumberRatio(Ratio), /// A resolution.
Resolution(Resolution), /// An enumerated value, defined by the variant keyword table in the /// feature's `mData` member.
Enumerated(KeywordDiscriminant), /// Value types only used by style-range query expressions, not feature queries. /// A CSS-wide keyword.
Keyword(CSSWideKeyword), /// A percentage.
Percentage(Percentage), /// An angle.
Angle(Angle), /// A time value.
Time(Time), /// A custom property name.
Custom(DashedIdent), /// An arbitrary substitution function (var(), attr(), env()), stored as a string /// for later evaluation. We store this as a custom-property value to make it easy /// to resolve later.
Function(Box<CustomVariableValue>),
}
impl QueryStyleRange { /// Parses the following range syntax: /// /// value <operator> value /// value <operator> value <operator> value /// /// This is only used when parsing @container style() queries; the feature_type /// and index is hardcoded (and ignored). pubfn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> { let value1 = QueryExpressionValue::parse_for_style_range(context, input)?; let op1 = Operator::parse(input)?; let value2 = QueryExpressionValue::parse_for_style_range(context, input)?;
// Resolve a QueryExpressionValue to its computed value for comparison. fn resolve_value(
value: &QueryExpressionValue,
context: &computed::Context,
attribute_tracker: &mut AttributeTracker,
visited_set: &mut PrecomputedHashSet<DashedIdent>,
) -> Option<Component> { match value {
QueryExpressionValue::Custom(ident) => { // `ident` is the dashed ident, but we need the name // without "--" for custom-property lookup. let name = ident.undashed(); let stylist = context
.builder
.stylist
.expect("container queries should have a stylist around"); let registration = stylist.get_custom_property_registration(&name); let current_value = context
.inherited_custom_properties()
.get(registration, &name)?; match ¤t_value.v {
ValueInner::Component(component) => Some(component.clone()),
ValueInner::Universal(v) => { // If visited_set.insert() returns false, ident was already seen // and we risk infinite recursion, so instead return None // (i.e. the value cannot be resolved). if visited_set.insert(ident.clone()) { Self::resolve_universal(
&v.css,
&v.url_data,
context,
attribute_tracker,
visited_set,
)
} else {
None
}
},
ValueInner::List(_) => {
debug_assert!(false, "We don't parse list values in style queries");
None
},
}
},
QueryExpressionValue::Function(value) => { let sub_funcs = ComputedSubstitutionFunctions::new(
Some(context.inherited_custom_properties().clone()),
None,
); let stylist = context
.builder
.stylist
.expect("container queries should have a stylist around"); let substituted = custom_properties::substitute(
&value,
&sub_funcs,
stylist,
context,
attribute_tracker,
)
.ok()?; Self::resolve_universal(
&substituted.css,
&value.url_data,
context,
attribute_tracker,
visited_set,
)
},
QueryExpressionValue::Length(v) => {
Some(Component::Length(v.to_computed_value(context)))
},
QueryExpressionValue::Float(v) => Some(Component::Number(v.to_computed_value(context))),
QueryExpressionValue::Resolution(v) => {
Some(Component::Resolution(v.to_computed_value(context)))
},
QueryExpressionValue::Percentage(v) => {
Some(Component::Percentage(v.to_computed_value(context)))
},
QueryExpressionValue::Angle(v) => Some(Component::Angle(v.to_computed_value(context))),
QueryExpressionValue::Time(v) => Some(Component::Time(v.to_computed_value(context))), // It's unclear to me what CSS-wide keywords would mean in a style-range query; // for now, at least, they'll just fail to resolve.
QueryExpressionValue::Keyword(_) => None,
_ => {
debug_assert!(false, "unexpected value type in style range");
None
},
}
}
// If a custom-property QueryExpressionValue has a "universal-syntax" value, we need to // send the current CSS text of the value to QueryExpressionValue::parse_for_style_range // to try and resolve to a specific typed value. // After parsing, this will call back to QueryExpressionValue::resolve_value with the // parsed result, which has the potential for mutual recursion; we keep track of a // visited_set of custom property names to protect against this. fn resolve_universal(
css_text: &str,
url_data: &UrlExtraData,
context: &computed::Context,
attribute_tracker: &mut AttributeTracker,
visited_set: &mut PrecomputedHashSet<DashedIdent>,
) -> Option<Component> { let parser_context = ParserContext::new(
Origin::Author,
url_data,
Some(CssRuleType::Container),
ParsingMode::DEFAULT,
QuirksMode::NoQuirks, /* namespaces = */ Default::default(), /* error_reporter = */ None, /* use_counters = */ None, /* attr_taint */ Default::default(),
); letmut input = ParserInput::new(css_text);
QueryExpressionValue::parse_for_style_range(&parser_context, &mut Parser::new(&mut input))
.ok()
.and_then(|parsed| { Self::resolve_value(&parsed, context, attribute_tracker, visited_set)
})
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.