• 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

98.46
/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\Modifier;
12
use Texy\Nodes\ContentNode;
13
use Texy\Nodes\TableCellNode;
14
use Texy\Nodes\TableNode;
15
use Texy\Nodes\TableRowNode;
16
use Texy\ParseContext;
17
use Texy\Patterns;
18
use Texy\Range;
19
use Texy\Regexp;
20
use Texy\Syntax;
21
use function array_pop, count, ltrim, rtrim, strlen;
22

23

24
/**
25
 * Table module.
26
 */
27
final class TableModule extends Texy\Module
28
{
29
        private bool $disableTables = false;
30

31

32
        public function __construct(
1✔
33
                private Texy\Texy $texy,
34
        ) {
35
        }
1✔
36

37

38
        public function beforeParse(string &$text): void
1✔
39
        {
40
                $this->texy->registerBlockPattern(
1✔
41
                        $this->parseTable(...),
1✔
42
                        '~^
43
                                (?:' . Patterns::ModifierHVAlign . '\n)? # modifier (1)
1✔
44
                                \|                                   # table start
45
                                .*                                   # content
46
                        $~mUx',
47
                        Syntax::Table,
1✔
48
                );
49
        }
1✔
50

51

52
        /**
53
         * Parses tables.
54
         * @param  array{string, ?string}  $matches
55
         * @param  array{int, ?int}  $offsets
56
         */
57
        public function parseTable(ParseContext $context, array $matches, array $offsets): ?TableNode
1✔
58
        {
59
                if ($this->disableTables) {
1✔
60
                        return null;
1✔
61
                }
62

63
                [, $mMod] = $matches;
1✔
64

65
                $startOffset = $offsets[0];
1✔
66

67
                $context->getBlockParser()->moveBackward();
1✔
68

69
                $rows = [];
1✔
70
                $isHead = false;
1✔
71
                /** @var array<int, array{node: TableCellNode, text: string}> $prevRow */
72
                $prevRow = [];
1✔
73
                $colModifier = [];
1✔
74
                $colCounter = 0;
1✔
75
                /** @var \SplObjectStorage<TableCellNode, list<array{content: string, offset: int}>> $cellTexts */
76
                $cellTexts = new \SplObjectStorage;
1✔
77

78
                while (true) {
1✔
79
                        $lineMatches = null;
1✔
80
                        $lineOffsets = null;
1✔
81
                        if ($context->getBlockParser()->next('~^ \| ([=-]) [+|=-]{2,} $~Umx', $lineMatches, $lineOffsets)) {
1✔
82
                                $isHead = !$isHead;
1✔
83
                                $prevRow = [];
1✔
84
                                continue;
1✔
85
                        }
86

87
                        if ($context->getBlockParser()->next('~^ ( \| ) (.*) (?: | \| [ \t]* ' . Patterns::ModifierHVAlign . '?)$~Ux', $lineMatches, $lineOffsets)) {
1✔
88
                                // smarter head detection: if first row is followed by separator line, it's a head row
89
                                if (count($rows) === 0 && !$isHead && $context->getBlockParser()->next('~^ \| [=-] [+|=-]{2,} $~Umx', $foo)) {
1✔
90
                                        $isHead = true;
1✔
91
                                        $context->getBlockParser()->moveBackward();
1✔
92
                                }
93

94
                                // groups 1 and 2 always participate in a successful match, but next() cannot type that
95
                                $mContent = $lineMatches[2] ?? throw new \LogicException('Match without group 2.');
1✔
96
                                $mRowMod = $lineMatches[3] ?? null;
1✔
97
                                $lineBaseOffset = $lineOffsets[2] ?? throw new \LogicException('Match without group 2.'); // content after first |
1✔
98

99
                                $cells = [];
1✔
100
                                $originalContent = $mContent;
1✔
101
                                $content = str_replace('\|', "\x13", $mContent);
1✔
102
                                $content = Regexp::replace($content, '~(\[[^]]*)\|~', "$1\x13");
1✔
103

104
                                $col = 0;
1✔
105
                                $lastCell = null;
1✔
106
                                $cellOffset = 0; // position within $content
1✔
107

108
                                foreach (explode('|', $content) as $cellIndex => $cell) {
1✔
109
                                        $originalCell = $cell;
1✔
110
                                        $cell = strtr($cell, "\x13", '|');
1✔
111
                                        $cellAbsoluteOffset = $lineBaseOffset + $cellOffset;
1✔
112

113
                                        // rowSpan: ^ at end of cell or cell is just ^
114
                                        if (isset($prevRow[$col]) && ($m = Regexp::match($cell, '~\^[ \t]*$|\*??(.*)[ \t]+\^$~AU', captureOffset: true))) {
1✔
115
                                                $prevRow[$col]['node']->rowspan++;
1✔
116
                                                /** @var non-empty-array<array{?string, int}> $m */
117
                                                $cellText = $m[1][0] ?? '';
1✔
118
                                                $cellTextOffset = ($m[1][1] ?? -1) >= 0 ? $cellAbsoluteOffset + $m[1][1] : $cellAbsoluteOffset;
1✔
119
                                                // Append text to the cell above
120
                                                $lines = $cellTexts[$prevRow[$col]['node']];
1✔
121
                                                $lines[] = ['content' => (string) $cellText, 'offset' => $cellTextOffset];
1✔
122
                                                $cellTexts[$prevRow[$col]['node']] = $lines;
1✔
123
                                                $col += $prevRow[$col]['node']->colspan;
1✔
124
                                                $lastCell = null;
1✔
125
                                                $cellOffset += strlen($originalCell) + 1; // +1 for |
1✔
126
                                                continue;
1✔
127
                                        }
128

129
                                        // colSpan: empty cell extends previous cell
130
                                        if ($cell === '' && $lastCell !== null) {
1✔
131
                                                $lastCell->colspan++;
1✔
132
                                                unset($prevRow[$col]);
1✔
133
                                                $col++;
1✔
134
                                                $cellOffset += strlen($originalCell) + 1;
1✔
135
                                                continue;
1✔
136
                                        }
137

138
                                        // common cell
139
                                        $cellMatches = Regexp::match($cell, '~
1✔
140
                                                ( \*?? )                          # head mark (1)
141
                                                [ \t]*
142
                                                ' . Patterns::ModifierHVAlign . '??   # modifier (2)
143
                                                (.*)                              # content (3)
144
                                                ' . Patterns::ModifierHVAlign . '?    # modifier (4)
1✔
145
                                                [ \t]*
146
                                        $~AUx', captureOffset: true);
1✔
147

148
                                        if ($cellMatches) {
1✔
149
                                                $mHead = $cellMatches[1][0];
1✔
150
                                                $mModCol = $cellMatches[2][0];
1✔
151
                                                // group 3 is (.*), so it always participates
152
                                                $mCellContent = $cellMatches[3][0] ?? throw new \LogicException('Cell without group 3.');
1✔
153
                                                $mCellContentOffset = $cellMatches[3][1];
1✔
154
                                                $mCellMod = $cellMatches[4][0];
1✔
155

156
                                                $cellIsHeader = $isHead || ($mHead === '*');
1✔
157

158
                                                // column modifier inheritance
159
                                                if ($mModCol) {
1✔
UNCOV
160
                                                        $colModifier[$col] = Modifier::parse($mModCol, $cellMatches[2][1] >= 0 ? $cellAbsoluteOffset + $cellMatches[2][1] : null);
×
161
                                                }
162
                                                $cellMod = isset($colModifier[$col]) ? clone $colModifier[$col] : new Modifier;
1✔
163
                                                $cellMod->setProperties($mCellMod);
1✔
164

165
                                                // Calculate absolute offset of cell content
166
                                                $contentAbsoluteOffset = $cellAbsoluteOffset + $mCellContentOffset;
1✔
167

168
                                                // Create cell node - text will be parsed later
169
                                                $lastCell = new TableCellNode(new ContentNode, 1, 1, $cellIsHeader, $cellMod, new Range($cellAbsoluteOffset, strlen($originalCell)));
1✔
170
                                                $cells[] = $lastCell;
1✔
171
                                                $cellTexts[$lastCell] = [['content' => $mCellContent, 'offset' => $contentAbsoluteOffset]];
1✔
172
                                                $prevRow[$col] = ['node' => $lastCell, 'text' => $mCellContent];
1✔
173
                                                $col++;
1✔
174
                                        }
175

176
                                        $cellOffset += strlen($originalCell) + 1; // +1 for |
1✔
177
                                }
178

179
                                // even up with empty cells
180
                                while ($col < $colCounter) {
1✔
181
                                        $cellMod = isset($colModifier[$col]) ? clone $colModifier[$col] : new Modifier;
1✔
182
                                        $emptyCell = new TableCellNode(new ContentNode, 1, 1, $isHead, $cellMod);
1✔
183
                                        $cells[] = $emptyCell;
1✔
184
                                        $cellTexts[$emptyCell] = [['content' => '', 'offset' => 0]];
1✔
185
                                        $prevRow[$col] = ['node' => $emptyCell, 'text' => ''];
1✔
186
                                        $col++;
1✔
187
                                }
188

189
                                $colCounter = $col;
1✔
190

191
                                if ($cells) {
1✔
192
                                        $rowMod = Modifier::parse($mRowMod, $lineOffsets[3] ?? null);
1✔
193
                                        // group 0 always participates in a successful match, but next() cannot type that
194
                                        $rowOffset = $lineOffsets[0] ?? throw new \LogicException('Match without group 0.');
1✔
195
                                        $rowText = $lineMatches[0] ?? throw new \LogicException('Match without group 0.');
1✔
196
                                        $rows[] = new TableRowNode($cells, $isHead, $rowMod, new Range($rowOffset, strlen($rowText)));
1✔
197
                                } else {
198
                                        // redundant row - decrement rowspan
199
                                        foreach ($prevRow as $item) {
1✔
200
                                                $item['node']->rowspan--;
1✔
201
                                        }
202
                                }
203

204
                                continue;
1✔
205
                        }
206

207
                        break;
1✔
208
                }
209

210
                if (!$rows) {
1✔
UNCOV
211
                        return null;
×
212
                }
213

214
                // Parse cell text content after rowspan/colspan is determined
215
                foreach ($rows as $row) {
1✔
216
                        foreach ($row->cells as $cell) {
1✔
217
                                if (isset($cellTexts[$cell])) {
1✔
218
                                        $lines = $cellTexts[$cell];
1✔
219

220
                                        // right-trim: drop blank trailing fragments, trim the last one
221
                                        while ($lines && rtrim($lines[count($lines) - 1]['content']) === '') {
1✔
222
                                                array_pop($lines);
1✔
223
                                        }
224
                                        if ($lines) {
1✔
225
                                                $last = count($lines) - 1;
1✔
226
                                                $lines[$last]['content'] = rtrim($lines[$last]['content']);
1✔
227
                                        }
228

229
                                        if (count($lines) > 1) {
1✔
230
                                                // multiline - parse as block (disable nested tables)
231
                                                [$outdented, $map] = Texy\OffsetMap::outdentLines($lines);
1✔
232
                                                $this->disableTables = true;
1✔
233
                                                try {
234
                                                        $parsed = $context->parseBlock($outdented);
1✔
235
                                                } finally {
1✔
236
                                                        $this->disableTables = false;
1✔
237
                                                }
238
                                                $map->applyTo($parsed);
1✔
239
                                                $cell->content->children = $parsed->children;
1✔
240
                                        } else {
241
                                                // single line - parse as inline
242
                                                $text = $lines[0]['content'] ?? '';
1✔
243
                                                $trimmed = ltrim($text);
1✔
244
                                                $trimOffset = strlen($text) - strlen($trimmed);
1✔
245
                                                $baseOffset = ($lines[0]['offset'] ?? 0) + $trimOffset;
1✔
246
                                                $cell->content->children = $context->parseInline($trimmed, $baseOffset)->children;
1✔
247
                                        }
248

249
                                        // empty cell gets &nbsp;
250
                                        if ($cell->content->children === []) {
1✔
251
                                                $cell->content->children = [new Texy\Nodes\TextNode("\u{A0}")];
1✔
252
                                        }
253
                                }
254
                        }
255
                }
256

257
                return new TableNode(
1✔
258
                        $rows,
1✔
259
                        Modifier::parse($mMod, $offsets[1]),
1✔
260
                        new Range($startOffset, strlen($matches[0])),
1✔
261
                );
262
        }
263
}
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