• 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

92.25
/src/Texy/Modules/TableModule.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\HtmlElement;
12
use Texy\Modifier;
13
use Texy\Patterns;
14
use Texy\Regexp;
15
use function explode, ltrim, rtrim, str_contains, str_replace, strtr;
16

17

18
/**
19
 * Processes table syntax with headers, colspan, and rowspan support.
20
 */
21
final class TableModule extends Texy\Module
22
{
23
        private ?bool $disableTables = null;
24

25

26
        public function __construct(
1✔
27
                private Texy\Texy $texy,
28
        ) {
29
        }
1✔
30

31

32
        public function beforeParse(string &$text): void
1✔
33
        {
34
                $this->texy->registerBlockPattern(
1✔
35
                        $this->parseTable(...),
1✔
36
                        '~^
37
                                (?:' . Patterns::MODIFIER_HV . '\n)? # modifier (1)
1✔
38
                                \|                                   # table start
39
                                .*                                   # content
40
                        $~mU',
41
                        'table',
1✔
42
                );
43
        }
1✔
44

45

46
        /**
47
         * Parses tables.
48
         * @param  array<?string>  $matches
49
         */
50
        public function parseTable(Texy\BlockParser $parser, array $matches): ?HtmlElement
1✔
51
        {
52
                if ($this->disableTables) {
1✔
53
                        return null;
1✔
54
                }
55

56
                [, $mMod] = $matches;
1✔
57
                // [1] => .(title)[class]{style}<>_
58

59
                $texy = $this->texy;
1✔
60

61
                $el = new HtmlElement('table');
1✔
62
                $mod = Modifier::parse($mMod);
1✔
63
                $mod->decorate($texy, $el);
1✔
64

65
                $parser->moveBackward();
1✔
66

67
                if ($parser->next(
1✔
68
                        '~^
1✔
69
                                \|                               # opening pipe
70
                                ( [\#=] ){2,}  (?! [|#=+] )      # opening chars (1)
71
                                (.+)                             # caption (2)
72
                                \1*                              # matching closing chars
73
                                \|? \ *                              # optional closing pipe and spaces
74
                                ' . Patterns::MODIFIER_H . '?    # modifier (3)
1✔
75
                        $~Um',
76
                        $matches,
77
                )) {
78
                        /** @var array{string, string, string, ?string} $matches */
UNCOV
79
                        [, , $mContent, $mMod] = $matches;
×
80
                        // [1] => # / =
81
                        // [2] => ....
82
                        // [3] => .(title)[class]{style}<>
83

84
                        $caption = $el->create('caption');
×
85
                        $mod = Modifier::parse($mMod);
×
86
                        $mod->decorate($texy, $caption);
×
UNCOV
87
                        $caption->parseLine($texy, $mContent);
×
88
                }
89

90
                $isHead = false;
1✔
91
                $colModifier = [];
1✔
92
                $prevRow = []; // rowSpan building helper
1✔
93
                $rowCounter = 0;
1✔
94
                $colCounter = 0;
1✔
95
                $elPart = null;
1✔
96

97
                while (true) {
1✔
98
                        if ($parser->next('~^ \| ([=-]) [+|=-]{2,} $~Um', $matches)) { // line
1✔
99
                                $isHead = !$isHead;
1✔
100
                                $prevRow = [];
1✔
101
                                continue;
1✔
102
                        }
103

104
                        if ($parser->next('~^ \| (.*) (?: | \| [ \t]* ' . Patterns::MODIFIER_HV . '?)$~U', $matches)) {
1✔
105
                                // smarter head detection
106
                                if ($rowCounter === 0 && !$isHead && $parser->next('~^ \| [=-] [+|=-]{2,} $~Um', $foo)) {
1✔
107
                                        $isHead = true;
1✔
108
                                        $parser->moveBackward();
1✔
109
                                }
110

111
                                if ($elPart === null) {
1✔
112
                                        $elPart = $el->create($isHead ? 'thead' : 'tbody');
1✔
113

114
                                } elseif (!$isHead && $elPart->getName() === 'thead') {
1✔
115
                                        $this->finishPart($elPart);
1✔
116
                                        $elPart = $el->create('tbody');
1✔
117
                                }
118

119
                                /** @var array{string, string, ?string} $matches */
120
                                [, $mContent, $mMod] = $matches;
1✔
121
                                // [1] => ....
122
                                // [2] => .(title)[class]{style}<>_
123

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

126
                                if ($elRow->count()) {
1✔
127
                                        $elPart->add($elRow);
1✔
128
                                        $rowCounter++;
1✔
129
                                } else { // redundant row
130
                                        foreach ($prevRow as $elCell) {
1✔
131
                                                $elCell->rowSpan--;
1✔
132
                                        }
133
                                }
134

135
                                continue;
1✔
136
                        }
137

138
                        break;
1✔
139
                }
140

141
                if ($elPart === null) { // invalid table
1✔
UNCOV
142
                        return null;
×
143
                }
144

145
                if ($elPart->getName() === 'thead') {
1✔
146
                        // thead is optional, tbody is required
UNCOV
147
                        $elPart->setName('tbody');
×
148
                }
149

150
                $this->finishPart($elPart);
1✔
151

152
                // event listener
153
                $texy->invokeHandlers('afterTable', [$parser, $el, $mod]);
1✔
154

155
                return $el;
1✔
156
        }
157

158

159
        /**
160
         * @param  array<int, TableCellElement>  $prevRow
161
         * @param  array<int, Modifier|null>  $colModifier
162
         */
163
        private function processRow(
1✔
164
                string $content,
165
                ?string $mMod,
166
                bool $isHead,
167
                Texy\Texy $texy,
168
                array &$prevRow,
169
                array &$colModifier,
170
                int &$colCounter,
171
        ): HtmlElement
172
        {
173
                $elRow = new HtmlElement('tr');
1✔
174
                $mod = Modifier::parse($mMod);
1✔
175
                $mod->decorate($texy, $elRow);
1✔
176

177
                $col = 0;
1✔
178
                $elCell = null;
1✔
179

180
                // special escape sequence \|
181
                $content = str_replace('\|', "\x13", $content);
1✔
182
                $content = Regexp::replace($content, '~(\[[^]]*)\|~', "$1\x13"); // HACK: support for [..|..]
1✔
183

184
                foreach (explode('|', $content) as $cell) {
1✔
185
                        $cell = strtr($cell, "\x13", '|');
1✔
186
                        // rowSpan
187
                        if (isset($prevRow[$col]) && ($matches = Regexp::match($cell, '~\^[ \t]*$|\*??(.*)[ \t]+\^$~AU'))) {
1✔
188
                                $prevRow[$col]->rowSpan++;
1✔
189
                                $cell = $matches[1] ?? '';
1✔
190
                                $prevRow[$col]->text .= "\n" . $cell;
1✔
191
                                $col += $prevRow[$col]->colSpan;
1✔
192
                                $elCell = null;
1✔
193
                                continue;
1✔
194
                        }
195

196
                        // colSpan
197
                        if ($cell === '' && $elCell) {
1✔
198
                                $elCell->colSpan++;
1✔
199
                                unset($prevRow[$col]);
1✔
200
                                $col++;
1✔
201
                                continue;
1✔
202
                        }
203

204
                        // common cell
205
                        if ($elCell = $this->processCell($cell, $colModifier[$col], $isHead, $texy)) {
1✔
206
                                $elRow->add($elCell);
1✔
207
                                $prevRow[$col] = $elCell;
1✔
208
                                $col++;
1✔
209
                        }
210
                }
211

212
                // even up with empty cells
213
                while ($col < $colCounter) {
1✔
214
                        $elCell = new TableCellElement;
1✔
215
                        $elCell->setName($isHead ? 'th' : 'td');
1✔
216
                        if (isset($colModifier[$col])) {
1✔
UNCOV
217
                                $colModifier[$col]->decorate($texy, $elCell);
×
218
                        }
219

220
                        $elRow->add($elCell);
1✔
221
                        $prevRow[$col] = $elCell;
1✔
222
                        $col++;
1✔
223
                }
224

225
                $colCounter = $col;
1✔
226
                return $elRow;
1✔
227
        }
228

229

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

249
                [, $mHead, $mModCol, $mContent, $mMod] = $matches;
1✔
250
                // [1] => * ^
251
                // [2] => .(title)[class]{style}<>_
252
                // [3] => ....
253
                // [4] => .(title)[class]{style}<>_
254

255
                if ($mModCol) {
1✔
UNCOV
256
                        $cellModifier = Modifier::parse($mModCol);
×
257
                }
258

259
                $mod = $cellModifier ? clone $cellModifier : new Modifier;
1✔
260
                $mod->setProperties($mMod);
1✔
261

262
                $elCell = new TableCellElement;
1✔
263
                $elCell->setName($isHead || ($mHead === '*') ? 'th' : 'td');
1✔
264
                $mod->decorate($texy, $elCell);
1✔
265
                $elCell->text = $mContent;
1✔
266
                return $elCell;
1✔
267
        }
268

269

270
        /**
271
         * Parse text in all cells.
272
         */
273
        private function finishPart(HtmlElement $elPart): void
1✔
274
        {
275
                foreach ($elPart->getChildren() as $elRow) {
1✔
276
                        assert($elRow instanceof HtmlElement);
277
                        foreach ($elRow->getChildren() as $elCell) {
1✔
278
                                assert($elCell instanceof TableCellElement);
279
                                if ($elCell->colSpan > 1) {
1✔
280
                                        $elCell->attrs['colspan'] = $elCell->colSpan;
1✔
281
                                }
282

283
                                if ($elCell->rowSpan > 1) {
1✔
284
                                        $elCell->attrs['rowspan'] = $elCell->rowSpan;
1✔
285
                                }
286

287
                                $text = rtrim((string) $elCell->text);
1✔
288
                                if (str_contains($text, "\n")) {
1✔
289
                                        // multiline parse as block
290
                                        // HACK: disable tables
291
                                        $this->disableTables = true;
1✔
292
                                        $elCell->parseBlock($this->texy, Texy\Helpers::outdent($text));
1✔
293
                                        $this->disableTables = false;
1✔
294
                                } else {
295
                                        $elCell->parseLine($this->texy, ltrim($text));
1✔
296
                                }
297

298
                                if ($elCell->getText() === '') {
1✔
299
                                        $elCell->setText("\u{A0}"); // &nbsp;
1✔
300
                                }
301
                        }
302
                }
303
        }
1✔
304
}
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