Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Behat\Mink\Selector\Xpath; Chris@0: Chris@0: /** Chris@0: * XPath manipulation utility. Chris@0: * Chris@0: * @author Graham Bates Chris@0: * @author Christophe Coevoet Chris@0: */ Chris@0: class Manipulator Chris@0: { Chris@0: /** Chris@0: * Regex to find union operators not inside brackets. Chris@0: */ Chris@0: const UNION_PATTERN = '/\|(?![^\[]*\])/'; Chris@0: Chris@0: /** Chris@0: * Prepends the XPath prefix to the given XPath. Chris@0: * Chris@0: * The returned XPath will match elements matching the XPath inside an element Chris@0: * matching the prefix. Chris@0: * Chris@0: * @param string $xpath Chris@0: * @param string $prefix Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: public function prepend($xpath, $prefix) Chris@0: { Chris@0: $expressions = array(); Chris@0: Chris@0: // If the xpath prefix contains a union we need to wrap it in parentheses. Chris@0: if (preg_match(self::UNION_PATTERN, $prefix)) { Chris@0: $prefix = '('.$prefix.')'; Chris@0: } Chris@0: Chris@0: // Split any unions into individual expressions. Chris@0: foreach ($this->splitUnionParts($xpath) as $expression) { Chris@0: $expression = trim($expression); Chris@0: $parenthesis = ''; Chris@0: Chris@0: // If the union is inside some braces, we need to preserve the opening braces and apply Chris@0: // the prefix only inside it. Chris@0: if (preg_match('/^[\(\s*]+/', $expression, $matches)) { Chris@0: $parenthesis = $matches[0]; Chris@0: $expression = substr($expression, strlen($parenthesis)); Chris@0: } Chris@0: Chris@0: // add prefix before element selector Chris@0: if (0 === strpos($expression, '/')) { Chris@0: $expression = $prefix.$expression; Chris@0: } else { Chris@0: $expression = $prefix.'/'.$expression; Chris@0: } Chris@0: $expressions[] = $parenthesis.$expression; Chris@0: } Chris@0: Chris@0: return implode(' | ', $expressions); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Splits the XPath into parts that are separated by the union operator. Chris@0: * Chris@0: * @param string $xpath Chris@0: * Chris@0: * @return string[] Chris@0: */ Chris@0: private function splitUnionParts($xpath) Chris@0: { Chris@0: if (false === strpos($xpath, '|')) { Chris@0: return array($xpath); // If there is no pipe in the string, we know for sure that there is no union Chris@0: } Chris@0: Chris@0: $xpathLen = strlen($xpath); Chris@0: $openedBrackets = 0; Chris@0: // Consume whitespaces chars at the beginning of the string (this is the list of chars removed by trim() by default) Chris@0: $startPosition = strspn($xpath, " \t\n\r\0\x0B"); Chris@0: Chris@0: $unionParts = array(); Chris@0: Chris@0: for ($i = $startPosition; $i <= $xpathLen; ++$i) { Chris@0: // Consume all chars until we reach a quote, a bracket or a pipe Chris@0: $i += strcspn($xpath, '"\'[]|', $i); Chris@0: Chris@0: if ($i < $xpathLen) { Chris@0: switch ($xpath[$i]) { Chris@0: case '"': Chris@0: case "'": Chris@0: // Move to the end of the string literal Chris@0: if (false === $i = strpos($xpath, $xpath[$i], $i + 1)) { Chris@0: return array($xpath); // The XPath expression is invalid, don't split it Chris@0: } Chris@0: continue 2; Chris@0: case '[': Chris@0: ++$openedBrackets; Chris@0: continue 2; Chris@0: case ']': Chris@0: --$openedBrackets; Chris@0: continue 2; Chris@0: } Chris@0: } Chris@0: if ($openedBrackets) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: $unionParts[] = substr($xpath, $startPosition, $i - $startPosition); Chris@0: Chris@0: if ($i === $xpathLen) { Chris@0: return $unionParts; Chris@0: } Chris@0: Chris@0: // Consume any whitespace chars after the pipe Chris@0: $i += strspn($xpath, " \t\n\r\0\x0B", $i + 1); Chris@0: $startPosition = $i + 1; Chris@0: } Chris@0: Chris@0: return array($xpath); // The XPath expression is invalid Chris@0: } Chris@0: Chris@0: }