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

dg / texy / 22262497275

21 Feb 2026 07:01PM UTC coverage: 93.057% (+0.7%) from 92.367%
22262497275

push

github

dg
added CLAUDE.md

2426 of 2607 relevant lines covered (93.06%)

0.93 hits per line

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

92.44
/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 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
        /** @deprecated */
26
        public ?string $linkedRoot = 'images/';
27

28
        /** physical location of images on server */
29
        public ?string $fileRoot = null;
30

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

34
        /** right-floated images CSS class */
35
        public ?string $rightClass = null;
36

37
        /** @deprecated */
38
        public ?string $defaultAlt = '';
39

40
        /** @var array<string, Image> image references */
41
        private array $references = [];
42

43

44
        public function __construct(Texy\Texy $texy)
1✔
45
        {
46
                $this->texy = $texy;
1✔
47

48
                $texy->allowed['image/definition'] = true;
1✔
49
                $texy->addHandler('image', $this->solve(...));
1✔
50
                $texy->addHandler('beforeParse', $this->beforeParse(...));
1✔
51

52
                // [*image*]:LINK
53
                $texy->registerLinePattern(
1✔
54
                        $this->patternImage(...),
1✔
55
                        '#\[\*\ *+([^\n' . Patterns::MARK . ']{1,1000})' . Patterns::MODIFIER . '?\ *+(\*|(?<!<)>|<)\]' // [* urls .(title)[class]{style} >]
56
                        . '(?::(' . Patterns::LINK_URL . '|:))??()#Uu',
1✔
57
                        'image',
1✔
58
                );
59
        }
1✔
60

61

62
        /**
63
         * Text pre-processing.
64
         */
65
        private function beforeParse(Texy\Texy $texy, string &$text): void
1✔
66
        {
67
                if (!empty($texy->allowed['image/definition'])) {
1✔
68
                        // [*image*]: urls .(title)[class]{style}
69
                        $text = Texy\Regexp::replace(
1✔
70
                                $text,
1✔
71
                                '#^\[\*([^\n]{1,100})\*\]:[\ \t]+(.{1,1000})[\ \t]*' . Patterns::MODIFIER . '?\s*()$#mUu',
1✔
72
                                $this->patternReferenceDef(...),
1✔
73
                        );
74
                }
75
        }
1✔
76

77

78
        /**
79
         * Callback for: [*image*]: urls .(title)[class]{style}.
80
         * @param  string[]  $matches
81
         */
82
        private function patternReferenceDef(array $matches): string
1✔
83
        {
84
                [, $mRef, $mURLs, $mMod] = $matches;
1✔
85
                // [1] => [* (reference) *]
86
                // [2] => urls
87
                // [3] => .(title)[class]{style}<>
88

89
                $image = $this->factoryImage($mURLs, $mMod, tryRef: false);
1✔
90
                $this->addReference($mRef, $image);
1✔
91
                return '';
1✔
92
        }
93

94

95
        /**
96
         * Callback for [* small.jpg 80x13 || big.jpg .(alternative text)[class]{style}>]:LINK.
97
         * @param  string[]  $matches
98
         */
99
        public function patternImage(Texy\LineParser $parser, array $matches): Texy\HtmlElement|string|null
1✔
100
        {
101
                [, $mURLs, $mMod, $mAlign, $mLink] = $matches;
1✔
102
                // [1] => URLs
103
                // [2] => .(title)[class]{style}<>
104
                // [3] => * < >
105
                // [4] => url | [ref] | [*image*]
106

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

109
                if ($mLink) {
1✔
110
                        if ($mLink === ':') {
1✔
111
                                $link = new Texy\Link($image->linkedURL ?? $image->URL);
1✔
112
                                $link->raw = ':';
1✔
113
                                $link->type = $link::IMAGE;
1✔
114
                        } else {
115
                                $link = $this->texy->linkModule->factoryLink($mLink, null, null);
1✔
116
                        }
117
                } else {
118
                        $link = null;
1✔
119
                }
120

121
                return $this->texy->invokeAroundHandlers('image', $parser, [$image, $link]);
1✔
122
        }
123

124

125
        /**
126
         * Adds new named reference to image.
127
         */
128
        public function addReference(string $name, Image $image): void
1✔
129
        {
130
                $image->name = Helpers::toLower($name);
1✔
131
                $this->references[$image->name] = $image;
1✔
132
        }
1✔
133

134

135
        /**
136
         * Returns named reference.
137
         */
138
        public function getReference(string $name): ?Image
1✔
139
        {
140
                $name = Helpers::toLower($name);
1✔
141
                if (isset($this->references[$name])) {
1✔
142
                        return clone $this->references[$name];
1✔
143
                }
144

145
                return null;
1✔
146
        }
147

148

149
        /**
150
         * Parses image's syntax. Input: small.jpg 80x13 || linked.jpg
151
         */
152
        public function factoryImage(string $content, string $mod, bool $tryRef = true): Image
1✔
153
        {
154
                $image = $tryRef ? $this->getReference(trim($content)) : null;
1✔
155

156
                if (!$image) {
1✔
157
                        $texy = $this->texy;
1✔
158
                        $content = explode('|', $content);
1✔
159
                        $image = new Image;
1✔
160

161
                        // dimensions
162
                        $matches = null;
1✔
163
                        if ($matches = Texy\Regexp::match($content[0], '#^(.*) (\d+|\?) *(X|x) *(\d+|\?) *()$#U')) {
1✔
164
                                $image->URL = trim($matches[1]);
1✔
165
                                $image->asMax = $matches[3] === 'X';
1✔
166
                                $image->width = $matches[2] === '?' ? null : (int) $matches[2];
1✔
167
                                $image->height = $matches[4] === '?' ? null : (int) $matches[4];
1✔
168
                        } else {
169
                                $image->URL = trim($content[0]);
1✔
170
                        }
171

172
                        if (!$texy->checkURL($image->URL, $texy::FILTER_IMAGE)) {
1✔
173
                                $image->URL = null;
×
174
                        }
175

176
                        // linked image
177
                        if (isset($content[2])) {
1✔
178
                                $tmp = trim($content[2]);
1✔
179
                                if ($tmp !== '' && $texy->checkURL($tmp, $texy::FILTER_ANCHOR)) {
1✔
180
                                        $image->linkedURL = $tmp;
1✔
181
                                }
182
                        }
183
                }
184

185
                $image->modifier->setProperties($mod);
1✔
186
                return $image;
1✔
187
        }
188

189

190
        /**
191
         * Finish invocation.
192
         */
193
        public function solve(
1✔
194
                ?Texy\HandlerInvocation $invocation,
195
                Image $image,
196
                ?Texy\Link $link = null,
197
        ): Texy\HtmlElement|string|null
198
        {
199
                if ($image->URL === null) {
1✔
200
                        return null;
×
201
                }
202

203
                $texy = $this->texy;
1✔
204

205
                $mod = $image->modifier;
1✔
206
                $alt = $mod->title;
1✔
207
                $mod->title = null;
1✔
208
                $hAlign = $mod->hAlign;
1✔
209
                $mod->hAlign = null;
1✔
210

211
                $el = new Texy\HtmlElement('img');
1✔
212
                $el->attrs['src'] = null; // trick - move to front
1✔
213
                $mod->decorate($texy, $el);
1✔
214
                $el->attrs['src'] = Helpers::prependRoot($image->URL, $this->root);
1✔
215
                if (!isset($el->attrs['alt'])) {
1✔
216
                        $el->attrs['alt'] = $alt === null
1✔
217
                                ? $this->defaultAlt
1✔
218
                                : $texy->typographyModule->postLine($alt);
1✔
219
                }
220

221
                if ($hAlign) {
1✔
222
                        $var = $hAlign . 'Class'; // leftClass, rightClass
1✔
223
                        if (!empty($this->$var)) {
1✔
224
                                $el->attrs['class'] = (array) ($el->attrs['class'] ?? []);
1✔
225
                                $el->attrs['class'][] = $this->$var;
1✔
226

227
                        } elseif (empty($texy->alignClasses[$hAlign])) {
1✔
228
                                $el->attrs['style'] = (array) ($el->attrs['style'] ?? []);
1✔
229
                                $el->attrs['style']['float'] = $hAlign;
1✔
230

231
                        } else {
232
                                $el->attrs['class'] = (array) ($el->attrs['class'] ?? []);
1✔
233
                                $el->attrs['class'][] = $texy->alignClasses[$hAlign];
1✔
234
                        }
235
                }
236

237
                if (!is_int($image->width) || !is_int($image->height) || $image->asMax) {
1✔
238
                        $this->detectDimensions($image);
1✔
239
                }
240

241
                $el->attrs['width'] = $image->width;
1✔
242
                $el->attrs['height'] = $image->height;
1✔
243

244
                $texy->summary['images'][] = (string) $el->attrs['src'];
1✔
245

246
                if ($link) {
1✔
247
                        return $texy->linkModule->solve(null, $link, $el);
1✔
248
                }
249

250
                return $el;
1✔
251
        }
252

253

254
        private function detectDimensions(Image $image): void
1✔
255
        {
256
                // absolute URL & security check for double dot
257
                if ($image->URL === null || !Helpers::isRelative($image->URL) || str_contains($image->URL, '..')) {
1✔
258
                        return;
1✔
259
                }
260

261
                $file = rtrim((string) $this->fileRoot, '/\\') . '/' . $image->URL;
1✔
262
                if (!@is_file($file) || !($size = @getimagesize($file))) { // intentionally @
1✔
263
                        return;
1✔
264
                }
265

266
                if ($image->asMax) {
1✔
267
                        $ratio = 1;
×
268
                        if (is_int($image->width)) {
×
269
                                $ratio = min($ratio, $image->width / $size[0]);
×
270
                        }
271

272
                        if (is_int($image->height)) {
×
273
                                $ratio = min($ratio, $image->height / $size[1]);
×
274
                        }
275

276
                        $image->width = (int) round($ratio * $size[0]);
×
277
                        $image->height = (int) round($ratio * $size[1]);
×
278

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

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

285
                } else {
286
                        $image->width = $size[0];
1✔
287
                        $image->height = $size[1];
1✔
288
                }
289
        }
1✔
290
}
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