Loading src/css.cpp +46 −17 Original line number Diff line number Diff line Loading @@ -225,28 +225,48 @@ namespace { return tagOk && idOk && classesOk; } // Whether `compound` is a plain bare-tag selector (no class/id/attribute // qualifier of its own) that only shows up as the TARGET compound // because a leading ancestor combinator was stripped off it -- e.g. // "[data-atom=header]>div" reduces to a bare "div" here. See this // function's callers for why that matters: on its own such a target // can't be trusted (confirmed against a real scraped page where exactly // this pattern matched ~24,000 times and blew up an HTML-import's // output to 36MB, back when this matcher had no way to verify the // ancestor side of the selector either) -- it's only safe to accept once // every one of this rule's ancestor compounds has ALSO been genuinely // verified against a real candidate ancestor, not vacuously satisfied // (see ancestorChainSatisfies/usedUnverifiableAncestor). A class/id/ // attribute qualifier alongside the tag (e.g. "div.foo") is already // specific enough to check on its own and never counts as "bare" here. bool isBareTagAfterCombinator(const CompoundParts &compound, bool hadCombinator) { return hadCombinator && compound.classes.empty() && compound.id.empty() && compound.attrConditions.empty() && !compound.tag.empty(); } // A selector matches only if EVERY part it specifies is present on this // element -- in particular every class listed, not just one. A bare tag // left over after stripping a combinator's ancestor part (e.g. // "[data-atom=header]>div" reduces to bare "div") can't be verified when // no ancestor chain is available to check it against -- treating it as a // match would apply the rule to literally every element of that tag in // the whole document (confirmed against a real scraped page where // exactly this pattern matched ~24,000 times and blew up an HTML-import's // output to 36MB). A class/id qualifier alongside the tag (e.g. // "div.foo") is still specific enough to check, and this guard only ever // applies to the trailing/target compound -- ancestorChainSatisfies // checks leading compounds against a real candidate frame instead, so a // bare-tag ancestor requirement is safe there. // element -- in particular every class listed, not just one. Whether a // bare-tag target (see isBareTagAfterCombinator) should be trusted here // is the CALLER's decision (it depends on how the ancestor side of the // same rule verifies, which isn't known yet at this point) -- this // function only checks the target compound itself. bool compoundMatches(const CompoundParts &compound, bool hadCombinator, const std::string &tag, const std::vector<std::string> &classes, const std::string &id, const std::map<std::string,std::string> *targetAttrs = nullptr) { bool bareTagAfterCombinator = hadCombinator && compound.classes.empty() && compound.id.empty() && !compound.tag.empty(); bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() || !compound.id.empty(); if (bareTagAfterCombinator || !specifiedSomething) return false; (void)hadCombinator; // kept for signature/call-site symmetry with isBareTagAfterCombinator // An attribute condition only counts as "this compound specifies a // real requirement" when @p targetAttrs is actually available to // check it against -- a caller with no attribute map (e.g. // approximateSelectorMatch's own callers, which never pass one) // still can't verify it, so a purely-attribute compound like // "[data-x]" reverts to the old "nothing specified, reject" outcome // for them instead of permissively matching every element. bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() || !compound.id.empty() || (!compound.attrConditions.empty() && targetAttrs != nullptr); if (!specifiedSomething) return false; return compoundPartsMatch(compound, tag, classes, id) && attributeConditionsSatisfied(compound.attrConditions, targetAttrs); Loading Loading @@ -955,8 +975,10 @@ bool libhtmlpp::CSSStyleSheet::approximateSelectorMatch(const std::string &selec if (hasUnsupportedSelectorSyntax(matchSel)) continue; CompoundParts compound = parseCompoundSelector(matchSel); compound.attrConditions = parseAttributeConditions(chain.back()); if (!compoundMatches(compound, hadCombinator, tagLower, classes, id)) continue; bool usedUnverifiableAncestor = false; if (ancestors && chain.size() > 1) { std::vector<CompoundParts> ancestorCompounds; for (size_t i = 0; i + 1 < chain.size(); ++i) { Loading @@ -969,8 +991,11 @@ bool libhtmlpp::CSSStyleSheet::approximateSelectorMatch(const std::string &selec ancestorCompounds.push_back(std::move(aCompound)); } } if (!ancestorChainSatisfies(ancestorCompounds, *ancestors)) continue; if (!ancestorChainSatisfies(ancestorCompounds, *ancestors, &usedUnverifiableAncestor)) continue; } if (isBareTagAfterCombinator(compound, hadCombinator) && (!ancestors || usedUnverifiableAncestor)) continue; return true; } return false; Loading Loading @@ -1163,6 +1188,10 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches( continue; } if (isBareTagAfterCombinator(compound, branch.hadCombinator) && (!ancestors || usedUnverifiableAncestor)) continue; if (branch.isAtRule) { // The same @media block, once present anywhere in the final // output, applies document-wide regardless of which element Loading src/css.h +34 −25 Original line number Diff line number Diff line Loading @@ -180,32 +180,41 @@ namespace libhtmlpp { * callers that can't supply ancestor context are unaffected). When * @p ancestors is non-null, each leading compound must be found * somewhere in it, in left-to-right order (see AncestorFrame) -- * same all-classes-listed/tag/id rule as the target compound, but * without the "bare tag" rejection below, since a specific * candidate ancestor to check it against removes that hazard. * same all-classes-listed/tag/id rule as the target compound (plus * its own attribute conditions, see AncestorFrame::attributes). * Rejected outright for the target compound: pseudo-classes and * pseudo-elements (":hover", "::before"), the universal selector * ("*"), and -- after a combinator (space/">"/"+"/"~") is stripped * down to its trailing compound selector -- a bare tag left with no * class/id qualifier of its own (e.g. "[data-x]>div" reduces to a * bare "div", which would otherwise match every element of that tag * in the whole document; a qualified compound like "div.foo" is * still specific enough to check). Attribute selectors ("[href]") * have their "[...]" segment removed and the remaining compound (if * any) checked instead of being rejected outright -- this matcher * has no per-element attribute map to verify the condition itself, * so the condition is simply ignored rather than guessed, for both * the target and any ancestor compound. A compound selector like * "div.card.featured#hero" matches only if the tag (when given), * every class listed (all of them, not just one), and the id (when * given) are all present on the element being checked against it. * This is intentionally an approximation of real CSS selector * matching, not an implementation of it -- there is no specificity * calculation, and ancestor verification (when requested) doesn't * distinguish child/descendant/sibling combinators or require * adjacency -- built to be safe against false positives on * real-world scraped markup rather than complete; treat a `false` * result as "not proven to match", not "definitely doesn't". */ * pseudo-elements (":hover", "::before") and the universal selector * ("*") -- guessing their state is worse than not matching at all. * A bare tag left with no class/id/attribute qualifier of its own * after a combinator (space/">"/"+"/"~") is stripped off (e.g. * "[data-x]>div" reduces to a bare "div") is normally rejected too * -- on its own it would otherwise match every element of that tag * in the whole document -- UNLESS @p ancestors is supplied AND * every one of this rule's ancestor compounds was genuinely * verified against a real candidate ancestor rather than vacuously * satisfied (see ancestorChainSatisfies): a rule like * "header.site-nav > div" is safe to trust for a bare "div" once * its ancestor requirement is a real, checked constraint and not * just "ancestors exist" (a qualified compound like "div.foo" is * always specific enough to check on its own and never triggers * this rejection either way). An attribute-selector condition * ("[href=...]") on the target compound is checked against @p * collectApproximateMatches's @p targetAttributes when supplied -- * otherwise (or for an ancestor compound with no attributes * supplied on its AncestorFrame) it's ignored rather than guessed, * same "not proven to not match" bias as everywhere else here. A * compound selector like "div.card.featured#hero" matches only if * the tag (when given), every class listed (all of them, not just * one), and the id (when given) are all present on the element * being checked against it. This is intentionally an approximation * of real CSS selector matching, not an implementation of it -- * ancestor verification (when requested) doesn't distinguish * child/descendant/sibling combinators or require adjacency -- built * to be safe against false positives on real-world scraped markup * rather than complete; treat a `false` result as "not proven to * match", not "definitely doesn't". Approximate CSS specificity * (see computeSpecificity, used by collectApproximateMatches) is * computed separately from this function's own true/false verdict. */ static bool approximateSelectorMatch(const std::string &selector, const std::string &tag, const std::vector<std::string> &classes, Loading Loading
src/css.cpp +46 −17 Original line number Diff line number Diff line Loading @@ -225,28 +225,48 @@ namespace { return tagOk && idOk && classesOk; } // Whether `compound` is a plain bare-tag selector (no class/id/attribute // qualifier of its own) that only shows up as the TARGET compound // because a leading ancestor combinator was stripped off it -- e.g. // "[data-atom=header]>div" reduces to a bare "div" here. See this // function's callers for why that matters: on its own such a target // can't be trusted (confirmed against a real scraped page where exactly // this pattern matched ~24,000 times and blew up an HTML-import's // output to 36MB, back when this matcher had no way to verify the // ancestor side of the selector either) -- it's only safe to accept once // every one of this rule's ancestor compounds has ALSO been genuinely // verified against a real candidate ancestor, not vacuously satisfied // (see ancestorChainSatisfies/usedUnverifiableAncestor). A class/id/ // attribute qualifier alongside the tag (e.g. "div.foo") is already // specific enough to check on its own and never counts as "bare" here. bool isBareTagAfterCombinator(const CompoundParts &compound, bool hadCombinator) { return hadCombinator && compound.classes.empty() && compound.id.empty() && compound.attrConditions.empty() && !compound.tag.empty(); } // A selector matches only if EVERY part it specifies is present on this // element -- in particular every class listed, not just one. A bare tag // left over after stripping a combinator's ancestor part (e.g. // "[data-atom=header]>div" reduces to bare "div") can't be verified when // no ancestor chain is available to check it against -- treating it as a // match would apply the rule to literally every element of that tag in // the whole document (confirmed against a real scraped page where // exactly this pattern matched ~24,000 times and blew up an HTML-import's // output to 36MB). A class/id qualifier alongside the tag (e.g. // "div.foo") is still specific enough to check, and this guard only ever // applies to the trailing/target compound -- ancestorChainSatisfies // checks leading compounds against a real candidate frame instead, so a // bare-tag ancestor requirement is safe there. // element -- in particular every class listed, not just one. Whether a // bare-tag target (see isBareTagAfterCombinator) should be trusted here // is the CALLER's decision (it depends on how the ancestor side of the // same rule verifies, which isn't known yet at this point) -- this // function only checks the target compound itself. bool compoundMatches(const CompoundParts &compound, bool hadCombinator, const std::string &tag, const std::vector<std::string> &classes, const std::string &id, const std::map<std::string,std::string> *targetAttrs = nullptr) { bool bareTagAfterCombinator = hadCombinator && compound.classes.empty() && compound.id.empty() && !compound.tag.empty(); bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() || !compound.id.empty(); if (bareTagAfterCombinator || !specifiedSomething) return false; (void)hadCombinator; // kept for signature/call-site symmetry with isBareTagAfterCombinator // An attribute condition only counts as "this compound specifies a // real requirement" when @p targetAttrs is actually available to // check it against -- a caller with no attribute map (e.g. // approximateSelectorMatch's own callers, which never pass one) // still can't verify it, so a purely-attribute compound like // "[data-x]" reverts to the old "nothing specified, reject" outcome // for them instead of permissively matching every element. bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() || !compound.id.empty() || (!compound.attrConditions.empty() && targetAttrs != nullptr); if (!specifiedSomething) return false; return compoundPartsMatch(compound, tag, classes, id) && attributeConditionsSatisfied(compound.attrConditions, targetAttrs); Loading Loading @@ -955,8 +975,10 @@ bool libhtmlpp::CSSStyleSheet::approximateSelectorMatch(const std::string &selec if (hasUnsupportedSelectorSyntax(matchSel)) continue; CompoundParts compound = parseCompoundSelector(matchSel); compound.attrConditions = parseAttributeConditions(chain.back()); if (!compoundMatches(compound, hadCombinator, tagLower, classes, id)) continue; bool usedUnverifiableAncestor = false; if (ancestors && chain.size() > 1) { std::vector<CompoundParts> ancestorCompounds; for (size_t i = 0; i + 1 < chain.size(); ++i) { Loading @@ -969,8 +991,11 @@ bool libhtmlpp::CSSStyleSheet::approximateSelectorMatch(const std::string &selec ancestorCompounds.push_back(std::move(aCompound)); } } if (!ancestorChainSatisfies(ancestorCompounds, *ancestors)) continue; if (!ancestorChainSatisfies(ancestorCompounds, *ancestors, &usedUnverifiableAncestor)) continue; } if (isBareTagAfterCombinator(compound, hadCombinator) && (!ancestors || usedUnverifiableAncestor)) continue; return true; } return false; Loading Loading @@ -1163,6 +1188,10 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches( continue; } if (isBareTagAfterCombinator(compound, branch.hadCombinator) && (!ancestors || usedUnverifiableAncestor)) continue; if (branch.isAtRule) { // The same @media block, once present anywhere in the final // output, applies document-wide regardless of which element Loading
src/css.h +34 −25 Original line number Diff line number Diff line Loading @@ -180,32 +180,41 @@ namespace libhtmlpp { * callers that can't supply ancestor context are unaffected). When * @p ancestors is non-null, each leading compound must be found * somewhere in it, in left-to-right order (see AncestorFrame) -- * same all-classes-listed/tag/id rule as the target compound, but * without the "bare tag" rejection below, since a specific * candidate ancestor to check it against removes that hazard. * same all-classes-listed/tag/id rule as the target compound (plus * its own attribute conditions, see AncestorFrame::attributes). * Rejected outright for the target compound: pseudo-classes and * pseudo-elements (":hover", "::before"), the universal selector * ("*"), and -- after a combinator (space/">"/"+"/"~") is stripped * down to its trailing compound selector -- a bare tag left with no * class/id qualifier of its own (e.g. "[data-x]>div" reduces to a * bare "div", which would otherwise match every element of that tag * in the whole document; a qualified compound like "div.foo" is * still specific enough to check). Attribute selectors ("[href]") * have their "[...]" segment removed and the remaining compound (if * any) checked instead of being rejected outright -- this matcher * has no per-element attribute map to verify the condition itself, * so the condition is simply ignored rather than guessed, for both * the target and any ancestor compound. A compound selector like * "div.card.featured#hero" matches only if the tag (when given), * every class listed (all of them, not just one), and the id (when * given) are all present on the element being checked against it. * This is intentionally an approximation of real CSS selector * matching, not an implementation of it -- there is no specificity * calculation, and ancestor verification (when requested) doesn't * distinguish child/descendant/sibling combinators or require * adjacency -- built to be safe against false positives on * real-world scraped markup rather than complete; treat a `false` * result as "not proven to match", not "definitely doesn't". */ * pseudo-elements (":hover", "::before") and the universal selector * ("*") -- guessing their state is worse than not matching at all. * A bare tag left with no class/id/attribute qualifier of its own * after a combinator (space/">"/"+"/"~") is stripped off (e.g. * "[data-x]>div" reduces to a bare "div") is normally rejected too * -- on its own it would otherwise match every element of that tag * in the whole document -- UNLESS @p ancestors is supplied AND * every one of this rule's ancestor compounds was genuinely * verified against a real candidate ancestor rather than vacuously * satisfied (see ancestorChainSatisfies): a rule like * "header.site-nav > div" is safe to trust for a bare "div" once * its ancestor requirement is a real, checked constraint and not * just "ancestors exist" (a qualified compound like "div.foo" is * always specific enough to check on its own and never triggers * this rejection either way). An attribute-selector condition * ("[href=...]") on the target compound is checked against @p * collectApproximateMatches's @p targetAttributes when supplied -- * otherwise (or for an ancestor compound with no attributes * supplied on its AncestorFrame) it's ignored rather than guessed, * same "not proven to not match" bias as everywhere else here. A * compound selector like "div.card.featured#hero" matches only if * the tag (when given), every class listed (all of them, not just * one), and the id (when given) are all present on the element * being checked against it. This is intentionally an approximation * of real CSS selector matching, not an implementation of it -- * ancestor verification (when requested) doesn't distinguish * child/descendant/sibling combinators or require adjacency -- built * to be safe against false positives on real-world scraped markup * rather than complete; treat a `false` result as "not proven to * match", not "definitely doesn't". Approximate CSS specificity * (see computeSpecificity, used by collectApproximateMatches) is * computed separately from this function's own true/false verdict. */ static bool approximateSelectorMatch(const std::string &selector, const std::string &tag, const std::vector<std::string> &classes, Loading