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 36namespace 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 66MediaQuerySet::MediaQuerySet() 67 : m_fallbackToDescriptor(false) 68 , m_lastLine(0) 69{ 70} 71 72MediaQuerySet::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 91MediaQuerySet::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 101MediaQuerySet::~MediaQuerySet() 102{ 103} 104 105static 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 124bool 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 159bool 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 176bool 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 199void MediaQuerySet::addMediaQuery(PassOwnPtr<MediaQuery> mediaQuery) 200{ 201 m_queries.append(mediaQuery); 202} 203 204String 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 219MediaList::MediaList(MediaQuerySet* mediaQueries, CSSStyleSheet* parentSheet) 220 : m_mediaQueries(mediaQueries) 221 , m_parentStyleSheet(parentSheet) 222 , m_parentRule(0) 223{ 224} 225 226MediaList::MediaList(MediaQuerySet* mediaQueries, CSSRule* parentRule) 227 : m_mediaQueries(mediaQueries) 228 , m_parentStyleSheet(0) 229 , m_parentRule(parentRule) 230{ 231} 232 233MediaList::~MediaList() 234{ 235} 236 237void 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 250String 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 258void 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 271void 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 285void MediaList::reattach(MediaQuerySet* mediaQueries) 286{ 287 ASSERT(mediaQueries); 288 m_mediaQueries = mediaQueries; 289} 290 291#if ENABLE(RESOLUTION_MEDIA_QUERY) 292static 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 316void 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> >* exps = query->expressions(); 332 for (size_t j = 0; j < exps->size(); ++j) { 333 const MediaQueryExp* exp = exps->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} 349