Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 namespace Drupal\Core\Asset;
|
Chris@0
|
4
|
Chris@0
|
5 use Drupal\Component\Utility\Unicode;
|
Chris@0
|
6
|
Chris@0
|
7 /**
|
Chris@0
|
8 * Optimizes a CSS asset.
|
Chris@0
|
9 */
|
Chris@0
|
10 class CssOptimizer implements AssetOptimizerInterface {
|
Chris@0
|
11
|
Chris@0
|
12 /**
|
Chris@0
|
13 * The base path used by rewriteFileURI().
|
Chris@0
|
14 *
|
Chris@0
|
15 * @var string
|
Chris@0
|
16 */
|
Chris@0
|
17 public $rewriteFileURIBasePath;
|
Chris@0
|
18
|
Chris@0
|
19 /**
|
Chris@0
|
20 * {@inheritdoc}
|
Chris@0
|
21 */
|
Chris@0
|
22 public function optimize(array $css_asset) {
|
Chris@0
|
23 if ($css_asset['type'] != 'file') {
|
Chris@0
|
24 throw new \Exception('Only file CSS assets can be optimized.');
|
Chris@0
|
25 }
|
Chris@0
|
26 if (!$css_asset['preprocess']) {
|
Chris@0
|
27 throw new \Exception('Only file CSS assets with preprocessing enabled can be optimized.');
|
Chris@0
|
28 }
|
Chris@0
|
29
|
Chris@0
|
30 return $this->processFile($css_asset);
|
Chris@0
|
31 }
|
Chris@0
|
32
|
Chris@0
|
33 /**
|
Chris@0
|
34 * Processes the contents of a CSS asset for cleanup.
|
Chris@0
|
35 *
|
Chris@0
|
36 * @param string $contents
|
Chris@0
|
37 * The contents of the CSS asset.
|
Chris@0
|
38 *
|
Chris@0
|
39 * @return string
|
Chris@0
|
40 * Contents of the CSS asset.
|
Chris@0
|
41 */
|
Chris@0
|
42 public function clean($contents) {
|
Chris@0
|
43 // Remove multiple charset declarations for standards compliance (and fixing
|
Chris@0
|
44 // Safari problems).
|
Chris@0
|
45 $contents = preg_replace('/^@charset\s+[\'"](\S*?)\b[\'"];/i', '', $contents);
|
Chris@0
|
46
|
Chris@0
|
47 return $contents;
|
Chris@0
|
48 }
|
Chris@0
|
49
|
Chris@0
|
50 /**
|
Chris@0
|
51 * Build aggregate CSS file.
|
Chris@0
|
52 */
|
Chris@0
|
53 protected function processFile($css_asset) {
|
Chris@0
|
54 $contents = $this->loadFile($css_asset['data'], TRUE);
|
Chris@0
|
55
|
Chris@0
|
56 $contents = $this->clean($contents);
|
Chris@0
|
57
|
Chris@0
|
58 // Get the parent directory of this file, relative to the Drupal root.
|
Chris@0
|
59 $css_base_path = substr($css_asset['data'], 0, strrpos($css_asset['data'], '/'));
|
Chris@0
|
60 // Store base path.
|
Chris@0
|
61 $this->rewriteFileURIBasePath = $css_base_path . '/';
|
Chris@0
|
62
|
Chris@0
|
63 // Anchor all paths in the CSS with its base URL, ignoring external and absolute paths.
|
Chris@0
|
64 return preg_replace_callback('/url\(\s*[\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\s*\)/i', [$this, 'rewriteFileURI'], $contents);
|
Chris@0
|
65 }
|
Chris@0
|
66
|
Chris@0
|
67 /**
|
Chris@0
|
68 * Loads the stylesheet and resolves all @import commands.
|
Chris@0
|
69 *
|
Chris@0
|
70 * Loads a stylesheet and replaces @import commands with the contents of the
|
Chris@0
|
71 * imported file. Use this instead of file_get_contents when processing
|
Chris@0
|
72 * stylesheets.
|
Chris@0
|
73 *
|
Chris@0
|
74 * The returned contents are compressed removing white space and comments only
|
Chris@0
|
75 * when CSS aggregation is enabled. This optimization will not apply for
|
Chris@0
|
76 * color.module enabled themes with CSS aggregation turned off.
|
Chris@0
|
77 *
|
Chris@0
|
78 * Note: the only reason this method is public is so color.module can call it;
|
Chris@0
|
79 * it is not on the AssetOptimizerInterface, so future refactorings can make
|
Chris@0
|
80 * it protected.
|
Chris@0
|
81 *
|
Chris@0
|
82 * @param $file
|
Chris@0
|
83 * Name of the stylesheet to be processed.
|
Chris@0
|
84 * @param $optimize
|
Chris@0
|
85 * Defines if CSS contents should be compressed or not.
|
Chris@0
|
86 * @param $reset_basepath
|
Chris@0
|
87 * Used internally to facilitate recursive resolution of @import commands.
|
Chris@0
|
88 *
|
Chris@0
|
89 * @return
|
Chris@0
|
90 * Contents of the stylesheet, including any resolved @import commands.
|
Chris@0
|
91 */
|
Chris@0
|
92 public function loadFile($file, $optimize = NULL, $reset_basepath = TRUE) {
|
Chris@0
|
93 // These statics are not cache variables, so we don't use drupal_static().
|
Chris@0
|
94 static $_optimize, $basepath;
|
Chris@0
|
95 if ($reset_basepath) {
|
Chris@0
|
96 $basepath = '';
|
Chris@0
|
97 }
|
Chris@0
|
98 // Store the value of $optimize for preg_replace_callback with nested
|
Chris@0
|
99 // @import loops.
|
Chris@0
|
100 if (isset($optimize)) {
|
Chris@0
|
101 $_optimize = $optimize;
|
Chris@0
|
102 }
|
Chris@0
|
103
|
Chris@0
|
104 // Stylesheets are relative one to each other. Start by adding a base path
|
Chris@0
|
105 // prefix provided by the parent stylesheet (if necessary).
|
Chris@0
|
106 if ($basepath && !file_uri_scheme($file)) {
|
Chris@0
|
107 $file = $basepath . '/' . $file;
|
Chris@0
|
108 }
|
Chris@0
|
109 // Store the parent base path to restore it later.
|
Chris@0
|
110 $parent_base_path = $basepath;
|
Chris@0
|
111 // Set the current base path to process possible child imports.
|
Chris@0
|
112 $basepath = dirname($file);
|
Chris@0
|
113
|
Chris@0
|
114 // Load the CSS stylesheet. We suppress errors because themes may specify
|
Chris@0
|
115 // stylesheets in their .info.yml file that don't exist in the theme's path,
|
Chris@0
|
116 // but are merely there to disable certain module CSS files.
|
Chris@0
|
117 $content = '';
|
Chris@0
|
118 if ($contents = @file_get_contents($file)) {
|
Chris@0
|
119 // If a BOM is found, convert the file to UTF-8, then use substr() to
|
Chris@0
|
120 // remove the BOM from the result.
|
Chris@0
|
121 if ($encoding = (Unicode::encodingFromBOM($contents))) {
|
Chris@17
|
122 $contents = mb_substr(Unicode::convertToUtf8($contents, $encoding), 1);
|
Chris@0
|
123 }
|
Chris@0
|
124 // If no BOM, check for fallback encoding. Per CSS spec the regex is very strict.
|
Chris@0
|
125 elseif (preg_match('/^@charset "([^"]+)";/', $contents, $matches)) {
|
Chris@0
|
126 if ($matches[1] !== 'utf-8' && $matches[1] !== 'UTF-8') {
|
Chris@0
|
127 $contents = substr($contents, strlen($matches[0]));
|
Chris@0
|
128 $contents = Unicode::convertToUtf8($contents, $matches[1]);
|
Chris@0
|
129 }
|
Chris@0
|
130 }
|
Chris@0
|
131
|
Chris@0
|
132 // Return the processed stylesheet.
|
Chris@0
|
133 $content = $this->processCss($contents, $_optimize);
|
Chris@0
|
134 }
|
Chris@0
|
135
|
Chris@0
|
136 // Restore the parent base path as the file and its children are processed.
|
Chris@0
|
137 $basepath = $parent_base_path;
|
Chris@0
|
138 return $content;
|
Chris@0
|
139 }
|
Chris@0
|
140
|
Chris@0
|
141 /**
|
Chris@0
|
142 * Loads stylesheets recursively and returns contents with corrected paths.
|
Chris@0
|
143 *
|
Chris@0
|
144 * This function is used for recursive loading of stylesheets and
|
Chris@0
|
145 * returns the stylesheet content with all url() paths corrected.
|
Chris@0
|
146 *
|
Chris@0
|
147 * @param array $matches
|
Chris@0
|
148 * An array of matches by a preg_replace_callback() call that scans for
|
Chris@0
|
149 * @import-ed CSS files, except for external CSS files.
|
Chris@0
|
150 *
|
Chris@0
|
151 * @return
|
Chris@0
|
152 * The contents of the CSS file at $matches[1], with corrected paths.
|
Chris@0
|
153 *
|
Chris@0
|
154 * @see \Drupal\Core\Asset\AssetOptimizerInterface::loadFile()
|
Chris@0
|
155 */
|
Chris@0
|
156 protected function loadNestedFile($matches) {
|
Chris@0
|
157 $filename = $matches[1];
|
Chris@0
|
158 // Load the imported stylesheet and replace @import commands in there as
|
Chris@0
|
159 // well.
|
Chris@0
|
160 $file = $this->loadFile($filename, NULL, FALSE);
|
Chris@0
|
161
|
Chris@0
|
162 // Determine the file's directory.
|
Chris@0
|
163 $directory = dirname($filename);
|
Chris@0
|
164 // If the file is in the current directory, make sure '.' doesn't appear in
|
Chris@0
|
165 // the url() path.
|
Chris@0
|
166 $directory = $directory == '.' ? '' : $directory . '/';
|
Chris@0
|
167
|
Chris@0
|
168 // Alter all internal url() paths. Leave external paths alone. We don't need
|
Chris@0
|
169 // to normalize absolute paths here because that will be done later.
|
Chris@0
|
170 return preg_replace('/url\(\s*([\'"]?)(?![a-z]+:|\/+)([^\'")]+)([\'"]?)\s*\)/i', 'url(\1' . $directory . '\2\3)', $file);
|
Chris@0
|
171 }
|
Chris@0
|
172
|
Chris@0
|
173 /**
|
Chris@0
|
174 * Processes the contents of a stylesheet for aggregation.
|
Chris@0
|
175 *
|
Chris@0
|
176 * @param $contents
|
Chris@0
|
177 * The contents of the stylesheet.
|
Chris@0
|
178 * @param $optimize
|
Chris@0
|
179 * (optional) Boolean whether CSS contents should be minified. Defaults to
|
Chris@0
|
180 * FALSE.
|
Chris@0
|
181 *
|
Chris@0
|
182 * @return
|
Chris@0
|
183 * Contents of the stylesheet including the imported stylesheets.
|
Chris@0
|
184 */
|
Chris@0
|
185 protected function processCss($contents, $optimize = FALSE) {
|
Chris@0
|
186 // Remove unwanted CSS code that cause issues.
|
Chris@0
|
187 $contents = $this->clean($contents);
|
Chris@0
|
188
|
Chris@0
|
189 if ($optimize) {
|
Chris@0
|
190 // Perform some safe CSS optimizations.
|
Chris@0
|
191 // Regexp to match comment blocks.
|
Chris@17
|
192 $comment = '/\*[^*]*\*+(?:[^/*][^*]*\*+)*/';
|
Chris@0
|
193 // Regexp to match double quoted strings.
|
Chris@0
|
194 $double_quot = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"';
|
Chris@0
|
195 // Regexp to match single quoted strings.
|
Chris@0
|
196 $single_quot = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'";
|
Chris@0
|
197 // Strip all comment blocks, but keep double/single quoted strings.
|
Chris@0
|
198 $contents = preg_replace(
|
Chris@0
|
199 "<($double_quot|$single_quot)|$comment>Ss",
|
Chris@0
|
200 "$1",
|
Chris@0
|
201 $contents
|
Chris@0
|
202 );
|
Chris@0
|
203 // Remove certain whitespace.
|
Chris@0
|
204 // There are different conditions for removing leading and trailing
|
Chris@0
|
205 // whitespace.
|
Chris@0
|
206 // @see http://php.net/manual/regexp.reference.subpatterns.php
|
Chris@0
|
207 $contents = preg_replace('<
|
Chris@0
|
208 # Do not strip any space from within single or double quotes
|
Chris@0
|
209 (' . $double_quot . '|' . $single_quot . ')
|
Chris@0
|
210 # Strip leading and trailing whitespace.
|
Chris@0
|
211 | \s*([@{};,])\s*
|
Chris@0
|
212 # Strip only leading whitespace from:
|
Chris@0
|
213 # - Closing parenthesis: Retain "@media (bar) and foo".
|
Chris@0
|
214 | \s+([\)])
|
Chris@0
|
215 # Strip only trailing whitespace from:
|
Chris@0
|
216 # - Opening parenthesis: Retain "@media (bar) and foo".
|
Chris@0
|
217 # - Colon: Retain :pseudo-selectors.
|
Chris@0
|
218 | ([\(:])\s+
|
Chris@0
|
219 >xSs',
|
Chris@0
|
220 // Only one of the four capturing groups will match, so its reference
|
Chris@0
|
221 // will contain the wanted value and the references for the
|
Chris@0
|
222 // two non-matching groups will be replaced with empty strings.
|
Chris@0
|
223 '$1$2$3$4',
|
Chris@0
|
224 $contents
|
Chris@0
|
225 );
|
Chris@0
|
226 // End the file with a new line.
|
Chris@0
|
227 $contents = trim($contents);
|
Chris@0
|
228 $contents .= "\n";
|
Chris@0
|
229 }
|
Chris@0
|
230
|
Chris@0
|
231 // Replaces @import commands with the actual stylesheet content.
|
Chris@0
|
232 // This happens recursively but omits external files.
|
Chris@0
|
233 $contents = preg_replace_callback('/@import\s*(?:url\(\s*)?[\'"]?(?![a-z]+:)(?!\/\/)([^\'"\()]+)[\'"]?\s*\)?\s*;/', [$this, 'loadNestedFile'], $contents);
|
Chris@0
|
234
|
Chris@0
|
235 return $contents;
|
Chris@0
|
236 }
|
Chris@0
|
237
|
Chris@0
|
238 /**
|
Chris@0
|
239 * Prefixes all paths within a CSS file for processFile().
|
Chris@0
|
240 *
|
Chris@0
|
241 * Note: the only reason this method is public is so color.module can call it;
|
Chris@0
|
242 * it is not on the AssetOptimizerInterface, so future refactorings can make
|
Chris@0
|
243 * it protected.
|
Chris@0
|
244 *
|
Chris@0
|
245 * @param array $matches
|
Chris@0
|
246 * An array of matches by a preg_replace_callback() call that scans for
|
Chris@0
|
247 * url() references in CSS files, except for external or absolute ones.
|
Chris@0
|
248 *
|
Chris@0
|
249 * @return string
|
Chris@0
|
250 * The file path.
|
Chris@0
|
251 */
|
Chris@0
|
252 public function rewriteFileURI($matches) {
|
Chris@0
|
253 // Prefix with base and remove '../' segments where possible.
|
Chris@0
|
254 $path = $this->rewriteFileURIBasePath . $matches[1];
|
Chris@0
|
255 $last = '';
|
Chris@0
|
256 while ($path != $last) {
|
Chris@0
|
257 $last = $path;
|
Chris@0
|
258 $path = preg_replace('`(^|/)(?!\.\./)([^/]+)/\.\./`', '$1', $path);
|
Chris@0
|
259 }
|
Chris@0
|
260 return 'url(' . file_url_transform_relative(file_create_url($path)) . ')';
|
Chris@0
|
261 }
|
Chris@0
|
262
|
Chris@0
|
263 }
|