• 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

97.47
/src/Texy/Modules/ParagraphModule.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;
13
use Texy\Nodes\ContentNode;
14
use Texy\Nodes\LineBreakNode;
15
use Texy\Nodes\ParagraphNode;
16
use Texy\Nodes\TextNode;
17
use Texy\Output\Html;
18
use Texy\ParseContext;
19
use Texy\Patterns;
20
use Texy\Regexp;
21
use function is_array, str_contains, strlen;
22

23

24
/**
25
 * Processes paragraphs and handles line breaks.
26
 */
27
final class ParagraphModule extends Texy\Module
28
{
29
        public function __construct(
1✔
30
                private Texy\Texy $texy,
31
        ) {
32
        }
1✔
33

34

35
        /**
36
         * Parse text into paragraphs (split by blank lines).
37
         * @return array<ParagraphNode>
38
         */
39
        public function parseText(ParseContext $context, string $text, int $baseOffset = 0): array
1✔
40
        {
41
                $parts = Regexp::split($text, '~(\n{2,})~', captureOffset: true, skipEmpty: true);
1✔
42
                $res = [];
1✔
43
                foreach ($parts ?: [] as $partInfo) {
1✔
44
                        // With captureOffset, each part is [content, offset]
45
                        if (is_array($partInfo)) {
1✔
46
                                [$part, $partOffset] = $partInfo;
1✔
47
                                $partOffset += $baseOffset;
1✔
48
                        } else {
49
                                $part = $partInfo;
×
50
                                $partOffset = $baseOffset;
×
51
                        }
52

53
                        $trimmed = trim($part);
1✔
54
                        if ($trimmed === '') {
1✔
55
                                continue;
1✔
56
                        }
57

58
                        // Calculate offset after leading whitespace trim
59
                        $leadingTrim = strlen($part) - strlen(ltrim($part));
1✔
60
                        $contentOffset = $partOffset + $leadingTrim;
1✔
61

62
                        // Text starting with known block element - parse without soft line breaks
63
                        if ($this->startsWithBlockElement($trimmed)) {
1✔
64
                                $node = $this->parseBlockHtml($context, $trimmed, $contentOffset);
1✔
65
                        } else {
66
                                $node = $this->parseParagraph($context, $trimmed, $contentOffset);
1✔
67
                                // Check if parsed content contains block-level HTML tags
68
                                if ($this->containsBlockHtmlTag($node->content->children)) {
1✔
69
                                        $node->blockHtml = true;
1✔
70
                                }
71
                        }
72

73
                        if ($this->isEmptyParagraph($node)) {
1✔
74
                                continue;
1✔
75
                        }
76

77
                        $res[] = $node;
1✔
78
                }
79

80
                return $res;
1✔
81
        }
82

83

84
        /**
85
         * Check if text starts with a known block-level HTML element.
86
         */
87
        private function startsWithBlockElement(string $text): bool
1✔
88
        {
89
                if (!preg_match('~^<([a-z][a-z0-9]*)\b~i', $text, $m)) {
1✔
90
                        return false;
1✔
91
                }
92
                // Not in inline elements = block element
93
                return !isset(Html\Element::$inlineElements[strtolower($m[1])]);
1✔
94
        }
95

96

97
        /**
98
         * Parse text that starts with block HTML element (no soft line break processing).
99
         */
100
        private function parseBlockHtml(ParseContext $context, string $text, int $baseOffset = 0): ParagraphNode
1✔
101
        {
102
                $content = $context->parseInline($text, $baseOffset);
1✔
103
                $node = new ParagraphNode($content);
1✔
104
                $node->blockHtml = true;
1✔
105
                return $node;
1✔
106
        }
107

108

109
        /**
110
         * Check if content contains a block-level HtmlTagNode.
111
         * Block = any HtmlTagNode that is NOT an inline element.
112
         * @param  array<Texy\Node>  $content
113
         */
114
        private function containsBlockHtmlTag(array $content): bool
1✔
115
        {
116
                foreach ($content as $node) {
1✔
117
                        if ($node instanceof Nodes\HtmlTagNode && !$node->closing) {
1✔
118
                                $tagName = strtolower($node->name);
1✔
119
                                // Not an inline element = block element (includes custom elements)
120
                                if (!isset(Html\Element::$inlineElements[$tagName])) {
1✔
121
                                        return true;
1✔
122
                                }
123
                        }
124
                }
125
                return false;
1✔
126
        }
127

128

129
        /**
130
         * Check if paragraph contains only whitespace.
131
         */
132
        private function isEmptyParagraph(ParagraphNode $node): bool
1✔
133
        {
134
                foreach ($node->content->children as $child) {
1✔
135
                        if ($child instanceof TextNode) {
1✔
136
                                if (trim($child->content) !== '') {
1✔
137
                                        return false;
1✔
138
                                }
139
                        } else {
140
                                return false;
1✔
141
                        }
142
                }
143
                return true;
1✔
144
        }
145

146

147
        private function parseParagraph(ParseContext $context, string $text, int $baseOffset = 0): ParagraphNode
1✔
148
        {
149
                // Extract modifier from paragraph
150
                $modifier = null;
1✔
151
                if ($mx = Regexp::match($text, '~' . Patterns::MODIFIER_H . '(?= \n | \z)~sUm', captureOffset: true)) {
1✔
152
                        /** @var array{array{string, int}, array{string, int}} $mx */
153
                        [$mMod] = $mx[1];
1✔
154
                        $text = trim(substr_replace($text, '', $mx[0][1], strlen($mx[0][0])));
1✔
155
                        if ($text !== '') {
1✔
156
                                $modifier = Modifier::parse($mMod);
1✔
157
                        }
158
                }
159

160
                // Process line breaks - note: this changes text length, positions become approximate
161
                if ($this->texy->mergeLines) {
1✔
162
                        $text = Regexp::replace($text, '~\n\ +(?=\S)~', "\r");
1✔
163
                        $text = Regexp::replace($text, '~\n~', ' ');
1✔
164
                } else {
165
                        $text = Regexp::replace($text, '~\n~', "\r");
1✔
166
                }
167

168
                $content = $context->parseInline($text, $baseOffset);
1✔
169

170
                return new ParagraphNode(
1✔
171
                        new ContentNode($this->expandLineBreaks($content->children)),
1✔
172
                        $modifier,
173
                );
174
        }
175

176

177
        /**
178
         * Expand \r markers in TextNode content into LineBreakNode.
179
         * @param  array<Texy\Node>  $nodes
180
         * @return array<Texy\Node>
181
         */
182
        private function expandLineBreaks(array $nodes): array
1✔
183
        {
184
                $result = [];
1✔
185
                foreach ($nodes as $node) {
1✔
186
                        if ($node instanceof TextNode && str_contains($node->content, "\r")) {
1✔
187
                                foreach (explode("\r", $node->content) as $i => $part) {
1✔
188
                                        if ($i > 0) {
1✔
189
                                                $result[] = new LineBreakNode;
1✔
190
                                        }
191
                                        if ($part !== '') {
1✔
192
                                                $result[] = new TextNode($part, $node->position); // content of an already decoded TextNode
1✔
193
                                        }
194
                                }
195
                        } elseif ($node instanceof Nodes\PhraseNode) {
1✔
196
                                $node->content->children = $this->expandLineBreaks($node->content->children);
1✔
197
                                $result[] = $node;
1✔
198
                        } elseif ($node instanceof Nodes\LinkNode) {
1✔
199
                                $node->content->children = $this->expandLineBreaks($node->content->children);
1✔
200
                                $result[] = $node;
1✔
201
                        } else {
202
                                $result[] = $node;
1✔
203
                        }
204
                }
205
                return $result;
1✔
206
        }
207
}
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