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

dg / texy / 21344532034

26 Jan 2026 02:43AM UTC coverage: 91.98% (-0.4%) from 92.376%
21344532034

push

github

dg
added CLAUDE.md

2397 of 2606 relevant lines covered (91.98%)

0.92 hits per line

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

90.63
/src/Texy/Modules/TableModule.php
1
<?php
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
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
use function explode, ltrim, rtrim, str_contains, str_replace, strtr;
18

19

20
/**
21
 * Processes table syntax with headers, colspan, and rowspan support.
22
 */
23
final class TableModule extends Texy\Module
24
{
25
        /** @deprecated */
26
        public ?string $oddClass = null;
27

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

32

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

37
                $texy->registerBlockPattern(
1✔
38
                        $this->patternTable(...),
1✔
39
                        '#^(?:' . Patterns::MODIFIER_HV . '\n)?' // .{color: red}
40
                        . '\|.*()$#mU', // | ....
1✔
41
                        'table',
1✔
42
                );
43
        }
1✔
44

45

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

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

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

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

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

73
                if ($parser->next('#^\|(\#|\=){2,}(?![|\#=+])(.+)\1*\|?\ *' . Patterns::MODIFIER_H . '?()$#Um', $matches)) {
1✔
74
                        [, , $mContent, $mMod] = $matches;
×
75
                        // [1] => # / =
76
                        // [2] => ....
77
                        // [3] => .(title)[class]{style}<>
78

79
                        $caption = $el->create('caption');
×
80
                        $mod = new Modifier($mMod);
×
81
                        $mod->decorate($texy, $caption);
×
82
                        $caption->parseLine($texy, $mContent);
×
83
                }
84

85
                $isHead = false;
1✔
86
                $colModifier = [];
1✔
87
                $prevRow = []; // rowSpan building helper
1✔
88
                $rowCounter = 0;
1✔
89
                $colCounter = 0;
1✔
90
                $elPart = null;
1✔
91

92
                while (true) {
1✔
93
                        if ($parser->next('#^\|([=-])[+|=-]{2,}$#Um', $matches)) { // line
1✔
94
                                $isHead = !$isHead;
1✔
95
                                $prevRow = [];
1✔
96
                                continue;
1✔
97
                        }
98

99
                        if ($parser->next('#^\|(.*)(?:|\|[\ \t]*' . Patterns::MODIFIER_HV . '?)()$#U', $matches)) {
1✔
100
                                // smarter head detection
101
                                if ($rowCounter === 0 && !$isHead && $parser->next('#^\|[=-][+|=-]{2,}$#Um', $foo)) {
1✔
102
                                        $isHead = true;
1✔
103
                                        $parser->moveBackward();
1✔
104
                                }
105

106
                                if ($elPart === null) {
1✔
107
                                        $elPart = $el->create($isHead ? 'thead' : 'tbody');
1✔
108

109
                                } elseif (!$isHead && $elPart->getName() === 'thead') {
1✔
110
                                        $this->finishPart($elPart);
1✔
111
                                        $elPart = $el->create('tbody');
1✔
112
                                }
113

114
                                [, $mContent, $mMod] = $matches;
1✔
115
                                // [1] => ....
116
                                // [2] => .(title)[class]{style}<>_
117

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

120
                                if ($elRow->count()) {
1✔
121
                                        $elPart->add($elRow);
1✔
122
                                        $rowCounter++;
1✔
123
                                } else { // redundant row
124
                                        foreach ($prevRow as $elCell) {
1✔
125
                                                $elCell->rowSpan--;
1✔
126
                                        }
127
                                }
128

129
                                continue;
1✔
130
                        }
131

132
                        break;
1✔
133
                }
134

135
                if ($elPart === null) { // invalid table
1✔
136
                        return null;
×
137
                }
138

139
                if ($elPart->getName() === 'thead') {
1✔
140
                        // thead is optional, tbody is required
141
                        $elPart->setName('tbody');
×
142
                }
143

144
                $this->finishPart($elPart);
1✔
145

146
                // event listener
147
                $texy->invokeHandlers('afterTable', [$parser, $el, $mod]);
1✔
148

149
                return $el;
1✔
150
        }
151

152

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

172
                $rowClass = $rowCounter % 2 === 0 ? $this->oddClass : $this->evenClass;
1✔
173
                if ($rowClass && !isset($mod->classes[$this->oddClass ?? '']) && !isset($mod->classes[$this->evenClass ?? ''])) {
1✔
174
                        $elRow->attrs['class'] = (array) ($elRow->attrs['class'] ?? []);
×
175
                        $elRow->attrs['class'][] = $rowClass;
×
176
                }
177

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

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

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

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

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

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

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

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

230

231
        private function processCell(
1✔
232
                string $cell,
233
                ?Modifier &$cellModifier,
234
                bool $isHead,
235
                Texy\Texy $texy,
236
        ): ?TableCellElement
237
        {
238
                $matches = Regexp::match($cell, '#(\*??)[\ \t]*' . Patterns::MODIFIER_HV . '??(.*)' . Patterns::MODIFIER_HV . '?[\ \t]*()$#AU');
1✔
239
                if (!$matches) {
1✔
240
                        return null;
×
241
                }
242

243
                [, $mHead, $mModCol, $mContent, $mMod] = $matches;
1✔
244
                // [1] => * ^
245
                // [2] => .(title)[class]{style}<>_
246
                // [3] => ....
247
                // [4] => .(title)[class]{style}<>_
248

249
                if ($mModCol) {
1✔
250
                        $cellModifier = new Modifier($mModCol);
×
251
                }
252

253
                $mod = $cellModifier ? clone $cellModifier : new Modifier;
1✔
254
                $mod->setProperties($mMod);
1✔
255

256
                $elCell = new TableCellElement;
1✔
257
                $elCell->setName($isHead || ($mHead === '*') ? 'th' : 'td');
1✔
258
                $mod->decorate($texy, $elCell);
1✔
259
                $elCell->text = $mContent;
1✔
260
                return $elCell;
1✔
261
        }
262

263

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

277
                                if ($elCell->rowSpan > 1) {
1✔
278
                                        $elCell->attrs['rowspan'] = $elCell->rowSpan;
1✔
279
                                }
280

281
                                $text = rtrim((string) $elCell->text);
1✔
282
                                if (str_contains($text, "\n")) {
1✔
283
                                        // multiline parse as block
284
                                        // HACK: disable tables
285
                                        $this->disableTables = true;
1✔
286
                                        $elCell->parseBlock($this->texy, Texy\Helpers::outdent($text));
1✔
287
                                        $this->disableTables = false;
1✔
288
                                } else {
289
                                        $elCell->parseLine($this->texy, ltrim($text));
1✔
290
                                }
291

292
                                if ($elCell->getText() === '') {
1✔
293
                                        $elCell->setText("\u{A0}"); // &nbsp;
1✔
294
                                }
295
                        }
296
                }
297
        }
1✔
298
}
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