• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dg / texy / 22262497275

21 Feb 2026 07:01PM UTC coverage: 93.057% (+0.7%) from 92.367%
22262497275

push

github

dg
added CLAUDE.md

2426 of 2607 relevant lines covered (93.06%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.67
/src/Texy/Modifier.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the Texy! (https://texy.nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Texy;
9

10
use function array_flip, explode, is_array, preg_match, settype, str_replace, str_starts_with, strlen, strpos, strtolower, substr, trim;
11

12

13
/**
14
 * Modifier processor.
15
 *
16
 * Modifiers are texts like .(title)[class1 class2 #id]{color: red}>^
17
 *   .         starts with dot
18
 *   (...)     title or alt modifier
19
 *   [...]     classes or ID modifier
20
 *   {...}     inner style modifier
21
 *   < > <> =  horizontal align modifier
22
 *   ^ - _     vertical align modifier
23
 */
24
final class Modifier
25
{
26
        public ?string $id = null;
27

28
        /** @var array<string, true> of classes (as keys) */
29
        public array $classes = [];
30

31
        /** @var array<string, string> of CSS styles */
32
        public array $styles = [];
33

34
        /** @var array<string, string|string[]> of HTML element attributes */
35
        public array $attrs = [];
36
        public ?string $hAlign = null;
37
        public ?string $vAlign = null;
38
        public ?string $title = null;
39

40
        /** @var array<string, 1>  list of properties which are regarded as HTML element attributes */
41
        public static array $elAttrs = [
42
                'abbr' => 1, 'accesskey' => 1, 'alt' => 1, 'cite' => 1, 'colspan' => 1, 'contenteditable' => 1, 'crossorigin' => 1,
43
                'datetime' => 1, 'decoding' => 1, 'download' => 1, 'draggable' => 1, 'for' => 1, 'headers' => 1, 'hidden' => 1,
44
                'href' => 1, 'hreflang' => 1, 'id' => 1, 'itemid' => 1, 'itemprop' => 1, 'itemref' => 1, 'itemscope' => 1, 'itemtype' => 1,
45
                'lang' => 1, 'name' => 1, 'ping' => 1, 'referrerpolicy' => 1, 'rel' => 1, 'reversed' => 1, 'rowspan' => 1, 'scope' => 1,
46
                'slot' => 1, 'src' => 1, 'srcset' => 1, 'start' => 1, 'target' => 1, 'title' => 1, 'translate' => 1, 'type' => 1, 'value' => 1,
47
        ];
48

49

50
        public function __construct(?string $s = null)
1✔
51
        {
52
                $this->setProperties($s);
1✔
53
        }
1✔
54

55

56
        public function setProperties(?string $s): void
1✔
57
        {
58
                $p = 0;
1✔
59
                $len = $s ? strlen($s) : 0;
1✔
60

61
                while ($p < $len) {
1✔
62
                        $ch = $s[$p];
1✔
63

64
                        if ($ch === '(') { // title
1✔
65
                                preg_match('#(?:\\\\\)|[^)\n])++\)#', $s, $m, 0, $p);
1✔
66
                                if (isset($m[0])) {
1✔
67
                                        $this->title = Helpers::unescapeHtml(str_replace('\)', ')', trim(substr($m[0], 1, -1))));
1✔
68
                                        $p += strlen($m[0]);
1✔
69
                                }
70

71
                        } elseif ($ch === '{') { // style & attributes
1✔
72
                                $a = strpos($s, '}', $p) + 1;
1✔
73
                                $this->parseStyle(substr($s, $p + 1, $a - $p - 2));
1✔
74
                                $p = $a;
1✔
75

76
                        } elseif ($ch === '[') { // classes & ID
1✔
77
                                $a = strpos($s, ']', $p) + 1;
1✔
78
                                $this->parseClasses(str_replace('#', ' #', substr($s, $p + 1, $a - $p - 2)));
1✔
79
                                $p = $a;
1✔
80

81
                        } elseif ($val = ['^' => 'top', '-' => 'middle', '_' => 'bottom'][$ch] ?? null) { // alignment
1✔
82
                                $this->vAlign = $val;
×
83
                                $p++;
×
84

85
                        } elseif (substr($s, $p, 2) === '<>') {
1✔
86
                                $this->hAlign = 'center';
1✔
87
                                $p += 2;
1✔
88

89
                        } elseif ($val = ['=' => 'justify', '>' => 'right', '<' => 'left'][$ch] ?? null) {
1✔
90
                                $this->hAlign = $val;
1✔
91
                                $p++;
1✔
92
                        } else {
93
                                break;
1✔
94
                        }
95
                }
96
        }
1✔
97

98

99
        /**
100
         * Decorates HtmlElement element.
101
         */
102
        public function decorate(Texy $texy, HtmlElement $el): HtmlElement
1✔
103
        {
104
                $this->decorateAttrs($texy, $el->attrs, $el->getName() ?? '');
1✔
105
                $el->validateAttrs($texy->getDTD());
1✔
106
                $this->decorateClasses($texy, $el->attrs);
1✔
107
                $this->decorateStyles($texy, $el->attrs);
1✔
108
                $this->decorateAligns($texy, $el->attrs);
1✔
109
                return $el;
1✔
110
        }
111

112

113
        /** @param  array<string, mixed>  $attrs */
114
        private function decorateAttrs(Texy $texy, array &$attrs, string $name): void
1✔
115
        {
116
                if (!$this->attrs) {
1✔
117
                } elseif ($texy->allowedTags === $texy::ALL) {
1✔
118
                        $attrs = $this->attrs;
×
119

120
                } elseif (is_array($texy->allowedTags)) {
1✔
121
                        $tmp = $texy->allowedTags[$name] ?? [];
1✔
122

123
                        if ($tmp === $texy::ALL) {
1✔
124
                                $attrs = $this->attrs;
1✔
125

126
                        } elseif (is_array($tmp)) {
×
127
                                $attrs = array_flip($tmp);
×
128
                                foreach ($this->attrs as $key => $value) {
×
129
                                        if (isset($attrs[$key])) {
×
130
                                                $attrs[$key] = $value;
×
131
                                        }
132
                                }
133
                        }
134
                }
135

136
                if ($this->title !== null) {
1✔
137
                        $attrs['title'] = $texy->typographyModule->postLine($this->title);
1✔
138
                }
139
        }
1✔
140

141

142
        /** @param  array<string, mixed>  $attrs */
143
        private function decorateClasses(Texy $texy, array &$attrs): void
1✔
144
        {
145
                if ($this->classes || $this->id !== null) {
1✔
146
                        [$allowedClasses] = $texy->getAllowedProps();
1✔
147
                        settype($attrs['class'], 'array');
1✔
148
                        if ($allowedClasses === $texy::ALL) {
1✔
149
                                foreach ($this->classes as $value => $foo) {
1✔
150
                                        $attrs['class'][] = $value;
1✔
151
                                }
152

153
                                $attrs['id'] = $this->id;
1✔
154
                        } elseif (is_array($allowedClasses)) {
1✔
155
                                foreach ($this->classes as $value => $foo) {
1✔
156
                                        if (isset($allowedClasses[$value])) {
1✔
157
                                                $attrs['class'][] = $value;
1✔
158
                                        }
159
                                }
160

161
                                if (isset($allowedClasses['#' . $this->id])) {
1✔
162
                                        $attrs['id'] = $this->id;
1✔
163
                                }
164
                        }
165
                }
166
        }
1✔
167

168

169
        /** @param  array<string, mixed>  $attrs */
170
        private function decorateStyles(Texy $texy, array &$attrs): void
1✔
171
        {
172
                if ($this->styles) {
1✔
173
                        [, $allowedStyles] = $texy->getAllowedProps();
1✔
174
                        settype($attrs['style'], 'array');
1✔
175
                        if ($allowedStyles === $texy::ALL) {
1✔
176
                                foreach ($this->styles as $prop => $value) {
1✔
177
                                        $attrs['style'][$prop] = $value;
1✔
178
                                }
179
                        } elseif (is_array($allowedStyles)) {
1✔
180
                                foreach ($this->styles as $prop => $value) {
1✔
181
                                        if (isset($allowedStyles[$prop])) {
1✔
182
                                                $attrs['style'][$prop] = $value;
1✔
183
                                        }
184
                                }
185
                        }
186
                }
187
        }
1✔
188

189

190
        /** @param  array<string, mixed>  $attrs */
191
        private function decorateAligns(Texy $texy, array &$attrs): void
1✔
192
        {
193
                if ($this->hAlign) {
1✔
194
                        $class = $texy->alignClasses[$this->hAlign] ?? null;
1✔
195
                        if ($class) {
1✔
196
                                settype($attrs['class'], 'array');
×
197
                                $attrs['class'][] = $class;
×
198
                        } else {
199
                                settype($attrs['style'], 'array');
1✔
200
                                $attrs['style']['text-align'] = $this->hAlign;
1✔
201
                        }
202
                }
203

204
                if ($this->vAlign) {
1✔
205
                        $class = $texy->alignClasses[$this->vAlign] ?? null;
×
206
                        if ($class) {
×
207
                                settype($attrs['class'], 'array');
×
208
                                $attrs['class'][] = $class;
×
209
                        } else {
210
                                settype($attrs['style'], 'array');
×
211
                                $attrs['style']['vertical-align'] = $this->vAlign;
×
212
                        }
213
                }
214
        }
1✔
215

216

217
        private function parseStyle(string $s): void
1✔
218
        {
219
                foreach (explode(';', $s) as $value) {
1✔
220
                        $pair = explode(':', $value, 2);
1✔
221
                        $prop = strtolower(trim($pair[0]));
1✔
222
                        if ($prop === '' || !isset($pair[1])) {
1✔
223
                                continue;
1✔
224
                        }
225

226
                        $value = trim($pair[1]);
1✔
227

228
                        if (
229
                                isset(self::$elAttrs[$prop])
1✔
230
                                || str_starts_with($prop, 'data-')
1✔
231
                                || str_starts_with($prop, 'aria-')
1✔
232
                        ) { // attribute
233
                                $this->attrs[$prop] = $value;
1✔
234
                        } elseif ($value !== '') { // style
1✔
235
                                $this->styles[$prop] = $value;
1✔
236
                        }
237
                }
238
        }
1✔
239

240

241
        private function parseClasses(string $s): void
1✔
242
        {
243
                foreach (explode(' ', $s) as $value) {
1✔
244
                        if ($value === '') {
1✔
245
                                continue;
1✔
246
                        } elseif ($value[0] === '#') {
1✔
247
                                $this->id = substr($value, 1);
1✔
248
                        } else {
249
                                $this->classes[$value] = true;
1✔
250
                        }
251
                }
252
        }
1✔
253
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc