home
readme
diff
tree
note
docs
0commit 5e4c38ff3c212cdd9881427ef3f8c2706539a190
1Author: Mason Wright <mason@lakefox.net>
2Date: Sun Jun 29 13:33:00 2025 -0600
3
4 Reworked parseSelectorParts and tests
5
6diff --git a/Makefile b/Makefile
7index 714bc8e..7d1531f 100644
8--- a/Makefile
9+++ b/Makefile
10@@ -2,7 +2,7 @@
11 CCFLAGS = -Wall -Wextra -std=c++23
12 CC = clang++
13
14-.PHONY: all clean run docs tests
15+.PHONY: all clean run docs run_tests
16
17 all: main
18
19@@ -18,11 +18,24 @@ build/grim.o: src/grim.cc include/grim.h | build
20 build/parser.o: src/parser.cc include/parser.h | build
21 ${CC} ${CCFLAGS} -Iinclude/ -c src/parser.cc -o build/parser.o
22
23-tests: build/grim.o build/parser.o build/catch.o
24- clear && ${CC} ${CCFLAGS} -Itests/ -Iinclude/ ./tests/main.cc ./build/grim.o ./build/parser.o ./build/catch.o -o ./tests/main && ./tests/main
25
26-build/catch.o: ./tests/catch_amalgamated.cpp ./tests/catch_amalgamated.hpp | build
27- ${CC} ${CCFLAGS} -Itests/ -c ./tests/catch_amalgamated.cpp -o build/catch.o
28+
29+# --- Tests ---
30+
31+run_tests: build/html_parser_test
32+ ./build/html_parser_test
33+
34+build/css_selector_test: tests/css_selector.cc build/grim.o build/catch.o | build
35+ ${CC} ${CCFLAGS} -Iinclude/ ./tests/css_selector.cc ./build/grim.o ./build/catch.o -o ./build/css_selector_test
36+
37+build/html_parser_test: tests/html_parser.cc build/grim.o build/parser.o build/catch.o | build
38+ ${CC} ${CCFLAGS} -Iinclude/ ./tests/html_parser.cc ./build/grim.o ./build/parser.o ./build/catch.o -o ./build/html_parser_test
39+
40+build/catch.o: ./include/catch_amalgamated.cpp ./include/catch_amalgamated.hpp | build
41+ ${CC} ${CCFLAGS} -Iinclude/ -c ./include/catch_amalgamated.cpp -o build/catch.o
42+
43+
44+# --- Tools ---
45
46 clean:
47 rm -f main build/*.o
48diff --git a/tests/catch_amalgamated.cpp b/include/catch_amalgamated.cpp
49similarity index 100%
50rename from tests/catch_amalgamated.cpp
51rename to include/catch_amalgamated.cpp
52diff --git a/tests/catch_amalgamated.hpp b/include/catch_amalgamated.hpp
53similarity index 100%
54rename from tests/catch_amalgamated.hpp
55rename to include/catch_amalgamated.hpp
56diff --git a/include/grim.h b/include/grim.h
57index 239dc8f..d0fe315 100644
58--- a/include/grim.h
59+++ b/include/grim.h
60@@ -5,6 +5,7 @@
61 #include <string>
62 #include <vector>
63 #include <unordered_map>
64+#include <unordered_set>
65 #include <memory>
66
67 struct Styles {
68@@ -89,4 +90,6 @@ public:
69 std::string print(int indent = 0);
70 };
71
72+std::vector<std::vector<std::string>> parseSelectorParts(std::string);
73+
74 #endif // NODE_H
75diff --git a/src/grim.cc b/src/grim.cc
76index 6600855..25bad64 100755
77--- a/src/grim.cc
78+++ b/src/grim.cc
79@@ -181,127 +181,118 @@ std::string Node::print(int indent) {
80 return out;
81 }
82
83-struct Style {
84- std::unordered_map<std::string, std::string> properties;
85- std::string selector;
86- // Index of when it was added (for cascading)
87- size_t index;
88-};
89-
90-// struct BaseParts {
91-// std::string tagName;
92-// std::string id;
93-// std::vector<std::string> classes;
94-// std::unordered_map<std::string, std::string> attributes;
95-// }
96
97-std::vector<std::string> parseSelectorParts(std::string selector) {
98- // we want to find the right most (right of a " " > ~ + ) selector
99- // account for * for commas split the selector then for each parse right
100+std::string trimSpace(std::string str) {
101+ int start = 0;
102+ int end = str.length() - 1;
103
104- std::vector<std::string> parts;
105- std::string word;
106+ // Handle empty string case
107+ if (str.empty()) {
108+ return "";
109+ }
110
111- for (size_t s = 0; s < selector.length(); s++) {
112- // Start parsing if we see a comma or the end of the selector
113- word += selector[s];
114+ // Find the first non-space character
115+ for (/* start is already 0 */; start <= end; ++start) {
116+ if (!std::isspace(str[start])) {
117+ break;
118+ }
119+ }
120
121- if (selector[s] == ',' || s == selector.length()-1) {
122- if (selector[s] == ',') {
123- word.pop_back(); // Remove the trailing comma
124- }
125- // Break the tag and add the right most parts to parts
126- for (int i = word.length()-1; i > -1; i--) {
127- if (
128- word[i] == ' ' ||
129- word[i] == '>' ||
130- word[i] == '~' ||
131- word[i] == '+'
132- ) {
133- word = word.substr(i+1);
134- break;
135- }
136- }
137+ // If the loop finished, it means the string was all spaces or empty
138+ if (start > end) {
139+ return ""; // Or a string of spaces, depending on desired behavior for " "
140+ }
141
142- // Now word contains the right most selection
143- // we will extract the quick to verify parts from it
144- // so we can build the basemap
145- std::string part;
146- bool inbracket = false;
147- for (auto w : word) {
148-// Need to add :checked etc parsing
149- if (w == ':' && !inbracket) {
150- break;
151- }
152- // We don't care if the thing we are splitting is a id or class we just
153- // need them split up so we can use them as map keys
154+ // Find the last non-space character
155+ for (/* end is already str.length() - 1 */; end >= start; --end) {
156+ if (!std::isspace(str[end])) {
157+ break;
158+ }
159+ }
160
161- if (w == '#' && !inbracket) {
162- if (part != "") {
163- parts.push_back(part);
164- part = "";
165- }
166+ // Calculate the length of the substring
167+ // The length is (end_index - start_index) + 1
168+ return str.substr(start, end - start + 1);
169+}
170
171- } else if (w == '.' && !inbracket) {
172- if (part != "") {
173- parts.push_back(part);
174- part = "";
175- }
176- } else if (w == '[' || w == ']') {
177-
178- // We need to check if its in brackets or not because the [ will
179- // close the last .class, if we don't check then it will be in brackets
180- if (part != "" && inbracket) {
181- parts.push_back('['+part+']');
182- part = "";
183- } else if (!inbracket) {
184- parts.push_back(part);
185- part = "";
186- }
187+struct Style {
188+ std::unordered_map<std::string, std::string> properties;
189+ std::vector<std::vector<std::string>> selector;
190+ // Index of when it was added (for cascading)
191+ size_t index;
192+};
193
194- if (w=='[') {
195- inbracket = true;
196- } else if (w == ']'){
197- inbracket = false;
198- }
199- continue;
200- }
201+// parseSelectorParts deconstructs a selector into its indiviual parts
202+// so they can be store and used in things like finding styles for basemap
203+// and comparing a node to a selector in testSelector. This is a higher level function
204+// that should be ran once perselector and the results saved somewhere
205+std::vector<std::vector<std::string>> parseSelectorParts(std::string selector) {
206+ // need to account for selectors with parenthesis like: h1:(+ input:required)
207+ // need to convert all single quotes to double quotes
208+ // need to account for commas, will return
209+ // need to colapse all spaces to a single space
210+ // need to trim spaces
211+
212+ std::vector<std::vector<std::string>> parts;
213+ std::vector<std::string> buffer;
214+ std::string current;
215+ size_t nesting = 0;
216
217- // We need to keep a consistant quote mark to make matching reliable
218- if (inbracket && w == '\'') {
219- w = '"';
220- }
221+ size_t sl = selector.length();
222
223- part += w;
224- }
225+ for (size_t e = 0; e < sl; e++) {
226+ char s = selector[e];
227+
228+ if (s == '\'') {
229+ // convert single quotes to double quotes
230+ s = '\"';
231+ }
232
233- if (part != "") {
234- parts.push_back(part);
235+ if (s == ' ' && nesting == 0) {
236+ if (e > 0 && selector[e-1] != ':' && selector[e-1] != ',' && selector[e-1] != '#' && selector[e-1] != '.') {
237+ continue;
238+ } else
239+ if (e < sl-1 && selector[e+1] != ':' && selector[e+1] != ',' && selector[e+1] != '#' && selector[e+1] != '.') {
240+ continue;
241 }
242 }
243- }
244-
245- std::vector<std::string> deduplicated;
246
247- for (size_t p1 = 0; p1 < parts.size(); p1++) {
248- bool matches = false;
249-
250- if (parts[p1] == "") {
251- continue;
252+ if (nesting == 0 && !current.empty()) {
253+ // !ISSUE: Missing space
254+ if (s == ':' || s == '[' || s == ',' || s == '#' || s == '.' || s == ' ') {
255+ buffer.push_back(trimSpace(current));
256+ current = s;
257+ } else
258+ if (s == '>' || s == '+' || s == '~') {
259+ buffer.push_back(trimSpace(current));
260+ buffer.push_back("");
261+ buffer.back() += s;
262+ current = "";
263+ } else {
264+ current += s;
265+ }
266+ } else {
267+ current += s;
268 }
269
270- for (size_t p2 = p1+1; p2 < parts.size(); p2++) {
271- if (parts[p1] == parts[p2]) {
272- matches = true;
273+ if ((s == ',' && nesting == 0) || e == sl-1) {
274+ //std::cout << selector.substr(start, e-start+1) << std::endl;
275+ if (!current.empty() && current != ",") {
276+ buffer.push_back(trimSpace(current));
277 }
278+ parts.push_back(buffer);
279+ buffer = {};
280+ current = "";
281 }
282-
283- if (!matches) {
284- deduplicated.push_back(parts[p1]);
285+ if (s == '(' || s == '[' || s == '{') {
286+ nesting++;
287+ } else if (s == ')' || s == ']' || s == '}') {
288+ nesting--;
289 }
290- }
291
292- return deduplicated;
293+ }
294+
295+ return parts;
296 }
297
298 class StyleHandler {
299@@ -348,36 +339,17 @@ class StyleHandler {
300 return parts;
301 }
302
303- std::string cleanSelector(std::string selector) {
304- std::string cleaned;
305- for (size_t i = 0; i < selector.length(); i++) {
306- if (selector[i] == ' ' && i > 0 && i < selector.length()-1) {
307- char prev = selector[i-1];
308- char next = selector[i+1];
309-
310- if (prev != '>' && prev != '+' && prev != '~' && next != '>' && next != '+' && next != '~' && !std::isspace(prev) && !std::isspace(next)) {
311- cleaned += selector[i];
312- }
313- } else if (selector[i] == ' ' && (i == 0 || i == selector.length()-1)) {
314- continue;
315- } else {
316- cleaned += selector[i];
317- }
318- }
319- return cleaned;
320- }
321 public:
322- void add(Style style) {
323- std::string selector = cleanSelector(style.selector);
324- style.selector = selector;
325+ void add(std::string selector, std::unordered_map<std::string, std::string> properties) {
326+ // Type is 2d vector of selector parts
327+ auto parts = parseSelectorParts(selector);
328 size_t index = styles.size();
329+ Style style = {properties,parts,index};
330
331- style.index = index;
332 styles.push_back(std::move(style));
333-
334- std::vector<std::string> parts = parseSelectorParts(selector);
335-
336- for (auto p : parts) {
337+// !ISSUE: Re do the basemap mapping in the add function
338+// + will need to go through each 1d then once in start from right and go left until >+~" " or eol
339+/* for (auto p : parts) {
340 if (basemap.find(p) != basemap.end()) {
341 basemap[p].push_back(index);
342 } else {
343@@ -385,6 +357,7 @@ class StyleHandler {
344 basemap[p] = bm;
345 }
346 }
347+*/
348 }
349
350 std::unordered_map<std::string, std::string> getStyles(Node* node) {
351@@ -415,56 +388,7 @@ class StyleHandler {
352 }
353 };
354
355-std::vector<std::string> splitSelector(std::string selector, char key) {
356- size_t nesting = 0;
357-
358- std::vector<std::string> selectors;
359- std::string current = "";
360-
361- for (auto s : selector) {
362- if (s == '(') {
363- nesting++;
364- } else if (s == ')') {
365- nesting--;
366- } else if (s == '[') {
367- nesting++;
368- } else if (s == ']') {
369- nesting--;
370- } else if (s == '{') {
371- nesting++;
372- } else if (s == '}') {
373- nesting--;
374- } else if (s == key && nesting == 0) {
375- selectors.push_back(current);
376- current = "";
377- continue;
378- }
379- current += s;
380- }
381-
382- selectors.push_back(current);
383
384- // Trim the space
385- for (size_t i = 0; i < selectors.size(); i++) {
386- size_t start = 0;
387- size_t end = selectors[i].length()-1;
388- for (size_t s = 0; s < selectors[i].length(); s++) {
389- if (!std::isspace(selectors[i][s])) {
390- start = s;
391- break;
392- }
393- }
394- for (size_t e = selectors[i].length()-1; e > start; e--) {
395- if (!std::isspace(selectors[i][e])) {
396- end = e;
397- break;
398- }
399- }
400- selectors[i] = selectors[i].substr(start,end - start +1);
401- }
402-
403- return selectors;
404-}
405
406 void popSelector(std::string selector, std::string& trimmed, std::string& popped, char& symbol) {
407 for (size_t i = selector.length()-1; i >= 0; i--) {
408@@ -483,7 +407,7 @@ void popSelector(std::string selector, std::string& trimmed, std::string& popped
409 }
410 }
411
412-
413+/*
414 bool testSelector(Node* node, std::string selector) {
415 // By the time we get here, we know the right most selector some what matches
416 // so now if it is a compound selector we should see if the parent('s) some what match
417@@ -491,17 +415,6 @@ bool testSelector(Node* node, std::string selector) {
418 return true;
419 }
420
421- std::vector<std::string> selectors = splitSelector(selector, ',');
422-
423- if (selectors.size() > 1) {
424- for (auto s : selectors) {
425- bool match = testSelector(node, s);
426-
427- if (match) {
428- return true;
429- }
430- }
431- }
432
433 std::string trimmed, popped;
434 char symbol;
435@@ -510,7 +423,7 @@ bool testSelector(Node* node, std::string selector) {
436
437 // Check if popped matches this element
438
439- std::vector<std::string> pParts = parseSelectorParts(popped);
440+ std::vector<std::vector<std::string>> pParts = parseSelectorParts(popped);
441
442 bool matched = true;
443
444@@ -528,9 +441,10 @@ bool testSelector(Node* node, std::string selector) {
445 attrs.push_back("["+a.first+"]");
446 }
447 }
448-
449- for (size_t i = 0; i < pParts.size(); i++) {
450- std::string p = pParts[i];
451+
452+ size_t i = 0;
453+ for (auto p : pParts) {
454+ i++;
455 if (i == 0 && p[0] != '#' && p[0] != '.' && p[0] != '[') {
456 matched = node->getTagName() == p;
457 } else if (p[0] == '#') {
458@@ -623,4 +537,4 @@ bool testSelector(Node* node, std::string selector) {
459 return matched;
460 }
461
462-
463+*/
464diff --git a/src/grim.cc.bak b/src/grim.cc.bak
465new file mode 100755
466index 0000000..36ec981
467--- /dev/null
468+++ b/src/grim.cc.bak
469@@ -0,0 +1,602 @@
470+#include "grim.h"
471+#include <iostream>
472+#include <sstream>
473+#include <algorithm>
474+#include <stdexcept>
475+#include <cctype>
476+
477+std::string ClassList::value() const {
478+ if (classes.empty()) {
479+ return "";
480+ }
481+ std::string collection = classes[0];
482+ for (size_t i = 1; i < classes.size(); ++i) {
483+ collection += " " + classes[i];
484+ }
485+ return collection;
486+}
487+
488+std::vector<std::string> ClassList::values() {
489+ return classes;
490+}
491+
492+void ClassList::add(std::string value) {
493+ classes.push_back(value);
494+}
495+
496+void ClassList::remove(std::string value) {
497+ auto it_prev = std::find(classes.begin(), classes.end(), value);
498+ if (it_prev != classes.end()) {
499+ *it_prev = classes.back();
500+ classes.pop_back();
501+ }
502+}
503+
504+// Constructor
505+Node::Node() : parent(nullptr) {}
506+
507+std::string Node::getTagName() const { return TagName; }
508+void Node::setTagName(const std::string& name) { TagName = name; }
509+
510+const std::unordered_map<std::string, std::string>& Node::getAttributes() const {
511+ return Attributes;
512+}
513+
514+// Implement the getter/setter methods declared using the macro
515+#define IMPLEMENT_ATTRIBUTE_ACCESSORS(_Type, _FuncNameSuffix, _AttrKeyString) \
516+ _Type Node::get##_FuncNameSuffix() const { \
517+ return getAttribute<_Type>(_AttrKeyString); \
518+ } \
519+ void Node::set##_FuncNameSuffix(_Type value) { \
520+ setAttribute(_AttrKeyString, value); \
521+ }
522+
523+IMPLEMENT_ATTRIBUTE_ACCESSORS(std::string, Id, "id")
524+IMPLEMENT_ATTRIBUTE_ACCESSORS(std::string, InnerText, "innerText")
525+IMPLEMENT_ATTRIBUTE_ACCESSORS(bool, ContentEditable, "contenteditable")
526+IMPLEMENT_ATTRIBUTE_ACCESSORS(std::string, Href, "href")
527+IMPLEMENT_ATTRIBUTE_ACCESSORS(std::string, Src, "src")
528+IMPLEMENT_ATTRIBUTE_ACCESSORS(std::string, Title, "title")
529+IMPLEMENT_ATTRIBUTE_ACCESSORS(std::string, Value, "value")
530+IMPLEMENT_ATTRIBUTE_ACCESSORS(int, TabIndex, "tabindex")
531+IMPLEMENT_ATTRIBUTE_ACCESSORS(bool, Disabled, "disabled")
532+IMPLEMENT_ATTRIBUTE_ACCESSORS(bool, Required, "required")
533+IMPLEMENT_ATTRIBUTE_ACCESSORS(bool, Checked, "checked")
534+
535+Node* Node::createElement(std::string name) {
536+ std::unique_ptr<Node> newNode = std::make_unique<Node>();
537+ newNode->setTagName(name);
538+ newNode->parent = this;
539+ children.push_back(std::move(newNode));
540+ return children.back().get();
541+}
542+
543+template<typename T>
544+void Node::setAttribute(const std::string& name, const T& value) {
545+ if constexpr (std::is_same_v<T, bool>) {
546+ if (value) {
547+ Attributes[name] = "";
548+ } else {
549+ Attributes.erase(name);
550+ }
551+ } else if constexpr (std::is_arithmetic_v<T>) {
552+ Attributes[name] = std::to_string(value);
553+ } else {
554+ static_assert(std::is_convertible_v<T, std::string>,
555+ "setAttribute: Type cannot be converted to std::string automatically.");
556+ Attributes[name] = static_cast<std::string>(value);
557+ }
558+}
559+
560+void Node::setAttribute(const std::string& name, const std::string& value) {
561+ Attributes[name] = value;
562+}
563+
564+template<typename T>
565+T Node::getAttribute(const std::string& name) const {
566+ auto it = Attributes.find(name);
567+ if (it != Attributes.end()) {
568+ const std::string& s = it->second;
569+
570+ if constexpr (std::is_same_v<T, int>) {
571+ try {
572+ return std::stoi(s);
573+ } catch (const std::invalid_argument& e) {
574+ return 0;
575+ } catch (const std::out_of_range& e) {
576+ return 0;
577+ }
578+ } else if constexpr (std::is_same_v<T, bool>) {
579+ std::string lower_s = s;
580+ std::transform(lower_s.begin(), lower_s.end(), lower_s.begin(),
581+ [](unsigned char c){ return std::tolower(c); });
582+ return (!lower_s.empty() && lower_s != "false");
583+ } else if constexpr (std::is_same_v<T, double>) {
584+ try {
585+ return std::stod(s);
586+ } catch (const std::invalid_argument& e) {
587+ return 0.0;
588+ } catch (const std::out_of_range& e) {
589+ return 0.0;
590+ }
591+ } else {
592+ static_assert(std::is_convertible_v<std::string, T>,
593+ "getAttribute: Type conversion from std::string not implemented for this type.");
594+ return static_cast<T>(s);
595+ }
596+ }
597+ return T();
598+}
599+
600+std::string Node::getAttribute(const std::string& name) const {
601+ auto it = Attributes.find(name);
602+ if (it != Attributes.end()) {
603+ return it->second;
604+ }
605+ return "";
606+}
607+
608+std::vector<std::string> Node::getAttributeKeys() {
609+ std::vector<std::string> keys;
610+
611+ for(auto p : Attributes) {
612+ keys.push_back(p.first);
613+ }
614+ return keys;
615+}
616+
617+std::string Node::print(int indent) {
618+ std::string out = "";
619+ for (int i = 0; i < indent; ++i) {
620+ out += " ";
621+ }
622+
623+ out += "<" + getTagName();
624+ for (const auto& attr_pair : getAttributes()) {
625+ if (attr_pair.first == "innerText" || attr_pair.first == "tagName") {
626+ continue;
627+ }
628+ out += " " + attr_pair.first;
629+ if (!attr_pair.second.empty()) {
630+ out += "=\"" + attr_pair.second + "\"";
631+ }
632+ }
633+ out += ">";
634+
635+ if (!getAttribute("innerText").empty()) {
636+ out += "\n" + getAttribute("innerText");
637+ }
638+
639+ out += "\n";
640+
641+ for (const auto& child : children) {
642+ out += child->print(indent + 1)+"\n";
643+ }
644+
645+ for (int i = 0; i < indent; ++i) {
646+ out += " ";
647+ }
648+ out += "</" + getTagName() + ">\n";
649+
650+ return out;
651+}
652+
653+struct Style {
654+ std::unordered_map<std::string, std::string> properties;
655+ std::string selector;
656+ // Index of when it was added (for cascading)
657+ size_t index;
658+};
659+
660+// splitSelector takes a CSS selector and splits it by a key
661+// it ingores any key that is inside (), [], or {}
662+// Used in testSelector to take a compound selector and match each one to the
663+// passed node until a match is found.
664+std::vector<std::string> splitSelector(std::string selector, char key) {
665+ size_t nesting = 0;
666+
667+ std::vector<std::string> selectors;
668+ std::string current = "";
669+
670+ for (auto s : selector) {
671+ if (s == '(') {
672+ nesting++;
673+ } else if (s == ')') {
674+ nesting--;
675+ } else if (s == '[') {
676+ nesting++;
677+ } else if (s == ']') {
678+ nesting--;
679+ } else if (s == '{') {
680+ nesting++;
681+ } else if (s == '}') {
682+ nesting--;
683+ } else if (s == key && nesting == 0) {
684+ selectors.push_back(current);
685+ current = "";
686+ continue;
687+ }
688+ current += s;
689+ }
690+
691+ selectors.push_back(current);
692+
693+ // Trim the space
694+ for (size_t i = 0; i < selectors.size(); i++) {
695+ size_t start = 0;
696+ size_t end = selectors[i].length()-1;
697+ for (size_t s = 0; s < selectors[i].length(); s++) {
698+ if (!std::isspace(selectors[i][s])) {
699+ start = s;
700+ break;
701+ }
702+ }
703+ for (size_t e = selectors[i].length()-1; e > start; e--) {
704+ if (!std::isspace(selectors[i][e])) {
705+ end = e;
706+ break;
707+ }
708+ }
709+ selectors[i] = selectors[i].substr(start,end - start +1);
710+ }
711+
712+ return selectors;
713+}
714+
715+// This function is used to create a "short list" of full selectors
716+// that may apply to the element we are targeting.
717+// When we add a selector to the StyleHandler we use this function to
718+// create the basemap keys for the style we are adding.
719+// Additionally we will use this in the testSelector function to test each part of an
720+// of a selector to an element
721+// Example: given body > h1:required
722+// We use parseSelectorParts to get: [h1, :required]
723+// We test the first element given and it is a required h1
724+// Next in the testSelector function we see the selector has a >
725+// so we pass the parent and the second part of the selector:
726+// body
727+// Then the process repeats
728+std::vector<std::string> parseSelectorParts(std::string selector) {
729+ // we want to find the right most (right of a " " > ~ + ) selector
730+ // need to account for selectors with parenthesis like: h1:(+ input:required)
731+ // need to convert all single quotes to double quotes
732+ // need to account for commas, will return
733+ // need to claps all spaces to a single space
734+ // need to trim spaces
735+
736+ std::vector<std::string> parts;
737+ size_t nesting = 0;
738+
739+ std::string current = "";
740+
741+ for (auto s : selector) {
742+ if (s == '(') {
743+ nesting++;
744+ } else if (s == ')') {
745+ nesting--;
746+ } else if (s == '[') {
747+ nesting++;
748+ } else if (s == ']') {
749+ nesting--;
750+ } else if (s == '{') {
751+ nesting++;
752+ } else if (s == '}') {
753+ nesting--;
754+ } else if (s == ',' && nesting == 0) {
755+ // Same logic as splitSelector but we want to inline it here because we
756+ // are already returning a vector so why make two?
757+ //
758+ // Here is the selector part parsing logic, we go backwards until we hit a combined
759+ std::string part;
760+ bool prevWasSpace = false;
761+ for (size_t i = current.length()-1; i >= 0; i--) {
762+ char c = current[i];
763+ if (c == '(') {
764+ nesting++;
765+ continue;
766+ } else if (c == ')') {
767+ nesting--;
768+ continue;
769+ }
770+
771+ // We will still claps and convert while nested
772+ if (c == ' ') {
773+ // Claps spaces (will not trim)
774+ if (!prevWasSpace) {
775+ part = c + part;
776+ prevWasSpace = true;
777+ }
778+ continue;
779+ } else {
780+ prevWasSpace = false;
781+
782+ if (c == '\'') {
783+ // convert single quotes to double quotes
784+ part = '\"' + part;
785+ }
786+ continue;
787+ }
788+ if (nesting == 0) {
789+
790+ }
791+ }
792+ current = "";
793+ continue;
794+ }
795+ current += s;
796+ }
797+
798+ return deduplicated;
799+}
800+
801+class StyleHandler {
802+ private:
803+ // basemap: Maps baseparts to a index of the styles vector pointing to a Style object
804+ // because processing a CSS selector isn't trivial we want to make a short list of
805+ // the styles that can possibly be a match so we aren't spending a lot of compute
806+ // on a selector that applies to the wrong element. Todo this we take the right most part
807+ // of a selector (that is the part that targets the current element) and we do a few checks
808+ // like is the tagname the same, does it have that class, and do the id's match. If so that
809+ // is a candidate and we run the full selector on it. That includes checking parents,children,
810+ // or what ever else the selector needs to match.
811+ std::unordered_map<std::string, std::vector<size_t>> basemap;
812+ std::vector<Style> styles;
813+
814+
815+ std::vector<std::string> parseNodeParts(Node* node) {
816+ std::vector<std::string> parts;
817+
818+ parts.push_back(node->getTagName());
819+ parts.push_back("#"+node->getId());
820+
821+ // classes and attributes
822+ auto keys = node->getAttributeKeys();
823+
824+ for (auto k : keys) {
825+ if (k != "tagName" || k != "id" || k != "innerText" || k != "class") {
826+ std::string v;
827+ std::string av = node->getAttribute(k);
828+ if (av != "") {
829+ v = "=\""+av+"\"";
830+
831+ }
832+ parts.push_back('['+k+v+']');
833+ }
834+ }
835+
836+ auto classes = node->classList.values();
837+
838+ for (auto c : classes) {
839+ parts.push_back("."+c);
840+ }
841+
842+ return parts;
843+ }
844+
845+ std::string cleanSelector(std::string selector) {
846+ std::string cleaned;
847+ for (size_t i = 0; i < selector.length(); i++) {
848+ if (selector[i] == ' ' && i > 0 && i < selector.length()-1) {
849+ char prev = selector[i-1];
850+ char next = selector[i+1];
851+
852+ if (prev != '>' && prev != '+' && prev != '~' && next != '>' && next != '+' && next != '~' && !std::isspace(prev) && !std::isspace(next)) {
853+ cleaned += selector[i];
854+ }
855+ } else if (selector[i] == ' ' && (i == 0 || i == selector.length()-1)) {
856+ continue;
857+ } else {
858+ cleaned += selector[i];
859+ }
860+ }
861+ return cleaned;
862+ }
863+ public:
864+ void add(Style style) {
865+ std::string selector = cleanSelector(style.selector);
866+ style.selector = selector;
867+ size_t index = styles.size();
868+
869+ style.index = index;
870+ styles.push_back(std::move(style));
871+
872+ std::vector<std::string> parts = parseSelectorParts(selector);
873+
874+ for (auto p : parts) {
875+ if (basemap.find(p) != basemap.end()) {
876+ basemap[p].push_back(index);
877+ } else {
878+ std::vector<size_t> bm = {index};
879+ basemap[p] = bm;
880+ }
881+ }
882+ }
883+
884+ std::unordered_map<std::string, std::string> getStyles(Node* node) {
885+ std::vector<std::string> parts = parseNodeParts(node);
886+
887+ std::vector<size_t> canidates;
888+
889+ for (auto p : parts) {
890+ if (auto sty = basemap.find(p); sty != basemap.end()) {
891+ for (auto s : sty->second) {
892+ bool found = false;
893+ // This makes sure we aren't adding the same styles to the candidates list
894+ for (auto c : canidates) {
895+ if (c == s) {
896+ found = true;
897+ break;
898+ }
899+ }
900+ if (!found) {
901+ canidates.push_back(s);
902+ }
903+ }
904+ }
905+ }
906+
907+ // Now we have the list of candidates next we will refined our selection
908+ // we need to pull the selectors from each candidate then run testSelector on it
909+ }
910+};
911+
912+
913+
914+void popSelector(std::string selector, std::string& trimmed, std::string& popped, char& symbol) {
915+ for (size_t i = selector.length()-1; i >= 0; i--) {
916+ if (
917+ selector[i] == ' ' ||
918+ selector[i] == '>' ||
919+ selector[i] == '~' ||
920+ selector[i] == '+'
921+ ) {
922+ symbol = selector[i];
923+ trimmed = selector.substr(0,i);
924+ break;
925+ } else {
926+ popped = selector[i]+popped;
927+ }
928+ }
929+}
930+
931+
932+bool testSelector(Node* node, std::string selector) {
933+ // By the time we get here, we know the right most selector some what matches
934+ // so now if it is a compound selector we should see if the parent('s) some what match
935+ if (selector == "*") {
936+ return true;
937+ }
938+
939+ std::vector<std::string> selectors = splitSelector(selector, ',');
940+
941+ if (selectors.size() > 1) {
942+ for (auto s : selectors) {
943+ bool match = testSelector(node, s);
944+
945+ if (match) {
946+ return true;
947+ }
948+ }
949+ }
950+
951+ std::string trimmed, popped;
952+ char symbol;
953+ popSelector(selector, trimmed, popped, symbol);
954+
955+
956+ // Check if popped matches this element
957+
958+ std::vector<std::string> pParts = parseSelectorParts(popped);
959+
960+ bool matched = true;
961+
962+ std::vector<std::string> nodeClasses = node->classList.values();
963+ std::unordered_map<std::string, std::string> attributes = node->getAttributes();
964+
965+
966+ std::vector<std::string> attrs;
967+
968+ for (auto a : attributes) {
969+ if (a.second != "") {
970+ attrs.push_back("["+a.first+"=\""+a.second+"\"]");
971+ } else {
972+
973+ attrs.push_back("["+a.first+"]");
974+ }
975+ }
976+
977+ for (size_t i = 0; i < pParts.size(); i++) {
978+ std::string p = pParts[i];
979+ if (i == 0 && p[0] != '#' && p[0] != '.' && p[0] != '[') {
980+ matched = node->getTagName() == p;
981+ } else if (p[0] == '#') {
982+ matched = node->getId() == p;
983+ } else if (p[0] == '.') {
984+ // Remove the period
985+ std::string clipped = p.substr(1);
986+ bool found = false;
987+ for (auto c : nodeClasses) {
988+ if (clipped == c) {
989+ found = true;
990+ break;
991+ }
992+ }
993+ matched = found;
994+ } else if (p[0] == '[') {
995+ bool found = false;
996+ for (auto a : attrs) {
997+ if (p == a) {
998+ found = true;
999+ break;
1000+ }
1001+ }
1002+ matched = found;
1003+ }
1004+
1005+ if (!matched) {
1006+ // Return out of the function
1007+ return false;
1008+ }
1009+ }
1010+
1011+ // The rest of the logic assumes the other selectors do not match until proven otherwise
1012+ matched = false;
1013+
1014+ if (symbol == '\0' || node->parent != nullptr) {
1015+ return false;
1016+ }
1017+
1018+ if (symbol == '>') {
1019+ // We are just testing the parent node here
1020+ matched = testSelector(node->parent, trimmed);
1021+ } else if (symbol == '+') {
1022+ const auto& children = node->parent->children;
1023+ if (children.size() > 1) {
1024+ for (size_t i = 0; i < children.size(); i++) {
1025+ Node* child_ptr = children[i].get();
1026+
1027+ if (child_ptr == node) {
1028+ if (i == 0) {
1029+ return false;
1030+ } else {
1031+ // In this selector we just need to get the direct sibling and test it
1032+ matched = testSelector(children[i-1].get(), trimmed);
1033+ }
1034+ break;
1035+ }
1036+ }
1037+ }
1038+ } else if (symbol == '~') {
1039+ const auto& children = node->parent->children;
1040+ if (children.size() > 1) {
1041+ for (size_t i = 0; i < children.size(); i++) {
1042+ Node* child_ptr = children[i].get();
1043+
1044+ if (child_ptr == node) {
1045+ break;
1046+ } else {
1047+ // We have to test every selector before the current element,
1048+ // if any match then it matches
1049+ matched = testSelector(children[i].get(), trimmed);
1050+ if (matched) {
1051+ break;
1052+ }
1053+ }
1054+ }
1055+ }
1056+ } else if (symbol == ' ') {
1057+ // For the descendant combinator we have to check every parent until we match or nullptr out
1058+ Node* current = node->parent;
1059+ while (current != nullptr) {
1060+ if (testSelector(current, trimmed)) {
1061+ matched = true;
1062+ break;
1063+ }
1064+ current = current->parent;
1065+ }
1066+ }
1067+
1068+ return matched;
1069+}
1070+
1071+
1072diff --git a/tests/css_selector.cc b/tests/css_selector.cc
1073new file mode 100644
1074index 0000000..44a9589
1075--- /dev/null
1076+++ b/tests/css_selector.cc
1077@@ -0,0 +1,77 @@
1078+#include <catch_amalgamated.hpp>
1079+#include "../include/grim.h"
1080+#include <string>
1081+#include <unordered_set>
1082+#include <fstream>
1083+
1084+void checkSelectorParts(std::string selector, std::vector<std::vector<std::string>> expected) {
1085+ INFO("=========================");
1086+ INFO("Selector: "+selector+"\n");
1087+
1088+ std::vector<std::vector<std::string>> css = parseSelectorParts(selector);
1089+
1090+ REQUIRE(css.size() == expected.size());
1091+
1092+ std::string note = "";
1093+ for (size_t a = 0; a < css.size(); a++) {
1094+ std::string str = "{";
1095+ for (size_t b = 0; b < css[a].size(); b++) {
1096+ str += "\""+css[a][b]+"\",";
1097+ }
1098+ str += "},";
1099+
1100+ note += str;
1101+ }
1102+ INFO("PARSED: "+note);
1103+
1104+ for (size_t a = 0; a < css.size(); a++) {
1105+ REQUIRE(css[a].size() == expected[a].size());
1106+
1107+ for (size_t b = 0; b < css[a].size(); b++) {
1108+ REQUIRE(css[a][b] == expected[a][b]);
1109+ }
1110+ }
1111+
1112+
1113+}
1114+
1115+TEST_CASE( "CSS Selection", "[css]" ) {
1116+ SECTION("parseSelectorParts can parse a simple CSS selector") {
1117+ // This function only parses the last part of the selector
1118+ checkSelectorParts("div > h1", {{"div", ">", "h1"}});
1119+ BENCHMARK("div > h1") {
1120+ return parseSelectorParts("div > h1");
1121+ };
1122+ }
1123+ SECTION("parseSelectorParts can parse a simple CSS selector with a psuedo class selector") {
1124+ checkSelectorParts("div > h1:checked", {{"div", ">", "h1",":checked"}});
1125+ BENCHMARK("div > h1:checked") {
1126+ return parseSelectorParts("div > h1:checked");
1127+ };
1128+ }
1129+ SECTION("parseSelectorParts can parse a attribute selector") {
1130+ checkSelectorParts("a[href=\"https://www.example.com/\"]", {{"a", "[href=\"https://www.example.com/\"]"}});
1131+ BENCHMARK("a[href=\"https://www.example.com/\"]") {
1132+ return parseSelectorParts("a[href=\"https://www.example.com/\"]");
1133+ };
1134+ }
1135+ SECTION("parseSelectorParts can parse has()") {
1136+ checkSelectorParts("h1:has(+ h2)", {{"h1",":has(+ h2)"}});
1137+ BENCHMARK("h1:has(+ h2)") {
1138+ return parseSelectorParts("h1:has(+ h2)");
1139+ };
1140+ }
1141+
1142+ SECTION("parseSelectorParts can parse a complex where() selector") {
1143+ checkSelectorParts(":where(ol, ul, menu:unsupported)", {{":where(ol, ul, menu:unsupported)"}});
1144+ BENCHMARK(":where(ol, ul, menu:unsupported)") {
1145+ return parseSelectorParts(":where(ol, ul, menu:unsupported)");
1146+ };
1147+ }
1148+ SECTION("parseSelectorParts can parse multiple simple selectors"){
1149+ checkSelectorParts("input:required, div#id + h1.c1.c2, input[type='password'].class", {{"input", ":required"},{"div", "#id", "+", "h1", ".c1", ".c2"},{"input", "[type=\"password\"]", ".class"}});
1150+ BENCHMARK("input:required, div#id + h1.c1.c2, input[type='password'].class") {
1151+ return parseSelectorParts("input:required, div#id + h1.c1.c2, input[type='password'].class");
1152+ };
1153+ }
1154+}
1155diff --git a/tests/main.cc b/tests/html_parser.cc
1156similarity index 90%
1157rename from tests/main.cc
1158rename to tests/html_parser.cc
1159index e115601..ce55213 100644
1160--- a/tests/main.cc
1161+++ b/tests/html_parser.cc
1162@@ -43,6 +43,10 @@ TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
1163 std::stringstream ss(html);
1164 std::unique_ptr<Node> document = parseStream(ss);
1165
1166+ BENCHMARK("Simple HTML Document") {
1167+ return parseStream(ss);
1168+ };
1169+
1170 // Ensure the only child of root is html
1171 Node* current = document->children[0].get();
1172
1173@@ -79,6 +83,10 @@ TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
1174 std::stringstream ss(html);
1175 std::unique_ptr<Node> document = parseStream(ss);
1176
1177+ BENCHMARK("Inline comment") {
1178+ return parseStream(ss);
1179+ };
1180+
1181 // Ensure the only child of root is html
1182 Node* current = document->children[0].get();
1183
1184@@ -95,6 +103,10 @@ TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
1185 std::stringstream ss(html);
1186 std::unique_ptr<Node> document = parseStream(ss);
1187
1188+ BENCHMARK("Multiline comment") {
1189+ return parseStream(ss);
1190+ };
1191+
1192 // Ensure the only child of root is html
1193 Node* current = document->children[0].get();
1194
1195@@ -111,6 +123,10 @@ TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
1196 std::stringstream ss(html);
1197 std::unique_ptr<Node> document = parseStream(ss);
1198
1199+ BENCHMARK("Multiline comment with HTML") {
1200+ return parseStream(ss);
1201+ };
1202+
1203 // Ensure the only child of root is html
1204 Node* current = document->children[0].get();
1205
1206@@ -130,6 +146,10 @@ TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
1207 std::stringstream ss(html);
1208 std::unique_ptr<Node> document = parseStream(ss);
1209
1210+ BENCHMARK("Style tag parsing (with HTML in the content prop)") {
1211+ return parseStream(ss);
1212+ };
1213+
1214 // Ensure the only child of root is html
1215 Node* current = document->children[0].get();
1216
1217@@ -157,6 +177,10 @@ TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
1218 std::stringstream ss(html);
1219 std::unique_ptr<Node> document = parseStream(ss);
1220
1221+ BENCHMARK("Script tag prematurly closing a pre tag") {
1222+ return parseStream(ss);
1223+ };
1224+
1225 // Ensure the only child of root is html
1226 Node* current = document->children[0].get();
1227
1228@@ -177,6 +201,10 @@ TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
1229 std::stringstream ss(html);
1230 std::unique_ptr<Node> document = parseStream(ss);
1231
1232+ BENCHMARK("Textarea with script tag inside") {
1233+ return parseStream(ss);
1234+ };
1235+
1236 // Ensure the only child of root is html
1237 Node* current = document->children[0].get();
1238