merged branch jfsimon/master (PR #3869)
Commits -------5b5b2c8
[Console] Fixed and added formatter tests.4ee8cfb
[Console] Updated formatter to use style stack.bd1d28c
[Console] Added formatter style stack tests.b63bd0e
[Console] Added formatter style stack. Discussion ---------- [Console] Fixed formatter nested style appliance in a proper way. Bug fix: yes Feature addition: no Backwards compatibility break: no Symfony2 tests pass: yes When outputing styled text in the console, you sometimes face to a confusing behavior: style tags cannot be nested. If tou try something like `<fg=blue>Hello <fg=red>world</fg=red>!</fg=blue>`, the trailing `!` will not be styled. This PR introduce a new FormatterOutputStyleStack to keep open/closed styles informations up-to-date.
This commit is contained in:
commit
abe8ba1e1c
@ -23,10 +23,11 @@ class OutputFormatter implements OutputFormatterInterface
|
||||
/**
|
||||
* The pattern to phrase the format.
|
||||
*/
|
||||
const FORMAT_PATTERN = '#<([a-z][a-z0-9_=;-]+)>(.*?)</\\1?>#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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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 <contact@jfsimon.fr>
|
||||
*/
|
||||
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];
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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'));
|
||||
}
|
||||
}
|
@ -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('<error>some error</error>')
|
||||
"\033[37;41msome error\033[0m",
|
||||
$formatter->format('<error>some error</error>')
|
||||
);
|
||||
$this->assertEquals(
|
||||
"\033[32msome info\033[0m", $formatter->format('<info>some info</info>')
|
||||
"\033[32msome info\033[0m",
|
||||
$formatter->format('<info>some info</info>')
|
||||
);
|
||||
$this->assertEquals(
|
||||
"\033[33msome comment\033[0m", $formatter->format('<comment>some comment</comment>')
|
||||
"\033[33msome comment\033[0m",
|
||||
$formatter->format('<comment>some comment</comment>')
|
||||
);
|
||||
$this->assertEquals(
|
||||
"\033[30;46msome question\033[0m", $formatter->format('<question>some question</question>')
|
||||
"\033[30;46msome question\033[0m",
|
||||
$formatter->format('<question>some question</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('<error>some <info>some info</info> error</error>')
|
||||
"\033[37;41msome \033[0m\033[32msome info\033[0m\033[37;41m error\033[0m",
|
||||
$formatter->format('<error>some <info>some info</info> error</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('<error>error<info>info<comment>comment</info>error</error>')
|
||||
);
|
||||
}
|
||||
|
||||
@ -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('<test>some custom msg</test>'));
|
||||
$this->assertEquals("\033[34;47msome custom msg\033[0m", $formatter->format('<test>some custom msg</test>'));
|
||||
}
|
||||
|
||||
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('<info>some custom msg</info>')
|
||||
);
|
||||
$this->assertEquals("\033[34;47msome custom msg\033[0m", $formatter->format('<info>some custom msg</info>'));
|
||||
}
|
||||
|
||||
public function testInlineStyle()
|
||||
|
Reference in New Issue
Block a user