Loading src/css.cpp +128 −36 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "exception.h" #include <algorithm> #include <cctype> #include <sstream> namespace { Loading Loading @@ -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; } Loading @@ -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(); Loading Loading @@ -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; } } Loading Loading @@ -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) { Loading Loading @@ -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; Loading @@ -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()) { Loading @@ -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); Loading @@ -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() + "; "; } Loading src/css.h +26 −0 Original line number Diff line number Diff line Loading @@ -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 Loading test/csstest.cpp +39 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading
src/css.cpp +128 −36 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "exception.h" #include <algorithm> #include <cctype> #include <sstream> namespace { Loading Loading @@ -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; } Loading @@ -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(); Loading Loading @@ -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; } } Loading Loading @@ -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) { Loading Loading @@ -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; Loading @@ -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()) { Loading @@ -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); Loading @@ -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() + "; "; } Loading
src/css.h +26 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
test/csstest.cpp +39 −0 Original line number Diff line number Diff line Loading @@ -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; Loading