Completed
Branch XPathConvertorRefactor (0e46c7)
by Josh
09:34
created

Runner   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 226
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
wmc 22
lcom 1
cbo 9
dl 0
loc 226
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A convert() 0 12 2
A getType() 0 6 2
A setConvertors() 0 23 3
A addConvertor() 0 18 3
A getArguments() 0 12 2
A getDefaultConvertors() 0 17 2
A getMatch() 0 15 5
A insertCaptureNames() 0 13 1
A sortRegexps() 0 10 1
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\RendererGenerators\PHP\XPathConvertor;
9
10
use RuntimeException;
11
use s9e\TextFormatter\Configurator\RendererGenerators\PHP\XPathConvertor\Convertors\AbstractConvertor;
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\SingleByteStringFunctions;
19
use s9e\TextFormatter\Configurator\RendererGenerators\PHP\XPathConvertor\Convertors\SingleByteStringManipulation;
20
21
class Runner
22
{
23
	/**
24
	* @var array
25
	*/
26
	protected $callbacks;
27
28
	/**
29
	* @var array
30
	*/
31
	protected $groups;
32
33
	/**
34
	* @var array
35
	*/
36
	protected $matchGroup;
37
38
	/**
39
	* @var string
40
	*/
41
	protected $regexp = '((?!))';
42
43
	/**
44
	* @var array
45
	*/
46
	protected $regexps;
47
48
	/**
49
	* Constructor
50
	*
51
	* @return void
52
	*/
53
	public function __construct()
54
	{
55
		$this->setConvertors($this->getDefaultConvertors());
56
	}
57
58
	/**
59
	* Convert given XPath expression to PHP
60
	*
61
	* @param  string $expr
62
	* @return string
63
	*/
64
	public function convert($expr)
65
	{
66
		$match = $this->getMatch($expr);
67
		if (!isset($match))
68
		{
69
			throw new RuntimeException("Cannot convert '" . $expr . "'");
70
		}
71
72
		list($name, $args) = $match;
73
74
		return call_user_func_array($this->callbacks[$name], $args);
75
	}
76
77
	/**
78
	* Return the type/group associated with given XPath expression
79
	*
80
	* @param  string      $expr XPath expression
81
	* @return string|null       Expression's type/group, or NULL if unknown
82
	*/
83
	public function getType($expr)
84
	{
85
		$match = $this->getMatch($expr);
86
87
		return (isset($match, $this->matchGroup[$match[0]])) ? $this->matchGroup[$match[0]] : null;
88
	}
89
90
	/**
91
	* Set the list of convertors used by this instance
92
	*
93
	* @param  AbstractConvertor[] $convertors
94
	* @return void
95
	*/
96
	public function setConvertors(array $convertors)
97
	{
98
		$this->callbacks  = [];
99
		$this->matchGroup = [];
100
		$this->groups     = [];
101
		$this->regexps    = [];
102
		foreach ($convertors as $convertor)
103
		{
104
			$this->addConvertor($convertor);
105
		}
106
107
		// Sort regexps by length to keep their order consistent
108
		$this->sortRegexps();
109
110
		// Add regexp groups
111
		foreach ($this->groups as $group => $captures)
112
		{
113
			sort($captures);
114
			$this->regexps[$group] = '(?<' . $group . '>' . implode('|', $captures) . ')';
115
		}
116
117
		$this->regexp = '(^(?:' . implode('|', $this->regexps) . ')$)';
118
	}
119
120
	/**
121
	* Add a convertor to the list used by this instance
122
	*
123
	* @param  AbstractConvertor $convertor
124
	* @return void
125
	*/
126
	protected function addConvertor(AbstractConvertor $convertor)
127
	{
128
		foreach ($convertor->getRegexpGroups() as $name => $group)
129
		{
130
			$this->matchGroup[$name] = $group;
131
			$this->groups[$group][]  = '(?&' . $name . ')';
132
		}
133
134
		foreach ($convertor->getRegexps() as $name => $regexp)
135
		{
136
			$regexp = $this->insertCaptureNames($name, $regexp);
137
			$regexp = str_replace(' ', '\\s*', $regexp);
138
			$regexp = '(?<' . $name . '>' . $regexp . ')';
139
140
			$this->callbacks[$name] = [$convertor, 'convert' . $name];
141
			$this->regexps[$name]   = $regexp;
142
		}
143
	}
144
145
	/**
146
	* Get the list of arguments produced by a regexp's match
147
	*
148
	* @param  string[] $matches Regexp matches
149
	* @param  string   $name    Regexp name
150
	* @return string[]
151
	*/
152
	protected function getArguments(array $matches, $name)
153
	{
154
		$args = [];
155
		$i    = 0;
156
		while (isset($matches[$name . $i]))
157
		{
158
			$args[] = $matches[$name . $i];
159
			++$i;
160
		}
161
162
		return $args;
163
	}
164
165
	/**
166
	* Return the default list of convertors
167
	*
168
	* @return AbstractConvertor[]
169
	*/
170
	protected function getDefaultConvertors()
171
	{
172
		$convertors   = [];
173
		$convertors[] = new BooleanFunctions($this);
174
		$convertors[] = new BooleanOperators($this);
175
		$convertors[] = new Comparisons($this);
176
		$convertors[] = new Core($this);
177
		$convertors[] = new Math($this);
178
		if (extension_loaded('mbstring'))
179
		{
180
			$convertors[] = new MultiByteStringManipulation($this);
181
		}
182
		$convertors[] = new SingleByteStringFunctions($this);
183
		$convertors[] = new SingleByteStringManipulation($this);
184
185
		return $convertors;
186
	}
187
188
	/**
189
	* Get the match generated by this instance's regexp on given XPath expression
190
	*
191
	* @param  string     $expr XPath expression
192
	* @return array|null       Array of [<match name>, <arguments>] or NULL
193
	*/
194
	protected function getMatch($expr)
195
	{
196
		if (preg_match($this->regexp, $expr, $m))
197
		{
198
			foreach ($m as $name => $match)
199
			{
200
				if ($match !== '' && isset($this->callbacks[$name]))
201
				{
202
					return [$name, $this->getArguments($m, $name)];
203
				}
204
			}
205
		}
206
207
		return null;
208
	}
209
210
	/**
211
	* Insert capture names into given regexp
212
	*
213
	* @param  string $name   Name of the regexp, used to name captures
214
	* @param  string $regexp Original regexp
215
	* @return string         Modified regexp
216
	*/
217
	protected function insertCaptureNames($name, $regexp)
218
	{
219
		$i = 0;
220
221
		return preg_replace_callback(
222
			'((?<!\\\\)\\((?!\\?))',
223
			function ($m) use (&$i, $name)
224
			{
225
				return '(?<' . $name . $i++ . '>';
226
			},
227
			$regexp
228
		);
229
	}
230
231
	/**
232
	* Sort regexps by length
233
	*
234
	* @return void
235
	*/
236
	protected function sortRegexps()
237
	{
238
		uasort(
239
			$this->regexps,
240
			function ($a, $b)
241
			{
242
				return strlen($b) - strlen($a);
243
			}
244
		);
245
	}
246
}