e73a47e587db46a33c9d0ef51cbe09b647b2fc9f
[WebKit-https.git] / Source / WebCore / css / MediaList.cpp
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
21 #include "config.h"
22 #include "MediaList.h"
23
24 #include "CSSImportRule.h"
25 #include "CSSParser.h"
26 #include "CSSStyleSheet.h"
27 #include "DOMWindow.h"
28 #include "Document.h"
29 #include "ExceptionCode.h"
30 #include "HTMLParserIdioms.h"
31 #include "MediaFeatureNames.h"
32 #include "MediaQuery.h"
33 #include "MediaQueryParser.h"
34 #include "ScriptableDocumentParser.h"
35 #include <wtf/NeverDestroyed.h>
36 #include <wtf/text/StringBuilder.h>
37
38 namespace WebCore {
39
40 /* MediaList is used to store 3 types of media related entities which mean the same:
41  * Media Queries, Media Types and Media Descriptors.
42  * Currently MediaList always tries to parse media queries and if parsing fails,
43  * tries to fallback to Media Descriptors if m_fallbackToDescriptor flag is set.
44  * Slight problem with syntax error handling:
45  * CSS 2.1 Spec (http://www.w3.org/TR/CSS21/media.html)
46  * specifies that failing media type parsing is a syntax error
47  * CSS 3 Media Queries Spec (http://www.w3.org/TR/css3-mediaqueries/)
48  * specifies that failing media query is a syntax error
49  * HTML 4.01 spec (http://www.w3.org/TR/REC-html40/present/styles.html#adef-media)
50  * specifies that Media Descriptors should be parsed with forward-compatible syntax
51  * DOM Level 2 Style Sheet spec (http://www.w3.org/TR/DOM-Level-2-Style/)
52  * talks about MediaList.mediaText and refers
53  *   -  to Media Descriptors of HTML 4.0 in context of StyleSheet
54  *   -  to Media Types of CSS 2.0 in context of CSSMediaRule and CSSImportRule
55  *
56  * These facts create situation where same (illegal) media specification may result in
57  * different parses depending on whether it is media attr of style element or part of
58  * css @media rule.
59  * <style media="screen and resolution > 40dpi"> ..</style> will be enabled on screen devices where as
60  * @media screen and resolution > 40dpi {..} will not.
61  * This gets more counter-intuitive in JavaScript:
62  * document.styleSheets[0].media.mediaText = "screen and resolution > 40dpi" will be ok and
63  * enabled, while
64  * document.styleSheets[0].cssRules[0].media.mediaText = "screen and resolution > 40dpi" will
65  * throw SYNTAX_ERR exception.
66  */
67     
68 Ref<MediaQuerySet> MediaQuerySet::create(const String& mediaString)
69 {
70     if (mediaString.isEmpty())
71         return MediaQuerySet::create();
72     
73     return MediaQueryParser::parseMediaQuerySet(mediaString).releaseNonNull();
74 }
75
76 MediaQuerySet::MediaQuerySet()
77     : m_lastLine(0)
78 {
79 }
80
81 MediaQuerySet::MediaQuerySet(const MediaQuerySet& o)
82     : RefCounted()
83     , m_lastLine(o.m_lastLine)
84     , m_queries(o.m_queries)
85 {
86 }
87
88 MediaQuerySet::~MediaQuerySet()
89 {
90 }
91
92 bool MediaQuerySet::set(const String& mediaString)
93 {
94     auto result = create(mediaString);
95     m_queries.swap(result->m_queries);
96     return true;
97 }
98
99 bool MediaQuerySet::add(const String& queryString)
100 {
101     // To "parse a media query" for a given string means to follow "the parse
102     // a media query list" steps and return "null" if more than one media query
103     // is returned, or else the returned media query.
104     auto result = create(queryString);
105     
106     // Only continue if exactly one media query is found, as described above.
107     if (result->m_queries.size() != 1)
108         return true;
109     
110     // If comparing with any of the media queries in the collection of media
111     // queries returns true terminate these steps.
112     for (size_t i = 0; i < m_queries.size(); ++i) {
113         if (m_queries[i] == result->m_queries[0])
114             return true;
115     }
116     
117     m_queries.append(result->m_queries[0]);
118     return true;
119 }
120
121 bool MediaQuerySet::remove(const String& queryStringToRemove)
122 {
123     // To "parse a media query" for a given string means to follow "the parse
124     // a media query list" steps and return "null" if more than one media query
125     // is returned, or else the returned media query.
126     auto result = create(queryStringToRemove);
127     
128     // Only continue if exactly one media query is found, as described above.
129     if (result->m_queries.size() != 1)
130         return true;
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     
136     // Using signed int here, since for the first value, --i will result in -1.
137     for (int i = 0; i < (int)m_queries.size(); ++i) {
138         if (m_queries[i] == result->m_queries[0]) {
139             m_queries.remove(i);
140             --i;
141             found = true;
142         }
143     }
144     
145     return found;
146 }
147
148 void MediaQuerySet::addMediaQuery(MediaQuery&& mediaQuery)
149 {
150     m_queries.append(WTFMove(mediaQuery));
151 }
152
153 String MediaQuerySet::mediaText() const
154 {
155     StringBuilder text;
156     bool needComma = false;
157     for (auto& query : m_queries) {
158         if (needComma)
159             text.appendLiteral(", ");
160         text.append(query.cssText());
161         needComma = true;
162     }
163     return text.toString();
164 }
165
166 void MediaQuerySet::shrinkToFit()
167 {
168     m_queries.shrinkToFit();
169     for (auto& query : m_queries)
170         query.shrinkToFit();
171 }
172
173 MediaList::MediaList(MediaQuerySet* mediaQueries, CSSStyleSheet* parentSheet)
174     : m_mediaQueries(mediaQueries)
175     , m_parentStyleSheet(parentSheet)
176 {
177 }
178
179 MediaList::MediaList(MediaQuerySet* mediaQueries, CSSRule* parentRule)
180     : m_mediaQueries(mediaQueries)
181     , m_parentRule(parentRule)
182 {
183 }
184
185 MediaList::~MediaList()
186 {
187 }
188
189 ExceptionOr<void> MediaList::setMediaText(const String& value)
190 {
191     CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
192     m_mediaQueries->set(value);
193     if (m_parentStyleSheet)
194         m_parentStyleSheet->didMutate();
195     return { };
196 }
197
198 String MediaList::item(unsigned index) const
199 {
200     auto& queries = m_mediaQueries->queryVector();
201     if (index < queries.size())
202         return queries[index].cssText();
203     return String();
204 }
205
206 ExceptionOr<void> MediaList::deleteMedium(const String& medium)
207 {
208     CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
209
210     bool success = m_mediaQueries->remove(medium);
211     if (!success)
212         return Exception { NOT_FOUND_ERR };
213     if (m_parentStyleSheet)
214         m_parentStyleSheet->didMutate();
215     return { };
216 }
217
218 ExceptionOr<void> MediaList::appendMedium(const String& medium)
219 {
220     CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
221
222     bool success = m_mediaQueries->add(medium);
223     if (!success) {
224         // FIXME: Should this really be INVALID_CHARACTER_ERR?
225         return Exception { INVALID_CHARACTER_ERR };
226     }
227     if (m_parentStyleSheet)
228         m_parentStyleSheet->didMutate();
229     return { };
230 }
231
232 void MediaList::reattach(MediaQuerySet* mediaQueries)
233 {
234     ASSERT(mediaQueries);
235     m_mediaQueries = mediaQueries;
236 }
237
238 #if ENABLE(RESOLUTION_MEDIA_QUERY)
239
240 static void addResolutionWarningMessageToConsole(Document& document, const String& serializedExpression, const CSSPrimitiveValue& value)
241 {
242     static NeverDestroyed<String> mediaQueryMessage(ASCIILiteral("Consider using 'dppx' units instead of '%replacementUnits%', 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: "));
243     static NeverDestroyed<String> mediaValueDPI(ASCIILiteral("dpi"));
244     static NeverDestroyed<String> mediaValueDPCM(ASCIILiteral("dpcm"));
245     static NeverDestroyed<String> lengthUnitInch(ASCIILiteral("inch"));
246     static NeverDestroyed<String> lengthUnitCentimeter(ASCIILiteral("centimeter"));
247
248     String message;
249     if (value.isDotsPerInch())
250         message = mediaQueryMessage.get().replace("%replacementUnits%", mediaValueDPI).replace("%lengthUnit%", lengthUnitInch);
251     else if (value.isDotsPerCentimeter())
252         message = mediaQueryMessage.get().replace("%replacementUnits%", mediaValueDPCM).replace("%lengthUnit%", lengthUnitCentimeter);
253     else
254         ASSERT_NOT_REACHED();
255
256     message.append(serializedExpression);
257
258     document.addConsoleMessage(MessageSource::CSS, MessageLevel::Debug, message);
259 }
260
261 void reportMediaQueryWarningIfNeeded(Document* document, const MediaQuerySet* mediaQuerySet)
262 {
263     if (!mediaQuerySet || !document)
264         return;
265
266     for (auto& query : mediaQuerySet->queryVector()) {
267         if (!query.ignored() && !equalLettersIgnoringASCIICase(query.mediaType(), "print")) {
268             auto& expressions = query.expressions();
269             for (auto& expression : expressions) {
270                 if (expression.mediaFeature() == MediaFeatureNames::resolution || expression.mediaFeature() == MediaFeatureNames::maxResolution || expression.mediaFeature() == MediaFeatureNames::minResolution) {
271                     auto* value = expression.value();
272                     if (is<CSSPrimitiveValue>(value)) {
273                         auto& primitiveValue = downcast<CSSPrimitiveValue>(*value);
274                         if (primitiveValue.isDotsPerInch() || primitiveValue.isDotsPerCentimeter())
275                             addResolutionWarningMessageToConsole(*document, mediaQuerySet->mediaText(), primitiveValue);
276                     }
277                 }
278             }
279         }
280     }
281 }
282
283 #endif
284
285 }