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 }