Browse Source

LibWeb: Implement attribute selector case identifier

Daniel Glazman 3 years ago
parent
commit
91e1383b85

+ 24 - 1
Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp

@@ -336,6 +336,7 @@ Result<Selector::SimpleSelector, Parser::ParsingResult> Parser::parse_attribute_
             // correct with XML later, we'll need to keep the original case and then do
             // a case-insensitive compare later.
             .name = attribute_part.token().ident().to_lowercase_string(),
+            .case_type = Selector::SimpleSelector::Attribute::CaseType::DefaultMatch,
         }
     };
 
@@ -397,8 +398,30 @@ Result<Selector::SimpleSelector, Parser::ParsingResult> Parser::parse_attribute_
     simple_selector.attribute().value = value_part.token().is(Token::Type::Ident) ? value_part.token().ident() : value_part.token().string();
 
     attribute_tokens.skip_whitespace();
+    // Handle case-sensitivity suffixes. https://www.w3.org/TR/selectors-4/#attribute-case
+    if (attribute_tokens.has_next_token()) {
+        auto const& case_sensitivity_part = attribute_tokens.next_token();
+        if (case_sensitivity_part.is(Token::Type::Ident)) {
+            auto case_sensitivity = case_sensitivity_part.token().ident();
+            if (case_sensitivity.equals_ignoring_case("i")) {
+                simple_selector.attribute().case_type = Selector::SimpleSelector::Attribute::CaseType::CaseInsensitiveMatch;
+            } else if (case_sensitivity.equals_ignoring_case("s")) {
+                simple_selector.attribute().case_type = Selector::SimpleSelector::Attribute::CaseType::CaseSensitiveMatch;
+            } else {
+                dbgln_if(CSS_PARSER_DEBUG, "Expected a \"i\" or \"s\" attribute selector case sensitivity identifier, got: '{}'", case_sensitivity_part.to_debug_string());
+                return ParsingResult::SyntaxError;
+            }
+        } else {
+            dbgln_if(CSS_PARSER_DEBUG, "Expected an attribute selector case sensitivity identifier, got: '{}'", case_sensitivity_part.to_debug_string());
+            return ParsingResult::SyntaxError;
+        }
+    }
+
+    if (attribute_tokens.has_next_token()) {
+        dbgln_if(CSS_PARSER_DEBUG, "Was not expecting anything else inside attribute selector.");
+        return ParsingResult::SyntaxError;
+    }
 
-    // FIXME: Handle case-sensitivity suffixes. https://www.w3.org/TR/selectors-4/#attribute-case
     return simple_selector;
 }
 

+ 14 - 1
Userland/Libraries/LibWeb/CSS/Selector.cpp

@@ -174,7 +174,20 @@ String Selector::SimpleSelector::serialize() const
 
             serialize_a_string(s, attribute.value);
         }
-        // FIXME: 5. If the attribute selector has the case-sensitivity flag present, append " i" (U+0020 U+0069) to s.
+
+        // 5. If the attribute selector has the case-insensitivity flag present, append " i" (U+0020 U+0069) to s.
+        //    If the attribute selector has the case-insensitivity flag present, append " s" (U+0020 U+0073) to s.
+        //    (the line just above is an addition to CSS OM to match Selectors Level 4 last draft)
+        switch (attribute.case_type) {
+        case Selector::SimpleSelector::Attribute::CaseType::CaseInsensitiveMatch:
+            s.append(" i");
+            break;
+        case Selector::SimpleSelector::Attribute::CaseType::CaseSensitiveMatch:
+            s.append(" s");
+            break;
+        default:
+            break;
+        }
 
         // 6. Append "]" (U+005D) to s.
         s.append(']');

+ 6 - 0
Userland/Libraries/LibWeb/CSS/Selector.h

@@ -100,9 +100,15 @@ public:
                 StartsWithString,  // [att^=val]
                 EndsWithString,    // [att$=val]
             };
+            enum class CaseType {
+                DefaultMatch,
+                CaseSensitiveMatch,
+                CaseInsensitiveMatch,
+            };
             MatchType match_type;
             FlyString name {};
             String value {};
+            CaseType case_type;
         };
 
         Type type;

+ 38 - 10
Userland/Libraries/LibWeb/CSS/SelectorEngine.cpp

@@ -78,17 +78,41 @@ static inline bool matches_checked_pseudo_class(DOM::Element const& element)
 
 static inline bool matches_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute, DOM::Element const& element)
 {
-    switch (attribute.match_type) {
-    case CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute:
+    if (attribute.match_type == CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute) {
+        // Early way out in case of an attribute existence selector.
         return element.has_attribute(attribute.name);
+    }
+
+    auto const case_insensitive_match = (attribute.case_type == CSS::Selector::SimpleSelector::Attribute::CaseType::CaseInsensitiveMatch);
+    auto const case_sensitivity = case_insensitive_match
+        ? CaseSensitivity::CaseInsensitive
+        : CaseSensitivity::CaseSensitive;
+
+    switch (attribute.match_type) {
     case CSS::Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch:
-        return element.attribute(attribute.name) == attribute.value;
-    case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsWord:
-        return !attribute.value.is_empty()
-            && element.attribute(attribute.name).split_view(' ').contains_slow(attribute.value);
+        return case_insensitive_match
+            ? element.attribute(attribute.name).equals_ignoring_case(attribute.value)
+            : element.attribute(attribute.name) == 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;
+        }
+        auto const view = element.attribute(attribute.name).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
+                    ? value.equals_ignoring_case(attribute.value)
+                    : value == attribute.value) {
+                return true;
+            }
+        }
+        return false;
+    }
     case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsString:
         return !attribute.value.is_empty()
-            && element.attribute(attribute.name).contains(attribute.value);
+            && element.attribute(attribute.name).contains(attribute.value, case_sensitivity);
     case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment: {
         const auto element_attr_value = element.attribute(attribute.name);
         if (element_attr_value.is_empty()) {
@@ -100,14 +124,18 @@ static inline bool matches_attribute(CSS::Selector::SimpleSelector::Attribute co
             return false;
         }
         auto segments = element_attr_value.split_view('-');
-        return segments.first() == attribute.value;
+        return case_insensitive_match
+            ? segments.first().equals_ignoring_case(attribute.value)
+            : segments.first() == attribute.value;
     }
     case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithString:
         return !attribute.value.is_empty()
-            && element.attribute(attribute.name).starts_with(attribute.value);
+            && element.attribute(attribute.name).starts_with(attribute.value, case_sensitivity);
     case CSS::Selector::SimpleSelector::Attribute::MatchType::EndsWithString:
         return !attribute.value.is_empty()
-            && element.attribute(attribute.name).ends_with(attribute.value);
+            && element.attribute(attribute.name).ends_with(attribute.value, case_sensitivity);
+    default:
+        break;
     }
 
     return false;