1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-17 08:57:35 +00:00

LibWeb: Add 'Attribute' as a CSS SimpleSelector::Type

Previously, SimpleSelectors optionally had Attribute-selector data
as well as their main type. Now, they're either one or the other,
which better matches the spec, and makes parsing and matching more
straightforward.
This commit is contained in:
Sam Atkins 2021-07-12 14:58:03 +01:00 committed by Andreas Kling
parent dadcb46344
commit 96b2356cbb
5 changed files with 136 additions and 139 deletions

View file

@ -424,29 +424,31 @@ public:
if (!peek() || peek() == '{' || peek() == ',' || is_combinator(peek())) if (!peek() || peek() == '{' || peek() == ',' || is_combinator(peek()))
return {}; return {};
CSS::Selector::SimpleSelector::Type type; CSS::Selector::SimpleSelector simple_selector;
if (peek() == '*') { if (peek() == '*') {
type = CSS::Selector::SimpleSelector::Type::Universal; simple_selector.type = CSS::Selector::SimpleSelector::Type::Universal;
consume_one(); consume_one();
CSS::Selector::SimpleSelector result; return simple_selector;
result.type = type;
return result;
} }
if (peek() == '.') { if (peek() == '.') {
type = CSS::Selector::SimpleSelector::Type::Class; simple_selector.type = CSS::Selector::SimpleSelector::Type::Class;
consume_one(); consume_one();
} else if (peek() == '#') { } else if (peek() == '#') {
type = CSS::Selector::SimpleSelector::Type::Id; simple_selector.type = CSS::Selector::SimpleSelector::Type::Id;
consume_one(); consume_one();
} else if (isalpha(peek())) { } else if (isalpha(peek())) {
type = CSS::Selector::SimpleSelector::Type::TagName; simple_selector.type = CSS::Selector::SimpleSelector::Type::TagName;
} else if (peek() == '[') {
simple_selector.type = CSS::Selector::SimpleSelector::Type::Attribute;
} else { } else {
type = CSS::Selector::SimpleSelector::Type::Universal; simple_selector.type = CSS::Selector::SimpleSelector::Type::Universal;
} }
if (type != CSS::Selector::SimpleSelector::Type::Universal) { if ((simple_selector.type != CSS::Selector::SimpleSelector::Type::Universal)
&& (simple_selector.type != CSS::Selector::SimpleSelector::Type::Attribute)) {
while (is_valid_selector_char(peek())) while (is_valid_selector_char(peek()))
buffer.append(consume_one()); buffer.append(consume_one());
PARSE_VERIFY(!buffer.is_empty()); PARSE_VERIFY(!buffer.is_empty());
@ -454,18 +456,16 @@ public:
auto value = String::copy(buffer); auto value = String::copy(buffer);
if (type == CSS::Selector::SimpleSelector::Type::TagName) { if (simple_selector.type == CSS::Selector::SimpleSelector::Type::TagName) {
// Some stylesheets use uppercase tag names, so here's a hack to just lowercase them internally. // Some stylesheets use uppercase tag names, so here's a hack to just lowercase them internally.
value = value.to_lowercase(); value = value.to_lowercase();
} }
CSS::Selector::SimpleSelector simple_selector;
simple_selector.type = type;
simple_selector.value = value; simple_selector.value = value;
buffer.clear(); buffer.clear();
if (peek() == '[') { if (simple_selector.type == CSS::Selector::SimpleSelector::Type::Attribute) {
CSS::Selector::SimpleSelector::AttributeMatchType attribute_match_type = CSS::Selector::SimpleSelector::AttributeMatchType::HasAttribute; CSS::Selector::SimpleSelector::Attribute::MatchType attribute_match_type = CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute;
String attribute_name; String attribute_name;
String attribute_value; String attribute_value;
bool in_value = false; bool in_value = false;
@ -475,10 +475,10 @@ public:
char ch = consume_one(); char ch = consume_one();
if (ch == '=' || (ch == '~' && peek() == '=')) { if (ch == '=' || (ch == '~' && peek() == '=')) {
if (ch == '=') { if (ch == '=') {
attribute_match_type = CSS::Selector::SimpleSelector::AttributeMatchType::ExactValueMatch; attribute_match_type = CSS::Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch;
} else if (ch == '~') { } else if (ch == '~') {
consume_one(); consume_one();
attribute_match_type = CSS::Selector::SimpleSelector::AttributeMatchType::ContainsWord; attribute_match_type = CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsWord;
} }
attribute_name = String::copy(buffer); attribute_name = String::copy(buffer);
buffer.clear(); buffer.clear();
@ -503,9 +503,9 @@ public:
else else
attribute_name = String::copy(buffer); attribute_name = String::copy(buffer);
buffer.clear(); buffer.clear();
simple_selector.attribute_match_type = attribute_match_type; simple_selector.attribute.match_type = attribute_match_type;
simple_selector.attribute_name = attribute_name; simple_selector.attribute.name = attribute_name;
simple_selector.attribute_value = attribute_value; simple_selector.attribute.value = attribute_value;
if (expected_end_of_attribute_selector != ']') { if (expected_end_of_attribute_selector != ']') {
if (!consume_specific(expected_end_of_attribute_selector)) if (!consume_specific(expected_end_of_attribute_selector))
return {}; return {};

View file

@ -243,26 +243,18 @@ Optional<Selector> Parser::parse_single_selector(TokenStream<T>& tokens, bool is
if (check_for_eof_or_whitespace(current_value)) if (check_for_eof_or_whitespace(current_value))
return {}; return {};
Selector::SimpleSelector::Type type; Selector::SimpleSelector simple_selector;
String value;
// FIXME: Handle namespace prefixes. // FIXME: Handle namespace prefixes.
if (current_value.is(Token::Type::Delim) && ((Token)current_value).delim() == "*") { if (current_value.is(Token::Type::Delim) && ((Token)current_value).delim() == "*") {
simple_selector.type = Selector::SimpleSelector::Type::Universal;
// FIXME: Handle selectors like `*.foo`. } else if (current_value.is(Token::Type::Hash)) {
type = Selector::SimpleSelector::Type::Universal;
Selector::SimpleSelector result;
result.type = type;
return result;
}
if (current_value.is(Token::Type::Hash)) {
if (((Token)current_value).m_hash_type != Token::HashType::Id) { if (((Token)current_value).m_hash_type != Token::HashType::Id) {
dbgln("Selector contains hash token that is not an id: {}", current_value.to_debug_string()); dbgln("Selector contains hash token that is not an id: {}", current_value.to_debug_string());
return {}; return {};
} }
type = Selector::SimpleSelector::Type::Id; simple_selector.type = Selector::SimpleSelector::Type::Id;
value = ((Token)current_value).m_value.to_string(); simple_selector.value = ((Token)current_value).m_value.to_string();
} else if (current_value.is(Token::Type::Delim) && ((Token)current_value).delim() == ".") { } else if (current_value.is(Token::Type::Delim) && ((Token)current_value).delim() == ".") {
current_value = tokens.next_token(); current_value = tokens.next_token();
if (check_for_eof_or_whitespace(current_value)) if (check_for_eof_or_whitespace(current_value))
@ -273,33 +265,19 @@ Optional<Selector> Parser::parse_single_selector(TokenStream<T>& tokens, bool is
return {}; return {};
} }
type = Selector::SimpleSelector::Type::Class; simple_selector.type = Selector::SimpleSelector::Type::Class;
value = current_value.token().ident().to_lowercase_string(); simple_selector.value = current_value.token().ident().to_lowercase_string();
} else if (current_value.is(Token::Type::Delim) && current_value.token().delim() == "*") {
type = Selector::SimpleSelector::Type::Universal;
} else if (current_value.is(Token::Type::Ident)) { } else if (current_value.is(Token::Type::Ident)) {
type = Selector::SimpleSelector::Type::TagName; simple_selector.type = Selector::SimpleSelector::Type::TagName;
value = current_value.token().ident().to_lowercase_string(); simple_selector.value = current_value.token().ident().to_lowercase_string();
} else if ((current_value.is(Token::Type::Delim) && current_value.token().delim() == ":") } else if ((current_value.is(Token::Type::Delim) && current_value.token().delim() == ":")) {
|| (current_value.is_block() && current_value.block().is_square())) {
// FIXME: This is a temporary hack until we make the Selector::SimpleSelector::Type changes. // FIXME: This is a temporary hack until we make the Selector::SimpleSelector::Type changes.
type = Selector::SimpleSelector::Type::Universal; simple_selector.type = Selector::SimpleSelector::Type::Universal;
tokens.reconsume_current_input_token(); tokens.reconsume_current_input_token();
} else { } else if (current_value.is_block() && current_value.block().is_square()) {
dbgln("Invalid simple selector!"); simple_selector.type = Selector::SimpleSelector::Type::Attribute;
return {};
}
Selector::SimpleSelector simple_selector; auto& attribute = simple_selector.attribute;
simple_selector.type = type;
simple_selector.value = value;
current_value = tokens.next_token();
if (check_for_eof_or_whitespace(current_value))
return simple_selector;
// FIXME: Attribute selectors want to be their own Selector::SimpleSelector::Type according to the spec.
if (current_value.is_block() && current_value.block().is_square()) {
Vector<StyleComponentValueRule> const& attribute_parts = current_value.block().values(); Vector<StyleComponentValueRule> const& attribute_parts = current_value.block().values();
@ -315,8 +293,8 @@ Optional<Selector> Parser::parse_single_selector(TokenStream<T>& tokens, bool is
return {}; return {};
} }
simple_selector.attribute_match_type = Selector::SimpleSelector::AttributeMatchType::HasAttribute; attribute.match_type = Selector::SimpleSelector::Attribute::MatchType::HasAttribute;
simple_selector.attribute_name = attribute_part.token().ident(); attribute.name = attribute_part.token().ident();
if (attribute_parts.size() == 1) if (attribute_parts.size() == 1)
return simple_selector; return simple_selector;
@ -329,7 +307,7 @@ Optional<Selector> Parser::parse_single_selector(TokenStream<T>& tokens, bool is
} }
if (delim_part.token().delim() == "=") { if (delim_part.token().delim() == "=") {
simple_selector.attribute_match_type = Selector::SimpleSelector::AttributeMatchType::ExactValueMatch; attribute.match_type = Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch;
attribute_index++; attribute_index++;
} else { } else {
attribute_index++; attribute_index++;
@ -345,19 +323,19 @@ Optional<Selector> Parser::parse_single_selector(TokenStream<T>& tokens, bool is
} }
if (delim_part.token().delim() == "~") { if (delim_part.token().delim() == "~") {
simple_selector.attribute_match_type = Selector::SimpleSelector::AttributeMatchType::ContainsWord; attribute.match_type = Selector::SimpleSelector::Attribute::MatchType::ContainsWord;
attribute_index++; attribute_index++;
} else if (delim_part.token().delim() == "*") { } else if (delim_part.token().delim() == "*") {
simple_selector.attribute_match_type = Selector::SimpleSelector::AttributeMatchType::ContainsString; attribute.match_type = Selector::SimpleSelector::Attribute::MatchType::ContainsString;
attribute_index++; attribute_index++;
} else if (delim_part.token().delim() == "|") { } else if (delim_part.token().delim() == "|") {
simple_selector.attribute_match_type = Selector::SimpleSelector::AttributeMatchType::StartsWithSegment; attribute.match_type = Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment;
attribute_index++; attribute_index++;
} else if (delim_part.token().delim() == "^") { } else if (delim_part.token().delim() == "^") {
simple_selector.attribute_match_type = Selector::SimpleSelector::AttributeMatchType::StartsWithString; attribute.match_type = Selector::SimpleSelector::Attribute::MatchType::StartsWithString;
attribute_index++; attribute_index++;
} else if (delim_part.token().delim() == "$") { } else if (delim_part.token().delim() == "$") {
simple_selector.attribute_match_type = Selector::SimpleSelector::AttributeMatchType::EndsWithString; attribute.match_type = Selector::SimpleSelector::Attribute::MatchType::EndsWithString;
attribute_index++; attribute_index++;
} }
} }
@ -372,12 +350,18 @@ Optional<Selector> Parser::parse_single_selector(TokenStream<T>& tokens, bool is
dbgln("Expected a string or ident for the value to match attribute against, got: '{}'", value_part.to_debug_string()); dbgln("Expected a string or ident for the value to match attribute against, got: '{}'", value_part.to_debug_string());
return {}; return {};
} }
simple_selector.attribute_value = value_part.token().is(Token::Type::Ident) ? value_part.token().ident() : value_part.token().string(); attribute.value = value_part.token().is(Token::Type::Ident) ? value_part.token().ident() : value_part.token().string();
// FIXME: Handle case-sensitivity suffixes. https://www.w3.org/TR/selectors-4/#attribute-case // FIXME: Handle case-sensitivity suffixes. https://www.w3.org/TR/selectors-4/#attribute-case
return simple_selector; } else {
dbgln("Invalid simple selector!");
return {};
} }
current_value = tokens.next_token();
if (check_for_eof_or_whitespace(current_value))
return simple_selector;
// FIXME: Pseudo-class selectors want to be their own Selector::SimpleSelector::Type according to the spec. // FIXME: Pseudo-class selectors want to be their own Selector::SimpleSelector::Type according to the spec.
if (current_value.is(Token::Type::Colon)) { if (current_value.is(Token::Type::Colon)) {
bool is_pseudo = false; bool is_pseudo = false;

View file

@ -1,5 +1,6 @@
/* /*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Sam Atkins <atkinssj@gmail.com>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -21,6 +22,7 @@ public:
TagName, TagName,
Id, Id,
Class, Class,
Attribute,
}; };
Type type { Type::Invalid }; Type type { Type::Invalid };
@ -56,20 +58,22 @@ public:
FlyString value; FlyString value;
enum class AttributeMatchType { struct Attribute {
None, enum class MatchType {
HasAttribute, None,
ExactValueMatch, HasAttribute,
ContainsWord, // [att~=val] ExactValueMatch,
ContainsString, // [att*=val] ContainsWord, // [att~=val]
StartsWithSegment, // [att|=val] ContainsString, // [att*=val]
StartsWithString, // [att^=val] StartsWithSegment, // [att|=val]
EndsWithString, // [att$=val] StartsWithString, // [att^=val]
EndsWithString, // [att$=val]
};
MatchType match_type { MatchType::None };
FlyString name;
String value;
}; };
Attribute attribute;
AttributeMatchType attribute_match_type { AttributeMatchType::None };
FlyString attribute_name;
String attribute_value;
struct NthChildPattern { struct NthChildPattern {
int step_size = 0; int step_size = 0;

View file

@ -1,5 +1,6 @@
/* /*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Sam Atkins <atkinssj@gmail.com>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -24,6 +25,38 @@ static bool matches_hover_pseudo_class(DOM::Element const& element)
return element.is_ancestor_of(*hovered_node); return element.is_ancestor_of(*hovered_node);
} }
static bool matches_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute, DOM::Element const& element)
{
switch (attribute.match_type) {
case CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute:
return element.has_attribute(attribute.name);
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch:
return element.attribute(attribute.name) == attribute.value;
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsWord:
return element.attribute(attribute.name).split(' ').contains_slow(attribute.value);
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsString:
return element.attribute(attribute.name).contains(attribute.value);
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment:
return element.attribute(attribute.name).split('-').first() == attribute.value;
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithString:
return element.attribute(attribute.name).starts_with(attribute.value);
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::EndsWithString:
return element.attribute(attribute.name).ends_with(attribute.value);
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::None:
VERIFY_NOT_REACHED();
break;
}
return false;
}
static bool matches(CSS::Selector::SimpleSelector const& component, DOM::Element const& element) static bool matches(CSS::Selector::SimpleSelector const& component, DOM::Element const& element)
{ {
switch (component.pseudo_element) { switch (component.pseudo_element) {
@ -171,39 +204,6 @@ static bool matches(CSS::Selector::SimpleSelector const& component, DOM::Element
break; break;
} }
switch (component.attribute_match_type) {
case CSS::Selector::SimpleSelector::AttributeMatchType::HasAttribute:
if (!element.has_attribute(component.attribute_name))
return false;
break;
case CSS::Selector::SimpleSelector::AttributeMatchType::ExactValueMatch:
if (element.attribute(component.attribute_name) != component.attribute_value)
return false;
break;
case CSS::Selector::SimpleSelector::AttributeMatchType::ContainsWord:
if (!element.attribute(component.attribute_name).split(' ').contains_slow(component.attribute_value))
return false;
break;
case CSS::Selector::SimpleSelector::AttributeMatchType::ContainsString:
if (!element.attribute(component.attribute_name).contains(component.attribute_value))
return false;
break;
case CSS::Selector::SimpleSelector::AttributeMatchType::StartsWithSegment:
if (element.attribute(component.attribute_name).split('-').first() != component.attribute_value)
return false;
break;
case CSS::Selector::SimpleSelector::AttributeMatchType::StartsWithString:
if (!element.attribute(component.attribute_name).starts_with(component.attribute_value))
return false;
break;
case CSS::Selector::SimpleSelector::AttributeMatchType::EndsWithString:
if (!element.attribute(component.attribute_name).ends_with(component.attribute_value))
return false;
break;
default:
break;
}
switch (component.type) { switch (component.type) {
case CSS::Selector::SimpleSelector::Type::Universal: case CSS::Selector::SimpleSelector::Type::Universal:
return true; return true;
@ -213,6 +213,8 @@ static bool matches(CSS::Selector::SimpleSelector const& component, DOM::Element
return element.has_class(component.value); return element.has_class(component.value);
case CSS::Selector::SimpleSelector::Type::TagName: case CSS::Selector::SimpleSelector::Type::TagName:
return component.value == element.local_name(); return component.value == element.local_name();
case CSS::Selector::SimpleSelector::Type::Attribute:
return matches_attribute(component.attribute, element);
default: default:
VERIFY_NOT_REACHED(); VERIFY_NOT_REACHED();
} }

View file

@ -314,31 +314,8 @@ void dump_selector(StringBuilder& builder, CSS::Selector const& selector)
case CSS::Selector::SimpleSelector::Type::TagName: case CSS::Selector::SimpleSelector::Type::TagName:
type_description = "TagName"; type_description = "TagName";
break; break;
} case CSS::Selector::SimpleSelector::Type::Attribute:
const char* attribute_match_type_description = ""; type_description = "Attribute";
switch (simple_selector.attribute_match_type) {
case CSS::Selector::SimpleSelector::AttributeMatchType::None:
break;
case CSS::Selector::SimpleSelector::AttributeMatchType::HasAttribute:
attribute_match_type_description = "HasAttribute";
break;
case CSS::Selector::SimpleSelector::AttributeMatchType::ExactValueMatch:
attribute_match_type_description = "ExactValueMatch";
break;
case CSS::Selector::SimpleSelector::AttributeMatchType::ContainsWord:
attribute_match_type_description = "ContainsWord";
break;
case CSS::Selector::SimpleSelector::AttributeMatchType::ContainsString:
attribute_match_type_description = "ContainsString";
break;
case CSS::Selector::SimpleSelector::AttributeMatchType::StartsWithSegment:
attribute_match_type_description = "StartsWithSegment";
break;
case CSS::Selector::SimpleSelector::AttributeMatchType::StartsWithString:
attribute_match_type_description = "StartsWithString";
break;
case CSS::Selector::SimpleSelector::AttributeMatchType::EndsWithString:
attribute_match_type_description = "EndsWithString";
break; break;
} }
@ -406,8 +383,38 @@ void dump_selector(StringBuilder& builder, CSS::Selector const& selector)
builder.appendff("{}:{}", type_description, simple_selector.value); builder.appendff("{}:{}", type_description, simple_selector.value);
if (simple_selector.pseudo_class != CSS::Selector::SimpleSelector::PseudoClass::None) if (simple_selector.pseudo_class != CSS::Selector::SimpleSelector::PseudoClass::None)
builder.appendff(" pseudo_class={}", pseudo_class_description); builder.appendff(" pseudo_class={}", pseudo_class_description);
if (simple_selector.attribute_match_type != CSS::Selector::SimpleSelector::AttributeMatchType::None) {
builder.appendff(" [{}, name='{}', value='{}']", attribute_match_type_description, simple_selector.attribute_name, simple_selector.attribute_value); if (simple_selector.type == CSS::Selector::SimpleSelector::Type::Attribute) {
char const* attribute_match_type_description = "";
switch (simple_selector.attribute.match_type) {
case CSS::Selector::SimpleSelector::Attribute::MatchType::None:
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute:
type_description = "HasAttribute";
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch:
type_description = "ExactValueMatch";
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsWord:
type_description = "ContainsWord";
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsString:
type_description = "ContainsString";
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment:
type_description = "StartsWithSegment";
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithString:
type_description = "StartsWithString";
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::EndsWithString:
type_description = "EndsWithString";
break;
}
break;
builder.appendff(" [{}, name='{}', value='{}']", attribute_match_type_description, simple_selector.attribute.name, simple_selector.attribute.value);
} }
if (i != complex_selector.compound_selector.size() - 1) if (i != complex_selector.compound_selector.size() - 1)