Loading src/css.cpp +95 −2 Original line number Diff line number Diff line Loading @@ -78,8 +78,99 @@ namespace { std::string tag; std::vector<std::string> classes; std::string id; // Attribute-selector conditions from the ORIGINAL (pre- // stripAttributeSelectors) compound text -- only ever populated for // the target compound (see parseAttributeConditions's callers); // left empty for ancestor compounds, which have no attribute map to // check against. Checked by compoundMatches when a target attribute // map is available, ignored (as before) otherwise. std::vector<libhtmlpp::AttributeCondition> attrConditions; }; // Parses every "[name]"/"[name=value]"/"[name~=value]"/etc. group found // anywhere in `sel` (a compound selector's RAW text, before // stripAttributeSelectors removes them) into an AttributeCondition -- // e.g. "[data-fit=fill]" -> {name="data-fit", op="=", value="fill"}, // a bare "[data-atom]" -> {name="data-atom", op="", value=""}. A quoted // value has its surrounding '"'/'\'' stripped. Deliberately approximate // like the rest of this file: doesn't handle a case-sensitivity flag // ("i"/"s" before the closing "]") or backslash-escaped characters -- // this matcher only needs to compare against a plain attribute-value // map, not implement the full CSS attribute-selector grammar. std::vector<libhtmlpp::AttributeCondition> parseAttributeConditions(const std::string &sel) { std::vector<libhtmlpp::AttributeCondition> out; size_t pos = 0; while (pos < sel.size()) { if (sel[pos] != '[') { ++pos; continue; } size_t close = sel.find(']', pos + 1); if (close == std::string::npos) break; std::string inner = trim(sel.substr(pos + 1, close - pos - 1)); pos = close + 1; if (inner.empty()) continue; libhtmlpp::AttributeCondition cond; size_t eq = inner.find('='); if (eq == std::string::npos) { cond.name = inner; } else { size_t opStart = eq; if (eq > 0 && std::string("~|^$*").find(inner[eq - 1]) != std::string::npos) opStart = eq - 1; cond.op = inner.substr(opStart, eq - opStart + 1); cond.name = trim(inner.substr(0, opStart)); std::string value = trim(inner.substr(eq + 1)); if (value.size() >= 2 && ((value.front() == '"' && value.back() == '"') || (value.front() == '\'' && value.back() == '\''))) value = value.substr(1, value.size() - 2); cond.value = value; } out.push_back(std::move(cond)); } return out; } // Whether `actual` (the target element's real attribute value) satisfies // one attribute-selector operator/expected pair. An unrecognized // operator (shouldn't happen given parseAttributeConditions's own // grammar) is treated as satisfied rather than guessed wrong, same "not // proven to not match" philosophy as the rest of this file. bool attributeValueMatches(const std::string &op, const std::string &expected, const std::string &actual) { if (op.empty()) return true; // bare "[name]" -- existence already confirmed by the caller if (op == "=") return actual == expected; if (op == "~=") { std::istringstream iss(actual); std::string tok; while (iss >> tok) if (tok == expected) return true; return false; } if (op == "^=") return !expected.empty() && actual.compare(0, expected.size(), expected) == 0; if (op == "$=") return !expected.empty() && expected.size() <= actual.size() && actual.compare(actual.size() - expected.size(), expected.size(), expected) == 0; if (op == "*=") return !expected.empty() && actual.find(expected) != std::string::npos; if (op == "|=") return actual == expected || actual.compare(0, expected.size() + 1, expected + "-") == 0; return true; } // Whether every attribute condition parsed off the TARGET compound (see // CompoundParts::attrConditions) holds against @p targetAttrs. Absent // conditions (none, or no attribute map available -- e.g. an ancestor // compound, or a caller that didn't supply one) are vacuously satisfied, // preserving this matcher's original "ignore attribute selectors" // behavior wherever an attribute map genuinely isn't available. bool attributeConditionsSatisfied(const std::vector<libhtmlpp::AttributeCondition> &conditions, const std::map<std::string,std::string> *targetAttrs) { if (conditions.empty()) return true; if (!targetAttrs) return true; for (const auto &cond : conditions) { auto it = targetAttrs->find(cond.name); if (it == targetAttrs->end()) return false; if (!attributeValueMatches(cond.op, cond.value, it->second)) return false; } return true; } CompoundParts parseCompoundSelector(const std::string &matchSel) { CompoundParts result; size_t pos = 0; Loading Loading @@ -149,14 +240,16 @@ namespace { // bare-tag ancestor requirement is safe there. bool compoundMatches(const CompoundParts &compound, bool hadCombinator, const std::string &tag, const std::vector<std::string> &classes, const std::string &id) 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; return compoundPartsMatch(compound, tag, classes, id); return compoundPartsMatch(compound, tag, classes, id) && attributeConditionsSatisfied(compound.attrConditions, targetAttrs); } // Splits a single (already comma-branch-isolated) selector into its Loading src/css.h +57 −24 Original line number Diff line number Diff line Loading @@ -120,6 +120,21 @@ namespace libhtmlpp { std::string id; }; /** One "[name]"/"[name=value]"/"[name~=value]"/etc. attribute-selector * condition parsed out of a compound selector, for verifying against a * target element's own attribute map (see collectApproximateMatches's * @p targetAttributes) instead of being ignored the way this matcher * otherwise treats attribute selectors (see approximateSelectorMatch's * own doc comment) -- @p op is "" for a bare "[name]" existence check, * otherwise one of "=", "~=", "|=", "^=", "$=", "*=" per the CSS * attribute-selector spec; @p value is unquoted and only meaningful * when @p op is non-empty. */ struct AttributeCondition { std::string name; std::string op; std::string value; }; class CSSStyleSheet { public: CSSStyleSheet(); Loading Loading @@ -196,36 +211,49 @@ namespace libhtmlpp { * called (e.g. the element's own inline style, set by the caller * before calling this) outranks a later plain rule unless that * rule's value carries "!important" (stripped from the stored * value either way); among this sheet's own rules, a later rule * beats an earlier one at equal priority. This is "last rule of * equal-or-higher precedence wins", not full CSS specificity. * Each matching `@media`/other at-rule block's raw text is appended * to @p mediaRules, skipped if its exact text is already present in * @p seenMediaBlocks (both are caller-owned, so a caller processing * many elements from one document/sheet can share one instance of * each across all of them and avoid repeating an identical block * once per matching element). @p ancestors is forwarded to * approximateSelectorMatch's underlying per-compound check exactly * as described there -- nullptr (the default) preserves this * function's original behavior (leading compounds of a combinator * selector are never verified), a non-null chain makes leading * compounds required to be found in it, in order. Exception: if a * match only went through because one of its ancestor compounds * was unverifiable (an attribute selector or unsupported pseudo- * class stripped down to nothing -- see approximateSelectorMatch's * own doc comment), that rule's display:none/visibility:hidden * values are dropped rather than folded into @p props, since an * ordinary wrong guess mis-styles an element but a wrong guess on * those two properties makes it (and its subtree) disappear * outright; every other property from the same rule is unaffected * and still applies normally. */ * value either way); within each of those two precedence tiers, a * rule with higher approximate specificity (id/class-attribute- * pseudo-class/type-pseudo-element counts, compared the same way a * real cascade does) wins regardless of source order, and only * falls back to "later rule wins" when two matching rules are * exactly as specific as each other -- see computeSpecificity in * css.cpp. Each matching `@media`/other at-rule block's raw text is * appended to @p mediaRules, skipped if its exact text is already * present in @p seenMediaBlocks (both are caller-owned, so a caller * processing many elements from one document/sheet can share one * instance of each across all of them and avoid repeating an * identical block once per matching element). @p ancestors is * forwarded to approximateSelectorMatch's underlying per-compound * check exactly as described there -- nullptr (the default) * preserves this function's original behavior (leading compounds of * a combinator selector are never verified), a non-null chain makes * leading compounds required to be found in it, in order. * Exception: if a match only went through because one of its * ancestor compounds was unverifiable (an attribute selector or * unsupported pseudo-class stripped down to nothing -- see * approximateSelectorMatch's own doc comment), that rule's * display:none/visibility:hidden values are dropped rather than * folded into @p props, since an ordinary wrong guess mis-styles an * element but a wrong guess on those two properties makes it (and * its subtree) disappear outright; every other property from the * same rule is unaffected and still applies normally. @p * targetAttributes, when non-null, is the target element's own * attribute map (name -> value) -- unlike ancestors, the target * element's attributes ARE available to this function's caller * (see getCSSRules), so an attribute-selector condition on the * TARGET compound itself (e.g. ".fade-box[data-fit=fill]", as * opposed to one on an ancestor compound, which still can't be * verified) is checked against it instead of being ignored -- * nullptr (the default) preserves the old ignore-it behavior for * any existing caller that doesn't have an attribute map handy. */ 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 std::vector<AncestorFrame> *ancestors = nullptr) const; const std::vector<AncestorFrame> *ancestors = nullptr, const std::map<std::string,std::string> *targetAttributes = nullptr) const; private: void _skipWhitespace(const std::string &input, size_t &pos) const; Loading @@ -252,6 +280,11 @@ namespace libhtmlpp { std::string tag; std::vector<std::string> classes; std::string id; // Attribute-selector conditions found on the TARGET compound // itself (e.g. the "[data-fit=fill]" in ".fade-box[data-fit= // fill]"), verified against collectApproximateMatches's // @p targetAttributes when given -- see AttributeCondition. std::vector<AttributeCondition> attrConditions; bool hadCombinator; // Every compound left of the target one above, left-to-right/ // outermost-first (e.g. for ".a .b .c", this holds ".a" and ".b" Loading Loading
src/css.cpp +95 −2 Original line number Diff line number Diff line Loading @@ -78,8 +78,99 @@ namespace { std::string tag; std::vector<std::string> classes; std::string id; // Attribute-selector conditions from the ORIGINAL (pre- // stripAttributeSelectors) compound text -- only ever populated for // the target compound (see parseAttributeConditions's callers); // left empty for ancestor compounds, which have no attribute map to // check against. Checked by compoundMatches when a target attribute // map is available, ignored (as before) otherwise. std::vector<libhtmlpp::AttributeCondition> attrConditions; }; // Parses every "[name]"/"[name=value]"/"[name~=value]"/etc. group found // anywhere in `sel` (a compound selector's RAW text, before // stripAttributeSelectors removes them) into an AttributeCondition -- // e.g. "[data-fit=fill]" -> {name="data-fit", op="=", value="fill"}, // a bare "[data-atom]" -> {name="data-atom", op="", value=""}. A quoted // value has its surrounding '"'/'\'' stripped. Deliberately approximate // like the rest of this file: doesn't handle a case-sensitivity flag // ("i"/"s" before the closing "]") or backslash-escaped characters -- // this matcher only needs to compare against a plain attribute-value // map, not implement the full CSS attribute-selector grammar. std::vector<libhtmlpp::AttributeCondition> parseAttributeConditions(const std::string &sel) { std::vector<libhtmlpp::AttributeCondition> out; size_t pos = 0; while (pos < sel.size()) { if (sel[pos] != '[') { ++pos; continue; } size_t close = sel.find(']', pos + 1); if (close == std::string::npos) break; std::string inner = trim(sel.substr(pos + 1, close - pos - 1)); pos = close + 1; if (inner.empty()) continue; libhtmlpp::AttributeCondition cond; size_t eq = inner.find('='); if (eq == std::string::npos) { cond.name = inner; } else { size_t opStart = eq; if (eq > 0 && std::string("~|^$*").find(inner[eq - 1]) != std::string::npos) opStart = eq - 1; cond.op = inner.substr(opStart, eq - opStart + 1); cond.name = trim(inner.substr(0, opStart)); std::string value = trim(inner.substr(eq + 1)); if (value.size() >= 2 && ((value.front() == '"' && value.back() == '"') || (value.front() == '\'' && value.back() == '\''))) value = value.substr(1, value.size() - 2); cond.value = value; } out.push_back(std::move(cond)); } return out; } // Whether `actual` (the target element's real attribute value) satisfies // one attribute-selector operator/expected pair. An unrecognized // operator (shouldn't happen given parseAttributeConditions's own // grammar) is treated as satisfied rather than guessed wrong, same "not // proven to not match" philosophy as the rest of this file. bool attributeValueMatches(const std::string &op, const std::string &expected, const std::string &actual) { if (op.empty()) return true; // bare "[name]" -- existence already confirmed by the caller if (op == "=") return actual == expected; if (op == "~=") { std::istringstream iss(actual); std::string tok; while (iss >> tok) if (tok == expected) return true; return false; } if (op == "^=") return !expected.empty() && actual.compare(0, expected.size(), expected) == 0; if (op == "$=") return !expected.empty() && expected.size() <= actual.size() && actual.compare(actual.size() - expected.size(), expected.size(), expected) == 0; if (op == "*=") return !expected.empty() && actual.find(expected) != std::string::npos; if (op == "|=") return actual == expected || actual.compare(0, expected.size() + 1, expected + "-") == 0; return true; } // Whether every attribute condition parsed off the TARGET compound (see // CompoundParts::attrConditions) holds against @p targetAttrs. Absent // conditions (none, or no attribute map available -- e.g. an ancestor // compound, or a caller that didn't supply one) are vacuously satisfied, // preserving this matcher's original "ignore attribute selectors" // behavior wherever an attribute map genuinely isn't available. bool attributeConditionsSatisfied(const std::vector<libhtmlpp::AttributeCondition> &conditions, const std::map<std::string,std::string> *targetAttrs) { if (conditions.empty()) return true; if (!targetAttrs) return true; for (const auto &cond : conditions) { auto it = targetAttrs->find(cond.name); if (it == targetAttrs->end()) return false; if (!attributeValueMatches(cond.op, cond.value, it->second)) return false; } return true; } CompoundParts parseCompoundSelector(const std::string &matchSel) { CompoundParts result; size_t pos = 0; Loading Loading @@ -149,14 +240,16 @@ namespace { // bare-tag ancestor requirement is safe there. bool compoundMatches(const CompoundParts &compound, bool hadCombinator, const std::string &tag, const std::vector<std::string> &classes, const std::string &id) 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; return compoundPartsMatch(compound, tag, classes, id); return compoundPartsMatch(compound, tag, classes, id) && attributeConditionsSatisfied(compound.attrConditions, targetAttrs); } // Splits a single (already comma-branch-isolated) selector into its Loading
src/css.h +57 −24 Original line number Diff line number Diff line Loading @@ -120,6 +120,21 @@ namespace libhtmlpp { std::string id; }; /** One "[name]"/"[name=value]"/"[name~=value]"/etc. attribute-selector * condition parsed out of a compound selector, for verifying against a * target element's own attribute map (see collectApproximateMatches's * @p targetAttributes) instead of being ignored the way this matcher * otherwise treats attribute selectors (see approximateSelectorMatch's * own doc comment) -- @p op is "" for a bare "[name]" existence check, * otherwise one of "=", "~=", "|=", "^=", "$=", "*=" per the CSS * attribute-selector spec; @p value is unquoted and only meaningful * when @p op is non-empty. */ struct AttributeCondition { std::string name; std::string op; std::string value; }; class CSSStyleSheet { public: CSSStyleSheet(); Loading Loading @@ -196,36 +211,49 @@ namespace libhtmlpp { * called (e.g. the element's own inline style, set by the caller * before calling this) outranks a later plain rule unless that * rule's value carries "!important" (stripped from the stored * value either way); among this sheet's own rules, a later rule * beats an earlier one at equal priority. This is "last rule of * equal-or-higher precedence wins", not full CSS specificity. * Each matching `@media`/other at-rule block's raw text is appended * to @p mediaRules, skipped if its exact text is already present in * @p seenMediaBlocks (both are caller-owned, so a caller processing * many elements from one document/sheet can share one instance of * each across all of them and avoid repeating an identical block * once per matching element). @p ancestors is forwarded to * approximateSelectorMatch's underlying per-compound check exactly * as described there -- nullptr (the default) preserves this * function's original behavior (leading compounds of a combinator * selector are never verified), a non-null chain makes leading * compounds required to be found in it, in order. Exception: if a * match only went through because one of its ancestor compounds * was unverifiable (an attribute selector or unsupported pseudo- * class stripped down to nothing -- see approximateSelectorMatch's * own doc comment), that rule's display:none/visibility:hidden * values are dropped rather than folded into @p props, since an * ordinary wrong guess mis-styles an element but a wrong guess on * those two properties makes it (and its subtree) disappear * outright; every other property from the same rule is unaffected * and still applies normally. */ * value either way); within each of those two precedence tiers, a * rule with higher approximate specificity (id/class-attribute- * pseudo-class/type-pseudo-element counts, compared the same way a * real cascade does) wins regardless of source order, and only * falls back to "later rule wins" when two matching rules are * exactly as specific as each other -- see computeSpecificity in * css.cpp. Each matching `@media`/other at-rule block's raw text is * appended to @p mediaRules, skipped if its exact text is already * present in @p seenMediaBlocks (both are caller-owned, so a caller * processing many elements from one document/sheet can share one * instance of each across all of them and avoid repeating an * identical block once per matching element). @p ancestors is * forwarded to approximateSelectorMatch's underlying per-compound * check exactly as described there -- nullptr (the default) * preserves this function's original behavior (leading compounds of * a combinator selector are never verified), a non-null chain makes * leading compounds required to be found in it, in order. * Exception: if a match only went through because one of its * ancestor compounds was unverifiable (an attribute selector or * unsupported pseudo-class stripped down to nothing -- see * approximateSelectorMatch's own doc comment), that rule's * display:none/visibility:hidden values are dropped rather than * folded into @p props, since an ordinary wrong guess mis-styles an * element but a wrong guess on those two properties makes it (and * its subtree) disappear outright; every other property from the * same rule is unaffected and still applies normally. @p * targetAttributes, when non-null, is the target element's own * attribute map (name -> value) -- unlike ancestors, the target * element's attributes ARE available to this function's caller * (see getCSSRules), so an attribute-selector condition on the * TARGET compound itself (e.g. ".fade-box[data-fit=fill]", as * opposed to one on an ancestor compound, which still can't be * verified) is checked against it instead of being ignored -- * nullptr (the default) preserves the old ignore-it behavior for * any existing caller that doesn't have an attribute map handy. */ 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 std::vector<AncestorFrame> *ancestors = nullptr) const; const std::vector<AncestorFrame> *ancestors = nullptr, const std::map<std::string,std::string> *targetAttributes = nullptr) const; private: void _skipWhitespace(const std::string &input, size_t &pos) const; Loading @@ -252,6 +280,11 @@ namespace libhtmlpp { std::string tag; std::vector<std::string> classes; std::string id; // Attribute-selector conditions found on the TARGET compound // itself (e.g. the "[data-fit=fill]" in ".fade-box[data-fit= // fill]"), verified against collectApproximateMatches's // @p targetAttributes when given -- see AttributeCondition. std::vector<AttributeCondition> attrConditions; bool hadCombinator; // Every compound left of the target one above, left-to-right/ // outermost-first (e.g. for ".a .b .c", this holds ".a" and ".b" Loading