Bug 1330843 - Allow JS to create NAC pseudo-elements. r=bholley draft
authorCameron McCormack <cam@mcc.id.au>
Fri, 17 Mar 2017 15:30:34 +0800
changeset 500990 bc32cd9e4bd5c73af094718c795691da84abe5ba
parent 500857 23a4b7430dd7e83a2809bf3dc41471f154301eda
child 549774 b15ce2d305470edf08c593cd0ac28c380a8dfcc6
push id49868
push userbmo:cam@mcc.id.au
push dateSat, 18 Mar 2017 10:39:28 +0000
reviewersbholley
bugs1330843
milestone55.0a1
Bug 1330843 - Allow JS to create NAC pseudo-elements. r=bholley MozReview-Commit-ID: 2aBPoCOsT6R
dom/base/nsContentUtils.cpp
dom/base/nsContentUtils.h
dom/base/nsDocument.cpp
dom/webidl/Document.webidl
layout/base/GeckoRestyleManager.cpp
layout/base/nsCSSFrameConstructor.cpp
layout/style/nsCSSPseudoElements.h
layout/style/nsComputedDOMStyle.cpp
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -9970,8 +9970,21 @@ nsContentUtils::CreateJSValueFromSequenc
                                      JSPROP_ENUMERATE))) {
       return NS_ERROR_OUT_OF_MEMORY;
     }
   }
 
   aValue.setObject(*array);
   return NS_OK;
 }
+
+/* static */ Element*
+nsContentUtils::GetClosestNonNativeAnonymousAncestor(Element* aElement)
+{
+  MOZ_ASSERT(aElement);
+  MOZ_ASSERT(aElement->IsNativeAnonymous());
+
+  Element* e = aElement;
+  while (e && e->IsNativeAnonymous()) {
+    e = e->GetParentElement();
+  }
+  return e;
+}
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -2787,16 +2787,22 @@ public:
   static nsresult
   CreateJSValueFromSequenceOfObject(JSContext* aCx,
                                     const mozilla::dom::Sequence<JSObject*>& aTransfer,
                                     JS::MutableHandle<JS::Value> aValue);
 
   static bool
   IsWebComponentsEnabled() { return sIsWebComponentsEnabled; }
 
+  /**
+   * Walks up the tree from aElement until it finds an element that is
+   * not native anonymous content.  aElement must be NAC itself.
+   */
+  static Element* GetClosestNonNativeAnonymousAncestor(Element* aElement);
+
 private:
   static bool InitializeEventTable();
 
   static nsresult EnsureStringBundle(PropertiesFile aFile);
 
   static bool CanCallerAccess(nsIPrincipal* aSubjectPrincipal,
                                 nsIPrincipal* aPrincipal);
 
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -5623,16 +5623,30 @@ nsDocument::GetCustomElementRegistry()
   RefPtr<CustomElementRegistry> registry = window->CustomElements();
   if (!registry) {
     return nullptr;
   }
 
   return registry.forget();
 }
 
+// We only support pseudo-elements with two colons in this function.
+static CSSPseudoElementType
+GetPseudoElementType(const nsString& aString, ErrorResult& aRv)
+{
+  MOZ_ASSERT(!aString.IsEmpty(), "GetPseudoElementType aString should be non-null");
+  if (aString.Length() <= 2 || aString[0] != ':' || aString[1] != ':') {
+    aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
+    return CSSPseudoElementType::NotPseudo;
+  }
+  nsCOMPtr<nsIAtom> pseudo = NS_Atomize(Substring(aString, 1));
+  return nsCSSPseudoElements::GetPseudoType(pseudo,
+      nsCSSProps::EnabledState::eInUASheets);
+}
+
 already_AddRefed<Element>
 nsDocument::CreateElement(const nsAString& aTagName,
                           const ElementCreationOptionsOrString& aOptions,
                           ErrorResult& rv)
 {
   rv = nsContentUtils::CheckQName(aTagName, false);
   if (rv.Failed()) {
     return nullptr;
@@ -5640,28 +5654,48 @@ nsDocument::CreateElement(const nsAStrin
 
   bool needsLowercase = IsHTMLDocument() && !IsLowercaseASCII(aTagName);
   nsAutoString lcTagName;
   if (needsLowercase) {
     nsContentUtils::ASCIIToLower(aTagName, lcTagName);
   }
 
   const nsString* is = nullptr;
+  CSSPseudoElementType pseudoType = CSSPseudoElementType::NotPseudo;
   if (aOptions.IsElementCreationOptions()) {
+    const ElementCreationOptions& options =
+      aOptions.GetAsElementCreationOptions();
     // Throw NotFoundError if 'is' is not-null and definition is null
-    is = CheckCustomElementName(aOptions.GetAsElementCreationOptions(),
-      needsLowercase ? lcTagName : aTagName, mDefaultElementType, rv);
+    is = CheckCustomElementName(options,
+                                needsLowercase ? lcTagName : aTagName,
+                                mDefaultElementType, rv);
     if (rv.Failed()) {
       return nullptr;
     }
+
+    // Check 'pseudo' and throw an exception if it's not one allowed
+    // with CSS_PSEUDO_ELEMENT_IS_JS_CREATED_NAC.
+    if (options.mPseudo.WasPassed()) {
+      pseudoType = GetPseudoElementType(options.mPseudo.Value(), rv);
+      if (rv.Failed() ||
+          pseudoType == CSSPseudoElementType::NotPseudo ||
+          !nsCSSPseudoElements::PseudoElementIsJSCreatedNAC(pseudoType)) {
+        rv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+        return nullptr;
+      }
+    }
   }
 
   RefPtr<Element> elem = CreateElem(
     needsLowercase ? lcTagName : aTagName, nullptr, mDefaultElementType, is);
 
+  if (pseudoType != CSSPseudoElementType::NotPseudo) {
+    elem->SetPseudoElementType(pseudoType);
+  }
+
   return elem.forget();
 }
 
 NS_IMETHODIMP
 nsDocument::CreateElementNS(const nsAString& aNamespaceURI,
                             const nsAString& aQualifiedName,
                             nsIDOMElement** aReturn)
 {
--- a/dom/webidl/Document.webidl
+++ b/dom/webidl/Document.webidl
@@ -12,16 +12,19 @@ interface URI;
 interface nsIDocShell;
 interface nsILoadGroup;
 
 enum VisibilityState { "hidden", "visible", "prerender" };
 
 /* https://dom.spec.whatwg.org/#dictdef-elementcreationoptions */
 dictionary ElementCreationOptions {
   DOMString is;
+
+  [ChromeOnly]
+  DOMString pseudo;
 };
 
 /* http://dom.spec.whatwg.org/#interface-document */
 [Constructor]
 interface Document : Node {
   [Throws]
   readonly attribute DOMImplementation implementation;
   [Pure, Throws, BinaryName="documentURIFromJS", NeedsCallerType]
--- a/layout/base/GeckoRestyleManager.cpp
+++ b/layout/base/GeckoRestyleManager.cpp
@@ -726,16 +726,35 @@ ElementForStyleContext(nsIContent* aPare
     MOZ_ASSERT(f);
     while (f->GetType() != nsGkAtoms::numberControlFrame) {
       f = f->GetParent();
       MOZ_ASSERT(f);
     }
     return f->GetContent()->AsElement();
   }
 
+  Element* frameElement = aFrame->GetContent()->AsElement();
+  if (frameElement->IsNativeAnonymous() &&
+      nsCSSPseudoElements::PseudoElementIsJSCreatedNAC(aPseudoType)) {
+    // NAC-implemented pseudos use the closest non-NAC element as their
+    // element to inherit from.
+    //
+    // FIXME(heycam): In theory we shouldn't need to limit this only to
+    // JS-created pseudo-implementing NAC, as all pseudo-implementing
+    // should use the closest non-native anonymous ancestor element as
+    // its originating element.  But removing that part of the condition
+    // reveals some bugs in style resultion with display:contents and
+    // XBL.  See bug 1345809.
+    Element* originatingElement =
+      nsContentUtils::GetClosestNonNativeAnonymousAncestor(frameElement);
+    if (originatingElement) {
+      return originatingElement;
+    }
+  }
+
   if (aParentContent) {
     return aParentContent->AsElement();
   }
 
   MOZ_ASSERT(aFrame->GetContent()->GetParent(),
              "should not have got here for the root element");
   return aFrame->GetContent()->GetParent()->AsElement();
 }
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -5076,18 +5076,29 @@ nsCSSFrameConstructor::ResolveStyleConte
                                            LazyComputeBehavior::Assert,
                                            aState->mTreeMatchContext);
       } else {
         result = styleSet->ResolveStyleFor(aContent->AsElement(),
                                            aParentStyleContext,
                                            LazyComputeBehavior::Assert);
       }
     } else {
+      MOZ_ASSERT(aContent->IsInNativeAnonymousSubtree());
+      if (!aOriginatingElementOrNull) {
+        // For pseudo-implementing NAC created by JS using the ChromeOnly
+        // document.createElement(..., { pseudo: ... }) API, we find the
+        // originating element by lookup the tree until we find a non-NAC
+        // ancestor.  (These are the correct semantics for C++-generated pseudo-
+        // implementing NAC as well, but for those cases we already have a
+        // correct originating element passed in.)
+        MOZ_ASSERT(nsCSSPseudoElements::PseudoElementIsJSCreatedNAC(pseudoType));
+        aOriginatingElementOrNull =
+          nsContentUtils::GetClosestNonNativeAnonymousAncestor(aContent->AsElement());
+      }
       MOZ_ASSERT(aOriginatingElementOrNull);
-      MOZ_ASSERT(aContent->IsInNativeAnonymousSubtree());
       result = styleSet->ResolvePseudoElementStyle(aOriginatingElementOrNull,
                                                    pseudoType,
                                                    aParentStyleContext,
                                                    aContent->AsElement());
     }
   } else {
     MOZ_ASSERT(!aOriginatingElementOrNull);
     NS_ASSERTION(aContent->IsNodeOfType(nsINode::eTEXT),
--- a/layout/style/nsCSSPseudoElements.h
+++ b/layout/style/nsCSSPseudoElements.h
@@ -31,16 +31,20 @@
 // Flag that indicate the pseudo-element supports a user action pseudo-class
 // following it, such as :active or :hover.  This would normally correspond
 // to whether the pseudo-element is tree-like, but we don't support these
 // pseudo-classes on ::before and ::after generated content yet.  See
 // http://dev.w3.org/csswg/selectors4/#pseudo-elements.
 #define CSS_PSEUDO_ELEMENT_SUPPORTS_USER_ACTION_STATE  (1<<3)
 // Is content prevented from parsing selectors containing this pseudo-element?
 #define CSS_PSEUDO_ELEMENT_UA_SHEET_ONLY               (1<<4)
+// Can we use the ChromeOnly document.createElement(..., { pseudo: "::foo" })
+// API for creating pseudo-implementing native anonymous content in JS with this
+// pseudo-element?
+#define CSS_PSEUDO_ELEMENT_IS_JS_CREATED_NAC           (1<<5)
 
 namespace mozilla {
 
 // The total count of CSSPseudoElement is less than 256,
 // so use uint8_t as its underlying type.
 typedef uint8_t CSSPseudoElementTypeBase;
 enum class CSSPseudoElementType : CSSPseudoElementTypeBase {
   // If the actual pseudo-elements stop being first here, change
@@ -95,16 +99,21 @@ public:
   static bool PseudoElementSupportsStyleAttribute(const Type aType) {
     MOZ_ASSERT(aType < Type::Count);
     return PseudoElementHasFlags(aType,
                                  CSS_PSEUDO_ELEMENT_SUPPORTS_STYLE_ATTRIBUTE);
   }
 
   static bool PseudoElementSupportsUserActionState(const Type aType);
 
+  static bool PseudoElementIsJSCreatedNAC(Type aType)
+  {
+    return PseudoElementHasFlags(aType, CSS_PSEUDO_ELEMENT_IS_JS_CREATED_NAC);
+  }
+
   static bool IsEnabled(Type aType, EnabledState aEnabledState)
   {
     return !PseudoElementHasFlags(aType, CSS_PSEUDO_ELEMENT_UA_SHEET_ONLY) ||
            (aEnabledState & EnabledState::eInUASheets);
   }
 
 private:
   // Does the given pseudo-element have all of the flags given?
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -861,28 +861,31 @@ nsComputedDOMStyle::UpdateCurrentStyleSo
   }
 
   if (!mStyleContext || mStyleContext->HasPseudoElementData()) {
 #ifdef DEBUG
     if (mStyleContext) {
       // We want to check that going through this path because of
       // HasPseudoElementData is rare, because it slows us down a good
       // bit.  So check that we're really inside something associated
-      // with a pseudo-element that contains elements.
+      // with a pseudo-element that contains elements.  (We also allow
+      // the element to be NAC, just in case some chrome JS calls
+      // getComputedStyle on a NAC-implemented pseudo.)
       nsStyleContext* topWithPseudoElementData = mStyleContext;
       while (topWithPseudoElementData->GetParent()->HasPseudoElementData()) {
         topWithPseudoElementData = topWithPseudoElementData->GetParent();
       }
       CSSPseudoElementType pseudo = topWithPseudoElementData->GetPseudoType();
       nsIAtom* pseudoAtom = nsCSSPseudoElements::GetPseudoAtom(pseudo);
       nsAutoString assertMsg(
         NS_LITERAL_STRING("we should be in a pseudo-element that is expected to contain elements ("));
       assertMsg.Append(nsDependentString(pseudoAtom->GetUTF16String()));
       assertMsg.Append(')');
-      NS_ASSERTION(nsCSSPseudoElements::PseudoElementContainsElements(pseudo),
+      NS_ASSERTION(nsCSSPseudoElements::PseudoElementContainsElements(pseudo) ||
+                   mContent->IsNativeAnonymous(),
                    NS_LossyConvertUTF16toASCII(assertMsg).get());
     }
 #endif
     // Need to resolve a style context
     RefPtr<nsStyleContext> resolvedStyleContext =
       nsComputedDOMStyle::GetStyleContextForElement(mContent->AsElement(),
                                                     mPseudo,
                                                     mPresShell,