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

dg / texy / 21345922688

26 Jan 2026 04:05AM UTC coverage: 92.426% (+0.04%) from 92.382%
21345922688

push

github

dg
HtmlElement: removed toHtml() & toText()

18 of 19 new or added lines in 5 files covered. (94.74%)

28 existing lines in 7 files now uncovered.

2404 of 2601 relevant lines covered (92.43%)

0.92 hits per line

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

92.13
/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
 * Table module.
22
 */
23
final class TableModule extends Texy\Module
24
{
25
        private ?bool $disableTables = null;
26

27

28
        public function __construct(Texy\Texy $texy)
1✔
29
        {
30
                $this->texy = $texy;
1✔
31

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

43

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

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

63
                $texy = $this->texy;
1✔
64

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

69
                $parser->moveBackward();
1✔
70

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

87
                        $caption = $el->create('caption');
×
88
                        $mod = new Modifier($mMod);
×
89
                        $mod->decorate($texy, $caption);
×
UNCOV
90
                        $caption->inject($texy->parseLine($mContent));
×
91
                }
92

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

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

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

114
                                if ($elPart === null) {
1✔
115
                                        $elPart = $el->create($isHead ? 'thead' : 'tbody');
1✔
116

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

122
                                [, $mContent, $mMod] = $matches;
1✔
123
                                // [1] => ....
124
                                // [2] => .(title)[class]{style}<>_
125

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

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

137
                                continue;
1✔
138
                        }
139

140
                        break;
1✔
141
                }
142

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

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

152
                $this->finishPart($elPart);
1✔
153

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

157
                return $el;
1✔
158
        }
159

160

161
        /**
162
         * @param  array<int, TableCellElement>  $prevRow
163
         * @param  array<int, Modifier|null>  $colModifier
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
                $col = 0;
1✔
181
                $elCell = null;
1✔
182

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

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

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

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

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

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

228
                $colCounter = $col;
1✔
229
                return $elRow;
1✔
230
        }
231

232

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

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

258
                if ($mModCol) {
1✔
UNCOV
259
                        $cellModifier = new Modifier($mModCol);
×
260
                }
261

262
                $mod = $cellModifier ? clone $cellModifier : new Modifier;
1✔
263
                $mod->setProperties($mMod);
1✔
264

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

272

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

286
                                if ($elCell->rowSpan > 1) {
1✔
287
                                        $elCell->attrs['rowspan'] = $elCell->rowSpan;
1✔
288
                                }
289

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

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