Completed
Branch XPathConvertorRefactor (245ee2)
by Josh
12:06
created

XPathHelper::isExpressionNumeric()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2018 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
	public static function getVariables($expr)
58
	{
59
		// First, remove strings' contents to prevent false-positives
60
		$expr = preg_replace('/(["\']).*?\\1/s', '$1$1', $expr);
61
62
		// Capture all the variable names
63
		preg_match_all('/\\$(\\w+)/', $expr, $matches);
64
65
		// Dedupe and sort names
66
		$varNames = array_unique($matches[1]);
67
		sort($varNames);
68
69
		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
	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
		$expr = strrev(preg_replace('(\\((?!\\s*(?!vid(?!\\w))\\w))', ' ', strrev($expr)));
84
		$expr = str_replace(')', ' ', $expr);
85
		if (preg_match('(^\\s*([$@][-\\w]++|-?\\.\\d++|-?\\d++(?:\\.\\d++)?)(?>\\s*(?>[-+*]|div)\\s*(?1))++\\s*$)', $expr))
86
		{
87
			return true;
88
		}
89
90
		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
	public static function minify($expr)
100
	{
101
		$old     = $expr;
102
		$strings = [];
103
104
		// Trim the surrounding whitespace then temporarily remove literal strings
105
		$expr = preg_replace_callback(
106
			'/"[^"]*"|\'[^\']*\'/',
107
			function ($m) use (&$strings)
108
			{
109
				$uniqid = '(' . sha1(uniqid()) . ')';
110
				$strings[$uniqid] = $m[0];
111
112
				return $uniqid;
113
			},
114
			trim($expr)
115
		);
116
117
		if (preg_match('/[\'"]/', $expr))
118
		{
119
			throw new RuntimeException("Cannot parse XPath expression '" . $old . "'");
120
		}
121
122
		// Normalize whitespace to a single space
123
		$expr = preg_replace('/\\s+/', ' ', $expr);
124
125
		// Remove the space between a non-word character and a word character
126
		$expr = preg_replace('/([-a-z_0-9]) ([^-a-z_0-9])/i', '$1$2', $expr);
127
		$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
		$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
		$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
		$expr = preg_replace('/((?:^|[ \\(])\\d+) div ?/', '$1div', $expr);
137
138
		// Remove the space between the div operator the next token
139
		$expr = preg_replace('/([^-a-z_0-9]div) (?=[$0-9@])/', '$1', $expr);
140
141
		// Restore the literals
142
		$expr = strtr($expr, $strings);
143
144
		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
	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
		    . ')';
168
169
		// Match a string that is entirely composed of equality checks separated with "or"
170
		$regexp = '(^(?J)\\s*' . $eq . '\\s*(?:or\\s*(?&equality)\\s*)*$)';
171
172
		if (!preg_match($regexp, $expr))
173
		{
174
			return false;
175
		}
176
177
		preg_match_all("((?J)$eq)", $expr, $matches, PREG_SET_ORDER);
178
179
		$map = [];
180
		foreach ($matches as $m)
181
		{
182
			$key = $m['key'];
183
			if (!empty($m['concat']))
184
			{
185
				preg_match_all('(\'[^\']*\'|"[^"]*")', $m['concat'], $strings);
186
187
				$value = '';
188
				foreach ($strings[0] as $string)
189
				{
190
					$value .= substr($string, 1, -1);
191
				}
192
			}
193
			else
194
			{
195
				$value = $m['literal'];
196
				if ($value[0] === "'" || $value[0] === '"')
197
				{
198
					$value = substr($value, 1, -1);
199
				}
200
			}
201
202
			$map[$key][] = $value;
203
		}
204
205
		return $map;
206
	}
207
}