Passed
Push — master ( 0aaff6...b19a92 )
by Josh
03:07
created

XPathHelper::evaluateLiteral()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 10
c 1
b 0
f 0
cc 3
nc 2
nop 1
crap 3
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2019 The s9e Authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\TextFormatter\Configurator\Helpers;
9
10
use RuntimeException;
11
use s9e\TextFormatter\Configurator\RecursiveParser;
12
use s9e\TextFormatter\Configurator\RendererGenerators\PHP\XPathConvertor\Convertors\BooleanFunctions;
13
use s9e\TextFormatter\Configurator\RendererGenerators\PHP\XPathConvertor\Convertors\BooleanOperators;
14
use s9e\TextFormatter\Configurator\RendererGenerators\PHP\XPathConvertor\Convertors\Comparisons;
15
use s9e\TextFormatter\Configurator\RendererGenerators\PHP\XPathConvertor\Convertors\Core;
16
use s9e\TextFormatter\Configurator\RendererGenerators\PHP\XPathConvertor\Convertors\Math;
17
use s9e\TextFormatter\Configurator\RendererGenerators\PHP\XPathConvertor\Convertors\SingleByteStringFunctions;
18
use s9e\TextFormatter\Utils\XPath;
19
20
abstract class XPathHelper
21
{
22
	/**
23
	* Decode strings inside of an XPath expression
24
	*
25
	* @param  string $expr
26
	* @return string
27
	*/
28 1
	public static function decodeStrings($expr)
29
	{
30 1
		return preg_replace_callback(
31 1
			'(([\'"])(.*?)\\1)s',
32
			function ($m)
33
			{
34 1
				return $m[1] . hex2bin($m[2]) . $m[1];
35 1
			},
36
			$expr
37
		);
38
	}
39
40
	/**
41
	* Encode strings inside of an XPath expression
42
	*
43
	* @param  string $expr
44
	* @return string
45
	*/
46 1
	public static function encodeStrings($expr)
47
	{
48 1
		return preg_replace_callback(
49 1
			'(([\'"])(.*?)\\1)s',
50
			function ($m)
51
			{
52 1
				return $m[1] . bin2hex($m[2]) . $m[1];
53 1
			},
54
			$expr
55
		);
56
	}
57
58
	/**
59
	* Return the list of variables used in a given XPath expression
60
	*
61
	* @param  string $expr XPath expression
62
	* @return array        Alphabetically sorted list of unique variable names
63
	*/
64 4
	public static function getVariables($expr)
65
	{
66
		// First, remove strings' contents to prevent false-positives
67 4
		$expr = preg_replace('/(["\']).*?\\1/s', '$1$1', $expr);
68
69
		// Capture all the variable names
70 4
		preg_match_all('/\\$(\\w+)/', $expr, $matches);
71
72
		// Dedupe and sort names
73 4
		$varNames = array_unique($matches[1]);
74 4
		sort($varNames);
75
76 4
		return $varNames;
77
	}
78
79
	/**
80
	* Determine whether given XPath expression definitely evaluates to a number
81
	*
82
	* @param  string $expr XPath expression
83
	* @return bool         Whether given XPath expression definitely evaluates to a number
84
	*/
85 21
	public static function isExpressionNumeric($expr)
86
	{
87
		// Detect simple arithmetic operations
88 21
		if (preg_match('(^([$@][-\\w]++|-?[.\\d]++)(?: *(?:[-*+]|div) *(?1))+$)', $expr))
89
		{
90 12
			return true;
91
		}
92
93
		// Try parsing the expression as a math expression
94
		try
95
		{
96 9
			return (bool) self::getXPathParser()->parse($expr, 'Math');
97
		}
98 5
		catch (RuntimeException $e)
99
		{
100
			// Do nothing
101
		}
102
103 5
		return false;
104
	}
105
106
	/**
107
	* Remove extraneous space in a given XPath expression
108
	*
109
	* @param  string $expr Original XPath expression
110
	* @return string       Minified XPath expression
111
	*/
112 16
	public static function minify($expr)
113
	{
114 16
		$old     = $expr;
115 16
		$strings = [];
116
117
		// Trim the surrounding whitespace then temporarily remove literal strings
118 16
		$expr = preg_replace_callback(
119 16
			'/"[^"]*"|\'[^\']*\'/',
120
			function ($m) use (&$strings)
121
			{
122 1
				$uniqid = '(' . sha1(uniqid()) . ')';
123 1
				$strings[$uniqid] = $m[0];
124
125 1
				return $uniqid;
126 16
			},
127 16
			trim($expr)
128
		);
129
130 16
		if (preg_match('/[\'"]/', $expr))
131
		{
132 1
			throw new RuntimeException("Cannot parse XPath expression '" . $old . "'");
133
		}
134
135
		// Normalize whitespace to a single space
136 15
		$expr = preg_replace('/\\s+/', ' ', $expr);
137
138
		// Remove the space between a non-word character and a word character
139 15
		$expr = preg_replace('/([-a-z_0-9]) ([^-a-z_0-9])/i', '$1$2', $expr);
140 15
		$expr = preg_replace('/([^-a-z_0-9]) ([-a-z_0-9])/i', '$1$2', $expr);
141
142
		// Remove the space between two non-word characters as long as they're not two -
143 15
		$expr = preg_replace('/(?!- -)([^-a-z_0-9]) ([^-a-z_0-9])/i', '$1$2', $expr);
144
145
		// Remove the space between a - and a word character, as long as there's a space before -
146 15
		$expr = preg_replace('/ - ([a-z_0-9])/i', ' -$1', $expr);
147
148
		// Remove the spaces between a number and a div or "-" operator and the next token
149 15
		$expr = preg_replace('/(?:^|[ \\(])\\d+\\K (div|-) ?/', '$1', $expr);
150
151
		// Remove the space between the div operator the next token
152 15
		$expr = preg_replace('/([^-a-z_0-9]div) (?=[$0-9@])/', '$1', $expr);
153
154
		// Restore the literals
155 15
		$expr = strtr($expr, $strings);
156
157 15
		return $expr;
158
	}
159
160
	/**
161
	* Parse an XPath expression that is composed entirely of equality tests between a variable part
162
	* and a constant part
163
	*
164
	* @param  string      $expr
165
	* @return array|false
166
	*/
167 8
	public static function parseEqualityExpr($expr)
168
	{
169
		// Match an equality between a variable and a literal or the concatenation of strings
170
		$eq = '(?<equality>'
171
		    . '(?<key>@[-\\w]+|\\$\\w+|\\.)'
172
		    . '(?<operator>\\s*=\\s*)'
173
		    . '(?:'
174
		    . '(?<literal>(?<string>"[^"]*"|\'[^\']*\')|0|[1-9][0-9]*)'
175
		    . '|'
176
		    . '(?<concat>concat\\(\\s*(?&string)\\s*(?:,\\s*(?&string)\\s*)+\\))'
177
		    . ')'
178
		    . '|'
179
		    . '(?:(?<literal>(?&literal))|(?<concat>(?&concat)))(?&operator)(?<key>(?&key))'
180 8
		    . ')';
181
182
		// Match a string that is entirely composed of equality checks separated with "or"
183 8
		$regexp = '(^(?J)\\s*' . $eq . '\\s*(?:or\\s*(?&equality)\\s*)*$)';
184 8
		if (!preg_match($regexp, $expr))
185
		{
186 1
			return false;
187
		}
188
189 7
		preg_match_all("((?J)$eq)", $expr, $matches, PREG_SET_ORDER);
190
191 7
		$map = [];
192 7
		foreach ($matches as $m)
193
		{
194 7
			$key   = $m['key'];
195 7
			$value = (!empty($m['concat']))
196 1
			       ? self::evaluateConcat($m['concat'])
197 7
			       : self::evaluateLiteral($m['literal']);
198
199 7
			$map[$key][] = $value;
200
		}
201
202 7
		return $map;
203
	}
204
205
	/**
206
	* Evaluate a concat() expression where all arguments are string literals
207
	*
208
	* @param  string $expr concat() expression
209
	* @return string       Expression's value
210
	*/
211 1
	protected static function evaluateConcat($expr)
212
	{
213 1
		preg_match_all('(\'[^\']*\'|"[^"]*")', $expr, $strings);
214
215 1
		$value = '';
216 1
		foreach ($strings[0] as $string)
217
		{
218 1
			$value .= substr($string, 1, -1);
219
		}
220
221 1
		return $value;
222
	}
223
224
	/**
225
	* Evaluate an XPath literal
226
	*
227
	* @param  string $expr XPath literal
228
	* @return string       Literal's string value
229
	*/
230 6
	protected static function evaluateLiteral($expr)
231
	{
232 6
		if ($expr[0] === '"' || $expr[0] === "'")
233
		{
234 6
			$expr = substr($expr, 1, -1);
235
		}
236
237 6
		return $expr;
238
	}
239
240
	/**
241
	* Generate and return a cached XPath parser with a default set of matchers
242
	*
243
	* @return RecursiveParser
244
	*/
245 9
	protected static function getXPathParser()
246
	{
247 9
		static $parser;
248 9
		if (!isset($parser))
249
		{
250 1
			$parser     = new RecursiveParser;
251 1
			$matchers   = [];
252 1
			$matchers[] = new BooleanFunctions($parser);
253 1
			$matchers[] = new BooleanOperators($parser);
254 1
			$matchers[] = new Comparisons($parser);
255 1
			$matchers[] = new Core($parser);
256 1
			$matchers[] = new Math($parser);
257 1
			$matchers[] = new SingleByteStringFunctions($parser);
258
259 1
			$parser->setMatchers($matchers);
260
		}
261
262 9
		return $parser;
263
	}
264
}