• 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

96.91
/src/Texy/Modules/ListModule.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\Modules;
11

12
use Texy;
13
use Texy\BlockParser;
14
use Texy\HtmlElement;
15
use Texy\Modifier;
16
use Texy\Patterns;
17
use function implode, ord, preg_match, strlen;
18

19

20
/**
21
 * Processes ordered, unordered, and definition lists with nesting.
22
 */
23
final class ListModule extends Texy\Module
24
{
25
        /** @var array<string, array{string, int, string, 3?: string}> [regex, ordered?, list-style-type, next-regex?] */
26
        public array $bullets = [
27
                // first-rexexp ordered? list-style-type next-regexp
28
                '*' => ['\*[\ \t]', 0, ''],
29
                '-' => ['[\x{2013}-](?![>-])', 0, ''],
30
                '+' => ['\+[\ \t]', 0, ''],
31
                '1.' => ['1\.[\ \t]', /* not \d !*/ 1, '', '\d{1,3}\.[\ \t]'],
32
                '1)' => ['\d{1,3}\)[\ \t]', 1, ''],
33
                'I.' => ['I\.[\ \t]', 1, 'upper-roman', '[IVX]{1,4}\.[\ \t]'],
34
                'I)' => ['[IVX]+\)[\ \t]', 1, 'upper-roman'], // before A) !
35
                'a)' => ['[a-z]\)[\ \t]', 1, 'lower-alpha'],
36
                'A)' => ['[A-Z]\)[\ \t]', 1, 'upper-alpha'],
37
        ];
38

39

40
        public function __construct(Texy\Texy $texy)
1✔
41
        {
42
                $this->texy = $texy;
1✔
43

44
                $texy->addHandler('beforeParse', $this->beforeParse(...));
1✔
45
                $texy->allowed['list'] = true;
1✔
46
                $texy->allowed['list/definition'] = true;
1✔
47
        }
1✔
48

49

50
        private function beforeParse(): void
51
        {
52
                $RE = $REul = [];
1✔
53
                foreach ($this->bullets as $desc) {
1✔
54
                        $RE[] = $desc[0];
1✔
55
                        if (!$desc[1]) {
1✔
56
                                $REul[] = $desc[0];
1✔
57
                        }
58
                }
59

60
                $this->texy->registerBlockPattern(
1✔
61
                        $this->patternList(...),
1✔
62
                        '#^(?:' . Patterns::MODIFIER_H . '\n)?' // .{color: red}
63
                        . '(' . implode('|', $RE) . ')[\ \t]*+\S.*$#mUu', // item (unmatched)
1✔
64
                        'list',
1✔
65
                );
66

67
                $this->texy->registerBlockPattern(
1✔
68
                        $this->patternDefList(...),
1✔
69
                        '#^(?:' . Patterns::MODIFIER_H . '\n)?' // .{color:red}
70
                        . '(\S.{0,2000})\:[\ \t]*' . Patterns::MODIFIER_H . '?\n' // Term:
71
                        . '([\ \t]++)(' . implode('|', $REul) . ')[\ \t]*+\S.*$#mUu', // - description
1✔
72
                        'list/definition',
1✔
73
                );
74
        }
1✔
75

76

77
        /**
78
         * Callback for:.
79
         *
80
         * 1) .... .(title)[class]{style}>
81
         * 2) ....
82
         *   + ...
83
         *   + ...
84
         * 3) ....
85
         * @param  string[]  $matches
86
         */
87
        public function patternList(BlockParser $parser, array $matches): ?HtmlElement
1✔
88
        {
89
                [, $mMod, $mBullet] = $matches;
1✔
90
                // [1] => .(title)[class]{style}<>
91
                // [2] => bullet * + - 1) a) A) IV)
92

93
                $el = new HtmlElement;
1✔
94

95
                $bullet = $min = null;
1✔
96
                foreach ($this->bullets as $type => $desc) {
1✔
97
                        if (preg_match('#' . $desc[0] . '#Au', $mBullet)) {
1✔
98
                                $bullet = $desc[3] ?? $desc[0];
1✔
99
                                $min = isset($desc[3]) ? 2 : 1;
1✔
100
                                $el->setName($desc[1] ? 'ol' : 'ul');
1✔
101
                                $el->attrs['style'] = (array) ($el->attrs['style'] ?? []);
1✔
102
                                $el->attrs['style']['list-style-type'] = $desc[2];
1✔
103
                                if ($desc[1]) { // ol
1✔
104
                                        if ($type[0] === '1' && (int) $mBullet > 1) {
1✔
105
                                                $el->attrs['start'] = (int) $mBullet;
×
106
                                        } elseif ($type[0] === 'a' && $mBullet[0] > 'a') {
1✔
107
                                                $el->attrs['start'] = ord($mBullet[0]) - 96;
×
108
                                        } elseif ($type[0] === 'A' && $mBullet[0] > 'A') {
1✔
109
                                                $el->attrs['start'] = ord($mBullet[0]) - 64;
×
110
                                        }
111
                                }
112

113
                                break;
1✔
114
                        }
115
                }
116
                assert($bullet !== null);
117

118
                $mod = new Modifier($mMod);
1✔
119
                $mod->decorate($this->texy, $el);
1✔
120

121
                $parser->moveBackward(1);
1✔
122

123
                while ($elItem = $this->patternItem($parser, $bullet, false, 'li')) {
1✔
124
                        $el->add($elItem);
1✔
125
                }
126

127
                if ($el->count() < $min) {
1✔
128
                        return null;
1✔
129
                }
130

131
                // event listener
132
                $this->texy->invokeHandlers('afterList', [$parser, $el, $mod]);
1✔
133

134
                return $el;
1✔
135
        }
136

137

138
        /**
139
         * Callback for:.
140
         *
141
         * Term: .(title)[class]{style}>
142
         * - description 1
143
         * - description 2
144
         * - description 3
145
         * @param  string[]  $matches
146
         */
147
        public function patternDefList(BlockParser $parser, array $matches): HtmlElement
1✔
148
        {
149
                [, $mMod, , , , $mBullet] = $matches;
1✔
150
                // [1] => .(title)[class]{style}<>
151
                // [2] => ...
152
                // [3] => .(title)[class]{style}<>
153
                // [4] => space
154
                // [5] => - * +
155

156
                $texy = $this->texy;
1✔
157

158
                $bullet = null;
1✔
159
                foreach ($this->bullets as $desc) {
1✔
160
                        if (preg_match('#' . $desc[0] . '#Au', $mBullet)) {
1✔
161
                                $bullet = $desc[3] ?? $desc[0];
1✔
162
                                break;
1✔
163
                        }
164
                }
165
                assert($bullet !== null);
166

167
                $el = new HtmlElement('dl');
1✔
168
                $mod = new Modifier($mMod);
1✔
169
                $mod->decorate($texy, $el);
1✔
170
                $parser->moveBackward(2);
1✔
171

172
                $patternTerm = '#^\n?(\S.*)\:[\ \t]*' . Patterns::MODIFIER_H . '?()$#mUA';
1✔
173

174
                while (true) {
1✔
175
                        if ($elItem = $this->patternItem($parser, $bullet, true, 'dd')) {
1✔
176
                                $el->add($elItem);
1✔
177
                                continue;
1✔
178
                        }
179

180
                        if ($parser->next($patternTerm, $matches)) {
1✔
181
                                [, $mContent, $mMod] = $matches;
1✔
182
                                // [1] => ...
183
                                // [2] => .(title)[class]{style}<>
184

185
                                $elItem = new HtmlElement('dt');
1✔
186
                                $modItem = new Modifier($mMod);
1✔
187
                                $modItem->decorate($texy, $elItem);
1✔
188

189
                                $elItem->parseLine($texy, $mContent);
1✔
190
                                $el->add($elItem);
1✔
191
                                continue;
1✔
192
                        }
193

194
                        break;
1✔
195
                }
196

197
                // event listener
198
                $texy->invokeHandlers('afterDefinitionList', [$parser, $el, $mod]);
1✔
199

200
                return $el;
1✔
201
        }
202

203

204
        /**
205
         * Callback for single list item.
206
         */
207
        private function patternItem(BlockParser $parser, string $bullet, bool $indented, string $tag): ?HtmlElement
1✔
208
        {
209
                $spacesBase = $indented ? ('[\ \t]{1,}') : '';
1✔
210
                $patternItem = "#^\n?($spacesBase){$bullet}[ \\t]*(\\S.*)?" . Patterns::MODIFIER_H . '?()$#mAUu';
1✔
211

212
                // first line with bullet
213
                $matches = null;
1✔
214
                if (!$parser->next($patternItem, $matches)) {
1✔
215
                        return null;
1✔
216
                }
217

218
                [, $mIndent, $mContent, $mMod] = $matches;
1✔
219
                // [1] => indent
220
                // [2] => ...
221
                // [3] => .(title)[class]{style}<>
222

223
                $elItem = new HtmlElement($tag);
1✔
224
                $mod = new Modifier($mMod);
1✔
225
                $mod->decorate($this->texy, $elItem);
1✔
226

227
                // next lines
228
                $spaces = '';
1✔
229
                $content = ' ' . $mContent; // trick
1✔
230
                while ($parser->next('#^(\n*)' . $mIndent . '([\ \t]{1,' . $spaces . '})(.*)()$#Am', $matches)) {
1✔
231
                        [, $mBlank, $mSpaces, $mContent] = $matches;
1✔
232
                        // [1] => blank line?
233
                        // [2] => spaces
234
                        // [3] => ...
235

236
                        if ($spaces === '') {
1✔
237
                                $spaces = strlen($mSpaces);
1✔
238
                        }
239

240
                        $content .= "\n" . $mBlank . $mContent;
1✔
241
                }
242

243
                // parse content
244
                $elItem->parseBlock($this->texy, $content, true);
1✔
245

246
                if (isset($elItem[0]) && $elItem[0] instanceof HtmlElement) {
1✔
247
                        $elItem[0]->setName(null);
1✔
248
                }
249

250
                return $elItem;
1✔
251
        }
252
}
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