Skip to content

Commit ebed689

Browse files
committed
implemented iterative shortcode processing, added tests
1 parent fdaa5c6 commit ebed689

2 files changed

Lines changed: 82 additions & 5 deletions

File tree

src/Processor.php

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ final class Processor implements ProcessorInterface
1111
private $parser;
1212
private $defaultHandler;
1313
private $recursionDepth = null;
14+
private $maxIterations = 1;
1415

1516
public function __construct(ExtractorInterface $extractor, ParserInterface $parser)
1617
{
@@ -74,17 +75,42 @@ public function setDefaultHandler(callable $handler)
7475
$this->defaultHandler = $handler;
7576
}
7677

78+
/**
79+
* Entry point for shortcode processing. Implements iterative algorithm for
80+
* both limited and unlimited number of iterations.
81+
*
82+
* @param string $text Text to process
83+
*
84+
* @return string
85+
*/
86+
public function process($text)
87+
{
88+
$iterations = $this->maxIterations === null ? 1 : $this->maxIterations;
89+
while($iterations--)
90+
{
91+
$newText = $this->processIteration($text, 0);
92+
if($newText === $text)
93+
{
94+
break;
95+
}
96+
$text = $newText;
97+
$iterations += $this->maxIterations === null ? 1 : 0;
98+
}
99+
100+
return $text;
101+
}
102+
77103
/**
78104
* Expects matches sorted by position returned from Extractor. Matches are
79105
* processed from the last to the first to avoid replace position errors.
80106
* Edge cases are described in README.
81107
*
82-
* @param string $text
83-
* @param int $level
108+
* @param string $text Current text state
109+
* @param int $level Current recursion depth level
84110
*
85111
* @return string
86112
*/
87-
public function process($text, $level = 0)
113+
private function processIteration($text, $level)
88114
{
89115
if(null !== $this->recursionDepth && $level > $this->recursionDepth)
90116
{
@@ -93,12 +119,11 @@ public function process($text, $level = 0)
93119

94120
/** @var $matches Match[] */
95121
$matches = array_reverse($this->extractor->extract($text));
96-
97122
foreach($matches as $match)
98123
{
99124
$shortcode = $this->parser->parse($match->getString());
100125
$content = $shortcode->hasContent()
101-
? $this->process($shortcode->getContent(), $level + 1)
126+
? $this->processIteration($shortcode->getContent(), $level + 1)
102127
: $shortcode->getContent();
103128
$shortcode = new Shortcode($shortcode->getName(), $shortcode->getParameters(), $content);
104129
$handler = $this->getHandler($shortcode->getName());
@@ -133,6 +158,27 @@ public function setRecursionDepth($depth)
133158
return $this;
134159
}
135160

161+
/**
162+
* Maximum number of iterations, null means infinite, any integer greater
163+
* than or equal to zero sets value.
164+
*
165+
* @param int|null $iterations
166+
*
167+
* @return self
168+
*/
169+
public function setMaxIterations($iterations)
170+
{
171+
if(null !== $iterations && !(is_int($iterations) && $iterations >= 0))
172+
{
173+
$msg = 'Maximum number of iterations must be null (infinite) or integer >= 0!';
174+
throw new \InvalidArgumentException($msg);
175+
}
176+
177+
$this->maxIterations = $iterations;
178+
179+
return $this;
180+
}
181+
136182
/**
137183
* @deprecated Use self::setRecursionDepth() instead
138184
*

tests/ProcessorTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,23 @@ public function testProcessorWithoutRecursion()
6363
$this->assertSame('x a-[name][/name]-b y', $result);
6464
}
6565

66+
public function testProcessorIterative()
67+
{
68+
$processor = $this
69+
->getProcessor()
70+
->addHandlerAlias('d', 'c')
71+
->addHandlerAlias('e', 'c')
72+
->setRecursion(false);
73+
74+
$processor->setMaxIterations(2);
75+
$this->assertSame('x a y', $processor->process('x [c]a[/c] y'));
76+
$this->assertSame('x abc y', $processor->process('x [c]a[d]b[/d]c[/c] y'));
77+
$this->assertSame('x ab[e]c[/e]de y', $processor->process('x [c]a[d]b[e]c[/e]d[/d]e[/c] y'));
78+
79+
$processor->setMaxIterations(null);
80+
$this->assertSame('x abcde y', $processor->process('x [c]a[d]b[e]c[/e]d[/d]e[/c] y'));
81+
}
82+
6683
public function testExceptionOnInvalidHandler()
6784
{
6885
$processor = $this->getProcessor();
@@ -91,4 +108,18 @@ public function testExceptionOnInvalidRecursionDepth()
91108
$this->setExpectedException('InvalidArgumentException');
92109
$processor->setRecursionDepth(new \stdClass());
93110
}
111+
112+
public function testPreventInfiniteLoop()
113+
{
114+
$processor = $this
115+
->getProcessor()
116+
->addHandler('self', function(Shortcode $s) { return '[self]'; })
117+
->addHandler('other', function(Shortcode $s) { return '[self]'; })
118+
->addHandler('random', function(Shortcode $s) { return '[various]'; })
119+
->setMaxIterations(null);
120+
121+
$processor->process('[self]');
122+
$processor->process('[other]');
123+
$processor->process('[random]');
124+
}
94125
}

0 commit comments

Comments
 (0)