.../EventSubscriber/ActiveLinkResponseFilter.php | 150 +++++++++++++++++++-- .../system/src/Controller/SystemController.php | 128 ------------------ .../ActiveLinkResponseFilterTest.php} | 17 ++- 3 files changed, 145 insertions(+), 150 deletions(-) diff --git a/core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php b/core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php index 1f5d2ec..8f932da 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php +++ b/core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php @@ -7,6 +7,7 @@ namespace Drupal\Core\EventSubscriber; +use Drupal\Component\Serialization\Json; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Path\PathMatcherInterface; @@ -81,21 +82,144 @@ public function onResponse(FilterResponseEvent $event) { return; } - $context = [ - 'path' => $this->routeMatch->getRouteName() ? Url::fromRouteMatch($this->routeMatch)->getInternalPath() : '', - 'front' => $this->pathMatcher->isFrontPage(), - 'language' => $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId(), - 'query' => $event->getRequest()->query->all(), - ]; - + $current_path = $this->routeMatch->getRouteName() ? Url::fromRouteMatch($this->routeMatch)->getInternalPath() : ''; $response = $event->getResponse(); - $html_markup = $response->getContent(); + $response->setContent(static::setLinkActiveClass( + $response->getContent(), + $current_path, + $this->pathMatcher->isFrontPage(), + $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId(), + $event->getRequest()->query->all() + )); + } + + + /** + * Sets the "is-active" class on relevant links. + * + * This is a PHP implementation of the drupal.active-link JavaScript library. + * + * @param string $html_markup. + * The HTML markup to update. + * @param string $current_path + * The system path of the currently active page. + * @param bool $is_front + * Whether the current page is the front page (which implies the current + * path might also be ). + * @param string $url_language + * The language code of the current URL. + * @param array $query + * The query string for the current URL. + * + * @return string + * The updated HTML markup. + * + * @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($html_markup, $current_path, $is_front, $url_language, array $query) { + $search_key_current_path = 'data-drupal-link-system-path="' . $current_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($html_markup, $search_key_current_path, $offset) !== FALSE || ($is_front && strpos($html_markup, $search_key_front, $offset) !== FALSE)) { + $pos_current_path = strpos($html_markup, $search_key_current_path, $offset); + $pos_front = strpos($html_markup, $search_key_front, $offset); + + // Determine which of the two values is the next match: the exact path, or + // the special case. + $pos_match = NULL; + if ($pos_front === FALSE) { + $pos_match = $pos_current_path; + } + elseif ($pos_current_path === FALSE) { + $pos_match = $pos_front; + } + elseif ($pos_current_path < $pos_front) { + $pos_match = $pos_current_path; + } + else { + $pos_match = $pos_front; + } + + // Find beginning and ending of opening tag. + $pos_tag_start = NULL; + for ($i = $pos_match; $pos_tag_start === NULL && $i > 0; $i--) { + if ($html_markup[$i] === '<') { + $pos_tag_start = $i; + } + } + $pos_tag_end = NULL; + for ($i = $pos_match; $pos_tag_end === NULL && $i < strlen($html_markup); $i++) { + if ($html_markup[$i] === '>') { + $pos_tag_end = $i; + } + } + + // Get the HTML: this will be the opening part of a single tag, e.g.: + // + $tag = substr($html_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; + + // Ensure we don't set the "active" class twice on the same element. + $class = $node->getAttribute('class'); + $add_active = !in_array('is-active', explode(' ', $class)); + + // The language of an active link is equal to the current language. + if ($add_active && $url_language) { + if ($node->hasAttribute('hreflang') && $node->getAttribute('hreflang') !== $url_language) { + $add_active = FALSE; + } + } + // The query parameters of an active link are equal to the current + // parameters. + if ($add_active) { + if ($query) { + if (!$node->hasAttribute('data-drupal-link-query') || $node->getAttribute('data-drupal-link-query') !== Json::encode($query)) { + $add_active = FALSE; + } + } + else { + if ($node->hasAttribute('data-drupal-link-query')) { + $add_active = FALSE; + } + } + } + + // Only if the path, the language and the query match, we set the + // "is-active" class. + if ($add_active) { + if (strlen($class) > 0) { + $class .= ' '; + } + $class .= 'is-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, '<')); + + $html_markup = str_replace($tag, $updated_tag, $html_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; + } + } - $element = [ - '#markup' => $html_markup, - ]; - $element = \Drupal\system\Controller\SystemController::setLinkActiveClass($element, $context); - $response->setContent($element['#markup']); + return $html_markup; } /** diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php index 90c260c..13e2f73 100644 --- a/core/modules/system/src/Controller/SystemController.php +++ b/core/modules/system/src/Controller/SystemController.php @@ -328,132 +328,4 @@ public function themesPage() { return $build; } - /** - * #post_render_cache callback; sets the "is-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 ) - * - 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="<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'], $search_key_current_path, $offset) !== FALSE || ($context['front'] && strpos($element['#markup'], $search_key_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 is the next match: the exact path, or - // the special case. - $pos_match = NULL; - if ($pos_front === FALSE) { - $pos_match = $pos_current_path; - } - elseif ($pos_current_path === FALSE) { - $pos_match = $pos_front; - } - elseif ($pos_current_path < $pos_front) { - $pos_match = $pos_current_path; - } - else { - $pos_match = $pos_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.: - // - $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; - - // Ensure we don't set the "active" class twice on the same element. - $class = $node->getAttribute('class'); - $add_active = !in_array('is-active', explode(' ', $class)); - - // The language of an active link is equal to the current language. - if ($add_active && $context['language']) { - if ($node->hasAttribute('hreflang') && $node->getAttribute('hreflang') !== $context['language']) { - $add_active = FALSE; - } - } - // The query parameters of an active link are equal to the current - // parameters. - if ($add_active) { - if ($context['query']) { - if (!$node->hasAttribute('data-drupal-link-query') || $node->getAttribute('data-drupal-link-query') !== Json::encode($context['query'])) { - $add_active = FALSE; - } - } - else { - if ($node->hasAttribute('data-drupal-link-query')) { - $add_active = FALSE; - } - } - } - - // Only if the path, the language and the query match, we set the - // "is-active" class. - if ($add_active) { - if (strlen($class) > 0) { - $class .= ' '; - } - $class .= 'is-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/tests/src/Unit/Controller/SystemControllerTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/ActiveLinkResponseFilterTest.php similarity index 97% rename from core/modules/system/tests/src/Unit/Controller/SystemControllerTest.php rename to core/tests/Drupal/Tests/Core/EventSubscriber/ActiveLinkResponseFilterTest.php index 173a430..b79428d 100644 --- a/core/modules/system/tests/src/Unit/Controller/SystemControllerTest.php +++ b/core/tests/Drupal/Tests/Core/EventSubscriber/ActiveLinkResponseFilterTest.php @@ -2,27 +2,26 @@ /** * @file - * Contains \Drupal\Tests\system\Unit\Controller\SystemControllerTest. + * Contains \Drupal\Tests\Core\EventSubscriber\ActiveLinkResponseFilterTest. */ -namespace Drupal\Tests\system\Unit\Controller; +namespace Drupal\Tests\Core\EventSubscriber; use Drupal\Component\Serialization\Json; -use Symfony\Component\HttpFoundation\Request; +use Drupal\Core\EventSubscriber\ActiveLinkResponseFilter; use Drupal\Core\Template\Attribute; -use Drupal\system\Controller\SystemController; use Drupal\Tests\UnitTestCase; /** - * @coversDefaultClass \Drupal\system\Controller\SystemController - * @group system + * @coversDefaultClass \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter + * @group EventSubscriber */ -class SystemControllerTest extends UnitTestCase { +class ActiveLinkResponseFilterTest extends UnitTestCase { /** * Provides test data for testSetLinkActiveClass(). * - * @see \Drupal\system\Controller\SystemController::setLinkActiveClass() + * @see \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter::setLinkActiveClass() */ public function providerTestSetLinkActiveClass() { // Define all the variations that *don't* affect whether or not an @@ -350,7 +349,7 @@ public function providerTestSetLinkActiveClass() { * @covers ::setLinkActiveClass */ public function testSetLinkActiveClass(array $element, array $context, $expected_element) { - $this->assertSame($expected_element, SystemController::setLinkActiveClass($element, $context)); + $this->assertSame($expected_element['#markup'], ActiveLinkResponseFilter::setLinkActiveClass($element['#markup'], $context['path'], $context['front'], $context['language'], $context['query'])); } }