1/*
2 * (C) 1999-2003 Lars Knoll (knoll@kde.org)
3 * Copyright (C) 2004, 2006, 2010, 2012 Apple Inc. All rights reserved.
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Library General Public
7 * License as published by the Free Software Foundation; either
8 * version 2 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 * Library General Public License for more details.
14 *
15 * You should have received a copy of the GNU Library General Public License
16 * along with this library; see the file COPYING.LIB.  If not, write to
17 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 * Boston, MA 02110-1301, USA.
19 */
20#include "config.h"
21#include "core/css/MediaList.h"
22
23#include "bindings/core/v8/ExceptionState.h"
24#include "core/MediaFeatureNames.h"
25#include "core/css/CSSStyleSheet.h"
26#include "core/css/MediaQuery.h"
27#include "core/css/MediaQueryExp.h"
28#include "core/css/parser/MediaQueryParser.h"
29#include "core/dom/Document.h"
30#include "core/frame/LocalDOMWindow.h"
31#include "core/inspector/ConsoleMessage.h"
32#include "wtf/text/StringBuilder.h"
33
34namespace blink {
35
36/* MediaList is used to store 3 types of media related entities which mean the same:
37 *
38 * Media Queries, Media Types and Media Descriptors.
39 *
40 * Media queries, as described in the Media Queries Level 3 specification, build on
41 * the mechanism outlined in HTML4. The syntax of media queries fit into the media
42 * type syntax reserved in HTML4. The media attribute of HTML4 also exists in XHTML
43 * and generic XML. The same syntax can also be used inside the @media and @import
44 * rules of CSS.
45 *
46 * However, the parsing rules for media queries are incompatible with those of HTML4
47 * and are consistent with those of media queries used in CSS.
48 *
49 * HTML5 (at the moment of writing still work in progress) references the Media Queries
50 * specification directly and thus updates the rules for HTML.
51 *
52 * CSS 2.1 Spec (http://www.w3.org/TR/CSS21/media.html)
53 * CSS 3 Media Queries Spec (http://www.w3.org/TR/css3-mediaqueries/)
54 */
55
56MediaQuerySet::MediaQuerySet()
57{
58}
59
60MediaQuerySet::MediaQuerySet(const MediaQuerySet& o)
61    : m_queries(o.m_queries.size())
62{
63    for (unsigned i = 0; i < m_queries.size(); ++i)
64        m_queries[i] = o.m_queries[i]->copy();
65}
66
67DEFINE_EMPTY_DESTRUCTOR_WILL_BE_REMOVED(MediaQuerySet)
68
69PassRefPtrWillBeRawPtr<MediaQuerySet> MediaQuerySet::create(const String& mediaString)
70{
71    if (mediaString.isEmpty())
72        return MediaQuerySet::create();
73
74    return MediaQueryParser::parseMediaQuerySet(mediaString);
75}
76
77PassRefPtrWillBeRawPtr<MediaQuerySet> MediaQuerySet::createOffMainThread(const String& mediaString)
78{
79    if (mediaString.isEmpty())
80        return MediaQuerySet::create();
81
82    return MediaQueryParser::parseMediaQuerySet(mediaString);
83}
84
85bool MediaQuerySet::set(const String& mediaString)
86{
87    RefPtrWillBeRawPtr<MediaQuerySet> result = create(mediaString);
88    m_queries.swap(result->m_queries);
89    return true;
90}
91
92bool MediaQuerySet::add(const String& queryString)
93{
94    // To "parse a media query" for a given string means to follow "the parse
95    // a media query list" steps and return "null" if more than one media query
96    // is returned, or else the returned media query.
97    RefPtrWillBeRawPtr<MediaQuerySet> result = create(queryString);
98
99    // Only continue if exactly one media query is found, as described above.
100    if (result->m_queries.size() != 1)
101        return true;
102
103    OwnPtrWillBeRawPtr<MediaQuery> newQuery = result->m_queries[0].release();
104    ASSERT(newQuery);
105
106    // If comparing with any of the media queries in the collection of media
107    // queries returns true terminate these steps.
108    for (size_t i = 0; i < m_queries.size(); ++i) {
109        MediaQuery* query = m_queries[i].get();
110        if (*query == *newQuery)
111            return true;
112    }
113
114    m_queries.append(newQuery.release());
115    return true;
116}
117
118bool MediaQuerySet::remove(const String& queryStringToRemove)
119{
120    // To "parse a media query" for a given string means to follow "the parse
121    // a media query list" steps and return "null" if more than one media query
122    // is returned, or else the returned media query.
123    RefPtrWillBeRawPtr<MediaQuerySet> result = create(queryStringToRemove);
124
125    // Only continue if exactly one media query is found, as described above.
126    if (result->m_queries.size() != 1)
127        return true;
128
129    OwnPtrWillBeRawPtr<MediaQuery> newQuery = result->m_queries[0].release();
130    ASSERT(newQuery);
131
132    // Remove any media query from the collection of media queries for which
133    // comparing with the media query returns true.
134    bool found = false;
135    for (size_t i = 0; i < m_queries.size(); ++i) {
136        MediaQuery* query = m_queries[i].get();
137        if (*query == *newQuery) {
138            m_queries.remove(i);
139            --i;
140            found = true;
141        }
142    }
143
144    return found;
145}
146
147void MediaQuerySet::addMediaQuery(PassOwnPtrWillBeRawPtr<MediaQuery> mediaQuery)
148{
149    m_queries.append(mediaQuery);
150}
151
152String MediaQuerySet::mediaText() const
153{
154    StringBuilder text;
155
156    bool first = true;
157    for (size_t i = 0; i < m_queries.size(); ++i) {
158        if (!first)
159            text.appendLiteral(", ");
160        else
161            first = false;
162        text.append(m_queries[i]->cssText());
163    }
164    return text.toString();
165}
166
167void MediaQuerySet::trace(Visitor* visitor)
168{
169    // We don't support tracing of vectors of OwnPtrs (ie. OwnPtr<Vector<OwnPtr<MediaQuery> > >).
170    // Since this is a transitional object we are just ifdef'ing it out when oilpan is not enabled.
171#if ENABLE(OILPAN)
172    visitor->trace(m_queries);
173#endif
174}
175
176MediaList::MediaList(MediaQuerySet* mediaQueries, CSSStyleSheet* parentSheet)
177    : m_mediaQueries(mediaQueries)
178    , m_parentStyleSheet(parentSheet)
179    , m_parentRule(nullptr)
180{
181}
182
183MediaList::MediaList(MediaQuerySet* mediaQueries, CSSRule* parentRule)
184    : m_mediaQueries(mediaQueries)
185    , m_parentStyleSheet(nullptr)
186    , m_parentRule(parentRule)
187{
188}
189
190MediaList::~MediaList()
191{
192}
193
194void MediaList::setMediaText(const String& value)
195{
196    CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
197
198    m_mediaQueries->set(value);
199
200    if (m_parentStyleSheet)
201        m_parentStyleSheet->didMutate();
202}
203
204String MediaList::item(unsigned index) const
205{
206    const WillBeHeapVector<OwnPtrWillBeMember<MediaQuery> >& queries = m_mediaQueries->queryVector();
207    if (index < queries.size())
208        return queries[index]->cssText();
209    return String();
210}
211
212void MediaList::deleteMedium(const String& medium, ExceptionState& exceptionState)
213{
214    CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
215
216    bool success = m_mediaQueries->remove(medium);
217    if (!success) {
218        exceptionState.throwDOMException(NotFoundError, "Failed to delete '" + medium + "'.");
219        return;
220    }
221    if (m_parentStyleSheet)
222        m_parentStyleSheet->didMutate();
223}
224
225void MediaList::appendMedium(const String& medium, ExceptionState& exceptionState)
226{
227    CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
228
229    bool success = m_mediaQueries->add(medium);
230    if (!success) {
231        exceptionState.throwDOMException(InvalidCharacterError, "The value provided ('" + medium + "') is not a valid medium.");
232        return;
233    }
234
235    if (m_parentStyleSheet)
236        m_parentStyleSheet->didMutate();
237}
238
239void MediaList::reattach(MediaQuerySet* mediaQueries)
240{
241    ASSERT(mediaQueries);
242    m_mediaQueries = mediaQueries;
243}
244
245void MediaList::trace(Visitor* visitor)
246{
247    visitor->trace(m_mediaQueries);
248    visitor->trace(m_parentStyleSheet);
249    visitor->trace(m_parentRule);
250}
251
252static void addResolutionWarningMessageToConsole(Document* document, const String& serializedExpression, CSSPrimitiveValue::UnitType type)
253{
254    ASSERT(document);
255
256    DEFINE_STATIC_LOCAL(String, mediaQueryMessage, ("Consider using 'dppx' units, as in CSS '%replacementUnits%' means dots-per-CSS-%lengthUnit%, not dots-per-physical-%lengthUnit%, so does not correspond to the actual '%replacementUnits%' of a screen. In media query expression: "));
257    DEFINE_STATIC_LOCAL(String, mediaValueDPI, ("dpi"));
258    DEFINE_STATIC_LOCAL(String, mediaValueDPCM, ("dpcm"));
259    DEFINE_STATIC_LOCAL(String, lengthUnitInch, ("inch"));
260    DEFINE_STATIC_LOCAL(String, lengthUnitCentimeter, ("centimeter"));
261
262    StringBuilder message;
263    if (CSSPrimitiveValue::isDotsPerInch(type))
264        message.append(String(mediaQueryMessage).replace("%replacementUnits%", mediaValueDPI).replace("%lengthUnit%", lengthUnitInch));
265    else if (CSSPrimitiveValue::isDotsPerCentimeter(type))
266        message.append(String(mediaQueryMessage).replace("%replacementUnits%", mediaValueDPCM).replace("%lengthUnit%", lengthUnitCentimeter));
267    else
268        ASSERT_NOT_REACHED();
269
270    message.append(serializedExpression);
271
272    document->addConsoleMessage(ConsoleMessage::create(CSSMessageSource, DebugMessageLevel, message.toString()));
273}
274
275static inline bool isResolutionMediaFeature(const String& mediaFeature)
276{
277    return mediaFeature == MediaFeatureNames::resolutionMediaFeature
278        || mediaFeature == MediaFeatureNames::maxResolutionMediaFeature
279        || mediaFeature == MediaFeatureNames::minResolutionMediaFeature;
280}
281
282void reportMediaQueryWarningIfNeeded(Document* document, const MediaQuerySet* mediaQuerySet)
283{
284    if (!mediaQuerySet || !document)
285        return;
286
287    const WillBeHeapVector<OwnPtrWillBeMember<MediaQuery> >& mediaQueries = mediaQuerySet->queryVector();
288    const size_t queryCount = mediaQueries.size();
289
290    if (!queryCount)
291        return;
292
293    CSSPrimitiveValue::UnitType suspiciousType = CSSPrimitiveValue::CSS_UNKNOWN;
294    bool dotsPerPixelUsed = false;
295    for (size_t i = 0; i < queryCount; ++i) {
296        const MediaQuery* query = mediaQueries[i].get();
297        if (equalIgnoringCase(query->mediaType(), "print"))
298            continue;
299
300        const ExpressionHeapVector& expressions = query->expressions();
301        for (size_t j = 0; j < expressions.size(); ++j) {
302            const MediaQueryExp* expression = expressions.at(j).get();
303            if (isResolutionMediaFeature(expression->mediaFeature())) {
304                MediaQueryExpValue expValue = expression->expValue();
305                if (expValue.isValue) {
306                    if (CSSPrimitiveValue::isDotsPerPixel(expValue.unit))
307                        dotsPerPixelUsed = true;
308                    else if (CSSPrimitiveValue::isDotsPerInch(expValue.unit) || CSSPrimitiveValue::isDotsPerCentimeter(expValue.unit))
309                        suspiciousType = expValue.unit;
310                }
311            }
312        }
313    }
314
315    if (suspiciousType && !dotsPerPixelUsed)
316        addResolutionWarningMessageToConsole(document, mediaQuerySet->mediaText(), suspiciousType);
317}
318
319}
320