Browse Source

Add :not() selector to RCSS

Michael Ragazzon 3 years ago
parent
commit
377550d164

+ 44 - 4
Source/Core/StyleSheetFactory.cpp

@@ -31,6 +31,7 @@
 #include "../../Include/RmlUi/Core/StyleSheetContainer.h"
 #include "../../Include/RmlUi/Core/StyleSheetContainer.h"
 #include "StreamFile.h"
 #include "StreamFile.h"
 #include "StyleSheetNode.h"
 #include "StyleSheetNode.h"
+#include "StyleSheetParser.h"
 #include "StyleSheetSelector.h"
 #include "StyleSheetSelector.h"
 
 
 namespace Rml {
 namespace Rml {
@@ -50,6 +51,7 @@ StyleSheetFactory::StyleSheetFactory() :
 		{"only-child", StructuralSelectorType::Only_Child},
 		{"only-child", StructuralSelectorType::Only_Child},
 		{"only-of-type", StructuralSelectorType::Only_Of_Type},
 		{"only-of-type", StructuralSelectorType::Only_Of_Type},
 		{"empty", StructuralSelectorType::Empty},
 		{"empty", StructuralSelectorType::Empty},
+		{"not", StructuralSelectorType::Not},
 	}
 	}
 {}
 {}
 
 
@@ -107,14 +109,52 @@ StructuralSelector StyleSheetFactory::GetSelector(const String& name)
 	if (it == instance->selectors.end())
 	if (it == instance->selectors.end())
 		return StructuralSelector(StructuralSelectorType::Invalid, 0, 0);
 		return StructuralSelector(StructuralSelectorType::Invalid, 0, 0);
 
 
+	const StructuralSelectorType selector_type = it->second;
+
+	bool requires_parameter = false;
+	switch (selector_type)
+	{
+	case StructuralSelectorType::Nth_Child:
+	case StructuralSelectorType::Nth_Last_Child:
+	case StructuralSelectorType::Nth_Of_Type:
+	case StructuralSelectorType::Nth_Last_Of_Type:
+	case StructuralSelectorType::Not:
+		requires_parameter = true;
+		break;
+	default:
+		break;
+	}
+
+	const size_t parameter_end = name.rfind(')');
+	const bool has_parameter = (parameter_start != String::npos && parameter_end != String::npos && parameter_start < parameter_end);
+
+	if (requires_parameter != has_parameter)
+	{
+		Log::Message(Log::LT_WARNING, "Invalid selector ':%s' encountered, expected %s parameters", name.c_str(),
+			requires_parameter ? "parenthesized" : "no");
+		return StructuralSelector(StructuralSelectorType::Invalid, 0, 0);
+	}
+
 	// Parse the 'a' and 'b' values.
 	// Parse the 'a' and 'b' values.
 	int a = 1;
 	int a = 1;
 	int b = 0;
 	int b = 0;
 
 
-	const size_t parameter_end = name.find(')', parameter_start + 1);
-	if (parameter_start != String::npos && parameter_end != String::npos)
+	if (has_parameter)
 	{
 	{
-		String parameters = StringUtilities::StripWhitespace(name.substr(parameter_start + 1, parameter_end - (parameter_start + 1)));
+		const String parameters = StringUtilities::StripWhitespace(StringView(name, parameter_start + 1, parameter_end - (parameter_start + 1)));
+
+		if (selector_type == StructuralSelectorType::Not)
+		{
+			auto list = MakeShared<SelectorTree>();
+			list->root = MakeUnique<StyleSheetNode>();
+			list->leafs = StyleSheetParser::ConstructNodes(*list->root, parameters);
+
+			int specificity = 0;
+			for (const StyleSheetNode* node : list->leafs)
+				specificity = Math::Max(specificity, node->GetSpecificity());
+
+			return StructuralSelector(selector_type, std::move(list), specificity);
+		}
 
 
 		// Check for 'even' or 'odd' first.
 		// Check for 'even' or 'odd' first.
 		if (parameters == "even")
 		if (parameters == "even")
@@ -168,7 +208,7 @@ StructuralSelector StyleSheetFactory::GetSelector(const String& name)
 		}
 		}
 	}
 	}
 
 
-	return StructuralSelector(it->second, a, b);
+	return StructuralSelector(selector_type, a, b);
 }
 }
 
 
 UniquePtr<const StyleSheetContainer> StyleSheetFactory::LoadStyleSheetContainer(const String& sheet)
 UniquePtr<const StyleSheetContainer> StyleSheetFactory::LoadStyleSheetContainer(const String& sheet)

+ 9 - 8
Source/Core/StyleSheetNode.cpp

@@ -355,21 +355,22 @@ bool StyleSheetNode::IsStructurallyVolatile() const
 
 
 void StyleSheetNode::CalculateAndSetSpecificity()
 void StyleSheetNode::CalculateAndSetSpecificity()
 {
 {
-	// Calculate the specificity of just this node; tags are worth 10,000, IDs 1,000,000 and other specifiers (classes
-	// and pseudo-classes) 100,000.
+	// First calculate the specificity of this node alone.
 	specificity = 0;
 	specificity = 0;
 
 
 	if (!tag.empty())
 	if (!tag.empty())
-		specificity += 10'000;
+		specificity += SelectorSpecificity::Tag;
 
 
 	if (!id.empty())
 	if (!id.empty())
-		specificity += 1'000'000;
+		specificity += SelectorSpecificity::ID;
 
 
-	specificity += 100'000 * (int)class_names.size();
-	specificity += 100'000 * (int)pseudo_class_names.size();
-	specificity += 100'000 * (int)structural_selectors.size();
+	specificity += SelectorSpecificity::Class * (int)class_names.size();
+	specificity += SelectorSpecificity::PseudoClass * (int)pseudo_class_names.size();
+	
+	for (const StructuralSelector& selector : structural_selectors)
+		specificity += selector.specificity; 
 
 
-	// Add our parent's specificity onto ours.
+	// Then add our parent's specificity onto ours.
 	if (parent)
 	if (parent)
 		specificity += parent->specificity;
 		specificity += parent->specificity;
 }
 }

+ 2 - 2
Source/Core/StyleSheetParser.cpp

@@ -558,7 +558,7 @@ bool StyleSheetParser::Parse(MediaBlockList& style_sheets, Stream* _stream, int
 						continue;
 						continue;
 
 
 					StringList rule_name_list;
 					StringList rule_name_list;
-					StringUtilities::ExpandString(rule_name_list, pre_token_str);
+					StringUtilities::ExpandString(rule_name_list, pre_token_str, ',', '(', ')');
 
 
 					// Add style nodes to the root of the tree
 					// Add style nodes to the root of the tree
 					for (size_t i = 0; i < rule_name_list.size(); i++)
 					for (size_t i = 0; i < rule_name_list.size(); i++)
@@ -754,7 +754,7 @@ StyleSheetNodeListRaw StyleSheetParser::ConstructNodes(StyleSheetNode& root_node
 	const PropertyDictionary empty_properties;
 	const PropertyDictionary empty_properties;
 
 
 	StringList selector_list;
 	StringList selector_list;
-	StringUtilities::ExpandString(selector_list, selectors);
+	StringUtilities::ExpandString(selector_list, selectors, ',', '(', ')');
 
 
 	StyleSheetNodeListRaw leaf_nodes;
 	StyleSheetNodeListRaw leaf_nodes;
 
 

+ 23 - 1
Source/Core/StyleSheetSelector.cpp

@@ -48,7 +48,7 @@ static bool IsNth(int a, int b, int count)
 	return (x >= 0 && x * a + b == count);
 	return (x >= 0 && x * a + b == count);
 }
 }
 
 
-bool IsSelectorApplicable(const Element* element, StructuralSelector selector)
+bool IsSelectorApplicable(const Element* element, const StructuralSelector& selector)
 {
 {
 	RMLUI_ASSERT(element);
 	RMLUI_ASSERT(element);
 
 
@@ -313,6 +313,28 @@ bool IsSelectorApplicable(const Element* element, StructuralSelector selector)
 		return element->GetNumChildren() == 0;
 		return element->GetNumChildren() == 0;
 	}
 	}
 	break;
 	break;
+	case StructuralSelectorType::Not:
+	{
+		if (!selector.selector_tree)
+		{
+			RMLUI_ERROR;
+			return false;
+		}
+
+		bool inner_selector_matches = false;
+
+		for (const StyleSheetNode* node : selector.selector_tree->leafs)
+		{
+			if (node->IsApplicable(element))
+			{
+				inner_selector_matches = true;
+				break;
+			}
+		}
+
+		return !inner_selector_matches;
+	}
+	break;
 	case StructuralSelectorType::Invalid:
 	case StructuralSelectorType::Invalid:
 	{
 	{
 		RMLUI_ERROR;
 		RMLUI_ERROR;

+ 42 - 7
Source/Core/StyleSheetSelector.h

@@ -35,6 +35,18 @@
 namespace Rml {
 namespace Rml {
 
 
 class Element;
 class Element;
+class StyleSheetNode;
+struct SelectorTree;
+
+namespace SelectorSpecificity {
+	enum {
+		// Constants used to determine the specificity of a selector.
+		Tag = 10'000,
+		Class = 100'000,
+		PseudoClass = Class,
+		ID = 1'000'000,
+	};
+}
 
 
 enum class StructuralSelectorType {
 enum class StructuralSelectorType {
 	Invalid,
 	Invalid,
@@ -48,25 +60,48 @@ enum class StructuralSelectorType {
 	Last_Of_Type,
 	Last_Of_Type,
 	Only_Child,
 	Only_Child,
 	Only_Of_Type,
 	Only_Of_Type,
-	Empty
+	Empty,
+	Not
 };
 };
 
 
 struct StructuralSelector {
 struct StructuralSelector {
 	StructuralSelector(StructuralSelectorType type, int a, int b) : type(type), a(a), b(b) {}
 	StructuralSelector(StructuralSelectorType type, int a, int b) : type(type), a(a), b(b) {}
-	StructuralSelectorType type;
+	StructuralSelector(StructuralSelectorType type, SharedPtr<const SelectorTree> tree, int specificity) :
+		type(type), specificity(specificity), selector_tree(std::move(tree))
+	{}
+
+	StructuralSelectorType type = StructuralSelectorType::Invalid;
+
 	// For counting selectors, the following are the 'a' and 'b' variables of an + b.
 	// For counting selectors, the following are the 'a' and 'b' variables of an + b.
-	int a;
-	int b;
+	int a = 0;
+	int b = 0;
+
+	// Specificity is usually determined like a pseudo class, but some types override this value.
+	int specificity = SelectorSpecificity::PseudoClass;
+
+	// For selectors that contain internal selectors such as :not().
+	SharedPtr<const SelectorTree> selector_tree;
 };
 };
+
 inline bool operator==(const StructuralSelector& a, const StructuralSelector& b)
 inline bool operator==(const StructuralSelector& a, const StructuralSelector& b)
 {
 {
-	return a.type == b.type && a.a == b.a && a.b == b.b;
+	// Currently sub-selectors (selector_tree) are only superficially compared. This mainly has the consequence that selectors with a sub-selector
+	// which are instantiated separately will never compare equal, even if they have the exact same sub-selector expression. This further results in
+	// such selectors not being de-duplicated. This should not lead to any functional differences but leads to potentially missed memory/performance
+	// optimizations. E.g. 'div a, div b' will combine the two div nodes, while ':not(div) a, :not(div) b' will not combine the two not-div nodes.
+	return a.type == b.type && a.a == b.a && a.b == b.b && a.selector_tree == b.selector_tree;
 }
 }
 inline bool operator<(const StructuralSelector& a, const StructuralSelector& b)
 inline bool operator<(const StructuralSelector& a, const StructuralSelector& b)
 {
 {
-	return std::tie(a.type, a.a, a.b) < std::tie(b.type, b.a, b.b);
+	return std::tie(a.type, a.a, a.b, a.selector_tree) < std::tie(b.type, b.a, b.b, b.selector_tree);
 }
 }
 
 
+// A tree of unstyled style sheet nodes.
+struct SelectorTree {
+	UniquePtr<StyleSheetNode> root;
+	Vector<StyleSheetNode*> leafs; // Owned by root.
+};
+
 enum class SelectorCombinator : byte {
 enum class SelectorCombinator : byte {
 	None,
 	None,
 	Child,             // The 'E > F' combinator: Matches if F is a child of E.
 	Child,             // The 'E > F' combinator: Matches if F is a child of E.
@@ -77,7 +112,7 @@ enum class SelectorCombinator : byte {
 /// Returns true if the the node the given selector is discriminating for is applicable to a given element.
 /// Returns true if the 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 element[in] The element to determine node applicability for.
 /// @param selector[in] The selector to test against the element.
 /// @param selector[in] The selector to test against the element.
-bool IsSelectorApplicable(const Element* element, StructuralSelector selector);
+bool IsSelectorApplicable(const Element* element, const StructuralSelector& selector);
 
 
 } // namespace Rml
 } // namespace Rml
 #endif
 #endif

+ 10 - 0
Tests/Source/Benchmarks/Selectors.cpp

@@ -149,6 +149,15 @@ static String GenerateRCSS(SelectorFlags selectors, const String& complex_select
 
 
 			// Set a property that does not require a layout change
 			// Set a property that does not require a layout change
 			result += CreateString(64, " { scrollbar-margin: %dpx; }\n", int(c - 'a') + 1);
 			result += CreateString(64, " { scrollbar-margin: %dpx; }\n", int(c - 'a') + 1);
+
+			
+#if 1
+			// This conditions ensures that only a single version of the complex selector is included. This can be disabled to test how well the rules
+			// are de-duplicated, since then a lot more selectors will be tested per update call. Rules that contain sub-selectors are currently not
+			// de-duplicated, such as :not().
+			if (!complex_selector.empty())
+				return result;
+#endif
 		}
 		}
 	}
 	}
 
 
@@ -222,6 +231,7 @@ TEST_CASE("Selectors")
 		":first-child div",
 		":first-child div",
 		":nth-child(2n+3) div",
 		":nth-child(2n+3) div",
 		":nth-of-type(2n+3) div",
 		":nth-of-type(2n+3) div",
+		":not(div) div",
 	};
 	};
 
 
 	for (int i = 0; i < NUM_COMBINATIONS + (int)complex_selectors.size(); i++)
 	for (int i = 0; i < NUM_COMBINATIONS + (int)complex_selectors.size(); i++)

+ 7 - 1
Tests/Source/UnitTests/Selectors.cpp

@@ -36,7 +36,6 @@
 
 
 using namespace Rml;
 using namespace Rml;
 
 
-// clang-format off
 static const String doc_begin = R"(
 static const String doc_begin = R"(
 <rml>
 <rml>
 <head>
 <head>
@@ -90,6 +89,7 @@ struct QuerySelector {
 	String expected_ids_after_operation;
 	String expected_ids_after_operation;
 };
 };
 
 
+// clang-format off
 static const Vector<QuerySelector> query_selectors =
 static const Vector<QuerySelector> query_selectors =
 {
 {
 	{ "span",                        "Y D0 D1 F0" },
 	{ "span",                        "Y D0 D1 F0" },
@@ -141,6 +141,12 @@ static const Vector<QuerySelector> query_selectors =
 	{ "#F ~ #B",                     "" },
 	{ "#F ~ #B",                     "" },
 	{ "div.parent > #B ~ p:empty",   "C G H",           SelectorOp::InsertElementBefore,  "H",     "C G Inserted H" },
 	{ "div.parent > #B ~ p:empty",   "C G H",           SelectorOp::InsertElementBefore,  "H",     "C G Inserted H" },
 	{ "div.parent > #B ~ * span",    "D0 D1 F0" },
 	{ "div.parent > #B ~ * span",    "D0 D1 F0" },
+	{ ":not(*)",                     "" },
+	{ ":not(span)",                  "X Z P A B C D E F G H I" },
+	{ "#D :not(#D0)",                "D1" },
+	{ "body > :not(:checked)",       "X Y Z P",         SelectorOp::RemoveChecked,        "I", "X Y Z P I" },
+	{ "div.hello:not(.world)",       "X" },
+	{ ":not(div,:nth-child(2),p *)", "A C D E F G H I" },
 };
 };
 
 
 struct ClosestSelector {
 struct ClosestSelector {