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

dg / texy / 21344532034

26 Jan 2026 02:43AM UTC coverage: 91.98% (-0.4%) from 92.376%
21344532034

push

github

dg
added CLAUDE.md

2397 of 2606 relevant lines covered (91.98%)

0.92 hits per line

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

86.67
/src/Texy/Modifier.php
1
<?php
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
declare(strict_types=1);
9

10
namespace Texy;
11

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

14

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

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

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

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

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

51

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

57

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

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

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

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

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

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

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

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

100

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

114

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

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

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

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

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

143

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

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

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

170

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

191

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

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

218

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

228
                        $value = trim($pair[1]);
1✔
229

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

242

243
        private function parseClasses(string $s): void
1✔
244
        {
245
                foreach (explode(' ', $s) as $value) {
1✔
246
                        if ($value === '') {
1✔
247
                                continue;
1✔
248
                        } elseif ($value[0] === '#') {
1✔
249
                                $this->id = substr($value, 1);
1✔
250
                        } else {
251
                                $this->classes[$value] = true;
1✔
252
                        }
253
                }
254
        }
1✔
255
}
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