Completed
Push — master ( c607ee...214549 )
by Michał
02:06
created

Formatter::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 2
1
<?php namespace nyx\console\output\formatting;
2
3
/**
4
 * Output Formatter
5
 *
6
 * Responsible for processing strings, detecting formatting tags within them and applying the respective
7
 * Output Formatting Styles or removing any styling, while leaving any tags not matching the pattern unharmed.
8
 *
9
 * Styling can be applied either by:
10
 *  - defining Styles, whose name in turn becomes the name of the tag to use, ie. for a Style named 'error',
11
 *    in order to apply it to the string 'An error occurred!', you would write '<error>An error occurred!</error>';
12
 *  - defining inline styling, by using tags resembling inline styles in HTML tags, ie.:
13
 *    '<color: white; bg: red>An error occurred!</>'. The order is irrelevant (and whitespaces are optional),
14
 *    but it is necessary to specify 'color:' to define a foreground' color and 'bg:' for a background color.
15
 *    In order to apply additional options, you may use any other prefix. For instance '<weight: bold>Woohoo</>'
16
 *    will apply the 'bold' additional option (the word 'weight', however, has no meaning to the parser - it is
17
 *    nonetheless needed to match the pattern).
18
 *
19
 * Please {@see console\output\formatting\Style} to see which colors and additional options are supported.
20
 * ANSI support varies from system to system (and terminal to terminal) so use it with caution when portability
21
 * is one of your concerns.
22
 *
23
 * @version     0.1.0
24
 * @author      Michal Chojnacki <[email protected]>
25
 * @copyright   2012-2017 Nyx Dev Team
26
 * @link        https://github.com/unyx/nyx
27
 */
28
class Formatter implements interfaces\Formatter
29
{
30
    /**
31
     * The formatting tag matching pattern.
32
     */
33
    protected const PATTERN = '[a-z][a-z0-9,_:;\s-]*+';
34
35
    /**
36
     * @var styles\Map      The Styles available to this Formatter.
37
     */
38
    private $styles;
39
40
    /**
41
     * @var styles\Stack    The style processing Stack for this Formatter.
42
     */
43
    private $stack;
44
45
    /**
46
     * Constructs a new Output Formatter.
47
     *
48
     * @param   styles\Map          $styles     A Map of Styles to be used instead of the default.
49
     * @param   interfaces\Style    $default    The default text styling.
50
     */
51
    public function __construct(styles\Map $styles = null, interfaces\Style $default = null)
52
    {
53
        $this->styles = $styles ?? $this->createDefaultStyles();
54
        $this->stack  = new styles\Stack($default);
55
    }
56
57
    /**
58
     * {@inheritdoc}
59
     */
60
    public function format(string $text, bool $decorated = true) : string
61
    {
62
        $out    = '';
63
        $offset = 0;
64
65
        preg_match_all("#<((".static::PATTERN.") | /(".static::PATTERN.")?)>#ix", $text, $matches, PREG_OFFSET_CAPTURE);
66
67
        foreach ($matches[0] as $i => $match) {
68
69
            if (0 !== $match[1] && '\\' === $text[$match[1] - 1]) {
70
                continue;
71
            }
72
73
            $pos    = $match[1];
74
            $match  = $match[0];
75
            $out   .= $this->apply(substr($text, $offset, $pos - $offset), $decorated);
76
            $offset = $pos + strlen($match);
77
78
            // Check whether the second character is a slash, which would indicate that we are to close (pop)
79
            // the given tag.
80
            if ($opening = '/' !== $match[1]) {
81
                $tag = $matches[1][$i][0];
82
            } else {
83
                $tag = $matches[3][$i][0] ?? '';
84
            }
85
86
            // Inline styles will have empty closing tags, since they have no names (</>).
87
            // Empty closing tags will also work for non-inline styles however (closing the last stacked style
88
            // just the same).
89
            if (!$opening && !$tag) {
90
                $this->stack->pop();
91
                continue;
92
            }
93
94
            // See if the tag points to a Style name or contains inline formatting options and handle this
95
            // accordingly. If it does not refer to a Style, continue applying the topmost Style in the Stack.
96
            if (!$style = $this->handleFormattingTag($tag, $opening)) {
97
                $out .= $this->apply($text, $decorated);
98
            }
99
        }
100
101
        return $out . $this->apply(substr($text, $offset), $decorated);
102
    }
103
104
    /**
105
     * Returns the Styles in use by this Formatter.
106
     *
107
     * @return  styles\Map
108
     */
109
    public function getStyles() : styles\Map
110
    {
111
        return $this->styles;
112
    }
113
114
    /**
115
     * Returns the style processing Stack in use by this Formatter.
116
     *
117
     * @return  styles\Stack
118
     */
119
    public function getStack() : styles\Stack
120
    {
121
        return $this->stack;
122
    }
123
124
    /**
125
     * Creates a default Map of Styles to be used by this Formatter.
126
     *
127
     * @return  styles\Map
128
     */
129
    protected function createDefaultStyles() : styles\Map
130
    {
131
        return new styles\Map([
132
            'error'     => new Style('white', 'red'),
133
            'info'      => new Style('green'),
134
            'comment'   => new Style('cyan'),
135
            'important' => new Style('red'),
136
            'header'    => new Style('black', 'cyan')
137
        ]);
138
    }
139
140
    /**
141
     * Handles an encountered formatting tag by determining what Style it refers to (if applicable)
142
     * and pushing/popping the Style from the Stack, depending on whether it was an opening tag.
143
     *
144
     * @param   string              $tag        The encountered formatting tag.
145
     * @param   bool                $opening    Whether it was an opening tag.
146
     * @return  interfaces\Style                The Style matching the tag, if applicable.
147
     */
148
    protected function handleFormattingTag(string $tag, bool $opening) : ?interfaces\Style
149
    {
150
        // First attempt to grab a Style matching the tag from our Collection. If none is found,
151
        // see if it's an inline style.
152
        if ((!$style = $this->styles->get($tag)) && (!$style = Style::fromString($tag))) {
153
            return null;
154
        }
155
156
        if ($opening) {
157
            $this->stack->push($style);
0 ignored issues
show
Bug introduced by
It seems like $style defined by \nyx\console\output\form...Style::fromString($tag) on line 152 can be null; however, nyx\console\output\formatting\styles\Stack::push() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
158
        } else {
159
            $this->stack->pop($style);
160
        }
161
162
        return $style;
163
    }
164
165
    /**
166
     * Applies formatting to the given text.
167
     *
168
     * @param   string  $text       The text that should be formatted.
169
     * @param   bool    $decorated  Whether decorations, like colors, should be applied to the text.
170
     * @return  string              The resulting text.
171
     */
172
    protected function apply(string $text, bool $decorated) : string
173
    {
174
        return $decorated && !empty($text) ? $this->stack->current()->apply($text) : $text;
175
    }
176
}
177