home readme diff tree note docs

0commit 629756d7d038cfa24e7f72c196ac2b5022faa12c
1Author: Mason Wright <mason@lakefox.net>
2Date:   Sun Jul 13 16:21:48 2025 -0600
3
4    Made Attribute class with implisit type casting
5
6diff --git a/include/grim.h b/include/grim.h
7index 2a251f4..d10e9d0 100644
8--- a/include/grim.h
9+++ b/include/grim.h
10@@ -8,25 +8,69 @@
11 #include <unordered_map>
12 #include <unordered_set>
13 #include <memory>
14+#include <charconv>
15+#include <optional>
16+#include <stdexcept>
17 
18-struct Bounds {
19-	int top;
20-	int right;
21-	int bottom;
22-	int left;
23+class Attribute {
24+	private:
25+		std::string VALUE;
26+	public:
27+		Attribute(const std::string& value) : VALUE(value) {}
28+
29+		std::optional<std::string> String() const {return VALUE;}
30+		std::optional<int> Int() const {
31+			int iVal = 0;
32+			auto [ptr, ec] = std::from_chars(VALUE.data(), VALUE.data() + VALUE.size(), iVal);
33+
34+			if (ec == std::errc()) {
35+				return iVal;
36+			} else {
37+				return std::nullopt;
38+			}
39+		}
40+
41+		std::optional<double> Double() const {
42+			int dVal = 0.0;
43+			auto [ptr, ec] = std::from_chars(VALUE.data(), VALUE.data() + VALUE.size(), dVal);
44+
45+			if (ec == std::errc()) {
46+				return dVal;
47+			} else {
48+				return std::nullopt;
49+			}
50+		}
51+
52+		std::optional<bool> Bool() const {
53+			if (VALUE == "true") {
54+				return true;
55+			} else 
56+				if (VALUE == "false") {
57+					return false;
58+				} else {
59+					return std::nullopt;
60+				}
61+		}
62+
63+		operator std::string() const { return VALUE; }
64+		operator int() const {
65+			if (auto val = Int()) { return *val; }
66+			throw std::runtime_error("Cannot convert '" + VALUE + "' to int.");
67+		}
68+		operator double() const {
69+			if (auto val = Double()) { return *val; }
70+			throw std::runtime_error("Cannot convert '" + VALUE + "' to double.");
71+		}
72+		operator bool() const {
73+			if (auto val = Bool()) { return *val; }
74+			throw std::runtime_error("Cannot convert '" + VALUE + "' to bool.");
75+		};
76 };
77 
78 class Node {
79 	private:
80 		std::string TagName;
81-		std::unordered_map<std::string, std::string> Attributes;
82-
83-		template<typename T>
84-			void setAttribute(const std::string& name, const T& value);
85-
86-		template<typename T>
87-			T getAttribute(const std::string& name) const;
88-
89+		std::unordered_map<std::string, std::string> attributes;
90 	public:
91 		Node* parent;
92 		std::vector<std::unique_ptr<Node>> children;
93@@ -41,7 +85,6 @@ class Node {
94 
95 			public:
96 				ClassList(Node* node) : self(node) {}
97-				std::string value() const;
98 				void add(std::string value);
99 				void remove(std::string value);
100 				void toggle(std::string value);
101@@ -53,48 +96,40 @@ class Node {
102 
103 		ClassList classList;
104 
105-		Node() : parent(nullptr), classList(this) {
106+		struct StyleList {
107+			private:
108+				Node* self;
109+				std::vector<std::tuple<int, int, int>> indexes;
110+				size_t len;
111+				void createIndex();
112+				bool checkIndex();
113+			public:
114+				StyleList(Node* node) : self(node) {}
115+				std::string get(std::string key);
116+				void set(std::string key, std::string value);
117+				std::pair<std::string, std::string> item(size_t index);
118+				size_t length();
119+		};
120+
121+		StyleList style;
122+
123+		Node() : parent(nullptr), classList(this), style(this) {
124 		}
125+
126 		std::string getTagName() const;
127 		void setTagName(const std::string& name);
128-		const std::unordered_map<std::string, std::string>& getAttributes() const;
129-
130-		// --- Define Getters and Setters
131-		// !TODO: Add all global attributes
132-		// + Src: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes
133-		// The macro should stay here as it's part of the class definition
134-		#define GENERATE_ATTRIBUTE_ACCESSORS(_Type, _FuncNameSuffix, _AttrKeyString) \
135-			_Type get##_FuncNameSuffix() const; \
136-			void set##_FuncNameSuffix(_Type value);
137-
138-		GENERATE_ATTRIBUTE_ACCESSORS(std::string, Id, "id")
139-		GENERATE_ATTRIBUTE_ACCESSORS(std::string, InnerText, "innerText")
140-		GENERATE_ATTRIBUTE_ACCESSORS(bool, ContentEditable, "contenteditable")
141-		GENERATE_ATTRIBUTE_ACCESSORS(std::string, Href, "href")
142-		GENERATE_ATTRIBUTE_ACCESSORS(std::string, Src, "src")
143-		GENERATE_ATTRIBUTE_ACCESSORS(std::string, Title, "title")
144-		GENERATE_ATTRIBUTE_ACCESSORS(std::string, Value, "value")
145-		GENERATE_ATTRIBUTE_ACCESSORS(int, TabIndex, "tabindex")
146-		GENERATE_ATTRIBUTE_ACCESSORS(bool, Disabled, "disabled")
147-		GENERATE_ATTRIBUTE_ACCESSORS(bool, Required, "required")
148-		GENERATE_ATTRIBUTE_ACCESSORS(bool, Checked, "checked")
149 
150 		Node* createElement(std::string name);
151 
152 		void setAttribute(const std::string& name, const std::string& value);
153-		std::string getAttribute(const std::string& name) const;
154+		std::optional<Attribute> getAttribute(const std::string& name) const;
155 
156+		const std::unordered_map<std::string, std::string>& getAttributes() const;
157 		std::vector<std::string> getAttributeKeys();
158 
159 		std::string print(int indent = 0);
160 };
161 
162-/*!
163- * Parse the parts of a CSS selector
164- *
165- * @param selector A CSS selector
166- * @return 
167- */
168 std::vector<std::vector<std::string>> parseSelectorParts(std::string_view);
169 
170 bool testSelector(Node*, std::vector<std::vector<std::string>>);
171diff --git a/src/grim.cc b/src/grim.cc
172index 796c77d..39afe4f 100755
173--- a/src/grim.cc
174+++ b/src/grim.cc
175@@ -7,21 +7,130 @@
176 #include <string_view>
177 #include <queue>
178 #include <set>
179+#include <tuple>
180+#include <optional>
181+#include <stdexcept>
182 
183 // +------------------------------------------------------+
184-// |Table of contents                                     |
185+// |(TOC) Table of contents                               |
186 // +------------------------------------------------------+
187+// |(p98ym) Utils                                         |
188+// |(tigfn) Attribute Handling                            |
189+// |(f98y0) ClassList                                     |
190+// |(lhujn) StyleList                                     |
191+// |(kuib7) Other Node functions                          |
192+// |(mnjki) CSS Style Handling                            |
193 // |(jhsfk) Has selection                                 |
194 // |(d678a) Nth child selection                           |
195 // +------------------------------------------------------+
196 
197-// Constructor
198+
199+// +---------------------------------------------------+
200+// | (p98ym) Utils                                     |
201+// +---------------------------------------------------+
202+
203+void trimSpace(std::string& str) {
204+    if (str.empty()) {
205+        return;
206+    }
207+
208+    // Find first non-space
209+    size_t first_non_space = 0;
210+    while (first_non_space < str.length() && std::isspace(str[first_non_space])) {
211+        first_non_space++;
212+    }
213+
214+    // If all spaces or empty
215+    if (first_non_space == str.length()) {
216+        str.clear(); // Set to empty string
217+        return;
218+    }
219+
220+    // Find last non-space
221+    size_t last_non_space = str.length() - 1;
222+    while (last_non_space >= first_non_space && std::isspace(str[last_non_space])) {
223+        last_non_space--;
224+    }
225+
226+    // Erase trailing spaces
227+    if (last_non_space < str.length() - 1) {
228+        str.erase(last_non_space + 1);
229+    }
230+
231+    // Erase leading spaces
232+    if (first_non_space > 0) {
233+        str.erase(0, first_non_space);
234+    }
235+}
236+
237+// +---------------------------------------------------+
238+// | (tigfn) Attribute Handling                        |
239+// +---------------------------------------------------+
240+// In the grim engine everything except the tagname of 
241+// an node is considered a string stored attribute. To 
242+// get the value of an attribute in its desired type the
243+// attribute getter function uses implicit conversion
244+// operators. The types that are supported are double,
245+// bool, int, std::string. There are special attribute 
246+// handlers like ClassList (.classList) and StyleList 
247+// (.style) for more information on each view
248+// f98y0 and lhujn respectively.
249+
250+// The Attribute class is defined in the header file
251+
252+static const std::unordered_map<std::string, std::string> DEFAULT_ATTRIBUTE_VALUES = {
253+	{"tabindex", "-1"},
254+	{"hidden", "false"}
255+};
256+
257+// Tag Name is not a attribute so it needs to be handled differently
258+std::string Node::getTagName() const { return TagName; }
259+void Node::setTagName(const std::string& name) { TagName = name; }
260+
261+void Node::setAttribute(const std::string& name, const std::string& value) {
262+	attributes[name] = value;
263+}
264+
265+std::optional<Attribute> Node::getAttribute(const std::string& name) const {
266+	// First check in the user set attributes
267+	auto it = attributes.find(name);
268+	if (it != attributes.end()) {
269+		// return the value wrapped in the Attribute class
270+		return Attribute(it->second);
271+	} else {
272+		// If its not set by the user then find it in the
273+		// default values map
274+		it = DEFAULT_ATTRIBUTE_VALUES.find(name);
275+		if (it != DEFAULT_ATTRIBUTE_VALUES.end()) {
276+			return Attribute(it->second);
277+		} else {
278+			return std::nullopt;
279+		}
280+	}
281+}
282+
283+std::vector<std::string> Node::getAttributeKeys() {
284+	std::vector<std::string> keys;
285+
286+	for(auto p : attributes) {
287+		keys.push_back(p.first);
288+	}
289+	return keys;
290+}
291+
292+const std::unordered_map<std::string, std::string>& Node::getAttributes() const {
293+    return attributes;
294+}
295+
296+// +---------------------------------------------------+
297+// | (f98y0) ClassList                                 |
298+// +---------------------------------------------------+
299 
300 void Node::ClassList::createIndex() {
301 	int index = 0;
302 	// Add a space to the back to help trigger the adding of the 
303 	// last item
304-	std::string classes = self->Attributes["class"]+" ";
305+	std::string classes = self->attributes["class"]+" ";
306 	// Update the stored length
307 	len = classes.length();
308 	indexes.clear();
309@@ -43,7 +152,7 @@ void Node::ClassList::createIndex() {
310 
311 
312 bool Node::ClassList::checkIndex() {
313-	std::string classes = self->Attributes["class"];
314+	std::string classes = self->attributes["class"];
315 	if (classes.length() != len) {
316 		return false;
317 	} else {
318@@ -76,7 +185,7 @@ std::string Node::ClassList::item(size_t key) {
319 	}
320 	if (key < indexes.size()) {
321 		std::pair<int, int> pair = indexes[key];
322-		return self->Attributes["class"].substr(pair.first, pair.second);
323+		return self->attributes["class"].substr(pair.first, pair.second);
324 	} else {
325 		return "";
326 	}
327@@ -87,10 +196,10 @@ void Node::ClassList::add(std::string value) {
328 		createIndex();
329 	}
330 
331-	if (self->Attributes["class"].length() == 0) {
332-		self->Attributes["class"] += value;
333+	if (self->attributes["class"].length() == 0) {
334+		self->attributes["class"] += value;
335 	} else {
336-		self->Attributes["class"] += " "+value;
337+		self->attributes["class"] += " "+value;
338 	}
339 	// len because the index of the added space will be the length of 
340 	// the prevous string
341@@ -106,7 +215,7 @@ void Node::ClassList::remove(std::string value) {
342 
343 	int newLen = value.length();
344 	size_t cLen = indexes.size();
345-	std::string classes = self->Attributes["class"];	
346+	std::string classes = self->attributes["class"];	
347 
348 	for (size_t i = 0; i < cLen; i++) {
349 		std::pair<int, int> pair = indexes[i];
350@@ -115,7 +224,7 @@ void Node::ClassList::remove(std::string value) {
351 				// Splice out the value to be removed 
352 				// !ISSUE: This will keep the surounding spaces and cause the size to grow when 
353 				// + classes are added and removed
354-				self->Attributes["class"] = classes.substr(0, pair.first)+classes.substr(pair.first+pair.second);
355+				self->attributes["class"] = classes.substr(0, pair.first)+classes.substr(pair.first+pair.second);
356 			}
357 		}
358 	}	
359@@ -128,7 +237,7 @@ bool Node::ClassList::contains(std::string value) {
360 
361 	int newLen = value.length();
362 	size_t cLen = indexes.size();
363-	std::string classes = self->Attributes["class"];	
364+	std::string classes = self->attributes["class"];	
365 	bool found = false;
366 
367 	for (size_t i = 0; i < cLen; i++) {
368@@ -156,8 +265,6 @@ std::string Node::ClassList::value() const {
369 	return self->Attributes["class"];
370 }
371 
372-std::string Node::getTagName() const { return TagName; }
373-void Node::setTagName(const std::string& name) { TagName = name; }
374 
375 const std::unordered_map<std::string, std::string>& Node::getAttributes() const {
376     return Attributes;
377@@ -172,45 +279,26 @@ const std::unordered_map<std::string, std::string>& Node::getAttributes() const
378         setAttribute(_AttrKeyString, value); \
379     }
380 
381-IMPLEMENT_ATTRIBUTE_ACCESSORS(std::string, Id, "id")
382-IMPLEMENT_ATTRIBUTE_ACCESSORS(std::string, InnerText, "innerText")
383-IMPLEMENT_ATTRIBUTE_ACCESSORS(bool, ContentEditable, "contenteditable")
384-IMPLEMENT_ATTRIBUTE_ACCESSORS(std::string, Href, "href")
385-IMPLEMENT_ATTRIBUTE_ACCESSORS(std::string, Src, "src")
386-IMPLEMENT_ATTRIBUTE_ACCESSORS(std::string, Title, "title")
387-IMPLEMENT_ATTRIBUTE_ACCESSORS(std::string, Value, "value")
388-IMPLEMENT_ATTRIBUTE_ACCESSORS(int, TabIndex, "tabindex")
389-IMPLEMENT_ATTRIBUTE_ACCESSORS(bool, Disabled, "disabled")
390-IMPLEMENT_ATTRIBUTE_ACCESSORS(bool, Required, "required")
391-IMPLEMENT_ATTRIBUTE_ACCESSORS(bool, Checked, "checked")
392+	std::pair<std::string, std::string> property;
393+	std::string styles = self->attributes["style"];
394 
395-Node* Node::createElement(std::string name) {
396-    std::unique_ptr<Node> newNode = std::make_unique<Node>();
397-    newNode->setTagName(name);
398-    newNode->parent = this;
399-    children.push_back(std::move(newNode));
400-    return children.back().get();
401-}
402+	int f = std::get<0>(indexes[index]);
403+	int s = std::get<1>(indexes[index]);
404+	property.first = styles.substr(f, f-s);
405+	trimSpace(property.first);
406+
407+	int t = std::get<2>(indexes[index]);
408+	property.second = styles.substr(s, t-s);
409+	trimSpace(property.second);
410+	return property;
411 
412-template<typename T>
413-void Node::setAttribute(const std::string& name, const T& value) {
414-    if constexpr (std::is_same_v<T, bool>) {
415-        if (value) {
416-            Attributes[name] = "";
417-        } else {
418-            Attributes.erase(name);
419-        }
420-    } else if constexpr (std::is_arithmetic_v<T>) {
421-        Attributes[name] = std::to_string(value);
422-    } else {
423-        static_assert(std::is_convertible_v<T, std::string>,
424-            "setAttribute: Type cannot be converted to std::string automatically.");
425-        Attributes[name] = static_cast<std::string>(value);
426-    }
427 }
428 
429-void Node::setAttribute(const std::string& name, const std::string& value) {
430-    Attributes[name] = value;
431+size_t Node::StyleList::length() {
432+	if (!checkIndex()) {
433+		createIndex();
434+	}
435+	return indexes.size();
436 }
437 
438 template<typename T>
439@@ -257,13 +345,16 @@ std::string Node::getAttribute(const std::string& name) const {
440     return "";
441 }
442 
443-std::vector<std::string> Node::getAttributeKeys() {
444-	std::vector<std::string> keys;
445+// +---------------------------------------------------+
446+// | (kuib7) Other Node functions                      |
447+// +---------------------------------------------------+
448 
449-	for(auto p : Attributes) {
450-		keys.push_back(p.first);
451-	}
452-	return keys;
453+Node* Node::createElement(std::string name) {
454+    std::unique_ptr<Node> newNode = std::make_unique<Node>();
455+    newNode->setTagName(name);
456+    newNode->parent = this;
457+    children.push_back(std::move(newNode));
458+    return children.back().get();
459 }
460 
461 std::string Node::print(int indent) {
462@@ -284,8 +375,10 @@ std::string Node::print(int indent) {
463     }
464     out += ">";
465 
466-    if (!getAttribute("innerText").empty()) {
467-        out += "\n" + getAttribute("innerText");
468+    if (auto innerText = getAttribute("innerText")) {
469+	    if (innerText != std::nullopt) {
470+		    out += "\n" + *innerText->String();
471+	    }
472     }
473 
474     out += "\n";
475@@ -302,46 +395,22 @@ std::string Node::print(int indent) {
476     return out;
477 }
478 
479+// +---------------------------------------------------+
480+// | (mnjki) CSS Style Handling                        |
481+// +---------------------------------------------------+
482+// | (78658) Selector Parsing                          |
483+// | (luoiy) Node Selector Parsing                     |
484+// +---------------------------------------------------+
485 
486-void trimSpace(std::string& str) {
487-    if (str.empty()) {
488-        return;
489-    }
490-
491-    // Find first non-space
492-    size_t first_non_space = 0;
493-    while (first_non_space < str.length() && std::isspace(str[first_non_space])) {
494-        first_non_space++;
495-    }
496-
497-    // If all spaces or empty
498-    if (first_non_space == str.length()) {
499-        str.clear(); // Set to empty string
500-        return;
501-    }
502-
503-    // Find last non-space
504-    size_t last_non_space = str.length() - 1;
505-    while (last_non_space >= first_non_space && std::isspace(str[last_non_space])) {
506-        last_non_space--;
507-    }
508-
509-    // Erase trailing spaces
510-    if (last_non_space < str.length() - 1) {
511-        str.erase(last_non_space + 1);
512-    }
513-
514-    // Erase leading spaces
515-    if (first_non_space > 0) {
516-        str.erase(0, first_non_space);
517-    }
518-}
519-
520+// +---------------------------------------------------+
521+// | (78658) Selector Parsing                          |
522+// +---------------------------------------------------+
523 // parseSelectorParts deconstructs a selector into its indiviual parts
524 // so they can be store and used in things like finding styles for basemap
525 // and comparing a node to a selector in testSelector. This is a higher level function
526-// that should be ran once perselector and the results saved somewhere
527-// selector: any CSS selector
528+// that should be ran once perselector and the results saved somewhere due to
529+// the high cost to run the function
530+
531 std::vector<std::vector<std::string>> parseSelectorParts(std::string_view selector) {
532 	// need to account for selectors with parenthesis like: h1:(+ input:required)
533 	// need to convert all single quotes to double quotes
534@@ -460,7 +529,10 @@ std::vector<std::vector<std::string>> parseSelectorParts(std::string_view select
535 }
536 
537 
538-		
539+// +---------------------------------------------------+
540+// | (luoiy) Node Selector Parsing                     |
541+// +---------------------------------------------------+
542+// 
543 std::vector<std::string> StyleHandler::parseNodeParts(Node* node) {
544 	// This isn't a all inclusive list of parts that can
545 	// apply to the node but it narrows our search for matching 
546@@ -468,7 +540,11 @@ std::vector<std::string> StyleHandler::parseNodeParts(Node* node) {
547 	std::vector<std::string> parts;
548 
549 	parts.push_back(node->getTagName());
550-	parts.push_back("#"+node->getId());
551+	auto id = node->getAttribute("id");
552+
553+	if (id != std::nullopt) {
554+		parts.push_back("#"+*id->String());
555+	}
556 
557 	// classes and attributes
558 	auto keys = node->getAttributeKeys();
559@@ -476,9 +552,10 @@ std::vector<std::string> StyleHandler::parseNodeParts(Node* node) {
560 	for (auto k : keys) {
561 		if (k != "tagName" || k != "id" || k != "innerText" || k != "class") {
562 			std::string v;
563-			std::string av = node->getAttribute(k);
564-			if (av != "") {
565-				v = "=\""+av+"\"";
566+			auto av = node->getAttribute(k);
567+
568+			if (av != std::nullopt) {
569+				v = "=\""+*av->String()+"\"";
570 
571 			}
572 			parts.push_back('['+k+v+']');
573@@ -711,7 +788,7 @@ bool testSelector(Node* node, std::vector<std::vector<std::string>> selector) {
574 	}
575 	
576 	if (tagName == "input") {
577-		std::string type = node->getAttribute("type");
578+		std::string type = attributes["type"];
579 		bool readonly = attributes.find("readonly") != attributes.end();
580 		bool disabled = attributes.find("disabled") != attributes.end();
581 		if (type == "text" || type == "search" || type == "tel" || type == "url" || type == "email" 
582@@ -734,7 +811,7 @@ bool testSelector(Node* node, std::vector<std::vector<std::string>> selector) {
583 			attrs.push_back("read-write");
584 		}
585 	} else {
586-		std::string contentEditable = node->getAttribute("contenteditable");
587+		std::string contentEditable = attributes["contenteditable"];
588 		if (contentEditable == "true") {
589 			attrs.push_back("read-write");
590 		} else {
591@@ -752,7 +829,7 @@ bool testSelector(Node* node, std::vector<std::vector<std::string>> selector) {
592 		if (i == 0 && p[0] != '#' && p[0] != '.' &&  p[0] != '[' && p[0] != ':') {
593 			matched = tagName == p;
594 		} else if (p[0] == '#') {
595-			matched = "#"+node->getId() == p;
596+			matched = "#"+attributes["id"] == p;
597 		} else if (p[0] == '.') {
598 			bool found = false;
599 			p.erase(0,1);
600diff --git a/tests/css_parser.cc b/tests/css_parser.cc
601index 02eac9f..9c4f37a 100644
602--- a/tests/css_parser.cc
603+++ b/tests/css_parser.cc
604@@ -231,6 +231,10 @@ html#nested {
605 				{"font-size", "10px"}
606 				  }, 9, handler->item(9));
607 	}
608+	printSelector(handler->item(10).selector);
609+	printSelector(handler->item(11).selector);
610+	printSelector(handler->item(12).selector);
611+	printSelector(handler->item(13).selector);
612 }
613 
614 TEST_CASE("parseCSSInline can parse inline styles") {
615@@ -265,6 +269,4 @@ TEST_CASE("parseCSSInline can parse inline styles") {
616 				});
617 	}
618 }
619-// When nesting tests are done update webpage
620-// inline parsing tests
621-// make .style act like classList
622+// !TODO: Figure out how to handle @ rules
623diff --git a/tests/html_node.cc b/tests/html_node.cc
624index 58d3520..ae1f346 100644
625--- a/tests/html_node.cc
626+++ b/tests/html_node.cc
627@@ -14,8 +14,6 @@ TEST_CASE("ClassList", "[html]") {
628 	auto div = document->children[0].get();
629 
630 	SECTION("ClassList can get the length and access items correctly") {
631-		INFO(div->getAttribute("class"));
632-
633 		REQUIRE(div->classList.length() == 3);
634 		REQUIRE(div->classList.item(0) == "class1");
635 		REQUIRE(div->classList.item(1) == "class2");
636@@ -25,7 +23,6 @@ TEST_CASE("ClassList", "[html]") {
637 	SECTION("ClassList can add a class and get a class") {
638 		div->classList.add("class4");
639 
640-		INFO(div->getAttribute("class"));
641 		REQUIRE(div->classList.length() == 4);
642 		REQUIRE(div->classList.item(0) == "class1");
643 		REQUIRE(div->classList.item(3) == "class4");
644@@ -34,7 +31,6 @@ TEST_CASE("ClassList", "[html]") {
645 	SECTION("ClassList can remove a class") {
646 		div->classList.remove("class2");
647 
648-		INFO(div->getAttribute("class"));
649 		REQUIRE(div->classList.length() == 2);
650 		REQUIRE(div->classList.item(0) == "class1");
651 		REQUIRE(div->classList.item(1) == "class3");
652@@ -56,7 +52,6 @@ TEST_CASE("ClassList", "[html]") {
653 
654 		div->classList.toggle("class4");
655 
656-		INFO(div->getAttribute("class"));
657 		REQUIRE(div->classList.length() == 3);
658 	}
659 
660diff --git a/tests/html_parser.cc b/tests/html_parser.cc
661index 4b6bbe7..e2f727e 100644
662--- a/tests/html_parser.cc
663+++ b/tests/html_parser.cc
664@@ -12,14 +12,20 @@ void checkNode(Node* node, std::string tagName, unsigned long children, std::str
665 
666 	REQUIRE(node->getTagName() == tagName);
667 	REQUIRE(node->children.size() == children);
668-	REQUIRE(node->getId() == id);
669+	if (id != "") {
670+		REQUIRE(*node->getAttribute("id")->String() == id);
671+	}
672 
673 	for (size_t i = 0; i < node->classList.length(); i++) {
674 		REQUIRE(node->classList.item(i) == classes[i]);
675 	}
676 
677 	for (auto pair : attributes) {
678-		REQUIRE(node->getAttribute(pair.first) == pair.second);
679+		auto attr = node->getAttribute(pair.first);
680+
681+		if (attr != std::nullopt) {
682+			REQUIRE(*attr->String() == pair.second);
683+		}
684 	}
685 }
686