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

dg / texy / 29746802114

20 Jul 2026 01:19PM UTC coverage: 94.677% (+0.04%) from 94.641%
29746802114

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

74 existing lines in 15 files now uncovered.

3344 of 3532 relevant lines covered (94.68%)

0.95 hits per line

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

95.8
/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\Compat;
12
use Texy\Helpers;
13
use Texy\Modifier;
14
use Texy\Node;
15
use Texy\Nodes\ContentNode;
16
use Texy\Nodes\DocumentNode;
17
use Texy\Nodes\FigureNode;
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\Range;
25
use Texy\Regexp;
26
use Texy\Syntax;
27
use function count, strlen;
28

29

30
/**
31
 * Processes image and figure (image with caption) syntax.
32
 */
33
final class ImageModule extends Texy\Module
34
{
35
        /** the [* url .(title) alignment ] part, shared by the image and figure syntaxes */
36
        private const ImagePattern = '
37
                        \[\* \ *+                         # opening bracket with asterisk
38
                        ([^\n]{1,1000})                   # URLs (1)
39
                        ' . Patterns::Modifier . '?       # modifier (2)
40
                        \ *+
41
                        ( \* | (?<! < ) > | < )           # alignment (3)
42
                        ]';
43

44
        /** the optional :LINK (or :: self link) suffix */
45
        private const LinkPattern = '
46
                        (?:
47
                                :(' . Patterns::LinkUrl . ' | : ) # link or just colon (4)
48
                        )??';
49

50
        /** @var array<string, ImageDefinitionNode> collected image definitions */
51
        private array $definitions = [];
52

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

56

57
        public function __construct(
1✔
58
                private Texy\Texy $texy,
59
        ) {
60
                $texy->allowed[Syntax::ImageDefinition] = true;
1✔
61
                $texy->addHandler('afterParse', $this->resolveReferences(...));
1✔
62
        }
1✔
63

64

65
        public function beforeParse(string &$text): void
1✔
66
        {
67
                // [*image*]:LINK
68
                $this->texy->registerLinePattern(
1✔
69
                        $this->parseImage(...),
1✔
70
                        '~' . self::ImagePattern . self::LinkPattern . '
1✔
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

91
                // [*image*]:link *** caption
92
                $this->texy->registerBlockPattern(
1✔
93
                        $this->parseFigure(...),
1✔
94
                        '~^
95
                                (?>' . self::ImagePattern . ')' . self::LinkPattern . '
1✔
96
                                (?:
97
                                        \ ++ \*\*\* \ ++              # separator
98
                                        (.{0,2000})                   # caption (5)
99
                                )?
100
                                ' . Patterns::ModifierHAlign . '?     # modifier (6)
1✔
101
                        $~mUx',
102
                        Syntax::Figure,
1✔
103
                );
104
        }
1✔
105

106

107
        /**
108
         * Parses [*image*]: urls .(title)[class]{style}
109
         * @param  array{string, string, string, ?string}  $matches
110
         * @param  array{int, int, int, ?int}  $offsets
111
         */
112
        public function parseDefinition(ParseContext $context, array $matches, array $offsets): ImageDefinitionNode
1✔
113
        {
114
                [, $mRef, $mURLs, $mMod] = $matches;
1✔
115
                $parsed = $this->parseImageContent($mURLs);
1✔
116
                $modifier = Modifier::parse($mMod, $offsets[3] ?? null);
1✔
117

118
                return new ImageDefinitionNode(
1✔
119
                        trim($mRef),
1✔
120
                        $parsed['url'],
1✔
121
                        $parsed['width'],
1✔
122
                        $parsed['height'],
1✔
123
                        $modifier,
124
                        new Range($offsets[0], strlen($matches[0])),
1✔
125
                );
126
        }
127

128

129
        /**
130
         * Parses [* small.jpg 80x13 .(alternative text)[class]{style}>]:LINK
131
         * @param  array{string, string, ?string, string, ?string}  $matches
132
         * @param  array{int, int, ?int, int, ?int}  $offsets
133
         */
134
        public function parseImage(ParseContext $context, array $matches, array $offsets): ImageNode|LinkNode
1✔
135
        {
136
                [, $mURLs, $mMod, $mAlign, $mLink] = $matches;
1✔
137
                $parsed = $this->parseImageContent($mURLs);
1✔
138
                $modifier = Modifier::parse($mMod . $mAlign);
1✔
139
                $range = new Range($offsets[0], strlen($matches[0]));
1✔
140

141
                $imageNode = new ImageNode(
1✔
142
                        $parsed['url'],
1✔
143
                        $parsed['width'],
1✔
144
                        $parsed['height'],
1✔
145
                        $modifier,
146
                        $range,
147
                );
148

149
                return $mLink
1✔
150
                        ? $this->wrapInLink($imageNode, $mLink, $range)
1✔
151
                        : $imageNode;
1✔
152
        }
153

154

155
        /**
156
         * Parses [*image*]:link *** caption .(title)[class]{style}>.
157
         * @param  array{string, string, ?string, string, ?string, ?string, ?string}  $matches
158
         * @param  array{int, int, ?int, int, ?int, ?int, ?int}  $offsets
159
         */
160
        public function parseFigure(ParseContext $context, array $matches, array $offsets): ?FigureNode
1✔
161
        {
162
                [, $mURLs, $mImgMod, $mAlign, $mLink, $mContent, $mMod] = $matches;
1✔
163

164
                $parsed = $this->parseImageContent($mURLs);
1✔
165
                $modifier = Modifier::parse($mImgMod . $mAlign);
1✔
166

167
                if ($parsed['url'] === null) {
1✔
UNCOV
168
                        return null;
×
169
                }
170

171
                // ImageNode source span is "[* ... <alignment>]"
172
                $imageEnd = $offsets[3] + strlen($mAlign) + 1; // 1 = "]"
1✔
173
                $imageNode = new ImageNode(
1✔
174
                        $parsed['url'],
1✔
175
                        $parsed['width'],
1✔
176
                        $parsed['height'],
1✔
177
                        $modifier,
178
                        new Range($offsets[0], $imageEnd - $offsets[0]),
1✔
179
                );
180

181
                $image = $mLink
1✔
182
                        ? $this->wrapInLink(
1✔
183
                                $imageNode,
1✔
184
                                $mLink,
185
                                new Range($offsets[0], $offsets[4] + strlen($mLink) - $offsets[0]),
1✔
186
                        )
187
                        : $imageNode;
1✔
188

189
                // Parse caption as inline content
190
                $caption = null;
1✔
191
                $mContent = trim($mContent ?? '');
1✔
192
                if ($mContent !== '') {
1✔
193
                        $caption = $context->parseInline($mContent, $offsets[5] ?? $offsets[0]);
1✔
194
                }
195

196
                return new FigureNode(
1✔
197
                        $image,
1✔
198
                        $caption,
199
                        Modifier::parse($mMod, $offsets[6] ?? null),
1✔
200
                        new Range($offsets[0], strlen($matches[0])),
1✔
201
                );
202
        }
203

204

205
        private function wrapInLink(ImageNode $imageNode, string $mLink, Range $range): LinkNode
1✔
206
        {
207
                if ($mLink === ':') {
1✔
208
                        // Link to the image itself
209
                        $linkUrl = $imageNode->url;
1✔
210
                        $isImageLink = true;
1✔
211
                } else {
212
                        // Direct URL or reference like [ref] or [*img*]
213
                        $linkUrl = $mLink;
1✔
214
                        // Image reference [*...*] → uses imageRoot
215
                        $len = strlen($mLink);
1✔
216
                        $isImageLink = $len > 4 && $mLink[0] === '[' && $mLink[1] === '*'
1✔
217
                                && $mLink[$len - 1] === ']' && $mLink[$len - 2] === '*';
1✔
218
                }
219

220
                return new LinkNode(
1✔
221
                        url: $linkUrl,
1✔
222
                        content: new ContentNode([$imageNode]),
1✔
223
                        range: $range,
224
                        isImageLink: $isImageLink,
225
                );
226
        }
227

228

229
        /**
230
         * Parse image content: "image.jpg 100x200"
231
         * @return array{url: ?string, width: ?int, height: ?int}
232
         */
233
        public function parseImageContent(string $content): array
1✔
234
        {
235
                $parts = explode('|', $content);
1✔
236
                $main = trim($parts[0]);
1✔
237

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

242
                $url = $main;
1✔
243
                $width = $height = null;
1✔
244

245
                // Parse dimensions: "image.jpg 100x200" or "image.jpg 100X200" (asMax)
246
                if ($m = Regexp::match($main, '~^(.*)\ (\d+|\?)\ *[xX]\ *(\d+|\?)\ *$~U')) {
1✔
247
                        /** @var array{string, string, string, string} $m */
248
                        $url = trim($m[1]);
1✔
249
                        $width = $m[2] === '?' ? null : (int) $m[2];
1✔
250
                        $height = $m[3] === '?' ? null : (int) $m[3];
1✔
251
                }
252

253
                // Check URL
254
                if (!$this->texy->urlPolicy->isImageAllowed($url)) {
1✔
UNCOV
255
                        $url = null;
×
256
                }
257

258
                return [
259
                        'url' => $url,
1✔
260
                        'width' => $width,
1✔
261
                        'height' => $height,
1✔
262
                ];
263
        }
264

265

266
        /**
267
         * Adds a user-defined image definition (persists across process() calls).
268
         */
269
        public function addDefinition(
1✔
270
                string $name,
271
                string $url,
272
                ?int $width = null,
273
                ?int $height = null,
274
                ?string $alt = null,
275
        ): void
276
        {
277
                $modifier = $alt !== null ? Modifier::parse('(' . $alt . ')') : null;
1✔
278
                $this->userDefinitions[Helpers::toLower($name)] = new ImageDefinitionNode($name, $url, $width, $height, $modifier);
1✔
279
        }
1✔
280

281

282
        /**
283
         * Resolve image references in the document.
284
         * Called via afterParse handler.
285
         */
286
        public function resolveReferences(DocumentNode $doc): void
1✔
287
        {
288
                // Start with user-defined definitions
289
                $this->definitions = $this->userDefinitions;
1✔
290
                $traverser = new NodeTraverser;
1✔
291

292
                // Pass 1: Collect document definitions (overwrites user-defined)
293
                $traverser->traverse($doc, function (Node $node): ?int {
1✔
294
                        if ($node instanceof ImageDefinitionNode) {
1✔
295
                                $this->definitions[Helpers::toLower($node->reference)] = $node;
1✔
296
                                return NodeTraverser::DontTraverseChildren;
1✔
297
                        }
298
                        return null;
1✔
299
                });
1✔
300

301
                // Pass 2: Resolve ImageNode references (NodeTraverser visits all nodes including FigureNode.image)
302
                $traverser->traverse($doc, function (Node $node): ?int {
1✔
303
                        if ($node instanceof ImageNode) {
1✔
304
                                $this->resolveImageNode($node);
1✔
305
                        } elseif (
306
                                $node instanceof LinkNode
1✔
307
                                && ($imageNode = $node->content->children[0] ?? null) instanceof ImageNode
1✔
308
                        ) {
309
                                // LinkNode wrapping ImageNode - resolve image and possibly the link URL
310
                                $imageKey = $imageNode->url !== null ? Helpers::toLower(trim($imageNode->url)) : null;
1✔
311
                                $linkKey = $node->url !== null ? Helpers::toLower(trim($node->url)) : null;
1✔
312

313
                                // Resolve the image
314
                                $this->resolveImageNode($imageNode);
1✔
315

316
                                // If link URL matches the original image reference (from :: syntax), resolve it too
317
                                if ($imageKey !== null && $linkKey === $imageKey && isset($this->definitions[$imageKey])) {
1✔
318
                                        $node->url = $this->definitions[$imageKey]->url;
1✔
319
                                }
320
                        }
321
                        return null;
1✔
322
                });
1✔
323
        }
1✔
324

325

326
        private function resolveImageNode(ImageNode $node): void
1✔
327
        {
328
                if ($node->url === null) {
1✔
UNCOV
329
                        return;
×
330
                }
331

332
                $key = Helpers::toLower(trim($node->url));
1✔
333
                if (!isset($this->definitions[$key])) {
1✔
334
                        return;
1✔
335
                }
336

337
                $def = $this->definitions[$key];
1✔
338

339
                $node->ref = trim($node->url); // keep the written reference name for analyzers
1✔
340
                $node->url = $def->url;
1✔
341
                $node->width ??= $def->width;
1✔
342
                $node->height ??= $def->height;
1✔
343

344
                // Merge modifier from definition if node doesn't have one
345
                if ($def->modifier && !$node->modifier) {
1✔
UNCOV
346
                        $node->modifier = clone $def->modifier;
×
347
                } elseif ($def->modifier) {
1✔
348
                        // Merge: node modifier takes precedence, but inherit missing values from definition
349
                        if ($node->modifier->title === null && $def->modifier->title !== null) {
1✔
350
                                $node->modifier->title = $def->modifier->title;
1✔
351
                        }
352
                }
353
        }
1✔
354

355

356
        /**
357
         * Get image definition by name (for LinkReferenceModule to resolve [*img*] links).
358
         */
359
        public function getDefinition(string $name): ?ImageDefinitionNode
1✔
360
        {
361
                return $this->definitions[Helpers::toLower($name)] ?? null;
1✔
362
        }
363

364

365
        /**
366
         * @deprecated use $texy->htmlOutput->imageRoot etc. instead
367
         */
368
        public function &__get(string $name): mixed
369
        {
UNCOV
370
                return Compat\Legacy::ref($this->texy, Compat\Legacy::OfModule['imageModule'], '$texy->imageModule', $name, 'read');
×
371
        }
372

373

374
        /**
375
         * @deprecated use $texy->htmlOutput->imageRoot etc. instead
376
         */
377
        public function __set(string $name, mixed $value): void
1✔
378
        {
379
                Compat\Legacy::set($this->texy, Compat\Legacy::OfModule['imageModule'], '$texy->imageModule', $name, $value);
1✔
380
        }
1✔
381

382

383
        public function __isset(string $name): bool
384
        {
UNCOV
385
                return Compat\Legacy::isSet($this->texy, Compat\Legacy::OfModule['imageModule'], $name);
×
386
        }
387
}
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