diff --git a/src/Symfony/Component/Console/Formatter/OutputFormatter.php b/src/Symfony/Component/Console/Formatter/OutputFormatter.php index 8d60c74f8d..8f696c06eb 100644 --- a/src/Symfony/Component/Console/Formatter/OutputFormatter.php +++ b/src/Symfony/Component/Console/Formatter/OutputFormatter.php @@ -23,10 +23,11 @@ class OutputFormatter implements OutputFormatterInterface /** * The pattern to phrase the format. */ - const FORMAT_PATTERN = '#<([a-z][a-z0-9_=;-]+)>(.*?)#is'; + const FORMAT_PATTERN = '#<(/?)([a-z][a-z0-9_=;-]+)?>([^<]*)#is'; private $decorated; private $styles = array(); + private $styleStack; /** * Initializes console output formatter. @@ -48,6 +49,8 @@ class OutputFormatter implements OutputFormatterInterface foreach ($styles as $name => $style) { $this->setStyle($name, $style); } + + $this->styleStack = new OutputFormatterStyleStack(); } /** @@ -144,21 +147,35 @@ class OutputFormatter implements OutputFormatterInterface */ private function replaceStyle($match) { - if (!$this->isDecorated()) { - return $match[2]; + if ('' === $match[2]) { + if ('/' === $match[1]) { + // we got "" tag + $this->styleStack->pop(); + + return $this->applyStyle($this->styleStack->getCurrent(), $match[3]); + } + + // we got "<>" tag + return '<>'.$match[3]; } - if (isset($this->styles[strtolower($match[1])])) { - $style = $this->styles[strtolower($match[1])]; + if (isset($this->styles[strtolower($match[2])])) { + $style = $this->styles[strtolower($match[2])]; } else { - $style = $this->createStyleFromString($match[1]); + $style = $this->createStyleFromString($match[2]); if (false === $style) { - return $match[0]; + return $match[3]; } } - return $style->apply($this->format($match[2])); + if ('/' === $match[1]) { + $this->styleStack->pop($style); + } else { + $this->styleStack->push($style); + } + + return $this->applyStyle($this->styleStack->getCurrent(), $match[3]); } /** @@ -166,7 +183,7 @@ class OutputFormatter implements OutputFormatterInterface * * @param string $string * - * @return Symfony\Component\Console\Format\FormatterStyle|Boolean false if string is not format string + * @return \Symfony\Component\Console\Formatter\OutputFormatterStyle|Boolean false if string is not format string */ private function createStyleFromString($string) { @@ -189,4 +206,17 @@ class OutputFormatter implements OutputFormatterInterface return $style; } + + /** + * Applies style to text if must be applied. + * + * @param OutputFormatterStyleInterface $style Style to apply + * @param string $text Input text + * + * @return string string Styled text + */ + private function applyStyle(OutputFormatterStyleInterface $style, $text) + { + return $this->isDecorated() && strlen($text) > 0 ? $style->apply($text) : $text; + } } diff --git a/src/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php b/src/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php new file mode 100644 index 0000000000..3f88e0a758 --- /dev/null +++ b/src/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +/** + * @author Jean-François Simon + */ +class OutputFormatterStyleStack +{ + /** + * @var OutputFormatterStyle[] + */ + private $styles; + + /** + * Constructor. + */ + public function __construct() + { + $this->reset(); + } + + /** + * Resets stack (ie. empty internal arrays). + */ + public function reset() + { + $this->styles = array(); + } + + /** + * Pushes a style in the stack. + * + * @param OutputFormatterStyleInterface $style + */ + public function push(OutputFormatterStyleInterface $style) + { + $this->styles[] = $style; + } + + /** + * Pops a style from the stack. + * + * @param OutputFormatterStyleInterface $style + * + * @return OutputFormatterStyleInterface + * + * @throws \InvalidArgumentException When style tags incorrectly nested + */ + public function pop(OutputFormatterStyleInterface $style = null) + { + if (empty($this->styles)) { + return new OutputFormatterStyle(); + } + + if (null === $style) { + return array_pop($this->styles); + } + + foreach (array_reverse($this->styles, true) as $index => $stackedStyle) { + if ($style->apply('') === $stackedStyle->apply('')) { + $this->styles = array_slice($this->styles, 0, $index); + + return $stackedStyle; + } + } + + throw new \InvalidArgumentException('Incorrectly nested style tag found.'); + } + + /** + * Computes current style with stacks top codes. + * + * @return OutputFormatterStyle + */ + public function getCurrent() + { + if (empty($this->styles)) { + return new OutputFormatterStyle(); + } + + return $this->styles[count($this->styles)-1]; + } +} diff --git a/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterStyleStackTest.php b/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterStyleStackTest.php new file mode 100644 index 0000000000..e99ebc5778 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterStyleStackTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Formatter; + +use Symfony\Component\Console\Formatter\OutputFormatterStyleStack; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; + +class OutputFormatterStyleStackTest extends \PHPUnit_Framework_TestCase +{ + public function testPush() + { + $stack = new OutputFormatterStyleStack(); + $stack->push($s1 = new OutputFormatterStyle('white', 'black')); + $stack->push($s2 = new OutputFormatterStyle('yellow', 'blue')); + + $this->assertEquals($s2, $stack->getCurrent()); + + $stack->push($s3 = new OutputFormatterStyle('green', 'red')); + + $this->assertEquals($s3, $stack->getCurrent()); + } + + public function testPop() + { + $stack = new OutputFormatterStyleStack(); + $stack->push($s1 = new OutputFormatterStyle('white', 'black')); + $stack->push($s2 = new OutputFormatterStyle('yellow', 'blue')); + + $this->assertEquals($s2, $stack->pop()); + $this->assertEquals($s1, $stack->pop()); + } + + public function testPopEmpty() + { + $stack = new OutputFormatterStyleStack(); + $style = new OutputFormatterStyle(); + + $this->assertEquals($style, $stack->pop()); + } + + public function testPopNotLast() + { + $stack = new OutputFormatterStyleStack(); + $stack->push($s1 = new OutputFormatterStyle('white', 'black')); + $stack->push($s2 = new OutputFormatterStyle('yellow', 'blue')); + $stack->push($s3 = new OutputFormatterStyle('green', 'red')); + + $this->assertEquals($s2, $stack->pop($s2)); + $this->assertEquals($s1, $stack->pop()); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testInvalidPop() + { + $stack = new OutputFormatterStyleStack(); + $stack->push(new OutputFormatterStyle('white', 'black')); + $stack->pop(new OutputFormatterStyle('yellow', 'blue')); + } +} diff --git a/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php b/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php index e90a85897c..0d1bf4104a 100644 --- a/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php +++ b/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php @@ -12,9 +12,16 @@ namespace Symfony\Component\Console\Tests\Formatter; use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; class FormatterStyleTest extends \PHPUnit_Framework_TestCase { + public function testEmptyTag() + { + $formatter = new OutputFormatter(true); + $this->assertEquals("foo<>bar", $formatter->format('foo<>bar')); + } + public function testBundledStyles() { $formatter = new OutputFormatter(true); @@ -25,16 +32,20 @@ class FormatterStyleTest extends \PHPUnit_Framework_TestCase $this->assertTrue($formatter->hasStyle('question')); $this->assertEquals( - "\033[37;41msome error\033[0m", $formatter->format('some error') + "\033[37;41msome error\033[0m", + $formatter->format('some error') ); $this->assertEquals( - "\033[32msome info\033[0m", $formatter->format('some info') + "\033[32msome info\033[0m", + $formatter->format('some info') ); $this->assertEquals( - "\033[33msome comment\033[0m", $formatter->format('some comment') + "\033[33msome comment\033[0m", + $formatter->format('some comment') ); $this->assertEquals( - "\033[30;46msome question\033[0m", $formatter->format('some question') + "\033[30;46msome question\033[0m", + $formatter->format('some question') ); } @@ -43,7 +54,18 @@ class FormatterStyleTest extends \PHPUnit_Framework_TestCase $formatter = new OutputFormatter(true); $this->assertEquals( - "\033[37;41msome \033[32msome info\033[0m error\033[0m", $formatter->format('some some info error') + "\033[37;41msome \033[0m\033[32msome info\033[0m\033[37;41m error\033[0m", + $formatter->format('some some info error') + ); + } + + public function testDeepNestedStyles() + { + $formatter = new OutputFormatter(true); + + $this->assertEquals( + "\033[37;41merror\033[0m\033[32minfo\033[0m\033[33mcomment\033[0m\033[37;41merror\033[0m", + $formatter->format('errorinfocommenterror') ); } @@ -51,36 +73,23 @@ class FormatterStyleTest extends \PHPUnit_Framework_TestCase { $formatter = new OutputFormatter(true); - $style = $this->getMockBuilder('Symfony\Component\Console\Formatter\OutputFormatterStyleInterface')->getMock(); + $style = new OutputFormatterStyle('blue', 'white'); $formatter->setStyle('test', $style); $this->assertEquals($style, $formatter->getStyle('test')); $this->assertNotEquals($style, $formatter->getStyle('info')); - $style - ->expects($this->once()) - ->method('apply') - ->will($this->returnValue('[STYLE_BEG]some custom msg[STYLE_END]')); - - $this->assertEquals("[STYLE_BEG]some custom msg[STYLE_END]", $formatter->format('some custom msg')); + $this->assertEquals("\033[34;47msome custom msg\033[0m", $formatter->format('some custom msg')); } public function testRedefineStyle() { $formatter = new OutputFormatter(true); - $style = $this->getMockBuilder('Symfony\Component\Console\Formatter\OutputFormatterStyleInterface') - ->getMock(); + $style = new OutputFormatterStyle('blue', 'white'); $formatter->setStyle('info', $style); - $style - ->expects($this->once()) - ->method('apply') - ->will($this->returnValue('[STYLE_BEG]some custom msg[STYLE_END]')); - - $this->assertEquals( - "[STYLE_BEG]some custom msg[STYLE_END]", $formatter->format('some custom msg') - ); + $this->assertEquals("\033[34;47msome custom msg\033[0m", $formatter->format('some custom msg')); } public function testInlineStyle()