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

tito10047 / progressive-image-bundle / 21317976930

24 Jan 2026 04:19PM UTC coverage: 90.353% (-0.3%) from 90.641%
21317976930

push

github

Jozef Mostka
Fix #8. zerom min_viewport is not generated to css

871 of 964 relevant lines covered (90.35%)

346.07 hits per line

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

90.91
/src/Command/GenerateCustomCssCommand.php
1
<?php
2

3
/*
4
 * This file is part of the Progressive Image Bundle.
5
 *
6
 * (c) Jozef Môstka <https://github.com/tito10047/progressive-image-bundle>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
namespace Tito10047\ProgressiveImageBundle\Command;
13

14
use Symfony\Component\Console\Attribute\AsCommand;
15
use Symfony\Component\Console\Command\Command;
16
use Symfony\Component\Console\Input\InputArgument;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Console\Output\OutputInterface;
19
use Symfony\Component\Console\Style\SymfonyStyle;
20
use Symfony\Component\Filesystem\Filesystem;
21

22
#[AsCommand(
23
    name: 'progressive-image:generate-custom-css',
24
    description: 'Generates a custom Tailwind CSS file based on the bundle configuration.',
25
)]
26
class GenerateCustomCssCommand extends Command
27
{
28
    /**
29
     * @param array{
30
     *     layouts: array<string, array{
31
     *         min_viewport: int,
32
     *         max_container: int|null
33
     *     }>,
34
     *     columns: int
35
     * } $gridConfig
36
     */
37
    public function __construct(
38
        private readonly array $gridConfig,
39
        private readonly string $projectDir,
40
    ) {
41
        parent::__construct();
30✔
42
    }
43

44
    protected function configure(): void
45
    {
46
        $this
30✔
47
            ->addArgument('path', InputArgument::OPTIONAL, 'Path to the directory where the CSS file will be generated.', 'assets/styles');
30✔
48
    }
49

50
    protected function execute(InputInterface $input, OutputInterface $output): int
51
    {
52
        $io = new SymfonyStyle($input, $output);
30✔
53
        $path = $input->getArgument('path');
30✔
54

55
        if (!str_starts_with($path, '/')) {
30✔
56
            $path = $this->projectDir.'/'.$path;
×
57
        }
58

59
        $filesystem = new Filesystem();
30✔
60
        if (!$filesystem->exists($path)) {
30✔
61
            $filesystem->mkdir($path);
×
62
        }
63

64
        $filePath = $path.'/progressive-image-custom.css';
30✔
65

66
        $cssContent = $this->generateCssContent();
30✔
67

68
        $filesystem->dumpFile($filePath, $cssContent);
30✔
69

70
        $io->success(sprintf('CSS file generated at %s', $filePath));
30✔
71

72
        return Command::SUCCESS;
30✔
73
    }
74

75
    private function generateCssContent(): string
76
    {
77
        $layouts = $this->gridConfig['layouts'];
30✔
78

79
        // Sort layouts by min_viewport ascending to easily find the next breakpoint
80
        $mediaLayouts = $layouts;
30✔
81
        uasort($mediaLayouts, fn ($a, $b) => $a['min_viewport'] <=> $b['min_viewport']);
30✔
82

83
        $content = "/* Progressive Image Container - Custom Breakpoints */\n";
30✔
84
        $content = "@layer vendor {\n";
30✔
85
        $content .= "\t.progressive-image-container {\n";
30✔
86
        $content .= "\t\tdisplay: block;\n";
30✔
87

88
        // Sort layouts by min_viewport descending for the root .progressive-image-container nested variables
89
        $sortedLayouts = $layouts;
30✔
90
        uasort($sortedLayouts, fn ($a, $b) => $b['min_viewport'] <=> $a['min_viewport']);
30✔
91

92
        // Root width variable with fallbacks
93
        $content .= "\t\twidth: ".$this->generateVariableFallback($sortedLayouts, 'width', false).";\n";
30✔
94

95
        // Root aspect-ratio variable with fallbacks
96
        $content .= "\t\taspect-ratio: ".$this->generateVariableFallback($sortedLayouts, 'aspect', false).";\n";
30✔
97

98
        $content .= "\t\tposition: relative;\n";
30✔
99
        $content .= "\t\toverflow: hidden;\n";
30✔
100
        $content .= "\t}\n\n";
30✔
101

102
        $layoutNames = array_keys($mediaLayouts);
30✔
103
        $i = 0;
30✔
104
        foreach ($mediaLayouts as $name => $layout) {
30✔
105
            $nextBreakpoint = isset($layoutNames[$i + 1]) ? $mediaLayouts[$layoutNames[$i + 1]] : null;
30✔
106

107
            if (0 === $layout['min_viewport'] && $nextBreakpoint) {
30✔
108
                $content .= sprintf("/* %s: %dpx */\n", $name, $layout['min_viewport']);
30✔
109
                $content .= sprintf("@media (max-width: %dpx) {\n", $nextBreakpoint['min_viewport']);
30✔
110
                $content .= "\t\t.progressive-image-container {\n";
30✔
111
                $content .= sprintf("\t\t\twidth: var(--img-width-%s);\n", $name);
30✔
112
                $content .= sprintf("\t\t\taspect-ratio: var(--img-aspect-%s);\n", $name);
30✔
113
                $content .= "\t\t}\n";
30✔
114
                $content .= "\t}\n\n";
30✔
115

116
                $i++;
30✔
117
                continue;
30✔
118
            }
119

120
            if (0 === $layout['min_viewport']) {
30✔
121
                $i++;
×
122
                continue;
×
123
            }
124

125
            $content .= sprintf("/* %s: %dpx */\n", $name, $layout['min_viewport']);
30✔
126
            $content .= sprintf("@media (min-width: %dpx) {\n", $layout['min_viewport']);
30✔
127
            $content .= "\t\t.progressive-image-container {\n";
30✔
128

129
            // For width
130
            $content .= "\t\t\twidth: ".$this->generateVariableFallbackForBreakpoint($name, $layouts, 'width').";\n";
30✔
131
            // For aspect
132
            $content .= "\t\t\taspect-ratio: ".$this->generateVariableFallbackForBreakpoint($name, $layouts, 'aspect').";\n";
30✔
133

134
            $content .= "\t\t}\n";
30✔
135
            $content .= "\t}\n\n";
30✔
136

137
            $i++;
30✔
138
        }
139
        $content .= "}\n\n";
30✔
140

141
        return $content;
30✔
142
    }
143

144
    private function generateVariableFallback(array $sortedLayouts, string $type, bool $includeGeneric = true): string
145
    {
146
        $variables = [];
30✔
147
        foreach ($sortedLayouts as $name => $layout) {
30✔
148
            $variables[] = sprintf('var(--img-%s-%s', $type, $name);
30✔
149
        }
150

151
        if ($includeGeneric) {
30✔
152
            $variables[] = sprintf('var(--img-%s', $type);
×
153
        }
154

155
        $result = implode(",\n\t\t\t", $variables);
30✔
156
        if (count($variables) > 1) {
30✔
157
            $result .= str_repeat(')', count($variables));
30✔
158
        } else {
159
            $result .= ')';
×
160
        }
161

162
        return $result;
30✔
163
    }
164

165
    private function generateVariableFallbackForBreakpoint(string $currentBreakpoint, array $allLayouts, string $type): string
166
    {
167
        // Get all layouts with min_viewport <= current min_viewport, sorted descending
168
        $currentMinViewport = $allLayouts[$currentBreakpoint]['min_viewport'];
30✔
169
        $filteredLayouts = array_filter($allLayouts, fn ($l) => $l['min_viewport'] <= $currentMinViewport);
30✔
170
        uasort($filteredLayouts, fn ($a, $b) => $b['min_viewport'] <=> $a['min_viewport']);
30✔
171

172
        $variables = [];
30✔
173
        foreach ($filteredLayouts as $name => $layout) {
30✔
174
            $variables[] = sprintf('var(--img-%s-%s', $type, $name);
30✔
175
        }
176

177
        $result = implode(', ', $variables);
30✔
178
        if (count($variables) > 1) {
30✔
179
            $result .= str_repeat(')', count($variables));
30✔
180
        } else {
181
            $result .= ')';
×
182
        }
183

184
        return $result;
30✔
185
    }
186
}
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