• 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

90.91
/src/Texy/Modules/ImageModule.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\Helpers;
12
use Texy\Image;
13
use Texy\Patterns;
14
use function count, explode, getimagesize, is_file, is_int, min, round, rtrim, str_contains, trim;
15

16

17
/**
18
 * Processes image syntax and detects image dimensions.
19
 */
20
final class ImageModule extends Texy\Module
21
{
22
        /** root of relative images (http) */
23
        public ?string $root = 'images/';
24

25
        /** physical location of images on server */
26
        public ?string $fileRoot = null;
27

28
        /** left-floated images CSS class */
29
        public ?string $leftClass = null;
30

31
        /** right-floated images CSS class */
32
        public ?string $rightClass = null;
33

34
        /** @var array<string, Image> image references */
35
        private array $references = [];
36

37

38
        public function __construct(
1✔
39
                private Texy\Texy $texy,
40
        ) {
41
                $texy->allowed['image/definition'] = true;
1✔
42
                $texy->addHandler('image', $this->solve(...));
1✔
43
        }
1✔
44

45

46
        public function beforeParse(string &$text): void
1✔
47
        {
48
                // [*image*]:LINK
49
                $this->texy->registerLinePattern(
1✔
50
                        $this->parseImage(...),
1✔
51
                        '~
52
                                \[\* \ *+                         # opening bracket with asterisk
53
                                ([^\n' . Patterns::MARK . ']{1,1000}) # URLs (1)
1✔
54
                                ' . Patterns::MODIFIER . '?       # modifier (2)
1✔
55
                                \ *+
56
                                ( \* | (?<! < ) > | < )           # alignment (3)
57
                                ]
58
                                (?:
59
                                        :(' . Patterns::LINK_URL . ' | : ) # link or just colon (4)
1✔
60
                                )??
61
                        ~U',
62
                        'image',
1✔
63
                );
64

65
                if (!empty($this->texy->allowed['image/definition'])) {
1✔
66
                        // [*image*]: urls .(title)[class]{style}
67
                        $text = Texy\Regexp::replace(
1✔
68
                                $text,
1✔
69
                                '~^
70
                                        \[\*                              # opening [*
71
                                        ( [^\n]{1,100} )                  # reference (1)
72
                                        \*]                               # closing *]
73
                                        : [ \t]+
74
                                        (.{1,1000})                       # URL (2)
75
                                        [ \t]*
76
                                        ' . Patterns::MODIFIER . '?       # modifier (3)
1✔
77
                                        \s*
78
                                $~mU',
79
                                $this->parseDefinition(...),
1✔
80
                        );
81
                }
82
        }
1✔
83

84

85
        /**
86
         * Parses [*image*]: urls .(title)[class]{style}
87
         * @param  array<?string>  $matches
88
         */
89
        private function parseDefinition(array $matches): string
1✔
90
        {
91
                /** @var array{string, string, string, ?string} $matches */
92
                [, $mRef, $mURLs, $mMod] = $matches;
1✔
93
                // [1] => [* (reference) *]
94
                // [2] => urls
95
                // [3] => .(title)[class]{style}<>
96

97
                $image = $this->factoryImage($mURLs, $mMod, tryRef: false);
1✔
98
                $image->name = Helpers::toLower($mRef);
1✔
99
                $this->references[$image->name] = $image;
1✔
100
                return '';
1✔
101
        }
102

103

104
        /**
105
         * Parses [* small.jpg 80x13 .(alternative text)[class]{style}>]:LINK
106
         * @param  array<?string>  $matches
107
         */
108
        public function parseImage(Texy\InlineParser $parser, array $matches): Texy\HtmlElement|string|null
1✔
109
        {
110
                /** @var array{string, string, ?string, string, ?string} $matches */
111
                [, $mURLs, $mMod, $mAlign, $mLink] = $matches;
1✔
112
                // [1] => URLs
113
                // [2] => .(title)[class]{style}<>
114
                // [3] => * < >
115
                // [4] => url | [ref] | [*image*]
116

117
                $image = $this->factoryImage($mURLs, $mMod . $mAlign);
1✔
118

119
                if ($mLink) {
1✔
120
                        if ($mLink === ':') {
1✔
121
                                $link = new Texy\Link($image->linkedURL ?? $image->URL);
1✔
122
                                $link->raw = ':';
1✔
123
                                $link->type = $link::IMAGE;
1✔
124
                        } else {
125
                                $link = $this->texy->linkModule->factoryLink($mLink, null, null);
1✔
126
                        }
127
                } else {
128
                        $link = null;
1✔
129
                }
130

131
                return $this->texy->invokeAroundHandlers('image', $parser, [$image, $link]);
1✔
132
        }
133

134

135
        /**
136
         * Adds a user-defined image definition (persists across process() calls).
137
         */
138
        public function addDefinition(
1✔
139
                string $name,
140
                string $url,
141
                ?int $width = null,
142
                ?int $height = null,
143
                ?string $alt = null,
144
        ): void
145
        {
146
                $image = new Image;
1✔
147
                $image->URL = $url;
1✔
148
                $image->width = $width;
1✔
149
                $image->height = $height;
1✔
150
                if ($alt !== null) {
1✔
151
                        $image->modifier->title = $alt;
1✔
152
                }
153
                $image->name = Helpers::toLower($name);
1✔
154
                $this->references[$image->name] = $image;
1✔
155
        }
1✔
156

157

158
        /**
159
         * Returns named reference.
160
         */
161
        public function getReference(string $name): ?Image
1✔
162
        {
163
                $name = Helpers::toLower($name);
1✔
164
                if (isset($this->references[$name])) {
1✔
165
                        return clone $this->references[$name];
1✔
166
                }
167

168
                return null;
1✔
169
        }
170

171

172
        /**
173
         * Parses image's syntax. Input: small.jpg 80x13
174
         */
175
        public function factoryImage(string $content, ?string $mod, bool $tryRef = true): Image
1✔
176
        {
177
                $image = $tryRef ? $this->getReference(trim($content)) : null;
1✔
178

179
                if (!$image) {
1✔
180
                        $texy = $this->texy;
1✔
181
                        $parts = explode('|', $content);
1✔
182
                        $image = new Image;
1✔
183

184
                        if (count($parts) > 1) {
1✔
185
                                trigger_error("Image syntax with '|' or '||' inside brackets is deprecated. Use [* image *]:url for linked images.", E_USER_DEPRECATED);
1✔
186
                        }
187

188
                        // dimensions
189
                        $matches = null;
1✔
190
                        if ($matches = Texy\Regexp::match($parts[0], '~^(.*)\ (\d+|\?)\ *([Xx])\ *(\d+|\?)\ *$~U')) {
1✔
191
                                /** @var array{string, string, string, string, string} $matches */
192
                                $image->URL = trim($matches[1]);
1✔
193
                                $image->asMax = $matches[3] === 'X';
1✔
194
                                $image->width = $matches[2] === '?' ? null : (int) $matches[2];
1✔
195
                                $image->height = $matches[4] === '?' ? null : (int) $matches[4];
1✔
196
                        } else {
197
                                $image->URL = trim($parts[0]);
1✔
198
                        }
199

200
                        if (!$texy->checkURL($image->URL, $texy::FILTER_IMAGE)) {
1✔
UNCOV
201
                                $image->URL = null;
×
202
                        }
203
                }
204

205
                $image->modifier->setProperties($mod);
1✔
206
                return $image;
1✔
207
        }
208

209

210
        /**
211
         * Finish invocation.
212
         */
213
        public function solve(
1✔
214
                ?Texy\HandlerInvocation $invocation,
215
                Image $image,
216
                ?Texy\Link $link = null,
217
        ): Texy\HtmlElement|string|null
218
        {
219
                if ($image->URL === null) {
1✔
UNCOV
220
                        return null;
×
221
                }
222

223
                $texy = $this->texy;
1✔
224

225
                $mod = $image->modifier;
1✔
226
                $alt = $mod->title;
1✔
227
                $mod->title = null;
1✔
228
                $hAlign = $mod->hAlign;
1✔
229
                $mod->hAlign = null;
1✔
230

231
                $el = new Texy\HtmlElement('img');
1✔
232
                $el->attrs['src'] = null; // trick - move to front
1✔
233
                $mod->decorate($texy, $el);
1✔
234
                $el->attrs['src'] = Helpers::prependRoot($image->URL, $this->root);
1✔
235
                if (!isset($el->attrs['alt'])) {
1✔
236
                        $el->attrs['alt'] = $alt === null ? '' : $texy->typographyModule->postLine($alt);
1✔
237
                }
238

239
                if ($hAlign) {
1✔
240
                        $var = $hAlign . 'Class'; // leftClass, rightClass
1✔
241
                        if (!empty($this->$var)) {
1✔
242
                                $el->attrs['class'] = (array) ($el->attrs['class'] ?? []);
1✔
243
                                $el->attrs['class'][] = $this->$var;
1✔
244

245
                        } elseif (empty($texy->alignClasses[$hAlign])) {
1✔
246
                                $el->attrs['style'] = (array) ($el->attrs['style'] ?? []);
1✔
247
                                $el->attrs['style']['float'] = $hAlign;
1✔
248

249
                        } else {
UNCOV
250
                                $el->attrs['class'] = (array) ($el->attrs['class'] ?? []);
×
UNCOV
251
                                $el->attrs['class'][] = $texy->alignClasses[$hAlign];
×
252
                        }
253
                }
254

255
                if (!is_int($image->width) || !is_int($image->height) || $image->asMax) {
1✔
256
                        $this->detectDimensions($image);
1✔
257
                }
258

259
                $el->attrs['width'] = $image->width;
1✔
260
                $el->attrs['height'] = $image->height;
1✔
261

262
                if ($link) {
1✔
263
                        return $texy->linkModule->solve(null, $link, $el);
1✔
264
                }
265

266
                return $el;
1✔
267
        }
268

269

270
        private function detectDimensions(Image $image): void
1✔
271
        {
272
                // absolute URL & security check for double dot
273
                if ($image->URL === null || !Helpers::isRelative($image->URL) || str_contains($image->URL, '..')) {
1✔
274
                        return;
1✔
275
                }
276

277
                $file = rtrim((string) $this->fileRoot, '/\\') . '/' . $image->URL;
1✔
278
                if (!@is_file($file) || !($size = @getimagesize($file))) { // intentionally @
1✔
279
                        return;
1✔
280
                }
281

282
                if ($image->asMax) {
1✔
UNCOV
283
                        $ratio = 1;
×
UNCOV
284
                        if (is_int($image->width)) {
×
285
                                $ratio = min($ratio, $image->width / $size[0]);
×
286
                        }
287

UNCOV
288
                        if (is_int($image->height)) {
×
289
                                $ratio = min($ratio, $image->height / $size[1]);
×
290
                        }
291

UNCOV
292
                        $image->width = (int) round($ratio * $size[0]);
×
UNCOV
293
                        $image->height = (int) round($ratio * $size[1]);
×
294

295
                } elseif (is_int($image->width)) {
1✔
296
                        $image->height = (int) round($size[1] / $size[0] * $image->width);
1✔
297

298
                } elseif (is_int($image->height)) {
1✔
299
                        $image->width = (int) round($size[0] / $size[1] * $image->height);
1✔
300

301
                } else {
302
                        $image->width = $size[0];
1✔
303
                        $image->height = $size[1];
1✔
304
                }
305
        }
1✔
306
}
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