1
|
|
|
<?php |
2
|
|
|
namespace Thunder\Shortcode\Processor; |
3
|
|
|
|
4
|
|
|
use Thunder\Shortcode\Event\ReplaceShortcodesEvent; |
5
|
|
|
use Thunder\Shortcode\Event\FilterShortcodesEvent; |
6
|
|
|
use Thunder\Shortcode\EventContainer\EventContainerInterface; |
7
|
|
|
use Thunder\Shortcode\Events; |
8
|
|
|
use Thunder\Shortcode\HandlerContainer\HandlerContainerInterface as Handlers; |
9
|
|
|
use Thunder\Shortcode\Parser\ParserInterface; |
10
|
|
|
use Thunder\Shortcode\Shortcode\ReplacedShortcode; |
11
|
|
|
use Thunder\Shortcode\Shortcode\ParsedShortcodeInterface; |
12
|
|
|
use Thunder\Shortcode\Shortcode\ProcessedShortcode; |
13
|
|
|
use Thunder\Shortcode\Shortcode\ShortcodeInterface; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* @author Tomasz Kowalczyk <[email protected]> |
17
|
|
|
*/ |
18
|
|
|
final class Processor implements ProcessorInterface |
19
|
|
|
{ |
20
|
|
|
/** @var Handlers */ |
21
|
|
|
private $handlers; |
22
|
|
|
/** @var ParserInterface */ |
23
|
|
|
private $parser; |
24
|
|
|
/** @var EventContainerInterface */ |
25
|
|
|
private $eventContainer; |
26
|
|
|
|
27
|
|
|
private $recursionDepth = null; // infinite recursion |
28
|
|
|
private $maxIterations = 1; // one iteration |
29
|
|
|
private $autoProcessContent = true; // automatically process shortcode content |
30
|
|
|
|
31
|
62 |
|
public function __construct(ParserInterface $parser, Handlers $handlers) |
32
|
|
|
{ |
33
|
62 |
|
$this->parser = $parser; |
34
|
62 |
|
$this->handlers = $handlers; |
35
|
62 |
|
} |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Entry point for shortcode processing. Implements iterative algorithm for |
39
|
|
|
* both limited and unlimited number of iterations. |
40
|
|
|
* |
41
|
|
|
* @param string $text Text to process |
42
|
|
|
* |
43
|
|
|
* @return string |
44
|
|
|
*/ |
45
|
55 |
|
public function process($text) |
46
|
|
|
{ |
47
|
55 |
|
$iterations = $this->maxIterations === null ? 1 : $this->maxIterations; |
48
|
55 |
|
$context = new ProcessorContext(); |
49
|
55 |
|
$context->processor = $this; |
50
|
|
|
|
51
|
55 |
|
while ($iterations--) { |
52
|
55 |
|
$context->iterationNumber++; |
53
|
55 |
|
$newText = $this->processIteration($text, $context, null); |
54
|
55 |
|
if ($newText === $text) { |
55
|
8 |
|
break; |
56
|
|
|
} |
57
|
51 |
|
$text = $newText; |
58
|
51 |
|
$iterations += $this->maxIterations === null ? 1 : 0; |
59
|
51 |
|
} |
60
|
|
|
|
61
|
55 |
|
return $text; |
62
|
|
|
} |
63
|
|
|
|
64
|
55 |
|
private function dispatchEvent($name, $event) |
65
|
|
|
{ |
66
|
55 |
|
if(null === $this->eventContainer) { |
67
|
50 |
|
return $event; |
68
|
|
|
} |
69
|
|
|
|
70
|
5 |
|
$handlers = $this->eventContainer->getListeners($name); |
71
|
5 |
|
foreach($handlers as $handler) { |
72
|
4 |
|
call_user_func_array($handler, array($event)); |
73
|
5 |
|
} |
74
|
|
|
|
75
|
5 |
|
return $event; |
76
|
|
|
} |
77
|
|
|
|
78
|
55 |
|
private function processIteration($text, ProcessorContext $context, ProcessedShortcode $parent = null) |
79
|
|
|
{ |
80
|
55 |
|
if (null !== $this->recursionDepth && $context->recursionLevel > $this->recursionDepth) { |
81
|
2 |
|
return $text; |
82
|
|
|
} |
83
|
|
|
|
84
|
55 |
|
$context->parent = $parent; |
85
|
55 |
|
$context->text = $text; |
86
|
55 |
|
$filterEvent = new FilterShortcodesEvent($this->parser->parse($text), $parent); |
87
|
55 |
|
$this->dispatchEvent(Events::FILTER_SHORTCODES, $filterEvent); |
88
|
55 |
|
$shortcodes = $filterEvent->getShortcodes(); |
89
|
55 |
|
$replaces = array(); |
90
|
55 |
|
$baseOffset = $parent && $shortcodes ? mb_strpos($parent->getShortcodeText(), $shortcodes[0]->getText()) - $shortcodes[0]->getOffset() + $parent->getOffset() : 0; |
|
|
|
|
91
|
55 |
|
foreach ($shortcodes as $shortcode) { |
92
|
55 |
|
$hasNamePosition = array_key_exists($shortcode->getName(), $context->namePosition); |
93
|
|
|
|
94
|
55 |
|
$context->baseOffset = $baseOffset + $shortcode->getOffset(); |
95
|
55 |
|
$context->position++; |
96
|
55 |
|
$context->namePosition[$shortcode->getName()] = $hasNamePosition ? $context->namePosition[$shortcode->getName()] + 1 : 1; |
97
|
55 |
|
$context->shortcodeText = $shortcode->getText(); |
98
|
55 |
|
$context->offset = $shortcode->getOffset(); |
99
|
55 |
|
$context->shortcode = $shortcode; |
100
|
55 |
|
$context->textContent = $shortcode->getContent(); |
101
|
|
|
|
102
|
55 |
|
$handler = $this->handlers->get($shortcode->getName()); |
103
|
55 |
|
$replace = $this->processHandler($shortcode, $context, $handler); |
104
|
|
|
|
105
|
55 |
|
$replaces[] = new ReplacedShortcode($shortcode, $replace); |
106
|
55 |
|
} |
107
|
55 |
|
$replaces = array_filter($replaces); |
108
|
|
|
|
109
|
55 |
|
$applyEvent = new ReplaceShortcodesEvent($text, $replaces, $parent); |
110
|
55 |
|
$this->dispatchEvent(Events::REPLACE_SHORTCODES, $applyEvent); |
111
|
|
|
|
112
|
55 |
|
return $applyEvent->hasResult() ? $applyEvent->getResult() : $this->applyReplaces($text, $replaces); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
private function applyReplaces($text, array $replaces) |
116
|
|
|
{ |
117
|
54 |
View Code Duplication |
return array_reduce(array_reverse($replaces), function($state, ReplacedShortcode $s) { |
|
|
|
|
118
|
54 |
|
$offset = $s->getOffset(); |
119
|
54 |
|
$length = mb_strlen($s->getText()); |
120
|
|
|
|
121
|
54 |
|
return mb_substr($state, 0, $offset).$s->getReplacement().mb_substr($state, $offset + $length); |
122
|
54 |
|
}, $text); |
123
|
|
|
} |
124
|
|
|
|
125
|
55 |
|
private function processHandler(ParsedShortcodeInterface $parsed, ProcessorContext $context, $handler) |
126
|
|
|
{ |
127
|
55 |
|
$processed = ProcessedShortcode::createFromContext(clone $context); |
128
|
55 |
|
$content = $this->processRecursion($processed, $context); |
129
|
55 |
|
$processed = $processed->withContent($content); |
130
|
|
|
|
131
|
|
|
return $handler |
132
|
55 |
|
? call_user_func_array($handler, array($processed)) |
133
|
55 |
|
: substr_replace($parsed->getText(), $processed->getContent(), strrpos($parsed->getText(), $parsed->getContent()), mb_strlen($parsed->getContent())); |
134
|
|
|
} |
135
|
|
|
|
136
|
55 |
|
private function processRecursion(ParsedShortcodeInterface $shortcode, ProcessorContext $context) |
137
|
|
|
{ |
138
|
55 |
|
if ($this->autoProcessContent && null !== $shortcode->getContent()) { |
139
|
45 |
|
$context->recursionLevel++; |
140
|
|
|
// this is safe from using max iterations value because it's manipulated in process() method |
141
|
45 |
|
$content = $this->processIteration($shortcode->getContent(), clone $context, $shortcode); |
|
|
|
|
142
|
45 |
|
$context->recursionLevel--; |
143
|
|
|
|
144
|
45 |
|
return $content; |
145
|
|
|
} |
146
|
|
|
|
147
|
27 |
|
return $shortcode->getContent(); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Container for event handlers used in this processor. |
152
|
|
|
* |
153
|
|
|
* @param EventContainerInterface $eventContainer |
154
|
|
|
* |
155
|
|
|
* @return self |
156
|
|
|
*/ |
157
|
8 |
|
public function withEventContainer(EventContainerInterface $eventContainer) |
158
|
|
|
{ |
159
|
8 |
|
$self = clone $this; |
160
|
8 |
|
$self->eventContainer = $eventContainer; |
161
|
|
|
|
162
|
8 |
|
return $self; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Recursion depth level, null means infinite, any integer greater than or |
167
|
|
|
* equal to zero sets value (number of recursion levels). Zero disables |
168
|
|
|
* recursion. Defaults to null. |
169
|
|
|
* |
170
|
|
|
* @param int|null $depth |
171
|
|
|
* |
172
|
|
|
* @return self |
173
|
|
|
*/ |
174
|
3 |
View Code Duplication |
public function withRecursionDepth($depth) |
|
|
|
|
175
|
|
|
{ |
176
|
3 |
|
if (null !== $depth && !(is_int($depth) && $depth >= 0)) { |
177
|
1 |
|
$msg = 'Recursion depth must be null (infinite) or integer >= 0!'; |
178
|
1 |
|
throw new \InvalidArgumentException($msg); |
179
|
|
|
} |
180
|
|
|
|
181
|
2 |
|
$self = clone $this; |
182
|
2 |
|
$self->recursionDepth = $depth; |
183
|
|
|
|
184
|
2 |
|
return $self; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* Maximum number of iterations, null means infinite, any integer greater |
189
|
|
|
* than zero sets value. Zero is invalid because there must be at least one |
190
|
|
|
* iteration. Defaults to 1. Loop breaks if result of two consequent |
191
|
|
|
* iterations shows no change in processed text. |
192
|
|
|
* |
193
|
|
|
* @param int|null $iterations |
194
|
|
|
* |
195
|
|
|
* @return self |
196
|
|
|
*/ |
197
|
3 |
View Code Duplication |
public function withMaxIterations($iterations) |
|
|
|
|
198
|
|
|
{ |
199
|
3 |
|
if (null !== $iterations && !(is_int($iterations) && $iterations > 0)) { |
200
|
1 |
|
$msg = 'Maximum number of iterations must be null (infinite) or integer > 0!'; |
201
|
1 |
|
throw new \InvalidArgumentException($msg); |
202
|
|
|
} |
203
|
|
|
|
204
|
2 |
|
$self = clone $this; |
205
|
2 |
|
$self->maxIterations = $iterations; |
206
|
|
|
|
207
|
2 |
|
return $self; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Whether shortcode content will be automatically processed and handler |
212
|
|
|
* already receives shortcode with processed content. If false, every |
213
|
|
|
* shortcode handler needs to process content on its own. Default true. |
214
|
|
|
* |
215
|
|
|
* @param bool $flag True if enabled (default), false otherwise |
216
|
|
|
* |
217
|
|
|
* @return self |
218
|
|
|
*/ |
219
|
3 |
|
public function withAutoProcessContent($flag) |
220
|
|
|
{ |
221
|
3 |
|
if (!is_bool($flag)) { |
222
|
1 |
|
$msg = 'Auto processing flag must be a boolean value!'; |
223
|
1 |
|
throw new \InvalidArgumentException($msg); |
224
|
|
|
} |
225
|
|
|
|
226
|
2 |
|
$self = clone $this; |
227
|
2 |
|
$self->autoProcessContent = (bool)$flag; |
228
|
|
|
|
229
|
2 |
|
return $self; |
230
|
|
|
} |
231
|
|
|
} |
232
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.