Completed
Push — master ( 9f0299...84da34 )
by Josh
15:47
created

XPathHelper   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 214
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 0

Test Coverage

Coverage 85.07%

Importance

Changes 0
Metric Value
wmc 16
lcom 0
cbo 0
dl 0
loc 214
ccs 57
cts 67
cp 0.8507
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A decodeStrings() 0 11 1
A encodeStrings() 0 11 1
A getVariables() 0 14 1
A isExpressionNumeric() 0 14 2
A minify() 0 47 2
A parseEqualityExpr() 0 37 4
A evaluateConcat() 0 12 2
A evaluateLiteral() 0 9 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\Utils\XPath;
12
13
abstract class XPathHelper
14
{
15
	/**
16
	* Decode strings inside of an XPath expression
17
	*
18
	* @param  string $expr
19
	* @return string
20
	*/
21
	public static function decodeStrings($expr)
22
	{
23
		return preg_replace_callback(
24
			'(([\'"])(.*?)\\1)s',
25
			function ($m)
26
			{
27
				return $m[1] . hex2bin($m[2]) . $m[1];
28
			},
29
			$expr
30
		);
31
	}
32
33
	/**
34
	* Encode strings inside of an XPath expression
35
	*
36
	* @param  string $expr
37
	* @return string
38
	*/
39
	public static function encodeStrings($expr)
40
	{
41
		return preg_replace_callback(
42
			'(([\'"])(.*?)\\1)s',
43
			function ($m)
44
			{
45
				return $m[1] . bin2hex($m[2]) . $m[1];
46
			},
47
			$expr
48
		);
49
	}
50
51
	/**
52
	* Return the list of variables used in a given XPath expression
53
	*
54
	* @param  string $expr XPath expression
55
	* @return array        Alphabetically sorted list of unique variable names
56
	*/
57 4
	public static function getVariables($expr)
58
	{
59
		// First, remove strings' contents to prevent false-positives
60 4
		$expr = preg_replace('/(["\']).*?\\1/s', '$1$1', $expr);
61
62
		// Capture all the variable names
63 4
		preg_match_all('/\\$(\\w+)/', $expr, $matches);
64
65
		// Dedupe and sort names
66 4
		$varNames = array_unique($matches[1]);
67 4
		sort($varNames);
68
69 4
		return $varNames;
70
	}
71
72
	/**
73
	* Determine whether given XPath expression definitely evaluates to a number
74
	*
75
	* @param  string $expr XPath expression
76
	* @return bool         Whether given XPath expression definitely evaluates to a number
77
	*/
78 20
	public static function isExpressionNumeric($expr)
79
	{
80
		// Trim the expression and remove parentheses that are not part of a function call. PCRE
81
		// does not support lookbehind assertions of variable length so we have to flip the string.
82
		// We exclude the XPath operator "div" (flipped into "vid") to avoid false positives
83 20
		$expr = strrev(preg_replace('(\\((?!\\s*(?!vid(?!\\w))\\w))', ' ', strrev($expr)));
84 20
		$expr = str_replace(')', ' ', $expr);
85 20
		if (preg_match('(^\\s*([$@][-\\w]++|-?\\.\\d++|-?\\d++(?:\\.\\d++)?)(?>\\s*(?>[-+*]|div)\\s*(?1))++\\s*$)', $expr))
86
		{
87 15
			return true;
88
		}
89
90 5
		return false;
91
	}
92
93
	/**
94
	* Remove extraneous space in a given XPath expression
95
	*
96
	* @param  string $expr Original XPath expression
97
	* @return string       Minified XPath expression
98
	*/
99 14
	public static function minify($expr)
100
	{
101 14
		$old     = $expr;
102 14
		$strings = [];
103
104
		// Trim the surrounding whitespace then temporarily remove literal strings
105 14
		$expr = preg_replace_callback(
106 14
			'/"[^"]*"|\'[^\']*\'/',
107
			function ($m) use (&$strings)
108
			{
109 1
				$uniqid = '(' . sha1(uniqid()) . ')';
110 1
				$strings[$uniqid] = $m[0];
111
112 1
				return $uniqid;
113 14
			},
114 14
			trim($expr)
115
		);
116
117 14
		if (preg_match('/[\'"]/', $expr))
118
		{
119 1
			throw new RuntimeException("Cannot parse XPath expression '" . $old . "'");
120
		}
121
122
		// Normalize whitespace to a single space
123 13
		$expr = preg_replace('/\\s+/', ' ', $expr);
124
125
		// Remove the space between a non-word character and a word character
126 13
		$expr = preg_replace('/([-a-z_0-9]) ([^-a-z_0-9])/i', '$1$2', $expr);
127 13
		$expr = preg_replace('/([^-a-z_0-9]) ([-a-z_0-9])/i', '$1$2', $expr);
128
129
		// Remove the space between two non-word characters as long as they're not two -
130 13
		$expr = preg_replace('/(?!- -)([^-a-z_0-9]) ([^-a-z_0-9])/i', '$1$2', $expr);
131
132
		// Remove the space between a - and a word character, as long as there's a space before -
133 13
		$expr = preg_replace('/ - ([a-z_0-9])/i', ' -$1', $expr);
134
135
		// Remove the spaces between a number and the div operator and the next token
136 13
		$expr = preg_replace('/((?:^|[ \\(])\\d+) div ?/', '$1div', $expr);
137
138
		// Remove the space between the div operator the next token
139 13
		$expr = preg_replace('/([^-a-z_0-9]div) (?=[$0-9@])/', '$1', $expr);
140
141
		// Restore the literals
142 13
		$expr = strtr($expr, $strings);
143
144 13
		return $expr;
145
	}
146
147
	/**
148
	* Parse an XPath expression that is composed entirely of equality tests between a variable part
149
	* and a constant part
150
	*
151
	* @param  string      $expr
152
	* @return array|false
153
	*/
154 8
	public static function parseEqualityExpr($expr)
155
	{
156
		// Match an equality between a variable and a literal or the concatenation of strings
157
		$eq = '(?<equality>'
158
		    . '(?<key>@[-\\w]+|\\$\\w+|\\.)'
159
		    . '(?<operator>\\s*=\\s*)'
160
		    . '(?:'
161
		    . '(?<literal>(?<string>"[^"]*"|\'[^\']*\')|0|[1-9][0-9]*)'
162
		    . '|'
163
		    . '(?<concat>concat\\(\\s*(?&string)\\s*(?:,\\s*(?&string)\\s*)+\\))'
164
		    . ')'
165
		    . '|'
166
		    . '(?:(?<literal>(?&literal))|(?<concat>(?&concat)))(?&operator)(?<key>(?&key))'
167 8
		    . ')';
168
169
		// Match a string that is entirely composed of equality checks separated with "or"
170 8
		$regexp = '(^(?J)\\s*' . $eq . '\\s*(?:or\\s*(?&equality)\\s*)*$)';
171 8
		if (!preg_match($regexp, $expr))
172
		{
173 1
			return false;
174
		}
175
176 7
		preg_match_all("((?J)$eq)", $expr, $matches, PREG_SET_ORDER);
177
178 7
		$map = [];
179 7
		foreach ($matches as $m)
180
		{
181 7
			$key   = $m['key'];
182 7
			$value = (!empty($m['concat']))
183 1
			       ? self::evaluateConcat($m['concat'])
184 7
			       : self::evaluateLiteral($m['literal']);
185
186 7
			$map[$key][] = $value;
187
		}
188
189 7
		return $map;
190
	}
191
192
	/**
193
	* Evaluate a concat() expression where all arguments are string literals
194
	*
195
	* @param  string $expr concat() expression
196
	* @return string       Expression's value
197
	*/
198 1
	protected static function evaluateConcat($expr)
199
	{
200 1
		preg_match_all('(\'[^\']*\'|"[^"]*")', $expr, $strings);
201
202 1
		$value = '';
203 1
		foreach ($strings[0] as $string)
204
		{
205 1
			$value .= substr($string, 1, -1);
206
		}
207
208 1
		return $value;
209
	}
210
211
	/**
212
	* Evaluate an XPath literal
213
	*
214
	* @param  string $expr XPath literal
215
	* @return string       Literal's string value
216
	*/
217 6
	protected static function evaluateLiteral($expr)
218
	{
219 6
		if ($expr[0] === '"' || $expr[0] === "'")
220
		{
221 6
			$expr = substr($expr, 1, -1);
222
		}
223
224 6
		return $expr;
225
	}
226
}