Kaynağa Gözat

RCSS attribute selector support (based on #240)

Co-authored-by: aquawicket <[email protected]>
Michael Ragazzon 3 yıl önce
ebeveyn
işleme
a96164c451

+ 124 - 79
Source/Core/StyleSheetNode.cpp

@@ -33,6 +33,7 @@
 #include "StyleSheetFactory.h"
 #include "StyleSheetSelector.h"
 #include <algorithm>
+#include <tuple>
 
 namespace Rml {
 
@@ -46,35 +47,27 @@ StyleSheetNode::StyleSheetNode()
 	CalculateAndSetSpecificity();
 }
 
-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)
+StyleSheetNode::StyleSheetNode(StyleSheetNode* parent, const CompoundSelector& selector) : parent(parent), selector(selector)
 {
 	CalculateAndSetSpecificity();
 }
 
-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)
+StyleSheetNode::StyleSheetNode(StyleSheetNode* parent, CompoundSelector&& selector) : parent(parent), selector(std::move(selector))
 {
 	CalculateAndSetSpecificity();
 }
 
-StyleSheetNode* StyleSheetNode::GetOrCreateChildNode(const StyleSheetNode& other)
+StyleSheetNode* StyleSheetNode::GetOrCreateChildNode(const CompoundSelector& other)
 {
-	// See if we match the target child
+	// See if we match an existing child
 	for (const auto& child : children)
 	{
-		if (child->EqualRequirements(other.tag, other.id, other.class_names, other.pseudo_class_names, other.structural_selectors, other.combinator))
+		if (child->selector == other)
 			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.combinator);
+	auto child = MakeUnique<StyleSheetNode>(this, other);
 	StyleSheetNode* result = child.get();
 
 	children.push_back(std::move(child));
@@ -82,19 +75,17 @@ StyleSheetNode* StyleSheetNode::GetOrCreateChildNode(const StyleSheetNode& other
 	return result;
 }
 
-StyleSheetNode* StyleSheetNode::GetOrCreateChildNode(String&& tag, String&& id, StringList&& classes, StringList&& pseudo_classes,
-	StructuralSelectorList&& structural_pseudo_classes, SelectorCombinator combinator)
+StyleSheetNode* StyleSheetNode::GetOrCreateChildNode(CompoundSelector&& other)
 {
 	// See if we match an existing child
 	for (const auto& child : children)
 	{
-		if (child->EqualRequirements(tag, id, classes, pseudo_classes, structural_pseudo_classes, combinator))
+		if (child->selector == other)
 			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), combinator);
+	auto child = MakeUnique<StyleSheetNode>(this, std::move(other));
 	StyleSheetNode* result = child.get();
 
 	children.push_back(std::move(child));
@@ -112,7 +103,7 @@ void StyleSheetNode::MergeHierarchy(StyleSheetNode* node, int specificity_offset
 
 	for (const auto& other_child : node->children)
 	{
-		StyleSheetNode* local_node = GetOrCreateChildNode(*other_child);
+		StyleSheetNode* local_node = GetOrCreateChildNode(other_child->selector);
 		local_node->MergeHierarchy(other_child.get(), specificity_offset);
 	}
 }
@@ -121,7 +112,7 @@ 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, combinator);
+	auto node = MakeUnique<StyleSheetNode>(in_parent, selector);
 
 	node->properties = properties;
 	node->children.resize(children.size());
@@ -149,19 +140,19 @@ void StyleSheetNode::BuildIndex(StyleSheetIndex& styled_node_index) const
 
 		// Add this node to the appropriate index for looking up applicable nodes later. Prioritize the most unique requirement first and the most
 		// general requirement last. This way we are able to rule out as many nodes as possible as quickly as possible.
-		if (!id.empty())
+		if (!selector.id.empty())
 		{
-			IndexInsertNode(styled_node_index.ids, id, this);
+			IndexInsertNode(styled_node_index.ids, selector.id, this);
 		}
-		else if (!class_names.empty())
+		else if (!selector.class_names.empty())
 		{
 			// @performance Right now we just use the first class for simplicity. Later we may want to devise a better strategy to try to add the
 			// class with the most unique name. For example by adding the class from this node's list that has the fewest existing matches.
-			IndexInsertNode(styled_node_index.classes, class_names.front(), this);
+			IndexInsertNode(styled_node_index.classes, selector.class_names.front(), this);
 		}
-		else if (!tag.empty())
+		else if (!selector.tag.empty())
 		{
-			IndexInsertNode(styled_node_index.tags, tag, this);
+			IndexInsertNode(styled_node_index.tags, selector.tag, this);
 		}
 		else
 		{
@@ -173,25 +164,6 @@ void StyleSheetNode::BuildIndex(StyleSheetIndex& styled_node_index) const
 		child->BuildIndex(styled_node_index);
 }
 
-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;
-	if (id != _id)
-		return false;
-	if (class_names != _class_names)
-		return false;
-	if (pseudo_class_names != _pseudo_class_names)
-		return false;
-	if (structural_selectors != _structural_selectors)
-		return false;
-	if (combinator != _combinator)
-		return false;
-
-	return true;
-}
-
 // Returns the specificity of this node.
 int StyleSheetNode::GetSpecificity() const
 {
@@ -213,41 +185,36 @@ const PropertyDictionary& StyleSheetNode::GetProperties() const
 
 bool StyleSheetNode::Match(const Element* element) const
 {
-	if (!tag.empty() && tag != element->GetTagName())
-		return false;
-
-	if (!id.empty() && id != element->GetId())
+	if (!selector.tag.empty() && selector.tag != element->GetTagName())
 		return false;
 
-	if (!MatchClassPseudoClass(element))
+	if (!selector.id.empty() && selector.id != element->GetId())
 		return false;
 
-	if (!MatchStructuralSelector(element))
-		return false;
-
-	return true;
-}
-
-inline bool StyleSheetNode::MatchClassPseudoClass(const Element* element) const
-{
-	for (auto& name : class_names)
+	for (auto& name : selector.class_names)
 	{
 		if (!element->IsClassSet(name))
 			return false;
 	}
 
-	for (auto& name : pseudo_class_names)
+	for (auto& name : selector.pseudo_class_names)
 	{
 		if (!element->IsPseudoClassSet(name))
 			return false;
 	}
 
+	if (!selector.attributes.empty() && !MatchAttributes(element))
+		return false;
+
+	if (!selector.structural_selectors.empty() && !MatchStructuralSelector(element))
+		return false;
+
 	return true;
 }
 
-inline bool StyleSheetNode::MatchStructuralSelector(const Element* element) const
+bool StyleSheetNode::MatchStructuralSelector(const Element* element) const
 {
-	for (auto& node_selector : structural_selectors)
+	for (auto& node_selector : selector.structural_selectors)
 	{
 		if (!IsSelectorApplicable(element, node_selector))
 			return false;
@@ -256,13 +223,87 @@ inline bool StyleSheetNode::MatchStructuralSelector(const Element* element) cons
 	return true;
 }
 
+bool StyleSheetNode::MatchAttributes(const Element* element) const
+{
+	for (const AttributeSelector& attribute : selector.attributes)
+	{
+		const Variant* variant = element->GetAttribute(attribute.name);
+		if (!variant)
+			return false;
+		if (attribute.type == AttributeSelectorType::Always)
+			continue;
+
+		String buffer;
+		const String* element_value_ptr = &buffer;
+		if (variant->GetType() == Variant::STRING)
+			element_value_ptr = &variant->GetReference<String>();
+		else
+			variant->GetInto(buffer);
+
+		const String& element_value = *element_value_ptr;
+		const String& css_value = attribute.value;
+
+		auto BeginsWith = [](const String& target, const String& prefix) {
+			return prefix.size() <= target.size() && std::equal(prefix.begin(), prefix.end(), target.begin());
+		};
+		auto EndsWith = [](const String& target, const String& suffix) {
+			return suffix.size() <= target.size() && std::equal(suffix.rbegin(), suffix.rend(), target.rbegin());
+		};
+
+		switch (attribute.type)
+		{
+		case AttributeSelectorType::Always: break;
+		case AttributeSelectorType::Equal:
+			if (element_value != css_value)
+				return false;
+			break;
+		case AttributeSelectorType::InList:
+		{
+			bool found = false;
+			for (size_t index = element_value.find(css_value); index != String::npos; index = element_value.find(css_value, index + 1))
+			{
+				const size_t index_right = index + css_value.size();
+				const bool whitespace_left = (index == 0 || element_value[index - 1] == ' ');
+				const bool whitespace_right = (index_right == element_value.size() || element_value[index_right] == ' ');
+
+				if (whitespace_left && whitespace_right)
+				{
+					found = true;
+					break;
+				}
+			}
+			if (!found)
+				return false;
+		}
+		break;
+		case AttributeSelectorType::BeginsWithThenHyphen:
+			if (!BeginsWith(element_value, css_value) || element_value[css_value.size()] != '-')
+				return false;
+			break;
+		case AttributeSelectorType::BeginsWith:
+			if (!BeginsWith(element_value, css_value))
+				return false;
+			break;
+		case AttributeSelectorType::EndsWith:
+			if (!EndsWith(element_value, css_value))
+				return false;
+			break;
+		case AttributeSelectorType::Contains:
+			if (element_value.find(css_value) == String::npos)
+				return false;
+			break;
+		}
+	}
+	return true;
+}
+
 bool StyleSheetNode::TraverseMatch(const Element* element) const
 {
 	RMLUI_ASSERT(parent);
 	if (!parent->parent)
 		return true;
 
-	switch (combinator)
+	switch (selector.combinator)
 	{
 	case SelectorCombinator::Descendant:
 	case SelectorCombinator::Child:
@@ -274,7 +315,7 @@ bool StyleSheetNode::TraverseMatch(const Element* element) const
 			if (parent->Match(element) && parent->TraverseMatch(element))
 				return true;
 			// If the node has a child combinator we must match this first ancestor.
-			else if (combinator == SelectorCombinator::Child)
+			else if (selector.combinator == SelectorCombinator::Child)
 				return false;
 		}
 	}
@@ -292,7 +333,7 @@ bool StyleSheetNode::TraverseMatch(const Element* element) const
 			else if (parent->Match(element) && parent->TraverseMatch(element))
 				return true;
 			// If the node has a next-sibling combinator we must match this first sibling.
-			else if (combinator == SelectorCombinator::NextSibling)
+			else if (selector.combinator == SelectorCombinator::NextSibling)
 				return false;
 		}
 	}
@@ -310,30 +351,33 @@ bool StyleSheetNode::IsApplicable(const Element* element) const
 
 	// We could in principle just call Match() here and then go on with the ancestor style nodes. Instead, we test the requirements of this node in a
 	// particular order for performance reasons.
-	for (const String& name : pseudo_class_names)
+	for (const String& name : selector.pseudo_class_names)
 	{
 		if (!element->IsPseudoClassSet(name))
 			return false;
 	}
 
-	if (!tag.empty() && tag != element->GetTagName())
+	if (!selector.tag.empty() && selector.tag != element->GetTagName())
 		return false;
 
-	for (const String& name : class_names)
+	for (const String& name : selector.class_names)
 	{
 		if (!element->IsClassSet(name))
 			return false;
 	}
 
-	if (!id.empty() && id != element->GetId())
+	if (!selector.id.empty() && selector.id != element->GetId())
 		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 (!selector.attributes.empty() && !MatchAttributes(element))
+		return false;
+
+	// Check the structural selector requirements last as they can be quite slow.
+	if (!selector.structural_selectors.empty() && !MatchStructuralSelector(element))
 		return false;
 
-	// Finally, check the structural selector requirements last as they can be quite slow.
-	if (!MatchStructuralSelector(element))
+	// Walk up through all our parent nodes, each one of them must be matched by some ancestor or sibling element.
+	if (parent && !TraverseMatch(element))
 		return false;
 
 	return true;
@@ -344,16 +388,17 @@ void StyleSheetNode::CalculateAndSetSpecificity()
 	// First calculate the specificity of this node alone.
 	specificity = 0;
 
-	if (!tag.empty())
+	if (!selector.tag.empty())
 		specificity += SelectorSpecificity::Tag;
 
-	if (!id.empty())
+	if (!selector.id.empty())
 		specificity += SelectorSpecificity::ID;
 
-	specificity += SelectorSpecificity::Class * (int)class_names.size();
-	specificity += SelectorSpecificity::PseudoClass * (int)pseudo_class_names.size();
+	specificity += SelectorSpecificity::Class * (int)selector.class_names.size();
+	specificity += SelectorSpecificity::Attribute * (int)selector.attributes.size();
+	specificity += SelectorSpecificity::PseudoClass * (int)selector.pseudo_class_names.size();
 
-	for (const StructuralSelector& selector : structural_selectors)
+	for (const StructuralSelector& selector : selector.structural_selectors)
 		specificity += selector.specificity;
 
 	// Then add our parent's specificity onto ours.

+ 7 - 20
Source/Core/StyleSheetNode.h

@@ -37,7 +37,6 @@ namespace Rml {
 
 struct StyleSheetIndex;
 class StyleSheetNode;
-using StructuralSelectorList = Vector<StructuralSelector>;
 using StyleSheetNodeList = Vector<UniquePtr<StyleSheetNode>>;
 
 /**
@@ -49,16 +48,13 @@ using StyleSheetNodeList = Vector<UniquePtr<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, SelectorCombinator combinator);
-	StyleSheetNode(StyleSheetNode* parent, String&& tag, String&& id, StringList&& classes, StringList&& pseudo_classes,
-		StructuralSelectorList&& structural_selectors, SelectorCombinator combinator);
+	StyleSheetNode(StyleSheetNode* parent, const CompoundSelector& selector);
+	StyleSheetNode(StyleSheetNode* parent, CompoundSelector&& selector);
 
-	/// 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, SelectorCombinator combinator);
 	/// Retrieves or creates a child node with requirements equivalent to the 'other' node.
-	StyleSheetNode* GetOrCreateChildNode(const StyleSheetNode& other);
+	StyleSheetNode* GetOrCreateChildNode(const CompoundSelector& other);
+	/// Retrieves a child node with the given requirements if they match an existing node, or else creates a new one.
+	StyleSheetNode* GetOrCreateChildNode(CompoundSelector&& other);
 
 	/// Merges an entire tree hierarchy into our hierarchy.
 	void MergeHierarchy(StyleSheetNode* node, int specificity_offset = 0);
@@ -84,16 +80,12 @@ public:
 	int GetSpecificity() 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, SelectorCombinator combinator) const;
-
 	void CalculateAndSetSpecificity();
 
 	// Match an element to the local node requirements.
 	inline bool Match(const Element* element) const;
-	inline bool MatchClassPseudoClass(const Element* element) const;
 	inline bool MatchStructuralSelector(const Element* element) 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;
@@ -102,12 +94,7 @@ private:
 	StyleSheetNode* parent = nullptr;
 
 	// Node requirements
-	String tag;
-	String id;
-	StringList class_names;
-	StringList pseudo_class_names;
-	StructuralSelectorList structural_selectors;                    // Represents structural pseudo classes
-	SelectorCombinator combinator = SelectorCombinator::Descendant; // Determines how to match with our parent node.
+	CompoundSelector selector;
 
 	// 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;

+ 88 - 39
Source/Core/StyleSheetParser.cpp

@@ -565,7 +565,11 @@ bool StyleSheetParser::Parse(MediaBlockList& style_sheets, Stream* _stream, int
 					{
 						auto source = MakeShared<PropertySource>(stream_file_name, rule_line_number, rule_name_list[i]);
 						properties.SetSourceOfAllProperties(source);
-						ImportProperties(current_block.stylesheet->root.get(), rule_name_list[i], properties, rule_count);
+						if (!ImportProperties(current_block.stylesheet->root.get(), rule_name_list[i], properties, rule_count))
+						{
+							Log::Message(Log::LT_WARNING, "Invalid selector '%s' encountered while parsing stylesheet at %s:%d.",
+								rule_name_list[i].c_str(), stream_file_name.c_str(), line_number);
+						}
 					}
 
 					rule_count++;
@@ -762,7 +766,9 @@ StyleSheetNodeListRaw StyleSheetParser::ConstructNodes(StyleSheetNode& root_node
 	{
 		StyleSheetNode* leaf_node = ImportProperties(&root_node, selector, empty_properties, 0);
 
-		if (leaf_node != &root_node)
+		if (!leaf_node)
+			Log::Message(Log::LT_WARNING, "Invalid selector '%s' encountered.", selector.c_str());
+		else if (leaf_node != &root_node)
 			leaf_nodes.push_back(leaf_node);
 	}
 
@@ -878,70 +884,113 @@ StyleSheetNode* StyleSheetParser::ImportProperties(StyleSheetNode* node, const S
 	// Create each node going down the tree.
 	for (size_t index = 0; index < rule.size();)
 	{
+		CompoundSelector selector;
+
 		// Determine the combinator connecting the previous node if any.
-		SelectorCombinator combinator = SelectorCombinator::Descendant;
 		for (; index > 0 && index < rule.size(); index++)
 		{
 			bool reached_end_of_combinators = false;
 			switch (rule[index])
 			{
 			case ' ': break;
-			case '>': combinator = SelectorCombinator::Child; break;
-			case '+': combinator = SelectorCombinator::NextSibling; break;
-			case '~': combinator = SelectorCombinator::SubsequentSibling; break;
+			case '>': selector.combinator = SelectorCombinator::Child; break;
+			case '+': selector.combinator = SelectorCombinator::NextSibling; break;
+			case '~': selector.combinator = SelectorCombinator::SubsequentSibling; break;
 			default: reached_end_of_combinators = true; break;
 			}
 			if (reached_end_of_combinators)
 				break;
 		}
 
-		String tag;
-		String id;
-		StringList classes;
-		StringList pseudo_classes;
-		StructuralSelectorList structural_pseudo_classes;
-
 		// Determine the node's requirements.
 		while (index < rule.size())
 		{
 			size_t start_index = index;
 			size_t end_index = index + 1;
-			int parenthesis_count = 0;
 
-			// Read until we hit the next identifier. Don't match inside parenthesis in case of structural selectors.
-			for (; end_index < rule.size(); end_index++)
-			{
-				static const String identifiers = "#.: >+~";
-				if (parenthesis_count == 0 && identifiers.find(rule[end_index]) != String::npos)
-					break;
+			if (rule[start_index] == '*')
+				start_index += 1;
 
-				if (rule[end_index] == '(')
-					parenthesis_count += 1;
-				else if (rule[end_index] == ')')
-					parenthesis_count -= 1;
+			if (rule[start_index] == '[')
+			{
+				end_index = rule.find(']', start_index + 1);
+				if (end_index == String::npos)
+					return nullptr;
+				end_index += 1;
 			}
+			else
+			{
+				int parenthesis_count = 0;
 
-			if (rule[start_index] == '*')
-				start_index += 1;
+				// Read until we hit the next identifier. Don't match inside parenthesis in case of structural selectors.
+				for (; end_index < rule.size(); end_index++)
+				{
+					static const String identifiers = "#.:[ >+~";
+					if (parenthesis_count == 0 && identifiers.find(rule[end_index]) != String::npos)
+						break;
+
+					if (rule[end_index] == '(')
+						parenthesis_count += 1;
+					else if (rule[end_index] == ')')
+						parenthesis_count -= 1;
+				}
+			}
 
-			StringView identifier = StringView(rule, start_index, end_index - start_index);
-			if (identifier.size() > 0)
+			if (end_index > start_index)
 			{
-				switch (*identifier.begin())
+				const char* p_begin = rule.data() + start_index;
+				const char* p_end = rule.data() + end_index;
+
+				switch (rule[start_index])
 				{
-				case '#': id = String(identifier.begin() + 1, identifier.end()); break;
-				case '.': classes.push_back(String(identifier.begin() + 1, identifier.end())); break;
+				case '#': selector.id = String(p_begin + 1, p_end); break;
+				case '.': selector.class_names.push_back(String(p_begin + 1, p_end)); break;
 				case ':':
 				{
-					String pseudo_class_name = String(identifier.begin() + 1, identifier.end());
+					String pseudo_class_name = String(p_begin + 1, p_end);
 					StructuralSelector node_selector = StyleSheetFactory::GetSelector(pseudo_class_name);
 					if (node_selector.type != StructuralSelectorType::Invalid)
-						structural_pseudo_classes.push_back(node_selector);
+						selector.structural_selectors.push_back(node_selector);
 					else
-						pseudo_classes.push_back(std::move(pseudo_class_name));
+						selector.pseudo_class_names.push_back(std::move(pseudo_class_name));
+				}
+				break;
+				case '[':
+				{
+					const size_t i_attr_begin = start_index + 1;
+					const size_t i_attr_end = end_index - 1;
+					if (i_attr_end <= i_attr_begin)
+						return nullptr;
+
+					AttributeSelector attribute;
+
+					static const String attribute_operators = "=~|^$*]";
+					size_t i_cursor = Math::Min(rule.find_first_of(attribute_operators, i_attr_begin), i_attr_end);
+					attribute.name = rule.substr(i_attr_begin, i_cursor - i_attr_begin);
+
+					if (i_cursor < i_attr_end)
+					{
+						const char c = rule[i_cursor];
+						attribute.type = AttributeSelectorType(c);
+
+						// Move cursor past operator. Non-'=' symbols are always followed by '=' so move two characters.
+						i_cursor += (c == '=' ? 1 : 2);
+
+						size_t i_value_end = i_attr_end;
+						if (i_cursor < i_attr_end && (rule[i_cursor] == '"' || rule[i_cursor] == '\''))
+						{
+							i_cursor += 1;
+							i_value_end -= 1;
+						}
+
+						if (i_cursor < i_value_end)
+							attribute.value = rule.substr(i_cursor, i_value_end - i_cursor);
+					}
+
+					selector.attributes.push_back(std::move(attribute));
 				}
 				break;
-				default: tag = String(identifier);
+				default: selector.tag = String(p_begin, p_end); break;
 				}
 			}
 
@@ -954,13 +1003,13 @@ StyleSheetNode* StyleSheetParser::ImportProperties(StyleSheetNode* node, const S
 		}
 
 		// Sort the classes and pseudo-classes so they are consistent across equivalent declarations that shuffle the order around.
-		std::sort(classes.begin(), classes.end());
-		std::sort(pseudo_classes.begin(), pseudo_classes.end());
-		std::sort(structural_pseudo_classes.begin(), structural_pseudo_classes.end());
+		std::sort(selector.class_names.begin(), selector.class_names.end());
+		std::sort(selector.attributes.begin(), selector.attributes.end());
+		std::sort(selector.pseudo_class_names.begin(), selector.pseudo_class_names.end());
+		std::sort(selector.structural_selectors.begin(), selector.structural_selectors.end());
 
 		// Add the new child node, or retrieve the existing child if we have an exact match.
-		leaf_node = leaf_node->GetOrCreateChildNode(std::move(tag), std::move(id), std::move(classes), std::move(pseudo_classes),
-			std::move(structural_pseudo_classes), combinator);
+		leaf_node = leaf_node->GetOrCreateChildNode(std::move(selector));
 	}
 
 	// Merge the new properties with those already on the leaf node.

+ 1 - 1
Source/Core/StyleSheetParser.h

@@ -99,7 +99,7 @@ private:
 	// @param rule The rule name to parse
 	// @param properties The dictionary of properties
 	// @param rule_specificity The specifity of the rule
-	// @return The leaf node of the rule
+	// @return The leaf node of the rule, or nullptr on parse failure.
 	static StyleSheetNode* ImportProperties(StyleSheetNode* node, const String& rule, const PropertyDictionary& properties, int rule_specificity);
 
 	// Attempts to parse a @keyframes block

+ 42 - 0
Source/Core/StyleSheetSelector.cpp

@@ -29,6 +29,7 @@
 #include "StyleSheetSelector.h"
 #include "../../Include/RmlUi/Core/Element.h"
 #include "StyleSheetNode.h"
+#include <tuple>
 
 namespace Rml {
 
@@ -48,6 +49,47 @@ static bool IsNth(int a, int b, int count)
 	return (x >= 0 && x * a + b == count);
 }
 
+bool operator==(const AttributeSelector& a, const AttributeSelector& b)
+{
+	return a.type == b.type && a.name == b.name && a.value == b.value;
+}
+bool operator<(const AttributeSelector& a, const AttributeSelector& b)
+{
+	return std::tie(a.type, a.name, a.value) < std::tie(b.type, b.name, b.value);
+}
+
+bool operator==(const StructuralSelector& a, const StructuralSelector& 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;
+}
+bool operator<(const StructuralSelector& a, const StructuralSelector& b)
+{
+	return std::tie(a.type, a.a, a.b, a.selector_tree) < std::tie(b.type, b.a, b.b, b.selector_tree);
+}
+
+bool operator==(const CompoundSelector& a, const CompoundSelector& b)
+{
+	if (a.tag != b.tag)
+		return false;
+	if (a.id != b.id)
+		return false;
+	if (a.class_names != b.class_names)
+		return false;
+	if (a.pseudo_class_names != b.pseudo_class_names)
+		return false;
+	if (a.attributes != b.attributes)
+		return false;
+	if (a.structural_selectors != b.structural_selectors)
+		return false;
+	if (a.combinator != b.combinator)
+		return false;
+	return true;
+}
+
 bool IsSelectorApplicable(const Element* element, const StructuralSelector& selector)
 {
 	RMLUI_ASSERT(element);

+ 66 - 27
Source/Core/StyleSheetSelector.h

@@ -30,24 +30,71 @@
 #define RMLUI_CORE_STYLESHEETSELECTOR_H
 
 #include "../../Include/RmlUi/Core/Types.h"
-#include <tuple>
 
 namespace Rml {
 
 class Element;
 class StyleSheetNode;
-struct SelectorTree;
 
+/**
+    Constants used to determine the specificity of a selector.
+ */
 namespace SelectorSpecificity {
 	enum {
-		// Constants used to determine the specificity of a selector.
 		Tag = 10'000,
 		Class = 100'000,
+		Attribute = Class,
 		PseudoClass = Class,
 		ID = 1'000'000,
 	};
 }
 
+/**
+    Combinator determines how two connected compound selectors are matched against the element hierarchy.
+ */
+enum class SelectorCombinator {
+	Descendant,        // The 'E F' (whitespace) combinator: Matches if F is a descendant of E.
+	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.
+};
+
+/**
+    Attribute selector.
+
+    Such as [unit], [unit=meter], [href^=http]
+ */
+enum class AttributeSelectorType {
+	Always,
+	Equal = '=',
+	InList = '~',
+	BeginsWithThenHyphen = '|',
+	BeginsWith = '^',
+	EndsWith = '$',
+	Contains = '*',
+};
+struct AttributeSelector {
+	AttributeSelectorType type = AttributeSelectorType::Always;
+	String name;
+	String value;
+};
+bool operator==(const AttributeSelector& a, const AttributeSelector& b);
+bool operator<(const AttributeSelector& a, const AttributeSelector& b);
+using AttributeSelectorList = Vector<AttributeSelector>;
+
+/**
+    A tree of unstyled style sheet nodes.
+ */
+struct SelectorTree {
+	UniquePtr<StyleSheetNode> root;
+	Vector<StyleSheetNode*> leafs; // Owned by root.
+};
+
+/**
+    Tree-structural selector.
+
+    Such as :nth-child(2n+1), :empty(), :not(div)
+ */
 enum class StructuralSelectorType {
 	Invalid,
 	Nth_Child,
@@ -63,7 +110,6 @@ enum class StructuralSelectorType {
 	Empty,
 	Not
 };
-
 struct StructuralSelector {
 	StructuralSelector(StructuralSelectorType type, int a, int b) : type(type), a(a), b(b) {}
 	StructuralSelector(StructuralSelectorType type, SharedPtr<const SelectorTree> tree, int specificity) :
@@ -82,32 +128,25 @@ struct StructuralSelector {
 	// For selectors that contain internal selectors such as :not().
 	SharedPtr<const SelectorTree> selector_tree;
 };
+bool operator==(const StructuralSelector& a, const StructuralSelector& b);
+bool operator<(const StructuralSelector& a, const StructuralSelector& b);
+using StructuralSelectorList = Vector<StructuralSelector>;
 
-inline bool operator==(const StructuralSelector& a, const StructuralSelector& 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)
-{
-	return std::tie(a.type, a.a, a.b, a.selector_tree) < std::tie(b.type, b.a, b.b, b.selector_tree);
-}
+/**
+    Compound selector contains all the basic selectors for a single node.
 
-// A tree of unstyled style sheet nodes.
-struct SelectorTree {
-	UniquePtr<StyleSheetNode> root;
-	Vector<StyleSheetNode*> leafs; // Owned by root.
-};
-
-enum class SelectorCombinator : byte {
-	Descendant,        // The 'E F' (whitespace) combinator: Matches if F is a descendant of E.
-	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.
+    Such as div#foo.bar:nth-child(2)
+ */
+struct CompoundSelector {
+	String tag;
+	String id;
+	StringList class_names;
+	StringList pseudo_class_names;
+	AttributeSelectorList attributes;
+	StructuralSelectorList structural_selectors;
+	SelectorCombinator combinator = SelectorCombinator::Descendant; // Determines how to match with our parent node.
 };
+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.
 /// @param element[in] The element to determine node applicability for.

+ 12 - 1
Tests/Source/Benchmarks/Selectors.cpp

@@ -98,7 +98,8 @@ enum SelectorFlags {
 	ID = 1 << 1,
 	CLASS = 1 << 2,
 	PSEUDO_CLASS = 1 << 3,
-	NUM_COMBINATIONS = 1 << 4,
+	ATTRIBUTE = 1 << 4,
+	NUM_COMBINATIONS = 1 << 5,
 };
 
 static String GenerateRCSS(SelectorFlags selectors, const String& complex_selector, String& out_rule_name)
@@ -131,6 +132,8 @@ static String GenerateRCSS(SelectorFlags selectors, const String& complex_select
 				rule += '.' + name;
 			if (selectors & PSEUDO_CLASS)
 				rule += ':' + name;
+			if (selectors & ATTRIBUTE)
+				rule += '[' + name + ']';
 		}
 
 		return rule;
@@ -221,6 +224,7 @@ TEST_CASE("Selectors")
 	bench.relative(true);
 
 	const Vector<String> complex_selectors = {
+		"*",
 		"div",
 		"div div",
 		"div > div",
@@ -232,6 +236,13 @@ TEST_CASE("Selectors")
 		":nth-child(2n+3) div",
 		":nth-of-type(2n+3) div",
 		":not(div) div",
+		"[class] div",
+		"[class=col] div",
+		"[class~=col] div",
+		"[class|=col] div",
+		"[class^=col] div",
+		"[class$=col] div",
+		"[class*=col] div",
 	};
 
 	for (int i = 0; i < NUM_COMBINATIONS + (int)complex_selectors.size(); i++)

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

@@ -59,13 +59,13 @@ static const String doc_end = R"(
 	<div  id="P" class="parent">
 		<h1 id="A"/>
 		Some text
-		<p  id="B"/>
+		<p  id="B" unit="m"/>
 		<p  id="C"/>
 		<p  id="D"> <span id="D0">  </span><span id="D1">Text</span> </p>
 		<h3 id="E"/>
-		<p  id="F"> <span id="F0"/> </p>
-		<p  id="G"/>
-		<p  id="H" class="hello"/>
+		<p  id="F"> <span id="F0" class="hello-world"/> </p>
+		<p  id="G" class/>
+		<p  id="H" class="world hello"/>
 	</div>
 	<input id="I" type="checkbox" checked/>
 </body>
@@ -75,7 +75,10 @@ static const String doc_end = R"(
 enum class SelectorOp { None, RemoveElementsByIds, InsertElementBefore, RemoveClasses, RemoveId, RemoveChecked, SetHover };
 
 struct QuerySelector {
-	QuerySelector(String selector, String expected_ids) : selector(std::move(selector)), expected_ids(std::move(expected_ids)) {}
+	QuerySelector(String selector, String expected_ids, int expect_num_warnings = 0, int expect_num_query_warnings = 0) :
+		selector(std::move(selector)), expected_ids(std::move(expected_ids)), expect_num_warnings(expect_num_warnings),
+		expect_num_query_warnings(expect_num_query_warnings)
+	{}
 	QuerySelector(String selector, String expected_ids, SelectorOp operation, String operation_argument, String expected_ids_after_operation) :
 		selector(std::move(selector)), expected_ids(std::move(expected_ids)), operation(operation), operation_argument(std::move(operation_argument)),
 		expected_ids_after_operation(std::move(expected_ids_after_operation))
@@ -83,6 +86,9 @@ struct QuerySelector {
 	String selector;
 	String expected_ids;
 
+	// If this rule should fail to parse or otherwise produce warnings, set these to non-zero values.
+	int expect_num_warnings = 0, expect_num_query_warnings = 0;
+
 	// Optionally also test the selector after dynamically making a structural operation on the document.
 	SelectorOp operation = SelectorOp::None;
 	String operation_argument;
@@ -94,7 +100,7 @@ static const Vector<QuerySelector> query_selectors =
 {
 	{ "span",                        "Y D0 D1 F0" },
 	{ ".hello",                      "X Z H" },
-	{ ".hello.world",                "Z" },
+	{ ".hello.world",                "Z H" },
 	{ "div.hello",                   "X Z" },
 	{ "body .hello",                 "X Z H" },
 	{ "body>.hello",                 "X Z" },
@@ -108,6 +114,47 @@ static const Vector<QuerySelector> query_selectors =
 	{ "*.hello",                     "X Z H" },
 	{ "*:checked",                   "I" },
 	
+	{ "p[unit=m]",                   "B" },
+	{ "p[unit='m']",                 "B" },
+	{ "p[unit=\"m\"]",               "B" },
+	{ "[class]",                     "X Y Z P F0 G H" },
+	{ "[class=hello]",               "X" },
+	{ "[class=]",                    "G" },
+	{ "[class='']",                  "G" },
+	{ "[class=\"\"]",                "G" },
+	{ "[class~=hello]",              "X Z H" },
+	{ "[class~=ello]",               "" },
+	{ "[class|=hello]",              "F0" },
+	{ "[class^=hello]",              "X Z F0" },
+	{ "[class^=]",                   "X Y Z P F0 G H" },
+	{ "[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" },
+	{ "[class~=hello][class~=world]", "Z H" },
+
+	{ "[class=hello world]",          "Z" },
+	{ "[class='hello world']",        "Z" },
+	{ "[class=\"hello world\"]",      "Z" },
+
+	{ "[invalid",                     "", 1, 4 },
+	{ "[]",                           "", 1, 4 },
+	{ "[x=Rule{What}]",               "", 2, 0 },
+	{ "[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[class=hello]",              "X" },
+	{ "[class=hello]#X",              "X" },
+	{ "#Y[class=hello]",              "" },
+	{ "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" },
@@ -131,7 +178,7 @@ static const Vector<QuerySelector> query_selectors =
 	{ ":only-of-type",               "Y A E F0 I" },
 	{ "span:empty",                  "Y D0 F0" },
 	
-	{ ".hello.world, #P span, #I",   "Z D0 D1 F0 I",    SelectorOp::RemoveClasses,        "world", "D0 D1 F0 I" },
+	{ ".hello.world, #P span, #I",   "Z D0 D1 F0 H I",  SelectorOp::RemoveClasses,        "world", "D0 D1 F0 I" },
 	{ "body * span",                 "D0 D1 F0" },
 	{ "D1 *",                        "" },
 	
@@ -267,6 +314,7 @@ TEST_CASE("Selectors")
 	{
 		for (const QuerySelector& selector : query_selectors)
 		{
+			TestsShell::SetNumExpectedWarnings(selector.expect_num_warnings);
 			const String selector_css = selector.selector + " { drag: drag; } ";
 			const String document_string = doc_begin + selector_css + doc_end;
 			ElementDocument* document = context->LoadDocumentFromMemory(document_string);
@@ -331,6 +379,8 @@ TEST_CASE("Selectors")
 
 		for (const QuerySelector& selector : query_selectors)
 		{
+			TestsShell::SetNumExpectedWarnings(selector.expect_num_query_warnings);
+
 			ElementList elements;
 			document->QuerySelectorAll(elements, selector.selector);
 			String matching_ids = ElementListToIds(elements);