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

dg / texy / 29746802114

20 Jul 2026 01:19PM UTC coverage: 94.677% (+0.04%) from 94.641%
29746802114

push

github

dg
Dead code removed

- Engine::getPatternNames() and Texy::getEngine(): no callers in src or tests.
- LinkDefinitionModule::resolveUrl(): unreachable. Both bracket forms it
  handles ([*img*] and [ref]) are already consumed by earlier returns in
  resolveLinkNodeUrl(), so it could only ever return its argument unchanged.
- Text\Renderer: CodeBlockNode type 'comment' never exists; comments are a
  separate CommentNode. Leftover from before the node split.

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

74 existing lines in 15 files now uncovered.

3344 of 3532 relevant lines covered (94.68%)

0.95 hits per line

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

95.8
/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\Modifier;
12
use Texy\Nodes\DefinitionListNode;
13
use Texy\Nodes\ListItemNode;
14
use Texy\Nodes\ListNode;
15
use Texy\Nodes\ListType;
16
use Texy\ParseContext;
17
use Texy\Patterns;
18
use Texy\Range;
19
use Texy\Regexp;
20
use Texy\Syntax;
21
use function count, implode, ord, strlen;
22

23

24
/**
25
 * Processes ordered, unordered, and definition lists with nesting.
26
 */
27
final class ListModule extends Texy\Module
28
{
29
        /** @var array<string, array{string, ListType, 2?: string}> [regex, type, next-regex?] */
30
        public array $bullets = [
31
                '*' => ['\* [ \t]', ListType::Unordered],
32
                '-' => ['[\x{2013}-] (?! [>-] )', ListType::Unordered],
33
                '+' => ['\+ [ \t]', ListType::Unordered],
34
                '1.' => ['1 \. [ \t]' /* not \d! */, ListType::Decimal, '\d{1,3} \. [ \t]'],
35
                '1)' => ['\d{1,3} \) [ \t]', ListType::Decimal],
36
                'I.' => ['I \. [ \t]', ListType::UpperRoman, '[IVX]{1,4} \. [ \t]'],
37
                'I)' => ['[IVX]+ \) [ \t]', ListType::UpperRoman], // before A) !
38
                'a)' => ['[a-z] \) [ \t]', ListType::LowerAlpha],
39
                'A)' => ['[A-Z] \) [ \t]', ListType::UpperAlpha],
40
        ];
41

42

43
        public function __construct(
1✔
44
                private Texy\Texy $texy,
45
        ) {
46
                $texy->allowed[Syntax::List] = true;
1✔
47
                $texy->allowed[Syntax::DefinitionList] = true;
1✔
48
        }
1✔
49

50

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

61
                $this->texy->registerBlockPattern(
1✔
62
                        $this->parseList(...),
1✔
63
                        '~^
64
                                (?:' . Patterns::ModifierHAlign . '\n)? # modifier (1)
65
                                (' . implode('|', $RE) . ')         # list marker (2)
1✔
66
                                [ \t]*+
67
                                \S .*                               # content
68
                        $~mUx',
69
                        Syntax::List,
1✔
70
                );
71

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

89

90
        /**
91
         * Parses list.
92
         * @param  array{string, ?string, string}  $matches
93
         * @param  array{int, ?int, int}  $offsets
94
         */
95
        public function parseList(ParseContext $context, array $matches, array $offsets): ?ListNode
1✔
96
        {
97
                [, $mMod, $mBullet] = $matches;
1✔
98

99
                $bullet = null;
1✔
100
                $min = 1;
1✔
101
                $start = null;
1✔
102
                $listType = ListType::Unordered;
1✔
103

104
                foreach ($this->bullets as $key => $desc) {
1✔
105
                        if (Regexp::match($mBullet, '~' . $desc[0] . '~Ax')) {
1✔
106
                                $bullet = $desc[2] ?? $desc[0];
1✔
107
                                $min = isset($desc[2]) ? 2 : 1;
1✔
108
                                $listType = $desc[1];
1✔
109
                                if ($listType->isOrdered()) {
1✔
110
                                        if ($key[0] === '1' && (int) $mBullet > 1) {
1✔
UNCOV
111
                                                $start = (int) $mBullet;
×
112
                                        } elseif ($key[0] === 'a' && $mBullet[0] > 'a') {
1✔
UNCOV
113
                                                $start = ord($mBullet[0]) - 96;
×
114
                                        } elseif ($key[0] === 'A' && $mBullet[0] > 'A') {
1✔
UNCOV
115
                                                $start = ord($mBullet[0]) - 64;
×
116
                                        }
117
                                }
118
                                break;
1✔
119
                        }
120
                }
121

122
                if ($bullet === null) {
1✔
UNCOV
123
                        return null;
×
124
                }
125

126
                $context->getBlockParser()->moveBackward();
1✔
127

128
                $items = [];
1✔
129
                while ($item = $this->parseItem($context, $bullet, false)) {
1✔
130
                        $items[] = $item;
1✔
131
                }
132

133
                if (count($items) < $min) {
1✔
134
                        return null;
1✔
135
                }
136

137
                return new ListNode(
1✔
138
                        $items,
1✔
139
                        $listType,
140
                        $start,
141
                        Modifier::parse($mMod, $offsets[1]),
1✔
142
                        new Range($offsets[0], strlen($matches[0])),
1✔
143
                );
144
        }
145

146

147
        /**
148
         * Parses definition list.
149
         * @param  array{string, ?string, string, ?string, string, string}  $matches
150
         * @param  array{int, ?int, int, ?int, int, int}  $offsets
151
         */
152
        public function parseDefList(ParseContext $context, array $matches, array $offsets): ?DefinitionListNode
1✔
153
        {
154
                [, $mMod, , , , $mBullet] = $matches;
1✔
155

156
                $bullet = null;
1✔
157

158
                foreach ($this->bullets as $desc) {
1✔
159
                        if (Regexp::match($mBullet, '~' . $desc[0] . '~Ax')) {
1✔
160
                                $bullet = $desc[2] ?? $desc[0];
1✔
161
                                break;
1✔
162
                        }
163
                }
164

165
                if ($bullet === null) {
1✔
UNCOV
166
                        return null;
×
167
                }
168

169
                $context->getBlockParser()->moveBackward(2);
1✔
170

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

179
                while (true) {
1✔
180
                        if ($item = $this->parseItem($context, $bullet, true)) {
1✔
181
                                $items[] = $item;
1✔
182
                                continue;
1✔
183
                        }
184

185
                        $termMatches = null;
1✔
186
                        $termOffsets = null;
1✔
187
                        if ($context->getBlockParser()->next($patternTerm, $termMatches, $termOffsets)) {
1✔
188
                                /** @var array{string, string, ?string} $termMatches */
189
                                [, $mContent, $mTermMod] = $termMatches;
1✔
190
                                $termMod = Modifier::parse($mTermMod, $termOffsets[2] ?? null);
1✔
191
                                // groups 0 and 1 always participate in a successful match, but next() cannot type that
192
                                $termOffset = $termOffsets[0] ?? throw new \LogicException('Match without group 0.');
1✔
193
                                $contentOffset = $termOffsets[1] ?? throw new \LogicException('Match without group 1.');
1✔
194
                                $termContent = $context->parseInline($mContent, $contentOffset);
1✔
195
                                $items[] = new ListItemNode($termContent, true, $termMod, new Range($termOffset, strlen($termMatches[0])));
1✔
196
                                continue;
1✔
197
                        }
198

199
                        break;
1✔
200
                }
201

202
                return new DefinitionListNode(
1✔
203
                        $items,
1✔
204
                        Modifier::parse($mMod, $offsets[1]),
1✔
205
                        new Range($offsets[0], strlen($matches[0])),
1✔
206
                );
207
        }
208

209

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

225
                $matches = null;
1✔
226
                $offsets = null;
1✔
227
                if (!$context->getBlockParser()->next($patternItem, $matches, $offsets)) {
1✔
228
                        return null;
1✔
229
                }
230

231
                /** @var array{string, string, ?string, ?string} $matches */
232
                [, $mIndent, $mContent, $mMod] = $matches;
1✔
233
                // group 0 always participates in a successful match, but next() cannot type that
234
                $itemOffset = $offsets[0] ?? throw new \LogicException('Match without group 0.');
1✔
235
                $modOffset = $offsets[3] ?? null;
1✔
236
                $itemEnd = $itemOffset + strlen($matches[0]);
1✔
237

238
                // Assemble content and map local line starts to absolute source offsets
239
                $map = [];
1✔
240
                $content = ' ' . ($mContent ?? '');
1✔
241
                if ($mContent !== null) {
1✔
242
                        // content matched, so group 2 participated and carries an offset
243
                        $map[1] = $offsets[2] ?? throw new \LogicException('Content without group 2.'); // 1 = the leading space
1✔
244
                }
245

246
                // next lines
247
                $spaces = '';
1✔
248
                while ($context->getBlockParser()->next('~^
1✔
249
                        (\n*)
250
                        ' . Regexp::quote($mIndent) . '
1✔
251
                        ([ \t]{1,' . $spaces . '})
1✔
252
                        (.*)                                     # content (3)
253
                $~Amx', $matches, $offsets)) {
254
                        /** @var array{string, string, string, string} $matches */
255
                        [, $mBlank, $mSpaces, $mContent] = $matches;
1✔
256

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

261
                        if ($mContent !== '' && $offsets[3] !== null) {
1✔
262
                                $map[strlen($content) + 1 + strlen($mBlank)] = $offsets[3]; // 1 = "\n"
1✔
263
                        }
264

265
                        $content .= "\n" . $mBlank . $mContent;
1✔
266
                        $itemEnd = ($offsets[0] ?? throw new \LogicException('Match without group 0.')) + strlen($matches[0]);
1✔
267
                }
268

269
                // Parse content as blocks
270
                $parsed = $context->parseBlock(trim($content));
1✔
271

272
                // Fix positions in parsed content using offset mapping
273
                if ($map) {
1✔
274
                        $skipped = strlen($content) - strlen(ltrim($content));
1✔
275
                        (new Texy\OffsetMap($map, $skipped))->applyTo($parsed);
1✔
276
                }
277

278
                return new ListItemNode(
1✔
279
                        $parsed,
1✔
280
                        false,
1✔
281
                        Modifier::parse($mMod, $modOffset),
1✔
282
                        new Range($itemOffset, $itemEnd - $itemOffset),
1✔
283
                );
284
        }
285
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc