Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.31% covered (success)
97.31%
181 / 186
94.12% covered (success)
94.12%
16 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
Css
97.31% covered (success)
97.31%
181 / 186
94.12% covered (success)
94.12%
16 / 17
83
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 loadArgument
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 addMedia
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMedia
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllMedia
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeMedia
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 removeAllMedia
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parseString
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 parseFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 parseUri
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 parseCss
94.85% covered (success)
94.85%
92 / 97
0.00% covered (danger)
0.00%
0 / 1
38.20
 parseCssFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 parseCssUri
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 writeToFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 render
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
14
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseSelectors
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Css;
15
16/**
17 * Pop CSS class
18 *
19 * @category   Pop
20 * @package    Pop\Css
21 * @author     Nick Sagona, III <dev@nolainteractive.com>
22 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
23 * @license    http://www.popphp.org/license     New BSD License
24 * @version    2.0.0
25 */
26class Css extends AbstractCss
27{
28
29    /**
30     * Media queries
31     * @var array
32     */
33    protected array $media = [];
34
35    /**
36     * CSS object constructor
37     *
38     */
39    public function __construct()
40    {
41        $args = func_get_args();
42
43        foreach ($args as $arg) {
44            $this->loadArgument($arg);
45        }
46    }
47
48    /**
49     * Load argument
50     *
51     * @param  mixed $arg
52     * @return void
53     */
54    protected function loadArgument(mixed $arg): void
55    {
56        if ($arg instanceof Selector) {
57            $this->addSelector($arg);
58        } else if (($arg instanceof Comment) || is_string($arg)) {
59            $this->addComment($arg);
60        } else if ($arg instanceof Media) {
61            $this->addMedia($arg);
62        } else if (is_array($arg)) {
63            foreach ($arg as $a) {
64                $this->loadArgument($a);
65            }
66        }
67    }
68
69    /**
70     * Add media query
71     *
72     * @param  Media $media
73     * @return Css
74     */
75    public function addMedia(Media $media): Css
76    {
77        $this->media[] = $media;
78        return $this;
79    }
80
81    /**
82     * Get media query by index
83     *
84     * @param  int $i
85     * @return Media|null
86     */
87    public function getMedia(int $i): Media|null
88    {
89        return $this->media[$i] ?? null;
90    }
91
92    /**
93     * Get all media queries
94     *
95     * @return array
96     */
97    public function getAllMedia(): array
98    {
99        return $this->media;
100    }
101
102    /**
103     * Remove media query by index
104     *
105     * @param  int $i
106     * @return Css
107     */
108    public function removeMedia(int $i): Css
109    {
110        if (isset($this->media[(int)$i])) {
111            unset($this->media[(int)$i]);
112        }
113        return $this;
114    }
115
116    /**
117     * Remove all media queries
118     *
119     * @return Css
120     */
121    public function removeAllMedia(): Css
122    {
123        $this->media = [];
124        return $this;
125    }
126
127    /**
128     * Parse CSS string
129     *
130     * @param  string $cssString
131     * @return Css
132     */
133    public static function parseString(string $cssString): Css
134    {
135        $css = new self();
136        $css->parseCss($cssString);
137
138        return $css;
139    }
140
141    /**
142     * Parse CSS from file
143     *
144     * @param  string $cssFile
145     * @throws Exception
146     * @return Css
147     */
148    public static function parseFile(string $cssFile): Css
149    {
150        $css = new self();
151        $css->parseCssFile($cssFile);
152
153        return $css;
154    }
155
156    /**
157     * Parse CSS from URI
158     *
159     * @param  string $cssUri
160     * @return Css
161     */
162    public static function parseUri(string $cssUri): Css
163    {
164        $css = new self();
165        $css->parseCssUri($cssUri);
166
167        return $css;
168    }
169
170    /**
171     * Parse CSS string
172     *
173     * @param  string $cssString
174     * @return Css
175     */
176    public function parseCss(string $cssString): Css
177    {
178
179        // Parse media queries
180        $origCssString = $cssString;
181        $mediaComments = [];
182        $matches       = [];
183        preg_match_all('~@media\b[^{]*({((?:[^{}]+|(?1))*)})~', $cssString, $matches, PREG_OFFSET_CAPTURE);
184
185        if (isset($matches[0]) && isset($matches[0][0])) {
186            foreach ($matches[0] as $match) {
187                // See if media query has a top-level comment
188                $mediaComment = null;
189                $char         = null;
190                $pos          = $match[1];
191                while (($char != '/') && ($char != '}') && ($pos != 0)) {
192                    $pos--;
193                    $char = $origCssString[$pos];
194                }
195                if ($char == '/') {
196                    $mediaComment = substr($origCssString, 0, ($pos + 1));
197                    $mediaComment = substr($mediaComment, strrpos($mediaComment, '/*'));
198                    $mediaComment = explode(PHP_EOL, $mediaComment);
199                    foreach ($mediaComment as $key => $line) {
200                        $mediaComment[$key] = trim(str_replace(['/*', '*/', '*'], ['', '', ''], $line));
201                    }
202                }
203
204                $cssString      = str_replace($match[0], '', $cssString);
205                $mediaType      = null;
206                $mediaCondition = null;
207                $mediaFeatures  = [];
208                $mediaQuery     = substr($match[0], 6);
209                $mediaQuery     = trim(substr($mediaQuery, 0, strpos($mediaQuery, ' {')));
210                $mediaQueryCss  = substr($match[0], (strpos($match[0], '{') + 1));
211                $mediaQueryCss  = trim(substr($mediaQueryCss, 0, strrpos($match[0], '}')));
212
213                if (str_contains($mediaQuery, 'all')) {
214                    $mediaType = 'all';
215                } else if (str_contains($mediaQuery, 'print')) {
216                    $mediaType = 'print';
217                } else if (str_contains($mediaQuery, 'screen')) {
218                    $mediaType = 'screen';
219                } else if (str_contains($mediaQuery, 'speech')) {
220                    $mediaType = 'speech';
221                }
222
223                if (str_contains($mediaQuery, 'not')) {
224                    $mediaCondition = 'not';
225                }
226                if (str_contains($mediaQuery, 'only')) {
227                    $mediaCondition = 'only';
228                }
229
230                if ((str_contains($mediaQuery, '(')) && (str_contains($mediaQuery, ')'))) {
231                    $features = substr($mediaQuery, strpos($mediaQuery, '('));
232                    $features = substr($features, 0, (strrpos($features, ')') + 1));
233                    $features = explode('and', $features);
234                    foreach ($features as $feature) {
235                        $feature = str_replace(['(', ')'], ['', ''], $feature);
236                        $feature = explode(':', $feature);
237                        if (count($feature) == 2) {
238                            $mediaFeatures[trim($feature[0])] = trim($feature[1]);
239                        }
240                    }
241                }
242                $media = new Media($mediaType, $mediaFeatures, $mediaCondition);
243                if ($mediaComment !== null) {
244                    $media->addComment(new Comment(trim(implode(PHP_EOL, $mediaComment))));
245                }
246
247                $commentsMatches = [];
248                preg_match_all('!/\*.*?\*/!s', $mediaQueryCss, $commentsMatches, PREG_OFFSET_CAPTURE);
249
250                if (isset($commentsMatches[0]) && isset($commentsMatches[0][0])) {
251                    foreach ($commentsMatches[0] as $match) {
252                        $selectorName = substr($mediaQueryCss, $match[1]);
253                        $selectorName = substr($selectorName, 0, strpos($selectorName, '{'));
254                        $selectorName = trim(substr($selectorName, (strpos($selectorName, '*/') + 2)));
255                        $comment = explode(PHP_EOL, $match[0]);
256                        foreach ($comment as $key => $line) {
257                            $comment[$key] = trim(str_replace(['/*', '*/', '*'], ['', '', ''], $line));
258                        }
259                        $mediaComments[$selectorName] = new Comment(trim(implode(PHP_EOL, $comment)));
260                    }
261                }
262
263                $mediaQueryCss = preg_replace('!/\*.*?\*/!s', '', $mediaQueryCss);
264                $mediaQueryCss = preg_replace('/\n\s*\n/', "\n", $mediaQueryCss);
265
266                $selectors = $this->parseSelectors($mediaQueryCss);
267                foreach ($selectors as $selector) {
268                    $media->addSelector($selector);
269                }
270
271                if (count($mediaComments) > 0) {
272                    foreach ($mediaComments as $selectorName => $comment) {
273                        if ($media->hasSelector($selectorName)) {
274                            $media->getSelector($selectorName)->addComment($comment);
275                        }
276                    }
277                }
278
279                $this->addMedia($media);
280            }
281        }
282
283        // Parse comments
284        $comments = [];
285        $matches  = [];
286        preg_match_all('!/\*.*?\*/!s', $cssString, $matches, PREG_OFFSET_CAPTURE);
287
288        if (isset($matches[0]) && isset($matches[0][0])) {
289            foreach ($matches[0] as $match) {
290                $selectorName = null;
291                if ($match[1] != 0) {
292                    $selectorName = substr($cssString, $match[1]);
293                    $selectorName = substr($selectorName, 0, strpos($selectorName, '{'));
294                    $selectorName = trim(substr($selectorName, (strpos($selectorName, '*/') + 2)));
295                }
296                $comment = explode(PHP_EOL, $match[0]);
297                foreach ($comment as $key => $line) {
298                    $comment[$key] = trim(str_replace(['/*', '*/', '*'], ['', '', ''], $line));
299                }
300                if ($selectorName === null) {
301                    $this->addComment(new Comment(trim(implode(PHP_EOL, $comment))));
302                } else {
303                    $comments[$selectorName] = new Comment(trim(implode(PHP_EOL, $comment)));
304                }
305            }
306        }
307
308        $cssString = preg_replace('!/\*.*?\*/!s', '', $cssString);
309        $cssString = preg_replace('/\n\s*\n/', "\n", $cssString);
310
311        // Parse everything else
312        $selectors = $this->parseSelectors($cssString);
313        foreach ($selectors as $selector) {
314            $this->addSelector($selector);
315        }
316
317        if (count($comments) > 0) {
318            foreach ($comments as $selectorName => $comment) {
319                if ($this->hasSelector($selectorName)) {
320                    $this->getSelector($selectorName)->addComment($comment);
321                }
322            }
323        }
324
325        return $this;
326    }
327
328    /**
329     * Parse CSS string from file
330     *
331     * @param  string $cssFile
332     * @throws Exception
333     * @return Css
334     */
335    public function parseCssFile(string $cssFile): Css
336    {
337        if (!file_exists($cssFile)) {
338            throw new Exception("Error: That file '" . $cssFile . "' does not exist.");
339        }
340        return $this->parseCss(file_get_contents($cssFile));
341    }
342
343    /**
344     * Parse CSS string from URI
345     *
346     * @param  string $cssUri
347     * @return Css
348     */
349    public function parseCssUri(string $cssUri): Css
350    {
351        return $this->parseCss(file_get_contents($cssUri));
352    }
353
354    /**
355     * Method to write CSS to file
356     *
357     * @param  string $filename
358     * @return void
359     */
360    public function writeToFile(string $filename): void
361    {
362        file_put_contents($filename, $this->render());
363    }
364
365    /**
366     * Method to render the selector CSS
367     *
368     * @return string
369     */
370    public function render(): string
371    {
372        $css = '';
373
374        if (!$this->minify) {
375            foreach ($this->comments as $comment) {
376                $css .= (string)$comment . PHP_EOL;
377            }
378        }
379        foreach ($this->elements as $element) {
380            if (isset($this->selectors[$element])) {
381                $selector = $this->selectors[$element];
382                $selector->minify($this->minify);
383                $css .= (string)$selector;
384                if (!$this->minify) {
385                    $css .= PHP_EOL;
386                }
387            }
388        }
389        foreach ($this->ids as $id) {
390            if (isset($this->selectors[$id])) {
391                $selector = $this->selectors[$id];
392                $selector->minify($this->minify);
393                $css .= (string)$selector;
394                if (!$this->minify) {
395                    $css .= PHP_EOL;
396                }
397            }
398        }
399        foreach ($this->classes as $class) {
400            if (isset($this->selectors[$class])) {
401                $selector = $this->selectors[$class];
402                $selector->minify($this->minify);
403                $css .= (string)$selector;
404                if (!$this->minify) {
405                    $css .= PHP_EOL;
406                }
407            }
408        }
409        foreach ($this->media as $media) {
410            $media->minify($this->minify);
411            $css .= (string)$media;
412            if (!$this->minify) {
413                $css .= PHP_EOL;
414            }
415        }
416
417        return $css;
418    }
419
420    /**
421     * To string method
422     *
423     * @return string
424     */
425    public function __toString(): string
426    {
427        return $this->render();
428    }
429
430    /**
431     * Method to parse the CSS selectors from a string
432     *
433     * @param  string $cssString
434     * @return array
435     */
436    protected function parseSelectors(string $cssString): array
437    {
438        $selectors = [];
439
440        $matches = [];
441        preg_match_all('/\{\s*([^}]*?)\s*}/m', $cssString, $matches, PREG_OFFSET_CAPTURE);
442
443        if (isset($matches[0]) && isset($matches[0][0])) {
444            $curPos = 0;
445            foreach ($matches[0] as $match) {
446                $selectorName = trim(substr($cssString, $curPos, $match[1]));
447                if (strpos($selectorName, '{') !== false) {
448                    $selectorName = trim(substr($selectorName, 0, strpos($selectorName, '{')));
449                }
450                $rules    = explode(';', trim(str_replace(['{', '}'], ['', ''], trim($match[0]))));
451                $cssRules = [];
452                foreach ($rules as $key => $value) {
453                    if (!empty($value)) {
454                        $value = trim($value);
455                        $v = explode(':', $value);
456                        if (count($v) == 2) {
457                            $cssRules[trim($v[0])] = trim($v[1]);
458                        }
459                    }
460                }
461
462                $selector = new Selector($selectorName);
463                $selector->setProperties($cssRules);
464                $selectors[] = $selector;
465                $curPos = $match[1] + strlen($match[0]);
466            }
467        }
468
469        return $selectors;
470    }
471
472}