Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
125 / 125
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Minify
100.00% covered (success)
100.00%
125 / 125
100.00% covered (success)
100.00%
4 / 4
23
100.00% covered (success)
100.00%
1 / 1
 all
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
15
 html
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
1 / 1
3
 css
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
3
 js
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
2
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework Minify Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\Minify;
11
12use DOMDocument;
13
14/**
15 * Class Minify.
16 *
17 * @see https://github.com/terrylinooo/CodeIgniter-Minifier
18 *
19 * @package minify
20 */
21class Minify
22{
23    /**
24     * Minify all contents, optionally can disable any.
25     *
26     * Make preference to use html() if you have not css and js code and have HTML5 tags.
27     *
28     * @param string $contents
29     * @param bool $enableHtml
30     * @param bool $enableJs
31     * @param bool $enableCss
32     *
33     * @return string
34     */
35    public static function all(
36        string $contents,
37        bool $enableHtml = true,
38        bool $enableJs = true,
39        bool $enableCss = true
40    ) : string {
41        $contents = \trim($contents);
42        if ($contents && ! ( ! $enableHtml && ! $enableCss && ! $enableJs)) {
43            if ($enableJs || $enableCss) {
44                // You need php-xml to support PHP DOM
45                $dom = new DOMDocument();
46                // Prevent DOMDocument::loadHTML error
47                $useErrors = \libxml_use_internal_errors(true);
48                $dom->loadHTML($contents, \LIBXML_HTML_NOIMPLIED | \LIBXML_HTML_NODEFDTD);
49                if ($enableJs) {
50                    // Get all script Tags and minify them
51                    $scripts = $dom->getElementsByTagName('script');
52                    foreach ($scripts as $script) {
53                        if ( ! empty($script->nodeValue)) {
54                            $script->nodeValue = static::js($script->nodeValue);
55                        }
56                    }
57                }
58                if ($enableCss) {
59                    // Get all style Tags and minify them
60                    $styles = $dom->getElementsByTagName('style');
61                    foreach ($styles as $style) {
62                        if ( ! empty($style->nodeValue)) {
63                            $style->nodeValue = static::css($style->nodeValue);
64                        }
65                    }
66                }
67                // @phpstan-ignore-next-line
68                $newContents = $enableHtml ? static::html($dom->saveHTML()) : $dom->saveHTML();
69                \libxml_use_internal_errors($useErrors);
70                unset($dom);
71            } elseif ($enableHtml) {
72                $newContents = static::html($contents);
73            }
74        }
75        return \trim($newContents ?? $contents); // @phpstan-ignore-line
76    }
77
78    /**
79     * Minify HTML.
80     *
81     * @param string $input
82     *
83     * @see https://github.com/mecha-cms/mecha-cms/blob/master/engine/kernel/converter.php
84     *
85     * @return string
86     *
87     * @author Taufik Nurrohman
88     * @license GPL version 3 License Copyright
89     */
90    public static function html(string $input) : string
91    {
92        $input = \trim($input);
93        if (empty($input)) {
94            return $input;
95        }
96        // Remove extra white-space(s) between HTML attribute(s)
97        $input = \preg_replace_callback(
98            '#<([^\/\s<>!]+)(?:\s+([^<>]*?)\s*|\s*)(\/?)>#s',
99            static function ($matches) {
100                return '<' . $matches[1] . \preg_replace(
101                    '#([^\s=]+)(\=([\'"]?)(.*?)\3)?(\s+|$)#s',
102                    ' $1$2',
103                    $matches[2]
104                ) . $matches[3] . '>';
105            },
106            \str_replace("\r", '', $input)
107        );
108        // Minify inline CSS declaration(s)
109        if (\str_contains($input, ' style=')) {
110            $input = \preg_replace_callback(
111                '#<([^<]+?)\s+style=([\'"])(.*?)\2(?=[\/\s>])#s',
112                static function ($matches) {
113                    return '<' . $matches[1] . ' style=' . $matches[2] . static::css($matches[3]) . $matches[2];
114                },
115                $input
116            );
117        }
118        return \preg_replace([
119            // t = text
120            // o = tag open
121            // c = tag close
122            // Keep important white-space(s) after self-closing HTML tag(s)
123            '#<(img|input)(>| .*?>)#s',
124            // Remove a line break and two or more white-space(s) between tag(s)
125            '#(<!--.*?-->)|(>)(?:\n*|\s{2,})(<)|^\s*|\s*$#s',
126            '#(<!--.*?-->)|(?<!\>)\s+(<\/.*?>)|(<[^\/]*?>)\s+(?!\<)#s',
127            // t+c || o+t
128            '#(<!--.*?-->)|(<[^\/]*?>)\s+(<[^\/]*?>)|(<\/.*?>)\s+(<\/.*?>)#s',
129            // o+o || c+c
130            '#(<!--.*?-->)|(<\/.*?>)\s+(\s)(?!\<)|(?<!\>)\s+(\s)(<[^\/]*?\/?>)|(<[^\/]*?\/?>)\s+(\s)(?!\<)#s',
131            // c+t || t+o || o+t -- separated by long white-space(s)
132            '#(<!--.*?-->)|(<[^\/]*?>)\s+(<\/.*?>)#s',
133            // empty tag
134            '#<(img|input)(>| .*?>)<\/\1\x1A>#s',
135            // reset previous fix
136            '#(&nbsp;)&nbsp;(?![<\s])#',
137            // clean up ...
138            // Force line-break with `&#10;` or `&#xa;`
139            '#&\#(?:10|xa);#',
140            // Force white-space with `&#32;` or `&#x20;`
141            '#&\#(?:32|x20);#',
142            // Remove HTML comment(s) except IE comment(s)
143            '#\s*<!--(?!\[if\s).*?-->\s*|(?<!\>)\n+(?=\<[^!])#s',
144        ], [
145            "<$1$2</$1\x1A>",
146            '$1$2$3',
147            '$1$2$3',
148            '$1$2$3$4$5',
149            '$1$2$3$4$5$6$7',
150            '$1$2$3',
151            '<$1$2',
152            '$1 ',
153            "\n",
154            ' ',
155            '',
156        ], $input);
157    }
158
159    /**
160     * Minify CSS.
161     *
162     * @param string $input
163     *
164     * @see http://ideone.com/Q5USEF + improvement(s)
165     *
166     * @return string
167     *
168     * @author Unknown, improved by Taufik Nurrohman
169     * @license GPL version 3 License Copyright
170     */
171    public static function css(string $input) : string
172    {
173        $input = \trim($input);
174        if (empty($input)) {
175            return $input;
176        }
177        // Force white-space(s) in `calc()`
178        if (\str_contains($input, 'calc(')) {
179            $input = \preg_replace_callback('#(?<=[\s:])calc\(\s*(.*?)\s*\)#', static function ($matches) {
180                return 'calc(' . \preg_replace('#\s+#', "\x1A", $matches[1]) . ')';
181            }, $input);
182        }
183        return \preg_replace([
184            // Remove comment(s)
185            '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')|\/\*(?!\!)(?>.*?\*\/)|^\s*|\s*$#s',
186            // Remove unused white-space(s)
187            '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|\/\*(?>.*?\*\/))|\s*+;\s*+(})\s*+|\s*+([*$~^|]?+=|[{};,>~+]|\s*+-(?![0-9\.])|!important\b)\s*+|([[(:])\s++|\s++([])])|\s++(:)\s*+(?!(?>[^{}"\']++|"(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')*+{)|^\s++|\s++\z|(\s)\s+#si',
188            // Replace `0(cm|em|ex|in|mm|pc|pt|px|vh|vw|%)` with `0`
189            '#(?<=[\s:])(0)(cm|em|ex|in|mm|pc|pt|px|vh|vw|%)#si',
190            // Replace `:0 0 0 0` with `:0`
191            '#:(0\s+0|0\s+0\s+0\s+0)(?=[;\}]|\!important)#i',
192            // Replace `background-position:0` with `background-position:0 0`
193            '#(background-position):0(?=[;\}])#si',
194            // Replace `0.6` with `.6`, but only when preceded by a white-space or `=`, `:`, `,`, `(`, `-`
195            '#(?<=[\s=:,\(\-]|&\#32;)0+\.(\d+)#s',
196            // Minify string value
197            '#(\/\*(?>.*?\*\/))|(?<!content\:)([\'"])([a-z_][-\w]*?)\2(?=[\s\{\}\];,])#si',
198            '#(\/\*(?>.*?\*\/))|(\burl\()([\'"])([^\s]+?)\3(\))#si',
199            // Minify HEX color code
200            '#(?<=[\s=:,\(]\#)([a-f0-6]+)\1([a-f0-6]+)\2([a-f0-6]+)\3#i',
201            // Replace `(border|outline):none` with `(border|outline):0`
202            '#(?<=[\{;])(border|outline):none(?=[;\}\!])#',
203            // Remove empty selector(s)
204            '#(\/\*(?>.*?\*\/))|(^|[\{\}])(?:[^\s\{\}]+)\{\}#s',
205            '#\x1A#',
206        ], [
207            '$1',
208            '$1$2$3$4$5$6$7',
209            '$1',
210            ':0',
211            '$1:0 0',
212            '.$1',
213            '$1$3',
214            '$1$2$4$5',
215            '$1$2$3',
216            '$1:0',
217            '$1$2',
218            ' ',
219        ], $input);
220    }
221
222    /**
223     * Minify JavaScript.
224     *
225     * Be careful:
226     * This method doesn't support "javascript automatic semicolon insertion", you must add
227     * semicolon by yourself, otherwise your javascript code will not work and generate error
228     * messages.
229     *
230     * @param string $input
231     *
232     * @see https://github.com/mecha-cms/mecha-cms/blob/master/engine/kernel/converter.php
233     *
234     * @return string
235     *
236     * @author Taufik Nurrohman
237     * @license GPL version 3 License Copyright
238     */
239    public static function js(string $input) : string
240    {
241        $input = \trim($input);
242        if (empty($input)) {
243            return $input;
244        }
245        return \preg_replace([
246            // Remove comment(s)
247            '#\s*("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')\s*|\s*\/\*(?!\!|@cc_on)(?>[\s\S]*?\*\/)\s*|\s*(?<![\:\=])\/\/.*(?=[\n\r]|$)|^\s*|\s*$#',
248            // Remove white-space(s) outside the string and regex
249            '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|\/\*(?>.*?\*\/)|\/(?!\/)[^\n\r]*?\/(?=[\s.,;]|[gimuy]|$))|\s*([!%&*\(\)\-=+\[\]\{\}|;:,.<>?\/])\s*#s',
250            // Remove the last semicolon
251            '#;+\}#',
252            // Minify object attribute(s) except JSON attribute(s). From `{'foo':'bar'}` to `{foo:'bar'}`
253            '#([\{,])([\'])(\d+|[a-z_]\w*)\2(?=\:)#i',
254            // --ibid. From `foo['bar']` to `foo.bar`
255            '#([\w\)\]])\[([\'"])([a-z_]\w*)\2\]#i',
256            // Replace `true` with `!0`
257            '#(?<=return |[=:,\(\[])true\b#',
258            // Replace `false` with `!1`
259            '#(?<=return |[=:,\(\[])false\b#',
260            // Clean up ...
261            '#\s*(\/\*|\*\/)\s*#',
262        ], [
263            '$1',
264            '$1$2',
265            '}',
266            '$1$3',
267            '$1.$3',
268            '!0',
269            '!1',
270            '$1',
271        ], $input);
272    }
273}