XPathConvertor   A
last analyzed

Complexity

Total Complexity 20

Size/Duplication

Total Lines 220
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 20
eloc 65
dl 0
loc 220
ccs 66
cts 66
cp 1
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A exportXPathFragment() 0 3 1
A getDefaultParser() 0 23 3
A exportXPathParam() 0 5 1
A convertXPath() 0 19 3
A convertCondition() 0 33 3
A exportXPathCurrent() 0 3 1
A getParser() 0 8 2
A __construct() 0 9 2
A exportXPath() 0 10 2
A tokenizeXPathForExport() 0 16 2
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) The s9e authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP;
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\MultiByteStringManipulation;
18
use s9e\TextFormatter\Configurator\RendererGenerators\PHP\XPathConvertor\Convertors\PHP80Functions;
19
use s9e\TextFormatter\Configurator\RendererGenerators\PHP\XPathConvertor\Convertors\SingleByteStringFunctions;
20
use s9e\TextFormatter\Configurator\RendererGenerators\PHP\XPathConvertor\Convertors\SingleByteStringManipulation;
21
22
class XPathConvertor
23
{
24
	/**
25
	* @var array Array of togglable PHP features ("mbstring" and "php80")
26
	*/
27
	public $features;
28
29
	/**
30
	* @var RecursiveParser
31
	*/
32
	protected $parser;
33
34
	/**
35
	* Constructor
36
	*/
37 75
	public function __construct(?RecursiveParser $parser = null)
38
	{
39 75
		$this->features = [
40 75
			'mbstring' => extension_loaded('mbstring'),
41 75
			'php80'    => version_compare(PHP_VERSION, '8.0', '>=')
42
		];
43 75
		if (isset($parser))
44
		{
45 1
			$this->parser = $parser;
46
		}
47
	}
48
49
	/**
50
	* Convert an XPath expression (used in a condition) into PHP code
51
	*
52
	* This method is similar to convertXPath() but it selectively replaces some simple conditions
53
	* with the corresponding DOM method for performance reasons
54
	*
55
	* @param  string $expr XPath expression
56
	* @return string       PHP code
57
	*/
58 35
	public function convertCondition($expr)
59
	{
60
		// Replace @attr with boolean(@attr) in boolean expressions
61 35
		$expr = preg_replace(
62 35
			'((^|(?<!\\bboolean)\\(\\s*|\\b(?:and|or)\\s*)([\\(\\s]*)([$@][-\\w]+|@\\*)([\\)\\s]*)(?=$|\\s+(?:and|or)))',
63 35
			'$1$2boolean($3)$4',
64 35
			trim($expr)
65
		);
66
67
		// Replace not(boolean(@attr)) with not(@attr)
68 35
		$expr = preg_replace(
69 35
			'(not\\(boolean\\(([$@][-\\w]+)\\)\\))',
70 35
			'not($1)',
71
			$expr
72
		);
73
74
		try
75
		{
76 35
			return $this->getParser()->parse($expr)['value'];
77
		}
78 2
		catch (RuntimeException $e)
79
		{
80
			// Do nothing
81
		}
82
83
		// If the condition does not seem to contain a relational expression, or start with a
84
		// function call, we wrap it inside of a boolean() call
85 2
		if (!preg_match('([=<>]|\\bor\\b|\\band\\b|^[-\\w]+\\s*\\()', $expr))
86
		{
87 1
			$expr = 'boolean(' . $expr . ')';
88
		}
89
90 2
		return '$this->xpath->evaluate(' . $this->exportXPath($expr) . ',$node)';
91
	}
92
93
	/**
94
	* Convert an XPath expression (used as value) into PHP code
95
	*
96
	* @param  string $expr XPath expression
97
	* @return string       PHP code
98
	*/
99 40
	public function convertXPath($expr)
100
	{
101 40
		$expr = trim($expr);
102
		try
103
		{
104 40
			return $this->getParser()->parse($expr)['value'];
105
		}
106 4
		catch (RuntimeException $e)
107
		{
108
			// Do nothing
109
		}
110
111
		// Make sure the expression evaluates as a string
112 4
		if (!preg_match('(^[-\\w]*s(?:late|pace|tring)[-\\w]*\\()', $expr))
113
		{
114 1
			$expr = 'string(' . $expr . ')';
115
		}
116
117 4
		return '$this->xpath->evaluate(' . $this->exportXPath($expr) . ',$node)';
118
	}
119
120
	/**
121
	* Export an XPath expression as PHP with special consideration for XPath variables
122
	*
123
	* Will return PHP source representing the XPath expression, with special consideration for XPath
124
	* variables which are returned as a method call to XPath::export()
125
	*
126
	* @param  string $expr XPath expression
127
	* @return string       PHP representation of the expression
128
	*/
129 6
	protected function exportXPath($expr)
130
	{
131 6
		$phpTokens = [];
132 6
		foreach ($this->tokenizeXPathForExport($expr) as [$type, $content])
133
		{
134 6
			$methodName  = 'exportXPath' . $type;
135 6
			$phpTokens[] = $this->$methodName($content);
136
		}
137
138 6
		return implode('.', $phpTokens);
139
	}
140
141
	/**
142
	* Convert a "current()" XPath expression to its PHP source representation
143
	*
144
	* @return string
145
	*/
146 1
	protected function exportXPathCurrent()
147
	{
148 1
		return '$node->getNodePath()';
149
	}
150
151
	/**
152
	* Convert a fragment of an XPath expression to its PHP source representation
153
	*
154
	* @param  string $fragment
155
	* @return string
156
	*/
157 6
	protected function exportXPathFragment($fragment)
158
	{
159 6
		return var_export($fragment, true);
160
	}
161
162
	/**
163
	* Convert an XSLT parameter to its PHP source representation
164
	*
165
	* @param  string $param Parameter, including the leading $
166
	* @return string
167
	*/
168 1
	protected function exportXPathParam($param)
169
	{
170 1
		$paramName = ltrim($param, '$');
171
172 1
		return '$this->getParamAsXPath(' . var_export($paramName, true) . ')';
173
	}
174
175
	/**
176
	* Generate and return the a parser with the default set of matchers
177
	*
178
	* @return RecursiveParser
179
	*/
180 74
	protected function getDefaultParser()
181
	{
182 74
		$parser     = new RecursiveParser;
183 74
		$matchers   = [];
184 74
		$matchers[] = new SingleByteStringFunctions($parser);
185 74
		$matchers[] = new BooleanFunctions($parser);
186 74
		$matchers[] = new BooleanOperators($parser);
187 74
		$matchers[] = new Comparisons($parser);
188 74
		$matchers[] = new Core($parser);
189 74
		$matchers[] = new Math($parser);
190 74
		if (!empty($this->features['mbstring']))
191
		{
192 71
			$matchers[] = new MultiByteStringManipulation($parser);
193
		}
194 74
		$matchers[] = new SingleByteStringManipulation($parser);
195 74
		if (!empty($this->features['php80']))
196
		{
197 1
			$matchers[] = new PHP80Functions($parser);
198
		}
199
200 74
		$parser->setMatchers($matchers);
201
202 74
		return $parser;
203
	}
204
205
	/**
206
	* Return (and if necessary, create) the cached instance of the XPath parser
207
	*
208
	* @return RecursiveParser
209
	*/
210 75
	protected function getParser(): RecursiveParser
211
	{
212 75
		if (!isset($this->parser))
213
		{
214 74
			$this->parser = $this->getDefaultParser();
215
		}
216
217 75
		return $this->parser;
218
	}
219
220
	/**
221
	* Tokenize an XPath expression for use in PHP
222
	*
223
	* @param  string $expr XPath expression
224
	* @return array
225
	*/
226 6
	protected function tokenizeXPathForExport($expr)
227
	{
228
		$tokenExprs = [
229 6
			'(*:Current)\\bcurrent\\(\\)',
230
			'(*:Param)\\$\\w+',
231
			'(*:Fragment)(?:"[^"]*"|\'[^\']*\'|(?!current\\(\\)|\\$\\w).)++'
232
		];
233 6
		preg_match_all('(' . implode('|', $tokenExprs) . ')s', $expr, $matches, PREG_SET_ORDER);
234
235 6
		$tokens = [];
236 6
		foreach ($matches as $m)
237
		{
238 6
			$tokens[] = [$m['MARK'], $m[0]];
239
		}
240
241 6
		return $tokens;
242
	}
243
}