merged branch jfsimon/master (PR #3613)
Commits -------2a90871
[Console] Removed previously introduced BC break.90a2a6e
[Console] Undecorated formatter must update style stack too.bd7e01a
[Console] Fixed output formatter test broken by new implementation.a1add4b
[Console] Updated output formatter to use style stack.4f298dd
[Console] Added formatter style stack.93ffe54
[Console] Added getters to output formatter style (and its interface).48e6b49
[Console] Updated formatter test to match styles bug fix.ad334b6
[Console] Fixed empty style appliance.31d5fe5
[Console] Fixed output formatter docblock. Discussion ---------- [Console] Fixes formatter nested style appliance. 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. It slightly changes OutputFormatter implementation which no longer uses `OutputFormatterStyle::apply()` method, but the new `OutputFormatterStyle::getTerminalSequence()`. **Question:** I don't une `OutputFormatterStyleInterface` but `OutputFormatterStyle` to type `OutputFormatterStyleStack` methods arguments (to avoid BC break on the interface). Do you think it's right? **Notice:** I also needed to fix some tests broken by new implementation. --------------------------------------------------------------------------- by stof at 2012-03-16T10:27:56Z Adding new methods in an interface is a BC break for people implementing it --------------------------------------------------------------------------- by jfsimon at 2012-03-16T10:33:21Z @stof indeed... this is a problem, should I remove them? If I do so, I should use `OutputFormatterStyle` instead of the interface to type arguments in `OutputFormatterStyleStack` right?
This commit is contained in:
commit
959158f9b9
@ -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,30 @@ class OutputFormatter implements OutputFormatterInterface
|
||||
*/
|
||||
private function replaceStyle($match)
|
||||
{
|
||||
if (!$this->isDecorated()) {
|
||||
return $match[2];
|
||||
// Special "</>" tag resets all styles.
|
||||
if (!isset($match[2])) {
|
||||
$this->styleStack->reset();
|
||||
|
||||
return $this->isDecorated() ? "\033[0m" : '';
|
||||
}
|
||||
|
||||
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 '';
|
||||
}
|
||||
}
|
||||
|
||||
return $style->apply($this->format($match[2]));
|
||||
if ('/' === $match[1]) {
|
||||
$this->styleStack->popStyle($style);
|
||||
} else {
|
||||
$this->styleStack->pushStyle($style);
|
||||
}
|
||||
|
||||
return $this->isDecorated() ? $this->styleStack->getCurrentStyle()->getTerminalSequence() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -166,7 +178,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)
|
||||
{
|
||||
|
@ -102,6 +102,22 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
|
||||
$this->foreground = static::$availableForegroundColors[$color];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets style foreground color.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getForeground()
|
||||
{
|
||||
if (null === $this->foreground) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$names = array_flip(static::$availableForegroundColors);
|
||||
|
||||
return $names[$this->foreground];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets style background color.
|
||||
*
|
||||
@ -130,6 +146,22 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
|
||||
$this->background = static::$availableBackgroundColors[$color];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets style background color.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getBackground()
|
||||
{
|
||||
if (null === $this->background) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$names = array_flip(static::$availableBackgroundColors);
|
||||
|
||||
return $names[$this->background];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets some specific style option.
|
||||
*
|
||||
@ -192,6 +224,23 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets specific style options.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getOptions()
|
||||
{
|
||||
$names = array_flip(static::$availableOptions);
|
||||
$options = array();
|
||||
|
||||
foreach ($this->options as $code) {
|
||||
$options[] = $names[$code];
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the style to a given text.
|
||||
*
|
||||
@ -200,6 +249,16 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
|
||||
* @return string
|
||||
*/
|
||||
public function apply($text)
|
||||
{
|
||||
return sprintf("%s%s\033[0m", $this->getTerminalSequence(), $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets terminal colorization sequence.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTerminalSequence()
|
||||
{
|
||||
$codes = array();
|
||||
|
||||
@ -212,7 +271,10 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
|
||||
if (count($this->options)) {
|
||||
$codes = array_merge($codes, $this->options);
|
||||
}
|
||||
if (0 === count($codes)) {
|
||||
$codes = array(0);
|
||||
}
|
||||
|
||||
return sprintf("\033[%sm%s\033[0m", implode(';', $codes), $text);
|
||||
return sprintf("\033[%sm", implode(';', $codes));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,136 @@
|
||||
<?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 array
|
||||
*/
|
||||
private $foregrounds;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $backgrounds;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets stack (ie. empty internal arrays).
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->foregrounds = array();
|
||||
$this->backgrounds = array();
|
||||
$this->options = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a style in the stack.
|
||||
*
|
||||
* @param OutputFormatterStyle $style
|
||||
*/
|
||||
public function pushStyle(OutputFormatterStyle $style)
|
||||
{
|
||||
$foreground = $style->getForeground();
|
||||
if (null !== $foreground) {
|
||||
$this->foregrounds[] = $foreground;
|
||||
}
|
||||
|
||||
$background = $style->getBackground();
|
||||
if (null !== $background) {
|
||||
$this->backgrounds[] = $background;
|
||||
}
|
||||
|
||||
foreach ($style->getOptions() as $option) {
|
||||
if (!isset($this->options[$option])) {
|
||||
$this->options[$option] = 0;
|
||||
}
|
||||
$this->options[$option] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops a style from the stack.
|
||||
*
|
||||
* @param OutputFormatterStyle $style
|
||||
*
|
||||
* @throws \InvalidArgumentException When style tags incorrectly nested
|
||||
*/
|
||||
public function popStyle(OutputFormatterStyle $style)
|
||||
{
|
||||
$this->popArrayCode($this->foregrounds, $style->getForeground());
|
||||
$this->popArrayCode($this->backgrounds, $style->getBackground());
|
||||
|
||||
foreach ($style->getOptions() as $option) {
|
||||
if (!isset($this->options[$option])) {
|
||||
throw new \InvalidArgumentException('Unexpected style "'.$option.'" option end.');
|
||||
}
|
||||
|
||||
$this->options[$option] -= 1;
|
||||
if (0 === $this->options[$option]) {
|
||||
unset($this->options[$option]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes current style with stacks top codes.
|
||||
*
|
||||
* @return OutputFormatterStyle
|
||||
*/
|
||||
public function getCurrentStyle()
|
||||
{
|
||||
return new OutputFormatterStyle(
|
||||
end($this->foregrounds) ?: null,
|
||||
end($this->backgrounds) ?: null,
|
||||
array_keys($this->options)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops a color from a stack.
|
||||
*
|
||||
* @param array $stack An array of color names
|
||||
* @param string $color A color name
|
||||
*
|
||||
* @throws \InvalidArgumentException When poped color is not the expected one
|
||||
*/
|
||||
private function popArrayCode(&$stack, $color)
|
||||
{
|
||||
if (null === $color) {
|
||||
return;
|
||||
}
|
||||
|
||||
$current = end($stack);
|
||||
if ($current !== $color) {
|
||||
throw new \InvalidArgumentException('Expected style "'.$current.'" color end but "'.$color.'" end found.');
|
||||
}
|
||||
|
||||
array_pop($stack);
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\Console\Tests\Formatter;
|
||||
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
|
||||
|
||||
class FormatterStyleTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
@ -43,7 +44,7 @@ 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[32;41msome info\033[37;41m error\033[0m", $formatter->format('<error>some <info>some info</info> error</error>')
|
||||
);
|
||||
}
|
||||
|
||||
@ -51,36 +52,23 @@ class FormatterStyleTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
$formatter = new OutputFormatter(true);
|
||||
|
||||
$style = $this->getMockBuilder('Symfony\Component\Console\Formatter\OutputFormatterStyleInterface')->getMock();
|
||||
$style = new OutputFormatterStyle('red', 'blue');
|
||||
$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[31;44msome 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('red', 'blue');
|
||||
$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[31;44msome custom msg\033[0m", $formatter->format('<info>some custom msg</info>'));
|
||||
}
|
||||
|
||||
public function testInlineStyle()
|
||||
|
@ -0,0 +1,74 @@
|
||||
<?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\Tests\Component\Console\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->pushStyle(new OutputFormatterStyle('white'));
|
||||
$stack->pushStyle(new OutputFormatterStyle(null, 'black'));
|
||||
$stack->pushStyle(new OutputFormatterStyle(null, null, array('bold')));
|
||||
|
||||
$style = $stack->getCurrentStyle();
|
||||
$this->assertEquals('white', $style->getForeground());
|
||||
$this->assertEquals('black', $style->getBackground());
|
||||
$this->assertEquals(array('bold'), $style->getOptions());
|
||||
|
||||
$stack->pushStyle(new OutputFormatterStyle('yellow', null, array('blink')));
|
||||
|
||||
$style = $stack->getCurrentStyle();
|
||||
$this->assertEquals('yellow', $style->getForeground());
|
||||
$this->assertEquals('black', $style->getBackground());
|
||||
$this->assertEquals(array('bold', 'blink'), $style->getOptions());
|
||||
}
|
||||
|
||||
public function testPop()
|
||||
{
|
||||
$stack = new OutputFormatterStyleStack();
|
||||
$stack->pushStyle(new OutputFormatterStyle('white', 'black', array('blink', 'bold')));
|
||||
$stack->pushStyle(new OutputFormatterStyle('yellow', 'blue'));
|
||||
|
||||
$style = $stack->getCurrentStyle();
|
||||
$this->assertEquals('yellow', $style->getForeground());
|
||||
$this->assertEquals('blue', $style->getBackground());
|
||||
$this->assertEquals(array('blink', 'bold'), $style->getOptions());
|
||||
|
||||
$stack->popStyle(new OutputFormatterStyle(null, 'blue', array('blink')));
|
||||
|
||||
$style = $stack->getCurrentStyle();
|
||||
$this->assertEquals('yellow', $style->getForeground());
|
||||
$this->assertEquals('black', $style->getBackground());
|
||||
$this->assertEquals(array('bold'), $style->getOptions());
|
||||
|
||||
$stack->popStyle(new OutputFormatterStyle('yellow', 'black', array('bold')));
|
||||
|
||||
$style = $stack->getCurrentStyle();
|
||||
$this->assertEquals('white', $style->getForeground());
|
||||
$this->assertEquals(null, $style->getBackground());
|
||||
$this->assertEquals(array(), $style->getOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testInvalidPop()
|
||||
{
|
||||
$stack = new OutputFormatterStyleStack();
|
||||
$stack->pushStyle(new OutputFormatterStyle('white', 'black', array('blink', 'bold')));
|
||||
$stack->popStyle(new OutputFormatterStyle('yellow'));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user