/* * (C) 1999-2003 Lars Knoll (knoll@kde.org) * Copyright (C) 2004, 2006, 2010, 2012 Apple Inc. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "config.h" #include "MediaList.h" #include "CSSImportRule.h" #include "CSSParser.h" #include "CSSStyleSheet.h" #include "DOMWindow.h" #include "Document.h" #include "ExceptionCode.h" #include "HTMLParserIdioms.h" #include "MediaFeatureNames.h" #include "MediaQuery.h" #include "MediaQueryParser.h" #include "ScriptableDocumentParser.h" #include #include namespace WebCore { /* MediaList is used to store 3 types of media related entities which mean the same: * Media Queries, Media Types and Media Descriptors. * Currently MediaList always tries to parse media queries and if parsing fails, * tries to fallback to Media Descriptors if m_fallbackToDescriptor flag is set. * Slight problem with syntax error handling: * CSS 2.1 Spec (http://www.w3.org/TR/CSS21/media.html) * specifies that failing media type parsing is a syntax error * CSS 3 Media Queries Spec (http://www.w3.org/TR/css3-mediaqueries/) * specifies that failing media query is a syntax error * HTML 4.01 spec (http://www.w3.org/TR/REC-html40/present/styles.html#adef-media) * specifies that Media Descriptors should be parsed with forward-compatible syntax * DOM Level 2 Style Sheet spec (http://www.w3.org/TR/DOM-Level-2-Style/) * talks about MediaList.mediaText and refers * - to Media Descriptors of HTML 4.0 in context of StyleSheet * - to Media Types of CSS 2.0 in context of CSSMediaRule and CSSImportRule * * These facts create situation where same (illegal) media specification may result in * different parses depending on whether it is media attr of style element or part of * css @media rule. * will be enabled on screen devices where as * @media screen and resolution > 40dpi {..} will not. * This gets more counter-intuitive in JavaScript: * document.styleSheets[0].media.mediaText = "screen and resolution > 40dpi" will be ok and * enabled, while * document.styleSheets[0].cssRules[0].media.mediaText = "screen and resolution > 40dpi" will * throw SYNTAX_ERR exception. */ Ref MediaQuerySet::create(const String& mediaString) { if (mediaString.isEmpty()) return MediaQuerySet::create(); return MediaQueryParser::parseMediaQuerySet(mediaString).releaseNonNull(); } MediaQuerySet::MediaQuerySet() : m_lastLine(0) { } MediaQuerySet::MediaQuerySet(const MediaQuerySet& o) : RefCounted() , m_lastLine(o.m_lastLine) , m_queries(o.m_queries) { } MediaQuerySet::~MediaQuerySet() { } bool MediaQuerySet::set(const String& mediaString) { auto result = create(mediaString); m_queries.swap(result->m_queries); return true; } bool MediaQuerySet::add(const String& queryString) { // To "parse a media query" for a given string means to follow "the parse // a media query list" steps and return "null" if more than one media query // is returned, or else the returned media query. auto result = create(queryString); // Only continue if exactly one media query is found, as described above. if (result->m_queries.size() != 1) return true; // If comparing with any of the media queries in the collection of media // queries returns true terminate these steps. for (size_t i = 0; i < m_queries.size(); ++i) { if (m_queries[i] == result->m_queries[0]) return true; } m_queries.append(result->m_queries[0]); return true; } bool MediaQuerySet::remove(const String& queryStringToRemove) { // To "parse a media query" for a given string means to follow "the parse // a media query list" steps and return "null" if more than one media query // is returned, or else the returned media query. auto result = create(queryStringToRemove); // Only continue if exactly one media query is found, as described above. if (result->m_queries.size() != 1) return true; // Remove any media query from the collection of media queries for which // comparing with the media query returns true. bool found = false; // Using signed int here, since for the first value, --i will result in -1. for (int i = 0; i < (int)m_queries.size(); ++i) { if (m_queries[i] == result->m_queries[0]) { m_queries.remove(i); --i; found = true; } } return found; } void MediaQuerySet::addMediaQuery(MediaQuery&& mediaQuery) { m_queries.append(WTFMove(mediaQuery)); } String MediaQuerySet::mediaText() const { StringBuilder text; bool needComma = false; for (auto& query : m_queries) { if (needComma) text.appendLiteral(", "); text.append(query.cssText()); needComma = true; } return text.toString(); } void MediaQuerySet::shrinkToFit() { m_queries.shrinkToFit(); for (auto& query : m_queries) query.shrinkToFit(); } MediaList::MediaList(MediaQuerySet* mediaQueries, CSSStyleSheet* parentSheet) : m_mediaQueries(mediaQueries) , m_parentStyleSheet(parentSheet) { } MediaList::MediaList(MediaQuerySet* mediaQueries, CSSRule* parentRule) : m_mediaQueries(mediaQueries) , m_parentRule(parentRule) { } MediaList::~MediaList() { } ExceptionOr MediaList::setMediaText(const String& value) { CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule); m_mediaQueries->set(value); if (m_parentStyleSheet) m_parentStyleSheet->didMutate(); return { }; } String MediaList::item(unsigned index) const { auto& queries = m_mediaQueries->queryVector(); if (index < queries.size()) return queries[index].cssText(); return String(); } ExceptionOr MediaList::deleteMedium(const String& medium) { CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule); bool success = m_mediaQueries->remove(medium); if (!success) return Exception { NOT_FOUND_ERR }; if (m_parentStyleSheet) m_parentStyleSheet->didMutate(); return { }; } ExceptionOr MediaList::appendMedium(const String& medium) { CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule); bool success = m_mediaQueries->add(medium); if (!success) { // FIXME: Should this really be INVALID_CHARACTER_ERR? return Exception { INVALID_CHARACTER_ERR }; } if (m_parentStyleSheet) m_parentStyleSheet->didMutate(); return { }; } void MediaList::reattach(MediaQuerySet* mediaQueries) { ASSERT(mediaQueries); m_mediaQueries = mediaQueries; } #if ENABLE(RESOLUTION_MEDIA_QUERY) static void addResolutionWarningMessageToConsole(Document& document, const String& serializedExpression, const CSSPrimitiveValue& value) { static NeverDestroyed 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: ")); static NeverDestroyed mediaValueDPI(ASCIILiteral("dpi")); static NeverDestroyed mediaValueDPCM(ASCIILiteral("dpcm")); static NeverDestroyed lengthUnitInch(ASCIILiteral("inch")); static NeverDestroyed lengthUnitCentimeter(ASCIILiteral("centimeter")); String message; if (value.isDotsPerInch()) message = mediaQueryMessage.get().replace("%replacementUnits%", mediaValueDPI).replace("%lengthUnit%", lengthUnitInch); else if (value.isDotsPerCentimeter()) message = mediaQueryMessage.get().replace("%replacementUnits%", mediaValueDPCM).replace("%lengthUnit%", lengthUnitCentimeter); else ASSERT_NOT_REACHED(); message.append(serializedExpression); document.addConsoleMessage(MessageSource::CSS, MessageLevel::Debug, message); } void reportMediaQueryWarningIfNeeded(Document* document, const MediaQuerySet* mediaQuerySet) { if (!mediaQuerySet || !document) return; for (auto& query : mediaQuerySet->queryVector()) { if (!query.ignored() && !equalLettersIgnoringASCIICase(query.mediaType(), "print")) { auto& expressions = query.expressions(); for (auto& expression : expressions) { if (expression.mediaFeature() == MediaFeatureNames::resolution || expression.mediaFeature() == MediaFeatureNames::maxResolution || expression.mediaFeature() == MediaFeatureNames::minResolution) { auto* value = expression.value(); if (is(value)) { auto& primitiveValue = downcast(*value); if (primitiveValue.isDotsPerInch() || primitiveValue.isDotsPerCentimeter()) addResolutionWarningMessageToConsole(*document, mediaQuerySet->mediaText(), primitiveValue); } } } } } } #endif }