1/*
2 * CSS Media Query
3 *
4 * Copyright (C) 2006 Kimmo Kinnunen <kimmo.t.kinnunen@nokia.com>.
5 * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
6 * Copyright (C) 2013 Apple Inc. All rights reserved.
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
10 * are met:
11 * 1. Redistributions of source code must retain the above copyright
12 *    notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 *    notice, this list of conditions and the following disclaimer in the
15 *    documentation and/or other materials provided with the distribution.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
21 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
25 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30#include "config.h"
31#include "core/css/MediaQueryExp.h"
32
33#include "core/css/CSSAspectRatioValue.h"
34#include "core/css/CSSPrimitiveValue.h"
35#include "core/css/parser/CSSParserValues.h"
36#include "core/html/parser/HTMLParserIdioms.h"
37#include "platform/Decimal.h"
38#include "platform/RuntimeEnabledFeatures.h"
39#include "wtf/text/StringBuffer.h"
40#include "wtf/text/StringBuilder.h"
41
42namespace blink {
43
44using namespace MediaFeatureNames;
45
46static inline bool featureWithCSSValueID(const String& mediaFeature, const CSSParserValue* value)
47{
48    if (!value->id)
49        return false;
50
51    return mediaFeature == orientationMediaFeature
52        || mediaFeature == pointerMediaFeature
53        || mediaFeature == anyPointerMediaFeature
54        || (mediaFeature == hoverMediaFeature && RuntimeEnabledFeatures::hoverMediaQueryKeywordsEnabled())
55        || mediaFeature == anyHoverMediaFeature
56        || mediaFeature == scanMediaFeature;
57}
58
59static inline bool featureWithValidIdent(const String& mediaFeature, CSSValueID ident)
60{
61    if (mediaFeature == orientationMediaFeature)
62        return ident == CSSValuePortrait || ident == CSSValueLandscape;
63
64    if (mediaFeature == pointerMediaFeature || mediaFeature == anyPointerMediaFeature)
65        return ident == CSSValueNone || ident == CSSValueCoarse || ident == CSSValueFine;
66
67    if ((mediaFeature == hoverMediaFeature && RuntimeEnabledFeatures::hoverMediaQueryKeywordsEnabled())
68        || mediaFeature == anyHoverMediaFeature)
69        return ident == CSSValueNone || ident == CSSValueOnDemand || ident == CSSValueHover;
70
71    if (mediaFeature == scanMediaFeature)
72        return ident == CSSValueInterlace || ident == CSSValueProgressive;
73
74    ASSERT_NOT_REACHED();
75    return false;
76}
77
78static bool positiveLengthUnit(const int unit)
79{
80    switch (unit) {
81    case CSSPrimitiveValue::CSS_EMS:
82    case CSSPrimitiveValue::CSS_EXS:
83    case CSSPrimitiveValue::CSS_PX:
84    case CSSPrimitiveValue::CSS_CM:
85    case CSSPrimitiveValue::CSS_MM:
86    case CSSPrimitiveValue::CSS_IN:
87    case CSSPrimitiveValue::CSS_PT:
88    case CSSPrimitiveValue::CSS_PC:
89    case CSSPrimitiveValue::CSS_REMS:
90    case CSSPrimitiveValue::CSS_CHS:
91        return true;
92    }
93    return false;
94}
95
96static inline bool featureWithValidPositiveLength(const String& mediaFeature, const CSSParserValue* value)
97{
98    if (!(positiveLengthUnit(value->unit) || (value->unit == CSSPrimitiveValue::CSS_NUMBER && value->fValue == 0)) || value->fValue < 0)
99        return false;
100
101
102    return mediaFeature == heightMediaFeature
103        || mediaFeature == maxHeightMediaFeature
104        || mediaFeature == minHeightMediaFeature
105        || mediaFeature == widthMediaFeature
106        || mediaFeature == maxWidthMediaFeature
107        || mediaFeature == minWidthMediaFeature
108        || mediaFeature == deviceHeightMediaFeature
109        || mediaFeature == maxDeviceHeightMediaFeature
110        || mediaFeature == minDeviceHeightMediaFeature
111        || mediaFeature == deviceWidthMediaFeature
112        || mediaFeature == minDeviceWidthMediaFeature
113        || mediaFeature == maxDeviceWidthMediaFeature;
114}
115
116static inline bool featureWithValidDensity(const String& mediaFeature, const CSSParserValue* value)
117{
118    if ((value->unit != CSSPrimitiveValue::CSS_DPPX && value->unit != CSSPrimitiveValue::CSS_DPI && value->unit != CSSPrimitiveValue::CSS_DPCM) || value->fValue <= 0)
119        return false;
120
121    return mediaFeature == resolutionMediaFeature
122        || mediaFeature == minResolutionMediaFeature
123        || mediaFeature == maxResolutionMediaFeature;
124}
125
126static inline bool featureWithPositiveInteger(const String& mediaFeature, const CSSParserValue* value)
127{
128    if (!value->isInt || value->fValue < 0)
129        return false;
130
131    return mediaFeature == colorMediaFeature
132        || mediaFeature == maxColorMediaFeature
133        || mediaFeature == minColorMediaFeature
134        || mediaFeature == colorIndexMediaFeature
135        || mediaFeature == maxColorIndexMediaFeature
136        || mediaFeature == minColorIndexMediaFeature
137        || mediaFeature == monochromeMediaFeature
138        || mediaFeature == maxMonochromeMediaFeature
139        || mediaFeature == minMonochromeMediaFeature;
140}
141
142static inline bool featureWithPositiveNumber(const String& mediaFeature, const CSSParserValue* value)
143{
144    if (value->unit != CSSPrimitiveValue::CSS_NUMBER || value->fValue < 0)
145        return false;
146
147    return mediaFeature == transform3dMediaFeature
148        || mediaFeature == devicePixelRatioMediaFeature
149        || mediaFeature == maxDevicePixelRatioMediaFeature
150        || mediaFeature == minDevicePixelRatioMediaFeature;
151}
152
153static inline bool featureWithZeroOrOne(const String& mediaFeature, const CSSParserValue* value)
154{
155    if (!value->isInt || !(value->fValue == 1 || !value->fValue))
156        return false;
157
158    return mediaFeature == gridMediaFeature
159        || (mediaFeature == hoverMediaFeature && !RuntimeEnabledFeatures::hoverMediaQueryKeywordsEnabled());
160}
161
162static inline bool featureWithAspectRatio(const String& mediaFeature)
163{
164    return mediaFeature == aspectRatioMediaFeature
165        || mediaFeature == deviceAspectRatioMediaFeature
166        || mediaFeature == minAspectRatioMediaFeature
167        || mediaFeature == maxAspectRatioMediaFeature
168        || mediaFeature == minDeviceAspectRatioMediaFeature
169        || mediaFeature == maxDeviceAspectRatioMediaFeature;
170}
171
172static inline bool featureWithoutValue(const String& mediaFeature)
173{
174    // Media features that are prefixed by min/max cannot be used without a value.
175    return mediaFeature == monochromeMediaFeature
176        || mediaFeature == colorMediaFeature
177        || mediaFeature == colorIndexMediaFeature
178        || mediaFeature == gridMediaFeature
179        || mediaFeature == heightMediaFeature
180        || mediaFeature == widthMediaFeature
181        || mediaFeature == deviceHeightMediaFeature
182        || mediaFeature == deviceWidthMediaFeature
183        || mediaFeature == orientationMediaFeature
184        || mediaFeature == aspectRatioMediaFeature
185        || mediaFeature == deviceAspectRatioMediaFeature
186        || mediaFeature == hoverMediaFeature
187        || mediaFeature == anyHoverMediaFeature
188        || mediaFeature == transform3dMediaFeature
189        || mediaFeature == pointerMediaFeature
190        || mediaFeature == anyPointerMediaFeature
191        || mediaFeature == devicePixelRatioMediaFeature
192        || mediaFeature == resolutionMediaFeature
193        || mediaFeature == scanMediaFeature;
194}
195
196bool MediaQueryExp::isViewportDependent() const
197{
198    return m_mediaFeature == widthMediaFeature
199        || m_mediaFeature == heightMediaFeature
200        || m_mediaFeature == minWidthMediaFeature
201        || m_mediaFeature == minHeightMediaFeature
202        || m_mediaFeature == maxWidthMediaFeature
203        || m_mediaFeature == maxHeightMediaFeature
204        || m_mediaFeature == orientationMediaFeature
205        || m_mediaFeature == aspectRatioMediaFeature
206        || m_mediaFeature == minAspectRatioMediaFeature
207        || m_mediaFeature == devicePixelRatioMediaFeature
208        || m_mediaFeature == resolutionMediaFeature
209        || m_mediaFeature == maxAspectRatioMediaFeature;
210}
211
212MediaQueryExp::MediaQueryExp(const MediaQueryExp& other)
213    : m_mediaFeature(other.mediaFeature())
214    , m_expValue(other.expValue())
215{
216}
217
218MediaQueryExp::MediaQueryExp(const String& mediaFeature, const MediaQueryExpValue& expValue)
219    : m_mediaFeature(mediaFeature)
220    , m_expValue(expValue)
221{
222}
223
224PassOwnPtrWillBeRawPtr<MediaQueryExp> MediaQueryExp::createIfValid(const String& mediaFeature, CSSParserValueList* valueList)
225{
226    ASSERT(!mediaFeature.isNull());
227
228    MediaQueryExpValue expValue;
229    bool isValid = false;
230    String lowerMediaFeature = attemptStaticStringCreation(mediaFeature.lower());
231
232    // Create value for media query expression that must have 1 or more values.
233    if (valueList && valueList->size() > 0) {
234        if (valueList->size() == 1) {
235            CSSParserValue* value = valueList->current();
236            ASSERT(value);
237
238            if (featureWithCSSValueID(lowerMediaFeature, value) && featureWithValidIdent(lowerMediaFeature, value->id)) {
239                // Media features that use CSSValueIDs.
240                expValue.id = value->id;
241                expValue.unit = CSSPrimitiveValue::CSS_VALUE_ID;
242                expValue.isID = true;
243            } else if (featureWithValidDensity(lowerMediaFeature, value)
244                || featureWithValidPositiveLength(lowerMediaFeature, value)) {
245                // Media features that must have non-negative <density>, ie. dppx, dpi or dpcm,
246                // or Media features that must have non-negative <length> or number value.
247                expValue.value = value->fValue;
248                expValue.unit = (CSSPrimitiveValue::UnitType)value->unit;
249                expValue.isValue = true;
250            } else if (featureWithPositiveInteger(lowerMediaFeature, value)
251                || featureWithPositiveNumber(lowerMediaFeature, value)
252                || featureWithZeroOrOne(lowerMediaFeature, value)) {
253                // Media features that must have non-negative integer value,
254                // or media features that must have non-negative number value,
255                // or media features that must have (0|1) value.
256                expValue.value = value->fValue;
257                expValue.unit = CSSPrimitiveValue::CSS_NUMBER;
258                expValue.isValue = true;
259            }
260
261            isValid = (expValue.isID || expValue.isValue);
262
263        } else if (valueList->size() == 3 && featureWithAspectRatio(lowerMediaFeature)) {
264            // Create list of values.
265            // Currently accepts only <integer>/<integer>.
266            // Applicable to device-aspect-ratio and aspec-ratio.
267            isValid = true;
268            float numeratorValue = 0;
269            float denominatorValue = 0;
270            // The aspect-ratio must be <integer> (whitespace)? / (whitespace)? <integer>.
271            for (unsigned i = 0; i < 3; ++i, valueList->next()) {
272                const CSSParserValue* value = valueList->current();
273                if (i != 1 && value->unit == CSSPrimitiveValue::CSS_NUMBER && value->fValue > 0 && value->isInt) {
274                    if (!i)
275                        numeratorValue = value->fValue;
276                    else
277                        denominatorValue = value->fValue;
278                } else if (i == 1 && value->unit == CSSParserValue::Operator && value->iValue == '/') {
279                    continue;
280                } else {
281                    isValid = false;
282                    break;
283                }
284            }
285
286            if (isValid) {
287                expValue.numerator = (unsigned)numeratorValue;
288                expValue.denominator = (unsigned)denominatorValue;
289                expValue.isRatio = true;
290            }
291        }
292    } else if (featureWithoutValue(lowerMediaFeature)) {
293        isValid = true;
294    }
295
296    if (!isValid)
297        return nullptr;
298
299    return adoptPtrWillBeNoop(new MediaQueryExp(lowerMediaFeature, expValue));
300}
301
302MediaQueryExp::~MediaQueryExp()
303{
304}
305
306bool MediaQueryExp::operator==(const MediaQueryExp& other) const
307{
308    return (other.m_mediaFeature == m_mediaFeature)
309        && ((!other.m_expValue.isValid() && !m_expValue.isValid())
310            || (other.m_expValue.isValid() && m_expValue.isValid() && other.m_expValue.equals(m_expValue)));
311}
312
313String MediaQueryExp::serialize() const
314{
315    StringBuilder result;
316    result.append('(');
317    result.append(m_mediaFeature.lower());
318    if (m_expValue.isValid()) {
319        result.appendLiteral(": ");
320        result.append(m_expValue.cssText());
321    }
322    result.append(')');
323
324    return result.toString();
325}
326
327static inline String printNumber(double number)
328{
329    return Decimal::fromDouble(number).toString();
330}
331
332String MediaQueryExpValue::cssText() const
333{
334    StringBuilder output;
335    if (isValue) {
336        output.append(printNumber(value));
337        output.append(CSSPrimitiveValue::unitTypeToString(unit));
338    } else if (isRatio) {
339        output.append(printNumber(numerator));
340        output.append('/');
341        output.append(printNumber(denominator));
342    } else if (isID) {
343        output.append(getValueName(id));
344    }
345
346    return output.toString();
347}
348
349} // namespace
350