blob: 10657c160a63f984c46a595af88d05d7fa06a17d [file] [log] [blame] [edit]
//! Media queries.
use crate::error::{ErrorWithLocation, MinifyError, MinifyErrorKind, ParserError, PrinterError};
use crate::macros::enum_property;
use crate::parser::starts_with_ignore_ascii_case;
use crate::printer::Printer;
use crate::properties::custom::EnvironmentVariable;
#[cfg(feature = "visitor")]
use crate::rules::container::ContainerSizeFeatureId;
use crate::rules::custom_media::CustomMediaRule;
use crate::rules::Location;
use crate::stylesheet::ParserOptions;
use crate::targets::{should_compile, Targets};
use crate::traits::{Parse, ParseWithOptions, ToCss};
use crate::values::ident::{DashedIdent, Ident};
use crate::values::number::{CSSInteger, CSSNumber};
use crate::values::string::CowArcStr;
use crate::values::{length::Length, ratio::Ratio, resolution::Resolution};
use crate::vendor_prefix::VendorPrefix;
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use bitflags::bitflags;
use cssparser::*;
#[cfg(feature = "into_owned")]
use static_self::IntoOwned;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
#[cfg(feature = "serde")]
use crate::serialization::ValueWrapper;
/// A [media query list](https://drafts.csswg.org/mediaqueries/#mq-list).
#[derive(Clone, Debug, PartialEq, Default)]
#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_media_list, MEDIA_QUERIES))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "camelCase")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct MediaList<'i> {
/// The list of media queries.
#[cfg_attr(feature = "serde", serde(borrow))]
pub media_queries: Vec<MediaQuery<'i>>,
}
impl<'i> MediaList<'i> {
/// Creates an empty media query list.
pub fn new() -> Self {
MediaList { media_queries: vec![] }
}
/// Parse a media query list from CSS.
pub fn parse<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let mut media_queries = vec![];
loop {
match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse_with_options(i, options)) {
Ok(mq) => {
media_queries.push(mq);
}
Err(err) => match err.kind {
ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput) => break,
_ => return Err(err),
},
}
match input.next() {
Ok(&Token::Comma) => {}
Ok(_) => unreachable!(),
Err(_) => break,
}
}
Ok(MediaList { media_queries })
}
pub(crate) fn transform_custom_media(
&mut self,
loc: Location,
custom_media: &HashMap<CowArcStr<'i>, CustomMediaRule<'i>>,
) -> Result<(), MinifyError> {
for query in self.media_queries.iter_mut() {
query.transform_custom_media(loc, custom_media)?;
}
Ok(())
}
pub(crate) fn transform_resolution(&mut self, targets: Targets) {
let mut i = 0;
while i < self.media_queries.len() {
let query = &self.media_queries[i];
let mut prefixes = query.get_necessary_prefixes(targets);
prefixes.remove(VendorPrefix::None);
if !prefixes.is_empty() {
let query = query.clone();
for prefix in prefixes {
let mut transformed = query.clone();
transformed.transform_resolution(prefix);
if !self.media_queries.contains(&transformed) {
self.media_queries.insert(i, transformed);
}
i += 1;
}
}
i += 1;
}
}
/// Returns whether the media query list always matches.
pub fn always_matches(&self) -> bool {
// If the media list is empty, it always matches.
self.media_queries.is_empty() || self.media_queries.iter().all(|mq| mq.always_matches())
}
/// Returns whether the media query list never matches.
pub fn never_matches(&self) -> bool {
!self.media_queries.is_empty() && self.media_queries.iter().all(|mq| mq.never_matches())
}
/// Attempts to combine the given media query list into this one. The resulting media query
/// list matches if both the original media query lists would have matched.
///
/// Returns an error if the boolean logic is not possible.
pub fn and(&mut self, b: &MediaList<'i>) -> Result<(), ()> {
if self.media_queries.is_empty() {
self.media_queries.extend(b.media_queries.iter().cloned());
return Ok(());
}
for b in &b.media_queries {
if self.media_queries.contains(&b) {
continue;
}
for a in &mut self.media_queries {
a.and(&b)?;
}
}
Ok(())
}
/// Combines the given media query list into this one. The resulting media query list
/// matches if either of the original media query lists would have matched.
pub fn or(&mut self, b: &MediaList<'i>) {
for mq in &b.media_queries {
if !self.media_queries.contains(&mq) {
self.media_queries.push(mq.clone())
}
}
}
}
impl<'i> ToCss for MediaList<'i> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
if self.media_queries.is_empty() {
dest.write_str("not all")?;
return Ok(());
}
let mut first = true;
for query in &self.media_queries {
if !first {
dest.delim(',', false)?;
}
first = false;
query.to_css(dest)?;
}
Ok(())
}
}
enum_property! {
/// A [media query qualifier](https://drafts.csswg.org/mediaqueries/#mq-prefix).
pub enum Qualifier {
/// Prevents older browsers from matching the media query.
Only,
/// Negates a media query.
Not,
}
}
/// A [media type](https://drafts.csswg.org/mediaqueries/#media-types) within a media query.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case", into = "CowArcStr", from = "CowArcStr")
)]
pub enum MediaType<'i> {
/// Matches all devices.
All,
/// Matches printers, and devices intended to reproduce a printed
/// display, such as a web browser showing a document in “Print Preview”.
Print,
/// Matches all devices that aren’t matched by print.
Screen,
/// An unknown media type.
#[cfg_attr(feature = "serde", serde(borrow))]
Custom(CowArcStr<'i>),
}
impl<'i> From<CowArcStr<'i>> for MediaType<'i> {
fn from(name: CowArcStr<'i>) -> Self {
match_ignore_ascii_case! { &*name,
"all" => MediaType::All,
"print" => MediaType::Print,
"screen" => MediaType::Screen,
_ => MediaType::Custom(name)
}
}
}
impl<'i> Into<CowArcStr<'i>> for MediaType<'i> {
fn into(self) -> CowArcStr<'i> {
match self {
MediaType::All => "all".into(),
MediaType::Print => "print".into(),
MediaType::Screen => "screen".into(),
MediaType::Custom(desc) => desc,
}
}
}
impl<'i> Parse<'i> for MediaType<'i> {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let name: CowArcStr = input.expect_ident()?.into();
Ok(Self::from(name))
}
}
#[cfg(feature = "jsonschema")]
#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
impl<'a> schemars::JsonSchema for MediaType<'a> {
fn is_referenceable() -> bool {
true
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
str::json_schema(gen)
}
fn schema_name() -> String {
"MediaType".into()
}
}
/// A [media query](https://drafts.csswg.org/mediaqueries/#media).
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "visitor", visit(visit_media_query, MEDIA_QUERIES))]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(rename_all = "camelCase"))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct MediaQuery<'i> {
/// The qualifier for this query.
pub qualifier: Option<Qualifier>,
/// The media type for this query, that can be known, unknown, or "all".
#[cfg_attr(feature = "serde", serde(borrow))]
pub media_type: MediaType<'i>,
/// The condition that this media query contains. This cannot have `or`
/// in the first level.
pub condition: Option<MediaCondition<'i>>,
}
impl<'i> ParseWithOptions<'i> for MediaQuery<'i> {
fn parse_with_options<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let (qualifier, explicit_media_type) = input
.try_parse(|input| -> Result<_, ParseError<'i, ParserError<'i>>> {
let qualifier = input.try_parse(Qualifier::parse).ok();
let media_type = MediaType::parse(input)?;
Ok((qualifier, Some(media_type)))
})
.unwrap_or_default();
let condition = if explicit_media_type.is_none() {
Some(MediaCondition::parse_with_flags(
input,
QueryConditionFlags::ALLOW_OR,
options,
)?)
} else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() {
Some(MediaCondition::parse_with_flags(
input,
QueryConditionFlags::empty(),
options,
)?)
} else {
None
};
let media_type = explicit_media_type.unwrap_or(MediaType::All);
Ok(Self {
qualifier,
media_type,
condition,
})
}
}
impl<'i> MediaQuery<'i> {
fn transform_custom_media(
&mut self,
loc: Location,
custom_media: &HashMap<CowArcStr<'i>, CustomMediaRule<'i>>,
) -> Result<(), MinifyError> {
if let Some(condition) = &mut self.condition {
let used = process_condition(
loc,
custom_media,
&mut self.media_type,
&mut self.qualifier,
condition,
&mut HashSet::new(),
)?;
if !used {
self.condition = None;
}
}
Ok(())
}
fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
if let Some(condition) = &self.condition {
condition.get_necessary_prefixes(targets)
} else {
VendorPrefix::empty()
}
}
fn transform_resolution(&mut self, prefix: VendorPrefix) {
if let Some(condition) = &mut self.condition {
condition.transform_resolution(prefix)
}
}
/// Returns whether the media query is guaranteed to always match.
pub fn always_matches(&self) -> bool {
self.qualifier == None && self.media_type == MediaType::All && self.condition == None
}
/// Returns whether the media query is guaranteed to never match.
pub fn never_matches(&self) -> bool {
self.qualifier == Some(Qualifier::Not) && self.media_type == MediaType::All && self.condition == None
}
/// Attempts to combine the given media query into this one. The resulting media query
/// matches if both of the original media queries would have matched.
///
/// Returns an error if the boolean logic is not possible.
pub fn and<'a>(&mut self, b: &MediaQuery<'i>) -> Result<(), ()> {
let at = (&self.qualifier, &self.media_type);
let bt = (&b.qualifier, &b.media_type);
let (qualifier, media_type) = match (at, bt) {
// `not all and screen` => not all
// `screen and not all` => not all
((&Some(Qualifier::Not), &MediaType::All), _) |
(_, (&Some(Qualifier::Not), &MediaType::All)) => (Some(Qualifier::Not), MediaType::All),
// `not screen and not print` => ERROR
// `not screen and not screen` => not screen
((&Some(Qualifier::Not), a), (&Some(Qualifier::Not), b)) => {
if a == b {
(Some(Qualifier::Not), a.clone())
} else {
return Err(())
}
},
// `all and print` => print
// `print and all` => print
// `all and not print` => not print
((_, MediaType::All), (q, t)) |
((q, t), (_, MediaType::All)) |
// `not screen and print` => print
// `print and not screen` => print
((&Some(Qualifier::Not), _), (q, t)) |
((q, t), (&Some(Qualifier::Not), _)) => (q.clone(), t.clone()),
// `print and screen` => not all
((_, a), (_, b)) if a != b => (Some(Qualifier::Not), MediaType::All),
((_, a), _) => (None, a.clone())
};
self.qualifier = qualifier;
self.media_type = media_type;
if let Some(cond) = &b.condition {
self.condition = if let Some(condition) = &self.condition {
if condition != cond {
Some(MediaCondition::Operation {
conditions: vec![condition.clone(), cond.clone()],
operator: Operator::And,
})
} else {
Some(condition.clone())
}
} else {
Some(cond.clone())
}
}
Ok(())
}
}
impl<'i> ToCss for MediaQuery<'i> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
if let Some(qual) = self.qualifier {
qual.to_css(dest)?;
dest.write_char(' ')?;
}
match self.media_type {
MediaType::All => {
// We need to print "all" if there's a qualifier, or there's
// just an empty list of expressions.
//
// Otherwise, we'd serialize media queries like "(min-width:
// 40px)" in "all (min-width: 40px)", which is unexpected.
if self.qualifier.is_some() || self.condition.is_none() {
dest.write_str("all")?;
}
}
MediaType::Print => dest.write_str("print")?,
MediaType::Screen => dest.write_str("screen")?,
MediaType::Custom(ref desc) => dest.write_str(desc)?,
}
let condition = match self.condition {
Some(ref c) => c,
None => return Ok(()),
};
let needs_parens = if self.media_type != MediaType::All || self.qualifier.is_some() {
dest.write_str(" and ")?;
matches!(condition, MediaCondition::Operation { operator, .. } if *operator != Operator::And)
} else {
false
};
to_css_with_parens_if_needed(condition, dest, needs_parens)
}
}
#[cfg(feature = "serde")]
#[derive(serde::Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
enum MediaQueryOrRaw<'i> {
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
MediaQuery {
qualifier: Option<Qualifier>,
#[cfg_attr(feature = "serde", serde(borrow))]
media_type: MediaType<'i>,
condition: Option<MediaCondition<'i>>,
},
Raw {
raw: CowArcStr<'i>,
},
}
#[cfg(feature = "serde")]
impl<'i, 'de: 'i> serde::Deserialize<'de> for MediaQuery<'i> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let mq = MediaQueryOrRaw::deserialize(deserializer)?;
match mq {
MediaQueryOrRaw::MediaQuery {
qualifier,
media_type,
condition,
} => Ok(MediaQuery {
qualifier,
media_type,
condition,
}),
MediaQueryOrRaw::Raw { raw } => {
let res = MediaQuery::parse_string_with_options(raw.as_ref(), ParserOptions::default())
.map_err(|_| serde::de::Error::custom("Could not parse value"))?;
Ok(res.into_owned())
}
}
}
}
enum_property! {
/// A binary `and` or `or` operator.
pub enum Operator {
/// The `and` operator.
And,
/// The `or` operator.
Or,
}
}
/// Represents a media condition.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum MediaCondition<'i> {
/// A media feature, implicitly parenthesized.
#[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<MediaFeature>"))]
Feature(MediaFeature<'i>),
/// A negation of a condition.
#[cfg_attr(feature = "visitor", skip_type)]
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<MediaCondition>>"))]
Not(Box<MediaCondition<'i>>),
/// A set of joint operations.
#[cfg_attr(feature = "visitor", skip_type)]
Operation {
/// The operator for the conditions.
operator: Operator,
/// The conditions for the operator.
conditions: Vec<MediaCondition<'i>>,
},
}
/// A trait for conditions such as media queries and container queries.
pub(crate) trait QueryCondition<'i>: Sized {
fn parse_feature<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>>;
fn create_negation(condition: Box<Self>) -> Self;
fn create_operation(operator: Operator, conditions: Vec<Self>) -> Self;
fn parse_style_query<'t>(
input: &mut Parser<'i, 't>,
_options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
Err(input.new_error_for_next_token())
}
fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool;
}
impl<'i> QueryCondition<'i> for MediaCondition<'i> {
#[inline]
fn parse_feature<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let feature = MediaFeature::parse_with_options(input, options)?;
Ok(Self::Feature(feature))
}
#[inline]
fn create_negation(condition: Box<MediaCondition<'i>>) -> Self {
Self::Not(condition)
}
#[inline]
fn create_operation(operator: Operator, conditions: Vec<MediaCondition<'i>>) -> Self {
Self::Operation { operator, conditions }
}
fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool {
match self {
MediaCondition::Not(_) => true,
MediaCondition::Operation { operator, .. } => Some(*operator) != parent_operator,
MediaCondition::Feature(f) => f.needs_parens(parent_operator, targets),
}
}
}
bitflags! {
/// Flags for `parse_query_condition`.
#[derive(PartialEq, Eq, Clone, Copy)]
pub(crate) struct QueryConditionFlags: u8 {
/// Whether to allow top-level "or" boolean logic.
const ALLOW_OR = 1 << 0;
/// Whether to allow style container queries.
const ALLOW_STYLE = 1 << 1;
}
}
impl<'i> MediaCondition<'i> {
/// Parse a single media condition.
fn parse_with_flags<'t>(
input: &mut Parser<'i, 't>,
flags: QueryConditionFlags,
options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
parse_query_condition(input, flags, options)
}
fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
match self {
MediaCondition::Feature(MediaFeature::Range {
name: MediaFeatureName::Standard(MediaFeatureId::Resolution),
..
}) => targets.prefixes(VendorPrefix::None, crate::prefixes::Feature::AtResolution),
MediaCondition::Not(not) => not.get_necessary_prefixes(targets),
MediaCondition::Operation { conditions, .. } => {
let mut prefixes = VendorPrefix::empty();
for condition in conditions {
prefixes |= condition.get_necessary_prefixes(targets);
}
prefixes
}
_ => VendorPrefix::empty(),
}
}
fn transform_resolution(&mut self, prefix: VendorPrefix) {
match self {
MediaCondition::Feature(MediaFeature::Range {
name: MediaFeatureName::Standard(MediaFeatureId::Resolution),
operator,
value: MediaFeatureValue::Resolution(value),
}) => match prefix {
VendorPrefix::WebKit | VendorPrefix::Moz => {
*self = MediaCondition::Feature(MediaFeature::Range {
name: MediaFeatureName::Standard(match prefix {
VendorPrefix::WebKit => MediaFeatureId::WebKitDevicePixelRatio,
VendorPrefix::Moz => MediaFeatureId::MozDevicePixelRatio,
_ => unreachable!(),
}),
operator: *operator,
value: MediaFeatureValue::Number(match value {
Resolution::Dpi(dpi) => *dpi / 96.0,
Resolution::Dpcm(dpcm) => *dpcm * 2.54 / 96.0,
Resolution::Dppx(dppx) => *dppx,
}),
});
}
_ => {}
},
MediaCondition::Not(not) => not.transform_resolution(prefix),
MediaCondition::Operation { conditions, .. } => {
for condition in conditions {
condition.transform_resolution(prefix);
}
}
_ => {}
}
}
}
impl<'i> ParseWithOptions<'i> for MediaCondition<'i> {
fn parse_with_options<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
Self::parse_with_flags(input, QueryConditionFlags::ALLOW_OR, options)
}
}
/// Parse a single query condition.
pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>(
input: &mut Parser<'i, 't>,
flags: QueryConditionFlags,
options: &ParserOptions<'_, 'i>,
) -> Result<P, ParseError<'i, ParserError<'i>>> {
let location = input.current_source_location();
let (is_negation, is_style) = match *input.next()? {
Token::ParenthesisBlock => (false, false),
Token::Ident(ref ident) if ident.eq_ignore_ascii_case("not") => (true, false),
Token::Function(ref f)
if flags.contains(QueryConditionFlags::ALLOW_STYLE) && f.eq_ignore_ascii_case("style") =>
{
(false, true)
}
ref t => return Err(location.new_unexpected_token_error(t.clone())),
};
let first_condition = match (is_negation, is_style) {
(true, false) => {
let inner_condition = parse_parens_or_function(input, flags, options)?;
return Ok(P::create_negation(Box::new(inner_condition)));
}
(true, true) => {
let inner_condition = P::parse_style_query(input, options)?;
return Ok(P::create_negation(Box::new(inner_condition)));
}
(false, false) => parse_paren_block(input, flags, options)?,
(false, true) => P::parse_style_query(input, options)?,
};
let operator = match input.try_parse(Operator::parse) {
Ok(op) => op,
Err(..) => return Ok(first_condition),
};
if !flags.contains(QueryConditionFlags::ALLOW_OR) && operator == Operator::Or {
return Err(location.new_unexpected_token_error(Token::Ident("or".into())));
}
let mut conditions = vec![];
conditions.push(first_condition);
conditions.push(parse_parens_or_function(input, flags, options)?);
let delim = match operator {
Operator::And => "and",
Operator::Or => "or",
};
loop {
if input.try_parse(|i| i.expect_ident_matching(delim)).is_err() {
return Ok(P::create_operation(operator, conditions));
}
conditions.push(parse_parens_or_function(input, flags, options)?);
}
}
/// Parse a media condition in parentheses, or a style() function.
fn parse_parens_or_function<'t, 'i, P: QueryCondition<'i>>(
input: &mut Parser<'i, 't>,
flags: QueryConditionFlags,
options: &ParserOptions<'_, 'i>,
) -> Result<P, ParseError<'i, ParserError<'i>>> {
let location = input.current_source_location();
match *input.next()? {
Token::ParenthesisBlock => parse_paren_block(input, flags, options),
Token::Function(ref f)
if flags.contains(QueryConditionFlags::ALLOW_STYLE) && f.eq_ignore_ascii_case("style") =>
{
P::parse_style_query(input, options)
}
ref t => return Err(location.new_unexpected_token_error(t.clone())),
}
}
fn parse_paren_block<'t, 'i, P: QueryCondition<'i>>(
input: &mut Parser<'i, 't>,
flags: QueryConditionFlags,
options: &ParserOptions<'_, 'i>,
) -> Result<P, ParseError<'i, ParserError<'i>>> {
input.parse_nested_block(|input| {
if let Ok(inner) =
input.try_parse(|i| parse_query_condition(i, flags | QueryConditionFlags::ALLOW_OR, options))
{
return Ok(inner);
}
P::parse_feature(input, options)
})
}
pub(crate) fn to_css_with_parens_if_needed<V: ToCss, W>(
value: V,
dest: &mut Printer<W>,
needs_parens: bool,
) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
if needs_parens {
dest.write_char('(')?;
}
value.to_css(dest)?;
if needs_parens {
dest.write_char(')')?;
}
Ok(())
}
pub(crate) fn operation_to_css<'i, V: ToCss + QueryCondition<'i>, W>(
operator: Operator,
conditions: &Vec<V>,
dest: &mut Printer<W>,
) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
let mut iter = conditions.iter();
let first = iter.next().unwrap();
to_css_with_parens_if_needed(first, dest, first.needs_parens(Some(operator), &dest.targets.current))?;
for item in iter {
dest.write_char(' ')?;
operator.to_css(dest)?;
dest.write_char(' ')?;
to_css_with_parens_if_needed(item, dest, item.needs_parens(Some(operator), &dest.targets.current))?;
}
Ok(())
}
impl<'i> MediaCondition<'i> {
fn negate(&self) -> Option<MediaCondition<'i>> {
match self {
MediaCondition::Not(not) => Some((**not).clone()),
MediaCondition::Feature(f) => f.negate().map(MediaCondition::Feature),
_ => None,
}
}
}
impl<'i> ToCss for MediaCondition<'i> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match *self {
MediaCondition::Feature(ref f) => f.to_css(dest),
MediaCondition::Not(ref c) => {
if let Some(negated) = c.negate() {
negated.to_css(dest)
} else {
dest.write_str("not ")?;
to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current))
}
}
MediaCondition::Operation {
ref conditions,
operator,
} => operation_to_css(operator, conditions, dest),
}
}
}
/// A [comparator](https://drafts.csswg.org/mediaqueries/#typedef-mf-comparison) within a media query.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum MediaFeatureComparison {
/// `=`
Equal,
/// `>`
GreaterThan,
/// `>=`
GreaterThanEqual,
/// `<`
LessThan,
/// `<=`
LessThanEqual,
}
impl ToCss for MediaFeatureComparison {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
use MediaFeatureComparison::*;
match self {
Equal => dest.delim('=', true),
GreaterThan => dest.delim('>', true),
GreaterThanEqual => {
dest.whitespace()?;
dest.write_str(">=")?;
dest.whitespace()
}
LessThan => dest.delim('<', true),
LessThanEqual => {
dest.whitespace()?;
dest.write_str("<=")?;
dest.whitespace()
}
}
}
}
impl MediaFeatureComparison {
fn opposite(&self) -> MediaFeatureComparison {
match self {
MediaFeatureComparison::GreaterThan => MediaFeatureComparison::LessThan,
MediaFeatureComparison::GreaterThanEqual => MediaFeatureComparison::LessThanEqual,
MediaFeatureComparison::LessThan => MediaFeatureComparison::GreaterThan,
MediaFeatureComparison::LessThanEqual => MediaFeatureComparison::GreaterThanEqual,
MediaFeatureComparison::Equal => MediaFeatureComparison::Equal,
}
}
fn negate(&self) -> MediaFeatureComparison {
match self {
MediaFeatureComparison::GreaterThan => MediaFeatureComparison::LessThanEqual,
MediaFeatureComparison::GreaterThanEqual => MediaFeatureComparison::LessThan,
MediaFeatureComparison::LessThan => MediaFeatureComparison::GreaterThanEqual,
MediaFeatureComparison::LessThanEqual => MediaFeatureComparison::GreaterThan,
MediaFeatureComparison::Equal => MediaFeatureComparison::Equal,
}
}
}
/// A generic media feature or container feature.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "visitor",
derive(Visit),
visit(visit_media_feature, MEDIA_QUERIES, <'i, MediaFeatureId>),
visit(<'i, ContainerSizeFeatureId>)
)]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum QueryFeature<'i, FeatureId> {
/// A plain media feature, e.g. `(min-width: 240px)`.
Plain {
/// The name of the feature.
#[cfg_attr(feature = "serde", serde(borrow))]
name: MediaFeatureName<'i, FeatureId>,
/// The feature value.
value: MediaFeatureValue<'i>,
},
/// A boolean feature, e.g. `(hover)`.
Boolean {
/// The name of the feature.
name: MediaFeatureName<'i, FeatureId>,
},
/// A range, e.g. `(width > 240px)`.
Range {
/// The name of the feature.
name: MediaFeatureName<'i, FeatureId>,
/// A comparator.
operator: MediaFeatureComparison,
/// The feature value.
value: MediaFeatureValue<'i>,
},
/// An interval, e.g. `(120px < width < 240px)`.
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
Interval {
/// The name of the feature.
name: MediaFeatureName<'i, FeatureId>,
/// A start value.
start: MediaFeatureValue<'i>,
/// A comparator for the start value.
start_operator: MediaFeatureComparison,
/// The end value.
end: MediaFeatureValue<'i>,
/// A comparator for the end value.
end_operator: MediaFeatureComparison,
},
}
/// A [media feature](https://drafts.csswg.org/mediaqueries/#typedef-media-feature)
pub type MediaFeature<'i> = QueryFeature<'i, MediaFeatureId>;
impl<'i, FeatureId> ParseWithOptions<'i> for QueryFeature<'i, FeatureId>
where
FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType + Clone,
{
fn parse_with_options<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
match input.try_parse(|input| Self::parse_name_first(input, options)) {
Ok(res) => Ok(res),
Err(
err @ ParseError {
kind: ParseErrorKind::Custom(ParserError::InvalidMediaQuery),
..
},
) => Err(err),
_ => Self::parse_value_first(input),
}
}
}
impl<'i, FeatureId> QueryFeature<'i, FeatureId>
where
FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType + Clone,
{
fn parse_name_first<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let (name, legacy_op) = MediaFeatureName::parse(input)?;
let operator = input.try_parse(|input| consume_operation_or_colon(input, true));
let operator = match operator {
Err(..) => return Ok(QueryFeature::Boolean { name }),
Ok(operator) => operator,
};
if operator.is_some() && legacy_op.is_some() {
dbg!();
return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
}
let value = MediaFeatureValue::parse(input, name.value_type())?;
if !value.check_type(name.value_type()) {
if options.error_recovery {
options.warn(ParseError {
kind: ParseErrorKind::Custom(ParserError::InvalidMediaQuery),
location: input.current_source_location(),
});
} else {
return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
}
}
if let Some(operator) = operator.or(legacy_op) {
if !name.value_type().allows_ranges() {
dbg!();
return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
}
Ok(QueryFeature::Range { name, operator, value })
} else {
Ok(QueryFeature::Plain { name, value })
}
}
fn parse_value_first<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
// We need to find the feature name first so we know the type.
let start = input.state();
let name = loop {
if let Ok((name, legacy_op)) = MediaFeatureName::parse(input) {
if legacy_op.is_some() {
dbg!();
return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
}
break name;
}
if input.is_exhausted() {
dbg!();
return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
}
};
input.reset(&start);
// Now we can parse the first value.
let value = MediaFeatureValue::parse(input, name.value_type())?;
let operator = consume_operation_or_colon(input, false)?;
// Skip over the feature name again.
{
let (feature_name, _) = MediaFeatureName::parse(input)?;
debug_assert_eq!(name, feature_name);
}
if !name.value_type().allows_ranges() || !value.check_type(name.value_type()) {
dbg!();
return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
}
if let Ok(end_operator) = input.try_parse(|input| consume_operation_or_colon(input, false)) {
let start_operator = operator.unwrap();
let end_operator = end_operator.unwrap();
// Start and end operators must be matching.
match (start_operator, end_operator) {
(MediaFeatureComparison::GreaterThan, MediaFeatureComparison::GreaterThan)
| (MediaFeatureComparison::GreaterThan, MediaFeatureComparison::GreaterThanEqual)
| (MediaFeatureComparison::GreaterThanEqual, MediaFeatureComparison::GreaterThanEqual)
| (MediaFeatureComparison::GreaterThanEqual, MediaFeatureComparison::GreaterThan)
| (MediaFeatureComparison::LessThan, MediaFeatureComparison::LessThan)
| (MediaFeatureComparison::LessThan, MediaFeatureComparison::LessThanEqual)
| (MediaFeatureComparison::LessThanEqual, MediaFeatureComparison::LessThanEqual)
| (MediaFeatureComparison::LessThanEqual, MediaFeatureComparison::LessThan) => {}
_ => return Err(input.new_custom_error(ParserError::InvalidMediaQuery)),
};
let end_value = MediaFeatureValue::parse(input, name.value_type())?;
if !end_value.check_type(name.value_type()) {
return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
}
Ok(QueryFeature::Interval {
name,
start: value,
start_operator,
end: end_value,
end_operator,
})
} else {
let operator = operator.unwrap().opposite();
Ok(QueryFeature::Range { name, operator, value })
}
}
pub(crate) fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool {
if !should_compile!(targets, MediaIntervalSyntax) {
return false;
}
match self {
QueryFeature::Interval { .. } => parent_operator != Some(Operator::And),
QueryFeature::Range { operator, .. } => {
matches!(
operator,
MediaFeatureComparison::GreaterThan | MediaFeatureComparison::LessThan
)
}
_ => false,
}
}
fn negate(&self) -> Option<QueryFeature<'i, FeatureId>> {
match self {
QueryFeature::Range { name, operator, value } => Some(QueryFeature::Range {
name: (*name).clone(),
operator: operator.negate(),
value: value.clone(),
}),
_ => None,
}
}
}
impl<'i, FeatureId: FeatureToCss> ToCss for QueryFeature<'i, FeatureId> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match self {
QueryFeature::Boolean { name } => {
dest.write_char('(')?;
name.to_css(dest)?;
}
QueryFeature::Plain { name, value } => {
dest.write_char('(')?;
name.to_css(dest)?;
dest.delim(':', false)?;
value.to_css(dest)?;
}
QueryFeature::Range { name, operator, value } => {
// If range syntax is unsupported, use min/max prefix if possible.
if should_compile!(dest.targets.current, MediaRangeSyntax) {
return write_min_max(operator, name, value, dest, false);
}
dest.write_char('(')?;
name.to_css(dest)?;
operator.to_css(dest)?;
value.to_css(dest)?;
}
QueryFeature::Interval {
name,
start,
start_operator,
end,
end_operator,
} => {
if should_compile!(dest.targets.current, MediaIntervalSyntax) {
write_min_max(&start_operator.opposite(), name, start, dest, true)?;
dest.write_str(" and ")?;
return write_min_max(end_operator, name, end, dest, true);
}
dest.write_char('(')?;
start.to_css(dest)?;
start_operator.to_css(dest)?;
name.to_css(dest)?;
end_operator.to_css(dest)?;
end.to_css(dest)?;
}
}
dest.write_char(')')
}
}
/// A media feature name.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum MediaFeatureName<'i, FeatureId> {
/// A standard media query feature identifier.
Standard(FeatureId),
/// A custom author-defined environment variable.
#[cfg_attr(feature = "serde", serde(borrow))]
Custom(DashedIdent<'i>),
/// An unknown environment variable.
Unknown(Ident<'i>),
}
impl<'i, FeatureId: for<'x> Parse<'x>> MediaFeatureName<'i, FeatureId> {
/// Parses a media feature name.
pub fn parse<'t>(
input: &mut Parser<'i, 't>,
) -> Result<(Self, Option<MediaFeatureComparison>), ParseError<'i, ParserError<'i>>> {
let ident = input.expect_ident()?;
if ident.starts_with("--") {
return Ok((MediaFeatureName::Custom(DashedIdent(ident.into())), None));
}
let mut name = ident.as_ref();
// Webkit places its prefixes before "min" and "max". Remove it first, and
// re-add after removing min/max.
let is_webkit = starts_with_ignore_ascii_case(&name, "-webkit-");
if is_webkit {
name = &name[8..];
}
let comparator = if starts_with_ignore_ascii_case(&name, "min-") {
name = &name[4..];
Some(MediaFeatureComparison::GreaterThanEqual)
} else if starts_with_ignore_ascii_case(&name, "max-") {
name = &name[4..];
Some(MediaFeatureComparison::LessThanEqual)
} else {
None
};
let name = if is_webkit {
Cow::Owned(format!("-webkit-{}", name))
} else {
Cow::Borrowed(name)
};
if let Ok(standard) = FeatureId::parse_string(&name) {
return Ok((MediaFeatureName::Standard(standard), comparator));
}
Ok((MediaFeatureName::Unknown(Ident(ident.into())), None))
}
}
mod private {
use super::*;
/// A trait for feature ids which can get a value type.
pub trait ValueType {
/// Returns the value type for this feature id.
fn value_type(&self) -> MediaFeatureType;
}
}
pub(crate) use private::ValueType;
impl<'i, FeatureId: ValueType> ValueType for MediaFeatureName<'i, FeatureId> {
fn value_type(&self) -> MediaFeatureType {
match self {
Self::Standard(standard) => standard.value_type(),
_ => MediaFeatureType::Unknown,
}
}
}
impl<'i, FeatureId: FeatureToCss> ToCss for MediaFeatureName<'i, FeatureId> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match self {
Self::Standard(v) => v.to_css(dest),
Self::Custom(v) => v.to_css(dest),
Self::Unknown(v) => v.to_css(dest),
}
}
}
impl<'i, FeatureId: FeatureToCss> FeatureToCss for MediaFeatureName<'i, FeatureId> {
fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match self {
Self::Standard(v) => v.to_css_with_prefix(prefix, dest),
Self::Custom(v) => {
dest.write_str(prefix)?;
v.to_css(dest)
}
Self::Unknown(v) => {
dest.write_str(prefix)?;
v.to_css(dest)
}
}
}
}
/// The type of a media feature.
#[derive(PartialEq)]
pub enum MediaFeatureType {
/// A length value.
Length,
/// A number value.
Number,
/// An integer value.
Integer,
/// A boolean value, either 0 or 1.
Boolean,
/// A resolution.
Resolution,
/// A ratio.
Ratio,
/// An identifier.
Ident,
/// An unknown type.
Unknown,
}
impl MediaFeatureType {
fn allows_ranges(&self) -> bool {
use MediaFeatureType::*;
match self {
Length => true,
Number => true,
Integer => true,
Boolean => false,
Resolution => true,
Ratio => true,
Ident => false,
Unknown => true,
}
}
}
macro_rules! define_query_features {
(
$(#[$outer:meta])*
$vis:vis enum $name:ident {
$(
$(#[$meta: meta])*
$str: literal: $id: ident = $ty: ident,
)+
}
) => {
crate::macros::enum_property! {
$(#[$outer])*
$vis enum $name {
$(
$(#[$meta])*
$str: $id,
)+
}
}
impl ValueType for $name {
fn value_type(&self) -> MediaFeatureType {
match self {
$(
Self::$id => MediaFeatureType::$ty,
)+
}
}
}
}
}
pub(crate) use define_query_features;
define_query_features! {
/// A media query feature identifier.
pub enum MediaFeatureId {
/// The [width](https://w3c.github.io/csswg-drafts/mediaqueries-5/#width) media feature.
"width": Width = Length,
/// The [height](https://w3c.github.io/csswg-drafts/mediaqueries-5/#height) media feature.
"height": Height = Length,
/// The [aspect-ratio](https://w3c.github.io/csswg-drafts/mediaqueries-5/#aspect-ratio) media feature.
"aspect-ratio": AspectRatio = Ratio,
/// The [orientation](https://w3c.github.io/csswg-drafts/mediaqueries-5/#orientation) media feature.
"orientation": Orientation = Ident,
/// The [overflow-block](https://w3c.github.io/csswg-drafts/mediaqueries-5/#overflow-block) media feature.
"overflow-block": OverflowBlock = Ident,
/// The [overflow-inline](https://w3c.github.io/csswg-drafts/mediaqueries-5/#overflow-inline) media feature.
"overflow-inline": OverflowInline = Ident,
/// The [horizontal-viewport-segments](https://w3c.github.io/csswg-drafts/mediaqueries-5/#horizontal-viewport-segments) media feature.
"horizontal-viewport-segments": HorizontalViewportSegments = Integer,
/// The [vertical-viewport-segments](https://w3c.github.io/csswg-drafts/mediaqueries-5/#vertical-viewport-segments) media feature.
"vertical-viewport-segments": VerticalViewportSegments = Integer,
/// The [display-mode](https://w3c.github.io/csswg-drafts/mediaqueries-5/#display-mode) media feature.
"display-mode": DisplayMode = Ident,
/// The [resolution](https://w3c.github.io/csswg-drafts/mediaqueries-5/#resolution) media feature.
"resolution": Resolution = Resolution, // | infinite??
/// The [scan](https://w3c.github.io/csswg-drafts/mediaqueries-5/#scan) media feature.
"scan": Scan = Ident,
/// The [grid](https://w3c.github.io/csswg-drafts/mediaqueries-5/#grid) media feature.
"grid": Grid = Boolean,
/// The [update](https://w3c.github.io/csswg-drafts/mediaqueries-5/#update) media feature.
"update": Update = Ident,
/// The [environment-blending](https://w3c.github.io/csswg-drafts/mediaqueries-5/#environment-blending) media feature.
"environment-blending": EnvironmentBlending = Ident,
/// The [color](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color) media feature.
"color": Color = Integer,
/// The [color-index](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color-index) media feature.
"color-index": ColorIndex = Integer,
/// The [monochrome](https://w3c.github.io/csswg-drafts/mediaqueries-5/#monochrome) media feature.
"monochrome": Monochrome = Integer,
/// The [color-gamut](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color-gamut) media feature.
"color-gamut": ColorGamut = Ident,
/// The [dynamic-range](https://w3c.github.io/csswg-drafts/mediaqueries-5/#dynamic-range) media feature.
"dynamic-range": DynamicRange = Ident,
/// The [inverted-colors](https://w3c.github.io/csswg-drafts/mediaqueries-5/#inverted-colors) media feature.
"inverted-colors": InvertedColors = Ident,
/// The [pointer](https://w3c.github.io/csswg-drafts/mediaqueries-5/#pointer) media feature.
"pointer": Pointer = Ident,
/// The [hover](https://w3c.github.io/csswg-drafts/mediaqueries-5/#hover) media feature.
"hover": Hover = Ident,
/// The [any-pointer](https://w3c.github.io/csswg-drafts/mediaqueries-5/#any-pointer) media feature.
"any-pointer": AnyPointer = Ident,
/// The [any-hover](https://w3c.github.io/csswg-drafts/mediaqueries-5/#any-hover) media feature.
"any-hover": AnyHover = Ident,
/// The [nav-controls](https://w3c.github.io/csswg-drafts/mediaqueries-5/#nav-controls) media feature.
"nav-controls": NavControls = Ident,
/// The [video-color-gamut](https://w3c.github.io/csswg-drafts/mediaqueries-5/#video-color-gamut) media feature.
"video-color-gamut": VideoColorGamut = Ident,
/// The [video-dynamic-range](https://w3c.github.io/csswg-drafts/mediaqueries-5/#video-dynamic-range) media feature.
"video-dynamic-range": VideoDynamicRange = Ident,
/// The [scripting](https://w3c.github.io/csswg-drafts/mediaqueries-5/#scripting) media feature.
"scripting": Scripting = Ident,
/// The [prefers-reduced-motion](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-motion) media feature.
"prefers-reduced-motion": PrefersReducedMotion = Ident,
/// The [prefers-reduced-transparency](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-transparency) media feature.
"prefers-reduced-transparency": PrefersReducedTransparency = Ident,
/// The [prefers-contrast](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-contrast) media feature.
"prefers-contrast": PrefersContrast = Ident,
/// The [forced-colors](https://w3c.github.io/csswg-drafts/mediaqueries-5/#forced-colors) media feature.
"forced-colors": ForcedColors = Ident,
/// The [prefers-color-scheme](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-color-scheme) media feature.
"prefers-color-scheme": PrefersColorScheme = Ident,
/// The [prefers-reduced-data](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-data) media feature.
"prefers-reduced-data": PrefersReducedData = Ident,
/// The [device-width](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-width) media feature.
"device-width": DeviceWidth = Length,
/// The [device-height](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-height) media feature.
"device-height": DeviceHeight = Length,
/// The [device-aspect-ratio](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-aspect-ratio) media feature.
"device-aspect-ratio": DeviceAspectRatio = Ratio,
/// The non-standard -webkit-device-pixel-ratio media feature.
"-webkit-device-pixel-ratio": WebKitDevicePixelRatio = Number,
/// The non-standard -moz-device-pixel-ratio media feature.
"-moz-device-pixel-ratio": MozDevicePixelRatio = Number,
// TODO: parse non-standard media queries?
// -moz-device-orientation
// -webkit-transform-3d
}
}
pub(crate) trait FeatureToCss: ToCss {
fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write;
}
impl FeatureToCss for MediaFeatureId {
fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match self {
MediaFeatureId::WebKitDevicePixelRatio => {
dest.write_str("-webkit-")?;
dest.write_str(prefix)?;
dest.write_str("device-pixel-ratio")
}
_ => {
dest.write_str(prefix)?;
self.to_css(dest)
}
}
}
}
#[inline]
fn write_min_max<W, FeatureId: FeatureToCss>(
operator: &MediaFeatureComparison,
name: &MediaFeatureName<FeatureId>,
value: &MediaFeatureValue,
dest: &mut Printer<W>,
is_range: bool,
) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
let prefix = match operator {
MediaFeatureComparison::GreaterThan => {
if is_range {
dest.write_char('(')?;
}
dest.write_str("not ")?;
Some("max-")
}
MediaFeatureComparison::GreaterThanEqual => Some("min-"),
MediaFeatureComparison::LessThan => {
if is_range {
dest.write_char('(')?;
}
dest.write_str("not ")?;
Some("min-")
}
MediaFeatureComparison::LessThanEqual => Some("max-"),
MediaFeatureComparison::Equal => None,
};
dest.write_char('(')?;
if let Some(prefix) = prefix {
name.to_css_with_prefix(prefix, dest)?;
} else {
name.to_css(dest)?;
}
dest.delim(':', false)?;
value.to_css(dest)?;
if is_range
&& matches!(
operator,
MediaFeatureComparison::GreaterThan | MediaFeatureComparison::LessThan
)
{
dest.write_char(')')?;
}
dest.write_char(')')?;
Ok(())
}
/// [media feature value](https://drafts.csswg.org/mediaqueries/#typedef-mf-value) within a media query.
///
/// See [MediaFeature](MediaFeature).
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_media_feature_value, MEDIA_QUERIES))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", content = "value", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum MediaFeatureValue<'i> {
/// A length value.
Length(Length),
/// A number value.
Number(CSSNumber),
/// An integer value.
Integer(CSSInteger),
/// A boolean value.
Boolean(bool),
/// A resolution.
Resolution(Resolution),
/// A ratio.
Ratio(Ratio),
/// An identifier.
#[cfg_attr(feature = "serde", serde(borrow))]
Ident(Ident<'i>),
/// An environment variable reference.
Env(EnvironmentVariable<'i>),
}
impl<'i> MediaFeatureValue<'i> {
fn value_type(&self) -> MediaFeatureType {
use MediaFeatureValue::*;
match self {
Length(..) => MediaFeatureType::Length,
Number(..) => MediaFeatureType::Number,
Integer(..) => MediaFeatureType::Integer,
Boolean(..) => MediaFeatureType::Boolean,
Resolution(..) => MediaFeatureType::Resolution,
Ratio(..) => MediaFeatureType::Ratio,
Ident(..) => MediaFeatureType::Ident,
Env(..) => MediaFeatureType::Unknown,
}
}
fn check_type(&self, expected_type: MediaFeatureType) -> bool {
match (expected_type, self.value_type()) {
(_, MediaFeatureType::Unknown) | (MediaFeatureType::Unknown, _) => true,
(a, b) => a == b,
}
}
}
impl<'i> MediaFeatureValue<'i> {
/// Parses a single media query feature value, with an expected type.
/// If the type is unknown, pass MediaFeatureType::Unknown instead.
pub fn parse<'t>(
input: &mut Parser<'i, 't>,
expected_type: MediaFeatureType,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
if let Ok(value) = input.try_parse(|input| Self::parse_known(input, expected_type)) {
return Ok(value);
}
Self::parse_unknown(input)
}
fn parse_known<'t>(
input: &mut Parser<'i, 't>,
expected_type: MediaFeatureType,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
match expected_type {
MediaFeatureType::Boolean => {
let value = CSSInteger::parse(input)?;
if value != 0 && value != 1 {
return Err(input.new_custom_error(ParserError::InvalidValue));
}
Ok(MediaFeatureValue::Boolean(value == 1))
}
MediaFeatureType::Number => Ok(MediaFeatureValue::Number(CSSNumber::parse(input)?)),
MediaFeatureType::Integer => Ok(MediaFeatureValue::Integer(CSSInteger::parse(input)?)),
MediaFeatureType::Length => Ok(MediaFeatureValue::Length(Length::parse(input)?)),
MediaFeatureType::Resolution => Ok(MediaFeatureValue::Resolution(Resolution::parse(input)?)),
MediaFeatureType::Ratio => Ok(MediaFeatureValue::Ratio(Ratio::parse(input)?)),
MediaFeatureType::Ident => Ok(MediaFeatureValue::Ident(Ident::parse(input)?)),
MediaFeatureType::Unknown => Err(input.new_custom_error(ParserError::InvalidValue)),
}
}
fn parse_unknown<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
// Ratios are ambiguous with numbers because the second param is optional (e.g. 2/1 == 2).
// We require the / delimiter when parsing ratios so that 2/1 ends up as a ratio and 2 is
// parsed as a number.
if let Ok(ratio) = input.try_parse(Ratio::parse_required) {
return Ok(MediaFeatureValue::Ratio(ratio));
}
// Parse number next so that unitless values are not parsed as lengths.
if let Ok(num) = input.try_parse(CSSNumber::parse) {
return Ok(MediaFeatureValue::Number(num));
}
if let Ok(length) = input.try_parse(Length::parse) {
return Ok(MediaFeatureValue::Length(length));
}
if let Ok(res) = input.try_parse(Resolution::parse) {
return Ok(MediaFeatureValue::Resolution(res));
}
if let Ok(env) = input.try_parse(|input| EnvironmentVariable::parse(input, &ParserOptions::default(), 0)) {
return Ok(MediaFeatureValue::Env(env));
}
let ident = Ident::parse(input)?;
Ok(MediaFeatureValue::Ident(ident))
}
}
impl<'i> ToCss for MediaFeatureValue<'i> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match self {
MediaFeatureValue::Length(len) => len.to_css(dest),
MediaFeatureValue::Number(num) => num.to_css(dest),
MediaFeatureValue::Integer(num) => num.to_css(dest),
MediaFeatureValue::Boolean(b) => {
if *b {
dest.write_char('1')
} else {
dest.write_char('0')
}
}
MediaFeatureValue::Resolution(res) => res.to_css(dest),
MediaFeatureValue::Ratio(ratio) => ratio.to_css(dest),
MediaFeatureValue::Ident(id) => {
id.to_css(dest)?;
Ok(())
}
MediaFeatureValue::Env(env) => env.to_css(dest, false),
}
}
}
/// Consumes an operation or a colon, or returns an error.
fn consume_operation_or_colon<'i, 't>(
input: &mut Parser<'i, 't>,
allow_colon: bool,
) -> Result<Option<MediaFeatureComparison>, ParseError<'i, ParserError<'i>>> {
let location = input.current_source_location();
let first_delim = {
let location = input.current_source_location();
let next_token = input.next()?;
match next_token {
Token::Colon if allow_colon => return Ok(None),
Token::Delim(oper) => oper,
t => return Err(location.new_unexpected_token_error(t.clone())),
}
};
Ok(Some(match first_delim {
'=' => MediaFeatureComparison::Equal,
'>' => {
if input.try_parse(|i| i.expect_delim('=')).is_ok() {
MediaFeatureComparison::GreaterThanEqual
} else {
MediaFeatureComparison::GreaterThan
}
}
'<' => {
if input.try_parse(|i| i.expect_delim('=')).is_ok() {
MediaFeatureComparison::LessThanEqual
} else {
MediaFeatureComparison::LessThan
}
}
d => return Err(location.new_unexpected_token_error(Token::Delim(*d))),
}))
}
fn process_condition<'i>(
loc: Location,
custom_media: &HashMap<CowArcStr<'i>, CustomMediaRule<'i>>,
media_type: &mut MediaType<'i>,
qualifier: &mut Option<Qualifier>,
condition: &mut MediaCondition<'i>,
seen: &mut HashSet<DashedIdent<'i>>,
) -> Result<bool, MinifyError> {
match condition {
MediaCondition::Not(cond) => {
let used = process_condition(loc, custom_media, media_type, qualifier, &mut *cond, seen)?;
if !used {
// If unused, only a media type remains so apply a not qualifier.
// If it is already not, then it cancels out.
*qualifier = if *qualifier == Some(Qualifier::Not) {
None
} else {
Some(Qualifier::Not)
};
return Ok(false);
}
// Unwrap nested nots
match &**cond {
MediaCondition::Not(cond) => {
*condition = (**cond).clone();
}
_ => {}
}
}
MediaCondition::Operation { conditions, .. } => {
let mut res = Ok(true);
conditions.retain_mut(|condition| {
let r = process_condition(loc, custom_media, media_type, qualifier, condition, seen);
if let Ok(used) = r {
used
} else {
res = r;
false
}
});
return res;
}
MediaCondition::Feature(QueryFeature::Boolean { name }) => {
let name = match name {
MediaFeatureName::Custom(name) => name,
_ => return Ok(true),
};
if seen.contains(name) {
return Err(ErrorWithLocation {
kind: MinifyErrorKind::CircularCustomMedia { name: name.to_string() },
loc,
});
}
let rule = custom_media.get(&name.0).ok_or_else(|| ErrorWithLocation {
kind: MinifyErrorKind::CustomMediaNotDefined { name: name.to_string() },
loc,
})?;
seen.insert(name.clone());
let mut res = Ok(true);
let mut conditions: Vec<MediaCondition> = rule
.query
.media_queries
.iter()
.filter_map(|query| {
if query.media_type != MediaType::All || query.qualifier != None {
if *media_type == MediaType::All {
// `not all` will never match.
if *qualifier == Some(Qualifier::Not) {
res = Ok(false);
return None;
}
// Propagate media type and qualifier to @media rule.
*media_type = query.media_type.clone();
*qualifier = query.qualifier.clone();
} else if query.media_type != *media_type || query.qualifier != *qualifier {
// Boolean logic with media types is hard to emulate, so we error for now.
res = Err(ErrorWithLocation {
kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic {
custom_media_loc: rule.loc,
},
loc,
});
return None;
}
}
if let Some(condition) = &query.condition {
let mut condition = condition.clone();
let r = process_condition(loc, custom_media, media_type, qualifier, &mut condition, seen);
if r.is_err() {
res = r;
}
// Parentheses are required around the condition unless there is a single media feature.
match condition {
MediaCondition::Feature(..) => Some(condition),
_ => Some(condition),
}
} else {
None
}
})
.collect();
seen.remove(name);
if res.is_err() {
return res;
}
if conditions.is_empty() {
return Ok(false);
}
if conditions.len() == 1 {
*condition = conditions.pop().unwrap();
} else {
*condition = MediaCondition::Operation {
conditions,
operator: Operator::Or,
};
}
}
_ => {}
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
stylesheet::PrinterOptions,
targets::{Browsers, Targets},
};
fn parse(s: &str) -> MediaQuery {
let mut input = ParserInput::new(&s);
let mut parser = Parser::new(&mut input);
MediaQuery::parse_with_options(&mut parser, &ParserOptions::default()).unwrap()
}
fn and(a: &str, b: &str) -> String {
let mut a = parse(a);
let b = parse(b);
a.and(&b).unwrap();
a.to_css_string(PrinterOptions::default()).unwrap()
}
#[test]
fn test_and() {
assert_eq!(and("(min-width: 250px)", "(color)"), "(width >= 250px) and (color)");
assert_eq!(
and("(min-width: 250px) or (color)", "(orientation: landscape)"),
"((width >= 250px) or (color)) and (orientation: landscape)"
);
assert_eq!(
and("(min-width: 250px) and (color)", "(orientation: landscape)"),
"(width >= 250px) and (color) and (orientation: landscape)"
);
assert_eq!(and("all", "print"), "print");
assert_eq!(and("print", "all"), "print");
assert_eq!(and("all", "not print"), "not print");
assert_eq!(and("not print", "all"), "not print");
assert_eq!(and("not all", "print"), "not all");
assert_eq!(and("print", "not all"), "not all");
assert_eq!(and("print", "screen"), "not all");
assert_eq!(and("not print", "screen"), "screen");
assert_eq!(and("print", "not screen"), "print");
assert_eq!(and("not screen", "print"), "print");
assert_eq!(and("not screen", "not all"), "not all");
assert_eq!(and("print", "(min-width: 250px)"), "print and (width >= 250px)");
assert_eq!(and("(min-width: 250px)", "print"), "print and (width >= 250px)");
assert_eq!(
and("print and (min-width: 250px)", "(color)"),
"print and (width >= 250px) and (color)"
);
assert_eq!(and("all", "only screen"), "only screen");
assert_eq!(and("only screen", "all"), "only screen");
assert_eq!(and("print", "print"), "print");
}
#[test]
fn test_negated_interval_parens() {
let media_query = parse("screen and not (200px <= width < 500px)");
let printer_options = PrinterOptions {
targets: Targets {
browsers: Some(Browsers {
chrome: Some(95 << 16),
..Default::default()
}),
..Default::default()
},
..Default::default()
};
assert_eq!(
media_query.to_css_string(printer_options).unwrap(),
"screen and not ((min-width: 200px) and (not (min-width: 500px)))"
);
}
}