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) {