Browse Source

Add sibling selectors + and ~ to RCSS

Michael Ragazzon 3 years ago
parent
commit
539ba2cee1

+ 58 - 28
Source/Core/StyleSheetNode.cpp

@@ -15,7 +15,7 @@
  *
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
- * 
+ *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -41,14 +41,19 @@ StyleSheetNode::StyleSheetNode()
 	CalculateAndSetSpecificity();
 }
 
-StyleSheetNode::StyleSheetNode(StyleSheetNode* parent, const String& tag, const String& id, const StringList& classes, const StringList& pseudo_classes, const StructuralSelectorList& structural_selectors, bool child_combinator)
-	: parent(parent), tag(tag), id(id), class_names(classes), pseudo_class_names(pseudo_classes), structural_selectors(structural_selectors), child_combinator(child_combinator)
+StyleSheetNode::StyleSheetNode(StyleSheetNode* parent, const String& tag, const String& id, const StringList& classes,
+	const StringList& pseudo_classes, const StructuralSelectorList& structural_selectors, SelectorCombinator combinator) :
+	parent(parent),
+	tag(tag), id(id), class_names(classes), pseudo_class_names(pseudo_classes), structural_selectors(structural_selectors), combinator(combinator)
 {
 	CalculateAndSetSpecificity();
 }
 
-StyleSheetNode::StyleSheetNode(StyleSheetNode* parent, String&& tag, String&& id, StringList&& classes, StringList&& pseudo_classes, StructuralSelectorList&& structural_selectors, bool child_combinator)
-	: parent(parent), tag(std::move(tag)), id(std::move(id)), class_names(std::move(classes)), pseudo_class_names(std::move(pseudo_classes)), structural_selectors(std::move(structural_selectors)), child_combinator(child_combinator)
+StyleSheetNode::StyleSheetNode(StyleSheetNode* parent, String&& tag, String&& id, StringList&& classes, StringList&& pseudo_classes,
+	StructuralSelectorList&& structural_selectors, SelectorCombinator combinator) :
+	parent(parent),
+	tag(std::move(tag)), id(std::move(id)), class_names(std::move(classes)), pseudo_class_names(std::move(pseudo_classes)),
+	structural_selectors(std::move(structural_selectors)), combinator(combinator)
 {
 	CalculateAndSetSpecificity();
 }
@@ -58,12 +63,13 @@ StyleSheetNode* StyleSheetNode::GetOrCreateChildNode(const StyleSheetNode& other
 	// See if we match the target child
 	for (const auto& child : children)
 	{
-		if (child->EqualRequirements(other.tag, other.id, other.class_names, other.pseudo_class_names, other.structural_selectors, other.child_combinator))
+		if (child->EqualRequirements(other.tag, other.id, other.class_names, other.pseudo_class_names, other.structural_selectors, other.combinator))
 			return child.get();
 	}
 
 	// We don't, so create a new child
-	auto child = MakeUnique<StyleSheetNode>(this, other.tag, other.id, other.class_names, other.pseudo_class_names, other.structural_selectors, other.child_combinator);
+	auto child = MakeUnique<StyleSheetNode>(this, other.tag, other.id, other.class_names, other.pseudo_class_names, other.structural_selectors,
+		other.combinator);
 	StyleSheetNode* result = child.get();
 
 	children.push_back(std::move(child));
@@ -71,17 +77,19 @@ StyleSheetNode* StyleSheetNode::GetOrCreateChildNode(const StyleSheetNode& other
 	return result;
 }
 
-StyleSheetNode* StyleSheetNode::GetOrCreateChildNode(String&& tag, String&& id, StringList&& classes, StringList&& pseudo_classes, StructuralSelectorList&& structural_pseudo_classes, bool child_combinator)
+StyleSheetNode* StyleSheetNode::GetOrCreateChildNode(String&& tag, String&& id, StringList&& classes, StringList&& pseudo_classes,
+	StructuralSelectorList&& structural_pseudo_classes, SelectorCombinator combinator)
 {
 	// See if we match an existing child
 	for (const auto& child : children)
 	{
-		if (child->EqualRequirements(tag, id, classes, pseudo_classes, structural_pseudo_classes, child_combinator))
+		if (child->EqualRequirements(tag, id, classes, pseudo_classes, structural_pseudo_classes, combinator))
 			return child.get();
 	}
 
 	// We don't, so create a new child
-	auto child = MakeUnique<StyleSheetNode>(this, std::move(tag), std::move(id), std::move(classes), std::move(pseudo_classes), std::move(structural_pseudo_classes), child_combinator);
+	auto child = MakeUnique<StyleSheetNode>(this, std::move(tag), std::move(id), std::move(classes), std::move(pseudo_classes),
+		std::move(structural_pseudo_classes), combinator);
 	StyleSheetNode* result = child.get();
 
 	children.push_back(std::move(child));
@@ -108,11 +116,11 @@ UniquePtr<StyleSheetNode> StyleSheetNode::DeepCopy(StyleSheetNode* in_parent) co
 {
 	RMLUI_ZoneScoped;
 
-	auto node = MakeUnique<StyleSheetNode>(in_parent, tag, id, class_names, pseudo_class_names, structural_selectors, child_combinator);
+	auto node = MakeUnique<StyleSheetNode>(in_parent, tag, id, class_names, pseudo_class_names, structural_selectors, combinator);
 
 	node->properties = properties;
 	node->children.resize(children.size());
-	
+
 	for (size_t i = 0; i < children.size(); i++)
 	{
 		node->children[i] = children[i]->DeepCopy(node.get());
@@ -178,7 +186,8 @@ bool StyleSheetNode::SetStructurallyVolatileRecursive(bool ancestor_is_structura
 	return (self_is_structural_pseudo_class || descendant_is_structural_pseudo_class);
 }
 
-bool StyleSheetNode::EqualRequirements(const String& _tag, const String& _id, const StringList& _class_names, const StringList& _pseudo_class_names, const StructuralSelectorList& _structural_selectors, bool _child_combinator) const
+bool StyleSheetNode::EqualRequirements(const String& _tag, const String& _id, const StringList& _class_names, const StringList& _pseudo_class_names,
+	const StructuralSelectorList& _structural_selectors, SelectorCombinator _combinator) const
 {
 	if (tag != _tag)
 		return false;
@@ -190,7 +199,7 @@ bool StyleSheetNode::EqualRequirements(const String& _tag, const String& _id, co
 		return false;
 	if (structural_selectors != _structural_selectors)
 		return false;
-	if (child_combinator != _child_combinator)
+	if (combinator != _combinator)
 		return false;
 
 	return true;
@@ -256,7 +265,7 @@ inline bool StyleSheetNode::MatchStructuralSelector(const Element* element) cons
 		if (!node_selector.selector->IsApplicable(element, node_selector.a, node_selector.b))
 			return false;
 	}
-	
+
 	return true;
 }
 
@@ -286,17 +295,39 @@ bool StyleSheetNode::IsApplicable(const Element* const in_element) const
 
 	const Element* element = in_element;
 
-	// Walk up through all our parent nodes, each one of them must be matched by some ancestor element.
-	for(const StyleSheetNode* node = parent; node && node->parent; node = node->parent)
+	// Walk up through all our parent nodes, each one of them must be matched by some ancestor or sibling element.
+	for (const StyleSheetNode* node = parent; node && node->parent; node = node->parent)
 	{
-		// Try a match on every element ancestor. If it succeeds, we continue on to the next node.
-		for(element = element->GetParentNode(); element; element = element->GetParentNode())
+		switch (node->combinator)
+		{
+		case SelectorCombinator::None:
+		case SelectorCombinator::Child:
 		{
-			if (node->Match(element))
-				break;
-			// If we have a child combinator on the node, we must match this first ancestor.
-			else if (node->child_combinator)
-				return false;
+			// Try a match on every element ancestor. If it succeeds, we continue on to the next node.
+			for (element = element->GetParentNode(); element; element = element->GetParentNode())
+			{
+				if (node->Match(element))
+					break;
+				// If the node has a child combinator we must match this first ancestor.
+				else if (node->combinator == SelectorCombinator::Child)
+					return false;
+			}
+		}
+		break;
+		case SelectorCombinator::NextSibling:
+		case SelectorCombinator::SubsequentSibling:
+		{
+			// Try a match on every element ancestor. If it succeeds, we continue on to the next node.
+			for (element = element->GetPreviousSibling(); element; element = element->GetPreviousSibling())
+			{
+				if (node->Match(element))
+					break;
+				// If the node has a next-sibling combinator we must match this first sibling.
+				else if (node->combinator == SelectorCombinator::NextSibling)
+					return false;
+			}
+		}
+		break;
 		}
 
 		// We have run out of element ancestors before we matched every node. Bail out.
@@ -316,7 +347,6 @@ bool StyleSheetNode::IsStructurallyVolatile() const
 	return is_structurally_volatile;
 }
 
-
 void StyleSheetNode::CalculateAndSetSpecificity()
 {
 	// Calculate the specificity of just this node; tags are worth 10,000, IDs 1,000,000 and other specifiers (classes
@@ -329,9 +359,9 @@ void StyleSheetNode::CalculateAndSetSpecificity()
 	if (!id.empty())
 		specificity += 1'000'000;
 
-	specificity += 100'000*(int)class_names.size();
-	specificity += 100'000*(int)pseudo_class_names.size();
-	specificity += 100'000*(int)structural_selectors.size();
+	specificity += 100'000 * (int)class_names.size();
+	specificity += 100'000 * (int)pseudo_class_names.size();
+	specificity += 100'000 * (int)structural_selectors.size();
 
 	// Add our parent's specificity onto ours.
 	if (parent)

+ 34 - 22
Source/Core/StyleSheetNode.h

@@ -15,7 +15,7 @@
  *
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
- * 
+ *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -45,28 +45,42 @@ struct StructuralSelector {
 	int a;
 	int b;
 };
-inline bool operator==(const StructuralSelector& a, const StructuralSelector& b) { return a.selector == b.selector && a.a == b.a && a.b == b.b; }
-inline bool operator<(const StructuralSelector& a, const StructuralSelector& b) { return std::tie(a.selector, a.a, a.b) < std::tie(b.selector, b.a, b.b); }
-
-using StructuralSelectorList = Vector< StructuralSelector >;
-using StyleSheetNodeList = Vector< UniquePtr<StyleSheetNode> >;
+inline bool operator==(const StructuralSelector& a, const StructuralSelector& b)
+{
+	return a.selector == b.selector && a.a == b.a && a.b == b.b;
+}
+inline bool operator<(const StructuralSelector& a, const StructuralSelector& b)
+{
+	return std::tie(a.selector, a.a, a.b) < std::tie(b.selector, b.a, b.b);
+}
+
+enum class SelectorCombinator : byte {
+	None,
+	Child,             // The 'E > F' combinator: Matches if F is a child of E.
+	NextSibling,       // The 'E + F' combinator: Matches if F is immediately preceded by E.
+	SubsequentSibling, // The 'E ~ F' combinator: Matches if F is preceded by E.
+};
 
+using StructuralSelectorList = Vector<StructuralSelector>;
+using StyleSheetNodeList = Vector<UniquePtr<StyleSheetNode>>;
 
 /**
-	A style sheet is composed of a tree of nodes.
+    A style sheet is composed of a tree of nodes.
 
-	@author Pete / Lloyd
+    @author Pete / Lloyd
  */
 
-class StyleSheetNode
-{
+class StyleSheetNode {
 public:
 	StyleSheetNode();
-	StyleSheetNode(StyleSheetNode* parent, const String& tag, const String& id, const StringList& classes, const StringList& pseudo_classes, const StructuralSelectorList& structural_selectors, bool child_combinator);
-	StyleSheetNode(StyleSheetNode* parent, String&& tag, String&& id, StringList&& classes, StringList&& pseudo_classes, StructuralSelectorList&& structural_selectors, bool child_combinator);
+	StyleSheetNode(StyleSheetNode* parent, const String& tag, const String& id, const StringList& classes, const StringList& pseudo_classes,
+		const StructuralSelectorList& structural_selectors, SelectorCombinator combinator);
+	StyleSheetNode(StyleSheetNode* parent, String&& tag, String&& id, StringList&& classes, StringList&& pseudo_classes,
+		StructuralSelectorList&& structural_selectors, SelectorCombinator combinator);
 
 	/// Retrieves a child node with the given requirements if they match an existing node, or else creates a new one.
-	StyleSheetNode* GetOrCreateChildNode(String&& tag, String&& id, StringList&& classes, StringList&& pseudo_classes, StructuralSelectorList&& structural_selectors, bool child_combinator);
+	StyleSheetNode* GetOrCreateChildNode(String&& tag, String&& id, StringList&& classes, StringList&& pseudo_classes,
+		StructuralSelectorList&& structural_selectors, SelectorCombinator combinator);
 	/// Retrieves or creates a child node with requirements equivalent to the 'other' node.
 	StyleSheetNode* GetOrCreateChildNode(const StyleSheetNode& other);
 
@@ -79,9 +93,8 @@ public:
 	/// Builds up a style sheet's index recursively.
 	void BuildIndex(StyleSheetIndex& styled_node_index) const;
 
-	/// Imports properties from a single rule definition into the node's properties and sets the
-	/// appropriate specificity on them. Any existing attributes sharing a key with a new attribute
-	/// will be overwritten if they are of a lower specificity.
+	/// Imports properties from a single rule definition into the node's properties and sets the appropriate specificity on them. Any existing
+	/// attributes sharing a key with a new attribute will be overwritten if they are of a lower specificity.
 	/// @param[in] properties The properties to import.
 	/// @param[in] rule_specificity The specificity of the importing rule.
 	void ImportProperties(const PropertyDictionary& properties, int rule_specificity);
@@ -93,14 +106,14 @@ public:
 
 	/// Returns the specificity of this node.
 	int GetSpecificity() const;
-	/// Returns true if this node employs a structural selector, and therefore generates element definitions that are
-	/// sensitive to sibling changes. 
+	/// Returns true if this node employs a structural selector, and therefore generates element definitions that are sensitive to sibling changes.
 	/// @warning Result is only valid if structural volatility is set since any changes to the node tree.
 	bool IsStructurallyVolatile() const;
 
 private:
 	// Returns true if the requirements of this node equals the given arguments.
-	bool EqualRequirements(const String& tag, const String& id, const StringList& classes, const StringList& pseudo_classes, const StructuralSelectorList& structural_pseudo_classes, bool child_combinator) const;
+	bool EqualRequirements(const String& tag, const String& id, const StringList& classes, const StringList& pseudo_classes,
+		const StructuralSelectorList& structural_pseudo_classes, SelectorCombinator combinator) const;
 
 	void CalculateAndSetSpecificity();
 
@@ -118,13 +131,12 @@ private:
 	StringList class_names;
 	StringList pseudo_class_names;
 	StructuralSelectorList structural_selectors; // Represents structural pseudo classes
-	bool child_combinator = false; // The '>' combinator: This node only matches if the element is a parent of the previous matching element.
+	SelectorCombinator combinator = SelectorCombinator::None;
 
 	// True if any ancestor, descendent, or self is a structural pseudo class.
 	bool is_structurally_volatile = true;
 
-	// A measure of specificity of this node; the attribute in a node with a higher value will override those of a
-	// node with a lower value.
+	// A measure of specificity of this node; the attribute in a node with a higher value will override those of a node with a lower value.
 	int specificity = 0;
 
 	PropertyDictionary properties;

+ 60 - 34
Source/Core/StyleSheetParser.cpp

@@ -876,20 +876,26 @@ StyleSheetNode* StyleSheetParser::ImportProperties(StyleSheetNode* node, String
 
 	StringList nodes;
 
-	// Find child combinators, the RCSS '>' rule.
-	size_t i_child = rule_name.find('>');
-	while (i_child != String::npos)
+	// Combinator rules can be formatted several ways by users, here we ensure consistent formatting before we can continue with the parsing below.
+	// E.g. converts combinations such as "A > B", "A >B", "A>B"  to  "A> B".
+	for (const char combinator : {'>', '+', '~'})
 	{
-		// So we found one! Next, we want to format the rule such that the '>' is located at the 
-		// end of the left-hand-side node, and that there is a space to the right-hand-side. This ensures that
-		// the selector is applied to the "parent", and that parent and child are expanded properly below.
-		size_t i_begin = i_child;
-		while (i_begin > 0 && rule_name[i_begin - 1] == ' ')
-			i_begin--;
-
-		const size_t i_end = i_child + 1;
-		rule_name.replace(i_begin, i_end - i_begin, "> ");
-		i_child = rule_name.find('>', i_begin + 1);
+		// Find all combinators of the given type.
+		size_t i_child = rule_name.find(combinator);
+		while (i_child != String::npos)
+		{
+			// So we found one! Next, we want to format the rule such that e.g. the '>' is located at the
+			// end of the left-hand-side node, and that there is a space to the right-hand-side. This ensures that
+			// the selector is applied to the "parent", and that parent and child are expanded properly below.
+			size_t i_begin = i_child;
+			while (i_begin > 0 && rule_name[i_begin - 1] == ' ')
+				i_begin--;
+
+			const size_t i_end = i_child + 1;
+			const char replacement_str[] = {combinator, ' ', '\0'};
+			rule_name.replace(i_begin, i_end - i_begin, (const char*)replacement_str);
+			i_child = rule_name.find(combinator, i_begin + 1);
+		}
 	}
 
 	// Expand each individual node separated by spaces. Don't expand inside parenthesis because of structural selectors.
@@ -905,42 +911,62 @@ StyleSheetNode* StyleSheetParser::ImportProperties(StyleSheetNode* node, String
 		StringList classes;
 		StringList pseudo_classes;
 		StructuralSelectorList structural_pseudo_classes;
-		bool child_combinator = false;
+		SelectorCombinator combinator = SelectorCombinator::None;
 
 		size_t index = 0;
 		while (index < name.size())
 		{
 			size_t start_index = index;
 			size_t end_index = index + 1;
+			int parenthesis_count = 0;
 
 			// Read until we hit the next identifier.
-			while (end_index < name.size() &&
-				   name[end_index] != '#' &&
-				   name[end_index] != '.' &&
-				   name[end_index] != ':' &&
-				   name[end_index] != '>')
-				end_index++;
+			for (; end_index < name.size(); end_index++)
+			{
+				static const String identifiers = "#.:>+~";
+				if (parenthesis_count == 0 && identifiers.find(name[end_index]) != String::npos)
+					break;
+
+				if (name[end_index] == '(')
+					parenthesis_count += 1;
+				else if (name[end_index] == ')')
+					parenthesis_count -= 1;
+			}
 
 			String identifier = name.substr(start_index, end_index - start_index);
 			if (!identifier.empty())
 			{
 				switch (identifier[0])
 				{
-					case '#':	id = identifier.substr(1); break;
-					case '.':	classes.push_back(identifier.substr(1)); break;
-					case ':':
-					{
-						String pseudo_class_name = identifier.substr(1);
-						StructuralSelector node_selector = StyleSheetFactory::GetSelector(pseudo_class_name);
-						if (node_selector.selector)
-							structural_pseudo_classes.push_back(node_selector);
-						else
-							pseudo_classes.push_back(pseudo_class_name);
-					}
+				case '#':
+					id = identifier.substr(1);
+					break;
+				case '.':
+					classes.push_back(identifier.substr(1));
+					break;
+				case ':':
+				{
+					String pseudo_class_name = identifier.substr(1);
+					StructuralSelector node_selector = StyleSheetFactory::GetSelector(pseudo_class_name);
+					if (node_selector.selector)
+						structural_pseudo_classes.push_back(node_selector);
+					else
+						pseudo_classes.push_back(pseudo_class_name);
+				}
+				break;
+				case '>':
+					combinator = SelectorCombinator::Child;
+					break;
+				case '+':
+					combinator = SelectorCombinator::NextSibling;
+					break;
+				case '~':
+					combinator = SelectorCombinator::SubsequentSibling;
 					break;
-					case '>':	child_combinator = true; break;
 
-					default:	if(identifier != "*") tag = identifier;
+				default:
+					if (identifier != "*")
+						tag = identifier;
 				}
 			}
 
@@ -953,7 +979,7 @@ StyleSheetNode* StyleSheetParser::ImportProperties(StyleSheetNode* node, String
 		std::sort(structural_pseudo_classes.begin(), structural_pseudo_classes.end());
 
 		// Get the named child node.
-		leaf_node = leaf_node->GetOrCreateChildNode(std::move(tag), std::move(id), std::move(classes), std::move(pseudo_classes), std::move(structural_pseudo_classes), child_combinator);
+		leaf_node = leaf_node->GetOrCreateChildNode(std::move(tag), std::move(id), std::move(classes), std::move(pseudo_classes), std::move(structural_pseudo_classes), combinator);
 	}
 
 	// Merge the new properties with those already on the leaf node.

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

@@ -115,7 +115,7 @@ static const Vector<QuerySelector> query_selectors =
 	{ "body span:first-child",       "D0 F0" },
 	{ "body > p span:first-child",   "" },
 	{ "body > div span:first-child", "D0 F0" },
-	{ ":nth-child(4) * span:first-child", "D0 F0", SelectorOp::RemoveElementsByIds,  "X",    "" },
+	{ ":nth-child(4) * span:first-child", "D0 F0",      SelectorOp::RemoveElementsByIds,  "X",    "" },
 	{ "p:nth-last-of-type(3n+1)",    "D H" },
 	{ ":first-of-type",              "X Y A B D0 E F0 I" },
 	{ ":last-of-type",               "Y P A D1 E F0 H I" },
@@ -124,6 +124,20 @@ static const Vector<QuerySelector> query_selectors =
 	{ "span:empty",                  "Y D0 D1 F0" },
 	{ ".hello.world, #P span, #I",   "Z D0 D1 F0 I",    SelectorOp::RemoveClasses,        "world", "D0 D1 F0 I" },
 	{ "body * span",                 "D0 D1 F0" },
+	{ "#E + #F",                     "F",               SelectorOp::InsertElementBefore,  "F",     "" },
+	{ "#E+#F",                       "F" },
+	{ "#E +#F",                      "F" },
+	{ "#E+ #F",                      "F" },
+	{ "#F + #E",                     "" },
+	{ "div.parent > #B + p",         "C" },
+	{ "div.parent > #B + div",       "" },
+	{ "#B ~ #F",                     "F" },
+	{ "#B~#F",                       "F" },
+	{ "#B ~#F",                      "F" },
+	{ "#B~ #F",                      "F" },
+	{ "#F ~ #B",                     "" },
+	{ "div.parent > #B ~ p:empty",   "C G H",           SelectorOp::InsertElementBefore,  "H",     "C G Inserted H" },
+	{ "div.parent > #B ~ * span",    "D0 D1 F0" },
 };
 
 struct ClosestSelector {