Loading src/css.cpp +41 −26 Original line number Diff line number Diff line Loading @@ -285,30 +285,34 @@ namespace { // farther out than one an earlier compound already matched -- // approximating real left-to-right descendant-selector semantics // without distinguishing combinator type, requiring adjacency, or // computing true specificity. An empty compound (unsafe syntax reduced // to nothing, see splitCombinatorChain's callers) is vacuously satisfied // without consuming an ancestor slot -- an unverifiable requirement // shouldn't by itself invalidate an otherwise plausible match, same // "not proven to not match" philosophy as the rest of this file. If @p // usedUnverifiableAncestor is non-null, it's set to true whenever this // happens, so a caller merging declarations into a props map (see // collectApproximateMatches) can single out display:none/ // visibility:hidden from such a match -- unlike most properties, a wrong // guess there doesn't just mis-style an element, it makes the element // and its whole subtree disappear from the import (confirmed on a real // page-builder site: literally every spacer div was hidden because its // visibility rule's ancestor condition was an attribute selector like // "[data-mode=horizontal]", which this matcher can't verify and so // reduces to an empty, vacuously-satisfied compound -- see // stripAttributeSelectors). // computing true specificity. A compound with NEITHER tag/class/id NOR // any attribute condition (unsafe syntax reduced to nothing, see // splitCombinatorChain's callers -- an unsupported pseudo-class was // present) is vacuously satisfied without consuming an ancestor slot -- // an unverifiable requirement shouldn't by itself invalidate an // otherwise plausible match, same "not proven to not match" philosophy // as the rest of this file. If @p usedUnverifiableAncestor is non-null, // it's set to true whenever this happens, so a caller merging // declarations into a props map (see collectApproximateMatches) can // single out display:none/visibility:hidden from such a match -- unlike // most properties, a wrong guess there doesn't just mis-style an // element, it makes the element and its whole subtree disappear from // the import. A compound that DOES carry attribute conditions (e.g. // "[data-fit=fill]" in ".fade-box[data-fit=fill] img") is a real, // checkable requirement: it's matched against each candidate frame's own // attributes (see AncestorFrame::attributes) the same way // compoundPartsMatch checks tag/class/id -- a frame with no attributes // supplied (an empty map, meaning the caller didn't populate that field) // can't refute it, so it's treated as satisfied for that frame rather // than guessed wrong. bool ancestorChainSatisfies(const std::vector<CompoundParts> &ancestorCompounds, const std::vector<libhtmlpp::AncestorFrame> &ancestors, bool *usedUnverifiableAncestor = nullptr) { size_t idx = 0; for (const auto &compound : ancestorCompounds) { bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() || !compound.id.empty(); bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() || !compound.id.empty() || !compound.attrConditions.empty(); if (!specifiedSomething) { if (usedUnverifiableAncestor) *usedUnverifiableAncestor = true; continue; Loading @@ -318,7 +322,9 @@ namespace { while (idx < ancestors.size()) { const libhtmlpp::AncestorFrame &frame = ancestors[idx]; ++idx; if (compoundPartsMatch(compound, frame.tag, frame.classes, frame.id)) { if (compoundPartsMatch(compound, frame.tag, frame.classes, frame.id) && attributeConditionsSatisfied(compound.attrConditions, frame.attributes.empty() ? nullptr : &frame.attributes)) { found = true; break; } Loading Loading @@ -955,8 +961,13 @@ bool libhtmlpp::CSSStyleSheet::approximateSelectorMatch(const std::string &selec std::vector<CompoundParts> ancestorCompounds; for (size_t i = 0; i + 1 < chain.size(); ++i) { std::string aSel = stripAttributeSelectors(chain[i]); ancestorCompounds.push_back( hasUnsupportedSelectorSyntax(aSel) ? CompoundParts{} : parseCompoundSelector(aSel)); if (hasUnsupportedSelectorSyntax(aSel)) { ancestorCompounds.push_back({}); } else { CompoundParts aCompound = parseCompoundSelector(aSel); aCompound.attrConditions = parseAttributeConditions(chain[i]); ancestorCompounds.push_back(std::move(aCompound)); } } if (!ancestorChainSatisfies(ancestorCompounds, *ancestors)) continue; } Loading Loading @@ -1064,10 +1075,14 @@ void libhtmlpp::CSSStyleSheet::_rebuildCompoundCache() const { branch.hadCombinator = hadCombinator; for (size_t i = 0; i + 1 < chain.size(); ++i) { std::string aSel = stripAttributeSelectors(chain[i]); CompoundParts aCompound = hasUnsupportedSelectorSyntax(aSel) ? CompoundParts{} : parseCompoundSelector(aSel); if (hasUnsupportedSelectorSyntax(aSel)) { branch.ancestorCompounds.push_back({}); } else { CompoundParts aCompound = parseCompoundSelector(aSel); branch.ancestorCompounds.push_back( {aCompound.tag, aCompound.classes, aCompound.id}); {aCompound.tag, aCompound.classes, aCompound.id, parseAttributeConditions(chain[i])}); } } } branches.push_back(std::move(branch)); Loading Loading @@ -1142,7 +1157,7 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches( if (ancestors && !branch.ancestorCompounds.empty()) { std::vector<CompoundParts> ancestorCompounds; for (const auto &ac : branch.ancestorCompounds) { ancestorCompounds.push_back({ac.tag, ac.classes, ac.id}); ancestorCompounds.push_back({ac.tag, ac.classes, ac.id, ac.attrConditions}); } if (!ancestorChainSatisfies(ancestorCompounds, *ancestors, &usedUnverifiableAncestor)) continue; Loading src/css.h +20 −6 Original line number Diff line number Diff line Loading @@ -113,11 +113,20 @@ namespace libhtmlpp { * Only presence/order in the chain is checked, not adjacency or * combinator type: this remains an approximation of real CSS descendant * matching, just a considerably safer one than ignoring ancestors * entirely. */ * entirely. @p attributes (name -> value, optional -- an empty map * means "not supplied", not "this ancestor has no attributes") lets an * attribute-selector condition on an ANCESTOR compound (e.g. the * "[data-fit=fill]" in ".fade-box[data-fit=fill] img") be verified the * same way collectApproximateMatches's @p targetAttributes verifies one * on the target compound -- a caller that doesn't populate this for its * ancestor frames gets the old "ignore it" behavior for THOSE * attribute-only ancestor conditions specifically (a tag/class/id * requirement in the same compound is still checked as before). */ struct AncestorFrame { std::string tag; std::vector<std::string> classes; std::string id; std::map<std::string,std::string> attributes; }; /** One "[name]"/"[name=value]"/"[name~=value]"/etc. attribute-selector Loading Loading @@ -289,15 +298,20 @@ namespace libhtmlpp { // Every compound left of the target one above, left-to-right/ // outermost-first (e.g. for ".a .b .c", this holds ".a" and ".b" // -- "c" is the tag/classes/id/hadCombinator fields already // above). Empty tag/classes/id within one entry means that // compound's syntax couldn't be safely evaluated (same // attribute-selector/pseudo-class handling as the target) -- // ancestorChainSatisfies treats that as vacuously satisfied // rather than an unmet requirement. // above). Empty tag/classes/id/attrConditions within one entry // means that compound's syntax couldn't be safely evaluated at // all (an unsupported pseudo-class was present -- see // hasUnsupportedSelectorSyntax) -- ancestorChainSatisfies treats // that as vacuously satisfied rather than an unmet requirement. // A compound with ONLY attrConditions (no tag/class/id, e.g. // "[data-fit=fill]") is a real, checkable requirement now that // AncestorFrame can carry an ancestor's own attributes -- see // ancestorChainSatisfies. struct AncestorCompound { std::string tag; std::vector<std::string> classes; std::string id; std::vector<AttributeCondition> attrConditions; }; std::vector<AncestorCompound> ancestorCompounds; // Approximate CSS specificity of this branch's full selector Loading Loading
src/css.cpp +41 −26 Original line number Diff line number Diff line Loading @@ -285,30 +285,34 @@ namespace { // farther out than one an earlier compound already matched -- // approximating real left-to-right descendant-selector semantics // without distinguishing combinator type, requiring adjacency, or // computing true specificity. An empty compound (unsafe syntax reduced // to nothing, see splitCombinatorChain's callers) is vacuously satisfied // without consuming an ancestor slot -- an unverifiable requirement // shouldn't by itself invalidate an otherwise plausible match, same // "not proven to not match" philosophy as the rest of this file. If @p // usedUnverifiableAncestor is non-null, it's set to true whenever this // happens, so a caller merging declarations into a props map (see // collectApproximateMatches) can single out display:none/ // visibility:hidden from such a match -- unlike most properties, a wrong // guess there doesn't just mis-style an element, it makes the element // and its whole subtree disappear from the import (confirmed on a real // page-builder site: literally every spacer div was hidden because its // visibility rule's ancestor condition was an attribute selector like // "[data-mode=horizontal]", which this matcher can't verify and so // reduces to an empty, vacuously-satisfied compound -- see // stripAttributeSelectors). // computing true specificity. A compound with NEITHER tag/class/id NOR // any attribute condition (unsafe syntax reduced to nothing, see // splitCombinatorChain's callers -- an unsupported pseudo-class was // present) is vacuously satisfied without consuming an ancestor slot -- // an unverifiable requirement shouldn't by itself invalidate an // otherwise plausible match, same "not proven to not match" philosophy // as the rest of this file. If @p usedUnverifiableAncestor is non-null, // it's set to true whenever this happens, so a caller merging // declarations into a props map (see collectApproximateMatches) can // single out display:none/visibility:hidden from such a match -- unlike // most properties, a wrong guess there doesn't just mis-style an // element, it makes the element and its whole subtree disappear from // the import. A compound that DOES carry attribute conditions (e.g. // "[data-fit=fill]" in ".fade-box[data-fit=fill] img") is a real, // checkable requirement: it's matched against each candidate frame's own // attributes (see AncestorFrame::attributes) the same way // compoundPartsMatch checks tag/class/id -- a frame with no attributes // supplied (an empty map, meaning the caller didn't populate that field) // can't refute it, so it's treated as satisfied for that frame rather // than guessed wrong. bool ancestorChainSatisfies(const std::vector<CompoundParts> &ancestorCompounds, const std::vector<libhtmlpp::AncestorFrame> &ancestors, bool *usedUnverifiableAncestor = nullptr) { size_t idx = 0; for (const auto &compound : ancestorCompounds) { bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() || !compound.id.empty(); bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() || !compound.id.empty() || !compound.attrConditions.empty(); if (!specifiedSomething) { if (usedUnverifiableAncestor) *usedUnverifiableAncestor = true; continue; Loading @@ -318,7 +322,9 @@ namespace { while (idx < ancestors.size()) { const libhtmlpp::AncestorFrame &frame = ancestors[idx]; ++idx; if (compoundPartsMatch(compound, frame.tag, frame.classes, frame.id)) { if (compoundPartsMatch(compound, frame.tag, frame.classes, frame.id) && attributeConditionsSatisfied(compound.attrConditions, frame.attributes.empty() ? nullptr : &frame.attributes)) { found = true; break; } Loading Loading @@ -955,8 +961,13 @@ bool libhtmlpp::CSSStyleSheet::approximateSelectorMatch(const std::string &selec std::vector<CompoundParts> ancestorCompounds; for (size_t i = 0; i + 1 < chain.size(); ++i) { std::string aSel = stripAttributeSelectors(chain[i]); ancestorCompounds.push_back( hasUnsupportedSelectorSyntax(aSel) ? CompoundParts{} : parseCompoundSelector(aSel)); if (hasUnsupportedSelectorSyntax(aSel)) { ancestorCompounds.push_back({}); } else { CompoundParts aCompound = parseCompoundSelector(aSel); aCompound.attrConditions = parseAttributeConditions(chain[i]); ancestorCompounds.push_back(std::move(aCompound)); } } if (!ancestorChainSatisfies(ancestorCompounds, *ancestors)) continue; } Loading Loading @@ -1064,10 +1075,14 @@ void libhtmlpp::CSSStyleSheet::_rebuildCompoundCache() const { branch.hadCombinator = hadCombinator; for (size_t i = 0; i + 1 < chain.size(); ++i) { std::string aSel = stripAttributeSelectors(chain[i]); CompoundParts aCompound = hasUnsupportedSelectorSyntax(aSel) ? CompoundParts{} : parseCompoundSelector(aSel); if (hasUnsupportedSelectorSyntax(aSel)) { branch.ancestorCompounds.push_back({}); } else { CompoundParts aCompound = parseCompoundSelector(aSel); branch.ancestorCompounds.push_back( {aCompound.tag, aCompound.classes, aCompound.id}); {aCompound.tag, aCompound.classes, aCompound.id, parseAttributeConditions(chain[i])}); } } } branches.push_back(std::move(branch)); Loading Loading @@ -1142,7 +1157,7 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches( if (ancestors && !branch.ancestorCompounds.empty()) { std::vector<CompoundParts> ancestorCompounds; for (const auto &ac : branch.ancestorCompounds) { ancestorCompounds.push_back({ac.tag, ac.classes, ac.id}); ancestorCompounds.push_back({ac.tag, ac.classes, ac.id, ac.attrConditions}); } if (!ancestorChainSatisfies(ancestorCompounds, *ancestors, &usedUnverifiableAncestor)) continue; Loading
src/css.h +20 −6 Original line number Diff line number Diff line Loading @@ -113,11 +113,20 @@ namespace libhtmlpp { * Only presence/order in the chain is checked, not adjacency or * combinator type: this remains an approximation of real CSS descendant * matching, just a considerably safer one than ignoring ancestors * entirely. */ * entirely. @p attributes (name -> value, optional -- an empty map * means "not supplied", not "this ancestor has no attributes") lets an * attribute-selector condition on an ANCESTOR compound (e.g. the * "[data-fit=fill]" in ".fade-box[data-fit=fill] img") be verified the * same way collectApproximateMatches's @p targetAttributes verifies one * on the target compound -- a caller that doesn't populate this for its * ancestor frames gets the old "ignore it" behavior for THOSE * attribute-only ancestor conditions specifically (a tag/class/id * requirement in the same compound is still checked as before). */ struct AncestorFrame { std::string tag; std::vector<std::string> classes; std::string id; std::map<std::string,std::string> attributes; }; /** One "[name]"/"[name=value]"/"[name~=value]"/etc. attribute-selector Loading Loading @@ -289,15 +298,20 @@ namespace libhtmlpp { // Every compound left of the target one above, left-to-right/ // outermost-first (e.g. for ".a .b .c", this holds ".a" and ".b" // -- "c" is the tag/classes/id/hadCombinator fields already // above). Empty tag/classes/id within one entry means that // compound's syntax couldn't be safely evaluated (same // attribute-selector/pseudo-class handling as the target) -- // ancestorChainSatisfies treats that as vacuously satisfied // rather than an unmet requirement. // above). Empty tag/classes/id/attrConditions within one entry // means that compound's syntax couldn't be safely evaluated at // all (an unsupported pseudo-class was present -- see // hasUnsupportedSelectorSyntax) -- ancestorChainSatisfies treats // that as vacuously satisfied rather than an unmet requirement. // A compound with ONLY attrConditions (no tag/class/id, e.g. // "[data-fit=fill]") is a real, checkable requirement now that // AncestorFrame can carry an ancestor's own attributes -- see // ancestorChainSatisfies. struct AncestorCompound { std::string tag; std::vector<std::string> classes; std::string id; std::vector<AttributeCondition> attrConditions; }; std::vector<AncestorCompound> ancestorCompounds; // Approximate CSS specificity of this branch's full selector Loading