Unreviewed, rolling out r156253.
[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 #include "config.h"
21 #include "MediaList.h"
22
23 #include "CSSImportRule.h"
24 #include "CSSParser.h"
25 #include "CSSStyleSheet.h"
26 #include "Console.h"
27 #include "DOMWindow.h"
28 #include "Document.h"
29 #include "ExceptionCode.h"
30 #include "MediaFeatureNames.h"
31 #include "MediaQuery.h"
32 #include "MediaQueryExp.h"
33 #include "ScriptableDocumentParser.h"
34 #include <wtf/text/StringBuilder.h>
35
36 namespace WebCore {
37
38 /* MediaList is used to store 3 types of media related entities which mean the same:
39  * Media Queries, Media Types and Media Descriptors.
40  * Currently MediaList always tries to parse media queries and if parsing fails,
41  * tries to fallback to Media Descriptors if m_fallbackToDescriptor flag is set.
42  * Slight problem with syntax error handling:
43  * CSS 2.1 Spec (http://www.w3.org/TR/CSS21/media.html)
44  * specifies that failing media type parsing is a syntax error
45  * CSS 3 Media Queries Spec (http://www.w3.org/TR/css3-mediaqueries/)
46  * specifies that failing media query is a syntax error
47  * HTML 4.01 spec (http://www.w3.org/TR/REC-html40/present/styles.html#adef-media)
48  * specifies that Media Descriptors should be parsed with forward-compatible syntax
49  * DOM Level 2 Style Sheet spec (http://www.w3.org/TR/DOM-Level-2-Style/)
50  * talks about MediaList.mediaText and refers
51  *   -  to Media Descriptors of HTML 4.0 in context of StyleSheet
52  *   -  to Media Types of CSS 2.0 in context of CSSMediaRule and CSSImportRule
53  *
54  * These facts create situation where same (illegal) media specification may result in
55  * different parses depending on whether it is media attr of style element or part of
56  * css @media rule.
57  * <style media="screen and resolution > 40dpi"> ..</style> will be enabled on screen devices where as
58  * @media screen and resolution > 40dpi {..} will not.
59  * This gets more counter-intuitive in JavaScript:
60  * document.styleSheets[0].media.mediaText = "screen and resolution > 40dpi" will be ok and
61  * enabled, while
62  * document.styleSheets[0].cssRules[0].media.mediaText = "screen and resolution > 40dpi" will
63  * throw SYNTAX_ERR exception.
64  */
65     
66 MediaQuerySet::MediaQuerySet()
67     : m_fallbackToDescriptor(false)
68     , m_lastLine(0)
69 {
70 }
71
72 MediaQuerySet::MediaQuerySet(const String& mediaString, bool fallbackToDescriptor)
73     : m_fallbackToDescriptor(fallbackToDescriptor)
74     , m_lastLine(0)
75 {
76     bool success = parse(mediaString);
77     // FIXME: parsing can fail. The problem with failing constructor is that
78     // we would need additional flag saying MediaList is not valid
79     // Parse can fail only when fallbackToDescriptor == false, i.e when HTML4 media descriptor
80     // forward-compatible syntax is not in use.
81     // DOMImplementationCSS seems to mandate that media descriptors are used
82     // for both html and svg, even though svg:style doesn't use media descriptors
83     // Currently the only places where parsing can fail are
84     // creating <svg:style>, creating css media / import rules from js
85     
86     // FIXME: This doesn't make much sense.
87     if (!success)
88         parse("invalid");
89 }
90
91 MediaQuerySet::MediaQuerySet(const MediaQuerySet& o)
92     : RefCounted<MediaQuerySet>()
93     , m_fallbackToDescriptor(o.m_fallbackToDescriptor)
94     , m_lastLine(o.m_lastLine)
95     , m_queries(o.m_queries.size())
96 {
97     for (unsigned i = 0; i < m_queries.size(); ++i)
98         m_queries[i] = o.m_queries[i]->copy();
99 }
100
101 MediaQuerySet::~MediaQuerySet()
102 {
103 }
104
105 static String parseMediaDescriptor(const String& string)
106 {
107     // http://www.w3.org/TR/REC-html40/types.html#type-media-descriptors
108     // "Each entry is truncated just before the first character that isn't a
109     // US ASCII letter [a-zA-Z] (ISO 10646 hex 41-5a, 61-7a), digit [0-9] (hex 30-39),
110     // or hyphen (hex 2d)."
111     unsigned length = string.length();
112     unsigned i = 0;
113     for (; i < length; ++i) {
114         unsigned short c = string[i];
115         if (! ((c >= 'a' && c <= 'z')
116                || (c >= 'A' && c <= 'Z')
117                || (c >= '1' && c <= '9')
118                || (c == '-')))
119             break;
120     }
121     return string.left(i);
122 }
123
124 bool MediaQuerySet::parse(const String& mediaString)
125 {
126     CSSParser parser(CSSStrictMode);
127     
128     Vector<OwnPtr<MediaQuery> > result;
129     Vector<String> list;
130     mediaString.split(',', list);
131     for (unsigned i = 0; i < list.size(); ++i) {
132         String medium = list[i].stripWhiteSpace();
133         if (medium.isEmpty()) {
134             if (!m_fallbackToDescriptor)
135                 return false;
136             continue;
137         }
138         OwnPtr<MediaQuery> mediaQuery = parser.parseMediaQuery(medium);
139         if (!mediaQuery) {
140             if (!m_fallbackToDescriptor)
141                 return false;
142             String mediaDescriptor = parseMediaDescriptor(medium);
143             if (mediaDescriptor.isNull())
144                 continue;
145             mediaQuery = adoptPtr(new MediaQuery(MediaQuery::None, mediaDescriptor, nullptr));
146         }
147         result.append(mediaQuery.release());
148     }
149     // ",,,," falls straight through, but is not valid unless fallback
150     if (!m_fallbackToDescriptor && list.isEmpty()) {
151         String strippedMediaString = mediaString.stripWhiteSpace();
152         if (!strippedMediaString.isEmpty())
153             return false;
154     }
155     m_queries.swap(result);
156     return true;
157 }
158
159 bool MediaQuerySet::add(const String& queryString)
160 {
161     CSSParser parser(CSSStrictMode);
162
163     OwnPtr<MediaQuery> parsedQuery = parser.parseMediaQuery(queryString);
164     if (!parsedQuery && m_fallbackToDescriptor) {
165         String medium = parseMediaDescriptor(queryString);
166         if (!medium.isNull())
167             parsedQuery = adoptPtr(new MediaQuery(MediaQuery::None, medium, nullptr));
168     }
169     if (!parsedQuery)
170         return false;
171
172     m_queries.append(parsedQuery.release());
173     return true;
174 }
175
176 bool MediaQuerySet::remove(const String& queryStringToRemove)
177 {
178     CSSParser parser(CSSStrictMode);
179
180     OwnPtr<MediaQuery> parsedQuery = parser.parseMediaQuery(queryStringToRemove);
181     if (!parsedQuery && m_fallbackToDescriptor) {
182         String medium = parseMediaDescriptor(queryStringToRemove);
183         if (!medium.isNull())
184             parsedQuery = adoptPtr(new MediaQuery(MediaQuery::None, medium, nullptr));
185     }
186     if (!parsedQuery)
187         return false;
188     
189     for (size_t i = 0; i < m_queries.size(); ++i) {
190         MediaQuery* query = m_queries[i].get();
191         if (*query == *parsedQuery) {
192             m_queries.remove(i);
193             return true;
194         }
195     }
196     return false;
197 }
198
199 void MediaQuerySet::addMediaQuery(PassOwnPtr<MediaQuery> mediaQuery)
200 {
201     m_queries.append(mediaQuery);
202 }
203
204 String MediaQuerySet::mediaText() const
205 {
206     StringBuilder text;
207     
208     bool first = true;
209     for (size_t i = 0; i < m_queries.size(); ++i) {
210         if (!first)
211             text.appendLiteral(", ");
212         else
213             first = false;
214         text.append(m_queries[i]->cssText());
215     }
216     return text.toString();
217 }
218
219 MediaList::MediaList(MediaQuerySet* mediaQueries, CSSStyleSheet* parentSheet)
220     : m_mediaQueries(mediaQueries)
221     , m_parentStyleSheet(parentSheet)
222     , m_parentRule(0)
223 {
224 }
225
226 MediaList::MediaList(MediaQuerySet* mediaQueries, CSSRule* parentRule)
227     : m_mediaQueries(mediaQueries)
228     , m_parentStyleSheet(0)
229     , m_parentRule(parentRule)
230 {
231 }
232
233 MediaList::~MediaList()
234 {
235 }
236
237 void MediaList::setMediaText(const String& value, ExceptionCode& ec)
238 {
239     CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
240
241     bool success = m_mediaQueries->parse(value);
242     if (!success) {
243         ec = SYNTAX_ERR;
244         return;
245     }
246     if (m_parentStyleSheet)
247         m_parentStyleSheet->didMutate();
248 }
249
250 String MediaList::item(unsigned index) const
251 {
252     const Vector<OwnPtr<MediaQuery> >& queries = m_mediaQueries->queryVector();
253     if (index < queries.size())
254         return queries[index]->cssText();
255     return String();
256 }
257
258 void MediaList::deleteMedium(const String& medium, ExceptionCode& ec)
259 {
260     CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
261
262     bool success = m_mediaQueries->remove(medium);
263     if (!success) {
264         ec = NOT_FOUND_ERR;
265         return;
266     }
267     if (m_parentStyleSheet)
268         m_parentStyleSheet->didMutate();
269 }
270
271 void MediaList::appendMedium(const String& medium, ExceptionCode& ec)
272 {
273     CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
274
275     bool success = m_mediaQueries->add(medium);
276     if (!success) {
277         // FIXME: Should this really be INVALID_CHARACTER_ERR?
278         ec = INVALID_CHARACTER_ERR;
279         return;
280     }
281     if (m_parentStyleSheet)
282         m_parentStyleSheet->didMutate();
283 }
284
285 void MediaList::reattach(MediaQuerySet* mediaQueries)
286 {
287     ASSERT(mediaQueries);
288     m_mediaQueries = mediaQueries;
289 }
290
291 #if ENABLE(RESOLUTION_MEDIA_QUERY)
292 static void addResolutionWarningMessageToConsole(Document* document, const String& serializedExpression, const CSSPrimitiveValue* value)
293 {
294     ASSERT(document);
295     ASSERT(value);
296
297     DEFINE_STATIC_LOCAL(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: ")));
298     DEFINE_STATIC_LOCAL(String, mediaValueDPI, (ASCIILiteral("dpi")));
299     DEFINE_STATIC_LOCAL(String, mediaValueDPCM, (ASCIILiteral("dpcm")));
300     DEFINE_STATIC_LOCAL(String, lengthUnitInch, (ASCIILiteral("inch")));
301     DEFINE_STATIC_LOCAL(String, lengthUnitCentimeter, (ASCIILiteral("centimeter")));
302
303     String message;
304     if (value->isDotsPerInch())
305         message = String(mediaQueryMessage).replace("%replacementUnits%", mediaValueDPI).replace("%lengthUnit%", lengthUnitInch);
306     else if (value->isDotsPerCentimeter())
307         message = String(mediaQueryMessage).replace("%replacementUnits%", mediaValueDPCM).replace("%lengthUnit%", lengthUnitCentimeter);
308     else
309         ASSERT_NOT_REACHED();
310
311     message.append(serializedExpression);
312
313     document->addConsoleMessage(CSSMessageSource, DebugMessageLevel, message);
314 }
315
316 void reportMediaQueryWarningIfNeeded(Document* document, const MediaQuerySet* mediaQuerySet)
317 {
318     if (!mediaQuerySet || !document)
319         return;
320
321     const Vector<OwnPtr<MediaQuery> >& mediaQueries = mediaQuerySet->queryVector();
322     const size_t queryCount = mediaQueries.size();
323
324     if (!queryCount)
325         return;
326
327     for (size_t i = 0; i < queryCount; ++i) {
328         const MediaQuery* query = mediaQueries[i].get();
329         String mediaType = query->mediaType();
330         if (!query->ignored() && !equalIgnoringCase(mediaType, "print")) {
331             const Vector<OwnPtr<MediaQueryExp>>& expressions = query->expressions();
332             for (size_t j = 0; j < expressions.size(); ++j) {
333                 const MediaQueryExp* exp = expressions.at(j).get();
334                 if (exp->mediaFeature() == MediaFeatureNames::resolutionMediaFeature || exp->mediaFeature() == MediaFeatureNames::max_resolutionMediaFeature || exp->mediaFeature() == MediaFeatureNames::min_resolutionMediaFeature) {
335                     CSSValue* cssValue =  exp->value();
336                     if (cssValue && cssValue->isPrimitiveValue()) {
337                         CSSPrimitiveValue* primitiveValue = static_cast<CSSPrimitiveValue*>(cssValue);
338                         if (primitiveValue->isDotsPerInch() || primitiveValue->isDotsPerCentimeter())
339                             addResolutionWarningMessageToConsole(document, mediaQuerySet->mediaText(), primitiveValue);
340                     }
341                 }
342             }
343         }
344     }
345 }
346 #endif
347
348 }