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

tito10047 / progressive-image-bundle / 20991836456

14 Jan 2026 11:04AM UTC coverage: 90.185% (+0.3%) from 89.9%
20991836456

push

github

tito10047
Add custom CSS generator for Progressive Image Bundle, update documentation, and include real-world examples

60 of 64 new or added lines in 2 files covered. (93.75%)

781 of 866 relevant lines covered (90.18%)

249.26 hits per line

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

93.33
/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();
15✔
42
        }
43

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

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

53
                if (!str_starts_with($path, '/')) {
15✔
NEW
54
                        $path = $this->projectDir . '/' . $path;
×
55
                }
56

57
                $filesystem = new Filesystem();
15✔
58
                if (!$filesystem->exists($path)) {
15✔
NEW
59
                        $filesystem->mkdir($path);
×
60
                }
61

62
                $filePath = $path . '/progressive-image-custom.css';
15✔
63

64
                $cssContent = $this->generateCssContent();
15✔
65

66
                $filesystem->dumpFile($filePath, $cssContent);
15✔
67

68
                $io->success(sprintf('CSS file generated at %s', $filePath));
15✔
69

70
                return Command::SUCCESS;
15✔
71
        }
72

73
        private function generateCssContent(): string {
74
                $layouts = $this->gridConfig['layouts'] ?? [];
15✔
75

76
                // Sort layouts by min_viewport descending for the root .progressive-image-container nested variables
77
                $sortedLayouts = $layouts;
15✔
78
                uasort($sortedLayouts, fn($a, $b) => $b['min_viewport'] <=> $a['min_viewport']);
15✔
79

80
                $content = "/* Progressive Image Container - Custom Breakpoints */\n";
15✔
81
                $content .= ".progressive-image-container {\n";
15✔
82
                $content .= "\tdisplay: block;\n";
15✔
83

84
                // Root width variable with fallbacks
85
                $content .= "\twidth: " . $this->generateVariableFallback($sortedLayouts, 'width') . ";\n";
15✔
86

87
                // Root aspect-ratio variable with fallbacks
88
                $content .= "\taspect-ratio: " . $this->generateVariableFallback($sortedLayouts, 'aspect') . ";\n";
15✔
89

90
                $content .= "\tposition: relative;\n";
15✔
91
                $content .= "\toverflow: hidden;\n";
15✔
92
                $content .= "}\n\n";
15✔
93

94
                // Media queries - sort by min_viewport ascending
95
                $mediaLayouts = $layouts;
15✔
96
                uasort($mediaLayouts, fn($a, $b) => $a['min_viewport'] <=> $b['min_viewport']);
15✔
97

98
                foreach ($mediaLayouts as $name => $layout) {
15✔
99
                        if ($layout['min_viewport'] === 0) {
15✔
100
                                continue;
15✔
101
                        }
102

103
                        $content .= sprintf("/* %s: %dpx */\n", $name, $layout['min_viewport']);
15✔
104
                        $content .= sprintf("@media (min-width: %dpx) {\n", $layout['min_viewport']);
15✔
105
                        $content .= "\t.progressive-image-container {\n";
15✔
106

107
                        // For width
108
                        $content .= "\t\twidth: " . $this->generateVariableFallbackForBreakpoint($name, $layouts, 'width') . ";\n";
15✔
109
                        // For aspect
110
                        $content .= "\t\taspect-ratio: " . $this->generateVariableFallbackForBreakpoint($name, $layouts, 'aspect') . ";\n";
15✔
111

112
                        $content .= "\t}\n";
15✔
113
                        $content .= "}\n\n";
15✔
114
                }
115

116
                return $content;
15✔
117
        }
118

119
        private function generateVariableFallback(array $sortedLayouts, string $type): string {
120
                $variables = [];
15✔
121
                foreach ($sortedLayouts as $name => $layout) {
15✔
122
                        $suffix      = $layout['min_viewport'] === 0 ? '' : '-' . $name;
15✔
123
                        $variables[] = sprintf('var(--img-%s%s', $type, $suffix);
15✔
124
                }
125

126
                $result = implode(",\n\t\t\t", $variables);
15✔
127
                if (count($variables) > 1) {
15✔
128
                        $result .= str_repeat(')', count($variables));
15✔
129
                } else {
NEW
130
                        $result .= ')';
×
131
                }
132

133
                return $result;
15✔
134
        }
135

136
        private function generateVariableFallbackForBreakpoint(string $currentBreakpoint, array $allLayouts, string $type): string {
137
                // Get all layouts with min_viewport <= current min_viewport, sorted descending
138
                $currentMinViewport = $allLayouts[$currentBreakpoint]['min_viewport'];
15✔
139
                $filteredLayouts    = array_filter($allLayouts, fn($l) => $l['min_viewport'] <= $currentMinViewport);
15✔
140
                uasort($filteredLayouts, fn($a, $b) => $b['min_viewport'] <=> $a['min_viewport']);
15✔
141

142
                $variables = [];
15✔
143
                foreach ($filteredLayouts as $name => $layout) {
15✔
144
                        $suffix      = $layout['min_viewport'] === 0 ? '' : '-' . $name;
15✔
145
                        $variables[] = sprintf('var(--img-%s%s', $type, $suffix);
15✔
146
                }
147

148
                $result = implode(", ", $variables);
15✔
149
                if (count($variables) > 1) {
15✔
150
                        $result .= str_repeat(')', count($variables));
15✔
151
                } else {
NEW
152
                        $result .= ')';
×
153
                }
154

155
                return $result;
15✔
156
        }
157
}
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