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

dg / texy / 22283286087

22 Feb 2026 06:58PM UTC coverage: 93.01% (+0.02%) from 92.991%
22283286087

push

github

dg
LinkModule: deprecated label and modifiers in link definitions

3 of 3 new or added lines in 1 file covered. (100.0%)

72 existing lines in 16 files now uncovered.

2089 of 2246 relevant lines covered (93.01%)

0.93 hits per line

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

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

10
use Texy;
11
use Texy\BlockParser;
12
use Texy\HtmlElement;
13
use Texy\Modifier;
14
use Texy\Patterns;
15
use Texy\Regexp;
16
use function implode, ord, strlen;
17

18

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

38

39
        public function __construct(
1✔
40
                private Texy\Texy $texy,
41
        ) {
42
                $texy->allowed['list'] = true;
1✔
43
                $texy->allowed['list/definition'] = true;
1✔
44
        }
1✔
45

46

47
        public function beforeParse(string &$text): void
1✔
48
        {
49
                $RE = $REul = [];
1✔
50
                foreach ($this->bullets as $desc) {
1✔
51
                        $RE[] = $desc[0];
1✔
52
                        if (!$desc[1]) {
1✔
53
                                $REul[] = $desc[0];
1✔
54
                        }
55
                }
56

57
                $this->texy->registerBlockPattern(
1✔
58
                        $this->parseList(...),
1✔
59
                        '~^
60
                                (?:' . Patterns::MODIFIER_H . '\n)? # modifier (1)
1✔
61
                                (' . implode('|', $RE) . ')         # list marker (2)
1✔
62
                                [ \t]*+
63
                                \S .*                               # content
64
                        $~mU',
65
                        'list',
1✔
66
                );
67

68
                $this->texy->registerBlockPattern(
1✔
69
                        $this->parseDefList(...),
1✔
70
                        '~^
71
                                (?:' . Patterns::MODIFIER_H . '\n)?   # modifier (1)
1✔
72
                                ( \S .{0,2000} )                      # definition term (2)
73
                                : [ \t]*                              # colon separator
74
                                ' . Patterns::MODIFIER_H . '?         # modifier (3)
1✔
75
                                \n
76
                                ([ \t]++)                             # indentation (4)
77
                                (' . implode('|', $REul) . ')         # description marker (5)
1✔
78
                                [ \t]*+
79
                                \S .*                                 # content
80
                        $~mU',
81
                        'list/definition',
1✔
82
                );
83
        }
1✔
84

85

86
        /**
87
         * Parses list.
88
         * @param  array<?string>  $matches
89
         */
90
        public function parseList(BlockParser $parser, array $matches): ?HtmlElement
1✔
91
        {
92
                /** @var array{string, ?string, string} $matches */
93
                [, $mMod, $mBullet] = $matches;
1✔
94
                // [1] => .(title)[class]{style}<>
95
                // [2] => bullet * + - 1) a) A) IV)
96

97
                $el = new HtmlElement;
1✔
98

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

117
                                break;
1✔
118
                        }
119
                }
120
                assert($bullet !== null);
121

122
                $mod = Modifier::parse($mMod);
1✔
123
                $mod->decorate($this->texy, $el);
1✔
124

125
                $parser->moveBackward(1);
1✔
126

127
                while ($elItem = $this->parseItem($parser, $bullet, false, 'li')) {
1✔
128
                        $el->add($elItem);
1✔
129
                }
130

131
                if ($el->count() < $min) {
1✔
132
                        return null;
1✔
133
                }
134

135
                // event listener
136
                $this->texy->invokeHandlers('afterList', [$parser, $el, $mod]);
1✔
137

138
                return $el;
1✔
139
        }
140

141

142
        /**
143
         * Parses definition list.
144
         * @param  array<?string>  $matches
145
         */
146
        public function parseDefList(BlockParser $parser, array $matches): HtmlElement
1✔
147
        {
148
                /** @var array{string, ?string, string, ?string, string, string} $matches */
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 (Regexp::match($mBullet, '~' . $desc[0] . '~A')) {
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 = Modifier::parse($mMod);
1✔
169
                $mod->decorate($texy, $el);
1✔
170
                $parser->moveBackward(2);
1✔
171

172
                $patternTerm = '~^
1✔
173
                        \n?
174
                        ( \S .* )                       # term content
175
                        : [ \t]*                        # colon separator
176
                        ' . Patterns::MODIFIER_H . '?
1✔
177
                $~mUA';
178

179
                while (true) {
1✔
180
                        if ($elItem = $this->parseItem($parser, $bullet, true, 'dd')) {
1✔
181
                                $el->add($elItem);
1✔
182
                                continue;
1✔
183
                        }
184

185
                        if ($parser->next($patternTerm, $matches)) {
1✔
186
                                /** @var array{string, string, ?string} $matches */
187
                                [, $mContent, $mMod] = $matches;
1✔
188
                                // [1] => ...
189
                                // [2] => .(title)[class]{style}<>
190

191
                                $elItem = new HtmlElement('dt');
1✔
192
                                $modItem = Modifier::parse($mMod);
1✔
193
                                $modItem->decorate($texy, $elItem);
1✔
194

195
                                $elItem->parseLine($texy, $mContent);
1✔
196
                                $el->add($elItem);
1✔
197
                                continue;
1✔
198
                        }
199

200
                        break;
1✔
201
                }
202

203
                // event listener
204
                $texy->invokeHandlers('afterDefinitionList', [$parser, $el, $mod]);
1✔
205

206
                return $el;
1✔
207
        }
208

209

210
        /**
211
         * Parses single list item.
212
         */
213
        private function parseItem(BlockParser $parser, string $bullet, bool $indented, string $tag): ?HtmlElement
1✔
214
        {
215
                $spacesBase = $indented ? ('[\ \t]{1,}') : '';
1✔
216
                $patternItem = "~^
1✔
217
                        \\n?
218
                        ($spacesBase)                            # base indentation
1✔
219
                        {$bullet}                                # bullet character
1✔
220
                        [ \\t]*
221
                        ( \\S .* )?                              # content
222
                        " . Patterns::MODIFIER_H . '?
1✔
223
                $~mAU';
224

225
                // first line with bullet
226
                $matches = null;
1✔
227
                if (!$parser->next($patternItem, $matches)) {
1✔
228
                        return null;
1✔
229
                }
230

231
                /** @var array{string, string, ?string, ?string} $matches */
232
                [, $mIndent, $mContent, $mMod] = $matches;
1✔
233
                // [1] => indent
234
                // [2] => ...
235
                // [3] => .(title)[class]{style}<>
236

237
                $elItem = new HtmlElement($tag);
1✔
238
                $mod = Modifier::parse($mMod);
1✔
239
                $mod->decorate($this->texy, $elItem);
1✔
240

241
                // next lines
242
                $spaces = '';
1✔
243
                $content = ' ' . $mContent; // trick
1✔
244
                while ($parser->next('~^
1✔
245
                        (\n*)
246
                        ' . Regexp::quote($mIndent) . '
1✔
247
                        ([ \t]{1,' . $spaces . '})
1✔
248
                        (.*)
249
                $~Am', $matches)) {
250
                        /** @var array{string, string, string, string} $matches */
251
                        [, $mBlank, $mSpaces, $mContent] = $matches;
1✔
252
                        // [1] => blank line?
253
                        // [2] => spaces
254
                        // [3] => ...
255

256
                        if ($spaces === '') {
1✔
257
                                $spaces = strlen($mSpaces);
1✔
258
                        }
259

260
                        $content .= "\n" . $mBlank . $mContent;
1✔
261
                }
262

263
                // parse content
264
                $elItem->parseBlock($this->texy, $content, true);
1✔
265

266
                if (isset($elItem[0]) && $elItem[0] instanceof HtmlElement) {
1✔
267
                        $elItem[0]->setName(null);
1✔
268
                }
269

270
                return $elItem;
1✔
271
        }
272
}
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