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
34namespace 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
64Ref<MediaQuerySet> MediaQuerySet::create(const String& mediaString, MediaQueryParserContext context)
65{
66 if (mediaString.isEmpty())
67 return MediaQuerySet::create();
68
69 return MediaQueryParser::parseMediaQuerySet(mediaString, context).releaseNonNull();
70}
71
72MediaQuerySet::MediaQuerySet() = default;
73
74MediaQuerySet::MediaQuerySet(const MediaQuerySet& o)
75 : RefCounted()
76 , m_lastLine(o.m_lastLine)
77 , m_queries(o.m_queries)
78{
79}
80
81MediaQuerySet::~MediaQuerySet() = default;
82
83bool MediaQuerySet::set(const String& mediaString)
84{
85 auto result = create(mediaString);
86 m_queries.swap(result->m_queries);
87 return true;
88}
89
90bool 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 false;
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 false;
106 }
107
108 m_queries.append(result->m_queries[0]);
109 return true;
110}
111
112bool 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
139void MediaQuerySet::addMediaQuery(MediaQuery&& mediaQuery)
140{
141 m_queries.append(WTFMove(mediaQuery));
142}
143
144String 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
157void MediaQuerySet::shrinkToFit()
158{
159 m_queries.shrinkToFit();
160 for (auto& query : m_queries)
161 query.shrinkToFit();
162}
163
164MediaList::MediaList(MediaQuerySet* mediaQueries, CSSStyleSheet* parentSheet)
165 : m_mediaQueries(mediaQueries)
166 , m_parentStyleSheet(parentSheet)
167{
168}
169
170MediaList::MediaList(MediaQuerySet* mediaQueries, CSSRule* parentRule)
171 : m_mediaQueries(mediaQueries)
172 , m_parentRule(parentRule)
173{
174}
175
176MediaList::~MediaList() = default;
177
178ExceptionOr<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
187String 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
195ExceptionOr<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
207void MediaList::appendMedium(const String& medium)
208{
209 CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
210
211 if (!m_mediaQueries->add(medium))
212 return;
213 if (m_parentStyleSheet)
214 m_parentStyleSheet->didMutate();
215}
216
217void MediaList::reattach(MediaQuerySet* mediaQueries)
218{
219 ASSERT(mediaQueries);
220 m_mediaQueries = mediaQueries;
221}
222
223#if ENABLE(RESOLUTION_MEDIA_QUERY)
224
225static void addResolutionWarningMessageToConsole(Document& document, const String& serializedExpression, const CSSPrimitiveValue& value)
226{
227 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: "));
228 static NeverDestroyed<String> mediaValueDPI(MAKE_STATIC_STRING_IMPL("dpi"));
229 static NeverDestroyed<String> mediaValueDPCM(MAKE_STATIC_STRING_IMPL("dpcm"));
230 static NeverDestroyed<String> lengthUnitInch(MAKE_STATIC_STRING_IMPL("inch"));
231 static NeverDestroyed<String> lengthUnitCentimeter(MAKE_STATIC_STRING_IMPL("centimeter"));
232
233 String message;
234 if (value.isDotsPerInch())
235 message = mediaQueryMessage.get().replace("%replacementUnits%", mediaValueDPI).replace("%lengthUnit%", lengthUnitInch);
236 else if (value.isDotsPerCentimeter())
237 message = mediaQueryMessage.get().replace("%replacementUnits%", mediaValueDPCM).replace("%lengthUnit%", lengthUnitCentimeter);
238 else
239 ASSERT_NOT_REACHED();
240
241 message.append(serializedExpression);
242
243 document.addConsoleMessage(MessageSource::CSS, MessageLevel::Debug, message);
244}
245
246void reportMediaQueryWarningIfNeeded(Document* document, const MediaQuerySet* mediaQuerySet)
247{
248 if (!mediaQuerySet || !document)
249 return;
250
251 for (auto& query : mediaQuerySet->queryVector()) {
252 if (!query.ignored() && !equalLettersIgnoringASCIICase(query.mediaType(), "print")) {
253 auto& expressions = query.expressions();
254 for (auto& expression : expressions) {
255 if (expression.mediaFeature() == MediaFeatureNames::resolution || expression.mediaFeature() == MediaFeatureNames::maxResolution || expression.mediaFeature() == MediaFeatureNames::minResolution) {
256 auto* value = expression.value();
257 if (is<CSSPrimitiveValue>(value)) {
258 auto& primitiveValue = downcast<CSSPrimitiveValue>(*value);
259 if (primitiveValue.isDotsPerInch() || primitiveValue.isDotsPerCentimeter())
260 addResolutionWarningMessageToConsole(*document, mediaQuerySet->mediaText(), primitiveValue);
261 }
262 }
263 }
264 }
265 }
266}
267
268#endif
269
270TextStream& operator<<(TextStream& ts, const MediaQuerySet& querySet)
271{
272 ts << querySet.mediaText();
273 return ts;
274}
275
276TextStream& operator<<(TextStream& ts, const MediaList& mediaList)
277{
278 ts << mediaList.mediaText();
279 return ts;
280}
281
282} // namespace WebCore
283
284