Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
118 / 118
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
NameFormatTokens
100.00% covered (success)
100.00%
118 / 118
100.00% covered (success)
100.00%
10 / 10
33
100.00% covered (success)
100.00%
1 / 1
 build
100.00% covered (success)
100.00%
81 / 81
100.00% covered (success)
100.00%
1 / 1
9
 resolveValue
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 renderComponent
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 applyComponentModifier
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 formatWithMarkup
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 wrapSimpleSpan
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 wrapMicrodataSpan
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 wrapRdfaSpan
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 schemaAttribute
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 renderFirstComponent
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\name\Utility;
6
7use Drupal\Component\Utility\Html;
8
9/**
10 * Builds the token map used by NameFormatLexer from name components.
11 *
12 * All rendering decisions (markup mode, separators) are passed in as plain
13 * parameters; this class carries no instance state.
14 *
15 * @internal
16 */
17final class NameFormatTokens {
18
19  /**
20   * Schema.org property mapping for component keys.
21   *
22   * Note that credentials intentionally use "credential" here, so component
23   * key "credentials" does not get a schema attribute.
24   *
25   * @var array<string, string>
26   */
27  private const SCHEMA_PROPERTY_MAP = [
28    'title'       => 'honorificPrefix',
29    'given'       => 'givenName',
30    'middle'      => 'additionalName',
31    'family'      => 'familyName',
32    'credential'  => 'honorificSuffix',
33    'alternative' => 'alternateName',
34  ];
35
36  /**
37   * Builds the full token map from an array of name components.
38   *
39   * The returned array is keyed by single-character token letters.
40   * Conditional tokens (d, D, e, E) are populated only when the required
41   * source components are non-empty; they fall back to NULL so that
42   * NameFormatParser::resolveTokenValue() converts them to empty strings.
43   *
44   * @param array $name_components
45   *   Keyed array of raw name component values.
46   * @param string $sep1
47   *   First separator (token i).
48   * @param string $sep2
49   *   Second separator (token j).
50   * @param string $sep3
51   *   Third separator (token k).
52   * @param string $markup
53   *   Markup mode (none, simple, microdata, rdfa).
54   *
55   * @return array<string, string|null>
56   *   Token map keyed by format letter.
57   */
58  public static function build(
59    array $name_components,
60    string $sep1,
61    string $sep2,
62    string $sep3,
63    string $markup,
64  ): array {
65    $name_components = (array) $name_components;
66    $name_components += [
67      'title'        => '',
68      'given'        => '',
69      'middle'       => '',
70      'family'       => '',
71      'credentials'  => '',
72      'generational' => '',
73      'preferred'    => '',
74      'alternative'  => '',
75    ];
76
77    $tokens = [
78      't' => self::renderComponent($name_components['title'], 'title', $markup),
79      'g' => self::renderComponent($name_components['given'], 'given', $markup),
80      'p' => self::renderFirstComponent(
81        [$name_components['preferred'], $name_components['given']],
82        'given',
83        $markup,
84      ),
85      'q' => self::renderComponent($name_components['preferred'], 'preferred', $markup),
86      'm' => self::renderComponent($name_components['middle'], 'middle', $markup),
87      'f' => self::renderComponent($name_components['family'], 'family', $markup),
88      'c' => self::renderComponent($name_components['credentials'], 'credentials', $markup),
89      'a' => self::renderComponent($name_components['alternative'], 'alternative', $markup),
90      's' => self::renderComponent($name_components['generational'], 'generational', $markup),
91      'v' => self::renderComponent($name_components['preferred'], 'preferred', $markup, 'initial'),
92      'w' => self::renderFirstComponent(
93        [$name_components['preferred'], $name_components['given']],
94        'given',
95        $markup,
96        'initial',
97      ),
98      'x' => self::renderComponent($name_components['given'], 'given', $markup, 'initial'),
99      'y' => self::renderComponent($name_components['middle'], 'middle', $markup, 'initial'),
100      'z' => self::renderComponent($name_components['family'], 'family', $markup, 'initial'),
101      'A' => self::renderComponent($name_components['alternative'], 'alternative', $markup, 'initial'),
102      'I' => self::renderComponent(
103        $name_components['given'] . ' ' . $name_components['family'],
104        'initials',
105        $markup,
106        'initials',
107      ),
108      'J' => self::renderComponent(
109        $name_components['given'] . ' ' . $name_components['middle'] . ' ' . $name_components['family'],
110        'initials',
111        $markup,
112        'initials',
113      ),
114      'K' => self::renderComponent($name_components['given'], 'initials', $markup, 'initials'),
115      'M' => self::renderComponent(
116        $name_components['given'] . ' ' . $name_components['middle'],
117        'initials',
118        $markup,
119        'initials',
120      ),
121      'i' => $sep1,
122      'j' => $sep2,
123      'k' => $sep3,
124    ];
125
126    $preferred = $tokens['p'];
127    $given     = $tokens['g'];
128    $family    = $tokens['f'];
129
130    $has_preferred_or_family = ($preferred || $family);
131    if ($has_preferred_or_family) {
132      $tokens += [
133        'd' => $preferred ? $preferred : $family,
134        'D' => $family ? $family : $preferred,
135      ];
136    }
137    $has_given_or_family = ($given || $family);
138    if ($has_given_or_family) {
139      $tokens += [
140        'e' => $given ? $given : $family,
141        'E' => $family ? $family : $given,
142      ];
143    }
144
145    // Ensure d, D, e, E are always present so resolveValue() converts
146    // them to empty strings rather than treating them as literal characters.
147    $tokens += [
148      'd' => NULL,
149      'D' => NULL,
150      'e' => NULL,
151      'E' => NULL,
152    ];
153
154    return $tokens;
155  }
156
157  /**
158   * Resolves a single format character to its token value.
159   *
160   * Returns the token's string value when a matching entry exists in the token
161   * map. Non-string token values (e.g. NULL) resolve to an empty string.
162   * Characters with no matching token are returned as-is (literal output).
163   *
164   * @param string $char
165   *   A single format character.
166   * @param array $tokens
167   *   The token map.
168   *
169   * @return string
170   *   The resolved value or the original character.
171   */
172  public static function resolveValue(string $char, array $tokens): string {
173    if (array_key_exists($char, $tokens)) {
174      return is_string($tokens[$char]) ? $tokens[$char] : '';
175    }
176
177    return $char;
178  }
179
180  /**
181   * Renders a single name component value.
182   *
183   * Returns NULL for empty or zero-length values. When markup mode is active,
184   * the value is HTML-escaped and wrapped in a <span> element carrying the
185   * component class and, for microdata/rdfa, the appropriate schema attribute.
186   *
187   * Note: the schema.org map uses the key 'credential' (singular) while the
188   * component array uses 'credentials' (plural), so the credentials component
189   * intentionally omits the itemprop/property attribute.
190   *
191   * @param string|null $value
192   *   The raw component value.
193   * @param string $component_key
194   *   The component machine name (e.g. 'given', 'family').
195   * @param string $markup
196   *   Markup mode: none, simple, microdata, or rdfa.
197   * @param string|null $modifier
198   *   Modifier key: 'initial' for first-letter, 'initials' for all-word
199   *   initials, or NULL for no modifier.
200   *
201   * @return string|null
202   *   The rendered string, or NULL when the value is empty.
203   */
204  public static function renderComponent(
205    ?string $value,
206    string $component_key,
207    string $markup,
208    ?string $modifier = NULL,
209  ): ?string {
210    $value_is_empty = (empty($value) || !mb_strlen($value));
211    if ($value_is_empty) {
212      return NULL;
213    }
214
215    $value = self::applyComponentModifier($value, $modifier);
216    return self::formatWithMarkup($value, $component_key, $markup);
217  }
218
219  /**
220   * Applies initial/initials transforms for a component value.
221   */
222  private static function applyComponentModifier(
223    string $value,
224    ?string $modifier,
225  ): string {
226    return match ($modifier) {
227      'initial' => mb_substr($value, 0, 1),
228      'initials' => UnicodeExtras::initials($value),
229      default => $value,
230    };
231  }
232
233  /**
234   * Formats a component value according to the configured markup mode.
235   */
236  private static function formatWithMarkup(
237    string $value,
238    string $component_key,
239    string $markup,
240  ): string {
241    return match ($markup) {
242      'simple' => self::wrapSimpleSpan($component_key, $value),
243      'microdata' => self::wrapMicrodataSpan($component_key, $value),
244      'rdfa' => self::wrapRdfaSpan($component_key, $value),
245      default => $value,
246    };
247  }
248
249  /**
250   * Wraps a value in a simple classed span.
251   */
252  private static function wrapSimpleSpan(
253    string $component_key,
254    string $value,
255  ): string {
256    return '<span class="' . Html::escape($component_key) . '">'
257      . Html::escape($value) . '</span>';
258  }
259
260  /**
261   * Wraps a value in a microdata span.
262   */
263  private static function wrapMicrodataSpan(
264    string $component_key,
265    string $value,
266  ): string {
267    $itemprop = self::schemaAttribute($component_key, 'itemprop');
268    return '<span class="' . Html::escape($component_key) . '"'
269      . $itemprop . '>' . Html::escape($value) . '</span>';
270  }
271
272  /**
273   * Wraps a value in an RDFa span.
274   */
275  private static function wrapRdfaSpan(
276    string $component_key,
277    string $value,
278  ): string {
279    $property = self::schemaAttribute($component_key, 'property', 'schema:');
280    return '<span class="' . Html::escape($component_key) . '"'
281      . $property . '>' . Html::escape($value) . '</span>';
282  }
283
284  /**
285   * Returns a schema attribute string for the given component key.
286   */
287  private static function schemaAttribute(
288    string $component_key,
289    string $attribute_name,
290    string $prefix = '',
291  ): string {
292    if (!isset(self::SCHEMA_PROPERTY_MAP[$component_key])) {
293      return '';
294    }
295
296    return ' ' . $attribute_name . '="' . $prefix
297      . self::SCHEMA_PROPERTY_MAP[$component_key] . '"';
298  }
299
300  /**
301   * Returns the rendered output of the first non-empty value in the list.
302   *
303   * @param array $values
304   *   Candidate values in priority order.
305   * @param string $component_key
306   *   The component context used for markup wrapping.
307   * @param string $markup
308   *   Markup mode: none, simple, microdata, or rdfa.
309   * @param string|null $modifier
310   *   Optional modifier passed through to renderComponent().
311   *
312   * @return string|null
313   *   The first non-empty rendered value, or NULL when all are empty.
314   */
315  public static function renderFirstComponent(
316    array $values,
317    string $component_key,
318    string $markup,
319    ?string $modifier = NULL,
320  ): ?string {
321    foreach ($values as $value) {
322      $output = self::renderComponent($value, $component_key, $markup, $modifier);
323      $has_output = (isset($output) && strlen($output));
324      if ($has_output) {
325        return $output;
326      }
327    }
328
329    return NULL;
330  }
331
332}