Skip to content

Commit

Permalink
RCSS attribute selector support (based on #240)
Browse files Browse the repository at this point in the history
Co-authored-by: aquawicket <[email protected]>
  • Loading branch information
mikke89 and aquawicket committed Aug 4, 2022
1 parent a44eef7 commit a96164c
Show file tree
Hide file tree
Showing 8 changed files with 397 additions and 174 deletions.
203 changes: 124 additions & 79 deletions Source/Core/StyleSheetNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include "StyleSheetFactory.h"
#include "StyleSheetSelector.h"
#include <algorithm>
#include <tuple>

namespace Rml {

Expand All @@ -46,55 +47,45 @@ 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));

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));
Expand All @@ -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);
}
}
Expand All @@ -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());
Expand Down Expand Up @@ -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
{
Expand All @@ -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
{
Expand All @@ -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;
Expand All @@ -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:
Expand All @@ -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;
}
}
Expand All @@ -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;
}
}
Expand All @@ -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;
Expand All @@ -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.
Expand Down
Loading

0 comments on commit a96164c

Please sign in to comment.