annotate core/lib/Drupal/Core/Asset/CssOptimizer.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
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 }