• 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

91.54
/src/Texy/Modules/TableModule.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\HtmlElement;
14
use Texy\Modifier;
15
use Texy\Patterns;
16
use Texy\Regexp;
17

18

19
/**
20
 * Table module.
21
 */
22
final class TableModule extends Texy\Module
23
{
24
        /** @deprecated */
25
        public ?string $oddClass = null;
26

27
        /** @deprecated */
28
        public ?string $evenClass = null;
29
        private ?bool $disableTables = null;
30

31

32
        public function __construct(Texy\Texy $texy)
1✔
33
        {
34
                $this->texy = $texy;
1✔
35

36
                $texy->registerBlockPattern(
1✔
37
                        $this->patternTable(...),
1✔
38
                        '~^
39
                                (?:' . Patterns::MODIFIER_HV . '\n)? # modifier (1)
1✔
40
                                \|                                   # table start
41
                                .*                                   # content
42
                        $~mU',
43
                        'table',
1✔
44
                );
45
        }
1✔
46

47

48
        /**
49
         * Callback for:.
50
         *
51
         * .(title)[class]{style}>
52
         * |------------------
53
         * | xxx | xxx | xxx | .(..){..}[..]
54
         * |------------------
55
         * | aa | bb | cc |
56
         */
57
        public function patternTable(Texy\BlockParser $parser, array $matches): HtmlElement|string|null
1✔
58
        {
59
                if ($this->disableTables) {
1✔
60
                        return null;
1✔
61
                }
62

63
                [, $mMod] = $matches;
1✔
64
                // [1] => .(title)[class]{style}<>_
65

66
                $texy = $this->texy;
1✔
67

68
                $el = new HtmlElement('table');
1✔
69
                $mod = new Modifier($mMod);
1✔
70
                $mod->decorate($texy, $el);
1✔
71

72
                $parser->moveBackward();
1✔
73

74
                if ($parser->next(
1✔
75
                        '~^
1✔
76
                                \|                               # opening pipe
77
                                ( \# | \= ){2,}  (?! [|#=+] )    # opening chars (1)
78
                                (.+)                             # caption (2)
79
                                \1*                              # matching closing characters
80
                                \|?                              # optional closing pipe
81
                                \ *                              # optional spaces
82
                                ' . Patterns::MODIFIER_H . '?    # modifier (3)
1✔
83
                        $~Um',
84
                        $matches,
85
                )) {
UNCOV
86
                        [, , $mContent, $mMod] = $matches;
×
87
                        // [1] => # / =
88
                        // [2] => ....
89
                        // [3] => .(title)[class]{style}<>
90

UNCOV
91
                        $caption = $el->create('caption');
×
UNCOV
92
                        $mod = new Modifier($mMod);
×
UNCOV
93
                        $mod->decorate($texy, $caption);
×
UNCOV
94
                        $caption->parseLine($texy, $mContent);
×
95
                }
96

97
                $isHead = false;
1✔
98
                $colModifier = [];
1✔
99
                $prevRow = []; // rowSpan building helper
1✔
100
                $rowCounter = 0;
1✔
101
                $colCounter = 0;
1✔
102
                $elPart = null;
1✔
103

104
                while (true) {
1✔
105
                        if ($parser->next('~^\|([=-])[+|=-]{2,}$~Um', $matches)) { // line
1✔
106
                                $isHead = !$isHead;
1✔
107
                                $prevRow = [];
1✔
108
                                continue;
1✔
109
                        }
110

111
                        if ($parser->next('~^ \| (.*) (?: | \| [\ \t]* ' . Patterns::MODIFIER_HV . '?)$~U', $matches)) {
1✔
112
                                // smarter head detection
113
                                if ($rowCounter === 0 && !$isHead && $parser->next('~^\|[=-][+|=-]{2,}$~Um', $foo)) {
1✔
114
                                        $isHead = true;
1✔
115
                                        $parser->moveBackward();
1✔
116
                                }
117

118
                                if ($elPart === null) {
1✔
119
                                        $elPart = $el->create($isHead ? 'thead' : 'tbody');
1✔
120

121
                                } elseif (!$isHead && $elPart->getName() === 'thead') {
1✔
122
                                        $this->finishPart($elPart);
1✔
123
                                        $elPart = $el->create('tbody');
1✔
124
                                }
125

126
                                [, $mContent, $mMod] = $matches;
1✔
127
                                // [1] => ....
128
                                // [2] => .(title)[class]{style}<>_
129

130
                                $elRow = $this->processRow($mContent, $mMod, $isHead, $texy, $prevRow, $colModifier, $colCounter, $rowCounter);
1✔
131

132
                                if ($elRow->count()) {
1✔
133
                                        $elPart->add($elRow);
1✔
134
                                        $rowCounter++;
1✔
135
                                } else { // redundant row
136
                                        foreach ($prevRow as $elCell) {
1✔
137
                                                $elCell->rowSpan--;
1✔
138
                                        }
139
                                }
140

141
                                continue;
1✔
142
                        }
143

144
                        break;
1✔
145
                }
146

147
                if ($elPart === null) { // invalid table
1✔
UNCOV
148
                        return null;
×
149
                }
150

151
                if ($elPart->getName() === 'thead') {
1✔
152
                        // thead is optional, tbody is required
UNCOV
153
                        $elPart->setName('tbody');
×
154
                }
155

156
                $this->finishPart($elPart);
1✔
157

158
                // event listener
159
                $texy->invokeHandlers('afterTable', [$parser, $el, $mod]);
1✔
160

161
                return $el;
1✔
162
        }
163

164

165
        private function processRow(
1✔
166
                string $content,
167
                ?string $mMod,
168
                bool $isHead,
169
                Texy\Texy $texy,
170
                array &$prevRow,
171
                array &$colModifier,
172
                int &$colCounter,
173
                int $rowCounter,
174
        ): HtmlElement
175
        {
176
                $elRow = new HtmlElement('tr');
1✔
177
                $mod = new Modifier($mMod);
1✔
178
                $mod->decorate($texy, $elRow);
1✔
179

180
                $rowClass = $rowCounter % 2 === 0 ? $this->oddClass : $this->evenClass;
1✔
181
                if ($rowClass && !isset($mod->classes[$this->oddClass]) && !isset($mod->classes[$this->evenClass])) {
1✔
UNCOV
182
                        $elRow->attrs['class'][] = $rowClass;
×
183
                }
184

185
                $col = 0;
1✔
186
                $elCell = null;
1✔
187

188
                // special escape sequence \|
189
                $content = str_replace('\|', "\x13", $content);
1✔
190
                $content = Regexp::replace($content, '~(\[[^\]]*)\|~', "$1\x13"); // HACK: support for [..|..]
1✔
191

192
                foreach (explode('|', $content) as $cell) {
1✔
193
                        $cell = strtr($cell, "\x13", '|');
1✔
194
                        // rowSpan
195
                        if (isset($prevRow[$col]) && ($matches = Regexp::match($cell, '~\^[\ \t]*$|\*??(.*)[\ \t]+\^$~AU'))) {
1✔
196
                                $prevRow[$col]->rowSpan++;
1✔
197
                                $cell = $matches[1] ?? '';
1✔
198
                                $prevRow[$col]->text .= "\n" . $cell;
1✔
199
                                $col += $prevRow[$col]->colSpan;
1✔
200
                                $elCell = null;
1✔
201
                                continue;
1✔
202
                        }
203

204
                        // colSpan
205
                        if ($cell === '' && $elCell) {
1✔
206
                                $elCell->colSpan++;
1✔
207
                                unset($prevRow[$col]);
1✔
208
                                $col++;
1✔
209
                                continue;
1✔
210
                        }
211

212
                        // common cell
213
                        if ($elCell = $this->processCell($cell, $colModifier[$col], $isHead, $texy)) {
1✔
214
                                $elRow->add($elCell);
1✔
215
                                $prevRow[$col] = $elCell;
1✔
216
                                $col++;
1✔
217
                        }
218
                }
219

220
                // even up with empty cells
221
                while ($col < $colCounter) {
1✔
222
                        $elCell = new TableCellElement;
1✔
223
                        $elCell->setName($isHead ? 'th' : 'td');
1✔
224
                        if (isset($colModifier[$col])) {
1✔
UNCOV
225
                                $colModifier[$col]->decorate($texy, $elCell);
×
226
                        }
227

228
                        $elRow->add($elCell);
1✔
229
                        $prevRow[$col] = $elCell;
1✔
230
                        $col++;
1✔
231
                }
232

233
                $colCounter = $col;
1✔
234
                return $elRow;
1✔
235
        }
236

237

238
        private function processCell(
1✔
239
                string $cell,
240
                ?Modifier &$cellModifier,
241
                bool $isHead,
242
                Texy\Texy $texy,
243
        ): ?TableCellElement
244
        {
245
                $matches = Regexp::match($cell, '~
1✔
246
                        ( \*?? )                          # head mark (1)
247
                        [\ \t]*
248
                        ' . Patterns::MODIFIER_HV . '??   # modifier (2)
249
                        (.*)                              # content (3)
250
                        ' . Patterns::MODIFIER_HV . '?    # modifier (4)
1✔
251
                        [\ \t]*
252
                $~AU');
253
                if (!$matches) {
1✔
UNCOV
254
                        return null;
×
255
                }
256

257
                [, $mHead, $mModCol, $mContent, $mMod] = $matches;
1✔
258
                // [1] => * ^
259
                // [2] => .(title)[class]{style}<>_
260
                // [3] => ....
261
                // [4] => .(title)[class]{style}<>_
262

263
                if ($mModCol) {
1✔
UNCOV
264
                        $cellModifier = new Modifier($mModCol);
×
265
                }
266

267
                $mod = $cellModifier ? clone $cellModifier : new Modifier;
1✔
268
                $mod->setProperties($mMod);
1✔
269

270
                $elCell = new TableCellElement;
1✔
271
                $elCell->setName($isHead || ($mHead === '*') ? 'th' : 'td');
1✔
272
                $mod->decorate($texy, $elCell);
1✔
273
                $elCell->text = $mContent;
1✔
274
                return $elCell;
1✔
275
        }
276

277

278
        /**
279
         * Parse text in all cells.
280
         */
281
        private function finishPart(HtmlElement $elPart): void
1✔
282
        {
283
                foreach ($elPart->getChildren() as $elRow) {
1✔
284
                        foreach ($elRow->getChildren() as $elCell) {
1✔
285
                                if ($elCell->colSpan > 1) {
1✔
286
                                        $elCell->attrs['colspan'] = $elCell->colSpan;
1✔
287
                                }
288

289
                                if ($elCell->rowSpan > 1) {
1✔
290
                                        $elCell->attrs['rowspan'] = $elCell->rowSpan;
1✔
291
                                }
292

293
                                $text = rtrim((string) $elCell->text);
1✔
294
                                if (str_contains($text, "\n")) {
1✔
295
                                        // multiline parse as block
296
                                        // HACK: disable tables
297
                                        $this->disableTables = true;
1✔
298
                                        $elCell->parseBlock($this->texy, Texy\Helpers::outdent($text));
1✔
299
                                        $this->disableTables = false;
1✔
300
                                } else {
301
                                        $elCell->parseLine($this->texy, ltrim($text));
1✔
302
                                }
303

304
                                if ($elCell->getText() === '') {
1✔
305
                                        $elCell->setText("\u{A0}"); // &nbsp;
1✔
306
                                }
307
                        }
308
                }
309
        }
1✔
310
}
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