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

dg / texy / 29654833201

18 Jul 2026 04:09PM UTC coverage: 93.039% (+0.04%) from 93.004%
29654833201

push

github

dg
Restored test coverage dropped during the AST migration

The migration removed five test files coupled to v3 contracts (block,
content-model, formatter-indent, html, latte) with the intent to bring
them back once the output layer settled. Restored here, ported to the
AST-era API; latte and most of content-model pass unchanged.

Expected outputs were re-derived and every difference against the v3
files reviewed case by case. The differences fall into the intentional
categories: list items merge continuation lines, whitespace-sensitive
typography follows visible length instead of protection-mark key length,
rejected tags are escaped consistently (both halves of a pair) with
typography applied to the escaped text, paragraphs of escaped markup are
<p>-wrapped, and <p> inside <form> is preserved. Inner texysource
fragments are parsed without the transform phase, so their passthrough
tags stay live - a documented wrinkle.

content-model.phpt again guards the WellFormer content model, nesting
prohibitions, transparent and unknown elements and auto-closing in the
public suite, independently of the private regression corpus.

3248 of 3491 relevant lines covered (93.04%)

0.93 hits per line

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

98.44
/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\Position;
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::MODIFIER_HV . '\n)? # modifier (1)
1✔
44
                                \|                                   # table start
45
                                .*                                   # content
46
                        $~mU',
47
                        Syntax::Table,
1✔
48
                );
49
        }
1✔
50

51

52
        /**
53
         * Parses tables.
54
         * @param  array<?string>  $matches
55
         * @param  array<?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,} $~Um', $lineMatches, $lineOffsets)) {
1✔
82
                                $isHead = !$isHead;
1✔
83
                                $prevRow = [];
1✔
84
                                continue;
1✔
85
                        }
86

87
                        if ($context->getBlockParser()->next('~^ ( \| ) (.*) (?: | \| [ \t]* ' . Patterns::MODIFIER_HV . '?)$~U', $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,} $~Um', $foo)) {
1✔
90
                                        $isHead = true;
1✔
91
                                        $context->getBlockParser()->moveBackward();
1✔
92
                                }
93

94
                                [, , $mContent, $mRowMod] = $lineMatches;
1✔
95
                                $lineBaseOffset = $lineOffsets[2] ?? $lineOffsets[0]; // offset of content after first |
1✔
96

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

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

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

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

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

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

146
                                        if ($cellMatches) {
1✔
147
                                                $mHead = $cellMatches[1][0];
1✔
148
                                                $mModCol = $cellMatches[2][0];
1✔
149
                                                $mCellContent = $cellMatches[3][0];
1✔
150
                                                $mCellContentOffset = $cellMatches[3][1];
1✔
151
                                                $mCellMod = $cellMatches[4][0];
1✔
152

153
                                                $cellIsHeader = $isHead || ($mHead === '*');
1✔
154

155
                                                // column modifier inheritance
156
                                                if ($mModCol) {
1✔
157
                                                        $colModifier[$col] = Modifier::parse($mModCol);
×
158
                                                }
159
                                                $cellMod = isset($colModifier[$col]) ? clone $colModifier[$col] : new Modifier;
1✔
160
                                                $cellMod->setProperties($mCellMod);
1✔
161

162
                                                // Calculate absolute offset of cell content
163
                                                $contentAbsoluteOffset = $cellAbsoluteOffset + $mCellContentOffset;
1✔
164

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

173
                                        $cellOffset += strlen($originalCell) + 1; // +1 for |
1✔
174
                                }
175

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

186
                                $colCounter = $col;
1✔
187

188
                                if ($cells) {
1✔
189
                                        $rowMod = Modifier::parse($mRowMod);
1✔
190
                                        $rows[] = new TableRowNode($cells, $isHead, $rowMod);
1✔
191
                                } else {
192
                                        // redundant row - decrement rowspan
193
                                        foreach ($prevRow as $item) {
1✔
194
                                                $item['node']->rowspan--;
1✔
195
                                        }
196
                                }
197

198
                                continue;
1✔
199
                        }
200

201
                        break;
1✔
202
                }
203

204
                if (!$rows) {
1✔
205
                        return null;
×
206
                }
207

208
                // Parse cell text content after rowspan/colspan is determined
209
                foreach ($rows as $row) {
1✔
210
                        foreach ($row->cells as $cell) {
1✔
211
                                if (isset($cellTexts[$cell])) {
1✔
212
                                        $lines = $cellTexts[$cell];
1✔
213

214
                                        // right-trim: drop blank trailing fragments, trim the last one
215
                                        while ($lines && rtrim($lines[count($lines) - 1]['content']) === '') {
1✔
216
                                                array_pop($lines);
1✔
217
                                        }
218
                                        if ($lines) {
1✔
219
                                                $last = count($lines) - 1;
1✔
220
                                                $lines[$last]['content'] = rtrim($lines[$last]['content']);
1✔
221
                                        }
222

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

243
                                        // empty cell gets &nbsp;
244
                                        if ($cell->content->children === []) {
1✔
245
                                                $cell->content->children = [new Texy\Nodes\TextNode("\u{A0}")];
1✔
246
                                        }
247
                                }
248
                        }
249
                }
250

251
                return new TableNode(
1✔
252
                        $rows,
1✔
253
                        Modifier::parse($mMod),
1✔
254
                        new Position($startOffset, strlen($matches[0])),
1✔
255
                );
256
        }
257
}
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