You are here

class NameFormatParser in Name Field 8

Converts a name from an array of components into a defined format.

Hierarchy

Expanded class hierarchy of NameFormatParser

3 files declare their use of NameFormatParser
NameFormatForm.php in src/Form/NameFormatForm.php
NameFormatParserTest.php in tests/src/Unit/NameFormatParserTest.php
NameFormatter.php in src/Plugin/Field/FieldFormatter/NameFormatter.php
1 string reference to 'NameFormatParser'
name.services.yml in ./name.services.yml
name.services.yml
1 service uses NameFormatParser
name.format_parser in ./name.services.yml
Drupal\name\NameFormatParser

File

src/NameFormatParser.php, line 14

Namespace

Drupal\name
View source
class NameFormatParser {
  use StringTranslationTrait;

  /**
   * Markup style for decorating name components.
   *
   * @var string
   */
  protected $markup = 'none';

  /**
   * First separator.
   *
   * @var string
   */
  protected $sep1 = ' ';

  /**
   * Second separator.
   *
   * @var string
   */
  protected $sep2 = ', ';

  /**
   * Third separator.
   *
   * @var string
   */
  protected $sep3 = '';

  /**
   * Used to seperate words using the "b" and "B" modifiers.
   *
   * @var string
   */
  protected $boundaryRegExp = '/[\\b,\\s]/';

  /**
   * Parses a name component array into the given format.
   *
   * @param array $name_components
   *   Keyed array of name components.
   * @param string $format
   *   The name format pattern to generate the name.
   * @param array $settings
   *   Additional settings to control the parser.
   *   - sep1 (string): first separator.
   *   - sep2 (string): second separator.
   *   - sep3 (string): third separator.
   *   - markup (string): key of the markup type.
   *   - boundary (string): regexp for word boundary.
   *
   * @return \Drupal\Component\Render\MarkupInterface
   *   A renderable object representing the name.
   */
  public function parse(array $name_components, $format = '', array $settings = []) {
    foreach ([
      'sep1',
      'sep2',
      'sep3',
    ] as $sep_key) {
      if (isset($settings[$sep_key])) {
        $this->{$sep_key} = (string) $settings[$sep_key];
      }
    }
    $this->markup = !empty($settings['markup']) ? $settings['markup'] : 'none';
    if (isset($settings['boundary']) && strlen($settings['boundary'])) {
      $this->boundary = $settings['boundary'];
    }
    $name_string = $this
      ->format($name_components, $format);
    switch ($this->markup) {

      // Component values are already escaped.
      case 'simple':
      case 'rdfa':
      case 'microdata':
        return new FormattableMarkup($name_string, []);

      // Unescaped text.
      case 'raw':
        return $name_string;

      // Raw component values.
      case 'none':
      default:
        return new HtmlEscapedText($name_string);
    }
  }

  /**
   * Formats an array of name components into the supplied format.
   *
   * @param array $name_components
   *   A keyed array of the components.
   * @param string $format
   *   The name format string or segment to parse.
   * @param array $tokens
   *   The generated tokens.
   *
   * @return string
   *   The formatted string.
   */
  protected function format(array $name_components, $format = '', array $tokens = NULL) {
    if (empty($format)) {
      return '';
    }
    if (!isset($tokens)) {
      $tokens = $this
        ->generateTokens($name_components);
    }

    // Neutralise any escaped backslashes.
    $format = str_replace('\\\\', "\t", $format);
    $pieces = [];
    $modifiers = '';
    $conditions = '';
    for ($i = 0; $i < strlen($format); $i++) {
      $char = $format[$i];
      $last_char = $i > 0 ? $format[$i - 1] : FALSE;

      // Handle escaped letters.
      if ($char == '\\') {
        continue;
      }
      if ($last_char == '\\') {
        $pieces[] = $this
          ->addComponent($char, $modifiers, $conditions);
        continue;
      }
      switch ($char) {
        case 'L':
        case 'U':
        case 'F':
        case 'T':
        case 'S':
        case 'G':
        case 'B':
        case 'b':
          $modifiers .= $char;
          break;
        case '=':
        case '^':
        case '|':
        case '+':
        case '-':
        case '~':
          $conditions .= $char;
          break;
        case '(':
        case ')':
          $remaining_string = substr($format, $i);
          if ($char == '(' && ($closing_bracket = $this
            ->closingBracketPosition($remaining_string))) {
            $sub_string = $this
              ->format($tokens, substr($format, $i + 1, $closing_bracket - 1), $tokens);

            // Increment the counter past the closing bracket.
            $i += $closing_bracket;
            $pieces[] = $this
              ->addComponent($sub_string, $modifiers, $conditions);
          }
          else {

            // Unmatched, add it.
            $pieces[] = $this
              ->addComponent($char, $modifiers, $conditions);
          }
          break;
        default:
          if (array_key_exists($char, $tokens)) {
            $char = $tokens[$char];
          }
          $pieces[] = $this
            ->addComponent($char, $modifiers, $conditions);
          break;
      }
    }
    $parsed_pieces = [];
    for ($i = 0; $i < count($pieces); $i++) {
      $component = $pieces[$i]['value'];
      $conditions = $pieces[$i]['conditions'];
      $last_component = $i > 0 ? $pieces[$i - 1]['value'] : FALSE;
      $next_component = $i < count($pieces) - 1 ? $pieces[$i + 1]['value'] : FALSE;
      if (empty($conditions)) {
        $parsed_pieces[$i] = $component;
      }
      else {

        // Modifier: Conditional insertion. Insert if both the surrounding
        // tokens are not empty.
        if (strpos($conditions, '+') !== FALSE && !empty($last_component) && !empty($next_component)) {
          $parsed_pieces[$i] = $component;
        }

        // Modifier: Conditional insertion. Insert if the previous token is
        // not empty.
        if (strpos($conditions, '-') !== FALSE && !empty($last_component)) {
          $parsed_pieces[$i] = $component;
        }

        // Modifier: Conditional insertion. Insert if the previous token is
        // empty.
        if (strpos($conditions, '~') !== FALSE && empty($last_component)) {
          $parsed_pieces[$i] = $component;
        }

        // Modifier: Insert the token if the next token is empty.
        if (strpos($conditions, '^') !== FALSE && empty($next_component)) {
          $parsed_pieces[$i] = $component;
        }

        // Modifier: Insert the token if the next token is not empty.
        // This overrides the above two settings.
        if (strpos($conditions, '=') !== FALSE && !empty($next_component)) {
          $parsed_pieces[$i] = $component;
        }

        // Modifier: Conditional insertion. Uses the previous token unless
        // empty, otherwise insert this token.
        if (strpos($conditions, '|') !== FALSE) {
          if (empty($last_component)) {
            $parsed_pieces[$i] = $component;
          }
          else {
            unset($parsed_pieces[$i]);
          }
        }
      }
    }
    return str_replace('\\\\', "\t", implode('', $parsed_pieces));
  }

  /**
   * Adds a component.
   *
   * @param string $string
   *   The token string to process.
   * @param string $modifiers
   *   The modifiers to apply.
   * @param string $conditions
   *   The conditional flags.
   *
   * @return array
   *   The processed piece.
   */
  protected function addComponent($string, &$modifiers = '', &$conditions = '') {
    $string = $this
      ->applyModifiers($string, $modifiers);
    $piece = [
      'value' => $string,
      'conditions' => $conditions,
    ];
    $conditions = '';
    $modifiers = '';
    return $piece;
  }

  /**
   * Applies the specified modifiers to the string.
   *
   * @param string $string
   *   The token string to process.
   * @param string $modifiers
   *   The modifiers to apply.
   *
   * @return string
   *   The processed string.
   */
  protected function applyModifiers($string, $modifiers) {
    if (!is_null($string) || strlen($string)) {
      if ($modifiers) {
        $prefix = '';
        $suffix = '';
        if (preg_match('/^(<span[^>]*>)(.*)(<\\/span>)$/i', $string, $matches)) {
          $prefix = $matches[1];
          $string = $matches[2];
          $suffix = $matches[3];
        }
        for ($j = 0; $j < strlen($modifiers); $j++) {
          switch ($modifiers[$j]) {
            case 'L':
              $string = mb_strtolower($string);
              break;
            case 'U':
              $string = mb_strtoupper($string);
              break;
            case 'F':
              $string = Unicode::ucfirst($string);
              break;
            case 'G':
              $string = UniCode::ucwords($string);
              break;
            case 'T':
              $string = trim(preg_replace('/\\s+/', ' ', $string));
              break;
            case 'S':
              $string = Html::escape($string);
              break;
            case 'B':
              $parts = preg_split($this->boundaryRegExp, $string);
              $string = (string) array_shift($parts);
              break;
            case 'b':
              $parts = preg_split($this->boundaryRegExp, $string);
              $string = (string) array_pop($parts);
              break;
          }
        }
        $string = $prefix . $string . $suffix;
      }
    }
    return $string;
  }

  /**
   * Helper function to put out the first matched bracket position.
   *
   * @param string $string
   *   Accepts strings in the format, ^ marks the matched bracket.
   *
   *   i.e. '(xxx^)xxx(xxxx)xxxx' or '(xxx(xxx(xxxx))xxx^)'.
   *
   * @return mixed
   *   The closing bracket position or FALSE if not found.
   */
  protected function closingBracketPosition($string) {

    // Simplify the string by removing escaped brackets.
    $depth = 0;
    $string = str_replace([
      '\\(',
      '\\)',
    ], [
      '__',
      '__',
    ], $string);
    for ($i = 0; $i < strlen($string); $i++) {
      $char = $string[$i];
      if ($char == '(') {
        $depth++;
      }
      elseif ($char == ')') {
        $depth--;
        if ($depth == 0) {
          return $i;
        }
      }
    }
    return FALSE;
  }

  /**
   * Generates the tokens from the name item.
   *
   * @param array $name_components
   *   The array of name components.
   *
   * @return array
   *   The keyed tokens generated for the given name.
   */
  protected function generateTokens(array $name_components) {
    $name_components = (array) $name_components;
    $name_components += [
      'title' => '',
      'given' => '',
      'middle' => '',
      'family' => '',
      'credentials' => '',
      'generational' => '',
      'preferred' => '',
      'alternative' => '',
    ];
    $tokens = [
      't' => $this
        ->renderComponent($name_components['title'], 'title'),
      'g' => $this
        ->renderComponent($name_components['given'], 'given'),
      'p' => $this
        ->renderFirstComponent([
        $name_components['preferred'],
        $name_components['given'],
      ], 'given'),
      'q' => $this
        ->renderComponent($name_components['preferred'], 'preferred'),
      'm' => $this
        ->renderComponent($name_components['middle'], 'middle'),
      'f' => $this
        ->renderComponent($name_components['family'], 'family'),
      'c' => $this
        ->renderComponent($name_components['credentials'], 'credentials'),
      'a' => $this
        ->renderComponent($name_components['alternative'], 'alternative'),
      's' => $this
        ->renderComponent($name_components['generational'], 'generational'),
      'v' => $this
        ->renderComponent($name_components['preferred'], 'preferred', 'initial'),
      'w' => $this
        ->renderFirstComponent([
        $name_components['preferred'],
        $name_components['given'],
      ], 'given', 'initial'),
      'x' => $this
        ->renderComponent($name_components['given'], 'given', 'initial'),
      'y' => $this
        ->renderComponent($name_components['middle'], 'middle', 'initial'),
      'z' => $this
        ->renderComponent($name_components['family'], 'family', 'initial'),
      'A' => $this
        ->renderComponent($name_components['alternative'], 'alternative', 'initial'),
      'I' => $this
        ->renderComponent($name_components['given'] . ' ' . $name_components['family'], 'initials', 'initials'),
      'J' => $this
        ->renderComponent($name_components['given'] . ' ' . $name_components['middle'] . ' ' . $name_components['family'], 'initials', 'initials'),
      'K' => $this
        ->renderComponent($name_components['given'], 'initials', 'initials'),
      'M' => $this
        ->renderComponent($name_components['given'] . ' ' . $name_components['middle'], 'initials', 'initials'),
      'i' => $this->sep1,
      'j' => $this->sep2,
      'k' => $this->sep3,
    ];
    $preferred = $tokens['p'];
    $given = $tokens['g'];
    $family = $tokens['f'];
    if ($preferred || $family) {
      $tokens += [
        'd' => $preferred ? $preferred : $family,
        'D' => $family ? $family : $preferred,
      ];
    }
    if ($given || $family) {
      $tokens += [
        'e' => $given ? $given : $family,
        'E' => $family ? $family : $given,
      ];
    }
    $tokens += [
      'd' => NULL,
      'D' => NULL,
      'e' => NULL,
      'E' => NULL,
    ];
    return $tokens;
  }

  /**
   * Finds and renders the first renderable name component value.
   *
   * This function does not by default sanitize the output unless the markup
   * flag is set. If this is set, it runs the component through check_plain()
   * and wraps the component in a span with the component name set as the class.
   *
   * @param array $values
   *   An array of walues to find the first to render.
   * @param string $component_key
   *   The component context.
   * @param string $modifier
   *   Internal flag for processing.
   *
   * @return string
   *   The rendered component.
   */
  protected function renderFirstComponent(array $values, $component_key, $modifier = NULL) {
    foreach ($values as $value) {
      $output = $this
        ->renderComponent($value, $component_key, $modifier);
      if (isset($output) && strlen($output)) {
        return $output;
      }
    }
    return NULL;
  }

  /**
   * Renders a name component value.
   *
   * This function does not by default sanitize the output unless the markup
   * flag is set. If set, it runs the component through Html::escape() and
   * wraps the component in a span with the component name set as the class.
   *
   * @param string $value
   *   A value to render.
   * @param string $component_key
   *   The componenet context.
   * @param string $modifier
   *   Internal flag for processing.
   *
   * @return string
   *   The rendered componenet.
   */
  protected function renderComponent($value, $component_key, $modifier = NULL) {
    if (empty($value) || !mb_strlen($value)) {
      return NULL;
    }
    switch ($modifier) {

      // First letter first word.
      case 'initial':
        $value = mb_substr($value, 0, 1);
        break;

      // First letter all words.
      case 'initials':

        // First letter all words.
        $value = NameUnicodeExtras::initials($value);
        break;
    }

    // Based on http://schema.org/Person that doesn't cover generational suffix
    // or preferred names directly.
    $map = [
      'title' => 'honorificPrefix',
      'given' => 'givenName',
      'middle' => 'additionalName',
      'family' => 'familyName',
      'credential' => 'honorificSuffix',
      'alternative' => 'alternateName',
    ];
    switch ($this->markup) {
      case 'simple':
        return '<span class="' . Html::escape($component_key) . '">' . Html::escape($value) . '</span>';
      case 'microdata':
        return '<span class="' . Html::escape($component_key) . '"' . (isset($map[$component_key]) ? ' itemprop="' . $map[$component_key] . '"' : '') . '>' . Html::escape($value) . '</span>';
      case 'rdfa':
        return '<span class="' . Html::escape($component_key) . '"' . (isset($map[$component_key]) ? ' property="schema:' . $map[$component_key] . '"' : '') . '>' . Html::escape($value) . '</span>';
      case 'none':
      default:
        return $value;
    }
  }

  /**
   * Supported markup options.
   *
   * @return array
   *   A keyed array of markup options.
   */
  public function getMarkupOptions() {
    return [
      // Raw component values.
      'none' => $this
        ->t('No markup'),
      // Unescaped text.
      'raw' => $this
        ->t('Raw, unescaped text'),
      // Escaped component values.
      'simple' => $this
        ->t('Component classes'),
      // Escaped component values.
      'microdata' => $this
        ->t('Microdata itemprop components'),
      // Escaped component values.
      'rdfa' => $this
        ->t('RDFa property components'),
    ];
  }

  /**
   * Supported tokens.
   *
   * @param bool $describe
   *   Appends the description of the letter to the description.
   *
   * @return string[]
   *   An array of strings keyed by the token.
   */
  public function tokenHelp($describe = TRUE) {
    $tokens = [
      't' => $this
        ->t('Title.'),
      'p' => $this
        ->t('Preferred name, use given name if not set.'),
      'q' => $this
        ->t('Preferred name.'),
      'g' => $this
        ->t('Given name.'),
      'm' => $this
        ->t('Middle name(s).'),
      'f' => $this
        ->t('Family name.'),
      'c' => $this
        ->t('Credentials.'),
      's' => $this
        ->t('Generational suffix.'),
      'a' => $this
        ->t('Alternative value.'),
      'v' => $this
        ->t('First letter preferred name.'),
      'w' => $this
        ->t('First letter preferred or given name.'),
      'x' => $this
        ->t('First letter given.'),
      'y' => $this
        ->t('First letter middle.'),
      'z' => $this
        ->t('First letter family.'),
      'A' => $this
        ->t('First letter of alternative value.'),
      'I' => $this
        ->t('Initials (all) from given and family.'),
      'J' => $this
        ->t('Initials (all) from given, middle and family.'),
      'K' => $this
        ->t('Initials (all) from given.'),
      'M' => $this
        ->t('Initials (all) from given and middle.'),
      'd' => $this
        ->t('Conditional: Either the preferred given or family name. Preferred name is given preference over given or family names.'),
      'D' => $this
        ->t('Conditional: Either the preferred given or family name. Family name is given preference over preferred or given names.'),
      'e' => $this
        ->t('Conditional: Either the given or family name. Given name is given preference.'),
      'E' => $this
        ->t('Conditional: Either the given or family name. Family name is given preference.'),
      'i' => $this
        ->t('Separator 1.'),
      'j' => $this
        ->t('Separator 2.'),
      'k' => $this
        ->t('Separator 3.'),
      '\\' => $this
        ->t('You can prevent a character in the format string from being expanded by escaping it with a preceding backslash.'),
      'L' => $this
        ->t('Modifier: Converts the next token to all lowercase.'),
      'U' => $this
        ->t('Modifier: Converts the next token to all uppercase.'),
      'F' => $this
        ->t('Modifier: Converts the first letter to uppercase.'),
      'G' => $this
        ->t('Modifier: Converts the first letter of ALL words to uppercase.'),
      'T' => $this
        ->t('Modifier: Trims whitespace around the next token.'),
      'S' => $this
        ->t('Modifier: Ensures that the next token is safe for the display.'),
      'B' => $this
        ->t('Modifier: Use the first word of the next token.'),
      'b' => $this
        ->t('Modifier: Use the last word of the next token.'),
      '+' => $this
        ->t('Conditional: Insert the token if both the surrounding tokens are not empty.'),
      '-' => $this
        ->t('Conditional: Insert the token if the previous token is not empty.'),
      '~' => $this
        ->t('Conditional: Insert the token if the previous token is empty.'),
      '=' => $this
        ->t('Conditional: Insert the token if the next token is not empty.'),
      '^' => $this
        ->t('Conditional: Insert the token if the next token is empty.'),
      '|' => $this
        ->t('Conditional: Uses the previous token unless empty, otherwise it uses this token.'),
      '(' => $this
        ->t('Group: Start of token grouping.'),
      ')' => $this
        ->t('Group: End of token grouping.'),
    ];
    if ($describe) {
      foreach ($tokens as $letter => $description) {
        if (preg_match('/^[a-z]+$/', $letter)) {
          $tokens[$letter] = $this
            ->t('@description<br><small>(lowercase @letter)</small>', [
            '@description' => $description,
            '@letter' => mb_strtoupper($letter),
          ]);
        }
        elseif (preg_match('/^[A-Z]+$/', $letter)) {
          $tokens[$letter] = $this
            ->t('@description<br><small>(uppercase @letter)</small>', [
            '@description' => $description,
            '@letter' => mb_strtoupper($letter),
          ]);
        }
      }
    }
    return $tokens;
  }

  /**
   * Helper function to provide name format token help.
   *
   * @return array
   *   A renderable array of tokens in a details element..
   */
  public function renderableTokenHelp() {
    return [
      '#type' => 'details',
      '#title' => $this
        ->t('Format string help'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#parents' => [],
      'format_parameters' => [
        '#theme' => 'name_format_parameter_help',
        '#tokens' => $this
          ->tokenHelp(),
      ],
    ];
  }

}

Members

Namesort descending Modifiers Type Description Overrides
NameFormatParser::$boundaryRegExp protected property Used to seperate words using the "b" and "B" modifiers.
NameFormatParser::$markup protected property Markup style for decorating name components.
NameFormatParser::$sep1 protected property First separator.
NameFormatParser::$sep2 protected property Second separator.
NameFormatParser::$sep3 protected property Third separator.
NameFormatParser::addComponent protected function Adds a component.
NameFormatParser::applyModifiers protected function Applies the specified modifiers to the string.
NameFormatParser::closingBracketPosition protected function Helper function to put out the first matched bracket position.
NameFormatParser::format protected function Formats an array of name components into the supplied format.
NameFormatParser::generateTokens protected function Generates the tokens from the name item.
NameFormatParser::getMarkupOptions public function Supported markup options.
NameFormatParser::parse public function Parses a name component array into the given format.
NameFormatParser::renderableTokenHelp public function Helper function to provide name format token help.
NameFormatParser::renderComponent protected function Renders a name component value.
NameFormatParser::renderFirstComponent protected function Finds and renders the first renderable name component value.
NameFormatParser::tokenHelp public function Supported tokens.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.