37 return c ==
' ' || c ==
'\t' || c ==
'\n' || c ==
'\r';
40 std::string
trim(
const std::string &s) {
42 while (start < s.size() &&
isWhitespace(s[start])) ++start;
43 size_t end = s.size();
45 return s.substr(start, end - start);
54 size_t bang = value.rfind(
'!');
55 if (bang == std::string::npos)
return false;
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;
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;
67 while (valEnd > 0 &&
isWhitespace(value[valEnd - 1])) --valEnd;
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;
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);
100 std::transform(result.
tag.begin(), result.
tag.end(), result.
tag.begin(),
101 [](
unsigned char c) { return std::tolower(c); });
117 const std::string &tag,
const std::vector<std::string> &classes,
118 const std::string &
id)
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;
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()) {
134 return tagOk && idOk && classesOk;
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;
161 return matchSel.find(
'[') != std::string::npos ||
162 matchSel.find(
':') != std::string::npos ||
171 for (
size_t i = openPos; i < s.size(); ++i) {
172 if (s[i] ==
'(') ++depth;
173 else if (s[i] ==
')') {
175 if (depth == 0)
return i;
178 return std::string::npos;
185 void splitVarArgs(
const std::string &inner, std::string &name, std::string &fallback) {
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; }
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);
204 const std::map<std::string,std::string> &customProperties,
205 std::set<std::string> &resolving)
207 if (value.find(
"var(") == std::string::npos)
return value;
210 out.reserve(value.size());
212 while (pos < value.size()) {
213 if (value.compare(pos, 4,
"var(") == 0) {
214 size_t openParen = pos + 3;
216 if (closeParen == std::string::npos) {
219 out += value.substr(pos);
222 std::string inner = value.substr(openParen + 1, closeParen - openParen - 1);
223 std::string name, fallback;
226 std::string substitution;
227 if (resolving.count(name)) {
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);
242 pos = closeParen + 1;
258 : _Name(name), _Value(value) {}
261 : _Name(prop._Name), _Value(prop._Value) {}
268 _Value = prop._Value;
284 : _Properties(decl._Properties) {}
290 _Properties = decl._Properties;
296 std::string tname = trim(name);
297 std::string tvalue = trim(value);
299 if (tname.empty())
return;
301 for (
auto &prop : _Properties) {
302 if (prop.getName() == tname) {
303 prop.setValue(tvalue);
307 _Properties.emplace_back(tname, tvalue);
311 std::string tname = trim(name);
313 std::remove_if(_Properties.begin(), _Properties.end(),
314 [&tname](
const CSSProperty &p) { return p.getName() == tname; }),
320 std::string tname = trim(name);
321 for (
const auto &prop : _Properties) {
322 if (prop.getName() == tname)
return ∝
333 for (
size_t i = 0; i < _Properties.size(); ++i) {
334 result += _Properties[i].getName();
336 result += _Properties[i].getValue();
338 if (i + 1 < _Properties.size()) result +=
" ";
347 size_t len = input.size();
351 while (pos < len && isWhitespace(input[pos])) ++pos;
352 if (pos >= len)
break;
355 size_t colon = input.find(
':', pos);
356 if (colon == std::string::npos)
break;
358 std::string propName = trim(input.substr(pos, colon - pos));
364 size_t valueStart = pos;
366 bool inSingleQuote =
false;
367 bool inDoubleQuote =
false;
373 if (c ==
'\'') inSingleQuote =
false;
378 if (c ==
'"') inDoubleQuote =
false;
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; }
388 if (c ==
';' && parenDepth == 0) {
394 std::string propValue = trim(input.substr(valueStart, pos - valueStart));
396 if (!propName.empty()) {
397 addProperty(propName, propValue);
400 if (pos < len && input[pos] ==
';') ++pos;
413 : _Selector(trim(selector)) {}
416 : _Selector(rule._Selector), _Declaration(rule._Declaration) {}
422 _Selector = rule._Selector;
423 _Declaration = rule._Declaration;
437 result += _Selector +
" {\n";
438 for (
const auto &prop : _Declaration.getProperties()) {
439 result +=
" " + prop.getName() +
": " + prop.getValue() +
";\n";
443 result += _Selector +
"{";
455 : _Rules(sheet._Rules) {}
460 if (
this != &sheet) {
461 _Rules = sheet._Rules;
466void libhtmlpp::CSSStyleSheet::_skipWhitespace(
const std::string &input,
size_t &pos)
const {
467 while (pos < input.size() && isWhitespace(input[pos])) ++pos;
470void libhtmlpp::CSSStyleSheet::_skipComment(
const std::string &input,
size_t &pos)
const {
471 if (pos + 1 < input.size() && input[pos] ==
'/' && input[pos + 1] ==
'*') {
473 while (pos + 1 < input.size()) {
474 if (input[pos] ==
'*' && input[pos + 1] ==
'/') {
488 size_t len = input.size();
491 _skipWhitespace(input, pos);
492 if (pos >= len)
break;
495 if (pos + 1 < len && input[pos] ==
'/' && input[pos + 1] ==
'*') {
496 _skipComment(input, pos);
501 if (input[pos] ==
'@') {
502 size_t atStart = pos;
506 size_t semiPos = input.find(
';', pos);
507 size_t bracePos = input.find(
'{', pos);
509 if (bracePos != std::string::npos && (semiPos == std::string::npos || bracePos < semiPos)) {
511 std::string atSelector = trim(input.substr(atStart, bracePos - atStart));
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;
522 std::string blockContent = input.substr(blockStart, pos - blockStart);
523 pos = (pos < len) ? pos + 1 : pos;
527 nested.
parse(blockContent);
530 for (
const auto &nestedRule : nested.
getRules()) {
532 rule.
setSelector(atSelector +
" " + nestedRule.getSelector());
533 for (
const auto &prop : nestedRule.getDeclaration().getProperties()) {
536 _Rules.push_back(rule);
540 if (nested.
getRuleCount() == 0 && !blockContent.empty()) {
543 _Rules.push_back(rule);
545 }
else if (semiPos != std::string::npos) {
547 std::string atRule = trim(input.substr(atStart, semiPos - atStart));
549 _Rules.push_back(rule);
559 size_t braceOpen = input.find(
'{', pos);
560 if (braceOpen == std::string::npos)
break;
562 std::string selector = trim(input.substr(pos, braceOpen - pos));
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);
573 if (input[pos] ==
'{') ++depth;
574 else if (input[pos] ==
'}') --depth;
575 if (depth > 0) ++pos;
578 std::string declarations = input.substr(declStart, pos - declStart);
579 pos = (pos < len) ? pos + 1 : pos;
581 if (!selector.empty()) {
584 _Rules.push_back(rule);
590 _Rules.push_back(rule);
594 if (index < _Rules.size()) {
595 _Rules.erase(_Rules.begin() +
static_cast<std::ptrdiff_t
>(index));
600 if (index < _Rules.size())
return &_Rules[index];
605 return _Rules.size();
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";
633 const std::string &tag,
634 const std::vector<std::string> &classes,
635 const std::string &
id)
637 std::string tagLower = tag;
638 std::transform(tagLower.begin(), tagLower.end(), tagLower.begin(),
639 [](
unsigned char c) { return std::tolower(c); });
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);
649 bool hadCombinator =
false;
650 std::string matchSel = stripCombinator(singleSel, hadCombinator);
651 if (hasUnsupportedSelectorSyntax(matchSel))
continue;
653 CompoundParts compound = parseCompoundSelector(matchSel);
654 if (compoundMatches(compound, hadCombinator, tagLower, classes,
id))
return true;
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
667 std::vector<std::string> classes;
668 if (!cssClass.empty()) {
669 std::istringstream iss(cssClass);
671 while (iss >> cls) classes.push_back(cls);
678 std::set<std::string> inlineKeys;
679 for (
const auto &kv : props) inlineKeys.insert(kv.first);
680 std::set<std::string> importantKeys;
682 for (
const auto &rule : _Rules) {
683 const std::string &sel = rule.getSelector();
684 if (sel.empty())
continue;
686 bool isAtRule = sel[0] ==
'@';
687 std::string atWrapper;
688 std::string innerSel;
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; }
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);
705 if (innerSel.empty())
continue;
708 const std::string &matchTarget = isAtRule ? innerSel : sel;
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);
718 if (!approximateSelectorMatch(singleSel, tag, classes,
id))
continue;
726 std::string block = atWrapper +
" { " + singleSel +
" { ";
727 for (
const auto &prop : rule.getDeclaration().getProperties()) {
728 block += prop.getName() +
": " + prop.getValue() +
"; ";
731 if (seenMediaBlocks.insert(block).second) {
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();
746 if (inlineKeys.count(key) && !isImportant)
continue;
747 if (importantKeys.count(key) && !isImportant)
continue;
750 if (isImportant) importantKeys.insert(key);
758 const std::map<std::string,std::string> &customProperties)
760 std::set<std::string> resolving;
761 return substituteVars(value, customProperties, resolving);
void addProperty(const std::string &name, const std::string &value)
void removeProperty(const std::string &name)
const std::vector< CSSProperty > & getProperties() const
std::string serialize() const
const CSSProperty * getProperty(const std::string &name) const
void parse(const std::string &input)
CSSDeclaration & operator=(const CSSDeclaration &decl)
void setName(const std::string &name)
const std::string & getName() const
void setValue(const std::string &value)
CSSProperty & operator=(const CSSProperty &prop)
const std::string & getValue() const
CSSDeclaration & getDeclaration()
std::string serialize(bool formatted=false) const
void setSelector(const std::string &selector)
CSSRule & operator=(const CSSRule &rule)
const std::string & getSelector() const
void parse(const std::string &input)
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...
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/...
const std::vector< CSSRule > & getRules() const
CSSStyleSheet & operator=(const CSSStyleSheet &sheet)
static CSSDeclaration parseInlineStyle(const std::string &style)
std::string serialize(bool formatted=false) const
const CSSRule * getRule(size_t index) const
void addRule(const CSSRule &rule)
void removeRule(size_t index)
size_t getRuleCount() const
size_t findMatchingParen(const std::string &s, size_t openPos)
bool compoundMatches(const CompoundParts &compound, bool hadCombinator, const std::string &tag, const std::vector< std::string > &classes, const std::string &id)
bool isWhitespace(char c)
std::string substituteVars(const std::string &value, const std::map< std::string, std::string > &customProperties, std::set< std::string > &resolving)
void splitVarArgs(const std::string &inner, std::string &name, std::string &fallback)
std::string stripCombinator(const std::string &singleSel, bool &hadCombinator)
CompoundParts parseCompoundSelector(const std::string &matchSel)
bool stripImportant(std::string &value)
std::string trim(const std::string &s)
bool hasUnsupportedSelectorSyntax(const std::string &matchSel)
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-...
std::vector< std::string > classes