libhtmlpp 1.0.0
Loading...
Searching...
No Matches
css.cpp
Go to the documentation of this file.
1/*******************************************************************************
2Copyright (c) 2021, Jan Koester jan.koester@gmx.net
3All rights reserved.
4
5Redistribution and use in source and binary forms, with or without
6modification, are permitted provided that the following conditions are met:
7 * Redistributions of source code must retain the above copyright
8 notice, this list of conditions and the following disclaimer.
9 * Redistributions in binary form must reproduce the above copyright
10 notice, this list of conditions and the following disclaimer in the
11 documentation and/or other materials provided with the distribution.
12 * Neither the name of the <organization> nor the
13 names of its contributors may be used to endorse or promote products
14 derived from this software without specific prior written permission.
15
16THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
20DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26*******************************************************************************/
27
28#include "css.h"
29#include "exception.h"
30
31#include <algorithm>
32#include <array>
33#include <cctype>
34#include <sstream>
35
36namespace {
37
38 bool isWhitespace(char c) {
39 return c == ' ' || c == '\t' || c == '\n' || c == '\r';
40 }
41
42 std::string trim(const std::string &s) {
43 size_t start = 0;
44 while (start < s.size() && isWhitespace(s[start])) ++start;
45 size_t end = s.size();
46 while (end > start && isWhitespace(s[end - 1])) --end;
47 return s.substr(start, end - start);
48 }
49
50 // Strips a trailing "!important" (case-insensitive, any whitespace
51 // before the "!") from a CSS declaration value, returning whether it was
52 // present. CSSDeclaration's parser captures "!important" as ordinary
53 // trailing text with no special handling, so a caller comparing/storing
54 // plain property values needs this to see past it.
55 bool stripImportant(std::string &value) {
56 size_t bang = value.rfind('!');
57 if (bang == std::string::npos) return false;
58
59 size_t start = value.find_first_not_of(" \t\n\r", bang + 1);
60 size_t end = value.find_last_not_of(" \t\n\r");
61 if (start == std::string::npos || end == std::string::npos || start > end) return false;
62
63 std::string tail = value.substr(start, end - start + 1);
64 std::transform(tail.begin(), tail.end(), tail.begin(),
65 [](unsigned char c) { return std::tolower(c); });
66 if (tail != "important") return false;
67
68 size_t valEnd = bang;
69 while (valEnd > 0 && isWhitespace(value[valEnd - 1])) --valEnd;
70 value.resize(valEnd);
71 return true;
72 }
73
74 // A combinator-stripped compound selector like "div.card.featured#hero"
75 // broken into its optional bare tag (lowercased), every ".class" token,
76 // and an optional "#id" token.
78 std::string tag;
79 std::vector<std::string> classes;
80 std::string id;
81 // Attribute-selector conditions from the ORIGINAL (pre-
82 // stripAttributeSelectors) compound text -- only ever populated for
83 // the target compound (see parseAttributeConditions's callers);
84 // left empty for ancestor compounds, which have no attribute map to
85 // check against. Checked by compoundMatches when a target attribute
86 // map is available, ignored (as before) otherwise.
87 std::vector<libhtmlpp::AttributeCondition> attrConditions;
88 };
89
90 // Parses every "[name]"/"[name=value]"/"[name~=value]"/etc. group found
91 // anywhere in `sel` (a compound selector's RAW text, before
92 // stripAttributeSelectors removes them) into an AttributeCondition --
93 // e.g. "[data-fit=fill]" -> {name="data-fit", op="=", value="fill"},
94 // a bare "[data-atom]" -> {name="data-atom", op="", value=""}. A quoted
95 // value has its surrounding '"'/'\'' stripped. Deliberately approximate
96 // like the rest of this file: doesn't handle a case-sensitivity flag
97 // ("i"/"s" before the closing "]") or backslash-escaped characters --
98 // this matcher only needs to compare against a plain attribute-value
99 // map, not implement the full CSS attribute-selector grammar.
100 std::vector<libhtmlpp::AttributeCondition> parseAttributeConditions(const std::string &sel) {
101 std::vector<libhtmlpp::AttributeCondition> out;
102 size_t pos = 0;
103 while (pos < sel.size()) {
104 if (sel[pos] != '[') { ++pos; continue; }
105 size_t close = sel.find(']', pos + 1);
106 if (close == std::string::npos) break;
107 std::string inner = trim(sel.substr(pos + 1, close - pos - 1));
108 pos = close + 1;
109 if (inner.empty()) continue;
110
112 size_t eq = inner.find('=');
113 if (eq == std::string::npos) {
114 cond.name = inner;
115 } else {
116 size_t opStart = eq;
117 if (eq > 0 && std::string("~|^$*").find(inner[eq - 1]) != std::string::npos)
118 opStart = eq - 1;
119 cond.op = inner.substr(opStart, eq - opStart + 1);
120 cond.name = trim(inner.substr(0, opStart));
121 std::string value = trim(inner.substr(eq + 1));
122 if (value.size() >= 2 &&
123 ((value.front() == '"' && value.back() == '"') ||
124 (value.front() == '\'' && value.back() == '\'')))
125 value = value.substr(1, value.size() - 2);
126 cond.value = value;
127 }
128 out.push_back(std::move(cond));
129 }
130 return out;
131 }
132
133 // Whether `actual` (the target element's real attribute value) satisfies
134 // one attribute-selector operator/expected pair. An unrecognized
135 // operator (shouldn't happen given parseAttributeConditions's own
136 // grammar) is treated as satisfied rather than guessed wrong, same "not
137 // proven to not match" philosophy as the rest of this file.
138 bool attributeValueMatches(const std::string &op, const std::string &expected, const std::string &actual) {
139 if (op.empty()) return true; // bare "[name]" -- existence already confirmed by the caller
140 if (op == "=") return actual == expected;
141 if (op == "~=") {
142 std::istringstream iss(actual);
143 std::string tok;
144 while (iss >> tok) if (tok == expected) return true;
145 return false;
146 }
147 if (op == "^=") return !expected.empty() && actual.compare(0, expected.size(), expected) == 0;
148 if (op == "$=") return !expected.empty() && expected.size() <= actual.size() &&
149 actual.compare(actual.size() - expected.size(), expected.size(), expected) == 0;
150 if (op == "*=") return !expected.empty() && actual.find(expected) != std::string::npos;
151 if (op == "|=") return actual == expected || actual.compare(0, expected.size() + 1, expected + "-") == 0;
152 return true;
153 }
154
155 // Whether every attribute condition parsed off the TARGET compound (see
156 // CompoundParts::attrConditions) holds against @p targetAttrs. Absent
157 // conditions (none, or no attribute map available -- e.g. an ancestor
158 // compound, or a caller that didn't supply one) are vacuously satisfied,
159 // preserving this matcher's original "ignore attribute selectors"
160 // behavior wherever an attribute map genuinely isn't available.
161 bool attributeConditionsSatisfied(const std::vector<libhtmlpp::AttributeCondition> &conditions,
162 const std::map<std::string,std::string> *targetAttrs)
163 {
164 if (conditions.empty()) return true;
165 if (!targetAttrs) return true;
166 for (const auto &cond : conditions) {
167 auto it = targetAttrs->find(cond.name);
168 if (it == targetAttrs->end()) return false;
169 if (!attributeValueMatches(cond.op, cond.value, it->second)) return false;
170 }
171 return true;
172 }
173
174 CompoundParts parseCompoundSelector(const std::string &matchSel) {
175 CompoundParts result;
176 size_t pos = 0;
177 while (pos < matchSel.size()) {
178 char c = matchSel[pos];
179 if (c == '.' || c == '#') {
180 size_t next = matchSel.find_first_of(".#", pos + 1);
181 size_t tokenEnd = (next == std::string::npos) ? matchSel.size() : next;
182 std::string token = matchSel.substr(pos + 1, tokenEnd - (pos + 1));
183 if (c == '.') result.classes.push_back(token);
184 else result.id = token;
185 pos = tokenEnd;
186 } else {
187 size_t next = matchSel.find_first_of(".#", pos);
188 size_t tokenEnd = (next == std::string::npos) ? matchSel.size() : next;
189 result.tag = matchSel.substr(pos, tokenEnd - pos);
190 pos = tokenEnd;
191 }
192 }
193 std::transform(result.tag.begin(), result.tag.end(), result.tag.begin(),
194 [](unsigned char c) { return std::tolower(c); });
195 return result;
196 }
197
198 // Whether `compound`'s tag/classes/id are ALL present on the given
199 // candidate tag/classes/id -- every class listed must be present, not
200 // just one. Shared by compoundMatches (the target element, which adds
201 // its own "bare tag after combinator" guard on top -- see there) and
202 // ancestorChainSatisfies (a specific ancestor frame, which needs no such
203 // guard: a lone tag requirement checked against one concrete candidate
204 // is a meaningful constraint, not the blanket-match hazard a lone tag
205 // with no ancestor to check it against would be). A compound with
206 // nothing specified at all (tag/classes/id all empty -- e.g. it was
207 // purely an attribute selector or pseudo-class, see
208 // stripAttributeSelectors/hasUnsupportedSelectorSyntax) trivially
209 // matches anything: this helper has no opinion on whether an empty
210 // compound should count as a real requirement, that's each caller's
211 // call (compoundMatches rejects it via specifiedSomething,
212 // ancestorChainSatisfies treats it as vacuously satisfied).
213 bool compoundPartsMatch(const CompoundParts &compound, const std::string &tag,
214 const std::vector<std::string> &classes, const std::string &id)
215 {
216 bool tagOk = compound.tag.empty() || compound.tag == tag;
217 bool idOk = compound.id.empty() || compound.id == id;
218 bool classesOk = true;
219 for (const auto &cls : compound.classes) {
220 if (std::find(classes.begin(), classes.end(), cls) == classes.end()) {
221 classesOk = false;
222 break;
223 }
224 }
225 return tagOk && idOk && classesOk;
226 }
227
228 // A selector matches only if EVERY part it specifies is present on this
229 // element -- in particular every class listed, not just one. A bare tag
230 // left over after stripping a combinator's ancestor part (e.g.
231 // "[data-atom=header]>div" reduces to bare "div") is rejected outright
232 // for the TARGET compound, even when @p targetAttrs/ancestor
233 // verification is available: on a real component-library/page-builder
234 // site, a generic "layout plumbing" ancestor condition like
235 // "[data-atom][data-atom=header]" is common enough (used on nearly
236 // every content block) that a bare "div" child of it is still
237 // effectively a near-blanket match once combined with this file's
238 // specificity ordering (see computeSpecificity) -- confirmed on a real
239 // scraped page: relaxing this guard for a "genuinely verified" ancestor
240 // let "[data-atom][data-atom=header]>div{width:var(--a-width)}" (pure
241 // layout plumbing, specificity (0,2,1)) win over a much more specific,
242 // content-driven ".status-dot{width:12px}" (specificity (0,1,0)) rule
243 // purely because the plumbing rule now qualified as a real match, even
244 // though genuinely verifying its own ancestor chain didn't make it safe
245 // to trust the bare-tag side too. A class/id/attribute qualifier
246 // alongside the tag (e.g. "div.foo") is already specific enough to
247 // check on its own and never triggers this rejection.
248 bool compoundMatches(const CompoundParts &compound, bool hadCombinator,
249 const std::string &tag, const std::vector<std::string> &classes,
250 const std::string &id,
251 const std::map<std::string,std::string> *targetAttrs = nullptr)
252 {
253 bool bareTagAfterCombinator = hadCombinator &&
254 compound.classes.empty() && compound.id.empty() &&
255 compound.attrConditions.empty() && !compound.tag.empty();
256 // An attribute condition only counts as "this compound specifies a
257 // real requirement" when @p targetAttrs is actually available to
258 // check it against -- a caller with no attribute map (e.g.
259 // approximateSelectorMatch's own callers, which never pass one)
260 // still can't verify it, so a purely-attribute compound like
261 // "[data-x]" reverts to the old "nothing specified, reject" outcome
262 // for them instead of permissively matching every element.
263 bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() ||
264 !compound.id.empty() ||
265 (!compound.attrConditions.empty() && targetAttrs != nullptr);
266 if (bareTagAfterCombinator || !specifiedSomething) return false;
267
268 return compoundPartsMatch(compound, tag, classes, id) &&
269 attributeConditionsSatisfied(compound.attrConditions, targetAttrs);
270 }
271
272 // Splits a single (already comma-branch-isolated) selector into its
273 // combinator-separated compounds, left-to-right / outermost-ancestor-
274 // first -- e.g. ".a .b > .c" -> [".a", ".b", ".c"]. Combinator type
275 // (space/">"/"+"/"~") is discarded, same simplification this matcher
276 // always made even when it only ever looked at the trailing compound:
277 // it never distinguished child from descendant/sibling combinators.
278 // Verifying ancestors approximately -- present somewhere in the chain,
279 // in left-to-right order, not necessarily adjacent -- is still far more
280 // correct than not verifying them at all (see ancestorChainSatisfies).
281 std::vector<std::string> splitCombinatorChain(const std::string &selector) {
282 std::vector<std::string> parts;
283 std::string cur;
284 for (char c : selector) {
285 if (c == ' ' || c == '\t' || c == '\n' || c == '\r' ||
286 c == '>' || c == '+' || c == '~') {
287 if (!cur.empty()) { parts.push_back(cur); cur.clear(); }
288 } else {
289 cur += c;
290 }
291 }
292 if (!cur.empty()) parts.push_back(cur);
293 return parts;
294 }
295
296 // Whether every compound in `ancestorCompounds` (left-to-right /
297 // outermost-first -- see splitCombinatorChain) can be matched, in that
298 // order, against some frame in `ancestors` (also outermost-first,
299 // closest/immediate-parent last -- see AncestorFrame). Each compound
300 // consumes the ancestor it matched and everything closer to the target
301 // than it, so a later compound can never be satisfied by an ancestor
302 // farther out than one an earlier compound already matched --
303 // approximating real left-to-right descendant-selector semantics
304 // without distinguishing combinator type, requiring adjacency, or
305 // computing true specificity. A compound with NEITHER tag/class/id NOR
306 // any attribute condition (unsafe syntax reduced to nothing, see
307 // splitCombinatorChain's callers -- an unsupported pseudo-class was
308 // present) is vacuously satisfied without consuming an ancestor slot --
309 // an unverifiable requirement shouldn't by itself invalidate an
310 // otherwise plausible match, same "not proven to not match" philosophy
311 // as the rest of this file. If @p usedUnverifiableAncestor is non-null,
312 // it's set to true whenever this happens, so a caller merging
313 // declarations into a props map (see collectApproximateMatches) can
314 // single out display:none/visibility:hidden from such a match -- unlike
315 // most properties, a wrong guess there doesn't just mis-style an
316 // element, it makes the element and its whole subtree disappear from
317 // the import. A compound that DOES carry attribute conditions (e.g.
318 // "[data-fit=fill]" in ".fade-box[data-fit=fill] img") is a real,
319 // checkable requirement: it's matched against each candidate frame's own
320 // attributes (see AncestorFrame::attributes) the same way
321 // compoundPartsMatch checks tag/class/id -- a frame with no attributes
322 // supplied (an empty map, meaning the caller didn't populate that field)
323 // can't refute it, so it's treated as satisfied for that frame rather
324 // than guessed wrong.
325 bool ancestorChainSatisfies(const std::vector<CompoundParts> &ancestorCompounds,
326 const std::vector<libhtmlpp::AncestorFrame> &ancestors,
327 bool *usedUnverifiableAncestor = nullptr)
328 {
329 size_t idx = 0;
330 for (const auto &compound : ancestorCompounds) {
331 bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() ||
332 !compound.id.empty() || !compound.attrConditions.empty();
333 if (!specifiedSomething) {
334 if (usedUnverifiableAncestor) *usedUnverifiableAncestor = true;
335 continue;
336 }
337
338 bool found = false;
339 while (idx < ancestors.size()) {
340 const libhtmlpp::AncestorFrame &frame = ancestors[idx];
341 ++idx;
342 if (compoundPartsMatch(compound, frame.tag, frame.classes, frame.id) &&
343 attributeConditionsSatisfied(compound.attrConditions,
344 frame.attributes.empty() ? nullptr : &frame.attributes)) {
345 found = true;
346 break;
347 }
348 }
349 if (!found) return false;
350 }
351 return true;
352 }
353
354 // Selector syntax this matcher can't safely evaluate -- pseudo-
355 // classes/elements describe state (":hover") this matcher has no way to
356 // check, and "*" would blanket-match every element. Guessing wrong here
357 // is worse than not matching at all. Attribute selectors ("[href]") are
358 // handled separately (see stripAttributeSelectors) rather than rejected
359 // here, since by the time this runs they've already been removed from
360 // whatever compound they were attached to.
361 bool hasUnsupportedSelectorSyntax(const std::string &matchSel) {
362 return matchSel.find(':') != std::string::npos ||
363 matchSel == "*";
364 }
365
366 // Removes every "[...]" attribute-selector segment from a compound
367 // selector (already combinator-stripped -- see stripCombinator), so a
368 // compound like ".card[data-variant=featured]" is still checkable on
369 // its verifiable ".card" part instead of being thrown away entirely.
370 // This matcher has no per-element attribute map to verify the
371 // condition itself (collectApproximateMatches only ever gets a single
372 // element's tag/class/id, see its own doc comment), so the condition is
373 // simply ignored rather than guessed -- on a component-library site
374 // (e.g. a page builder) nearly every layout/visibility toggle is
375 // expressed as "known class + [data-variant=x]" rather than a distinct
376 // class per variant, so rejecting the whole compound meant NONE of
377 // those rules were ever seen. This can still occasionally pick a
378 // sibling variant's declarations when several "[data-variant=...]"
379 // rules share the same base class (last rule of equal priority wins,
380 // the same cascade approximation used everywhere else in this file),
381 // but that is far closer to the source page than matching nothing.
382 // Malformed/unterminated "[" is left in place rather than risk
383 // corrupting the rest of the compound.
384 std::string stripAttributeSelectors(const std::string &matchSel) {
385 if (matchSel.find('[') == std::string::npos) return matchSel;
386 std::string out;
387 out.reserve(matchSel.size());
388 size_t pos = 0;
389 while (pos < matchSel.size()) {
390 if (matchSel[pos] == '[') {
391 size_t close = matchSel.find(']', pos + 1);
392 if (close == std::string::npos) { out += matchSel.substr(pos); break; }
393 pos = close + 1;
394 } else {
395 out += matchSel[pos];
396 ++pos;
397 }
398 }
399 return out;
400 }
401
402 // Finds the index of the ')' matching the '(' at `openPos` (which must
403 // point at '(' itself), respecting nested parens. Returns npos if
404 // unterminated.
405 size_t findMatchingParen(const std::string &s, size_t openPos) {
406 int depth = 0;
407 for (size_t i = openPos; i < s.size(); ++i) {
408 if (s[i] == '(') ++depth;
409 else if (s[i] == ')') {
410 --depth;
411 if (depth == 0) return i;
412 }
413 }
414 return std::string::npos;
415 }
416
417 // Approximate CSS specificity of one full comma-branch selector (e.g.
418 // "div.card#hero[data-x]:hover .child"), as the standard (id-count,
419 // class/attribute/pseudo-class-count, type/pseudo-element-count) triple
420 // -- compared lexicographically, so any number of classes always loses
421 // to a single id, any number of tags always loses to a single class,
422 // same as a real browser's cascade. Used to let a more specific
423 // matching rule win regardless of source order (see
424 // collectApproximateMatches), instead of this file's usual plain
425 // last-rule-wins approximation -- unlike matching itself, specificity
426 // doesn't need per-element context, so it's computed once per rule
427 // from the raw selector text and cached (see _CompoundCacheBranch),
428 // covering compounds this matcher can't safely verify (attribute
429 // selectors, pseudo-classes -- see stripAttributeSelectors/
430 // hasUnsupportedSelectorSyntax) exactly the same as ones it can: real
431 // CSS specificity is a property of the selector text, independent of
432 // whether this approximate matcher happens to be able to verify every
433 // part of it. Deliberately approximate like the rest of this file: a
434 // literal '.'/'#'/'[' inside a quoted attribute value (e.g.
435 // "[href=\"a.b\"]") could miscount since this scans raw text rather
436 // than a real tokenizer, but attribute selectors are always skipped as
437 // one atomic "[...]" unit (never scanned inside), so that specific case
438 // doesn't actually miscount.
439 std::array<int,3> computeSpecificity(const std::string &selector) {
440 std::array<int,3> spec{0, 0, 0};
441 size_t pos = 0;
442 auto skipIdent = [&](size_t p) {
443 while (p < selector.size() &&
444 (std::isalnum(static_cast<unsigned char>(selector[p])) ||
445 selector[p] == '-' || selector[p] == '_' || selector[p] == '\\')) {
446 ++p;
447 }
448 return p;
449 };
450 while (pos < selector.size()) {
451 char c = selector[pos];
452 if (c == '#') {
453 ++spec[0];
454 pos = skipIdent(pos + 1);
455 } else if (c == '.') {
456 ++spec[1];
457 pos = skipIdent(pos + 1);
458 } else if (c == '[') {
459 ++spec[1];
460 size_t close = selector.find(']', pos + 1);
461 pos = (close == std::string::npos) ? selector.size() : close + 1;
462 } else if (c == ':') {
463 bool pseudoElement = pos + 1 < selector.size() && selector[pos + 1] == ':';
464 ++spec[pseudoElement ? 2 : 1];
465 pos = skipIdent(pos + (pseudoElement ? 2 : 1));
466 if (pos < selector.size() && selector[pos] == '(') {
467 size_t close = findMatchingParen(selector, pos);
468 pos = (close == std::string::npos) ? selector.size() : close + 1;
469 }
470 } else if (c == '*') {
471 ++pos; // universal selector: contributes nothing, per spec
472 } else if (std::isalpha(static_cast<unsigned char>(c)) || c == '_') {
473 size_t next = skipIdent(pos);
474 if (next > pos) ++spec[2];
475 pos = next;
476 } else {
477 ++pos; // whitespace/combinators/etc -- not part of any token
478 }
479 }
480 return spec;
481 }
482
483 // Splits `inner` (the text between "var(" and its matching ")") at the
484 // first top-level comma (depth 0, not inside a nested "(...)") into a
485 // trimmed custom-property name and a trimmed fallback ("" if no comma
486 // is present, i.e. no fallback was given).
487 void splitVarArgs(const std::string &inner, std::string &name, std::string &fallback) {
488 int depth = 0;
489 size_t commaPos = std::string::npos;
490 for (size_t i = 0; i < inner.size(); ++i) {
491 if (inner[i] == '(') ++depth;
492 else if (inner[i] == ')') --depth;
493 else if (inner[i] == ',' && depth == 0) { commaPos = i; break; }
494 }
495 std::string rawName = (commaPos == std::string::npos) ? inner : inner.substr(0, commaPos);
496 std::string rawFallback = (commaPos == std::string::npos) ? "" : inner.substr(commaPos + 1);
497 name = trim(rawName);
498 fallback = trim(rawFallback);
499 }
500
501 // Substitutes every var(...) occurrence in `value`, recursively --
502 // `resolving` (the set of custom-property names currently being
503 // expanded on this call stack) provides cycle detection: a name already
504 // in it resolves to "" instead of recursing forever. `sawUnresolved` is
505 // set to true (never reset to false) whenever a var() reference turns
506 // out invalid per spec (cyclic, or absent with no fallback) -- callers
507 // use this to tell "resolved cleanly" apart from "silently substituted
508 // an empty string", since the latter, left in a still-live declaration
509 // like `calc( * 1px + ...)`, is worse than dropping the declaration:
510 // it's syntactically-broken-but-not-obviously-invalid text rather than
511 // a clean absence.
512 std::string substituteVars(const std::string &value,
513 const std::map<std::string,std::string> &customProperties,
514 std::set<std::string> &resolving,
515 bool &sawUnresolved)
516 {
517 if (value.find("var(") == std::string::npos) return value;
518
519 std::string out;
520 out.reserve(value.size());
521 size_t pos = 0;
522 while (pos < value.size()) {
523 if (value.compare(pos, 4, "var(") == 0) {
524 size_t openParen = pos + 3;
525 size_t closeParen = findMatchingParen(value, openParen);
526 if (closeParen == std::string::npos) {
527 // Malformed/unterminated var( -- leave the rest as-is
528 // rather than risk corrupting it.
529 out += value.substr(pos);
530 break;
531 }
532 std::string inner = value.substr(openParen + 1, closeParen - openParen - 1);
533 std::string name, fallback;
534 splitVarArgs(inner, name, fallback);
535
536 std::string substitution;
537 if (resolving.count(name)) {
538 substitution = ""; // cyclic reference -- invalid per spec
539 sawUnresolved = true;
540 } else {
541 auto it = customProperties.find(name);
542 if (it != customProperties.end()) {
543 resolving.insert(name);
544 substitution = substituteVars(it->second, customProperties, resolving, sawUnresolved);
545 resolving.erase(name);
546 } else if (!fallback.empty()) {
547 substitution = substituteVars(fallback, customProperties, resolving, sawUnresolved);
548 } else {
549 substitution = ""; // unresolvable, no fallback -- invalid per spec
550 sawUnresolved = true;
551 }
552 }
553 out += substitution;
554 pos = closeParen + 1;
555 } else {
556 out += value[pos];
557 ++pos;
558 }
559 }
560 return out;
561 }
562
563}
564
565// --- CSSProperty ---
566
568
569libhtmlpp::CSSProperty::CSSProperty(const std::string &name, const std::string &value)
570 : _Name(name), _Value(value) {}
571
573 : _Name(prop._Name), _Value(prop._Value) {}
574
576
578 if (this != &prop) {
579 _Name = prop._Name;
580 _Value = prop._Value;
581 }
582 return *this;
583}
584
585const std::string& libhtmlpp::CSSProperty::getName() const { return _Name; }
586void libhtmlpp::CSSProperty::setName(const std::string &name) { _Name = name; }
587
588const std::string& libhtmlpp::CSSProperty::getValue() const { return _Value; }
589void libhtmlpp::CSSProperty::setValue(const std::string &value) { _Value = value; }
590
591// --- CSSDeclaration ---
592
594
596 : _Properties(decl._Properties) {}
597
599
601 if (this != &decl) {
602 _Properties = decl._Properties;
603 }
604 return *this;
605}
606
607void libhtmlpp::CSSDeclaration::addProperty(const std::string &name, const std::string &value) {
608 std::string tname = trim(name);
609 std::string tvalue = trim(value);
610
611 if (tname.empty()) return;
612
613 for (auto &prop : _Properties) {
614 if (prop.getName() == tname) {
615 prop.setValue(tvalue);
616 return;
617 }
618 }
619 _Properties.emplace_back(tname, tvalue);
620}
621
622void libhtmlpp::CSSDeclaration::removeProperty(const std::string &name) {
623 std::string tname = trim(name);
624 _Properties.erase(
625 std::remove_if(_Properties.begin(), _Properties.end(),
626 [&tname](const CSSProperty &p) { return p.getName() == tname; }),
627 _Properties.end()
628 );
629}
630
632 std::string tname = trim(name);
633 for (const auto &prop : _Properties) {
634 if (prop.getName() == tname) return &prop;
635 }
636 return nullptr;
637}
638
639const std::vector<libhtmlpp::CSSProperty>& libhtmlpp::CSSDeclaration::getProperties() const {
640 return _Properties;
641}
642
644 std::string result;
645 for (size_t i = 0; i < _Properties.size(); ++i) {
646 result += _Properties[i].getName();
647 result += ": ";
648 result += _Properties[i].getValue();
649 result += ";";
650 if (i + 1 < _Properties.size()) result += " ";
651 }
652 return result;
653}
654
655void libhtmlpp::CSSDeclaration::parse(const std::string &input) {
656 _Properties.clear();
657
658 size_t pos = 0;
659 size_t len = input.size();
660
661 while (pos < len) {
662 // Skip whitespace
663 while (pos < len && isWhitespace(input[pos])) ++pos;
664 if (pos >= len) break;
665
666 // Find the colon separating property name from value
667 size_t colon = input.find(':', pos);
668 if (colon == std::string::npos) break;
669
670 std::string propName = trim(input.substr(pos, colon - pos));
671
672 pos = colon + 1;
673
674 // Find the semicolon ending this declaration
675 // Handle parentheses for functions like rgb(), url()
676 size_t valueStart = pos;
677 int parenDepth = 0;
678 bool inSingleQuote = false;
679 bool inDoubleQuote = false;
680
681 while (pos < len) {
682 char c = input[pos];
683
684 if (inSingleQuote) {
685 if (c == '\'') inSingleQuote = false;
686 ++pos;
687 continue;
688 }
689 if (inDoubleQuote) {
690 if (c == '"') inDoubleQuote = false;
691 ++pos;
692 continue;
693 }
694
695 if (c == '\'') { inSingleQuote = true; ++pos; continue; }
696 if (c == '"') { inDoubleQuote = true; ++pos; continue; }
697 if (c == '(') { ++parenDepth; ++pos; continue; }
698 if (c == ')') { if (parenDepth > 0) --parenDepth; ++pos; continue; }
699
700 if (c == ';' && parenDepth == 0) {
701 break;
702 }
703 ++pos;
704 }
705
706 std::string propValue = trim(input.substr(valueStart, pos - valueStart));
707
708 if (!propName.empty()) {
709 addProperty(propName, propValue);
710 }
711
712 if (pos < len && input[pos] == ';') ++pos;
713 }
714}
715
717 _Properties.clear();
718}
719
720// --- CSSRule ---
721
723
724libhtmlpp::CSSRule::CSSRule(const std::string &selector)
725 : _Selector(trim(selector)) {}
726
728 : _Selector(rule._Selector), _Declaration(rule._Declaration) {}
729
731
733 if (this != &rule) {
734 _Selector = rule._Selector;
735 _Declaration = rule._Declaration;
736 }
737 return *this;
738}
739
740const std::string& libhtmlpp::CSSRule::getSelector() const { return _Selector; }
741void libhtmlpp::CSSRule::setSelector(const std::string &selector) { _Selector = trim(selector); }
742
745
746std::string libhtmlpp::CSSRule::serialize(bool formatted) const {
747 std::string result;
748 if (formatted) {
749 result += _Selector + " {\n";
750 for (const auto &prop : _Declaration.getProperties()) {
751 result += " " + prop.getName() + ": " + prop.getValue() + ";\n";
752 }
753 result += "}";
754 } else {
755 result += _Selector + "{";
756 result += _Declaration.serialize();
757 result += "}";
758 }
759 return result;
760}
761
762// --- CSSStyleSheet ---
763
765
767 : _Rules(sheet._Rules) {}
768
770
772 if (this != &sheet) {
773 _Rules = sheet._Rules;
774 _compoundCacheValid = false;
775 }
776 return *this;
777}
778
779void libhtmlpp::CSSStyleSheet::_skipWhitespace(const std::string &input, size_t &pos) const {
780 while (pos < input.size() && isWhitespace(input[pos])) ++pos;
781}
782
783void libhtmlpp::CSSStyleSheet::_skipComment(const std::string &input, size_t &pos) const {
784 if (pos + 1 < input.size() && input[pos] == '/' && input[pos + 1] == '*') {
785 pos += 2;
786 while (pos + 1 < input.size()) {
787 if (input[pos] == '*' && input[pos + 1] == '/') {
788 pos += 2;
789 return;
790 }
791 ++pos;
792 }
793 pos = input.size();
794 }
795}
796
797void libhtmlpp::CSSStyleSheet::parse(const std::string &input) {
798 _Rules.clear();
799 _compoundCacheValid = false;
800
801 size_t pos = 0;
802 size_t len = input.size();
803
804 while (pos < len) {
805 _skipWhitespace(input, pos);
806 if (pos >= len) break;
807
808 // Skip comments
809 if (pos + 1 < len && input[pos] == '/' && input[pos + 1] == '*') {
810 _skipComment(input, pos);
811 continue;
812 }
813
814 // Handle @-rules
815 if (input[pos] == '@') {
816 size_t atStart = pos;
817
818 // Check for @-rules with blocks like @media, @keyframes, @supports, @font-face
819 // and simple @-rules like @import, @charset
820 size_t semiPos = input.find(';', pos);
821 size_t bracePos = input.find('{', pos);
822
823 if (bracePos != std::string::npos && (semiPos == std::string::npos || bracePos < semiPos)) {
824 // Block @-rule: find matching closing brace
825 std::string atSelector = trim(input.substr(atStart, bracePos - atStart));
826 pos = bracePos + 1;
827
828 int depth = 1;
829 size_t blockStart = pos;
830 while (pos < len && depth > 0) {
831 if (input[pos] == '{') ++depth;
832 else if (input[pos] == '}') --depth;
833 if (depth > 0) ++pos;
834 }
835
836 std::string blockContent = input.substr(blockStart, pos - blockStart);
837 pos = (pos < len) ? pos + 1 : pos;
838
839 // Parse nested rules inside the @-block
840 CSSStyleSheet nested;
841 nested.parse(blockContent);
842
843 // Wrap each nested rule with the @-selector
844 for (const auto &nestedRule : nested.getRules()) {
845 CSSRule rule;
846 rule.setSelector(atSelector + " " + nestedRule.getSelector());
847 for (const auto &prop : nestedRule.getDeclaration().getProperties()) {
848 rule.getDeclaration().addProperty(prop.getName(), prop.getValue());
849 }
850 _Rules.push_back(rule);
851 }
852
853 // If no nested rules (e.g. @font-face), store as single rule
854 if (nested.getRuleCount() == 0 && !blockContent.empty()) {
855 CSSRule rule(atSelector);
856 rule.getDeclaration().parse(blockContent);
857 _Rules.push_back(rule);
858 }
859 } else if (semiPos != std::string::npos) {
860 // Simple @-rule ending with semicolon (e.g. @import, @charset)
861 std::string atRule = trim(input.substr(atStart, semiPos - atStart));
862 CSSRule rule(atRule);
863 _Rules.push_back(rule);
864 pos = semiPos + 1;
865 } else {
866 // Malformed @-rule, skip
867 ++pos;
868 }
869 continue;
870 }
871
872 // Standard rule: find selector then { declarations }
873 size_t braceOpen = input.find('{', pos);
874 if (braceOpen == std::string::npos) break;
875
876 std::string selector = trim(input.substr(pos, braceOpen - pos));
877 pos = braceOpen + 1;
878
879 // Find matching closing brace
880 int depth = 1;
881 size_t declStart = pos;
882 while (pos < len && depth > 0) {
883 if (pos + 1 < len && input[pos] == '/' && input[pos + 1] == '*') {
884 _skipComment(input, pos);
885 continue;
886 }
887 if (input[pos] == '{') ++depth;
888 else if (input[pos] == '}') --depth;
889 if (depth > 0) ++pos;
890 }
891
892 std::string declarations = input.substr(declStart, pos - declStart);
893 pos = (pos < len) ? pos + 1 : pos;
894
895 if (!selector.empty()) {
896 CSSRule rule(selector);
897 rule.getDeclaration().parse(declarations);
898 _Rules.push_back(rule);
899 }
900 }
901}
902
904 _Rules.push_back(rule);
905 _compoundCacheValid = false;
906}
907
909 if (index < _Rules.size()) {
910 _Rules.erase(_Rules.begin() + static_cast<std::ptrdiff_t>(index));
911 _compoundCacheValid = false;
912 }
913}
914
916 if (index < _Rules.size()) return &_Rules[index];
917 return nullptr;
918}
919
921 return _Rules.size();
922}
923
924const std::vector<libhtmlpp::CSSRule>& libhtmlpp::CSSStyleSheet::getRules() const {
925 return _Rules;
926}
927
928std::string libhtmlpp::CSSStyleSheet::serialize(bool formatted) const {
929 std::string result;
930 for (size_t i = 0; i < _Rules.size(); ++i) {
931 result += _Rules[i].serialize(formatted);
932 if (formatted) result += "\n";
933 if (i + 1 < _Rules.size() && formatted) result += "\n";
934 }
935 return result;
936}
937
939 _Rules.clear();
940 _compoundCacheValid = false;
941}
942
944 CSSDeclaration decl;
945 decl.parse(style);
946 return decl;
947}
948
950 const std::string &tag,
951 const std::vector<std::string> &classes,
952 const std::string &id,
953 const std::vector<AncestorFrame> *ancestors)
954{
955 std::string tagLower = tag;
956 std::transform(tagLower.begin(), tagLower.end(), tagLower.begin(),
957 [](unsigned char c) { return std::tolower(c); });
958
959 std::istringstream selStream(selector);
960 std::string singleSel;
961 while (std::getline(selStream, singleSel, ',')) {
962 size_t start = singleSel.find_first_not_of(" \t\n\r");
963 if (start == std::string::npos) continue;
964 size_t end = singleSel.find_last_not_of(" \t\n\r");
965 singleSel = singleSel.substr(start, end - start + 1);
966
967 std::vector<std::string> chain = splitCombinatorChain(singleSel);
968 if (chain.empty()) continue;
969 bool hadCombinator = chain.size() > 1;
970
971 std::string matchSel = stripAttributeSelectors(chain.back());
972 if (hasUnsupportedSelectorSyntax(matchSel)) continue;
973
974 CompoundParts compound = parseCompoundSelector(matchSel);
975 compound.attrConditions = parseAttributeConditions(chain.back());
976 if (!compoundMatches(compound, hadCombinator, tagLower, classes, id)) continue;
977
978 if (ancestors && chain.size() > 1) {
979 std::vector<CompoundParts> ancestorCompounds;
980 for (size_t i = 0; i + 1 < chain.size(); ++i) {
981 std::string aSel = stripAttributeSelectors(chain[i]);
982 if (hasUnsupportedSelectorSyntax(aSel)) {
983 ancestorCompounds.push_back({});
984 } else {
985 CompoundParts aCompound = parseCompoundSelector(aSel);
986 aCompound.attrConditions = parseAttributeConditions(chain[i]);
987 ancestorCompounds.push_back(std::move(aCompound));
988 }
989 }
990 if (!ancestorChainSatisfies(ancestorCompounds, *ancestors)) continue;
991 }
992 return true;
993 }
994 return false;
995}
996
997// Pre-parses every rule's selector list (comma-split, trim, strip combinator,
998// reject unsupported syntax, split into tag/classes/id) exactly the way
999// collectApproximateMatches used to redo inline on every call -- see this
1000// cache's doc comment in css.h for why. Must be re-run (via
1001// _compoundCacheValid) whenever _Rules changes.
1002void libhtmlpp::CSSStyleSheet::_rebuildCompoundCache() const {
1003 _compoundCache.clear();
1004 _compoundCache.reserve(_Rules.size());
1005
1006 for (const auto &rule : _Rules) {
1007 std::vector<_CompoundCacheBranch> branches;
1008 const std::string &sel = rule.getSelector();
1009
1010 if (!sel.empty()) {
1011 bool isAtRule = sel[0] == '@';
1012 std::string atWrapper;
1013 std::string innerSel;
1014 bool atRuleUnparseable = false;
1015
1016 if (isAtRule) {
1017 size_t parenDepth = 0;
1018 size_t splitPos = std::string::npos;
1019 for (size_t i = 0; i < sel.size(); ++i) {
1020 if (sel[i] == '(') ++parenDepth;
1021 else if (sel[i] == ')') {
1022 if (parenDepth > 0) --parenDepth;
1023 if (parenDepth == 0) {
1024 splitPos = i + 1;
1025 // A compound condition -- e.g. "@media
1026 // (min-width:768px) and (max-width:992px)" --
1027 // chains multiple parenthesized feature tests
1028 // with "and"/"or"/"not" (media queries and
1029 // @supports both use this). Without this check,
1030 // the wrapper would end after the FIRST feature
1031 // test and everything from "and (...)" onward
1032 // would be misread as the rule's own selector
1033 // list instead of the rest of its condition.
1034 size_t next = sel.find_first_not_of(" \t\n\r", splitPos);
1035 if (next == std::string::npos) break;
1036 size_t kwEnd = next;
1037 while (kwEnd < sel.size() &&
1038 std::isalpha(static_cast<unsigned char>(sel[kwEnd]))) {
1039 ++kwEnd;
1040 }
1041 std::string kw = sel.substr(next, kwEnd - next);
1042 std::transform(kw.begin(), kw.end(), kw.begin(),
1043 [](unsigned char c) { return std::tolower(c); });
1044 if (kw == "and" || kw == "or" || kw == "not") {
1045 size_t afterKw = sel.find_first_not_of(" \t\n\r", kwEnd);
1046 if (afterKw != std::string::npos && sel[afterKw] == '(') {
1047 i = afterKw - 1; // for-loop's ++i lands on afterKw
1048 continue;
1049 }
1050 }
1051 break;
1052 }
1053 }
1054 }
1055 if (splitPos != std::string::npos && splitPos < sel.size()) {
1056 atWrapper = sel.substr(0, splitPos);
1057 size_t innerStart = sel.find_first_not_of(" \t\n\r", splitPos);
1058 if (innerStart != std::string::npos) innerSel = sel.substr(innerStart);
1059 }
1060 if (innerSel.empty()) atRuleUnparseable = true;
1061 }
1062
1063 if (!atRuleUnparseable) {
1064 const std::string &matchTarget = isAtRule ? innerSel : sel;
1065
1066 std::istringstream selStream(matchTarget);
1067 std::string singleSel;
1068 while (std::getline(selStream, singleSel, ',')) {
1069 size_t start = singleSel.find_first_not_of(" \t\n\r");
1070 if (start == std::string::npos) continue;
1071 size_t end = singleSel.find_last_not_of(" \t\n\r");
1072 singleSel = singleSel.substr(start, end - start + 1);
1073
1074 _CompoundCacheBranch branch;
1075 branch.isAtRule = isAtRule;
1076 branch.atWrapper = atWrapper;
1077 branch.trimmedSelector = singleSel;
1078 branch.specificity = computeSpecificity(singleSel);
1079
1080 std::vector<std::string> chain = splitCombinatorChain(singleSel);
1081 bool hadCombinator = chain.size() > 1;
1082 std::string matchSel = chain.empty() ? std::string()
1083 : stripAttributeSelectors(chain.back());
1084 if (chain.empty() || hasUnsupportedSelectorSyntax(matchSel)) {
1085 branch.skip = true;
1086 } else {
1087 CompoundParts compound = parseCompoundSelector(matchSel);
1088 branch.skip = false;
1089 branch.tag = compound.tag;
1090 branch.classes = compound.classes;
1091 branch.id = compound.id;
1092 branch.attrConditions = parseAttributeConditions(chain.back());
1093 branch.hadCombinator = hadCombinator;
1094 for (size_t i = 0; i + 1 < chain.size(); ++i) {
1095 std::string aSel = stripAttributeSelectors(chain[i]);
1096 if (hasUnsupportedSelectorSyntax(aSel)) {
1097 branch.ancestorCompounds.push_back({});
1098 } else {
1099 CompoundParts aCompound = parseCompoundSelector(aSel);
1100 branch.ancestorCompounds.push_back(
1101 {aCompound.tag, aCompound.classes, aCompound.id,
1102 parseAttributeConditions(chain[i])});
1103 }
1104 }
1105 }
1106 branches.push_back(std::move(branch));
1107 }
1108 }
1109 }
1110
1111 _compoundCache.push_back(std::move(branches));
1112 }
1113
1114 _compoundCacheValid = true;
1115}
1116
1118 const std::string &tag,
1119 const std::string &cssClass,
1120 const std::string &id,
1121 std::map<std::string,std::string> &props,
1122 std::string &mediaRules,
1123 std::set<std::string> &seenMediaBlocks,
1124 const std::vector<AncestorFrame> *ancestors,
1125 const std::map<std::string,std::string> *targetAttributes) const
1126{
1127 std::string tagLower = tag;
1128 std::transform(tagLower.begin(), tagLower.end(), tagLower.begin(),
1129 [](unsigned char c) { return std::tolower(c); });
1130
1131 std::vector<std::string> classes;
1132 if (!cssClass.empty()) {
1133 std::istringstream iss(cssClass);
1134 std::string cls;
1135 while (iss >> cls) classes.push_back(cls);
1136 }
1137
1138 // Property names already present in @p props when we're called (e.g.
1139 // the element's own inline style, set by the caller before calling this)
1140 // outrank a plain stylesheet rule, unless that rule is "!important" --
1141 // matching real cascade precedence.
1142 std::set<std::string> inlineKeys;
1143 for (const auto &kv : props) inlineKeys.insert(kv.first);
1144 std::set<std::string> importantKeys;
1145 // Specificity of whichever rule currently "owns" each property, kept
1146 // separately per precedence tier (plain vs !important) so a more
1147 // specific rule wins regardless of source order within its own tier --
1148 // real CSS cascade order is (origin/importance, specificity, source
1149 // order); a later-but-less-specific rule should never beat an earlier
1150 // one (confirmed on a real page-builder site: a plain
1151 // ".fade-box img{width:auto}" was winning over an earlier but more
1152 // specific ".fade-box[data-fit=fill] img{object-fit:cover}" purely
1153 // because it came later in the stylesheet).
1154 std::map<std::string, std::array<int,3>> plainSpecOf;
1155 std::map<std::string, std::array<int,3>> importantSpecOf;
1156
1157 if (!_compoundCacheValid) _rebuildCompoundCache();
1158
1159 for (size_t ri = 0; ri < _Rules.size(); ++ri) {
1160 const CSSRule &rule = _Rules[ri];
1161 const auto &branches = _compoundCache[ri];
1162
1163 for (const auto &branch : branches) {
1164 if (branch.skip) continue;
1165
1166 CompoundParts compound;
1167 compound.tag = branch.tag;
1168 compound.classes = branch.classes;
1169 compound.id = branch.id;
1170 compound.attrConditions = branch.attrConditions;
1171 if (!compoundMatches(compound, branch.hadCombinator, tagLower, classes, id, targetAttributes))
1172 continue;
1173
1174 bool usedUnverifiableAncestor = false;
1175 if (ancestors && !branch.ancestorCompounds.empty()) {
1176 std::vector<CompoundParts> ancestorCompounds;
1177 for (const auto &ac : branch.ancestorCompounds) {
1178 ancestorCompounds.push_back({ac.tag, ac.classes, ac.id, ac.attrConditions});
1179 }
1180 if (!ancestorChainSatisfies(ancestorCompounds, *ancestors, &usedUnverifiableAncestor))
1181 continue;
1182 }
1183
1184 if (branch.isAtRule) {
1185 // The same @media block, once present anywhere in the final
1186 // output, applies document-wide regardless of which element
1187 // it's attached to -- so including it more than once across
1188 // many matching elements is pure bloat, not a correctness
1189 // requirement.
1190 std::string block = branch.atWrapper + " { " + branch.trimmedSelector + " { ";
1191 for (const auto &prop : rule.getDeclaration().getProperties()) {
1192 block += prop.getName() + ": " + prop.getValue() + "; ";
1193 }
1194 block += "} } ";
1195 if (seenMediaBlocks.insert(block).second) {
1196 mediaRules += block;
1197 }
1198 } else {
1199 for (const auto &prop : rule.getDeclaration().getProperties()) {
1200 std::string value = prop.getValue();
1201 bool isImportant = stripImportant(value);
1202 const std::string &key = prop.getName();
1203
1204 // Inline style and an already-!important value both
1205 // outrank a later plain rule.
1206 if (inlineKeys.count(key) && !isImportant) continue;
1207 if (importantKeys.count(key) && !isImportant) continue;
1208
1209 // Within a precedence tier (plain vs !important, see
1210 // above), a rule with LOWER specificity than whichever
1211 // one currently owns this property loses -- same as a
1212 // real cascade. Equal specificity still falls through
1213 // to "later rule wins" below, matching real CSS.
1214 auto &specOf = isImportant ? importantSpecOf : plainSpecOf;
1215 auto specIt = specOf.find(key);
1216 if (specIt != specOf.end() && specIt->second > branch.specificity)
1217 continue;
1218
1219 // A match that only went through because an ancestor
1220 // condition we can't verify (see
1221 // ancestorChainSatisfies/usedUnverifiableAncestor) was
1222 // treated as vacuously satisfied is exactly the kind of
1223 // "maybe wrong" guess this file otherwise tolerates --
1224 // except here: display:none/visibility:hidden don't
1225 // just mis-style the element, they remove it and its
1226 // whole subtree from the import. Drop just these two
1227 // destructive values from an unverifiable match; every
1228 // other property from the same rule still applies.
1229 if (usedUnverifiableAncestor &&
1230 ((key == "display" && value == "none") ||
1231 (key == "visibility" && value == "hidden")))
1232 continue;
1233
1234 props[key] = value;
1235 specOf[key] = branch.specificity;
1236 if (isImportant) importantKeys.insert(key);
1237 }
1238 }
1239 }
1240 }
1241}
1242
1243std::string libhtmlpp::resolveCSSVariables(const std::string &value,
1244 const std::map<std::string,std::string> &customProperties,
1245 bool *unresolved)
1246{
1247 std::set<std::string> resolving;
1248 bool sawUnresolved = false;
1249 std::string result = substituteVars(value, customProperties, resolving, sawUnresolved);
1250 if (unresolved) *unresolved = sawUnresolved;
1251 return result;
1252}
1253
void addProperty(const std::string &name, const std::string &value)
Definition css.cpp:607
void removeProperty(const std::string &name)
Definition css.cpp:622
const std::vector< CSSProperty > & getProperties() const
Definition css.cpp:639
std::string serialize() const
Definition css.cpp:643
const CSSProperty * getProperty(const std::string &name) const
Definition css.cpp:631
void parse(const std::string &input)
Definition css.cpp:655
CSSDeclaration & operator=(const CSSDeclaration &decl)
Definition css.cpp:600
void setName(const std::string &name)
Definition css.cpp:586
const std::string & getName() const
Definition css.cpp:585
void setValue(const std::string &value)
Definition css.cpp:589
CSSProperty & operator=(const CSSProperty &prop)
Definition css.cpp:577
const std::string & getValue() const
Definition css.cpp:588
CSSDeclaration & getDeclaration()
Definition css.cpp:743
std::string serialize(bool formatted=false) const
Definition css.cpp:746
void setSelector(const std::string &selector)
Definition css.cpp:741
CSSRule & operator=(const CSSRule &rule)
Definition css.cpp:732
const std::string & getSelector() const
Definition css.cpp:740
void parse(const std::string &input)
Definition css.cpp:797
const std::vector< CSSRule > & getRules() const
Definition css.cpp:924
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 std::map< std::string, std::string > *targetAttributes=nullptr) const
Runs every rule in this sheet through approximateSelectorMatch against the element identified by tag/...
Definition css.cpp:1117
CSSStyleSheet & operator=(const CSSStyleSheet &sheet)
Definition css.cpp:771
static CSSDeclaration parseInlineStyle(const std::string &style)
Definition css.cpp:943
std::string serialize(bool formatted=false) const
Definition css.cpp:928
const CSSRule * getRule(size_t index) const
Definition css.cpp:915
static bool approximateSelectorMatch(const std::string &selector, const std::string &tag, const std::vector< std::string > &classes, const std::string &id, const std::vector< AncestorFrame > *ancestors=nullptr)
Conservative, NOT spec-complete selector match: selector (a single selector, or a comma-separated lis...
Definition css.cpp:949
void addRule(const CSSRule &rule)
Definition css.cpp:903
void removeRule(size_t index)
Definition css.cpp:908
size_t getRuleCount() const
Definition css.cpp:920
size_t findMatchingParen(const std::string &s, size_t openPos)
Definition css.cpp:405
std::vector< libhtmlpp::AttributeCondition > parseAttributeConditions(const std::string &sel)
Definition css.cpp:100
bool isWhitespace(char c)
Definition css.cpp:38
std::string stripAttributeSelectors(const std::string &matchSel)
Definition css.cpp:384
void splitVarArgs(const std::string &inner, std::string &name, std::string &fallback)
Definition css.cpp:487
bool attributeValueMatches(const std::string &op, const std::string &expected, const std::string &actual)
Definition css.cpp:138
CompoundParts parseCompoundSelector(const std::string &matchSel)
Definition css.cpp:174
bool compoundMatches(const CompoundParts &compound, bool hadCombinator, const std::string &tag, const std::vector< std::string > &classes, const std::string &id, const std::map< std::string, std::string > *targetAttrs=nullptr)
Definition css.cpp:248
bool attributeConditionsSatisfied(const std::vector< libhtmlpp::AttributeCondition > &conditions, const std::map< std::string, std::string > *targetAttrs)
Definition css.cpp:161
bool stripImportant(std::string &value)
Definition css.cpp:55
std::string substituteVars(const std::string &value, const std::map< std::string, std::string > &customProperties, std::set< std::string > &resolving, bool &sawUnresolved)
Definition css.cpp:512
std::array< int, 3 > computeSpecificity(const std::string &selector)
Definition css.cpp:439
std::string trim(const std::string &s)
Definition css.cpp:42
bool hasUnsupportedSelectorSyntax(const std::string &matchSel)
Definition css.cpp:361
bool ancestorChainSatisfies(const std::vector< CompoundParts > &ancestorCompounds, const std::vector< libhtmlpp::AncestorFrame > &ancestors, bool *usedUnverifiableAncestor=nullptr)
Definition css.cpp:325
std::vector< std::string > splitCombinatorChain(const std::string &selector)
Definition css.cpp:281
bool compoundPartsMatch(const CompoundParts &compound, const std::string &tag, const std::vector< std::string > &classes, const std::string &id)
Definition css.cpp:213
std::string resolveCSSVariables(const std::string &value, const std::map< std::string, std::string > &customProperties, bool *unresolved=nullptr)
Resolves every var(--name) / var(--name, fallback) reference in value using customProperties (custom-...
Definition css.cpp:1243
std::vector< libhtmlpp::AttributeCondition > attrConditions
Definition css.cpp:87
std::vector< std::string > classes
Definition css.cpp:79
One ancestor of the element being matched, for the optional ancestor chain approximateSelectorMatch/c...
Definition css.h:125
std::string tag
Definition css.h:126
std::vector< std::string > classes
Definition css.h:127
std::map< std::string, std::string > attributes
Definition css.h:129
std::string id
Definition css.h:128
One "[name]"/"[name=value]"/"[name~=value]"/etc.
Definition css.h:141