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

dg / texy / 21501721037

30 Jan 2026 02:00AM UTC coverage: 91.159% (-1.3%) from 92.426%
21501721037

push

github

dg
wip

2681 of 2941 relevant lines covered (91.16%)

0.91 hits per line

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

97.17
/src/Texy/Modules/ImageModule.php
1
<?php
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
declare(strict_types=1);
9

10
namespace Texy\Modules;
11

12
use Texy;
13
use Texy\Helpers;
14
use Texy\Modifier;
15
use Texy\Node;
16
use Texy\Nodes\ContentNode;
17
use Texy\Nodes\DocumentNode;
18
use Texy\Nodes\ImageDefinitionNode;
19
use Texy\Nodes\ImageNode;
20
use Texy\Nodes\LinkNode;
21
use Texy\NodeTraverser;
22
use Texy\ParseContext;
23
use Texy\Patterns;
24
use Texy\Position;
25
use Texy\Regexp;
26
use Texy\Syntax;
27
use function count, strlen;
28

29

30
/**
31
 * Processes image syntax and detects image dimensions.
32
 */
33
final class ImageModule extends Texy\Module
34
{
35
        /** @var array<string, ImageDefinitionNode> collected image definitions */
36
        private array $definitions = [];
37

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

41

42
        public function __construct(
1✔
43
                private Texy\Texy $texy,
44
        ) {
45
                $texy->allowed[Syntax::ImageDefinition] = true;
1✔
46
                $texy->addHandler('afterParse', $this->resolveReferences(...));
1✔
47
        }
1✔
48

49

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

69
                // [*ref*]: url .(title)[class]{style}
70
                $this->texy->registerBlockPattern(
1✔
71
                        $this->parseDefinition(...),
1✔
72
                        '~^
73
                                \[\*                              # opening [*
74
                                ( [^\n]{1,100} )                  # reference (1)
75
                                \*]                               # closing *]
76
                                : [ \t]+
77
                                (.{1,1000})                       # URL (2)
78
                                [ \t]*
79
                                ' . Patterns::MODIFIER . '?       # modifier (3)
1✔
80
                                \s*
81
                        $~mU',
82
                        Syntax::ImageDefinition,
1✔
83
                );
84
        }
1✔
85

86

87
        /**
88
         * Parses [*image*]: urls .(title)[class]{style}
89
         * @param  array<?string>  $matches
90
         * @param  array<?int>  $offsets
91
         */
92
        public function parseDefinition(ParseContext $context, array $matches, array $offsets): ImageDefinitionNode
1✔
93
        {
94
                [, $mRef, $mURLs, $mMod] = $matches;
1✔
95
                $parsed = $this->parseImageContent($mURLs);
1✔
96
                $modifier = Modifier::parse($mMod);
1✔
97

98
                return new ImageDefinitionNode(
1✔
99
                        trim($mRef),
1✔
100
                        $parsed['url'],
1✔
101
                        $parsed['width'],
1✔
102
                        $parsed['height'],
1✔
103
                        $modifier,
104
                        new Position($offsets[0], strlen($matches[0])),
1✔
105
                );
106
        }
107

108

109
        /**
110
         * Parses [* small.jpg 80x13 .(alternative text)[class]{style}>]:LINK
111
         * @param  array<?string>  $matches
112
         * @param  array<?int>  $offsets
113
         */
114
        public function parseImage(ParseContext $context, array $matches, array $offsets): ImageNode|LinkNode
1✔
115
        {
116
                [, $mURLs, $mMod, $mAlign, $mLink] = $matches;
1✔
117
                $parsed = $this->parseImageContent($mURLs);
1✔
118
                $modifier = Modifier::parse($mMod . $mAlign);
1✔
119
                $position = new Position($offsets[0], strlen($matches[0]));
1✔
120

121
                $imageNode = new ImageNode(
1✔
122
                        $parsed['url'],
1✔
123
                        $parsed['width'],
1✔
124
                        $parsed['height'],
1✔
125
                        $modifier,
126
                        $position,
127
                );
128

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

144
                        return new LinkNode(
1✔
145
                                url: $linkUrl,
1✔
146
                                content: new ContentNode([$imageNode]),
1✔
147
                                position: $position,
148
                                isImageLink: $isImageLink,
149
                        );
150
                }
151

152
                return $imageNode;
1✔
153
        }
154

155

156
        /**
157
         * Parse image content: "image.jpg 100x200"
158
         * @return array{url: ?string, width: ?int, height: ?int, linkedUrl: ?string}
159
         */
160
        public function parseImageContent(string $content): array
1✔
161
        {
162
                $parts = explode('|', $content);
1✔
163
                $main = trim($parts[0]);
1✔
164

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

169
                $url = $main;
1✔
170
                $width = $height = null;
1✔
171

172
                // Parse dimensions: "image.jpg 100x200" or "image.jpg 100X200" (asMax)
173
                if ($m = Regexp::match($main, '~^(.*)\ (\d+|\?)\ *[xX]\ *(\d+|\?)\ *$~U')) {
1✔
174
                        $url = trim($m[1]);
1✔
175
                        $width = $m[2] === '?' ? null : (int) $m[2];
1✔
176
                        $height = $m[3] === '?' ? null : (int) $m[3];
1✔
177
                }
178

179
                // Check URL
180
                if (!$this->texy->checkURL($url, $this->texy::FILTER_IMAGE)) {
1✔
181
                        $url = null;
×
182
                }
183

184
                return [
185
                        'url' => $url,
1✔
186
                        'width' => $width,
1✔
187
                        'height' => $height,
1✔
188
                        'linkedUrl' => null,
189
                ];
190
        }
191

192

193
        /**
194
         * Adds a user-defined image definition (persists across process() calls).
195
         */
196
        public function addDefinition(
1✔
197
                string $name,
198
                string $url,
199
                ?int $width = null,
200
                ?int $height = null,
201
                ?string $alt = null,
202
        ): void
203
        {
204
                $modifier = $alt !== null ? Modifier::parse('(' . $alt . ')') : null;
1✔
205
                $this->userDefinitions[Helpers::toLower($name)] = new ImageDefinitionNode($name, $url, $width, $height, $modifier);
1✔
206
        }
1✔
207

208

209
        /**
210
         * Resolve image references in the document.
211
         * Called via afterParse handler.
212
         */
213
        public function resolveReferences(DocumentNode $doc): void
1✔
214
        {
215
                // Start with user-defined definitions
216
                $this->definitions = $this->userDefinitions;
1✔
217
                $traverser = new NodeTraverser;
1✔
218

219
                // Pass 1: Collect document definitions (overwrites user-defined)
220
                $traverser->traverse($doc, function (Node $node): ?int {
1✔
221
                        if ($node instanceof ImageDefinitionNode) {
1✔
222
                                $this->definitions[Helpers::toLower($node->identifier)] = $node;
1✔
223
                                return NodeTraverser::DontTraverseChildren;
1✔
224
                        }
225
                        return null;
1✔
226
                });
1✔
227

228
                // Pass 2: Resolve ImageNode references (NodeTraverser visits all nodes including FigureNode.image)
229
                $traverser->traverse($doc, function (Node $node): void {
1✔
230
                        if ($node instanceof ImageNode) {
1✔
231
                                $this->resolveImageNode($node);
1✔
232
                        } elseif (
233
                                $node instanceof LinkNode
1✔
234
                                && ($imageNode = $node->content->children[0] ?? null) instanceof ImageNode
1✔
235
                        ) {
236
                                // LinkNode wrapping ImageNode - resolve image and possibly the link URL
237
                                $imageKey = $imageNode->url !== null ? Helpers::toLower(trim($imageNode->url)) : null;
1✔
238
                                $linkKey = $node->url !== null ? Helpers::toLower(trim($node->url)) : null;
1✔
239

240
                                // Resolve the image
241
                                $this->resolveImageNode($imageNode);
1✔
242

243
                                // If link URL matches the original image reference (from :: syntax), resolve it too
244
                                if ($imageKey !== null && $linkKey === $imageKey && isset($this->definitions[$imageKey])) {
1✔
245
                                        $node->url = $this->definitions[$imageKey]->url;
1✔
246
                                }
247
                        }
248
                });
1✔
249
        }
1✔
250

251

252
        private function resolveImageNode(ImageNode $node): void
1✔
253
        {
254
                if ($node->url === null) {
1✔
255
                        return;
×
256
                }
257

258
                $key = Helpers::toLower(trim($node->url));
1✔
259
                if (!isset($this->definitions[$key])) {
1✔
260
                        return;
1✔
261
                }
262

263
                $def = $this->definitions[$key];
1✔
264

265
                $node->url = $def->url;
1✔
266
                $node->width ??= $def->width;
1✔
267
                $node->height ??= $def->height;
1✔
268

269
                // Merge modifier from definition if node doesn't have one
270
                if ($def->modifier && !$node->modifier) {
1✔
271
                        $node->modifier = clone $def->modifier;
×
272
                } elseif ($def->modifier && $node->modifier) {
1✔
273
                        // Merge: node modifier takes precedence, but inherit missing values from definition
274
                        if ($node->modifier->title === null && $def->modifier->title !== null) {
1✔
275
                                $node->modifier->title = $def->modifier->title;
1✔
276
                        }
277
                }
278
        }
1✔
279

280

281
        /**
282
         * Get image definition by name (for LinkModule to resolve [*img*] links).
283
         */
284
        public function getDefinition(string $name): ?ImageDefinitionNode
1✔
285
        {
286
                return $this->definitions[Helpers::toLower($name)] ?? null;
1✔
287
        }
288
}
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