1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Copyright (C) 2016 Apple Inc. All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8// * Redistributions of source code must retain the above copyright
9// notice, this list of conditions and the following disclaimer.
10// * Redistributions in binary form must reproduce the above
11// copyright notice, this list of conditions and the following disclaimer
12// in the documentation and/or other materials provided with the
13// distribution.
14// * Neither the name of Google Inc. nor the names of its
15// contributors may be used to endorse or promote products derived from
16// this software without specific prior written permission.
17//
18// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30#include "config.h"
31#include "MediaQueryParser.h"
32
33#include "CSSTokenizer.h"
34#include "MediaList.h"
35#include "MediaQueryParserContext.h"
36#include <wtf/Vector.h>
37
38namespace WebCore {
39
40RefPtr<MediaQuerySet> MediaQueryParser::parseMediaQuerySet(const String& queryString, MediaQueryParserContext context)
41{
42 return parseMediaQuerySet(CSSTokenizer(queryString).tokenRange(), context);
43}
44
45RefPtr<MediaQuerySet> MediaQueryParser::parseMediaQuerySet(CSSParserTokenRange range, MediaQueryParserContext context)
46{
47 return MediaQueryParser(MediaQuerySetParser, context).parseInternal(range);
48}
49
50RefPtr<MediaQuerySet> MediaQueryParser::parseMediaCondition(CSSParserTokenRange range, MediaQueryParserContext context)
51{
52 return MediaQueryParser(MediaConditionParser, context).parseInternal(range);
53}
54
55const MediaQueryParser::State MediaQueryParser::ReadRestrictor = &MediaQueryParser::readRestrictor;
56const MediaQueryParser::State MediaQueryParser::ReadMediaNot = &MediaQueryParser::readMediaNot;
57const MediaQueryParser::State MediaQueryParser::ReadMediaType = &MediaQueryParser::readMediaType;
58const MediaQueryParser::State MediaQueryParser::ReadAnd = &MediaQueryParser::readAnd;
59const MediaQueryParser::State MediaQueryParser::ReadFeatureStart = &MediaQueryParser::readFeatureStart;
60const MediaQueryParser::State MediaQueryParser::ReadFeature = &MediaQueryParser::readFeature;
61const MediaQueryParser::State MediaQueryParser::ReadFeatureColon = &MediaQueryParser::readFeatureColon;
62const MediaQueryParser::State MediaQueryParser::ReadFeatureValue = &MediaQueryParser::readFeatureValue;
63const MediaQueryParser::State MediaQueryParser::ReadFeatureEnd = &MediaQueryParser::readFeatureEnd;
64const MediaQueryParser::State MediaQueryParser::SkipUntilComma = &MediaQueryParser::skipUntilComma;
65const MediaQueryParser::State MediaQueryParser::SkipUntilBlockEnd = &MediaQueryParser::skipUntilBlockEnd;
66const MediaQueryParser::State MediaQueryParser::Done = &MediaQueryParser::done;
67
68MediaQueryParser::MediaQueryParser(ParserType parserType, MediaQueryParserContext context)
69 : m_parserType(parserType)
70 , m_mediaQueryData(context)
71 , m_querySet(MediaQuerySet::create())
72
73{
74 if (parserType == MediaQuerySetParser)
75 m_state = &MediaQueryParser::readRestrictor;
76 else // MediaConditionParser
77 m_state = &MediaQueryParser::readMediaNot;
78}
79
80MediaQueryParser::~MediaQueryParser() = default;
81
82void MediaQueryParser::setStateAndRestrict(State state, MediaQuery::Restrictor restrictor)
83{
84 m_mediaQueryData.setRestrictor(restrictor);
85 m_state = state;
86}
87
88// State machine member functions start here
89void MediaQueryParser::readRestrictor(CSSParserTokenType type, const CSSParserToken& token, CSSParserTokenRange& range)
90{
91 readMediaType(type, token, range);
92}
93
94void MediaQueryParser::readMediaNot(CSSParserTokenType type, const CSSParserToken& token, CSSParserTokenRange& range)
95{
96 if (type == IdentToken && equalIgnoringASCIICase(token.value(), "not"))
97 setStateAndRestrict(ReadFeatureStart, MediaQuery::Not);
98 else
99 readFeatureStart(type, token, range);
100}
101
102static bool isRestrictorOrLogicalOperator(const CSSParserToken& token)
103{
104 // FIXME: it would be more efficient to use lower-case always for tokenValue.
105 return equalIgnoringASCIICase(token.value(), "not")
106 || equalIgnoringASCIICase(token.value(), "and")
107 || equalIgnoringASCIICase(token.value(), "or")
108 || equalIgnoringASCIICase(token.value(), "only");
109}
110
111void MediaQueryParser::readMediaType(CSSParserTokenType type, const CSSParserToken& token, CSSParserTokenRange& range)
112{
113 if (type == LeftParenthesisToken) {
114 if (m_mediaQueryData.restrictor() != MediaQuery::None)
115 m_state = SkipUntilComma;
116 else
117 m_state = ReadFeature;
118 } else if (type == IdentToken) {
119 if (m_state == ReadRestrictor && equalIgnoringASCIICase(token.value(), "not"))
120 setStateAndRestrict(ReadMediaType, MediaQuery::Not);
121 else if (m_state == ReadRestrictor && equalIgnoringASCIICase(token.value(), "only"))
122 setStateAndRestrict(ReadMediaType, MediaQuery::Only);
123 else if (m_mediaQueryData.restrictor() != MediaQuery::None
124 && isRestrictorOrLogicalOperator(token)) {
125 m_state = SkipUntilComma;
126 } else {
127 m_mediaQueryData.setMediaType(token.value().toString());
128 m_state = ReadAnd;
129 }
130 } else if (type == EOFToken && (!m_querySet->queryVector().size() || m_state != ReadRestrictor))
131 m_state = Done;
132 else {
133 m_state = SkipUntilComma;
134 if (type == CommaToken)
135 skipUntilComma(type, token, range);
136 }
137}
138
139void MediaQueryParser::commitMediaQuery()
140{
141 // FIXME-NEWPARSER: Convoluted and awful, but we can't change the MediaQuerySet yet because of the
142 // old parser.
143 static const NeverDestroyed<String> defaultMediaType { "all"_s };
144 MediaQuery mediaQuery { m_mediaQueryData.restrictor(), m_mediaQueryData.mediaType().valueOr(defaultMediaType), WTFMove(m_mediaQueryData.expressions()) };
145 m_mediaQueryData.clear();
146 m_querySet->addMediaQuery(WTFMove(mediaQuery));
147}
148
149void MediaQueryParser::readAnd(CSSParserTokenType type, const CSSParserToken& token, CSSParserTokenRange& /*range*/)
150{
151 if (type == IdentToken && equalIgnoringASCIICase(token.value(), "and")) {
152 m_state = ReadFeatureStart;
153 } else if (type == CommaToken && m_parserType != MediaConditionParser) {
154 commitMediaQuery();
155 m_state = ReadRestrictor;
156 } else if (type == EOFToken)
157 m_state = Done;
158 else
159 m_state = SkipUntilComma;
160}
161
162void MediaQueryParser::readFeatureStart(CSSParserTokenType type, const CSSParserToken& /*token*/, CSSParserTokenRange& /*range*/)
163{
164 if (type == LeftParenthesisToken)
165 m_state = ReadFeature;
166 else
167 m_state = SkipUntilComma;
168}
169
170void MediaQueryParser::readFeature(CSSParserTokenType type, const CSSParserToken& token, CSSParserTokenRange& /*range*/)
171{
172 if (type == IdentToken) {
173 m_mediaQueryData.setMediaFeature(token.value().toString());
174 m_state = ReadFeatureColon;
175 } else
176 m_state = SkipUntilComma;
177}
178
179void MediaQueryParser::readFeatureColon(CSSParserTokenType type, const CSSParserToken& token, CSSParserTokenRange& range)
180{
181 if (type == ColonToken) {
182 while (range.peek().type() == WhitespaceToken)
183 range.consume();
184 if (range.peek().type() == RightParenthesisToken || range.peek().type() == EOFToken)
185 m_state = SkipUntilBlockEnd;
186 else
187 m_state = ReadFeatureValue;
188 } else if (type == RightParenthesisToken || type == EOFToken) {
189 m_mediaQueryData.addExpression(range);
190 readFeatureEnd(type, token, range);
191 } else
192 m_state = SkipUntilBlockEnd;
193}
194
195void MediaQueryParser::readFeatureValue(CSSParserTokenType type, const CSSParserToken& token, CSSParserTokenRange& range)
196{
197 if (type == DimensionToken && token.unitType() == CSSPrimitiveValue::UnitType::CSS_UNKNOWN) {
198 range.consume();
199 m_state = SkipUntilComma;
200 } else {
201 m_mediaQueryData.addExpression(range);
202 m_state = ReadFeatureEnd;
203 }
204}
205
206void MediaQueryParser::readFeatureEnd(CSSParserTokenType type, const CSSParserToken& /*token*/, CSSParserTokenRange& /*range*/)
207{
208 if (type == RightParenthesisToken || type == EOFToken) {
209 if (type != EOFToken && m_mediaQueryData.lastExpressionValid())
210 m_state = ReadAnd;
211 else
212 m_state = SkipUntilComma;
213 } else {
214 m_mediaQueryData.removeLastExpression();
215 m_state = SkipUntilBlockEnd;
216 }
217}
218
219void MediaQueryParser::skipUntilComma(CSSParserTokenType type, const CSSParserToken& /*token*/, CSSParserTokenRange& /*range*/)
220{
221 if ((type == CommaToken && !m_blockWatcher.blockLevel()) || type == EOFToken) {
222 m_state = ReadRestrictor;
223 m_mediaQueryData.clear();
224 MediaQuery query = MediaQuery(MediaQuery::Not, "all", Vector<MediaQueryExpression>());
225 m_querySet->addMediaQuery(WTFMove(query));
226 }
227}
228
229void MediaQueryParser::skipUntilBlockEnd(CSSParserTokenType /*type */, const CSSParserToken& token, CSSParserTokenRange& /*range*/)
230{
231 if (token.getBlockType() == CSSParserToken::BlockEnd && !m_blockWatcher.blockLevel())
232 m_state = SkipUntilComma;
233}
234
235void MediaQueryParser::done(CSSParserTokenType /*type*/, const CSSParserToken& /*token*/, CSSParserTokenRange& /*range*/) { }
236
237void MediaQueryParser::handleBlocks(const CSSParserToken& token)
238{
239 if (token.getBlockType() == CSSParserToken::BlockStart
240 && (token.type() != LeftParenthesisToken || m_blockWatcher.blockLevel()))
241 m_state = SkipUntilBlockEnd;
242}
243
244void MediaQueryParser::processToken(const CSSParserToken& token, CSSParserTokenRange& range)
245{
246 CSSParserTokenType type = token.type();
247
248 if (m_state != ReadFeatureValue || type == WhitespaceToken) {
249 handleBlocks(token);
250 m_blockWatcher.handleToken(token);
251 range.consume();
252 }
253
254 // Call the function that handles current state
255 if (type != WhitespaceToken)
256 ((this)->*(m_state))(type, token, range);
257}
258
259// The state machine loop
260RefPtr<MediaQuerySet> MediaQueryParser::parseInternal(CSSParserTokenRange range)
261{
262 while (!range.atEnd())
263 processToken(range.peek(), range);
264
265 // FIXME: Can we get rid of this special case?
266 if (m_parserType == MediaQuerySetParser)
267 processToken(CSSParserToken(EOFToken), range);
268
269 if (m_state != ReadAnd && m_state != ReadRestrictor && m_state != Done && m_state != ReadMediaNot) {
270 MediaQuery query = MediaQuery(MediaQuery::Not, "all", Vector<MediaQueryExpression>());
271 m_querySet->addMediaQuery(WTFMove(query));
272 } else if (m_mediaQueryData.currentMediaQueryChanged())
273 commitMediaQuery();
274
275 m_querySet->shrinkToFit();
276
277 return m_querySet;
278}
279
280MediaQueryParser::MediaQueryData::MediaQueryData(MediaQueryParserContext context)
281 : m_context(context)
282{
283}
284
285void MediaQueryParser::MediaQueryData::clear()
286{
287 m_restrictor = MediaQuery::None;
288 m_mediaType = WTF::nullopt;
289 m_mediaFeature = String();
290 m_expressions.clear();
291}
292
293void MediaQueryParser::MediaQueryData::addExpression(CSSParserTokenRange& range)
294{
295 m_expressions.append(MediaQueryExpression { m_mediaFeature, range, m_context });
296}
297
298bool MediaQueryParser::MediaQueryData::lastExpressionValid()
299{
300 return m_expressions.last().isValid();
301}
302
303void MediaQueryParser::MediaQueryData::removeLastExpression()
304{
305 m_expressions.removeLast();
306}
307
308} // namespace WebCore
309