Code development platform for open source projects from the European Union institutions

Skip to content
Snippets Groups Projects
oe_whitelabel_paragraphs.module 23.7 KiB
Newer Older
<?php

/**
 * @file
 * OE Whitelabel Paragraphs module.
 *
 * Preprocess hooks are implemented on behalf of the oe_whitelabel theme.
 * This prevents hooks from running if oe_whitelabel is not the active theme.
 *
 * phpcs:disable Drupal.NamingConventions.ValidFunctionName.InvalidPrefix
 */

declare(strict_types = 1);

use Drupal\Component\Utility\Html;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\media\Entity\Media;
use Drupal\media\MediaInterface;
use Drupal\media\MediaSourceInterface;
use Drupal\media\Plugin\media\Source\Image;
use Drupal\media\Plugin\media\Source\OEmbed;
use Drupal\media_avportal\Plugin\media\Source\MediaAvPortalPhotoSource;
use Drupal\media_avportal\Plugin\media\Source\MediaAvPortalSourceInterface;
use Drupal\media_avportal\Plugin\media\Source\MediaAvPortalVideoSource;
use Drupal\oe_bootstrap_theme\ValueObject\ImageValueObject;
use Drupal\oe_media_iframe\Plugin\media\Source\Iframe;
use Drupal\paragraphs\Entity\Paragraph;

/**
 * Implements hook_modules_installed().
 */
function oe_whitelabel_paragraphs_modules_installed($modules, $is_syncing) {
  if ($is_syncing) {
    return;
  }

  // Import the gallery paragraph view display override when the related
  // module is installed.
  if (in_array('oe_paragraphs_gallery', $modules)) {
    ConfigImporter::importSingle('module', 'oe_whitelabel_paragraphs', '/config/overrides/', 'core.entity_view_display.paragraph.oe_gallery.default');
  }
}

/**
 * Implements hook_theme_suggestions_HOOK_alter().
 *
 * Adds a bare, markup-free template suggestion to all paragraph fields.
 */
function oe_whitelabel_paragraphs_theme_suggestions_field_alter(array &$suggestions, array $variables): void {
  $element = $variables['element'];

  // Do not output field labels and wrapping markup for paragraph fields.
  if (isset($element['#entity_type']) && $element['#entity_type'] === 'paragraph') {
    // Prepend the new suggestion to the list. This will put it right after the
    // default field template. By doing this we allow to override single
    // fields, while keeping all the rest markup-free.
    array_unshift($suggestions, 'field__bare');
  }
}

/**
 * Implements hook_preprocess_paragraph().
 */
function oe_whitelabel_preprocess_paragraph__oe_links_block(array &$variables): void {
  /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */
  $paragraph = $variables['paragraph'];
  $variables['orientation'] = $paragraph->get('oe_w_links_block_orientation')->value;
  $variables['background'] = $paragraph->get('oe_w_links_block_background')->value;
  if (!$paragraph->get('field_oe_text')->isEmpty()) {
    $variables['title'] = $paragraph->get('field_oe_text')->value;
  }

  foreach (Element::children($variables['content']['field_oe_links']) as $index) {
    $variables['links'][] = [
      'label' => $variables['content']['field_oe_links'][$index]['#title'],
      'path' => $variables['content']['field_oe_links'][$index]['#url'],
    ];
  }
}

/**
 * Implements hook_preprocess_paragraph() for oe_social_media_follow paragraph.
 */
function oe_whitelabel_preprocess_paragraph__oe_social_media_follow(array &$variables): void {
  /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */
  $paragraph = $variables['paragraph'];
  $variables['orientation'] = $paragraph->get('field_oe_social_media_variant')->value;
  $variables['background'] = $paragraph->get('oe_w_links_block_background')->value;
  if (!$paragraph->get('field_oe_title')->isEmpty()) {
    $variables['title'] = $paragraph->get('field_oe_title')->value;
  }
  $links = $paragraph->get('field_oe_social_media_links')->getValue();
  $variables['links'] = [];
  foreach ($links as $key => $link) {
    $variables['links'][$key]['icon_position'] = 'before';
    $variables['links'][$key]['icon']['path'] = $variables['bcl_icon_path'];
    $variables['links'][$key]['icon']['name'] = $link['link_type'];
    $variables['links'][$key]['label'] = $link['title'];
    $variables['links'][$key]['path'] = Url::fromUri($link['uri']);
  }
  if (!$paragraph->get('field_oe_social_media_see_more')->isEmpty()) {
    $other_link = $paragraph->get('field_oe_social_media_see_more')
      ->first()
      ->getValue();
    $variables['links'][] = [
      'label' => $other_link['title'],
      'path' => Url::fromUri($other_link['uri']),
    ];
  }
}

/**
 * Implements hook_preprocess_paragraph__oe_accordion().
 */
function oe_whitelabel_preprocess_paragraph__oe_accordion(array &$variables): void {
  // Massage data to be compliant with OE Bootstrap Theme accordion pattern
  // data structure.
  $builder = \Drupal::entityTypeManager()->getViewBuilder('paragraph');
  $variables['items'] = [];

  /** @var \Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem $field_item */
  foreach ($variables['paragraph']->get('field_oe_paragraphs') as $field_item) {
    $paragraph = \Drupal::service('entity.repository')->getTranslationFromContext($field_item->entity);
    $variables['items'][] = [
      'title' => $builder->viewField($paragraph->get('field_oe_text')),
      'content' => $builder->viewField($paragraph->get('field_oe_text_long')),
    ];
  }
}

/**
 * Implements hook_preprocess_paragraph() for paragraph--oe-text-feature-media.html.twig.
 */
function oe_whitelabel_preprocess_paragraph__oe_text_feature_media(array &$variables): void {
  /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */
  $paragraph = $variables['paragraph'];

  // Bail out if there is no media present.
  if ($paragraph->get('field_oe_media')->isEmpty()) {
    return;
  }

  /** @var \Drupal\media\Entity\Media $media */
  $media = $paragraph->get('field_oe_media')->entity;
  if (!$media instanceof MediaInterface) {
    // The media entity is not available anymore, bail out.
    return;
  }

  // Retrieve the correct media translation.
  /** @var \Drupal\media\Entity\Media $media */
  $media = \Drupal::service('entity.repository')->getTranslationFromContext($media, $paragraph->language()->getId());

  // Caches are handled by the formatter usually. Since we are not rendering
  // the original render arrays, we need to propagate our caches to the
  // paragraph template.
  $cacheability = CacheableMetadata::createFromRenderArray($variables);
  $cacheability->addCacheableDependency($media);

  // Run access checks on the media entity.
  $access = $media->access('view', $variables['user'], TRUE);
  $cacheability->addCacheableDependency($access);
  if (!$access->isAllowed()) {
    $cacheability->applyTo($variables);
    return;
  }

  // Get the media source.
  $source = $media->getSource();

  $is_image = $source instanceof MediaAvPortalPhotoSource || $source instanceof Image;
  $is_video = $source instanceof MediaAvPortalVideoSource || $source instanceof OEmbed || $source instanceof Iframe;

  // If it's not an image and not a video, bail out.
  if (!$is_image && !$is_video) {
    $cacheability->applyTo($variables);
    return;
  }

  $variant = $paragraph->get('oe_paragraphs_variant')->value ?? 'default';
  $variables['text_position'] = str_replace([
    '_featured',
    '_simple',
  ], '', $variant);

  if ($is_image) {
    $thumbnail = $media->get('thumbnail')->first();
    $variables['image'] = ImageValueObject::fromStyledImageItem($thumbnail, 'oe_bootstrap_theme_medium_no_crop');
  }
  elseif ($is_video) {
    _oe_whitelabel_featured_media_set_embedded_media($variables, $media, $cacheability, $source);
  }

  $cacheability->applyTo($variables);

  if (empty($paragraph->get('field_oe_link')->first())) {
    return;
  }

  /** @var \Drupal\link\Plugin\Field\FieldType\LinkItem $link_item */
  $link_item = $paragraph->get('field_oe_link')->first();
  $variables['link'] = [
    'path' => $link_item->getUrl(),
    'label' => $link_item->get('title')->getValue(),
  ];
}

/**
 * Implements hook_preprocess_paragraph() for paragraph--oe-list-item-block.html.twig.
 */
function oe_whitelabel_preprocess_paragraph__oe_list_item_block(array &$variables): void {
  /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */
  $paragraph = $variables['paragraph'];

  // @todo Use ->isEmpty() as in other preprocess functions.
  //   In the OpenEuropa team it was decided that ->isEmpty() calls should be
  //   the preferred way to deal with empty field values.
  //   This function instead relies on ->value or ->first() returning NULL for
  //   empty fields, which the team is not fully confident about, and which does
  //   not follow the team convention.
  //   It was agreed to keep it like this for now in this function, but refactor
  //   it in the future.
  //   See also https://www.drupal.org/project/drupal/issues/3268137.
  $variables['variant'] = $paragraph->get('oe_paragraphs_variant')->value;
  $variables['title'] = $paragraph->get('field_oe_title')->value;

  $layout_name = $paragraph->get('field_oe_list_item_block_layout')->value;
  $variables['columns'] = ['two_columns' => '2', 'three_columns' => '3'][$layout_name] ?? '1';

  $variables['items'] = [];
  foreach ($variables['paragraph']->get('field_oe_paragraphs') as $card_paragraph_item) {
    /** @var \Drupal\paragraphs\ParagraphInterface $card_paragraph */
    $card_paragraph = $card_paragraph_item->entity;
    $card_image_item = $card_paragraph->get('field_oe_image')->first();
    $card_image = $card_image_item ? ImageValueObject::fromImageItem($card_image_item) : NULL;

    // Prepare the metas if available.
    $card_badges = [];
    foreach ($card_paragraph->get('field_oe_meta') as $meta_item) {
      $card_badges[] = $meta_item->value;
    }

    /** @var \Drupal\link\LinkItemInterface|null $card_link_item */
    $card_link_item = $card_paragraph->get('field_oe_link')->first();
    $variables['items'][] = [
      'title' => $card_paragraph->get('field_oe_title')->value,
      'url' => $card_link_item ? $card_link_item->getUrl() : '',
      'text' => $card_paragraph->get('field_oe_text_long')->value,
      'image' => $card_image,
      'badges' => $card_badges,
    ];
  }

  // Prepare the button variables if a link has been specified.
  /** @var \Drupal\link\Plugin\Field\FieldType\LinkItem $link_item */
  $link_item = $paragraph->get('field_oe_link')->first();
  $variables['link'] = $link_item ? [
    'path' => $link_item->getUrl(),
    'label' => $link_item->title,
    'icon' => [
      'path' => $variables['bcl_icon_path'],
      'name' => 'chevron-right',
    ],
  ] : NULL;
}

/**
 * Implements hook_preprocess_paragraph() for oe_banner paragraph.
 */
function oe_whitelabel_preprocess_paragraph__oe_banner(array &$variables): void {
  /** @var Drupal\paragraphs\Entity\Paragraph $paragraph */
  $paragraph = $variables['paragraph'];
  $variables['title'] = $paragraph->get('field_oe_title')->value;
  $variables['description'] = $paragraph->get('field_oe_text')->value;
  $variables['full_width'] = (bool) $paragraph->get('field_oe_banner_full_width')->value;
  _oe_whitelabel_set_banner_link($paragraph, $variables);

  // The alignment field value contains the information regarding the pattern
  // type and centering.
  $alignment = $paragraph->get('field_oe_banner_type')->value;
  [$banner_type, $banner_alignment] = explode('_', $alignment);
  // The beginning of the string determines the pattern.
  $variables['pattern'] = 'banner_' . $banner_type;
  // The end of the string determines the position.
  $variables['alignment'] = $banner_alignment;

  $variant = $paragraph->get('oe_paragraphs_variant')->value ?? 'default';
  $variables['variant'] = str_replace('oe_banner_', '', $variant);

  if ($variables['variant'] === 'default' || $variables['variant'] === 'primary') {
    return;
  }

  // Bail out if there is no media present.
  if ($paragraph->get('field_oe_media')->isEmpty()) {
    return;
  }
  $cacheability = CacheableMetadata::createFromRenderArray($variables);

  /** @var \Drupal\media\Entity\Media $media */
  $media = $paragraph->get('field_oe_media')->entity;
  if (!$media instanceof MediaInterface) {
    // The media entity is not available anymore, bail out.
    return;
  }

  // Retrieve the correct translation to display.
  $media = \Drupal::service('entity.repository')->getTranslationFromContext($media, $paragraph->language()->getId());

  // Caches are handled by the formatter usually. Since we are not rendering
  // the original render arrays, we need to propagate our caches to the
  // paragraph template.
  $cacheability->addCacheableDependency($media);

  // Run access checks on the media entity.
  $access = $media->access('view', $variables['user'], TRUE);
  $cacheability->addCacheableDependency($access);
  if (!$access->isAllowed()) {
    $cacheability->applyTo($variables);
    return;
  }

  $source = $media->getSource();
  // We only support images and AV Portal photos for now.
  if (!$source instanceof MediaAvPortalSourceInterface && !$source instanceof Image) {
    $cacheability->applyTo($variables);
    return;
  }

  $uri = _oe_whitelabel_get_media_uri($source, $media, $cacheability);

  // The uri might be empty if the source is of type Image and the file entity
  // was deleted.
  if (empty($uri)) {
    $cacheability->applyTo($variables);
    return;
  }

  $variables['image'] = ImageValueObject::fromArray([
    'src' => file_create_url($uri),
    'alt' => $source->getMetadata($media, 'thumbnail_alt_value') ?? $media->label(),
    'name' => $media->getName(),
  ]);
  $cacheability->applyTo($variables);
}

/**
 * Implements hook_preprocess_paragraph() for timeline paragraph.
 */
function oe_whitelabel_preprocess_paragraph__oe_timeline(array &$variables): void {
  $paragraph = $variables['paragraph'];
  if (!$paragraph->get('field_oe_title')->isEmpty()) {
    $variables['heading'] = $paragraph->get('field_oe_title')->value;
  }

  if (!isset($variables['content']['field_oe_timeline']['#items'])) {
    return;
  }
  // Adapting body to content as defined in pattern.
  foreach ($variables['content']['field_oe_timeline']['#items'] as &$timeline_item) {
    $timeline_item['content'] = $timeline_item['body'];
    unset($timeline_item['body']);
    $variables['content']['items'][] = $timeline_item;
  }
  $variables['hide_from'] = $paragraph->get('field_oe_timeline_expand')->value;
}

/**
 * Implements hook_preprocess_paragraph() for paragraph--oe-content-row--variant-inpage-navigation.html.twig.
 */
function oe_whitelabel_preprocess_paragraph__oe_content_row__variant_inpage_navigation(array &$variables): void {
  /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */
  $paragraph = $variables['paragraph'];

  if ($paragraph->get('field_oe_paragraphs')->isEmpty()) {
    return;
  }

  $variables['attributes']['id'] = Html::getUniqueId('bcl-inpage-navigation-pid-' . $paragraph->id());

  $variables['title'] = t('Page contents');
  if (!$paragraph->get('field_oe_title')->isEmpty()) {
    $variables['title'] = $paragraph->get('field_oe_title')->value;
  }

  $field_render = &$variables['content']['field_oe_paragraphs'];
  $links = [];
  foreach ($paragraph->get('field_oe_paragraphs')->referencedEntities() as $delta => $sub_paragraph) {
    /** @var \Drupal\paragraphs\Entity\Paragraph $sub_paragraph */
    if (!$sub_paragraph->hasField('field_oe_title') || $sub_paragraph->get('field_oe_title')->isEmpty()) {
      continue;
    }

    $unique_id = Html::getUniqueId('bcl-inpage-item-' . $sub_paragraph->id());
    $field_render[$delta]['#theme_wrappers']['container'] = [
      '#attributes' => ['id' => $unique_id],
    ];

    $sub_paragraph = \Drupal::service('entity.repository')
      ->getTranslationFromContext($sub_paragraph, $paragraph->language()->getId());

    $links[] = [
      'path' => '#' . $unique_id,
      'label' => $sub_paragraph->get('field_oe_title')->first()->value,
    ];
  }

  $variables['links'] = $links;
}

/**
 * Implements hook_preprocess_paragraph() for oe_description-list paragraph.
 */
function oe_whitelabel_preprocess_paragraph__oe_description_list(array &$variables): void {
  /** @var Drupal\paragraphs\Entity\Paragraph $paragraph */
  $paragraph = $variables['paragraph'];
  $variables['title'] = $paragraph->get('field_oe_title')->value ?? '';
  $variables['orientation'] = $paragraph->get('oe_w_orientation')->value;
  // Reuse the output of the description list formatter, where the correct
  // text format is specified for the items.
  // The description list formatter doesn't wrap its output with field template
  // rendering, so we are forced to process the data here.
  foreach ($variables['content']['field_oe_description_list_items']['#items'] ?? [] as $item) {
    // The term and definition are render arrays, so we need to use the verbose
    // syntax for the description list pattern items.
      'term' => [
        ['label' => $item['term']],
      ],
      'definition' => [
        ['label' => $item['description']],
      ],
function oe_whitelabel_preprocess_paragraph__oe_facts_figures(array &$variables): void {
  /** @var \Drupal\paragraphs\ParagraphInterface $paragraph */
  $paragraph = $variables['paragraph'];
  if (!$paragraph->get('field_oe_title')->isEmpty()) {
    $variables['title'] = $paragraph->get('field_oe_title')->value;
  }

  if (!$paragraph->get('field_oe_link')->isEmpty()) {
    $link_item = $paragraph->get('field_oe_link')->first();
    $variables['link_more']['path'] = $link_item->getUrl();
    $variables['link_more']['label'] = $link_item->get('title')->getValue();
  }
  $variables['items'] = [];

  /** @var \Drupal\paragraphs\Entity\Paragraph $sub_paragraph */
  foreach ($paragraph->get('field_oe_paragraphs')->referencedEntities() as $sub_paragraph) {
    // Get the paragraph translation.
    $sub_paragraph = \Drupal::service('entity.repository')
      ->getTranslationFromContext($sub_paragraph, $paragraph->language()->getId());
    $description = '';
    if (!$sub_paragraph->get('field_oe_plain_text_long')->isEmpty()) {
      $description = $sub_paragraph->get('field_oe_plain_text_long')->value;
    }
    $variables['items'][] = [
      'icon' => $sub_paragraph->get('field_oe_icon')->value,
      'title' => $sub_paragraph->get('field_oe_title')->value,
      'subtitle' => $sub_paragraph->get('field_oe_subtitle')->value,
      'description' => $description,
    ];
  }

  if (!$paragraph->get('oe_w_n_columns')->isEmpty()) {
    $variables['columns'] = $paragraph->get('oe_w_n_columns')->value;
/**
 * Implements hook_preprocess_paragraph() for oe_carousel paragraph.
 */
function oe_whitelabel_preprocess_paragraph__oe_carousel(array &$variables): void {
  /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */
  $paragraph = $variables['paragraph'];
  $variables['items'] = [];
  $entity_repository = \Drupal::service('entity.repository');
  $cacheability = CacheableMetadata::createFromRenderArray($variables);

  /** @var \Drupal\paragraphs\Entity\Paragraph $sub_paragraph */
  foreach ($paragraph->get('field_oe_carousel_items')->referencedEntities() as $sub_paragraph) {
    // Get sub-paragraph translation.
    $sub_paragraph = \Drupal::service('entity.repository')
      ->getTranslationFromContext($sub_paragraph, $paragraph->language()->getId());
    /** @var \Drupal\media\Entity\Media $media */
    $media = $sub_paragraph->get('field_oe_media')->entity;
    if (!$media instanceof MediaInterface) {
      // The media entity is not available anymore, skip the item.
      continue;
    }
    // Retrieve the correct media translation.
    $media = $entity_repository->getTranslationFromContext($media, $paragraph->language()->getId());
    // Caches are handled by the formatter usually. Since we are not rendering
    // the original render arrays, we need to propagate our caches to the
    // paragraph template.
    $cacheability->addCacheableDependency($media);
    // Run access checks on the media entity.
    $access = $media->access('view', $variables['user'], TRUE);
    $cacheability->addCacheableDependency($access);
    if (!$access->isAllowed()) {
      $cacheability->applyTo($variables);
      continue;
    }
    $source = $media->getSource();
    // We only support images and AV Portal photos for now.
    if (!$source instanceof MediaAvPortalSourceInterface && !$source instanceof Image) {
      $cacheability->applyTo($variables);
      continue;
    }

    $uri = _oe_whitelabel_get_media_uri($source, $media, $cacheability);

    // The uri might be empty if the source is of type Image and the file entity
    // was deleted.
    if (empty($uri)) {
      $cacheability->applyTo($variables);
      continue;
    }

    $slide = [
      'caption_title' => $sub_paragraph->get('field_oe_title')->value,
      'caption' => !$sub_paragraph->get('field_oe_text')->isEmpty() ? $sub_paragraph->get('field_oe_text')->value : '',
      'image' => [
        'src' => file_create_url($uri),
      ],
    ];

    if (!$sub_paragraph->get('field_oe_link')->isEmpty()) {
      /** @var \Drupal\link\Plugin\Field\FieldType\LinkItem $link_item */
drishu's avatar
drishu committed
      $link_item = $sub_paragraph->get('field_oe_link')->first();
      $slide['link'] = [
        'path' => $link_item->getUrl(),
        'label' => $link_item->get('title')->getValue(),
      ];
    }

    $variables['slides'][] = $slide;
  }

  $cacheability->applyTo($variables);
/**
 * Sets link variable for banner paragraph.
 *
 * @param \Drupal\paragraphs\Entity\Paragraph $paragraph
 *   The paragraph.
 * @param array $variables
 *   The render array.
 */
function _oe_whitelabel_set_banner_link(Paragraph $paragraph, array &$variables): void {
  if ($paragraph->get('field_oe_link')->isEmpty()) {
    return;
  }

  $link = $paragraph->get('field_oe_link')->first();
  $variables['url'] = $link->getUrl();
  $variables['label'] = $link->get('title')->getValue();
}

/**
 * Gets the uri from a media object.
 *
 * @param \Drupal\media\MediaSourceInterface $source
 *   The media source.
 * @param \Drupal\media\Entity\Media $media
 *   The media object.
 * @param \Drupal\Core\Cache\CacheableMetadata $cacheability
 *   The cacheability object.
 *
 * @return string
 *   The uri string.
 */
function _oe_whitelabel_get_media_uri(MediaSourceInterface $source, Media $media, CacheableMetadata $cacheability): string {
  $field_name = $source->getConfiguration()['source_field'];

  if ($source instanceof Image && ($file_entity = $media->get($field_name)->entity)) {
    $cacheability->addCacheableDependency($file_entity);
    return $file_entity->getFileUri();
  }

  if ($source instanceof MediaAvPortalSourceInterface) {
    $resource_ref = $media->get($field_name)->value;
    return 'avportal://' . $resource_ref . '.jpg';
  }

  return '';
}

/**
 * Prepares embedded media variables for "text with featured media" paragraph.
 *
 * @param array $variables
 *   The render array.
 * @param \Drupal\media\MediaInterface $media
 *   Media object.
 * @param \Drupal\Core\Cache\CacheableMetadata $cacheability
 *   CacheableMetadata object.
 * @param \Drupal\media\MediaSourceInterface $source
 *   Media source.
 */
function _oe_whitelabel_featured_media_set_embedded_media(array &$variables, MediaInterface $media, CacheableMetadata $cacheability, MediaSourceInterface $source): void {
  // Default video aspect ratio is set to 16x9.
  $variables['ratio'] = '16x9';

  // Load information about the media and the display.
  $media_type = \Drupal::entityTypeManager()->getStorage('media_type')->load($media->bundle());
  $cacheability->addCacheableDependency($media_type);
  $source_field = $source->getSourceFieldDefinition($media_type);
  $display = EntityViewDisplay::collectRenderDisplay($media, 'default');
  $cacheability->addCacheableDependency($display);
  $display_options = $display->getComponent($source_field->getName());

  $variables['embedded_media'] = $media->{$source_field->getName()}->view($display_options);

  if ($media->bundle() === 'video_iframe') {
    $ratio = $media->get('oe_media_iframe_ratio')->value;
    $variables['ratio'] = str_replace('_', 'x', $ratio);
  }
}