From 5d4d6b1c80e3c91253bc015a7cbad1518e5b559d Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Thu, 23 Jan 2014 18:04:41 +0000
Subject: [PATCH] Issue #1979468 by Wim Leers, nod_, pwolanin, longwave,
 thedavidmeister, jessebeach, larowlan | catch: .active from linkGenerator(),
 l() and theme_links() forces an upper limit of per-page caching for all
 content containing links.

---
 core/assets/vendor/classList/LICENSE.md       |   2 +
 core/assets/vendor/classList/README.md        |   7 +
 core/assets/vendor/classList/classList.js     | 179 +++++++++
 core/assets/vendor/classList/classList.min.js |   2 +
 core/core.services.yml                        |   2 +-
 core/includes/common.inc                      |  65 ++--
 core/includes/menu.inc                        |   4 +
 core/includes/theme.inc                       |  93 +++--
 .../lib/Drupal/Core/Utility/LinkGenerator.php |  66 ++--
 .../Core/Utility/LinkGeneratorInterface.php   |  10 +-
 core/misc/active-link.js                      |  63 ++++
 core/misc/ajax.js                             |  38 +-
 core/misc/drupal.js                           |   2 +-
 core/misc/machine-name.js                     |   2 +-
 core/misc/tabledrag.js                        |  10 +-
 core/modules/contextual/js/contextual.js      |   2 +-
 core/modules/edit/js/editors/formEditor.js    |   2 +-
 core/modules/edit/js/views/AppView.js         |   2 +-
 .../image/Tests/ImageFieldDisplayTest.php     |   2 +-
 .../language/Plugin/Block/LanguageBlock.php   |   1 +
 .../language/Tests/LanguageSwitchingTest.php  | 144 +++++++-
 .../LanguageUILanguageNegotiationTest.php     |   7 +-
 .../Controller/LanguageTestController.php     |   3 +
 .../system/Controller/SystemController.php    | 124 +++++++
 .../system/Tests/Batch/ProcessingTest.php     |  12 +-
 .../system/Tests/Common/JavaScriptTest.php    |   2 +-
 .../system/Tests/Theme/FunctionsTest.php      |  28 +-
 core/modules/system/system.module             |  51 +++
 .../batch_test/batch_test.callbacks.inc       |   2 +-
 .../Controller/CommonTestController.php       |   5 +
 core/modules/toolbar/js/escapeAdmin.js        |   2 +-
 core/modules/toolbar/js/toolbar.menu.js       |   2 +-
 core/modules/views/js/base.js                 |   2 +-
 .../Tests/Controller/SystemControllerTest.php | 347 ++++++++++++++++++
 .../Tests/Core/Utility/LinkGeneratorTest.php  | 111 ++++--
 35 files changed, 1233 insertions(+), 163 deletions(-)
 create mode 100644 core/assets/vendor/classList/LICENSE.md
 create mode 100644 core/assets/vendor/classList/README.md
 create mode 100644 core/assets/vendor/classList/classList.js
 create mode 100644 core/assets/vendor/classList/classList.min.js
 create mode 100644 core/misc/active-link.js
 create mode 100644 core/modules/views/tests/Drupal/views/Tests/Controller/SystemControllerTest.php

diff --git a/core/assets/vendor/classList/LICENSE.md b/core/assets/vendor/classList/LICENSE.md
new file mode 100644
index 000000000000..cb518af7825d
--- /dev/null
+++ b/core/assets/vendor/classList/LICENSE.md
@@ -0,0 +1,2 @@
+This software is dedicated to the public domain. No warranty is expressed or implied.
+Use this software at your own risk.
diff --git a/core/assets/vendor/classList/README.md b/core/assets/vendor/classList/README.md
new file mode 100644
index 000000000000..dd7359c0acb6
--- /dev/null
+++ b/core/assets/vendor/classList/README.md
@@ -0,0 +1,7 @@
+classList.js is a cross-browser JavaScript shim that fully implements `element.classList`. Refer to [the MDN page on `element.classList`][1] for more information.
+
+
+![Tracking image](https://in.getclicky.com/212712ns.gif)
+
+
+  [1]: https://developer.mozilla.org/en/DOM/element.classList "MDN / DOM / element.classList"
diff --git a/core/assets/vendor/classList/classList.js b/core/assets/vendor/classList/classList.js
new file mode 100644
index 000000000000..1faaec86e61d
--- /dev/null
+++ b/core/assets/vendor/classList/classList.js
@@ -0,0 +1,179 @@
+/*
+ * classList.js: Cross-browser full element.classList implementation.
+ * 2012-11-15
+ *
+ * By Eli Grey, http://eligrey.com
+ * Public Domain.
+ * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+ */
+
+/*global self, document, DOMException */
+
+/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/
+
+if ("document" in self && !(
+		"classList" in document.createElement("_") &&
+		"classList" in document.createElementNS("http://www.w3.org/2000/svg", "svg")
+	)) {
+
+(function (view) {
+
+"use strict";
+
+if (!('Element' in view)) return;
+
+var
+	  classListProp = "classList"
+	, protoProp = "prototype"
+	, elemCtrProto = view.Element[protoProp]
+	, objCtr = Object
+	, strTrim = String[protoProp].trim || function () {
+		return this.replace(/^\s+|\s+$/g, "");
+	}
+	, arrIndexOf = Array[protoProp].indexOf || function (item) {
+		var
+			  i = 0
+			, len = this.length
+		;
+		for (; i < len; i++) {
+			if (i in this && this[i] === item) {
+				return i;
+			}
+		}
+		return -1;
+	}
+	// Vendors: please allow content code to instantiate DOMExceptions
+	, DOMEx = function (type, message) {
+		this.name = type;
+		this.code = DOMException[type];
+		this.message = message;
+	}
+	, checkTokenAndGetIndex = function (classList, token) {
+		if (token === "") {
+			throw new DOMEx(
+				  "SYNTAX_ERR"
+				, "An invalid or illegal string was specified"
+			);
+		}
+		if (/\s/.test(token)) {
+			throw new DOMEx(
+				  "INVALID_CHARACTER_ERR"
+				, "String contains an invalid character"
+			);
+		}
+		return arrIndexOf.call(classList, token);
+	}
+	, ClassList = function (elem) {
+		var
+			  trimmedClasses = strTrim.call(elem.getAttribute("class") || "")
+			, classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []
+			, i = 0
+			, len = classes.length
+		;
+		for (; i < len; i++) {
+			this.push(classes[i]);
+		}
+		this._updateClassName = function () {
+			elem.setAttribute("class", this.toString());
+		};
+	}
+	, classListProto = ClassList[protoProp] = []
+	, classListGetter = function () {
+		return new ClassList(this);
+	}
+;
+// Most DOMException implementations don't allow calling DOMException's toString()
+// on non-DOMExceptions. Error's toString() is sufficient here.
+DOMEx[protoProp] = Error[protoProp];
+classListProto.item = function (i) {
+	return this[i] || null;
+};
+classListProto.contains = function (token) {
+	token += "";
+	return checkTokenAndGetIndex(this, token) !== -1;
+};
+classListProto.add = function () {
+	var
+		  tokens = arguments
+		, i = 0
+		, l = tokens.length
+		, token
+		, updated = false
+	;
+	do {
+		token = tokens[i] + "";
+		if (checkTokenAndGetIndex(this, token) === -1) {
+			this.push(token);
+			updated = true;
+		}
+	}
+	while (++i < l);
+
+	if (updated) {
+		this._updateClassName();
+	}
+};
+classListProto.remove = function () {
+	var
+		  tokens = arguments
+		, i = 0
+		, l = tokens.length
+		, token
+		, updated = false
+	;
+	do {
+		token = tokens[i] + "";
+		var index = checkTokenAndGetIndex(this, token);
+		if (index !== -1) {
+			this.splice(index, 1);
+			updated = true;
+		}
+	}
+	while (++i < l);
+
+	if (updated) {
+		this._updateClassName();
+	}
+};
+classListProto.toggle = function (token, forse) {
+	token += "";
+
+	var
+		  result = this.contains(token)
+		, method = result ?
+			forse !== true && "remove"
+		:
+			forse !== false && "add"
+	;
+
+	if (method) {
+		this[method](token);
+	}
+
+	return !result;
+};
+classListProto.toString = function () {
+	return this.join(" ");
+};
+
+if (objCtr.defineProperty) {
+	var classListPropDesc = {
+		  get: classListGetter
+		, enumerable: true
+		, configurable: true
+	};
+	try {
+		objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
+	} catch (ex) { // IE 8 doesn't support enumerable:true
+		if (ex.number === -0x7FF5EC54) {
+			classListPropDesc.enumerable = false;
+			objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
+		}
+	}
+} else if (objCtr[protoProp].__defineGetter__) {
+	elemCtrProto.__defineGetter__(classListProp, classListGetter);
+}
+
+}(self));
+
+}
diff --git a/core/assets/vendor/classList/classList.min.js b/core/assets/vendor/classList/classList.min.js
new file mode 100644
index 000000000000..fa988254960d
--- /dev/null
+++ b/core/assets/vendor/classList/classList.min.js
@@ -0,0 +1,2 @@
+/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/
+if("document" in self&&!("classList" in document.createElement("_")&&"classList" in document.createElementNS("http://www.w3.org/2000/svg","svg"))){(function(j){"use strict";if(!("Element" in j)){return}var a="classList",f="prototype",m=j.Element[f],b=Object,k=String[f].trim||function(){return this.replace(/^\s+|\s+$/g,"")},c=Array[f].indexOf||function(q){var p=0,o=this.length;for(;p<o;p++){if(p in this&&this[p]===q){return p}}return -1},n=function(o,p){this.name=o;this.code=DOMException[o];this.message=p},g=function(p,o){if(o===""){throw new n("SYNTAX_ERR","An invalid or illegal string was specified")}if(/\s/.test(o)){throw new n("INVALID_CHARACTER_ERR","String contains an invalid character")}return c.call(p,o)},d=function(s){var r=k.call(s.getAttribute("class")),q=r?r.split(/\s+/):[],p=0,o=q.length;for(;p<o;p++){this.push(q[p])}this._updateClassName=function(){s.setAttribute("class",this.toString())}},e=d[f]=[],i=function(){return new d(this)};n[f]=Error[f];e.item=function(o){return this[o]||null};e.contains=function(o){o+="";return g(this,o)!==-1};e.add=function(){var s=arguments,r=0,p=s.length,q,o=false;do{q=s[r]+"";if(g(this,q)===-1){this.push(q);o=true}}while(++r<p);if(o){this._updateClassName()}};e.remove=function(){var t=arguments,s=0,p=t.length,r,o=false;do{r=t[s]+"";var q=g(this,r);if(q!==-1){this.splice(q,1);o=true}}while(++s<p);if(o){this._updateClassName()}};e.toggle=function(p,q){p+="";var o=this.contains(p),r=o?q!==true&&"remove":q!==false&&"add";if(r){this[r](p)}return !o};e.toString=function(){return this.join(" ")};if(b.defineProperty){var l={get:i,enumerable:true,configurable:true};try{b.defineProperty(m,a,l)}catch(h){if(h.number===-2146823252){l.enumerable=false;b.defineProperty(m,a,l)}}}else{if(b[f].__defineGetter__){m.__defineGetter__(a,i)}}}(self))};
diff --git a/core/core.services.yml b/core/core.services.yml
index 50d2640ca5b6..8af072f2d880 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -276,7 +276,7 @@ services:
       - [setContext, ['@?router.request_context']]
   link_generator:
     class: Drupal\Core\Utility\LinkGenerator
-    arguments: ['@url_generator', '@module_handler', '@language_manager']
+    arguments: ['@url_generator', '@module_handler', '@language_manager', '@path.alias_manager.cached']
     calls:
       - [setRequest, ['@?request']]
   router.dynamic:
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 3841d902f5be..a3b1910325dd 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -1214,12 +1214,24 @@ function drupal_http_header_attributes(array $attributes = array()) {
  *     internal to the site, $options['language'] is used to determine whether
  *     the link is "active", or pointing to the current page (the language as
  *     well as the path must match). This element is also used by url().
+ *   - 'set_active_class': Whether l() should compare the $path, language and
+ *     query options to the current URL to determine whether the link is
+ *     "active". Defaults to FALSE. If TRUE, an "active" class will be applied
+ *     to the link. It is important to use this sparingly since it is usually
+ *     unnecessary and requires extra processing.
+ *     For anonymous users, the "active" class will be calculated on the server,
+ *     because most sites serve each anonymous user the same cached page anyway.
+ *     For authenticated users, the "active" class will be calculated on the
+ *     client (through JavaScript), only data- attributes are added to links to
+ *     prevent breaking the render cache. The JavaScript is added in
+ *     system_page_build().
  *   - Additional $options elements used by the url() function.
  *
  * @return string
  *   An HTML string containing a link to the given path.
  *
  * @see url()
+ * @see system_page_build()
  */
 function l($text, $path, array $options = array()) {
   // Start building a structured representation of our link to be altered later.
@@ -1235,6 +1247,7 @@ function l($text, $path, array $options = array()) {
     'query' => array(),
     'html' => FALSE,
     'language' => NULL,
+    'set_active_class' => FALSE,
   );
 
   // Add a hreflang attribute if we know the language of this link's url and
@@ -1243,35 +1256,21 @@ function l($text, $path, array $options = array()) {
     $variables['options']['attributes']['hreflang'] = $variables['options']['language']->id;
   }
 
-  // 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)->id,
-      'query' => \Drupal::service('request')->query->all(),
-    );
-  }
-
-  // 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']->id == $active['language'])
-  // The query parameters of an active link are equal to the current parameters.
-  && ($variables['options']['query'] == $active['query']);
+  // Set the "active" class if the 'set_active_class' option is not empty.
+  if (!empty($variables['options']['set_active_class'])) {
+    // Add a "data-drupal-link-query" attribute to let the drupal.active-link
+    // library know the query in a standardized manner.
+    if (!empty($variables['options']['query'])) {
+      $query = $variables['options']['query'];
+      ksort($query);
+      $variables['options']['attributes']['data-drupal-link-query'] = Json::encode($query);
+    }
 
-  // Add the "active" class if appropriate.
-  if ($variables['url_is_active']) {
-    $variables['options']['attributes']['class'][] = 'active';
+    // Add a "data-drupal-link-system-path" attribute to let the
+    // drupal.active-link library know the path in a standardized manner.
+    if (!isset($variables['options']['attributes']['data-drupal-link-system-path'])) {
+      $variables['options']['attributes']['data-drupal-link-system-path'] = \Drupal::service('path.alias_manager.cached')->getSystemPath($path);
+    }
   }
 
   // Remove all HTML and PHP tags from a tooltip, calling expensive strip_tags()
@@ -2149,6 +2148,7 @@ function _drupal_add_js($data = NULL, $options = NULL) {
           // @todo Make this less hacky: http://drupal.org/node/1547376.
           $scriptPath = $GLOBALS['script_path'];
           $pathPrefix = '';
+          $current_query = \Drupal::service('request')->query->all();
           url('', array('script' => &$scriptPath, 'prefix' => &$pathPrefix));
           $current_path = current_path();
           $current_path_is_admin = FALSE;
@@ -2156,13 +2156,20 @@ function _drupal_add_js($data = NULL, $options = NULL) {
           if (!(defined('MAINTENANCE_MODE') && MAINTENANCE_MODE === 'update')) {
             $current_path_is_admin = path_is_admin($current_path);
           }
-          $javascript['settings']['data'][] = array(
+          $path = array(
             'basePath' => base_path(),
             'scriptPath' => $scriptPath,
             'pathPrefix' => $pathPrefix,
             'currentPath' => $current_path,
             'currentPathIsAdmin' => $current_path_is_admin,
+            'isFront' => drupal_is_front_page(),
+            'currentLanguage' => \Drupal::languageManager()->getCurrentLanguage(Language::TYPE_URL)->id,
           );
+          if (!empty($current_query)) {
+            ksort($current_query);
+            $path['currentQuery'] = (object) $current_query;
+          }
+          $javascript['settings']['data'][] = array('path' => $path);
         }
         // All JavaScript settings are placed in the header of the page with
         // the library weight so that inline scripts appear afterwards.
diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index d0432dd44d7b..ca3ed515d6a0 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -1700,6 +1700,7 @@ function theme_menu_link(array $variables) {
   if ($element['#below']) {
     $sub_menu = drupal_render($element['#below']);
   }
+  $element['#localized_options']['set_active_class'] = TRUE;
   $output = l($element['#title'], $element['#href'], $element['#localized_options']);
   return '<li' . new Attribute($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";
 }
@@ -1735,6 +1736,8 @@ function theme_menu_local_task($variables) {
     $link['localized_options']['html'] = TRUE;
     $link_text = t('!local-task-title!active', array('!local-task-title' => $link['title'], '!active' => $active));
   }
+  $link['localized_options']['set_active_class'] = TRUE;
+
   if (!empty($link['href'])) {
     // @todo - remove this once all pages are converted to routes.
     $a_tag = l($link_text, $link['href'], $link['localized_options']);
@@ -1766,6 +1769,7 @@ function theme_menu_local_action($variables) {
   );
   $link['localized_options']['attributes']['class'][] = 'button';
   $link['localized_options']['attributes']['class'][] = 'button-action';
+  $link['localized_options']['set_active_class'] = TRUE;
 
   $output = '<li>';
   // @todo Remove this check and the call to l() when all pages are converted to
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index ef54c93c5f4b..d14f7f8874c6 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -1194,6 +1194,19 @@ function template_preprocess_status_messages(&$variables) {
  *     l() as its $options parameter.
  *   - attributes: A keyed array of attributes for the UL containing the
  *     list of links.
+ *   - set_active_class: (optional) Whether theme_links() should compare the
+ *     route_name + route_parameters or href (path), language and query options
+ *     to the current URL for each of the links, to determine whether the link
+ *     is "active". If so, an "active" class will be applied to the list item
+ *     containing the link, as well as the link itself. It is important to use
+ *     this sparingly since it is usually unnecessary and requires extra
+ *     processing.
+ *     For anonymous users, the "active" class will be calculated on the server,
+ *     because most sites serve each anonymous user the same cached page anyway.
+ *     For authenticated users, the "active" class will be calculated on the
+ *     client (through JavaScript), only data- attributes are added to list
+ *     items and contained links, to prevent breaking the render cache. The
+ *     JavaScript is added in system_page_build().
  *   - heading: (optional) A heading to precede the links. May be an
  *     associative array or a string. If it's an array, it can have the
  *     following elements:
@@ -1209,6 +1222,19 @@ function template_preprocess_status_messages(&$variables) {
  *     navigate to or skip the links. See
  *     http://juicystudio.com/article/screen-readers-display-none.php and
  *     http://www.w3.org/TR/WCAG-TECHS/H42.html for more information.
+ *
+ * theme_links() unfortunately duplicates the "active" class handling of l() and
+ * LinkGenerator::generate() because it needs to be able to set the "active"
+ * class not on the links themselves ("a" tags), but on the list items ("li"
+ * tags) that contain the links. This is necessary for CSS to be able to style
+ * list items differently when the link is active, since CSS does not yet allow
+ * one to style list items only if it contains a certain element with a certain
+ * class. I.e. we cannot yet convert this jQuery selector to a CSS selector:
+ *   jQuery('li:has("a.active")')
+ *
+ * @see l()
+ * @see \Drupal\Core\Utility\LinkGenerator::generate()
+ * @see system_page_build()
  */
 function theme_links($variables) {
   $links = $variables['links'];
@@ -1240,9 +1266,6 @@ function theme_links($variables) {
 
     $output .= '<ul' . new Attribute($attributes) . '>';
 
-    $active = \Drupal::linkGenerator()->getActive();
-    $language_url = \Drupal::languageManager()->getCurrentLanguage(Language::TYPE_URL);
-
     foreach ($links as $key => $link) {
       $link += array(
         'href' => NULL,
@@ -1251,9 +1274,9 @@ function theme_links($variables) {
         'ajax' => NULL,
       );
 
-      $class = array();
+      $li_attributes = array('class' => array());
       // Use the array key as class name.
-      $class[] = drupal_html_class($key);
+      $li_attributes['class'][] = drupal_html_class($key);
 
       $link_element = array(
         '#type' => 'link',
@@ -1265,30 +1288,38 @@ function theme_links($variables) {
         '#ajax' => $link['ajax'],
       );
 
-      // Handle links and ensure that the active class is added on the LIs.
-      if (isset($link['route_name'])) {
-        $variables = array(
-          'options' => array(),
-        );
-        if (!empty($link['language'])) {
-          $variables['options']['language'] = $link['language'];
-        }
+      // Handle links and ensure that the active class is added on the LIs, but
+      // only if the 'set_active_class' option is not empty.
+      if (isset($link['href']) || isset($link['route_name'])) {
+        if (!empty($variables['set_active_class'])) {
 
-        if (($link['route_name'] == $active['route_name'])
-        // The language of an active link is equal to the current language.
-        && (empty($variables['options']['language']) || ($variables['options']['language']->id == $active['language']))
-        && ($link['route_parameters'] == $active['parameters'])) {
-          $class[] = 'active';
-        }
+          // Also enable set_active_class for the contained link.
+          $link_element['#options']['set_active_class'] = TRUE;
 
-        $item = drupal_render($link_element);
-      }
-      elseif (isset($link['href'])) {
-        $is_current_path = ($link['href'] == current_path() || ($link['href'] == '<front>' && drupal_is_front_page()));
-        $is_current_language = (empty($link['language']) || $link['language']->id == $language_url->id);
-        if ($is_current_path && $is_current_language) {
-          $class[] = 'active';
+          if (!empty($link['language'])) {
+            $li_attributes['hreflang'] = $link['language']->id;
+          }
+
+          // Add a "data-drupal-link-query" attribute to let the
+          // drupal.active-link library know the query in a standardized manner.
+          if (!empty($link['query'])) {
+            $query = $link['query'];
+            ksort($query);
+            $li_attributes['data-drupal-link-query'] = Json::encode($query);
+          }
+
+          if (isset($link['route_name'])) {
+            $path = \Drupal::service('url_generator')->getPathFromRoute($link['route_name'], $link['route_parameters']);
+          }
+          else {
+            $path = $link['href'];
+          }
+
+          // Add a "data-drupal-link-system-path" attribute to let the
+          // drupal.active-link library know the path in a standardized manner.
+          $li_attributes['data-drupal-link-system-path'] = \Drupal::service('path.alias_manager.cached')->getSystemPath($path);
         }
+
         $item = drupal_render($link_element);
       }
       // Handle title-only text items.
@@ -1303,7 +1334,7 @@ function theme_links($variables) {
         }
       }
 
-      $output .= '<li' . new Attribute(array('class' => $class)) . '>';
+      $output .= '<li' . new Attribute($li_attributes) . '>';
       $output .= $item;
       $output .= '</li>';
     }
@@ -2227,7 +2258,8 @@ function template_preprocess_page(&$variables) {
       '#heading' => array(
         'text' => t('Main menu'),
         'class' => array('visually-hidden'),
-      )
+      ),
+      '#set_active_class' => TRUE,
     );
   }
   if (!empty($variables['secondary_menu'])) {
@@ -2237,7 +2269,8 @@ function template_preprocess_page(&$variables) {
       '#heading' => array(
         'text' => t('Secondary menu'),
         'class' => array('visually-hidden'),
-      )
+      ),
+      '#set_active_class' => TRUE,
     );
   }
 
@@ -2567,7 +2600,7 @@ function drupal_common_theme() {
       'template' => 'status-messages',
     ),
     'links' => array(
-      'variables' => array('links' => array(), 'attributes' => array('class' => array('links')), 'heading' => array()),
+      'variables' => array('links' => array(), 'attributes' => array('class' => array('links')), 'heading' => array(), 'set_active_class' => FALSE),
     ),
     'dropbutton_wrapper' => array(
       'variables' => array('children' => NULL),
diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php
index 1628e635e9e4..2874da471198 100644
--- a/core/lib/Drupal/Core/Utility/LinkGenerator.php
+++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php
@@ -7,12 +7,15 @@
 
 namespace Drupal\Core\Utility;
 
+use Drupal\Component\Utility\Json;
 use Drupal\Component\Utility\String;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Path\AliasManagerInterface;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Routing\UrlGeneratorInterface;
+use Drupal\Core\Session\AccountInterface;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
 
@@ -21,13 +24,6 @@
  */
 class LinkGenerator implements LinkGeneratorInterface {
 
-  /**
-   * Stores some information about the current request, like the language.
-   *
-   * @var array
-   */
-  protected $active;
-
   /**
    * The url generator.
    *
@@ -49,6 +45,13 @@ class LinkGenerator implements LinkGeneratorInterface {
    */
   protected $languageManager;
 
+  /**
+   * The path alias manager.
+   *
+   * @var \Drupal\Core\Path\AliasManagerInterface
+   */
+  protected $aliasManager;
+
   /**
    * Constructs a LinkGenerator instance.
    *
@@ -58,11 +61,14 @@ class LinkGenerator implements LinkGeneratorInterface {
    *   The module handler.
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
    *   The language manager.
+   * @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
+   *   The path alias manager.
    */
-  public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager) {
+  public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, AliasManagerInterface $alias_manager) {
     $this->urlGenerator = $url_generator;
     $this->moduleHandler = $module_handler;
     $this->languageManager = $language_manager;
+    $this->aliasManager = $alias_manager;
   }
 
   /**
@@ -93,6 +99,15 @@ public function getActive() {
 
   /**
    * {@inheritdoc}
+   *
+   * For anonymous users, the "active" class will be calculated on the server,
+   * because most sites serve each anonymous user the same cached page anyway.
+   * For authenticated users, the "active" class will be calculated on the
+   * client (through JavaScript), only data- attributes are added to links to
+   * prevent breaking the render cache. The JavaScript is added in
+   * system_page_build().
+   *
+   * @see system_page_build()
    */
   public function generate($text, $route_name, array $parameters = array(), array $options = array()) {
     // Start building a structured representation of our link to be altered later.
@@ -110,30 +125,31 @@ public function generate($text, $route_name, array $parameters = array(), array
       'query' => array(),
       'html' => FALSE,
       'language' => NULL,
+      'set_active_class' => FALSE,
     );
+
     // Add a hreflang attribute if we know the language of this link's url and
     // hreflang has not already been set.
     if (!empty($variables['options']['language']) && !isset($variables['options']['attributes']['hreflang'])) {
       $variables['options']['attributes']['hreflang'] = $variables['options']['language']->id;
     }
 
-    // This is only needed for the active class. The generator also combines
-    // the parameters and $options['query'] and adds parameters that are not
-    // path slugs as query strings.
-    $full_parameters = $parameters + (array) $variables['options']['query'];
-
-    // Determine whether this link is "active", meaning that it has the same
-    // URL path and query string as the current page. Note that this may be
-    // removed from l() in https://drupal.org/node/1979468 and would be removed
-    // or altered here also.
-    $variables['url_is_active'] = $route_name == $this->active['route_name']
-      // The language of an active link is equal to the current language.
-      && (empty($variables['options']['language']) || $variables['options']['language']->id == $this->active['language'])
-      && $full_parameters == $this->active['parameters'];
-
-    // Add the "active" class if appropriate.
-    if ($variables['url_is_active']) {
-      $variables['options']['attributes']['class'][] = 'active';
+    // Set the "active" class if the 'set_active_class' option is not empty.
+    if (!empty($variables['options']['set_active_class'])) {
+      // Add a "data-drupal-link-query" attribute to let the
+      // drupal.active-link library know the query in a standardized manner.
+      if (!empty($variables['options']['query'])) {
+        $query = $variables['options']['query'];
+        ksort($query);
+        $variables['options']['attributes']['data-drupal-link-query'] = Json::encode($query);
+      }
+
+      // Add a "data-drupal-link-system-path" attribute to let the
+      // drupal.active-link library know the path in a standardized manner.
+      if (!isset($variables['options']['attributes']['data-drupal-link-system-path'])) {
+        $path = $this->urlGenerator->getPathFromRoute($route_name, $parameters);
+        $variables['options']['attributes']['data-drupal-link-system-path'] = $this->aliasManager->getSystemPath($path);
+      }
     }
 
     // Remove all HTML and PHP tags from a tooltip, calling expensive strip_tags()
diff --git a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
index 8bc7eb6cc6f2..02e31af5c781 100644
--- a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
+++ b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
@@ -36,8 +36,8 @@ interface LinkGeneratorInterface {
    * @param array $options
    *   (optional) An associative array of additional options. Defaults to an
    *   empty array. It may contain the following elements:
-   *   - 'query': An array of query key/value-pairs (without any URL-encoding) to
-   *     append to the URL.
+   *   - 'query': An array of query key/value-pairs (without any URL-encoding)
+   *     to append to the URL.
    *   - absolute: Whether to force the output to be an absolute link (beginning
    *     with http:). Useful for links that will be displayed outside the site,
    *     such as in an RSS feed. Defaults to FALSE.
@@ -55,6 +55,12 @@ interface LinkGeneratorInterface {
    *     internal to the site, $options['language'] is used to determine whether
    *     the link is "active", or pointing to the current page (the language as
    *     well as the path must match).
+   *   - 'set_active_class': Whether this method should compare the $route_name,
+   *     $parameters, language and query options to the current URL to determine
+   *     whether the link is "active". Defaults to FALSE. If TRUE, an "active"
+   *     class will be applied to the link. It is important to use this
+   *     sparingly since it is usually unnecessary and requires extra
+   *     processing.
    *
    * @return string
    *   An HTML string containing a link to the given route and parameters.
diff --git a/core/misc/active-link.js b/core/misc/active-link.js
new file mode 100644
index 000000000000..6054faa7f5f1
--- /dev/null
+++ b/core/misc/active-link.js
@@ -0,0 +1,63 @@
+/**
+ * @file
+ * Attaches behaviors for Drupal's active link marking.
+ */
+
+(function (Drupal, drupalSettings) {
+
+"use strict";
+
+/**
+ * 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, exposed View filters).
+ *
+ * Does not discriminate based on element type, so allows you to set the active
+ * class on any element: a, li…
+ */
+Drupal.behaviors.activeLinks = {
+  attach: function (context) {
+    // Start by finding all potentially active links.
+    var path = drupalSettings.path;
+    var queryString = JSON.stringify(path.currentQuery);
+    var querySelector = path.currentQuery ? "[data-drupal-link-query='" + queryString + "']" : ':not([data-drupal-link-query])';
+    var originalSelectors = ['[data-drupal-link-system-path="' + path.currentPath + '"]'];
+    var selectors;
+
+    // If this is the front page, we have to check for the <front> path as well.
+    if (path.isFront) {
+      originalSelectors.push('[data-drupal-link-system-path="<front>"]');
+    }
+
+    // Add language filtering.
+    selectors = [].concat(
+      // Links without any hreflang attributes (most of them).
+      originalSelectors.map(function (selector) { return selector + ':not([hreflang])';}),
+      // Links with hreflang equals to the current language.
+      originalSelectors.map(function (selector) { return selector + '[hreflang="' + path.currentLanguage + '"]';})
+    );
+
+    // Add query string selector for pagers, exposed filters.
+    selectors = selectors.map(function (current) { return current + querySelector; });
+
+    // Query the DOM.
+    var activeLinks = context.querySelectorAll(selectors.join(','));
+    for (var i = 0, il = activeLinks.length; i < il; i += 1) {
+      activeLinks[i].classList.add('active');
+    }
+  },
+  detach: function (context, settings, trigger) {
+    if (trigger === 'unload') {
+      var activeLinks = context.querySelectorAll('[data-drupal-link-system-path].active');
+      for (var i = 0, il = activeLinks.length; i < il; i += 1) {
+        activeLinks[i].classList.remove('active');
+      }
+    }
+  }
+};
+
+})(Drupal, drupalSettings);
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index 6ecde951719b..3754af6c771e 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -180,7 +180,7 @@ Drupal.ajax = function (base, element, element_settings) {
   // If there isn't a form, jQuery.ajax() will be used instead, allowing us to
   // bind Ajax to links as well.
   if (this.element.form) {
-    this.form = $(this.element.form);
+    this.$form = $(this.element.form);
   }
 
   // If no Ajax callback URL was given, use the link href or form action.
@@ -189,7 +189,7 @@ Drupal.ajax = function (base, element, element_settings) {
       this.url = $(element).attr('href');
     }
     else if (element.form) {
-      this.url = this.form.attr('action');
+      this.url = this.$form.attr('action');
 
       // @todo If there's a file input on this form, then jQuery will submit the
       //   AJAX response with a hidden Iframe rather than the XHR object. If the
@@ -200,7 +200,7 @@ Drupal.ajax = function (base, element, element_settings) {
       //   elements that submit to the same URL as the form when there's a file
       //   input. For example, this means the Delete button on the edit form of
       //   an Article node doesn't open its confirmation form in a dialog.
-      if (this.form.find(':file').length) {
+      if (this.$form.find(':file').length) {
         return;
       }
     }
@@ -327,7 +327,7 @@ Drupal.ajax.prototype.eventResponse = function (element, event) {
   }
 
   try {
-    if (ajax.form) {
+    if (ajax.$form) {
       // If setClick is set, we must set this to ensure that the button's
       // value is passed.
       if (ajax.setClick) {
@@ -338,7 +338,7 @@ Drupal.ajax.prototype.eventResponse = function (element, event) {
         element.form.clk = element;
       }
 
-      ajax.form.ajaxSubmit(ajax.options);
+      ajax.$form.ajaxSubmit(ajax.options);
     }
     else {
       ajax.beforeSerialize(ajax.element, ajax.options);
@@ -362,12 +362,12 @@ Drupal.ajax.prototype.eventResponse = function (element, event) {
 Drupal.ajax.prototype.beforeSerialize = function (element, options) {
   // Allow detaching behaviors to update field values before collecting them.
   // This is only needed when field values are added to the POST data, so only
-  // when there is a form such that this.form.ajaxSubmit() is used instead of
+  // when there is a form such that this.$form.ajaxSubmit() is used instead of
   // $.ajax(). When there is no form and $.ajax() is used, beforeSerialize()
-  // isn't called, but don't rely on that: explicitly check this.form.
-  if (this.form) {
+  // isn't called, but don't rely on that: explicitly check this.$form.
+  if (this.$form) {
     var settings = this.settings || drupalSettings;
-    Drupal.detachBehaviors(this.form, settings, 'serialize');
+    Drupal.detachBehaviors(this.$form.get(0), settings, 'serialize');
   }
 
   // Prevent duplicate HTML ids in the returned markup.
@@ -421,7 +421,7 @@ Drupal.ajax.prototype.beforeSend = function (xmlhttprequest, options) {
   // to the form to submit the values in options.extraData. There is no simple
   // way to know which submission mechanism will be used, so we add to extraData
   // regardless, and allow it to be ignored in the former case.
-  if (this.form) {
+  if (this.$form) {
     options.extraData = options.extraData || {};
 
     // Let the server know when the IFRAME submission mechanism is used. The
@@ -491,9 +491,9 @@ Drupal.ajax.prototype.success = function (response, status) {
   // attachBehaviors() called on the new content from processing the response
   // commands is not sufficient, because behaviors from the entire form need
   // to be reattached.
-  if (this.form) {
+  if (this.$form) {
     var settings = this.settings || drupalSettings;
-    Drupal.attachBehaviors(this.form, settings);
+    Drupal.attachBehaviors(this.$form.get(0), settings);
   }
 
   // Remove any response-specific settings so they don't get used on the next
@@ -544,9 +544,9 @@ Drupal.ajax.prototype.error = function (response, uri) {
   // Re-enable the element.
   $(this.element).removeClass('progress-disabled').prop('disabled', false);
   // Reattach behaviors, if they were detached in beforeSerialize().
-  if (this.form) {
+  if (this.$form) {
     var settings = response.settings || this.settings || drupalSettings;
-    Drupal.attachBehaviors(this.form, settings);
+    Drupal.attachBehaviors(this.$form.get(0), settings);
   }
   throw new Drupal.AjaxError(response, uri);
 };
@@ -597,7 +597,7 @@ Drupal.AjaxCommands.prototype = {
       case 'empty':
       case 'remove':
         settings = response.settings || ajax.settings || drupalSettings;
-        Drupal.detachBehaviors(wrapper, settings);
+        Drupal.detachBehaviors(wrapper.get(0), settings);
     }
 
     // Add the new content to the page.
@@ -625,7 +625,7 @@ Drupal.AjaxCommands.prototype = {
     if (new_content.parents('html').length > 0) {
       // Apply any settings from the returned JSON if available.
       settings = response.settings || ajax.settings || drupalSettings;
-      Drupal.attachBehaviors(new_content, settings);
+      Drupal.attachBehaviors(new_content.get(0), settings);
     }
   },
 
@@ -634,8 +634,10 @@ Drupal.AjaxCommands.prototype = {
    */
   remove: function (ajax, response, status) {
     var settings = response.settings || ajax.settings || drupalSettings;
-    Drupal.detachBehaviors($(response.selector), settings);
-    $(response.selector).remove();
+    $(response.selector).each(function() {
+      Drupal.detachBehaviors(this, settings);
+    })
+    .remove();
   },
 
   /**
diff --git a/core/misc/drupal.js b/core/misc/drupal.js
index 6c04130376db..e994ec669b56 100644
--- a/core/misc/drupal.js
+++ b/core/misc/drupal.js
@@ -267,7 +267,7 @@ Drupal.t = function (str, args, options) {
  * Returns the URL to a Drupal page.
  */
 Drupal.url = function (path) {
-  return drupalSettings.basePath + drupalSettings.scriptPath + path;
+  return drupalSettings.path.basePath + drupalSettings.path.scriptPath + path;
 };
 
 /**
diff --git a/core/misc/machine-name.js b/core/misc/machine-name.js
index 53ed6687fca0..f399e20335c2 100644
--- a/core/misc/machine-name.js
+++ b/core/misc/machine-name.js
@@ -158,7 +158,7 @@ Drupal.behaviors.machineName = {
    *   The transliterated source string.
    */
   transliterate: function (source, settings) {
-    return $.get(drupalSettings.basePath + 'machine_name/transliterate', {
+    return $.get(drupalSettings.path.basePath + 'machine_name/transliterate', {
       text: source,
       langcode: drupalSettings.langcode,
       replace_pattern: settings.replace_pattern,
diff --git a/core/misc/tabledrag.js b/core/misc/tabledrag.js
index ae2dec02a8e5..e78d6935f4bd 100644
--- a/core/misc/tabledrag.js
+++ b/core/misc/tabledrag.js
@@ -1069,9 +1069,15 @@ Drupal.tableDrag.prototype.row.prototype.isValidSwap = function (row) {
  *   DOM element what will be swapped with the row group.
  */
 Drupal.tableDrag.prototype.row.prototype.swap = function (position, row) {
-  Drupal.detachBehaviors(this.group, drupalSettings, 'move');
+  // Makes sure only DOM object are passed to Drupal.detachBehaviors().
+  this.group.forEach(function (row) {
+    Drupal.detachBehaviors(row, drupalSettings, 'move');
+  });
   $(row)[position](this.group);
-  Drupal.attachBehaviors(this.group, drupalSettings);
+  // Makes sure only DOM object are passed to Drupal.attachBehaviors()s.
+  this.group.forEach(function (row) {
+    Drupal.attachBehaviors(row, drupalSettings);
+  });
   this.changed = true;
   this.onSwap(row);
 };
diff --git a/core/modules/contextual/js/contextual.js b/core/modules/contextual/js/contextual.js
index c6400692fb4f..92ecbd7f23ef 100644
--- a/core/modules/contextual/js/contextual.js
+++ b/core/modules/contextual/js/contextual.js
@@ -55,7 +55,7 @@ function initContextual ($contextual, html) {
     .prepend(Drupal.theme('contextualTrigger'));
 
   // Set the destination parameter on each of the contextual links.
-  var destination = 'destination=' + Drupal.encodePath(drupalSettings.currentPath);
+  var destination = 'destination=' + Drupal.encodePath(drupalSettings.path.currentPath);
   $contextual.find('.contextual-links a').each(function () {
     var url = this.getAttribute('href');
     var glue = (url.indexOf('?') === -1) ? '?' : '&';
diff --git a/core/modules/edit/js/editors/formEditor.js b/core/modules/edit/js/editors/formEditor.js
index 7eda4e1b6309..9687f58484f1 100644
--- a/core/modules/edit/js/editors/formEditor.js
+++ b/core/modules/edit/js/editors/formEditor.js
@@ -144,7 +144,7 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
 
     delete this.formSaveAjax;
     // Allow form widgets to detach properly.
-    Drupal.detachBehaviors(this.$formContainer, null, 'unload');
+    Drupal.detachBehaviors(this.$formContainer.get(0), null, 'unload');
     this.$formContainer
       .off('change.edit', ':input')
       .off('keypress.edit', 'input')
diff --git a/core/modules/edit/js/views/AppView.js b/core/modules/edit/js/views/AppView.js
index ba65c3ee65b6..599ba2bf0541 100644
--- a/core/modules/edit/js/views/AppView.js
+++ b/core/modules/edit/js/views/AppView.js
@@ -448,7 +448,7 @@ Drupal.edit.AppView = Backbone.View.extend({
       // Attach behaviors again to the modified piece of HTML; this will
       // create a new field model and call rerenderedFieldToCandidate() with
       // it.
-      Drupal.attachBehaviors($context);
+      Drupal.attachBehaviors($context.get(0));
     };
 
     // When propagating the changes of another instance of this field, this
diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php
index aa54d8976891..c8f205f6dc00 100644
--- a/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php
+++ b/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php
@@ -113,7 +113,7 @@ function _testImageFieldFormatters($scheme) {
       '#width' => 40,
       '#height' => 20,
     );
-    $default_output = l($image, 'node/' . $nid, array('html' => TRUE, 'attributes' => array('class' => 'active')));
+    $default_output = l($image, 'node/' . $nid, array('html' => TRUE));
     $this->drupalGet('node/' . $nid);
     $this->assertRaw($default_output, 'Image linked to content formatter displaying correctly on full node view.');
 
diff --git a/core/modules/language/lib/Drupal/language/Plugin/Block/LanguageBlock.php b/core/modules/language/lib/Drupal/language/Plugin/Block/LanguageBlock.php
index 3ccae2250537..49c066ecc02d 100644
--- a/core/modules/language/lib/Drupal/language/Plugin/Block/LanguageBlock.php
+++ b/core/modules/language/lib/Drupal/language/Plugin/Block/LanguageBlock.php
@@ -88,6 +88,7 @@ public function build() {
             "language-switcher-{$links->method_id}",
           ),
         ),
+        '#set_active_class' => TRUE,
       );
     }
     return $build;
diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php
index 4613f4311721..b4c4616ff79e 100644
--- a/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php
+++ b/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php
@@ -42,8 +42,12 @@ function setUp() {
    * Functional tests for the language switcher block.
    */
   function testLanguageBlock() {
-    // Enable the language switching block.
-    $block = $this->drupalPlaceBlock('language_block:' . Language::TYPE_INTERFACE, array('id' => 'test_language_block'));
+    // Enable the language switching block..
+    $block = $this->drupalPlaceBlock('language_block:' . Language::TYPE_INTERFACE, array(
+      'id' => 'test_language_block',
+      // Ensure a 2-byte UTF-8 sequence is in the tested output.
+      'label' => $this->randomName(8) . '×',
+    ));
 
     // Add language.
     $edit = array(
@@ -55,9 +59,70 @@ function testLanguageBlock() {
     $edit = array('language_interface[enabled][language-url]' => '1');
     $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
 
+    $this->doTestLanguageBlockAuthenticated($block->label());
+    $this->doTestLanguageBlockAnonymous($block->label());
+  }
+
+  /**
+   * For authenticated users, the "active" class is set by JavaScript.
+   *
+   * @param string $block_label
+   *   The label of the language switching block.
+   *
+   * @see testLanguageBlock()
+   */
+  protected function doTestLanguageBlockAuthenticated($block_label) {
+    // Assert that the language switching block is displayed on the frontpage.
+    $this->drupalGet('');
+    $this->assertText($block_label, 'Language switcher block found.');
+
+    // Assert that each list item and anchor element has the appropriate data-
+    // attributes.
+    list($language_switcher) = $this->xpath('//div[@id=:id]/div[contains(@class, "content")]', array(':id' => 'block-test-language-block'));
+    $list_items = array();
+    $anchors = array();
+    foreach ($language_switcher->ul->li as $list_item) {
+      $classes = explode(" ", (string) $list_item['class']);
+      list($langcode) = array_intersect($classes, array('en', 'fr'));
+      $list_items[] = array(
+        'langcode_class' => $langcode,
+        'data-drupal-link-system-path' => (string) $list_item['data-drupal-link-system-path'],
+      );
+      $anchors[] = array(
+        'hreflang' => (string) $list_item->a['hreflang'],
+        'data-drupal-link-system-path' => (string) $list_item->a['data-drupal-link-system-path'],
+      );
+    }
+    $expected_list_items = array(
+      0 => array('langcode_class' => 'en', 'data-drupal-link-system-path' => 'user/2'),
+      1 => array('langcode_class' => 'fr', 'data-drupal-link-system-path' => 'user/2'),
+    );
+    $this->assertIdentical($list_items, $expected_list_items, 'The list items have the correct attributes that will allow the drupal.active-link library to mark them as active.');
+    $expected_anchors = array(
+      0 => array('hreflang' => 'en', 'data-drupal-link-system-path' => 'user/2'),
+      1 => array('hreflang' => 'fr', 'data-drupal-link-system-path' => 'user/2'),
+    );
+    $this->assertIdentical($anchors, $expected_anchors, 'The anchors have the correct attributes that will allow the drupal.active-link library to mark them as active.');
+    $settings = $this->drupalGetSettings();
+    $this->assertIdentical($settings['path']['currentPath'], 'user/2', 'drupalSettings.path.currentPath is set correctly to allow drupal.active-link to mark the correct links as active.');
+    $this->assertIdentical($settings['path']['isFront'], FALSE, 'drupalSettings.path.isFront is set correctly to allow drupal.active-link to mark the correct links as active.');
+    $this->assertIdentical($settings['path']['currentLanguage'], 'en', 'drupalSettings.path.currentLanguage is set correctly to allow drupal.active-link to mark the correct links as active.');
+  }
+
+  /**
+   * For anonymous users, the "active" class is set by PHP.
+   *
+   * @param string $block_label
+   *   The label of the language switching block.
+   *
+   * @see testLanguageBlock()
+   */
+  protected function doTestLanguageBlockAnonymous($block_label) {
+    $this->drupalLogout();
+
     // Assert that the language switching block is displayed on the frontpage.
     $this->drupalGet('');
-    $this->assertText($block->label(), 'Language switcher block found.');
+    $this->assertText($block_label, 'Language switcher block found.');
 
     // Assert that only the current language is marked as active.
     list($language_switcher) = $this->xpath('//div[@id=:id]/div[contains(@class, "content")]', array(':id' => 'block-test-language-block'));
@@ -104,7 +169,80 @@ function testLanguageLinkActiveClass() {
     $edit = array('language_interface[enabled][language-url]' => '1');
     $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
 
+    $this->doTestLanguageLinkActiveClassAuthenticated();
+    $this->doTestLanguageLinkActiveClassAnonymous();
+  }
+
+  /**
+   * For authenticated users, the "active" class is set by JavaScript.
+   *
+   * @see testLanguageLinkActiveClass()
+   */
+  protected function doTestLanguageLinkActiveClassAuthenticated() {
+    $function_name = '#type link';
+    $path = 'language_test/type-link-active-class';
+
+    // Test links generated by l() on an English page.
+    $current_language = 'English';
+    $this->drupalGet($path);
+
+    // Language code 'none' link should be active.
+    $langcode = 'none';
+    $links = $this->xpath('//a[@id = :id and @data-drupal-link-system-path = :path]', array(':id' => 'no_lang_link', ':path' => $path));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as 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 @hreflang = :lang and @data-drupal-link-system-path = :path]', array(':id' => 'en_link', ':lang' => 'en', ':path' => $path));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as 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 @hreflang = :lang and @data-drupal-link-system-path = :path]', array(':id' => 'fr_link', ':lang' => 'fr', ':path' => $path));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to NOT mark it as active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Verify that drupalSettings contains the correct values.
+    $settings = $this->drupalGetSettings();
+    $this->assertIdentical($settings['path']['currentPath'], $path, 'drupalSettings.path.currentPath is set correctly to allow drupal.active-link to mark the correct links as active.');
+    $this->assertIdentical($settings['path']['isFront'], FALSE, 'drupalSettings.path.isFront is set correctly to allow drupal.active-link to mark the correct links as active.');
+    $this->assertIdentical($settings['path']['currentLanguage'], 'en', 'drupalSettings.path.currentLanguage is set correctly to allow drupal.active-link to mark the correct links as active.');
+
+    // Test links generated by l() on a French page.
+    $current_language = 'French';
+    $this->drupalGet('fr/language_test/type-link-active-class');
+
+    // Language code 'none' link should be active.
+    $langcode = 'none';
+    $links = $this->xpath('//a[@id = :id and @data-drupal-link-system-path = :path]', array(':id' => 'no_lang_link', ':path' => $path));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as 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 @hreflang = :lang and @data-drupal-link-system-path = :path]', array(':id' => 'en_link', ':lang' => 'en', ':path' => $path));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to NOT mark it as 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 @hreflang = :lang and @data-drupal-link-system-path = :path]', array(':id' => 'fr_link', ':lang' => 'fr', ':path' => $path));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Verify that drupalSettings contains the correct values.
+    $settings = $this->drupalGetSettings();
+    $this->assertIdentical($settings['path']['currentPath'], $path, 'drupalSettings.path.currentPath is set correctly to allow drupal.active-link to mark the correct links as active.');
+    $this->assertIdentical($settings['path']['isFront'], FALSE, 'drupalSettings.path.isFront is set correctly to allow drupal.active-link to mark the correct links as active.');
+    $this->assertIdentical($settings['path']['currentLanguage'], 'fr', 'drupalSettings.path.currentLanguage is set correctly to allow drupal.active-link to mark the correct links as active.');
+  }
+
+  /**
+   * For anonymous users, the "active" class is set by PHP.
+   *
+   * @see testLanguageLinkActiveClass()
+   */
+  protected function doTestLanguageLinkActiveClassAnonymous() {
     $function_name = '#type link';
+    $path = 'language_test/type-link-active-class';
+
+    $this->drupalLogout();
 
     // Test links generated by l() on an English page.
     $current_language = 'English';
diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php
index e8b3b2661f9a..1a07175b830a 100644
--- a/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php
+++ b/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php
@@ -378,6 +378,11 @@ function testUrlLanguageFallback() {
     // Enable the language switcher block.
     $this->drupalPlaceBlock('language_block:' . Language::TYPE_INTERFACE, array('id' => 'test_language_block'));
 
+    // Log out, because for anonymous users, the "active" class is set by PHP
+    // (which means we can easily test it here), whereas for authenticated users
+    // it is set by JavaScript.
+    $this->drupalLogout();
+
     // Access the front page without specifying any valid URL language prefix
     // and having as browser language preference a non-default language.
     $http_header = array("Accept-Language: $langcode_browser_fallback;q=1");
@@ -431,7 +436,7 @@ function testLanguageDomain() {
     $italian_url = url('admin', array('language' => $languages['it'], 'script' => ''));
     $url_scheme = $this->request->isSecure() ? 'https://' : 'http://';
     $correct_link = $url_scheme . $link;
-    $this->assertTrue($italian_url == $correct_link, format_string('The url() function returns the right URL (@url) in accordance with the chosen language', array('@url' => $italian_url)));
+    $this->assertEqual($italian_url, $correct_link, format_string('The url() function returns the right URL (@url) in accordance with the chosen language', array('@url' => $italian_url)));
 
     // Test HTTPS via options.
     $this->settingsSet('mixed_mode_sessions', 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
index 001fe39ec733..781af064359a 100644
--- 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
@@ -58,6 +58,7 @@ public function typeLinkActiveClass() {
           'attributes' => array(
             'id' => 'no_lang_link',
           ),
+          'set_active_class' => TRUE,
         ),
       ),
       'fr' => array(
@@ -69,6 +70,7 @@ public function typeLinkActiveClass() {
           'attributes' => array(
             'id' => 'fr_link',
           ),
+          'set_active_class' => TRUE,
         ),
       ),
       'en' => array(
@@ -80,6 +82,7 @@ public function typeLinkActiveClass() {
           'attributes' => array(
             'id' => 'en_link',
           ),
+          'set_active_class' => TRUE,
         ),
       ),
     );
diff --git a/core/modules/system/lib/Drupal/system/Controller/SystemController.php b/core/modules/system/lib/Drupal/system/Controller/SystemController.php
index fce6ba12599f..d07752cd67a9 100644
--- a/core/modules/system/lib/Drupal/system/Controller/SystemController.php
+++ b/core/modules/system/lib/Drupal/system/Controller/SystemController.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Controller;
 
+use Drupal\Component\Utility\Json;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Entity\Query\QueryFactory;
@@ -329,4 +330,127 @@ public function themeSetDefault() {
     return system_theme_default();
   }
 
+  /**
+   * #post_render_cache callback; sets the "active" class on relevant links.
+   *
+   * This is a PHP implementation of the drupal.active-link JavaScript library.
+   *
+   * @param array $element
+   *  A renderable array with the following keys:
+   *    - #markup
+   *    - #attached
+   * @param array $context
+   *   An array with the following keys:
+   *   - path: the system path of the currently active page
+   *   - front: whether the current page is the front page (which implies the
+   *     current path might also be <front>)
+   *   - language: the language code of the currently active page
+   *   - query: the query string for the currently active page
+   *
+   * @return array
+   *   The updated renderable array.
+   *
+   * @todo Once a future version of PHP supports parsing HTML5 properly
+   *   (i.e. doesn't fail on https://drupal.org/comment/7938201#comment-7938201)
+   *   then we can get rid of this manual parsing and use DOMDocument instead.
+   */
+  public static function setLinkActiveClass(array $element, array $context) {
+    $search_key_current_path = 'data-drupal-link-system-path="' . $context['path'] . '"';
+    $search_key_front = 'data-drupal-link-system-path="&lt;front&gt;"';
+
+    // An active link's path is equal to the current path, so search the HTML
+    // for an attribute with that value.
+    $offset = 0;
+    while ((strpos($element['#markup'], 'data-drupal-link-system-path="' . $context['path'] . '"', $offset) !== FALSE || ($context['front'] && strpos($element['#markup'], 'data-drupal-link-system-path="&lt;front&gt;"', $offset) !== FALSE))) {
+      $pos_current_path = strpos($element['#markup'], $search_key_current_path, $offset);
+      $pos_front = strpos($element['#markup'], $search_key_front, $offset);
+
+      // Determine which of the two values matched: the exact path, or the
+      // <front> special case.
+      $pos_match = NULL;
+      $type_match = NULL;
+      if ($pos_current_path !== FALSE) {
+        $pos_match = $pos_current_path;
+        $type_match = 'path';
+      }
+      elseif ($context['front'] && $pos_front !== FALSE) {
+        $pos_match = $pos_front;
+        $type_match = 'front';
+      }
+
+      // Find beginning and ending of opening tag.
+      $pos_tag_start = NULL;
+      for ($i = $pos_match; $pos_tag_start === NULL && $i > 0; $i--) {
+        if ($element['#markup'][$i] === '<') {
+          $pos_tag_start = $i;
+        }
+      }
+      $pos_tag_end = NULL;
+      for ($i = $pos_match; $pos_tag_end === NULL && $i < strlen($element['#markup']); $i++) {
+        if ($element['#markup'][$i] === '>') {
+          $pos_tag_end = $i;
+        }
+      }
+
+      // Get the HTML: this will be the opening part of a single tag, e.g.:
+      //   <a href="/" data-drupal-link-system-path="&lt;front&gt;">
+      $tag = substr($element['#markup'], $pos_tag_start, $pos_tag_end - $pos_tag_start + 1);
+
+      // Parse it into a DOMDocument so we can reliably read and modify
+      // attributes.
+      $dom = new \DOMDocument();
+      @$dom->loadHTML('<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>' . $tag . '</body></html>');
+      $node = $dom->getElementsByTagName('body')->item(0)->firstChild;
+
+      // The language of an active link is equal to the current language.
+      $is_active = TRUE;
+      if ($context['language']) {
+        if ($node->hasAttribute('hreflang') && $node->getAttribute('hreflang') !== $context['language']) {
+          $is_active = FALSE;
+        }
+      }
+      // The query parameters of an active link are equal to the current
+      // parameters.
+      if ($is_active) {
+        if ($context['query']) {
+          if (!$node->hasAttribute('data-drupal-link-query') || $node->getAttribute('data-drupal-link-query') !== Json::encode($context['query'])) {
+            $is_active = FALSE;
+          }
+        }
+        else {
+          if ($node->hasAttribute('data-drupal-link-query')) {
+            $is_active = FALSE;
+          }
+        }
+      }
+
+      // Only if the the path, the language and the query match, we set the
+      // "active" class.
+      if ($is_active) {
+        $class = $node->getAttribute('class');
+        if (strlen($class) > 0) {
+          $class .= ' ';
+        }
+        $class .= 'active';
+        $node->setAttribute('class', $class);
+
+        // Get the updated tag.
+        $updated_tag = $dom->saveXML($node, LIBXML_NOEMPTYTAG);
+        // saveXML() added a closing tag, remove it.
+        $updated_tag = substr($updated_tag, 0, strrpos($updated_tag, '<'));
+
+        $element['#markup'] = str_replace($tag, $updated_tag, $element['#markup']);
+
+        // Ensure we only search the remaining HTML.
+        $offset = $pos_tag_end - strlen($tag) + strlen($updated_tag);
+      }
+      else {
+        // Ensure we only search the remaining HTML.
+        $offset = $pos_tag_end + 1;
+      }
+    }
+
+    return $element;
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Batch/ProcessingTest.php b/core/modules/system/lib/Drupal/system/Tests/Batch/ProcessingTest.php
index 11204fe6860a..8450b9c1344e 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Batch/ProcessingTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Batch/ProcessingTest.php
@@ -250,28 +250,28 @@ function _resultMessages($id) {
 
     switch ($id) {
       case 'batch_0':
-        $messages[] = 'results for batch 0<br />none';
+        $messages[] = 'results for batch 0<br>none';
         break;
 
       case 'batch_1':
-        $messages[] = 'results for batch 1<br />op 1: processed 10 elements';
+        $messages[] = 'results for batch 1<br>op 1: processed 10 elements';
         break;
 
       case 'batch_2':
-        $messages[] = 'results for batch 2<br />op 2: processed 10 elements';
+        $messages[] = 'results for batch 2<br>op 2: processed 10 elements';
         break;
 
       case 'batch_3':
-        $messages[] = 'results for batch 3<br />op 1: processed 10 elements<br />op 2: processed 10 elements';
+        $messages[] = 'results for batch 3<br>op 1: processed 10 elements<br>op 2: processed 10 elements';
         break;
 
       case 'batch_4':
-        $messages[] = 'results for batch 4<br />op 1: processed 10 elements';
+        $messages[] = 'results for batch 4<br>op 1: processed 10 elements';
         $messages = array_merge($messages, $this->_resultMessages('batch_2'));
         break;
 
       case 'batch_5':
-        $messages[] = 'results for batch 5<br />op 5: processed 10 elements';
+        $messages[] = 'results for batch 5<br>op 5: processed 10 elements';
         break;
 
       case 'chained':
diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php
index 2e529e43feba..565e35eddc47 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php
@@ -83,7 +83,7 @@ function testAddSetting() {
     drupal_add_library('system', 'drupalSettings');
     $javascript = _drupal_add_js();
     $last_settings = reset($javascript['settings']['data']);
-    $this->assertTrue(array_key_exists('currentPath', $last_settings), 'The current path JavaScript setting is set correctly.');
+    $this->assertTrue(array_key_exists('currentPath', $last_settings['path']), 'The current path JavaScript setting is set correctly.');
 
     $javascript = _drupal_add_js(array('drupal' => 'rocks', 'dries' => 280342800), 'setting');
     $last_settings = end($javascript['settings']['data']);
diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/FunctionsTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/FunctionsTest.php
index 658d9cd050c7..0ae413f73fb1 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Theme/FunctionsTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/FunctionsTest.php
@@ -2,12 +2,15 @@
 
 /**
  * @file
- * Definition of Drupal\system\Tests\Theme\FunctionsTest.
+ * Contains \Drupal\system\Tests\Theme\FunctionsTest.
  */
 
 namespace Drupal\system\Tests\Theme;
 
+use Drupal\Core\Session\UserSession;
 use Drupal\simpletest\WebTestBase;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Tests for common theme functions.
@@ -159,12 +162,6 @@ function testLinks() {
     $expected = '';
     $this->assertThemeOutput('links', $variables, $expected, 'Empty %callback with heading generates no output.');
 
-    // Set the current path to the front page path.
-    // Required to verify the "active" class in expected links below, and
-    // because the current path is different when running tests manually via
-    // simpletest.module ('batch') and via the testing framework ('').
-    _current_path(\Drupal::config('system.site')->get('page.front'));
-
     // Verify that a list of links is properly rendered.
     $variables = array();
     $variables['attributes'] = array('id' => 'somelinks');
@@ -191,7 +188,7 @@ function testLinks() {
     $expected_links .= '<ul id="somelinks">';
     $expected_links .= '<li class="a-link"><a href="' . url('a/link') . '">' . check_plain('A <link>') . '</a></li>';
     $expected_links .= '<li class="plain-text">' . check_plain('Plain "text"') . '</li>';
-    $expected_links .= '<li class="front-page active"><a href="' . url('<front>') . '" class="active">' . check_plain('Front page') . '</a></li>';
+    $expected_links .= '<li class="front-page"><a href="' . url('<front>') . '">' . check_plain('Front page') . '</a></li>';
     $expected_links .= '<li class="router-test"><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '">' . check_plain('Test route') . '</a></li>';
     $expected_links .= '</ul>';
 
@@ -224,11 +221,24 @@ function testLinks() {
     $expected_links .= '<ul id="somelinks">';
     $expected_links .= '<li class="a-link"><a href="' . url('a/link') . '" class="a/class">' . check_plain('A <link>') . '</a></li>';
     $expected_links .= '<li class="plain-text"><span class="a/class">' . check_plain('Plain "text"') . '</span></li>';
-    $expected_links .= '<li class="front-page active"><a href="' . url('<front>') . '" class="active">' . check_plain('Front page') . '</a></li>';
+    $expected_links .= '<li class="front-page"><a href="' . url('<front>') . '">' . check_plain('Front page') . '</a></li>';
     $expected_links .= '<li class="router-test"><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '">' . check_plain('Test route') . '</a></li>';
     $expected_links .= '</ul>';
     $expected = $expected_heading . $expected_links;
     $this->assertThemeOutput('links', $variables, $expected);
+
+    // Verify the data- attributes for setting the "active" class on links.
+    $this->container->set('current_user', new UserSession(array('uid' => 1)));
+    $variables['set_active_class'] = TRUE;
+    $expected_links = '';
+    $expected_links .= '<ul id="somelinks">';
+    $expected_links .= '<li class="a-link" data-drupal-link-system-path="a/link"><a href="' . url('a/link') . '" class="a/class" data-drupal-link-system-path="a/link">' . check_plain('A <link>') . '</a></li>';
+    $expected_links .= '<li class="plain-text"><span class="a/class">' . check_plain('Plain "text"') . '</span></li>';
+    $expected_links .= '<li class="front-page" data-drupal-link-system-path="&lt;front&gt;"><a href="' . url('<front>') . '" data-drupal-link-system-path="&lt;front&gt;">' . check_plain('Front page') . '</a></li>';
+    $expected_links .= '<li class="router-test" data-drupal-link-system-path="router_test/test1"><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '" data-drupal-link-system-path="router_test/test1">' . check_plain('Test route') . '</a></li>';
+    $expected_links .= '</ul>';
+    $expected = $expected_heading . $expected_links;
+    $this->assertThemeOutput('links', $variables, $expected);
   }
 
   /**
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 53fc6324f7a9..c2cf62012f6f 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -888,6 +888,20 @@ function system_library_info() {
     ),
   );
 
+  // Drupal's active link marking.
+  $libraries['drupal.active-link'] = array(
+    'title' => 'Drupal active link marking',
+    'version' => \Drupal::VERSION,
+    'js' => array(
+      'core/misc/active-link.js' => array(),
+    ),
+    'dependencies' => array(
+      array('system', 'drupal'),
+      array('system', 'drupalSettings'),
+      array('system', 'classList'),
+    ),
+  );
+
   // Drupal's Ajax framework.
   $libraries['drupal.ajax'] = array(
     'title' => 'Drupal AJAX',
@@ -1135,6 +1149,20 @@ function system_library_info() {
     ),
   );
 
+  // IE9 classList polyfill.
+  $libraries['classList'] = array(
+    'title' => 'classList.js',
+    'website' => 'https://github.com/eligrey/classList.js',
+    'version' => 'master',
+    'js' => array(
+      'core/assets/vendor/classList/classList.min.js' => array(
+        'group' => JS_LIBRARY,
+        'weight' => -21,
+        'browsers' => array('IE' => 'lte IE 9', '!IE' => FALSE),
+      ),
+    ),
+  );
+
   // jQuery.
   $libraries['jquery'] = array(
     'title' => 'jQuery',
@@ -2101,6 +2129,7 @@ function system_filetransfer_info() {
  * Implements hook_page_build().
  *
  * @see template_preprocess_maintenance_page()
+ * @see \Drupal\system\Controller\SystemController::setLinkActiveClass()
  */
 function system_page_build(&$page) {
   // Ensure the same CSS is loaded in template_preprocess_maintenance_page().
@@ -2120,6 +2149,28 @@ function system_page_build(&$page) {
       'weight' => CSS_COMPONENT - 10,
     );
   }
+
+  // Handle setting the "active" class on links by:
+  // - loading the active-link library if the current user is authenticated;
+  // - applying a post-render cache callback if the current user is anonymous.
+  // @see l()
+  // @see \Drupal\Core\Utility\LinkGenerator::generate()
+  // @see theme_links()
+  // @see \Drupal\system\Controller\SystemController::setLinkActiveClass
+  if (\Drupal::currentUser()->isAuthenticated()) {
+    $page['#attached']['library'][] = array('system', 'drupal.active-link');
+  }
+  else {
+    $page['#post_render_cache']['\Drupal\system\Controller\SystemController::setLinkActiveClass'] = array(
+      // Collect the current state that determines whether a link is active.
+      array(
+        'path' => current_path(),
+        'front' => drupal_is_front_page(),
+        'language' => language(\Drupal\Core\Language\Language::TYPE_URL)->id,
+        'query' => \Drupal::service('request')->query->all(),
+      )
+    );
+  }
 }
 
 /**
diff --git a/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc b/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc
index 6d9a9a60ac3d..ca1ea6f8c09f 100644
--- a/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc
+++ b/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc
@@ -94,7 +94,7 @@ function _batch_test_finished_helper($batch_id, $success, $results, $operations)
     $messages[] = t('An error occurred while processing @op with arguments:<br />@args', array('@op' => $error_operation[0], '@args' => print_r($error_operation[1], TRUE)));
   }
 
-  drupal_set_message(implode('<br />', $messages));
+  drupal_set_message(implode('<br>', $messages));
 }
 
 /**
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
index 12d287fbdc0d..ffce7dc46912 100644
--- 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
@@ -35,6 +35,9 @@ public function typeLinkActiveClass() {
         '#type' => 'link',
         '#title' => t('Link with no query string'),
         '#href' => current_path(),
+        '#options' => array(
+          'set_active_class' => TRUE,
+        ),
       ),
       'with_query' => array(
         '#type' => 'link',
@@ -45,6 +48,7 @@ public function typeLinkActiveClass() {
             'foo' => 'bar',
             'one' => 'two',
           ),
+          'set_active_class' => TRUE,
         ),
       ),
       'with_query_reversed' => array(
@@ -56,6 +60,7 @@ public function typeLinkActiveClass() {
             'one' => 'two',
             'foo' => 'bar',
           ),
+          'set_active_class' => TRUE,
         ),
       ),
     );
diff --git a/core/modules/toolbar/js/escapeAdmin.js b/core/modules/toolbar/js/escapeAdmin.js
index a602766924e3..72a9fb8ecfcf 100644
--- a/core/modules/toolbar/js/escapeAdmin.js
+++ b/core/modules/toolbar/js/escapeAdmin.js
@@ -7,7 +7,7 @@
 
 "use strict";
 
-var pathInfo = drupalSettings;
+var pathInfo = drupalSettings.path;
 var escapeAdminPath = sessionStorage.getItem('escapeAdminPath');
 
 // Saves the last non-administrative page in the browser to be able to link back
diff --git a/core/modules/toolbar/js/toolbar.menu.js b/core/modules/toolbar/js/toolbar.menu.js
index e440f23d15da..7e5c38a6750e 100644
--- a/core/modules/toolbar/js/toolbar.menu.js
+++ b/core/modules/toolbar/js/toolbar.menu.js
@@ -12,7 +12,7 @@
 /**
  * Store the open menu tray.
  */
-var activeItem = drupalSettings.basePath + Drupal.encodePath(drupalSettings.currentPath);
+var activeItem = Drupal.url(drupalSettings.path.currentPath);
 
   $.fn.drupalToolbarMenu = function () {
 
diff --git a/core/modules/views/js/base.js b/core/modules/views/js/base.js
index f929e4314c54..1ac8d23e1a6d 100644
--- a/core/modules/views/js/base.js
+++ b/core/modules/views/js/base.js
@@ -63,7 +63,7 @@
    */
   Drupal.Views.getPath = function (href) {
     href = Drupal.Views.pathPortion(href);
-    href = href.substring(drupalSettings.basePath.length, href.length);
+    href = href.substring(drupalSettings.path.basePath.length, href.length);
     // 3 is the length of the '?q=' added to the url without clean urls.
     if (href.substring(0, 3) === '?q=') {
       href = href.substring(3, href.length);
diff --git a/core/modules/views/tests/Drupal/views/Tests/Controller/SystemControllerTest.php b/core/modules/views/tests/Drupal/views/Tests/Controller/SystemControllerTest.php
new file mode 100644
index 000000000000..82c694eb2bae
--- /dev/null
+++ b/core/modules/views/tests/Drupal/views/Tests/Controller/SystemControllerTest.php
@@ -0,0 +1,347 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\edit\Tests\Access\EditEntityAccessCheckTest.
+ */
+
+namespace Drupal\edit\Tests\Access;
+
+use Symfony\Component\HttpFoundation\Request;
+use Drupal\Component\Utility\Json;
+use Drupal\Core\Template\Attribute;
+use Drupal\system\Controller\SystemController;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the System controller's #post_render_cache callback for active links.
+ *
+ * @group Drupal
+ * @group System
+ *
+ * @see \Drupal\system\Controller\SystemController::setLinkActiveClass()
+ */
+class SystemControllerTest extends UnitTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'System controller set active link class test',
+      'description' => 'Unit test of system controller #post_render_cache callback for marking active links.',
+      'group' => 'System'
+    );
+  }
+
+  /**
+   * Provides test data for testSetLinkActiveClass().
+   *
+   * @see \Drupal\system\Controller\SystemController::setLinkActiveClass()
+   */
+  public function providerTestSetLinkActiveClass() {
+    // Define all the variations that *don't* affect whether or not an "active"
+    // class is set, but that should remain unchanged:
+    // - surrounding HTML
+    // - tags for which to test the setting of the "active" class
+    // - content of said tags
+    $edge_case_html5 = '<audio src="foo.ogg">
+  <track kind="captions" src="foo.en.vtt" srclang="en" label="English">
+  <track kind="captions" src="foo.sv.vtt" srclang="sv" label="Svenska">
+</audio>';
+    $html = array(
+      // Simple HTML.
+      0 => array('prefix' => '<div><p>', 'suffix' => '</p></div>'),
+      // Tricky HTML5 example that's unsupported by PHP <=5.4's DOMDocument:
+      // https://drupal.org/comment/7938201#comment-7938201.
+      1 => array('prefix' => '<div><p>', 'suffix' => '</p>' . $edge_case_html5 . '</div>'),
+      // Multi-byte content *before* the HTML that needs the "active" class.
+      2 => array('prefix' => '<div><p>αβγδεζηθικλμνξοσὠ</p><p>', 'suffix' => '</p></div>'),
+    );
+    $tags = array(
+      // Of course, it must work on anchors.
+      'a',
+      // Unfortunately, it must also work on list items.
+      'li',
+      // … and therefor, on *any* tag, really.
+      'foo',
+    );
+    $contents = array(
+      // Regular content.
+      'test',
+      // Mix of UTF-8 and HTML entities, both must be retained.
+      '☆ 3 × 4 = €12 and 4 &times; 3 = &euro;12 &#9734',
+      // Multi-byte content.
+      'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ',
+      // Text that closely approximates an important attribute, but should be
+      // ignored.
+      'data-drupal-link-system-path=&quot;&lt;front&gt;&quot;',
+    );
+
+    // Define all variations that *do* affect whether or not an "active" class
+    // is set: all possible situations that can be encountered.
+    $situations = array();
+
+    // Situations with context: front page, English, no query.
+    $context = array(
+      'path' => 'myfrontpage',
+      'front' => TRUE,
+      'language' => 'en',
+      'query' => array(),
+    );
+    // Nothing to do.
+    $markup = '<foo>bar</foo>';
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => array());
+    // Matching path, plus all matching variations.
+    $attributes = array(
+      'data-drupal-link-system-path' => 'myfrontpage',
+    );
+    $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes);
+    $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes + array('hreflang' => 'en'));
+    // Matching path, plus all non-matching variations.
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl'));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => '{"foo":"bar"}'));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => '{"foo":"bar"}'));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => TRUE));
+    // Special matching path, plus all variations.
+    $attributes = array(
+      'data-drupal-link-system-path' => '<front>',
+    );
+    $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes);
+    $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes + array('hreflang' => 'en'));
+    // Special matching path, plus all non-matching variations.
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl'));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => '{"foo":"bar"}'));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => '{"foo":"bar"}'));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => TRUE));
+
+    // Situations with context: non-front page, Dutch, no query.
+    $context = array(
+      'path' => 'llama',
+      'front' => FALSE,
+      'language' => 'nl',
+      'query' => array(),
+    );
+    // Nothing to do.
+    $markup = '<foo>bar</foo>';
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => array());
+    // Matching path, plus all matching variations.
+    $attributes = array(
+      'data-drupal-link-system-path' => 'llama',
+    );
+    $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes);
+    $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes + array('hreflang' => 'nl'));
+    // Matching path, plus all non-matching variations.
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en'));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => '{"foo":"bar"}'));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => '{"foo":"bar"}'));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => TRUE));
+    // Special non-matching path, plus all variations.
+    $attributes = array(
+      'data-drupal-link-system-path' => '<front>',
+    );
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes);
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en'));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => '{"foo":"bar"}'));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => '{"foo":"bar"}'));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => TRUE));
+
+    // Situations with context: non-front page, Dutch, with query.
+    $context = array(
+      'path' => 'llama',
+      'front' => FALSE,
+      'language' => 'nl',
+      'query' => array('foo' => 'bar'),
+    );
+    // Nothing to do.
+    $markup = '<foo>bar</foo>';
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => array());
+    // Matching path, plus all matching variations.
+    $attributes = array(
+      'data-drupal-link-system-path' => 'llama',
+      'data-drupal-link-query' => Json::encode(array('foo' => 'bar')),
+    );
+    $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes);
+    $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes + array('hreflang' => 'nl'));
+    // Matching path, plus all non-matching variations.
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en'));
+    unset($attributes['data-drupal-link-query']);
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => TRUE));
+    // Special non-matching path, plus all variations.
+    $attributes = array(
+      'data-drupal-link-system-path' => '<front>',
+    );
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes);
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl'));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en'));
+    unset($attributes['data-drupal-link-query']);
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => TRUE));
+
+    // Situations with context: non-front page, Dutch, with query.
+    $context = array(
+      'path' => 'llama',
+      'front' => FALSE,
+      'language' => 'nl',
+      'query' => array('foo' => 'bar'),
+    );
+    // Nothing to do.
+    $markup = '<foo>bar</foo>';
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => array());
+    // Matching path, plus all matching variations.
+    $attributes = array(
+      'data-drupal-link-system-path' => 'llama',
+      'data-drupal-link-query' => Json::encode(array('foo' => 'bar')),
+    );
+    $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes);
+    $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes + array('hreflang' => 'nl'));
+    // Matching path, plus all non-matching variations.
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en'));
+    unset($attributes['data-drupal-link-query']);
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => TRUE));
+    // Special non-matching path, plus all variations.
+    $attributes = array(
+      'data-drupal-link-system-path' => '<front>',
+    );
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes);
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl'));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en'));
+    unset($attributes['data-drupal-link-query']);
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => TRUE));
+
+    // Situations with context: front page, English, query.
+    $context = array(
+      'path' => 'myfrontpage',
+      'front' => TRUE,
+      'language' => 'en',
+      'query' => array('foo' => 'bar'),
+    );
+    // Nothing to do.
+    $markup = '<foo>bar</foo>';
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => array());
+    // Matching path, plus all matching variations.
+    $attributes = array(
+      'data-drupal-link-system-path' => 'myfrontpage',
+      'data-drupal-link-query' => Json::encode(array('foo' => 'bar')),
+    );
+    $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes);
+    $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes + array('hreflang' => 'en'));
+    // Matching path, plus all non-matching variations.
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl'));
+    unset($attributes['data-drupal-link-query']);
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => TRUE));
+    // Special matching path, plus all variations.
+    $attributes = array(
+      'data-drupal-link-system-path' => '<front>',
+      'data-drupal-link-query' => Json::encode(array('foo' => 'bar')),
+    );
+    $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes);
+    $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes + array('hreflang' => 'en'));
+    // Special matching path, plus all non-matching variations.
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl'));
+    unset($attributes['data-drupal-link-query']);
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => ""));
+    $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => TRUE));
+
+    // Helper function to generate a stubbed renderable array.
+    $create_element = function ($markup) {
+      return array(
+        '#markup' => $markup,
+        '#attached' => array(),
+      );
+    };
+
+    // Loop over the surrounding HTML variations.
+    $data = array();
+    for ($h = 0; $h < count($html); $h++) {
+      $html_prefix = $html[$h]['prefix'];
+      $html_suffix = $html[$h]['suffix'];
+      // Loop over the tag variations.
+      for ($t = 0; $t < count($tags); $t++) {
+        $tag = $tags[$t];
+        // Loop over the tag contents variations.
+        for ($c = 0; $c < count($contents); $c++) {
+          $tag_content = $contents[$c];
+
+          $create_markup = function (Attribute $attributes) use ($html_prefix, $html_suffix, $tag, $tag_content) {
+            return $html_prefix . '<' . $tag . $attributes . '>' . $tag_content . '</' . $tag . '>' . $html_suffix;
+          };
+
+          // Loop over the situations.
+          for ($s = 0; $s < count($situations); $s++) {
+            $situation = $situations[$s];
+
+            // Build the source markup.
+            $source_markup = $create_markup(new Attribute($situation['attributes']));
+
+            // Build the target markup. If no "active" class should be set, the
+            // resulting HTML should be identical. Otherwise, it should get an
+            // "active" class, either by extending an existing "class" attribute
+            // or by adding a "class" attribute.
+            $target_markup = NULL;
+            if (!$situation['is active']) {
+              $target_markup = $source_markup;
+            }
+            else {
+              $active_attributes = $situation['attributes'];
+              if (!isset($active_attributes['class'])) {
+                $active_attributes['class'] = array();
+              }
+              $active_attributes['class'][] = 'active';
+              $target_markup = $create_markup(new Attribute($active_attributes));
+            }
+
+            $data[] = array($create_element($source_markup), $situation['context'], $create_element($target_markup));
+          }
+        }
+      }
+    }
+
+    return $data;
+  }
+
+  /**
+   * Tests setLinkActiveClass().
+   *
+   * @param array $element
+   *  A renderable array with the following keys:
+   *    - #markup
+   *    - #attached
+   * @param array $context
+   *   The page context to simulate. An array with the following keys:
+   *   - path: the system path of the currently active page
+   *   - front: whether the current page is the front page (which implies the
+   *     current path might also be <front>)
+   *   - language: the language code of the currently active page
+   *   - query: the query string for the currently active page
+   * @param array $expected_element
+   *   The returned renderable array.
+   *
+   * @dataProvider providerTestSetLinkActiveClass
+   */
+  public function testSetLinkActiveClass(array $element, array $context, $expected_element) {
+    $this->assertSame($expected_element, SystemController::setLinkActiveClass($element, $context));
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
index 469b0e39f58b..80d9d5859952 100644
--- a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
+++ b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
@@ -43,13 +43,19 @@ class LinkGeneratorTest extends UnitTestCase {
   protected $moduleHandler;
 
   /**
-   *
    * The mocked language manager.
    *
    * @var \PHPUnit_Framework_MockObject_MockObject
    */
   protected $languageManager;
 
+  /**
+   * The mocked path alias manager.
+   *
+   * @var \Drupal\Core\Path\AliasManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $aliasManager;
+
   /**
    * Contains the LinkGenerator default options.
    */
@@ -57,6 +63,7 @@ class LinkGeneratorTest extends UnitTestCase {
     'query' => array(),
     'html' => FALSE,
     'language' => NULL,
+    'set_active_class' => FALSE,
   );
 
   /**
@@ -80,8 +87,9 @@ protected function setUp() {
     $this->urlGenerator = $this->getMock('\Drupal\Core\Routing\UrlGenerator', array(), array(), '', FALSE);
     $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
     $this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
+    $this->aliasManager = $this->getMock('\Drupal\Core\Path\AliasManagerInterface');
 
-    $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->moduleHandler, $this->languageManager);
+    $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->moduleHandler, $this->languageManager, $this->aliasManager);
   }
 
   /**
@@ -312,19 +320,31 @@ public function testGenerateWithHtml() {
    *   service.
    */
   public function testGenerateActive() {
-    $this->urlGenerator->expects($this->exactly(7))
+    $this->urlGenerator->expects($this->exactly(8))
       ->method('generateFromRoute')
       ->will($this->returnValueMap(array(
-        array('test_route_1', array(), FALSE, '/test-route-1'),
-        array('test_route_1', array(), FALSE, '/test-route-1'),
-        array('test_route_1', array(), FALSE, '/test-route-1'),
         array('test_route_1', array(), FALSE, '/test-route-1'),
         array('test_route_3', array(), FALSE, '/test-route-3'),
-        array('test_route_3', array(), FALSE, '/test-route-3'),
         array('test_route_4', array('object' => '1'), FALSE, '/test-route-4/1'),
       )));
 
-    $this->moduleHandler->expects($this->exactly(7))
+    $this->urlGenerator->expects($this->exactly(7))
+      ->method('getPathFromRoute')
+      ->will($this->returnValueMap(array(
+        array('test_route_1', array(), 'test-route-1'),
+        array('test_route_3', array(), 'test-route-3'),
+        array('test_route_4', array('object' => '1'), 'test-route-4/1'),
+      )));
+
+    $this->aliasManager->expects($this->exactly(7))
+      ->method('getSystemPath')
+      ->will($this->returnValueMap(array(
+        array('test-route-1', NULL, 'test-route-1'),
+        array('test-route-3', NULL, 'test-route-3'),
+        array('test-route-4/1', NULL, 'test-route-4/1'),
+      )));
+
+    $this->moduleHandler->expects($this->exactly(8))
       ->method('alter');
 
     $this->setUpLanguageManager();
@@ -332,10 +352,10 @@ public function testGenerateActive() {
     // Render a link with a path different from the current path.
     $request = new Request(array(), array(), array('system_path' => 'test-route-2'));
     $this->linkGenerator->setRequest($request);
-    $result = $this->linkGenerator->generate('Test', 'test_route_1');
-    $this->assertNotTag(array(
+    $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array('set_active_class' => TRUE));
+    $this->assertTag(array(
       'tag' => 'a',
-      'attributes' => array('class' => 'active'),
+      'attributes' => array('data-drupal-link-system-path' => 'test-route-1'),
     ), $result);
 
     // Render a link with the same path as the current path.
@@ -345,17 +365,31 @@ public function testGenerateActive() {
     $raw_variables = new ParameterBag();
     $request->attributes->set('_raw_variables', $raw_variables);
     $this->linkGenerator->setRequest($request);
-    $result = $this->linkGenerator->generate('Test', 'test_route_1');
+    $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array('set_active_class' => TRUE));
     $this->assertTag(array(
       'tag' => 'a',
-      'attributes' => array('class' => 'active'),
+      'attributes' => array('data-drupal-link-system-path' => 'test-route-1'),
+    ), $result);
+
+    // Render a link with the same path as the current path, but with the
+    // set_active_class option disabled.
+    $request = new Request(array(), array(), array('system_path' => 'test-route-1', RouteObjectInterface::ROUTE_NAME => 'test_route_1'));
+    // This attribute is expected to be set in a Drupal request by
+    // \Drupal\Core\ParamConverter\ParamConverterManager
+    $raw_variables = new ParameterBag();
+    $request->attributes->set('_raw_variables', $raw_variables);
+    $this->linkGenerator->setRequest($request);
+    $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array('set_active_class' => FALSE));
+    $this->assertNotTag(array(
+      'tag' => 'a',
+      'attributes' => array('data-drupal-link-system-path' => 'test-route-1'),
     ), $result);
 
     // Render a link with the same path and language as the current path.
-    $result = $this->linkGenerator->generate('Test', 'test_route_1');
+    $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array('set_active_class' => TRUE));
     $this->assertTag(array(
       'tag' => 'a',
-      'attributes' => array('class' => 'active'),
+      'attributes' => array('data-drupal-link-system-path' => 'test-route-1'),
     ), $result);
 
     // Render a link with the same path but a different language than the current
@@ -364,11 +398,17 @@ public function testGenerateActive() {
       'Test',
       'test_route_1',
       array(),
-      array('language' => new Language(array('id' => 'de')))
+      array(
+        'language' => new Language(array('id' => 'de')),
+        'set_active_class' => TRUE,
+      )
     );
-    $this->assertNotTag(array(
+    $this->assertTag(array(
       'tag' => 'a',
-      'attributes' => array('class' => 'active'),
+      'attributes' => array(
+        'data-drupal-link-system-path' => 'test-route-1',
+        'hreflang' => 'de',
+      ),
     ), $result);
 
     // Render a link with the same path and query parameter as the current path.
@@ -380,11 +420,17 @@ public function testGenerateActive() {
       'Test',
       'test_route_3',
       array(),
-      array('query' => array('value' => 'example_1')
-    ));
+      array(
+        'query' => array('value' => 'example_1'),
+        'set_active_class' => TRUE,
+      )
+    );
     $this->assertTag(array(
       'tag' => 'a',
-      'attributes' => array('class' => 'active'),
+      'attributes' => array(
+        'data-drupal-link-system-path' => 'test-route-3',
+        'data-drupal-link-query' => 'regexp:/.*value.*example_1.*/',
+      ),
     ), $result);
 
     // Render a link with the same path but a different query parameter than the
@@ -393,12 +439,19 @@ public function testGenerateActive() {
       'Test',
       'test_route_3',
       array(),
-      array('query' => array('value' => 'example_2'))
+      array(
+        'query' => array('value' => 'example_2'),
+        'set_active_class' => TRUE,
+      )
     );
-    $this->assertNotTag(array(
+    $this->assertTag(array(
       'tag' => 'a',
-      'attributes' => array('class' => 'active'),
+      'attributes' => array(
+        'data-drupal-link-system-path' => 'test-route-3',
+        'data-drupal-link-query' => 'regexp:/.*value.*example_2.*/',
+      ),
     ), $result);
+
     // Render a link with the same path and query parameter as the current path.
     $request = new Request(array('value' => 'example_1'), array(), array('system_path' => 'test-route-4/1', RouteObjectInterface::ROUTE_NAME => 'test_route_4'));
     $raw_variables = new ParameterBag(array('object' => '1'));
@@ -408,11 +461,17 @@ public function testGenerateActive() {
       'Test',
       'test_route_4',
       array('object' => '1'),
-      array('query' => array('value' => 'example_1'))
+      array(
+        'query' => array('value' => 'example_1'),
+        'set_active_class' => TRUE,
+      )
     );
     $this->assertTag(array(
       'tag' => 'a',
-      'attributes' => array('class' => 'active'),
+      'attributes' => array(
+        'data-drupal-link-system-path' => 'test-route-4/1',
+        'data-drupal-link-query' => 'regexp:/.*value.*example_1.*/',
+      ),
     ), $result);
   }
 
-- 
GitLab