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

dragomano / scss-php / 23675604391

28 Mar 2026 02:28AM UTC coverage: 92.642% (+8.1%) from 84.536%
23675604391

push

github

dragomano
Update README

11357 of 12259 relevant lines covered (92.64%)

87.82 hits per line

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

99.3
/src/Utils/CompressedCssFormatter.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Bugo\SCSS\Utils;
6

7
use function ctype_space;
8
use function ctype_xdigit;
9
use function in_array;
10
use function ltrim;
11
use function max;
12
use function str_replace;
13
use function str_starts_with;
14
use function strlen;
15
use function strpos;
16
use function strtolower;
17
use function substr;
18
use function trim;
19

20
final readonly class CompressedCssFormatter
21
{
22
    public function __construct() {}
23

24
    public function format(string $css): string
25
    {
26
        $css = $this->removeRegularComments($css);
39✔
27
        $css = $this->compactCss($css);
39✔
28
        $css = $this->optimizeCompressedLiterals($css);
39✔
29

30
        return trim($css);
39✔
31
    }
32

33
    private function removeRegularComments(string $css): string
34
    {
35
        $result = '';
39✔
36
        $length = strlen($css);
39✔
37
        $index  = 0;
39✔
38

39
        while ($index < $length) {
39✔
40
            if ($css[$index] === '/' && $index + 1 < $length && $css[$index + 1] === '*') {
38✔
41
                $end = strpos($css, '*/', $index + 2);
7✔
42

43
                if ($end === false) {
7✔
44
                    break;
1✔
45
                }
46

47
                $end += 2;
6✔
48

49
                $comment = substr($css, $index, $end - $index);
6✔
50

51
                if ($this->shouldPreserveComment($comment)) {
6✔
52
                    $result .= $comment;
5✔
53
                }
54

55
                $index = $end;
6✔
56

57
                continue;
6✔
58
            }
59

60
            $result .= $css[$index];
38✔
61

62
            $index++;
38✔
63
        }
64

65
        if ($index < $length) {
39✔
66
            $result .= substr($css, $index);
1✔
67
        }
68

69
        return $result;
39✔
70
    }
71

72
    private function shouldPreserveComment(string $comment): bool
73
    {
74
        if (str_starts_with($comment, '/*!')) {
6✔
75
            return true;
3✔
76
        }
77

78
        $inner = trim(substr($comment, 2, -2));
3✔
79

80
        return str_starts_with(ltrim($inner), '# sourceMappingURL=');
3✔
81
    }
82

83
    private function compactCss(string $css): string
84
    {
85
        $result         = '';
39✔
86
        $length         = strlen($css);
39✔
87
        $inString       = false;
39✔
88
        $quote          = '';
39✔
89
        $escaped        = false;
39✔
90
        $pendingSpace   = false;
39✔
91
        $lastOutputChar = '';
39✔
92
        $parenDepth     = 0;
39✔
93

94
        for ($i = 0; $i < $length; $i++) {
39✔
95
            $char = $css[$i];
38✔
96

97
            if ($inString) {
38✔
98
                $result .= $char;
5✔
99

100
                $lastOutputChar = $char;
5✔
101

102
                if ($escaped) {
5✔
103
                    $escaped = false;
1✔
104
                } elseif ($char === '\\') {
5✔
105
                    $escaped = true;
1✔
106
                } elseif ($char === $quote) {
5✔
107
                    $inString = false;
5✔
108
                    $quote    = '';
5✔
109
                }
110

111
                continue;
5✔
112
            }
113

114
            if (ctype_space($char)) {
38✔
115
                $pendingSpace = true;
33✔
116

117
                continue;
33✔
118
            }
119

120
            if ($char === ';') {
38✔
121
                $next = $this->nextNonSpaceChar($css, $i + 1);
29✔
122

123
                if ($next === '}' || $next === ';') {
29✔
124
                    $pendingSpace = false;
28✔
125

126
                    continue;
28✔
127
                }
128

129
                $result .= ';';
15✔
130

131
                $lastOutputChar = ';';
15✔
132
                $pendingSpace   = false;
15✔
133

134
                continue;
15✔
135
            }
136

137
            if ($pendingSpace) {
38✔
138
                $next = $char;
33✔
139

140
                if (
141
                    $lastOutputChar !== ''
33✔
142
                    && ! $this->isTightPunctuation($lastOutputChar)
33✔
143
                    && ! $this->isTightPunctuation($char)
33✔
144
                    && ! $this->shouldSkipSpace($lastOutputChar, $next, $parenDepth)
33✔
145
                ) {
146
                    $result .= ' ';
12✔
147
                }
148
            }
149

150
            $result .= $char;
38✔
151

152
            $lastOutputChar = $char;
38✔
153
            $pendingSpace   = false;
38✔
154

155
            if ($char === '"' || $char === "'") {
38✔
156
                $inString = true;
5✔
157
                $quote    = $char;
5✔
158
            }
159

160
            if ($char === '(') {
38✔
161
                $parenDepth++;
13✔
162
            } elseif ($char === ')') {
38✔
163
                $parenDepth = max(0, $parenDepth - 1);
13✔
164
            }
165
        }
166

167
        return $result;
39✔
168
    }
169

170
    private function isTightPunctuation(string $char): bool
171
    {
172
        return in_array($char, ['{', '}', ':', ';', ',', '/'], true);
32✔
173
    }
174

175
    private function nextNonSpaceChar(string $css, int $start): string
176
    {
177
        $length = strlen($css);
29✔
178

179
        for ($i = $start; $i < $length; $i++) {
29✔
180
            if (! ctype_space($css[$i])) {
28✔
181
                return $css[$i];
28✔
182
            }
183
        }
184

185
        return '';
1✔
186
    }
187

188
    private function shouldSkipSpace(string $previous, string $next, int $parenDepth): bool
189
    {
190
        if ($previous === ')' && $this->isIdentifierStart($next)) {
15✔
191
            return true;
2✔
192
        }
193

194
        if ($parenDepth <= 0) {
13✔
195
            return false;
10✔
196
        }
197

198
        if ($next === '*' || $next === '/') {
3✔
199
            return true;
1✔
200
        }
201

202
        return ($previous === '*' || $previous === '/') && $this->isMathOperandStart($next);
3✔
203
    }
204

205
    private function isMathOperandStart(string $char): bool
206
    {
207
        return ($char >= '0' && $char <= '9')
1✔
208
            || ($char >= 'a' && $char <= 'z')
1✔
209
            || ($char >= 'A' && $char <= 'Z')
1✔
210
            || in_array($char, ['(', '.', '%', '#', '$', '-'], true);
1✔
211
    }
212

213
    private function isIdentifierStart(string $char): bool
214
    {
215
        return ($char >= 'a' && $char <= 'z')
3✔
216
            || ($char >= 'A' && $char <= 'Z')
3✔
217
            || in_array($char, ['_', '-'], true);
3✔
218
    }
219

220
    private function shortenHex(string $candidate): string
221
    {
222
        $hex = '#' . strtolower($candidate);
5✔
223
        $len = strlen($hex);
5✔
224

225
        if ($len === 7 && $hex[1] === $hex[2] && $hex[3] === $hex[4] && $hex[5] === $hex[6]) {
5✔
226
            return '#' . $hex[1] . $hex[3] . $hex[5];
1✔
227
        }
228

229
        if ($len === 9 && $hex[1] === $hex[2] && $hex[3] === $hex[4] && $hex[5] === $hex[6] && $hex[7] === $hex[8]) {
4✔
230
            return '#' . $hex[1] . $hex[3] . $hex[5] . $hex[7];
1✔
231
        }
232

233
        return $hex;
3✔
234
    }
235

236
    private function optimizeCompressedLiterals(string $css): string
237
    {
238
        $css = $this->shortenHexColors($css);
39✔
239

240
        return str_replace('hue-rotate(0deg)', 'hue-rotate(0)', $css);
39✔
241
    }
242

243
    private function shortenHexColors(string $css): string
244
    {
245
        $result   = '';
39✔
246
        $length   = strlen($css);
39✔
247
        $inString = false;
39✔
248
        $quote    = '';
39✔
249
        $i        = 0;
39✔
250

251
        while ($i < $length) {
39✔
252
            $char = $css[$i];
38✔
253

254
            if (! $inString && ($char === '"' || $char === "'")) {
38✔
255
                $inString = true;
5✔
256
                $quote    = $char;
5✔
257
                $result  .= $char;
5✔
258

259
                $i++;
5✔
260

261
                continue;
5✔
262
            }
263

264
            if ($inString) {
38✔
265
                if ($char === $quote && ($i === 0 || $css[$i - 1] !== '\\')) {
5✔
266
                    $inString = false;
5✔
267
                }
268

269
                $result .= $char;
5✔
270

271
                $i++;
5✔
272

273
                continue;
5✔
274
            }
275

276
            if ($char === '#') {
38✔
277
                foreach ([8, 6] as $hexLen) {
11✔
278
                    if ($i + $hexLen >= $length) {
11✔
279
                        continue;
6✔
280
                    }
281

282
                    $candidate = substr($css, $i + 1, $hexLen);
9✔
283
                    if (! ctype_xdigit($candidate)) {
9✔
284
                        continue;
5✔
285
                    }
286

287
                    $after = $i + 1 + $hexLen;
5✔
288
                    if ($after < $length && ctype_xdigit($css[$after])) {
5✔
289
                        continue;
×
290
                    }
291

292
                    $result .= $this->shortenHex($candidate);
5✔
293

294
                    $i += 1 + $hexLen;
5✔
295

296
                    continue 2;
5✔
297
                }
298
            }
299

300
            $result .= $char;
38✔
301

302
            $i++;
38✔
303
        }
304

305
        return $result;
39✔
306
    }
307
}
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