Skip to content

Commit

Permalink
[CSS] Matched element should be within the @scope
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=265241
rdar://118713295

Reviewed by Antti Koivisto.

This patch adds one part of the @scope matching algorithm: the matched element
has to be within the scope.

https://drafts.csswg.org/css-cascade-6/#scoped-styles

* LayoutTests/TestExpectations:
* LayoutTests/fast/css/scope-at-rule-expected.html:
* LayoutTests/fast/css/scope-at-rule.html:
* LayoutTests/imported/w3c/web-platform-tests/css/css-cascade/scope-deep-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-cascade/scope-evaluation-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-cascade/scope-focus-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-cascade/scope-hover-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-cascade/scope-implicit-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-cascade/scope-invalidation-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-cascade/scope-nesting-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-cascade/scope-proximity-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-cascade/scope-specificity-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-cascade/scope-visited-cssom-expected.txt:
* Source/WebCore/style/ElementRuleCollector.cpp:
(WebCore::Style::ElementRuleCollector::matchUARules):
(WebCore::Style::ElementRuleCollector::ruleMatches):
(WebCore::Style::ElementRuleCollector::scopeRulesMatch):
(WebCore::Style::ElementRuleCollector::matchAllRules):
* Source/WebCore/style/RuleSetBuilder.cpp:
(WebCore::Style::RuleSetBuilder::resolveSelectorListWithNesting):
(WebCore::Style::RuleSetBuilder::addStyleRule):
* Source/WebCore/style/RuleSetBuilder.h:

Canonical link: https://commits.webkit.org/271104@main
  • Loading branch information
mdubet committed Nov 24, 2023
1 parent 720b818 commit ee0320f
Show file tree
Hide file tree
Showing 16 changed files with 117 additions and 66 deletions.
11 changes: 2 additions & 9 deletions LayoutTests/TestExpectations
Original file line number Diff line number Diff line change
Expand Up @@ -6856,13 +6856,9 @@ imported/w3c/web-platform-tests/scroll-animations/view-timelines/view-timeline-s
imported/w3c/web-platform-tests/scroll-animations/view-timelines/view-timeline-sticky-inline.html [ Skip ]
imported/w3c/web-platform-tests/scroll-animations/view-timelines/view-timeline-subject-size-changes.html [ Skip ]




# webkit.org/b/229520 CSS @scope unimplemented
imported/w3c/web-platform-tests/css/css-cascade/import-conditional-002.html [ Skip ]
imported/w3c/web-platform-tests/css/css-cascade/scope-visited.html [ Skip ]

imported/w3c/web-platform-tests/css/css-cascade/import-conditional-002.html [ ImageOnlyFailure ]
imported/w3c/web-platform-tests/css/css-cascade/scope-visited.html [ ImageOnlyFailure ]

# other scroll-animations failures
webkit.org/b/263871 imported/w3c/web-platform-tests/scroll-animations/css/animation-timeline-none.html [ Failure ]
Expand Down Expand Up @@ -6904,9 +6900,6 @@ imported/w3c/web-platform-tests/navigation-api/scroll-behavior/scroll-on-synthet
imported/w3c/web-platform-tests/navigation-api/scroll-behavior/scroll-without-intercept.html [ Pass ]
imported/w3c/web-platform-tests/navigation-api/updateCurrentEntry-method/no-args.html [ Pass ]

# @scope is not yet fully implemented
fast/css/scope-at-rule.html [ ImageOnlyFailure ]

# View Transitions
imported/w3c/web-platform-tests/css/css-view-transitions/3d-transform-incoming.html [ ImageOnlyFailure ]
imported/w3c/web-platform-tests/css/css-view-transitions/3d-transform-outgoing.html [ ImageOnlyFailure ]
Expand Down
4 changes: 4 additions & 0 deletions LayoutTests/fast/css/scope-at-rule-expected.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@
<div>
<span class="green-4">should not be green</span>
</div>

<div>
<span class="green">should be green</span>
</div>
9 changes: 9 additions & 0 deletions LayoutTests/fast/css/scope-at-rule.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
.green-4 { color: green; }
}
}
@scope (.b) to (.a) {
.green-5 { color: green; }
}
</style>

<div class="a">
Expand All @@ -42,3 +45,9 @@
<div>
<span class="green-4">should not be green</span>
</div>

<div class="a">
<div class="b">
<span class="green-5">should be green</span>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@

FAIL Deep @scope nesting assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
PASS Deep @scope nesting

Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@

FAIL Single scope assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
PASS Single scope
PASS Scope can not match its own root without :scope
FAIL Selecting self with :scope assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Single scope with limit assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
PASS Single scope with limit
FAIL Single scope, :scope pseudo in main selector assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Single scope, :scope pseudo in to-selector assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Multiple scopes, :scope pseudo in to-selector assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Single scope, :scope pseudo in to-selector assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"
FAIL Multiple scopes, :scope pseudo in to-selector assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"
FAIL Inner @scope with :scope in from-selector assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Multiple scopes from same @scope-rule, only one limited assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
PASS Multiple scopes from same @scope-rule, only one limited
PASS Multiple scopes from same @scope-rule, both limited
FAIL Nested scopes assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
PASS Nested scopes, reverse
FAIL Nested scopes, with to-selector assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
PASS Nested scopes
FAIL Nested scopes, reverse assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"
PASS Nested scopes, with to-selector
FAIL :scope selecting itself assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL The scoping limit is not in scope assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Simulated inclusive scoping limit assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
PASS Scope with no elements
FAIL The scoping limit is not in scope assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"
FAIL Simulated inclusive scoping limit assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"
FAIL Scope with no elements assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"
PASS :scope direct adjacent sibling
PASS :scope indirect adjacent sibling
FAIL Relative selector inside @scope assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
PASS :scope in two different compounds
FAIL Scope root with :has() assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
PASS Scope root with :has()

Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
FAIL :focus via :scope in subject assert_equals: expected "1" but got "auto"
FAIL :focus via :scope in non-subject assert_equals: expected "1" but got "auto"
FAIL :focus in limit, :scope in subject assert_equals: expected "1" but got "auto"
FAIL :focus in intermediate limit, :scope in subject assert_equals: expected "1" but got "auto"
FAIL :focus in intermediate limit, :scope in subject assert_equals: expected "auto" but got "1"

Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
FAIL :hover via :scope in subject assert_equals: expected "1" but got "auto"
FAIL :hover via :scope in non-subject assert_equals: expected "1" but got "auto"
FAIL :hover in limit, :scope in subject assert_equals: expected "1" but got "auto"
FAIL :hover in intermediate limit, :scope in subject assert_equals: expected "1" but got "auto"
FAIL :hover in intermediate limit, :scope in subject assert_equals: expected "auto" but got "1"

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@

FAIL @scope without prelude implicitly scopes to parent of owner node assert_equals: expected "1" but got "auto"
FAIL @scope without prelude implicitly scopes to parent of owner node assert_equals: expected "auto" but got "1"
FAIL :scope can style implicit root assert_equals: expected "1" but got "auto"
FAIL @scope works with two identical stylesheets assert_equals: expected "1" but got "auto"
FAIL @scope works with two identical stylesheets assert_equals: expected "auto" but got "1"
PASS @scope with effectively empty :is() must not match anything
PASS Implicit @scope has implicitly added :scope descendant combinator
FAIL Implicit @scope has implicitly added :scope descendant combinator assert_equals: expected "auto" but got "1"
FAIL Implicit @scope with inner relative selector assert_equals: expected "1" but got "auto"
FAIL Implicit @scope with inner nesting selector assert_equals: expected "1" but got "auto"
FAIL Implicit @scope with limit assert_equals: expected "1" but got "auto"
FAIL Implicit @scope with limit assert_equals: expected "auto" but got "1"

Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@
FAIL Element becoming scope root assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Element becoming scope root (selector list) assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Element becoming scope root, with inner :scope rule assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Parent element becoming scope limit assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Parent element becoming scope limit (selector list) assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Subject element becoming scope limit assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Parent element affecting scope limit assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Sibling element affecting scope limit assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Parent element becoming scope limit assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"
FAIL Parent element becoming scope limit (selector list) assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"
FAIL Subject element becoming scope limit assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"
FAIL Parent element affecting scope limit assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"
FAIL Sibling element affecting scope limit assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"
FAIL Toggling inner/outer scope roots assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Element becoming root, with :scope in subject assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Scope root with :has() assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Scope root with :has(), :scope subject assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Scope root with :has(), :scope both subject and non-subject assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Scope limit with :has() assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Scope limit with :has() assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"
FAIL Element becoming root, with :scope selected by ~ combinator assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Element becoming root via ~ combinator assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Element becoming root via + combinator assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL :not(scope) in subject assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL :not(scope) in ancestor assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL :not(scope) in subject assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"
FAIL :not(scope) in ancestor assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"
FAIL :not(scope) in limit subject assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL :not(scope) in limit ancestor assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL :nth-child() in scope root assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL :nth-child() in scope limit assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL :nth-child() in scope limit assert_equals: expected "rgb(0, 0, 0)" but got "rgb(0, 128, 0)"

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

PASS Nesting-selector in <scope-end>
FAIL Nesting-selector in <scope-end> assert_equals: expected "auto" but got "1"
FAIL Implicit :scope in <scope-end> assert_equals: expected "1" but got "auto"
FAIL Relative selectors in <scope-end> assert_equals: expected "1" but got "auto"
FAIL Nesting-selector in the scope's <stylesheet> assert_equals: expected "1" but got "auto"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

FAIL Alternating light/dark assert_equals: expected "rgb(100, 100, 100)" but got "rgb(0, 0, 0)"
FAIL Proximity wins over order of appearance assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Specificity wins over proximity assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
FAIL Alternating light/dark assert_equals: expected "rgb(100, 100, 100)" but got "rgb(200, 200, 200)"
FAIL Proximity wins over order of appearance assert_equals: expected "rgb(0, 128, 0)" but got "rgb(255, 0, 0)"
PASS Specificity wins over proximity

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@

FAIL @scope (#main) { .b { } } assert_equals: scoped rule expected "1" but got "auto"
FAIL @scope (#main) to (.b) { .a { } } assert_equals: scoped rule expected "1" but got "auto"
FAIL @scope (#main, .foo, .bar) { #a { } } assert_equals: scoped rule expected "1" but got "auto"
FAIL @scope (#main) { div.b { } } assert_equals: scoped rule expected "1" but got "auto"
FAIL @scope (#main) { :scope .b { } } assert_equals: scoped rule expected "1" but got "auto"
FAIL @scope (#main) { & .b { } } assert_equals: scoped rule expected "1" but got "auto"
FAIL @scope (#main) { div .b { } } assert_equals: scoped rule expected "1" but got "auto"
FAIL @scope (#main) { @scope (.a) { .b { } } } assert_equals: scoped rule expected "1" but got "auto"
FAIL @scope (#main) { .b { } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main) to (.b) { .a { } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main, .foo, .bar) { #a { } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main) { div.b { } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main) { :scope .b { } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main) { & .b { } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main) { div .b { } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main) { @scope (.a) { .b { } } } assert_equals: scoped + unscoped expected "1" but got "2"

Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@

FAIL :link as scoped selector assert_equals: expected "rgb(0, 128, 0)" but got "rgb(255, 255, 255)"
PASS :link as scoped selector
PASS :visited as scoped selector
PASS :not(:link) as scoped selector
FAIL :not(:visited) as scoped selector assert_equals: expected "rgb(0, 128, 0)" but got "rgb(255, 255, 255)"
FAIL :link as scoping root assert_equals: expected "rgb(0, 128, 0)" but got "rgb(255, 255, 255)"
PASS :visited as scoping root
FAIL :not(:visited) as scoping root assert_equals: expected "rgb(0, 128, 0)" but got "rgb(255, 255, 255)"
PASS :not(:visited) as scoped selector
PASS :link as scoping root
FAIL :visited as scoping root assert_equals: expected "rgb(255, 255, 255)" but got "rgb(0, 128, 0)"
PASS :not(:visited) as scoping root
PASS :not(:link) as scoping root
FAIL :link as scoping root, :scope assert_equals: expected "rgb(0, 128, 0)" but got "rgb(255, 255, 255)"
PASS :visited as scoping root, :scope
Expand Down
61 changes: 53 additions & 8 deletions Source/WebCore/style/ElementRuleCollector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ void ElementRuleCollector::matchUARules()
void ElementRuleCollector::matchUARules(const RuleSet& rules)
{
clearMatchedRules();

collectMatchingRules(MatchRequest(rules));

sortAndTransferMatchedRules(DeclarationOrigin::UserAgent);
Expand Down Expand Up @@ -493,7 +493,7 @@ inline bool ElementRuleCollector::ruleMatches(const RuleData& ruleData, unsigned
context.nameIdentifier = m_pseudoElementRequest.nameIdentifier;
context.styleScopeOrdinal = styleScopeOrdinal;
context.selectorMatchingState = m_selectorMatchingState;

bool selectorMatches;
#if ENABLE(CSS_SELECTOR_JIT)
if (compiledSelector.status == SelectorCompilationStatus::SelectorCheckerWithCheckingContext) {
Expand Down Expand Up @@ -584,14 +584,59 @@ bool ElementRuleCollector::containerQueriesMatch(const RuleData& ruleData, const

bool ElementRuleCollector::scopeRulesMatch(const RuleData& ruleData, const MatchRequest& matchRequest)
{
auto queries = matchRequest.ruleSet.scopeRulesFor(ruleData);
auto scopeRules = matchRequest.ruleSet.scopeRulesFor(ruleData);

if (queries.isEmpty())
if (scopeRules.isEmpty())
return true;

SelectorChecker checker(element().rootElement()->document());
SelectorChecker::CheckingContext context(SelectorChecker::Mode::CollectingRulesIgnoringVirtualPseudoElements);

auto isWithinScope = [&](auto& rule) {
const Element* scopingRoot = nullptr;

auto ancestorsMatch = [&](const auto& selectorList) {
const auto* ancestor = element().parentElement();
while (ancestor) {
if (ancestor == scopingRoot) {
// The end of the scope has to be a descendant of the start of the scope.
return false;
}
for (const auto* selector = selectorList.first(); selector; selector = CSSSelectorList::next(selector)) {
auto match = checker.match(*selector, *ancestor, context);
if (match) {
scopingRoot = ancestor;
return true;
}
}
ancestor = ancestor->parentElement();
}
return false;
};

const auto& scopeStart = rule->scopeStart();
if (!scopeStart.isEmpty()) {
if (!ancestorsMatch(scopeStart))
return false;
}

const auto& scopeEnd = rule->scopeEnd();
if (!scopeEnd.isEmpty()) {
if (ancestorsMatch(scopeEnd))
return false;
}

// element is in the @scope donut
return true;
};

// We need to respect each nested @scope to collect this rule
for (auto& rule : scopeRules) {
if (!isWithinScope(rule))
return false;
}

// FIXME: to implement
// https://bugs.webkit.org/show_bug.cgi?id=265241
return false;
return true;
}

static inline bool compareRules(MatchedRule r1, MatchedRule r2)
Expand Down Expand Up @@ -638,7 +683,7 @@ void ElementRuleCollector::matchAllRules(bool matchAuthorAndUserStyles, bool inc
addMatchedProperties({ properties }, DeclarationOrigin::Author);
}
}

if (matchAuthorAndUserStyles) {
clearMatchedRules();

Expand Down
6 changes: 3 additions & 3 deletions Source/WebCore/style/RuleSetBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,15 @@ void RuleSetBuilder::addRulesFromSheetContents(const StyleSheetContents& sheet)
void RuleSetBuilder::resolveSelectorListWithNesting(StyleRuleWithNesting& rule)
{
const CSSSelectorList* parentResolvedSelectorList = nullptr;
if (m_styleRuleStack.size())
if (m_styleRuleStack.size())
parentResolvedSelectorList = m_styleRuleStack.last();

// If it's a top-level rule without a nesting parent selector, keep the selector list as is.
if (!rule.originalSelectorList().hasExplicitNestingParent() && !parentResolvedSelectorList)
return;

auto resolvedSelectorList = CSSSelectorParser::resolveNestingParent(rule.originalSelectorList(), parentResolvedSelectorList);
rule.wrapperAdoptSelectorList(WTFMove(resolvedSelectorList));
rule.wrapperAdoptSelectorList(WTFMove(resolvedSelectorList));
}

void RuleSetBuilder::addStyleRuleWithSelectorList(const CSSSelectorList& selectorList, const StyleRule& rule)
Expand All @@ -264,7 +264,7 @@ void RuleSetBuilder::addStyleRule(StyleRuleWithNesting& rule)

auto& selectorList = rule.selectorList();
addStyleRuleWithSelectorList(selectorList, rule);

// Process nested rules
m_styleRuleStack.append(&selectorList);
for (auto& nestedRule : rule.nestedRules())
Expand Down
2 changes: 1 addition & 1 deletion Source/WebCore/style/RuleSetBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class RuleSetBuilder {
void pushCascadeLayer(const CascadeLayerName&);
void popCascadeLayer(const CascadeLayerName&);
void updateCascadeLayerPriorities();

void addMutatingRulesToResolver();
void updateDynamicMediaQueries();
void resolveSelectorListWithNesting(StyleRuleWithNesting&);
Expand Down

0 comments on commit ee0320f

Please sign in to comment.