Commit c773feff authored by jan.koester's avatar jan.koester
Browse files

test

parent 464005e5
Loading
Loading
Loading
Loading
+95 −2
Original line number Diff line number Diff line
@@ -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;
@@ -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
+57 −24
Original line number Diff line number Diff line
@@ -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();
@@ -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;
@@ -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"