[CSS Shadow Parts] Support 'exportparts' attribute
authorantti@apple.com <antti@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 4 Oct 2019 08:33:05 +0000 (08:33 +0000)
committerantti@apple.com <antti@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 4 Oct 2019 08:33:05 +0000 (08:33 +0000)
https://bugs.webkit.org/show_bug.cgi?id=202520

Reviewed by Ryosuke Niwa.

LayoutTests/imported/w3c:

* web-platform-tests/css/css-shadow-parts/double-forward-expected.txt:
* web-platform-tests/css/css-shadow-parts/invalidation-change-exportparts-forward-expected.txt:
* web-platform-tests/css/css-shadow-parts/invalidation-change-part-name-forward-expected.txt:
* web-platform-tests/css/css-shadow-parts/invalidation-complex-selector-forward-expected.txt:
* web-platform-tests/css/css-shadow-parts/precedence-part-vs-part-expected.txt:
* web-platform-tests/css/css-shadow-parts/simple-forward-expected.txt:
* web-platform-tests/css/css-shadow-parts/simple-forward-shorthand-expected.txt:

Source/WebCore:

Support 'exportparts' attribute for exporting part mappings from subcomponents.

* css/ElementRuleCollector.cpp:
(WebCore::ElementRuleCollector::matchAuthorRules):
(WebCore::ElementRuleCollector::matchPartPseudoElementRules):

Recurse to containing scopes to collect part rules if there are exported mappings.

(WebCore::ElementRuleCollector::ruleMatches):
* css/ElementRuleCollector.h:
* css/SelectorChecker.cpp:
(WebCore::SelectorChecker::matchRecursively const):

Make ShadowDescendant fake combinator skip directly to the scope where the part rules are coming from.

(WebCore::SelectorChecker::checkOne const):

Resolve names via mappings if needed.

* css/SelectorChecker.h:
* dom/Element.cpp:
(WebCore::Element::attributeChanged):

Invalidate mappings as needed.

* dom/ShadowRoot.cpp:
(WebCore::parsePartMappings):

Parse the mappings microsyntax.

(WebCore::ShadowRoot::partMappings const):
(WebCore::ShadowRoot::invalidatePartMappings):
* dom/ShadowRoot.h:
* html/HTMLAttributeNames.in:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@250712 268f45cc-cd09-0410-ab3c-d52691b4dbfc

17 files changed:
LayoutTests/imported/w3c/ChangeLog
LayoutTests/imported/w3c/web-platform-tests/css/css-shadow-parts/double-forward-expected.txt
LayoutTests/imported/w3c/web-platform-tests/css/css-shadow-parts/invalidation-change-exportparts-forward-expected.txt
LayoutTests/imported/w3c/web-platform-tests/css/css-shadow-parts/invalidation-change-part-name-forward-expected.txt
LayoutTests/imported/w3c/web-platform-tests/css/css-shadow-parts/invalidation-complex-selector-forward-expected.txt
LayoutTests/imported/w3c/web-platform-tests/css/css-shadow-parts/precedence-part-vs-part-expected.txt
LayoutTests/imported/w3c/web-platform-tests/css/css-shadow-parts/simple-forward-expected.txt
LayoutTests/imported/w3c/web-platform-tests/css/css-shadow-parts/simple-forward-shorthand-expected.txt
Source/WebCore/ChangeLog
Source/WebCore/css/ElementRuleCollector.cpp
Source/WebCore/css/ElementRuleCollector.h
Source/WebCore/css/SelectorChecker.cpp
Source/WebCore/css/SelectorChecker.h
Source/WebCore/dom/Element.cpp
Source/WebCore/dom/ShadowRoot.cpp
Source/WebCore/dom/ShadowRoot.h
Source/WebCore/html/HTMLAttributeNames.in

index 1c1cc64..9d4a9a8 100644 (file)
@@ -1,3 +1,18 @@
+2019-10-04  Antti Koivisto  <antti@apple.com>
+
+        [CSS Shadow Parts] Support 'exportparts' attribute
+        https://bugs.webkit.org/show_bug.cgi?id=202520
+
+        Reviewed by Ryosuke Niwa.
+
+        * web-platform-tests/css/css-shadow-parts/double-forward-expected.txt:
+        * web-platform-tests/css/css-shadow-parts/invalidation-change-exportparts-forward-expected.txt:
+        * web-platform-tests/css/css-shadow-parts/invalidation-change-part-name-forward-expected.txt:
+        * web-platform-tests/css/css-shadow-parts/invalidation-complex-selector-forward-expected.txt:
+        * web-platform-tests/css/css-shadow-parts/precedence-part-vs-part-expected.txt:
+        * web-platform-tests/css/css-shadow-parts/simple-forward-expected.txt:
+        * web-platform-tests/css/css-shadow-parts/simple-forward-shorthand-expected.txt:
+
 2019-10-04  Ryosuke Niwa  <rniwa@webkit.org>
 
         Radio button groups are not scoped by shadow boundaries
index f3692c0..a4628a6 100644 (file)
@@ -1,4 +1,4 @@
 The following text should be green: 
 
-FAIL Part in inner host is forwarded through the middle host for styling by document style sheet assert_equals: expected "rgb(0, 128, 0)" but got "rgb(255, 0, 0)"
+PASS Part in inner host is forwarded through the middle host for styling by document style sheet 
 
index 2284f2b..6b5302a 100644 (file)
@@ -1,4 +1,4 @@
 The following text should be green: 
 
-FAIL Part in selected host changed color assert_not_equals: got disallowed value "rgb(0, 128, 0)"
+FAIL Part in selected host changed color assert_not_equals: got disallowed value "rgb(255, 0, 0)"
 
index 2284f2b..6b5302a 100644 (file)
@@ -1,4 +1,4 @@
 The following text should be green: 
 
-FAIL Part in selected host changed color assert_not_equals: got disallowed value "rgb(0, 128, 0)"
+FAIL Part in selected host changed color assert_not_equals: got disallowed value "rgb(255, 0, 0)"
 
index c7f8716..40ee669 100644 (file)
@@ -1,4 +1,4 @@
 The following text should be green: 
 
-FAIL Style from document overrides style from outer CE assert_equals: expected "rgb(0, 128, 0)" but got "rgb(255, 0, 0)"
+PASS Style from document overrides style from outer CE 
 
index 31f09a6..30895e1 100644 (file)
@@ -1,4 +1,4 @@
 The following text should be green: 
 
-FAIL Part in inner host is forwarded for styling by document style sheet assert_equals: expected "rgb(0, 128, 0)" but got "rgb(255, 0, 0)"
+PASS Part in inner host is forwarded for styling by document style sheet 
 
index bf30427..7e65d1a 100644 (file)
@@ -1,4 +1,4 @@
 The following text should be green: 
 
-FAIL Part in inner host is forwarded, under the same name, for styling by document style sheet assert_equals: expected "rgb(0, 128, 0)" but got "rgb(255, 0, 0)"
+PASS Part in inner host is forwarded, under the same name, for styling by document style sheet 
 
index a58e18f..4458eda 100644 (file)
@@ -1,3 +1,45 @@
+2019-10-04  Antti Koivisto  <antti@apple.com>
+
+        [CSS Shadow Parts] Support 'exportparts' attribute
+        https://bugs.webkit.org/show_bug.cgi?id=202520
+
+        Reviewed by Ryosuke Niwa.
+
+        Support 'exportparts' attribute for exporting part mappings from subcomponents.
+
+        * css/ElementRuleCollector.cpp:
+        (WebCore::ElementRuleCollector::matchAuthorRules):
+        (WebCore::ElementRuleCollector::matchPartPseudoElementRules):
+
+        Recurse to containing scopes to collect part rules if there are exported mappings.
+
+        (WebCore::ElementRuleCollector::ruleMatches):
+        * css/ElementRuleCollector.h:
+        * css/SelectorChecker.cpp:
+        (WebCore::SelectorChecker::matchRecursively const):
+
+        Make ShadowDescendant fake combinator skip directly to the scope where the part rules are coming from.
+
+        (WebCore::SelectorChecker::checkOne const):
+
+        Resolve names via mappings if needed.
+
+        * css/SelectorChecker.h:
+        * dom/Element.cpp:
+        (WebCore::Element::attributeChanged):
+
+        Invalidate mappings as needed.
+
+        * dom/ShadowRoot.cpp:
+        (WebCore::parsePartMappings):
+
+        Parse the mappings microsyntax.
+
+        (WebCore::ShadowRoot::partMappings const):
+        (WebCore::ShadowRoot::invalidatePartMappings):
+        * dom/ShadowRoot.h:
+        * html/HTMLAttributeNames.in:
+
 2019-10-04  Ryosuke Niwa  <rniwa@webkit.org>
 
         A newly inserted element doesn't get assigned to a named slot if slot assignments had already happened
index e683a97..207cb53 100644 (file)
@@ -205,7 +205,7 @@ void ElementRuleCollector::matchAuthorRules(bool includeEmptyRules)
         matchAuthorShadowPseudoElementRules(includeEmptyRules, ruleRange);
 
         if (!m_element.partNames().isEmpty())
-            matchPartPseudoElementRules(includeEmptyRules, ruleRange);
+            matchPartPseudoElementRules(*m_element.containingShadowRoot(), includeEmptyRules, ruleRange);
     }
 
     sortAndTransferMatchedRules();
@@ -263,14 +263,26 @@ void ElementRuleCollector::matchSlottedPseudoElementRules(bool includeEmptyRules
     }
 }
 
-void ElementRuleCollector::matchPartPseudoElementRules(bool includeEmptyRules, StyleResolver::RuleRange& ruleRange)
+void ElementRuleCollector::matchPartPseudoElementRules(const ShadowRoot& containingShadowRoot, bool includeEmptyRules, StyleResolver::RuleRange& ruleRange)
 {
     ASSERT(m_element.isInShadowTree());
-    auto& shadowRoot = *m_element.containingShadowRoot();
-    auto& hostAuthorRules = Style::Scope::forNode(*shadowRoot.host()).resolver().ruleSets().authorStyle();
+    ASSERT(!m_element.partNames().isEmpty());
 
-    MatchRequest hostAuthorRequest { &hostAuthorRules, includeEmptyRules, Style::ScopeOrdinal::ContainingHost };
-    collectMatchingRulesForList(&hostAuthorRules.partPseudoElementRules(), hostAuthorRequest, ruleRange);
+    auto& shadowHost = *containingShadowRoot.host();
+    {
+        SetForScope<const Element*> partMatchingScope(m_shadowHostInPartRuleScope, &shadowHost);
+
+        auto& hostAuthorRules = Style::Scope::forNode(shadowHost).resolver().ruleSets().authorStyle();
+        MatchRequest hostAuthorRequest { &hostAuthorRules, includeEmptyRules, Style::ScopeOrdinal::ContainingHost };
+        collectMatchingRulesForList(&hostAuthorRules.partPseudoElementRules(), hostAuthorRequest, ruleRange);
+    }
+
+    // Element may be exposed to styling from enclosing scopes via exportparts attributes.
+    if (containingShadowRoot.partMappings().isEmpty())
+        return;
+
+    if (auto* parentShadowRoot = shadowHost.containingShadowRoot())
+        matchPartPseudoElementRules(*parentShadowRoot, includeEmptyRules, ruleRange);
 }
 
 void ElementRuleCollector::collectMatchingShadowPseudoElementRules(const MatchRequest& matchRequest, StyleResolver::RuleRange& ruleRange)
@@ -428,6 +440,7 @@ inline bool ElementRuleCollector::ruleMatches(const RuleData& ruleData, unsigned
     context.scrollbar = m_pseudoStyleRequest.scrollbar;
     context.scrollbarPart = m_pseudoStyleRequest.scrollbarPart;
     context.isMatchingHostPseudoClass = m_isMatchingHostPseudoClass;
+    context.shadowHostInPartRuleScope = m_shadowHostInPartRuleScope;
 
     bool selectorMatches;
 #if ENABLE(CSS_SELECTOR_JIT)
index a062378..da90198 100644 (file)
@@ -75,7 +75,7 @@ private:
     void matchAuthorShadowPseudoElementRules(bool includeEmptyRules, StyleResolver::RuleRange&);
     void matchHostPseudoClassRules(bool includeEmptyRules, StyleResolver::RuleRange&);
     void matchSlottedPseudoElementRules(bool includeEmptyRules, StyleResolver::RuleRange&);
-    void matchPartPseudoElementRules(bool includeEmptyRules, StyleResolver::RuleRange&);
+    void matchPartPseudoElementRules(const ShadowRoot& containingShadowRoot, bool includeEmptyRules, StyleResolver::RuleRange&);
 
     void collectMatchingShadowPseudoElementRules(const MatchRequest&, StyleResolver::RuleRange&);
     std::unique_ptr<RuleSet::RuleDataVector> collectSlottedPseudoElementRulesForSlot(bool includeEmptyRules);
@@ -100,6 +100,7 @@ private:
     SelectorChecker::Mode m_mode { SelectorChecker::Mode::ResolvingStyle };
     bool m_isMatchingSlottedPseudoElements { false };
     bool m_isMatchingHostPseudoClass { false };
+    const Element* m_shadowHostInPartRuleScope { nullptr };
     Vector<std::unique_ptr<RuleSet::RuleDataVector>> m_keepAliveSlottedPseudoElementRules;
 
     Vector<MatchedRule, 64> m_matchedRules;
index 29d0cb0..02c7b69 100644 (file)
@@ -429,10 +429,11 @@ SelectorChecker::MatchResult SelectorChecker::matchRecursively(CheckingContext&
         }
     case CSSSelector::ShadowDescendant:
         {
-            Element* shadowHostNode = context.element->shadowHost();
-            if (!shadowHostNode)
+            // When matching foo::part(bar) we skip directly to the tree of element 'foo'.
+            auto* shadowHost = checkingContext.shadowHostInPartRuleScope ? checkingContext.shadowHostInPartRuleScope : context.element->shadowHost();
+            if (!shadowHost)
                 return MatchResult::fails(Match::SelectorFailsCompletely);
-            nextContext.element = shadowHostNode;
+            nextContext.element = shadowHost;
             nextContext.firstSelectorOfTheFragment = nextContext.selector;
             nextContext.isSubjectOrAdjacentElement = false;
             PseudoIdSet ignoreDynamicPseudo;
@@ -1149,13 +1150,32 @@ bool SelectorChecker::checkOne(CheckingContext& checkingContext, const LocalCont
             ASSERT(checkingContext.resolvingMode == Mode::CollectingRules);
             return is<HTMLSlotElement>(element);
 
-        case CSSSelector::PseudoElementPart:
+        case CSSSelector::PseudoElementPart: {
+            auto translatePartNameToRuleScope = [&](AtomString partName) {
+                for (auto* shadowRoot = element.containingShadowRoot(); shadowRoot; shadowRoot = shadowRoot->host()->containingShadowRoot()) {
+                    // Apply mappings up to the scope the rules are coming from.
+                    if (shadowRoot->host() == checkingContext.shadowHostInPartRuleScope)
+                        break;
+                    partName = shadowRoot->partMappings().get(partName);
+                    if (partName.isEmpty())
+                        return AtomString();
+                }
+                return partName;
+            };
+
+            Vector<AtomString, 4> translatedPartNames;
+            for (unsigned i = 0; i < element.partNames().size(); ++i) {
+                auto translatedPartName = translatePartNameToRuleScope(element.partNames()[i]);
+                if (!translatedPartName.isEmpty())
+                    translatedPartNames.append(translatedPartName);
+            }
+
             for (auto& part : *selector.argumentList()) {
-                if (!element.partNames().contains(part))
+                if (!translatedPartNames.contains(part))
                     return false;
             }
             return true;
-
+        }
         default:
             return true;
         }
index 60b7eea..2a48cc2 100644 (file)
@@ -84,6 +84,7 @@ public:
         ScrollbarPart scrollbarPart { NoPart };
         const ContainerNode* scope { nullptr };
         bool isMatchingHostPseudoClass { false };
+        const Element* shadowHostInPartRuleScope { nullptr };
 
         // FIXME: It would be nicer to have a separate object for return values. This requires some more work in the selector compiler.
         Style::Relations styleRelations;
index 0b9c138..08c49e0 100644 (file)
@@ -1719,8 +1719,6 @@ void Element::attributeChanged(const QualifiedName& name, const AtomString& oldV
             document().invalidateAccessKeyCache();
         else if (name == HTMLNames::classAttr)
             classAttributeChanged(newValue);
-        else if (name == HTMLNames::partAttr)
-            partAttributeChanged(newValue);
         else if (name == HTMLNames::idAttr) {
             AtomString oldId = elementData()->idForStyleResolution();
             AtomString newId = makeIdForStyleResolution(newValue, document().inQuirksMode());
@@ -1743,6 +1741,11 @@ void Element::attributeChanged(const QualifiedName& name, const AtomString& oldV
                 if (auto* shadowRoot = parent->shadowRoot())
                     shadowRoot->hostChildElementDidChangeSlotAttribute(*this, oldValue, newValue);
             }
+        } else if (name == HTMLNames::partAttr)
+            partAttributeChanged(newValue);
+        else if (name == HTMLNames::exportpartsAttr) {
+            if (auto* shadowRoot = this->shadowRoot())
+                shadowRoot->invalidatePartMappings();
         }
     }
 
index 6786c0d..6cdf6ad 100644 (file)
@@ -30,6 +30,7 @@
 
 #include "CSSStyleSheet.h"
 #include "ElementTraversal.h"
+#include "HTMLParserIdioms.h"
 #include "HTMLSlotElement.h"
 #include "RenderElement.h"
 #include "RuntimeEnabledFeatures.h"
@@ -50,6 +51,7 @@ struct SameSizeAsShadowRoot : public DocumentFragment, public TreeScope {
     void* styleSheetList;
     void* host;
     void* slotAssignment;
+    Optional<HashMap<AtomString, AtomString>> partMappings;
 };
 
 COMPILE_ASSERT(sizeof(ShadowRoot) == sizeof(SameSizeAsShadowRoot), shadowroot_should_stay_small);
@@ -252,6 +254,92 @@ const Vector<Node*>* ShadowRoot::assignedNodesForSlot(const HTMLSlotElement& slo
     return m_slotAssignment->assignedNodesForSlot(slot, *this);
 }
 
+static Optional<std::pair<AtomString, AtomString>> parsePartMapping(StringView mappingString)
+{
+    const auto end = mappingString.length();
+
+    auto skipWhitespace = [&] (auto position) {
+        while (position < end && isHTMLSpace(mappingString[position]))
+            ++position;
+        return position;
+    };
+
+    auto collectValue = [&] (auto position) {
+        while (position < end && (!isHTMLSpace(mappingString[position]) && mappingString[position] != ':'))
+            ++position;
+        return position;
+    };
+
+    size_t begin = 0;
+    begin = skipWhitespace(begin);
+
+    auto firstPartEnd = collectValue(begin);
+    if (firstPartEnd == begin)
+        return { };
+
+    auto firstPart = mappingString.substring(begin, firstPartEnd - begin).toAtomString();
+
+    begin = skipWhitespace(firstPartEnd);
+    if (begin == end)
+        return std::make_pair(firstPart, firstPart);
+
+    if (mappingString[begin] != ':')
+        return { };
+
+    begin = skipWhitespace(begin + 1);
+
+    auto secondPartEnd = collectValue(begin);
+    if (secondPartEnd == begin)
+        return { };
+
+    auto secondPart = mappingString.substring(begin, secondPartEnd - begin).toAtomString();
+
+    begin = skipWhitespace(secondPartEnd);
+    if (begin != end)
+        return { };
+
+    return std::make_pair(firstPart, secondPart);
+}
+
+static void parsePartMappingsList(HashMap<AtomString, AtomString>& mappings, StringView mappingsListString)
+{
+    const auto end = mappingsListString.length();
+
+    size_t begin = 0;
+    while (begin < end) {
+        size_t mappingEnd = begin;
+        while (mappingEnd < end && mappingsListString[mappingEnd] != ',')
+            ++mappingEnd;
+
+        auto result = parsePartMapping(mappingsListString.substring(begin, mappingEnd - begin));
+        if (result)
+            mappings.add(result->first, result->second);
+
+        if (mappingEnd == end)
+            break;
+
+        begin = mappingEnd + 1;
+    }
+}
+
+const HashMap<AtomString, AtomString>& ShadowRoot::partMappings() const
+{
+    if (!m_partMappings) {
+        m_partMappings = HashMap<AtomString, AtomString>();
+
+        auto exportpartsValue = host()->attributeWithoutSynchronization(HTMLNames::exportpartsAttr);
+        if (!exportpartsValue.isEmpty() && RuntimeEnabledFeatures::sharedFeatures().cssShadowPartsEnabled())
+            parsePartMappingsList(*m_partMappings, exportpartsValue);
+    }
+
+    return *m_partMappings;
+}
+
+void ShadowRoot::invalidatePartMappings()
+{
+    m_partMappings = { };
+}
+
 Vector<ShadowRoot*> assignedShadowRootsIfSlotted(const Node& node)
 {
     Vector<ShadowRoot*> result;
index 9cbee5a..c523f7e 100644 (file)
@@ -30,6 +30,7 @@
 #include "DocumentFragment.h"
 #include "Element.h"
 #include "ShadowRootMode.h"
+#include <wtf/HashMap.h>
 
 namespace WebCore {
 
@@ -92,6 +93,9 @@ public:
     void moveShadowRootToNewParentScope(TreeScope&, Document&);
     void moveShadowRootToNewDocument(Document&);
 
+    const HashMap<AtomString, AtomString>& partMappings() const;
+    void invalidatePartMappings();
+
 protected:
     ShadowRoot(Document&, ShadowRootMode);
 
@@ -116,6 +120,7 @@ private:
 
     std::unique_ptr<Style::Scope> m_styleScope;
     std::unique_ptr<SlotAssignment> m_slotAssignment;
+    mutable Optional<HashMap<AtomString, AtomString>> m_partMappings;
 };
 
 inline Element* ShadowRoot::activeElement() const
index fb26db7..05b37a4 100644 (file)
@@ -123,6 +123,7 @@ enctype
 end
 event
 expanded
+exportparts
 face
 filename
 focused