home readme diff tree note docs

0commit e13ac692b67c41b78a06058d707f66b524fd3ace
1Author: Mason Wright <mason@lakefox.net>
2Date:   Fri Jul 11 09:55:02 2025 -0600
3
4    Added a CSS Parser and fixed the benchmarks for all parsers
5
6diff --git a/Makefile b/Makefile
7index 8373da0..99678fc 100644
8--- a/Makefile
9+++ b/Makefile
10@@ -22,9 +22,6 @@ build/parser.o: src/parser.cc include/parser.h | build
11 
12 # --- Tests ---
13 
14-run_tests: build/html_parser_test
15-	build/html_parser_test
16-
17 build/html_node_test: tests/html_node.cc build/grim.o build/catch.o build/parser.o | build
18 	${CC} ${CCFLAGS} -Iinclude/ tests/html_node.cc build/grim.o build/catch.o build/parser.o -o build/html_node_test 
19 
20@@ -34,9 +31,16 @@ build/css_selector_test: tests/css_selector.cc build/grim.o build/catch.o build/
21 build/html_parser_test: tests/html_parser.cc build/grim.o build/parser.o build/catch.o | build
22 	${CC} ${CCFLAGS} -Iinclude/ tests/html_parser.cc build/grim.o build/parser.o build/catch.o -o build/html_parser_test
23 
24+build/css_parser_test: tests/css_parser.cc build/grim.o build/parser.o build/catch.o | build
25+	${CC} ${CCFLAGS} -Iinclude/ tests/css_parser.cc build/grim.o build/parser.o build/catch.o -o build/css_parser_test
26+
27 build/catch.o: ./include/catch_amalgamated.cpp ./include/catch_amalgamated.hpp | build
28 	${CC} ${CCFLAGS} -Iinclude/ -c include/catch_amalgamated.cpp -o build/catch.o
29 
30+# --- Prebuild ---
31+
32+# use to prebuild the master.css
33+
34 
35 # --- Tools ---
36 
37diff --git a/docs/content/features/supported-css-selectors.md b/docs/content/features/supported-css-selectors.md
38index 709935e..725f27c 100644
39--- a/docs/content/features/supported-css-selectors.md
40+++ b/docs/content/features/supported-css-selectors.md
41@@ -7,9 +7,9 @@
42 | Attribute | X | testSelector | testSelector can match a element by it's attributes only | tests/css_selector.cc |
43 | ID | X | testSelector | testSelector can match a element by it's id only | tests/css_selector.cc |
44 | Class | X | testSelector | testSelector can match a element by it's class and not matach on a non class | tests/css_selector.cc |
45-| Universal | testSelector can match a universal selector | testSelector | |  |
46-| Nesting |  | testSelector | |  |
47-| & Nesting |  | testSelector | |  |
48+| Universal | X | testSelector | testSelector can match a universal selector |  |
49+| Nesting | X | testSelector | |  |
50+| & Nesting | X | testSelector | |  |
51 
52 ## Combinators
53 | Name | Support | Test Case | Section | Test File |
54diff --git a/docs/content/logo.webp b/docs/content/logo.webp
55new file mode 100755
56index 0000000..f8806a8
57Binary files /dev/null and b/docs/content/logo.webp differ
58diff --git a/docs/public/logo.webp b/docs/public/logo.webp
59new file mode 100755
60index 0000000..f8806a8
61Binary files /dev/null and b/docs/public/logo.webp differ
62diff --git a/include/grim.h b/include/grim.h
63index 9c190fe..5cfe725 100644
64--- a/include/grim.h
65+++ b/include/grim.h
66@@ -9,12 +9,6 @@
67 #include <unordered_set>
68 #include <memory>
69 
70-struct Styles {
71-	std::vector<std::unordered_map<std::string, std::string>> stylesheets;
72-	std::unordered_map<std::string, std::string> inlineStyles;
73-	std::unordered_map<std::string, std::unordered_map<std::string, std::string>> psuedoStyles;
74-};
75-
76 struct Bounds {
77 	int top;
78 	int right;
79@@ -69,23 +63,23 @@ class Node {
80 		// !TODO: Add all global attributes
81 		// + Src: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes
82 		// The macro should stay here as it's part of the class definition
83-#define GENERATE_ATTRIBUTE_ACCESSORS(_Type, _FuncNameSuffix, _AttrKeyString) \
84-		_Type get##_FuncNameSuffix() const; \
85-		void set##_FuncNameSuffix(_Type value);
86+		#define GENERATE_ATTRIBUTE_ACCESSORS(_Type, _FuncNameSuffix, _AttrKeyString) \
87+			_Type get##_FuncNameSuffix() const; \
88+			void set##_FuncNameSuffix(_Type value);
89 
90 		GENERATE_ATTRIBUTE_ACCESSORS(std::string, Id, "id")
91-			GENERATE_ATTRIBUTE_ACCESSORS(std::string, InnerText, "innerText")
92-			GENERATE_ATTRIBUTE_ACCESSORS(bool, ContentEditable, "contenteditable")
93-			GENERATE_ATTRIBUTE_ACCESSORS(std::string, Href, "href")
94-			GENERATE_ATTRIBUTE_ACCESSORS(std::string, Src, "src")
95-			GENERATE_ATTRIBUTE_ACCESSORS(std::string, Title, "title")
96-			GENERATE_ATTRIBUTE_ACCESSORS(std::string, Value, "value")
97-			GENERATE_ATTRIBUTE_ACCESSORS(int, TabIndex, "tabindex")
98-			GENERATE_ATTRIBUTE_ACCESSORS(bool, Disabled, "disabled")
99-			GENERATE_ATTRIBUTE_ACCESSORS(bool, Required, "required")
100-			GENERATE_ATTRIBUTE_ACCESSORS(bool, Checked, "checked")
101-
102-			Node* createElement(std::string name);
103+		GENERATE_ATTRIBUTE_ACCESSORS(std::string, InnerText, "innerText")
104+		GENERATE_ATTRIBUTE_ACCESSORS(bool, ContentEditable, "contenteditable")
105+		GENERATE_ATTRIBUTE_ACCESSORS(std::string, Href, "href")
106+		GENERATE_ATTRIBUTE_ACCESSORS(std::string, Src, "src")
107+		GENERATE_ATTRIBUTE_ACCESSORS(std::string, Title, "title")
108+		GENERATE_ATTRIBUTE_ACCESSORS(std::string, Value, "value")
109+		GENERATE_ATTRIBUTE_ACCESSORS(int, TabIndex, "tabindex")
110+		GENERATE_ATTRIBUTE_ACCESSORS(bool, Disabled, "disabled")
111+		GENERATE_ATTRIBUTE_ACCESSORS(bool, Required, "required")
112+		GENERATE_ATTRIBUTE_ACCESSORS(bool, Checked, "checked")
113+
114+		Node* createElement(std::string name);
115 
116 		void setAttribute(const std::string& name, const std::string& value);
117 		std::string getAttribute(const std::string& name) const;
118@@ -105,4 +99,35 @@ std::vector<std::vector<std::string>> parseSelectorParts(std::string_view);
119 
120 bool testSelector(Node*, std::vector<std::vector<std::string>>);
121 
122+struct Style {
123+	std::unordered_map<std::string, std::string> properties;
124+	std::vector<std::vector<std::string>> selector;
125+	size_t index;
126+};
127+
128+class StyleHandler {
129+	private:
130+		// basemap: Maps baseparts to a index of the styles vector pointing to a Style object
131+		// because processing a CSS selector isn't trivial we want to make a short list of 
132+		// the styles that can possibly be a match so we aren't spending a lot of compute 
133+		// on a selector that applies to the wrong element. To do this we take the right most part
134+		// of a selector (that is the part that targets the current element) and we do a few checks
135+		// like is the tagname the same, does it have that class, and do the id's match. If so that 
136+		// is a candidate and we run the full selector on it. That includes checking parents,children, 
137+		// or what ever else the selector needs to match.
138+		std::unordered_map<std::string, std::vector<size_t>> basemap;
139+		std::vector<Style> styles;
140+		
141+		std::vector<std::string> parseNodeParts(Node* node);
142+
143+	public:
144+		void add(std::string selector, std::unordered_map<std::string, std::string> properties);
145+		
146+		std::unordered_map<std::string, std::string> getStyles(Node* node);
147+
148+		// make a function that will return a list of attributes the will cause style changes if set
149+};
150+
151+
152+
153 #endif // NODE_H
154diff --git a/include/parser.h b/include/parser.h
155index 155b798..2fbf8b9 100644
156--- a/include/parser.h
157+++ b/include/parser.h
158@@ -9,9 +9,11 @@
159 #include <string_view>
160 
161 class Node;
162+class StyleHandler;
163 
164 // Declare the parsing functions
165 std::unordered_map<std::string, std::string> parseAttributes(std::string_view& token);
166-std::unique_ptr<Node> parseStream(std::istream& inputStream);
167+std::unique_ptr<Node> parseHTML(std::istream& inputStream);
168+std::unique_ptr<StyleHandler> parseCSS(std::istream& inputStream);
169 
170 #endif
171diff --git a/main.cc b/main.cc
172index 4cca14d..d505de3 100755
173--- a/main.cc
174+++ b/main.cc
175@@ -12,13 +12,13 @@ int main() {
176         return 1;
177     }
178 
179-    std::unique_ptr<Node> document = parseStream(inputFile);
180+    std::unique_ptr<Node> document = parseHTML(inputFile);
181     inputFile.close(); // Close the file after parsing
182     //
183     // // Option 2: Parse from a string (for testing)
184     // // std::string html_content = "<div>Hello <b>world</b>!</div><p>Another paragraph.</p>";
185     // // std::stringstream ss(html_content);
186-    // // std::unique_ptr<Node> document = parseStream(ss);
187+    // // std::unique_ptr<Node> document = parseHTML(ss);
188     //
189     // Print the parsed document tree
190     if (document) {
191diff --git a/src/grim.cc b/src/grim.cc
192index 09efbb0..82bd879 100755
193--- a/src/grim.cc
194+++ b/src/grim.cc
195@@ -337,19 +337,11 @@ void trimSpace(std::string& str) {
196     }
197 }
198 
199-struct Style {
200-	std::unordered_map<std::string, std::string> properties;
201-	std::vector<std::vector<std::string>> selector;
202-	// Index of when it was added (for cascading)
203-	size_t index;
204-};
205-
206 // parseSelectorParts deconstructs a selector into its indiviual parts
207 // so they can be store and used in things like finding styles for basemap
208 // and comparing a node to a selector in testSelector. This is a higher level function
209 // that should be ran once perselector and the results saved somewhere
210 // selector: any CSS selector
211-// //!TODO: have parseSelectorParts return string_views
212 std::vector<std::vector<std::string>> parseSelectorParts(std::string_view selector) {
213 	// need to account for selectors with parenthesis like: h1:(+ input:required)
214 	// need to convert all single quotes to double quotes
215@@ -460,98 +452,120 @@ std::vector<std::vector<std::string>> parseSelectorParts(std::string_view select
216 	return parts;
217 }
218 
219-class StyleHandler {
220-	private:
221-		// basemap: Maps baseparts to a index of the styles vector pointing to a Style object
222-		// because processing a CSS selector isn't trivial we want to make a short list of 
223-		// the styles that can possibly be a match so we aren't spending a lot of compute 
224-		// on a selector that applies to the wrong element. Todo this we take the right most part
225-		// of a selector (that is the part that targets the current element) and we do a few checks
226-		// like is the tagname the same, does it have that class, and do the id's match. If so that 
227-		// is a candidate and we run the full selector on it. That includes checking parents,children, 
228-		// or what ever else the selector needs to match.
229-		std::unordered_map<std::string, std::vector<size_t>> basemap;
230-		std::vector<Style> styles;
231 
232 		
233-		std::vector<std::string> parseNodeParts(Node* node) {
234-			std::vector<std::string> parts;
235+std::vector<std::string> StyleHandler::parseNodeParts(Node* node) {
236+	// This isn't a all inclusive list of parts that can
237+	// apply to the node but it narrows our search for matching 
238+	// classes down quite a bit
239+	std::vector<std::string> parts;
240 
241-			parts.push_back(node->getTagName());
242-			parts.push_back("#"+node->getId());
243+	parts.push_back(node->getTagName());
244+	parts.push_back("#"+node->getId());
245 
246-			// classes and attributes
247-			auto keys = node->getAttributeKeys();
248+	// classes and attributes
249+	auto keys = node->getAttributeKeys();
250 
251-			for (auto k : keys) {
252-				if (k != "tagName" || k != "id" || k != "innerText" || k != "class") {
253-					std::string v;
254-					std::string av = node->getAttribute(k);
255-					if (av != "") {
256-							v = "=\""+av+"\"";
257+	for (auto k : keys) {
258+		if (k != "tagName" || k != "id" || k != "innerText" || k != "class") {
259+			std::string v;
260+			std::string av = node->getAttribute(k);
261+			if (av != "") {
262+				v = "=\""+av+"\"";
263 
264-					}
265-					parts.push_back('['+k+v+']');
266-				}
267 			}
268+			parts.push_back('['+k+v+']');
269+		}
270+	}
271 
272-			auto classLen = node->classList.length();
273+	auto classLen = node->classList.length();
274 
275-			for (size_t i = 0; i < classLen; i++) {
276-				parts.push_back("."+node->classList.item(i));
277-			}
278+	for (size_t i = 0; i < classLen; i++) {
279+		parts.push_back("."+node->classList.item(i));
280+	}
281 
282-			return parts;
283-		}
284+	return parts;
285+}
286 
287-	public:
288-		void add(std::string selector, std::unordered_map<std::string, std::string> properties) {
289-			// Type is 2d vector of selector parts
290-			auto parts = parseSelectorParts(selector);
291-			size_t index = styles.size();
292-			Style style = {properties,parts,index};
293-
294-			styles.push_back(std::move(style));
295-// !ISSUE: Re do the basemap mapping in the add function
296-// + will need to go through each 1d then once in start from right and go left until >+~" " or eol 
297-/*			for (auto p : parts) {
298-				if (basemap.find(p) != basemap.end()) {
299-					basemap[p].push_back(index);
300-				} else {
301-					std::vector<size_t> bm = {index};
302-					basemap[p] = bm;
303-				}
304-			}
305-*/
306+void StyleHandler::add(std::string selector, std::unordered_map<std::string, std::string> properties) {
307+	// Type is 2d vector of selector parts
308+
309+	auto parts = parseSelectorParts(selector);
310+	size_t index = styles.size();
311+
312+	styles.push_back({properties,parts,index});
313+
314+	// Get the last selector group from the parsed selector parts
315+	// because that is the selector that applies to an element
316+	// and map them into basemap
317+	std::vector<std::string> targetParts = parts[parts.size()-1];
318+	for (auto p : targetParts) {
319+		if (basemap.find(p) != basemap.end()) {
320+			basemap[p].push_back(index);
321+		} else {
322+			std::vector<size_t> bm = {index};
323+			basemap[p] = bm;
324 		}
325+	}
326+}
327 		
328-		std::unordered_map<std::string, std::string> getStyles(Node* node) {
329-			std::vector<std::string> parts = parseNodeParts(node);
330-
331-			std::vector<size_t> canidates;
332-
333-			for (auto p : parts) {
334-				if (auto sty = basemap.find(p); sty != basemap.end()) {
335-					for (auto s : sty->second) {
336-						bool found = false;
337-						// This makes sure we aren't adding the same styles to the candidates list
338-						for (auto c : canidates) {
339-							if (c == s) {
340-								found = true;
341-								break;
342-							}
343-						}
344-						if (!found) {
345-							canidates.push_back(s);
346-						}
347+std::unordered_map<std::string, std::string> StyleHandler::getStyles(Node* node) {
348+	std::vector<std::string> parts = parseNodeParts(node);
349+
350+	std::vector<size_t> canidates;
351+
352+	for (auto p : parts) {
353+		if (auto sty = basemap.find(p); sty != basemap.end()) {
354+			for (auto s : sty->second) {
355+				bool found = false;
356+				// This makes sure we aren't adding the same styles to the candidates list
357+				for (auto c : canidates) {
358+					if (c == s) {
359+						found = true;
360+						break;
361 					}
362 				}
363+				if (!found) {
364+					canidates.push_back(s);
365+				}
366 			}
367+		}
368+	}
369+
370+	// Now we have the list of candidates next we will refined our selection
371+	// we need to pull the selectors from each candidate then run testSelector on it
372 
373-			// Now we have the list of candidates next we will refined our selection
374-			// we need to pull the selectors from each candidate then run testSelector on it
375+	Style* matchingStyles = new Style[canidates.size()];
376+	int index = 0;
377+
378+	for (size_t c : canidates) {
379+		// Get the possible style data
380+		Style style = styles[c];
381+		// Run testSelector with the preparsed parts
382+		if (testSelector(node, style.selector)) {
383+			matchingStyles[index] = style;
384+			index++;
385+		}
386+	}
387+
388+	// matchingStyles contains the filtered canidates. The next step is to compute 
389+	// the cascade. the higher the index the later it's applied
390+
391+	// Bubble sort the styles (not using quick sort because the
392+	// matching style list should be pretty small)		
393+	for (int i = 0; i < index-1; i++) {
394+		for (int j = 0; j < index-i-1; j++) {
395+			if (matchingStyles[j].index > matchingStyles[j+1].index) {
396+				std::swap(matchingStyles[j], matchingStyles[j+1]);
397+			}
398 		}
399-};
400+
401+	}
402+
403+	// Now we have all styles except the inline styles and the inherited styles
404+
405+	delete[] matchingStyles;
406+}
407 
408 
409 // Used for root node without a parent
410diff --git a/src/parser.cc b/src/parser.cc
411index bf2bd02..555e3e9 100644
412--- a/src/parser.cc
413+++ b/src/parser.cc
414@@ -102,7 +102,7 @@ std::unordered_map<std::string, std::string> parseAttributes(std::string_view to
415 	return attrs;
416 }
417 
418-std::unique_ptr<Node> parseStream(std::istream& inputStream) {
419+std::unique_ptr<Node> parseHTML(std::istream& inputStream) {
420 	std::unique_ptr<Node> root = std::make_unique<Node>();
421 	root->setTagName("root");
422 
423@@ -334,3 +334,112 @@ std::unique_ptr<Node> parseStream(std::istream& inputStream) {
424 
425 	return root;
426 }
427+
428+struct SelectorAndProps {
429+	std::string selector;
430+	std::unordered_map<std::string, std::string> props;
431+};
432+
433+std::unique_ptr<StyleHandler> parseCSS(std::istream& inputStream) {
434+	// nowhitespace: value(can have whitespace); = property
435+	// a selector name is anything before a { up to a ; or a }
436+	std::unique_ptr<StyleHandler> handler = std::make_unique<StyleHandler>();
437+	std::vector<SelectorAndProps> stack;
438+	SelectorAndProps current;
439+	std::string buffer;
440+
441+	bool incomment = false;
442+
443+	char ch;
444+	while (inputStream.get(ch)) {
445+		if (ch == '*' && inputStream.peek() == '/') {
446+			incomment = false;
447+			// Skip ahead to the next letter so we don't caputure the trailing /
448+			inputStream.get(ch);
449+			continue;
450+		}
451+		if (ch == '/' && inputStream.peek() == '*') {
452+			incomment = true;
453+		}
454+		
455+		if (incomment) {
456+			continue;
457+		}
458+
459+
460+		if (ch == '{') {
461+			// start of a group of properties
462+			// need to parse the selector and add it to the stack
463+			current.selector = buffer;
464+
465+			stack.push_back(current);
466+			current = SelectorAndProps();
467+			
468+			buffer.clear();
469+		} else if (ch == '}') {
470+			// compile the selector and add to the handler
471+			// This handles the nested selectors
472+			// if the first non whitespace charactor is a &
473+			// then join them without a space else join them with
474+			std::string selector;
475+			for (size_t i = 0; i < stack.size(); i++) {
476+				bool firstChar = false;
477+				for (size_t j = 0; j < stack[i].selector.length(); j++) {
478+					char c = stack[i].selector[j];
479+					if (!std::isspace(c) && !firstChar) {
480+						if (c != '&') {
481+							selector.push_back(' ');
482+							selector.push_back(c);
483+						}
484+						
485+						firstChar = true;
486+					} else if (firstChar) {
487+						if (std::isspace(c)) { 
488+							if (j < stack[i].selector.length()-1 && !std::isspace(stack[i].selector[j+1])) {
489+								selector.push_back(c);
490+							}
491+						} else {
492+							selector.push_back(c);
493+						}
494+					}
495+				}
496+			}
497+			handler->add(selector, current.props);
498+
499+			if (!stack.empty()) {
500+				current = stack.back();
501+				stack.pop_back();
502+			} else {
503+				current = SelectorAndProps();
504+			}
505+			buffer.clear();
506+		} else if (ch == ';') {
507+			// end of a style property
508+			std::string key;
509+			std::string value;
510+			bool keyFound = false;
511+			for (size_t i = 0; i < buffer.length(); i++) {
512+				char c = buffer[i];
513+				if (!keyFound && c == ':') {
514+					keyFound = true;
515+				} else if (!std::isspace(c) && !keyFound) {
516+					key += c;
517+				} else if (keyFound  &&
518+						!(std::isspace(c) && i < buffer.length()-1 && std::isspace(buffer[i+1]))) {
519+					// if the key is found and the charactor isn't a ':'
520+					// 	also (if c is a space and the next c will be a space)
521+					// 	this is to auto trim duplicate spaces from the value
522+					value += c;
523+					
524+				}
525+			}
526+
527+			current.props[key] = value;
528+			buffer.clear();
529+		} else {
530+			buffer += ch;
531+		}
532+	}
533+
534+	return handler;
535+}
536diff --git a/tests/css_parser.cc b/tests/css_parser.cc
537new file mode 100644
538index 0000000..6d30bf2
539--- /dev/null
540+++ b/tests/css_parser.cc
541@@ -0,0 +1,23 @@
542+#include <catch_amalgamated.hpp>
543+#include "../include/grim.h"
544+#include "../include/parser.h"
545+#include <iostream>
546+#include <fstream>
547+
548+TEST_CASE("parseCSS can parse a CSS sheet") {
549+	std::ifstream inputFile("./tests/master.css");
550+	if (!inputFile.is_open()) {
551+		std::cerr << "Error: Could not open style.css" << std::endl;
552+	}
553+
554+	//std::unique_ptr<StyleHandler> document = parseCSS(inputFile);
555+	SECTION("StyleHandler Benchmark") {
556+		BENCHMARK("style.css") {
557+			std::ifstream tempFile("./tests/master.css");
558+			return parseCSS(tempFile);
559+		};
560+	}
561+	inputFile.close();
562+}
563+
564+// When nesting tests are done update webpage
565diff --git a/tests/css_selector.cc b/tests/css_selector.cc
566index eb0fcfe..edc4107 100644
567--- a/tests/css_selector.cc
568+++ b/tests/css_selector.cc
569@@ -138,7 +138,7 @@ TEST_CASE("testSelector", "[CSS]") {
570 
571 
572 	std::stringstream ss(html);
573-	std::unique_ptr<Node> document = parseStream(ss);
574+	std::unique_ptr<Node> document = parseHTML(ss);
575 
576 	SECTION("testSelector can match a element given it's tagName") {
577 		auto html = document->children[0].get();
578diff --git a/tests/html_node.cc b/tests/html_node.cc
579index f4b48c5..58d3520 100644
580--- a/tests/html_node.cc
581+++ b/tests/html_node.cc
582@@ -10,7 +10,7 @@ TEST_CASE("ClassList", "[html]") {
583 	std::string html = "<div class=\"class1  class2 class3\"></div>";
584 
585 	std::stringstream ss(html);
586-	std::unique_ptr<Node> document = parseStream(ss);
587+	std::unique_ptr<Node> document = parseHTML(ss);
588 	auto div = document->children[0].get();
589 
590 	SECTION("ClassList can get the length and access items correctly") {
591diff --git a/tests/html_parser.cc b/tests/html_parser.cc
592index 8aee109..4b6bbe7 100644
593--- a/tests/html_parser.cc
594+++ b/tests/html_parser.cc
595@@ -24,7 +24,7 @@ void checkNode(Node* node, std::string tagName, unsigned long children, std::str
596 }
597 
598 
599-TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
600+TEST_CASE( "parseHTML can parse various types of HTML strings", "[html]" ) {
601 	SECTION("Parses a simple HTML document") {
602 		std::string html = "<!DOCTYPE html>"
603 			"<html>"
604@@ -39,10 +39,11 @@ TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
605 
606 
607 		std::stringstream ss(html);
608-		std::unique_ptr<Node> document = parseStream(ss);
609+		std::unique_ptr<Node> document = parseHTML(ss);
610 
611 		BENCHMARK("Simple HTML Document") {
612-			return parseStream(ss);
613+			std::stringstream ss2(html);
614+			return parseHTML(ss2);
615 		};
616 
617 		// Ensure the only child of root is html
618@@ -79,10 +80,11 @@ TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
619 				   "</div>";
620 
621 		std::stringstream ss(html);
622-		std::unique_ptr<Node> document = parseStream(ss);
623+		std::unique_ptr<Node> document = parseHTML(ss);
624 
625 		BENCHMARK("Inline comment") {
626-			return parseStream(ss);
627+			std::stringstream ss2(html);
628+			return parseHTML(ss2);
629 		};
630 
631 		// Ensure the only child of root is html
632@@ -99,10 +101,11 @@ TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
633 				   "</div>";
634 
635 		std::stringstream ss(html);
636-		std::unique_ptr<Node> document = parseStream(ss);
637+		std::unique_ptr<Node> document = parseHTML(ss);
638 
639 		BENCHMARK("Multiline comment") {
640-			return parseStream(ss);
641+			std::stringstream ss2(html);
642+			return parseHTML(ss2);
643 		};
644 
645 		// Ensure the only child of root is html
646@@ -119,10 +122,11 @@ TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
647 				   "</div>";
648 
649 		std::stringstream ss(html);
650-		std::unique_ptr<Node> document = parseStream(ss);
651+		std::unique_ptr<Node> document = parseHTML(ss);
652 
653 		BENCHMARK("Multiline comment with HTML") {
654-			return parseStream(ss);
655+			std::stringstream ss2(html);
656+			return parseHTML(ss2);
657 		};
658 
659 		// Ensure the only child of root is html
660@@ -142,10 +146,11 @@ TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
661 				   "<h1>test</h1>";
662 
663 		std::stringstream ss(html);
664-		std::unique_ptr<Node> document = parseStream(ss);
665+		std::unique_ptr<Node> document = parseHTML(ss);
666 
667 		BENCHMARK("Style tag parsing (with HTML in the content prop)") {
668-			return parseStream(ss);
669+			std::stringstream ss2(html);
670+			return parseHTML(ss2);
671 		};
672 
673 		// Ensure the only child of root is html
674@@ -173,10 +178,11 @@ TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
675 				"</pre>";
676 
677 		std::stringstream ss(html);
678-		std::unique_ptr<Node> document = parseStream(ss);
679+		std::unique_ptr<Node> document = parseHTML(ss);
680 
681 		BENCHMARK("Script tag prematurly closing a pre tag") {
682-			return parseStream(ss);
683+			std::stringstream ss2(html);
684+			return parseHTML(ss2);
685 		};
686 
687 		// Ensure the only child of root is html
688@@ -197,10 +203,11 @@ TEST_CASE( "parseStream can parse various types of HTML strings", "[html]" ) {
689 				"</textarea>";
690 
691 		std::stringstream ss(html);
692-		std::unique_ptr<Node> document = parseStream(ss);
693+		std::unique_ptr<Node> document = parseHTML(ss);
694 
695 		BENCHMARK("Textarea with script tag inside") {
696-			return parseStream(ss);
697+			std::stringstream ss2(html);
698+			return parseHTML(ss2);
699 		};
700 
701 		// Ensure the only child of root is html
702diff --git a/tests/master.css b/tests/master.css
703new file mode 100755
704index 0000000..43ac28a
705--- /dev/null
706+++ b/tests/master.css
707@@ -0,0 +1,752 @@
708+/* https://chromium.googlesource.com/chromium/blink/+/master/Source/core/css/html.css
709+ */
710+
711+* {
712+	cursor: default;
713+}
714+
715+html {
716+	display: block;
717+	width: 100%;
718+	height: 100%;
719+	position: relative;
720+	font-size: 16px;
721+	font-family: serif;
722+	color: #000000;
723+	background-color: #ffffff;
724+	overflow: auto;
725+	scrollbar-color: #6b6b6b #2b2b2b;
726+	scrollbar-width: auto;
727+}
728+
729+head {
730+	display: none;
731+}
732+
733+meta {
734+	display: none;
735+}
736+
737+title {
738+	display: none;
739+}
740+
741+link {
742+	display: none;
743+}
744+
745+style {
746+	display: none;
747+}
748+
749+script {
750+	display: none;
751+}
752+
753+span {
754+	display: inline;
755+}
756+
757+text {
758+	display: inline;
759+	font-size: 1em;
760+}
761+
762+body {
763+	display: block;
764+	margin: 8px;
765+	min-height: 100%;
766+}
767+
768+p {
769+	display: block;
770+}
771+
772+div {
773+	display: block;
774+}
775+
776+layer {
777+	display: block;
778+}
779+
780+article,
781+aside,
782+footer,
783+header,
784+hgroup,
785+main,
786+nav,
787+section {
788+	display: block;
789+}
790+
791+marquee {
792+	display: inline-block;
793+}
794+
795+address {
796+	display: block;
797+}
798+
799+blockquote {
800+	display: block;
801+	margin-top: 1em;
802+	margin-bottom: 1em;
803+	margin-left: 40px;
804+	margin-right: 40px;
805+}
806+
807+figcaption {
808+	display: block;
809+}
810+
811+figure {
812+	display: block;
813+	margin-top: 1em;
814+	margin-bottom: 1em;
815+	margin-left: 40px;
816+	margin-right: 40px;
817+}
818+
819+center {
820+	display: block;
821+	text-align: center;
822+}
823+
824+hr {
825+	display: block;
826+	margin-top: 0.5em;
827+	margin-bottom: 0.5em;
828+	margin-left: auto;
829+	margin-right: auto;
830+	border-style: inset;
831+	border-width: 1px;
832+}
833+
834+map {
835+	display: inline;
836+}
837+
838+video {
839+	object-fit: contain;
840+}
841+
842+h1 {
843+	display: block;
844+	font-size: 2em;
845+	margin-block: 0.67em;
846+	font-weight: bold;
847+}
848+
849+article h1,
850+aside h1,
851+nav h1,
852+section h1 {
853+	font-size: 1.5em;
854+	margin-top: 0.83em;
855+	margin-bottom: 0.83em;
856+}
857+
858+h2 {
859+	display: block;
860+	font-size: 1.5em;
861+	margin-top: 0.83em;
862+	margin-bottom: 0.83em;
863+	margin-left: 0;
864+	margin-right: 0;
865+	font-weight: bold;
866+}
867+
868+h3 {
869+	display: block;
870+	font-size: 1.17em;
871+	margin-top: 1em;
872+	margin-bottom: 1em;
873+	margin-left: 0;
874+	margin-right: 0;
875+	font-weight: bold;
876+}
877+
878+h4 {
879+	display: block;
880+	margin-top: 1.33em;
881+	margin-bottom: 1.33em;
882+	margin-left: 0;
883+	margin-right: 0;
884+	font-weight: bold;
885+}
886+
887+h5 {
888+	display: block;
889+	font-size: 0.83em;
890+	margin-top: 1.67em;
891+	margin-bottom: 1.67em;
892+	margin-left: 0;
893+	margin-right: 0;
894+	font-weight: bold;
895+}
896+
897+h6 {
898+	display: block;
899+	font-size: 0.67em;
900+	margin-top: 2.33em;
901+	margin-bottom: 2.33em;
902+	margin-left: 0;
903+	margin-right: 0;
904+	font-weight: bold;
905+}
906+
907+table {
908+	display: table;
909+	border-collapse: separate;
910+	border-spacing: 2px;
911+	border-color: gray;
912+}
913+
914+thead {
915+	display: table-header-group;
916+	vertical-align: middle;
917+	border-color: inherit;
918+}
919+
920+tbody {
921+	display: table-row-group;
922+	vertical-align: middle;
923+	border-color: inherit;
924+}
925+
926+tfoot {
927+	display: table-footer-group;
928+	vertical-align: middle;
929+	border-color: inherit;
930+}
931+
932+table>tr {
933+	vertical-align: middle;
934+}
935+
936+col {
937+	display: table-column;
938+}
939+
940+colgroup {
941+	display: table-column-group;
942+}
943+
944+tr {
945+	display: table-row;
946+	vertical-align: inherit;
947+	border-color: inherit;
948+}
949+
950+td,
951+th {
952+	display: table-cell;
953+	vertical-align: inherit;
954+}
955+
956+th {
957+	font-weight: bold;
958+}
959+
960+caption {
961+	display: table-caption;
962+	text-align: center;
963+}
964+
965+ul,
966+menu,
967+dir {
968+	display: block;
969+	list-style-type: disc;
970+	margin-top: 1em;
971+	margin-bottom: 1em;
972+	margin-left: 0;
973+	margin-right: 0;
974+	padding-left: 40px;
975+}
976+
977+ol {
978+	display: block;
979+	list-style-type: decimal;
980+	margin-top: 1em;
981+	margin-bottom: 1em;
982+	margin-left: 0;
983+	margin-right: 0;
984+	padding-left: 40px;
985+}
986+
987+li {
988+	/* display: list-item;
989+    text-align: inline-block; */
990+	display: flex;
991+	align-items: center;
992+}
993+
994+ul ul,
995+ol ul {
996+	list-style-type: circle;
997+}
998+
999+ol ol ul,
1000+ol ul ul,
1001+ul ol ul,
1002+ul ul ul {
1003+	list-style-type: square;
1004+}
1005+
1006+dd {
1007+	display: block;
1008+	margin-left: 40px;
1009+}
1010+
1011+dl {
1012+	display: block;
1013+	margin-top: 1em;
1014+	margin-bottom: 1em;
1015+	margin-left: 0;
1016+	margin-right: 0;
1017+}
1018+
1019+dt {
1020+	display: block;
1021+}
1022+
1023+ol ul,
1024+ul ol,
1025+ul ul,
1026+ol ol {
1027+	margin-top: 0;
1028+	margin-bottom: 0;
1029+}
1030+
1031+form {
1032+	display: block;
1033+	margin-top: 0em;
1034+}
1035+
1036+label {
1037+	cursor: default;
1038+}
1039+
1040+legend {
1041+	display: block;
1042+	padding-left: 2px;
1043+	padding-right: 2px;
1044+	border: none;
1045+}
1046+
1047+fieldset {
1048+	display: block;
1049+	margin-left: 2px;
1050+	margin-right: 2px;
1051+	padding-top: 0.35em;
1052+	padding-left: 0.75em;
1053+	padding-right: 0.75em;
1054+	padding-bottom: 0.625em;
1055+	border: 2px groove ThreeDFace;
1056+	min-width: min-content;
1057+}
1058+
1059+button {
1060+	border: 1px solid #eee;
1061+}
1062+
1063+input,
1064+textarea,
1065+keygen,
1066+select,
1067+button {
1068+	margin: 0em;
1069+	font: small;
1070+	text-rendering: auto;
1071+	color: initial;
1072+	letter-spacing: normal;
1073+	word-spacing: normal;
1074+	line-height: normal;
1075+	text-transform: none;
1076+	text-indent: 0;
1077+	text-shadow: none;
1078+	display: inline-block;
1079+	text-align: start;
1080+}
1081+
1082+input[type="hidden" i] {
1083+	display: none;
1084+}
1085+
1086+input {
1087+	padding: 1px;
1088+	background-color: white;
1089+	border: 1px inset;
1090+	user-select: text;
1091+	cursor: text;
1092+	height: 1em;
1093+	width: 153px;
1094+	font-size: 16px;
1095+}
1096+
1097+input[type="search" i] {
1098+	box-sizing: border-box;
1099+}
1100+
1101+keygen,
1102+select {
1103+	border-radius: 5px;
1104+	margin: 0px;
1105+}
1106+
1107+textarea {
1108+	background-color: white;
1109+	border: 1px solid;
1110+	user-select: text;
1111+	flex-direction: column;
1112+	resize: auto;
1113+	cursor: auto;
1114+	padding: 2px;
1115+	white-space: pre-wrap;
1116+	word-wrap: break-word;
1117+}
1118+
1119+input[type="password" i] {
1120+	text-security: disc !important;
1121+}
1122+
1123+input[type="hidden" i],
1124+input[type="image" i],
1125+input[type="file" i] {
1126+	padding: initial;
1127+	background-color: initial;
1128+	border: initial;
1129+}
1130+
1131+input[type="file" i] {
1132+	align-items: baseline;
1133+	color: inherit;
1134+	text-align: start !important;
1135+}
1136+
1137+input[type="radio" i],
1138+input[type="checkbox" i] {
1139+	margin: 3px 0.5ex;
1140+	padding: initial;
1141+	background-color: initial;
1142+	border: initial;
1143+}
1144+
1145+input[type="button" i],
1146+input[type="submit" i],
1147+input[type="reset" i] {
1148+	user-select: none;
1149+	white-space: pre;
1150+}
1151+
1152+input[type="file" i] {
1153+	white-space: nowrap;
1154+	margin: 0;
1155+	font-size: inherit;
1156+}
1157+
1158+input[type="button" i],
1159+input[type="submit" i],
1160+input[type="reset" i],
1161+input[type="file" i] button {
1162+	align-items: flex-start;
1163+	text-align: center;
1164+	cursor: default;
1165+	color: ButtonText;
1166+	padding: 2px 6px 3px 6px;
1167+	border: 2px outset ButtonFace;
1168+	background-color: ButtonFace;
1169+	box-sizing: border-box;
1170+}
1171+
1172+input[type="button" i]:disabled,
1173+input[type="submit" i]:disabled,
1174+input[type="reset" i]:disabled,
1175+input[type="file" i]:disabled,
1176+button:disabled,
1177+select:disabled,
1178+keygen:disabled,
1179+optgroup:disabled,
1180+option:disabled,
1181+select[disabled]>option {
1182+	color: GrayText;
1183+}
1184+
1185+input[type="button" i]:active,
1186+input[type="submit" i]:active,
1187+input[type="reset" i]:active,
1188+input[type="file" i]:active,
1189+button:active {
1190+	border-style: inset;
1191+}
1192+
1193+input[type="button" i]:active:disabled,
1194+input[type="submit" i]:active:disabled,
1195+input[type="reset" i]:active:disabled,
1196+input[type="file" i]:active:disabled,
1197+button:active:disabled {
1198+	border-style: outset;
1199+}
1200+
1201+option {
1202+	outline: black dashed 1px;
1203+	outline-offset: -1px;
1204+}
1205+
1206+datalist {
1207+	display: none;
1208+}
1209+
1210+area {
1211+	display: inline;
1212+	cursor: pointer;
1213+}
1214+
1215+param {
1216+	display: none;
1217+}
1218+
1219+input[type="checkbox" i] {
1220+	appearance: checkbox;
1221+	box-sizing: border-box;
1222+}
1223+
1224+input[type="radio" i] {
1225+	appearance: radio;
1226+	box-sizing: border-box;
1227+}
1228+
1229+select {
1230+	box-sizing: border-box;
1231+	align-items: center;
1232+	border: 1px solid;
1233+	white-space: pre;
1234+	color: black;
1235+	background-color: white;
1236+	cursor: default;
1237+}
1238+
1239+optgroup {
1240+	font-weight: bolder;
1241+	display: block;
1242+}
1243+
1244+option {
1245+	font-weight: normal;
1246+	display: block;
1247+	padding: 0 2px 1px 2px;
1248+	white-space: pre;
1249+	min-height: 1.2em;
1250+}
1251+
1252+output {
1253+	display: inline;
1254+}
1255+
1256+meter {
1257+	appearance: meter;
1258+	box-sizing: border-box;
1259+	display: inline-block;
1260+	height: 1em;
1261+	width: 5em;
1262+	vertical-align: -0.2em;
1263+}
1264+
1265+u,
1266+ins {
1267+	text-decoration: underline;
1268+	display: inline;
1269+}
1270+
1271+strong,
1272+b {
1273+	font-weight: bold;
1274+	display: inline;
1275+}
1276+
1277+i,
1278+cite,
1279+em,
1280+var,
1281+address,
1282+dfn {
1283+	font-style: italic;
1284+	display: inline;
1285+}
1286+
1287+tt,
1288+code,
1289+kbd,
1290+samp {
1291+	font-family: monospace;
1292+}
1293+
1294+pre,
1295+xmp,
1296+plaintext,
1297+listing {
1298+	display: block;
1299+	font-family: monospace;
1300+	white-space: pre;
1301+	margin: 1em 0;
1302+}
1303+
1304+mark {
1305+	background-color: yellow;
1306+	color: black;
1307+}
1308+
1309+big {
1310+	font-size: larger;
1311+}
1312+
1313+small {
1314+	font-size: smaller;
1315+}
1316+
1317+s,
1318+strike,
1319+del {
1320+	text-decoration: line-through;
1321+}
1322+
1323+sub {
1324+	vertical-align: sub;
1325+	font-size: smaller;
1326+}
1327+
1328+sup {
1329+	vertical-align: super;
1330+	font-size: smaller;
1331+}
1332+
1333+br {
1334+	display: block;
1335+	font-size: 0;
1336+	width: 0;
1337+	/* height: 1em; */
1338+}
1339+
1340+nobr {
1341+	white-space: nowrap;
1342+}
1343+
1344+/* states */
1345+:focus {
1346+	outline: auto 5px #00f;
1347+}
1348+
1349+/* Read-only text fields do not show a focus ring but do still receive focus */
1350+input[readonly]:focus {
1351+	outline: none;
1352+}
1353+
1354+embed:focus,
1355+iframe:focus,
1356+object:focus {
1357+	outline: none;
1358+}
1359+
1360+input:focus,
1361+textarea:focus,
1362+keygen:focus,
1363+select:focus {
1364+	outline-offset: -2px;
1365+}
1366+
1367+input[type="button" i]:focus,
1368+input[type="checkbox" i]:focus,
1369+input[type="file" i]:focus,
1370+input[type="hidden" i]:focus,
1371+input[type="image" i]:focus,
1372+input[type="radio" i]:focus,
1373+input[type="reset" i]:focus,
1374+input[type="search" i]:focus,
1375+input[type="submit" i]:focus,
1376+input[type="file" i]:focus {
1377+	outline-offset: 0;
1378+}
1379+
1380+input:disabled {
1381+	cursor: default;
1382+	background-color: light-dark(rgba(239, 239, 239, 0.3), rgba(59, 59, 59, 0.3));
1383+	color: light-dark(rgb(84, 84, 84), rgb(170, 170, 170));
1384+	border-color: rgba(118, 118, 118, 0.3);
1385+}
1386+
1387+a {
1388+	color: #0100ee;
1389+	text-decoration: underline;
1390+	cursor: pointer;
1391+	text-decoration-thickness: 1px;
1392+	text-decoration-color: #0100ee;
1393+	display: inline;
1394+}
1395+
1396+a:focus {
1397+	border: 10px solid blue;
1398+	background: red;
1399+}
1400+
1401+a:active {
1402+	color: #551a8b;
1403+}
1404+
1405+ruby,
1406+rt {
1407+	text-indent: 0;
1408+}
1409+
1410+rt {
1411+	line-height: normal;
1412+	text-emphasis: none;
1413+}
1414+
1415+ruby>rt {
1416+	display: block;
1417+	font-size: 50%;
1418+	text-align: start;
1419+}
1420+
1421+ruby>rp {
1422+	display: none;
1423+}
1424+
1425+noframes {
1426+	display: none;
1427+}
1428+
1429+frameset,
1430+frame {
1431+	display: block;
1432+}
1433+
1434+frameset {
1435+	border-color: inherit;
1436+}
1437+
1438+iframe {
1439+	border: 2px inset;
1440+}
1441+
1442+details {
1443+	display: block;
1444+}
1445+
1446+summary {
1447+	display: block;
1448+}
1449+
1450+summary {
1451+	display: inline-block;
1452+	width: 0.66em;
1453+	height: 0.66em;
1454+	margin-right: 0.4em;
1455+}
1456+
1457+template {
1458+	display: none;
1459+}
1460diff --git a/tests/style.css b/tests/style.css
1461index 2e59320..12b3364 100644
1462--- a/tests/style.css
1463+++ b/tests/style.css
1464@@ -5,3 +5,69 @@ h1 {
1465 h1 > #id {
1466 	background: purple;
1467 }
1468+
1469+div:checked {
1470+	display: flex;
1471+	margin: 1px 100% 20vh;
1472+}
1473+
1474+
1475+html#nested {
1476+	background: url("https://example.com");
1477+
1478+	h1[value="nested"] {
1479+		color: red;	
1480+	}
1481+
1482+	+ h2 {
1483+		border:
1484+		none;
1485+	}
1486+}
1487+.notice {
1488+  width: 90%;
1489+  justify-content: center;
1490+  border-radius: 1rem;
1491+  border: black solid 2px;
1492+  background-color: #ffc107;
1493+  color: black;
1494+  padding: 1rem;
1495+  .notice-heading::before {
1496+    /* equivalent to `.notice .notice-heading::before` */
1497+    content: "ℹ︎ ";
1498+  }
1499+  font-size: 10px;
1500+  &.warning {
1501+    /* equivalent to `.notice.warning` */
1502+    background-color: #d81b60;
1503+    border-color: #d81b60;
1504+    color: white;
1505+    .warning-heading::before {
1506+      /* equivalent to `.notice.warning .warning-heading::before` */
1507+      content: "! ";
1508+    }
1509+  }
1510+  &.success {
1511+    /* equivalent to `.notice.success` */
1512+    background-color: #004d40;
1513+    border-color: #004d40;
1514+    color: white;
1515+    .success-heading::before {
1516+      /* equivalent to `.notice.success .success-heading::before` */
1517+      content: "✓ ";
1518+    }
1519+  }
1520+}
1521+@media (hover: hover) {
1522+  abbr:hover {
1523+    color: #001ca8;
1524+    transition-duration: 0.5s;
1525+  }
1526+}
1527+
1528+@media not all and (hover: hover) {
1529+  abbr::after {
1530+    content: " (" attr(title) ")";
1531+  }
1532+}
1533+