Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
108 / 108 |
|
100.00% |
17 / 17 |
CRAP | |
100.00% |
1 / 1 |
| NameFormatterService | |
100.00% |
108 / 108 |
|
100.00% |
17 / 17 |
36 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
| setSetting | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| getSetting | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| format | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
| formatList | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
| formatNameItems | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| shouldUseEtAl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| limitItemsForEtAl | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
| formatEtAlList | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
3 | |||
| formatConjunctionList | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
| delimiterPrecedesLast | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
3 | |||
| getNameFormatString | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| getListSettings | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| getLastDelimitorTypes | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| getLastDelimiterTypes | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
| getLastDelimitorBehaviors | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| getLastDelimiterBehaviors | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Drupal\name\Service; |
| 6 | |
| 7 | use Drupal\Component\Render\FormattableMarkup; |
| 8 | use Drupal\Component\Render\MarkupInterface; |
| 9 | use Drupal\Component\Utility\Html; |
| 10 | use Drupal\Core\Config\ConfigFactoryInterface; |
| 11 | use Drupal\Core\Entity\EntityTypeManagerInterface; |
| 12 | use Drupal\Core\Language\LanguageManagerInterface; |
| 13 | use Drupal\Core\StringTranslation\StringTranslationTrait; |
| 14 | use Drupal\Core\StringTranslation\TranslationInterface; |
| 15 | use Drupal\name\Render\NameListFormattableMarkup; |
| 16 | |
| 17 | /** |
| 18 | * Primary name formatter for an array of name components. |
| 19 | * |
| 20 | * This service should be used for any name formatting requests and direct |
| 21 | * calls to the "name.format_parser" service should be avoided. |
| 22 | * |
| 23 | * Usage: |
| 24 | * \Drupal::service('name.formatter')->format(). |
| 25 | */ |
| 26 | class NameFormatterService implements NameFormatterInterface { |
| 27 | |
| 28 | use StringTranslationTrait; |
| 29 | |
| 30 | /** |
| 31 | * The name format parser. |
| 32 | */ |
| 33 | protected NameFormatParserInterface $parser; |
| 34 | |
| 35 | /** |
| 36 | * The name format storage. |
| 37 | * |
| 38 | * @var \Drupal\Core\Entity\EntityStorageInterface |
| 39 | */ |
| 40 | protected $nameFormatStorage; |
| 41 | |
| 42 | /** |
| 43 | * The name list format storage. |
| 44 | * |
| 45 | * @var \Drupal\Core\Entity\EntityStorageInterface |
| 46 | */ |
| 47 | protected $listFormatStorage; |
| 48 | |
| 49 | /** |
| 50 | * Language manager for retrieving the default language code if needed. |
| 51 | * |
| 52 | * @var \Drupal\Core\Language\LanguageManagerInterface |
| 53 | */ |
| 54 | protected $languageManager; |
| 55 | |
| 56 | /** |
| 57 | * The factory for configuration objects. |
| 58 | * |
| 59 | * @var \Drupal\Core\Config\ConfigFactoryInterface |
| 60 | */ |
| 61 | protected $configFactory; |
| 62 | |
| 63 | /** |
| 64 | * Settings for the formatter. |
| 65 | * |
| 66 | * Values include: |
| 67 | * - sep1: First defined separator. |
| 68 | * - sep2: Second defined separator. |
| 69 | * - sep3: Third defined separator. |
| 70 | * - markup: To markup the individual components. |
| 71 | * |
| 72 | * @var array |
| 73 | */ |
| 74 | protected $settings = [ |
| 75 | 'sep1' => ' ', |
| 76 | 'sep2' => ', ', |
| 77 | 'sep3' => '', |
| 78 | 'markup' => 'none', |
| 79 | ]; |
| 80 | |
| 81 | /** |
| 82 | * Constructs a name formatter object. |
| 83 | * |
| 84 | * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager |
| 85 | * The entity manager. |
| 86 | * @param \Drupal\name\Service\NameFormatParserInterface $parser |
| 87 | * The name format parser. |
| 88 | * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager |
| 89 | * The language manager. |
| 90 | * @param \Drupal\Core\StringTranslation\TranslationInterface $translation |
| 91 | * The string translation. |
| 92 | * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory |
| 93 | * The factory for configuration objects. |
| 94 | */ |
| 95 | public function __construct(EntityTypeManagerInterface $entityTypeManager, NameFormatParserInterface $parser, LanguageManagerInterface $language_manager, TranslationInterface $translation, ConfigFactoryInterface $config_factory) { |
| 96 | $this->nameFormatStorage = $entityTypeManager->getStorage('name_format'); |
| 97 | $this->listFormatStorage = $entityTypeManager->getStorage('name_list_format'); |
| 98 | $this->parser = $parser; |
| 99 | $this->languageManager = $language_manager; |
| 100 | $this->stringTranslation = $translation; |
| 101 | $this->configFactory = $config_factory; |
| 102 | $config = $this->configFactory->get('name.settings'); |
| 103 | $this->settings['sep1'] = $config->get('sep1'); |
| 104 | $this->settings['sep2'] = $config->get('sep2'); |
| 105 | $this->settings['sep3'] = $config->get('sep3'); |
| 106 | } |
| 107 | |
| 108 | /** |
| 109 | * {@inheritdoc} |
| 110 | */ |
| 111 | public function setSetting($key, $value) { |
| 112 | $this->settings[$key] = $value; |
| 113 | return $this; |
| 114 | } |
| 115 | |
| 116 | /** |
| 117 | * {@inheritdoc} |
| 118 | */ |
| 119 | public function getSetting($key) { |
| 120 | return $this->settings[$key] ?? NULL; |
| 121 | } |
| 122 | |
| 123 | /** |
| 124 | * {@inheritdoc} |
| 125 | */ |
| 126 | public function format(array $components, $type = 'default', $langcode = NULL) { |
| 127 | $format_string = $this->getNameFormatString($type); |
| 128 | $name = $this->parser->parse($components, $format_string, $this->settings); |
| 129 | if (!empty($components['url'])) { |
| 130 | $safe_name = $name instanceof MarkupInterface ? $name : Html::escape((string) $name); |
| 131 | $name = new FormattableMarkup('<a href=":link">@name</a>', [ |
| 132 | ':link' => $components['url']->toString(), |
| 133 | '@name' => $safe_name, |
| 134 | ]); |
| 135 | } |
| 136 | |
| 137 | return $name; |
| 138 | } |
| 139 | |
| 140 | /** |
| 141 | * {@inheritdoc} |
| 142 | */ |
| 143 | public function formatList(array $items, $type = 'default', $list_type = 'default', $langcode = NULL) { |
| 144 | $name_count = count($items); |
| 145 | |
| 146 | // Avoid any computations if none or one names only. |
| 147 | if (!$name_count) { |
| 148 | return ''; |
| 149 | } |
| 150 | if ($name_count == 1) { |
| 151 | $item = reset($items); |
| 152 | return $this->format($item, $type, $langcode); |
| 153 | } |
| 154 | |
| 155 | $settings = $this->getListSettings($list_type); |
| 156 | $use_et_al = $this->shouldUseEtAl($settings, $name_count); |
| 157 | $items_to_format = $this->limitItemsForEtAl($items, $settings, $name_count); |
| 158 | $names = $this->formatNameItems($items_to_format, $type, $langcode); |
| 159 | |
| 160 | if ($use_et_al) { |
| 161 | return $this->formatEtAlList($names, $settings); |
| 162 | } |
| 163 | |
| 164 | return $this->formatConjunctionList($names, $settings, $name_count); |
| 165 | } |
| 166 | |
| 167 | /** |
| 168 | * Formats a list of item component arrays using the selected name format. |
| 169 | * |
| 170 | * @param array<int, array<string, mixed>> $items |
| 171 | * Name component arrays. |
| 172 | * @param string $type |
| 173 | * Name format entity id. |
| 174 | * @param string|null $langcode |
| 175 | * Language code or NULL for UI language. |
| 176 | * |
| 177 | * @return array<int, \Drupal\Component\Render\MarkupInterface|string> |
| 178 | * A list of already formatted names. |
| 179 | */ |
| 180 | protected function formatNameItems(array $items, string $type, ?string $langcode): array { |
| 181 | $names = []; |
| 182 | foreach ($items as $item) { |
| 183 | $names[] = $this->format($item, $type, $langcode); |
| 184 | } |
| 185 | |
| 186 | return $names; |
| 187 | } |
| 188 | |
| 189 | /** |
| 190 | * Determines whether list output should switch to et al style. |
| 191 | * |
| 192 | * @param array<string, mixed> $settings |
| 193 | * Name list format settings. |
| 194 | * @param int $name_count |
| 195 | * Total incoming number of names. |
| 196 | */ |
| 197 | protected function shouldUseEtAl(array $settings, int $name_count): bool { |
| 198 | return $name_count > $settings['el_al_min']; |
| 199 | } |
| 200 | |
| 201 | /** |
| 202 | * Removes names that don't need to be formatted for et al output. |
| 203 | * |
| 204 | * @param array<int, array<string, mixed>> $items |
| 205 | * Name component arrays. |
| 206 | * @param array<string, mixed> $settings |
| 207 | * Name list format settings. |
| 208 | * @param int $name_count |
| 209 | * Total incoming number of names. |
| 210 | * |
| 211 | * @return array<int, array<string, mixed>> |
| 212 | * The original list or a list reduced to the configured first names. |
| 213 | */ |
| 214 | protected function limitItemsForEtAl(array $items, array $settings, int $name_count): array { |
| 215 | $should_limit = ($settings['el_al_min'] && $name_count > $settings['el_al_min']); |
| 216 | if ($should_limit) { |
| 217 | return array_slice($items, 0, $settings['el_al_first']); |
| 218 | } |
| 219 | |
| 220 | return $items; |
| 221 | } |
| 222 | |
| 223 | /** |
| 224 | * Formats a pre-formatted name list as an et al output. |
| 225 | * |
| 226 | * @param array<int, \Drupal\Component\Render\MarkupInterface|string> $names |
| 227 | * A list of already formatted names. |
| 228 | * @param array<string, mixed> $settings |
| 229 | * Name list format settings. |
| 230 | */ |
| 231 | protected function formatEtAlList(array $names, array $settings): MarkupInterface { |
| 232 | $etal = $this->t('et al', [], ['context' => 'name']); |
| 233 | if ($this->settings['markup'] !== 'none') { |
| 234 | $etal = new FormattableMarkup('<em>@etal</em>', ['@etal' => $etal]); |
| 235 | } |
| 236 | |
| 237 | if (count($names) == 1) { |
| 238 | return $this->t('@name@delimiter @etal', [ |
| 239 | '@name' => reset($names), |
| 240 | '@delimiter' => trim($settings['delimiter']), |
| 241 | '@etal' => $etal, |
| 242 | ]); |
| 243 | } |
| 244 | |
| 245 | $names = new NameListFormattableMarkup($names, $settings['delimiter']); |
| 246 | return $this->t('@names@delimiter @etal', [ |
| 247 | '@names' => $names, |
| 248 | '@delimiter' => trim($settings['delimiter']), |
| 249 | '@etal' => $etal, |
| 250 | ]); |
| 251 | } |
| 252 | |
| 253 | /** |
| 254 | * Formats a pre-formatted list with conjunction rules. |
| 255 | * |
| 256 | * @param array<int, \Drupal\Component\Render\MarkupInterface|string> $names |
| 257 | * A list of already formatted names. |
| 258 | * @param array<string, mixed> $settings |
| 259 | * Name list format settings. |
| 260 | * @param int $name_count |
| 261 | * Total incoming number of names. |
| 262 | */ |
| 263 | protected function formatConjunctionList(array $names, array $settings, int $name_count): MarkupInterface { |
| 264 | if ($settings['and'] == 'inherit') { |
| 265 | return new NameListFormattableMarkup($names, $settings['delimiter']); |
| 266 | } |
| 267 | |
| 268 | $t_args = [ |
| 269 | '@lastname' => array_pop($names), |
| 270 | '@names' => new NameListFormattableMarkup($names, $settings['delimiter']), |
| 271 | '@delimiter' => trim($settings['delimiter']), |
| 272 | ]; |
| 273 | $t_args['@and'] = $settings['and'] == 'text' |
| 274 | ? $this->t('and', [], ['context' => 'name']) |
| 275 | : $this->t('&', [], ['context' => 'name']); |
| 276 | |
| 277 | // Strange rule from citationstyles.org. |
| 278 | // @see http://citationstyles.org/downloads/specification.html |
| 279 | if ($this->delimiterPrecedesLast($settings, $name_count)) { |
| 280 | return $this->t('@names@delimiter @and @lastname', $t_args); |
| 281 | } |
| 282 | |
| 283 | return $this->t('@names @and @lastname', $t_args); |
| 284 | } |
| 285 | |
| 286 | /** |
| 287 | * Determines whether to include the delimiter before the final conjunction. |
| 288 | * |
| 289 | * @param array<string, mixed> $settings |
| 290 | * Name list format settings. |
| 291 | * @param int $name_count |
| 292 | * Total incoming number of names. |
| 293 | */ |
| 294 | protected function delimiterPrecedesLast(array $settings, int $name_count): bool { |
| 295 | return ($settings['delimiter_precedes_last'] == 'contextual' && $name_count > 2) |
| 296 | || $settings['delimiter_precedes_last'] == 'always'; |
| 297 | } |
| 298 | |
| 299 | /** |
| 300 | * Helper function to get the format pattern. |
| 301 | * |
| 302 | * @param string $format |
| 303 | * The ID of the preferred format to use. This will fallback to the default |
| 304 | * format if the format can not be loaded. |
| 305 | * |
| 306 | * @return string |
| 307 | * The pattern to parse. |
| 308 | */ |
| 309 | protected function getNameFormatString($format) { |
| 310 | $config = $this->nameFormatStorage->load($format); |
| 311 | if (!$config) { |
| 312 | $config = $this->nameFormatStorage->load('default'); |
| 313 | } |
| 314 | return $config->get('pattern'); |
| 315 | } |
| 316 | |
| 317 | /** |
| 318 | * Helper function to load and get the format list settings. |
| 319 | * |
| 320 | * @param string $format |
| 321 | * The ID of the preferred format to use. This will fallback to the default |
| 322 | * format if the format can not be loaded. |
| 323 | * |
| 324 | * @return array |
| 325 | * The settings to use to format the list. |
| 326 | */ |
| 327 | protected function getListSettings($format) { |
| 328 | /** @var \Drupal\name\Entity\NameListFormat $list_format */ |
| 329 | $list_format = $this->listFormatStorage->load($format); |
| 330 | if (!$list_format) { |
| 331 | $list_format = $this->listFormatStorage->load('default'); |
| 332 | } |
| 333 | return $list_format->listSettings(); |
| 334 | } |
| 335 | |
| 336 | /** |
| 337 | * {@inheritdoc} |
| 338 | */ |
| 339 | public function getLastDelimitorTypes($include_examples = TRUE) { |
| 340 | // cspell:ignore delimitor |
| 341 | @trigger_error('getLastDelimitorTypes() is deprecated in name:8.x-1.1 and is removed from name:2.0.0. use getLastDelimiterTypes(). See https://www.drupal.org/project/name/issues/3518599', E_USER_DEPRECATED); |
| 342 | return $this->getLastDelimiterTypes($include_examples); |
| 343 | } |
| 344 | |
| 345 | /** |
| 346 | * {@inheritdoc} |
| 347 | */ |
| 348 | public function getLastDelimiterTypes($include_examples = TRUE) { |
| 349 | if (!$include_examples) { |
| 350 | return [ |
| 351 | 'text' => $this->t('Textual'), |
| 352 | 'symbol' => $this->t('Ampersand'), |
| 353 | 'inherit' => $this->t('Inherit delimiter'), |
| 354 | ]; |
| 355 | } |
| 356 | |
| 357 | return [ |
| 358 | 'text' => $this->t('Textual (and)'), |
| 359 | 'symbol' => $this->t('Ampersand (&)'), |
| 360 | 'inherit' => $this->t('Inherit delimiter'), |
| 361 | ]; |
| 362 | } |
| 363 | |
| 364 | /** |
| 365 | * {@inheritdoc} |
| 366 | */ |
| 367 | public function getLastDelimitorBehaviors($include_examples = TRUE) { |
| 368 | // cspell:ignore delimitor |
| 369 | @trigger_error('getLastDelimitorBehaviors() is deprecated in name:8.x-1.1 and is removed from name:2.0.0. use getLastDelimiterBehaviors(). See https://www.drupal.org/project/name/issues/3518599', E_USER_DEPRECATED); |
| 370 | return $this->getLastDelimiterBehaviors($include_examples); |
| 371 | } |
| 372 | |
| 373 | /** |
| 374 | * {@inheritdoc} |
| 375 | */ |
| 376 | public function getLastDelimiterBehaviors($include_examples = TRUE) { |
| 377 | if (!$include_examples) { |
| 378 | return [ |
| 379 | 'never' => $this->t('Never'), |
| 380 | 'always' => $this->t('Always'), |
| 381 | 'contextual' => $this->t('Contextual'), |
| 382 | ]; |
| 383 | } |
| 384 | |
| 385 | return [ |
| 386 | 'never' => $this->t('Never (i.e. "J. Doe and T. Williams")'), |
| 387 | 'always' => $this->t('Always (i.e. "J. Doe<strong>,</strong> and T. Williams")'), |
| 388 | 'contextual' => $this->t('Contextual (i.e. "J. Doe and T. Williams" <em>or</em> "J. Doe, S. Smith<strong>,</strong> and T. Williams")'), |
| 389 | ]; |
| 390 | } |
| 391 | |
| 392 | } |