diff --git a/src/web/vaev-style/rules.cpp b/src/web/vaev-style/rules.cpp index bd3ca857..f72d600d 100644 --- a/src/web/vaev-style/rules.cpp +++ b/src/web/vaev-style/rules.cpp @@ -1,5 +1,5 @@ #include "rules.h" - +#include "selector-matching.h" #include "decls.h" namespace Vaev::Style { @@ -9,7 +9,7 @@ static bool DEBUG_RULE = false; // MARK: StyleRule ------------------------------------------------------------- Opt StyleRule::matchWithSpecificity(Markup::Element const& el) const { - return selector.matchWithSpecificity(el); + return matchSelectorWithSpecificity(selector,el); } void StyleRule::repr(Io::Emit& e) const { diff --git a/src/web/vaev-style/select.cpp b/src/web/vaev-style/select.cpp index aa25b211..cad9a172 100644 --- a/src/web/vaev-style/select.cpp +++ b/src/web/vaev-style/select.cpp @@ -1,5 +1,4 @@ #include "select.h" - #include "values.h" namespace Vaev::Style { @@ -51,258 +50,6 @@ Spec spec(Selector const& s) { }); } -// MARK: Selector Matching ----------------------------------------------------- - -// https://www.w3.org/TR/selectors-4/#descendant-combinators -static bool _matchDescendant(Selector const& s, Markup::Element const& e) { - Cursor curr = &e; - while (curr->hasParent()) { - auto& parent = curr->parentNode(); - if (auto el = parent.is()) - if (s.match(*el)) - return true; - curr = &parent; - } - return false; -} - -// https://www.w3.org/TR/selectors-4/#child-combinators -static bool _matchChild(Selector const& s, Markup::Element const& e) { - if (not e.hasParent()) - return false; - - auto& parent = e.parentNode(); - if (auto el = parent.is()) - return s.match(*el); - return false; -} - -// https://www.w3.org/TR/selectors-4/#adjacent-sibling-combinators -static bool _matchAdjacent(Selector const& s, Markup::Element const& e) { - if (not e.hasPreviousSibling()) - return false; - - auto prev = e.previousSibling(); - if (auto el = prev.is()) - return s.match(*el); - return false; -} - -// https://www.w3.org/TR/selectors-4/#general-sibling-combinators -static bool _matchSubsequent(Selector const& s, Markup::Element const& e) { - Cursor curr = &e; - while (curr->hasPreviousSibling()) { - auto prev = curr->previousSibling(); - if (auto el = prev.is()) - if (s.match(*el)) - return true; - curr = &prev.unwrap(); - } - return false; -} - -static bool _match(Infix const& s, Markup::Element const& e) { - if (not s.rhs->match(e)) - return false; - - switch (s.type) { - case Infix::DESCENDANT: // ' ' - return _matchDescendant(*s.lhs, e); - - case Infix::CHILD: // > - return _matchChild(*s.lhs, e); - - case Infix::ADJACENT: // + - return _matchAdjacent(*s.lhs, e); - - case Infix::SUBSEQUENT: // ~ - return _matchSubsequent(*s.lhs, e); - - default: - logWarnIf(DEBUG_SELECTORS, "unimplemented selector: {}", s); - return false; - } -} - -static bool _match(Nfix const& s, Markup::Element const& el) { - switch (s.type) { - case Nfix::AND: - for (auto& inner : s.inners) - if (not inner.match(el)) - return false; - return true; - - // 4.1. Selector Lists - // https://www.w3.org/TR/selectors-4/#grouping - // and - // 4.2. The Matches-Any Pseudo-class: :is() - // https://www.w3.org/TR/selectors-4/#matchess - case Nfix::OR: - for (auto& inner : s.inners) - if (inner.match(el)) - return true; - return false; - - case Nfix::NOT: - return not s.inners[0].match(el); - - case Nfix::WHERE: - return not s.inners[0].match(el); - - default: - logWarnIf(DEBUG_SELECTORS, "unimplemented selector: {}", s); - return false; - } -} - -// 5.1. Type (tag name) selector -// https://www.w3.org/TR/selectors-4/#type -static bool _match(TypeSelector const& s, Markup::Element const& el) { - return el.tagName == s.type; -} - -static bool _match(IdSelector const& s, Markup::Element const& el) { - return el.id() == s.id; -} - -static bool _match(ClassSelector const& s, Markup::Element const& el) { - return el.classList.contains(s.class_); -} - -// 8.2. The Link History Pseudo-classes: :link and :visited -// https://www.w3.org/TR/selectors-4/#link - -static bool _matchLink(Markup::Element const& el) { - return el.tagName == Html::A and el.hasAttribute(Html::HREF_ATTR); -} - -// 14.3.3. :first-child pseudo-class -// https://www.w3.org/TR/selectors-4/#the-first-child-pseudo - -static bool _matchFirstChild(Markup::Element const& e) { - Cursor curr = &e; - while (curr->hasPreviousSibling()) { - auto prev = curr->previousSibling(); - if (auto el = prev.is()) - return false; - curr = &prev.unwrap(); - } - return true; -} - -// 14.3.4. :last-child pseudo-class -// https://www.w3.org/TR/selectors-4/#the-last-child-pseudo - -static bool _matchLastChild(Markup::Element const& e) { - Cursor curr = &e; - while (curr->hasNextSibling()) { - auto next = curr->nextSibling(); - if (auto el = next.is()) - return false; - curr = &next.unwrap(); - } - return true; -} - -// 14.4.3. :first-of-type pseudo-class -// https://www.w3.org/TR/selectors-4/#the-first-of-type-pseudo - -static bool _matchFirstOfType(Markup::Element const& e) { - Cursor curr = &e; - auto tag = e.tagName; - - while (curr->hasPreviousSibling()) { - auto prev = curr->previousSibling(); - if (auto el = prev.is()) - if (e.tagName == tag) - return false; - curr = &prev.unwrap(); - } - return true; -} - -// 14.4.4. :last-of-type pseudo-class -// https://www.w3.org/TR/selectors-4/#the-last-of-type-pseudo - -static bool _matchLastOfType(Markup::Element const& e) { - Cursor curr = &e; - auto tag = e.tagName; - - while (curr->hasNextSibling()) { - auto prev = curr->nextSibling(); - if (auto el = prev.is()) - if (e.tagName == tag) - return false; - curr = &prev.unwrap(); - } - return true; -} - -static bool _match(Pseudo const& s, Markup::Element const& el) { - switch (s.type) { - case Pseudo::LINK: - return _matchLink(el); - - case Pseudo::ROOT: - return el.tagName == Html::HTML; - - case Pseudo::FIRST_OF_TYPE: - return _matchFirstOfType(el); - - case Pseudo::LAST_OF_TYPE: - return _matchLastOfType(el); - - case Pseudo::FIRST_CHILD: - return _matchFirstChild(el); - - case Pseudo::LAST_CHILD: - return _matchLastChild(el); - - default: - logDebugIf(DEBUG_SELECTORS, "unimplemented pseudo class: {}", s); - return false; - } -} - -// 5.2. Universal selector -// https://www.w3.org/TR/selectors-4/#the-universal-selector -static bool _match(UniversalSelector const&, Markup::Element const&) { - return true; -} - -// MARK: Selector -------------------------------------------------------------- - -bool Selector::match(Markup::Element const& el) const { - return visit( - [&](auto const& s) { - if constexpr (requires { _match(s, el); }) - return _match(s, el); - - logWarnIf(DEBUG_SELECTORS, "unimplemented selector: {}", s); - return false; - } - ); -} - -Opt Selector::matchWithSpecificity(Markup::Element const& el) const { - return visit(Visitor{ - [&](Nfix const& n) -> Opt { - if (n.type == Nfix::OR) { - Opt specificity; - for (auto& inner : n.inners) { - if (inner.match(el)) - specificity = max(specificity, spec(inner)); - } - return specificity; - } - return match(el) ? Opt{spec(n)} : NONE; - }, - [&](auto const& s) -> Opt { - return match(el) ? Opt{spec(s)} : NONE; - } - }); -} - // MARK: Parser ---------------------------------------------------------------- // enum order is the operator priority (the lesser the most important) diff --git a/src/web/vaev-style/select.h b/src/web/vaev-style/select.h index 13738953..5356f555 100644 --- a/src/web/vaev-style/select.h +++ b/src/web/vaev-style/select.h @@ -357,10 +357,6 @@ struct Selector : public _Selector { }); } - bool match(Markup::Element const& el) const; - - Opt matchWithSpecificity(Markup::Element const& el) const; - bool operator==(Selector const&) const = default; static Res parse(Cursor& c); diff --git a/src/web/vaev-style/selector-matching.cpp b/src/web/vaev-style/selector-matching.cpp new file mode 100644 index 00000000..b271ac33 --- /dev/null +++ b/src/web/vaev-style/selector-matching.cpp @@ -0,0 +1,259 @@ +#include "selector-matching.h" + +namespace Vaev::Style { + +static constexpr bool DEBUG_MATCHING = false; + +// https://www.w3.org/TR/selectors-4/#descendant-combinators +static bool _matchDescendant(Selector const& s, Markup::Element const& e) { + Cursor curr = &e; + while (curr->hasParent()) { + auto& parent = curr->parentNode(); + if (auto el = parent.is()) + if (matchSelector(s, *el)) + return true; + curr = &parent; + } + return false; +} + +// https://www.w3.org/TR/selectors-4/#child-combinators +static bool _matchChild(Selector const& s, Markup::Element const& e) { + if (not e.hasParent()) + return false; + + auto& parent = e.parentNode(); + if (auto el = parent.is()) + return matchSelector(s, *el); + return false; +} + +// https://www.w3.org/TR/selectors-4/#adjacent-sibling-combinators +static bool _matchAdjacent(Selector const& s, Markup::Element const& e) { + if (not e.hasPreviousSibling()) + return false; + + auto prev = e.previousSibling(); + if (auto el = prev.is()) + return matchSelector(s, *el); + return false; +} + +// https://www.w3.org/TR/selectors-4/#general-sibling-combinators +static bool _matchSubsequent(Selector const& s, Markup::Element const& e) { + Cursor curr = &e; + while (curr->hasPreviousSibling()) { + auto prev = curr->previousSibling(); + if (auto el = prev.is()) + if (matchSelector(s, *el)) + return true; + curr = &prev.unwrap(); + } + return false; +} + +static bool _match(Infix const& s, Markup::Element const& e) { + if (not matchSelector(s.rhs.unwrap(), e)) + return false; + + switch (s.type) { + case Infix::DESCENDANT: // ' ' + return _matchDescendant(*s.lhs, e); + + case Infix::CHILD: // > + return _matchChild(*s.lhs, e); + + case Infix::ADJACENT: // + + return _matchAdjacent(*s.lhs, e); + + case Infix::SUBSEQUENT: // ~ + return _matchSubsequent(*s.lhs, e); + + default: + logWarnIf(DEBUG_MATCHING, "unimplemented selector: {}", s); + return false; + } +} + +static bool _match(Nfix const& s, Markup::Element const& el) { + switch (s.type) { + case Nfix::AND: + for (auto& inner : s.inners) + if (not matchSelector(inner, el)) + return false; + return true; + + // 4.1. Selector Lists + // https://www.w3.org/TR/selectors-4/#grouping + // and + // 4.2. The Matches-Any Pseudo-class: :is() + // https://www.w3.org/TR/selectors-4/#matchess + case Nfix::OR: + for (auto& inner : s.inners) + if (matchSelector(inner, el)) + return true; + return false; + + case Nfix::NOT: + return not matchSelector(s.inners[0], el); + + case Nfix::WHERE: + return not matchSelector(s.inners[0], el); + + default: + logWarnIf(DEBUG_MATCHING, "unimplemented selector: {}", s); + return false; + } +} + +// 5.1. Type (tag name) selector +// https://www.w3.org/TR/selectors-4/#type +static bool _match(TypeSelector const& s, Markup::Element const& el) { + return el.tagName == s.type; +} + +static bool _match(IdSelector const& s, Markup::Element const& el) { + return el.id() == s.id; +} + +static bool _match(ClassSelector const& s, Markup::Element const& el) { + return el.classList.contains(s.class_); +} + +// 8.2. The Link History Pseudo-classes: :link and :visited +// https://www.w3.org/TR/selectors-4/#link + +static bool _matchLink(Markup::Element const& el) { + return el.tagName == Html::A and el.hasAttribute(Html::HREF_ATTR); +} + +// 14.3.3. :first-child pseudo-class +// https://www.w3.org/TR/selectors-4/#the-first-child-pseudo + +static bool _matchFirstChild(Markup::Element const& e) { + Cursor curr = &e; + while (curr->hasPreviousSibling()) { + auto prev = curr->previousSibling(); + if (auto el = prev.is()) + return false; + curr = &prev.unwrap(); + } + return true; +} + +// 14.3.4. :last-child pseudo-class +// https://www.w3.org/TR/selectors-4/#the-last-child-pseudo + +static bool _matchLastChild(Markup::Element const& e) { + Cursor curr = &e; + while (curr->hasNextSibling()) { + auto next = curr->nextSibling(); + if (auto el = next.is()) + return false; + curr = &next.unwrap(); + } + return true; +} + +// 14.4.3. :first-of-type pseudo-class +// https://www.w3.org/TR/selectors-4/#the-first-of-type-pseudo + +static bool _matchFirstOfType(Markup::Element const& e) { + Cursor curr = &e; + auto tag = e.tagName; + + while (curr->hasPreviousSibling()) { + auto prev = curr->previousSibling(); + if (auto el = prev.is()) + if (e.tagName == tag) + return false; + curr = &prev.unwrap(); + } + return true; +} + +// 14.4.4. :last-of-type pseudo-class +// https://www.w3.org/TR/selectors-4/#the-last-of-type-pseudo + +static bool _matchLastOfType(Markup::Element const& e) { + Cursor curr = &e; + auto tag = e.tagName; + + while (curr->hasNextSibling()) { + auto prev = curr->nextSibling(); + if (auto el = prev.is()) + if (e.tagName == tag) + return false; + curr = &prev.unwrap(); + } + return true; +} + +static bool _match(Pseudo const& s, Markup::Element const& el) { + switch (s.type) { + case Pseudo::LINK: + return _matchLink(el); + + case Pseudo::ROOT: + return el.tagName == Html::HTML; + + case Pseudo::FIRST_OF_TYPE: + return _matchFirstOfType(el); + + case Pseudo::LAST_OF_TYPE: + return _matchLastOfType(el); + + case Pseudo::FIRST_CHILD: + return _matchFirstChild(el); + + case Pseudo::LAST_CHILD: + return _matchLastChild(el); + + default: + logDebugIf(DEBUG_MATCHING, "unimplemented pseudo class: {}", s); + return false; + } +} + +// 5.2. Universal selector +// https://www.w3.org/TR/selectors-4/#the-universal-selector +static bool _match(UniversalSelector const&, Markup::Element const&) { + return true; +} + +// MARK: Selector -------------------------------------------------------------- + +Opt matchSelectorWithSpecificity(Selector const& selector, Markup::Element const& el) { + return selector.visit(Visitor{ + [&](Nfix const& n) -> Opt { + if (n.type == Nfix::OR) { + Opt specificity; + for (auto& inner : n.inners) { + if (matchSelector(inner, el)) + specificity = max(specificity, spec(inner)); + } + return specificity; + } + return matchSelector(n, el) ? Opt{spec(n)} : NONE; + }, + [&](auto const& s) -> Opt { + return matchSelector(s, el) ? Opt{spec(s)} : NONE; + } + }); +} + +bool matchSelector(Selector const& selector, Markup::Element const& el) { + // Route the selector to the appropriate matching function. + + return selector.visit(Visitor{ + [&](auto const& s) { + if constexpr (requires { _match(s, el); }) + return _match(s, el); + + logWarnIf(DEBUG_MATCHING, "unimplemented selector: {}", s); + return false; + } + }); +} + +} // namespace Vaev::Style diff --git a/src/web/vaev-style/selector-matching.h b/src/web/vaev-style/selector-matching.h new file mode 100644 index 00000000..2a4f56c8 --- /dev/null +++ b/src/web/vaev-style/selector-matching.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "select.h" + +namespace Vaev::Style { + +bool matchSelector(Selector const& selector, Markup::Element const& el); + +Opt matchSelectorWithSpecificity(Selector const& selector, Markup::Element const& el); + +} // namespace Vaev::Style diff --git a/src/web/vaev-style/tests/test-select-spec.cpp b/src/web/vaev-style/tests/test-select-spec.cpp index b3466e0c..4a8a7bbb 100644 --- a/src/web/vaev-style/tests/test-select-spec.cpp +++ b/src/web/vaev-style/tests/test-select-spec.cpp @@ -1,13 +1,15 @@ #include #include +#include + namespace Vaev::Style::Tests { test$("select-class-spec") { Selector sel = ClassSelector{"foo"s}; auto el = makeRc(Html::DIV); el->classList.add("foo"); - expect$(sel.match(*el)); + expect$(matchSelector(sel, *el)); return Ok(); }