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 | |
38 | namespace WebCore { |
39 | |
40 | RefPtr<MediaQuerySet> MediaQueryParser::parseMediaQuerySet(const String& queryString, MediaQueryParserContext context) |
41 | { |
42 | return parseMediaQuerySet(CSSTokenizer(queryString).tokenRange(), context); |
43 | } |
44 | |
45 | RefPtr<MediaQuerySet> MediaQueryParser::parseMediaQuerySet(CSSParserTokenRange range, MediaQueryParserContext context) |
46 | { |
47 | return MediaQueryParser(MediaQuerySetParser, context).parseInternal(range); |
48 | } |
49 | |
50 | RefPtr<MediaQuerySet> MediaQueryParser::parseMediaCondition(CSSParserTokenRange range, MediaQueryParserContext context) |
51 | { |
52 | return MediaQueryParser(MediaConditionParser, context).parseInternal(range); |
53 | } |
54 | |
55 | const MediaQueryParser::State MediaQueryParser::ReadRestrictor = &MediaQueryParser::readRestrictor; |
56 | const MediaQueryParser::State MediaQueryParser::ReadMediaNot = &MediaQueryParser::readMediaNot; |
57 | const MediaQueryParser::State MediaQueryParser::ReadMediaType = &MediaQueryParser::readMediaType; |
58 | const MediaQueryParser::State MediaQueryParser::ReadAnd = &MediaQueryParser::readAnd; |
59 | const MediaQueryParser::State MediaQueryParser::ReadFeatureStart = &MediaQueryParser::readFeatureStart; |
60 | const MediaQueryParser::State MediaQueryParser::ReadFeature = &MediaQueryParser::readFeature; |
61 | const MediaQueryParser::State MediaQueryParser::ReadFeatureColon = &MediaQueryParser::readFeatureColon; |
62 | const MediaQueryParser::State MediaQueryParser::ReadFeatureValue = &MediaQueryParser::readFeatureValue; |
63 | const MediaQueryParser::State MediaQueryParser::ReadFeatureEnd = &MediaQueryParser::readFeatureEnd; |
64 | const MediaQueryParser::State MediaQueryParser::SkipUntilComma = &MediaQueryParser::skipUntilComma; |
65 | const MediaQueryParser::State MediaQueryParser::SkipUntilBlockEnd = &MediaQueryParser::skipUntilBlockEnd; |
66 | const MediaQueryParser::State MediaQueryParser::Done = &MediaQueryParser::done; |
67 | |
68 | MediaQueryParser::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 | |
80 | MediaQueryParser::~MediaQueryParser() = default; |
81 | |
82 | void 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 |
89 | void MediaQueryParser::readRestrictor(CSSParserTokenType type, const CSSParserToken& token, CSSParserTokenRange& range) |
90 | { |
91 | readMediaType(type, token, range); |
92 | } |
93 | |
94 | void 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 | |
102 | static 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 | |
111 | void 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 | |
139 | void 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 | |
149 | void 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 | |
162 | void 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 | |
170 | void 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 | |
179 | void 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 | |
195 | void 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 | |
206 | void 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 | |
219 | void 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 | |
229 | void 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 | |
235 | void MediaQueryParser::done(CSSParserTokenType /*type*/, const CSSParserToken& /*token*/, CSSParserTokenRange& /*range*/) { } |
236 | |
237 | void 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 | |
244 | void 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 |
260 | RefPtr<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 | |
280 | MediaQueryParser::MediaQueryData::MediaQueryData(MediaQueryParserContext context) |
281 | : m_context(context) |
282 | { |
283 | } |
284 | |
285 | void MediaQueryParser::MediaQueryData::clear() |
286 | { |
287 | m_restrictor = MediaQuery::None; |
288 | m_mediaType = WTF::nullopt; |
289 | m_mediaFeature = String(); |
290 | m_expressions.clear(); |
291 | } |
292 | |
293 | void MediaQueryParser::MediaQueryData::addExpression(CSSParserTokenRange& range) |
294 | { |
295 | m_expressions.append(MediaQueryExpression { m_mediaFeature, range, m_context }); |
296 | } |
297 | |
298 | bool MediaQueryParser::MediaQueryData::lastExpressionValid() |
299 | { |
300 | return m_expressions.last().isValid(); |
301 | } |
302 | |
303 | void MediaQueryParser::MediaQueryData::removeLastExpression() |
304 | { |
305 | m_expressions.removeLast(); |
306 | } |
307 | |
308 | } // namespace WebCore |
309 | |