Assets_processing::js()   C
last analyzed

Complexity

Conditions 11
Paths 32

Size

Total Lines 82
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 38
CRAP Score 11

Importance

Changes 0
Metric Value
cc 11
eloc 38
nc 32
nop 1
dl 0
loc 82
ccs 38
cts 38
cp 1
crap 11
rs 5.2653
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @package CleverStyle Framework
4
 * @author  Nazar Mokrynskyi <[email protected]>
5
 * @license 0BSD
6
 */
7
namespace cs\Page;
8
9
/**
10
 * Class includes few methods used for processing CSS, JS and HTML files before putting into cache
11
 *
12
 * This is because CSS and HTML files may include other CSS, JS files, images, fonts and so on with absolute and relative paths.
13
 * Methods of this class handle all this assets, applies basic minification to CSS and JS files and produce single resulting file, nested files are also copied
14
 * to target directory and processed if needed.
15
 */
16
class Assets_processing {
17
	/**
18
	 * Analyses file for images, fonts and css links and include they content into single resulting css file.
19
	 *
20
	 * Supports next file extensions for possible assets:
21
	 * jpeg, jpe, jpg, gif, png, ttf, ttc, svg, svgz, woff, css
22
	 *
23
	 * @param string   $data                   Content of processed file
24
	 * @param string   $file                   Path to file, that contains specified in previous parameter content
25
	 * @param string   $target_directory_path  Target directory for resulting combined files
26
	 * @param string[] $not_embedded_resources Some resources like images and fonts might not be embedded into resulting CSS because of their size
27
	 *
28
	 * @return string    $data
29
	 */
30 15
	public static function css ($data, $file, $target_directory_path = PUBLIC_CACHE, &$not_embedded_resources = []) {
31 15
		$dir = dirname($file);
32
		/**
33
		 * Remove comments, tabs and new lines
34
		 */
35 15
		$data = preg_replace('#(/\*.*?\*/)|\t|\n|\r#s', ' ', $data);
36
		/**
37
		 * Remove unnecessary spaces
38
		 */
39 15
		$data = preg_replace('/\s*([,;>{}(])\s*/', '$1', $data);
40 15
		$data = preg_replace('/\s+/', ' ', $data);
41
		/**
42
		 * Return spaces required in media queries
43
		 */
44 15
		$data = preg_replace('/\s(and|or)\(/', ' $1 (', $data);
45
		/**
46
		 * Duplicated semicolons
47
		 */
48 15
		$data = preg_replace('/;+/m', ';', $data);
49
		/**
50
		 * Minify rgb colors declarations
51
		 */
52 15
		$data = preg_replace_callback(
53 15
			'/rgb\(([0-9,.]+)\)/i',
54 15
			function ($rgb) {
55 9
				$rgb = explode(',', $rgb[1]);
56
				return
57
					'#'.
58 9
					str_pad(dechex($rgb[0]), 2, 0, STR_PAD_LEFT).
59 9
					str_pad(dechex($rgb[1]), 2, 0, STR_PAD_LEFT).
60 9
					str_pad(dechex($rgb[2]), 2, 0, STR_PAD_LEFT);
61 15
			},
62 15
			$data
63
		);
64
		/**
65
		 * Minify repeated colors declarations
66
		 */
67 15
		$data = preg_replace('/#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3/i', '#$1$2$3', $data);
68
		/**
69
		 * Remove unnecessary zeros
70
		 */
71 15
		$data = preg_replace('/(\D)0\.(\d+)/i', '$1.$2', $data);
72
		/**
73
		 * Unnecessary spaces around colons (should have whitespace character after, otherwise `.class :disabled` will be handled incorrectly)
74
		 */
75 15
		$data = preg_replace('/\s*:\s+/', ':', $data);
76
		/**
77
		 * Assets processing
78
		 */
79
		// TODO: replace by loop, track duplicated stuff that are subject to inlining and if they appear more than once, don't inline them
80 15
		$data = preg_replace_callback(
81 15
			'/url\((.*)\)|@import\s*(?:url\()?\s*([\'"].*[\'"])\s*\)??.*;/U',
82 15
			function ($match) use ($dir, $target_directory_path, &$not_embedded_resources) {
83 15
				$path_matched = $match[2] ?? $match[1];
84 15
				$path         = trim($path_matched, '\'" ');
85 15
				$link         = explode('?', $path, 2)[0];
86 15
				if (!static::is_relative_path_and_exists($link, $dir)) {
87 6
					return $match[0];
88
				}
89 15
				$extension     = file_extension($link);
90 15
				$absolute_path = static::absolute_path($link, $dir);
91 15
				$content       = file_get_contents($absolute_path);
92
				/**
93
				 * Process recursively CSS imports, but ignore non embedded resources there
94
				 */
95
				/** @noinspection UsageOfSilenceOperatorInspection */
96 15
				if ($extension == 'css' && @$match[2]) {
97 9
					$content = static::css($content, $absolute_path, $target_directory_path);
98
				}
99 15
				$filename = static::file_put_contents_with_hash($target_directory_path, $extension, $content);
100
				/** @noinspection UsageOfSilenceOperatorInspection */
101
				/**
102
				 * We ignore files with query parameters as well as CSS imports with media queries
103
				 */
104 15
				if (strpos($path, '?') === false && !trim(@$match[3])) {
105 9
					$not_embedded_resources[] = str_replace(getcwd(), '', "$target_directory_path/$filename");
106
				}
107 15
				return str_replace($path_matched, "'./$filename'", $match[0]);
108 15
			},
109 15
			$data
110
		);
111 15
		return trim($data);
112
	}
113
	/**
114
	 * Put `$content` into `$dir` where filename is `md5($content)` with specified extension
115
	 *
116
	 * @param string $dir
117
	 * @param string $extension
118
	 * @param string $content
119
	 *
120
	 * @return string Filename (without full path)
121
	 */
122 15
	protected static function file_put_contents_with_hash ($dir, $extension, $content) {
123 15
		$hash = md5($content);
124 15
		file_put_contents("$dir/$hash.$extension", $content, LOCK_EX | FILE_BINARY);
125 15
		return "$hash.$extension";
126
	}
127
	/**
128
	 * Simple and fast JS minification
129
	 *
130
	 * @param string $data
131
	 *
132
	 * @return string
133
	 */
134 15
	public static function js ($data) {
135
		/**
136
		 * Split into array of lines
137
		 */
138 15
		$data = explode("\n", $data);
139
		/**
140
		 * Flag that is `true` when inside comment
141
		 */
142 15
		$in_comment              = false;
143 15
		$continue_after_position = -1;
144 15
		foreach ($data as $index => &$current_line) {
145 15
			if ($continue_after_position >= $index) {
146 3
				continue;
147
			}
148 15
			$next_line = isset($data[$index + 1]) ? trim($data[$index + 1]) : '';
149
			/**
150
			 * Remove starting and trailing spaces
151
			 */
152 15
			$current_line = trim($current_line);
153
			/**
154
			 * Remove single-line comments
155
			 */
156 15
			if (mb_strpos($current_line, '//') === 0) {
157 15
				$current_line = '';
158 15
				continue;
159
			}
160
			/**
161
			 * Starts with multi-line comment
162
			 */
163 15
			if (mb_strpos($current_line, '/*') === 0) {
164 15
				$in_comment = true;
165
			}
166 15
			if (!$in_comment) {
167 15
				$backticks_position = strpos($current_line, '`');
168
				/**
169
				 * Handling template strings can be tricky (since they might be multi-line), so let's fast-forward to the last backticks position and continue
170
				 * from there
171
				 */
172 15
				if ($backticks_position !== false) {
173 9
					$last_item_with_backticks = array_keys(
174 9
						array_filter(
175 9
							$data,
176 9
							function ($d) {
177 9
								return strpos($d, '`') !== false;
178 9
							}
179
						)
180
					);
181 9
					$last_item_with_backticks = array_pop($last_item_with_backticks);
182 9
					if ($last_item_with_backticks > $index) {
183 3
						$continue_after_position = $last_item_with_backticks;
184 3
						continue;
185
					}
186
				}
187
				/**
188
				 * Add new line at the end if only needed
189
				 */
190 15
				if (static::new_line_needed($current_line, $next_line)) {
191 9
					$current_line .= "\n";
192
				}
193
				/**
194
				 * Single-line comment
195
				 */
196 15
				$current_line = preg_replace('#^\s*//[^\'"]+$#', '', $current_line);
197
				/**
198
				 * If we are not sure - just add new line afterwards
199
				 */
200 15
				$current_line = preg_replace('#//.*$#', "\\0\n", $current_line);
201
			} else {
202
				/**
203
				 * End of multi-line comment
204
				 */
205 15
				if (strpos($current_line, '*/') !== false) {
206 15
					$current_line = explode('*/', $current_line)[1];
207 15
					$in_comment   = false;
208
				} else {
209 15
					$current_line = '';
210
				}
211
			}
212
		}
213 15
		$data = implode('', $data);
214 15
		$data = str_replace('</script>', '<\/script>', $data);
215 15
		return trim($data, ';').';';
216
	}
217
	/**
218
	 * @param string $current_line
219
	 * @param string $next_line
220
	 *
221
	 * @return bool
222
	 */
223 15
	protected static function new_line_needed ($current_line, $next_line) {
224
		/**
225
		 * Set of symbols that are safe to be concatenated without new line with anything else
226
		 */
227
		$regexp = /** @lang PhpRegExp */
228 15
			'[:;,.+\-*/{}?><^\'"\[\]=&(]';
229
		return
230 15
			$current_line &&
231 15
			$next_line &&
232 15
			!preg_match("#$regexp\$#", $current_line) &&
233 15
			!preg_match("#^$regexp#", $next_line);
234
	}
235
	/**
236
	 * Analyses file for scripts and styles, combines them into resulting files in order to optimize loading process
237
	 * (files with combined scripts and styles will be created)
238
	 *
239
	 * @param string   $data                   Content of processed file
240
	 * @param string   $file                   Path to file, that contains specified in previous parameter content
241
	 * @param string   $target_directory_path  Target directory for resulting combined files
242
	 * @param bool     $vulcanization          Whether to put combined files separately or to make included assets built-in (vulcanization)
243
	 * @param string[] $not_embedded_resources Resources like images/fonts might not be embedded into resulting CSS because of big size or CSS/JS because of CSP
244
	 *
245
	 * @return string
246
	 */
247 12
	public static function html ($data, $file, $target_directory_path, $vulcanization, &$not_embedded_resources = []) {
248 12
		static::html_process_links_and_styles($data, $file, $target_directory_path, $vulcanization, $not_embedded_resources);
249 12
		static::html_process_scripts($data, $file, $target_directory_path, $vulcanization, $not_embedded_resources);
250
		// Removing HTML comments (those that are mostly likely comments, to avoid problems)
251 12
		$data = preg_replace_callback(
252 12
			'/^\s*<!--([^>-].*[^-])?-->/Ums',
253 12
			function ($matches) {
254 12
				return mb_strpos('--', $matches[1]) === false ? '' : $matches[0];
255 12
			},
256 12
			$data
257
		);
258 12
		return preg_replace("/\n+/", "\n", $data);
259
	}
260
	/**
261
	 * @param string   $data                   Content of processed file
262
	 * @param string   $file                   Path to file, that contains specified in previous parameter content
263
	 * @param string   $target_directory_path  Target directory for resulting combined files
264
	 * @param bool     $vulcanization          Whether to put combined files separately or to make included assets built-in (vulcanization)
265
	 * @param string[] $not_embedded_resources Resources like images/fonts might not be embedded into resulting CSS because of big size or CSS/JS because of CSP
266
	 */
267 12
	protected static function html_process_scripts (&$data, $file, $target_directory_path, $vulcanization, &$not_embedded_resources) {
268 12
		if (!preg_match_all('/<script(.*)<\/script>/Uims', $data, $scripts)) {
269 12
			return;
270
		}
271 12
		$scripts_content    = '';
272 12
		$scripts_to_replace = [];
273 12
		$dir                = dirname($file);
274 12
		foreach ($scripts[1] as $index => $script) {
275 12
			$script = explode('>', $script, 2);
276 12
			if (preg_match('/src\s*=\s*[\'"](.*)[\'"]/Uims', $script[0], $url)) {
277 12
				$url = $url[1];
278 12
				if (!static::is_relative_path_and_exists($url, $dir)) {
279 6
					continue;
280
				}
281 12
				$scripts_to_replace[] = $scripts[0][$index];
282 12
				$scripts_content      .= file_get_contents("$dir/$url").";\n";
283
			} else {
284 12
				$scripts_to_replace[] = $scripts[0][$index];
285 12
				$scripts_content      .= "$script[1];\n";
286
			}
287
		}
288 12
		$scripts_content = static::js($scripts_content);
289 12
		if (!$scripts_to_replace) {
290 6
			return;
291
		}
292
		// Remove all scripts
293 12
		$data = str_replace($scripts_to_replace, '', $data);
294
		/**
295
		 * If vulcanization is not used - put contents into separate file, and put link to it, otherwise put minified content back
296
		 */
297 12
		if (!$vulcanization) {
298 3
			$filename = static::file_put_contents_with_hash($target_directory_path, 'js', $scripts_content);
299
			// Add script with combined content file to the end
300 3
			$data                     .= "<script src=\"./$filename\"></script>";
301 3
			$not_embedded_resources[] = str_replace(getcwd(), '', "$target_directory_path/$filename");
302
		} else {
303
			// Add combined content inline script to the end
304 9
			$data .= "<script>$scripts_content</script>";
305
		}
306 12
	}
307
	/**
308
	 * @param string   $data                   Content of processed file
309
	 * @param string   $file                   Path to file, that contains specified in previous parameter content
310
	 * @param string   $target_directory_path  Target directory for resulting combined files
311
	 * @param bool     $vulcanization          Whether to put combined files separately or to make included assets built-in (vulcanization)
312
	 * @param string[] $not_embedded_resources Resources like images/fonts might not be embedded into resulting CSS because of big size or CSS/JS because of CSP
313
	 */
314 12
	protected static function html_process_links_and_styles (&$data, $file, $target_directory_path, $vulcanization, &$not_embedded_resources) {
315 12
		if (!preg_match_all('/<link(.*)>|<style(.*)<\/style>/Uims', $data, $links_and_styles)) {
316 12
			return;
317
		}
318 12
		$dir = dirname($file);
319
		// With reverse order we can add inlined CSS imports directly to the beginning of `<template>` element
320 12
		$links_and_styles = array_map(
321 12
			function ($item) {
322 12
				return array_reverse($item, true);
323 12
			},
324 12
			$links_and_styles
325
		);
326 12
		foreach ($links_and_styles[1] as $index => $link) {
327
			/**
328
			 * For plain styles we do not do anything fancy besides minifying its sources (no rearrangement or anything like that)
329
			 */
330 12
			if (mb_strpos($links_and_styles[0][$index], '</style>') > 0) {
331 12
				$content = explode('>', $links_and_styles[2][$index], 2)[1];
332 12
				$data    = str_replace(
333 12
					$content,
334 12
					static::css($content, $file, $target_directory_path, $not_embedded_resources),
335 12
					$data
336
				);
337 12
				continue;
338
			}
339 12
			if (!static::has_relative_href($link, $url, $dir)) {
340 6
				continue;
341
			}
342 12
			$import = preg_match('/rel\s*=\s*[\'"]import[\'"]/Uim', $link);
343
			/**
344
			 * CSS imports are available in Polymer alongside with HTML imports
345
			 */
346 12
			$css_import = $import && preg_match('/type\s*=\s*[\'"]css[\'"]/Uim', $link);
347 12
			$stylesheet = preg_match('/rel\s*=\s*[\'"]stylesheet[\'"]/Uim', $link);
348
			/**
349
			 * TODO: Polymer only supports `custom-style > style`, but no `link`-based counterpart, so we can't provide CSP-compatibility in general,
350
			 * thus always inlining styles into HTML
351
			 */
352 12
			if ($css_import || $stylesheet) {
353
				/**
354
				 * If content is link to CSS file
355
				 */
356 12
				$css  = static::css(
357 12
					file_get_contents("$dir/$url"),
358 12
					"$dir/$url",
359 12
					$target_directory_path,
360 12
					$not_embedded_resources
361
				);
362 12
				$data = preg_replace(
363 12
					'/'.preg_quote($links_and_styles[0][$index], '/').'(.*)<template>/Uims',
364 12
					"$1<template><style>$css</style>",
365 12
					$data
366
				);
367 12
				$data = preg_replace(
368 12
					'/(<template><style>[^<]+)<\/style><style>/Uim',
369 12
					'$1',
370 12
					$data
371
				);
372 6
			} elseif ($import) {
373
				/**
374
				 * If content is HTML import
375
				 */
376 6
				$data = str_replace(
377 6
					$links_and_styles[0][$index],
378 6
					static::html(
379 6
						file_get_contents("$dir/$url"),
380 6
						"$dir/$url",
381 6
						$target_directory_path,
382 6
						$vulcanization,
383 6
						$not_embedded_resources
384
					),
385 12
					$data
386
				);
387
			}
388
		}
389 12
	}
390
	/**
391
	 * @param string $link
392
	 * @param string $url
393
	 * @param string $dir
394
	 *
395
	 * @return bool
396
	 */
397 12
	protected static function has_relative_href ($link, &$url, $dir) {
398
		$result =
399 12
			$link &&
400 12
			preg_match('/href\s*=\s*[\'"](.*)[\'"]/Uims', $link, $url);
0 ignored issues
show
Bug introduced by
$url of type string is incompatible with the type null|array expected by parameter $matches of preg_match(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

400
			preg_match('/href\s*=\s*[\'"](.*)[\'"]/Uims', $link, /** @scrutinizer ignore-type */ $url);
Loading history...
401 12
		if ($result && static::is_relative_path_and_exists($url[1], $dir)) {
402 12
			$url = $url[1];
403 12
			return true;
404
		}
405 6
		return false;
406
	}
407
	/**
408
	 * @param string $path
409
	 * @param string $dir
410
	 *
411
	 * @return bool
412
	 */
413 15
	protected static function is_relative_path_and_exists ($path, $dir) {
414 15
		return $dir && !preg_match('#^https?://#i', $path) && file_exists(static::absolute_path($path, $dir));
415
	}
416
	/**
417
	 * @param string $path
418
	 * @param string $dir
419
	 *
420
	 * @return string
421
	 */
422 15
	protected static function absolute_path ($path, $dir) {
423 15
		if (strpos($path, '/') === 0) {
424 9
			return realpath(getcwd().$path);
425
		}
426 15
		return realpath("$dir/$path");
427
	}
428
}
429