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
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* @author Tomasz Kowalczyk <[email protected]> |
16
|
|
|
*/ |
17
|
|
|
final class Processor implements ProcessorInterface |
18
|
|
|
{ |
19
|
|
|
/** @var Handlers */ |
20
|
|
|
private $handlers; |
21
|
|
|
/** @var ParserInterface */ |
22
|
|
|
private $parser; |
23
|
|
|
/** @var EventContainerInterface */ |
24
|
|
|
private $eventContainer; |
25
|
|
|
|
26
|
|
|
private $recursionDepth = null; // infinite recursion |
27
|
|
|
private $maxIterations = 1; // one iteration |
28
|
|
|
private $autoProcessContent = true; // automatically process shortcode content |
29
|
|
|
|
30
|
64 |
|
public function __construct(ParserInterface $parser, Handlers $handlers) |
31
|
|
|
{ |
32
|
64 |
|
$this->parser = $parser; |
33
|
64 |
|
$this->handlers = $handlers; |
34
|
64 |
|
} |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* Entry point for shortcode processing. Implements iterative algorithm for |
38
|
|
|
* both limited and unlimited number of iterations. |
39
|
|
|
* |
40
|
|
|
* @param string $text Text to process |
41
|
|
|
* |
42
|
|
|
* @return string |
43
|
|
|
*/ |
44
|
57 |
|
public function process($text) |
45
|
|
|
{ |
46
|
57 |
|
$iterations = $this->maxIterations === null ? 1 : $this->maxIterations; |
47
|
57 |
|
$context = new ProcessorContext(); |
48
|
57 |
|
$context->processor = $this; |
49
|
|
|
|
50
|
57 |
|
while ($iterations--) { |
51
|
57 |
|
$context->iterationNumber++; |
52
|
57 |
|
$newText = $this->processIteration($text, $context, null); |
53
|
57 |
|
if ($newText === $text) { |
54
|
9 |
|
break; |
55
|
|
|
} |
56
|
52 |
|
$text = $newText; |
57
|
52 |
|
$iterations += $this->maxIterations === null ? 1 : 0; |
58
|
52 |
|
} |
59
|
|
|
|
60
|
57 |
|
return $text; |
61
|
|
|
} |
62
|
|
|
|
63
|
57 |
|
private function dispatchEvent($name, $event) |
64
|
|
|
{ |
65
|
57 |
|
if(null === $this->eventContainer) { |
66
|
52 |
|
return $event; |
67
|
|
|
} |
68
|
|
|
|
69
|
5 |
|
$handlers = $this->eventContainer->getListeners($name); |
70
|
5 |
|
foreach($handlers as $handler) { |
71
|
4 |
|
$handler($event); |
72
|
5 |
|
} |
73
|
|
|
|
74
|
5 |
|
return $event; |
75
|
|
|
} |
76
|
|
|
|
77
|
57 |
|
private function processIteration($text, ProcessorContext $context, ProcessedShortcode $parent = null) |
78
|
|
|
{ |
79
|
57 |
|
if (null !== $this->recursionDepth && $context->recursionLevel > $this->recursionDepth) { |
80
|
2 |
|
return $text; |
81
|
|
|
} |
82
|
|
|
|
83
|
57 |
|
$context->parent = $parent; |
84
|
57 |
|
$context->text = $text; |
85
|
57 |
|
$filterEvent = new FilterShortcodesEvent($this->parser->parse($text), $parent); |
86
|
57 |
|
$this->dispatchEvent(Events::FILTER_SHORTCODES, $filterEvent); |
87
|
57 |
|
$shortcodes = $filterEvent->getShortcodes(); |
88
|
57 |
|
$replaces = array(); |
89
|
57 |
|
$baseOffset = $parent && $shortcodes |
|
|
|
|
90
|
57 |
|
? mb_strpos($parent->getShortcodeText(), $shortcodes[0]->getText(), null, 'utf-8') - $shortcodes[0]->getOffset() + $parent->getOffset() |
91
|
57 |
|
: 0; |
92
|
57 |
|
foreach ($shortcodes as $shortcode) { |
93
|
57 |
|
$name = $shortcode->getName(); |
94
|
57 |
|
$hasNamePosition = array_key_exists($name, $context->namePosition); |
95
|
|
|
|
96
|
57 |
|
$context->baseOffset = $baseOffset + $shortcode->getOffset(); |
97
|
57 |
|
$context->position++; |
98
|
57 |
|
$context->namePosition[$name] = $hasNamePosition ? $context->namePosition[$name] + 1 : 1; |
99
|
57 |
|
$context->shortcodeText = $shortcode->getText(); |
100
|
57 |
|
$context->offset = $shortcode->getOffset(); |
101
|
57 |
|
$context->shortcode = $shortcode; |
102
|
57 |
|
$context->textContent = $shortcode->getContent(); |
103
|
|
|
|
104
|
57 |
|
$handler = $this->handlers->get($name); |
105
|
57 |
|
$replace = $this->processHandler($shortcode, $context, $handler); |
106
|
|
|
|
107
|
57 |
|
$replaces[] = new ReplacedShortcode($shortcode, $replace); |
108
|
57 |
|
} |
109
|
57 |
|
$replaces = array_filter($replaces); |
110
|
|
|
|
111
|
57 |
|
$applyEvent = new ReplaceShortcodesEvent($text, $replaces, $parent); |
112
|
57 |
|
$this->dispatchEvent(Events::REPLACE_SHORTCODES, $applyEvent); |
113
|
|
|
|
114
|
57 |
|
return $applyEvent->hasResult() ? $applyEvent->getResult() : $this->applyReplaces($text, $replaces); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
private function applyReplaces($text, array $replaces) |
118
|
|
|
{ |
119
|
56 |
|
return array_reduce(array_reverse($replaces), function($state, ReplacedShortcode $s) { |
120
|
56 |
|
$offset = $s->getOffset(); |
121
|
56 |
|
$length = mb_strlen($s->getText(), 'utf-8'); |
122
|
56 |
|
$textLength = mb_strlen($state, 'utf-8'); |
123
|
|
|
|
124
|
56 |
|
return mb_substr($state, 0, $offset, 'utf-8').$s->getReplacement().mb_substr($state, $offset + $length, $textLength, 'utf-8'); |
125
|
56 |
|
}, $text); |
126
|
|
|
} |
127
|
|
|
|
128
|
57 |
|
private function processHandler(ParsedShortcodeInterface $parsed, ProcessorContext $context, $handler) |
129
|
|
|
{ |
130
|
57 |
|
$processed = ProcessedShortcode::createFromContext(clone $context); |
131
|
57 |
|
$content = $this->processRecursion($processed, $context); |
132
|
57 |
|
$processed = $processed->withContent($content); |
133
|
|
|
|
134
|
57 |
|
if($handler) { |
135
|
53 |
|
return $handler($processed); |
136
|
|
|
} |
137
|
|
|
|
138
|
10 |
|
$state = $parsed->getText(); |
139
|
10 |
|
$length = mb_strlen($processed->getTextContent(), 'utf-8'); |
140
|
10 |
|
$offset = mb_strrpos($state, $processed->getTextContent(), 'utf-8'); |
141
|
|
|
|
142
|
10 |
|
return mb_substr($state, 0, $offset, 'utf-8').$processed->getContent().mb_substr($state, $offset + $length, mb_strlen($state, 'utf-8'), 'utf-8'); |
143
|
|
|
} |
144
|
|
|
|
145
|
57 |
|
private function processRecursion(ParsedShortcodeInterface $shortcode, ProcessorContext $context) |
146
|
|
|
{ |
147
|
57 |
|
if ($this->autoProcessContent && null !== $shortcode->getContent()) { |
148
|
46 |
|
$context->recursionLevel++; |
149
|
|
|
// this is safe from using max iterations value because it's manipulated in process() method |
150
|
46 |
|
$content = $this->processIteration($shortcode->getContent(), clone $context, $shortcode); |
|
|
|
|
151
|
46 |
|
$context->recursionLevel--; |
152
|
|
|
|
153
|
46 |
|
return $content; |
154
|
|
|
} |
155
|
|
|
|
156
|
28 |
|
return $shortcode->getContent(); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* Set container for event handlers used in this processor. |
161
|
|
|
* |
162
|
|
|
* @param EventContainerInterface $eventContainer |
163
|
|
|
* |
164
|
|
|
* @return self |
165
|
|
|
*/ |
166
|
8 |
|
public function setEventContainer(EventContainerInterface $eventContainer) |
167
|
|
|
{ |
168
|
8 |
|
$this->eventContainer = $eventContainer; |
169
|
|
|
|
170
|
8 |
|
return $this; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Create a new Processor using a given event container |
175
|
|
|
* |
176
|
|
|
* @param EventContainerInterface $eventContainer |
177
|
|
|
* |
178
|
|
|
* @return self |
179
|
|
|
*/ |
180
|
8 |
|
public function withEventContainer(EventContainerInterface $eventContainer) |
181
|
|
|
{ |
182
|
8 |
|
$self = clone $this; |
183
|
|
|
|
184
|
8 |
|
return $self->setEventContainer($eventContainer); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* Set recursion depth level |
189
|
|
|
* |
190
|
|
|
* Null means infinite, any integer greater than or |
191
|
|
|
* equal to zero sets value (number of recursion levels). Zero disables |
192
|
|
|
* recursion. Defaults to null. |
193
|
|
|
* |
194
|
|
|
* @param int|null $depth |
195
|
|
|
* |
196
|
|
|
* @return self |
197
|
|
|
*/ |
198
|
3 |
|
public function setRecursionDepth($depth) |
199
|
|
|
{ |
200
|
3 |
|
if (null !== $depth && !(is_int($depth) && $depth >= 0)) { |
201
|
1 |
|
$msg = 'Recursion depth must be null (infinite) or integer >= 0!'; |
202
|
1 |
|
throw new \InvalidArgumentException($msg); |
203
|
|
|
} |
204
|
|
|
|
205
|
2 |
|
$this->recursionDepth = $depth; |
206
|
|
|
|
207
|
2 |
|
return $this; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Create a new Processor using a given resursion depth |
212
|
|
|
* |
213
|
|
|
* @param int|null $depth |
214
|
|
|
* |
215
|
|
|
* @return self |
216
|
|
|
*/ |
217
|
3 |
|
public function withRecursionDepth($depth) |
218
|
|
|
{ |
219
|
3 |
|
$self = clone $this; |
220
|
|
|
|
221
|
3 |
|
return $self->setRecursionDepth($depth); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Set maximum number of iterations |
226
|
|
|
* |
227
|
|
|
* Null means infinite, any integer greater |
228
|
|
|
* than zero sets value. Zero is invalid because there must be at least one |
229
|
|
|
* iteration. Defaults to 1. Loop breaks if result of two consequent |
230
|
|
|
* iterations shows no change in processed text. |
231
|
|
|
* |
232
|
|
|
* @param int|null $iterations |
233
|
|
|
* |
234
|
|
|
* @return self |
235
|
|
|
*/ |
236
|
3 |
|
public function setMaxIterations($iterations) |
237
|
|
|
{ |
238
|
3 |
|
if (null !== $iterations && !(is_int($iterations) && $iterations > 0)) { |
239
|
1 |
|
$msg = 'Maximum number of iterations must be null (infinite) or integer > 0!'; |
240
|
1 |
|
throw new \InvalidArgumentException($msg); |
241
|
|
|
} |
242
|
|
|
|
243
|
2 |
|
$this->maxIterations = $iterations; |
244
|
|
|
|
245
|
2 |
|
return $this; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Create a new Processor using a given maximum number of iterations |
250
|
|
|
* |
251
|
|
|
* @param int|null $iterations |
252
|
|
|
* |
253
|
|
|
* @return self |
254
|
|
|
*/ |
255
|
3 |
|
public function withMaxIterations($iterations) |
256
|
|
|
{ |
257
|
3 |
|
$self = clone $this; |
258
|
|
|
|
259
|
3 |
|
return $self->setMaxIterations($iterations); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Sets whether shortcode content will be automatically processed and handler |
264
|
|
|
* already receives shortcode with processed content. |
265
|
|
|
* |
266
|
|
|
* If false, every shortcode handler needs to process content on its own. |
267
|
|
|
* Default true. |
268
|
|
|
* |
269
|
|
|
* @param bool $flag True if enabled (default), false otherwise |
270
|
|
|
* |
271
|
|
|
* @return self |
272
|
|
|
*/ |
273
|
3 |
|
public function setAutoProcessContent($flag) |
274
|
|
|
{ |
275
|
3 |
|
if (!is_bool($flag)) { |
276
|
1 |
|
$msg = 'Auto processing flag must be a boolean value!'; |
277
|
1 |
|
throw new \InvalidArgumentException($msg); |
278
|
|
|
} |
279
|
|
|
|
280
|
2 |
|
$this->autoProcessContent = (bool)$flag; |
281
|
|
|
|
282
|
2 |
|
return $this; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* Creates a new Processor with a given setting for automatic content processing |
287
|
|
|
* |
288
|
|
|
* @param bool $flag True if enabled (default), false otherwise |
289
|
|
|
* |
290
|
|
|
* @return self |
291
|
|
|
*/ |
292
|
3 |
|
public function withAutoProcessContent($flag) |
293
|
|
|
{ |
294
|
3 |
|
$self = clone $this; |
295
|
|
|
|
296
|
3 |
|
return $self->setAutoProcessContent($flag); |
297
|
|
|
} |
298
|
|
|
} |
299
|
|
|
|
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.