libhtmlpp 1.0.0
Loading...
Searching...
No Matches
css.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 "css.h"
29#include "exception.h"
30
31#include <algorithm>
32#include <sstream>
33
34namespace {
35
36 bool isWhitespace(char c) {
37 return c == ' ' || c == '\t' || c == '\n' || c == '\r';
38 }
39
40 std::string trim(const std::string &s) {
41 size_t start = 0;
42 while (start < s.size() && isWhitespace(s[start])) ++start;
43 size_t end = s.size();
44 while (end > start && isWhitespace(s[end - 1])) --end;
45 return s.substr(start, end - start);
46 }
47
48 // Strips a trailing "!important" (case-insensitive, any whitespace
49 // before the "!") from a CSS declaration value, returning whether it was
50 // present. CSSDeclaration's parser captures "!important" as ordinary
51 // trailing text with no special handling, so a caller comparing/storing
52 // plain property values needs this to see past it.
53 bool stripImportant(std::string &value) {
54 size_t bang = value.rfind('!');
55 if (bang == std::string::npos) return false;
56
57 size_t start = value.find_first_not_of(" \t\n\r", bang + 1);
58 size_t end = value.find_last_not_of(" \t\n\r");
59 if (start == std::string::npos || end == std::string::npos || start > end) return false;
60
61 std::string tail = value.substr(start, end - start + 1);
62 std::transform(tail.begin(), tail.end(), tail.begin(),
63 [](unsigned char c) { return std::tolower(c); });
64 if (tail != "important") return false;
65
66 size_t valEnd = bang;
67 while (valEnd > 0 && isWhitespace(value[valEnd - 1])) --valEnd;
68 value.resize(valEnd);
69 return true;
70 }
71
72 // A combinator-stripped compound selector like "div.card.featured#hero"
73 // broken into its optional bare tag (lowercased), every ".class" token,
74 // and an optional "#id" token.
76 std::string tag;
77 std::vector<std::string> classes;
78 std::string id;
79 };
80
81 CompoundParts parseCompoundSelector(const std::string &matchSel) {
82 CompoundParts result;
83 size_t pos = 0;
84 while (pos < matchSel.size()) {
85 char c = matchSel[pos];
86 if (c == '.' || c == '#') {
87 size_t next = matchSel.find_first_of(".#", pos + 1);
88 size_t tokenEnd = (next == std::string::npos) ? matchSel.size() : next;
89 std::string token = matchSel.substr(pos + 1, tokenEnd - (pos + 1));
90 if (c == '.') result.classes.push_back(token);
91 else result.id = token;
92 pos = tokenEnd;
93 } else {
94 size_t next = matchSel.find_first_of(".#", pos);
95 size_t tokenEnd = (next == std::string::npos) ? matchSel.size() : next;
96 result.tag = matchSel.substr(pos, tokenEnd - pos);
97 pos = tokenEnd;
98 }
99 }
100 std::transform(result.tag.begin(), result.tag.end(), result.tag.begin(),
101 [](unsigned char c) { return std::tolower(c); });
102 return result;
103 }
104
105 // A selector matches only if EVERY part it specifies is present on this
106 // element -- in particular every class listed, not just one. A bare tag
107 // left over after stripping a combinator's ancestor part (e.g.
108 // "[data-atom=header]>div" reduces to bare "div") can't be verified --
109 // this matcher only ever sees the CURRENT element's own tag/class/id,
110 // never its ancestor chain -- so treating it as a match would apply the
111 // rule to literally every element of that tag in the whole document
112 // (confirmed against a real scraped page where exactly this pattern
113 // matched ~24,000 times and blew up an HTML-import's output to 36MB). A
114 // class/id qualifier alongside the tag (e.g. "div.foo") is still
115 // specific enough to check.
116 bool compoundMatches(const CompoundParts &compound, bool hadCombinator,
117 const std::string &tag, const std::vector<std::string> &classes,
118 const std::string &id)
119 {
120 bool bareTagAfterCombinator = hadCombinator &&
121 compound.classes.empty() && compound.id.empty() && !compound.tag.empty();
122 bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() || !compound.id.empty();
123 if (bareTagAfterCombinator || !specifiedSomething) return false;
124
125 bool tagOk = compound.tag.empty() || compound.tag == tag;
126 bool idOk = compound.id.empty() || compound.id == id;
127 bool classesOk = true;
128 for (const auto &cls : compound.classes) {
129 if (std::find(classes.begin(), classes.end(), cls) == classes.end()) {
130 classesOk = false;
131 break;
132 }
133 }
134 return tagOk && idOk && classesOk;
135 }
136
137 // Strips a trailing combinator (space/">"/"+"/"~") from `singleSel`,
138 // returning the trailing compound selector text and whether a
139 // combinator was actually present. Only the trailing compound is ever
140 // checked -- this matcher never verifies ancestor/parent context.
141 std::string stripCombinator(const std::string &singleSel, bool &hadCombinator) {
142 std::string matchSel = singleSel;
143 hadCombinator = false;
144 size_t lastSep = matchSel.find_last_of(" >+~");
145 if (lastSep != std::string::npos) {
146 size_t mstart = matchSel.find_first_not_of(" >+~", lastSep);
147 if (mstart != std::string::npos) {
148 matchSel = matchSel.substr(mstart);
149 hadCombinator = true;
150 }
151 }
152 return matchSel;
153 }
154
155 // Selector syntax this matcher can't safely evaluate -- attribute
156 // selectors and pseudo-classes/elements describe context (an ancestor's
157 // attribute) or state (":hover") this matcher has no way to check, and
158 // "*" would blanket-match every element. Guessing wrong here is worse
159 // than not matching at all.
160 bool hasUnsupportedSelectorSyntax(const std::string &matchSel) {
161 return matchSel.find('[') != std::string::npos ||
162 matchSel.find(':') != std::string::npos ||
163 matchSel == "*";
164 }
165
166 // Finds the index of the ')' matching the '(' at `openPos` (which must
167 // point at '(' itself), respecting nested parens. Returns npos if
168 // unterminated.
169 size_t findMatchingParen(const std::string &s, size_t openPos) {
170 int depth = 0;
171 for (size_t i = openPos; i < s.size(); ++i) {
172 if (s[i] == '(') ++depth;
173 else if (s[i] == ')') {
174 --depth;
175 if (depth == 0) return i;
176 }
177 }
178 return std::string::npos;
179 }
180
181 // Splits `inner` (the text between "var(" and its matching ")") at the
182 // first top-level comma (depth 0, not inside a nested "(...)") into a
183 // trimmed custom-property name and a trimmed fallback ("" if no comma
184 // is present, i.e. no fallback was given).
185 void splitVarArgs(const std::string &inner, std::string &name, std::string &fallback) {
186 int depth = 0;
187 size_t commaPos = std::string::npos;
188 for (size_t i = 0; i < inner.size(); ++i) {
189 if (inner[i] == '(') ++depth;
190 else if (inner[i] == ')') --depth;
191 else if (inner[i] == ',' && depth == 0) { commaPos = i; break; }
192 }
193 std::string rawName = (commaPos == std::string::npos) ? inner : inner.substr(0, commaPos);
194 std::string rawFallback = (commaPos == std::string::npos) ? "" : inner.substr(commaPos + 1);
195 name = trim(rawName);
196 fallback = trim(rawFallback);
197 }
198
199 // Substitutes every var(...) occurrence in `value`, recursively --
200 // `resolving` (the set of custom-property names currently being
201 // expanded on this call stack) provides cycle detection: a name already
202 // in it resolves to "" instead of recursing forever.
203 std::string substituteVars(const std::string &value,
204 const std::map<std::string,std::string> &customProperties,
205 std::set<std::string> &resolving)
206 {
207 if (value.find("var(") == std::string::npos) return value;
208
209 std::string out;
210 out.reserve(value.size());
211 size_t pos = 0;
212 while (pos < value.size()) {
213 if (value.compare(pos, 4, "var(") == 0) {
214 size_t openParen = pos + 3;
215 size_t closeParen = findMatchingParen(value, openParen);
216 if (closeParen == std::string::npos) {
217 // Malformed/unterminated var( -- leave the rest as-is
218 // rather than risk corrupting it.
219 out += value.substr(pos);
220 break;
221 }
222 std::string inner = value.substr(openParen + 1, closeParen - openParen - 1);
223 std::string name, fallback;
224 splitVarArgs(inner, name, fallback);
225
226 std::string substitution;
227 if (resolving.count(name)) {
228 substitution = ""; // cyclic reference -- invalid per spec
229 } else {
230 auto it = customProperties.find(name);
231 if (it != customProperties.end()) {
232 resolving.insert(name);
233 substitution = substituteVars(it->second, customProperties, resolving);
234 resolving.erase(name);
235 } else if (!fallback.empty()) {
236 substitution = substituteVars(fallback, customProperties, resolving);
237 } else {
238 substitution = ""; // unresolvable, no fallback -- invalid per spec
239 }
240 }
241 out += substitution;
242 pos = closeParen + 1;
243 } else {
244 out += value[pos];
245 ++pos;
246 }
247 }
248 return out;
249 }
250
251}
252
253// --- CSSProperty ---
254
256
257libhtmlpp::CSSProperty::CSSProperty(const std::string &name, const std::string &value)
258 : _Name(name), _Value(value) {}
259
261 : _Name(prop._Name), _Value(prop._Value) {}
262
264
266 if (this != &prop) {
267 _Name = prop._Name;
268 _Value = prop._Value;
269 }
270 return *this;
271}
272
273const std::string& libhtmlpp::CSSProperty::getName() const { return _Name; }
274void libhtmlpp::CSSProperty::setName(const std::string &name) { _Name = name; }
275
276const std::string& libhtmlpp::CSSProperty::getValue() const { return _Value; }
277void libhtmlpp::CSSProperty::setValue(const std::string &value) { _Value = value; }
278
279// --- CSSDeclaration ---
280
282
284 : _Properties(decl._Properties) {}
285
287
289 if (this != &decl) {
290 _Properties = decl._Properties;
291 }
292 return *this;
293}
294
295void libhtmlpp::CSSDeclaration::addProperty(const std::string &name, const std::string &value) {
296 std::string tname = trim(name);
297 std::string tvalue = trim(value);
298
299 if (tname.empty()) return;
300
301 for (auto &prop : _Properties) {
302 if (prop.getName() == tname) {
303 prop.setValue(tvalue);
304 return;
305 }
306 }
307 _Properties.emplace_back(tname, tvalue);
308}
309
310void libhtmlpp::CSSDeclaration::removeProperty(const std::string &name) {
311 std::string tname = trim(name);
312 _Properties.erase(
313 std::remove_if(_Properties.begin(), _Properties.end(),
314 [&tname](const CSSProperty &p) { return p.getName() == tname; }),
315 _Properties.end()
316 );
317}
318
320 std::string tname = trim(name);
321 for (const auto &prop : _Properties) {
322 if (prop.getName() == tname) return &prop;
323 }
324 return nullptr;
325}
326
327const std::vector<libhtmlpp::CSSProperty>& libhtmlpp::CSSDeclaration::getProperties() const {
328 return _Properties;
329}
330
332 std::string result;
333 for (size_t i = 0; i < _Properties.size(); ++i) {
334 result += _Properties[i].getName();
335 result += ": ";
336 result += _Properties[i].getValue();
337 result += ";";
338 if (i + 1 < _Properties.size()) result += " ";
339 }
340 return result;
341}
342
343void libhtmlpp::CSSDeclaration::parse(const std::string &input) {
344 _Properties.clear();
345
346 size_t pos = 0;
347 size_t len = input.size();
348
349 while (pos < len) {
350 // Skip whitespace
351 while (pos < len && isWhitespace(input[pos])) ++pos;
352 if (pos >= len) break;
353
354 // Find the colon separating property name from value
355 size_t colon = input.find(':', pos);
356 if (colon == std::string::npos) break;
357
358 std::string propName = trim(input.substr(pos, colon - pos));
359
360 pos = colon + 1;
361
362 // Find the semicolon ending this declaration
363 // Handle parentheses for functions like rgb(), url()
364 size_t valueStart = pos;
365 int parenDepth = 0;
366 bool inSingleQuote = false;
367 bool inDoubleQuote = false;
368
369 while (pos < len) {
370 char c = input[pos];
371
372 if (inSingleQuote) {
373 if (c == '\'') inSingleQuote = false;
374 ++pos;
375 continue;
376 }
377 if (inDoubleQuote) {
378 if (c == '"') inDoubleQuote = false;
379 ++pos;
380 continue;
381 }
382
383 if (c == '\'') { inSingleQuote = true; ++pos; continue; }
384 if (c == '"') { inDoubleQuote = true; ++pos; continue; }
385 if (c == '(') { ++parenDepth; ++pos; continue; }
386 if (c == ')') { if (parenDepth > 0) --parenDepth; ++pos; continue; }
387
388 if (c == ';' && parenDepth == 0) {
389 break;
390 }
391 ++pos;
392 }
393
394 std::string propValue = trim(input.substr(valueStart, pos - valueStart));
395
396 if (!propName.empty()) {
397 addProperty(propName, propValue);
398 }
399
400 if (pos < len && input[pos] == ';') ++pos;
401 }
402}
403
405 _Properties.clear();
406}
407
408// --- CSSRule ---
409
411
412libhtmlpp::CSSRule::CSSRule(const std::string &selector)
413 : _Selector(trim(selector)) {}
414
416 : _Selector(rule._Selector), _Declaration(rule._Declaration) {}
417
419
421 if (this != &rule) {
422 _Selector = rule._Selector;
423 _Declaration = rule._Declaration;
424 }
425 return *this;
426}
427
428const std::string& libhtmlpp::CSSRule::getSelector() const { return _Selector; }
429void libhtmlpp::CSSRule::setSelector(const std::string &selector) { _Selector = trim(selector); }
430
433
434std::string libhtmlpp::CSSRule::serialize(bool formatted) const {
435 std::string result;
436 if (formatted) {
437 result += _Selector + " {\n";
438 for (const auto &prop : _Declaration.getProperties()) {
439 result += " " + prop.getName() + ": " + prop.getValue() + ";\n";
440 }
441 result += "}";
442 } else {
443 result += _Selector + "{";
444 result += _Declaration.serialize();
445 result += "}";
446 }
447 return result;
448}
449
450// --- CSSStyleSheet ---
451
453
455 : _Rules(sheet._Rules) {}
456
458
460 if (this != &sheet) {
461 _Rules = sheet._Rules;
462 }
463 return *this;
464}
465
466void libhtmlpp::CSSStyleSheet::_skipWhitespace(const std::string &input, size_t &pos) const {
467 while (pos < input.size() && isWhitespace(input[pos])) ++pos;
468}
469
470void libhtmlpp::CSSStyleSheet::_skipComment(const std::string &input, size_t &pos) const {
471 if (pos + 1 < input.size() && input[pos] == '/' && input[pos + 1] == '*') {
472 pos += 2;
473 while (pos + 1 < input.size()) {
474 if (input[pos] == '*' && input[pos + 1] == '/') {
475 pos += 2;
476 return;
477 }
478 ++pos;
479 }
480 pos = input.size();
481 }
482}
483
484void libhtmlpp::CSSStyleSheet::parse(const std::string &input) {
485 _Rules.clear();
486
487 size_t pos = 0;
488 size_t len = input.size();
489
490 while (pos < len) {
491 _skipWhitespace(input, pos);
492 if (pos >= len) break;
493
494 // Skip comments
495 if (pos + 1 < len && input[pos] == '/' && input[pos + 1] == '*') {
496 _skipComment(input, pos);
497 continue;
498 }
499
500 // Handle @-rules
501 if (input[pos] == '@') {
502 size_t atStart = pos;
503
504 // Check for @-rules with blocks like @media, @keyframes, @supports, @font-face
505 // and simple @-rules like @import, @charset
506 size_t semiPos = input.find(';', pos);
507 size_t bracePos = input.find('{', pos);
508
509 if (bracePos != std::string::npos && (semiPos == std::string::npos || bracePos < semiPos)) {
510 // Block @-rule: find matching closing brace
511 std::string atSelector = trim(input.substr(atStart, bracePos - atStart));
512 pos = bracePos + 1;
513
514 int depth = 1;
515 size_t blockStart = pos;
516 while (pos < len && depth > 0) {
517 if (input[pos] == '{') ++depth;
518 else if (input[pos] == '}') --depth;
519 if (depth > 0) ++pos;
520 }
521
522 std::string blockContent = input.substr(blockStart, pos - blockStart);
523 pos = (pos < len) ? pos + 1 : pos;
524
525 // Parse nested rules inside the @-block
526 CSSStyleSheet nested;
527 nested.parse(blockContent);
528
529 // Wrap each nested rule with the @-selector
530 for (const auto &nestedRule : nested.getRules()) {
531 CSSRule rule;
532 rule.setSelector(atSelector + " " + nestedRule.getSelector());
533 for (const auto &prop : nestedRule.getDeclaration().getProperties()) {
534 rule.getDeclaration().addProperty(prop.getName(), prop.getValue());
535 }
536 _Rules.push_back(rule);
537 }
538
539 // If no nested rules (e.g. @font-face), store as single rule
540 if (nested.getRuleCount() == 0 && !blockContent.empty()) {
541 CSSRule rule(atSelector);
542 rule.getDeclaration().parse(blockContent);
543 _Rules.push_back(rule);
544 }
545 } else if (semiPos != std::string::npos) {
546 // Simple @-rule ending with semicolon (e.g. @import, @charset)
547 std::string atRule = trim(input.substr(atStart, semiPos - atStart));
548 CSSRule rule(atRule);
549 _Rules.push_back(rule);
550 pos = semiPos + 1;
551 } else {
552 // Malformed @-rule, skip
553 ++pos;
554 }
555 continue;
556 }
557
558 // Standard rule: find selector then { declarations }
559 size_t braceOpen = input.find('{', pos);
560 if (braceOpen == std::string::npos) break;
561
562 std::string selector = trim(input.substr(pos, braceOpen - pos));
563 pos = braceOpen + 1;
564
565 // Find matching closing brace
566 int depth = 1;
567 size_t declStart = pos;
568 while (pos < len && depth > 0) {
569 if (pos + 1 < len && input[pos] == '/' && input[pos + 1] == '*') {
570 _skipComment(input, pos);
571 continue;
572 }
573 if (input[pos] == '{') ++depth;
574 else if (input[pos] == '}') --depth;
575 if (depth > 0) ++pos;
576 }
577
578 std::string declarations = input.substr(declStart, pos - declStart);
579 pos = (pos < len) ? pos + 1 : pos;
580
581 if (!selector.empty()) {
582 CSSRule rule(selector);
583 rule.getDeclaration().parse(declarations);
584 _Rules.push_back(rule);
585 }
586 }
587}
588
590 _Rules.push_back(rule);
591}
592
594 if (index < _Rules.size()) {
595 _Rules.erase(_Rules.begin() + static_cast<std::ptrdiff_t>(index));
596 }
597}
598
600 if (index < _Rules.size()) return &_Rules[index];
601 return nullptr;
602}
603
605 return _Rules.size();
606}
607
608const std::vector<libhtmlpp::CSSRule>& libhtmlpp::CSSStyleSheet::getRules() const {
609 return _Rules;
610}
611
612std::string libhtmlpp::CSSStyleSheet::serialize(bool formatted) const {
613 std::string result;
614 for (size_t i = 0; i < _Rules.size(); ++i) {
615 result += _Rules[i].serialize(formatted);
616 if (formatted) result += "\n";
617 if (i + 1 < _Rules.size() && formatted) result += "\n";
618 }
619 return result;
620}
621
623 _Rules.clear();
624}
625
627 CSSDeclaration decl;
628 decl.parse(style);
629 return decl;
630}
631
633 const std::string &tag,
634 const std::vector<std::string> &classes,
635 const std::string &id)
636{
637 std::string tagLower = tag;
638 std::transform(tagLower.begin(), tagLower.end(), tagLower.begin(),
639 [](unsigned char c) { return std::tolower(c); });
640
641 std::istringstream selStream(selector);
642 std::string singleSel;
643 while (std::getline(selStream, singleSel, ',')) {
644 size_t start = singleSel.find_first_not_of(" \t\n\r");
645 if (start == std::string::npos) continue;
646 size_t end = singleSel.find_last_not_of(" \t\n\r");
647 singleSel = singleSel.substr(start, end - start + 1);
648
649 bool hadCombinator = false;
650 std::string matchSel = stripCombinator(singleSel, hadCombinator);
651 if (hasUnsupportedSelectorSyntax(matchSel)) continue;
652
653 CompoundParts compound = parseCompoundSelector(matchSel);
654 if (compoundMatches(compound, hadCombinator, tagLower, classes, id)) return true;
655 }
656 return false;
657}
658
660 const std::string &tag,
661 const std::string &cssClass,
662 const std::string &id,
663 std::map<std::string,std::string> &props,
664 std::string &mediaRules,
665 std::set<std::string> &seenMediaBlocks) const
666{
667 std::vector<std::string> classes;
668 if (!cssClass.empty()) {
669 std::istringstream iss(cssClass);
670 std::string cls;
671 while (iss >> cls) classes.push_back(cls);
672 }
673
674 // Property names already present in @p props when we're called (e.g.
675 // the element's own inline style, set by the caller before calling this)
676 // outrank a plain stylesheet rule, unless that rule is "!important" --
677 // matching real cascade precedence.
678 std::set<std::string> inlineKeys;
679 for (const auto &kv : props) inlineKeys.insert(kv.first);
680 std::set<std::string> importantKeys;
681
682 for (const auto &rule : _Rules) {
683 const std::string &sel = rule.getSelector();
684 if (sel.empty()) continue;
685
686 bool isAtRule = sel[0] == '@';
687 std::string atWrapper;
688 std::string innerSel;
689
690 if (isAtRule) {
691 size_t parenDepth = 0;
692 size_t splitPos = std::string::npos;
693 for (size_t i = 0; i < sel.size(); ++i) {
694 if (sel[i] == '(') ++parenDepth;
695 else if (sel[i] == ')') {
696 if (parenDepth > 0) --parenDepth;
697 if (parenDepth == 0) { splitPos = i + 1; break; }
698 }
699 }
700 if (splitPos != std::string::npos && splitPos < sel.size()) {
701 atWrapper = sel.substr(0, splitPos);
702 size_t innerStart = sel.find_first_not_of(" \t\n\r", splitPos);
703 if (innerStart != std::string::npos) innerSel = sel.substr(innerStart);
704 }
705 if (innerSel.empty()) continue;
706 }
707
708 const std::string &matchTarget = isAtRule ? innerSel : sel;
709
710 std::istringstream selStream(matchTarget);
711 std::string singleSel;
712 while (std::getline(selStream, singleSel, ',')) {
713 size_t start = singleSel.find_first_not_of(" \t\n\r");
714 if (start == std::string::npos) continue;
715 size_t end = singleSel.find_last_not_of(" \t\n\r");
716 singleSel = singleSel.substr(start, end - start + 1);
717
718 if (!approximateSelectorMatch(singleSel, tag, classes, id)) continue;
719
720 if (isAtRule) {
721 // The same @media block, once present anywhere in the final
722 // output, applies document-wide regardless of which element
723 // it's attached to -- so including it more than once across
724 // many matching elements is pure bloat, not a correctness
725 // requirement.
726 std::string block = atWrapper + " { " + singleSel + " { ";
727 for (const auto &prop : rule.getDeclaration().getProperties()) {
728 block += prop.getName() + ": " + prop.getValue() + "; ";
729 }
730 block += "} } ";
731 if (seenMediaBlocks.insert(block).second) {
732 mediaRules += block;
733 }
734 } else {
735 for (const auto &prop : rule.getDeclaration().getProperties()) {
736 std::string value = prop.getValue();
737 bool isImportant = stripImportant(value);
738 const std::string &key = prop.getName();
739
740 // Inline style and an already-!important value both
741 // outrank a later plain rule; a later rule of equal
742 // priority (both plain, or both !important) wins --
743 // approximating the real cascade's "last rule of
744 // equal-or-higher precedence wins" without computing
745 // full selector specificity.
746 if (inlineKeys.count(key) && !isImportant) continue;
747 if (importantKeys.count(key) && !isImportant) continue;
748
749 props[key] = value;
750 if (isImportant) importantKeys.insert(key);
751 }
752 }
753 }
754 }
755}
756
757std::string libhtmlpp::resolveCSSVariables(const std::string &value,
758 const std::map<std::string,std::string> &customProperties)
759{
760 std::set<std::string> resolving;
761 return substituteVars(value, customProperties, resolving);
762}
763
void addProperty(const std::string &name, const std::string &value)
Definition css.cpp:295
void removeProperty(const std::string &name)
Definition css.cpp:310
const std::vector< CSSProperty > & getProperties() const
Definition css.cpp:327
std::string serialize() const
Definition css.cpp:331
const CSSProperty * getProperty(const std::string &name) const
Definition css.cpp:319
void parse(const std::string &input)
Definition css.cpp:343
CSSDeclaration & operator=(const CSSDeclaration &decl)
Definition css.cpp:288
void setName(const std::string &name)
Definition css.cpp:274
const std::string & getName() const
Definition css.cpp:273
void setValue(const std::string &value)
Definition css.cpp:277
CSSProperty & operator=(const CSSProperty &prop)
Definition css.cpp:265
const std::string & getValue() const
Definition css.cpp:276
CSSDeclaration & getDeclaration()
Definition css.cpp:431
std::string serialize(bool formatted=false) const
Definition css.cpp:434
void setSelector(const std::string &selector)
Definition css.cpp:429
CSSRule & operator=(const CSSRule &rule)
Definition css.cpp:420
const std::string & getSelector() const
Definition css.cpp:428
void parse(const std::string &input)
Definition css.cpp:484
static bool approximateSelectorMatch(const std::string &selector, const std::string &tag, const std::vector< std::string > &classes, const std::string &id)
Conservative, NOT spec-complete selector match: selector (a single selector, or a comma-separated lis...
Definition css.cpp:632
void collectApproximateMatches(const std::string &tag, const std::string &cssClass, const std::string &id, std::map< std::string, std::string > &props, std::string &mediaRules, std::set< std::string > &seenMediaBlocks) const
Runs every rule in this sheet through approximateSelectorMatch against the element identified by tag/...
Definition css.cpp:659
const std::vector< CSSRule > & getRules() const
Definition css.cpp:608
CSSStyleSheet & operator=(const CSSStyleSheet &sheet)
Definition css.cpp:459
static CSSDeclaration parseInlineStyle(const std::string &style)
Definition css.cpp:626
std::string serialize(bool formatted=false) const
Definition css.cpp:612
const CSSRule * getRule(size_t index) const
Definition css.cpp:599
void addRule(const CSSRule &rule)
Definition css.cpp:589
void removeRule(size_t index)
Definition css.cpp:593
size_t getRuleCount() const
Definition css.cpp:604
size_t findMatchingParen(const std::string &s, size_t openPos)
Definition css.cpp:169
bool compoundMatches(const CompoundParts &compound, bool hadCombinator, const std::string &tag, const std::vector< std::string > &classes, const std::string &id)
Definition css.cpp:116
bool isWhitespace(char c)
Definition css.cpp:36
std::string substituteVars(const std::string &value, const std::map< std::string, std::string > &customProperties, std::set< std::string > &resolving)
Definition css.cpp:203
void splitVarArgs(const std::string &inner, std::string &name, std::string &fallback)
Definition css.cpp:185
std::string stripCombinator(const std::string &singleSel, bool &hadCombinator)
Definition css.cpp:141
CompoundParts parseCompoundSelector(const std::string &matchSel)
Definition css.cpp:81
bool stripImportant(std::string &value)
Definition css.cpp:53
std::string trim(const std::string &s)
Definition css.cpp:40
bool hasUnsupportedSelectorSyntax(const std::string &matchSel)
Definition css.cpp:160
std::string resolveCSSVariables(const std::string &value, const std::map< std::string, std::string > &customProperties)
Resolves every var(--name) / var(--name, fallback) reference in value using customProperties (custom-...
Definition css.cpp:757
std::vector< std::string > classes
Definition css.cpp:77