diff --git a/Libraries/LibWeb/CSS/SelectorEngine.cpp b/Libraries/LibWeb/CSS/SelectorEngine.cpp index efe08cfb15a..a694f8c1a85 100644 --- a/Libraries/LibWeb/CSS/SelectorEngine.cpp +++ b/Libraries/LibWeb/CSS/SelectorEngine.cpp @@ -247,52 +247,127 @@ static inline bool matches_indeterminate_pseudo_class(DOM::Element const& elemen return false; } -static inline Web::DOM::Attr const* get_optionally_namespaced_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute, GC::Ptr style_sheet_for_rule, DOM::Element const& element) +static inline void for_each_matching_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute_selector, GC::Ptr style_sheet_for_rule, DOM::Element const& element, Function const& process_attribute) { - auto const& qualified_name = attribute.qualified_name; + auto const& qualified_name = attribute_selector.qualified_name; auto const& attribute_name = qualified_name.name.name; - auto const& namespace_type = qualified_name.namespace_type; - if (element.namespace_uri() == Namespace::HTML) { - if (namespace_type == CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Named) { - return nullptr; - } - return element.attributes()->get_attribute(attribute_name); - } - - switch (namespace_type) { + switch (qualified_name.namespace_type) { // "In keeping with the Namespaces in the XML recommendation, default namespaces do not apply to attributes, // therefore attribute selectors without a namespace component apply only to attributes that have no namespace (equivalent to "|attr")" case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Default: case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::None: - return element.attributes()->get_attribute(attribute_name); - case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Any: - return element.attributes()->get_attribute_namespace_agnostic(attribute_name); + if (auto const* attribute = element.attributes()->get_attribute(attribute_name)) + (void)process_attribute(*attribute); + return; + case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Any: { + // When comparing the name part of a CSS attribute selector to the names of attributes on HTML elements in HTML + // documents, the name part of the CSS attribute selector must first be converted to ASCII lowercase. The same + // selector when compared to other attributes must be compared according to its original case. In both cases, the + // comparison is case-sensitive. + // https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors + bool const case_insensitive = element.document().is_html_document() && element.namespace_uri() == Namespace::HTML; + + for (auto i = 0u; i < element.attributes()->length(); ++i) { + auto const* attr = element.attributes()->item(i); + bool matches = case_insensitive + ? Infra::is_ascii_case_insensitive_match(attr->local_name(), attribute_name) + : attr->local_name() == attribute_name; + if (matches) { + if (process_attribute(*attr) == IterationDecision::Break) + break; + } + } + return; + } case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Named: if (!style_sheet_for_rule) - return nullptr; + return; auto const& selector_namespace = style_sheet_for_rule->namespace_uri(qualified_name.namespace_); if (!selector_namespace.has_value()) - return nullptr; - return element.attributes()->get_attribute_ns(selector_namespace, attribute_name); + return; + + if (auto const* attribute = element.attributes()->get_attribute_ns(selector_namespace, attribute_name)) + (void)process_attribute(*attribute); + return; } VERIFY_NOT_REACHED(); } +static bool matches_single_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute_selector, DOM::Attr const& attribute, CaseSensitivity case_sensitivity) +{ + auto const case_insensitive_match = case_sensitivity == CaseSensitivity::CaseInsensitive; + + switch (attribute_selector.match_type) { + case CSS::Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch: + return case_insensitive_match + ? Infra::is_ascii_case_insensitive_match(attribute.value(), attribute_selector.value) + : attribute.value() == attribute_selector.value; + case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsWord: { + if (attribute_selector.value.is_empty()) { + // This selector is always false is match value is empty. + return false; + } + auto const& attribute_value = attribute.value(); + auto const view = attribute_value.bytes_as_string_view().split_view(' '); + auto const size = view.size(); + for (size_t i = 0; i < size; ++i) { + auto const value = view.at(i); + if (case_insensitive_match + ? Infra::is_ascii_case_insensitive_match(value, attribute_selector.value) + : value == attribute_selector.value) { + return true; + } + } + return false; + } + case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsString: + return !attribute_selector.value.is_empty() + && attribute.value().contains(attribute_selector.value, case_sensitivity); + case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment: { + // https://www.w3.org/TR/CSS2/selector.html#attribute-selectors + // [att|=val] + // Represents an element with the att attribute, its value either being exactly "val" or beginning with "val" immediately followed by "-" (U+002D). + + auto const& element_attr_value = attribute.value(); + if (element_attr_value.is_empty()) { + // If the attribute value on element is empty, the selector is true + // if the match value is also empty and false otherwise. + return attribute_selector.value.is_empty(); + } + if (attribute_selector.value.is_empty()) { + return false; + } + + auto element_attribute_length = element_attr_value.bytes_as_string_view().length(); + auto attribute_length = attribute_selector.value.bytes_as_string_view().length(); + if (element_attribute_length < attribute_length) + return false; + + if (attribute_length == element_attribute_length) { + return case_insensitive_match + ? Infra::is_ascii_case_insensitive_match(element_attr_value, attribute_selector.value) + : element_attr_value == attribute_selector.value; + } + + return element_attr_value.starts_with_bytes(attribute_selector.value, case_insensitive_match ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive) && element_attr_value.bytes_as_string_view()[attribute_length] == '-'; + } + case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithString: + return !attribute_selector.value.is_empty() + && attribute.value().bytes_as_string_view().starts_with(attribute_selector.value, case_sensitivity); + case CSS::Selector::SimpleSelector::Attribute::MatchType::EndsWithString: + return !attribute_selector.value.is_empty() + && attribute.value().bytes_as_string_view().ends_with(attribute_selector.value, case_sensitivity); + case CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute: + return true; + } + return false; +} + static inline bool matches_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute, [[maybe_unused]] GC::Ptr style_sheet_for_rule, DOM::Element const& element) { auto const& attribute_name = attribute.qualified_name.name.name; - auto const* attr = get_optionally_namespaced_attribute(attribute, style_sheet_for_rule, element); - - if (attribute.match_type == CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute) { - // Early way out in case of an attribute existence selector. - return attr != nullptr; - } - - if (!attr) - return false; - auto case_sensitivity = [&](CSS::Selector::SimpleSelector::Attribute::CaseType case_type) { switch (case_type) { case CSS::Selector::SimpleSelector::Attribute::CaseType::CaseInsensitiveMatch: @@ -324,73 +399,17 @@ static inline bool matches_attribute(CSS::Selector::SimpleSelector::Attribute co } VERIFY_NOT_REACHED(); }(attribute.case_type); - auto case_insensitive_match = case_sensitivity == CaseSensitivity::CaseInsensitive; - switch (attribute.match_type) { - case CSS::Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch: - return case_insensitive_match - ? Infra::is_ascii_case_insensitive_match(attr->value(), attribute.value) - : attr->value() == attribute.value; - case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsWord: { - if (attribute.value.is_empty()) { - // This selector is always false is match value is empty. - return false; + bool found_matching_attribute = false; + for_each_matching_attribute(attribute, style_sheet_for_rule, element, [&attribute, case_sensitivity, &found_matching_attribute](DOM::Attr const& attr) { + if (matches_single_attribute(attribute, attr, case_sensitivity)) { + found_matching_attribute = true; + return IterationDecision::Break; } - auto const& attribute_value = attr->value(); - auto const view = attribute_value.bytes_as_string_view().split_view(' '); - auto const size = view.size(); - for (size_t i = 0; i < size; ++i) { - auto const value = view.at(i); - if (case_insensitive_match - ? Infra::is_ascii_case_insensitive_match(value, attribute.value) - : value == attribute.value) { - return true; - } - } - return false; - } - case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsString: - return !attribute.value.is_empty() - && attr->value().contains(attribute.value, case_sensitivity); - case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment: { - // https://www.w3.org/TR/CSS2/selector.html#attribute-selectors - // [att|=val] - // Represents an element with the att attribute, its value either being exactly "val" or beginning with "val" immediately followed by "-" (U+002D). + return IterationDecision::Continue; + }); - auto const& element_attr_value = attr->value(); - if (element_attr_value.is_empty()) { - // If the attribute value on element is empty, the selector is true - // if the match value is also empty and false otherwise. - return attribute.value.is_empty(); - } - if (attribute.value.is_empty()) { - return false; - } - - auto element_attribute_length = element_attr_value.bytes_as_string_view().length(); - auto attribute_length = attribute.value.bytes_as_string_view().length(); - if (element_attribute_length < attribute_length) - return false; - - if (attribute_length == element_attribute_length) { - return case_insensitive_match - ? Infra::is_ascii_case_insensitive_match(element_attr_value, attribute.value) - : element_attr_value == attribute.value; - } - - return element_attr_value.starts_with_bytes(attribute.value, case_insensitive_match ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive) && element_attr_value.bytes_as_string_view()[attribute_length] == '-'; - } - case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithString: - return !attribute.value.is_empty() - && attr->value().bytes_as_string_view().starts_with(attribute.value, case_sensitivity); - case CSS::Selector::SimpleSelector::Attribute::MatchType::EndsWithString: - return !attribute.value.is_empty() - && attr->value().bytes_as_string_view().ends_with(attribute.value, case_sensitivity); - default: - break; - } - - return false; + return found_matching_attribute; } static inline DOM::Element const* previous_sibling_with_same_tag_name(DOM::Element const& element) diff --git a/Libraries/LibWeb/DOM/NamedNodeMap.cpp b/Libraries/LibWeb/DOM/NamedNodeMap.cpp index 6dba3b0419d..71e03a45e15 100644 --- a/Libraries/LibWeb/DOM/NamedNodeMap.cpp +++ b/Libraries/LibWeb/DOM/NamedNodeMap.cpp @@ -194,16 +194,6 @@ Attr const* NamedNodeMap::get_attribute_ns(Optional const& namespace_ return nullptr; } -Attr const* NamedNodeMap::get_attribute_namespace_agnostic(FlyString const& local_name) const -{ - for (auto const& attribute : m_attributes) { - if (attribute->local_name() == local_name) - return attribute.ptr(); - } - - return nullptr; -} - // https://dom.spec.whatwg.org/#concept-element-attributes-set WebIDL::ExceptionOr> NamedNodeMap::set_attribute(Attr& attribute) { diff --git a/Libraries/LibWeb/DOM/NamedNodeMap.h b/Libraries/LibWeb/DOM/NamedNodeMap.h index 0c29df87625..bf59c3a13db 100644 --- a/Libraries/LibWeb/DOM/NamedNodeMap.h +++ b/Libraries/LibWeb/DOM/NamedNodeMap.h @@ -53,8 +53,6 @@ public: Attr const* remove_attribute(FlyString const& qualified_name); Attr const* remove_attribute_ns(Optional const& namespace_, FlyString const& local_name); - Attr const* get_attribute_namespace_agnostic(FlyString const& local_name) const; - WebIDL::ExceptionOr> remove_attribute_node(GC::Ref); private: diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/selectors/attribute-selectors/attribute-case/semantics.txt b/Tests/LibWeb/Text/expected/wpt-import/css/selectors/attribute-selectors/attribute-case/semantics.txt index e8148608463..d5c407a6d7e 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/selectors/attribute-selectors/attribute-case/semantics.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/selectors/attribute-selectors/attribute-case/semantics.txt @@ -2,8 +2,7 @@ Harness status: OK Found 955 tests -934 Pass -21 Fail +955 Pass Pass [foo='BAR'] /* sanity check (match) */
in standards mode Pass [foo='BAR'] /* sanity check (match) */
with querySelector in standards mode Pass [foo='bar'] /* sanity check (match) */
in standards mode @@ -22,7 +21,7 @@ Pass [lang|='a'] /* sanity check (match) */
in standards mode Pass [lang|='a'] /* sanity check (match) */
with querySelector in standards mode Pass [lang*='A'] /* sanity check (match) */
in standards mode Pass [lang*='A'] /* sanity check (match) */
with querySelector in standards mode -Fail @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A'] /* sanity check (match) */
in standards mode +Pass @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A'] /* sanity check (match) */
in standards mode Pass [foo='bar' i]
in standards mode Pass [foo='bar' i]
with querySelector in standards mode Pass [foo='' i]
in standards mode @@ -31,8 +30,8 @@ Pass [foo='ä' i] /* COMBINING in both */
in standards mode Pass [foo='ä' i] /* COMBINING in both */
with querySelector in standards mode Pass [foo='Ä' i] /* COMBINING in both */
in standards mode Pass [foo='Ä' i] /* COMBINING in both */
with querySelector in standards mode -Fail [*|foo='bar' i]
in standards mode -Fail [*|foo='bar' i]
with querySelector in standards mode +Pass [*|foo='bar' i]
in standards mode +Pass [*|foo='bar' i]
with querySelector in standards mode Pass [*|foo='bar' i]
in standards mode Pass [*|foo='bar' i]
with querySelector in standards mode Pass [align='left' i]
in standards mode @@ -55,7 +54,7 @@ Pass [*|lang='a' i]
in stan Pass [*|lang='a' i]
with querySelector in standards mode Pass [*|lang='A' i]
in standards mode Pass [*|lang='A' i]
with querySelector in standards mode -Fail @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' i]
in standards mode +Pass @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' i]
in standards mode Pass [foo='bar' i][foo='bar' i]
in standards mode Pass [foo='bar' i][foo='bar' i]
with querySelector in standards mode Pass [foo='BAR'][foo='bar' i]
in standards mode @@ -68,8 +67,8 @@ Pass [foo='' s]
in standards mode Pass [foo='' s]
with querySelector in standards mode Pass [foo='ä' s] /* COMBINING in both */
in standards mode Pass [foo='ä' s] /* COMBINING in both */
with querySelector in standards mode -Fail [*|foo='bar' s]
in standards mode -Fail [*|foo='bar' s]
with querySelector in standards mode +Pass [*|foo='bar' s]
in standards mode +Pass [*|foo='bar' s]
with querySelector in standards mode Pass [*|foo='bar' s]
in standards mode Pass [*|foo='bar' s]
with querySelector in standards mode Pass [align='left' s]
in standards mode @@ -92,7 +91,7 @@ Pass [*|lang='a' s]
in stan Pass [*|lang='a' s]
with querySelector in standards mode Pass [*|lang='A' s]
in standards mode Pass [*|lang='A' s]
with querySelector in standards mode -Fail @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in standards mode +Pass @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in standards mode Pass [foo='BAR' s][foo='BAR' s]
in standards mode Pass [foo='BAR' s][foo='BAR' s]
with querySelector in standards mode Pass [align='left'] /* sanity check (match HTML) */
in standards mode @@ -343,7 +342,7 @@ Pass [lang|='a'] /* sanity check (match) */
in quirks mode Pass [lang|='a'] /* sanity check (match) */
with querySelector in quirks mode Pass [lang*='A'] /* sanity check (match) */
in quirks mode Pass [lang*='A'] /* sanity check (match) */
with querySelector in quirks mode -Fail @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A'] /* sanity check (match) */
in quirks mode +Pass @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A'] /* sanity check (match) */
in quirks mode Pass [foo='bar' i]
in quirks mode Pass [foo='bar' i]
with querySelector in quirks mode Pass [foo='' i]
in quirks mode @@ -352,8 +351,8 @@ Pass [foo='ä' i] /* COMBINING in both */
in quirks mode Pass [foo='ä' i] /* COMBINING in both */
with querySelector in quirks mode Pass [foo='Ä' i] /* COMBINING in both */
in quirks mode Pass [foo='Ä' i] /* COMBINING in both */
with querySelector in quirks mode -Fail [*|foo='bar' i]
in quirks mode -Fail [*|foo='bar' i]
with querySelector in quirks mode +Pass [*|foo='bar' i]
in quirks mode +Pass [*|foo='bar' i]
with querySelector in quirks mode Pass [*|foo='bar' i]
in quirks mode Pass [*|foo='bar' i]
with querySelector in quirks mode Pass [align='left' i]
in quirks mode @@ -376,7 +375,7 @@ Pass [*|lang='a' i]
in quir Pass [*|lang='a' i]
with querySelector in quirks mode Pass [*|lang='A' i]
in quirks mode Pass [*|lang='A' i]
with querySelector in quirks mode -Fail @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' i]
in quirks mode +Pass @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' i]
in quirks mode Pass [foo='bar' i][foo='bar' i]
in quirks mode Pass [foo='bar' i][foo='bar' i]
with querySelector in quirks mode Pass [foo='BAR'][foo='bar' i]
in quirks mode @@ -389,8 +388,8 @@ Pass [foo='' s]
in quirks mode Pass [foo='' s]
with querySelector in quirks mode Pass [foo='ä' s] /* COMBINING in both */
in quirks mode Pass [foo='ä' s] /* COMBINING in both */
with querySelector in quirks mode -Fail [*|foo='bar' s]
in quirks mode -Fail [*|foo='bar' s]
with querySelector in quirks mode +Pass [*|foo='bar' s]
in quirks mode +Pass [*|foo='bar' s]
with querySelector in quirks mode Pass [*|foo='bar' s]
in quirks mode Pass [*|foo='bar' s]
with querySelector in quirks mode Pass [align='left' s]
in quirks mode @@ -413,7 +412,7 @@ Pass [*|lang='a' s]
in quir Pass [*|lang='a' s]
with querySelector in quirks mode Pass [*|lang='A' s]
in quirks mode Pass [*|lang='A' s]
with querySelector in quirks mode -Fail @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in quirks mode +Pass @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in quirks mode Pass [foo='BAR' s][foo='BAR' s]
in quirks mode Pass [foo='BAR' s][foo='BAR' s]
with querySelector in quirks mode Pass [align='left'] /* sanity check (match HTML) */
in quirks mode @@ -664,7 +663,7 @@ Pass [lang|='a'] /* sanity check (match) */
in XML Pass [lang|='a'] /* sanity check (match) */
with querySelector in XML Pass [lang*='A'] /* sanity check (match) */
in XML Pass [lang*='A'] /* sanity check (match) */
with querySelector in XML -Fail @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A'] /* sanity check (match) */
in XML +Pass @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A'] /* sanity check (match) */
in XML Pass [foo='bar' i]
in XML Pass [foo='bar' i]
with querySelector in XML Pass [foo='' i]
in XML @@ -673,8 +672,8 @@ Pass [foo='ä' i] /* COMBINING in both */
in XML Pass [foo='ä' i] /* COMBINING in both */
with querySelector in XML Pass [foo='Ä' i] /* COMBINING in both */
in XML Pass [foo='Ä' i] /* COMBINING in both */
with querySelector in XML -Fail [*|foo='bar' i]
in XML -Fail [*|foo='bar' i]
with querySelector in XML +Pass [*|foo='bar' i]
in XML +Pass [*|foo='bar' i]
with querySelector in XML Pass [*|foo='bar' i]
in XML Pass [*|foo='bar' i]
with querySelector in XML Pass [align='left' i]
in XML @@ -697,7 +696,7 @@ Pass [*|lang='a' i]
in XML Pass [*|lang='a' i]
with querySelector in XML Pass [*|lang='A' i]
in XML Pass [*|lang='A' i]
with querySelector in XML -Fail @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' i]
in XML +Pass @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' i]
in XML Pass [foo='bar' i][foo='bar' i]
in XML Pass [foo='bar' i][foo='bar' i]
with querySelector in XML Pass [foo='BAR'][foo='bar' i]
in XML @@ -710,8 +709,8 @@ Pass [foo='' s]
in XML Pass [foo='' s]
with querySelector in XML Pass [foo='ä' s] /* COMBINING in both */
in XML Pass [foo='ä' s] /* COMBINING in both */
with querySelector in XML -Fail [*|foo='bar' s]
in XML -Fail [*|foo='bar' s]
with querySelector in XML +Pass [*|foo='bar' s]
in XML +Pass [*|foo='bar' s]
with querySelector in XML Pass [*|foo='bar' s]
in XML Pass [*|foo='bar' s]
with querySelector in XML Pass [align='left' s]
in XML @@ -734,7 +733,7 @@ Pass [*|lang='a' s]
in XML Pass [*|lang='a' s]
with querySelector in XML Pass [*|lang='A' s]
in XML Pass [*|lang='A' s]
with querySelector in XML -Fail @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in XML +Pass @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in XML Pass [foo='BAR' s][foo='BAR' s]
in XML Pass [foo='BAR' s][foo='BAR' s]
with querySelector in XML Pass [missingattr] /* sanity check (no match) */
in XML