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

dg / texy / 22262589061

21 Feb 2026 07:04PM UTC coverage: 92.991% (+1.8%) from 91.178%
22262589061

push

github

dg
LinkModule: deprecated label and modifiers in link definitions

3 of 3 new or added lines in 1 file covered. (100.0%)

126 existing lines in 22 files now uncovered.

2083 of 2240 relevant lines covered (92.99%)

0.93 hits per line

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

96.67
/src/Texy/Modules/LinkModule.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\HandlerInvocation;
12
use Texy\Link;
13
use Texy\Patterns;
14
use Texy\Regexp;
15
use function str_contains, str_replace, strlen, strncasecmp, strpos, substr, trim, urlencode;
16

17

18
/**
19
 * Processes link references and generates link elements.
20
 */
21
final class LinkModule extends Texy\Module
22
{
23
        /** root of relative links */
24
        public ?string $root = null;
25

26
        /** always use rel="nofollow" for absolute links? */
27
        public bool $forceNoFollow = false;
28

29
        /** @var array<string, Link> link references */
30
        private array $references = [];
31

32

33
        public function __construct(
1✔
34
                private Texy\Texy $texy,
35
        ) {
36
                $texy->allowed['link/definition'] = true;
1✔
37
        }
1✔
38

39

40
        public function beforeParse(string &$text): void
1✔
41
        {
42
                // [la trine]: http://www.latrine.cz/ text odkazu .(title)[class]{style}
43
                if (!empty($this->texy->allowed['link/definition'])) {
1✔
44
                        $text = Texy\Regexp::replace(
1✔
45
                                $text,
1✔
46
                                '~^
47
                                        \[
48
                                        ( [^\[\]#?*\n]{1,100} )           # reference (1)
49
                                        ] : \ ++
50
                                        ( \S{1,1000} )                    # URL (2)
51
                                        ( [ \t] .{1,1000} )?              # optional description (3)
52
                                        ' . Patterns::MODIFIER . '?       # modifier (4)
1✔
53
                                        \s*
54
                                $~mU',
55
                                $this->parseDefinition(...),
1✔
56
                        );
57
                }
58
        }
1✔
59

60

61
        /**
62
         * Parses [la trine]: http://www.latrine.cz/
63
         * @param  array<?string>  $matches
64
         */
65
        private function parseDefinition(array $matches): string
1✔
66
        {
67
                [, $mRef, $mLink, $mLabel, $mMod] = $matches;
1✔
68
                if ($mMod || $mLabel) {
1✔
69
                        trigger_error('Modifiers and label in link definitions are deprecated.', E_USER_DEPRECATED);
1✔
70
                }
71

72
                $link = new Link($mLink);
1✔
73
                $this->checkLink($link);
1✔
74
                $link->name = Texy\Helpers::toLower($mRef);
1✔
75
                $this->references[$link->name] = $link;
1✔
76
                return '';
1✔
77
        }
78

79

80
        /**
81
         * Adds a user-defined link definition (persists across process() calls).
82
         */
83
        public function addDefinition(string $name, string $url): void
1✔
84
        {
85
                $link = new Link($url);
1✔
86
                $link->name = Texy\Helpers::toLower($name);
1✔
87
                $this->references[$link->name] = $link;
1✔
88
        }
1✔
89

90

91
        /**
92
         * Returns named reference.
93
         */
94
        public function getReference(string $name): ?Link
1✔
95
        {
96
                $name = Texy\Helpers::toLower($name);
1✔
97
                if (isset($this->references[$name])) {
1✔
98
                        return clone $this->references[$name];
1✔
99

100
                } else {
101
                        $pos = strpos($name, '?');
1✔
102
                        if ($pos === false) {
1✔
103
                                $pos = strpos($name, '#');
1✔
104
                        }
105

106
                        if ($pos !== false) { // try to extract ?... #... part
1✔
107
                                $name2 = substr($name, 0, $pos);
1✔
108
                                if (isset($this->references[$name2])) {
1✔
109
                                        $link = clone $this->references[$name2];
1✔
110
                                        $link->URL .= substr($name, $pos);
1✔
111
                                        return $link;
1✔
112
                                }
113
                        }
114
                }
115

116
                return null;
1✔
117
        }
118

119

120
        public function factoryLink(string $dest, ?string $mMod, ?string $label): Link
1✔
121
        {
122
                $texy = $this->texy;
1✔
123
                $type = Link::COMMON;
1✔
124

125
                // [ref]
126
                if (strlen($dest) > 1 && $dest[0] === '[' && $dest[1] !== '*') {
1✔
127
                        $type = Link::BRACKET;
1✔
128
                        $dest = substr($dest, 1, -1);
1✔
129
                        $link = $this->getReference($dest);
1✔
130

131
                // [* image *]
132
                } elseif (strlen($dest) > 1 && $dest[0] === '[' && $dest[1] === '*') {
1✔
133
                        $type = Link::IMAGE;
1✔
134
                        $dest = trim(substr($dest, 2, -2));
1✔
135
                        $image = $texy->imageModule->getReference($dest);
1✔
136
                        if ($image) {
1✔
137
                                $link = new Link($image->linkedURL ?? $image->URL);
1✔
138
                        }
139
                }
140

141
                if (empty($link)) {
1✔
142
                        $link = new Link(trim($dest));
1✔
143
                        $this->checkLink($link);
1✔
144
                }
145

146
                if (str_contains((string) $link->URL, '%s')) {
1✔
UNCOV
147
                        $link->URL = str_replace('%s', urlencode($texy->stringToText($label)), $link->URL);
×
148
                }
149

150
                $link->modifier->setProperties($mMod);
1✔
151
                $link->type = $type;
1✔
152
                return $link;
1✔
153
        }
154

155

156
        /**
157
         * Generates <a> element from Link.
158
         */
159
        public function solve(
1✔
160
                ?HandlerInvocation $invocation,
161
                Link $link,
162
                Texy\HtmlElement|string|null $content = null,
163
        ): Texy\HtmlElement|string|null
164
        {
165
                if ($link->URL === null) {
1✔
166
                        return $content;
1✔
167
                }
168

169
                $texy = $this->texy;
1✔
170

171
                $el = new Texy\HtmlElement('a');
1✔
172

173
                if (empty($link->modifier)) {
1✔
UNCOV
174
                        $nofollow = false;
×
175
                } else {
176
                        $nofollow = isset($link->modifier->classes['nofollow']);
1✔
177
                        unset($link->modifier->classes['nofollow']);
1✔
178
                        $el->attrs['href'] = null; // trick - move to front
1✔
179
                        $link->modifier->decorate($texy, $el);
1✔
180
                }
181

182
                if ($link->type === Link::IMAGE) {
1✔
183
                        $el->attrs['href'] = Texy\Helpers::prependRoot($link->URL, $texy->imageModule->root);
1✔
184
                } else {
185
                        $el->attrs['href'] = Texy\Helpers::prependRoot($link->URL, $this->root);
1✔
186

187
                        // rel="nofollow"
188
                        if ($nofollow || ($this->forceNoFollow && str_contains($el->attrs['href'], '//'))) {
1✔
189
                                $el->attrs['rel'] = 'nofollow';
1✔
190
                        }
191
                }
192

193
                if ($content !== null) {
1✔
194
                        $el->add($content);
1✔
195
                }
196

197
                return $el;
1✔
198
        }
199

200

201
        /**
202
         * Checks and corrects URL in Link.
203
         */
204
        public function checkLink(Link $link): void
1✔
205
        {
206
                if ($link->URL === null) {
1✔
207
                        return;
×
208
                }
209

210
                // remove soft hyphens; if not removed by Texy\Texy::process()
211
                $link->URL = str_replace("\u{AD}", '', $link->URL);
1✔
212

213
                if (strncasecmp($link->URL, 'www.', 4) === 0) {
1✔
214
                        // special supported case
215
                        $link->URL = 'http://' . $link->URL;
1✔
216

217
                } elseif (Regexp::match($link->URL, '~' . Patterns::EMAIL . '$~A')) {
1✔
218
                        // email
219
                        $link->URL = 'mailto:' . $link->URL;
1✔
220

221
                } elseif (!$this->texy->checkURL($link->URL, Texy\Texy::FILTER_ANCHOR)) {
1✔
222
                        $link->URL = null;
1✔
223

224
                } else {
225
                        $link->URL = str_replace('&amp;', '&', $link->URL); // replace unwanted &amp;
1✔
226
                }
227
        }
1✔
228
}
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