Loading src/css.cpp +92 −0 Original line number Diff line number Diff line Loading @@ -163,6 +163,91 @@ namespace { matchSel == "*"; } // Finds the index of the ')' matching the '(' at `openPos` (which must // point at '(' itself), respecting nested parens. Returns npos if // unterminated. size_t findMatchingParen(const std::string &s, size_t openPos) { int depth = 0; for (size_t i = openPos; i < s.size(); ++i) { if (s[i] == '(') ++depth; else if (s[i] == ')') { --depth; if (depth == 0) return i; } } return std::string::npos; } // Splits `inner` (the text between "var(" and its matching ")") at the // first top-level comma (depth 0, not inside a nested "(...)") into a // trimmed custom-property name and a trimmed fallback ("" if no comma // is present, i.e. no fallback was given). void splitVarArgs(const std::string &inner, std::string &name, std::string &fallback) { int depth = 0; size_t commaPos = std::string::npos; for (size_t i = 0; i < inner.size(); ++i) { if (inner[i] == '(') ++depth; else if (inner[i] == ')') --depth; else if (inner[i] == ',' && depth == 0) { commaPos = i; break; } } std::string rawName = (commaPos == std::string::npos) ? inner : inner.substr(0, commaPos); std::string rawFallback = (commaPos == std::string::npos) ? "" : inner.substr(commaPos + 1); name = trim(rawName); fallback = trim(rawFallback); } // Substitutes every var(...) occurrence in `value`, recursively -- // `resolving` (the set of custom-property names currently being // expanded on this call stack) provides cycle detection: a name already // in it resolves to "" instead of recursing forever. std::string substituteVars(const std::string &value, const std::map<std::string,std::string> &customProperties, std::set<std::string> &resolving) { if (value.find("var(") == std::string::npos) return value; std::string out; out.reserve(value.size()); size_t pos = 0; while (pos < value.size()) { if (value.compare(pos, 4, "var(") == 0) { size_t openParen = pos + 3; size_t closeParen = findMatchingParen(value, openParen); if (closeParen == std::string::npos) { // Malformed/unterminated var( -- leave the rest as-is // rather than risk corrupting it. out += value.substr(pos); break; } std::string inner = value.substr(openParen + 1, closeParen - openParen - 1); std::string name, fallback; splitVarArgs(inner, name, fallback); std::string substitution; if (resolving.count(name)) { substitution = ""; // cyclic reference -- invalid per spec } else { auto it = customProperties.find(name); if (it != customProperties.end()) { resolving.insert(name); substitution = substituteVars(it->second, customProperties, resolving); resolving.erase(name); } else if (!fallback.empty()) { substitution = substituteVars(fallback, customProperties, resolving); } else { substitution = ""; // unresolvable, no fallback -- invalid per spec } } out += substitution; pos = closeParen + 1; } else { out += value[pos]; ++pos; } } return out; } } // --- CSSProperty --- Loading Loading @@ -669,3 +754,10 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches( } } std::string libhtmlpp::resolveCSSVariables(const std::string &value, const std::map<std::string,std::string> &customProperties) { std::set<std::string> resolving; return substituteVars(value, customProperties, resolving); } src/css.h +19 −0 Original line number Diff line number Diff line Loading @@ -183,5 +183,24 @@ namespace libhtmlpp { std::vector<CSSRule> _Rules; }; /** Resolves every `var(--name)` / `var(--name, fallback)` reference in * @p value using @p customProperties (custom-property name -> its own * raw stored value, e.g. from parsing "--color-bg: 0,0,0;"), * recursively -- a custom property's own stored value may itself * reference other custom properties (e.g. "--gradient-bg: * var(--something)") -- with cycle detection: a reference cycle is * invalid per the CSS Custom Properties spec, and resolves to "" * rather than looping forever. A referenced name that's both absent * from @p customProperties and has no fallback also resolves to "" * (invalid, matching spec behavior for an unresolvable var()). A * value containing no "var(" at all is returned unchanged (the * overwhelmingly common case). This is pure text substitution -- the * stored value of a custom property is inserted verbatim at the * `var(...)` call's position, exactly as a browser's cascade would; * no CSS value-type awareness (color/length/etc.) is needed or * applied. */ std::string resolveCSSVariables(const std::string &value, const std::map<std::string,std::string> &customProperties); } test/htmlcsstest.cpp +47 −0 Original line number Diff line number Diff line Loading @@ -205,6 +205,53 @@ int main(){ } } // --- resolveCSSVariables --- std::cout << "=== resolveCSSVariables ===" << std::endl; { std::map<std::string,std::string> props = {{"--color-bg", "0,0,0"}, {"--alpha-bg", "1"}}; check(libhtmlpp::resolveCSSVariables("rgba(var(--color-bg),var(--alpha-bg))", props) == "rgba(0,0,0,1)", "simple substitution, multiple var() in one value"); } { std::map<std::string,std::string> props; check(libhtmlpp::resolveCSSVariables("var(--undefined, blue)", props) == "blue", "fallback used when the referenced name is unset"); } { std::map<std::string,std::string> props; check(libhtmlpp::resolveCSSVariables("var(--undefined)", props) == "", "no fallback and unset name resolves to empty (invalid, per spec)"); } { // Mirrors the real-world pattern: --gradient-bg's OWN stored value // references --color-bg/--alpha-bg (multi-hop resolution). std::map<std::string,std::string> props = { {"--color-bg", "10,20,30"}, {"--alpha-bg", "0.5"}, {"--gradient-bg", "rgba(var(--color-bg),var(--alpha-bg))"}, }; check(libhtmlpp::resolveCSSVariables("var(--gradient-bg)", props) == "rgba(10,20,30,0.5)", "multi-hop resolution through an intermediate custom property"); } { // Fallback itself contains a nested var() -- also resolved. std::map<std::string,std::string> props = {{"--color-bg", "0,0,0"}}; check(libhtmlpp::resolveCSSVariables( "var(--gradient-bg, rgba(var(--color-bg),1))", props) == "rgba(0,0,0,1)", "nested var() inside a fallback value is resolved too"); } { // a -> var(b), b -> var(a): cyclic, must not infinite-loop. std::map<std::string,std::string> props = {{"--a", "var(--b)"}, {"--b", "var(--a)"}}; check(libhtmlpp::resolveCSSVariables("var(--a)", props) == "", "cyclic custom-property reference resolves to empty instead of looping forever"); } { check(libhtmlpp::resolveCSSVariables("10px solid red", {}) == "10px solid red", "a value with no var( at all is returned unchanged"); } // --- Summary --- std::cout << "\n=== " << passCount << "/" << testCount << " tests passed ===" << std::endl; Loading Loading
src/css.cpp +92 −0 Original line number Diff line number Diff line Loading @@ -163,6 +163,91 @@ namespace { matchSel == "*"; } // Finds the index of the ')' matching the '(' at `openPos` (which must // point at '(' itself), respecting nested parens. Returns npos if // unterminated. size_t findMatchingParen(const std::string &s, size_t openPos) { int depth = 0; for (size_t i = openPos; i < s.size(); ++i) { if (s[i] == '(') ++depth; else if (s[i] == ')') { --depth; if (depth == 0) return i; } } return std::string::npos; } // Splits `inner` (the text between "var(" and its matching ")") at the // first top-level comma (depth 0, not inside a nested "(...)") into a // trimmed custom-property name and a trimmed fallback ("" if no comma // is present, i.e. no fallback was given). void splitVarArgs(const std::string &inner, std::string &name, std::string &fallback) { int depth = 0; size_t commaPos = std::string::npos; for (size_t i = 0; i < inner.size(); ++i) { if (inner[i] == '(') ++depth; else if (inner[i] == ')') --depth; else if (inner[i] == ',' && depth == 0) { commaPos = i; break; } } std::string rawName = (commaPos == std::string::npos) ? inner : inner.substr(0, commaPos); std::string rawFallback = (commaPos == std::string::npos) ? "" : inner.substr(commaPos + 1); name = trim(rawName); fallback = trim(rawFallback); } // Substitutes every var(...) occurrence in `value`, recursively -- // `resolving` (the set of custom-property names currently being // expanded on this call stack) provides cycle detection: a name already // in it resolves to "" instead of recursing forever. std::string substituteVars(const std::string &value, const std::map<std::string,std::string> &customProperties, std::set<std::string> &resolving) { if (value.find("var(") == std::string::npos) return value; std::string out; out.reserve(value.size()); size_t pos = 0; while (pos < value.size()) { if (value.compare(pos, 4, "var(") == 0) { size_t openParen = pos + 3; size_t closeParen = findMatchingParen(value, openParen); if (closeParen == std::string::npos) { // Malformed/unterminated var( -- leave the rest as-is // rather than risk corrupting it. out += value.substr(pos); break; } std::string inner = value.substr(openParen + 1, closeParen - openParen - 1); std::string name, fallback; splitVarArgs(inner, name, fallback); std::string substitution; if (resolving.count(name)) { substitution = ""; // cyclic reference -- invalid per spec } else { auto it = customProperties.find(name); if (it != customProperties.end()) { resolving.insert(name); substitution = substituteVars(it->second, customProperties, resolving); resolving.erase(name); } else if (!fallback.empty()) { substitution = substituteVars(fallback, customProperties, resolving); } else { substitution = ""; // unresolvable, no fallback -- invalid per spec } } out += substitution; pos = closeParen + 1; } else { out += value[pos]; ++pos; } } return out; } } // --- CSSProperty --- Loading Loading @@ -669,3 +754,10 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches( } } std::string libhtmlpp::resolveCSSVariables(const std::string &value, const std::map<std::string,std::string> &customProperties) { std::set<std::string> resolving; return substituteVars(value, customProperties, resolving); }
src/css.h +19 −0 Original line number Diff line number Diff line Loading @@ -183,5 +183,24 @@ namespace libhtmlpp { std::vector<CSSRule> _Rules; }; /** Resolves every `var(--name)` / `var(--name, fallback)` reference in * @p value using @p customProperties (custom-property name -> its own * raw stored value, e.g. from parsing "--color-bg: 0,0,0;"), * recursively -- a custom property's own stored value may itself * reference other custom properties (e.g. "--gradient-bg: * var(--something)") -- with cycle detection: a reference cycle is * invalid per the CSS Custom Properties spec, and resolves to "" * rather than looping forever. A referenced name that's both absent * from @p customProperties and has no fallback also resolves to "" * (invalid, matching spec behavior for an unresolvable var()). A * value containing no "var(" at all is returned unchanged (the * overwhelmingly common case). This is pure text substitution -- the * stored value of a custom property is inserted verbatim at the * `var(...)` call's position, exactly as a browser's cascade would; * no CSS value-type awareness (color/length/etc.) is needed or * applied. */ std::string resolveCSSVariables(const std::string &value, const std::map<std::string,std::string> &customProperties); }
test/htmlcsstest.cpp +47 −0 Original line number Diff line number Diff line Loading @@ -205,6 +205,53 @@ int main(){ } } // --- resolveCSSVariables --- std::cout << "=== resolveCSSVariables ===" << std::endl; { std::map<std::string,std::string> props = {{"--color-bg", "0,0,0"}, {"--alpha-bg", "1"}}; check(libhtmlpp::resolveCSSVariables("rgba(var(--color-bg),var(--alpha-bg))", props) == "rgba(0,0,0,1)", "simple substitution, multiple var() in one value"); } { std::map<std::string,std::string> props; check(libhtmlpp::resolveCSSVariables("var(--undefined, blue)", props) == "blue", "fallback used when the referenced name is unset"); } { std::map<std::string,std::string> props; check(libhtmlpp::resolveCSSVariables("var(--undefined)", props) == "", "no fallback and unset name resolves to empty (invalid, per spec)"); } { // Mirrors the real-world pattern: --gradient-bg's OWN stored value // references --color-bg/--alpha-bg (multi-hop resolution). std::map<std::string,std::string> props = { {"--color-bg", "10,20,30"}, {"--alpha-bg", "0.5"}, {"--gradient-bg", "rgba(var(--color-bg),var(--alpha-bg))"}, }; check(libhtmlpp::resolveCSSVariables("var(--gradient-bg)", props) == "rgba(10,20,30,0.5)", "multi-hop resolution through an intermediate custom property"); } { // Fallback itself contains a nested var() -- also resolved. std::map<std::string,std::string> props = {{"--color-bg", "0,0,0"}}; check(libhtmlpp::resolveCSSVariables( "var(--gradient-bg, rgba(var(--color-bg),1))", props) == "rgba(0,0,0,1)", "nested var() inside a fallback value is resolved too"); } { // a -> var(b), b -> var(a): cyclic, must not infinite-loop. std::map<std::string,std::string> props = {{"--a", "var(--b)"}, {"--b", "var(--a)"}}; check(libhtmlpp::resolveCSSVariables("var(--a)", props) == "", "cyclic custom-property reference resolves to empty instead of looping forever"); } { check(libhtmlpp::resolveCSSVariables("10px solid red", {}) == "10px solid red", "a value with no var( at all is returned unchanged"); } // --- Summary --- std::cout << "\n=== " << passCount << "/" << testCount << " tests passed ===" << std::endl; Loading