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

dg / texy / 29698730446

19 Jul 2026 06:14PM UTC coverage: 93.477% (+0.4%) from 93.08%
29698730446

push

github

dg
PHPStan ignore paths follow the output layer split

3296 of 3526 relevant lines covered (93.48%)

0.93 hits per line

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

88.14
/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\Modifier;
13
use Texy\Node;
14
use Texy\Nodes\ContentNode;
15
use Texy\Nodes\DocumentNode;
16
use Texy\Nodes\ImageDefinitionNode;
17
use Texy\Nodes\ImageNode;
18
use Texy\Nodes\LinkNode;
19
use Texy\NodeTraverser;
20
use Texy\ParseContext;
21
use Texy\Patterns;
22
use Texy\Range;
23
use Texy\Regexp;
24
use Texy\Syntax;
25
use function count, strlen;
26

27

28
/**
29
 * Processes image syntax and detects image dimensions.
30
 */
31
final class ImageModule extends Texy\Module
32
{
33
        /** @deprecated legacy property mapping */
34
        private const LegacyProperties = [
35
                'root' => 'imageRoot',
36
                'fileRoot' => 'imageFileRoot',
37
                'leftClass' => 'imageLeftClass',
38
                'rightClass' => 'imageRightClass',
39
        ];
40

41
        /** @var array<string, ImageDefinitionNode> collected image definitions */
42
        private array $definitions = [];
43

44
        /** @var array<string, ImageDefinitionNode> user-defined definitions (persist across process() calls) */
45
        private array $userDefinitions = [];
46

47

48
        public function __construct(
1✔
49
                private Texy\Texy $texy,
50
        ) {
51
                $texy->allowed[Syntax::ImageDefinition] = true;
1✔
52
                $texy->addHandler('afterParse', $this->resolveReferences(...));
1✔
53
        }
1✔
54

55

56
        public function beforeParse(string &$text): void
1✔
57
        {
58
                // [*image*]:LINK
59
                $this->texy->registerLinePattern(
1✔
60
                        $this->parseImage(...),
1✔
61
                        '~
62
                                \[\* \ *+                         # opening bracket with asterisk
63
                                ([^\n]{1,1000})                   # URLs (1)
64
                                ' . Patterns::Modifier . '?       # modifier (2)
1✔
65
                                \ *+
66
                                ( \* | (?<! < ) > | < )           # alignment (3)
67
                                ]
68
                                (?:
69
                                        :(' . Patterns::LinkUrl . ' | : ) # link or just colon (4)
1✔
70
                                )??
71
                        ~Ux',
72
                        Syntax::Image,
1✔
73
                );
74

75
                // [*ref*]: url .(title)[class]{style}
76
                $this->texy->registerBlockPattern(
1✔
77
                        $this->parseDefinition(...),
1✔
78
                        '~^
79
                                \[\*                              # opening [*
80
                                ( [^\n]{1,100} )                  # reference (1)
81
                                \*]                               # closing *]
82
                                : [ \t]+
83
                                (.{1,1000})                       # URL (2)
84
                                [ \t]*
85
                                ' . Patterns::Modifier . '?       # modifier (3)
1✔
86
                                \s*
87
                        $~mUx',
88
                        Syntax::ImageDefinition,
1✔
89
                );
90
        }
1✔
91

92

93
        /**
94
         * Parses [*image*]: urls .(title)[class]{style}
95
         * @param  array<?string>  $matches
96
         * @param  array<?int>  $offsets
97
         */
98
        public function parseDefinition(ParseContext $context, array $matches, array $offsets): ImageDefinitionNode
1✔
99
        {
100
                /** @var array{string, string, string, ?string} $matches */
101
                [, $mRef, $mURLs, $mMod] = $matches;
1✔
102
                $parsed = $this->parseImageContent($mURLs);
1✔
103
                $modifier = Modifier::parse($mMod, $offsets[3] ?? null);
1✔
104

105
                return new ImageDefinitionNode(
1✔
106
                        trim($mRef),
1✔
107
                        $parsed['url'],
1✔
108
                        $parsed['width'],
1✔
109
                        $parsed['height'],
1✔
110
                        $modifier,
111
                        new Range($offsets[0], strlen($matches[0])),
1✔
112
                );
113
        }
114

115

116
        /**
117
         * Parses [* small.jpg 80x13 .(alternative text)[class]{style}>]:LINK
118
         * @param  array<?string>  $matches
119
         * @param  array<?int>  $offsets
120
         */
121
        public function parseImage(ParseContext $context, array $matches, array $offsets): ImageNode|LinkNode
1✔
122
        {
123
                /** @var array{string, string, ?string, string, ?string} $matches */
124
                [, $mURLs, $mMod, $mAlign, $mLink] = $matches;
1✔
125
                $parsed = $this->parseImageContent($mURLs);
1✔
126
                $modifier = Modifier::parse($mMod . $mAlign);
1✔
127
                $range = new Range($offsets[0], strlen($matches[0]));
1✔
128

129
                $imageNode = new ImageNode(
1✔
130
                        $parsed['url'],
1✔
131
                        $parsed['width'],
1✔
132
                        $parsed['height'],
1✔
133
                        $modifier,
134
                        $range,
135
                );
136

137
                // If image has link, wrap in LinkNode
138
                if ($mLink) {
1✔
139
                        if ($mLink === ':') {
1✔
140
                                // Link to image itself - use imageModule.root
141
                                $linkUrl = $parsed['linkedUrl'] ?? $parsed['url'];
1✔
142
                                $isImageLink = true;
1✔
143
                        } else {
144
                                // Direct URL or reference like [ref] or [*img*]
145
                                $linkUrl = $mLink;
1✔
146
                                // Check if it's an image reference [*...*] → use imageModule.root
147
                                $len = strlen($mLink);
1✔
148
                                $isImageLink = $len > 4 && $mLink[0] === '[' && $mLink[1] === '*'
1✔
149
                                        && $mLink[$len - 1] === ']' && $mLink[$len - 2] === '*';
1✔
150
                        }
151

152
                        return new LinkNode(
1✔
153
                                url: $linkUrl,
1✔
154
                                content: new ContentNode([$imageNode]),
1✔
155
                                range: $range,
156
                                isImageLink: $isImageLink,
157
                        );
158
                }
159

160
                return $imageNode;
1✔
161
        }
162

163

164
        /**
165
         * Parse image content: "image.jpg 100x200"
166
         * @return array{url: ?string, width: ?int, height: ?int, linkedUrl: ?string}
167
         */
168
        public function parseImageContent(string $content): array
1✔
169
        {
170
                $parts = explode('|', $content);
1✔
171
                $main = trim($parts[0]);
1✔
172

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

177
                $url = $main;
1✔
178
                $width = $height = null;
1✔
179

180
                // Parse dimensions: "image.jpg 100x200" or "image.jpg 100X200" (asMax)
181
                if ($m = Regexp::match($main, '~^(.*)\ (\d+|\?)\ *[xX]\ *(\d+|\?)\ *$~U')) {
1✔
182
                        /** @var array{string, string, string, string} $m */
183
                        $url = trim($m[1]);
1✔
184
                        $width = $m[2] === '?' ? null : (int) $m[2];
1✔
185
                        $height = $m[3] === '?' ? null : (int) $m[3];
1✔
186
                }
187

188
                // Check URL
189
                if (!$this->texy->urlPolicy->isImageAllowed($url)) {
1✔
190
                        $url = null;
×
191
                }
192

193
                return [
194
                        'url' => $url,
1✔
195
                        'width' => $width,
1✔
196
                        'height' => $height,
1✔
197
                        'linkedUrl' => null,
198
                ];
199
        }
200

201

202
        /**
203
         * Adds a user-defined image definition (persists across process() calls).
204
         */
205
        public function addDefinition(
1✔
206
                string $name,
207
                string $url,
208
                ?int $width = null,
209
                ?int $height = null,
210
                ?string $alt = null,
211
        ): void
212
        {
213
                $modifier = $alt !== null ? Modifier::parse('(' . $alt . ')') : null;
1✔
214
                $this->userDefinitions[Helpers::toLower($name)] = new ImageDefinitionNode($name, $url, $width, $height, $modifier);
1✔
215
        }
1✔
216

217

218
        /**
219
         * Resolve image references in the document.
220
         * Called via afterParse handler.
221
         */
222
        public function resolveReferences(DocumentNode $doc): void
1✔
223
        {
224
                // Start with user-defined definitions
225
                $this->definitions = $this->userDefinitions;
1✔
226
                $traverser = new NodeTraverser;
1✔
227

228
                // Pass 1: Collect document definitions (overwrites user-defined)
229
                $traverser->traverse($doc, function (Node $node): ?int {
1✔
230
                        if ($node instanceof ImageDefinitionNode) {
1✔
231
                                $this->definitions[Helpers::toLower($node->identifier)] = $node;
1✔
232
                                return NodeTraverser::DontTraverseChildren;
1✔
233
                        }
234
                        return null;
1✔
235
                });
1✔
236

237
                // Pass 2: Resolve ImageNode references (NodeTraverser visits all nodes including FigureNode.image)
238
                $traverser->traverse($doc, function (Node $node): ?int {
1✔
239
                        if ($node instanceof ImageNode) {
1✔
240
                                $this->resolveImageNode($node);
1✔
241
                        } elseif (
242
                                $node instanceof LinkNode
1✔
243
                                && ($imageNode = $node->content->children[0] ?? null) instanceof ImageNode
1✔
244
                        ) {
245
                                // LinkNode wrapping ImageNode - resolve image and possibly the link URL
246
                                $imageKey = $imageNode->url !== null ? Helpers::toLower(trim($imageNode->url)) : null;
1✔
247
                                $linkKey = $node->url !== null ? Helpers::toLower(trim($node->url)) : null;
1✔
248

249
                                // Resolve the image
250
                                $this->resolveImageNode($imageNode);
1✔
251

252
                                // If link URL matches the original image reference (from :: syntax), resolve it too
253
                                if ($imageKey !== null && $linkKey === $imageKey && isset($this->definitions[$imageKey])) {
1✔
254
                                        $node->url = $this->definitions[$imageKey]->url;
1✔
255
                                }
256
                        }
257
                        return null;
1✔
258
                });
1✔
259
        }
1✔
260

261

262
        private function resolveImageNode(ImageNode $node): void
1✔
263
        {
264
                if ($node->url === null) {
1✔
265
                        return;
×
266
                }
267

268
                $key = Helpers::toLower(trim($node->url));
1✔
269
                if (!isset($this->definitions[$key])) {
1✔
270
                        return;
1✔
271
                }
272

273
                $def = $this->definitions[$key];
1✔
274

275
                $node->ref = trim($node->url); // keep the written reference name for analyzers
1✔
276
                $node->url = $def->url;
1✔
277
                $node->width ??= $def->width;
1✔
278
                $node->height ??= $def->height;
1✔
279

280
                // Merge modifier from definition if node doesn't have one
281
                if ($def->modifier && !$node->modifier) {
1✔
282
                        $node->modifier = clone $def->modifier;
×
283
                } elseif ($def->modifier) {
1✔
284
                        // Merge: node modifier takes precedence, but inherit missing values from definition
285
                        if ($node->modifier->title === null && $def->modifier->title !== null) {
1✔
286
                                $node->modifier->title = $def->modifier->title;
1✔
287
                        }
288
                }
289
        }
1✔
290

291

292
        /**
293
         * Get image definition by name (for LinkDefinitionModule to resolve [*img*] links).
294
         */
295
        public function getDefinition(string $name): ?ImageDefinitionNode
1✔
296
        {
297
                return $this->definitions[Helpers::toLower($name)] ?? null;
1✔
298
        }
299

300

301
        /**
302
         * @deprecated use $texy->htmlOutput->imageRoot etc. instead
303
         */
304
        public function __get(string $name): mixed
305
        {
306
                if (isset(self::LegacyProperties[$name])) {
×
307
                        $prop = self::LegacyProperties[$name];
×
308
                        trigger_error("Property \$texy->imageModule->$name is deprecated, use \$texy->htmlOutput->$prop instead.", E_USER_DEPRECATED);
×
309
                        return $this->texy->htmlOutput->$prop;
×
310
                }
311
                throw new \LogicException("Cannot read an undeclared property \$texy->imageModule->$name.");
×
312
        }
313

314

315
        /**
316
         * @deprecated use $texy->htmlOutput->imageRoot etc. instead
317
         */
318
        public function __set(string $name, mixed $value): void
319
        {
320
                if (isset(self::LegacyProperties[$name])) {
×
321
                        $prop = self::LegacyProperties[$name];
×
322
                        trigger_error("Property \$texy->imageModule->$name is deprecated, use \$texy->htmlOutput->$prop instead.", E_USER_DEPRECATED);
×
323
                        $this->texy->htmlOutput->$prop = $value;
×
324
                        return;
×
325
                }
326
                throw new \LogicException("Cannot write to an undeclared property \$texy->imageModule->$name.");
×
327
        }
328
}
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