TinyHtmlMinifier.php
286 lines
| 1 | <?php |
| 2 | |
| 3 | namespace Minifier; |
| 4 | |
| 5 | class TinyHtmlMinifier |
| 6 | { |
| 7 | private $options; |
| 8 | private $output; |
| 9 | private $build; |
| 10 | private $skip; |
| 11 | private $skipName; |
| 12 | private $head; |
| 13 | private $elements; |
| 14 | |
| 15 | public function __construct(array $options) |
| 16 | { |
| 17 | $this->options = $options; |
| 18 | $this->output = ''; |
| 19 | $this->build = []; |
| 20 | $this->skip = 0; |
| 21 | $this->skipName = ''; |
| 22 | $this->head = false; |
| 23 | $this->elements = [ |
| 24 | 'skip' => [ |
| 25 | 'code', |
| 26 | 'pre', |
| 27 | 'script', |
| 28 | 'textarea', |
| 29 | ], |
| 30 | 'inline' => [ |
| 31 | 'a', |
| 32 | 'abbr', |
| 33 | 'acronym', |
| 34 | 'b', |
| 35 | 'bdo', |
| 36 | 'big', |
| 37 | 'br', |
| 38 | 'cite', |
| 39 | 'code', |
| 40 | 'dfn', |
| 41 | 'em', |
| 42 | 'i', |
| 43 | 'img', |
| 44 | 'kbd', |
| 45 | 'map', |
| 46 | 'object', |
| 47 | 'samp', |
| 48 | 'small', |
| 49 | 'span', |
| 50 | 'strong', |
| 51 | 'sub', |
| 52 | 'sup', |
| 53 | 'tt', |
| 54 | 'var', |
| 55 | 'q', |
| 56 | ], |
| 57 | 'hard' => [ |
| 58 | '!doctype', |
| 59 | 'body', |
| 60 | 'html', |
| 61 | ] |
| 62 | ]; |
| 63 | } |
| 64 | |
| 65 | // Run minifier |
| 66 | public function minify(string $html) : string |
| 67 | { |
| 68 | if (!isset($this->options['disable_comments']) || |
| 69 | !$this->options['disable_comments']) { |
| 70 | $html = $this->removeComments($html); |
| 71 | } |
| 72 | |
| 73 | $rest = $html; |
| 74 | |
| 75 | while (!empty($rest)) { |
| 76 | $parts = explode('<', $rest, 2); |
| 77 | $this->walk($parts[0]); |
| 78 | $rest = (isset($parts[1])) ? $parts[1] : ''; |
| 79 | } |
| 80 | |
| 81 | return $this->output; |
| 82 | } |
| 83 | |
| 84 | // Walk trough html |
| 85 | private function walk(&$part) |
| 86 | { |
| 87 | $tag_parts = explode('>', $part); |
| 88 | $tag_content = $tag_parts[0]; |
| 89 | |
| 90 | if (!empty($tag_content)) { |
| 91 | $name = $this->findName($tag_content); |
| 92 | $element = $this->toElement($tag_content, $part, $name); |
| 93 | $type = $this->toType($element); |
| 94 | |
| 95 | if ($name == 'head') { |
| 96 | $this->head = $type === 'open'; |
| 97 | } |
| 98 | |
| 99 | $this->build[] = [ |
| 100 | 'name' => $name, |
| 101 | 'content' => $element, |
| 102 | 'type' => $type |
| 103 | ]; |
| 104 | |
| 105 | $this->setSkip($name, $type); |
| 106 | |
| 107 | if (!empty($tag_content)) { |
| 108 | $content = (isset($tag_parts[1])) ? $tag_parts[1] : ''; |
| 109 | if ($content !== '') { |
| 110 | $this->build[] = [ |
| 111 | 'content' => $this->compact($content, $name, $element), |
| 112 | 'type' => 'content' |
| 113 | ]; |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | $this->buildHtml(); |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | // Remove comments |
| 122 | private function removeComments($content = '') |
| 123 | { |
| 124 | return preg_replace('/(?=<!--)([\s\S]*?)-->/', '', $content); |
| 125 | } |
| 126 | |
| 127 | // Check if string contains string |
| 128 | private function contains($needle, $haystack) |
| 129 | { |
| 130 | return strpos($haystack, $needle) !== false; |
| 131 | } |
| 132 | |
| 133 | // Return type of element |
| 134 | private function toType($element) |
| 135 | { |
| 136 | return (substr($element, 1, 1) == '/') ? 'close' : 'open'; |
| 137 | } |
| 138 | |
| 139 | // Create element |
| 140 | private function toElement($element, $noll, $name) |
| 141 | { |
| 142 | $element = $this->stripWhitespace($element); |
| 143 | $element = $this->addChevrons($element, $noll); |
| 144 | $element = $this->removeSelfSlash($element); |
| 145 | $element = $this->removeMeta($element, $name); |
| 146 | return $element; |
| 147 | } |
| 148 | |
| 149 | // Remove unneeded element meta |
| 150 | private function removeMeta($element, $name) |
| 151 | { |
| 152 | if ($name == 'style') { |
| 153 | $element = str_replace( |
| 154 | [ |
| 155 | ' type="text/css"', |
| 156 | "' type='text/css'" |
| 157 | ], |
| 158 | ['', ''], |
| 159 | $element |
| 160 | ); |
| 161 | } elseif ($name == 'script') { |
| 162 | $element = str_replace( |
| 163 | [ |
| 164 | ' type="text/javascript"', |
| 165 | " type='text/javascript'" |
| 166 | ], |
| 167 | ['', ''], |
| 168 | $element |
| 169 | ); |
| 170 | } |
| 171 | return $element; |
| 172 | } |
| 173 | |
| 174 | // Strip whitespace from element |
| 175 | private function stripWhitespace($element) |
| 176 | { |
| 177 | if ($this->skip == 0) { |
| 178 | $element = preg_replace('/\s+/', ' ', $element); |
| 179 | } |
| 180 | return trim($element); |
| 181 | } |
| 182 | |
| 183 | // Add chevrons around element |
| 184 | private function addChevrons($element, $noll) |
| 185 | { |
| 186 | if (empty($element)) { |
| 187 | return $element; |
| 188 | } |
| 189 | $char = ($this->contains('>', $noll)) ? '>' : ''; |
| 190 | $element = '<' . $element . $char; |
| 191 | return $element; |
| 192 | } |
| 193 | |
| 194 | // Remove unneeded self slash |
| 195 | private function removeSelfSlash($element) |
| 196 | { |
| 197 | if (substr($element, -3) == ' />') { |
| 198 | $element = substr($element, 0, -3) . '>'; |
| 199 | } |
| 200 | return $element; |
| 201 | } |
| 202 | |
| 203 | // Compact content |
| 204 | private function compact($content, $name, $element) |
| 205 | { |
| 206 | if ($this->skip != 0) { |
| 207 | $name = $this->skipName; |
| 208 | } else { |
| 209 | $content = preg_replace('/\s+/', ' ', $content); |
| 210 | } |
| 211 | |
| 212 | if (in_array($name, $this->elements['skip'])) { |
| 213 | return $content; |
| 214 | } elseif (in_array($name, $this->elements['hard']) || |
| 215 | $this->head) { |
| 216 | return $this->minifyHard($content); |
| 217 | } else { |
| 218 | return $this->minifyKeepSpaces($content); |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | // Build html |
| 223 | private function buildHtml() |
| 224 | { |
| 225 | foreach ($this->build as $build) { |
| 226 | |
| 227 | if (!empty($this->options['collapse_whitespace'])) { |
| 228 | |
| 229 | if (strlen(trim($build['content'])) == 0) |
| 230 | continue; |
| 231 | |
| 232 | elseif ($build['type'] != 'content' && !in_array($build['name'], $this->elements['inline'])) |
| 233 | trim($build['content']); |
| 234 | |
| 235 | } |
| 236 | |
| 237 | $this->output .= $build['content']; |
| 238 | } |
| 239 | |
| 240 | $this->build = []; |
| 241 | } |
| 242 | |
| 243 | // Find name by part |
| 244 | private function findName($part) |
| 245 | { |
| 246 | $name_cut = explode(" ", $part, 2)[0]; |
| 247 | $name_cut = explode(">", $name_cut, 2)[0]; |
| 248 | $name_cut = explode("\n", $name_cut, 2)[0]; |
| 249 | $name_cut = preg_replace('/\s+/', '', $name_cut); |
| 250 | $name_cut = strtolower(str_replace('/', '', $name_cut)); |
| 251 | return $name_cut; |
| 252 | } |
| 253 | |
| 254 | // Set skip if elements are blocked from minification |
| 255 | private function setSkip($name, $type) |
| 256 | { |
| 257 | foreach ($this->elements['skip'] as $element) { |
| 258 | if ($element == $name && $this->skip == 0) { |
| 259 | $this->skipName = $name; |
| 260 | } |
| 261 | } |
| 262 | if (in_array($name, $this->elements['skip'])) { |
| 263 | if ($type == 'open') { |
| 264 | $this->skip++; |
| 265 | } |
| 266 | if ($type == 'close') { |
| 267 | $this->skip--; |
| 268 | } |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | // Minify all, even spaces between elements |
| 273 | private function minifyHard($element) |
| 274 | { |
| 275 | $element = preg_replace('!\s+!', ' ', $element); |
| 276 | $element = trim($element); |
| 277 | return trim($element); |
| 278 | } |
| 279 | |
| 280 | // Strip but keep one space |
| 281 | private function minifyKeepSpaces($element) |
| 282 | { |
| 283 | return preg_replace('!\s+!', ' ', $element); |
| 284 | } |
| 285 | } |
| 286 |