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]") {