home
readme
diff
tree
note
docs
0commit 6dfb96c72f966734322c80cd77576867f462dc77
1Author: Mason Wright <mason@lakefox.net>
2Date: Wed Jul 9 10:30:07 2025 -0600
3
4 Added has and nth-*
5
6diff --git a/src/grim.cc b/src/grim.cc
7index 33fd5b7..c7909f0 100755
8--- a/src/grim.cc
9+++ b/src/grim.cc
10@@ -5,7 +5,15 @@
11 #include <stdexcept>
12 #include <cctype>
13 #include <string_view>
14-#include <span>
15+#include <queue>
16+#include <set>
17+
18+// +------------------------------------------------------+
19+// |Table of contents |
20+// +------------------------------------------------------+
21+// |(jhsfk) Has selection |
22+// |(d678a) Nth child selection |
23+// +------------------------------------------------------+
24
25 // Constructor
26
27@@ -638,12 +646,18 @@ bool testSelector(Node* node, std::vector<std::vector<std::string>> selector) {
28 // if you want those, node->children
29 const std::vector<std::unique_ptr<Node>>& children = (node->parent != nullptr) ? node->parent->children : EMPTY_NODE_CHILDREN_VECTOR;
30 size_t childIndex = 0;
31+ size_t childTypeIndex = 0;
32+ size_t childTypeCount = 0;
33 if (children.size() > 1) {
34 for (size_t i = 0; i < children.size(); i++) {
35 Node* child_ptr = children[i].get();
36-
37+
38 if (child_ptr == node) {
39 childIndex = i;
40+ childTypeIndex = childTypeCount;
41+ }
42+ if (child_ptr->getTagName() == node->getTagName()) {
43+ childTypeCount++;
44 }
45 }
46 }
47@@ -651,13 +665,24 @@ bool testSelector(Node* node, std::vector<std::vector<std::string>> selector) {
48 // Add the auto pseudo classes like first-child/last-child
49 if (childIndex == 0) {
50 attrs.push_back("first-child");
51- } else if (childIndex == children.size()-1) {
52+ }
53+ if (childIndex == children.size()-1) {
54 attrs.push_back("last-child");
55 }
56+ if (childTypeIndex == 0) {
57+ attrs.push_back("first-of-type");
58+ }
59+ if (childTypeIndex == childTypeCount-1) {
60+ attrs.push_back("last-of-type");
61+ }
62+
63
64
65 size_t i = 0;
66 for (auto p : parts) {
67+ if (p[0] == '*') {
68+ matched = true;
69+ } else
70 if (i == 0 && p[0] != '#' && p[0] != '.' && p[0] != '[' && p[0] != ':') {
71 matched = node->getTagName() == p;
72 } else if (p[0] == '#') {
73@@ -704,9 +729,11 @@ bool testSelector(Node* node, std::vector<std::vector<std::string>> selector) {
74 // Just inverts the output of testSelector
75 matched = !testSelector(node, parseSelectorParts(value));
76 } else if (p.starts_with(":has(")) {
77-
78- // We have to do a little work to get :has to work
79- // once parse the inner parts of the selector,
80+// +------------------------------------------------------+
81+// |(jhsfk) Has selection |
82+// +------------------------------------------------------+
83+ // We have to do a little work to get :has working
84+ // once we parse the inner parts of the selector,
85 // we need to find what the selector is targeting
86 std::string value = p.substr(5,p.length()-6);
87 auto innerParts = parseSelectorParts(value);
88@@ -714,13 +741,13 @@ bool testSelector(Node* node, std::vector<std::vector<std::string>> selector) {
89
90 for (auto ip : innerParts) {
91 // if the selector was :has(+ p, div, > video)
92- // ip each comma seperated value like {"+", "p"}
93- // these differ from how we parse the parts above
94+ // ip contains each comma seperated value like {"+", "p"}
95+ // these differ from how we parse the parts below
96 // if the first item in a selector is not a +~> then
97 // it is a decendent otherwise they are the same
98
99- // We need to have a 2d with the contents of ip without the first
100- // selector if it is't a decendent combinator so we do that here
101+ // We need to have a 2d vector with the contents of ip without the first
102+ // selector if it is't a decendent combinator, so we do that here
103 std::vector<std::string> sliceOfParts(ip.begin()+1, ip.end());
104 std::vector<std::vector<std::string>> slicedParts;
105 slicedParts.push_back(sliceOfParts);
106@@ -730,7 +757,6 @@ bool testSelector(Node* node, std::vector<std::vector<std::string>> selector) {
107 // can loop through and if we find one that matches ip[1:]
108 // (remove the ">") we can stop and return
109 for (const auto& c : node->children) {
110-
111 found = testSelector(c.get(), slicedParts);
112 if (found) {
113 break;
114@@ -747,6 +773,46 @@ bool testSelector(Node* node, std::vector<std::vector<std::string>> selector) {
115 found = false;
116 }
117 }
118+ } else if (ip[0] == "~") {
119+ if (childIndex < children.size()-1) {
120+ for (size_t i = childIndex+1; i < children.size(); i++) {
121+ // We have to test every selector after the current element,
122+ // if any match then it matches
123+ found = testSelector(children[i].get(), slicedParts);
124+ if (found) {
125+ break;
126+ }
127+ }
128+ } else {
129+ found = false;
130+ }
131+ } else {
132+ // No combinator default to decendent
133+ // Sadly we can't use the check parent trick here
134+ // as we do below. We need to BFS the children until we find a match
135+ std::queue<Node*> queue;
136+ std::set<Node*> visited;
137+ queue.push(node);
138+ visited.insert(node);
139+
140+ while (!queue.empty()) {
141+ Node* n = queue.front();
142+ queue.pop();
143+ std::vector<std::vector<std::string>> ipp;
144+ ipp.push_back(ip);
145+ if (testSelector(n, ipp)) {
146+ found = true;
147+ break;
148+ }
149+
150+ for (const auto& child_ptr : n->children) {
151+ Node* child = child_ptr.get();
152+ if (visited.find(child) == visited.end()) {
153+ visited.insert(child);
154+ queue.push(child);
155+ }
156+ }
157+ }
158 }
159
160 if (found) {
161@@ -754,6 +820,195 @@ bool testSelector(Node* node, std::vector<std::vector<std::string>> selector) {
162 }
163 }
164 matched = found;
165+ } else if (p.starts_with(":nth-")) {
166+// +------------------------------------------------------+
167+// |(d678a) Nth child selection |
168+// +------------------------------------------------------+
169+// This will match on:
170+// + nth-child()
171+// + nth-last-child()
172+// + nth-last-of_type()
173+// + nth-of-type()
174+// Instead of moving these up a layer I'm putting them
175+// here because most selectors will not be "nth-" so why
176+// check against every case
177+ std::string pattern;
178+ bool last = false;
179+ bool ofType = false;
180+ if (p.starts_with(":nth-child")) {
181+ pattern = p.substr(11, p.length()-12);
182+ } else if (p.starts_with(":nth-last-child(")) {
183+ last = true;
184+ pattern = p.substr(16,p.length()-17);
185+ } else if (p.starts_with(":nth-of-type(")) {
186+ ofType = true;
187+ pattern = p.substr(13,p.length()-14);
188+ } else if (p.starts_with(":nth-last-of-type(")) {
189+ ofType = true;
190+ last = true;
191+ pattern = p.substr(18, p.length()-19);
192+ }
193+ // +------------------------------------------------------+
194+ // |Parsing the pattern |
195+ // +------------------------------------------------------+
196+ // If you look closely these are reversed
197+ // its because the vector index starts with 0
198+ // and CSS starts with 1
199+ if (pattern == "even") {
200+ if (childIndex%2 == 1) {
201+ matched = true;
202+ } else {
203+ matched = false;
204+ }
205+ } else
206+ if (pattern == "odd") {
207+ if (childIndex%2 == 0) {
208+ matched = true;
209+ } else {
210+ matched = false;
211+ }
212+ } else {
213+ // loop through
214+ // first check if theres a symbol before any 0-9
215+ // go until n is found (if there is a n)
216+ // then find another symbol or "of"
217+ // if symbol find numbers
218+ // then of get that.
219+ // the of syntax can be used to make nth-of-type as well
220+ bool negFound = false;
221+ bool negative = false;
222+ bool Afound = false;
223+ bool nfound = false;
224+ int A = 0;
225+ int B = 0;
226+ char symbol = '+';
227+ bool symbolFound = false;
228+ std::string ofSelector = "";
229+
230+ for (size_t i = 0; i < pattern.length(); i++) {
231+ if (!std::isspace(pattern[i])) {
232+ if (pattern[i] == '-' && !negFound) {
233+ negFound = true;
234+ negative = true;
235+ } else if (pattern[i] == '+' && !negFound) {
236+ negFound = true;
237+ } else if (pattern[i] == 'n' && !nfound && !Afound ) {
238+ nfound = true;
239+ // At n stop adding to A
240+ Afound = true;
241+ negFound = true;
242+ } else if (Afound && nfound && !symbolFound && (pattern[i] == '-' || pattern[i] == '+')) {
243+ symbol = pattern[i];
244+ symbolFound = true;
245+ } else if (pattern[i]-'0' < 10) {
246+ negFound = true;
247+ if (!Afound && !nfound) {
248+ A *= 10;
249+ A += (pattern[i]-'0');
250+ } else if (nfound && symbolFound && Afound) {
251+ B *= 10;
252+ B += (pattern[i]-'0');
253+ }
254+ } else {
255+ ofSelector += pattern[i];
256+ }
257+ }
258+ }
259+
260+ if (negative) {
261+ A *= -1;
262+ }
263+ if (symbol == '-') {
264+ B *= -1;
265+ }
266+
267+ // Now we have:
268+ // polarity of A: negative
269+ // value of A: A
270+ // polarity of B: symbol
271+ // value of B: B
272+ // value of the of selector: ofSelector.substr(2) ("of" is included in the value of ofSelector)
273+ //
274+ // ^ We have already applied the polarities so we just need A,B and ofSelector
275+ //
276+ // Things to keep in mind:
277+ // if there is a A value it will be 0
278+
279+ // +------------------------------------------------------+
280+ // |Finding the index of node |
281+ // +------------------------------------------------------+
282+ int index = 0;
283+ // We have the index of the current element (childIndex)
284+ // if ofSelector is "" we can use it if not we need to run
285+ // testSelector on all children until we find it
286+ // and if "last" we need to do it in reverse
287+ //
288+ // If the nth child selector has -of-type we need to add of+tagname
289+ // the of is added because thats how the parser parses the rest of them
290+ if (ofType) {
291+ ofSelector = "of"+node->getTagName();
292+ }
293+ if (ofSelector == "") {
294+ if (last) {
295+ // If we are checking from the back we need to invert the
296+ // childIndex
297+ index = children.size() - childIndex;
298+ } else {
299+ index = childIndex;
300+ }
301+ } else {
302+ // Parse the of selector
303+ auto pOS = parseSelectorParts(ofSelector.substr(2));
304+ if (last) {
305+ // Reverse through the siblings
306+ for (size_t i = children.size() - 1; i >= 0; i--) {
307+ Node* child_ptr = children[i].get();
308+ if (child_ptr != node) {
309+ if (testSelector(child_ptr, pOS)) {
310+ // if the selector matches the ofSelector add one
311+ index++;
312+ }
313+ } else {
314+ // We are at the pointer address of the target node
315+ // add 1 to index and break
316+ index++;
317+ break;
318+ }
319+ }
320+ } else {
321+ for (size_t i = 0; i < children.size(); i++) {
322+ Node* child_ptr = children[i].get();
323+ if (child_ptr != node) {
324+ if (testSelector(child_ptr, pOS)) {
325+ // if the selector matches the ofSelector add one
326+ index++;
327+ }
328+ } else {
329+ // We are at the pointer address of the target node
330+ // add 1 to index and break
331+ index++;
332+ break;
333+ }
334+ }
335+ }
336+ }
337+
338+ // Add one to index because CSS starts at 1
339+ index++;
340+ // Now we have the index of the node and we can compute if its a match
341+ // this is done by calculating the An+B
342+ // The An+B is calculated based on two things
343+ // n must be >= 0
344+ // n cannot be a float
345+ // so we solve for n with (index-B)/A then we check those two conditions
346+ // if nfound is false and A != 0 then we can just compare the two numbers
347+ if (!nfound && A != 0) {
348+ matched = A == index;
349+ } else if (A != 0) {
350+ matched = (index-B)/A >= 0 && (index-B)%A == 0;
351+ }
352+
353+ }
354 }
355 // If there are any pseudo elements we will just skip them
356 // with the transformers being replaced with pseudo elements
357diff --git a/tests/css_selector.cc b/tests/css_selector.cc
358index 039e893..b4e7ca5 100644
359--- a/tests/css_selector.cc
360+++ b/tests/css_selector.cc
361@@ -116,6 +116,12 @@ TEST_CASE("testSelector", "[CSS]") {
362 "<p id=\"paragraph\" class=\"class1 class2\">This is some text for a simple test</p>"
363 "<div></div>"
364 "<input type=\"radio\" checked required/>"
365+ "<ul>"
366+ "<li>one</li>"
367+ "<li>two</li>"
368+ "<li>three</li>"
369+ "<li>four</li>"
370+ "</ul>"
371 "</body>"
372 "</html>";
373
374@@ -172,6 +178,8 @@ TEST_CASE("testSelector", "[CSS]") {
375 }
376 SECTION("testSelector can match a element with a pseudo class") {
377 auto input = document->children[0]->children[1]->children[3].get();
378+ // While not all pseudo classes are defined here, any that are defined
379+ // with a attribute will match: ie <input disabled>
380 REQUIRE(testSelector(input, parseSelectorParts("input:required")));
381 REQUIRE(testSelector(input, parseSelectorParts(":checked")));
382 REQUIRE_FALSE(testSelector(input, parseSelectorParts("input:enabled")));
383@@ -180,22 +188,41 @@ TEST_CASE("testSelector", "[CSS]") {
384 auto h1 = document->children[0]->children[1]->children[0].get();
385 REQUIRE(testSelector(h1, parseSelectorParts(":first-child")));
386 }
387+ SECTION("testSelector can match a element with first-of-type") {
388+ auto h1 = document->children[0]->children[1]->children[0].get();
389+ REQUIRE(testSelector(h1, parseSelectorParts(":first-of-type")));
390+
391+ auto li = document->children[0]->children[1]->children[4]->children[0].get();
392+ REQUIRE(testSelector(li, parseSelectorParts(":first-of-type")));
393+ }
394 SECTION("testSelector can match a element with last-child") {
395- auto input = document->children[0]->children[1]->children[3].get();
396- REQUIRE(testSelector(input, parseSelectorParts(":last-child")));
397+ auto ul = document->children[0]->children[1]->children[4].get();
398+ REQUIRE(testSelector(ul, parseSelectorParts(":last-child")));
399+ }
400+ SECTION("testSelector can match a element with last-of-type") {
401+ auto h1 = document->children[0]->children[1]->children[0].get();
402+ // There is only one h1
403+ REQUIRE(testSelector(h1, parseSelectorParts(":last-of-type")));
404+
405+ auto li = document->children[0]->children[1]->children[4]->children[0].get();
406+ REQUIRE_FALSE(testSelector(li, parseSelectorParts(":last-of-type")));
407+
408+ li = document->children[0]->children[1]->children[4]->children[3].get();
409+ REQUIRE(testSelector(li, parseSelectorParts(":last-of-type")));
410 }
411 SECTION("testSelector can match multiple selectors") {
412- auto input = document->children[0]->children[1]->children[3].get();
413- REQUIRE(testSelector(input, parseSelectorParts("h1, h1 ~p, :last-child")));
414- REQUIRE_FALSE(testSelector(input, parseSelectorParts("h1, h1 ~p, :first-child")));
415+ auto ul = document->children[0]->children[1]->children[4].get();
416+ // Should only match on :last-child
417+ REQUIRE(testSelector(ul, parseSelectorParts("h1, h1 ~p, :last-child")));
418+ REQUIRE_FALSE(testSelector(ul, parseSelectorParts("h1, h1 ~p, :first-child")));
419
420 BENCHMARK("Multiselector and parse") {
421- return testSelector(input, parseSelectorParts("h1, h1 ~p, :last-child"));
422+ return testSelector(ul, parseSelectorParts("h1, h1 ~p, :last-child"));
423 };
424
425 auto parts = parseSelectorParts("h1, h1 ~p, :last-child");
426 BENCHMARK("Multiselector pre-parse") {
427- return testSelector(input, parts);
428+ return testSelector(ul, parts);
429 };
430 }
431 SECTION("testSelector can match :is() selectors") {
432@@ -237,10 +264,80 @@ TEST_CASE("testSelector", "[CSS]") {
433 return testSelector(h1, parseSelectorParts("h1:has(+ p)"));
434 };
435 }
436+ SECTION("testSelector can match a :has() selector with a subsequent-sibling combinator") {
437+ auto h1 = document->children[0]->children[1]->children[0].get();
438+ REQUIRE(testSelector(h1, parseSelectorParts("h1:has(~ input)")));
439+ REQUIRE_FALSE(testSelector(h1, parseSelectorParts("h1:has(~ p:required)")));
440+
441+
442+ BENCHMARK("has with a next subsequent-sibling combinator") {
443+ return testSelector(h1, parseSelectorParts("h1:has(~ input)"));
444+ };
445+ }
446+
447+ SECTION("testSelector can match a :has() selector with a decendent combinator") {
448+ auto html = document->children[0].get();
449+ REQUIRE(testSelector(html, parseSelectorParts("html:has(input)")));
450+ REQUIRE_FALSE(testSelector(html, parseSelectorParts("html:has(p:required)")));
451+
452+
453+ BENCHMARK("has with a next subsequent-sibling combinator") {
454+ return testSelector(html, parseSelectorParts("html:has(input)"));
455+ };
456+ }
457+
458+ SECTION("testSelector can match a :is():has(:where()) selector") {
459+ auto html = document->children[0].get();
460+ REQUIRE(testSelector(html, parseSelectorParts(":is(html, div, p):has(:where(html, input:checked, input))")));
461+ REQUIRE_FALSE(testSelector(html, parseSelectorParts(":is(body, div, p):has(:where(html, input:checked, input))")));
462+
463+ BENCHMARK(":is():has(:where()) selector") {
464+ return testSelector(html, parseSelectorParts(":is(html, div, p):has(:where(html, input:checked, input))"));
465+ };
466+ }
467+
468+ SECTION("testSelector can match a :nth-child(even)") {
469+ auto li = document->children[0]->children[1]->children[4]->children[1].get();
470+ REQUIRE(testSelector(li, parseSelectorParts("li:nth-child(even)")));
471+ REQUIRE_FALSE(testSelector(li, parseSelectorParts("li:nth-child(odd)")));
472+ }
473+
474+ SECTION("testSelector can match a :nth-child(2)") {
475+ auto li = document->children[0]->children[1]->children[4]->children[1].get();
476+ REQUIRE(testSelector(li, parseSelectorParts("li:nth-child(2)")));
477+ REQUIRE_FALSE(testSelector(li, parseSelectorParts("li:nth-child(1)")));
478+
479+ BENCHMARK(":nth-child(2)") {
480+ return testSelector(li, parseSelectorParts("li:nth-child(2)"));
481+ };
482+ }
483+
484+ SECTION("testSelector can match a :nth-child(2n)") {
485+ auto li = document->children[0]->children[1]->children[4]->children[1].get();
486+ REQUIRE(testSelector(li, parseSelectorParts("li:nth-child(2n)")));
487+ REQUIRE_FALSE(testSelector(li, parseSelectorParts("li:nth-child(3n)")));
488+
489+ BENCHMARK(":nth-child(2n)") {
490+ return testSelector(li, parseSelectorParts("li:nth-child(2n)"));
491+ };
492+ }
493+
494+ SECTION("testSelector can match a :nth-child(2n+1)") {
495+ auto li = document->children[0]->children[1]->children[4]->children[2].get();
496+ REQUIRE(testSelector(li, parseSelectorParts("li:nth-child(2n+1)")));
497+ REQUIRE_FALSE(testSelector(li, parseSelectorParts("li:nth-child(3n+1)")));
498+
499+ BENCHMARK(":nth-child(2n+1)") {
500+ return testSelector(li, parseSelectorParts("li:nth-child(2n+1)"));
501+ };
502+ }
503+
504+
505+
506+ // need to make for all 4
507 // make for:
508 // pseudo elements
509 // *
510- // pseudo selectors(:has, :where etc)
511 // nth-child
512 // nth-*
513 }