home readme diff tree note docs

0commit 2b08fe91433aaeaab3ad87f3c47a82195e9d49db
1Author: Mason Wright <mason@lakefox.net>
2Date:   Fri Jul 11 14:49:41 2025 -0600
3
4    Done testing parseCSS and parseCSSInline
5
6diff --git a/docs/content/features/supported-css-selectors.md b/docs/content/features/supported-css-selectors.md
7index 725f27c..51f8f2a 100644
8--- a/docs/content/features/supported-css-selectors.md
9+++ b/docs/content/features/supported-css-selectors.md
10@@ -7,9 +7,9 @@
11 | Attribute | X | testSelector | testSelector can match a element by it's attributes only | tests/css_selector.cc |
12 | ID | X | testSelector | testSelector can match a element by it's id only | tests/css_selector.cc |
13 | Class | X | testSelector | testSelector can match a element by it's class and not matach on a non class | tests/css_selector.cc |
14-| Universal | X | testSelector | testSelector can match a universal selector |  |
15-| Nesting | X | testSelector | |  |
16-| & Nesting | X | testSelector | |  |
17+| Universal | X | testSelector | testSelector can match a universal selector | tests/css_selector.cc |
18+| Nesting | X | parseCSS can parse a CSS sheet | parseCSS can parse a nested CSS rule without a &, parseCSS can parse a the parent of a nested CSS rule, parseCSS can parse a nested CSS rule with a pseudo element, parseCSS can parse a parent CSS rule with a property in between sub-classes | tests/css_parser.cc |
19+| & Nesting | X | parseCSS can parse a CSS sheet | parseCSS can parse a nested CSS rule without a &, parseCSS can parse a the parent of a nested CSS rule, parseCSS can parse a nested CSS rule with a pseudo element, parseCSS can parse a double nested CSS rule with a pseudo element and &, parseCSS can parse a nested CSS rule with a &, parseCSS can parse a parent CSS rule with a property in between sub-classes | tests/css_parser.cc |
20 
21 ## Combinators
22 | Name | Support | Test Case | Section | Test File |
23diff --git a/include/grim.h b/include/grim.h
24index 5cfe725..2a251f4 100644
25--- a/include/grim.h
26+++ b/include/grim.h
27@@ -125,6 +125,8 @@ class StyleHandler {
28 		
29 		std::unordered_map<std::string, std::string> getStyles(Node* node);
30 
31+		Style item(int index);
32+
33 		// make a function that will return a list of attributes the will cause style changes if set
34 };
35 
36diff --git a/include/parser.h b/include/parser.h
37index 2fbf8b9..afb935d 100644
38--- a/include/parser.h
39+++ b/include/parser.h
40@@ -15,5 +15,6 @@ class StyleHandler;
41 std::unordered_map<std::string, std::string> parseAttributes(std::string_view& token);
42 std::unique_ptr<Node> parseHTML(std::istream& inputStream);
43 std::unique_ptr<StyleHandler> parseCSS(std::istream& inputStream);
44+std::unordered_map<std::string, std::string> parseCSSInline(std::string inlineStyles);
45 
46 #endif
47diff --git a/src/grim.cc b/src/grim.cc
48index 82bd879..796c77d 100755
49--- a/src/grim.cc
50+++ b/src/grim.cc
51@@ -390,20 +390,27 @@ std::vector<std::vector<std::string>> parseSelectorParts(std::string_view select
52 					trimSpace(current);
53 				}
54 				if (!current.empty()) {
55-					if (current[0] == ':' && current.back() != ')') {
56+					if (current[0] == ':' && current.back() != ')' && current.length() > 1 && current[1] != ':') {
57 						current[0] = '[';
58 						current.push_back(']');
59 					}
60 					buffer.push_back(std::move(current));
61 				}
62-				current = s;
63+				if (s == ':' && e+1 < sl && selector[e+1] == ':') {
64+					current.clear();
65+					current = "::";
66+					e++;
67+					continue;
68+				} else {
69+					current = s;
70+				}
71 			} else 
72 				if (s == '>' || s == '+' || s == '~' || s == ' ') {
73 					if (std::isspace(current.front()) || std::isspace(current.back())) {
74 						trimSpace(current);
75 					}		
76 					if (!current.empty()) {
77-						if (current[0] == ':' && current.back() != ')') {
78+						if (current[0] == ':' && current.back() != ')' && current.length() > 1 && current[1] != ':') {
79 							current[0] = '[';
80 							current.push_back(']');
81 						}
82@@ -430,7 +437,7 @@ std::vector<std::vector<std::string>> parseSelectorParts(std::string_view select
83 				trimSpace(current);
84 			};
85 			if (!current.empty() && current != ",") {
86-				if (current[0] == ':' && current.back() != ')') {
87+				if (current[0] == ':' && current.back() != ')' && current.length() > 1 && current[1] != ':') {
88 					current[0] = '[';
89 					current.push_back(']');
90 
91@@ -567,6 +574,9 @@ std::unordered_map<std::string, std::string> StyleHandler::getStyles(Node* node)
92 	delete[] matchingStyles;
93 }
94 
95+Style StyleHandler::item(int index) {
96+	return styles[index];
97+}
98 
99 // Used for root node without a parent
100 const std::vector<std::unique_ptr<Node>> EMPTY_NODE_CHILDREN_VECTOR;
101diff --git a/src/parser.cc b/src/parser.cc
102index 555e3e9..5d8d984 100644
103--- a/src/parser.cc
104+++ b/src/parser.cc
105@@ -437,9 +437,71 @@ std::unique_ptr<StyleHandler> parseCSS(std::istream& inputStream) {
106 			current.props[key] = value;
107 			buffer.clear();
108 		} else {
109-			buffer += ch;
110+			buffer.push_back(ch);
111 		}
112 	}
113 
114 	return handler;
115 }
116+
117+std::unordered_map<std::string, std::string> parseCSSInline(std::string inlineStyles) {
118+	std::unordered_map<std::string, std::string> props;
119+
120+	std::string buffer;
121+
122+	for (size_t i = 0; i < inlineStyles.length(); i++) {
123+		char ch = inlineStyles[i];
124+
125+		if (ch == ';') {
126+			std::string key;
127+			std::string value;
128+			bool keyFound = false;
129+			for (size_t i = 0; i < buffer.length(); i++) {
130+				char c = buffer[i];
131+				if (!keyFound && c == ':') {
132+					keyFound = true;
133+				} else if (!std::isspace(c) && !keyFound) {
134+					key += c;
135+				} else if (keyFound &&
136+						!(std::isspace(c) && i < buffer.length()-1 && std::isspace(buffer[i+1]))
137+					  ) {
138+					// if the key is found and the charactor isn't a ':'
139+					// 	also (if c is a space and the next c will be a space)
140+					// 	this is to auto trim duplicate spaces from the value
141+					value += c;
142+					
143+				}
144+			}
145+
146+			props[key] = value;
147+
148+			buffer.clear();
149+		} else {
150+			buffer.push_back(ch);
151+		}
152+	}
153+
154+	if (buffer.length() > 0) {
155+		std::string key;
156+		std::string value;
157+		bool keyFound = false;
158+		for (size_t i = 0; i < buffer.length(); i++) {
159+			char c = buffer[i];
160+			if (!keyFound && c == ':') {
161+				keyFound = true;
162+			} else if (!std::isspace(c) && !keyFound) {
163+				key += c;
164+			} else if (keyFound  &&
165+					!(std::isspace(c) && i < buffer.length()-1 && std::isspace(buffer[i+1]))) {
166+				// if the key is found and the charactor isn't a ':'
167+				// 	also (if c is a space and the next c will be a space)
168+				// 	this is to auto trim duplicate spaces from the value
169+				value += c;
170+
171+			}
172+		}
173+		props[key] = value;
174+	}
175+
176+	return props;
177+}
178diff --git a/tests/css_parser.cc b/tests/css_parser.cc
179index 6d30bf2..02eac9f 100644
180--- a/tests/css_parser.cc
181+++ b/tests/css_parser.cc
182@@ -4,20 +4,267 @@
183 #include <iostream>
184 #include <fstream>
185 
186+void printSelector(std::vector<std::vector<std::string>> selector) {
187+	for (size_t a = 0; a < selector.size(); a++) {
188+		for (size_t b = 0; b < selector[a].size(); b++) {
189+			std::cout << selector[a][b];
190+		}
191+	}
192+	std::cout << std::endl;
193+}
194+
195+void trimSpace2(std::string& str) {
196+    if (str.empty()) {
197+        return;
198+    }
199+
200+    // Find first non-space
201+    size_t first_non_space = 0;
202+    while (first_non_space < str.length() && std::isspace(str[first_non_space])) {
203+        first_non_space++;
204+    }
205+
206+    // If all spaces or empty
207+    if (first_non_space == str.length()) {
208+        str.clear(); // Set to empty string
209+        return;
210+    }
211+
212+    // Find last non-space
213+    size_t last_non_space = str.length() - 1;
214+    while (last_non_space >= first_non_space && std::isspace(str[last_non_space])) {
215+        last_non_space--;
216+    }
217+
218+    // Erase trailing spaces
219+    if (last_non_space < str.length() - 1) {
220+        str.erase(last_non_space + 1);
221+    }
222+
223+    // Erase leading spaces
224+    if (first_non_space > 0) {
225+        str.erase(0, first_non_space);
226+    }
227+}
228+
229+void compareStyle(std::string selector, std::unordered_map<std::string, std::string> props, size_t index, Style style) {
230+	REQUIRE(style.index == index);
231+	std::vector<std::vector<std::string>> parts = parseSelectorParts(selector);
232+	REQUIRE(parts.size() == style.selector.size());
233+
234+	for (size_t a = 0; a < parts.size(); a++) {
235+		
236+		REQUIRE(parts[a].size() == style.selector[a].size());
237+
238+		for (size_t b = 0; b < parts[a].size(); b++) {
239+			REQUIRE(parts[a][b] == style.selector[a][b]);
240+		}
241+	}
242+
243+	for (auto p : props) {
244+		std::string f = p.second;
245+		std::string s = style.properties[p.first];
246+		trimSpace2(f);
247+		trimSpace2(s);
248+
249+		REQUIRE(f == s);
250+	}
251+}
252+
253+void compareInlineStyles(std::string style, std::unordered_map<std::string, std::string> targetProps) {
254+	std::unordered_map<std::string, std::string> props = parseCSSInline(style);
255+	
256+	REQUIRE(props.size() == targetProps.size());
257+
258+	for (auto p : props) {
259+		std::string f = p.second;
260+		std::string s = targetProps[p.first];
261+		trimSpace2(f);
262+		trimSpace2(s);
263+
264+		REQUIRE(f == s);
265+	}
266+}
267+
268 TEST_CASE("parseCSS can parse a CSS sheet") {
269-	std::ifstream inputFile("./tests/master.css");
270-	if (!inputFile.is_open()) {
271-		std::cerr << "Error: Could not open style.css" << std::endl;
272+	    std::string cssContent = R"~~~(
273+h1 {
274+	color: red;
275+}
276+
277+h1 > #id {
278+	background: purple;
279+}
280+
281+div:checked {
282+	display: flex;
283+	margin: 1px 100% 20vh;
284+}
285+
286+
287+html#nested {
288+	background: url("https://example.com");
289+
290+	h1[value="nested"] {
291+		color: red;	
292+	}
293+
294+	&.class {
295+		border:
296+		none;
297 	}
298+}
299+.notice {
300+  width: 90%;
301+  justify-content: center;
302+  border-radius: 1rem;
303+  border: black solid 2px;
304+  background-color: #ffc107;
305+  color: black;
306+  padding: 1rem;
307+  .notice-heading::before {
308+    /* equivalent to `.notice .notice-heading::before` */
309+    content: "i ";
310+  }
311+  font-size: 10px;
312+  &.warning {
313+    /* equivalent to `.notice.warning` */
314+    background-color: #d81b60;
315+    border-color: #d81b60;
316+    color: white;
317+    .warning-heading::before {
318+      /* equivalent to `.notice.warning .warning-heading::before` */
319+      content: "! ";
320+    }
321+  }
322+}
323+@media (hover: hover) {
324+  abbr:hover {
325+    color: #001ca8;
326+    transition-duration: 0.5s;
327+  }
328+}
329+
330+@media not all and (hover: hover) {
331+  abbr::after {
332+    content: " (" attr(title) ")";
333+  }
334+}
335+)~~~";
336 
337-	//std::unique_ptr<StyleHandler> document = parseCSS(inputFile);
338+	std::istringstream inputFile(cssContent);	
339+
340+	std::unique_ptr<StyleHandler> handler = parseCSS(inputFile);
341 	SECTION("StyleHandler Benchmark") {
342 		BENCHMARK("style.css") {
343 			std::ifstream tempFile("./tests/master.css");
344 			return parseCSS(tempFile);
345 		};
346 	}
347-	inputFile.close();
348+
349+	SECTION("parseCSS can parse simple CSS rules") {
350+		compareStyle("h1", {
351+				{"color", "red"}
352+				}, 0, handler->item(0));
353+	}
354+
355+	SECTION("parseCSS can parse a CSS rule with a child combinator") {
356+		compareStyle("h1 > #id", {
357+				{"background", "purple"}
358+				}, 1, handler->item(1));
359+	}
360+
361+	SECTION("parseCSS can parse a CSS rule with a pseudo class") {
362+		compareStyle("div:checked", {
363+				{"display", "flex"},
364+				{"margin", "1px 100% 20vh"}
365+				}, 2, handler->item(2));
366+	}
367+
368+	SECTION("parseCSS can parse a nested CSS rule without a &") {
369+		compareStyle("html#nested h1[value=\"nested\"]", {
370+				{"color", "red"},
371+				}, 3, handler->item(3));
372+	}
373+
374+	SECTION("parseCSS can parse a nested CSS rule with a &") {
375+		compareStyle("html#nested.class", {
376+				{"border", "none"},
377+				}, 4, handler->item(4));
378+	}
379+
380+	SECTION("parseCSS can parse a the parent of a nested CSS rule") {
381+		compareStyle("html#nested", {
382+				{"background", "url(\"https://example.com\")"},
383+				}, 5, handler->item(5));
384+	}
385+
386+	SECTION("parseCSS can parse a nested CSS rule with a pseudo element") {
387+		compareStyle(".notice .notice-heading::before", {
388+				{"content", "\"i \""},
389+				}, 6, handler->item(6));
390+	}
391+
392+	SECTION("parseCSS can parse a double nested CSS rule with a pseudo element and &") {
393+		compareStyle(".notice.warning .warning-heading::before", {
394+				{"content", "\"! \""},
395+				}, 7, handler->item(7));
396+	}
397+
398+	SECTION("parseCSS can parse a nested CSS rule with a &") {
399+		compareStyle(".notice.warning", {
400+				{"background-color", "#d81b60"},
401+				{"border-color", "#d81b60"},
402+				{"color", "white"},
403+				}, 8, handler->item(8));
404+	}
405+
406+	SECTION("parseCSS can parse a parent CSS rule with a property in between sub-classes") {
407+		compareStyle(".notice", {
408+				{"width", "90%"},
409+				{"justify-content", "center"},
410+				{"border-radius", "1rem"},
411+				{"border", "black solid 2px"},
412+				{"background-color", "#ffc107"},
413+				{"color", "black"},
414+				{"padding", "1rem"},
415+				{"font-size", "10px"}
416+				  }, 9, handler->item(9));
417+	}
418 }
419 
420+TEST_CASE("parseCSSInline can parse inline styles") {
421+	SECTION("parseCSSInline can parse a single property with a ending ;") {
422+		std::string inlineStyles = "background: yellow;";
423+		compareInlineStyles(inlineStyles, {
424+				{"background", "yellow"}
425+				});
426+	}
427+
428+	SECTION("parseCSSInline can parse a single property without a ending ;") {
429+		std::string inlineStyles = "background: red";
430+		compareInlineStyles(inlineStyles, {
431+				{"background", "red"}
432+				});
433+	}
434+	SECTION("parseCSSInline can parse multiple properties with a ending ;") {
435+		std::string inlineStyles = "background: yellow; color: red; font-size: 10px;";
436+		compareInlineStyles(inlineStyles, {
437+				{"background", "yellow"},
438+				{"color", "red"},
439+				{"font-size", "10px"}
440+				});
441+	}
442+
443+	SECTION("parseCSSInline can parse multiple properties without a ending ;") {
444+		std::string inlineStyles = "background: red; color: yellow; font-size: 10px";
445+		compareInlineStyles(inlineStyles, {
446+				{"background", "red"},
447+				{"color", "yellow"},
448+				{"font-size", "10px"},
449+				});
450+	}
451+}
452 // When nesting tests are done update webpage
453+// inline parsing tests
454+// make .style act like classList
455diff --git a/tests/css_selector.cc b/tests/css_selector.cc
456index edc4107..eb4028c 100644
457--- a/tests/css_selector.cc
458+++ b/tests/css_selector.cc
459@@ -105,6 +105,9 @@ TEST_CASE( "CSS Selection", "[css]" ) {
460 	SECTION("parseSelectorParts can parse a tagName with a attribute selector and a pseudo class") {
461 		checkSelectorParts("input[type=\"text\"]:disabled", {{"input", "[type=\"text\"]", "[disabled]"}});
462 	}
463+	SECTION("parseSelectorParts can parse a tagName with a pseudo element") {
464+		checkSelectorParts("h1::before", {{"h1","::before"}});
465+	}
466 }
467 
468 TEST_CASE("testSelector", "[CSS]") {