MediaQueryParser.cpp   [plain text]


// Copyright 2014 The Chromium Authors. All rights reserved.
// Copyright (C) 2016 Apple Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//    * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//    * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//    * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include "config.h"
#include "MediaQueryParser.h"

#include "CSSTokenizer.h"
#include "MediaList.h"
#include <wtf/Vector.h>

namespace WebCore {

RefPtr<MediaQuerySet> MediaQueryParser::parseMediaQuerySet(const String& queryString)
{
    return parseMediaQuerySet(CSSTokenizer(queryString).tokenRange());
}

RefPtr<MediaQuerySet> MediaQueryParser::parseMediaQuerySet(CSSParserTokenRange range)
{
    return MediaQueryParser(MediaQuerySetParser).parseInternal(range);
}

RefPtr<MediaQuerySet> MediaQueryParser::parseMediaCondition(CSSParserTokenRange range)
{
    return MediaQueryParser(MediaConditionParser).parseInternal(range);
}

const MediaQueryParser::State MediaQueryParser::ReadRestrictor = &MediaQueryParser::readRestrictor;
const MediaQueryParser::State MediaQueryParser::ReadMediaNot = &MediaQueryParser::readMediaNot;
const MediaQueryParser::State MediaQueryParser::ReadMediaType = &MediaQueryParser::readMediaType;
const MediaQueryParser::State MediaQueryParser::ReadAnd = &MediaQueryParser::readAnd;
const MediaQueryParser::State MediaQueryParser::ReadFeatureStart = &MediaQueryParser::readFeatureStart;
const MediaQueryParser::State MediaQueryParser::ReadFeature = &MediaQueryParser::readFeature;
const MediaQueryParser::State MediaQueryParser::ReadFeatureColon = &MediaQueryParser::readFeatureColon;
const MediaQueryParser::State MediaQueryParser::ReadFeatureValue = &MediaQueryParser::readFeatureValue;
const MediaQueryParser::State MediaQueryParser::ReadFeatureEnd = &MediaQueryParser::readFeatureEnd;
const MediaQueryParser::State MediaQueryParser::SkipUntilComma = &MediaQueryParser::skipUntilComma;
const MediaQueryParser::State MediaQueryParser::SkipUntilBlockEnd = &MediaQueryParser::skipUntilBlockEnd;
const MediaQueryParser::State MediaQueryParser::Done = &MediaQueryParser::done;

MediaQueryParser::MediaQueryParser(ParserType parserType)
    : m_parserType(parserType)
    , m_querySet(MediaQuerySet::create())
{
    if (parserType == MediaQuerySetParser)
        m_state = &MediaQueryParser::readRestrictor;
    else // MediaConditionParser
        m_state = &MediaQueryParser::readMediaNot;
}

MediaQueryParser::~MediaQueryParser() { }

void MediaQueryParser::setStateAndRestrict(State state, MediaQuery::Restrictor restrictor)
{
    m_mediaQueryData.setRestrictor(restrictor);
    m_state = state;
}

// State machine member functions start here
void MediaQueryParser::readRestrictor(CSSParserTokenType type, const CSSParserToken& token)
{
    readMediaType(type, token);
}

void MediaQueryParser::readMediaNot(CSSParserTokenType type, const CSSParserToken& token)
{
    if (type == IdentToken && equalIgnoringASCIICase(token.value(), "not"))
        setStateAndRestrict(ReadFeatureStart, MediaQuery::Not);
    else
        readFeatureStart(type, token);
}

static bool isRestrictorOrLogicalOperator(const CSSParserToken& token)
{
    // FIXME: it would be more efficient to use lower-case always for tokenValue.
    return equalIgnoringASCIICase(token.value(), "not")
        || equalIgnoringASCIICase(token.value(), "and")
        || equalIgnoringASCIICase(token.value(), "or")
        || equalIgnoringASCIICase(token.value(), "only");
}

void MediaQueryParser::readMediaType(CSSParserTokenType type, const CSSParserToken& token)
{
    if (type == LeftParenthesisToken) {
        if (m_mediaQueryData.restrictor() != MediaQuery::None)
            m_state = SkipUntilComma;
        else
            m_state = ReadFeature;
    } else if (type == IdentToken) {
        if (m_state == ReadRestrictor && equalIgnoringASCIICase(token.value(), "not"))
            setStateAndRestrict(ReadMediaType, MediaQuery::Not);
        else if (m_state == ReadRestrictor && equalIgnoringASCIICase(token.value(), "only"))
            setStateAndRestrict(ReadMediaType, MediaQuery::Only);
        else if (m_mediaQueryData.restrictor() != MediaQuery::None
            && isRestrictorOrLogicalOperator(token)) {
            m_state = SkipUntilComma;
        } else {
            m_mediaQueryData.setMediaType(token.value().toString());
            m_state = ReadAnd;
        }
    } else if (type == EOFToken && (!m_querySet->queryVector().size() || m_state != ReadRestrictor))
        m_state = Done;
    else {
        m_state = SkipUntilComma;
        if (type == CommaToken)
            skipUntilComma(type, token);
    }
}

void MediaQueryParser::commitMediaQuery()
{
    // FIXME-NEWPARSER: Convoluted and awful, but we can't change the MediaQuerySet yet because of the
    // old parser.
    MediaQuery mediaQuery = MediaQuery(m_mediaQueryData.restrictor(), m_mediaQueryData.mediaType(), WTFMove(m_mediaQueryData.expressions()));
    m_mediaQueryData.clear();
    m_querySet->addMediaQuery(WTFMove(mediaQuery));
}

void MediaQueryParser::readAnd(CSSParserTokenType type, const CSSParserToken& token)
{
    if (type == IdentToken && equalIgnoringASCIICase(token.value(), "and")) {
        m_state = ReadFeatureStart;
    } else if (type == CommaToken && m_parserType != MediaConditionParser) {
        commitMediaQuery();
        m_state = ReadRestrictor;
    } else if (type == EOFToken)
        m_state = Done;
    else
        m_state = SkipUntilComma;
}

void MediaQueryParser::readFeatureStart(CSSParserTokenType type, const CSSParserToken& /*token*/)
{
    if (type == LeftParenthesisToken)
        m_state = ReadFeature;
    else
        m_state = SkipUntilComma;
}

void MediaQueryParser::readFeature(CSSParserTokenType type, const CSSParserToken& token)
{
    if (type == IdentToken) {
        m_mediaQueryData.setMediaFeature(token.value().toString());
        m_state = ReadFeatureColon;
    } else
        m_state = SkipUntilComma;
}

void MediaQueryParser::readFeatureColon(CSSParserTokenType type, const CSSParserToken& token)
{
    if (type == ColonToken)
        m_state = ReadFeatureValue;
    else if (type == RightParenthesisToken || type == EOFToken)
        readFeatureEnd(type, token);
    else
        m_state = SkipUntilBlockEnd;
}

void MediaQueryParser::readFeatureValue(CSSParserTokenType type, const CSSParserToken& token)
{
    if (type == DimensionToken && token.unitType() == CSSPrimitiveValue::UnitType::CSS_UNKNOWN)
        m_state = SkipUntilComma;
    else {
        if (m_mediaQueryData.tryAddParserToken(type, token))
            m_state = ReadFeatureEnd;
        else
            m_state = SkipUntilBlockEnd;
    }
}

void MediaQueryParser::readFeatureEnd(CSSParserTokenType type, const CSSParserToken& token)
{
    if (type == RightParenthesisToken || type == EOFToken) {
        if (type != EOFToken && m_mediaQueryData.addExpression())
            m_state = ReadAnd;
        else
            m_state = SkipUntilComma;
    } else if (type == DelimiterToken && token.delimiter() == '/') {
        m_mediaQueryData.tryAddParserToken(type, token);
        m_state = ReadFeatureValue;
    } else
        m_state = SkipUntilBlockEnd;
}

void MediaQueryParser::skipUntilComma(CSSParserTokenType type, const CSSParserToken& /*token*/)
{
    if ((type == CommaToken && !m_blockWatcher.blockLevel()) || type == EOFToken) {
        m_state = ReadRestrictor;
        m_mediaQueryData.clear();
        MediaQuery query = MediaQuery(MediaQuery::Not, "all", Vector<MediaQueryExpression>());
        m_querySet->addMediaQuery(WTFMove(query));
    }
}

void MediaQueryParser::skipUntilBlockEnd(CSSParserTokenType /*type */, const CSSParserToken& token)
{
    if (token.getBlockType() == CSSParserToken::BlockEnd && !m_blockWatcher.blockLevel())
        m_state = SkipUntilComma;
}

void MediaQueryParser::done(CSSParserTokenType /*type*/, const CSSParserToken& /*token*/) { }

void MediaQueryParser::handleBlocks(const CSSParserToken& token)
{
    if (token.getBlockType() == CSSParserToken::BlockStart
        && (token.type() != LeftParenthesisToken || m_blockWatcher.blockLevel()))
            m_state = SkipUntilBlockEnd;
}

void MediaQueryParser::processToken(const CSSParserToken& token)
{
    CSSParserTokenType type = token.type();

    handleBlocks(token);
    m_blockWatcher.handleToken(token);

    // Call the function that handles current state
    if (type != WhitespaceToken)
        ((this)->*(m_state))(type, token);
}

// The state machine loop
RefPtr<MediaQuerySet> MediaQueryParser::parseInternal(CSSParserTokenRange range)
{
    while (!range.atEnd())
        processToken(range.consume());

    // FIXME: Can we get rid of this special case?
    if (m_parserType == MediaQuerySetParser)
        processToken(CSSParserToken(EOFToken));

    if (m_state != ReadAnd && m_state != ReadRestrictor && m_state != Done && m_state != ReadMediaNot) {
        MediaQuery query = MediaQuery(MediaQuery::Not, "all", Vector<MediaQueryExpression>());
        m_querySet->addMediaQuery(WTFMove(query));
    } else if (m_mediaQueryData.currentMediaQueryChanged())
        commitMediaQuery();

    return m_querySet;
}

MediaQueryData::MediaQueryData()
    : m_restrictor(MediaQuery::None)
    , m_mediaType("all")
    , m_mediaTypeSet(false)
{
}

void MediaQueryData::clear()
{
    m_restrictor = MediaQuery::None;
    m_mediaType = "all";
    m_mediaTypeSet = false;
    m_mediaFeature = String();
    m_valueList.clear();
    m_expressions.clear();
}

bool MediaQueryData::addExpression()
{
    MediaQueryExpression expression = MediaQueryExpression(m_mediaFeature, m_valueList);
    bool isValid = expression.isValid();
    m_expressions.append(WTFMove(expression));
    m_valueList.clear();
    return isValid;
}

bool MediaQueryData::tryAddParserToken(CSSParserTokenType type, const CSSParserToken& token)
{
    if (type == NumberToken || type == PercentageToken || type == DimensionToken
        || type == DelimiterToken || type == IdentToken) {
        m_valueList.append(token);
        return true;
    }

    return false;
}

void MediaQueryData::setMediaType(const String& mediaType)
{
    m_mediaType = mediaType;
    m_mediaTypeSet = true;
}

} // namespace WebCsore