Bug 773296 - Part 8: Resolve and compute CSS variables. r=dbaron
authorCameron McCormack <cam@mcc.id.au>
Thu, 12 Dec 2013 13:09:41 +1100
changeset 177062 d08b6b8c6ecaa462871d1cb2e4522ea85ae10b01
parent 177061 407ca304dcdad27a18da76261dc82bd07b2bb1a5
child 177063 6c381791e1a1d10cc488beb07c9ebd567ef7bde1
push id462
push userraliiev@mozilla.com
push dateTue, 22 Apr 2014 00:22:30 +0000
treeherdermozilla-release@ac5db8c74ac0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdbaron
bugs773296
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 773296 - Part 8: Resolve and compute CSS variables. r=dbaron We add a new class CSSVariableResolver whose job is to take the inherited computed variables and the specified variable declarations and to perform cycle removal and resolution of the variables, storing the result in the CSSVariableValues object on an nsStyleVariables. We use CSSVariableResolver in nsRuleNode::ComputeVariablesData. The variable resolver does this: 1. Asks the CSSVariableValues and CSSVariableDeclarations objects to add their variables to it. 2. Calls in to a new nsCSSParser function EnumerateVariableReferences that informs the resolver which other variables a given variable references, and by doing so, builds a graph of variable dependencies. 3. Removes variables involved in cyclic references using Tarjan's strongly connected component algorithm, setting those variables to have an invalid value. 4. Calls in to a new nsCSSParser function ResolveVariableValue to resolve the remaining valid variables by substituting variable references. We extend nsCSSParser::ParseValueWithVariables to take a callback function to be invoked when encountering a variable reference. This lets EnumerateVariableReferences re-use ParseValueWithVariables. CSSParserImpl::ResolveValueWithVariableReferences needs different error handling behaviour from ParseValueWithVariables, so we don't re-use it. CSSParserImpl::AppendImpliedEOFCharacters is used to take the value returned from nsCSSScanner::GetImpliedEOFCharacters while resolving variable references that were declared using custom properties that encountered EOF before being closed properly. The SeparatorRequiredBetweenTokens helper function in nsCSSParser.cpp implements the serialization rules in CSS Syntax Module Level 3: https://dvcs.w3.org/hg/csswg/raw-file/3479cdefc59a/css-syntax/Overview.html#serialization
layout/style/CSSVariableDeclarations.cpp
layout/style/CSSVariableDeclarations.h
layout/style/CSSVariableResolver.cpp
layout/style/CSSVariableResolver.h
layout/style/CSSVariableValues.cpp
layout/style/CSSVariableValues.h
layout/style/generate-stylestructlist.py
layout/style/moz.build
layout/style/nsCSSParser.cpp
layout/style/nsCSSParser.h
layout/style/nsCSSScanner.cpp
layout/style/nsCSSScanner.h
layout/style/nsRuleNode.cpp
--- a/layout/style/CSSVariableDeclarations.cpp
+++ b/layout/style/CSSVariableDeclarations.cpp
@@ -2,16 +2,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* CSS Custom Property assignments for a Declaration at a given priority */
 
 #include "CSSVariableDeclarations.h"
 
+#include "CSSVariableResolver.h"
+#include "nsCSSScanner.h"
 #include "nsRuleData.h"
 
 // These two special string values are used to represent specified values of
 // 'initial' and 'inherit'.  (Note that neither is a valid variable value.)
 #define INITIAL_VALUE "!"
 #define INHERIT_VALUE ";"
 
 namespace mozilla {
@@ -142,16 +144,55 @@ CSSVariableDeclarations::MapRuleInfoInto
   if (!aRuleData->mVariables) {
     aRuleData->mVariables = new CSSVariableDeclarations(*this);
   } else {
     mVariables.EnumerateRead(EnumerateVariableForMapRuleInfoInto,
                              aRuleData->mVariables.get());
   }
 }
 
+/* static */ PLDHashOperator
+CSSVariableDeclarations::EnumerateVariableForAddVariablesToResolver(
+                                                         const nsAString& aName,
+                                                         nsString aValue,
+                                                         void* aData)
+{
+  CSSVariableResolver* resolver = static_cast<CSSVariableResolver*>(aData);
+  if (aValue.EqualsLiteral(INITIAL_VALUE)) {
+    // Values of 'initial' are treated the same as an invalid value in the
+    // variable resolver.
+    resolver->Put(aName, EmptyString(),
+                  eCSSTokenSerialization_Nothing,
+                  eCSSTokenSerialization_Nothing,
+                  false);
+  } else if (aValue.EqualsLiteral(INHERIT_VALUE)) {
+    // Values of 'inherit' don't need any handling, since it means we just need
+    // to keep whatever value is currently in the resolver.  This is because
+    // the specified variable declarations already have only the winning
+    // declaration for the variable and no longer have any of the others.
+  } else {
+    // At this point, we don't know what token types are at the start and end
+    // of the specified variable value.  These will be determined later during
+    // the resolving process.
+    resolver->Put(aName, aValue,
+                  eCSSTokenSerialization_Nothing,
+                  eCSSTokenSerialization_Nothing,
+                  false);
+  }
+  return PL_DHASH_NEXT;
+}
+
+void
+CSSVariableDeclarations::AddVariablesToResolver(
+                                           CSSVariableResolver* aResolver) const
+{
+  mVariables.EnumerateRead(EnumerateVariableForAddVariablesToResolver,
+                           aResolver);
+}
+
 static size_t
 SizeOfTableEntry(const nsAString& aKey,
                  const nsString& aValue,
                  MallocSizeOf aMallocSizeOf,
                  void* aUserArg)
 {
   size_t n = 0;
   n += aKey.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
--- a/layout/style/CSSVariableDeclarations.h
+++ b/layout/style/CSSVariableDeclarations.h
@@ -5,16 +5,19 @@
 
 /* CSS Custom Property assignments for a Declaration at a given priority */
 
 #ifndef mozilla_CSSVariableDeclarations_h
 #define mozilla_CSSVariableDeclarations_h
 
 #include "nsDataHashtable.h"
 
+namespace mozilla {
+class CSSVariableResolver;
+}
 class nsRuleData;
 
 namespace mozilla {
 
 class CSSVariableDeclarations
 {
 public:
   CSSVariableDeclarations();
@@ -99,29 +102,39 @@ public:
   uint32_t Count() const { return mVariables.Count(); }
 
   /**
    * Copies each variable value from this object into aRuleData, unless that
    * variable already exists on aRuleData.
    */
   void MapRuleInfoInto(nsRuleData* aRuleData);
 
+  /**
+   * Copies the variables from this object into aResolver, marking them as
+   * specified values.
+   */
+  void AddVariablesToResolver(CSSVariableResolver* aResolver) const;
+
   size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
 
 private:
   /**
    * Adds all the variable declarations from aOther into this object.
    */
   void CopyVariablesFrom(const CSSVariableDeclarations& aOther);
   static PLDHashOperator EnumerateVariableForCopy(const nsAString& aName,
                                                   nsString aValue,
                                                   void* aData);
   static PLDHashOperator
     EnumerateVariableForMapRuleInfoInto(const nsAString& aName,
                                         nsString aValue,
                                         void* aData);
+  static PLDHashOperator
+    EnumerateVariableForAddVariablesToResolver(const nsAString& aName,
+                                               nsString aValue,
+                                               void* aData);
 
   nsDataHashtable<nsStringHashKey, nsString> mVariables;
 };
 
 } // namespace mozilla
 
 #endif
new file mode 100644
--- /dev/null
+++ b/layout/style/CSSVariableResolver.cpp
@@ -0,0 +1,257 @@
+/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* object that resolves CSS variables using specified and inherited variable
+ * values
+ */
+
+#include "CSSVariableResolver.h"
+
+#include "CSSVariableDeclarations.h"
+#include "CSSVariableValues.h"
+#include "mozilla/PodOperations.h"
+#include <algorithm>
+
+namespace mozilla {
+
+/**
+ * Data used by the EnumerateVariableReferences callback.  Reset must be called
+ * on it before it is used.
+ */
+class EnumerateVariableReferencesData
+{
+public:
+  EnumerateVariableReferencesData(CSSVariableResolver& aResolver)
+    : mResolver(aResolver)
+    , mReferences(new bool[aResolver.mVariables.Length()])
+  {
+  }
+
+  /**
+   * Resets the data so that it can be passed to another call of
+   * EnumerateVariableReferences for a different variable.
+   */
+  void Reset()
+  {
+    PodZero(mReferences.get(), mResolver.mVariables.Length());
+    mReferencesNonExistentVariable = false;
+  }
+
+  void RecordVariableReference(const nsAString& aVariableName)
+  {
+    size_t id;
+    if (mResolver.mVariableIDs.Get(aVariableName, &id)) {
+      mReferences[id] = true;
+    } else {
+      mReferencesNonExistentVariable = true;
+    }
+  }
+
+  bool HasReferenceToVariable(size_t aID) const
+  {
+    return mReferences[aID];
+  }
+
+  bool ReferencesNonExistentVariable() const
+  {
+   return mReferencesNonExistentVariable;
+  }
+
+private:
+  CSSVariableResolver& mResolver;
+
+  // Array of booleans, where each index is a variable ID.  If an element is
+  // true, it indicates that the variable we have called
+  // EnumerateVariableReferences for has a reference to the variable with
+  // that ID.
+  nsAutoArrayPtr<bool> mReferences;
+
+  // Whether the variable we have called EnumerateVariableReferences for
+  // references a variable that does not exist in the resolver.
+  bool mReferencesNonExistentVariable;
+};
+
+static void
+RecordVariableReference(const nsAString& aVariableName,
+                        void* aData)
+{
+  static_cast<EnumerateVariableReferencesData*>(aData)->
+    RecordVariableReference(aVariableName);
+}
+
+void
+CSSVariableResolver::RemoveCycles(size_t v)
+{
+  mVariables[v].mIndex = mNextIndex;
+  mVariables[v].mLowLink = mNextIndex;
+  mVariables[v].mInStack = true;
+  mStack.AppendElement(v);
+  mNextIndex++;
+
+  for (size_t i = 0, n = mReferences[v].Length(); i < n; i++) {
+    size_t w = mReferences[v][i];
+    if (!mVariables[w].mIndex) {
+      RemoveCycles(w);
+      mVariables[v].mLowLink = std::min(mVariables[v].mLowLink,
+                                        mVariables[w].mLowLink);
+    } else if (mVariables[w].mInStack) {
+      mVariables[v].mLowLink = std::min(mVariables[v].mLowLink,
+                                        mVariables[w].mIndex);
+    }
+  }
+
+  if (mVariables[v].mLowLink == mVariables[v].mIndex) {
+    if (mStack.LastElement() == v) {
+      // A strongly connected component consisting of a single variable is not
+      // necessarily invalid.  We handle variables that reference themselves
+      // earlier, in CSSVariableResolver::Resolve.
+      mVariables[mStack.LastElement()].mInStack = false;
+      mStack.TruncateLength(mStack.Length() - 1);
+    } else {
+      size_t w;
+      do {
+        w = mStack.LastElement();
+        mVariables[w].mValue.Truncate(0);
+        mVariables[w].mInStack = false;
+        mStack.TruncateLength(mStack.Length() - 1);
+      } while (w != v);
+    }
+  }
+}
+
+void
+CSSVariableResolver::ResolveVariable(size_t aID)
+{
+  if (mVariables[aID].mValue.IsEmpty() || mVariables[aID].mWasInherited) {
+    // The variable is invalid or was inherited.   We can just copy the value
+    // and its first/last token information across.
+    mOutput->Put(mVariables[aID].mVariableName,
+                 mVariables[aID].mValue,
+                 mVariables[aID].mFirstToken,
+                 mVariables[aID].mLastToken);
+  } else {
+    // Otherwise we need to resolve the variable references, after resolving
+    // all of our dependencies first.  We do this even for variables that we
+    // know do not reference other variables so that we can find their
+    // first/last token.
+    //
+    // XXX We might want to do this first/last token finding during
+    // EnumerateVariableReferences, so that we can avoid calling
+    // ResolveVariableValue and parsing the value again.
+    for (size_t i = 0, n = mReferences[aID].Length(); i < n; i++) {
+      size_t j = mReferences[aID][i];
+      if (aID != j && !mVariables[j].mResolved) {
+        ResolveVariable(j);
+      }
+    }
+    nsString resolvedValue;
+    nsCSSTokenSerializationType firstToken, lastToken;
+    if (!mParser.ResolveVariableValue(mVariables[aID].mValue, mOutput,
+                                      resolvedValue, firstToken, lastToken)) {
+      resolvedValue.Truncate(0);
+    }
+    mOutput->Put(mVariables[aID].mVariableName, resolvedValue,
+                 firstToken, lastToken);
+  }
+  mVariables[aID].mResolved = true;
+}
+
+void
+CSSVariableResolver::Resolve(const CSSVariableValues* aInherited,
+                             const CSSVariableDeclarations* aSpecified)
+{
+  MOZ_ASSERT(!mResolved);
+
+  // The only time we would be worried about having a null aInherited is
+  // for the root, but in that case nsRuleNode::ComputeVariablesData will
+  // happen to pass in whatever we're using as mOutput for aInherited,
+  // which will initially be empty.
+  MOZ_ASSERT(aInherited);
+  MOZ_ASSERT(aSpecified);
+
+  aInherited->AddVariablesToResolver(this);
+  aSpecified->AddVariablesToResolver(this);
+
+  // First we look at each variable's value and record which other variables
+  // it references.
+  size_t n = mVariables.Length();
+  mReferences.SetLength(n);
+  EnumerateVariableReferencesData data(*this);
+  for (size_t id = 0; id < n; id++) {
+    data.Reset();
+    if (!mVariables[id].mWasInherited &&
+        !mVariables[id].mValue.IsEmpty()) {
+      if (mParser.EnumerateVariableReferences(mVariables[id].mValue,
+                                              RecordVariableReference,
+                                              &data)) {
+        // Convert the boolean array of dependencies in |data| to a list
+        // of dependencies.
+        for (size_t i = 0; i < n; i++) {
+          if (data.HasReferenceToVariable(i)) {
+            mReferences[id].AppendElement(i);
+          }
+        }
+        // Also record whether it referenced any variables that don't exist
+        // in the resolver, so that we can ensure we still resolve its value
+        // in ResolveVariable, even though its mReferences list is empty.
+        mVariables[id].mReferencesNonExistentVariable =
+          data.ReferencesNonExistentVariable();
+      } else {
+        MOZ_ASSERT(false, "EnumerateVariableReferences should not have failed "
+                          "if we previously parsed the specified value");
+        mVariables[id].mValue.Truncate(0);
+      }
+    }
+  }
+
+  // Next we remove any cycles in variable references using Tarjan's strongly
+  // connected components finding algorithm, setting variables in cycles to
+  // have an invalid value.
+  mNextIndex = 1;
+  for (size_t id = 0; id < n; id++) {
+    if (!mVariables[id].mIndex) {
+      RemoveCycles(id);
+      MOZ_ASSERT(mStack.IsEmpty());
+    }
+  }
+
+  // Finally we construct the computed value for the variable by substituting
+  // any variable references.
+  for (size_t id = 0; id < n; id++) {
+    if (!mVariables[id].mResolved) {
+      ResolveVariable(id);
+    }
+  }
+
+  mResolved = true;
+}
+
+void
+CSSVariableResolver::Put(const nsAString& aVariableName,
+                         nsString aValue,
+                         nsCSSTokenSerializationType aFirstToken,
+                         nsCSSTokenSerializationType aLastToken,
+                         bool aWasInherited)
+{
+  MOZ_ASSERT(!mResolved);
+
+  size_t id;
+  if (mVariableIDs.Get(aVariableName, &id)) {
+    MOZ_ASSERT(mVariables[id].mWasInherited && !aWasInherited,
+               "should only overwrite inherited variables with specified "
+               "variables");
+    mVariables[id].mValue = aValue;
+    mVariables[id].mFirstToken = aFirstToken;
+    mVariables[id].mLastToken = aLastToken;
+    mVariables[id].mWasInherited = aWasInherited;
+  } else {
+    id = mVariables.Length();
+    mVariableIDs.Put(aVariableName, id);
+    mVariables.AppendElement(Variable(aVariableName, aValue,
+                                      aFirstToken, aLastToken, aWasInherited));
+  }
+}
+
+}
new file mode 100644
--- /dev/null
+++ b/layout/style/CSSVariableResolver.h
@@ -0,0 +1,144 @@
+/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* object that resolves CSS variables using specified and inherited variable
+ * values
+ */
+
+#ifndef mozilla_CSSVariableResolver_h
+#define mozilla_CSSVariableResolver_h
+
+#include "mozilla/DebugOnly.h"
+#include "nsCSSParser.h"
+#include "nsCSSScanner.h"
+#include "nsDataHashtable.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+class CSSVariableDeclarations;
+class CSSVariableValues;
+class EnumerateVariableReferencesData;
+
+class CSSVariableResolver
+{
+  friend class CSSVariableDeclarations;
+  friend class CSSVariableValues;
+  friend class EnumerateVariableReferencesData;
+public:
+  /**
+   * Creates a new CSSVariableResolver that will output a set of resolved,
+   * computed variables into aOutput.
+   */
+  CSSVariableResolver(CSSVariableValues* aOutput)
+    : mOutput(aOutput)
+    , mResolved(false)
+  {
+    MOZ_ASSERT(aOutput);
+  }
+
+  /**
+   * Resolves the set of inherited variables from aInherited and the
+   * set of specified variables from aSpecified.  The resoled variables
+   * are written in to mOutput.
+   */
+  void Resolve(const CSSVariableValues* aInherited,
+               const CSSVariableDeclarations* aSpecified);
+
+private:
+  struct Variable
+  {
+    Variable(const nsAString& aVariableName,
+             nsString aValue,
+             nsCSSTokenSerializationType aFirstToken,
+             nsCSSTokenSerializationType aLastToken,
+             bool aWasInherited)
+      : mVariableName(aVariableName)
+      , mValue(aValue)
+      , mFirstToken(aFirstToken)
+      , mLastToken(aLastToken)
+      , mWasInherited(aWasInherited)
+      , mResolved(false)
+      , mReferencesNonExistentVariable(false)
+      , mInStack(false)
+      , mIndex(0)
+      , mLowLink(0) { }
+
+    nsString mVariableName;
+    nsString mValue;
+    nsCSSTokenSerializationType mFirstToken;
+    nsCSSTokenSerializationType mLastToken;
+
+    // Whether this variable came from the set of inherited variables.
+    bool mWasInherited;
+
+    // Whether this variable has been resolved yet.
+    bool mResolved;
+
+    // Whether this variables includes any references to non-existent variables.
+    bool mReferencesNonExistentVariable;
+
+    // Bookkeeping for the cycle remover algorithm.
+    bool mInStack;
+    size_t mIndex;
+    size_t mLowLink;
+  };
+
+  /**
+   * Adds or modifies an existing entry in the set of variables to be resolved.
+   * This is intended to be called by the AddVariablesToResolver functions on
+   * the CSSVariableDeclarations and CSSVariableValues objects passed in to
+   * Resolve.
+   *
+   * @param aName The variable name (not including any "var-" prefix that would
+   *   be part of the custom property name) whose value is to be set.
+   * @param aValue The variable value.
+   * @param aFirstToken The type of token at the start of the variable value.
+   * @param aLastToken The type of token at the en of the variable value.
+   * @param aWasInherited Whether this variable came from the set of inherited
+   *   variables.
+   */
+  void Put(const nsAString& aVariableName,
+           nsString aValue,
+           nsCSSTokenSerializationType aFirstToken,
+           nsCSSTokenSerializationType aLastToken,
+           bool aWasInherited);
+
+  // Helper functions for Resolve.
+  void RemoveCycles(size_t aID);
+  void ResolveVariable(size_t aID);
+
+  // A mapping of variable names to an ID that indexes into mVariables
+  // and mReferences.
+  nsDataHashtable<nsStringHashKey, size_t> mVariableIDs;
+
+  // The set of variables.
+  nsTArray<Variable> mVariables;
+
+  // The list of variables that each variable references.
+  nsTArray<nsTArray<size_t> > mReferences;
+
+  // The next index to assign to a variable found during the cycle removing
+  // algorithm's traversal of the variable reference graph.
+  size_t mNextIndex;
+
+  // Stack of variable IDs that we push to as we traverse the variable reference
+  // graph while looking for cycles.  Variable::mInStack reflects whether a
+  // given variable has its ID in mStack.
+  nsTArray<size_t> mStack;
+
+  // CSS parser to use for parsing property values with variable references.
+  nsCSSParser mParser;
+
+  // The object to output the resolved variables into.
+  CSSVariableValues* mOutput;
+
+  // Whether Resolve has been called.
+  DebugOnly<bool> mResolved;
+};
+
+}
+
+#endif
--- a/layout/style/CSSVariableValues.cpp
+++ b/layout/style/CSSVariableValues.cpp
@@ -2,16 +2,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* computed CSS Variable values */
 
 #include "CSSVariableValues.h"
 
+#include "CSSVariableResolver.h"
+
 namespace mozilla {
 
 CSSVariableValues::CSSVariableValues()
 {
   MOZ_COUNT_CTOR(CSSVariableValues);
 }
 
 CSSVariableValues::CSSVariableValues(const CSSVariableValues& aOther)
@@ -91,9 +93,21 @@ CSSVariableValues::CopyVariablesFrom(con
   for (size_t i = 0, n = aOther.mVariables.Length(); i < n; i++) {
     Put(aOther.mVariables[i].mVariableName,
         aOther.mVariables[i].mValue,
         aOther.mVariables[i].mFirstToken,
         aOther.mVariables[i].mLastToken);
   }
 }
 
+void
+CSSVariableValues::AddVariablesToResolver(CSSVariableResolver* aResolver) const
+{
+  for (size_t i = 0, n = mVariables.Length(); i < n; i++) {
+    aResolver->Put(mVariables[i].mVariableName,
+                   mVariables[i].mValue,
+                   mVariables[i].mFirstToken,
+                   mVariables[i].mLastToken,
+                   true);
+  }
+}
+
 } // namespace mozilla
--- a/layout/style/CSSVariableValues.h
+++ b/layout/style/CSSVariableValues.h
@@ -9,16 +9,18 @@
 #define mozilla_CSSVariableValues_h
 
 #include "nsCSSScanner.h"
 #include "nsDataHashtable.h"
 #include "nsTArray.h"
 
 namespace mozilla {
 
+class CSSVariableResolver;
+
 class CSSVariableValues
 {
 public:
   CSSVariableValues();
   CSSVariableValues(const CSSVariableValues& aOther);
 #ifdef DEBUG
   ~CSSVariableValues();
 #endif
@@ -64,16 +66,22 @@ public:
    * @param aFirstToken The type of token at the start of the variable value.
    * @param aLastToken The type of token at the en of the variable value.
    */
   void Put(const nsAString& aName,
            nsString aValue,
            nsCSSTokenSerializationType aFirstToken,
            nsCSSTokenSerializationType aLastToken);
 
+  /**
+   * Copies the variables from this object into aResolver, marking them as
+   * computed, inherited values.
+   */
+  void AddVariablesToResolver(CSSVariableResolver* aResolver) const;
+
 private:
   struct Variable
   {
     Variable()
       : mFirstToken(eCSSTokenSerialization_Nothing)
       , mLastToken(eCSSTokenSerialization_Nothing)
     {}
 
--- a/layout/style/generate-stylestructlist.py
+++ b/layout/style/generate-stylestructlist.py
@@ -47,17 +47,17 @@ STYLE_STRUCTS = [("INHERITED",) + x for 
     ("Color",          "CheckColorCallback"),
     ("List",           "nullptr"),
     ("Text",           "CheckTextCallback"),
     ("Visibility",     "nullptr"),
     ("Quotes",         "nullptr"),
     ("UserInterface",  "nullptr"),
     ("TableBorder",    "nullptr"),
     ("SVG",            "nullptr"),
-    ("Variables",      "nullptr"),
+    ("Variables",      "CheckVariablesCallback"),
 ]] + [("RESET",) + x for x in [
     # Reset style structs.
     ("Background",     "nullptr"),
     ("Position",       "nullptr"),
     ("TextReset",      "nullptr"),
     ("Display",        "nullptr"),
     ("Content",        "nullptr"),
     ("UIReset",        "nullptr"),
--- a/layout/style/moz.build
+++ b/layout/style/moz.build
@@ -55,16 +55,17 @@ EXPORTS += [
     'nsStyleStructFwd.h',
     'nsStyleStructInlines.h',
     'nsStyleTransformMatrix.h',
     'nsStyleUtil.h',
 ]
 
 EXPORTS.mozilla += [
     'CSSVariableDeclarations.h',
+    'CSSVariableResolver.h',
     'CSSVariableValues.h',
 ]
 
 EXPORTS.mozilla.dom += [
     'CSS.h',
     'CSSValue.h',
 ]
 
@@ -79,16 +80,17 @@ EXPORTS.mozilla.css += [
     'Rule.h',
     'StyleRule.h',
 ]
 
 UNIFIED_SOURCES += [
     'AnimationCommon.cpp',
     'CSS.cpp',
     'CSSVariableDeclarations.cpp',
+    'CSSVariableResolver.cpp',
     'CSSVariableValues.cpp',
     'Declaration.cpp',
     'ErrorReporter.cpp',
     'ImageLoader.cpp',
     'Loader.cpp',
     'nsAnimationManager.cpp',
     'nsComputedDOMStyle.cpp',
     'nsCSSAnonBoxes.cpp',
--- a/layout/style/nsCSSParser.cpp
+++ b/layout/style/nsCSSParser.cpp
@@ -149,16 +149,54 @@ public:
                                    nsIURI* aBaseURL,
                                    nsIPrincipal* aDocPrincipal);
 
   bool EvaluateSupportsCondition(const nsAString& aCondition,
                                  nsIURI* aDocURL,
                                  nsIURI* aBaseURL,
                                  nsIPrincipal* aDocPrincipal);
 
+  typedef nsCSSParser::VariableEnumFunc VariableEnumFunc;
+
+  /**
+   * Parses a CSS token stream value and invokes a callback function for each
+   * variable reference that is encountered.
+   *
+   * @param aPropertyValue The CSS token stream value.
+   * @param aFunc The callback function to invoke; its parameters are the
+   *   variable name found and the aData argument passed in to this function.
+   * @param aData User data to pass in to the callback.
+   * @return Whether aPropertyValue could be parsed as a valid CSS token stream
+   *   value (e.g., without syntactic errors in variable references).
+   */
+  bool EnumerateVariableReferences(const nsAString& aPropertyValue,
+                                   VariableEnumFunc aFunc,
+                                   void* aData);
+
+  /**
+   * Parses aPropertyValue as a CSS token stream value and resolves any
+   * variable references using the variables in aVariables.
+   *
+   * @param aPropertyValue The CSS token stream value.
+   * @param aVariables The set of variable values to use when resolving variable
+   *   references.
+   * @param aResult Out parameter that gets the resolved value.
+   * @param aFirstToken Out parameter that gets the type of the first token in
+   *   aResult.
+   * @param aLastToken Out parameter that gets the type of the last token in
+   *   aResult.
+   * @return Whether aResult could be parsed successfully and variable reference
+   *   substitution succeeded.
+   */
+  bool ResolveVariableValue(const nsAString& aPropertyValue,
+                            const CSSVariableValues* aVariables,
+                            nsString& aResult,
+                            nsCSSTokenSerializationType& aFirstToken,
+                            nsCSSTokenSerializationType& aLastToken);
+
 protected:
   class nsAutoParseCompoundProperty;
   friend class nsAutoParseCompoundProperty;
 
   class nsAutoFailingSupportsRule;
   friend class nsAutoFailingSupportsRule;
 
   class nsAutoSuppressErrors;
@@ -262,16 +300,20 @@ protected:
 
   // returns true when the stop symbol is found, and false for EOF
   bool SkipUntil(PRUnichar aStopSymbol);
   void SkipUntilOneOf(const PRUnichar* aStopSymbolChars);
   // For each character in aStopSymbolChars from the end of the array
   // to the start, calls SkipUntil with that character.
   typedef nsAutoTArray<PRUnichar, 16> StopSymbolCharStack;
   void SkipUntilAllOf(const StopSymbolCharStack& aStopSymbolChars);
+  // returns true if the stop symbol or EOF is found, and false for an
+  // unexpected ')', ']' or '}'; this not safe to call outside variable
+  // resolution, as it doesn't handle mismatched content
+  bool SkipBalancedContentUntil(PRUnichar aStopSymbol);
 
   void SkipRuleSet(bool aInsideBraces);
   bool SkipAtRule(bool aInsideBlock);
   bool SkipDeclaration(bool aCheckForBraces);
 
   void PushGroup(css::GroupRule* aRule);
   void PopGroup();
 
@@ -319,16 +361,39 @@ protected:
   bool ParseSupportsConditionInParens(bool& aConditionMet);
   bool ParseSupportsConditionInParensInsideParens(bool& aConditionMet);
   bool ParseSupportsConditionTerms(bool& aConditionMet);
   enum SupportsConditionTermOperator { eAnd, eOr };
   bool ParseSupportsConditionTermsAfterOperator(
                                        bool& aConditionMet,
                                        SupportsConditionTermOperator aOperator);
 
+  /**
+   * Parses the current input stream for a CSS token stream value and resolves
+   * any variable references using the variables in aVariables.
+   *
+   * @param aVariables The set of variable values to use when resolving variable
+   *   references.
+   * @param aResult Out parameter that, if the function returns true, will be
+   *   replaced with the resolved value.
+   * @return Whether aResult could be parsed successfully and variable reference
+   *   substitution succeeded.
+   */
+  bool ResolveValueWithVariableReferences(
+                              const CSSVariableValues* aVariables,
+                              nsString& aResult,
+                              nsCSSTokenSerializationType& aResultFirstToken,
+                              nsCSSTokenSerializationType& aResultLastToken);
+  // Helper function for ResolveValueWithVariableReferences.
+  bool ResolveValueWithVariableReferencesRec(
+                             nsString& aResult,
+                             nsCSSTokenSerializationType& aResultFirstToken,
+                             nsCSSTokenSerializationType& aResultLastToken,
+                             const CSSVariableValues* aVariables);
+
   enum nsSelectorParsingStatus {
     // we have parsed a selector and we saw a token that cannot be
     // part of a selector:
     eSelectorParsingStatus_Done,
     // we should continue parsing the selector:
     eSelectorParsingStatus_Continue,
     // we saw an unexpected token or token value,
     // or we saw end-of-file with an unfinished selector:
@@ -564,22 +629,43 @@ protected:
                                 nsString& aValue);
 
   /**
    * Parses a CSS variable value.  This could be 'initial', 'inherit'
    * or a token stream, which may or may not include variable references.
    *
    * @param aType Out parameter into which the type of the variable value
    *   will be stored.
-   * @param aClosingChars Out parameter appended to which will be any
-   *   closing characters that were implied when encountering EOF.
+   * @param aDropBackslash Out parameter indicating whether during variable
+   *   value parsing there was a trailing backslash before EOF that must
+   *   be dropped when serializing the variable value.
+   * @param aImpliedCharacters Out parameter appended to which will be any
+   *   characters that were implied when encountering EOF and which
+   *   must be included at the end of the serialized variable value.
+   * @param aFunc A callback function to invoke when a variable reference
+   *   is encountered.  May be null.  Arguments are the variable name
+   *   and the aData argument passed in to this function.
+   * @param User data to pass in to the callback.
    * @return Whether parsing succeeded.
    */
   bool ParseValueWithVariables(CSSVariableDeclarations::Type* aType,
-                               nsString& aClosingChars);
+                               bool* aDropBackslash,
+                               nsString& aImpliedCharacters,
+                               void (*aFunc)(const nsAString&, void*),
+                               void* aData);
+
+  /**
+   * Returns whether the scanner dropped a backslash just before EOF.
+   */
+  bool BackslashDropped();
+
+  /**
+   * Calls AppendImpliedEOFCharacters on mScanner.
+   */
+  void AppendImpliedEOFCharacters(nsAString& aResult);
 
   // Reused utility parsing routines
   void AppendValue(nsCSSProperty aPropID, const nsCSSValue& aValue);
   bool ParseBoxProperties(const nsCSSProperty aPropIDs[]);
   bool ParseGroupedBoxProperty(int32_t aVariantMask,
                                nsCSSValue& aValue);
   bool ParseDirectionalBoxProperty(nsCSSProperty aProperty,
                                      int32_t aSourceType);
@@ -1370,16 +1456,531 @@ CSSParserImpl::EvaluateSupportsCondition
   bool parsedOK = ParseSupportsCondition(conditionMet) && !GetToken(true);
 
   CLEAR_ERROR();
   ReleaseScanner();
 
   return parsedOK && conditionMet;
 }
 
+bool
+CSSParserImpl::EnumerateVariableReferences(const nsAString& aPropertyValue,
+                                           VariableEnumFunc aFunc,
+                                           void* aData)
+{
+  nsCSSScanner scanner(aPropertyValue, 0);
+  css::ErrorReporter reporter(scanner, nullptr, nullptr, nullptr);
+  InitScanner(scanner, reporter, nullptr, nullptr, nullptr);
+  nsAutoSuppressErrors suppressErrors(this);
+
+  CSSVariableDeclarations::Type type;
+  bool dropBackslash;
+  nsString impliedCharacters;
+  bool result = ParseValueWithVariables(&type, &dropBackslash,
+                                        impliedCharacters, aFunc, aData) &&
+                !GetToken(true);
+
+  ReleaseScanner();
+
+  return result;
+}
+
+static bool
+SeparatorRequiredBetweenTokens(nsCSSTokenSerializationType aToken1,
+                               nsCSSTokenSerializationType aToken2)
+{
+  // The two lines marked with (*) do not correspond to entries in
+  // the table in the css-syntax spec but which we need to handle,
+  // as we treat them as whole tokens.
+  switch (aToken1) {
+    case eCSSTokenSerialization_Ident:
+      return aToken2 == eCSSTokenSerialization_Ident ||
+             aToken2 == eCSSTokenSerialization_Function ||
+             aToken2 == eCSSTokenSerialization_URL_or_BadURL ||
+             aToken2 == eCSSTokenSerialization_Symbol_Minus ||
+             aToken2 == eCSSTokenSerialization_Number ||
+             aToken2 == eCSSTokenSerialization_Percentage ||
+             aToken2 == eCSSTokenSerialization_Dimension ||
+             aToken2 == eCSSTokenSerialization_URange ||
+             aToken2 == eCSSTokenSerialization_CDC ||
+             aToken2 == eCSSTokenSerialization_Symbol_OpenParen;
+    case eCSSTokenSerialization_AtKeyword_or_Hash:
+    case eCSSTokenSerialization_Dimension:
+      return aToken2 == eCSSTokenSerialization_Ident ||
+             aToken2 == eCSSTokenSerialization_Function ||
+             aToken2 == eCSSTokenSerialization_URL_or_BadURL ||
+             aToken2 == eCSSTokenSerialization_Symbol_Minus ||
+             aToken2 == eCSSTokenSerialization_Number ||
+             aToken2 == eCSSTokenSerialization_Percentage ||
+             aToken2 == eCSSTokenSerialization_Dimension ||
+             aToken2 == eCSSTokenSerialization_URange ||
+             aToken2 == eCSSTokenSerialization_CDC;
+    case eCSSTokenSerialization_Symbol_Hash:
+      return aToken2 == eCSSTokenSerialization_Ident ||
+             aToken2 == eCSSTokenSerialization_Function ||
+             aToken2 == eCSSTokenSerialization_URL_or_BadURL ||
+             aToken2 == eCSSTokenSerialization_Symbol_Minus ||
+             aToken2 == eCSSTokenSerialization_Number ||
+             aToken2 == eCSSTokenSerialization_Percentage ||
+             aToken2 == eCSSTokenSerialization_Dimension ||
+             aToken2 == eCSSTokenSerialization_URange;
+    case eCSSTokenSerialization_Symbol_Minus:
+    case eCSSTokenSerialization_Number:
+      return aToken2 == eCSSTokenSerialization_Ident ||
+             aToken2 == eCSSTokenSerialization_Function ||
+             aToken2 == eCSSTokenSerialization_URL_or_BadURL ||
+             aToken2 == eCSSTokenSerialization_Number ||
+             aToken2 == eCSSTokenSerialization_Percentage ||
+             aToken2 == eCSSTokenSerialization_Dimension ||
+             aToken2 == eCSSTokenSerialization_URange;
+    case eCSSTokenSerialization_Symbol_At:
+      return aToken2 == eCSSTokenSerialization_Ident ||
+             aToken2 == eCSSTokenSerialization_Function ||
+             aToken2 == eCSSTokenSerialization_URL_or_BadURL ||
+             aToken2 == eCSSTokenSerialization_Symbol_Minus ||
+             aToken2 == eCSSTokenSerialization_URange;
+    case eCSSTokenSerialization_URange:
+      return aToken2 == eCSSTokenSerialization_Ident ||
+             aToken2 == eCSSTokenSerialization_Function ||
+             aToken2 == eCSSTokenSerialization_Number ||
+             aToken2 == eCSSTokenSerialization_Percentage ||
+             aToken2 == eCSSTokenSerialization_Dimension ||
+             aToken2 == eCSSTokenSerialization_Symbol_Question;
+    case eCSSTokenSerialization_Symbol_Dot_or_Plus:
+      return aToken2 == eCSSTokenSerialization_Number ||
+             aToken2 == eCSSTokenSerialization_Percentage ||
+             aToken2 == eCSSTokenSerialization_Dimension;
+    case eCSSTokenSerialization_Symbol_Assorted:
+    case eCSSTokenSerialization_Symbol_Asterisk:
+      return aToken2 == eCSSTokenSerialization_Symbol_Equals;
+    case eCSSTokenSerialization_Symbol_Bar:
+      return aToken2 == eCSSTokenSerialization_Symbol_Equals ||
+             aToken2 == eCSSTokenSerialization_Symbol_Bar ||
+             aToken2 == eCSSTokenSerialization_DashMatch;              // (*)
+    case eCSSTokenSerialization_Symbol_Slash:
+      return aToken2 == eCSSTokenSerialization_Symbol_Asterisk ||
+             aToken2 == eCSSTokenSerialization_ContainsMatch;          // (*)
+    default:
+      MOZ_ASSERT(aToken1 == eCSSTokenSerialization_Nothing ||
+                 aToken1 == eCSSTokenSerialization_Whitespace ||
+                 aToken1 == eCSSTokenSerialization_Percentage ||
+                 aToken1 == eCSSTokenSerialization_URL_or_BadURL ||
+                 aToken1 == eCSSTokenSerialization_Function ||
+                 aToken1 == eCSSTokenSerialization_CDC ||
+                 aToken1 == eCSSTokenSerialization_Symbol_OpenParen ||
+                 aToken1 == eCSSTokenSerialization_Symbol_Question ||
+                 aToken1 == eCSSTokenSerialization_Symbol_Assorted ||
+                 aToken1 == eCSSTokenSerialization_Symbol_Asterisk ||
+                 aToken1 == eCSSTokenSerialization_Symbol_Equals ||
+                 aToken1 == eCSSTokenSerialization_Symbol_Bar ||
+                 aToken1 == eCSSTokenSerialization_Symbol_Slash ||
+                 aToken1 == eCSSTokenSerialization_Other ||
+                 "unexpected nsCSSTokenSerializationType value");
+      return false;
+  }
+}
+
+/**
+ * Appends aValue to aResult, possibly inserting an empty CSS
+ * comment between the two to ensure that tokens from both strings
+ * remain separated.
+ */
+static void
+AppendTokens(nsAString& aResult,
+             nsCSSTokenSerializationType& aResultFirstToken,
+             nsCSSTokenSerializationType& aResultLastToken,
+             nsCSSTokenSerializationType aValueFirstToken,
+             nsCSSTokenSerializationType aValueLastToken,
+             const nsAString& aValue)
+{
+  if (SeparatorRequiredBetweenTokens(aResultLastToken, aValueFirstToken)) {
+    aResult.AppendLiteral("/**/");
+  }
+  aResult.Append(aValue);
+  if (aResultFirstToken == eCSSTokenSerialization_Nothing) {
+    aResultFirstToken = aValueFirstToken;
+  }
+  if (aValueLastToken != eCSSTokenSerialization_Nothing) {
+    aResultLastToken = aValueLastToken;
+  }
+}
+
+/**
+ * Stops the given scanner recording, and appends the recorded result
+ * to aResult, possibly inserting an empty CSS comment between the two to
+ * ensure that tokens from both strings remain separated.
+ */
+static void
+StopRecordingAndAppendTokens(nsString& aResult,
+                             nsCSSTokenSerializationType& aResultFirstToken,
+                             nsCSSTokenSerializationType& aResultLastToken,
+                             nsCSSTokenSerializationType aValueFirstToken,
+                             nsCSSTokenSerializationType aValueLastToken,
+                             nsCSSScanner* aScanner)
+{
+  if (SeparatorRequiredBetweenTokens(aResultLastToken, aValueFirstToken)) {
+    aResult.AppendLiteral("/**/");
+  }
+  aScanner->StopRecording(aResult);
+  if (aResultFirstToken == eCSSTokenSerialization_Nothing) {
+    aResultFirstToken = aValueFirstToken;
+  }
+  if (aValueLastToken != eCSSTokenSerialization_Nothing) {
+    aResultLastToken = aValueLastToken;
+  }
+}
+
+bool
+CSSParserImpl::ResolveValueWithVariableReferencesRec(
+                                     nsString& aResult,
+                                     nsCSSTokenSerializationType& aResultFirstToken,
+                                     nsCSSTokenSerializationType& aResultLastToken,
+                                     const CSSVariableValues* aVariables)
+{
+  // This function assumes we are already recording, and will leave the scanner
+  // recording when it returns.
+  MOZ_ASSERT(mScanner->IsRecording());
+  MOZ_ASSERT(aResult.IsEmpty());
+
+  // Stack of closing characters for currently open constructs.
+  nsAutoTArray<PRUnichar, 16> stack;
+
+  // The resolved value for this ResolveValueWithVariableReferencesRec call.
+  nsString value;
+
+  // The length of the scanner's recording before the currently parsed token.
+  // This is used so that when we encounter a "var(" token, we can strip
+  // it off the end of the recording, regardless of how long the token was.
+  // (With escapes, it could be longer than four characters.)
+  uint32_t lengthBeforeVar = 0;
+
+  // Tracking the type of token that appears at the start and end of |value|
+  // and that appears at the start and end of the scanner recording.  These are
+  // used to determine whether we need to insert "/**/" when pasting token
+  // streams together.
+  nsCSSTokenSerializationType valueFirstToken = eCSSTokenSerialization_Nothing,
+                              valueLastToken  = eCSSTokenSerialization_Nothing,
+                              recFirstToken   = eCSSTokenSerialization_Nothing,
+                              recLastToken    = eCSSTokenSerialization_Nothing;
+
+#define UPDATE_RECORDING_TOKENS(type)                    \
+  if (recFirstToken == eCSSTokenSerialization_Nothing) { \
+    recFirstToken = type;                                \
+  }                                                      \
+  recLastToken = type;
+
+  while (GetToken(false)) {
+    switch (mToken.mType) {
+      case eCSSToken_Symbol: {
+        nsCSSTokenSerializationType type = eCSSTokenSerialization_Other;
+        if (mToken.mSymbol == '(') {
+          stack.AppendElement(')');
+          type = eCSSTokenSerialization_Symbol_OpenParen;
+        } else if (mToken.mSymbol == '[') {
+          stack.AppendElement(']');
+        } else if (mToken.mSymbol == '{') {
+          stack.AppendElement('}');
+        } else if (mToken.mSymbol == ';') {
+          if (stack.IsEmpty()) {
+            // A ';' that is at the top level of the value or at the top level
+            // of a variable reference's fallback is invalid.
+            return false;
+          }
+        } else if (mToken.mSymbol == '!') {
+          if (stack.IsEmpty()) {
+            // An '!' that is at the top level of the value or at the top level
+            // of a variable reference's fallback is invalid.
+            return false;
+          }
+        } else if (mToken.mSymbol == ')' &&
+                   stack.IsEmpty()) {
+          // We're closing a "var(".
+          nsString finalTokens;
+          mScanner->StopRecording(finalTokens);
+          MOZ_ASSERT(finalTokens[finalTokens.Length() - 1] == ')');
+          finalTokens.Truncate(finalTokens.Length() - 1);
+          aResult.Append(value);
+
+          AppendTokens(aResult, valueFirstToken, valueLastToken,
+                       recFirstToken, recLastToken, finalTokens);
+
+          mScanner->StartRecording();
+          UngetToken();
+          aResultFirstToken = valueFirstToken;
+          aResultLastToken = valueLastToken;
+          return true;
+        } else if (mToken.mSymbol == ')' ||
+                   mToken.mSymbol == ']' ||
+                   mToken.mSymbol == '}') {
+          if (stack.IsEmpty() ||
+              stack.LastElement() != mToken.mSymbol) {
+            // A mismatched closing bracket is invalid.
+            return false;
+          }
+          stack.TruncateLength(stack.Length() - 1);
+        } else if (mToken.mSymbol == '#') {
+          type = eCSSTokenSerialization_Symbol_Hash;
+        } else if (mToken.mSymbol == '@') {
+          type = eCSSTokenSerialization_Symbol_At;
+        } else if (mToken.mSymbol == '.' ||
+                   mToken.mSymbol == '+') {
+          type = eCSSTokenSerialization_Symbol_Dot_or_Plus;
+        } else if (mToken.mSymbol == '-') {
+          type = eCSSTokenSerialization_Symbol_Minus;
+        } else if (mToken.mSymbol == '?') {
+          type = eCSSTokenSerialization_Symbol_Question;
+        } else if (mToken.mSymbol == '$' ||
+                   mToken.mSymbol == '^' ||
+                   mToken.mSymbol == '~') {
+          type = eCSSTokenSerialization_Symbol_Assorted;
+        } else if (mToken.mSymbol == '=') {
+          type = eCSSTokenSerialization_Symbol_Equals;
+        } else if (mToken.mSymbol == '|') {
+          type = eCSSTokenSerialization_Symbol_Bar;
+        } else if (mToken.mSymbol == '/') {
+          type = eCSSTokenSerialization_Symbol_Slash;
+        } else if (mToken.mSymbol == '*') {
+          type = eCSSTokenSerialization_Symbol_Asterisk;
+        }
+        UPDATE_RECORDING_TOKENS(type);
+        break;
+      }
+
+      case eCSSToken_Function:
+        if (mToken.mIdent.LowerCaseEqualsLiteral("var")) {
+          // Save the tokens before the "var(" to our resolved value.
+          nsString recording;
+          mScanner->StopRecording(recording);
+          recording.Truncate(lengthBeforeVar);
+          AppendTokens(value, valueFirstToken, valueLastToken,
+                       recFirstToken, recLastToken, recording);
+          recFirstToken = eCSSTokenSerialization_Nothing;
+          recLastToken = eCSSTokenSerialization_Nothing;
+
+          if (!GetToken(true) ||
+              mToken.mType != eCSSToken_Ident) {
+            // "var(" must be followed by an identifier.
+            return false;
+          }
+
+          // Get the value of the identified variable.  Note that we
+          // check if the variable value is the empty string, as that means
+          // that the variable was invalid at computed value time due to
+          // unresolveable variable references or cycles.
+          const nsString& variableName = mToken.mIdent;
+          nsString variableValue;
+          nsCSSTokenSerializationType varFirstToken, varLastToken;
+          bool valid = aVariables->Get(variableName, variableValue,
+                                       varFirstToken, varLastToken) &&
+                       !variableValue.IsEmpty();
+
+          if (!GetToken(true) ||
+              mToken.IsSymbol(')')) {
+            mScanner->StartRecording();
+            if (!valid) {
+              // Invalid variable with no fallback.
+              return false;
+            }
+            // Valid variable with no fallback.
+            AppendTokens(value, valueFirstToken, valueLastToken,
+                         varFirstToken, varLastToken, variableValue);
+          } else if (mToken.IsSymbol(',')) {
+            mScanner->StartRecording();
+            if (!GetToken(false) ||
+                mToken.IsSymbol(')')) {
+              // Comma must be followed by at least one fallback token.
+              return false;
+            }
+            UngetToken();
+            if (valid) {
+              // Valid variable with ignored fallback.
+              mScanner->StopRecording();
+              AppendTokens(value, valueFirstToken, valueLastToken,
+                           varFirstToken, varLastToken, variableValue);
+              bool ok = SkipBalancedContentUntil(')');
+              mScanner->StartRecording();
+              if (!ok) {
+                return false;
+              }
+            } else {
+              nsString fallback;
+              if (!ResolveValueWithVariableReferencesRec(fallback,
+                                                         varFirstToken,
+                                                         varLastToken,
+                                                         aVariables)) {
+                // Fallback value had invalid tokens or an invalid variable reference
+                // that itself had no fallback.
+                return false;
+              }
+              AppendTokens(value, valueFirstToken, valueLastToken,
+                           varFirstToken, varLastToken, fallback);
+              // Now we're either at the pushed back ')' that finished the
+              // fallback or at EOF.
+              DebugOnly<bool> gotToken = GetToken(false);
+              MOZ_ASSERT(!gotToken || mToken.IsSymbol(')'));
+            }
+          } else {
+            // Expected ',' or ')' after the variable name.
+            mScanner->StartRecording();
+            return false;
+          }
+        } else {
+          stack.AppendElement(')');
+          UPDATE_RECORDING_TOKENS(eCSSTokenSerialization_Function);
+        }
+        break;
+
+      case eCSSToken_Bad_String:
+      case eCSSToken_Bad_URL:
+        return false;
+
+      case eCSSToken_Whitespace:
+        UPDATE_RECORDING_TOKENS(eCSSTokenSerialization_Whitespace);
+        break;
+
+      case eCSSToken_AtKeyword:
+      case eCSSToken_Hash:
+        UPDATE_RECORDING_TOKENS(eCSSTokenSerialization_AtKeyword_or_Hash);
+        break;
+
+      case eCSSToken_Number:
+        UPDATE_RECORDING_TOKENS(eCSSTokenSerialization_Number);
+        break;
+
+      case eCSSToken_Dimension:
+        UPDATE_RECORDING_TOKENS(eCSSTokenSerialization_Dimension);
+        break;
+
+      case eCSSToken_Ident:
+        UPDATE_RECORDING_TOKENS(eCSSTokenSerialization_Ident);
+        break;
+
+      case eCSSToken_Percentage:
+        UPDATE_RECORDING_TOKENS(eCSSTokenSerialization_Percentage);
+        break;
+
+      case eCSSToken_URange:
+        UPDATE_RECORDING_TOKENS(eCSSTokenSerialization_URange);
+        break;
+
+      case eCSSToken_URL:
+        UPDATE_RECORDING_TOKENS(eCSSTokenSerialization_URL_or_BadURL);
+        break;
+
+      case eCSSToken_HTMLComment:
+        if (mToken.mIdent[0] == '-') {
+          UPDATE_RECORDING_TOKENS(eCSSTokenSerialization_CDC);
+        } else {
+          UPDATE_RECORDING_TOKENS(eCSSTokenSerialization_Other);
+        }
+        break;
+
+      case eCSSToken_Dashmatch:
+        UPDATE_RECORDING_TOKENS(eCSSTokenSerialization_DashMatch);
+        break;
+
+      case eCSSToken_Containsmatch:
+        UPDATE_RECORDING_TOKENS(eCSSTokenSerialization_ContainsMatch);
+        break;
+
+      default:
+        NS_NOTREACHED("unexpected token type");
+        // fall through
+      case eCSSToken_ID:
+      case eCSSToken_String:
+      case eCSSToken_Includes:
+      case eCSSToken_Beginsmatch:
+      case eCSSToken_Endsmatch:
+        UPDATE_RECORDING_TOKENS(eCSSTokenSerialization_Other);
+        break;
+    }
+
+    lengthBeforeVar = mScanner->RecordingLength();
+  }
+
+#undef UPDATE_RECORDING_TOKENS
+
+  aResult.Append(value);
+  StopRecordingAndAppendTokens(aResult, valueFirstToken, valueLastToken,
+                               recFirstToken, recLastToken, mScanner);
+
+  // Append any implicitly closed brackets.
+  if (!stack.IsEmpty()) {
+    do {
+      aResult.Append(stack.LastElement());
+      stack.TruncateLength(stack.Length() - 1);
+    } while (!stack.IsEmpty());
+    valueLastToken = eCSSTokenSerialization_Other;
+  }
+
+  mScanner->StartRecording();
+  aResultFirstToken = valueFirstToken;
+  aResultLastToken = valueLastToken;
+  return true;
+}
+
+bool
+CSSParserImpl::ResolveValueWithVariableReferences(
+                                        const CSSVariableValues* aVariables,
+                                        nsString& aResult,
+                                        nsCSSTokenSerializationType& aFirstToken,
+                                        nsCSSTokenSerializationType& aLastToken)
+{
+  aResult.Truncate(0);
+
+  // Start recording before we read the first token.
+  mScanner->StartRecording();
+
+  if (!GetToken(false)) {
+    // Value was empty since we reached EOF.
+    mScanner->StopRecording();
+    return false;
+  }
+
+  UngetToken();
+
+  nsString value;
+  nsCSSTokenSerializationType firstToken, lastToken;
+  bool ok = ResolveValueWithVariableReferencesRec(value, firstToken, lastToken, aVariables) &&
+            !GetToken(true);
+
+  mScanner->StopRecording();
+
+  if (ok) {
+    aResult = value;
+    aFirstToken = firstToken;
+    aLastToken = lastToken;
+  }
+  return ok;
+}
+
+bool
+CSSParserImpl::ResolveVariableValue(const nsAString& aPropertyValue,
+                                    const CSSVariableValues* aVariables,
+                                    nsString& aResult,
+                                    nsCSSTokenSerializationType& aFirstToken,
+                                    nsCSSTokenSerializationType& aLastToken)
+{
+  nsCSSScanner scanner(aPropertyValue, 0);
+
+  // At this point, we know that aPropertyValue is syntactically correct
+  // for a token stream that has variable references.  We also won't be
+  // interpreting any of the stream as we parse it, apart from expanding
+  // var() references, so we don't need a base URL etc. or any useful
+  // error reporting.
+  css::ErrorReporter reporter(scanner, nullptr, nullptr, nullptr);
+  InitScanner(scanner, reporter, nullptr, nullptr, nullptr);
+
+  bool valid = ResolveValueWithVariableReferences(aVariables, aResult,
+                                                  aFirstToken, aLastToken);
+
+  ReleaseScanner();
+  return valid;
+}
+
 //----------------------------------------------------------------------
 
 bool
 CSSParserImpl::GetToken(bool aSkipWS)
 {
   if (mHavePushBack) {
     mHavePushBack = false;
     if (!aSkipWS || mToken.mType != eCSSToken_Whitespace) {
@@ -2976,16 +3577,57 @@ CSSParserImpl::SkipUntil(PRUnichar aStop
       }
     } else if (eCSSToken_Function == tk->mType ||
                eCSSToken_Bad_URL == tk->mType) {
       stack.AppendElement(')');
     }
   }
 }
 
+bool
+CSSParserImpl::SkipBalancedContentUntil(PRUnichar aStopSymbol)
+{
+  nsCSSToken* tk = &mToken;
+  nsAutoTArray<PRUnichar, 16> stack;
+  stack.AppendElement(aStopSymbol);
+  for (;;) {
+    if (!GetToken(true)) {
+      return true;
+    }
+    if (eCSSToken_Symbol == tk->mType) {
+      PRUnichar symbol = tk->mSymbol;
+      uint32_t stackTopIndex = stack.Length() - 1;
+      if (symbol == stack.ElementAt(stackTopIndex)) {
+        stack.RemoveElementAt(stackTopIndex);
+        if (stackTopIndex == 0) {
+          return true;
+        }
+
+      // Just handle out-of-memory by parsing incorrectly.  It's
+      // highly unlikely we're dealing with a legitimate style sheet
+      // anyway.
+      } else if ('{' == symbol) {
+        stack.AppendElement('}');
+      } else if ('[' == symbol) {
+        stack.AppendElement(']');
+      } else if ('(' == symbol) {
+        stack.AppendElement(')');
+      } else if (')' == symbol ||
+                 ']' == symbol ||
+                 '}' == symbol) {
+        UngetToken();
+        return false;
+      }
+    } else if (eCSSToken_Function == tk->mType ||
+               eCSSToken_Bad_URL == tk->mType) {
+      stack.AppendElement(')');
+    }
+  }
+}
+
 void
 CSSParserImpl::SkipUntilOneOf(const PRUnichar* aStopSymbolChars)
 {
   nsCSSToken* tk = &mToken;
   nsDependentString stopSymbolChars(aStopSymbolChars);
   for (;;) {
     if (!GetToken(true)) {
       break;
@@ -11275,16 +11917,30 @@ CSSParserImpl::ParsePaintOrder()
     return false;
   }
 
   AppendValue(eCSSProperty_paint_order, value);
   return true;
 }
 
 bool
+CSSParserImpl::BackslashDropped()
+{
+  return mScanner->GetEOFCharacters() &
+         nsCSSScanner::eEOFCharacters_DropBackslash;
+}
+
+void
+CSSParserImpl::AppendImpliedEOFCharacters(nsAString& aResult)
+{
+  nsCSSScanner::AppendImpliedEOFCharacters(mScanner->GetEOFCharacters(),
+                                           aResult);
+}
+
+bool
 CSSParserImpl::ParseAll()
 {
   nsCSSValue value;
   if (!ParseVariant(value, VARIANT_INHERIT, nullptr)) {
     return false;
   }
 
   CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(p, eCSSProperty_all) {
@@ -11294,29 +11950,36 @@ CSSParserImpl::ParseAll()
 }
 
 bool
 CSSParserImpl::ParseVariableDeclaration(CSSVariableDeclarations::Type* aType,
                                         nsString& aValue)
 {
   CSSVariableDeclarations::Type type;
   nsString variableValue;
-  nsString closingBrackets;
+  bool dropBackslash;
+  nsString impliedCharacters;
 
   // Record the token stream while parsing a variable value.
   mScanner->StartRecording();
-  if (!ParseValueWithVariables(&type, closingBrackets)) {
+  if (!ParseValueWithVariables(&type, &dropBackslash, impliedCharacters,
+                               nullptr, nullptr)) {
     mScanner->StopRecording();
     return false;
   }
 
   if (type == CSSVariableDeclarations::eTokenStream) {
     // This was indeed a token stream value, so store it in variableValue.
     mScanner->StopRecording(variableValue);
-    variableValue.Append(closingBrackets);
+    if (dropBackslash) {
+      MOZ_ASSERT(!variableValue.IsEmpty() &&
+                 variableValue[variableValue.Length() - 1] == '\\');
+      variableValue.Truncate(variableValue.Length() - 1);
+    }
+    variableValue.Append(impliedCharacters);
   } else {
     // This was either 'inherit' or 'initial'; we don't need the recorded input.
     mScanner->StopRecording();
   }
 
   if (mHavePushBack && type == CSSVariableDeclarations::eTokenStream) {
     // If we came to the end of a valid variable declaration and a token was
     // pushed back, then it would have been ended by '!', ')', ';', ']' or '}'.
@@ -11333,17 +11996,20 @@ CSSParserImpl::ParseVariableDeclaration(
 
   *aType = type;
   aValue = variableValue;
   return true;
 }
 
 bool
 CSSParserImpl::ParseValueWithVariables(CSSVariableDeclarations::Type* aType,
-                                       nsString& aClosingChars)
+                                       bool* aDropBackslash,
+                                       nsString& aImpliedCharacters,
+                                       void (*aFunc)(const nsAString&, void*),
+                                       void* aData)
 {
   // A property value is invalid if it contains variable references and also:
   //
   //   * has unbalanced parens, brackets or braces
   //   * has any BAD_STRING or BAD_URL tokens
   //   * has any ';' or '!' tokens at the top level of a variable reference's
   //     fallback
   //
@@ -11381,17 +12047,20 @@ CSSParserImpl::ParseValueWithVariables(C
     UngetToken();
     REPORT_UNEXPECTED_TOKEN(PEVariableEmpty);
     return false;
   }
 
   if (mToken.mType == eCSSToken_Whitespace) {
     if (!GetToken(true)) {
       // Variable value was white space only.  This is valid.
+      MOZ_ASSERT(!BackslashDropped());
       *aType = CSSVariableDeclarations::eTokenStream;
+      *aDropBackslash = false;
+      AppendImpliedEOFCharacters(aImpliedCharacters);
       return true;
     }
   }
 
   // Look for 'initial' or 'inherit' as the first non-white space token.
   CSSVariableDeclarations::Type type = CSSVariableDeclarations::eTokenStream;
   if (mToken.mType == eCSSToken_Ident) {
     if (mToken.mIdent.LowerCaseEqualsLiteral("initial")) {
@@ -11399,29 +12068,34 @@ CSSParserImpl::ParseValueWithVariables(C
     } else if (mToken.mIdent.LowerCaseEqualsLiteral("inherit")) {
       type = CSSVariableDeclarations::eInherit;
     }
   }
 
   if (type != CSSVariableDeclarations::eTokenStream) {
     if (!GetToken(true)) {
       // Variable value was 'initial' or 'inherit' followed by EOF.
+      MOZ_ASSERT(!BackslashDropped());
       *aType = type;
+      *aDropBackslash = false;
+      AppendImpliedEOFCharacters(aImpliedCharacters);
       return true;
     }
     UngetToken();
     if (mToken.mType == eCSSToken_Symbol &&
         (mToken.mSymbol == '!' ||
          mToken.mSymbol == ')' ||
          mToken.mSymbol == ';' ||
          mToken.mSymbol == ']' ||
          mToken.mSymbol == '}')) {
       // Variable value was 'initial' or 'inherit' followed by the end
       // of the declaration.
+      MOZ_ASSERT(!BackslashDropped());
       *aType = type;
+      *aDropBackslash = false;
       return true;
     }
   }
 
   do {
     switch (mToken.mType) {
       case eCSSToken_Symbol:
         if (mToken.mSymbol == '(') {
@@ -11429,30 +12103,34 @@ CSSParserImpl::ParseValueWithVariables(C
         } else if (mToken.mSymbol == '[') {
           stack.AppendElement(']');
         } else if (mToken.mSymbol == '{') {
           stack.AppendElement('}');
         } else if (mToken.mSymbol == ';' ||
                    mToken.mSymbol == '!') {
           if (stack.IsEmpty()) {
             UngetToken();
+            MOZ_ASSERT(!BackslashDropped());
             *aType = CSSVariableDeclarations::eTokenStream;
+            *aDropBackslash = false;
             return true;
           } else if (!references.IsEmpty() &&
                      references.LastElement() == stack.Length() - 1) {
             SkipUntilAllOf(stack);
             return false;
           }
         } else if (mToken.mSymbol == ')' ||
                    mToken.mSymbol == ']' ||
                    mToken.mSymbol == '}') {
           for (;;) {
             if (stack.IsEmpty()) {
               UngetToken();
+              MOZ_ASSERT(!BackslashDropped());
               *aType = CSSVariableDeclarations::eTokenStream;
+              *aDropBackslash = false;
               return true;
             }
             PRUnichar c = stack.LastElement();
             stack.TruncateLength(stack.Length() - 1);
             if (!references.IsEmpty() &&
                 references.LastElement() == stack.Length()) {
               references.TruncateLength(references.Length() - 1);
             }
@@ -11460,34 +12138,48 @@ CSSParserImpl::ParseValueWithVariables(C
               break;
             }
           }
         }
         break;
 
       case eCSSToken_Function:
         if (mToken.mIdent.LowerCaseEqualsLiteral("var")) {
-          if (GetToken(true)) {
-            if (mToken.mType != eCSSToken_Ident) {
-              UngetToken();
-              SkipUntil(')');
-              SkipUntilAllOf(stack);
-              return false;
-            }
+          if (!GetToken(true)) {
+            // EOF directly after "var(".
+            return false;
           }
-          if (ExpectSymbol(',', true)) {
-            if (ExpectSymbol(')', false)) {
+          if (mToken.mType != eCSSToken_Ident) {
+            // There must be an identifier directly after the "var(".
+            UngetToken();
+            SkipUntil(')');
+            SkipUntilAllOf(stack);
+            return false;
+          }
+          if (aFunc) {
+            aFunc(mToken.mIdent, aData);
+          }
+          if (!GetToken(true)) {
+            // EOF right after "var(<ident>".
+            stack.AppendElement(')');
+          } else if (mToken.IsSymbol(',')) {
+            // Variable reference with fallback.
+            if (!GetToken(false) || mToken.IsSymbol(')')) {
               // Comma must be followed by at least one fallback token.
               SkipUntilAllOf(stack);
               return false;
             }
+            UngetToken();
             references.AppendElement(stack.Length());
             stack.AppendElement(')');
-          } else if (!ExpectSymbol(')', true)) {
-            UngetToken();
+          } else if (mToken.IsSymbol(')')) {
+            // Correctly closed variable reference.
+          } else {
+            // Malformed variable reference.
+            SkipUntil(')');
             SkipUntilAllOf(stack);
             return false;
           }
         } else {
           stack.AppendElement(')');
         }
         break;
 
@@ -11501,19 +12193,21 @@ CSSParserImpl::ParseValueWithVariables(C
         return false;
 
       default:
         break;
     }
   } while (GetToken(true));
 
   // Append any implied closing characters.
+  *aDropBackslash = BackslashDropped();
+  AppendImpliedEOFCharacters(aImpliedCharacters);
   uint32_t i = stack.Length();
   while (i--) {
-    aClosingChars.Append(stack[i]);
+    aImpliedCharacters.Append(stack[i]);
   }
 
   *aType = type;
   return true;
 }
 
 } // anonymous namespace
 
@@ -11721,8 +12415,29 @@ bool
 nsCSSParser::EvaluateSupportsCondition(const nsAString& aCondition,
                                        nsIURI* aDocURL,
                                        nsIURI* aBaseURL,
                                        nsIPrincipal* aDocPrincipal)
 {
   return static_cast<CSSParserImpl*>(mImpl)->
     EvaluateSupportsCondition(aCondition, aDocURL, aBaseURL, aDocPrincipal);
 }
+
+bool
+nsCSSParser::EnumerateVariableReferences(const nsAString& aPropertyValue,
+                                         VariableEnumFunc aFunc,
+                                         void* aData)
+{
+  return static_cast<CSSParserImpl*>(mImpl)->
+    EnumerateVariableReferences(aPropertyValue, aFunc, aData);
+}
+
+bool
+nsCSSParser::ResolveVariableValue(const nsAString& aPropertyValue,
+                                  const CSSVariableValues* aVariables,
+                                  nsString& aResult,
+                                  nsCSSTokenSerializationType& aFirstToken,
+                                  nsCSSTokenSerializationType& aLastToken)
+{
+  return static_cast<CSSParserImpl*>(mImpl)->
+    ResolveVariableValue(aPropertyValue, aVariables,
+                         aResult, aFirstToken, aLastToken);
+}
--- a/layout/style/nsCSSParser.h
+++ b/layout/style/nsCSSParser.h
@@ -6,29 +6,31 @@
 /* parsing of CSS stylesheets, based on a token stream from the CSS scanner */
 
 #ifndef nsCSSParser_h___
 #define nsCSSParser_h___
 
 #include "mozilla/Attributes.h"
 
 #include "nsCSSProperty.h"
+#include "nsCSSScanner.h"
 #include "nsCOMPtr.h"
 #include "nsStringFwd.h"
 #include "nsTArrayForwardDeclare.h"
 
 class nsCSSStyleSheet;
 class nsIPrincipal;
 class nsIURI;
 struct nsCSSSelectorList;
 class nsMediaList;
 class nsCSSKeyframeRule;
 class nsCSSValue;
 
 namespace mozilla {
+class CSSVariableValues;
 namespace css {
 class Rule;
 class Declaration;
 class Loader;
 class StyleRule;
 }
 }
 
@@ -192,16 +194,37 @@ public:
    * Parse an @supports condition and returns the result of evaluating the
    * condition.
    */
   bool EvaluateSupportsCondition(const nsAString& aCondition,
                                  nsIURI* aDocURL,
                                  nsIURI* aBaseURL,
                                  nsIPrincipal* aDocPrincipal);
 
+  typedef void (*VariableEnumFunc)(const nsAString&, void*);
+
+  /**
+   * Parses aPropertyValue as a property value and calls aFunc for each
+   * variable reference that is found.  Returns false if there was
+   * a syntax error in the use of variable references.
+   */
+  bool EnumerateVariableReferences(const nsAString& aPropertyValue,
+                                   VariableEnumFunc aFunc,
+                                   void* aData);
+
+  /**
+   * Parses aPropertyValue as a property value and resolves variable references
+   * using the values in aVariables.
+   */
+  bool ResolveVariableValue(const nsAString& aPropertyValue,
+                            const mozilla::CSSVariableValues* aVariables,
+                            nsString& aResult,
+                            nsCSSTokenSerializationType& aFirstToken,
+                            nsCSSTokenSerializationType& aLastToken);
+
 protected:
   // This is a CSSParserImpl*, but if we expose that type name in this
   // header, we can't put the type definition (in nsCSSParser.cpp) in
   // the anonymous namespace.
   void* mImpl;
 };
 
 #endif /* nsCSSParser_h___ */
--- a/layout/style/nsCSSScanner.cpp
+++ b/layout/style/nsCSSScanner.cpp
@@ -381,16 +381,31 @@ void
 nsCSSScanner::StopRecording(nsString& aBuffer)
 {
   MOZ_ASSERT(mRecording, "haven't started recording");
   mRecording = false;
   aBuffer.Append(mBuffer + mRecordStartOffset,
                  mOffset - mRecordStartOffset);
 }
 
+uint32_t
+nsCSSScanner::RecordingLength() const
+{
+  MOZ_ASSERT(mRecording, "haven't started recording");
+  return mOffset - mRecordStartOffset;
+}
+
+#ifdef DEBUG
+bool
+nsCSSScanner::IsRecording() const
+{
+  return mRecording;
+}
+#endif
+
 nsDependentSubstring
 nsCSSScanner::GetCurrentLine() const
 {
   uint32_t end = mTokenOffset;
   while (end < mCount && !IsVertSpace(mBuffer[end])) {
     end++;
   }
   return nsDependentSubstring(mBuffer + mTokenLineOffset,
@@ -1075,28 +1090,21 @@ nsCSSScanner::AddEOFCharacters(uint32_t 
   mEOFCharacters = EOFCharacters(mEOFCharacters | aEOFCharacters);
 }
 
 static const PRUnichar kImpliedEOFCharacters[] = {
   UCS2_REPLACEMENT_CHAR, '*', '/', '"', '\'', ')', 0
 };
 
 /* static */ void
-nsCSSScanner::AdjustTokenStreamForEOFCharacters(EOFCharacters aEOFCharacters,
-                                                nsAString& aResult)
+nsCSSScanner::AppendImpliedEOFCharacters(EOFCharacters aEOFCharacters,
+                                         nsAString& aResult)
 {
-  uint32_t c = aEOFCharacters;
-
-  // First, handle eEOFCharacters_DropBackslash.
-  if (c & eEOFCharacters_DropBackslash) {
-    MOZ_ASSERT(aResult[aResult.Length() - 1] == '\\');
-    aResult.SetLength(aResult.Length() - 1);
-  }
-
-  c >>= 1;
+  // First, ignore eEOFCharacters_DropBackslash.
+  uint32_t c = aEOFCharacters >> 1;
 
   // All of the remaining EOFCharacters bits represent appended characters,
   // and the bits are in the order that they need appending.
   for (const PRUnichar* p = kImpliedEOFCharacters; *p && c; p++, c >>= 1) {
     if (c & 1) {
       aResult.Append(*p);
     }
   }
--- a/layout/style/nsCSSScanner.h
+++ b/layout/style/nsCSSScanner.h
@@ -225,16 +225,23 @@ class nsCSSScanner {
 
   // Abandons recording of the input stream.
   void StopRecording();
 
   // Stops recording of the input stream and appends the recorded
   // input to aBuffer.
   void StopRecording(nsString& aBuffer);
 
+  // Returns the length of the current recording.
+  uint32_t RecordingLength() const;
+
+#ifdef DEBUG
+  bool IsRecording() const;
+#endif
+
   enum EOFCharacters {
     eEOFCharacters_None =                    0x0000,
 
     // to handle \<EOF> inside strings
     eEOFCharacters_DropBackslash =           0x0001,
 
     // to handle \<EOF> outside strings
     eEOFCharacters_ReplacementChar =         0x0002,
@@ -248,21 +255,22 @@ class nsCSSScanner {
 
     // to close single-quoted strings
     eEOFCharacters_SingleQuote =             0x0020,
 
     // to close URLs
     eEOFCharacters_CloseParen =              0x0040,
   };
 
-  // Appends or drops any characters to/from the specified string
-  // the input stream to make the last token not rely on special EOF handling
-  // behavior.
-  static void AdjustTokenStreamForEOFCharacters(EOFCharacters aEOFCharacters,
-                                                nsAString& aString);
+  // Appends any characters to the specified string the input stream to make the
+  // last token not rely on special EOF handling behavior.
+  //
+  // If eEOFCharacters_DropBackslash is in aEOFCharacters, it is ignored.
+  static void AppendImpliedEOFCharacters(EOFCharacters aEOFCharacters,
+                                         nsAString& aString);
 
   EOFCharacters GetEOFCharacters() const {
 #ifdef DEBUG
     AssertEOFCharactersValid(mEOFCharacters);
 #endif
     return mEOFCharacters;
   }
 
--- a/layout/style/nsRuleNode.cpp
+++ b/layout/style/nsRuleNode.cpp
@@ -38,16 +38,17 @@
 #include "nsTArray.h"
 #include "nsContentUtils.h"
 #include "CSSCalc.h"
 #include "nsPrintfCString.h"
 #include "nsRenderingContext.h"
 #include "nsStyleUtil.h"
 #include "nsIDocument.h"
 #include "prtime.h"
+#include "CSSVariableResolver.h"
 
 #if defined(_MSC_VER) || defined(__MINGW32__)
 #include <malloc.h>
 #ifdef _MSC_VER
 #define alloca _alloca
 #endif
 #endif
 #ifdef SOLARIS
@@ -1657,16 +1658,28 @@ CheckTextCallback(const nsRuleData* aRul
       aResult = nsRuleNode::eRulePartialMixed;
     else if (aResult == nsRuleNode::eRuleFullReset)
       aResult = nsRuleNode::eRuleFullMixed;
   }
 
   return aResult;
 }
 
+static nsRuleNode::RuleDetail
+CheckVariablesCallback(const nsRuleData* aRuleData,
+                       nsRuleNode::RuleDetail aResult)
+{
+  // We don't actually have any properties on nsStyleVariables, so we do
+  // all of the RuleDetail calculation in here.
+  if (aRuleData->mVariables) {
+    return nsRuleNode::eRulePartialMixed;
+  }
+  return nsRuleNode::eRuleNone;
+}
+
 #define FLAG_DATA_FOR_PROPERTY(name_, id_, method_, flags_, pref_,          \
                                parsevariant_, kwtable_, stylestructoffset_, \
                                animtype_)                                   \
   flags_,
 
 // The order here must match the enums in *CheckCounter in nsCSSProps.cpp.
 
 static const uint32_t gFontFlags[] = {
@@ -8290,17 +8303,24 @@ nsRuleNode::ComputeVariablesData(void* a
                                  const nsRuleData* aRuleData,
                                  nsStyleContext* aContext,
                                  nsRuleNode* aHighestNode,
                                  const RuleDetail aRuleDetail,
                                  const bool aCanStoreInRuleTree)
 {
   COMPUTE_START_INHERITED(Variables, (), variables, parentVariables)
 
-  // ...
+  MOZ_ASSERT(aRuleData->mVariables,
+             "shouldn't be in ComputeVariablesData if there were no variable "
+             "declarations specified");
+
+  CSSVariableResolver resolver(&variables->mVariables);
+  resolver.Resolve(&parentVariables->mVariables,
+                   aRuleData->mVariables);
+  canStoreInRuleTree = false;
 
   COMPUTE_END_INHERITED(Variables, variables)
 }
 
 const void*
 nsRuleNode::GetStyleData(nsStyleStructID aSID,
                          nsStyleContext* aContext,
                          bool aComputeData)