libhtmlpp 1.0.0
Loading...
Searching...
No Matches
htmlcsstest.cpp
Go to the documentation of this file.
1/*******************************************************************************
2Copyright (c) 2021, Jan Koester jan.koester@gmx.net
3All rights reserved.
4
5Redistribution and use in source and binary forms, with or without
6modification, are permitted provided that the following conditions are met:
7 * Redistributions of source code must retain the above copyright
8 notice, this list of conditions and the following disclaimer.
9 * Redistributions in binary form must reproduce the above copyright
10 notice, this list of conditions and the following disclaimer in the
11 documentation and/or other materials provided with the distribution.
12 * Neither the name of the <organization> nor the
13 names of its contributors may be used to endorse or promote products
14 derived from this software without specific prior written permission.
15
16THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
20DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26*******************************************************************************/
27
28#include <iostream>
29#include <set>
30
31#include "../src/html.h"
32#include "../src/css.h"
33#include "../src/htmlcss.h"
34
35#define Red "\033[0;31m"
36#define Green "\033[0;32m"
37#define NOCOLOR "\033[0m"
38
39static int testCount = 0;
40static int passCount = 0;
41
42static void check(bool cond, const char *desc) {
43 ++testCount;
44 if (cond) {
45 ++passCount;
46 std::cout << Green << " PASS: " << desc << NOCOLOR << std::endl;
47 } else {
48 std::cout << Red << " FAIL: " << desc << NOCOLOR << std::endl;
49 }
50}
51
52int main(){
53 // --- approximateSelectorMatch: tag/class/id/compound ---
54 std::cout << "=== approximateSelectorMatch ===" << std::endl;
55 {
57 std::vector<std::string> classes = {"card", "featured"};
58
59 check(CSSStyleSheet::approximateSelectorMatch("div", "div", classes, "hero"),
60 "bare tag matches");
61 check(!CSSStyleSheet::approximateSelectorMatch("span", "div", classes, "hero"),
62 "wrong tag doesn't match");
63 check(CSSStyleSheet::approximateSelectorMatch(".card", "div", classes, "hero"),
64 "single class matches");
65 check(CSSStyleSheet::approximateSelectorMatch(".card.featured", "div", classes, "hero"),
66 "every class in compound must be present -- matches when all are");
67 check(!CSSStyleSheet::approximateSelectorMatch(".card.missing", "div", classes, "hero"),
68 "every class in compound must be present -- rejects when one is absent");
69 check(CSSStyleSheet::approximateSelectorMatch("#hero", "div", classes, "hero"),
70 "id selector matches");
71 check(CSSStyleSheet::approximateSelectorMatch("div.card.featured#hero", "div", classes, "hero"),
72 "full compound tag.class.class#id matches");
73 check(CSSStyleSheet::approximateSelectorMatch(".other, .card", "div", classes, "hero"),
74 "comma list matches if any branch matches");
75 }
76
77 // --- Unsupported syntax is rejected, never guessed ---
78 std::cout << "=== approximateSelectorMatch rejects unsafe syntax ===" << std::endl;
79 {
81 std::vector<std::string> classes = {"card"};
82 check(!CSSStyleSheet::approximateSelectorMatch("[data-x]", "div", classes, ""),
83 "attribute selector rejected");
84 check(!CSSStyleSheet::approximateSelectorMatch("div:hover", "div", classes, ""),
85 "pseudo-class rejected");
86 check(!CSSStyleSheet::approximateSelectorMatch("*", "div", classes, ""),
87 "universal selector rejected");
88 check(!CSSStyleSheet::approximateSelectorMatch("[data-atom=header]>div", "div", classes, ""),
89 "bare tag left after a combinator is rejected (would over-match every element of that tag)");
90 check(CSSStyleSheet::approximateSelectorMatch("[data-atom=header]>div.card", "div", classes, ""),
91 "a qualified compound after a combinator is still specific enough to check");
92 }
93
94 // --- approximateSelectorMatch: ancestor chain verification ---
95 // Regression coverage for a real-world bug: a descendant selector like
96 // ".header .frame" was being matched against ANY ".frame" element in the
97 // whole document, because this matcher never checked the ".header" part
98 // against anything (see approximateSelectorMatch's own doc comment). On
99 // a real page-builder site, this made a header-only padding rule
100 // silently apply to every section on the page, collapsing all of their
101 // intended vertical spacing.
102 std::cout << "=== approximateSelectorMatch with ancestor chain ===" << std::endl;
103 {
106 std::vector<std::string> classes = {"frame"};
107
108 check(CSSStyleSheet::approximateSelectorMatch(".header .frame", "div", classes, ""),
109 "with no ancestor chain given (nullptr), leading compounds are still ignored -- unchanged behavior");
110
111 std::vector<AncestorFrame> matchingAncestors = {
112 {"div", {"header"}, ""},
113 };
114 check(CSSStyleSheet::approximateSelectorMatch(".header .frame", "div", classes, "", &matchingAncestors),
115 "leading compound found in the supplied ancestor chain -- matches");
116
117 std::vector<AncestorFrame> unrelatedAncestors = {
118 {"div", {"hero"}, ""},
119 };
120 check(!CSSStyleSheet::approximateSelectorMatch(".header .frame", "div", classes, "", &unrelatedAncestors),
121 "leading compound absent from the supplied ancestor chain -- no longer matches (the actual bug)");
122
123 check(CSSStyleSheet::approximateSelectorMatch(".header .frame", "div", classes, "",
124 static_cast<const std::vector<AncestorFrame>*>(nullptr)),
125 "sanity: explicit nullptr behaves the same as the default (still matches, unverified)");
126
127 std::vector<AncestorFrame> outOfOrderAncestors = {
128 {"div", {"b"}, ""},
129 {"div", {"a"}, ""},
130 };
131 check(!CSSStyleSheet::approximateSelectorMatch(".a .b .frame", "div", classes, "", &outOfOrderAncestors),
132 "ancestor compounds must be found in left-to-right order, not just anywhere in the chain");
133
134 std::vector<AncestorFrame> inOrderAncestors = {
135 {"div", {"a"}, ""},
136 {"div", {"b"}, ""},
137 };
138 check(CSSStyleSheet::approximateSelectorMatch(".a .b .frame", "div", classes, "", &inOrderAncestors),
139 "same two ancestors in the correct left-to-right order do match");
140
141 std::vector<AncestorFrame> onlyOuterAncestor = {
142 {"div", {"a"}, ""},
143 };
144 check(!CSSStyleSheet::approximateSelectorMatch(".a .b .frame", "div", classes, "", &onlyOuterAncestor),
145 "a missing middle ancestor compound still fails the match");
146 }
147
148 // --- collectApproximateMatches: cascade priority ---
149 std::cout << "=== collectApproximateMatches cascade ===" << std::endl;
150 {
152 std::string css =
153 ".box { color: red; font-size: 12px; }\n"
154 ".box { color: blue; }\n";
155 CSSStyleSheet sheet;
156 sheet.parse(css);
157
158 std::map<std::string,std::string> props;
159 props["color"] = "inline-value"; // simulates an inline style="" already present
160 std::string mediaRules;
161 std::set<std::string> seen;
162 sheet.collectApproximateMatches("div", "box", "hero", props, mediaRules, seen);
163
164 check(props["color"] == "inline-value",
165 "inline value beats a later plain (non-important) rule");
166 check(props["font-size"] == "12px", "non-conflicting property still merged in");
167 }
168 {
170 std::string css = "#hero { color: green !important; }\n";
171 CSSStyleSheet sheet;
172 sheet.parse(css);
173
174 std::map<std::string,std::string> props;
175 props["color"] = "inline-value";
176 std::string mediaRules;
177 std::set<std::string> seen;
178 sheet.collectApproximateMatches("div", "box", "hero", props, mediaRules, seen);
179
180 check(props["color"] == "green",
181 "an !important rule overrides even an inline-set value (matches real cascade precedence)");
182 }
183 {
185 std::string css =
186 ".box { color: red; }\n"
187 ".box { color: blue; }\n";
188 CSSStyleSheet sheet;
189 sheet.parse(css);
190
191 std::map<std::string,std::string> props; // no inline style
192 std::string mediaRules;
193 std::set<std::string> seen;
194 sheet.collectApproximateMatches("div", "box", "", props, mediaRules, seen);
195
196 check(props["color"] == "blue", "later plain rule wins over an earlier plain rule");
197 }
198 {
200 std::string css =
201 ".box { color: red !important; }\n"
202 ".box { color: blue; }\n";
203 CSSStyleSheet sheet;
204 sheet.parse(css);
205
206 std::map<std::string,std::string> props;
207 std::string mediaRules;
208 std::set<std::string> seen;
209 sheet.collectApproximateMatches("div", "box", "", props, mediaRules, seen);
210
211 check(props["color"] == "red", "!important beats a later plain rule");
212 }
213
214 // --- @media dedup ---
215 std::cout << "=== collectApproximateMatches @media dedup ===" << std::endl;
216 {
218 std::string css = "@media (max-width: 600px) { .box { color: red; } }";
219 CSSStyleSheet sheet;
220 sheet.parse(css);
221
222 std::set<std::string> seen;
223 for (int i = 0; i < 3; ++i) {
224 std::map<std::string,std::string> props;
225 std::string mediaRules;
226 sheet.collectApproximateMatches("div", "box", "", props, mediaRules, seen);
227 check((i == 0) == !mediaRules.empty(),
228 "media block text only appended the first time it's seen");
229 }
230 check(seen.size() == 1, "seenMediaBlocks accumulated exactly one block");
231 }
232
233 // --- collectStyleBlocks + getCSSRules end-to-end ---
234 std::cout << "=== collectStyleBlocks + getCSSRules ===" << std::endl;
235 {
236 std::string html =
237 "<html><head>"
238 "<style>.card { color: red; } #hero { font-size: 20px; }</style>"
239 "</head><body>"
240 "<div id=\"hero\" class=\"card\" style=\"margin: 0\">hi</div>"
241 "</body></html>";
242
243 libhtmlpp::HtmlString htmlString(html);
244 libhtmlpp::Element &root = htmlString.parse();
245
248 check(sheet.getRuleCount() == 2, "collected both <style> block rules");
249
250 libhtmlpp::HtmlElement *target = static_cast<libhtmlpp::HtmlElement*>(&root)->getElementbyID("hero");
251 check(target != nullptr, "found target element by id");
252
253 if (target) {
254 std::set<std::string> seen;
255 auto result = libhtmlpp::getCSSRules(*target, sheet, seen);
256 check(result.properties["color"] == "red", "getCSSRules folds in matching <style> block rule by class");
257 check(result.properties["font-size"] == "20px", "getCSSRules folds in matching <style> block rule by id");
258 check(result.properties["margin"] == "0", "getCSSRules includes the element's own inline style");
259 }
260 }
261
262 // --- getCSSRules with an ancestor chain: the real bug, end-to-end ---
263 std::cout << "=== getCSSRules with ancestor chain ===" << std::endl;
264 {
266 std::string html =
267 "<html><head>"
268 "<style>"
269 ".frame { padding-top: 100px; }"
270 ".header .frame { padding-top: 0; }"
271 "</style>"
272 "</head><body>"
273 "<div class=\"header\"><div id=\"headerFrame\" class=\"frame\">hi</div></div>"
274 "<div class=\"hero\"><div id=\"heroFrame\" class=\"frame\">hi</div></div>"
275 "</body></html>";
276
277 libhtmlpp::HtmlString htmlString(html);
278 libhtmlpp::Element &root = htmlString.parse();
279
282
283 libhtmlpp::HtmlElement *headerFrame =
284 static_cast<libhtmlpp::HtmlElement*>(&root)->getElementbyID("headerFrame");
285 libhtmlpp::HtmlElement *heroFrame =
286 static_cast<libhtmlpp::HtmlElement*>(&root)->getElementbyID("heroFrame");
287 check(headerFrame != nullptr && heroFrame != nullptr, "found both target elements by id");
288
289 if (headerFrame && heroFrame) {
290 std::set<std::string> seen;
291 auto noAncestorsResult = libhtmlpp::getCSSRules(*heroFrame, sheet, seen);
292 check(noAncestorsResult.properties["padding-top"] == "0",
293 "without an ancestor chain, the header-only rule still bleeds onto an unrelated "
294 "element -- this is the pre-fix behavior, unchanged for callers that pass none");
295
296 std::vector<AncestorFrame> headerAncestors = {{"div", {"header"}, ""}};
297 std::set<std::string> seen2;
298 auto headerResult = libhtmlpp::getCSSRules(*headerFrame, sheet, seen2, &headerAncestors);
299 check(headerResult.properties["padding-top"] == "0",
300 "with its real ancestor chain, the header's own frame still gets the header-only rule");
301
302 std::vector<AncestorFrame> heroAncestors = {{"div", {"hero"}, ""}};
303 std::set<std::string> seen3;
304 auto heroResult = libhtmlpp::getCSSRules(*heroFrame, sheet, seen3, &heroAncestors);
305 check(heroResult.properties["padding-top"] == "100px",
306 "with its real (non-header) ancestor chain, the hero's frame keeps the general "
307 "rule instead of the header-only one -- the actual bug fix");
308 }
309 }
310
311 // --- resolveCSSVariables ---
312 std::cout << "=== resolveCSSVariables ===" << std::endl;
313 {
314 std::map<std::string,std::string> props = {{"--color-bg", "0,0,0"}, {"--alpha-bg", "1"}};
315 check(libhtmlpp::resolveCSSVariables("rgba(var(--color-bg),var(--alpha-bg))", props) ==
316 "rgba(0,0,0,1)",
317 "simple substitution, multiple var() in one value");
318 }
319 {
320 std::map<std::string,std::string> props;
321 check(libhtmlpp::resolveCSSVariables("var(--undefined, blue)", props) == "blue",
322 "fallback used when the referenced name is unset");
323 }
324 {
325 std::map<std::string,std::string> props;
326 check(libhtmlpp::resolveCSSVariables("var(--undefined)", props) == "",
327 "no fallback and unset name resolves to empty (invalid, per spec)");
328 }
329 {
330 // Mirrors the real-world pattern: --gradient-bg's OWN stored value
331 // references --color-bg/--alpha-bg (multi-hop resolution).
332 std::map<std::string,std::string> props = {
333 {"--color-bg", "10,20,30"},
334 {"--alpha-bg", "0.5"},
335 {"--gradient-bg", "rgba(var(--color-bg),var(--alpha-bg))"},
336 };
337 check(libhtmlpp::resolveCSSVariables("var(--gradient-bg)", props) == "rgba(10,20,30,0.5)",
338 "multi-hop resolution through an intermediate custom property");
339 }
340 {
341 // Fallback itself contains a nested var() -- also resolved.
342 std::map<std::string,std::string> props = {{"--color-bg", "0,0,0"}};
344 "var(--gradient-bg, rgba(var(--color-bg),1))", props) == "rgba(0,0,0,1)",
345 "nested var() inside a fallback value is resolved too");
346 }
347 {
348 // a -> var(b), b -> var(a): cyclic, must not infinite-loop.
349 std::map<std::string,std::string> props = {{"--a", "var(--b)"}, {"--b", "var(--a)"}};
350 check(libhtmlpp::resolveCSSVariables("var(--a)", props) == "",
351 "cyclic custom-property reference resolves to empty instead of looping forever");
352 }
353 {
354 check(libhtmlpp::resolveCSSVariables("10px solid red", {}) == "10px solid red",
355 "a value with no var( at all is returned unchanged");
356 }
357
358 // --- Summary ---
359 std::cout << "\n=== " << passCount << "/" << testCount << " tests passed ===" << std::endl;
360
361 return (passCount == testCount) ? 0 : -1;
362}
void parse(const std::string &input)
Definition css.cpp:797
size_t getRuleCount() const
Definition css.cpp:920
Abstract base class for all nodes in the HTML tree.
Definition html.h:76
libhtmlpp::Element & parse()
Parses the current buffer into a DOM-like tree and returns the root element.
Definition html.cpp:379
#define Green
#define NOCOLOR
int main()
#define Red
std::string resolveCSSVariables(const std::string &value, const std::map< std::string, std::string > &customProperties, bool *unresolved=nullptr)
Resolves every var(--name) / var(--name, fallback) reference in value using customProperties (custom-...
Definition css.cpp:1243
void collectStyleBlocks(CSSStyleSheet &sheet, Element &root)
Finds every <style> element anywhere in the subtree rooted at root (document order) and appends each ...
Definition htmlcss.cpp:67
CSSRuleResult getCSSRules(HtmlElement &target, const CSSStyleSheet &sheet, std::set< std::string > &seenMediaBlocks, const std::vector< AncestorFrame > *ancestors=nullptr)
Returns every CSS rule that applies to target: its own inline style="..." attribute,...
Definition htmlcss.cpp:116
One ancestor of the element being matched, for the optional ancestor chain approximateSelectorMatch/c...
Definition css.h:125