Commit 624296e7 authored by catch's avatar catch

Issue #1979468 by Wim Leers, pwolanin, nod_, thedavidmeister, larowlan:...

Issue #1979468 by Wim Leers, pwolanin, nod_, thedavidmeister, larowlan: .active from linkGenerator(), l() and theme_links() forces an upper limit of per-page caching for all content containing links.
parent a1a98382
......@@ -286,7 +286,7 @@ services:
- { name: persist }
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:
......
......@@ -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' (default FALSE): Whether l() should compare the $path,
* language and query options to the current URL to determine whether the
* link is "active". If so, 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()->getLanguage(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.
......
......@@ -1704,6 +1704,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";
}
......@@ -1739,6 +1740,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']);
......@@ -1770,6 +1773,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
......
......@@ -1188,6 +1188,18 @@ 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. 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 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:
......@@ -1203,6 +1215,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'];
......@@ -1236,8 +1261,7 @@ function theme_links($variables) {
$num_links = count($links);
$i = 0;
$active = \Drupal::linkGenerator()->getActive();
$language_url = \Drupal::languageManager()->getLanguage(Language::TYPE_URL);
$active_route = \Drupal::linkGenerator()->getActive();
foreach ($links as $key => $link) {
$i++;
......@@ -1249,16 +1273,16 @@ 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);
// Add odd/even, first, and last classes.
$class[] = ($i % 2 ? 'odd' : 'even');
$li_attributes['class'][] = ($i % 2 ? 'odd' : 'even');
if ($i == 1) {
$class[] = 'first';
$li_attributes['class'][] = 'first';
}
if ($i == $num_links) {
$class[] = 'last';
$li_attributes['class'][] = 'last';
}
$link_element = array(
......@@ -1271,30 +1295,34 @@ 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 (!empty($link['language'])) {
$li_attributes['hreflang'] = $link['language']->id;
}
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';
}
// 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);
}
$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 (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.
......@@ -1309,7 +1337,7 @@ function theme_links($variables) {
}
}
$output .= '<li' . new Attribute(array('class' => $class)) . '>';
$output .= '<li' . new Attribute($li_attributes) . '>';
$output .= $item;
$output .= '</li>';
}
......@@ -2243,7 +2271,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'])) {
......@@ -2253,7 +2282,8 @@ function template_preprocess_page(&$variables) {
'#heading' => array(
'text' => t('Secondary menu'),
'class' => array('visually-hidden'),
)
),
'#set_active_class' => TRUE,
);
}
......@@ -2583,7 +2613,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),
......
......@@ -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\LanguageManager;
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\LanguageManager $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, LanguageManager $language_manager) {
public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler, LanguageManager $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()
......
......@@ -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,11 @@ 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' (default FALSE): Whether this method should compare
* the $route_name, $parameters, language and query options to the current
* URL to determine whether the link is "active". If so, 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.
......
/**
* @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.l = {
attach: function queryL (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);
......@@ -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);
}
},
......
......@@ -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;
};
/**
......
......@@ -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.');
......
......@@ -399,6 +399,7 @@ function language_switcher_url($type, $path) {
'title' => $language->name,
'language' => $language,
'attributes' => array('class' => array('language-link')),
'set_active_class' => TRUE,
);
}
......
......@@ -47,6 +47,7 @@ public function build() {
"language-switcher-{$links->method_id}",
),
),
'#set_active_class' => TRUE,
);
}
return $build;
......
......@@ -55,9 +55,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.');