Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
NameFormatAssembler
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
8 / 8
25
100.00% covered (success)
100.00%
1 / 1
 assemble
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 pieceConditionMet
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 conditionMet
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
7
 plusConditionMet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 minusConditionMet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tildeConditionMet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 caretConditionMet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 equalsConditionMet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\name\Utility;
6
7/**
8 * Assembles condition-tagged format pieces into a final string.
9 *
10 * Each piece is an associative array with two keys:
11 *   - 'value' (string): the resolved token value after modifiers.
12 *   - 'conditions' (string): accumulated condition characters for the piece.
13 *
14 * Conditions determine whether a piece is included based on its neighbors:
15 *   + - Include if both surrounding pieces are non-empty.
16 *   - - Include if the preceding piece is non-empty.
17 *   ~ - Include if the preceding piece is empty.
18 *   = - Include if the following piece is non-empty.
19 *   ^ - Include if the following piece is empty.
20 *   | - Use the preceding piece unless empty; otherwise use this piece.
21 *
22 * @internal
23 */
24final class NameFormatAssembler {
25
26  /**
27   * Applies conditional inclusion rules to a list of pieces.
28   *
29   * Any literal double-backslashes (\\) remaining in assembled piece values
30   * after tokenization are converted to tab characters to finalize
31   * backslash-escape handling.
32   *
33   * @param array[] $pieces
34   *   Each piece is an array with 'value' and 'conditions' keys.
35   *
36   * @return string
37   *   The assembled output string.
38   */
39  public static function assemble(array $pieces): string {
40    $count         = count($pieces);
41    $parsed_pieces = [];
42
43    foreach ($pieces as $i => $piece) {
44      $component  = $piece['value'];
45      $conditions = $piece['conditions'];
46
47      $last_component = ($i > 0) ? $pieces[$i - 1]['value'] : FALSE;
48      $next_component = ($i < $count - 1) ? $pieces[$i + 1]['value'] : FALSE;
49
50      if (empty($conditions)) {
51        $parsed_pieces[$i] = $component;
52        continue;
53      }
54
55      if (self::pieceConditionMet($conditions, $last_component, $next_component)) {
56        $parsed_pieces[$i] = $component;
57      }
58
59      // Fallback conditional overrides all other conditionals.
60      if (str_contains($conditions, '|')) {
61        if (!empty($last_component)) {
62          unset($parsed_pieces[$i]);
63          continue;
64        }
65        $parsed_pieces[$i] = $component;
66      }
67    }
68
69    return str_replace('\\\\', "\t", implode('', $parsed_pieces));
70  }
71
72  /**
73   * Returns TRUE when the piece's conditions are satisfied by its neighbors.
74   *
75   * @param string $conditions
76   *   Accumulated condition characters for the piece.
77   * @param string|false $last_component
78   *   The preceding piece value, or FALSE when there is none.
79   * @param string|false $next_component
80   *   The following piece value, or FALSE when there is none.
81   *
82   * @return bool
83   *   TRUE when the conditions match the surrounding pieces.
84   */
85  public static function pieceConditionMet(
86    string $conditions,
87    string|false $last_component,
88    string|false $next_component,
89  ): bool {
90    foreach (['+', '-', '~', '^', '='] as $condition) {
91      $condition_is_met = (
92        str_contains($conditions, $condition)
93        && self::conditionMet($condition, $last_component, $next_component)
94      );
95      if ($condition_is_met) {
96        return TRUE;
97      }
98    }
99
100    return FALSE;
101  }
102
103  /**
104   * Evaluates one condition character against neighboring pieces.
105   */
106  private static function conditionMet(
107    string $condition,
108    string|false $last_component,
109    string|false $next_component,
110  ): bool {
111    return match ($condition) {
112      '+' => self::plusConditionMet($last_component, $next_component),
113      '-' => self::minusConditionMet($last_component),
114      '~' => self::tildeConditionMet($last_component),
115      '^' => self::caretConditionMet($next_component),
116      '=' => self::equalsConditionMet($next_component),
117      default => FALSE,
118    };
119  }
120
121  /**
122   * Returns TRUE when both neighboring pieces are non-empty.
123   */
124  private static function plusConditionMet(
125    string|false $last_component,
126    string|false $next_component,
127  ): bool {
128    return !empty($last_component) && !empty($next_component);
129  }
130
131  /**
132   * Returns TRUE when the previous piece is non-empty.
133   */
134  private static function minusConditionMet(
135    string|false $last_component,
136  ): bool {
137    return !empty($last_component);
138  }
139
140  /**
141   * Returns TRUE when the previous piece is empty.
142   */
143  private static function tildeConditionMet(
144    string|false $last_component,
145  ): bool {
146    return empty($last_component);
147  }
148
149  /**
150   * Returns TRUE when the next piece is empty.
151   */
152  private static function caretConditionMet(
153    string|false $next_component,
154  ): bool {
155    return empty($next_component);
156  }
157
158  /**
159   * Returns TRUE when the next piece is non-empty.
160   */
161  private static function equalsConditionMet(
162    string|false $next_component,
163  ): bool {
164    return !empty($next_component);
165  }
166
167}