Completed
Push — master ( 7cd28a...9f85d3 )
by Josh
04:13
created

RecursiveParser::setMatchers()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 17
cts 17
cp 1
rs 9.456
c 0
b 0
f 0
cc 4
nc 6
nop 1
crap 4
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;
9
10
use RuntimeException;
11
use s9e\TextFormatter\Configurator\RecursiveParser\MatcherInterface;
12
13
class RecursiveParser
14
{
15
	/**
16
	* @var array Callback associated with each match name
17
	*/
18
	protected $callbacks = [];
19
20
	/**
21
	* @var array Groups associated with each match name
22
	*/
23
	protected $matchGroups = [];
24
25
	/**
26
	* @var string Regexp used to match input
27
	*/
28
	protected $regexp;
29
30
	/**
31
	* Parse given string
32
	*
33
	* @param  string $str
34
	* @param  string $restrict Pipe-separated list of allowed matches (ignored if empty)
35
	* @return mixed
36
	*/
37 4
	public function parse(string $str, string $restrict = '')
38
	{
39 4
		$regexp = $this->regexp;
40 4
		if ($restrict !== '')
41
		{
42
			// Restrict matches to given list and numbered arguments
43 1
			$regexp = preg_replace('(\\(\\?<(?!(?:' . $restrict . '|\\w+\\d+)>))', '(*FAIL)$0', $regexp);
44
		}
45
46 4
		preg_match($regexp, $str, $m);
47 4
		if (!isset($m['MARK']))
48
		{
49 1
			throw new RuntimeException('Cannot parse ' . var_export($str, true));
50
		}
51
52 3
		$name = $m['MARK'];
53 3
		$args = $this->getArguments($m, $name);
54
55
		return [
56 3
			'groups' => $this->matchGroups[$name] ?? [],
57 3
			'match'  => $name,
58 3
			'value'  => call_user_func_array($this->callbacks[$name], $args)
59
		];
60
	}
61
62
	/**
63
	* Set the list of matchers used by this parser
64
	*
65
	* @param  MatcherInterface[]
66
	* @return void
67
	*/
68 4
	public function setMatchers(array $matchers): void
69
	{
70 4
		$groupMatches      = [];
71 4
		$this->matchGroups = [];
72 4
		$this->regexp      = '(^(?:';
73 4
		foreach ($this->getMatchersConfig($matchers) as $matchName => $matchConfig)
74
		{
75 4
			foreach ($matchConfig['groups'] as $group)
76
			{
77 4
				$groupMatches[$group][] = $matchName;
78
			}
79
80 4
			$regexp = $matchConfig['regexp'];
81 4
			$regexp = $this->insertCaptureNames($matchName , $regexp);
82 4
			$regexp = str_replace(' ', '\\s*', $regexp);
83 4
			$regexp = '(?<' . $matchName  . '>' . $regexp . '(*:' . $matchName  . '))';
84
85 4
			$this->regexp                 .= $regexp . '|';
86 4
			$this->callbacks[$matchName]   = $matchConfig['callback'];
87 4
			$this->matchGroups[$matchName] = $matchConfig['groups'];
88
		}
89
90 4
		foreach ($groupMatches as $group => $names)
91
		{
92 4
			$this->regexp .= '(?<' . $group . '>(?&' . implode(')|(?&', $names) . '))|';
93
		}
94
95 4
		$this->regexp = substr($this->regexp, 0, -1) . ')$)s';
96
	}
97
98
	/**
99
	* Get the list of arguments produced by a regexp's match
100
	*
101
	* @param  string[] $matches Regexp matches
102
	* @param  string   $name    Regexp name
103
	* @return string[]
104
	*/
105 3
	protected function getArguments(array $matches, string $name): array
106
	{
107 3
		$args = [];
108 3
		$i    = 0;
109 3
		while (isset($matches[$name . $i]))
110
		{
111 3
			$args[] = $matches[$name . $i];
112 3
			++$i;
113
		}
114
115 3
		return $args;
116
	}
117
118
	/**
119
	* Collect, normalize, sort and return the config for all matchers
120
	*
121
	* @param  MatcherInterface[] $matchers
122
	* @return array
123
	*/
124 4
	protected function getMatchersConfig(array $matchers): array
125
	{
126 4
		$matchersConfig = [];
127 4
		foreach ($matchers as $matcher)
128
		{
129 4
			foreach ($matcher->getMatchers() as $matchName => $matchConfig)
130
			{
131 4
				if (is_string($matchConfig))
132
				{
133 4
					$matchConfig = ['regexp' => $matchConfig];
134
				}
135 4
				$parts       = explode(':', $matchName);
136 4
				$matchName   = array_pop($parts);
137
				$matchConfig += [
138 4
					'callback' => [$matcher, 'parse' . $matchName],
139
					'groups'   => [],
140 4
					'order'    => 0
141
				];
142 4
				$matchConfig['groups'] = array_unique(array_merge($matchConfig['groups'], $parts));
143 4
				sort($matchConfig['groups']);
144
145 4
				$matchersConfig[$matchName] = $matchConfig;
146
			}
147
		}
148 4
		uasort($matchersConfig, 'static::sortMatcherConfig');
149
150 4
		return $matchersConfig;
151
	}
152
153
	/**
154
	* Insert capture names into given regexp
155
	*
156
	* @param  string $name   Name of the regexp, used to name captures
157
	* @param  string $regexp Original regexp
158
	* @return string         Modified regexp
159
	*/
160 4
	protected function insertCaptureNames(string $name, string $regexp): string
161
	{
162 4
		$i = 0;
163
164 4
		return preg_replace_callback(
165 4
			'((?<!\\\\)\\((?!\\?))',
166
			function ($m) use (&$i, $name)
167
			{
168 4
				return '(?<' . $name . $i++ . '>';
169 4
			},
170
			$regexp
171
		);
172
	}
173
174
	/**
175
	* Compare two matchers' config
176
	*
177
	* @param  array $a
178
	* @param  array $b
179
	* @return integer
180
	*/
181 4
	protected static function sortMatcherConfig(array $a, array $b): int
182
	{
183 4
		return $a['order'] - $b['order'];
184
	}
185
}