mirror of
https://github.com/RGBCube/serenity
synced 2025-05-31 16:18:12 +00:00
LibWeb: Parse media queries! :^)
While not complete by any means, we are now compatible with the [level 3 spec](https://www.w3.org/TR/css3-mediaqueries/#syntax) and some parts of [level 4.](https://www.w3.org/TR/mediaqueries-4#mq-syntax) Compatibility with level 4+ requires: - Implementing the range syntax: `(800px <= width <= 1200px)` - Parsing `<general-enclosed>`, which represents syntax that is not yet used but they may use in the future.
This commit is contained in:
parent
2ed0f880ee
commit
bd648d082c
2 changed files with 279 additions and 6 deletions
|
@ -672,13 +672,283 @@ RefPtr<MediaQuery> Parser::parse_as_media_query()
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
NonnullRefPtr<MediaQuery> Parser::parse_media_query(TokenStream<StyleComponentValueRule>&)
|
||||
NonnullRefPtr<MediaQuery> Parser::parse_media_query(TokenStream<StyleComponentValueRule>& tokens)
|
||||
{
|
||||
// "A media query that does not match the grammar in the previous section must be replaced by `not all`
|
||||
// during parsing." - https://www.w3.org/TR/mediaqueries-5/#error-handling
|
||||
// Returns whether to negate the query
|
||||
auto consume_initial_modifier = [](auto& tokens) -> Optional<bool> {
|
||||
auto& token = tokens.next_token();
|
||||
|
||||
// FIXME: Implement media queries!
|
||||
return MediaQuery::create_not_all();
|
||||
if (!token.is(Token::Type::Ident)) {
|
||||
tokens.reconsume_current_input_token();
|
||||
return {};
|
||||
}
|
||||
|
||||
auto ident = token.token().ident();
|
||||
if (ident.equals_ignoring_case("not")) {
|
||||
return true;
|
||||
} else if (ident.equals_ignoring_case("only")) {
|
||||
return false;
|
||||
}
|
||||
tokens.reconsume_current_input_token();
|
||||
return {};
|
||||
};
|
||||
|
||||
auto invalid_media_query = [&]() {
|
||||
// "A media query that does not match the grammar in the previous section must be replaced by `not all`
|
||||
// during parsing." - https://www.w3.org/TR/mediaqueries-5/#error-handling
|
||||
if constexpr (CSS_PARSER_DEBUG) {
|
||||
dbgln("Invalid media query:");
|
||||
tokens.dump_all_tokens();
|
||||
}
|
||||
return MediaQuery::create_not_all();
|
||||
};
|
||||
|
||||
auto media_query = MediaQuery::create();
|
||||
tokens.skip_whitespace();
|
||||
|
||||
// Only `<media-condition>`
|
||||
if (auto media_condition = consume_media_condition(tokens)) {
|
||||
tokens.skip_whitespace();
|
||||
if (tokens.has_next_token())
|
||||
return invalid_media_query();
|
||||
media_query->m_media_condition = move(media_condition);
|
||||
return media_query;
|
||||
}
|
||||
|
||||
// Optional `"only" | "not"`
|
||||
if (auto modifier = consume_initial_modifier(tokens); modifier.has_value()) {
|
||||
media_query->m_negated = modifier.value();
|
||||
tokens.skip_whitespace();
|
||||
}
|
||||
|
||||
// `<media-type>`
|
||||
if (auto media_type = consume_media_type(tokens); media_type.has_value()) {
|
||||
media_query->m_media_type = media_type.value();
|
||||
tokens.skip_whitespace();
|
||||
} else {
|
||||
return invalid_media_query();
|
||||
}
|
||||
|
||||
if (!tokens.has_next_token())
|
||||
return media_query;
|
||||
|
||||
// Optional "and <media-condition>"
|
||||
if (auto maybe_and = tokens.next_token(); maybe_and.is(Token::Type::Ident) && maybe_and.token().ident().equals_ignoring_case("and")) {
|
||||
if (auto media_condition = consume_media_condition(tokens)) {
|
||||
tokens.skip_whitespace();
|
||||
if (tokens.has_next_token())
|
||||
return invalid_media_query();
|
||||
media_query->m_media_condition = move(media_condition);
|
||||
return media_query;
|
||||
}
|
||||
return invalid_media_query();
|
||||
}
|
||||
|
||||
return invalid_media_query();
|
||||
}
|
||||
|
||||
OwnPtr<MediaQuery::MediaCondition> Parser::consume_media_condition(TokenStream<StyleComponentValueRule>& tokens)
|
||||
{
|
||||
// "not <media-condition>"
|
||||
auto position = tokens.position();
|
||||
auto& first_token = tokens.peek_token();
|
||||
if (first_token.is(Token::Type::Ident) && first_token.token().ident().equals_ignoring_case("not"sv)) {
|
||||
tokens.next_token();
|
||||
|
||||
auto condition = new MediaQuery::MediaCondition;
|
||||
condition->type = MediaQuery::MediaCondition::Type::Not;
|
||||
|
||||
if (auto child_condition = consume_media_condition(tokens)) {
|
||||
condition->conditions.append(child_condition.release_nonnull());
|
||||
return adopt_own(*condition);
|
||||
}
|
||||
|
||||
tokens.rewind_to_position(position);
|
||||
return {};
|
||||
}
|
||||
|
||||
// "<media-condition> ([and | or] <media-condition>)*"
|
||||
NonnullOwnPtrVector<MediaQuery::MediaCondition> child_conditions;
|
||||
Optional<MediaQuery::MediaCondition::Type> condition_type {};
|
||||
auto as_condition_type = [](auto& token) -> Optional<MediaQuery::MediaCondition::Type> {
|
||||
if (!token.is(Token::Type::Ident))
|
||||
return {};
|
||||
auto ident = token.token().ident();
|
||||
if (ident.equals_ignoring_case("and"))
|
||||
return MediaQuery::MediaCondition::Type::And;
|
||||
if (ident.equals_ignoring_case("or"))
|
||||
return MediaQuery::MediaCondition::Type::Or;
|
||||
return {};
|
||||
};
|
||||
|
||||
bool is_invalid = false;
|
||||
tokens.skip_whitespace();
|
||||
while (tokens.has_next_token()) {
|
||||
if (!child_conditions.is_empty()) {
|
||||
// Expect an "and" or "or" here
|
||||
auto maybe_combination = as_condition_type(tokens.next_token());
|
||||
if (!maybe_combination.has_value()) {
|
||||
is_invalid = true;
|
||||
break;
|
||||
}
|
||||
if (!condition_type.has_value()) {
|
||||
condition_type = maybe_combination.value();
|
||||
} else if (maybe_combination != condition_type) {
|
||||
is_invalid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tokens.skip_whitespace();
|
||||
|
||||
if (auto child_feature = consume_media_feature(tokens); child_feature.has_value()) {
|
||||
auto child = new MediaQuery::MediaCondition;
|
||||
child->type = MediaQuery::MediaCondition::Type::Single;
|
||||
child->feature = child_feature.value();
|
||||
child_conditions.append(adopt_own(*child));
|
||||
} else {
|
||||
auto& token = tokens.next_token();
|
||||
if (!token.is_block() || !token.block().is_paren()) {
|
||||
is_invalid = true;
|
||||
break;
|
||||
}
|
||||
auto block_tokens = TokenStream { token.block().values() };
|
||||
if (auto child = consume_media_condition(block_tokens)) {
|
||||
child_conditions.append(child.release_nonnull());
|
||||
} else {
|
||||
is_invalid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tokens.skip_whitespace();
|
||||
}
|
||||
|
||||
if (!is_invalid && !child_conditions.is_empty()) {
|
||||
if (child_conditions.size() == 1)
|
||||
return move(child_conditions.ptr_at(0));
|
||||
|
||||
auto condition = new MediaQuery::MediaCondition;
|
||||
condition->type = condition_type.value();
|
||||
condition->conditions = move(child_conditions);
|
||||
return adopt_own(*condition);
|
||||
}
|
||||
|
||||
// "<media-feature>"
|
||||
tokens.rewind_to_position(position);
|
||||
if (auto feature = consume_media_feature(tokens); feature.has_value()) {
|
||||
auto condition = new MediaQuery::MediaCondition;
|
||||
condition->type = MediaQuery::MediaCondition::Type::Single;
|
||||
condition->feature = feature.value();
|
||||
return adopt_own(*condition);
|
||||
}
|
||||
|
||||
tokens.rewind_to_position(position);
|
||||
return {};
|
||||
}
|
||||
|
||||
Optional<MediaQuery::MediaFeature> Parser::consume_media_feature(TokenStream<StyleComponentValueRule>& outer_tokens)
|
||||
{
|
||||
outer_tokens.skip_whitespace();
|
||||
|
||||
auto invalid_feature = [&]() -> Optional<MediaQuery::MediaFeature> {
|
||||
outer_tokens.reconsume_current_input_token();
|
||||
return {};
|
||||
};
|
||||
|
||||
auto& block_token = outer_tokens.next_token();
|
||||
if (block_token.is_block() && block_token.block().is_paren()) {
|
||||
TokenStream tokens { block_token.block().values() };
|
||||
|
||||
tokens.skip_whitespace();
|
||||
auto& name_token = tokens.next_token();
|
||||
|
||||
// FIXME: Range syntax allows a value to come before the name
|
||||
// https://www.w3.org/TR/mediaqueries-4/#mq-range-context
|
||||
if (!name_token.is(Token::Type::Ident))
|
||||
return invalid_feature();
|
||||
|
||||
auto feature_name = name_token.token().ident();
|
||||
tokens.skip_whitespace();
|
||||
|
||||
if (!tokens.has_next_token()) {
|
||||
return MediaQuery::MediaFeature {
|
||||
.type = MediaQuery::MediaFeature::Type::IsTrue,
|
||||
.name = feature_name,
|
||||
};
|
||||
}
|
||||
|
||||
if (!tokens.next_token().is(Token::Type::Colon))
|
||||
return invalid_feature();
|
||||
tokens.skip_whitespace();
|
||||
|
||||
auto value = parse_css_value(PropertyID::Custom, tokens);
|
||||
if (value.is_error())
|
||||
return invalid_feature();
|
||||
|
||||
if (tokens.has_next_token())
|
||||
return invalid_feature();
|
||||
|
||||
if (feature_name.starts_with("min-", CaseSensitivity::CaseInsensitive)) {
|
||||
return MediaQuery::MediaFeature {
|
||||
.type = MediaQuery::MediaFeature::Type::MinValue,
|
||||
.name = feature_name.substring_view(4),
|
||||
.value = value.release_value(),
|
||||
};
|
||||
} else if (feature_name.starts_with("max-", CaseSensitivity::CaseInsensitive)) {
|
||||
return MediaQuery::MediaFeature {
|
||||
.type = MediaQuery::MediaFeature::Type::MaxValue,
|
||||
.name = feature_name.substring_view(4),
|
||||
.value = value.release_value(),
|
||||
};
|
||||
}
|
||||
|
||||
return MediaQuery::MediaFeature {
|
||||
.type = MediaQuery::MediaFeature::Type::ExactValue,
|
||||
.name = feature_name,
|
||||
.value = value.release_value(),
|
||||
};
|
||||
}
|
||||
|
||||
return invalid_feature();
|
||||
}
|
||||
|
||||
Optional<MediaQuery::MediaType> Parser::consume_media_type(TokenStream<StyleComponentValueRule>& tokens)
|
||||
{
|
||||
auto& token = tokens.next_token();
|
||||
|
||||
if (!token.is(Token::Type::Ident)) {
|
||||
tokens.reconsume_current_input_token();
|
||||
return {};
|
||||
}
|
||||
|
||||
auto ident = token.token().ident();
|
||||
if (ident.equals_ignoring_case("all")) {
|
||||
return MediaQuery::MediaType::All;
|
||||
} else if (ident.equals_ignoring_case("aural")) {
|
||||
return MediaQuery::MediaType::Aural;
|
||||
} else if (ident.equals_ignoring_case("braille")) {
|
||||
return MediaQuery::MediaType::Braille;
|
||||
} else if (ident.equals_ignoring_case("embossed")) {
|
||||
return MediaQuery::MediaType::Embossed;
|
||||
} else if (ident.equals_ignoring_case("handheld")) {
|
||||
return MediaQuery::MediaType::Handheld;
|
||||
} else if (ident.equals_ignoring_case("print")) {
|
||||
return MediaQuery::MediaType::Print;
|
||||
} else if (ident.equals_ignoring_case("projection")) {
|
||||
return MediaQuery::MediaType::Projection;
|
||||
} else if (ident.equals_ignoring_case("screen")) {
|
||||
return MediaQuery::MediaType::Screen;
|
||||
} else if (ident.equals_ignoring_case("speech")) {
|
||||
return MediaQuery::MediaType::Speech;
|
||||
} else if (ident.equals_ignoring_case("tty")) {
|
||||
return MediaQuery::MediaType::TTY;
|
||||
} else if (ident.equals_ignoring_case("tv")) {
|
||||
return MediaQuery::MediaType::TV;
|
||||
}
|
||||
|
||||
tokens.reconsume_current_input_token();
|
||||
return {};
|
||||
}
|
||||
|
||||
NonnullRefPtrVector<StyleRule> Parser::consume_a_list_of_rules(bool top_level)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue