.../Drupal/system/Controller/SystemController.php | 121 +++++++++++++------- 1 file changed, 82 insertions(+), 39 deletions(-) diff --git a/core/modules/system/lib/Drupal/system/Controller/SystemController.php b/core/modules/system/lib/Drupal/system/Controller/SystemController.php index 3d61174..d6a1de9 100644 --- a/core/modules/system/lib/Drupal/system/Controller/SystemController.php +++ b/core/modules/system/lib/Drupal/system/Controller/SystemController.php @@ -349,50 +349,93 @@ public function themeSetDefault() { * * @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) { - // If none of the HTML in the current page contains even just the current - // page's attribute, return early. - if (strpos($element['#markup'], 'data-drupal-link-system-path="' . $context['path'] . '"') === FALSE && (!$context['front'] || strpos($element['#markup'], 'data-drupal-link-system-path="<front>"') === FALSE)) { - return $element; - } + $search_key_current_path = 'data-drupal-link-system-path="' . $context['path'] . '"'; + $search_key_front = 'data-drupal-link-system-path="<front>"'; + + // 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="<front>"', $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 + // special case. + $pos_match = NULL; + $type_match = NULL; + if ($pos_current_path !== FALSE) { + $pos_match = $pos_current_path; + $type_match = 'path'; + } + else if ($context['front'] && $pos_front !== FALSE) { + $pos_match = $pos_front; + $type_match = 'front'; + } - // Build XPath query to find links that should get the "active" class. - $query = "//*["; - // An active link's path is equal to the current path. - $query .= "@data-drupal-link-system-path='" . $context['path'] . "'"; - if ($context['front']) { - $query .= " or @data-drupal-link-system-path=''"; - } - // The language of an active link is equal to the current language. - if ($context['language']) { - $query .= " and (not(@hreflang) or @hreflang='" . $context['language'] . "')"; - } - // The query parameters of an active link are equal to the current - // parameters. - if ($context['query']) { - $query .= " and @data-drupal-link-query='" . Json::encode($context['query']) . "'"; - } - else { - $query .= " and not(@data-drupal-link-query)"; - } - $query .= "]"; - - // Set the "active" class on all matching HTML elements. - $dom = new \DOMDocument(); - @$dom->loadHTML('' . $element['#markup'] . ''); - $xpath = new \DOMXPath($dom); - foreach ($xpath->query($query) as $node) { - $class = $node->getAttribute('class'); - if (strlen($class) > 0) { - $class .= ' '; + // 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.: + // + $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('' . $tag . ''); + $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 && $context['query']) { + if ($node->hasAttribute('data-drupal-link-query') && $node->getAttribute('data-drupal-link-query') !== Json::encode($context['query'])) { + $is_active = FALSE; + } } - $class .= 'active'; - $node->setAttribute('class', $class); - } - $body_dom_node = $dom->getElementsByTagName('body')->item(0); - $element['#markup'] = substr($dom->saveHTML($body_dom_node), 6, -7); + // 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); + + $body_dom_node = $dom->getElementsByTagName('body')->item(0); + $updated_tag = substr($dom->saveHTML($body_dom_node), 6, -7); + + $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); + } return $element; }