Browse Source

Add support for `:scope` as a pseudo-class for QuerySelector[All] / Matches / Closest (#578)

Co-authored-by: Michael Ragazzon <[email protected]>
Jonathan 8 months ago
parent
commit
6851f412dc

+ 10 - 10
Source/Core/Element.cpp

@@ -1066,7 +1066,7 @@ Element* Element::Closest(const String& selectors) const
 	{
 		for (const StyleSheetNode* node : leaf_nodes)
 		{
-			if (node->IsApplicable(parent))
+			if (node->IsApplicable(parent, this))
 			{
 				return parent;
 			}
@@ -1523,7 +1523,7 @@ void Element::GetElementsByClassName(ElementList& elements, const String& class_
 	return ElementUtilities::GetElementsByClassName(elements, this, class_name);
 }
 
-static Element* QuerySelectorMatchRecursive(const StyleSheetNodeListRaw& nodes, Element* element)
+static Element* QuerySelectorMatchRecursive(const StyleSheetNodeListRaw& nodes, Element* element, Element* scope)
 {
 	const int num_children = element->GetNumChildren();
 
@@ -1535,11 +1535,11 @@ static Element* QuerySelectorMatchRecursive(const StyleSheetNodeListRaw& nodes,
 
 		for (const StyleSheetNode* node : nodes)
 		{
-			if (node->IsApplicable(child))
+			if (node->IsApplicable(child, scope))
 				return child;
 		}
 
-		Element* matching_element = QuerySelectorMatchRecursive(nodes, child);
+		Element* matching_element = QuerySelectorMatchRecursive(nodes, child, scope);
 		if (matching_element)
 			return matching_element;
 	}
@@ -1547,7 +1547,7 @@ static Element* QuerySelectorMatchRecursive(const StyleSheetNodeListRaw& nodes,
 	return nullptr;
 }
 
-static void QuerySelectorAllMatchRecursive(ElementList& matching_elements, const StyleSheetNodeListRaw& nodes, Element* element)
+static void QuerySelectorAllMatchRecursive(ElementList& matching_elements, const StyleSheetNodeListRaw& nodes, Element* element, Element* scope)
 {
 	const int num_children = element->GetNumChildren();
 
@@ -1559,14 +1559,14 @@ static void QuerySelectorAllMatchRecursive(ElementList& matching_elements, const
 
 		for (const StyleSheetNode* node : nodes)
 		{
-			if (node->IsApplicable(child))
+			if (node->IsApplicable(child, scope))
 			{
 				matching_elements.push_back(child);
 				break;
 			}
 		}
 
-		QuerySelectorAllMatchRecursive(matching_elements, nodes, child);
+		QuerySelectorAllMatchRecursive(matching_elements, nodes, child, scope);
 	}
 }
 
@@ -1581,7 +1581,7 @@ Element* Element::QuerySelector(const String& selectors)
 		return nullptr;
 	}
 
-	return QuerySelectorMatchRecursive(leaf_nodes, this);
+	return QuerySelectorMatchRecursive(leaf_nodes, this, this);
 }
 
 void Element::QuerySelectorAll(ElementList& elements, const String& selectors)
@@ -1595,7 +1595,7 @@ void Element::QuerySelectorAll(ElementList& elements, const String& selectors)
 		return;
 	}
 
-	QuerySelectorAllMatchRecursive(elements, leaf_nodes, this);
+	QuerySelectorAllMatchRecursive(elements, leaf_nodes, this, this);
 }
 
 bool Element::Matches(const String& selectors)
@@ -1611,7 +1611,7 @@ bool Element::Matches(const String& selectors)
 
 	for (const StyleSheetNode* node : leaf_nodes)
 	{
-		if (node->IsApplicable(this))
+		if (node->IsApplicable(this, this))
 		{
 			return true;
 		}

+ 2 - 2
Source/Core/StyleSheet.cpp

@@ -209,7 +209,7 @@ SharedPtr<const ElementDefinition> StyleSheet::GetElementDefinition(const Elemen
 				// We found a node that has at least one requirement matching the element. Now see if we satisfy the remaining requirements of the
 				// node, including all ancestor nodes. What this involves is traversing the style nodes backwards, trying to match nodes in the
 				// element's hierarchy to nodes in the style hierarchy.
-				if (node->IsApplicable(element))
+				if (node->IsApplicable(element, nullptr))
 					applicable_nodes.push_back(node);
 			}
 		}
@@ -236,7 +236,7 @@ SharedPtr<const ElementDefinition> StyleSheet::GetElementDefinition(const Elemen
 	// Also check all remaining nodes that don't contain any indexed requirements.
 	for (const StyleSheetNode* node : styled_node_index.other)
 	{
-		if (node->IsApplicable(element))
+		if (node->IsApplicable(element, nullptr))
 			applicable_nodes.push_back(node);
 	}
 

+ 1 - 0
Source/Core/StyleSheetFactory.cpp

@@ -52,6 +52,7 @@ StyleSheetFactory::StyleSheetFactory() :
 		{"only-of-type", StructuralSelectorType::Only_Of_Type},
 		{"empty", StructuralSelectorType::Empty},
 		{"not", StructuralSelectorType::Not},
+		{"scope", StructuralSelectorType::Scope},
 	}
 {}
 

+ 10 - 10
Source/Core/StyleSheetNode.cpp

@@ -177,7 +177,7 @@ const PropertyDictionary& StyleSheetNode::GetProperties() const
 	return properties;
 }
 
-bool StyleSheetNode::Match(const Element* element) const
+bool StyleSheetNode::Match(const Element* element, const Element* scope) const
 {
 	if (!selector.tag.empty() && selector.tag != element->GetTagName())
 		return false;
@@ -200,17 +200,17 @@ bool StyleSheetNode::Match(const Element* element) const
 	if (!selector.attributes.empty() && !MatchAttributes(element))
 		return false;
 
-	if (!selector.structural_selectors.empty() && !MatchStructuralSelector(element))
+	if (!selector.structural_selectors.empty() && !MatchStructuralSelector(element, scope))
 		return false;
 
 	return true;
 }
 
-bool StyleSheetNode::MatchStructuralSelector(const Element* element) const
+bool StyleSheetNode::MatchStructuralSelector(const Element* element, const Element* scope) const
 {
 	for (auto& node_selector : selector.structural_selectors)
 	{
-		if (!IsSelectorApplicable(element, node_selector))
+		if (!IsSelectorApplicable(element, node_selector, scope))
 			return false;
 	}
 
@@ -292,7 +292,7 @@ bool StyleSheetNode::MatchAttributes(const Element* element) const
 	return true;
 }
 
-bool StyleSheetNode::TraverseMatch(const Element* element) const
+bool StyleSheetNode::TraverseMatch(const Element* element, const Element* scope) const
 {
 	RMLUI_ASSERT(parent);
 	if (!parent->parent)
@@ -307,7 +307,7 @@ bool StyleSheetNode::TraverseMatch(const Element* element) const
 		// hierarchy using the next element parent. Repeat until we run out of elements.
 		for (element = element->GetParentNode(); element; element = element->GetParentNode())
 		{
-			if (parent->Match(element) && parent->TraverseMatch(element))
+			if (parent->Match(element, scope) && parent->TraverseMatch(element, scope))
 				return true;
 			// If the node has a child combinator we must match this first ancestor.
 			else if (selector.combinator == SelectorCombinator::Child)
@@ -341,7 +341,7 @@ bool StyleSheetNode::TraverseMatch(const Element* element) const
 			// text elements don't have children and thus any ancestor is not a text element.
 			if (IsTextElement(element))
 				continue;
-			else if (parent->Match(element) && parent->TraverseMatch(element))
+			else if (parent->Match(element, scope) && parent->TraverseMatch(element, scope))
 				return true;
 			// If the node has a next-sibling combinator we must match this first sibling.
 			else if (selector.combinator == SelectorCombinator::NextSibling)
@@ -355,7 +355,7 @@ bool StyleSheetNode::TraverseMatch(const Element* element) const
 	return false;
 }
 
-bool StyleSheetNode::IsApplicable(const Element* element) const
+bool StyleSheetNode::IsApplicable(const Element* element, const Element* scope) const
 {
 	// Determine whether the element matches the current node and its entire lineage. The entire hierarchy of the element's document will be
 	// considered during the match as necessary.
@@ -384,11 +384,11 @@ bool StyleSheetNode::IsApplicable(const Element* element) const
 		return false;
 
 	// Check the structural selector requirements last as they can be quite slow.
-	if (!selector.structural_selectors.empty() && !MatchStructuralSelector(element))
+	if (!selector.structural_selectors.empty() && !MatchStructuralSelector(element, scope))
 		return false;
 
 	// Walk up through all our parent nodes, each one of them must be matched by some ancestor or sibling element.
-	if (parent && !TraverseMatch(element))
+	if (parent && !TraverseMatch(element, scope))
 		return false;
 
 	return true;

+ 4 - 4
Source/Core/StyleSheetNode.h

@@ -74,7 +74,7 @@ public:
 	/// Returns true if this node is applicable to the given element, given its IDs, classes and heritage.
 	/// @note For performance reasons this call does not check whether 'element' is a text element. The caller must manually check this condition and
 	/// consider any text element not applicable.
-	bool IsApplicable(const Element* element) const;
+	bool IsApplicable(const Element* element, const Element* scope) const;
 
 	/// Returns the specificity of this node.
 	int GetSpecificity() const;
@@ -83,12 +83,12 @@ private:
 	void CalculateAndSetSpecificity();
 
 	// Match an element to the local node requirements.
-	inline bool Match(const Element* element) const;
-	inline bool MatchStructuralSelector(const Element* element) const;
+	inline bool Match(const Element* element, const Element* scope) const;
+	inline bool MatchStructuralSelector(const Element* element, const Element* scope) const;
 	inline bool MatchAttributes(const Element* element) const;
 
 	// Recursively traverse the nodes up towards the root to match the element and its hierarchy.
-	bool TraverseMatch(const Element* element) const;
+	bool TraverseMatch(const Element* element, const Element* scope) const;
 
 	// The parent of this node; is nullptr for the root node.
 	StyleSheetNode* parent = nullptr;

+ 7 - 2
Source/Core/StyleSheetSelector.cpp

@@ -90,7 +90,7 @@ bool operator==(const CompoundSelector& a, const CompoundSelector& b)
 	return true;
 }
 
-bool IsSelectorApplicable(const Element* element, const StructuralSelector& selector)
+bool IsSelectorApplicable(const Element* element, const StructuralSelector& selector, const Element* scope)
 {
 	RMLUI_ASSERT(element);
 
@@ -367,7 +367,7 @@ bool IsSelectorApplicable(const Element* element, const StructuralSelector& sele
 
 		for (const StyleSheetNode* node : selector.selector_tree->leafs)
 		{
-			if (node->IsApplicable(element))
+			if (node->IsApplicable(element, scope))
 			{
 				inner_selector_matches = true;
 				break;
@@ -377,6 +377,11 @@ bool IsSelectorApplicable(const Element* element, const StructuralSelector& sele
 		return !inner_selector_matches;
 	}
 	break;
+	case StructuralSelectorType::Scope:
+	{
+		return scope && element == scope;
+	}
+	break;
 	case StructuralSelectorType::Invalid:
 	{
 		RMLUI_ERROR;

+ 5 - 3
Source/Core/StyleSheetSelector.h

@@ -108,7 +108,8 @@ enum class StructuralSelectorType {
 	Only_Child,
 	Only_Of_Type,
 	Empty,
-	Not
+	Not,
+	Scope,
 };
 struct StructuralSelector {
 	StructuralSelector(StructuralSelectorType type, int a, int b) : type(type), a(a), b(b) {}
@@ -148,10 +149,11 @@ struct CompoundSelector {
 };
 bool operator==(const CompoundSelector& a, const CompoundSelector& b);
 
-/// Returns true if the the node the given selector is discriminating for is applicable to a given element.
+/// Returns true if the node the given selector is discriminating for is applicable to a given element.
 /// @param element[in] The element to determine node applicability for.
 /// @param selector[in] The selector to test against the element.
-bool IsSelectorApplicable(const Element* element, const StructuralSelector& selector);
+/// @param scope[in] The element considered as the reference point/scope (for :scope).
+bool IsSelectorApplicable(const Element* element, const StructuralSelector& selector, const Element* scope);
 
 } // namespace Rml
 #endif

+ 54 - 8
Tests/Source/UnitTests/Selectors.cpp

@@ -113,7 +113,7 @@ static const Vector<QuerySelector> query_selectors =
 	{ "*span",                       "Y D0 D1 F0" },
 	{ "*.hello",                     "X Z H" },
 	{ "*:checked",                   "I" },
-	
+
 	{ "p[unit='m']",                 "B" },
 	{ "p[unit=\"m\"]",               "B" },
 	{ "[class]",                     "X Y Z P F0 G H" },
@@ -129,7 +129,7 @@ static const Vector<QuerySelector> query_selectors =
 	{ "[class$=hello]",              "X H" },
 	{ "[class*=hello]",              "X Z F0 H" },
 	{ "[class*=ello]",               "X Z F0 H" },
-	
+
 	{ "[class~=hello].world",         "Z H" },
 	{ "*[class~=hello].world",        "Z H" },
 	{ ".world[class~=hello]",         "Z H" },
@@ -142,10 +142,10 @@ static const Vector<QuerySelector> query_selectors =
 	{ "[invalid",                     "", 1, 4 },
 	{ "[]",                           "", 1, 4 },
 	{ "[x=Rule{What}]",               "", 2, 0 },
-	{ "[x=Hello,world]",              "", 1, 2 }, 
+	{ "[x=Hello,world]",              "", 1, 2 },
 	// The next ones are valid in CSS but we currently don't bother handling them, just make sure we don't crash.
 	{ "[x='Rule{What}']",             "", 2, 0 },
-	{ "[x='Hello,world']",            "", 1, 2 }, 
+	{ "[x='Hello,world']",            "", 1, 2 },
 
 	{ "#X[class=hello]",              "X" },
 	{ "[class=hello]#X",              "X" },
@@ -153,7 +153,7 @@ static const Vector<QuerySelector> query_selectors =
 	{ "div[class=hello]",             "X" },
 	{ "[class=hello]div",             "X" },
 	{ "span[class=hello]",            "" },
-	
+
 	{ ".parent :nth-child(odd)",     "A C D0 E F0 G" },
 	{ ".parent > :nth-child(even)",  "B D F H",         SelectorOp::RemoveClasses,        "parent", "" },
 	{ ":first-child",                "X A D0 F0",       SelectorOp::RemoveElementsByIds,  "A F0", "X B D0" },
@@ -176,11 +176,11 @@ static const Vector<QuerySelector> query_selectors =
 	{ ":only-child",                 "F0",              SelectorOp::RemoveElementsByIds,  "D0",    "D1 F0" },
 	{ ":only-of-type",               "Y A E F0 I" },
 	{ "span:empty",                  "Y D0 F0" },
-	
+
 	{ ".hello.world, #P span, #I",   "Z D0 D1 F0 H I",  SelectorOp::RemoveClasses,        "world", "D0 D1 F0 I" },
 	{ "body * span",                 "D0 D1 F0" },
 	{ "D1 *",                        "" },
-	
+
 	{ "#E + #F",                     "F",               SelectorOp::InsertElementBefore,  "F",     "" },
 	{ "#E+#F",                       "F" },
 	{ "#E +#F",                      "F" },
@@ -192,7 +192,7 @@ static const Vector<QuerySelector> query_selectors =
 	{ "#P + *",                      "I" },
 	{ "div.parent > #B + p",         "C" },
 	{ "div.parent > #B + div",       "" },
-	
+
 	{ "#B ~ #F",                     "F" },
 	{ "#B~#F",                       "F" },
 	{ "#B ~#F",                      "F" },
@@ -259,6 +259,22 @@ static const Vector<MatchesSelector> matches_selectors =
 	{ "B", "[unit='m']",     true }
 };
 
+struct ScopeSelector : public QuerySelector {
+	String scope_selector;
+
+	ScopeSelector(const String& scope_selector, const String& selector, const String& expected_ids) :
+		QuerySelector(selector, expected_ids), scope_selector(scope_selector)
+	{}
+};
+static const Vector<ScopeSelector> scope_selectors =
+{
+	{ "",       ":scope *",                  "X Y Z P A B C D D0 D1 E F F0 G H I" }, // should be equivalent to just "*"
+	{ "",       ":scope > *",                "X Y Z P I" },
+	{ "",       ":scope > *:not(:checked)",  "X Y Z P" },
+	{ "#P",     ":scope > p",                "B C D F G H" },
+	{ "#P",     ":scope span",               "D0 D1 F0" },
+};
+
 struct ContainsSelector {
 	String element_id;
 	String target_id;
@@ -473,6 +489,36 @@ TEST_CASE("Selectors")
 		context->UnloadDocument(document);
 	}
 
+	SUBCASE("Scope")
+	{
+		const String document_string = doc_begin + doc_end;
+		ElementDocument* document = context->LoadDocumentFromMemory(document_string);
+		REQUIRE(document);
+
+		for (const ScopeSelector& selector : scope_selectors)
+		{
+			Element* start = (selector.scope_selector.empty() ? document : document->QuerySelector(selector.scope_selector));
+			REQUIRE(start);
+
+			ElementList elements;
+			start->QuerySelectorAll(elements, selector.selector);
+			String matching_ids = ElementListToIds(elements);
+
+			Element* first_element = start->QuerySelector(selector.selector);
+			if (first_element)
+			{
+				CHECK_MESSAGE(first_element == elements[0], "QuerySelector does not return the first match of QuerySelectorAll.");
+			}
+			else
+			{
+				CHECK_MESSAGE(elements.empty(), "QuerySelector found nothing, while QuerySelectorAll found " << elements.size() << " element(s).");
+			}
+
+			CHECK_MESSAGE(matching_ids == selector.expected_ids, "QuerySelector: " << selector.selector);
+		}
+		context->UnloadDocument(document);
+	}
+
 	SUBCASE("Contains")
 	{
 		const String document_string = doc_begin + doc_end;