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

dg / texy / 29705051386

19 Jul 2026 09:41PM UTC coverage: 94.641% (+1.2%) from 93.477%
29705051386

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%)

35 existing lines in 7 files now uncovered.

3338 of 3527 relevant lines covered (94.64%)

0.95 hits per line

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

95.28
/src/Texy/Modules/LinkDefinitionModule.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\Compat;
12
use Texy\Helpers;
13
use Texy\Node;
14
use Texy\Nodes;
15
use Texy\Nodes\DocumentNode;
16
use Texy\Nodes\LinkDefinitionNode;
17
use Texy\NodeTraverser;
18
use Texy\ParseContext;
19
use Texy\Patterns;
20
use Texy\Range;
21
use Texy\Syntax;
22
use function in_array, strlen;
23

24

25
/**
26
 * Processes link references and generates link elements.
27
 */
28
final class LinkDefinitionModule extends Texy\Module
29
{
30
        /** @var array<string, LinkDefinitionNode> link definitions */
31
        private array $definitions = [];
32

33
        /** @var array<string, LinkDefinitionNode> user-defined definitions (persist across process() calls) */
34
        private array $userDefinitions = [];
35

36

37
        public function __construct(
1✔
38
                private Texy\Texy $texy,
39
        ) {
40
                $texy->allowed[Syntax::LinkDefinition] = true;
1✔
41
                $texy->allowed[Syntax::LinkReference] = false;
1✔
42
                $texy->addHandler('afterParse', $this->resolveReferences(...));
1✔
43
        }
1✔
44

45

46
        public function beforeParse(string &$text): void
1✔
47
        {
48
                // [ref]: url label .(title)[class]{style}
49
                $this->texy->registerBlockPattern(
1✔
50
                        $this->parseDefinition(...),
1✔
51
                        '~^
52
                                \[
53
                                ( [^\[\]#?*\n]{1,100} )           # reference (1)
54
                                ] : \ ++
55
                                ( \S{1,1000} )                    # URL (2)
56
                                ( [ \t] .{1,1000} )?              # optional label (3)
57
                                ' . Patterns::Modifier . '?       # modifier (4)
1✔
58
                                \s*
59
                        $~mUx',
60
                        Syntax::LinkDefinition,
1✔
61
                );
62

63
                // [reference] - bare reference link (opt-in); the raw target is kept in
64
                // LinkNode::$ref for consumer resolution passes (wiki links etc.)
65
                $this->texy->registerLinePattern(
1✔
66
                        $this->parseReference(...),
1✔
67
                        '~
1✔
68
                                (?<! [.\#\[] )        # exclude modifier .[class], PHP attribute \#[Attr] and nested [
69
                                \[
70
                                ( [^\[\]*|\n]++ )     # reference name (1); * would be an image, | a labeled form
71
                                ]
72
                        ~Ux',
73
                        Syntax::LinkReference,
1✔
74
                );
75
        }
1✔
76

77

78
        /**
79
         * Parses bare [ref]
80
         * @param  array<?string>  $matches
81
         * @param  array<?int>  $offsets
82
         */
83
        public function parseReference(ParseContext $context, array $matches, array $offsets): Nodes\LinkNode
1✔
84
        {
85
                /** @var array{string, string} $matches */
86
                [, $mRef] = $matches;
1✔
87
                $range = new Range($offsets[0], strlen($matches[0]));
1✔
88
                return new Nodes\LinkNode(
1✔
89
                        url: '[' . $mRef . ']', // bracket form is resolved against definitions like "text":[ref]
1✔
90
                        content: new Nodes\ContentNode([new Nodes\TextNode(Helpers::decodeEntities($mRef), $range)]),
1✔
91
                        range: $range,
92
                        ref: $mRef,
93
                );
94
        }
95

96

97
        /**
98
         * Parses [ref]: url
99
         * @param  array<?string>  $matches
100
         * @param  array<?int>  $offsets
101
         */
102
        public function parseDefinition(ParseContext $context, array $matches, array $offsets): LinkDefinitionNode
1✔
103
        {
104
                /** @var array{string, string, string, ?string, ?string} $matches */
105
                [, $mRef, $mLink, $mLabel, $mMod] = $matches;
1✔
106
                if ($mMod || $mLabel) {
1✔
UNCOV
107
                        trigger_error('Modifiers and label in link definitions are deprecated.', E_USER_DEPRECATED);
×
108
                }
109
                return new LinkDefinitionNode(
1✔
110
                        $mRef,
1✔
111
                        $mLink,
112
                        new Range($offsets[0], strlen($matches[0])),
1✔
113
                );
114
        }
115

116

117
        /**
118
         * Adds a user-defined link definition (persists across process() calls).
119
         */
120
        public function addDefinition(string $name, string $url): void
1✔
121
        {
122
                $this->userDefinitions[Helpers::toLower($name)] = new LinkDefinitionNode($name, $url);
1✔
123
        }
1✔
124

125

126
        /**
127
         * Resolve link references in the document.
128
         * Called via afterParse handler (after ImageModule).
129
         */
130
        public function resolveReferences(DocumentNode $doc): void
1✔
131
        {
132
                // Start with user-defined definitions
133
                $this->definitions = $this->userDefinitions;
1✔
134
                $traverser = new NodeTraverser;
1✔
135

136
                // Pass 1: Collect document definitions (overwrites user-defined)
137
                $traverser->traverse($doc, function (Node $node): ?int {
1✔
138
                        if ($node instanceof LinkDefinitionNode) {
1✔
139
                                $this->definitions[Helpers::toLower($node->identifier)] = $node;
1✔
140
                                return NodeTraverser::DontTraverseChildren;
1✔
141
                        }
142
                        return null;
1✔
143
                });
1✔
144

145
                // Pass 2: Resolve references in LinkNode.url
146
                $traverser->traverse($doc, function (Node $node): ?Node {
1✔
147
                        if ($node instanceof Nodes\LinkNode && $node->url !== null) {
1✔
148
                                $this->resolveLinkNodeUrl($node);
1✔
149
                        }
150
                        return null;
1✔
151
                });
1✔
152
        }
1✔
153

154

155
        /**
156
         * Resolve URL in LinkNode that might be [ref] or [*img*].
157
         */
158
        private function resolveLinkNodeUrl(Nodes\LinkNode $node): void
1✔
159
        {
160
                if ($node->url === null) {
1✔
UNCOV
161
                        return;
×
162
                }
163

164
                $len = strlen($node->url);
1✔
165

166
                // [*img*], [*img <], [*img >] format - resolve to image URL
167
                if ($len > 4 && $node->url[0] === '[' && $node->url[1] === '*'
1✔
168
                        && $node->url[$len - 1] === ']'
1✔
169
                        && in_array($node->url[$len - 2], ['*', '<', '>'], true)) {
1✔
170
                        $imgRef = trim(substr($node->url, 2, -2));
1✔
171
                        $node->ref = $imgRef; // keep the written reference name for analyzers
1✔
172
                        $imgDef = $this->texy->imageModule->getDefinition($imgRef);
1✔
173
                        if ($imgDef !== null && $imgDef->url !== null) {
1✔
174
                                $node->url = $imgDef->url;
1✔
175
                        } else {
176
                                // Image reference not found - use content as URL
177
                                $node->url = $imgRef;
1✔
178
                        }
179
                        $node->isImageLink = true;
1✔
180
                        return;
1✔
181
                }
182

183
                // [ref] format
184
                if ($len > 2 && $node->url[0] === '[' && $node->url[$len - 1] === ']') {
1✔
185
                        $refName = substr($node->url, 1, -1);
1✔
186
                        $node->ref = $refName; // keep the written reference name for analyzers
1✔
187
                        $def = $this->resolveDefinition($refName);
1✔
188
                        if ($def !== null) {
1✔
189
                                $node->url = $def->url;
1✔
190
                                // Check if resolved URL is email
191
                                $this->convertEmailUrl($node);
1✔
192
                                return;
1✔
193
                        }
194
                        // Reference not found - use inner content as URL
195
                        $node->url = $refName;
1✔
196
                        // Check if it's an email
197
                        $this->convertEmailUrl($node);
1✔
198
                        return;
1✔
199
                }
200

201
                $this->convertEmailUrl($node);
1✔
202
        }
1✔
203

204

205
        /**
206
         * Convert e-mail URLs to mailto: scheme. Requires a full e-mail address
207
         * (Patterns::Email) so that wiki-style targets containing @ are left alone.
208
         */
209
        private function convertEmailUrl(Nodes\LinkNode $node): void
1✔
210
        {
211
                if ($node->url !== null
1✔
212
                        && !str_contains($node->url, '/')
1✔
213
                        && !preg_match('~^[a-z][a-z0-9+.-]*:~i', $node->url)
1✔
214
                        && Texy\Regexp::match($node->url, '~' . Patterns::Email . '(\?\S*)?$~A')) { // optional ?subject=... query
1✔
215
                        $node->url = 'mailto:' . $node->url;
1✔
216
                }
217
        }
1✔
218

219

220
        /**
221
         * Find definition by identifier, supports #fragment and ?query.
222
         */
223
        private function resolveDefinition(string $identifier): ?LinkDefinitionNode
1✔
224
        {
225
                $key = Helpers::toLower($identifier);
1✔
226

227
                if (isset($this->definitions[$key])) {
1✔
228
                        return $this->definitions[$key];
1✔
229
                }
230

231
                // Support #fragment and ?query
232
                $hashPos = strpos($key, '#');
1✔
233
                $queryPos = strpos($key, '?');
1✔
234

235
                // Find the earliest delimiter
236
                $pos = null;
1✔
237
                if ($hashPos !== false && $queryPos !== false) {
1✔
UNCOV
238
                        $pos = min($hashPos, $queryPos);
×
239
                } elseif ($hashPos !== false) {
1✔
240
                        $pos = $hashPos;
1✔
241
                } elseif ($queryPos !== false) {
1✔
242
                        $pos = $queryPos;
1✔
243
                }
244

245
                if ($pos !== null) {
1✔
246
                        $baseKey = substr($key, 0, $pos);
1✔
247
                        if (isset($this->definitions[$baseKey])) {
1✔
248
                                $def = clone $this->definitions[$baseKey];
1✔
249
                                $def->url .= substr($identifier, $pos);
1✔
250
                                return $def;
1✔
251
                        }
252
                }
253

254
                return null;
1✔
255
        }
256

257

258
        /**
259
         * @deprecated use $texy->htmlOutput->linkRoot etc. instead
260
         */
261
        public function &__get(string $name): mixed
262
        {
UNCOV
263
                return Compat\Legacy::ref($this->texy, Compat\Legacy::OfModule['linkModule'], '$texy->linkModule', $name, 'read');
×
264
        }
265

266

267
        /**
268
         * @deprecated use $texy->htmlOutput->linkRoot etc. instead
269
         */
270
        public function __set(string $name, mixed $value): void
1✔
271
        {
272
                Compat\Legacy::set($this->texy, Compat\Legacy::OfModule['linkModule'], '$texy->linkModule', $name, $value);
1✔
273
        }
1✔
274

275

276
        public function __isset(string $name): bool
277
        {
UNCOV
278
                return Compat\Legacy::isSet($this->texy, Compat\Legacy::OfModule['linkModule'], $name);
×
279
        }
280
}
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