home readme diff tree note docs

0commit 4e01ba8ad2c3361fa4be3d896288020948b58b5e
1Author: Mason Wright <mason@lakefox.net>
2Date:   Sun Jul 6 13:40:21 2025 -0600
3
4    Performance update on parseSelectorParts and tests
5
6diff --git a/include/grim.h b/include/grim.h
7index 3e87987..9c190fe 100644
8--- a/include/grim.h
9+++ b/include/grim.h
10@@ -3,6 +3,7 @@
11 #define GRIM_H
12 
13 #include <string>
14+#include <string_view>
15 #include <vector>
16 #include <unordered_map>
17 #include <unordered_set>
18@@ -100,7 +101,7 @@ class Node {
19  * @param selector A CSS selector
20  * @return 
21  */
22-std::vector<std::vector<std::string>> parseSelectorParts(std::string);
23+std::vector<std::vector<std::string>> parseSelectorParts(std::string_view);
24 
25 bool testSelector(Node*, std::vector<std::vector<std::string>>);
26 
27diff --git a/src/grim.cc b/src/grim.cc
28index 1ff8fc0..2bf0c39 100755
29--- a/src/grim.cc
30+++ b/src/grim.cc
31@@ -294,37 +294,38 @@ std::string Node::print(int indent) {
32 }
33 
34 
35-std::string trimSpace(std::string str) {
36-    int start = 0;
37-    int end = str.length() - 1;
38-
39-    // Handle empty string case
40+void trimSpace(std::string& str) {
41     if (str.empty()) {
42-        return "";
43+        return;
44     }
45 
46-    // Find the first non-space character
47-    for (/* start is already 0 */; start <= end; ++start) {
48-        if (!std::isspace(str[start])) {
49-            break;
50-        }
51+    // Find first non-space
52+    size_t first_non_space = 0;
53+    while (first_non_space < str.length() && std::isspace(str[first_non_space])) {
54+        first_non_space++;
55     }
56 
57-    // If the loop finished, it means the string was all spaces or empty
58-    if (start > end) {
59-        return ""; // Or a string of spaces, depending on desired behavior for "   "
60+    // If all spaces or empty
61+    if (first_non_space == str.length()) {
62+        str.clear(); // Set to empty string
63+        return;
64     }
65 
66-    // Find the last non-space character
67-    for (/* end is already str.length() - 1 */; end >= start; --end) {
68-        if (!std::isspace(str[end])) {
69-            break;
70-        }
71+    // Find last non-space
72+    size_t last_non_space = str.length() - 1;
73+    while (last_non_space >= first_non_space && std::isspace(str[last_non_space])) {
74+        last_non_space--;
75+    }
76+
77+    // Erase trailing spaces
78+    if (last_non_space < str.length() - 1) {
79+        str.erase(last_non_space + 1);
80     }
81 
82-    // Calculate the length of the substring
83-    // The length is (end_index - start_index) + 1
84-    return str.substr(start, end - start + 1);
85+    // Erase leading spaces
86+    if (first_non_space > 0) {
87+        str.erase(0, first_non_space);
88+    }
89 }
90 
91 struct Style {
92@@ -338,7 +339,8 @@ struct Style {
93 // so they can be store and used in things like finding styles for basemap
94 // and comparing a node to a selector in testSelector. This is a higher level function
95 // that should be ran once perselector and the results saved somewhere
96-std::vector<std::vector<std::string>> parseSelectorParts(std::string selector) {
97+// selector: any CSS selector
98+std::vector<std::vector<std::string>> parseSelectorParts(std::string_view selector) {
99 	// need to account for selectors with parenthesis like: h1:(+ input:required)
100 	// need to convert all single quotes to double quotes
101 	// need to account for commas, will return
102@@ -348,9 +350,11 @@ std::vector<std::vector<std::string>> parseSelectorParts(std::string selector) {
103 	std::vector<std::vector<std::string>> parts;
104 	std::vector<std::string> buffer;
105 	std::string current;
106+
107 	size_t nesting = 0;
108 
109 	size_t sl = selector.length();
110+	current.reserve(sl);
111 
112 	for (size_t e = 0; e < sl; e++) {
113 		char s = selector[e];
114@@ -374,51 +378,60 @@ std::vector<std::vector<std::string>> parseSelectorParts(std::string selector) {
115 		}
116 
117 		if (nesting == 0 && !current.empty()) {
118-			// !ISSUE: Missing space
119 			if (s == ':' || s == '[' || s == ',' || s == '#' || s == '.') {
120 				// We convert any : selectors (like :checked) to [checked]
121 				// because we store every thing as attributes on the Node
122 				// when test selector is ran it will convert all its attributes 
123 				// to [checked] or [href="url"]
124 				// so we can easily compare the two
125-				std::string trimmed = trimSpace(current);
126-				if (!trimmed.empty()) {
127-					if (trimmed[0] == ':' && trimmed.back() != ')') {
128-						trimmed = "[" +trimmed.substr(1)+"]";
129+				if (std::isspace(current.front()) || std::isspace(current.back())) {
130+					trimSpace(current);
131+				}
132+				if (!current.empty()) {
133+					if (current[0] == ':' && current.back() != ')') {
134+						current[0] = '[';
135+						current.push_back(']');
136 					}
137-					buffer.push_back(trimmed);
138+					buffer.push_back(std::move(current));
139 				}
140 				current = s;
141 			} else 
142 			if (s == '>' || s == '+' || s == '~' || s == ' ') {
143-				std::string trimmed = trimSpace(current);
144+				if (std::isspace(current.front()) || std::isspace(current.back())) {
145+					trimSpace(current);
146+				}
147 				
148-				if (trimmed[0] == ':' && trimmed.back() != ')') {
149-					trimmed = "[" +trimmed.substr(1)+"]";
150+				if (current[0] == ':' && current.back() != ')') {
151+					current[0] = '[';
152+					current.push_back(']');
153 				}
154 				
155-				buffer.push_back(trimmed);
156+				buffer.push_back(std::move(current));
157 				buffer.push_back("");
158 				buffer.back() += s;
159-				current = "";
160+				current.erase();
161 			} else if (s != '\0') {
162-				current += s;
163+				current.push_back(s);
164 			}
165 		} else if (s != '\0') {
166-			current += s;
167+			current.push_back(s);
168 		}
169 
170 		if ((s == ',' && nesting == 0) || e == sl-1) {
171 			if (!current.empty() && current != ",") {
172-				std::string trimmed = trimSpace(current);
173-				if (trimmed[0] == ':' && trimmed.back() != ')') {
174-					trimmed = "[" +trimmed.substr(1)+"]";
175+				if (std::isspace(current.front()) || std::isspace(current.back())) {
176+					trimSpace(current);
177+				};
178+				if (current[0] == ':' && current.back() != ')') {
179+					current[0] = '[';
180+					current.push_back(']');
181+
182 				}
183-				buffer.push_back(trimmed);
184+				buffer.push_back(std::move(current));
185 			}
186-			parts.push_back(buffer);
187+			parts.push_back(std::move(buffer));
188 			buffer.clear();
189-			current = "";
190+			current.erase();
191 		}
192 		if (s == '(' || s == '[' || s == '{') {
193 			nesting++;
194diff --git a/src/parser.cc b/src/parser.cc
195index dd54a62..bf2bd02 100644
196--- a/src/parser.cc
197+++ b/src/parser.cc
198@@ -6,7 +6,7 @@
199 #include <cctype>
200 #include <algorithm>
201 
202-std::unordered_map<std::string, std::string> parseAttributes(std::string token) {
203+std::unordered_map<std::string, std::string> parseAttributes(std::string_view token) {
204 	std::unordered_map<std::string, std::string> attrs;
205 	
206 	bool inQuote = false;
207diff --git a/tests/css_selector.cc b/tests/css_selector.cc
208index 25a89cd..38fb7a0 100644
209--- a/tests/css_selector.cc
210+++ b/tests/css_selector.cc
211@@ -2,6 +2,7 @@
212 #include "../include/grim.h"
213 #include "../include/parser.h" 
214 #include <string>
215+#include <string_view>
216 #include <fstream>
217 
218 void checkSelectorParts(std::string selector, std::vector<std::vector<std::string>> expected) {
219@@ -92,6 +93,12 @@ TEST_CASE( "CSS Selection", "[css]" ) {
220 			return parseSelectorParts("#id");
221 		};
222 	}
223+	SECTION("parseSelectorParts can parse a selector with multiple selectors") {
224+		checkSelectorParts("h1, h1 ~p, :last-child", {{"h1"},{"h1","~","p"}, {"[last-child]"}});
225+		BENCHMARK("h1, h1 ~p, :last-child") {
226+			return parseSelectorParts("h1, h1 ~p, :last-child");
227+		};
228+	}
229 }
230 
231 TEST_CASE("testSelector", "[CSS]") {
232@@ -104,6 +111,8 @@ TEST_CASE("testSelector", "[CSS]") {
233 				"<body>"
234 					"<h1 id=\"h1Tag\">Header</h1>"
235 					"<p id=\"paragraph\" class=\"class1 class2\">This is some text for a simple test</p>"
236+					"<div></div>"
237+					"<input type=\"radio\" checked required/>"
238 				"</body>"
239 			"</html>";
240 
241@@ -138,4 +147,80 @@ TEST_CASE("testSelector", "[CSS]") {
242 		REQUIRE(testSelector(p, parseSelectorParts(".class1")));
243 		REQUIRE_FALSE(testSelector(p, parseSelectorParts(".class3")));
244 	}
245+	SECTION("testSelector can match a child combinator") {
246+		auto h1 = document->children[0]->children[1]->children[0].get();
247+		REQUIRE(testSelector(h1, parseSelectorParts("body > h1")));
248+		REQUIRE_FALSE(testSelector(h1, parseSelectorParts("html > h1")));
249+	}
250+	SECTION("testSelector can match a next sibling combinator") {
251+		auto p = document->children[0]->children[1]->children[1].get();
252+		REQUIRE(testSelector(p, parseSelectorParts("h1 + p")));
253+		REQUIRE_FALSE(testSelector(p, parseSelectorParts("h1 + div")));
254+	}
255+	SECTION("testSelector can match a descendant combinator") {
256+		auto p = document->children[0]->children[1]->children[1].get();
257+		REQUIRE(testSelector(p, parseSelectorParts("body p")));
258+		REQUIRE_FALSE(testSelector(p, parseSelectorParts("h1 p")));
259+	}
260+	SECTION("testSelector can match a subsequent-sibling combinator") {
261+		auto p = document->children[0]->children[1]->children[1].get();
262+		REQUIRE(testSelector(p, parseSelectorParts("h1 ~ p")));
263+		REQUIRE_FALSE(testSelector(p, parseSelectorParts("div ~ p")));
264+	}
265+	SECTION("testSelector can match a element with a pseudo class") {
266+		auto input = document->children[0]->children[1]->children[3].get();
267+		REQUIRE(testSelector(input, parseSelectorParts("input:required")));
268+		REQUIRE(testSelector(input, parseSelectorParts(":checked")));
269+		REQUIRE_FALSE(testSelector(input, parseSelectorParts("input:enabled")));
270+	}
271+	SECTION("testSelector can match a element with first-child") {
272+		auto h1 = document->children[0]->children[1]->children[0].get();
273+		REQUIRE(testSelector(h1, parseSelectorParts(":first-child")));
274+	}
275+	SECTION("testSelector can match a element with last-child") {
276+		auto input = document->children[0]->children[1]->children[3].get();
277+		REQUIRE(testSelector(input, parseSelectorParts(":last-child")));
278+	}
279+	SECTION("testSelector can match multiple selectors") {
280+		auto input = document->children[0]->children[1]->children[3].get();
281+		REQUIRE(testSelector(input, parseSelectorParts("h1, h1 ~p, :last-child")));
282+		REQUIRE_FALSE(testSelector(input, parseSelectorParts("h1, h1 ~p, :first-child")));
283+
284+		BENCHMARK("Multiselector and parse") {
285+			return testSelector(input, parseSelectorParts("h1, h1 ~p, :last-child"));
286+		};
287+
288+		auto parts = parseSelectorParts("h1, h1 ~p, :last-child");
289+		BENCHMARK("Multiselector pre-parse") {
290+			return testSelector(input, parts);
291+		};
292+	}
293+	SECTION("testSelector can match :is() selectors") {
294+		auto input = document->children[0]->children[1]->children[3].get();
295+		REQUIRE(testSelector(input, parseSelectorParts(":is(h1, input, div)")));
296+		REQUIRE_FALSE(testSelector(input, parseSelectorParts(":is(h1, html, div)")));
297+	}
298+	SECTION("testSelector can match :where() selectors") {
299+		auto input = document->children[0]->children[1]->children[3].get();
300+		REQUIRE(testSelector(input, parseSelectorParts(":where(h1, input, div)")));
301+		REQUIRE_FALSE(testSelector(input, parseSelectorParts(":where(h1, html, div)")));
302+	}
303+	SECTION("testSelector can match :not() selectors") {
304+		auto input = document->children[0]->children[1]->children[3].get();
305+		REQUIRE(testSelector(input, parseSelectorParts(":not(h1, html, div)")));
306+		REQUIRE_FALSE(testSelector(input, parseSelectorParts(":not(h1, input, div)")));
307+	}
308+	SECTION("testSelector can match nested :not(:is()) selectors") {
309+		auto input = document->children[0]->children[1]->children[3].get();
310+		REQUIRE(testSelector(input, parseSelectorParts(":not(:is(h1, html, div))")));
311+		REQUIRE_FALSE(testSelector(input, parseSelectorParts(":not(:is(h1, input, div))")));
312+
313+		BENCHMARK("is not test") {
314+			return testSelector(input, parseSelectorParts(":not(:is(h1, html, div))"));
315+		};
316+	}
317+	// make for:
318+	// pseudo elements
319+	// *
320+	// pseudo selectors(:has, :where etc)
321 }
322diff --git a/tests/html_parser.cc b/tests/html_parser.cc
323index ce55213..8aee109 100644
324--- a/tests/html_parser.cc
325+++ b/tests/html_parser.cc
326@@ -14,10 +14,8 @@ void checkNode(Node* node, std::string tagName, unsigned long children, std::str
327 	REQUIRE(node->children.size() == children);
328 	REQUIRE(node->getId() == id);
329 
330-	size_t i = 0;
331-	for (auto c : node->classList.values()) {
332-		REQUIRE(c == classes[i]);
333-		i++;
334+	for (size_t i = 0; i < node->classList.length(); i++) {
335+		REQUIRE(node->classList.item(i) == classes[i]);
336 	}
337 
338 	for (auto pair : attributes) {