* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Behat\Behat\Definition\Pattern\Policy; use Behat\Behat\Definition\Pattern\Pattern; use Behat\Behat\Definition\Exception\InvalidPatternException; use Behat\Transliterator\Transliterator; /** * Defines a way to handle turnip patterns. * * @author Konstantin Kudryashov */ final class TurnipPatternPolicy implements PatternPolicy { const TOKEN_REGEX = "[\"']?(?P<%s>(?<=\")[^\"]*(?=\")|(?<=')[^']*(?=')|\-?[\w\.\,]+)['\"]?"; const PLACEHOLDER_REGEXP = "/\\\:(\w+)/"; const OPTIONAL_WORD_REGEXP = '/(\s)?\\\\\(([^\\\]+)\\\\\)(\s)?/'; const ALTERNATIVE_WORD_REGEXP = '/(\w+)\\\\\/(\w+)/'; /** * @var string[] */ private $regexCache = array(); /** * @var string[] */ private static $placeholderPatterns = array( "/(?escapeAlternationSyntax($pattern); $canonicalText = $this->generateCanonicalText($stepText); return new Pattern($canonicalText, $pattern, $count); } /** * {@inheritdoc} */ public function supportsPattern($pattern) { return true; } /** * {@inheritdoc} */ public function transformPatternToRegex($pattern) { if (!isset($this->regexCache[$pattern])) { $this->regexCache[$pattern] = $this->createTransformedRegex($pattern); } return $this->regexCache[$pattern]; } /** * @param string $pattern * @return string */ private function createTransformedRegex($pattern) { $regex = preg_quote($pattern, '/'); $regex = $this->replaceTokensWithRegexCaptureGroups($regex); $regex = $this->replaceTurnipOptionalEndingWithRegex($regex); $regex = $this->replaceTurnipAlternativeWordsWithRegex($regex); return '/^' . $regex . '$/i'; } /** * Generates canonical text for step text. * * @param string $stepText * * @return string */ private function generateCanonicalText($stepText) { $canonicalText = preg_replace(self::$placeholderPatterns, '', $stepText); $canonicalText = Transliterator::transliterate($canonicalText, ' '); $canonicalText = preg_replace('/[^a-zA-Z\_\ ]/', '', $canonicalText); $canonicalText = str_replace(' ', '', ucwords($canonicalText)); return $canonicalText; } /** * Replaces turnip tokens with regex capture groups. * * @param string $regex * * @return string */ private function replaceTokensWithRegexCaptureGroups($regex) { $tokenRegex = self::TOKEN_REGEX; return preg_replace_callback( self::PLACEHOLDER_REGEXP, array($this, 'replaceTokenWithRegexCaptureGroup'), $regex ); } private function replaceTokenWithRegexCaptureGroup($tokenMatch) { if (strlen($tokenMatch[1]) >= 32) { throw new InvalidPatternException( "Token name should not exceed 32 characters, but `{$tokenMatch[1]}` was used." ); } return sprintf(self::TOKEN_REGEX, $tokenMatch[1]); } /** * Replaces turnip optional ending with regex non-capturing optional group. * * @param string $regex * * @return string */ private function replaceTurnipOptionalEndingWithRegex($regex) { return preg_replace(self::OPTIONAL_WORD_REGEXP, '(?:\1)?(?:\2)?(?:\3)?', $regex); } /** * Replaces turnip alternative words with regex non-capturing alternating group. * * @param string $regex * * @return string */ private function replaceTurnipAlternativeWordsWithRegex($regex) { $regex = preg_replace(self::ALTERNATIVE_WORD_REGEXP, '(?:\1|\2)', $regex); $regex = $this->removeEscapingOfAlternationSyntax($regex); return $regex; } /** * Adds escaping to alternation syntax in pattern. * * By default, Turnip treats `/` as alternation syntax. Meaning `one/two` for Turnip * means either `one` or `two`. Sometimes though you'll want to use slash character * with different purpose (URL, UNIX paths). In this case, you would escape slashes * with backslash. * * This method adds escaping to all slashes in generated snippets. * * @param string $pattern * * @return string */ private function escapeAlternationSyntax($pattern) { return str_replace('/', '\/', $pattern); } /** * Removes escaping of alternation syntax from regex. * * This method removes those escaping backslashes from your slashes, so your steps * could be matched against your escaped definitions. * * @param string $regex * * @return string */ private function removeEscapingOfAlternationSyntax($regex) { return str_replace('\\\/', '/', $regex); } }