diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc index 30bd1c6b95..37d21ebb2d 100644 --- a/core/modules/node/node.tokens.inc +++ b/core/modules/node/node.tokens.inc @@ -130,23 +130,22 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta $translation = \Drupal::service('entity.repository')->getTranslationFromContext($node, $langcode, ['operation' => 'node_tokens']); if ($translation->hasField('body') && ($items = $translation->get('body')) && !$items->isEmpty()) { $item = $items[0]; - // If the summary was requested and is not empty, use it. - if ($name == 'summary' && !empty($item->summary)) { - $output = $item->summary_processed; + if ($name === 'body') { + $output = $item->processed; } - // Attempt to provide a suitable version of the 'body' field. else { - $output = $item->processed; - // A summary was requested. - if ($name == 'summary') { - // Generate an optionally trimmed summary of the body field. - + // If the summary was requested and is not empty, use it. + if (!empty($item->summary)) { + $output = $item->summary_processed; + } + // Else generate an optionally trimmed summary of the body field. + else { // Get the 'trim_length' size used for the 'teaser' mode, if // present, or use the default trim_length size. $display_options = \Drupal::service('entity_display.repository') ->getViewDisplay('node', $node->getType(), 'teaser') ->getComponent('body'); - if (isset($display_options['settings']['trim_length'])) { + if ($display_options && ($display_options['type'] === 'text_trimmed'|| $display_options['type'] === 'text_summary_or_trimmed')) { $length = $display_options['settings']['trim_length']; } else { @@ -154,11 +153,10 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta $length = $settings['trim_length']; } - $output = text_summary($output, $item->format, $length); + $output = text_summary($item->value, $item->format, $length); } } - // "processed" returns a \Drupal\Component\Render\MarkupInterface - // via check_markup(). + $replacements[$original] = $output; } break; diff --git a/core/modules/node/tests/src/Kernel/NodeTokenReplaceTest.php b/core/modules/node/tests/src/Kernel/NodeTokenReplaceTest.php index 61b8898345..02f942f660 100644 --- a/core/modules/node/tests/src/Kernel/NodeTokenReplaceTest.php +++ b/core/modules/node/tests/src/Kernel/NodeTokenReplaceTest.php @@ -7,6 +7,7 @@ use Drupal\Core\Render\BubbleableMetadata; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; +use Drupal\node\NodeInterface; use Drupal\Tests\system\Kernel\Token\TokenReplaceKernelTestBase; /** @@ -17,6 +18,13 @@ */ class NodeTokenReplaceTest extends TokenReplaceKernelTestBase { + /** + * User account used as a node author. + * + * @var \Drupal\user\Entity\User + */ + protected $account; + /** * Modules to enable. * @@ -34,10 +42,12 @@ protected function setUp(): void { $node_type = NodeType::create(['type' => 'article', 'name' => 'Article']); $node_type->save(); node_add_body_field($node_type); + + $this->account = $this->createUser(); } /** - * Creates a node, then tests the tokens generated from it. + * Tests all node tokens. */ public function testNodeTokenReplacement() { $url_options = [ @@ -45,13 +55,12 @@ public function testNodeTokenReplacement() { 'language' => $this->interfaceLanguage, ]; - // Create a user and a node. - $account = $this->createUser(); + // Create a node. /** @var \Drupal\node\NodeInterface $node */ $node = Node::create([ 'type' => 'article', 'tnid' => 0, - 'uid' => $account->id(), + 'uid' => $this->account->id(), 'title' => 'Blinking Text', 'body' => [['value' => 'Regular NODE body for the test.', 'summary' => 'Fancy NODE summary.', 'format' => 'plain_text']], ]); @@ -67,15 +76,14 @@ public function testNodeTokenReplacement() { $tests['[node:body]'] = $node->body->processed; $tests['[node:summary]'] = $node->body->summary_processed; $tests['[node:langcode]'] = $node->language()->getId(); + $tests['[node:url]'] = $node->toUrl('canonical', $url_options)->toString(); $tests['[node:edit-url]'] = $node->toUrl('edit-form', $url_options)->toString(); - $tests['[node:author]'] = $account->getAccountName(); + $$tests['[node:author]'] = $this->account->getAccountName(); $tests['[node:author:uid]'] = $node->getOwnerId(); - $tests['[node:author:name]'] = $account->getAccountName(); - /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */ - $date_formatter = $this->container->get('date.formatter'); - $tests['[node:created:since]'] = $date_formatter->formatTimeDiffSince($node->getCreatedTime(), ['langcode' => $this->interfaceLanguage->getId()]); - $tests['[node:changed:since]'] = $date_formatter->formatTimeDiffSince($node->getChangedTime(), ['langcode' => $this->interfaceLanguage->getId()]); + $tests['[node:author:name]'] = $this->account->getAccountName(); + $tests['[node:created:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($node->getCreatedTime(), ['langcode' => $this->interfaceLanguage->getId()]); + $tests['[node:changed:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($node->getChangedTime(), ['langcode' => $this->interfaceLanguage->getId()]); $base_bubbleable_metadata = BubbleableMetadata::createFromObject($node); @@ -107,27 +115,108 @@ public function testNodeTokenReplacement() { $this->assertEquals($expected, $output, new FormattableMarkup('Node token %token replaced.', ['%token' => $input])); $this->assertEquals($metadata_tests[$input], $bubbleable_metadata); } + } - // Repeat for a node without a summary. + /** + * Tests the [node:summary] token. + */ + public function testNodeSummaryTokenReplacement() { + // Length of this text is 843 characters and it's longer than a default + // trim length setting (600). + $text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + . 'Morbi diam dui, finibus et purus ac, elementum pretium augue. ' + . 'Fusce lacus nisl, feugiat sit amet blandit sed, mattis eu ligula. Duis laoreet dui non felis maximus fringilla. ' + . 'Maecenas tempor magna id urna dapibus, in elementum mauris consequat. ' + . 'Praesent in urna non felis fringilla ullamcorper interdum eu risus. ' + . 'Vestibulum id erat ultrices, varius est sed, dignissim dui. Vestibulum a dapibus nisl. ' + . 'Maecenas vestibulum nibh a aliquet mollis. Donec ac justo eget justo interdum faucibus. ' + . 'Aenean sit amet finibus turpis. Donec viverra vel eros eget varius. Praesent rutrum est diam. ' + . 'Mauris at odio scelerisque, mattis mi et, tempor eros. ' + . 'Vestibulum sollicitudin sem quis diam sollicitudin posuere. Nam finibus vestibulum suscipit. ' + . 'Fusce tempor tincidunt lorem vitae mollis.'; + // Create a node. + /** @var $node \Drupal\node\NodeInterface */ $node = Node::create([ 'type' => 'article', - 'uid' => $account->id(), + 'uid' => $this->account->id(), 'title' => 'Blinking Text', - 'body' => [['value' => 'A string that looks random like TR5c2I', 'format' => 'plain_text']], + 'body' => [['value' => $text, 'format' => 'plain_text']], ]); $node->save(); - // Generate and test token - use full body as expected value. - $tests = []; - $tests['[node:summary]'] = $node->body->processed; + // Get teaser node view display. + $view_display = \Drupal::entityTypeManager() + ->getStorage('entity_view_display') + ->load('node.article.teaser'); + if (!$view_display) { + $values = [ + 'targetEntityType' => 'node', + 'bundle' => 'article', + 'mode' => 'teaser', + 'status' => TRUE, + ]; + $view_display = \Drupal::entityTypeManager() + ->getStorage('entity_view_display') + ->create($values); + } - // Test to make sure that we generated something for each token. - $this->assertNotContains(0, array_map('strlen', $tests), 'No empty tokens generated for node without a summary.'); + // Token [node:summary] should use trim settings of "Summary or trimmed" + // formatter of teaser view mode. + $view_display->setComponent('body', [ + 'type' => 'text_summary_or_trimmed', + 'settings' => ['trim_length' => 160], + ]); + $view_display->save(); + $expected_trimmed_to_160 = Html::escape('Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + . 'Morbi diam dui, finibus et purus ac, elementum pretium augue.'); + $this->assertNodeSummaryTokenReplacement($node, $expected_trimmed_to_160); + + // Token [node:summary] should use trim settings of "Trimmed" + // formatter of teaser view mode. + $view_display->setComponent('body', [ + 'type' => 'text_trimmed', + 'settings' => ['trim_length' => 80], + ]); + $view_display->save(); + $expected_trimmed_to_80 = Html::escape('Lorem ipsum dolor sit amet, consectetur adipiscing elit.'); + $this->assertNodeSummaryTokenReplacement($node, $expected_trimmed_to_80); + + // Token [node:summary] should not pickup trim length setting of any other + // formatter rather than "Summary or trimmed" or "Trimmed", even if setting + // name matches. + // Fallbacks to default trim length setting (600). + $view_display->setComponent('body', [ + 'type' => 'text_default', + 'settings' => ['trim_length' => 42], + ]); + $view_display->save(); + $expected_trimmed_to_600 = Html::escape('Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + . 'Morbi diam dui, finibus et purus ac, elementum pretium augue. Fusce lacus nisl, feugiat sit amet blandit sed, mattis eu ligula. ' + . 'Duis laoreet dui non felis maximus fringilla. Maecenas tempor magna id urna dapibus, in elementum mauris consequat. ' + . 'Praesent in urna non felis fringilla ullamcorper interdum eu risus. ' + . 'Vestibulum id erat ultrices, varius est sed, dignissim dui. Vestibulum a dapibus nisl. ' + . 'Maecenas vestibulum nibh a aliquet mollis. Donec ac justo eget justo interdum faucibus. ' + . 'Aenean sit amet finibus turpis.'); + $this->assertNodeSummaryTokenReplacement($node, $expected_trimmed_to_600); + + // Token [node:summary] should not pickup trim length setting if teaser + // view mode does not exist. + // Fallbacks to "Summary or trimmed" default trim length setting (600). + $view_display->delete(); + $this->assertNodeSummaryTokenReplacement($node, $expected_trimmed_to_600); + } - foreach ($tests as $input => $expected) { - $output = $this->tokenService->replace($input, ['node' => $node], ['language' => $this->interfaceLanguage]); - $this->assertEquals($expected, $output, new FormattableMarkup('Node token %token replaced for node without a summary.', ['%token' => $input])); - } + /** + * Asserts that [node:summary] token works as expected. + * + * @param \Drupal\node\NodeInterface $node + * Node to assert against. + * @param string $expected + * Expected token replacement output. + */ + protected function assertNodeSummaryTokenReplacement(NodeInterface $node, $expected) { + $output = $this->tokenService->replace('[node:summary]', ['node' => $node], ['langcode' => $this->interfaceLanguage->getId()]); + $this->assertEquals($output, $expected, new FormattableMarkup('Node token %token replaced.', ['%token' => '[node:summary]'])); } }