Verified Commit acf3e5fa authored by godotislate's avatar godotislate
Browse files

refactor: #3568387 Move text_summary to TextSummary service and deprecate

By: smustgrave
By: dcam
By: nicxvan
By: berdir
By: mstrelan
By: godotislate
(cherry picked from commit f929b61a)
parent 979d45e3
Loading
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@

use Drupal\Core\Datetime\Entity\DateFormat;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\text\TextSummary;
use Drupal\user\Entity\User;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\BubbleableMetadata;
@@ -141,7 +142,7 @@ public function tokens($type, $tokens, array $data, array $options, BubbleableMe
                    $settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings('text_summary_or_trimmed');
                    $length = $settings['trim_length'];
                  }
                  $output = text_summary($output, $item->format, $length);
                  $output = \Drupal::service(TextSummary::class)->generate($output, $item->format, $length);
                }
              }
              // "processed" returns a \Drupal\Component\Render\MarkupInterface
+4 −3
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\text\TextSummary;

/**
 * Plugin implementation of the 'text_trimmed' formatter.
@@ -110,16 +111,16 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
   *     filter the text. Defaults to the fallback format. See
   *     filter_fallback_format().
   *   - #text_summary_trim_length: the desired character length of the summary
   *     (used by text_summary())
   *     (used by \Drupal\text\TextSummary::generate())
   *
   * @return array
   *   The passed-in element with the filtered text in '#markup' trimmed.
   *
   * @see filter_pre_render_text()
   * @see text_summary()
   * @see \Drupal\text\TextSummary::generate()
   */
  public static function preRenderSummary(array $element) {
    $element['#markup'] = text_summary($element['#markup'], $element['#format'], $element['#text_summary_trim_length']);
    $element['#markup'] = \Drupal::service(TextSummary::class)->generate($element['#markup'], $element['#format'], $element['#text_summary_trim_length']);
    return $element;
  }

+143 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\text;

use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\EntityTypeManagerInterface;

/**
 * Summary generator for text fields.
 */
class TextSummary {

  public function __construct(
    protected readonly EntityTypeManagerInterface $entityTypeManager,
  ) {}

  /**
   * Generates a trimmed, formatted version of a text field value.
   *
   * If the end of the summary is not indicated using the <!--break--> delimiter
   * then we generate the summary automatically, trying to end it at a sensible
   * place such as the end of a paragraph, a line break, or the end of a
   * sentence (in that order of preference).
   *
   * @param string|\Stringable $text
   *   The content for which a summary will be generated.
   * @param string|null $format
   *   The format of the content. If the line break filter is present then we
   *   treat newlines embedded in $text as line breaks. If the htmlcorrector
   *   filter is present, it will be run on the generated summary (if different
   *   from the incoming $text).
   * @param int $size
   *   The desired character length of the summary. If omitted, the default
   *   value will be used. Ignored if the special delimiter is present in $text.
   *
   * @return string|\Stringable
   *   The generated summary.
   */
  public function generate(string|\Stringable $text, string|null $format = NULL, int $size = 600): string|\Stringable {
    // Find where the delimiter is in the body.
    $delimiter = strpos((string) $text, '<!--break-->');

    // If the size is zero, and there is no delimiter, the entire body is the
    // summary.
    if ($size == 0 && $delimiter === FALSE) {
      return $text;
    }

    // If a valid delimiter has been specified, use it to chop off the summary.
    if ($delimiter !== FALSE) {
      return substr((string) $text, 0, $delimiter);
    }

    // Retrieve the filters of the specified text format, if any.
    if (isset($format)) {
      $filter_format = $this->entityTypeManager->getStorage('filter_format')->load($format);
      // If the specified format does not exist, return nothing.
      // $text is already filtered text, but the remainder of this function will
      // not be able to ensure a sane and secure summary.
      if (!$filter_format || !($filters = $filter_format->filters())) {
        return '';
      }
    }

    // If we have a short body, the entire body is the summary.
    if (mb_strlen((string) $text) <= $size) {
      return $text;
    }

    // If the delimiter has not been specified, try to split at paragraph or
    // sentence boundaries.

    // The summary may not be longer than the maximum length specified.
    // Initial slice.
    $summary = Unicode::truncate((string) $text, $size);

    // Store the actual length of the UTF8 string -- which might not be the same
    // as $size.
    $max_right_pos = strlen($summary);

    // How much to cut off the end of the summary so that it doesn't end in the
    // middle of a paragraph, sentence, or word.
    // Initialize it to maximum in order to find the minimum.
    $min_right_pos = $max_right_pos;

    // Store the reverse of the summary. We use strpos on the reversed needle
    // and haystack for speed and convenience.
    $reversed = strrev($summary);

    // Build an array of arrays of break points grouped by preference.
    $break_points = [];

    // A paragraph near the end of the sliced summary is most preferable.
    $break_points[] = ['</p>' => 0];

    // If no complete paragraph then treat line breaks as paragraphs.
    $line_breaks = ['<br />' => 6, '<br>' => 4];
    // Newline only indicates a line break if line break converter
    // filter is present.
    if (isset($format) && $filters->has('filter_autop') && $filters->get('filter_autop')->status) {
      $line_breaks["\n"] = 1;
    }
    $break_points[] = $line_breaks;

    // If the first paragraph is too long, split at the end of a sentence.
    $break_points[] = ['. ' => 1, '! ' => 1, '? ' => 1, '。' => 0, '؟ ' => 1];

    // Iterate over the groups of break points until a break point is found.
    foreach ($break_points as $points) {
      // Look for each break point, starting at the end of the summary.
      foreach ($points as $point => $offset) {
        // The summary is already reversed, but the break point isn't.
        $right_pos = strpos($reversed, strrev($point));
        if ($right_pos !== FALSE) {
          $min_right_pos = min($right_pos + $offset, $min_right_pos);
        }
      }

      // If a break point was found in this group, slice and stop searching.
      if ($min_right_pos !== $max_right_pos) {
        // Don't slice with length 0. Length must be <0 to slice from RHS.
        $summary = ($min_right_pos === 0) ? $summary : substr($summary, 0, 0 - $min_right_pos);
        break;
      }
    }

    // If filter_html or filter_htmlcorrector is enabled, normalize the output.
    if (isset($format)) {
      $filter_enabled = function (string $filter) use ($filters): bool {
        return $filters->has($filter) && $filters->get($filter)->status;
      };
      if ($filter_enabled('filter_html') || $filter_enabled('filter_htmlcorrector')) {
        $summary = Html::normalize($summary);
      }
    }

    return $summary;
  }

}
+14 −13
Original line number Diff line number Diff line
@@ -12,14 +12,17 @@
use Drupal\filter\Render\FilteredMarkup;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\text\TextSummary;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;

/**
 * Tests text_summary() with different strings and lengths.
 * Tests TextSummary::generate() with different strings and lengths.
 */
#[Group('text')]
#[RunTestsInSeparateProcesses]
#[CoversMethod(TextSummary::class, 'generate')]
class TextSummaryTest extends KernelTestBase {

  use UserCreationTrait;
@@ -106,8 +109,8 @@ public function testLength(): void {
    // This string tests a number of edge cases.
    $text = "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>";

    // The summaries we expect text_summary() to return when $size is the index
    // of each array item.
    // The summaries we expect TextSummary::generate() to return when
    // $size is the index of each array item.
    // Using no text format:
    $format = NULL;
    $i = 0;
@@ -237,8 +240,6 @@ public function testLength(): void {

  /**
   * Tests text summaries with an invalid filter format.
   *
   * @see text_summary()
   */
  public function testInvalidFilterFormat(): void {

@@ -246,12 +247,12 @@ public function testInvalidFilterFormat(): void {
  }

  /**
   * Calls text_summary() and asserts that the expected teaser is returned.
   * Calls TextSummary::generate() and asserts that the expected teaser is returned.
   *
   * @internal
   */
  public function assertTextSummary(string $text, string $expected, ?string $format = NULL, ?int $size = NULL): void {
    $summary = text_summary($text, $format, $size);
  public function assertTextSummary(string $text, string $expected, ?string $format = NULL, int $size = 600): void {
    $summary = \Drupal::service(TextSummary::class)->generate($text, $format, $size);
    $this->assertSame($expected, $summary, '<pre style="white-space: pre-wrap">' . $summary . '</pre> is identical to <pre style="white-space: pre-wrap">' . $expected . '</pre>');
  }

@@ -350,16 +351,16 @@ public function testNormalization(): void {
    ])->save();

    $filtered_markup = FilteredMarkup::create('<div><strong><span>Hello World</span></strong></div>');
    // With either HTML filter enabled, text_summary() will normalize the text
    // using HTML::normalize().
    $summary = text_summary($filtered_markup, 'filter_html_enabled', 30);
    // With either HTML filter enabled, TextSummary::generate() will
    // normalize the text using HTML::normalize().
    $summary = \Drupal::service(TextSummary::class)->generate($filtered_markup, 'filter_html_enabled', 30);
    $this->assertStringContainsString('<div><strong><span>', $summary);
    $this->assertStringContainsString('</span></strong></div>', $summary);
    $summary = text_summary($filtered_markup, 'filter_htmlcorrector_enabled', 30);
    $summary = \Drupal::service(TextSummary::class)->generate($filtered_markup, 'filter_htmlcorrector_enabled', 30);
    $this->assertStringContainsString('<div><strong><span>', $summary);
    $this->assertStringContainsString('</span></strong></div>', $summary);
    // If neither filter is enabled, the text will not be normalized.
    $summary = text_summary($filtered_markup, 'neither_filter_enabled', 30);
    $summary = \Drupal::service(TextSummary::class)->generate($filtered_markup, 'neither_filter_enabled', 30);
    $this->assertStringContainsString('<div><strong><span>', $summary);
    $this->assertStringNotContainsString('</span></strong></div>', $summary);
  }
+8 −101
Original line number Diff line number Diff line
@@ -4,9 +4,7 @@
 * @file
 */

use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\filter\Entity\FilterFormat;
use Drupal\text\TextSummary;

/**
 * Generates a trimmed, formatted version of a text field value.
@@ -29,108 +27,17 @@
 *
 * @return string
 *   The generated summary.
 *
 * @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Use
 *   \Drupal::service(TextSummary::class)->generate() instead.
 *
 * @see https://www.drupal.org/node/3568389
 */
function text_summary($text, $format = NULL, $size = NULL) {
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Use \Drupal::service(TextSummary::class)->generate() instead. See https://www.drupal.org/node/3568389', E_USER_DEPRECATED);

  if (!isset($size)) {
    $size = \Drupal::config('text.settings')->get('default_summary_length');
  }

  // Find where the delimiter is in the body.
  $delimiter = strpos($text, '<!--break-->');

  // If the size is zero, and there is no delimiter, the entire body is the
  // summary.
  if ($size == 0 && $delimiter === FALSE) {
    return $text;
  }

  // If a valid delimiter has been specified, use it to chop off the summary.
  if ($delimiter !== FALSE) {
    return substr($text, 0, $delimiter);
  }

  // Retrieve the filters of the specified text format, if any.
  if (isset($format)) {
    $filter_format = FilterFormat::load($format);
    // If the specified format does not exist, return nothing. $text is already
    // filtered text, but the remainder of this function will not be able to
    // ensure a sane and secure summary.
    if (!$filter_format || !($filters = $filter_format->filters())) {
      return '';
    }
  }

  // If we have a short body, the entire body is the summary.
  if (mb_strlen($text) <= $size) {
    return $text;
  }

  // If the delimiter has not been specified, try to split at paragraph or
  // sentence boundaries.

  // The summary may not be longer than maximum length specified. Initial slice.
  $summary = Unicode::truncate($text, $size);

  // Store the actual length of the UTF8 string -- which might not be the same
  // as $size.
  $max_right_pos = strlen($summary);

  // How much to cut off the end of the summary so that it doesn't end in the
  // middle of a paragraph, sentence, or word.
  // Initialize it to maximum in order to find the minimum.
  $min_right_pos = $max_right_pos;

  // Store the reverse of the summary. We use strpos on the reversed needle and
  // haystack for speed and convenience.
  $reversed = strrev($summary);

  // Build an array of arrays of break points grouped by preference.
  $break_points = [];

  // A paragraph near the end of sliced summary is most preferable.
  $break_points[] = ['</p>' => 0];

  // If no complete paragraph then treat line breaks as paragraphs.
  $line_breaks = ['<br />' => 6, '<br>' => 4];
  // Newline only indicates a line break if line break converter
  // filter is present.
  if (isset($format) && $filters->has('filter_autop') && $filters->get('filter_autop')->status) {
    $line_breaks["\n"] = 1;
  }
  $break_points[] = $line_breaks;

  // If the first paragraph is too long, split at the end of a sentence.
  $break_points[] = ['. ' => 1, '! ' => 1, '? ' => 1, '。' => 0, '؟ ' => 1];

  // Iterate over the groups of break points until a break point is found.
  foreach ($break_points as $points) {
    // Look for each break point, starting at the end of the summary.
    foreach ($points as $point => $offset) {
      // The summary is already reversed, but the break point isn't.
      $right_pos = strpos($reversed, strrev($point));
      if ($right_pos !== FALSE) {
        $min_right_pos = min($right_pos + $offset, $min_right_pos);
      }
    }

    // If a break point was found in this group, slice and stop searching.
    if ($min_right_pos !== $max_right_pos) {
      // Don't slice with length 0. Length must be <0 to slice from RHS.
      $summary = ($min_right_pos === 0) ? $summary : substr($summary, 0, 0 - $min_right_pos);
      break;
    }
  }

  // If filter_html or filter_htmlcorrector is enabled, normalize the output.
  if (isset($format)) {
    $filter_enabled = function (string $filter) use ($filters) : bool {
      return $filters->has($filter) && $filters->get($filter)->status;
    };
    if ($filter_enabled('filter_html') || $filter_enabled('filter_htmlcorrector')) {
      $summary = Html::normalize($summary);
    }
  }

  return $summary;
  return \Drupal::service(TextSummary::class)->generate($text, $format, (int) $size);
}
Loading