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