1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

//! A media query list:
//!
//! https://drafts.csswg.org/mediaqueries/#typedef-media-query-list

use super::{Device, MediaQuery, Qualifier};
use crate::context::QuirksMode;
use crate::error_reporting::ContextualParseError;
use crate::parser::ParserContext;
use crate::values::computed;
use cssparser::{Delimiter, Parser};
use cssparser::{ParserInput, Token};
use selectors::kleene_value::KleeneValue;

/// A type that encapsulates a media query list.
#[derive(Clone, MallocSizeOf, ToCss, ToShmem)]
#[css(comma, derive_debug)]
pub struct MediaList {
    /// The list of media queries.
    #[css(iterable)]
    pub media_queries: Vec<MediaQuery>,
}

impl MediaList {
    /// Parse a media query list from CSS.
    ///
    /// Always returns a media query list. If any invalid media query is
    /// found, the media query list is only filled with the equivalent of
    /// "not all", see:
    ///
    /// <https://drafts.csswg.org/mediaqueries/#error-handling>
    pub fn parse(context: &ParserContext, input: &mut Parser) -> Self {
        if input.is_exhausted() {
            return Self::empty();
        }

        let mut media_queries = vec![];
        loop {
            let start_position = input.position();
            match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse(context, i)) {
                Ok(mq) => {
                    media_queries.push(mq);
                },
                Err(err) => {
                    media_queries.push(MediaQuery::never_matching());
                    let location = err.location;
                    let error = ContextualParseError::InvalidMediaRule(
                        input.slice_from(start_position),
                        err,
                    );
                    context.log_css_error(location, error);
                },
            }

            match input.next() {
                Ok(&Token::Comma) => {},
                Ok(_) => unreachable!(),
                Err(_) => break,
            }
        }

        MediaList { media_queries }
    }

    /// Create an empty MediaList.
    pub fn empty() -> Self {
        MediaList {
            media_queries: vec![],
        }
    }

    /// Evaluate a whole `MediaList` against `Device`.
    pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> bool {
        // Check if it is an empty media query list or any queries match.
        // https://drafts.csswg.org/mediaqueries-4/#mq-list
        if self.media_queries.is_empty() {
            return true;
        }

        computed::Context::for_media_query_evaluation(device, quirks_mode, |context| {
            self.media_queries.iter().any(|mq| {
                let mut query_match = if mq.media_type.matches(device.media_type()) {
                    mq.condition
                        .as_ref()
                        .map_or(KleeneValue::True, |c| c.matches(context))
                } else {
                    KleeneValue::False
                };

                // Apply the logical NOT qualifier to the result
                if matches!(mq.qualifier, Some(Qualifier::Not)) {
                    query_match = !query_match;
                }
                query_match.to_bool(/* unknown = */ false)
            })
        })
    }

    /// Whether this `MediaList` contains no media queries.
    pub fn is_empty(&self) -> bool {
        self.media_queries.is_empty()
    }

    /// Whether this `MediaList` depends on the viewport size.
    pub fn is_viewport_dependent(&self) -> bool {
        self.media_queries.iter().any(|q| q.is_viewport_dependent())
    }

    /// Append a new media query item to the media list.
    /// <https://drafts.csswg.org/cssom/#dom-medialist-appendmedium>
    ///
    /// Returns true if added, false if fail to parse the medium string.
    pub fn append_medium(&mut self, context: &ParserContext, new_medium: &str) -> bool {
        let mut input = ParserInput::new(new_medium);
        let mut parser = Parser::new(&mut input);
        let new_query = match MediaQuery::parse(&context, &mut parser) {
            Ok(query) => query,
            Err(_) => {
                return false;
            },
        };
        // This algorithm doesn't actually matches the current spec,
        // but it matches the behavior of Gecko and Edge.
        // See https://github.com/w3c/csswg-drafts/issues/697
        self.media_queries.retain(|query| query != &new_query);
        self.media_queries.push(new_query);
        true
    }

    /// Delete a media query from the media list.
    /// <https://drafts.csswg.org/cssom/#dom-medialist-deletemedium>
    ///
    /// Returns true if found and deleted, false otherwise.
    pub fn delete_medium(&mut self, context: &ParserContext, old_medium: &str) -> bool {
        let mut input = ParserInput::new(old_medium);
        let mut parser = Parser::new(&mut input);
        let old_query = match MediaQuery::parse(context, &mut parser) {
            Ok(query) => query,
            Err(_) => {
                return false;
            },
        };
        let old_len = self.media_queries.len();
        self.media_queries.retain(|query| query != &old_query);
        old_len != self.media_queries.len()
    }
}