Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
54 / 54
89.47% covered (warning)
89.47%
51 / 57
8.49% covered (danger)
8.49%
9 / 106
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Markup
100.00% covered (success)
100.00%
54 / 54
89.47% covered (warning)
89.47%
51 / 57
8.49% covered (danger)
8.49%
9 / 106
100.00% covered (success)
100.00%
5 / 5
358.94
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
75.00% covered (warning)
75.00%
3 / 4
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
1.12
 render
100.00% covered (success)
100.00%
35 / 35
93.55% covered (success)
93.55%
29 / 31
2.44% covered (danger)
2.44%
2 / 82
100.00% covered (success)
100.00%
1 / 1
145.72
 sgr
100.00% covered (success)
100.00%
7 / 7
86.67% covered (warning)
86.67%
13 / 15
17.65% covered (danger)
17.65%
3 / 17
100.00% covered (success)
100.00%
1 / 1
12.94
 escape
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 width
100.00% covered (success)
100.00%
7 / 7
83.33% covered (warning)
83.33%
5 / 6
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Console;
6
7use ValueError;
8
9/**
10 * Renders the inline console markup to ANSI escape codes.
11 *
12 * Style tags are `<strong>`, `<em>`, `<dim>`, and `<u>`; color tags are
13 * the ANSI names — `<red>`, `<bright-red>`, `<gray>` — and the same set
14 * with a `bg-` prefix for backgrounds. Truecolor hex tags take a
15 * lowercase six-digit code: `<#ff7313>`, `<bg-#ff7313>`. Tags compose
16 * by nesting, the innermost tag wins on conflict.
17 *
18 * Only exact known tags are parsed; everything else — `<info@example.com>`,
19 * generics, unknown names — passes through untouched. A backslash renders
20 * a known tag literally: `\<green>`. Structural mistakes (a mismatched,
21 * dangling, or unclosed tag) throw a ValueError, also when colors are
22 * disabled, so they surface in tests.
23 *
24 * @internal
25 */
26final class Markup
27{
28    private const string RESET = "\033[0m";
29
30    /** SGR codes by tag name. */
31    private const array TAGS = [
32        'strong' => '1',
33        'em' => '3',
34        'dim' => '2',
35        'u' => '4',
36        'black' => '30',
37        'red' => '31',
38        'green' => '32',
39        'yellow' => '33',
40        'blue' => '34',
41        'magenta' => '35',
42        'cyan' => '36',
43        'white' => '37',
44        'gray' => '90',
45        'bright-black' => '90',
46        'bright-red' => '91',
47        'bright-green' => '92',
48        'bright-yellow' => '93',
49        'bright-blue' => '94',
50        'bright-magenta' => '95',
51        'bright-cyan' => '96',
52        'bright-white' => '97',
53        'bg-black' => '40',
54        'bg-red' => '41',
55        'bg-green' => '42',
56        'bg-yellow' => '43',
57        'bg-blue' => '44',
58        'bg-magenta' => '45',
59        'bg-cyan' => '46',
60        'bg-white' => '47',
61        'bg-gray' => '100',
62        'bg-bright-black' => '100',
63        'bg-bright-red' => '101',
64        'bg-bright-green' => '102',
65        'bg-bright-yellow' => '103',
66        'bg-bright-blue' => '104',
67        'bg-bright-magenta' => '105',
68        'bg-bright-cyan' => '106',
69        'bg-bright-white' => '107',
70    ];
71
72    /** @var non-empty-string */
73    private readonly string $split;
74
75    /** @var non-empty-string */
76    private readonly string $tag;
77
78    public function __construct()
79    {
80        $names = implode('|', array_keys(self::TAGS)) . '|(?:bg-)?\#[0-9a-f]{6}';
81        $this->split = "#(\\\\?</?(?:{$names})>)#";
82        $this->tag = "#^\\\\?</?(?:{$names})>$#";
83    }
84
85    /**
86     * Renders the markup as escape codes, or strips it without `$colors`.
87     */
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
91            return $text;
92        }
93
94        /** @var list<string> $parts */
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
150        }
151
152        return $out;
153    }
154
155    /** The SGR code for a tag name: a named lookup or a truecolor hex tag. */
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
159            return self::TAGS[$name];
160        }
161
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
169
170    /**
171     * Escapes known tags and strips control characters so the text
172     * prints literally.
173     *
174     * Everything C0 except newline and tab is removed, DEL included, so
175     * arbitrary text cannot inject terminal escape sequences (ESC, BEL,
176     * carriage returns, ...).
177     */
178    public function escape(string $text): string
179    {
180        $text = (string) preg_replace('/[\x00-\x08\x0B-\x1F\x7F]/', replacement: '', subject: $text);
181
182        return (string) preg_replace($this->split, replacement: '\\\\$0', subject: $text);
183    }
184
185    /**
186     * The visible width of the text: tags collapse to nothing, an
187     * escaped tag to the tag without its backslash.
188     */
189    public function width(string $text): int
190    {
191        if (str_contains($text, '<')) {
192            $text = (string) preg_replace_callback(
193                $this->split,
194                static fn(array $match): string => $match[0][0] === '\\' ? substr($match[0], offset: 1) : '',
195                $text,
196            );
197        }
198
199        return mb_strwidth($text);
200    }
201}

Paths

Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once. Please also be aware that some paths may include implicit rather than explicit branches, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

Markup->__construct
80        $names = implode('|', array_keys(self::TAGS)) . '|(?:bg-)?\#[0-9a-f]{6}';
 
80        $names = implode('|', array_keys(self::TAGS)) . '|(?:bg-)?\#[0-9a-f]{6}';
 
80        $names = implode('|', array_keys(self::TAGS)) . '|(?:bg-)?\#[0-9a-f]{6}';
81        $this->split = "#(\\\\?</?(?:{$names})>)#";
82        $this->tag = "#^\\\\?</?(?:{$names})>$#";
83    }
80        $names = implode('|', array_keys(self::TAGS)) . '|(?:bg-)?\#[0-9a-f]{6}';
 
80        $names = implode('|', array_keys(self::TAGS)) . '|(?:bg-)?\#[0-9a-f]{6}';
 
80        $names = implode('|', array_keys(self::TAGS)) . '|(?:bg-)?\#[0-9a-f]{6}';
81        $this->split = "#(\\\\?</?(?:{$names})>)#";
82        $this->tag = "#^\\\\?</?(?:{$names})>$#";
83    }
Markup->escape
178    public function escape(string $text): string
179    {
180        $text = (string) preg_replace('/[\x00-\x08\x0B-\x1F\x7F]/', replacement: '', subject: $text);
181
182        return (string) preg_replace($this->split, replacement: '\\\\$0', subject: $text);
183    }
Markup->render
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
91            return $text;
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
105                $out .= $part;
106
107                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
105                $out .= $part;
106
107                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
111                $out .= substr($part, offset: 1);
112
113                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
111                $out .= substr($part, offset: 1);
112
113                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
105                $out .= $part;
106
107                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
105                $out .= $part;
106
107                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
111                $out .= substr($part, offset: 1);
112
113                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
111                $out .= substr($part, offset: 1);
112
113                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
91            return $text;
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
105                $out .= $part;
106
107                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
105                $out .= $part;
106
107                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
111                $out .= substr($part, offset: 1);
112
113                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
111                $out .= substr($part, offset: 1);
112
113                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
105                $out .= $part;
106
107                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
105                $out .= $part;
106
107                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
111                $out .= substr($part, offset: 1);
112
113                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
111                $out .= substr($part, offset: 1);
112
113                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
 
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
 
124                if ($last !== $name) {
 
128                if ($colors) {
 
137                continue;
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
104            if (preg_match($this->tag, $part) !== 1) {
 
110            if ($part[0] === '\\') {
 
116            if ($part[1] === '/') {
 
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
149            throw new ValueError("Unclosed markup tag '<{$stack[array_key_last($stack)]}>'");
88    public function render(string $text, bool $colors): string
89    {
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
90        if (!str_contains($text, '<')) {
 
95        $parts = (array) preg_split(
96            $this->split,
97            $text,
98            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
99        );
100        $out = '';
101        $stack = [];
102
103        foreach ($parts as $part) {
 
103        foreach ($parts as $part) {
104            if (preg_match($this->tag, $part) !== 1) {
105                $out .= $part;
106
107                continue;
108            }
109
110            if ($part[0] === '\\') {
111                $out .= substr($part, offset: 1);
112
113                continue;
114            }
115
116            if ($part[1] === '/') {
117                $name = substr($part, offset: 2, length: -1);
118                $last = array_pop($stack);
119
120                if ($last === null) {
121                    throw new ValueError("Markup tag '</{$name}>' has no opening tag");
122                }
123
124                if ($last !== $name) {
125                    throw new ValueError("Markup tag '<{$last}>' closed by '</{$name}>'");
126                }
127
128                if ($colors) {
129                    // A blanket reset, then re-apply the enclosing tags.
130                    $out .= self::RESET;
131
132                    foreach ($stack as $open) {
133                        $out .= "\033[" . $this->sgr($open) . 'm';
134                    }
135                }
136
137                continue;
138            }
139
140            $name = substr($part, offset: 1, length: -1);
141            $stack[] = $name;
142
143            if ($colors) {
144                $out .= "\033[" . $this->sgr($name) . 'm';
145            }
146        }
147
148        if ($stack !== []) {
 
152        return $out;
153    }
Markup->sgr
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
159            return self::TAGS[$name];
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
156    private function sgr(string $name): string
157    {
158        if (array_key_exists($name, self::TAGS)) {
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
 
162        $bg = str_starts_with($name, 'bg-');
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
 
163        $hex = substr($name, offset: $bg ? 4 : 1);
164
165        return (
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
 
166            ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)))
167        );
168    }
Markup->width
189    public function width(string $text): int
190    {
191        if (str_contains($text, '<')) {
 
191        if (str_contains($text, '<')) {
 
191        if (str_contains($text, '<')) {
 
192            $text = (string) preg_replace_callback(
193                $this->split,
194                static fn(array $match): string => $match[0][0] === '\\' ? substr($match[0], offset: 1) : '',
195                $text,
196            );
197        }
198
199        return mb_strwidth($text);
 
199        return mb_strwidth($text);
200    }
189    public function width(string $text): int
190    {
191        if (str_contains($text, '<')) {
 
191        if (str_contains($text, '<')) {
 
191        if (str_contains($text, '<')) {
 
199        return mb_strwidth($text);
200    }
189    public function width(string $text): int
190    {
191        if (str_contains($text, '<')) {
 
191        if (str_contains($text, '<')) {
 
191        if (str_contains($text, '<')) {
 
192            $text = (string) preg_replace_callback(
193                $this->split,
194                static fn(array $match): string => $match[0][0] === '\\' ? substr($match[0], offset: 1) : '',
195                $text,
196            );
197        }
198
199        return mb_strwidth($text);
 
199        return mb_strwidth($text);
200    }
189    public function width(string $text): int
190    {
191        if (str_contains($text, '<')) {
 
191        if (str_contains($text, '<')) {
 
191        if (str_contains($text, '<')) {
 
199        return mb_strwidth($text);
200    }
{closure:/workspace/celema/console/src/Markup.php:194-194}
194                static fn(array $match): string => $match[0][0] === '\\' ? substr($match[0], offset: 1) : '',
 
194                static fn(array $match): string => $match[0][0] === '\\' ? substr($match[0], offset: 1) : '',
 
194                static fn(array $match): string => $match[0][0] === '\\' ? substr($match[0], offset: 1) : '',
194                static fn(array $match): string => $match[0][0] === '\\' ? substr($match[0], offset: 1) : '',
 
194                static fn(array $match): string => $match[0][0] === '\\' ? substr($match[0], offset: 1) : '',
 
194                static fn(array $match): string => $match[0][0] === '\\' ? substr($match[0], offset: 1) : '',