diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..be966d2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +* text=auto eol=lf + +.gitattributes export-ignore +.gitignore export-ignore +.php_cs export-ignore +.travis.yml export-ignore +phpunit.xml.dist export-ignore +SensioLabs/AnsiConverter/Tests/AnsiToHtmlConverterTest.php export-ignore diff --git a/.gitignore b/.gitignore index c55784d..5464500 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ composer.lock /vendor/ +/.php_cs.cache diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..4dfb291 --- /dev/null +++ b/.php_cs @@ -0,0 +1,47 @@ +in(__DIR__); + +$header = << true, + '@Symfony' => true, + 'cast_spaces' => [ + 'space' => 'none', + ], + 'concat_space' => [ + 'spacing' => 'none', + ], + 'native_function_invocation' => [ + 'scope' => 'namespaced', + ], + 'psr4' => true, + 'phpdoc_align' => [ + 'align' => 'left', + ], + 'array_syntax' => [ + 'syntax' => 'short', + ], + 'header_comment' => [ + 'header' => $header, + 'commentType' => PhpCsFixer\Fixer\Comment\HeaderCommentFixer::HEADER_PHPDOC, + ], + 'yoda_style' => false, +]; + +$cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__; + +return PhpCsFixer\Config::create() + ->setRiskyAllowed(true) + ->setRules($rules) + ->setFinder($finder) + ->setCacheFile($cacheDir . '/.php_cs.cache'); diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4a6df4d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,44 @@ +sudo: false + +language: php + +php: + - 7.0 + - 7.1 + - 7.2 + - 7.3 + - hhvm + +env: + - COMPOSER_FLAGS=--prefer-lowest + +matrix: + exclude: + - php: hhvm + env: COMPOSER_FLAGS=--prefer-lowest + include: + - php: hhvm + env: COMPOSER_FLAGS= + - php: 7 + env: PHPSTAN=1 + allow_failures: + - php: nightly + - php: hhvm + +cache: + directories: + - $HOME/.composer/cache + - $HOME/.php-cs-fixer + +before_script: + - travis_retry composer self-update + - travis_retry composer update --no-interaction --prefer-source --prefer-stable ${COMPOSER_FLAGS} + +script: + - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover + - composer cs + - composer sa + +after_script: + - wget https://scrutinizer-ci.com/ocular.phar + - php ocular.phar code-coverage:upload --format=php-clover coverage.clover diff --git a/README.md b/README.md index 1b5af89..81c7414 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ which you can then use in your HTML document: ``` +You can also override the theme by calling the `setTheme()` method with a new theme. + Twig Integration ---------------- diff --git a/SensioLabs/AnsiConverter/AnsiToHtmlConverter.php b/SensioLabs/AnsiConverter/AnsiToHtmlConverter.php index fdaa5f6..90d5e5f 100644 --- a/SensioLabs/AnsiConverter/AnsiToHtmlConverter.php +++ b/SensioLabs/AnsiConverter/AnsiToHtmlConverter.php @@ -1,6 +1,6 @@ theme = null === $theme ? new Theme() : $theme; - $this->inlineStyles = $inlineStyles; - $this->charset = $charset; - $this->inlineColors = $this->theme->asArray(); - $this->colorNames = array( + $this->setTheme($theme); + $this->setInlineStyles($inlineStyles); + $this->setCharset($charset); + + $this->colorNames = [ 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', '', '', 'brblack', 'brred', 'brgreen', 'bryellow', 'brblue', 'brmagenta', 'brcyan', 'brwhite', - ); + ]; } public function convert($text) { // remove cursor movement sequences - $text = preg_replace('#\e\[(K|s|u|2J|2K|\d+(A|B|C|D|E|F|G|J|K|S|T)|\d+;\d+(H|f))#', '', $text); + $text = \preg_replace('#\e\[(K|s|u|2J|2K|\d+(A|B|C|D|E|F|G|J|K|S|T)|\d+;\d+(H|f))#', '', $text); // remove character set sequences - $text = preg_replace('#\e(\(|\))(A|B|[0-2])#', '', $text); + $text = \preg_replace('#\e(\(|\))(A|B|[0-2])#', '', $text); - $text = htmlspecialchars($text, PHP_VERSION_ID >= 50400 ? ENT_QUOTES | ENT_SUBSTITUTE : ENT_QUOTES, $this->charset); + $text = \htmlspecialchars($text, \PHP_VERSION_ID >= 50400 ? ENT_QUOTES | ENT_SUBSTITUTE : ENT_QUOTES, $this->charset); // carriage return - $text = preg_replace('#^.*\r(?!\n)#m', '', $text); + $text = \preg_replace('#^.*\r(?!\n)#m', '', $text); $tokens = $this->tokenize($text); @@ -56,8 +65,8 @@ public function convert($text) if ('backspace' == $token[0]) { $j = $i; while (--$j >= 0) { - if ('text' == $tokens[$j][0] && strlen($tokens[$j][1]) > 0) { - $tokens[$j][1] = substr($tokens[$j][1], 0, -1); + if ('text' == $tokens[$j][0] && \strlen($tokens[$j][1]) > 0) { + $tokens[$j][1] = \substr($tokens[$j][1], 0, -1); break; } @@ -75,20 +84,35 @@ public function convert($text) } if ($this->inlineStyles) { - $html = sprintf('%s', $this->inlineColors['black'], $this->inlineColors['white'], $html); + $html = \sprintf('%s', $this->inlineColors['black'], $this->inlineColors['white'], $html); } else { - $html = sprintf('%s', $html); + $html = \sprintf('%s', $html); } // remove empty span - $html = preg_replace('#]*>#', '', $html); + $html = \preg_replace('#]*>#', '', $html); return $html; } - public function getTheme() + protected function tokenize($text) { - return $this->theme; + $tokens = []; + \preg_match_all('/(?:\e\[(.*?)m|(\x08))/', $text, $matches, PREG_OFFSET_CAPTURE); + + $offset = 0; + foreach ($matches[0] as $i => $match) { + if ($match[1] - $offset > 0) { + $tokens[] = ['text', \substr($text, $offset, $match[1] - $offset)]; + } + $tokens[] = ["\x08" == $match[0] ? 'backspace' : 'color', $matches[1][$i][0]]; + $offset = $match[1] + \strlen($match[0]); + } + if ($offset < \strlen($text)) { + $tokens[] = ['text', \substr($text, $offset)]; + } + + return $tokens; } protected function convertAnsiToColor($ansi) @@ -97,7 +121,7 @@ protected function convertAnsiToColor($ansi) $fg = 7; $as = ''; if ('0' != $ansi && '' != $ansi) { - $options = explode(';', $ansi); + $options = \explode(';', $ansi); foreach ($options as $option) { if ($option >= 30 && $option < 38) { @@ -112,16 +136,16 @@ protected function convertAnsiToColor($ansi) } // options: bold => 1, underscore => 4, blink => 5, reverse => 7, conceal => 8 - if (in_array(1, $options)) { + if (\in_array(1, $options)) { $fg += 10; $bg += 10; } - if (in_array(4, $options)) { + if (\in_array(4, $options)) { $as = '; text-decoration: underline'; } - if (in_array(7, $options)) { + if (\in_array(7, $options)) { $tmp = $fg; $fg = $bg; $bg = $tmp; @@ -129,29 +153,58 @@ protected function convertAnsiToColor($ansi) } if ($this->inlineStyles) { - return sprintf('', $this->inlineColors[$this->colorNames[$bg]], $this->inlineColors[$this->colorNames[$fg]], $as); + return \sprintf('', $this->inlineColors[$this->colorNames[$bg]], $this->inlineColors[$this->colorNames[$fg]], $as); } else { - return sprintf('', $this->colorNames[$bg], $this->colorNames[$fg]); + return \sprintf('', $this->colorNames[$bg], $this->colorNames[$fg]); } } - protected function tokenize($text) + /** + * @return Theme + */ + public function getTheme() { - $tokens = array(); - preg_match_all("/(?:\e\[(.*?)m|(\x08))/", $text, $matches, PREG_OFFSET_CAPTURE); + return $this->theme; + } - $offset = 0; - foreach ($matches[0] as $i => $match) { - if ($match[1] - $offset > 0) { - $tokens[] = array('text', substr($text, $offset, $match[1] - $offset)); - } - $tokens[] = array("\x08" == $match[0] ? 'backspace' : 'color', $matches[1][$i][0]); - $offset = $match[1] + strlen($match[0]); - } - if ($offset < strlen($text)) { - $tokens[] = array('text', substr($text, $offset)); - } + /** + * @param Theme|null $theme + */ + public function setTheme(Theme $theme = null) + { + $this->theme = null === $theme ? new Theme() : $theme; + $this->inlineColors = $this->theme->asArray(); + } - return $tokens; + /** + * @return bool + */ + public function isInlineStyles() + { + return $this->inlineStyles; + } + + /** + * @param bool $inlineStyles + */ + public function setInlineStyles($inlineStyles) + { + $this->inlineStyles = $inlineStyles; + } + + /** + * @return string + */ + public function getCharset() + { + return $this->charset; + } + + /** + * @param string $charset + */ + public function setCharset($charset) + { + $this->charset = $charset; } } diff --git a/SensioLabs/AnsiConverter/Bridge/Twig/AnsiExtension.php b/SensioLabs/AnsiConverter/Bridge/Twig/AnsiExtension.php index a109bc4..3675972 100644 --- a/SensioLabs/AnsiConverter/Bridge/Twig/AnsiExtension.php +++ b/SensioLabs/AnsiConverter/Bridge/Twig/AnsiExtension.php @@ -1,10 +1,22 @@ array('html'))), - ); + return [ + new TwigFilter('ansi_to_html', [$this, 'ansiToHtml'], ['is_safe' => ['html']]), + ]; } public function getFunctions() { - return array( - new \Twig_SimpleFunction('ansi_css', array($this, 'css'), array('is_safe' => array('css'))), - ); + return [ + new TwigFunction('ansi_css', [$this, 'css'], ['is_safe' => ['css']]), + ]; } public function ansiToHtml($string) diff --git a/SensioLabs/AnsiConverter/Tests/AnsiToHtmlConverterTest.php b/SensioLabs/AnsiConverter/Tests/AnsiToHtmlConverterTest.php index 61a3313..21a3213 100644 --- a/SensioLabs/AnsiConverter/Tests/AnsiToHtmlConverterTest.php +++ b/SensioLabs/AnsiConverter/Tests/AnsiToHtmlConverterTest.php @@ -1,6 +1,6 @@ assertEquals($expected, $converter->convert($input)); } - public function getConvertData() + /** + * @dataProvider getConvertDataWithSolarizedTheme + */ + public function testConvertWithSetTheme($expected, $input) + { + $converter = new AnsiToHtmlConverter(); + $converter->setTheme(new SolarizedTheme()); + $this->assertEquals($expected, $converter->convert($input)); + } + + /** + * @dataProvider getConvertDataWithSolarizedXTermTheme + */ + public function testConvertWithInjectedTheme($expected, $input) + { + $converter = new AnsiToHtmlConverter(new SolarizedXTermTheme()); + $this->assertEquals($expected, $converter->convert($input)); + } + + public function getConvertDataStandardTheme() + { + return [ + // text is escaped + ['foo <br />', 'foo
'], + + // newlines are preserved + ["foo\nbar", "foo\nbar"], + + // backspaces + ['foo ', "foobar\x08\x08\x08 "], + ['foo ', "foob\e[31;41ma\e[0mr\x08\x08\x08 "], + + // color + ['foo', "\e[31;41mfoo\e[0m"], + + // color with [m as a termination (equivalent to [0m]) + ['foo', "\e[31;41mfoo\e[m"], + + // bright color + ['foo', "\e[31;41;1mfoo\e[0m"], + + // carriage returns + ['foobar', "foo\rbar\rfoobar"], + + // underline + ['foo', "\e[4mfoo\e[0m"], + + // non valid unicode codepoints substitution (only available with PHP >= 5.4) + \PHP_VERSION_ID < 50400 ? ['', ''] : ['foo '."\xEF\xBF\xBD".'', "foo \xF4\xFF\xFF\xFF"], + ]; + } + + public function getConvertDataWithSolarizedTheme() + { + return [ + // text is escaped + ['foo <br />', 'foo
'], + + // newlines are preserved + ["foo\nbar", "foo\nbar"], + + // backspaces + ['foo ', "foobar\x08\x08\x08 "], + ['foo ', "foob\e[31;41ma\e[0mr\x08\x08\x08 "], + + // color + ['foo', "\e[31;41mfoo\e[0m"], + + // color with [m as a termination (equivalent to [0m]) + ['foo', "\e[31;41mfoo\e[m"], + + // bright color + ['foo', "\e[31;41;1mfoo\e[0m"], + + // carriage returns + ['foobar', "foo\rbar\rfoobar"], + + // underline + ['foo', "\e[4mfoo\e[0m"], + + // non valid unicode codepoints substitution (only available with PHP >= 5.4) + \PHP_VERSION_ID < 50400 ? ['', ''] : ['foo '."\xEF\xBF\xBD".'', "foo \xF4\xFF\xFF\xFF"], + ]; + } + + public function getConvertDataWithSolarizedXTermTheme() { - return array( + return [ // text is escaped - array('foo <br />', 'foo
'), + ['foo <br />', 'foo
'], // newlines are preserved - array("foo\nbar", "foo\nbar"), + ["foo\nbar", "foo\nbar"], // backspaces - array('foo ', "foobar\x08\x08\x08 "), - array('foo ', "foob\e[31;41ma\e[0mr\x08\x08\x08 "), + ['foo ', "foobar\x08\x08\x08 "], + ['foo ', "foob\e[31;41ma\e[0mr\x08\x08\x08 "], // color - array('foo', "\e[31;41mfoo\e[0m"), + ['foo', "\e[31;41mfoo\e[0m"], // color with [m as a termination (equivalent to [0m]) - array('foo', "\e[31;41mfoo\e[m"), + ['foo', "\e[31;41mfoo\e[m"], // bright color - array('foo', "\e[31;41;1mfoo\e[0m"), + ['foo', "\e[31;41;1mfoo\e[0m"], // carriage returns - array('foobar', "foo\rbar\rfoobar"), + ['foobar', "foo\rbar\rfoobar"], // underline - array('foo', "\e[4mfoo\e[0m"), + ['foo', "\e[4mfoo\e[0m"], // non valid unicode codepoints substitution (only available with PHP >= 5.4) - PHP_VERSION_ID < 50400 ?: array('foo '."\xEF\xBF\xBD".'', "foo \xF4\xFF\xFF\xFF"), - ); + \PHP_VERSION_ID < 50400 ? ['', ''] : ['foo '."\xEF\xBF\xBD".'', "foo \xF4\xFF\xFF\xFF"], + ]; } } diff --git a/SensioLabs/AnsiConverter/Theme/SolarizedTheme.php b/SensioLabs/AnsiConverter/Theme/SolarizedTheme.php index 2cb6682..291ea2c 100644 --- a/SensioLabs/AnsiConverter/Theme/SolarizedTheme.php +++ b/SensioLabs/AnsiConverter/Theme/SolarizedTheme.php @@ -1,6 +1,6 @@ '#073642', 'red' => '#dc322f', @@ -40,6 +40,6 @@ public function asArray() 'brmagenta' => '#6c71c4', 'brcyan' => '#93a1a1', 'brwhite' => '#fdf6e3', - ); + ]; } } diff --git a/SensioLabs/AnsiConverter/Theme/SolarizedXTermTheme.php b/SensioLabs/AnsiConverter/Theme/SolarizedXTermTheme.php index e9e087a..a877f17 100644 --- a/SensioLabs/AnsiConverter/Theme/SolarizedXTermTheme.php +++ b/SensioLabs/AnsiConverter/Theme/SolarizedXTermTheme.php @@ -1,6 +1,6 @@ '#262626', 'red' => '#d70000', @@ -40,6 +40,6 @@ public function asArray() 'brmagenta' => '#5f5faf', 'brcyan' => '#8a8a8a', 'brwhite' => '#ffffd7', - ); + ]; } } diff --git a/SensioLabs/AnsiConverter/Theme/Theme.php b/SensioLabs/AnsiConverter/Theme/Theme.php index 6b818e2..d6706a4 100644 --- a/SensioLabs/AnsiConverter/Theme/Theme.php +++ b/SensioLabs/AnsiConverter/Theme/Theme.php @@ -1,6 +1,6 @@ asArray() as $name => $color) { - $css[] = sprintf('.%s_fg_%s { color: %s }', $prefix, $name, $color); - $css[] = sprintf('.%s_bg_%s { background-color: %s }', $prefix, $name, $color); + $css[] = \sprintf('.%s_fg_%s { color: %s }', $prefix, $name, $color); + $css[] = \sprintf('.%s_bg_%s { background-color: %s }', $prefix, $name, $color); } - return implode("\n", $css); + return \implode("\n", $css); } public function asArray() { - return array( + return [ 'black' => 'black', 'red' => 'darkred', 'green' => 'green', @@ -47,6 +47,6 @@ public function asArray() 'brmagenta' => 'magenta', 'brcyan' => 'lightcyan', 'brwhite' => 'white', - ); + ]; } } diff --git a/composer.json b/composer.json index 8ad0c4e..19a55a4 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,13 @@ } ], "require": { - "php": ">=5.3.0" + "php": "^7" + }, + "require-dev": { + "phpunit/phpunit": "^6", + "phpstan/phpstan": "^0.9.2", + "twig/twig": "^2.12", + "friendsofphp/php-cs-fixer": "^2.15" }, "suggest": { "twig/twig": "Provides nice templating features" @@ -22,5 +28,9 @@ "branch-alias": { "dev-master": "1.1-dev" } + }, + "scripts": { + "cs": "vendor/bin/php-cs-fixer fix . -vvv", + "sa": "vendor/bin/phpstan analyse --ansi -l 7 --no-progress SensioLabs" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fe0e83b..b245384 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,19 +1,37 @@ - ./SensioLabs/AnsiConverter/Tests + + + + ./SensioLabs/ + + +