Loading src/css.cpp +91 −5 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "exception.h" #include <algorithm> #include <array> #include <cctype> #include <sstream> Loading Loading @@ -297,6 +298,72 @@ namespace { return std::string::npos; } // Approximate CSS specificity of one full comma-branch selector (e.g. // "div.card#hero[data-x]:hover .child"), as the standard (id-count, // class/attribute/pseudo-class-count, type/pseudo-element-count) triple // -- compared lexicographically, so any number of classes always loses // to a single id, any number of tags always loses to a single class, // same as a real browser's cascade. Used to let a more specific // matching rule win regardless of source order (see // collectApproximateMatches), instead of this file's usual plain // last-rule-wins approximation -- unlike matching itself, specificity // doesn't need per-element context, so it's computed once per rule // from the raw selector text and cached (see _CompoundCacheBranch), // covering compounds this matcher can't safely verify (attribute // selectors, pseudo-classes -- see stripAttributeSelectors/ // hasUnsupportedSelectorSyntax) exactly the same as ones it can: real // CSS specificity is a property of the selector text, independent of // whether this approximate matcher happens to be able to verify every // part of it. Deliberately approximate like the rest of this file: a // literal '.'/'#'/'[' inside a quoted attribute value (e.g. // "[href=\"a.b\"]") could miscount since this scans raw text rather // than a real tokenizer, but attribute selectors are always skipped as // one atomic "[...]" unit (never scanned inside), so that specific case // doesn't actually miscount. std::array<int,3> computeSpecificity(const std::string &selector) { std::array<int,3> spec{0, 0, 0}; size_t pos = 0; auto skipIdent = [&](size_t p) { while (p < selector.size() && (std::isalnum(static_cast<unsigned char>(selector[p])) || selector[p] == '-' || selector[p] == '_' || selector[p] == '\\')) { ++p; } return p; }; while (pos < selector.size()) { char c = selector[pos]; if (c == '#') { ++spec[0]; pos = skipIdent(pos + 1); } else if (c == '.') { ++spec[1]; pos = skipIdent(pos + 1); } else if (c == '[') { ++spec[1]; size_t close = selector.find(']', pos + 1); pos = (close == std::string::npos) ? selector.size() : close + 1; } else if (c == ':') { bool pseudoElement = pos + 1 < selector.size() && selector[pos + 1] == ':'; ++spec[pseudoElement ? 2 : 1]; pos = skipIdent(pos + (pseudoElement ? 2 : 1)); if (pos < selector.size() && selector[pos] == '(') { size_t close = findMatchingParen(selector, pos); pos = (close == std::string::npos) ? selector.size() : close + 1; } } else if (c == '*') { ++pos; // universal selector: contributes nothing, per spec } else if (std::isalpha(static_cast<unsigned char>(c)) || c == '_') { size_t next = skipIdent(pos); if (next > pos) ++spec[2]; pos = next; } else { ++pos; // whitespace/combinators/etc -- not part of any token } } return spec; } // Splits `inner` (the text between "var(" and its matching ")") at the // first top-level comma (depth 0, not inside a nested "(...)") into a // trimmed custom-property name and a trimmed fallback ("" if no comma Loading Loading @@ -886,6 +953,7 @@ void libhtmlpp::CSSStyleSheet::_rebuildCompoundCache() const { branch.isAtRule = isAtRule; branch.atWrapper = atWrapper; branch.trimmedSelector = singleSel; branch.specificity = computeSpecificity(singleSel); std::vector<std::string> chain = splitCombinatorChain(singleSel); bool hadCombinator = chain.size() > 1; Loading Loading @@ -946,6 +1014,17 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches( std::set<std::string> inlineKeys; for (const auto &kv : props) inlineKeys.insert(kv.first); std::set<std::string> importantKeys; // Specificity of whichever rule currently "owns" each property, kept // separately per precedence tier (plain vs !important) so a more // specific rule wins regardless of source order within its own tier -- // real CSS cascade order is (origin/importance, specificity, source // order); a later-but-less-specific rule should never beat an earlier // one (confirmed on a real page-builder site: a plain // ".fade-box img{width:auto}" was winning over an earlier but more // specific ".fade-box[data-fit=fill] img{object-fit:cover}" purely // because it came later in the stylesheet). std::map<std::string, std::array<int,3>> plainSpecOf; std::map<std::string, std::array<int,3>> importantSpecOf; if (!_compoundCacheValid) _rebuildCompoundCache(); Loading Loading @@ -993,14 +1072,20 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches( const std::string &key = prop.getName(); // Inline style and an already-!important value both // outrank a later plain rule; a later rule of equal // priority (both plain, or both !important) wins -- // approximating the real cascade's "last rule of // equal-or-higher precedence wins" without computing // full selector specificity. // outrank a later plain rule. if (inlineKeys.count(key) && !isImportant) continue; if (importantKeys.count(key) && !isImportant) continue; // Within a precedence tier (plain vs !important, see // above), a rule with LOWER specificity than whichever // one currently owns this property loses -- same as a // real cascade. Equal specificity still falls through // to "later rule wins" below, matching real CSS. auto &specOf = isImportant ? importantSpecOf : plainSpecOf; auto specIt = specOf.find(key); if (specIt != specOf.end() && specIt->second > branch.specificity) continue; // A match that only went through because an ancestor // condition we can't verify (see // ancestorChainSatisfies/usedUnverifiableAncestor) was Loading @@ -1017,6 +1102,7 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches( continue; props[key] = value; specOf[key] = branch.specificity; if (isImportant) importantKeys.insert(key); } } Loading src/css.h +9 −0 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include <map> #include <set> #include <memory> #include <array> namespace libhtmlpp { Loading Loading @@ -266,6 +267,14 @@ namespace libhtmlpp { std::string id; }; std::vector<AncestorCompound> ancestorCompounds; // Approximate CSS specificity of this branch's full selector // text (trimmedSelector), as the standard (id-count, // class/attribute/pseudo-class-count, type/pseudo-element-count) // triple -- see computeSpecificity in css.cpp. Used by // collectApproximateMatches to let a more specific matching rule // win regardless of source order, instead of the plain // last-rule-wins approximation this file used everywhere else. std::array<int,3> specificity{0,0,0}; }; mutable std::vector<std::vector<_CompoundCacheBranch>> _compoundCache; mutable bool _compoundCacheValid = false; Loading Loading
src/css.cpp +91 −5 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "exception.h" #include <algorithm> #include <array> #include <cctype> #include <sstream> Loading Loading @@ -297,6 +298,72 @@ namespace { return std::string::npos; } // Approximate CSS specificity of one full comma-branch selector (e.g. // "div.card#hero[data-x]:hover .child"), as the standard (id-count, // class/attribute/pseudo-class-count, type/pseudo-element-count) triple // -- compared lexicographically, so any number of classes always loses // to a single id, any number of tags always loses to a single class, // same as a real browser's cascade. Used to let a more specific // matching rule win regardless of source order (see // collectApproximateMatches), instead of this file's usual plain // last-rule-wins approximation -- unlike matching itself, specificity // doesn't need per-element context, so it's computed once per rule // from the raw selector text and cached (see _CompoundCacheBranch), // covering compounds this matcher can't safely verify (attribute // selectors, pseudo-classes -- see stripAttributeSelectors/ // hasUnsupportedSelectorSyntax) exactly the same as ones it can: real // CSS specificity is a property of the selector text, independent of // whether this approximate matcher happens to be able to verify every // part of it. Deliberately approximate like the rest of this file: a // literal '.'/'#'/'[' inside a quoted attribute value (e.g. // "[href=\"a.b\"]") could miscount since this scans raw text rather // than a real tokenizer, but attribute selectors are always skipped as // one atomic "[...]" unit (never scanned inside), so that specific case // doesn't actually miscount. std::array<int,3> computeSpecificity(const std::string &selector) { std::array<int,3> spec{0, 0, 0}; size_t pos = 0; auto skipIdent = [&](size_t p) { while (p < selector.size() && (std::isalnum(static_cast<unsigned char>(selector[p])) || selector[p] == '-' || selector[p] == '_' || selector[p] == '\\')) { ++p; } return p; }; while (pos < selector.size()) { char c = selector[pos]; if (c == '#') { ++spec[0]; pos = skipIdent(pos + 1); } else if (c == '.') { ++spec[1]; pos = skipIdent(pos + 1); } else if (c == '[') { ++spec[1]; size_t close = selector.find(']', pos + 1); pos = (close == std::string::npos) ? selector.size() : close + 1; } else if (c == ':') { bool pseudoElement = pos + 1 < selector.size() && selector[pos + 1] == ':'; ++spec[pseudoElement ? 2 : 1]; pos = skipIdent(pos + (pseudoElement ? 2 : 1)); if (pos < selector.size() && selector[pos] == '(') { size_t close = findMatchingParen(selector, pos); pos = (close == std::string::npos) ? selector.size() : close + 1; } } else if (c == '*') { ++pos; // universal selector: contributes nothing, per spec } else if (std::isalpha(static_cast<unsigned char>(c)) || c == '_') { size_t next = skipIdent(pos); if (next > pos) ++spec[2]; pos = next; } else { ++pos; // whitespace/combinators/etc -- not part of any token } } return spec; } // Splits `inner` (the text between "var(" and its matching ")") at the // first top-level comma (depth 0, not inside a nested "(...)") into a // trimmed custom-property name and a trimmed fallback ("" if no comma Loading Loading @@ -886,6 +953,7 @@ void libhtmlpp::CSSStyleSheet::_rebuildCompoundCache() const { branch.isAtRule = isAtRule; branch.atWrapper = atWrapper; branch.trimmedSelector = singleSel; branch.specificity = computeSpecificity(singleSel); std::vector<std::string> chain = splitCombinatorChain(singleSel); bool hadCombinator = chain.size() > 1; Loading Loading @@ -946,6 +1014,17 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches( std::set<std::string> inlineKeys; for (const auto &kv : props) inlineKeys.insert(kv.first); std::set<std::string> importantKeys; // Specificity of whichever rule currently "owns" each property, kept // separately per precedence tier (plain vs !important) so a more // specific rule wins regardless of source order within its own tier -- // real CSS cascade order is (origin/importance, specificity, source // order); a later-but-less-specific rule should never beat an earlier // one (confirmed on a real page-builder site: a plain // ".fade-box img{width:auto}" was winning over an earlier but more // specific ".fade-box[data-fit=fill] img{object-fit:cover}" purely // because it came later in the stylesheet). std::map<std::string, std::array<int,3>> plainSpecOf; std::map<std::string, std::array<int,3>> importantSpecOf; if (!_compoundCacheValid) _rebuildCompoundCache(); Loading Loading @@ -993,14 +1072,20 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches( const std::string &key = prop.getName(); // Inline style and an already-!important value both // outrank a later plain rule; a later rule of equal // priority (both plain, or both !important) wins -- // approximating the real cascade's "last rule of // equal-or-higher precedence wins" without computing // full selector specificity. // outrank a later plain rule. if (inlineKeys.count(key) && !isImportant) continue; if (importantKeys.count(key) && !isImportant) continue; // Within a precedence tier (plain vs !important, see // above), a rule with LOWER specificity than whichever // one currently owns this property loses -- same as a // real cascade. Equal specificity still falls through // to "later rule wins" below, matching real CSS. auto &specOf = isImportant ? importantSpecOf : plainSpecOf; auto specIt = specOf.find(key); if (specIt != specOf.end() && specIt->second > branch.specificity) continue; // A match that only went through because an ancestor // condition we can't verify (see // ancestorChainSatisfies/usedUnverifiableAncestor) was Loading @@ -1017,6 +1102,7 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches( continue; props[key] = value; specOf[key] = branch.specificity; if (isImportant) importantKeys.insert(key); } } Loading
src/css.h +9 −0 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include <map> #include <set> #include <memory> #include <array> namespace libhtmlpp { Loading Loading @@ -266,6 +267,14 @@ namespace libhtmlpp { std::string id; }; std::vector<AncestorCompound> ancestorCompounds; // Approximate CSS specificity of this branch's full selector // text (trimmedSelector), as the standard (id-count, // class/attribute/pseudo-class-count, type/pseudo-element-count) // triple -- see computeSpecificity in css.cpp. Used by // collectApproximateMatches to let a more specific matching rule // win regardless of source order, instead of the plain // last-rule-wins approximation this file used everywhere else. std::array<int,3> specificity{0,0,0}; }; mutable std::vector<std::vector<_CompoundCacheBranch>> _compoundCache; mutable bool _compoundCacheValid = false; Loading