condition.rs (18242B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 5 //! A query condition: 6 //! 7 //! https://drafts.csswg.org/mediaqueries-4/#typedef-media-condition 8 //! https://drafts.csswg.org/css-contain-3/#typedef-container-condition 9 10 use super::{FeatureFlags, FeatureType, QueryFeatureExpression}; 11 use crate::custom_properties; 12 use crate::derives::*; 13 use crate::stylesheets::CustomMediaEvaluator; 14 use crate::values::{computed, AtomString, DashedIdent}; 15 use crate::{error_reporting::ContextualParseError, parser::Parse, parser::ParserContext}; 16 use cssparser::{match_ignore_ascii_case, Parser, SourcePosition, Token}; 17 use selectors::kleene_value::KleeneValue; 18 use servo_arc::Arc; 19 use std::fmt::{self, Write}; 20 use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; 21 22 /// A binary `and` or `or` operator. 23 #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] 24 #[allow(missing_docs)] 25 pub enum Operator { 26 And, 27 Or, 28 } 29 30 /// Whether to allow an `or` condition or not during parsing. 31 #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss)] 32 enum AllowOr { 33 Yes, 34 No, 35 } 36 37 /// A style query feature: 38 /// https://drafts.csswg.org/css-conditional-5/#typedef-style-feature 39 #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] 40 pub struct StyleFeature { 41 name: custom_properties::Name, 42 // TODO: This is a "primary" reference, probably should be unconditionally measured. 43 #[ignore_malloc_size_of = "Arc"] 44 value: Option<Arc<custom_properties::SpecifiedValue>>, 45 } 46 47 impl ToCss for StyleFeature { 48 fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result 49 where 50 W: fmt::Write, 51 { 52 dest.write_str("--")?; 53 crate::values::serialize_atom_identifier(&self.name, dest)?; 54 if let Some(ref v) = self.value { 55 dest.write_str(": ")?; 56 v.to_css(dest)?; 57 } 58 Ok(()) 59 } 60 } 61 62 impl StyleFeature { 63 fn parse<'i, 't>( 64 context: &ParserContext, 65 input: &mut Parser<'i, 't>, 66 feature_type: FeatureType, 67 ) -> Result<Self, ParseError<'i>> { 68 if !static_prefs::pref!("layout.css.style-queries.enabled") 69 || feature_type != FeatureType::Container 70 { 71 return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); 72 } 73 // TODO: Allow parsing nested style feature queries. 74 let ident = input.expect_ident()?; 75 // TODO(emilio): Maybe support non-custom properties? 76 let name = match custom_properties::parse_name(ident.as_ref()) { 77 Ok(name) => custom_properties::Name::from(name), 78 Err(()) => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), 79 }; 80 let value = if input.try_parse(|i| i.expect_colon()).is_ok() { 81 input.skip_whitespace(); 82 Some(Arc::new(custom_properties::SpecifiedValue::parse( 83 input, 84 &context.url_data, 85 )?)) 86 } else { 87 None 88 }; 89 Ok(Self { name, value }) 90 } 91 92 fn matches(&self, ctx: &computed::Context) -> KleeneValue { 93 // FIXME(emilio): Confirm this is the right style to query. 94 let registration = ctx 95 .builder 96 .stylist 97 .expect("container queries should have a stylist around") 98 .get_custom_property_registration(&self.name); 99 let current_value = ctx 100 .inherited_custom_properties() 101 .get(registration, &self.name); 102 KleeneValue::from(match self.value { 103 Some(ref v) => current_value.is_some_and(|cur| { 104 custom_properties::compute_variable_value(v, registration, ctx) 105 .is_some_and(|v| v == *cur) 106 }), 107 None => current_value.is_some(), 108 }) 109 } 110 } 111 112 /// A boolean value for a pref query. 113 #[derive( 114 Clone, 115 Debug, 116 MallocSizeOf, 117 PartialEq, 118 Eq, 119 Parse, 120 SpecifiedValueInfo, 121 ToComputedValue, 122 ToCss, 123 ToShmem, 124 )] 125 #[repr(u8)] 126 #[allow(missing_docs)] 127 pub enum BoolValue { 128 False, 129 True, 130 } 131 132 /// Simple values we support for -moz-pref(). We don't want to deal with calc() and other 133 /// shenanigans for now. 134 #[derive( 135 Clone, 136 Debug, 137 Eq, 138 MallocSizeOf, 139 Parse, 140 PartialEq, 141 SpecifiedValueInfo, 142 ToComputedValue, 143 ToCss, 144 ToShmem, 145 )] 146 #[repr(u8)] 147 pub enum MozPrefFeatureValue<I> { 148 /// No pref value, implicitly bool, but also used to represent missing prefs. 149 #[css(skip)] 150 None, 151 /// A bool value. 152 Boolean(BoolValue), 153 /// An integer value, useful for int prefs. 154 Integer(I), 155 /// A string pref value. 156 String(crate::values::AtomString), 157 } 158 159 type SpecifiedMozPrefFeatureValue = MozPrefFeatureValue<crate::values::specified::Integer>; 160 /// The computed -moz-pref() value. 161 pub type ComputedMozPrefFeatureValue = MozPrefFeatureValue<crate::values::computed::Integer>; 162 163 /// A custom -moz-pref(<name>, <value>) query feature. 164 #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] 165 pub struct MozPrefFeature { 166 name: crate::values::AtomString, 167 value: SpecifiedMozPrefFeatureValue, 168 } 169 170 impl MozPrefFeature { 171 fn parse<'i, 't>( 172 context: &ParserContext, 173 input: &mut Parser<'i, 't>, 174 feature_type: FeatureType, 175 ) -> Result<Self, ParseError<'i>> { 176 use crate::parser::Parse; 177 if !context.chrome_rules_enabled() || feature_type != FeatureType::Media { 178 return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); 179 } 180 let name = AtomString::parse(context, input)?; 181 let value = if input.try_parse(|i| i.expect_comma()).is_ok() { 182 SpecifiedMozPrefFeatureValue::parse(context, input)? 183 } else { 184 SpecifiedMozPrefFeatureValue::None 185 }; 186 Ok(Self { name, value }) 187 } 188 189 #[cfg(feature = "gecko")] 190 fn matches(&self, ctx: &computed::Context) -> KleeneValue { 191 use crate::values::computed::ToComputedValue; 192 let value = self.value.to_computed_value(ctx); 193 KleeneValue::from(unsafe { 194 crate::gecko_bindings::bindings::Gecko_EvalMozPrefFeature(self.name.as_ptr(), &value) 195 }) 196 } 197 198 #[cfg(feature = "servo")] 199 fn matches(&self, _: &computed::Context) -> KleeneValue { 200 KleeneValue::Unknown 201 } 202 } 203 204 impl ToCss for MozPrefFeature { 205 fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result 206 where 207 W: fmt::Write, 208 { 209 self.name.to_css(dest)?; 210 if !matches!(self.value, MozPrefFeatureValue::None) { 211 dest.write_str(", ")?; 212 self.value.to_css(dest)?; 213 } 214 Ok(()) 215 } 216 } 217 218 /// Represents a condition. 219 #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] 220 pub enum QueryCondition { 221 /// A simple feature expression, implicitly parenthesized. 222 Feature(QueryFeatureExpression), 223 /// A custom media query reference in a boolean context, implicitly parenthesized. 224 Custom(DashedIdent), 225 /// A negation of a condition. 226 Not(Box<QueryCondition>), 227 /// A set of joint operations. 228 Operation(Box<[QueryCondition]>, Operator), 229 /// A condition wrapped in parenthesis. 230 InParens(Box<QueryCondition>), 231 /// A <style> query. 232 Style(StyleFeature), 233 /// A -moz-pref() query. 234 MozPref(MozPrefFeature), 235 /// [ <function-token> <any-value>? ) ] | [ ( <any-value>? ) ] 236 GeneralEnclosed(String), 237 } 238 239 impl ToCss for QueryCondition { 240 fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result 241 where 242 W: fmt::Write, 243 { 244 match *self { 245 // NOTE(emilio): QueryFeatureExpression already includes the 246 // parenthesis. 247 QueryCondition::Feature(ref f) => f.to_css(dest), 248 QueryCondition::Custom(ref name) => { 249 dest.write_char('(')?; 250 name.to_css(dest)?; 251 dest.write_char(')') 252 }, 253 QueryCondition::Not(ref c) => { 254 dest.write_str("not ")?; 255 c.to_css(dest) 256 }, 257 QueryCondition::InParens(ref c) => { 258 dest.write_char('(')?; 259 c.to_css(dest)?; 260 dest.write_char(')') 261 }, 262 QueryCondition::Style(ref c) => { 263 dest.write_str("style(")?; 264 c.to_css(dest)?; 265 dest.write_char(')') 266 }, 267 QueryCondition::MozPref(ref c) => { 268 dest.write_str("-moz-pref(")?; 269 c.to_css(dest)?; 270 dest.write_char(')') 271 }, 272 QueryCondition::Operation(ref list, op) => { 273 let mut iter = list.iter(); 274 iter.next().unwrap().to_css(dest)?; 275 for item in iter { 276 dest.write_char(' ')?; 277 op.to_css(dest)?; 278 dest.write_char(' ')?; 279 item.to_css(dest)?; 280 } 281 Ok(()) 282 }, 283 QueryCondition::GeneralEnclosed(ref s) => dest.write_str(&s), 284 } 285 } 286 } 287 288 /// <https://drafts.csswg.org/css-syntax-3/#typedef-any-value> 289 fn consume_any_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> { 290 input.expect_no_error_token().map_err(Into::into) 291 } 292 293 impl QueryCondition { 294 /// Parse a single condition. 295 pub fn parse<'i, 't>( 296 context: &ParserContext, 297 input: &mut Parser<'i, 't>, 298 feature_type: FeatureType, 299 ) -> Result<Self, ParseError<'i>> { 300 Self::parse_internal(context, input, feature_type, AllowOr::Yes) 301 } 302 303 fn visit<F>(&self, visitor: &mut F) 304 where 305 F: FnMut(&Self), 306 { 307 visitor(self); 308 match *self { 309 Self::Custom(..) 310 | Self::Feature(..) 311 | Self::GeneralEnclosed(..) 312 | Self::Style(..) 313 | Self::MozPref(..) => {}, 314 Self::Not(ref cond) => cond.visit(visitor), 315 Self::Operation(ref conds, _op) => { 316 for cond in conds.iter() { 317 cond.visit(visitor); 318 } 319 }, 320 Self::InParens(ref cond) => cond.visit(visitor), 321 } 322 } 323 324 /// Returns the union of all flags in the expression. This is useful for 325 /// container queries. 326 pub fn cumulative_flags(&self) -> FeatureFlags { 327 let mut result = FeatureFlags::empty(); 328 self.visit(&mut |condition| { 329 if let Self::Style(..) = condition { 330 result.insert(FeatureFlags::STYLE); 331 } 332 if let Self::Feature(ref f) = condition { 333 result.insert(f.feature_flags()) 334 } 335 }); 336 result 337 } 338 339 /// Parse a single condition, disallowing `or` expressions. 340 /// 341 /// To be used from the legacy query syntax. 342 pub fn parse_disallow_or<'i, 't>( 343 context: &ParserContext, 344 input: &mut Parser<'i, 't>, 345 feature_type: FeatureType, 346 ) -> Result<Self, ParseError<'i>> { 347 Self::parse_internal(context, input, feature_type, AllowOr::No) 348 } 349 350 /// https://drafts.csswg.org/mediaqueries-5/#typedef-media-condition or 351 /// https://drafts.csswg.org/mediaqueries-5/#typedef-media-condition-without-or 352 /// (depending on `allow_or`). 353 fn parse_internal<'i, 't>( 354 context: &ParserContext, 355 input: &mut Parser<'i, 't>, 356 feature_type: FeatureType, 357 allow_or: AllowOr, 358 ) -> Result<Self, ParseError<'i>> { 359 let location = input.current_source_location(); 360 if input.try_parse(|i| i.expect_ident_matching("not")).is_ok() { 361 let inner_condition = Self::parse_in_parens(context, input, feature_type)?; 362 return Ok(QueryCondition::Not(Box::new(inner_condition))); 363 } 364 365 let first_condition = Self::parse_in_parens(context, input, feature_type)?; 366 let operator = match input.try_parse(Operator::parse) { 367 Ok(op) => op, 368 Err(..) => return Ok(first_condition), 369 }; 370 371 if allow_or == AllowOr::No && operator == Operator::Or { 372 return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); 373 } 374 375 let mut conditions = vec![]; 376 conditions.push(first_condition); 377 conditions.push(Self::parse_in_parens(context, input, feature_type)?); 378 379 let delim = match operator { 380 Operator::And => "and", 381 Operator::Or => "or", 382 }; 383 384 loop { 385 if input.try_parse(|i| i.expect_ident_matching(delim)).is_err() { 386 return Ok(QueryCondition::Operation( 387 conditions.into_boxed_slice(), 388 operator, 389 )); 390 } 391 392 conditions.push(Self::parse_in_parens(context, input, feature_type)?); 393 } 394 } 395 396 fn parse_in_parenthesis_block<'i>( 397 context: &ParserContext, 398 input: &mut Parser<'i, '_>, 399 feature_type: FeatureType, 400 ) -> Result<Self, ParseError<'i>> { 401 // Base case. Make sure to preserve this error as it's more generally 402 // relevant. 403 let feature_error = match input.try_parse(|input| { 404 QueryFeatureExpression::parse_in_parenthesis_block(context, input, feature_type) 405 }) { 406 Ok(expr) => return Ok(Self::Feature(expr)), 407 Err(e) => e, 408 }; 409 if static_prefs::pref!("layout.css.custom-media.enabled") { 410 if let Ok(custom) = input.try_parse(|input| DashedIdent::parse(context, input)) { 411 return Ok(Self::Custom(custom)); 412 } 413 } 414 if let Ok(inner) = Self::parse(context, input, feature_type) { 415 return Ok(Self::InParens(Box::new(inner))); 416 } 417 Err(feature_error) 418 } 419 420 fn try_parse_block<'i, T, F>( 421 context: &ParserContext, 422 input: &mut Parser<'i, '_>, 423 start: SourcePosition, 424 parse: F, 425 ) -> Option<T> 426 where 427 F: for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i>>, 428 { 429 let nested = input.try_parse(|input| input.parse_nested_block(parse)); 430 match nested { 431 Ok(nested) => Some(nested), 432 Err(e) => { 433 // We're about to swallow the error in a `<general-enclosed>` 434 // condition, so report it while we can. 435 let loc = e.location; 436 let error = ContextualParseError::InvalidMediaRule(input.slice_from(start), e); 437 context.log_css_error(loc, error); 438 None 439 }, 440 } 441 } 442 443 /// Parse a condition in parentheses, or `<general-enclosed>`. 444 /// 445 /// https://drafts.csswg.org/mediaqueries/#typedef-media-in-parens 446 pub fn parse_in_parens<'i, 't>( 447 context: &ParserContext, 448 input: &mut Parser<'i, 't>, 449 feature_type: FeatureType, 450 ) -> Result<Self, ParseError<'i>> { 451 input.skip_whitespace(); 452 let start = input.position(); 453 let start_location = input.current_source_location(); 454 match *input.next()? { 455 Token::ParenthesisBlock => { 456 let nested = Self::try_parse_block(context, input, start, |input| { 457 Self::parse_in_parenthesis_block(context, input, feature_type) 458 }); 459 if let Some(nested) = nested { 460 return Ok(nested); 461 } 462 }, 463 Token::Function(ref name) => { 464 match_ignore_ascii_case! { name, 465 "style" => { 466 let feature = Self::try_parse_block(context, input, start, |input| { 467 StyleFeature::parse(context, input, feature_type) 468 }); 469 if let Some(feature) = feature { 470 return Ok(Self::Style(feature)); 471 } 472 }, 473 "-moz-pref" => { 474 let feature = Self::try_parse_block(context, input, start, |input| { 475 MozPrefFeature::parse(context, input, feature_type) 476 }); 477 if let Some(feature) = feature { 478 return Ok(Self::MozPref(feature)); 479 } 480 }, 481 _ => {}, 482 } 483 }, 484 ref t => return Err(start_location.new_unexpected_token_error(t.clone())), 485 } 486 input.parse_nested_block(consume_any_value)?; 487 Ok(Self::GeneralEnclosed(input.slice_from(start).to_owned())) 488 } 489 490 /// Whether this condition matches the device and quirks mode. 491 /// https://drafts.csswg.org/mediaqueries/#evaluating 492 /// https://drafts.csswg.org/mediaqueries/#typedef-general-enclosed 493 /// Kleene 3-valued logic is adopted here due to the introduction of 494 /// <general-enclosed>. 495 pub fn matches( 496 &self, 497 context: &computed::Context, 498 custom: &mut CustomMediaEvaluator, 499 ) -> KleeneValue { 500 match *self { 501 QueryCondition::Custom(ref f) => custom.matches(f, context), 502 QueryCondition::Feature(ref f) => f.matches(context), 503 QueryCondition::GeneralEnclosed(_) => KleeneValue::Unknown, 504 QueryCondition::InParens(ref c) => c.matches(context, custom), 505 QueryCondition::Not(ref c) => !c.matches(context, custom), 506 QueryCondition::Style(ref c) => c.matches(context), 507 QueryCondition::MozPref(ref c) => c.matches(context), 508 QueryCondition::Operation(ref conditions, op) => { 509 debug_assert!(!conditions.is_empty(), "We never create an empty op"); 510 match op { 511 Operator::And => { 512 KleeneValue::any_false(conditions.iter(), |c| c.matches(context, custom)) 513 }, 514 Operator::Or => { 515 KleeneValue::any(conditions.iter(), |c| c.matches(context, custom)) 516 }, 517 } 518 }, 519 } 520 } 521 }