Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
Image
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
3 / 3
12
100.00% covered (success)
100.00%
1 / 1
 render
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
5
 ladder
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 sizes
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Block;
6
7use Cosray\Assets\ResizeMode;
8use Cosray\Assets\SizeSpec;
9use Cosray\Contract\Block;
10use Cosray\Exception\RuntimeException;
11use Cosray\Field;
12use Cosray\Schema\Label;
13use Cosray\Schema\Limit;
14use Cosray\Schema\Required;
15use Cosray\Schema\Translate;
16use Cosray\Value\Block as BlockValue;
17
18use function Cosray\escape;
19
20#[Label('block:image')]
21final class Image implements Block
22{
23    private const array LADDER = ['block-sm', 'block', 'block-lg'];
24    private const string SIZES = '(min-width: 48rem) {pct}vw, 100vw';
25
26    #[Label('block:image'), Required, Limit(1), Translate]
27    protected Field\Image $image;
28
29    public function render(BlockValue $block, RenderContext $ctx): string
30    {
31        $image = $block->image;
32        $asset = $ctx->asset((string) ($image->unwrap()['uid'] ?? ''));
33
34        if ($asset === null) {
35            return '';
36        }
37
38        $alt = escape($image->alt() ?: strip_tags($image->title()));
39        $path = escape($asset->path());
40
41        if (!$asset->resizable()) {
42            return "<img src=\"{$path}\" alt=\"{$alt}\" data-path-original=\"{$path}\">";
43        }
44
45        $specs = $this->ladder($ctx);
46        $src = escape($asset->sizePath($specs[intdiv(count($specs), 2)]->name));
47
48        if (count($specs) === 1) {
49            return "<img src=\"{$src}\" alt=\"{$alt}\" data-path-original=\"{$path}\">";
50        }
51
52        $srcset = escape(implode(', ', array_map(
53            static fn(SizeSpec $spec) => $asset->sizePath($spec->name) . " {$spec->first}w",
54            $specs,
55        )));
56        $sizes = escape($this->sizes($ctx, $block->layout()->colspan));
57
58        return (
59            "<img src=\"{$src}\" srcset=\"{$srcset}\" sizes=\"{$sizes}\""
60                . " alt=\"{$alt}\" data-path-original=\"{$path}\">"
61        );
62    }
63
64    /**
65     * The named sizes forming the srcset ladder, ascending by width.
66     * Multi-rung ladders need `w` descriptors, so their entries must use
67     * the `width` mode; a single rung emits a plain `src` and may use
68     * any mode.
69     *
70     * @return list<SizeSpec>
71     */
72    private function ladder(RenderContext $ctx): array
73    {
74        $names = $ctx->args['imageSizes'] ?? self::LADDER;
75
76        if (!is_array($names) || $names === []) {
77            throw new RuntimeException('Blocks error: `imageSizes` must be a non-empty list of size names');
78        }
79
80        $registry = $ctx->owner->config()->media->sizes;
81        $specs = array_map(static fn($name) => $registry->get((string) $name), array_values($names));
82
83        if (count($specs) > 1) {
84            foreach ($specs as $spec) {
85                if ($spec->mode !== ResizeMode::Width) {
86                    throw new RuntimeException(
87                        "Blocks error: srcset entry '{$spec->name}' must use the `width` mode",
88                    );
89                }
90            }
91
92            usort($specs, static fn(SizeSpec $a, SizeSpec $b) => $a->first <=> $b->first);
93        }
94
95        return $specs;
96    }
97
98    private function sizes(RenderContext $ctx, int $span): string
99    {
100        $template = (string) ($ctx->args['sizes'] ?? self::SIZES);
101        $pct = (int) round(($span / max($ctx->columns, 1)) * 100);
102
103        return str_replace('{pct}', (string) $pct, $template);
104    }
105}