Commit 52da8ff7 authored by Jan Köster's avatar Jan Köster
Browse files

test

parent f351efa7
Loading
Loading
Loading
Loading
+128 −36
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "exception.h"

#include <algorithm>
#include <cctype>
#include <sstream>

namespace {
@@ -469,6 +470,7 @@ libhtmlpp::CSSStyleSheet::~CSSStyleSheet() {}
libhtmlpp::CSSStyleSheet& libhtmlpp::CSSStyleSheet::operator=(const CSSStyleSheet &sheet) {
    if (this != &sheet) {
        _Rules = sheet._Rules;
        _compoundCacheValid = false;
    }
    return *this;
}
@@ -493,6 +495,7 @@ void libhtmlpp::CSSStyleSheet::_skipComment(const std::string &input, size_t &po

void libhtmlpp::CSSStyleSheet::parse(const std::string &input) {
    _Rules.clear();
    _compoundCacheValid = false;

    size_t pos = 0;
    size_t len = input.size();
@@ -598,11 +601,13 @@ void libhtmlpp::CSSStyleSheet::parse(const std::string &input) {

void libhtmlpp::CSSStyleSheet::addRule(const CSSRule &rule) {
    _Rules.push_back(rule);
    _compoundCacheValid = false;
}

void libhtmlpp::CSSStyleSheet::removeRule(size_t index) {
    if (index < _Rules.size()) {
        _Rules.erase(_Rules.begin() + static_cast<std::ptrdiff_t>(index));
        _compoundCacheValid = false;
    }
}

@@ -631,6 +636,7 @@ std::string libhtmlpp::CSSStyleSheet::serialize(bool formatted) const {

void libhtmlpp::CSSStyleSheet::clear() {
    _Rules.clear();
    _compoundCacheValid = false;
}

libhtmlpp::CSSDeclaration libhtmlpp::CSSStyleSheet::parseInlineStyle(const std::string &style) {
@@ -666,36 +672,24 @@ bool libhtmlpp::CSSStyleSheet::approximateSelectorMatch(const std::string &selec
    return false;
}

void libhtmlpp::CSSStyleSheet::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<std::string> classes;
    if (!cssClass.empty()) {
        std::istringstream iss(cssClass);
        std::string cls;
        while (iss >> cls) classes.push_back(cls);
    }

    // Property names already present in @p props when we're called (e.g.
    // the element's own inline style, set by the caller before calling this)
    // outrank a plain stylesheet rule, unless that rule is "!important" --
    // matching real cascade precedence.
    std::set<std::string> inlineKeys;
    for (const auto &kv : props) inlineKeys.insert(kv.first);
    std::set<std::string> importantKeys;
// Pre-parses every rule's selector list (comma-split, trim, strip combinator,
// reject unsupported syntax, split into tag/classes/id) exactly the way
// collectApproximateMatches used to redo inline on every call -- see this
// cache's doc comment in css.h for why. Must be re-run (via
// _compoundCacheValid) whenever _Rules changes.
void libhtmlpp::CSSStyleSheet::_rebuildCompoundCache() const {
    _compoundCache.clear();
    _compoundCache.reserve(_Rules.size());

    for (const auto &rule : _Rules) {
        std::vector<_CompoundCacheBranch> branches;
        const std::string &sel = rule.getSelector();
        if (sel.empty()) continue;

        if (!sel.empty()) {
            bool isAtRule = sel[0] == '@';
            std::string atWrapper;
            std::string innerSel;
            bool atRuleUnparseable = false;

            if (isAtRule) {
                size_t parenDepth = 0;
@@ -704,7 +698,36 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches(
                    if (sel[i] == '(') ++parenDepth;
                    else if (sel[i] == ')') {
                        if (parenDepth > 0) --parenDepth;
                    if (parenDepth == 0) { splitPos = i + 1; break; }
                        if (parenDepth == 0) {
                            splitPos = i + 1;
                            // A compound condition -- e.g. "@media
                            // (min-width:768px) and (max-width:992px)" --
                            // chains multiple parenthesized feature tests
                            // with "and"/"or"/"not" (media queries and
                            // @supports both use this). Without this check,
                            // the wrapper would end after the FIRST feature
                            // test and everything from "and (...)" onward
                            // would be misread as the rule's own selector
                            // list instead of the rest of its condition.
                            size_t next = sel.find_first_not_of(" \t\n\r", splitPos);
                            if (next == std::string::npos) break;
                            size_t kwEnd = next;
                            while (kwEnd < sel.size() &&
                                   std::isalpha(static_cast<unsigned char>(sel[kwEnd]))) {
                                ++kwEnd;
                            }
                            std::string kw = sel.substr(next, kwEnd - next);
                            std::transform(kw.begin(), kw.end(), kw.begin(),
                                [](unsigned char c) { return std::tolower(c); });
                            if (kw == "and" || kw == "or" || kw == "not") {
                                size_t afterKw = sel.find_first_not_of(" \t\n\r", kwEnd);
                                if (afterKw != std::string::npos && sel[afterKw] == '(') {
                                    i = afterKw - 1; // for-loop's ++i lands on afterKw
                                    continue;
                                }
                            }
                            break;
                        }
                    }
                }
                if (splitPos != std::string::npos && splitPos < sel.size()) {
@@ -712,9 +735,10 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches(
                    size_t innerStart = sel.find_first_not_of(" \t\n\r", splitPos);
                    if (innerStart != std::string::npos) innerSel = sel.substr(innerStart);
                }
            if (innerSel.empty()) continue;
                if (innerSel.empty()) atRuleUnparseable = true;
            }

            if (!atRuleUnparseable) {
                const std::string &matchTarget = isAtRule ? innerSel : sel;

                std::istringstream selStream(matchTarget);
@@ -725,15 +749,83 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches(
                    size_t end = singleSel.find_last_not_of(" \t\n\r");
                    singleSel = singleSel.substr(start, end - start + 1);

            if (!approximateSelectorMatch(singleSel, tag, classes, id)) continue;
                    _CompoundCacheBranch branch;
                    branch.isAtRule = isAtRule;
                    branch.atWrapper = atWrapper;
                    branch.trimmedSelector = singleSel;

            if (isAtRule) {
                    bool hadCombinator = false;
                    std::string matchSel = stripCombinator(singleSel, hadCombinator);
                    if (hasUnsupportedSelectorSyntax(matchSel)) {
                        branch.skip = true;
                    } else {
                        CompoundParts compound = parseCompoundSelector(matchSel);
                        branch.skip = false;
                        branch.tag = compound.tag;
                        branch.classes = compound.classes;
                        branch.id = compound.id;
                        branch.hadCombinator = hadCombinator;
                    }
                    branches.push_back(std::move(branch));
                }
            }
        }

        _compoundCache.push_back(std::move(branches));
    }

    _compoundCacheValid = true;
}

void libhtmlpp::CSSStyleSheet::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::string tagLower = tag;
    std::transform(tagLower.begin(), tagLower.end(), tagLower.begin(),
        [](unsigned char c) { return std::tolower(c); });

    std::vector<std::string> classes;
    if (!cssClass.empty()) {
        std::istringstream iss(cssClass);
        std::string cls;
        while (iss >> cls) classes.push_back(cls);
    }

    // Property names already present in @p props when we're called (e.g.
    // the element's own inline style, set by the caller before calling this)
    // outrank a plain stylesheet rule, unless that rule is "!important" --
    // matching real cascade precedence.
    std::set<std::string> inlineKeys;
    for (const auto &kv : props) inlineKeys.insert(kv.first);
    std::set<std::string> importantKeys;

    if (!_compoundCacheValid) _rebuildCompoundCache();

    for (size_t ri = 0; ri < _Rules.size(); ++ri) {
        const CSSRule &rule = _Rules[ri];
        const auto &branches = _compoundCache[ri];

        for (const auto &branch : branches) {
            if (branch.skip) continue;

            CompoundParts compound;
            compound.tag = branch.tag;
            compound.classes = branch.classes;
            compound.id = branch.id;
            if (!compoundMatches(compound, branch.hadCombinator, tagLower, classes, id)) continue;

            if (branch.isAtRule) {
                // The same @media block, once present anywhere in the final
                // output, applies document-wide regardless of which element
                // it's attached to -- so including it more than once across
                // many matching elements is pure bloat, not a correctness
                // requirement.
                std::string block = atWrapper + " { " + singleSel + " { ";
                std::string block = branch.atWrapper + " { " + branch.trimmedSelector + " { ";
                for (const auto &prop : rule.getDeclaration().getProperties()) {
                    block += prop.getName() + ": " + prop.getValue() + "; ";
                }
+26 −0
Original line number Diff line number Diff line
@@ -181,6 +181,32 @@ namespace libhtmlpp {
        void _skipWhitespace(const std::string &input, size_t &pos) const;
        void _skipComment(const std::string &input, size_t &pos) const;
        std::vector<CSSRule> _Rules;

        // collectApproximateMatches is called once per imported HTML element
        // (see htmlimport.cpp), and re-parsing every rule's selector list
        // (comma-split, trim, strip combinator, reject unsupported syntax)
        // fresh on every single one of those calls is pure waste: none of
        // that depends on the element being matched, only on _Rules, which
        // doesn't change between them. On a real page-builder site (~2500
        // rules from ~115 <style> blocks, ~2000 importable elements) that
        // redundant re-parsing was confirmed to cost over half the total
        // import time. This caches each rule's already-split/filtered
        // selector branches the first time collectApproximateMatches runs
        // after a mutation, so the expensive parsing happens O(rules) times
        // total instead of O(rules * elements).
        struct _CompoundCacheBranch {
            bool skip;            // true if this branch's syntax can't be safely evaluated
            bool isAtRule;
            std::string atWrapper;      // e.g. "@media (max-width: 600px)" -- only set if isAtRule
            std::string trimmedSelector; // the (possibly per-branch) selector text, for at-rule block reconstruction
            std::string tag;
            std::vector<std::string> classes;
            std::string id;
            bool hadCombinator;
        };
        mutable std::vector<std::vector<_CompoundCacheBranch>> _compoundCache;
        mutable bool _compoundCacheValid = false;
        void _rebuildCompoundCache() const;
    };

    /** Resolves every `var(--name)` / `var(--name, fallback)` reference in
+39 −0
Original line number Diff line number Diff line
@@ -226,6 +226,45 @@ int main(){
        check(out.find("color: red;") != std::string::npos, "roundtrip contains color");
    }

    // --- collectApproximateMatches: mutation invalidates the cached,
    // pre-parsed selector list it relies on (see the doc comment on
    // CSSStyleSheet's private _compoundCache in css.h) -- a real page-
    // builder site with ~2500 rules across ~115 <style> blocks was found to
    // spend most of a 24s import re-parsing every rule's selector on every
    // single element queried against it; this cache fixes that, but only if
    // every mutator (addRule/removeRule/parse/clear/operator=) correctly
    // invalidates it. ---
    std::cout << "=== collectApproximateMatches cache invalidation ===" << std::endl;
    {
        libhtmlpp::CSSStyleSheet sheet;
        sheet.parse(".foo { color: red; }");

        std::map<std::string,std::string> props;
        std::string mediaRules;
        std::set<std::string> seenMediaBlocks;
        sheet.collectApproximateMatches("div", "foo", "", props, mediaRules, seenMediaBlocks);
        check(props["color"] == "red",
              "first collectApproximateMatches call builds the cache and matches the initial rule");

        // Mutate AFTER the cache above was already built and used.
        libhtmlpp::CSSRule extra(".bar");
        extra.getDeclaration().addProperty("background-color", "blue");
        sheet.addRule(extra);

        std::map<std::string,std::string> props2;
        std::set<std::string> seenMediaBlocks2;
        sheet.collectApproximateMatches("div", "foo bar", "", props2, mediaRules, seenMediaBlocks2);
        check(props2["color"] == "red" && props2["background-color"] == "blue",
              "addRule() after the cache is built invalidates it -- the new rule is matched, not silently ignored");

        sheet.removeRule(0);
        std::map<std::string,std::string> props3;
        std::set<std::string> seenMediaBlocks3;
        sheet.collectApproximateMatches("div", "foo bar", "", props3, mediaRules, seenMediaBlocks3);
        check(props3.count("color") == 0 && props3["background-color"] == "blue",
              "removeRule() after the cache is built invalidates it -- the removed rule stops matching");
    }

    // --- Summary ---
    std::cout << "\n=== " << passCount << "/" << testCount << " tests passed ===" << std::endl;