diff --git a/.github/workflows/apigen.yml b/.github/workflows/apigen.yml index 4db6741..b5a4ec9 100644 --- a/.github/workflows/apigen.yml +++ b/.github/workflows/apigen.yml @@ -10,11 +10,30 @@ on: jobs: Document_Generator: runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - - uses: actions/checkout@v2 - - name: 📝 ApiGen PHP Document Generator - uses: varunsridharan/action-apigen@2.0 + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + + - name: Download phpDocumentor + run: | + curl -fsSL -o phpDocumentor.phar https://phpdoc.org/phpDocumentor.phar + chmod +x phpDocumentor.phar + + - name: Generate API docs + run: php phpDocumentor.phar -d src -t docs --no-interaction + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 with: - cached_apigen: 'no' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs + publish_branch: gh-pages + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' + commit_message: 'Docs updated by GitHub Actions' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 74ee02d..8830b94 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,12 +10,12 @@ jobs: strategy: matrix: - php-versions: ['7.2', '7.3', '7.4', '8.0'] + php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] fail-fast: false steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.gitignore b/.gitignore index 9f150ad..8f47dcd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ vendor/ composer.lock apigen.phar docs/ +.phpdoc/ .phpunit.result.cache diff --git a/README.md b/README.md index 7c68cea..fcae998 100644 --- a/README.md +++ b/README.md @@ -75,12 +75,12 @@ for further info. ## Exceptions -By default the CLI class registers an exception handler and will print the exception's message to the end user and +By default, the CLI class registers an exception handler and will print the exception's message to the end user and exit the programm with a non-zero exit code. You can disable this behaviour and catch all exceptions yourself by passing false to the constructor. You can use the provided ``splitbrain\phpcli\Exception`` to signal any problems within your main code yourself. The -exceptions's code will be used as the exit code then. +exception's code will be used as the exit code then. Stacktraces will be printed on log level `debug`. @@ -129,11 +129,17 @@ The table formatter is used for the automatic help screen accessible when callin The CLI class is a fully PSR-3 compatible logger (printing colored log data to STDOUT and STDERR). This is useful when you call backend code from your CLI that expects a Logger instance to produce any sensible status output while running. - -To use this ability simply inherit from `splitbrain\phpcli\PSR3CLI` instead of `splitbrain\phpcli\CLI`, then pass `$this` -as the logger instance. Be sure you have the suggested `psr/log` composer package installed. -![Screenshot](screenshot2.png) +If you need to pass a class implementing the `Psr\Log\LoggerInterface` you can do so by inheriting from one of the two provided classes implementing this interface instead of `splitbrain\phpcli\CLI`. + + * Use `splitbrain\phpcli\PSR3CLI` if you're using version 2 of PSR3 (PHP < 8.0) + * Use `splitbrain\phpcli\PSR3CLIv3` if you're using version 3 of PSR3 (PHP >= 8.0) + +The resulting object then can be passed as the logger instance. The difference between the two is in adjusted method signatures (with appropriate type hinting) only. Be sure you have the suggested `psr/log` composer package installed when using these classes. + +Note: if your backend code calls for a PSR-3 logger but does not actually type check for the interface (AKA being LoggerAware only) you can also just pass an instance of `splitbrain\phpcli\CLI`. + +## Log Levels You can adjust the verbosity of your CLI tool using the `--loglevel` parameter. Supported loglevels are the PSR-3 loglevels and our own `success` level: @@ -141,13 +147,15 @@ loglevels and our own `success` level: * debug * info * notice -* success +* success (this is not defined in PSR-3) * warning * error * critical * alert * emergency +![Screenshot](screenshot2.png) + Convenience methods for all log levels are available. Placeholder interpolation as described in PSR-3 is available, too. Messages from `warning` level onwards are printed to `STDERR` all below are printed to `STDOUT`. diff --git a/apigen.neon b/apigen.neon deleted file mode 100644 index 4a7d196..0000000 --- a/apigen.neon +++ /dev/null @@ -1,4 +0,0 @@ -tree: Yes -deprecated: Yes -accessLevels: [public] -todo: Yes diff --git a/src/Base.php b/src/Base.php new file mode 100644 index 0000000..a3b6049 --- /dev/null +++ b/src/Base.php @@ -0,0 +1,333 @@ + + * @license MIT + */ +abstract class Base +{ + /** @var string the executed script itself */ + protected $bin; + /** @var Options the option parser */ + protected $options; + /** @var Colors */ + public $colors; + + /** @var array PSR-3 compatible loglevels and their prefix, color, output channel, enabled status */ + protected $loglevel = array( + 'debug' => array( + 'icon' => '', + 'color' => Colors::C_RESET, + 'channel' => STDOUT, + 'enabled' => true + ), + 'info' => array( + 'icon' => 'ℹ ', + 'color' => Colors::C_CYAN, + 'channel' => STDOUT, + 'enabled' => true + ), + 'notice' => array( + 'icon' => '☛ ', + 'color' => Colors::C_CYAN, + 'channel' => STDOUT, + 'enabled' => true + ), + 'success' => array( + 'icon' => '✓ ', + 'color' => Colors::C_GREEN, + 'channel' => STDOUT, + 'enabled' => true + ), + 'warning' => array( + 'icon' => '⚠ ', + 'color' => Colors::C_BROWN, + 'channel' => STDERR, + 'enabled' => true + ), + 'error' => array( + 'icon' => '✗ ', + 'color' => Colors::C_RED, + 'channel' => STDERR, + 'enabled' => true + ), + 'critical' => array( + 'icon' => '☠ ', + 'color' => Colors::C_LIGHTRED, + 'channel' => STDERR, + 'enabled' => true + ), + 'alert' => array( + 'icon' => '✖ ', + 'color' => Colors::C_LIGHTRED, + 'channel' => STDERR, + 'enabled' => true + ), + 'emergency' => array( + 'icon' => '✘ ', + 'color' => Colors::C_LIGHTRED, + 'channel' => STDERR, + 'enabled' => true + ), + ); + + /** @var string default log level */ + protected $logdefault = 'info'; + + /** + * constructor + * + * Initialize the arguments, set up helper classes and set up the CLI environment + * + * @param bool $autocatch should exceptions be catched and handled automatically? + */ + public function __construct($autocatch = true) + { + if ($autocatch) { + set_exception_handler(array($this, 'fatal')); + } + $this->setLogLevel($this->logdefault); + $this->colors = new Colors(); + $this->options = new Options($this->colors); + } + + /** + * Register options and arguments on the given $options object + * + * @param Options $options + * @return void + * + * @throws Exception + */ + abstract protected function setup(Options $options); + + /** + * Your main program + * + * Arguments and options have been parsed when this is run + * + * @param Options $options + * @return void + * + * @throws Exception + */ + abstract protected function main(Options $options); + + /** + * Execute the CLI program + * + * Executes the setup() routine, adds default options, initiate the options parsing and argument checking + * and finally executes main() - Each part is split into their own protected function below, so behaviour + * can easily be overwritten + * + * @throws Exception + */ + public function run() + { + if ('cli' != php_sapi_name()) { + throw new Exception('This has to be run from the command line'); + } + + $this->setup($this->options); + $this->registerDefaultOptions(); + $this->parseOptions(); + $this->handleDefaultOptions(); + $this->setupLogging(); + $this->checkArguments(); + $this->execute(); + } + + // region run handlers - for easier overriding + + /** + * Add the default help, color and log options + */ + protected function registerDefaultOptions() + { + $this->options->registerOption( + 'help', + 'Display this help screen and exit immediately.', + 'h' + ); + $this->options->registerOption( + 'no-colors', + 'Do not use any colors in output. Useful when piping output to other tools or files.' + ); + $this->options->registerOption( + 'loglevel', + 'Minimum level of messages to display. Default is ' . $this->colors->wrap($this->logdefault, Colors::C_CYAN) . '. ' . + 'Valid levels are: debug, info, notice, success, warning, error, critical, alert, emergency.', + null, + 'level' + ); + } + + /** + * Handle the default options + */ + protected function handleDefaultOptions() + { + if ($this->options->getOpt('no-colors')) { + $this->colors->disable(); + } + if ($this->options->getOpt('help')) { + echo $this->options->help(); + exit(0); + } + } + + /** + * Handle the logging options + */ + protected function setupLogging() + { + $level = $this->options->getOpt('loglevel', $this->logdefault); + $this->setLogLevel($level); + } + + /** + * Wrapper around the option parsing + */ + protected function parseOptions() + { + $this->options->parseOptions(); + } + + /** + * Wrapper around the argument checking + */ + protected function checkArguments() + { + $this->options->checkArguments(); + } + + /** + * Wrapper around main + */ + protected function execute() + { + $this->main($this->options); + } + + // endregion + + // region logging + + /** + * Set the current log level + * + * @param string $level + */ + public function setLogLevel($level) + { + if (!isset($this->loglevel[$level])) $this->fatal('Unknown log level'); + $enable = false; + foreach (array_keys($this->loglevel) as $l) { + if ($l == $level) $enable = true; + $this->loglevel[$l]['enabled'] = $enable; + } + } + + /** + * Check if a message with the given level should be logged + * + * @param string $level + * @return bool + */ + public function isLogLevelEnabled($level) + { + if (!isset($this->loglevel[$level])) $this->fatal('Unknown log level'); + return $this->loglevel[$level]['enabled']; + } + + /** + * Exits the program on a fatal error + * + * @param \Exception|string $error either an exception or an error message + * @param array $context + */ + public function fatal($error, array $context = array()) + { + $code = 0; + if (is_object($error) && is_a($error, 'Exception')) { + /** @var Exception $error */ + $this->logMessage('debug', get_class($error) . ' caught in ' . $error->getFile() . ':' . $error->getLine()); + $this->logMessage('debug', $error->getTraceAsString()); + $code = $error->getCode(); + $error = $error->getMessage(); + + } + if (!$code) { + $code = Exception::E_ANY; + } + + $this->logMessage('critical', $error, $context); + exit($code); + } + + /** + * Normal, positive outcome (This is not a PSR-3 level) + * + * @param string $string + * @param array $context + */ + public function success($string, array $context = array()) + { + $this->logMessage('success', $string, $context); + } + + /** + * @param string $level + * @param string $message + * @param array $context + */ + protected function logMessage($level, $message, array $context = array()) + { + // unknown level is always an error + if (!isset($this->loglevel[$level])) $level = 'error'; + + $info = $this->loglevel[$level]; + if (!$this->isLogLevelEnabled($level)) return; // no logging for this level + + $message = $this->interpolate($message, $context); + + // when colors are wanted, we also add the icon + if ($this->colors->isEnabled()) { + $message = $info['icon'] . $message; + } + + $this->colors->ptln($message, $info['color'], $info['channel']); + } + + /** + * Interpolates context values into the message placeholders. + * + * @param $message + * @param array $context + * @return string + */ + protected function interpolate($message, array $context = array()) + { + // build a replacement array with braces around the context keys + $replace = array(); + foreach ($context as $key => $val) { + // check that the value can be casted to string + if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { + $replace['{' . $key . '}'] = $val; + } + } + + // interpolate replacement values into the message and return + return strtr((string)$message, $replace); + } + + // endregion +} diff --git a/src/CLI.php b/src/CLI.php index 2ee7990..217983a 100644 --- a/src/CLI.php +++ b/src/CLI.php @@ -10,200 +10,8 @@ * @author Andreas Gohr * @license MIT */ -abstract class CLI +abstract class CLI extends Base { - /** @var string the executed script itself */ - protected $bin; - /** @var Options the option parser */ - protected $options; - /** @var Colors */ - public $colors; - - /** @var array PSR-3 compatible loglevels and their prefix, color, output channel */ - protected $loglevel = array( - 'debug' => array('', Colors::C_RESET, STDOUT), - 'info' => array('ℹ ', Colors::C_CYAN, STDOUT), - 'notice' => array('☛ ', Colors::C_CYAN, STDOUT), - 'success' => array('✓ ', Colors::C_GREEN, STDOUT), - 'warning' => array('⚠ ', Colors::C_BROWN, STDERR), - 'error' => array('✗ ', Colors::C_RED, STDERR), - 'critical' => array('☠ ', Colors::C_LIGHTRED, STDERR), - 'alert' => array('✖ ', Colors::C_LIGHTRED, STDERR), - 'emergency' => array('✘ ', Colors::C_LIGHTRED, STDERR), - ); - - protected $logdefault = 'info'; - - /** - * constructor - * - * Initialize the arguments, set up helper classes and set up the CLI environment - * - * @param bool $autocatch should exceptions be catched and handled automatically? - */ - public function __construct($autocatch = true) - { - if ($autocatch) { - set_exception_handler(array($this, 'fatal')); - } - - $this->colors = new Colors(); - $this->options = new Options($this->colors); - } - - /** - * Register options and arguments on the given $options object - * - * @param Options $options - * @return void - * - * @throws Exception - */ - abstract protected function setup(Options $options); - - /** - * Your main program - * - * Arguments and options have been parsed when this is run - * - * @param Options $options - * @return void - * - * @throws Exception - */ - abstract protected function main(Options $options); - - /** - * Execute the CLI program - * - * Executes the setup() routine, adds default options, initiate the options parsing and argument checking - * and finally executes main() - Each part is split into their own protected function below, so behaviour - * can easily be overwritten - * - * @throws Exception - */ - public function run() - { - if ('cli' != php_sapi_name()) { - throw new Exception('This has to be run from the command line'); - } - - $this->setup($this->options); - $this->registerDefaultOptions(); - $this->parseOptions(); - $this->handleDefaultOptions(); - $this->setupLogging(); - $this->checkArgments(); - $this->execute(); - - exit(0); - } - - // region run handlers - for easier overriding - - /** - * Add the default help, color and log options - */ - protected function registerDefaultOptions() - { - $this->options->registerOption( - 'help', - 'Display this help screen and exit immediately.', - 'h' - ); - $this->options->registerOption( - 'no-colors', - 'Do not use any colors in output. Useful when piping output to other tools or files.' - ); - $this->options->registerOption( - 'loglevel', - 'Minimum level of messages to display. Default is ' . $this->colors->wrap($this->logdefault, Colors::C_CYAN) . '. ' . - 'Valid levels are: debug, info, notice, success, warning, error, critical, alert, emergency.', - null, - 'level' - ); - } - - /** - * Handle the default options - */ - protected function handleDefaultOptions() - { - if ($this->options->getOpt('no-colors')) { - $this->colors->disable(); - } - if ($this->options->getOpt('help')) { - echo $this->options->help(); - exit(0); - } - } - - /** - * Handle the logging options - */ - protected function setupLogging() - { - $level = $this->options->getOpt('loglevel', $this->logdefault); - if (!isset($this->loglevel[$level])) $this->fatal('Unknown log level'); - foreach (array_keys($this->loglevel) as $l) { - if ($l == $level) break; - unset($this->loglevel[$l]); - } - } - - /** - * Wrapper around the option parsing - */ - protected function parseOptions() - { - $this->options->parseOptions(); - } - - /** - * Wrapper around the argument checking - */ - protected function checkArgments() - { - $this->options->checkArguments(); - } - - /** - * Wrapper around main - */ - protected function execute() - { - $this->main($this->options); - } - - // endregion - - // region logging - - /** - * Exits the program on a fatal error - * - * @param \Exception|string $error either an exception or an error message - * @param array $context - */ - public function fatal($error, array $context = array()) - { - $code = 0; - if (is_object($error) && is_a($error, 'Exception')) { - /** @var Exception $error */ - $this->debug(get_class($error) . ' caught in ' . $error->getFile() . ':' . $error->getLine()); - $this->debug($error->getTraceAsString()); - $code = $error->getCode(); - $error = $error->getMessage(); - - } - if (!$code) { - $code = Exception::E_ANY; - } - - $this->critical($error, $context); - exit($code); - } - /** * System is unusable. * @@ -270,16 +78,7 @@ public function warning($message, array $context = array()) $this->log('warning', $message, $context); } - /** - * Normal, positive outcome - * - * @param string $string - * @param array $context - */ - public function success($string, array $context = array()) - { - $this->log('success', $string, $context); - } + /** * Normal but significant events. @@ -323,40 +122,6 @@ public function debug($message, array $context = array()) */ public function log($level, $message, array $context = array()) { - // is this log level wanted? - if (!isset($this->loglevel[$level])) return; - - /** @var string $prefix */ - /** @var string $color */ - /** @var resource $channel */ - list($prefix, $color, $channel) = $this->loglevel[$level]; - if (!$this->colors->isEnabled()) $prefix = ''; - - $message = $this->interpolate($message, $context); - $this->colors->ptln($prefix . $message, $color, $channel); + $this->logMessage($level, $message, $context); } - - /** - * Interpolates context values into the message placeholders. - * - * @param $message - * @param array $context - * @return string - */ - function interpolate($message, array $context = array()) - { - // build a replacement array with braces around the context keys - $replace = array(); - foreach ($context as $key => $val) { - // check that the value can be casted to string - if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { - $replace['{' . $key . '}'] = $val; - } - } - - // interpolate replacement values into the message and return - return strtr($message, $replace); - } - - // endregion } diff --git a/src/Colors.php b/src/Colors.php index ae25256..dd1fd04 100644 --- a/src/Colors.php +++ b/src/Colors.php @@ -31,6 +31,9 @@ class Colors const C_LIGHTGRAY = 'lightgray'; const C_WHITE = 'white'; + // Regex pattern to match color codes + const C_CODE_REGEX = "/(\33\[[0-9;]+m)/"; + /** @var array known color names */ protected $colors = array( self::C_RESET => "\33[0m", @@ -70,6 +73,10 @@ public function __construct() $this->enabled = false; return; } + if (getenv('NO_COLOR')) { // https://no-color.org/ + $this->enabled = false; + return; + } } /** @@ -107,9 +114,9 @@ public function isEnabled() */ public function ptln($line, $color, $channel = STDOUT) { - $this->set($color); + $this->set($color, $channel); fwrite($channel, rtrim($line) . "\n"); - $this->reset(); + $this->reset($channel); } /** diff --git a/src/Exception.php b/src/Exception.php index 4d24d58..0dd58ca 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -25,7 +25,7 @@ class Exception extends \RuntimeException * @param int $code The Exception code * @param \Exception $previous The previous exception used for the exception chaining. */ - public function __construct($message = "", $code = 0, \Exception $previous = null) + public function __construct($message = "", $code = 0, ?\Exception $previous = null) { if (!$code) { $code = self::E_ANY; diff --git a/src/Options.php b/src/Options.php index 5ee6b69..1c0752b 100644 --- a/src/Options.php +++ b/src/Options.php @@ -40,7 +40,7 @@ class Options * @param Colors $colors optional configured color object * @throws Exception when arguments can't be read */ - public function __construct(Colors $colors = null) + public function __construct(?Colors $colors = null) { if (!is_null($colors)) { $this->colors = $colors; diff --git a/src/PSR3CLI.php b/src/PSR3CLI.php index ef744f3..0078cd7 100644 --- a/src/PSR3CLI.php +++ b/src/PSR3CLI.php @@ -7,7 +7,10 @@ /** * Class PSR3CLI * - * The same as CLI, but implements the PSR-3 logger interface + * This class can be used instead of the CLI class when a class implementing + * PSR3 version 2 is needed. + * + * @see PSR3CLIv3 for a version 3 compatible class */ abstract class PSR3CLI extends CLI implements LoggerInterface { -} \ No newline at end of file +} diff --git a/src/PSR3CLIv3.php b/src/PSR3CLIv3.php new file mode 100644 index 0000000..9d99a3a --- /dev/null +++ b/src/PSR3CLIv3.php @@ -0,0 +1,24 @@ +logMessage($level, $message, $context); + } +} diff --git a/src/TableFormatter.php b/src/TableFormatter.php index 23bb894..d952a6e 100644 --- a/src/TableFormatter.php +++ b/src/TableFormatter.php @@ -26,7 +26,7 @@ class TableFormatter * * @param Colors|null $colors */ - public function __construct(Colors $colors = null) + public function __construct(?Colors $colors = null) { // try to get terminal width $width = $this->getTerminalWidth(); @@ -293,6 +293,7 @@ protected function substr($string, $start = 0, $length = null) protected function wordwrap($str, $width = 75, $break = "\n", $cut = false) { $lines = explode($break, $str); + $color_reset = $this->colors->getColorCode(Colors::C_RESET); foreach ($lines as &$line) { $line = rtrim($line); if ($this->strlen($line) <= $width) { @@ -301,18 +302,30 @@ protected function wordwrap($str, $width = 75, $break = "\n", $cut = false) $words = explode(' ', $line); $line = ''; $actual = ''; + $color = ''; foreach ($words as $word) { + if (preg_match_all(Colors::C_CODE_REGEX, $word, $color_codes) ) { + # Word contains color codes + foreach ($color_codes[0] as $code) { + if ($code == $color_reset) { + $color = ''; + } else { + # Remember color so we can reapply it after a line break + $color = $code; + } + } + } if ($this->strlen($actual . $word) <= $width) { $actual .= $word . ' '; } else { if ($actual != '') { $line .= rtrim($actual) . $break; } - $actual = $word; + $actual = $color . $word; if ($cut) { while ($this->strlen($actual) > $width) { $line .= $this->substr($actual, 0, $width) . $break; - $actual = $this->substr($actual, $width); + $actual = $color . $this->substr($actual, $width); } } $actual .= ' '; @@ -322,4 +335,4 @@ protected function wordwrap($str, $width = 75, $break = "\n", $cut = false) } return implode($break, $lines); } -} \ No newline at end of file +} diff --git a/tests/LogLevelTest.php b/tests/LogLevelTest.php new file mode 100644 index 0000000..1e5fcc8 --- /dev/null +++ b/tests/LogLevelTest.php @@ -0,0 +1,91 @@ +setLogLevel($level); + foreach ($enabled as $e) { + $this->assertTrue($cli->isLogLevelEnabled($e), "$e is not enabled but should be"); + } + foreach ($disabled as $d) { + $this->assertFalse($cli->isLogLevelEnabled($d), "$d is enabled but should not be"); + } + } + + +} diff --git a/tests/TableFormatterTest.php b/tests/TableFormatterTest.php index 687643a..3b1860a 100644 --- a/tests/TableFormatterTest.php +++ b/tests/TableFormatterTest.php @@ -104,7 +104,7 @@ public function test_length() $tf = new TableFormatter(); $tf->setBorder('|'); - $result = $tf->format([20, '*'], [$text, 'test']); + $result = $tf->format(array(20, '*'), array($text, 'test')); $this->assertEquals($expect, trim($result)); } @@ -118,7 +118,7 @@ public function test_colorlength() $tf = new TableFormatter(); $tf->setBorder('|'); - $result = $tf->format([20, '*'], [$text, 'test']); + $result = $tf->format(array(20, '*'), array($text, 'test')); $this->assertEquals($expect, trim($result)); } @@ -135,7 +135,54 @@ public function test_onewrap() $tf->setMaxWidth(11); $tf->setBorder('|'); - $result = $tf->format([5, '*'], [$col1, $col2]); + $result = $tf->format(array(5, '*'), array($col1, $col2)); $this->assertEquals($expect, $result); } + + /** + * Test that colors are correctly applied when text is wrapping across lines. + * + * @dataProvider colorwrapProvider + */ + public function test_colorwrap($text, $expect) + { + $tf = new TableFormatter(); + $tf->setMaxWidth(15); + + $this->assertEquals($expect, $tf->format(array('*'), array($text))); + } + + /** + * Data provider for test_colorwrap. + * + * @return array[] + */ + public function colorwrapProvider() + { + $color = new Colors(); + $cyan = $color->getColorCode(Colors::C_CYAN); + $reset = $color->getColorCode(Colors::C_RESET); + $wrap = function ($str) use ($color) { + return $color->wrap($str, Colors::C_CYAN); + }; + + return array( + 'color word line 1' => array( + "This is ". $wrap("cyan") . " text wrapping", + "This is {$cyan}cyan{$reset} \ntext wrapping \n", + ), + 'color word line 2' => array( + "This is text ". $wrap("cyan") . " wrapping", + "This is text \n{$cyan}cyan{$reset} wrapping \n", + ), + 'color across lines' => array( + "This is ". $wrap("cyan text") . " wrapping", + "This is {$cyan}cyan \ntext{$reset} wrapping \n", + ), + 'color across lines until end' => array( + "This is ". $wrap("cyan text wrapping"), + "This is {$cyan}cyan \n{$cyan}text wrapping{$reset} \n", + ), + ); + } }