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