diff --git a/core/includes/common.inc b/core/includes/common.inc
index 654d849c153a44a642908bc6930a5f16376378e8..e0d1861168548466db1f72ce59fa3cc87f1b82ea 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -1917,68 +1917,77 @@ function drupal_http_header_attributes(array $attributes = array()) {
  *   An HTML string containing a link to the given path.
  *
  * @see url()
+ * @see theme_link()
  */
 function l($text, $path, array $options = array()) {
-  static $use_theme = NULL;
+  // Build a variables array to keep the structure of the alter consistent with
+  // theme_link().
+  $variables = array(
+    'text' => $text,
+    'path' => $path,
+    'options' => $options,
+  );
 
-  // Merge in defaults.
-  $options += array(
+  // Merge in default options.
+  $variables['options'] += array(
     'attributes' => array(),
     'query' => array(),
     'html' => FALSE,
+    'language' => NULL,
   );
 
-  // Append active class.
-  // The link is only active, if its path corresponds to the current path, the
-  // language of the linked path is equal to the current language, and if the
-  // query parameters of the link equal those of the current request, since the
-  // same request with different query parameters may yield a different page
-  // (e.g., pagers).
-  $is_active = ($path == current_path() || ($path == '<front>' && drupal_is_front_page()));
-  $is_active = $is_active && (empty($options['language']) || $options['language']->langcode == language(LANGUAGE_TYPE_URL)->langcode);
-  $is_active = $is_active && (drupal_container()->get('request')->query->all() == $options['query']);
-  if ($is_active) {
-    $options['attributes']['class'][] = 'active';
-  }
-
-  // Remove all HTML and PHP tags from a tooltip. For best performance, we act only
-  // if a quick strpos() pre-check gave a suspicion (because strip_tags() is expensive).
-  if (isset($options['attributes']['title']) && strpos($options['attributes']['title'], '<') !== FALSE) {
-    $options['attributes']['title'] = strip_tags($options['attributes']['title']);
-  }
-
-  // Determine if rendering of the link is to be done with a theme function
-  // or the inline default. Inline is faster, but if the theme system has been
-  // loaded and a module or theme implements a preprocess or process function
-  // or overrides the theme_link() function, then invoke theme(). Preliminary
-  // benchmarks indicate that invoking theme() can slow down the l() function
-  // by 20% or more, and that some of the link-heavy Drupal pages spend more
-  // than 10% of the total page request time in the l() function.
-  if (!isset($use_theme) && function_exists('theme')) {
-    // Allow edge cases to prevent theme initialization and force inline link
-    // rendering.
-    if (config('system.performance')->get('theme_link')) {
-      drupal_theme_initialize();
-      $registry = theme_get_registry(FALSE);
-      // We don't want to duplicate functionality that's in theme(), so any
-      // hint of a module or theme doing anything at all special with the 'link'
-      // theme hook should simply result in theme() being called. This includes
-      // the overriding of theme_link() with an alternate function or template,
-      // the presence of preprocess or process functions, or the presence of
-      // include files.
-      $use_theme = !isset($registry['link']['function']) || ($registry['link']['function'] != 'theme_link');
-      $use_theme = $use_theme || !empty($registry['link']['preprocess functions']) || !empty($registry['link']['process functions']) || !empty($registry['link']['includes']);
-    }
-    else {
-      $use_theme = FALSE;
-    }
+  // Because l() is called very often we statically cache values that require an
+  // extra function call.
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast['active'])) {
+    $drupal_static_fast['active'] = &drupal_static(__FUNCTION__);
+  }
+  $active = &$drupal_static_fast['active'];
+  if (!isset($active)) {
+    $active = array(
+      'path' => current_path(),
+      'front_page' => drupal_is_front_page(),
+      'language' => language(LANGUAGE_TYPE_URL)->langcode,
+      'query' => Drupal::service('request')->query->all(),
+    );
   }
-  if ($use_theme) {
-    return theme('link', array('text' => $text, 'path' => $path, 'options' => $options));
+
+  // Determine whether this link is "active', meaning that it links to the
+  // current page. It is important that we stop checking "active" conditions if
+  // we know the link is not active. This helps ensure that l() remains fast.
+  // An active link's path is equal to the current path.
+  $variables['url_is_active'] = ($path == $active['path'] || ($path == '<front>' && $active['front_page']))
+  // The language of an active link is equal to the current language.
+  && (empty($variables['options']['language']) || $variables['options']['language']->langcode == $active['language'])
+  // The query parameters of an active link are equal to the current parameters.
+  && ($variables['options']['query'] == $active['query']);
+
+  // Add the "active" class if appropriate.
+  if ($variables['url_is_active']) {
+    $variables['options']['attributes']['class'][] = 'active';
+  }
+
+  // Remove all HTML and PHP tags from a tooltip, calling expensive strip_tags()
+  // only when a quick strpos() gives suspicion tags are present.
+  if (isset($variables['options']['attributes']['title']) && strpos($variables['options']['attributes']['title'], '<') !== FALSE) {
+    $variables['options']['attributes']['title'] = strip_tags($variables['options']['attributes']['title']);
   }
+
+  // Allow other modules to modify the structure of the link.
+  Drupal::service('module_handler')->alter('link', $variables);
+
+  // Move attributes out of options. url() doesn't need them.
+  $attributes = new Attribute($variables['options']['attributes']);
+  unset($variables['options']['attributes']);
+
   // The result of url() is a plain-text URL. Because we are using it here
   // in an HTML argument context, we need to encode it properly.
-  return '<a href="' . check_plain(url($path, $options)) . '"' . new Attribute($options['attributes']) . '>' . ($options['html'] ? $text : check_plain($text)) . '</a>';
+  $url = check_plain(url($variables['path'], $variables['options']));
+
+  // Sanitize the link text if necessary.
+  $text = $variables['options']['html'] ? $variables['text'] : check_plain($variables['text']);
+
+  return '<a href="' . $url . '"' . $attributes . '>' . $text . '</a>';
 }
 
 /**
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 47c99d9126459eec5d42ef6025a7adc1319874e9..6ea8af2b9860df449fde930a048fe9fa2a495a87 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -1683,23 +1683,22 @@ function theme_status_messages($variables) {
 /**
  * Returns HTML for a link.
  *
- * All Drupal code that outputs a link should call the l() function. That
- * function performs some initial preprocessing, and then, if necessary, calls
- * theme('link') for rendering the anchor tag.
+ * This is a wrapper around l() to allow for more flexible link themeing.
  *
- * To optimize performance for sites that don't need custom theming of links,
- * the l() function includes an inline copy of this function, and uses that
- * copy if none of the enabled modules or the active theme implement any
- * preprocess or process functions or override this theme implementation.
+ * Where performance is more important than theme flexibility, Drupal code that
+ * outputs a link should call the l() function directly, as #theme 'link'
+ * implementations have a measurable performance impact.
  *
  * @param $variables
  *   An associative array containing the keys 'text', 'path', and 'options'.
- *   See the l() function for information about these variables.
+ *   See the l() function for information about these variables. However, unlike
+ *   'text' in l(), both render arrays and strings are supported here.
  *
  * @see l()
  */
 function theme_link($variables) {
-  return '<a href="' . check_plain(url($variables['path'], $variables['options'])) . '"' . new Attribute($variables['options']['attributes']) . '>' . ($variables['options']['html'] ? $variables['text'] : check_plain($variables['text'])) . '</a>';
+  $rendered_text = is_array($variables['text']) ? drupal_render($variables['text']) : $variables['text'];
+  return l($rendered_text, $variables['path'], $variables['options']);
 }
 
 /**
diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php
index ac834d1167efb362fddc0272441f8668b05742ec..8088f6a64d972b40b7c0029ee6a0e858adbb5722 100644
--- a/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php
+++ b/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php
@@ -19,7 +19,7 @@ class LanguageSwitchingTest extends WebTestBase {
    *
    * @var array
    */
-  public static $modules = array('language', 'block');
+  public static $modules = array('language', 'block', 'language_test');
 
   public static function getInfo() {
     return array(
@@ -89,4 +89,99 @@ function testLanguageBlock() {
     $this->assertIdentical($anchors, array('active' => array('en'), 'inactive' => array('fr')), 'Only the current language anchor is marked as active on the language switcher block.');
   }
 
+  /**
+   * Test active class on links when switching languages.
+   */
+  function testLanguageLinkActiveClass() {
+    // Add language.
+    $edit = array(
+      'predefined_langcode' => 'fr',
+    );
+    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
+
+    // Enable URL language detection and selection.
+    $edit = array('language_interface[enabled][language-url]' => '1');
+    $this->drupalPost('admin/config/regional/language/detection', $edit, t('Save settings'));
+
+    $function_name = 'l()';
+
+    // Test links generated by l() on an English page.
+    $current_language = 'English';
+    $this->drupalGet('language_test/l-active-class');
+
+    // Language code 'none' link should be active.
+    $langcode = 'none';
+    $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'no_lang_link', ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Language code 'en' link should be active.
+    $langcode = 'en';
+    $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'en_link', ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Language code 'fr' link should not be active.
+    $langcode = 'fr';
+    $links = $this->xpath('//a[@id = :id and not(contains(@class, :class))]', array(':id' => 'fr_link', ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is NOT marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Test links generated by l() on a French page.
+    $current_language = 'French';
+    $this->drupalGet('fr/language_test/l-active-class');
+
+    // Language code 'none' link should be active.
+    $langcode = 'none';
+    $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'no_lang_link', ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Language code 'en' link should not be active.
+    $langcode = 'en';
+    $links = $this->xpath('//a[@id = :id and not(contains(@class, :class))]', array(':id' => 'en_link', ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is NOT marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Language code 'fr' link should be active.
+    $langcode = 'fr';
+    $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'fr_link', ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    $function_name = "theme('link')";
+
+    // Test links generated by theme('link') on an English page.
+    $current_language = 'English';
+    $this->drupalGet('language_test/theme-link-active-class');
+
+    // Language code 'none' link should be active.
+    $langcode = 'none';
+    $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'no_lang_link', ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Language code 'en' link should be active.
+    $langcode = 'en';
+    $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'en_link', ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Language code 'fr' link should not be active.
+    $langcode = 'fr';
+    $links = $this->xpath('//a[@id = :id and not(contains(@class, :class))]', array(':id' => 'fr_link', ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is NOT marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Test links generated by theme('link') on a French page.
+    $current_language = 'French';
+    $this->drupalGet('fr/language_test/theme-link-active-class');
+
+    // Language code 'none' link should be active.
+    $langcode = 'none';
+    $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'no_lang_link', ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Language code 'en' link should not be active.
+    $langcode = 'en';
+    $links = $this->xpath('//a[@id = :id and not(contains(@class, :class))]', array(':id' => 'en_link', ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is NOT marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Language code 'fr' link should be active.
+    $langcode = 'fr';
+    $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'fr_link', ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+  }
+
 }
diff --git a/core/modules/language/tests/language_test/language_test.routing.yml b/core/modules/language/tests/language_test/language_test.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d95722b11ac4765358fab40d484238f5992d798c
--- /dev/null
+++ b/core/modules/language/tests/language_test/language_test.routing.yml
@@ -0,0 +1,13 @@
+language_test_l_active_class:
+  pattern: '/language_test/l-active-class'
+  defaults:
+    _content: '\Drupal\language_test\Controller\LanguageTestController::lActiveClass'
+  requirements:
+    _access: 'TRUE'
+
+language_test_theme_link_active_class:
+  pattern: '/language_test/theme-link-active-class'
+  defaults:
+    _content: '\Drupal\language_test\Controller\LanguageTestController::themeLinkActiveClass'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/language/tests/language_test/lib/Drupal/language_test/Controller/LanguageTestController.php b/core/modules/language/tests/language_test/lib/Drupal/language_test/Controller/LanguageTestController.php
new file mode 100644
index 0000000000000000000000000000000000000000..da5ad6404ee15aa3283100eb8c120a7739993e55
--- /dev/null
+++ b/core/modules/language/tests/language_test/lib/Drupal/language_test/Controller/LanguageTestController.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\language_test\Controller\LanguageTestController.
+ */
+
+namespace Drupal\language_test\Controller;
+
+use Drupal\Core\ControllerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Controller routines for language_test routes.
+ */
+class LanguageTestController implements ControllerInterface {
+
+  /**
+   * Implements \Drupal\Core\ControllerInterface::create().
+   */
+  public static function create(ContainerInterface $container) {
+    return new static();
+  }
+
+  /**
+   * Returns links to the current page with different langcodes.
+   *
+   * Using #theme causes these links to be rendered with theme_link().
+   */
+  public function themeLinkActiveClass() {
+    // We assume that 'en' and 'fr' have been configured.
+    $languages = language_list();
+    return array(
+      'no_language' => array(
+        '#theme' => 'link',
+        '#text' => t('Link to the current path with no langcode provided.'),
+        '#path' => current_path(),
+        '#options' => array(
+          'attributes' => array(
+            'id' => 'no_lang_link',
+          ),
+        ),
+      ),
+      'fr' => array(
+        '#theme' => 'link',
+        '#text' => t('Link to a French version of the current path.'),
+        '#path' => current_path(),
+        '#options' => array(
+          'language' => $languages['fr'],
+          'attributes' => array(
+            'id' => 'fr_link',
+          ),
+        ),
+      ),
+      'en' => array(
+        '#theme' => 'link',
+        '#text' => t('Link to an English version of the current path.'),
+        '#path' => current_path(),
+        '#options' => array(
+          'language' => $languages['en'],
+          'attributes' => array(
+            'id' => 'en_link',
+          ),
+        ),
+      ),
+    );
+  }
+
+  /**
+   * Returns links to the current page with different langcodes.
+   *
+   * Using #type causes these links to be rendered with l().
+   */
+  public function lActiveClass() {
+    // We assume that 'en' and 'fr' have been configured.
+    $languages = language_list();
+    return array(
+      'no_language' => array(
+        '#type' => 'link',
+        '#title' => t('Link to the current path with no langcode provided.'),
+        '#href' => current_path(),
+        '#options' => array(
+          'attributes' => array(
+            'id' => 'no_lang_link',
+          ),
+        ),
+      ),
+      'fr' => array(
+        '#type' => 'link',
+        '#title' => t('Link to a French version of the current path.'),
+        '#href' => current_path(),
+        '#options' => array(
+          'language' => $languages['fr'],
+          'attributes' => array(
+            'id' => 'fr_link',
+          ),
+        ),
+      ),
+      'en' => array(
+        '#type' => 'link',
+        '#title' => t('Link to an English version of the current path.'),
+        '#href' => current_path(),
+        '#options' => array(
+          'language' => $languages['en'],
+          'attributes' => array(
+            'id' => 'en_link',
+          ),
+        ),
+      ),
+    );
+  }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php
index eeb49ccd2f2d78ed9d62b309981cfff1485f2a93..a241a31e9496889f3b81828a365283a6e59a39f6 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php
@@ -28,47 +28,135 @@ public static function getInfo() {
   }
 
   /**
-   * Confirms that invalid URLs are filtered.
+   * Confirms that invalid URLs are filtered in link generating functions.
    */
-  function testLXSS() {
+  function testLinkXSS() {
+    // Test l().
     $text = $this->randomName();
     $path = "<SCRIPT>alert('XSS')</SCRIPT>";
     $link = l($text, $path);
     $sanitized_path = check_url(url($path));
-    $this->assertTrue(strpos($link, $sanitized_path) !== FALSE, format_string('XSS attack @path was filtered', array('@path' => $path)));
+    $this->assertTrue(strpos($link, $sanitized_path) !== FALSE, format_string('XSS attack @path was filtered by l().', array('@path' => $path)));
+
+    // Test #theme.
+    $link_array =  array(
+      '#theme' => 'link',
+      '#text' => $this->randomName(),
+      '#path' => $path,
+    );
+    $theme_link = drupal_render($link_array);
+    $sanitized_path = check_url(url($path));
+    $this->assertTrue(strpos($theme_link, $sanitized_path) !== FALSE, format_string('XSS attack @path was filtered by #theme', array('@path' => $path)));
   }
 
   /**
-   * Tests for active class in l() function.
+   * Tests for active class in links produced by l() and theme_link() functions.
    */
-  function testLActiveClass() {
+  function testLinkActiveClass() {
+    $options_no_query = array();
+    $options_query = array(
+      'query' => array(
+        'foo' => 'bar',
+        'one' => 'two',
+      ),
+    );
+    $options_query_reverse = array(
+      'query' => array(
+        'one' => 'two',
+        'foo' => 'bar',
+      ),
+    );
+
+    // Test l().
     $path = 'common-test/l-active-class';
-    $options = array();
 
-    $this->drupalGet($path, $options);
-    $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options), ':class' => 'active'));
-    $this->assertTrue(isset($links[0]), 'A link to the current page is marked active.');
+    $this->drupalGet($path, $options_no_query);
+    $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options_no_query), ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), 'A link generated by l() to the current page is marked active.');
+
+    $links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => url($path, $options_query), ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), 'A link generated by l() to the current page with a query string when the current page has no query string is not marked active.');
+
+    $this->drupalGet($path, $options_query);
+    $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options_query), ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), 'A link generated by l() to the current page with a query string that matches the current query string is marked active.');
+
+    $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options_query_reverse), ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), 'A link generated by l() to the current page with a query string that has matching parameters to the current query string but in a different order is marked active.');
+
+    $links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => url($path, $options_no_query), ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), 'A link generated by l() to the current page without a query string when the current page has a query string is not marked active.');
 
-    $options = array('query' => array('foo' => 'bar'));
-    $links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => url($path, $options), ':class' => 'active'));
-    $this->assertTrue(isset($links[0]), 'A link to the current page with a query string when the current page has no query string is not marked active.');
+    // Test #theme.
+    $path = 'common-test/theme-link-active-class';
 
-    $this->drupalGet($path, $options);
-    $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options), ':class' => 'active'));
-    $this->assertTrue(isset($links[0]), 'A link to the current page with a query string that matches the current query string is marked active.');
+    $this->drupalGet($path, $options_no_query);
+    $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options_no_query), ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), 'A link generated by #theme to the current page is marked active.');
 
-    $options = array();
-    $links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => url($path, $options), ':class' => 'active'));
-    $this->assertTrue(isset($links[0]), 'A link to the current page without a query string when the current page has a query string is not marked active.');
+    $links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => url($path, $options_query), ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), 'A link generated by #theme to the current page with a query string when the current page has no query string is not marked active.');
+
+    $this->drupalGet($path, $options_query);
+    $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options_query), ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), 'A link generated by #theme to the current page with a query string that matches the current query string is marked active.');
+
+    $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options_query_reverse), ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), 'A link generated by #theme to the current page with a query string that has matching parameters to the current query string but in a different order is marked active.');
+
+    $links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => url($path, $options_no_query), ':class' => 'active'));
+    $this->assertTrue(isset($links[0]), 'A link generated by #theme to the current page without a query string when the current page has a query string is not marked active.');
+  }
+
+  /**
+   * Tests for custom class in links produced by l() and theme_link() functions.
+   */
+  function testLinkCustomClass() {
+    // Test l().
+    $class_l = $this->randomName();
+    $link_l = l($this->randomName(), current_path(), array('attributes' => array('class' => array($class_l))));
+    $this->assertTrue($this->hasClass($link_l, $class_l), format_string('Custom class @class is present on link when requested by l()', array('@class' => $class_l)));
+
+    // Test #theme.
+    $class_theme = $this->randomName();
+    $theme_link = array(
+      '#theme' => 'link',
+      '#text' => $this->randomName(),
+      '#path' => current_path(),
+      '#options' => array(
+        'attributes' => array(
+          'class' => array($class_theme),
+        ),
+      ),
+    );
+    $link_theme = drupal_render($theme_link);
+    $this->assertTrue($this->hasClass($link_theme, $class_theme), format_string('Custom class @class is present on link when requested by #theme', array('@class' => $class_theme)));
   }
 
   /**
-   * Tests for custom class in l() function.
+   * Tests that theme_link() supports render arrays in 'text' parameter.
    */
-  function testLCustomClass() {
-    $class = $this->randomName();
-    $link = l($this->randomName(), current_path(), array('attributes' => array('class' => array($class))));
-    $this->assertTrue($this->hasClass($link, $class), format_string('Custom class @class is present on link when requested', array('@class' => $class)));
+  function testLinkNestedRenderArrays() {
+    // Build a link with l() for reference.
+    $l = l('foo', 'http://drupal.org');
+
+    // Test a themed link with plain text 'text'.
+    $theme_link_plain_array = array(
+      '#theme' => 'link',
+      '#text' => 'foo',
+      '#path' => 'http://drupal.org',
+    );
+    $theme_link_plain = drupal_render($theme_link_plain_array);
+    $this->assertEqual($theme_link_plain, $l);
+
+    // Build a themed link with renderable 'text'.
+    $theme_link_nested_array = array(
+      '#theme' => 'link',
+      '#text' => array('#markup' => 'foo'),
+      '#path' => 'http://drupal.org',
+    );
+    $theme_link_nested = drupal_render($theme_link_nested_array);
+    $this->assertEqual($theme_link_nested, $l);
   }
 
   /**
diff --git a/core/modules/system/tests/modules/common_test/common_test.module b/core/modules/system/tests/modules/common_test/common_test.module
index 3670d41171997481264fb80bef2107a763cc85c1..ef6f1a194af4ec2e1b75a423b667cea8fd2fe1af 100644
--- a/core/modules/system/tests/modules/common_test/common_test.module
+++ b/core/modules/system/tests/modules/common_test/common_test.module
@@ -64,12 +64,6 @@ function common_test_menu() {
     'access arguments' => array('access content'),
     'type' => MENU_CALLBACK,
   );
-  $items['common-test/l-active-class'] = array(
-    'title' => 'Test l() adding of active class',
-    'page callback' => 'common_test_l_active_class',
-    'access arguments' => array('access content'),
-    'type' => MENU_CALLBACK,
-  );
   return $items;
 }
 
@@ -340,26 +334,3 @@ function common_test_js_and_css_querystring() {
 function common_test_cron() {
   throw new Exception(t('Uncaught exception'));
 }
-
-/**
- * Page callback: Displays links to the current page, one with a query string.
- */
-function common_test_l_active_class() {
-  return array(
-    'no_query' => array(
-      '#type' => 'link',
-      '#title' => t('Link with no query string'),
-      '#href' => current_path(),
-    ),
-    'with_query' => array(
-      '#type' => 'link',
-      '#title' => t('Link with a query string'),
-      '#href' => current_path(),
-      '#options' => array(
-        'query' => array(
-          'foo' => 'bar',
-        ),
-      ),
-    ),
-  );
-}
diff --git a/core/modules/system/tests/modules/common_test/common_test.routing.yml b/core/modules/system/tests/modules/common_test/common_test.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..840297f5862c70398b5f2606567e45b1cdaf948b
--- /dev/null
+++ b/core/modules/system/tests/modules/common_test/common_test.routing.yml
@@ -0,0 +1,14 @@
+common_test_l_active_class:
+  pattern: '/common-test/l-active-class'
+  defaults:
+    _content: '\Drupal\common_test\Controller\CommonTestController::lActiveClass'
+  requirements:
+    _access: 'TRUE'
+
+common_test_theme_link_active_class:
+  pattern: '/common-test/theme-link-active-class'
+  defaults:
+    _content: '\Drupal\common_test\Controller\CommonTestController::themeLinkActiveClass'
+  requirements:
+    _access: 'TRUE'
+
diff --git a/core/modules/system/tests/modules/common_test/lib/Drupal/common_test/Controller/CommonTestController.php b/core/modules/system/tests/modules/common_test/lib/Drupal/common_test/Controller/CommonTestController.php
new file mode 100644
index 0000000000000000000000000000000000000000..da963ab6d9c200dbb1d6c37ce930e7459c39193a
--- /dev/null
+++ b/core/modules/system/tests/modules/common_test/lib/Drupal/common_test/Controller/CommonTestController.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\common_test\Controller\CommonTestController.
+ */
+
+namespace Drupal\common_test\Controller;
+
+use Drupal\Core\ControllerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Controller routines for common_test routes.
+ */
+class CommonTestController implements ControllerInterface {
+
+  /**
+   * Implements \Drupal\Core\ControllerInterface::create().
+   */
+  public static function create(ContainerInterface $container) {
+    return new static();
+  }
+
+  /**
+   * Returns links to the current page, with and without query strings.
+   *
+   * Using #type causes these links to be rendered with l().
+   */
+  public function lActiveClass() {
+    return array(
+      'no_query' => array(
+        '#type' => 'link',
+        '#title' => t('Link with no query string'),
+        '#href' => current_path(),
+      ),
+      'with_query' => array(
+        '#type' => 'link',
+        '#title' => t('Link with a query string'),
+        '#href' => current_path(),
+        '#options' => array(
+          'query' => array(
+            'foo' => 'bar',
+            'one' => 'two',
+          ),
+        ),
+      ),
+      'with_query_reversed' => array(
+        '#type' => 'link',
+        '#title' => t('Link with the same query string in reverse order'),
+        '#href' => current_path(),
+        '#options' => array(
+          'query' => array(
+            'one' => 'two',
+            'foo' => 'bar',
+          ),
+        ),
+      ),
+    );
+  }
+
+  /**
+   * Returns links to the current page, with and without query strings.
+   *
+   * Using #theme causes these links to be rendered with theme_link().
+   */
+  public function themeLinkActiveClass() {
+    return array(
+      'no_query' => array(
+        '#theme' => 'link',
+        '#text' => t('Link with no query string'),
+        '#path' => current_path(),
+      ),
+      'with_query' => array(
+        '#theme' => 'link',
+        '#text' => t('Link with a query string'),
+        '#path' => current_path(),
+        '#options' => array(
+          'query' => array(
+            'foo' => 'bar',
+            'one' => 'two',
+          ),
+        ),
+      ),
+      'with_query_reversed' => array(
+        '#theme' => 'link',
+        '#text' => t('Link with the same query string in reverse order'),
+        '#path' => current_path(),
+        '#options' => array(
+          'query' => array(
+            'one' => 'two',
+            'foo' => 'bar',
+          ),
+        ),
+      ),
+    );
+  }
+
+}
