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

dg / texy / 22283286087

22 Feb 2026 06:58PM UTC coverage: 93.01% (+0.02%) from 92.991%
22283286087

push

github

dg
LinkModule: deprecated label and modifiers in link definitions

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

72 existing lines in 16 files now uncovered.

2089 of 2246 relevant lines covered (93.01%)

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
                /** @var array{string, string, string, ?string, ?string} $matches */
68
                [, $mRef, $mLink, $mLabel, $mMod] = $matches;
1✔
69
                if ($mMod || $mLabel) {
1✔
70
                        trigger_error('Modifiers and label in link definitions are deprecated.', E_USER_DEPRECATED);
1✔
71
                }
72

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

80

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

91

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

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

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

117
                return null;
1✔
118
        }
119

120

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

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

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

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

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

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

156

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

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

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

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

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

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

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

198
                return $el;
1✔
199
        }
200

201

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

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

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

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

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

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