text: /*
   * (C) 1999-2003 Lars Knoll (knoll@kde.org)
   * Copyright (C) 2004-2017 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 "CSSStyleSheet.h"
  #include "DOMWindow.h"
  #include "Document.h"
  #include "MediaQuery.h"
  #include "MediaQueryParser.h"
  #include <wtf/NeverDestroyed.h>
  #include <wtf/text/StringBuilder.h>
  #include <wtf/text/TextStream.h>
  
  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.
   * <style media="screen and resolution > 40dpi"> ..</style> 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 SyntaxError exception.
   */
      
  Ref<MediaQuerySet> MediaQuerySet::create(const String& mediaString)
  {
      if (mediaString.isEmpty())
          return MediaQuerySet::create();
      
      return MediaQueryParser::parseMediaQuerySet(mediaString).releaseNonNull();
  }
  
  MediaQuerySet::MediaQuerySet() = default;
  
  MediaQuerySet::MediaQuerySet(const MediaQuerySet& o)
      : RefCounted()
      , m_lastLine(o.m_lastLine)
      , m_queries(o.m_queries)
  {
  }
  
  MediaQuerySet::~MediaQuerySet() = default;
  
  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() = default;
  
  ExceptionOr<void> 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<void> MediaList::deleteMedium(const String& medium)
  {
      CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
  
      bool success = m_mediaQueries->remove(medium);
      if (!success)
          return Exception { NotFoundError };
      if (m_parentStyleSheet)
          m_parentStyleSheet->didMutate();
      return { };
  }
  
  ExceptionOr<void> MediaList::appendMedium(const String& medium)
  {
      CSSStyleSheet::RuleMutationScope mutationScope(m_parentRule);
  
      bool success = m_mediaQueries->add(medium);
      if (!success) {
          // FIXME: Should this really be InvalidCharacterError?
          return Exception { InvalidCharacterError };
      }
      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<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: "));
      static NeverDestroyed<String> mediaValueDPI(MAKE_STATIC_STRING_IMPL("dpi"));
      static NeverDestroyed<String> mediaValueDPCM(MAKE_STATIC_STRING_IMPL("dpcm"));
      static NeverDestroyed<String> lengthUnitInch(MAKE_STATIC_STRING_IMPL("inch"));
      static NeverDestroyed<String> lengthUnitCentimeter(MAKE_STATIC_STRING_IMPL("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<CSSPrimitiveValue>(value)) {
                          auto& primitiveValue = downcast<CSSPrimitiveValue>(*value);
                          if (primitiveValue.isDotsPerInch() || primitiveValue.isDotsPerCentimeter())
                              addResolutionWarningMessageToConsole(*document, mediaQuerySet->mediaText(), primitiveValue);
                      }
                  }
              }
          }
      }
  }
  
  #endif
  
  TextStream& operator<<(TextStream& ts, const MediaQuerySet& querySet)
  {
      ts << querySet.mediaText();
      return ts;
  }
  
  TextStream& operator<<(TextStream& ts, const MediaList& mediaList)
  {
      ts << mediaList.mediaText();
      return ts;
  }
  
  } // namespace WebCore
  
  