Commit 9886834f authored by Youri van Koppen's avatar Youri van Koppen
Browse files

Issue #3322789 by DamienMcKenna, MegaChriz: Added plugin for multiline search/replace.

parent cbce28b6
Loading
Loading
Loading
Loading
+20 −0
Original line number Diff line number Diff line
@@ -82,6 +82,26 @@ tamper.find_replace:
      type: boolean
      label: 'Match whole word/phrase'

tamper.find_replace_multiline:
  mapping:
    find_replace:
      type: sequence
      label: 'Text to find and the replacements'
      sequence:
        type: string
    separator:
      type: string
      label: 'Search/replacement value separator'
    case_sensitive:
      type: boolean
      label: 'Case sensitive'
    word_boundaries:
      type: boolean
      label: 'Respect word boundaries'
    whole:
      type: boolean
      label: 'Match whole word/phrase'

tamper.find_replace_regex:
  mapping:
    find:
+230 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\tamper\Plugin\Tamper;

use Drupal\Core\Form\FormStateInterface;
use Drupal\tamper\Exception\TamperException;
use Drupal\tamper\TamperableItemInterface;
use Drupal\tamper\TamperBase;

/**
 * A plugin for performing a multiline search/replace.
 *
 * @Tamper(
 *   id = "find_replace_multiline",
 *   label = @Translation("Find replace (multiline)"),
 *   description = @Translation("Find and replace text, with multiple search/replacement patterns defined together."),
 *   category = "Text"
 * )
 */
class FindReplaceMultiline extends TamperBase {

  const SETTING_FIND_REPLACE = 'find_replace';
  const SETTING_SEPARATOR = 'separator';
  const SETTING_CASE_SENSITIVE = 'case_sensitive';
  const SETTING_WORD_BOUNDARIES = 'word_boundaries';
  const SETTING_WHOLE = 'whole';

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    $config = parent::defaultConfiguration();
    $config[self::SETTING_FIND_REPLACE] = [];
    $config[self::SETTING_SEPARATOR] = '|';
    $config[self::SETTING_CASE_SENSITIVE] = FALSE;
    $config[self::SETTING_WORD_BOUNDARIES] = FALSE;
    $config[self::SETTING_WHOLE] = FALSE;
    return $config;
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form[self::SETTING_FIND_REPLACE] = [
      '#type' => 'textarea',
      '#title' => $this->t('Text to find and the replacements'),
      '#default_value' => implode("\n", $this->getSetting(self::SETTING_FIND_REPLACE)),
      '#description' => $this->t("Enter one match per line in the format <code>search|replacement</code>, though the separator can be changed below.\nThe replacements will be processed in order provided above."),
      '#required' => TRUE,
    ];

    $form[self::SETTING_SEPARATOR] = [
      '#type' => 'textfield',
      '#title' => $this->t('Search/replacement value separator'),
      '#default_value' => $this->getSetting(self::SETTING_SEPARATOR),
      '#description' => $this->t('Control the character used to separate the "search" from the "replace" string in the field above. Defaults to "|", to match the value separator used on the Drupal core list fields.'),
      '#required' => TRUE,
    ];

    $form[self::SETTING_CASE_SENSITIVE] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Case sensitive'),
      '#default_value' => $this->getSetting(self::SETTING_CASE_SENSITIVE),
      '#description' => $this->t('If checked, "book" will match "book" but not "Book" or "BOOK".'),
    ];

    $form[self::SETTING_WORD_BOUNDARIES] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Respect word boundaries'),
      '#default_value' => $this->getSetting(self::SETTING_WORD_BOUNDARIES),
      '#description' => $this->t('If checked, "book" will match "book" but not "bookcase".'),
    ];

    $form[self::SETTING_WHOLE] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Match whole word/phrase'),
      '#default_value' => $this->getSetting(self::SETTING_WHOLE),
      '#description' => $this->t('If checked, then the whole word or phrase will be matched, e.g. "book" will match "book" but not "the book". If this option is selected then "Respect word boundaries" above will be ignored.'),
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
    $lines = explode("\n", $form_state->getValue(self::SETTING_FIND_REPLACE));
    $separator = $form_state->getValue(self::SETTING_SEPARATOR);

    // Check if each line contains the separator.
    $missing = [];
    foreach ($lines as $index => $line) {
      $line = trim($line);
      if (empty($line)) {
        continue;
      }
      if (strpos($line, $separator) === FALSE) {
        $missing[] = $index + 1;
      }
    }

    if (!empty($missing)) {
      $amount = count($missing);
      if ($amount > 1) {
        $last = array_pop($missing);
      }
      else {
        $last = '';
      }

      $error_message = $this->formatPlural($amount, 'Line @line is missing the separator "@separator".', 'Lines @lines and @last_line are missing the separator "@separator".', [
        '@line' => reset($missing),
        '@lines' => implode(', ', $missing),
        '@last_line' => $last,
        '@separator' => $separator,
      ]);
      $form_state->setErrorByName(self::SETTING_FIND_REPLACE, $error_message);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    parent::submitConfigurationForm($form, $form_state);
    $lines = explode("\n", $form_state->getValue(self::SETTING_FIND_REPLACE));

    // Remove empty lines.
    foreach ($lines as $index => $line) {
      $line = trim($line);
      if (strlen($line) < 1) {
        $lines[$index] = $line;
      }
    }
    $lines = array_filter($lines);

    $this->setConfiguration([
      self::SETTING_FIND_REPLACE => $lines,
      self::SETTING_SEPARATOR => $form_state->getValue(self::SETTING_SEPARATOR),
      self::SETTING_CASE_SENSITIVE => $form_state->getValue(self::SETTING_CASE_SENSITIVE),
      self::SETTING_WORD_BOUNDARIES => $form_state->getValue(self::SETTING_WORD_BOUNDARIES),
      self::SETTING_WHOLE => $form_state->getValue(self::SETTING_WHOLE),
    ]);
  }

  /**
   * {@inheritdoc}
   */
  public function tamper($data, TamperableItemInterface $item = NULL) {
    if (!is_string($data)) {
      throw new TamperException('Input should be a string.');
    }

    $function = $this->getFunction();

    $find_replace = $this->getSetting(self::SETTING_FIND_REPLACE);
    $separator = $this->getSetting(self::SETTING_SEPARATOR);

    // Process the find/replace string one line at a time.
    foreach ($find_replace as $line) {
      if (empty($line)) {
        continue;
      }
      // Verify the separator is found.
      if (strpos($line, $separator) === FALSE) {
        throw new TamperException(sprintf('In the configuration the string separator "%s" is missing.', $separator));
      }
      [$find, $replace] = explode($separator, $line);
      if ($this->useRegex()) {
        $find = $this->getRegexPattern($find);
      }
      $data = $function($find, $replace, $data);
    }

    return $data;
  }

  /**
   * {@inheritdoc}
   */
  public function multiple() {
    return FALSE;
  }

  /**
   * Check if we are using the regex callback.
   *
   * @return bool
   *   TRUE when regex will be used.
   */
  protected function useRegex() {
    return $this->getSetting(self::SETTING_WORD_BOUNDARIES) || $this->getSetting(self::SETTING_WHOLE);
  }

  /**
   * Get the function to use for the find and replace.
   *
   * @return string
   *   Function name to call.
   */
  protected function getFunction() {
    if ($this->useRegex()) {
      return 'preg_replace';
    }
    return $this->getSetting(self::SETTING_CASE_SENSITIVE) ? 'str_replace' : 'str_ireplace';
  }

  /**
   * Get the regex pattern.
   *
   * @param string $find
   *   The string to be found.
   *
   * @return string
   *   Regex pattern to use.
   */
  protected function getRegexPattern($find) {
    $regex = $this->getSetting(self::SETTING_WHOLE) ?
      '/^' . preg_quote($find, '/') . '$/u' :
      '/\b' . preg_quote($find, '/') . '\b/u';

    if (!$this->getSetting(self::SETTING_CASE_SENSITIVE)) {
      $regex .= 'i';
    }
    return $regex;
  }

}
+120 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\tamper\Functional\Plugin\Tamper;

/**
 * Tests the find and replace (multiline) plugin.
 *
 * @coversDefaultClass \Drupal\tamper\Plugin\Tamper\FindReplaceMultiline
 * @group tamper
 */
class FindReplaceMultilineTest extends TamperPluginTestBase {

  /**
   * The ID of the plugin to test.
   *
   * @var string
   */
  protected static $pluginId = 'find_replace_multiline';

  /**
   * {@inheritdoc}
   */
  public function formDataProvider(): array {
    return [
      'no values' => [
        'expected' => [],
        'edit' => [],
        'errors' => [
          'Text to find and the replacements field is required',
        ],
      ],
      'minimal values' => [
        'expected' => [
          'find_replace' => ['cat|dog'],
          'separator' => '|',
          'case_sensitive' => FALSE,
          'word_boundaries' => FALSE,
          'whole' => FALSE,
        ],
        'edit' => [
          'find_replace' => 'cat|dog',
        ],
      ],
      'check remove empty lines' => [
        'expected' => [
          'find_replace' => [
            'cat|dog ',
            ' Foo|Bar',
          ],
          'separator' => '|',
          'case_sensitive' => FALSE,
          'word_boundaries' => FALSE,
          'whole' => FALSE,
        ],
        'edit' => [
          'find_replace' => "\ncat|dog \n \n Foo|Bar\n\n",
        ],
      ],
      'missing separator' => [
        'expected' => [],
        'edit' => [
          'find_replace' => "John|Paul\ncat/dog\n",
        ],
        'errors' => [
          'Line 2 is missing the separator "|".',
        ],
      ],
      'missing separator, other separator' => [
        'expected' => [],
        'edit' => [
          'find_replace' => "John|Paul",
          'separator' => ',',
        ],
        'errors' => [
          'Line 1 is missing the separator ",".',
        ],
      ],
      'two lines missing separator' => [
        'expected' => [],
        'edit' => [
          'find_replace' => "John|Paul\nCat|Dog",
          'separator' => ',',
        ],
        'errors' => [
          'Lines 1 and 2 are missing the separator ",".',
        ],
      ],
      'multiple lines missing separator' => [
        'expected' => [],
        'edit' => [
          'find_replace' => "John|Paul\nCat|Dog\nFoo|Bar\nHello|Goodbye",
          'separator' => ',',
        ],
        'errors' => [
          'Lines 1, 2, 3 and 4 are missing the separator ",".',
        ],
      ],
      'with values' => [
        'expected' => [
          'find_replace' => [
            'John,Paul',
            'Cat,Dog',
          ],
          'separator' => ',',
          'case_sensitive' => TRUE,
          'word_boundaries' => TRUE,
          'whole' => TRUE,
        ],
        'edit' => [
          'find_replace' => "John,Paul\nCat,Dog",
          'separator' => ',',
          'case_sensitive' => 1,
          'word_boundaries' => 1,
          'whole' => 1,
        ],
      ],
    ];
  }

}
+146 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\tamper\Unit\Plugin\Tamper;

use Drupal\tamper\Exception\TamperException;
use Drupal\tamper\Plugin\Tamper\FindReplaceMultiline;

/**
 * Tests the multiline find and replace plugin.
 *
 * @coversDefaultClass \Drupal\tamper\Plugin\Tamper\FindReplaceMultiline
 * @group tamper
 */
class FindReplaceMultilineTest extends TamperPluginTestBase {

  /**
   * {@inheritdoc}
   */
  protected function instantiatePlugin() {
    $config = [
      FindReplaceMultiline::SETTING_FIND_REPLACE => [],
      FindReplaceMultiline::SETTING_SEPARATOR => '|',
      FindReplaceMultiline::SETTING_CASE_SENSITIVE => FALSE,
      FindReplaceMultiline::SETTING_WORD_BOUNDARIES => FALSE,
      FindReplaceMultiline::SETTING_WHOLE => FALSE,
    ];
    return new FindReplaceMultiline($config, 'find_replace_multiline', [], $this->getMockSourceDefinition());
  }

  /**
   * Test the plugin with a single value.
   */
  public function testSingleValue() {
    $config = [
      FindReplaceMultiline::SETTING_FIND_REPLACE => ['cat|dog'],
      FindReplaceMultiline::SETTING_SEPARATOR => '|',
      FindReplaceMultiline::SETTING_CASE_SENSITIVE => FALSE,
      FindReplaceMultiline::SETTING_WORD_BOUNDARIES => FALSE,
      FindReplaceMultiline::SETTING_WHOLE => FALSE,
    ];
    $plugin = new FindReplaceMultiline($config, 'find_replace_multiline', [], $this->getMockSourceDefinition());
    $this->assertEquals('The dog went to the park.', $plugin->tamper('The cat went to the park.'));
    $this->assertEquals('The dog went to the park.', $plugin->tamper('The Cat went to the park.'));
    $this->assertEquals('The dogwent to the park.', $plugin->tamper('The Catwent to the park.'));
  }

  /**
   * Test the plugin with a single value.
   */
  public function testSingleValues() {
    $config = [
      FindReplaceMultiline::SETTING_FIND_REPLACE => [
        'cat|dog',
        'orange|mango',
      ],
      FindReplaceMultiline::SETTING_SEPARATOR => '|',
      FindReplaceMultiline::SETTING_CASE_SENSITIVE => FALSE,
      FindReplaceMultiline::SETTING_WORD_BOUNDARIES => FALSE,
      FindReplaceMultiline::SETTING_WHOLE => FALSE,
    ];
    $plugin = new FindReplaceMultiline($config, 'find_replace_multiline', [], $this->getMockSourceDefinition());
    $this->assertEquals('The dog ate the mango.', $plugin->tamper('The cat ate the orange.'));
    $this->assertEquals('The mango was eaten by the dog.', $plugin->tamper('The orange was eaten by the cat.'));
    $this->assertEquals('The dog went to the park.', $plugin->tamper('The cat went to the park.'));
    $this->assertEquals('The mango is the best fruit.', $plugin->tamper('The orange is the best fruit.'));
  }

  /**
   * Tests with missing separator.
   */
  public function testWithMissingSeparator() {
    $config = [
      FindReplaceMultiline::SETTING_FIND_REPLACE => ['cat/dog'],
      FindReplaceMultiline::SETTING_SEPARATOR => ';',
      FindReplaceMultiline::SETTING_CASE_SENSITIVE => FALSE,
      FindReplaceMultiline::SETTING_WORD_BOUNDARIES => FALSE,
      FindReplaceMultiline::SETTING_WHOLE => FALSE,
    ];
    $plugin = new FindReplaceMultiline($config, 'find_replace_multiline', [], $this->getMockSourceDefinition());
    $this->expectException(TamperException::class);
    $this->expectExceptionMessage('In the configuration the string separator ";" is missing.');
    $plugin->tamper('The cat ate the orange.');
  }

  /**
   * Test the plugin as case sensitive.
   */
  public function testSingleValueCaseSensitive() {
    $config = [
      FindReplaceMultiline::SETTING_FIND_REPLACE => ['cat|dog'],
      FindReplaceMultiline::SETTING_SEPARATOR => '|',
      FindReplaceMultiline::SETTING_CASE_SENSITIVE => TRUE,
      FindReplaceMultiline::SETTING_WORD_BOUNDARIES => FALSE,
      FindReplaceMultiline::SETTING_WHOLE => FALSE,
    ];
    $plugin = new FindReplaceMultiline($config, 'find_replace_multiline', [], $this->getMockSourceDefinition());
    $this->assertEquals('The dog went to the park.', $plugin->tamper('The cat went to the park.'));
    $this->assertEquals('The Cat went to the park.', $plugin->tamper('The Cat went to the park.'));
    $this->assertEquals('The dogwent to the park.', $plugin->tamper('The catwent to the park.'));
  }

  /**
   * Test the plugin as respecting word boundaries.
   */
  public function testSingleValueWordBoundaries() {
    $config = [
      FindReplaceMultiline::SETTING_FIND_REPLACE => ['cat|dog'],
      FindReplaceMultiline::SETTING_SEPARATOR => '|',
      FindReplaceMultiline::SETTING_CASE_SENSITIVE => FALSE,
      FindReplaceMultiline::SETTING_WORD_BOUNDARIES => TRUE,
      FindReplaceMultiline::SETTING_WHOLE => FALSE,
    ];
    $plugin = new FindReplaceMultiline($config, 'find_replace_multiline', [], $this->getMockSourceDefinition());
    $this->assertEquals('The dog went to the park.', $plugin->tamper('The cat went to the park.'));
    $this->assertEquals('The dog went to the park.', $plugin->tamper('The Cat went to the park.'));
    $this->assertEquals('The catwent to the park.', $plugin->tamper('The catwent to the park.'));
  }

  /**
   * Test the plugin as replace whole words only.
   */
  public function testSingleValueWhole() {
    $config = [
      FindReplaceMultiline::SETTING_FIND_REPLACE => ['cat|dog'],
      FindReplaceMultiline::SETTING_SEPARATOR => '|',
      FindReplaceMultiline::SETTING_CASE_SENSITIVE => FALSE,
      FindReplaceMultiline::SETTING_WORD_BOUNDARIES => FALSE,
      FindReplaceMultiline::SETTING_WHOLE => TRUE,
    ];
    $plugin = new FindReplaceMultiline($config, 'find_replace_multiline', [], $this->getMockSourceDefinition());
    $this->assertEquals('The cat went to the park.', $plugin->tamper('The cat went to the park.'));
    $this->assertEquals('dog', $plugin->tamper('cat'));
    $this->assertEquals('dog', $plugin->tamper('Cat'));
  }

  /**
   * Test the plugin with a multiple values.
   */
  public function testMultipleValues() {
    $plugin = new FindReplaceMultiline([], 'find_replace_multiline', [], $this->getMockSourceDefinition());
    $this->expectException(TamperException::class);
    $this->expectExceptionMessage('Input should be a string.');
    $plugin->tamper(['foo', 'bar', 'baz']);
  }

}