1<?php
2
3/**
4 * @see       https://github.com/laminas/laminas-stdlib for the canonical source repository
5 * @copyright https://github.com/laminas/laminas-stdlib/blob/master/COPYRIGHT.md
6 * @license   https://github.com/laminas/laminas-stdlib/blob/master/LICENSE.md New BSD License
7 */
8
9namespace Laminas\Stdlib;
10
11use function function_exists;
12use function fwrite;
13use function getenv;
14use function posix_isatty;
15use function preg_replace;
16use function sprintf;
17use function str_replace;
18
19use const DIRECTORY_SEPARATOR;
20use const PHP_EOL;
21use const STDERR;
22use const STDOUT;
23
24/**
25 * Utilities for console tooling.
26 *
27 * Provides the following facilities:
28 *
29 * - Colorize strings using markup (e.g., `<info>message</info>`,
30 *   `<error>message</error>`)
31 * - Write output to a specified stream, optionally with colorization.
32 * - Write a line of output to a specified stream, optionally with
33 *   colorization, using the system EOL sequence..
34 * - Write an error message to STDERR.
35 *
36 * Colorization will only occur when expected sequences are discovered, and
37 * then, only if the console terminal allows it.
38 *
39 * Essentially, provides the bare minimum to allow you to provide messages to
40 * the current console.
41 */
42class ConsoleHelper
43{
44    const COLOR_GREEN = "\033[32m";
45    const COLOR_RED   = "\033[31m";
46    const COLOR_RESET = "\033[0m";
47
48    const HIGHLIGHT_INFO  = 'info';
49    const HIGHLIGHT_ERROR = 'error';
50
51    private $highlightMap = [
52        self::HIGHLIGHT_INFO  => self::COLOR_GREEN,
53        self::HIGHLIGHT_ERROR => self::COLOR_RED,
54    ];
55
56    /**
57     * @var string Exists only for testing.
58     */
59    private $eol = PHP_EOL;
60
61    /**
62     * @var resource Exists only for testing.
63     */
64    private $stderr = STDERR;
65
66    /**
67     * @var bool
68     */
69    private $supportsColor;
70
71    /**
72     * @param resource $resource
73     */
74    public function __construct($resource = STDOUT)
75    {
76        $this->supportsColor = $this->detectColorCapabilities($resource);
77    }
78
79    /**
80     * Colorize a string for use with the terminal.
81     *
82     * Takes strings formatted as `<key>string</key>` and formats them per the
83     * $highlightMap; if color support is disabled, simply removes the formatting
84     * tags.
85     *
86     * @param string $string
87     * @return string
88     */
89    public function colorize($string)
90    {
91        $reset = $this->supportsColor ? self::COLOR_RESET : '';
92        foreach ($this->highlightMap as $key => $color) {
93            $pattern = sprintf('#<%s>(.*?)</%s>#s', $key, $key);
94            $color   = $this->supportsColor ? $color : '';
95            $string  = preg_replace($pattern, $color . '$1' . $reset, $string);
96        }
97        return $string;
98    }
99
100    /**
101     * @param string $string
102     * @param bool $colorize Whether or not to colorize the string
103     * @param resource $resource Defaults to STDOUT
104     * @return void
105     */
106    public function write($string, $colorize = true, $resource = STDOUT)
107    {
108        if ($colorize) {
109            $string = $this->colorize($string);
110        }
111
112        $string = $this->formatNewlines($string);
113
114        fwrite($resource, $string);
115    }
116
117    /**
118     * @param string $string
119     * @param bool $colorize Whether or not to colorize the line
120     * @param resource $resource Defaults to STDOUT
121     * @return void
122     */
123    public function writeLine($string, $colorize = true, $resource = STDOUT)
124    {
125        $this->write($string . $this->eol, $colorize, $resource);
126    }
127
128    /**
129     * Emit an error message.
130     *
131     * Wraps the message in `<error></error>`, and passes it to `writeLine()`,
132     * using STDERR as the resource; emits an additional empty line when done,
133     * also to STDERR.
134     *
135     * @param string $message
136     * @return void
137     */
138    public function writeErrorMessage($message)
139    {
140        $this->writeLine(sprintf('<error>%s</error>', $message), true, $this->stderr);
141        $this->writeLine('', false, $this->stderr);
142    }
143
144    /**
145     * @param resource $resource
146     * @return bool
147     */
148    private function detectColorCapabilities($resource = STDOUT)
149    {
150        if ('\\' === DIRECTORY_SEPARATOR) {
151            // Windows
152            return false !== getenv('ANSICON')
153                || 'ON' === getenv('ConEmuANSI')
154                || 'xterm' === getenv('TERM');
155        }
156
157        return function_exists('posix_isatty') && posix_isatty($resource);
158    }
159
160    /**
161     * Ensure newlines are appropriate for the current terminal.
162     *
163     * @param string
164     * @return string
165     */
166    private function formatNewlines($string)
167    {
168        $string = str_replace($this->eol, "\0PHP_EOL\0", $string);
169        $string = preg_replace("/(\r\n|\n|\r)/", $this->eol, $string);
170        return str_replace("\0PHP_EOL\0", $this->eol, $string);
171    }
172}
173