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

dg / texy / 12879605443

21 Jan 2025 03:31AM UTC coverage: 92.224% (+0.03%) from 92.197%
12879605443

push

github

dg
regexp: uses unmatched as null (BC break)

14 of 14 new or added lines in 6 files covered. (100.0%)

101 existing lines in 14 files now uncovered.

2372 of 2572 relevant lines covered (92.22%)

0.92 hits per line

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

97.03
/src/Texy/Modules/ListModule.php
1
<?php
2

3
/**
4
 * This file is part of the Texy! (https://texy.info)
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 Texy\Regexp;
18

19

20
/**
21
 * Ordered / unordered nested list module.
22
 */
23
final class ListModule extends Texy\Module
24
{
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(Texy\Texy $texy)
1✔
40
        {
41
                $this->texy = $texy;
1✔
42

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

48

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

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

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

87

88
        /**
89
         * Callback for:.
90
         *
91
         * 1) .... .(title)[class]{style}>
92
         * 2) ....
93
         *   + ...
94
         *   + ...
95
         * 3) ....
96
         */
97
        public function patternList(BlockParser $parser, array $matches): ?HtmlElement
1✔
98
        {
99
                [, $mMod, $mBullet] = $matches;
1✔
100
                // [1] => .(title)[class]{style}<>
101
                // [2] => bullet * + - 1) a) A) IV)
102

103
                $el = new HtmlElement;
1✔
104

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

122
                                break;
1✔
123
                        }
124
                }
125

126
                $mod = new Modifier($mMod);
1✔
127
                $mod->decorate($this->texy, $el);
1✔
128

129
                $parser->moveBackward(1);
1✔
130

131
                while ($elItem = $this->patternItem($parser, $bullet, false, 'li')) {
1✔
132
                        $el->add($elItem);
1✔
133
                }
134

135
                if ($el->count() < $min) {
1✔
136
                        return null;
1✔
137
                }
138

139
                // event listener
140
                $this->texy->invokeHandlers('afterList', [$parser, $el, $mod]);
1✔
141

142
                return $el;
1✔
143
        }
144

145

146
        /**
147
         * Callback for:.
148
         *
149
         * Term: .(title)[class]{style}>
150
         * - description 1
151
         * - description 2
152
         * - description 3
153
         */
154
        public function patternDefList(BlockParser $parser, array $matches): HtmlElement
1✔
155
        {
156
                [, $mMod, , , , $mBullet] = $matches;
1✔
157
                // [1] => .(title)[class]{style}<>
158
                // [2] => ...
159
                // [3] => .(title)[class]{style}<>
160
                // [4] => space
161
                // [5] => - * +
162

163
                $texy = $this->texy;
1✔
164

165
                $bullet = null;
1✔
166
                foreach ($this->bullets as $desc) {
1✔
167
                        if (Regexp::match($mBullet, '~' . $desc[0] . '~A')) {
1✔
168
                                $bullet = $desc[3] ?? $desc[0];
1✔
169
                                break;
1✔
170
                        }
171
                }
172

173
                $el = new HtmlElement('dl');
1✔
174
                $mod = new Modifier($mMod);
1✔
175
                $mod->decorate($texy, $el);
1✔
176
                $parser->moveBackward(2);
1✔
177

178
                $patternTerm = '~^
1✔
179
                        \n?
180
                        ( \S .* )                         # term content
181
                        \: [\ \t]*                        # colon separator
182
                        ' . Patterns::MODIFIER_H . '?
183
                $~mUA';
184

185
                while (true) {
1✔
186
                        if ($elItem = $this->patternItem($parser, $bullet, true, 'dd')) {
1✔
187
                                $el->add($elItem);
1✔
188
                                continue;
1✔
189
                        }
190

191
                        if ($parser->next($patternTerm, $matches)) {
1✔
192
                                [, $mContent, $mMod] = $matches;
1✔
193
                                // [1] => ...
194
                                // [2] => .(title)[class]{style}<>
195

196
                                $elItem = new HtmlElement('dt');
1✔
197
                                $modItem = new Modifier($mMod);
1✔
198
                                $modItem->decorate($texy, $elItem);
1✔
199

200
                                $elItem->parseLine($texy, $mContent);
1✔
201
                                $el->add($elItem);
1✔
202
                                continue;
1✔
203
                        }
204

205
                        break;
1✔
206
                }
207

208
                // event listener
209
                $texy->invokeHandlers('afterDefinitionList', [$parser, $el, $mod]);
1✔
210

211
                return $el;
1✔
212
        }
213

214

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

230
                // first line with bullet
231
                $matches = null;
1✔
232
                if (!$parser->next($patternItem, $matches)) {
1✔
233
                        return null;
1✔
234
                }
235

236
                [, $mIndent, $mContent, $mMod] = $matches;
1✔
237
                // [1] => indent
238
                // [2] => ...
239
                // [3] => .(title)[class]{style}<>
240

241
                $elItem = new HtmlElement($tag);
1✔
242
                $mod = new Modifier($mMod);
1✔
243
                $mod->decorate($this->texy, $elItem);
1✔
244

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

259
                        if ($spaces === '') {
1✔
260
                                $spaces = strlen($mSpaces);
1✔
261
                        }
262

263
                        $content .= "\n" . $mBlank . $mContent;
1✔
264
                }
265

266
                // parse content
267
                $elItem->parseBlock($this->texy, $content, true);
1✔
268

269
                if (isset($elItem[0]) && $elItem[0] instanceof HtmlElement) {
1✔
270
                        $elItem[0]->setName(null);
1✔
271
                }
272

273
                return $elItem;
1✔
274
        }
275
}
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