Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
92 / 92
90.38% covered (success)
90.38%
94 / 104
10.12% covered (danger)
10.12%
26 / 257
20.00% covered (danger)
20.00%
2 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Markup
100.00% covered (success)
100.00%
92 / 92
90.38% covered (success)
90.38%
94 / 104
10.12% covered (danger)
10.12%
26 / 257
100.00% covered (success)
100.00%
10 / 10
1261.69
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%
12 / 12
88.89% covered (warning)
88.89%
8 / 9
16.67% covered (danger)
16.67%
2 / 12
100.00% covered (success)
100.00%
1 / 1
8.21
 format
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
4
 markup
100.00% covered (success)
100.00%
23 / 23
97.14% covered (success)
97.14%
34 / 35
4.15% covered (danger)
4.15%
8 / 193
100.00% covered (success)
100.00%
1 / 1
161.84
 paired
100.00% covered (success)
100.00%
14 / 14
94.12% covered (success)
94.12%
16 / 17
15.00% covered (danger)
15.00%
3 / 20
100.00% covered (success)
100.00%
1 / 1
37.09
 codes
100.00% covered (success)
100.00%
1 / 1
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
 sgr
100.00% covered (success)
100.00%
5 / 5
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%
5 / 5
85.71% covered (warning)
85.71%
6 / 7
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
2.50
 pad
100.00% covered (success)
100.00%
8 / 8
81.82% covered (warning)
81.82%
9 / 11
80.00% covered (warning)
80.00%
4 / 5
100.00% covered (success)
100.00%
1 / 1
5.20
 width
100.00% covered (success)
100.00%
1 / 1
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
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>`. A tag without its partner — dangling,
21 * mismatched, or unclosed — prints literally too: rendering never fails.
22 *
23 * Arguments fill the template's sprintf() conversions and print as plain
24 * text, never as markup. Control characters other than newlines and tabs
25 * are dropped from both, so no text can inject terminal escape sequences;
26 * only templates keep carriage returns, which redraw a progress line.
27 *
28 * @internal
29 */
30final class Markup
31{
32    private const string RESET = "\033[0m";
33
34    /**
35     * Follows a trailing backslash of escaped text, so that a tag placed
36     * right after it stays a tag: without it, `C:\` + `</red>` would read
37     * as an escaped `</red>`. escape() strips control characters from its
38     * input, so untrusted text cannot forge it; render() drops it again.
39     */
40    private const string BOUNDARY = "\x1F";
41
42    /**
43     * Stands in for a `%` of escaped text, which a template with
44     * arguments would read as a conversion. Like BOUNDARY, it cannot be
45     * forged and render() turns it back.
46     */
47    private const string PERCENT = "\x1E";
48
49    /** Stands in for a formatted argument while the markup is parsed. */
50    private const string SLOT = "\x00";
51
52    private const string CONTROLS = '/[\x00-\x08\x0B-\x1F\x7F]/';
53
54    /** Like CONTROLS, but keeps carriage returns and the markers of escaped text. */
55    private const string TEMPLATE_CONTROLS = '/[\x00-\x08\x0B\x0C\x0E-\x1D\x7F]/';
56
57    /**
58     * The C1 controls U+0080 to U+009F as UTF-8: U+009B opens an escape
59     * sequence like `ESC [`. A whole run matches, so that no removal
60     * joins the bytes around it to another one; the lookbehind keeps a
61     * long run from being scanned at each of its bytes.
62     */
63    private const string C1 = '/(?<!\xC2)\xC2+[\x80-\x9F]+/';
64
65    /** A sprintf() conversion, `%1$s` and `%%` included. */
66    private const string CONVERSION = '/%(?:%|(?:(\d+)\$)?((?:[-+ 0]|\'.)*\d*(?:\.\d+)?[bcdeEfFgGhHosuxX]))/';
67
68    /** SGR codes by tag name. */
69    private const array TAGS = [
70        'strong' => '1',
71        'em' => '3',
72        'dim' => '2',
73        'u' => '4',
74        'black' => '30',
75        'red' => '31',
76        'green' => '32',
77        'yellow' => '33',
78        'blue' => '34',
79        'magenta' => '35',
80        'cyan' => '36',
81        'white' => '37',
82        'gray' => '90',
83        'bright-black' => '90',
84        'bright-red' => '91',
85        'bright-green' => '92',
86        'bright-yellow' => '93',
87        'bright-blue' => '94',
88        'bright-magenta' => '95',
89        'bright-cyan' => '96',
90        'bright-white' => '97',
91        'bg-black' => '40',
92        'bg-red' => '41',
93        'bg-green' => '42',
94        'bg-yellow' => '43',
95        'bg-blue' => '44',
96        'bg-magenta' => '45',
97        'bg-cyan' => '46',
98        'bg-white' => '47',
99        'bg-gray' => '100',
100        'bg-bright-black' => '100',
101        'bg-bright-red' => '101',
102        'bg-bright-green' => '102',
103        'bg-bright-yellow' => '103',
104        'bg-bright-blue' => '104',
105        'bg-bright-magenta' => '105',
106        'bg-bright-cyan' => '106',
107        'bg-bright-white' => '107',
108    ];
109
110    /** @var non-empty-string */
111    private readonly string $split;
112
113    /** @var non-empty-string */
114    private readonly string $tag;
115
116    public function __construct()
117    {
118        $names = implode('|', array_keys(self::TAGS)) . '|(?:bg-)?\#[0-9a-f]{6}';
119        $this->split = "#(\\\\?</?(?:{$names})>)#";
120        $this->tag = "#^\\\\?</?(?:{$names})>$#";
121    }
122
123    /**
124     * Renders the template as escape codes, or strips the markup without
125     * `$colors`. A `$style` tag encloses the whole text.
126     *
127     * @param list<float|int|string|\Stringable> $args
128     */
129    public function render(string $template, array $args = [], bool $colors = false, string $style = ''): string
130    {
131        $text = (string) preg_replace(self::TEMPLATE_CONTROLS, replacement: '', subject: $template);
132        $values = [];
133
134        if ($args !== []) {
135            [$text, $values] = $this->format($text, $args);
136        }
137
138        $parts = explode(
139            self::SLOT,
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
141        );
142        $out = array_shift($parts);
143
144        foreach ($parts as $i => $part) {
145            $out .= $values[$i] . $part;
146        }
147
148        // On the finished text: its parts could join to a C1 control.
149        return (string) preg_replace(self::C1, replacement: '', subject: $out);
150    }
151
152    /**
153     * Replaces the conversions with slots and formats their arguments.
154     *
155     * @param list<float|int|string|\Stringable> $args
156     *
157     * @return array{string, list<string>}
158     */
159    private function format(string $template, array $args): array
160    {
161        $values = [];
162        $next = 0;
163        $text = (string) preg_replace_callback(
164            self::CONVERSION,
165            static function (array $match) use ($args, &$values, &$next): string {
166                if ($match[0] === '%%') {
167                    return '%';
168                }
169
170                $index = $match[1] === '' ? $next++ : (int) $match[1] - 1;
171
172                if (!array_key_exists($index, $args)) {
173                    throw new ValueError('Missing argument ' . ($index + 1) . " for '{$match[0]}'");
174                }
175
176                $values[] = (string) preg_replace(
177                    self::CONTROLS,
178                    replacement: '',
179                    subject: sprintf('%' . $match[2], $args[$index]),
180                );
181
182                return self::SLOT;
183            },
184            $template,
185        );
186
187        return [$text, $values];
188    }
189
190    private function markup(string $text, bool $colors, string $style): string
191    {
192        $parts = preg_split(
193            $this->split,
194            $text,
195            flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY,
196        );
197
198        // Like a failed preg_replace() in render(), a failed split drops the text.
199        if ($parts === false) {
200            return '';
201        }
202
203        $paired = $this->paired($parts);
204        $stack = $style === '' ? [] : [$style];
205        $out = $colors ? $this->codes($stack) : '';
206
207        foreach ($parts as $i => $part) {
208            if (!in_array($i, $paired, strict: true)) {
209                $escaped = $part[0] === '\\' && preg_match($this->tag, $part) === 1;
210                $out .= $escaped ? substr($part, offset: 1) : $part;
211
212                continue;
213            }
214
215            if ($part[1] === '/') {
216                array_pop($stack);
217
218                // A blanket reset, then re-apply the enclosing tags.
219                $out .= $colors ? self::RESET . $this->codes($stack) : '';
220
221                continue;
222            }
223
224            $name = substr($part, offset: 1, length: -1);
225            $stack[] = $name;
226            $out .= $colors ? $this->codes([$name]) : '';
227        }
228
229        return $colors && $style !== '' ? $out . self::RESET : $out;
230    }
231
232    /**
233     * The indexes of the tags that have a partner: a closing tag pairs
234     * with the innermost open tag if that has its name, else with none.
235     *
236     * @param list<string> $parts
237     *
238     * @return list<int>
239     */
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
268
269    /** @param list<string> $names */
270    private function codes(array $names): string
271    {
272        return implode('', array_map(fn(string $name): string => "\033[" . $this->sgr($name) . 'm', $names));
273    }
274
275    /** The SGR code for a tag name: a named lookup or a truecolor hex tag. */
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
279            return self::TAGS[$name];
280        }
281
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
287
288    /**
289     * Escapes known tags and `%` and strips control characters so the
290     * text prints literally.
291     *
292     * Everything C0 except newline and tab is removed, DEL and the C1
293     * controls included, so arbitrary text cannot inject terminal escape
294     * sequences (ESC, BEL, carriage returns, ...).
295     */
296    public function escape(string $text): string
297    {
298        $text = (string) preg_replace(self::CONTROLS, replacement: '', subject: $text);
299        $text = (string) preg_replace(self::C1, replacement: '', subject: $text);
300        $text = (string) preg_replace($this->split, replacement: '\\\\$0', subject: $text);
301        $text = str_replace('%', self::PERCENT, $text);
302
303        return str_ends_with($text, '\\') ? $text . self::BOUNDARY : $text;
304    }
305
306    /**
307     * Pads the text with spaces to the visible width `$width`; wider
308     * text is returned unchanged. An uneven center split leans left.
309     */
310    public function pad(string $text, int $width, Align $align): string
311    {
312        $missing = $width - $this->width($text);
313
314        if ($missing <= 0) {
315            return $text;
316        }
317
318        return match ($align) {
319            Align::Left => $text . str_repeat(' ', $missing),
320            Align::Right => str_repeat(' ', $missing) . $text,
321            Align::Center => str_repeat(' ', intdiv($missing, 2))
322                . $text
323                . str_repeat(' ', $missing - intdiv($missing, 2)),
324        };
325    }
326
327    /**
328     * The visible width of the text as rendered: tags collapse to
329     * nothing, unpaired and escaped ones print.
330     */
331    public function width(string $text): int
332    {
333        return mb_strwidth($this->render($text));
334    }
335}

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
118        $names = implode('|', array_keys(self::TAGS)) . '|(?:bg-)?\#[0-9a-f]{6}';
 
118        $names = implode('|', array_keys(self::TAGS)) . '|(?:bg-)?\#[0-9a-f]{6}';
 
118        $names = implode('|', array_keys(self::TAGS)) . '|(?:bg-)?\#[0-9a-f]{6}';
119        $this->split = "#(\\\\?</?(?:{$names})>)#";
120        $this->tag = "#^\\\\?</?(?:{$names})>$#";
121    }
118        $names = implode('|', array_keys(self::TAGS)) . '|(?:bg-)?\#[0-9a-f]{6}';
 
118        $names = implode('|', array_keys(self::TAGS)) . '|(?:bg-)?\#[0-9a-f]{6}';
 
118        $names = implode('|', array_keys(self::TAGS)) . '|(?:bg-)?\#[0-9a-f]{6}';
119        $this->split = "#(\\\\?</?(?:{$names})>)#";
120        $this->tag = "#^\\\\?</?(?:{$names})>$#";
121    }
Markup->codes
270    private function codes(array $names): string
271    {
272        return implode('', array_map(fn(string $name): string => "\033[" . $this->sgr($name) . 'm', $names));
 
272        return implode('', array_map(fn(string $name): string => "\033[" . $this->sgr($name) . 'm', $names));
 
272        return implode('', array_map(fn(string $name): string => "\033[" . $this->sgr($name) . 'm', $names));
273    }
270    private function codes(array $names): string
271    {
272        return implode('', array_map(fn(string $name): string => "\033[" . $this->sgr($name) . 'm', $names));
 
272        return implode('', array_map(fn(string $name): string => "\033[" . $this->sgr($name) . 'm', $names));
 
272        return implode('', array_map(fn(string $name): string => "\033[" . $this->sgr($name) . 'm', $names));
273    }
Markup->escape
296    public function escape(string $text): string
297    {
298        $text = (string) preg_replace(self::CONTROLS, replacement: '', subject: $text);
299        $text = (string) preg_replace(self::C1, replacement: '', subject: $text);
300        $text = (string) preg_replace($this->split, replacement: '\\\\$0', subject: $text);
301        $text = str_replace('%', self::PERCENT, $text);
 
301        $text = str_replace('%', self::PERCENT, $text);
 
301        $text = str_replace('%', self::PERCENT, $text);
302
303        return str_ends_with($text, '\\') ? $text . self::BOUNDARY : $text;
 
303        return str_ends_with($text, '\\') ? $text . self::BOUNDARY : $text;
 
303        return str_ends_with($text, '\\') ? $text . self::BOUNDARY : $text;
304    }
296    public function escape(string $text): string
297    {
298        $text = (string) preg_replace(self::CONTROLS, replacement: '', subject: $text);
299        $text = (string) preg_replace(self::C1, replacement: '', subject: $text);
300        $text = (string) preg_replace($this->split, replacement: '\\\\$0', subject: $text);
301        $text = str_replace('%', self::PERCENT, $text);
 
301        $text = str_replace('%', self::PERCENT, $text);
 
301        $text = str_replace('%', self::PERCENT, $text);
302
303        return str_ends_with($text, '\\') ? $text . self::BOUNDARY : $text;
 
303        return str_ends_with($text, '\\') ? $text . self::BOUNDARY : $text;
 
303        return str_ends_with($text, '\\') ? $text . self::BOUNDARY : $text;
304    }
296    public function escape(string $text): string
297    {
298        $text = (string) preg_replace(self::CONTROLS, replacement: '', subject: $text);
299        $text = (string) preg_replace(self::C1, replacement: '', subject: $text);
300        $text = (string) preg_replace($this->split, replacement: '\\\\$0', subject: $text);
301        $text = str_replace('%', self::PERCENT, $text);
 
301        $text = str_replace('%', self::PERCENT, $text);
 
301        $text = str_replace('%', self::PERCENT, $text);
302
303        return str_ends_with($text, '\\') ? $text . self::BOUNDARY : $text;
 
303        return str_ends_with($text, '\\') ? $text . self::BOUNDARY : $text;
 
303        return str_ends_with($text, '\\') ? $text . self::BOUNDARY : $text;
304    }
296    public function escape(string $text): string
297    {
298        $text = (string) preg_replace(self::CONTROLS, replacement: '', subject: $text);
299        $text = (string) preg_replace(self::C1, replacement: '', subject: $text);
300        $text = (string) preg_replace($this->split, replacement: '\\\\$0', subject: $text);
301        $text = str_replace('%', self::PERCENT, $text);
 
301        $text = str_replace('%', self::PERCENT, $text);
 
301        $text = str_replace('%', self::PERCENT, $text);
302
303        return str_ends_with($text, '\\') ? $text . self::BOUNDARY : $text;
 
303        return str_ends_with($text, '\\') ? $text . self::BOUNDARY : $text;
 
303        return str_ends_with($text, '\\') ? $text . self::BOUNDARY : $text;
304    }
Markup->format
159    private function format(string $template, array $args): array
160    {
161        $values = [];
162        $next = 0;
163        $text = (string) preg_replace_callback(
164            self::CONVERSION,
165            static function (array $match) use ($args, &$values, &$next): string {
166                if ($match[0] === '%%') {
167                    return '%';
168                }
169
170                $index = $match[1] === '' ? $next++ : (int) $match[1] - 1;
171
172                if (!array_key_exists($index, $args)) {
173                    throw new ValueError('Missing argument ' . ($index + 1) . " for '{$match[0]}'");
174                }
175
176                $values[] = (string) preg_replace(
177                    self::CONTROLS,
178                    replacement: '',
179                    subject: sprintf('%' . $match[2], $args[$index]),
180                );
181
182                return self::SLOT;
183            },
184            $template,
185        );
186
187        return [$text, $values];
188    }
Markup->pad
310    public function pad(string $text, int $width, Align $align): string
311    {
312        $missing = $width - $this->width($text);
313
314        if ($missing <= 0) {
 
315            return $text;
310    public function pad(string $text, int $width, Align $align): string
311    {
312        $missing = $width - $this->width($text);
313
314        if ($missing <= 0) {
 
319            Align::Left => $text . str_repeat(' ', $missing),
 
320            Align::Right => str_repeat(' ', $missing) . $text,
 
321            Align::Center => str_repeat(' ', intdiv($missing, 2))
 
321            Align::Center => str_repeat(' ', intdiv($missing, 2))
 
321            Align::Center => str_repeat(' ', intdiv($missing, 2))
310    public function pad(string $text, int $width, Align $align): string
311    {
312        $missing = $width - $this->width($text);
313
314        if ($missing <= 0) {
 
319            Align::Left => $text . str_repeat(' ', $missing),
 
320            Align::Right => str_repeat(' ', $missing) . $text,
 
321            Align::Center => str_repeat(' ', intdiv($missing, 2))
 
321            Align::Center => str_repeat(' ', intdiv($missing, 2))
322                . $text
323                . str_repeat(' ', $missing - intdiv($missing, 2)),
 
323                . str_repeat(' ', $missing - intdiv($missing, 2)),
324        };
325    }
310    public function pad(string $text, int $width, Align $align): string
311    {
312        $missing = $width - $this->width($text);
313
314        if ($missing <= 0) {
 
319            Align::Left => $text . str_repeat(' ', $missing),
 
320            Align::Right => str_repeat(' ', $missing) . $text,
 
320            Align::Right => str_repeat(' ', $missing) . $text,
 
323                . str_repeat(' ', $missing - intdiv($missing, 2)),
324        };
325    }
310    public function pad(string $text, int $width, Align $align): string
311    {
312        $missing = $width - $this->width($text);
313
314        if ($missing <= 0) {
 
319            Align::Left => $text . str_repeat(' ', $missing),
 
319            Align::Left => $text . str_repeat(' ', $missing),
 
323                . str_repeat(' ', $missing - intdiv($missing, 2)),
324        };
325    }
Markup->paired
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
247                continue;
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
259                continue;
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
259                continue;
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
247                continue;
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
259                continue;
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
259                continue;
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
247                continue;
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
259                continue;
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
259                continue;
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
 
250            if ($part[1] !== '/') {
 
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
240    private function paired(array $parts): array
241    {
242        $paired = [];
243        $open = [];
244
245        foreach ($parts as $i => $part) {
 
245        foreach ($parts as $i => $part) {
246            if ($part[0] === '\\' || preg_match($this->tag, $part) !== 1) {
247                continue;
248            }
249
250            if ($part[1] !== '/') {
251                $open[] = [$i, substr($part, offset: 1, length: -1)];
252
253                continue;
254            }
255
256            $last = end($open);
257
258            if ($last === false || $last[1] !== substr($part, offset: 2, length: -1)) {
259                continue;
260            }
261
262            array_pop($open);
263            array_push($paired, $last[0], $i);
264        }
265
266        return $paired;
267    }
Markup->render
129    public function render(string $template, array $args = [], bool $colors = false, string $style = ''): string
130    {
131        $text = (string) preg_replace(self::TEMPLATE_CONTROLS, replacement: '', subject: $template);
132        $values = [];
133
134        if ($args !== []) {
 
135            [$text, $values] = $this->format($text, $args);
136        }
137
138        $parts = explode(
 
138        $parts = explode(
139            self::SLOT,
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
141        );
142        $out = array_shift($parts);
143
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
145            $out .= $values[$i] . $part;
146        }
147
148        // On the finished text: its parts could join to a C1 control.
149        return (string) preg_replace(self::C1, replacement: '', subject: $out);
150    }
129    public function render(string $template, array $args = [], bool $colors = false, string $style = ''): string
130    {
131        $text = (string) preg_replace(self::TEMPLATE_CONTROLS, replacement: '', subject: $template);
132        $values = [];
133
134        if ($args !== []) {
 
135            [$text, $values] = $this->format($text, $args);
136        }
137
138        $parts = explode(
 
138        $parts = explode(
139            self::SLOT,
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
141        );
142        $out = array_shift($parts);
143
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
145            $out .= $values[$i] . $part;
146        }
147
148        // On the finished text: its parts could join to a C1 control.
149        return (string) preg_replace(self::C1, replacement: '', subject: $out);
150    }
129    public function render(string $template, array $args = [], bool $colors = false, string $style = ''): string
130    {
131        $text = (string) preg_replace(self::TEMPLATE_CONTROLS, replacement: '', subject: $template);
132        $values = [];
133
134        if ($args !== []) {
 
135            [$text, $values] = $this->format($text, $args);
136        }
137
138        $parts = explode(
 
138        $parts = explode(
139            self::SLOT,
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
141        );
142        $out = array_shift($parts);
143
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
145            $out .= $values[$i] . $part;
146        }
147
148        // On the finished text: its parts could join to a C1 control.
149        return (string) preg_replace(self::C1, replacement: '', subject: $out);
150    }
129    public function render(string $template, array $args = [], bool $colors = false, string $style = ''): string
130    {
131        $text = (string) preg_replace(self::TEMPLATE_CONTROLS, replacement: '', subject: $template);
132        $values = [];
133
134        if ($args !== []) {
 
135            [$text, $values] = $this->format($text, $args);
136        }
137
138        $parts = explode(
 
138        $parts = explode(
139            self::SLOT,
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
141        );
142        $out = array_shift($parts);
143
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
145            $out .= $values[$i] . $part;
146        }
147
148        // On the finished text: its parts could join to a C1 control.
149        return (string) preg_replace(self::C1, replacement: '', subject: $out);
150    }
129    public function render(string $template, array $args = [], bool $colors = false, string $style = ''): string
130    {
131        $text = (string) preg_replace(self::TEMPLATE_CONTROLS, replacement: '', subject: $template);
132        $values = [];
133
134        if ($args !== []) {
 
135            [$text, $values] = $this->format($text, $args);
136        }
137
138        $parts = explode(
 
138        $parts = explode(
139            self::SLOT,
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
141        );
142        $out = array_shift($parts);
143
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
145            $out .= $values[$i] . $part;
146        }
147
148        // On the finished text: its parts could join to a C1 control.
149        return (string) preg_replace(self::C1, replacement: '', subject: $out);
150    }
129    public function render(string $template, array $args = [], bool $colors = false, string $style = ''): string
130    {
131        $text = (string) preg_replace(self::TEMPLATE_CONTROLS, replacement: '', subject: $template);
132        $values = [];
133
134        if ($args !== []) {
 
135            [$text, $values] = $this->format($text, $args);
136        }
137
138        $parts = explode(
 
138        $parts = explode(
139            self::SLOT,
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
141        );
142        $out = array_shift($parts);
143
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
145            $out .= $values[$i] . $part;
146        }
147
148        // On the finished text: its parts could join to a C1 control.
149        return (string) preg_replace(self::C1, replacement: '', subject: $out);
150    }
129    public function render(string $template, array $args = [], bool $colors = false, string $style = ''): string
130    {
131        $text = (string) preg_replace(self::TEMPLATE_CONTROLS, replacement: '', subject: $template);
132        $values = [];
133
134        if ($args !== []) {
 
138        $parts = explode(
139            self::SLOT,
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
141        );
142        $out = array_shift($parts);
143
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
145            $out .= $values[$i] . $part;
146        }
147
148        // On the finished text: its parts could join to a C1 control.
149        return (string) preg_replace(self::C1, replacement: '', subject: $out);
150    }
129    public function render(string $template, array $args = [], bool $colors = false, string $style = ''): string
130    {
131        $text = (string) preg_replace(self::TEMPLATE_CONTROLS, replacement: '', subject: $template);
132        $values = [];
133
134        if ($args !== []) {
 
138        $parts = explode(
139            self::SLOT,
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
141        );
142        $out = array_shift($parts);
143
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
145            $out .= $values[$i] . $part;
146        }
147
148        // On the finished text: its parts could join to a C1 control.
149        return (string) preg_replace(self::C1, replacement: '', subject: $out);
150    }
129    public function render(string $template, array $args = [], bool $colors = false, string $style = ''): string
130    {
131        $text = (string) preg_replace(self::TEMPLATE_CONTROLS, replacement: '', subject: $template);
132        $values = [];
133
134        if ($args !== []) {
 
138        $parts = explode(
139            self::SLOT,
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
141        );
142        $out = array_shift($parts);
143
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
145            $out .= $values[$i] . $part;
146        }
147
148        // On the finished text: its parts could join to a C1 control.
149        return (string) preg_replace(self::C1, replacement: '', subject: $out);
150    }
129    public function render(string $template, array $args = [], bool $colors = false, string $style = ''): string
130    {
131        $text = (string) preg_replace(self::TEMPLATE_CONTROLS, replacement: '', subject: $template);
132        $values = [];
133
134        if ($args !== []) {
 
138        $parts = explode(
139            self::SLOT,
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
141        );
142        $out = array_shift($parts);
143
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
145            $out .= $values[$i] . $part;
146        }
147
148        // On the finished text: its parts could join to a C1 control.
149        return (string) preg_replace(self::C1, replacement: '', subject: $out);
150    }
129    public function render(string $template, array $args = [], bool $colors = false, string $style = ''): string
130    {
131        $text = (string) preg_replace(self::TEMPLATE_CONTROLS, replacement: '', subject: $template);
132        $values = [];
133
134        if ($args !== []) {
 
138        $parts = explode(
139            self::SLOT,
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
141        );
142        $out = array_shift($parts);
143
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
145            $out .= $values[$i] . $part;
146        }
147
148        // On the finished text: its parts could join to a C1 control.
149        return (string) preg_replace(self::C1, replacement: '', subject: $out);
150    }
129    public function render(string $template, array $args = [], bool $colors = false, string $style = ''): string
130    {
131        $text = (string) preg_replace(self::TEMPLATE_CONTROLS, replacement: '', subject: $template);
132        $values = [];
133
134        if ($args !== []) {
 
138        $parts = explode(
139            self::SLOT,
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
 
140            str_replace([self::BOUNDARY, self::PERCENT], ['', '%'], $this->markup($text, $colors, $style)),
141        );
142        $out = array_shift($parts);
143
144        foreach ($parts as $i => $part) {
 
144        foreach ($parts as $i => $part) {
145            $out .= $values[$i] . $part;
146        }
147
148        // On the finished text: its parts could join to a C1 control.
149        return (string) preg_replace(self::C1, replacement: '', subject: $out);
150    }
Markup->sgr
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
279            return self::TAGS[$name];
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
276    private function sgr(string $name): string
277    {
278        if (array_key_exists($name, self::TAGS)) {
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
 
282        $bg = str_starts_with($name, 'bg-');
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
 
283        $hex = substr($name, offset: $bg ? 4 : 1);
284
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
 
285        return ($bg ? '48' : '38') . ';2;' . implode(';', array_map(hexdec(...), str_split($hex, length: 2)));
286    }
Markup->width
331    public function width(string $text): int
332    {
333        return mb_strwidth($this->render($text));
334    }
{closure:/workspace/celema/console/src/Markup.php:165-183}
165            static function (array $match) use ($args, &$values, &$next): string {
166                if ($match[0] === '%%') {
 
167                    return '%';
165            static function (array $match) use ($args, &$values, &$next): string {
166                if ($match[0] === '%%') {
 
170                $index = $match[1] === '' ? $next++ : (int) $match[1] - 1;
 
170                $index = $match[1] === '' ? $next++ : (int) $match[1] - 1;
 
170                $index = $match[1] === '' ? $next++ : (int) $match[1] - 1;
171
172                if (!array_key_exists($index, $args)) {
 
173                    throw new ValueError('Missing argument ' . ($index + 1) . " for '{$match[0]}'");
165            static function (array $match) use ($args, &$values, &$next): string {
166                if ($match[0] === '%%') {
 
170                $index = $match[1] === '' ? $next++ : (int) $match[1] - 1;
 
170                $index = $match[1] === '' ? $next++ : (int) $match[1] - 1;
 
170                $index = $match[1] === '' ? $next++ : (int) $match[1] - 1;
171
172                if (!array_key_exists($index, $args)) {
 
176                $values[] = (string) preg_replace(
177                    self::CONTROLS,
178                    replacement: '',
179                    subject: sprintf('%' . $match[2], $args[$index]),
180                );
181
182                return self::SLOT;
183            },
165            static function (array $match) use ($args, &$values, &$next): string {
166                if ($match[0] === '%%') {
 
170                $index = $match[1] === '' ? $next++ : (int) $match[1] - 1;
 
170                $index = $match[1] === '' ? $next++ : (int) $match[1] - 1;
 
170                $index = $match[1] === '' ? $next++ : (int) $match[1] - 1;
171
172                if (!array_key_exists($index, $args)) {
 
173                    throw new ValueError('Missing argument ' . ($index + 1) . " for '{$match[0]}'");
165            static function (array $match) use ($args, &$values, &$next): string {
166                if ($match[0] === '%%') {
 
170                $index = $match[1] === '' ? $next++ : (int) $match[1] - 1;
 
170                $index = $match[1] === '' ? $next++ : (int) $match[1] - 1;
 
170                $index = $match[1] === '' ? $next++ : (int) $match[1] - 1;
171
172                if (!array_key_exists($index, $args)) {
 
176                $values[] = (string) preg_replace(
177                    self::CONTROLS,
178                    replacement: '',
179                    subject: sprintf('%' . $match[2], $args[$index]),
180                );
181
182                return self::SLOT;
183            },
{closure:/workspace/celema/console/src/Markup.php:272-272}
272        return implode('', array_map(fn(string $name): string => "\033[" . $this->sgr($name) . 'm', $names));