mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 15:42:44 +00:00 
			
		
		
		
	LibWeb: Implement :nth-child pseudo-class
This commit is contained in:
		
							parent
							
								
									1e0e8b27c0
								
							
						
					
					
						commit
						aa83539d7b
					
				
					 6 changed files with 161 additions and 36 deletions
				
			
		|  | @ -11,6 +11,7 @@ | ||||||
| #include <LibWeb/CSS/CSSStyleRule.h> | #include <LibWeb/CSS/CSSStyleRule.h> | ||||||
| #include <LibWeb/CSS/Parser/DeprecatedCSSParser.h> | #include <LibWeb/CSS/Parser/DeprecatedCSSParser.h> | ||||||
| #include <LibWeb/CSS/PropertyID.h> | #include <LibWeb/CSS/PropertyID.h> | ||||||
|  | #include <LibWeb/CSS/Selector.h> | ||||||
| #include <LibWeb/DOM/Document.h> | #include <LibWeb/DOM/Document.h> | ||||||
| #include <ctype.h> | #include <ctype.h> | ||||||
| #include <stdlib.h> | #include <stdlib.h> | ||||||
|  | @ -384,6 +385,17 @@ public: | ||||||
|         return ch == '~' || ch == '>' || ch == '+'; |         return ch == '~' || ch == '>' || ch == '+'; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     static StringView capture_selector_args(const String& pseudo_name) | ||||||
|  |     { | ||||||
|  |         if (const auto start_pos = pseudo_name.find('('); start_pos.has_value()) { | ||||||
|  |             const auto start = start_pos.value() + 1; | ||||||
|  |             if (const auto end_pos = pseudo_name.index_of(")", start); end_pos.has_value()) { | ||||||
|  |                 return pseudo_name.substring_view(start, end_pos.value() - start).trim_whitespace(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return {}; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     Optional<CSS::Selector::SimpleSelector> parse_simple_selector() |     Optional<CSS::Selector::SimpleSelector> parse_simple_selector() | ||||||
|     { |     { | ||||||
|         auto index_at_start = index; |         auto index_at_start = index; | ||||||
|  | @ -399,15 +411,9 @@ public: | ||||||
|         if (peek() == '*') { |         if (peek() == '*') { | ||||||
|             type = CSS::Selector::SimpleSelector::Type::Universal; |             type = CSS::Selector::SimpleSelector::Type::Universal; | ||||||
|             consume_one(); |             consume_one(); | ||||||
|             return CSS::Selector::SimpleSelector { |             CSS::Selector::SimpleSelector result; | ||||||
|                 type, |             result.type = type; | ||||||
|                 CSS::Selector::SimpleSelector::PseudoClass::None, |             return result; | ||||||
|                 CSS::Selector::SimpleSelector::PseudoElement::None, |  | ||||||
|                 String(), |  | ||||||
|                 CSS::Selector::SimpleSelector::AttributeMatchType::None, |  | ||||||
|                 String(), |  | ||||||
|                 String() |  | ||||||
|             }; |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (peek() == '.') { |         if (peek() == '.') { | ||||||
|  | @ -435,15 +441,9 @@ public: | ||||||
|             value = value.to_lowercase(); |             value = value.to_lowercase(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         CSS::Selector::SimpleSelector simple_selector { |         CSS::Selector::SimpleSelector simple_selector; | ||||||
|             type, |         simple_selector.type = type; | ||||||
|             CSS::Selector::SimpleSelector::PseudoClass::None, |         simple_selector.value = value; | ||||||
|             CSS::Selector::SimpleSelector::PseudoElement::None, |  | ||||||
|             value, |  | ||||||
|             CSS::Selector::SimpleSelector::AttributeMatchType::None, |  | ||||||
|             String(), |  | ||||||
|             String() |  | ||||||
|         }; |  | ||||||
|         buffer.clear(); |         buffer.clear(); | ||||||
| 
 | 
 | ||||||
|         if (peek() == '[') { |         if (peek() == '[') { | ||||||
|  | @ -563,6 +563,9 @@ public: | ||||||
|                 simple_selector.pseudo_class = CSS::Selector::SimpleSelector::PseudoClass::FirstOfType; |                 simple_selector.pseudo_class = CSS::Selector::SimpleSelector::PseudoClass::FirstOfType; | ||||||
|             } else if (pseudo_name.equals_ignoring_case("last-of-type")) { |             } else if (pseudo_name.equals_ignoring_case("last-of-type")) { | ||||||
|                 simple_selector.pseudo_class = CSS::Selector::SimpleSelector::PseudoClass::LastOfType; |                 simple_selector.pseudo_class = CSS::Selector::SimpleSelector::PseudoClass::LastOfType; | ||||||
|  |             } else if (pseudo_name.starts_with("nth-child", CaseSensitivity::CaseInsensitive)) { | ||||||
|  |                 simple_selector.pseudo_class = CSS::Selector::SimpleSelector::PseudoClass::NthChild; | ||||||
|  |                 simple_selector.nth_child_pattern = CSS::Selector::SimpleSelector::NthChildPattern::parse(capture_selector_args(pseudo_name)); | ||||||
|             } else if (pseudo_name.equals_ignoring_case("before")) { |             } else if (pseudo_name.equals_ignoring_case("before")) { | ||||||
|                 simple_selector.pseudo_element = CSS::Selector::SimpleSelector::PseudoElement::Before; |                 simple_selector.pseudo_element = CSS::Selector::SimpleSelector::PseudoElement::Before; | ||||||
|             } else if (pseudo_name.equals_ignoring_case("after")) { |             } else if (pseudo_name.equals_ignoring_case("after")) { | ||||||
|  |  | ||||||
|  | @ -104,15 +104,9 @@ Vector<CSS::Selector::ComplexSelector> Parser::parse_selectors(Vector<String> pa | ||||||
|         if (currentToken == "*") { |         if (currentToken == "*") { | ||||||
|             type = CSS::Selector::SimpleSelector::Type::Universal; |             type = CSS::Selector::SimpleSelector::Type::Universal; | ||||||
|             index++; |             index++; | ||||||
|             return CSS::Selector::SimpleSelector { |             CSS::Selector::SimpleSelector result; | ||||||
|                 type, |             result.type = type; | ||||||
|                 CSS::Selector::SimpleSelector::PseudoClass::None, |             return result; | ||||||
|                 CSS::Selector::SimpleSelector::PseudoElement::None, |  | ||||||
|                 String(), |  | ||||||
|                 CSS::Selector::SimpleSelector::AttributeMatchType::None, |  | ||||||
|                 String(), |  | ||||||
|                 String() |  | ||||||
|             }; |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (currentToken == ".") { |         if (currentToken == ".") { | ||||||
|  | @ -132,15 +126,9 @@ Vector<CSS::Selector::ComplexSelector> Parser::parse_selectors(Vector<String> pa | ||||||
|             value = value.to_lowercase(); |             value = value.to_lowercase(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         CSS::Selector::SimpleSelector simple_selector { |         CSS::Selector::SimpleSelector simple_selector; | ||||||
|             type, |         simple_selector.type = type; | ||||||
|             CSS::Selector::SimpleSelector::PseudoClass::None, |         simple_selector.value = value; | ||||||
|             CSS::Selector::SimpleSelector::PseudoElement::None, |  | ||||||
|             value, |  | ||||||
|             CSS::Selector::SimpleSelector::AttributeMatchType::None, |  | ||||||
|             String(), |  | ||||||
|             String() |  | ||||||
|         }; |  | ||||||
| 
 | 
 | ||||||
|         if (index >= parts.size()) { |         if (index >= parts.size()) { | ||||||
|             return simple_selector; |             return simple_selector; | ||||||
|  |  | ||||||
|  | @ -4,7 +4,10 @@ | ||||||
|  * SPDX-License-Identifier: BSD-2-Clause |  * SPDX-License-Identifier: BSD-2-Clause | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
|  | #include "Selector.h" | ||||||
|  | #include <AK/StringUtils.h> | ||||||
| #include <LibWeb/CSS/Selector.h> | #include <LibWeb/CSS/Selector.h> | ||||||
|  | #include <ctype.h> | ||||||
| 
 | 
 | ||||||
| namespace Web::CSS { | namespace Web::CSS { | ||||||
| 
 | 
 | ||||||
|  | @ -44,4 +47,73 @@ u32 Selector::specificity() const | ||||||
|     return ids * 0x10000 + classes * 0x100 + tag_names; |     return ids * 0x10000 + classes * 0x100 + tag_names; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | Selector::SimpleSelector::NthChildPattern Selector::SimpleSelector::NthChildPattern::parse(const StringView& args) | ||||||
|  | { | ||||||
|  |     CSS::Selector::SimpleSelector::NthChildPattern pattern; | ||||||
|  |     if (args.equals_ignoring_case("odd")) { | ||||||
|  |         pattern.step_size = 2; | ||||||
|  |         pattern.offset = 1; | ||||||
|  |     } else if (args.equals_ignoring_case("even")) { | ||||||
|  |         pattern.step_size = 2; | ||||||
|  |     } else { | ||||||
|  |         const auto consume_int = [](GenericLexer& lexer) -> Optional<int> { | ||||||
|  |             return AK::StringUtils::convert_to_int(lexer.consume_while([](char c) -> bool { | ||||||
|  |                 return isdigit(c) || c == '+' || c == '-'; | ||||||
|  |             })); | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         // Try to match any of following patterns:
 | ||||||
|  |         // 1. An+B
 | ||||||
|  |         // 2. An
 | ||||||
|  |         // 3. B
 | ||||||
|  |         // ...where "A" is "step_size", "B" is "offset" and rest are literals.
 | ||||||
|  |         // "A" can be omitted, in that case "A" = 1.
 | ||||||
|  |         // "A" may have "+" or "-" sign, "B" always must be predated by sign for pattern (1).
 | ||||||
|  | 
 | ||||||
|  |         int step_size_or_offset = 0; | ||||||
|  |         GenericLexer lexer { args }; | ||||||
|  | 
 | ||||||
|  |         // "When a=1, or a=-1, the 1 may be omitted from the rule."
 | ||||||
|  |         if (lexer.consume_specific("n") || lexer.consume_specific("+n")) { | ||||||
|  |             step_size_or_offset = +1; | ||||||
|  |             lexer.retreat(); | ||||||
|  |         } else if (lexer.consume_specific("-n")) { | ||||||
|  |             step_size_or_offset = -1; | ||||||
|  |             lexer.retreat(); | ||||||
|  |         } else { | ||||||
|  |             const auto value = consume_int(lexer); | ||||||
|  |             if (!value.has_value()) | ||||||
|  |                 return {}; | ||||||
|  |             step_size_or_offset = value.value(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (lexer.consume_specific("n")) { | ||||||
|  |             lexer.ignore_while(isspace); | ||||||
|  |             if (lexer.next_is('+') || lexer.next_is('-')) { | ||||||
|  |                 const auto sign = lexer.next_is('+') ? 1 : -1; | ||||||
|  |                 lexer.ignore(); | ||||||
|  |                 lexer.ignore_while(isspace); | ||||||
|  | 
 | ||||||
|  |                 // "An+B" pattern
 | ||||||
|  |                 const auto offset = consume_int(lexer); | ||||||
|  |                 if (!offset.has_value()) | ||||||
|  |                     return {}; | ||||||
|  |                 pattern.step_size = step_size_or_offset; | ||||||
|  |                 pattern.offset = sign * offset.value(); | ||||||
|  |             } else { | ||||||
|  |                 // "An" pattern
 | ||||||
|  |                 pattern.step_size = step_size_or_offset; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             // "B" pattern
 | ||||||
|  |             pattern.offset = step_size_or_offset; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (lexer.remaining().length() > 0) | ||||||
|  |             return {}; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return pattern; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -36,6 +36,7 @@ public: | ||||||
|             Root, |             Root, | ||||||
|             FirstOfType, |             FirstOfType, | ||||||
|             LastOfType, |             LastOfType, | ||||||
|  |             NthChild, | ||||||
|         }; |         }; | ||||||
|         PseudoClass pseudo_class { PseudoClass::None }; |         PseudoClass pseudo_class { PseudoClass::None }; | ||||||
| 
 | 
 | ||||||
|  | @ -59,6 +60,17 @@ public: | ||||||
|         AttributeMatchType attribute_match_type { AttributeMatchType::None }; |         AttributeMatchType attribute_match_type { AttributeMatchType::None }; | ||||||
|         FlyString attribute_name; |         FlyString attribute_name; | ||||||
|         String attribute_value; |         String attribute_value; | ||||||
|  | 
 | ||||||
|  |         struct NthChildPattern { | ||||||
|  |             int step_size = 0; | ||||||
|  |             int offset = 0; | ||||||
|  | 
 | ||||||
|  |             static NthChildPattern parse(const StringView& args); | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         // FIXME: We don't need this field on every single SimpleSelector, but it's also annoying to malloc it somewhere.
 | ||||||
|  |         // Only used when "pseudo_class" == PseudoClass::NthChild.
 | ||||||
|  |         NthChildPattern nth_child_pattern; | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     struct ComplexSelector { |     struct ComplexSelector { | ||||||
|  |  | ||||||
|  | @ -82,6 +82,53 @@ static bool matches(const CSS::Selector::SimpleSelector& component, const DOM::E | ||||||
|                 return false; |                 return false; | ||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|  |     case CSS::Selector::SimpleSelector::PseudoClass::NthChild: | ||||||
|  |         const auto step_size = component.nth_child_pattern.step_size; | ||||||
|  |         const auto offset = component.nth_child_pattern.offset; | ||||||
|  |         if (step_size == 0 && offset == 0) | ||||||
|  |             return false; // "If both a and b are equal to zero, the pseudo-class represents no element in the document tree."
 | ||||||
|  | 
 | ||||||
|  |         const auto* parent = element.parent_element(); | ||||||
|  |         if (!parent) | ||||||
|  |             return false; | ||||||
|  | 
 | ||||||
|  |         int index = 1; | ||||||
|  |         for (auto* child = parent->first_child_of_type<DOM::Element>(); child && child != &element; child = child->next_element_sibling()) { | ||||||
|  |             ++index; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (step_size < 0) { | ||||||
|  |             // When "step_size" is negative, selector represents first "offset" elements in document tree.
 | ||||||
|  |             if (offset <= 0 || index > offset) | ||||||
|  |                 return false; | ||||||
|  |             else | ||||||
|  |                 break; | ||||||
|  |         } else if (step_size == 1) { | ||||||
|  |             // When "step_size == 1", selector represents last "offset" elements in document tree.
 | ||||||
|  |             if (offset < 0 || index < offset) | ||||||
|  |                 return false; | ||||||
|  |             else | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Like "a % b", but handles negative integers correctly.
 | ||||||
|  |         const auto canonical_modulo = [](int a, int b) -> int { | ||||||
|  |             int c = a % b; | ||||||
|  |             if ((c < 0 && b > 0) || (c > 0 && b < 0)) { | ||||||
|  |                 c += b; | ||||||
|  |             } | ||||||
|  |             return c; | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         if (step_size == 0) { | ||||||
|  |             // Avoid divide by zero.
 | ||||||
|  |             if (index != offset) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         } else if (canonical_modulo(index - offset, step_size) != 0) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     switch (component.attribute_match_type) { |     switch (component.attribute_match_type) { | ||||||
|  |  | ||||||
|  | @ -351,6 +351,9 @@ void dump_selector(StringBuilder& builder, const CSS::Selector& selector) | ||||||
|             case CSS::Selector::SimpleSelector::PseudoClass::LastOfType: |             case CSS::Selector::SimpleSelector::PseudoClass::LastOfType: | ||||||
|                 pseudo_class_description = "LastOfType"; |                 pseudo_class_description = "LastOfType"; | ||||||
|                 break; |                 break; | ||||||
|  |             case CSS::Selector::SimpleSelector::PseudoClass::NthChild: | ||||||
|  |                 pseudo_class_description = "NthChild"; | ||||||
|  |                 break; | ||||||
|             case CSS::Selector::SimpleSelector::PseudoClass::Focus: |             case CSS::Selector::SimpleSelector::PseudoClass::Focus: | ||||||
|                 pseudo_class_description = "Focus"; |                 pseudo_class_description = "Focus"; | ||||||
|                 break; |                 break; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 miere43
						miere43