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+