Loading src/css.cpp +24 −39 Original line number Diff line number Diff line Loading @@ -225,37 +225,34 @@ 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. 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. // 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") is rejected outright // for the TARGET compound, even when @p targetAttrs/ancestor // verification is available: on a real component-library/page-builder // site, a generic "layout plumbing" ancestor condition like // "[data-atom][data-atom=header]" is common enough (used on nearly // every content block) that a bare "div" child of it is still // effectively a near-blanket match once combined with this file's // specificity ordering (see computeSpecificity) -- confirmed on a real // scraped page: relaxing this guard for a "genuinely verified" ancestor // let "[data-atom][data-atom=header]>div{width:var(--a-width)}" (pure // layout plumbing, specificity (0,2,1)) win over a much more specific, // content-driven ".status-dot{width:12px}" (specificity (0,1,0)) rule // purely because the plumbing rule now qualified as a real match, even // though genuinely verifying its own ancestor chain didn't make it safe // to trust the bare-tag side too. A class/id/attribute qualifier // alongside the tag (e.g. "div.foo") is already specific enough to // check on its own and never triggers this rejection. 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) { (void)hadCombinator; // kept for signature/call-site symmetry with isBareTagAfterCombinator bool bareTagAfterCombinator = hadCombinator && compound.classes.empty() && compound.id.empty() && compound.attrConditions.empty() && !compound.tag.empty(); // 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. Loading @@ -266,7 +263,7 @@ namespace { bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() || !compound.id.empty() || (!compound.attrConditions.empty() && targetAttrs != nullptr); if (!specifiedSomething) return false; if (bareTagAfterCombinator || !specifiedSomething) return false; return compoundPartsMatch(compound, tag, classes, id) && attributeConditionsSatisfied(compound.attrConditions, targetAttrs); Loading Loading @@ -978,7 +975,6 @@ bool libhtmlpp::CSSStyleSheet::approximateSelectorMatch(const std::string &selec 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 @@ -991,11 +987,8 @@ bool libhtmlpp::CSSStyleSheet::approximateSelectorMatch(const std::string &selec ancestorCompounds.push_back(std::move(aCompound)); } } if (!ancestorChainSatisfies(ancestorCompounds, *ancestors, &usedUnverifiableAncestor)) continue; if (!ancestorChainSatisfies(ancestorCompounds, *ancestors)) continue; } if (isBareTagAfterCombinator(compound, hadCombinator) && (!ancestors || usedUnverifiableAncestor)) continue; return true; } return false; Loading Loading @@ -1242,14 +1235,6 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches( (key == "visibility" && value == "hidden"))) continue; if (key == "width" && (std::find(classes.begin(), classes.end(), "status-dot") != classes.end() || std::find(classes.begin(), classes.end(), "appointment-badge") != classes.end())) { fprintf(stderr, "[WDBG] class=%s width=%s important=%d sel=[%s] spec=(%d,%d,%d)\n", cssClass.c_str(), value.c_str(), isImportant, branch.trimmedSelector.c_str(), branch.specificity[0], branch.specificity[1], branch.specificity[2]); } props[key] = value; specOf[key] = branch.specificity; if (isImportant) importantKeys.insert(key); Loading Loading
src/css.cpp +24 −39 Original line number Diff line number Diff line Loading @@ -225,37 +225,34 @@ 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. 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. // 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") is rejected outright // for the TARGET compound, even when @p targetAttrs/ancestor // verification is available: on a real component-library/page-builder // site, a generic "layout plumbing" ancestor condition like // "[data-atom][data-atom=header]" is common enough (used on nearly // every content block) that a bare "div" child of it is still // effectively a near-blanket match once combined with this file's // specificity ordering (see computeSpecificity) -- confirmed on a real // scraped page: relaxing this guard for a "genuinely verified" ancestor // let "[data-atom][data-atom=header]>div{width:var(--a-width)}" (pure // layout plumbing, specificity (0,2,1)) win over a much more specific, // content-driven ".status-dot{width:12px}" (specificity (0,1,0)) rule // purely because the plumbing rule now qualified as a real match, even // though genuinely verifying its own ancestor chain didn't make it safe // to trust the bare-tag side too. A class/id/attribute qualifier // alongside the tag (e.g. "div.foo") is already specific enough to // check on its own and never triggers this rejection. 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) { (void)hadCombinator; // kept for signature/call-site symmetry with isBareTagAfterCombinator bool bareTagAfterCombinator = hadCombinator && compound.classes.empty() && compound.id.empty() && compound.attrConditions.empty() && !compound.tag.empty(); // 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. Loading @@ -266,7 +263,7 @@ namespace { bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() || !compound.id.empty() || (!compound.attrConditions.empty() && targetAttrs != nullptr); if (!specifiedSomething) return false; if (bareTagAfterCombinator || !specifiedSomething) return false; return compoundPartsMatch(compound, tag, classes, id) && attributeConditionsSatisfied(compound.attrConditions, targetAttrs); Loading Loading @@ -978,7 +975,6 @@ bool libhtmlpp::CSSStyleSheet::approximateSelectorMatch(const std::string &selec 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 @@ -991,11 +987,8 @@ bool libhtmlpp::CSSStyleSheet::approximateSelectorMatch(const std::string &selec ancestorCompounds.push_back(std::move(aCompound)); } } if (!ancestorChainSatisfies(ancestorCompounds, *ancestors, &usedUnverifiableAncestor)) continue; if (!ancestorChainSatisfies(ancestorCompounds, *ancestors)) continue; } if (isBareTagAfterCombinator(compound, hadCombinator) && (!ancestors || usedUnverifiableAncestor)) continue; return true; } return false; Loading Loading @@ -1242,14 +1235,6 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches( (key == "visibility" && value == "hidden"))) continue; if (key == "width" && (std::find(classes.begin(), classes.end(), "status-dot") != classes.end() || std::find(classes.begin(), classes.end(), "appointment-badge") != classes.end())) { fprintf(stderr, "[WDBG] class=%s width=%s important=%d sel=[%s] spec=(%d,%d,%d)\n", cssClass.c_str(), value.c_str(), isImportant, branch.trimmedSelector.c_str(), branch.specificity[0], branch.specificity[1], branch.specificity[2]); } props[key] = value; specOf[key] = branch.specificity; if (isImportant) importantKeys.insert(key); Loading