Passed
Push — master ( 1aaaa0...b6fb13 )
by Josh
02:18
created

RecursiveParser::insertCaptureNames()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
1
<?php declare(strict_types=1);
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2023 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 Match names associated with each group
22
	*/
23
	protected $groupMatches = [];
24
25
	/**
26
	* @var array Groups associated with each match name
27
	*/
28
	protected $matchGroups = [];
29
30
	/**
31
	* @var string Regexp used to match input
32
	*/
33
	protected $regexp;
34
35
	/**
36
	* Parse given string
37
	*
38
	* @param  string $str
39
	* @param  string $name Allowed match, either match name or group name (default: allow all)
40
	* @return mixed
41
	*/
42 5
	public function parse(string $str, string $name = '')
43
	{
44 5
		$regexp = $this->regexp;
45 5
		if ($name !== '')
46
		{
47 1
			$restrict = (isset($this->groupMatches[$name])) ? implode('|', $this->groupMatches[$name]) : $name;
48 1
			$regexp   = preg_replace('(\\(\\?<(?!(?:' . $restrict . '|\\w+\\d+)>))', '(*F)$0', $regexp);
49
		}
50
51 5
		preg_match($regexp, $str, $m);
52 5
		if (!isset($m['MARK']))
53
		{
54 1
			throw new RuntimeException('Cannot parse ' . var_export($str, true));
55
		}
56
57 4
		$name = $m['MARK'];
58 4
		$args = $this->getArguments($m, $name);
59
60
		return [
61 4
			'groups' => $this->matchGroups[$name] ?? [],
62 4
			'match'  => $name,
63 4
			'value'  => call_user_func_array($this->callbacks[$name], $args)
64
		];
65
	}
66
67
	/**
68
	* Set the list of matchers used by this parser
69
	*
70
	* @param  MatcherInterface[]
71
	* @return void
72
	*/
73 5
	public function setMatchers(array $matchers): void
74
	{
75 5
		$matchRegexps       = [];
76 5
		$this->groupMatches = [];
77 5
		$this->matchGroups  = [];
78 5
		foreach ($this->getMatchersConfig($matchers) as $matchName => $matchConfig)
79
		{
80 5
			foreach ($matchConfig['groups'] as $group)
81
			{
82 5
				$this->groupMatches[$group][] = $matchName;
83
			}
84
85 5
			$regexp = $matchConfig['regexp'];
86 5
			$regexp = str_replace(' ', '\\s*+', $regexp);
87 5
			$regexp = '(?<' . $matchName  . '>' . $regexp . ')(*:' . $matchName  . ')';
88 5
89
			$matchRegexps[]                = $regexp;
90 5
			$this->callbacks[$matchName]   = $matchConfig['callback'];
91 5
			$this->matchGroups[$matchName] = $matchConfig['groups'];
92 5
		}
93
94
		$groupRegexps = [];
95 5
		foreach ($this->groupMatches as $group => $names)
96 5
		{
97
			$groupRegexps[] = '(?<' . $group . '>(?&' . implode(')|(?&', $names) . '))';
98 5
		}
99
100
		$this->regexp = '((?(DEFINE)' . implode('', $groupRegexps). ')'
101 5
		              . '^(?:' . implode('|', $matchRegexps) . ')$)s';
102 5
	}
103
104
	/**
105
	* Get the list of arguments produced by a regexp's match
106
	*
107
	* @param  string[] $matches Regexp matches
108
	* @param  string   $name    Regexp name
109
	* @return string[]
110
	*/
111
	protected function getArguments(array $matches, string $name): array
112 4
	{
113
		$args    = [];
114 4
		$collect = false;
115 4
		foreach ($matches as $k => $v)
116 4
		{
117
			if ($k === $name)
118 4
			{
119 4
				// Start collecting matches once we reach the target capture
120
				$collect = true;
121
			}
122 4
			elseif ($collect)
123
			{
124
				if (!is_int($k))
125
				{
126
					// Stop collecting when we reach the next capture
127
					break;
128
				}
129
				$args[] = $v;
130
			}
131 5
		}
132
133 5
		// Remove the first entry, which contains the whole string that was matched
134 5
		array_shift($args);
135
136 5
		return $args;
137
	}
138 5
139
	/**
140 5
	* Collect, normalize, sort and return the config for all matchers
141
	*
142 5
	* @param  MatcherInterface[] $matchers
143 5
	* @return array
144
	*/
145 5
	protected function getMatchersConfig(array $matchers): array
146
	{
147 5
		$matchersConfig = [];
148
		foreach ($matchers as $matcher)
149 5
		{
150 5
			foreach ($matcher->getMatchers() as $matchName => $matchConfig)
151 5
			{
152
				if (is_string($matchConfig))
153 5
				{
154
					$matchConfig = ['regexp' => $matchConfig];
155
				}
156 5
				$parts       = explode(':', $matchName);
157
				$matchName   = array_pop($parts);
158 5
				$matchConfig += [
159
					'callback' => [$matcher, 'parse' . $matchName],
160
					'groups'   => [],
161
					'order'    => 0
162
				];
163
				$matchConfig['name']   = $matchName;
164
				$matchConfig['groups'] = array_unique(array_merge($matchConfig['groups'], $parts));
165
				sort($matchConfig['groups']);
166
167
				$matchersConfig[$matchName] = $matchConfig;
168 5
			}
169
		}
170 5
		uasort($matchersConfig, static::class . '::sortMatcherConfig');
171
172 5
		return $matchersConfig;
173 5
	}
174 5
175
	/**
176 5
	* Compare two matchers' config
177 5
	*
178
	* @param  array $a
179
	* @param  array $b
180
	* @return integer
181
	*/
182
	protected static function sortMatcherConfig(array $a, array $b): int
183
	{
184
		if ($a['order'] !== $b['order'])
185
		{
186
			return $a['order'] - $b['order'];
187
		}
188
189 5
		return strcmp($a['name'], $b['name']);
190
	}
191
}