Skip to content

Commit 6851f41

Browse files
Parilmikke89
andauthored
Add support for :scope as a pseudo-class for QuerySelector[All] / Matches / Closest (#578)
Co-authored-by: Michael Ragazzon <[email protected]>
1 parent 86152b4 commit 6851f41

File tree

8 files changed

+93
-39
lines changed

8 files changed

+93
-39
lines changed

Source/Core/Element.cpp

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,7 +1066,7 @@ Element* Element::Closest(const String& selectors) const
10661066
{
10671067
for (const StyleSheetNode* node : leaf_nodes)
10681068
{
1069-
if (node->IsApplicable(parent))
1069+
if (node->IsApplicable(parent, this))
10701070
{
10711071
return parent;
10721072
}
@@ -1523,7 +1523,7 @@ void Element::GetElementsByClassName(ElementList& elements, const String& class_
15231523
return ElementUtilities::GetElementsByClassName(elements, this, class_name);
15241524
}
15251525

1526-
static Element* QuerySelectorMatchRecursive(const StyleSheetNodeListRaw& nodes, Element* element)
1526+
static Element* QuerySelectorMatchRecursive(const StyleSheetNodeListRaw& nodes, Element* element, Element* scope)
15271527
{
15281528
const int num_children = element->GetNumChildren();
15291529

@@ -1535,19 +1535,19 @@ static Element* QuerySelectorMatchRecursive(const StyleSheetNodeListRaw& nodes,
15351535

15361536
for (const StyleSheetNode* node : nodes)
15371537
{
1538-
if (node->IsApplicable(child))
1538+
if (node->IsApplicable(child, scope))
15391539
return child;
15401540
}
15411541

1542-
Element* matching_element = QuerySelectorMatchRecursive(nodes, child);
1542+
Element* matching_element = QuerySelectorMatchRecursive(nodes, child, scope);
15431543
if (matching_element)
15441544
return matching_element;
15451545
}
15461546

15471547
return nullptr;
15481548
}
15491549

1550-
static void QuerySelectorAllMatchRecursive(ElementList& matching_elements, const StyleSheetNodeListRaw& nodes, Element* element)
1550+
static void QuerySelectorAllMatchRecursive(ElementList& matching_elements, const StyleSheetNodeListRaw& nodes, Element* element, Element* scope)
15511551
{
15521552
const int num_children = element->GetNumChildren();
15531553

@@ -1559,14 +1559,14 @@ static void QuerySelectorAllMatchRecursive(ElementList& matching_elements, const
15591559

15601560
for (const StyleSheetNode* node : nodes)
15611561
{
1562-
if (node->IsApplicable(child))
1562+
if (node->IsApplicable(child, scope))
15631563
{
15641564
matching_elements.push_back(child);
15651565
break;
15661566
}
15671567
}
15681568

1569-
QuerySelectorAllMatchRecursive(matching_elements, nodes, child);
1569+
QuerySelectorAllMatchRecursive(matching_elements, nodes, child, scope);
15701570
}
15711571
}
15721572

@@ -1581,7 +1581,7 @@ Element* Element::QuerySelector(const String& selectors)
15811581
return nullptr;
15821582
}
15831583

1584-
return QuerySelectorMatchRecursive(leaf_nodes, this);
1584+
return QuerySelectorMatchRecursive(leaf_nodes, this, this);
15851585
}
15861586

15871587
void Element::QuerySelectorAll(ElementList& elements, const String& selectors)
@@ -1595,7 +1595,7 @@ void Element::QuerySelectorAll(ElementList& elements, const String& selectors)
15951595
return;
15961596
}
15971597

1598-
QuerySelectorAllMatchRecursive(elements, leaf_nodes, this);
1598+
QuerySelectorAllMatchRecursive(elements, leaf_nodes, this, this);
15991599
}
16001600

16011601
bool Element::Matches(const String& selectors)
@@ -1611,7 +1611,7 @@ bool Element::Matches(const String& selectors)
16111611

16121612
for (const StyleSheetNode* node : leaf_nodes)
16131613
{
1614-
if (node->IsApplicable(this))
1614+
if (node->IsApplicable(this, this))
16151615
{
16161616
return true;
16171617
}

Source/Core/StyleSheet.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ SharedPtr<const ElementDefinition> StyleSheet::GetElementDefinition(const Elemen
209209
// We found a node that has at least one requirement matching the element. Now see if we satisfy the remaining requirements of the
210210
// node, including all ancestor nodes. What this involves is traversing the style nodes backwards, trying to match nodes in the
211211
// element's hierarchy to nodes in the style hierarchy.
212-
if (node->IsApplicable(element))
212+
if (node->IsApplicable(element, nullptr))
213213
applicable_nodes.push_back(node);
214214
}
215215
}
@@ -236,7 +236,7 @@ SharedPtr<const ElementDefinition> StyleSheet::GetElementDefinition(const Elemen
236236
// Also check all remaining nodes that don't contain any indexed requirements.
237237
for (const StyleSheetNode* node : styled_node_index.other)
238238
{
239-
if (node->IsApplicable(element))
239+
if (node->IsApplicable(element, nullptr))
240240
applicable_nodes.push_back(node);
241241
}
242242

Source/Core/StyleSheetFactory.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ StyleSheetFactory::StyleSheetFactory() :
5252
{"only-of-type", StructuralSelectorType::Only_Of_Type},
5353
{"empty", StructuralSelectorType::Empty},
5454
{"not", StructuralSelectorType::Not},
55+
{"scope", StructuralSelectorType::Scope},
5556
}
5657
{}
5758

Source/Core/StyleSheetNode.cpp

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ const PropertyDictionary& StyleSheetNode::GetProperties() const
177177
return properties;
178178
}
179179

180-
bool StyleSheetNode::Match(const Element* element) const
180+
bool StyleSheetNode::Match(const Element* element, const Element* scope) const
181181
{
182182
if (!selector.tag.empty() && selector.tag != element->GetTagName())
183183
return false;
@@ -200,17 +200,17 @@ bool StyleSheetNode::Match(const Element* element) const
200200
if (!selector.attributes.empty() && !MatchAttributes(element))
201201
return false;
202202

203-
if (!selector.structural_selectors.empty() && !MatchStructuralSelector(element))
203+
if (!selector.structural_selectors.empty() && !MatchStructuralSelector(element, scope))
204204
return false;
205205

206206
return true;
207207
}
208208

209-
bool StyleSheetNode::MatchStructuralSelector(const Element* element) const
209+
bool StyleSheetNode::MatchStructuralSelector(const Element* element, const Element* scope) const
210210
{
211211
for (auto& node_selector : selector.structural_selectors)
212212
{
213-
if (!IsSelectorApplicable(element, node_selector))
213+
if (!IsSelectorApplicable(element, node_selector, scope))
214214
return false;
215215
}
216216

@@ -292,7 +292,7 @@ bool StyleSheetNode::MatchAttributes(const Element* element) const
292292
return true;
293293
}
294294

295-
bool StyleSheetNode::TraverseMatch(const Element* element) const
295+
bool StyleSheetNode::TraverseMatch(const Element* element, const Element* scope) const
296296
{
297297
RMLUI_ASSERT(parent);
298298
if (!parent->parent)
@@ -307,7 +307,7 @@ bool StyleSheetNode::TraverseMatch(const Element* element) const
307307
// hierarchy using the next element parent. Repeat until we run out of elements.
308308
for (element = element->GetParentNode(); element; element = element->GetParentNode())
309309
{
310-
if (parent->Match(element) && parent->TraverseMatch(element))
310+
if (parent->Match(element, scope) && parent->TraverseMatch(element, scope))
311311
return true;
312312
// If the node has a child combinator we must match this first ancestor.
313313
else if (selector.combinator == SelectorCombinator::Child)
@@ -341,7 +341,7 @@ bool StyleSheetNode::TraverseMatch(const Element* element) const
341341
// text elements don't have children and thus any ancestor is not a text element.
342342
if (IsTextElement(element))
343343
continue;
344-
else if (parent->Match(element) && parent->TraverseMatch(element))
344+
else if (parent->Match(element, scope) && parent->TraverseMatch(element, scope))
345345
return true;
346346
// If the node has a next-sibling combinator we must match this first sibling.
347347
else if (selector.combinator == SelectorCombinator::NextSibling)
@@ -355,7 +355,7 @@ bool StyleSheetNode::TraverseMatch(const Element* element) const
355355
return false;
356356
}
357357

358-
bool StyleSheetNode::IsApplicable(const Element* element) const
358+
bool StyleSheetNode::IsApplicable(const Element* element, const Element* scope) const
359359
{
360360
// Determine whether the element matches the current node and its entire lineage. The entire hierarchy of the element's document will be
361361
// considered during the match as necessary.
@@ -384,11 +384,11 @@ bool StyleSheetNode::IsApplicable(const Element* element) const
384384
return false;
385385

386386
// Check the structural selector requirements last as they can be quite slow.
387-
if (!selector.structural_selectors.empty() && !MatchStructuralSelector(element))
387+
if (!selector.structural_selectors.empty() && !MatchStructuralSelector(element, scope))
388388
return false;
389389

390390
// Walk up through all our parent nodes, each one of them must be matched by some ancestor or sibling element.
391-
if (parent && !TraverseMatch(element))
391+
if (parent && !TraverseMatch(element, scope))
392392
return false;
393393

394394
return true;

Source/Core/StyleSheetNode.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class StyleSheetNode {
7474
/// Returns true if this node is applicable to the given element, given its IDs, classes and heritage.
7575
/// @note For performance reasons this call does not check whether 'element' is a text element. The caller must manually check this condition and
7676
/// consider any text element not applicable.
77-
bool IsApplicable(const Element* element) const;
77+
bool IsApplicable(const Element* element, const Element* scope) const;
7878

7979
/// Returns the specificity of this node.
8080
int GetSpecificity() const;
@@ -83,12 +83,12 @@ class StyleSheetNode {
8383
void CalculateAndSetSpecificity();
8484

8585
// Match an element to the local node requirements.
86-
inline bool Match(const Element* element) const;
87-
inline bool MatchStructuralSelector(const Element* element) const;
86+
inline bool Match(const Element* element, const Element* scope) const;
87+
inline bool MatchStructuralSelector(const Element* element, const Element* scope) const;
8888
inline bool MatchAttributes(const Element* element) const;
8989

9090
// Recursively traverse the nodes up towards the root to match the element and its hierarchy.
91-
bool TraverseMatch(const Element* element) const;
91+
bool TraverseMatch(const Element* element, const Element* scope) const;
9292

9393
// The parent of this node; is nullptr for the root node.
9494
StyleSheetNode* parent = nullptr;

Source/Core/StyleSheetSelector.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ bool operator==(const CompoundSelector& a, const CompoundSelector& b)
9090
return true;
9191
}
9292

93-
bool IsSelectorApplicable(const Element* element, const StructuralSelector& selector)
93+
bool IsSelectorApplicable(const Element* element, const StructuralSelector& selector, const Element* scope)
9494
{
9595
RMLUI_ASSERT(element);
9696

@@ -367,7 +367,7 @@ bool IsSelectorApplicable(const Element* element, const StructuralSelector& sele
367367

368368
for (const StyleSheetNode* node : selector.selector_tree->leafs)
369369
{
370-
if (node->IsApplicable(element))
370+
if (node->IsApplicable(element, scope))
371371
{
372372
inner_selector_matches = true;
373373
break;
@@ -377,6 +377,11 @@ bool IsSelectorApplicable(const Element* element, const StructuralSelector& sele
377377
return !inner_selector_matches;
378378
}
379379
break;
380+
case StructuralSelectorType::Scope:
381+
{
382+
return scope && element == scope;
383+
}
384+
break;
380385
case StructuralSelectorType::Invalid:
381386
{
382387
RMLUI_ERROR;

Source/Core/StyleSheetSelector.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ enum class StructuralSelectorType {
108108
Only_Child,
109109
Only_Of_Type,
110110
Empty,
111-
Not
111+
Not,
112+
Scope,
112113
};
113114
struct StructuralSelector {
114115
StructuralSelector(StructuralSelectorType type, int a, int b) : type(type), a(a), b(b) {}
@@ -148,10 +149,11 @@ struct CompoundSelector {
148149
};
149150
bool operator==(const CompoundSelector& a, const CompoundSelector& b);
150151

151-
/// Returns true if the the node the given selector is discriminating for is applicable to a given element.
152+
/// Returns true if the node the given selector is discriminating for is applicable to a given element.
152153
/// @param element[in] The element to determine node applicability for.
153154
/// @param selector[in] The selector to test against the element.
154-
bool IsSelectorApplicable(const Element* element, const StructuralSelector& selector);
155+
/// @param scope[in] The element considered as the reference point/scope (for :scope).
156+
bool IsSelectorApplicable(const Element* element, const StructuralSelector& selector, const Element* scope);
155157

156158
} // namespace Rml
157159
#endif

Tests/Source/UnitTests/Selectors.cpp

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ static const Vector<QuerySelector> query_selectors =
113113
{ "*span", "Y D0 D1 F0" },
114114
{ "*.hello", "X Z H" },
115115
{ "*:checked", "I" },
116-
116+
117117
{ "p[unit='m']", "B" },
118118
{ "p[unit=\"m\"]", "B" },
119119
{ "[class]", "X Y Z P F0 G H" },
@@ -129,7 +129,7 @@ static const Vector<QuerySelector> query_selectors =
129129
{ "[class$=hello]", "X H" },
130130
{ "[class*=hello]", "X Z F0 H" },
131131
{ "[class*=ello]", "X Z F0 H" },
132-
132+
133133
{ "[class~=hello].world", "Z H" },
134134
{ "*[class~=hello].world", "Z H" },
135135
{ ".world[class~=hello]", "Z H" },
@@ -142,18 +142,18 @@ static const Vector<QuerySelector> query_selectors =
142142
{ "[invalid", "", 1, 4 },
143143
{ "[]", "", 1, 4 },
144144
{ "[x=Rule{What}]", "", 2, 0 },
145-
{ "[x=Hello,world]", "", 1, 2 },
145+
{ "[x=Hello,world]", "", 1, 2 },
146146
// The next ones are valid in CSS but we currently don't bother handling them, just make sure we don't crash.
147147
{ "[x='Rule{What}']", "", 2, 0 },
148-
{ "[x='Hello,world']", "", 1, 2 },
148+
{ "[x='Hello,world']", "", 1, 2 },
149149

150150
{ "#X[class=hello]", "X" },
151151
{ "[class=hello]#X", "X" },
152152
{ "#Y[class=hello]", "" },
153153
{ "div[class=hello]", "X" },
154154
{ "[class=hello]div", "X" },
155155
{ "span[class=hello]", "" },
156-
156+
157157
{ ".parent :nth-child(odd)", "A C D0 E F0 G" },
158158
{ ".parent > :nth-child(even)", "B D F H", SelectorOp::RemoveClasses, "parent", "" },
159159
{ ":first-child", "X A D0 F0", SelectorOp::RemoveElementsByIds, "A F0", "X B D0" },
@@ -176,11 +176,11 @@ static const Vector<QuerySelector> query_selectors =
176176
{ ":only-child", "F0", SelectorOp::RemoveElementsByIds, "D0", "D1 F0" },
177177
{ ":only-of-type", "Y A E F0 I" },
178178
{ "span:empty", "Y D0 F0" },
179-
179+
180180
{ ".hello.world, #P span, #I", "Z D0 D1 F0 H I", SelectorOp::RemoveClasses, "world", "D0 D1 F0 I" },
181181
{ "body * span", "D0 D1 F0" },
182182
{ "D1 *", "" },
183-
183+
184184
{ "#E + #F", "F", SelectorOp::InsertElementBefore, "F", "" },
185185
{ "#E+#F", "F" },
186186
{ "#E +#F", "F" },
@@ -192,7 +192,7 @@ static const Vector<QuerySelector> query_selectors =
192192
{ "#P + *", "I" },
193193
{ "div.parent > #B + p", "C" },
194194
{ "div.parent > #B + div", "" },
195-
195+
196196
{ "#B ~ #F", "F" },
197197
{ "#B~#F", "F" },
198198
{ "#B ~#F", "F" },
@@ -259,6 +259,22 @@ static const Vector<MatchesSelector> matches_selectors =
259259
{ "B", "[unit='m']", true }
260260
};
261261

262+
struct ScopeSelector : public QuerySelector {
263+
String scope_selector;
264+
265+
ScopeSelector(const String& scope_selector, const String& selector, const String& expected_ids) :
266+
QuerySelector(selector, expected_ids), scope_selector(scope_selector)
267+
{}
268+
};
269+
static const Vector<ScopeSelector> scope_selectors =
270+
{
271+
{ "", ":scope *", "X Y Z P A B C D D0 D1 E F F0 G H I" }, // should be equivalent to just "*"
272+
{ "", ":scope > *", "X Y Z P I" },
273+
{ "", ":scope > *:not(:checked)", "X Y Z P" },
274+
{ "#P", ":scope > p", "B C D F G H" },
275+
{ "#P", ":scope span", "D0 D1 F0" },
276+
};
277+
262278
struct ContainsSelector {
263279
String element_id;
264280
String target_id;
@@ -473,6 +489,36 @@ TEST_CASE("Selectors")
473489
context->UnloadDocument(document);
474490
}
475491

492+
SUBCASE("Scope")
493+
{
494+
const String document_string = doc_begin + doc_end;
495+
ElementDocument* document = context->LoadDocumentFromMemory(document_string);
496+
REQUIRE(document);
497+
498+
for (const ScopeSelector& selector : scope_selectors)
499+
{
500+
Element* start = (selector.scope_selector.empty() ? document : document->QuerySelector(selector.scope_selector));
501+
REQUIRE(start);
502+
503+
ElementList elements;
504+
start->QuerySelectorAll(elements, selector.selector);
505+
String matching_ids = ElementListToIds(elements);
506+
507+
Element* first_element = start->QuerySelector(selector.selector);
508+
if (first_element)
509+
{
510+
CHECK_MESSAGE(first_element == elements[0], "QuerySelector does not return the first match of QuerySelectorAll.");
511+
}
512+
else
513+
{
514+
CHECK_MESSAGE(elements.empty(), "QuerySelector found nothing, while QuerySelectorAll found " << elements.size() << " element(s).");
515+
}
516+
517+
CHECK_MESSAGE(matching_ids == selector.expected_ids, "QuerySelector: " << selector.selector);
518+
}
519+
context->UnloadDocument(document);
520+
}
521+
476522
SUBCASE("Contains")
477523
{
478524
const String document_string = doc_begin + doc_end;

0 commit comments

Comments
 (0)