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