Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.1055 diff -u -r1.1055 common.inc --- includes/common.inc 2 Dec 2009 15:09:16 -0000 1.1055 +++ includes/common.inc 2 Dec 2009 19:47:08 -0000 @@ -3285,7 +3285,7 @@ ), ); $rendered_css = array(); - $inline_css = ''; + $inline_css = array(); $external_css = ''; $preprocess_items = array(); foreach ($css as $data => $item) { @@ -3308,8 +3308,8 @@ } break; case 'inline': - // Include inline stylesheets. - $inline_css .= drupal_load_stylesheet_content($item['data'], $item['preprocess']); + // Group inline stylesheets on media type. + $inline_css[$item['media']][] = $item; break; case 'external': // Preprocessing for external CSS files is ignored. @@ -3332,17 +3332,61 @@ $rendered_css['preprocess'] .= theme('html_tag', array('element' => $element)); } } - // Enclose the inline CSS with the style tag if required. - if (!empty($inline_css)) { - $element = $css_element; - $element['#tag'] = 'style'; - $element['#value'] = $inline_css; - unset($element['#attributes']['rel']); - $inline_css = "\n" . theme('html_tag', array('element' => $element)); + + foreach ($inline_css as $media => $items) { + $element = array( + '#tag' => 'style', + '#attributes' => array( + 'type' => 'text/css', + ), + // Aggregate inline CSS by media type but don't replace @import rules. + '#value' => "\n" . drupal_build_css_aggregate($items, TRUE, FALSE), + ); + + // Only set media attribute when it's not "all". + if ($media != 'all') { + $element['#attributes']['media'] = $media; + } + + $inline_css[$media] = theme('html_tag', array('element' => $element)); } // Output all the CSS files with the inline stylesheets showing up last. - return implode($rendered_css) . $external_css . $inline_css; + return implode($rendered_css) . $external_css . implode($inline_css); +} + +/** + * Aggregate a set of CSS stylesheets into a single stylesheet. + * + * @param $css + * An array of CSS stylesheets to aggregate. + * @param $optimize + * (optional) Boolean whether CSS contents should be optimized. When set to TRUE + * individual stylesheet settings can override this. Defaults to TRUE. + * @param $flatten + * (optional) Boolean whether @import rules should be replaced with the + * actual stylesheet content. When set to TRUE individual stylesheet settings + * can override this. Defaults to TRUE. + * @return + * Contents of the aggregated stylesheet, including any replaced @import rules. + */ +function drupal_build_css_aggregate($css, $optimize = TRUE, $flatten = TRUE) { + $imports = ''; + $contents = ''; + + foreach ($css as $stylesheet) { + // Preprocessing for external CSS files is ignored. + if ($stylesheet['type'] == 'file') { + $data = drupal_load_stylesheet($stylesheet['data'], $optimize, $flatten, TRUE); + } + elseif ($stylesheet['type'] == 'inline') { + $data = drupal_load_stylesheet_content($stylesheet['data'], $stylesheet['preprocess'], $flatten, "", TRUE); + } + $imports .= $data['imports']; + $contents .= $data['contents'] . "\n"; + } + + return $imports . $contents; } /** @@ -3356,31 +3400,12 @@ * The name of the CSS file. */ function drupal_build_css_cache($css, $filename) { - $data = ''; - // Create the css/ within the files folder. $csspath = 'public://css'; file_prepare_directory($csspath, FILE_CREATE_DIRECTORY); if (!file_exists($csspath . '/' . $filename)) { // Build aggregate CSS file. - foreach ($css as $stylesheet) { - // Only 'file' stylesheets can be aggregated. - if ($stylesheet['type'] == 'file') { - $contents = drupal_load_stylesheet($stylesheet['data'], TRUE); - // Return the path to where this CSS file originated from. - $base = base_path() . dirname($stylesheet['data']) . '/'; - _drupal_build_css_path(NULL, $base); - // Prefix all paths within this CSS file, ignoring external and absolute paths. - $data .= preg_replace_callback('/url\([\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\)/i', '_drupal_build_css_path', $contents); - } - } - - // Per the W3C specification at http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, - // @import rules must proceed any other style, so we move those to the top. - $regexp = '/@import[^;]+;/i'; - preg_match_all($regexp, $data, $matches); - $data = preg_replace($regexp, '', $data); - $data = implode('', $matches[0]) . $data; + $data = drupal_build_css_aggregate($css); // Create the CSS file. file_unmanaged_save_data($data, $csspath . '/' . $filename, FILE_EXISTS_REPLACE); @@ -3389,29 +3414,43 @@ } /** - * Helper function for drupal_build_css_cache(). + * Helper function to normalize paths used in stylesheets as relative URLs may + * break when aggregating these stylesheets. * - * This function will prefix all paths within a CSS file. - */ -function _drupal_build_css_path($matches, $base = NULL) { - $_base = &drupal_static(__FUNCTION__); - // Store base path for preg_replace_callback. - if (isset($base)) { - $_base = $base; + * @param $path + * PAth to be normalized. + * @param $base_directory + * (optional) Prefix relative paths with this directory. + * @return + * The normalized path. + */ +function _drupal_build_css_path($path, $base_directory = NULL) { + if ($path{0} != '/') { + // Prefix relative paths with the base URL path and an optional base directory. + if (!empty($base_directory)) { + $path = base_path() . $base_directory . '/' . $path; + } + else { + $path = base_path() . $path; + } } - // Prefix with base and remove '../' segments where possible. - $path = $_base . $matches[1]; $last = ''; while ($path != $last) { $last = $path; + + // Replace '/./' segments for '/'. + $path = str_replace('/./', '/', $path); + + // Remove '../' segments where possible. $path = preg_replace('`(^|/)(?!\.\./)([^/]+)/\.\./`', '$1', $path); } - return 'url(' . $path . ')'; + + return $path; } /** - * Loads the stylesheet and resolves all @import commands. + * Load and process a stylesheet for aggregation. * * Loads a stylesheet and replaces @import commands with the contents of the * imported file. Use this instead of file_get_contents when processing @@ -3422,37 +3461,30 @@ * color.module enabled themes with CSS aggregation turned off. * * @param $file - * Name of the stylesheet to be processed. + * Filename of the stylesheet to be processed. * @param $optimize - * Defines if CSS contents should be compressed or not. + * (optional) Boolean whether CSS contents should be optimized. Defaults to + * TRUE. + * @param $flatten + * (optional) Boolean whether @import commands should be replaced with the + * actual stylesheet content. Defaults to TRUE. + * @param $return_imports + * (internal) When set to TRUE an array with seperate @import rules will be + * returned. The array has two keys: + * - 'imports': All non-replaced @import rules. + * - 'contents': Optimized and/or flattened stylesheet * @return - * Contents of the stylesheet, including any resolved @import commands. + * Contents of the (optimized) stylesheet, including any replaced @import rules + * or FALSE on failure. */ -function drupal_load_stylesheet($file, $optimize = NULL) { - // $_optimize does not use drupal_static as it is set by $optimize. - static $_optimize; - // Store optimization parameter for preg_replace_callback with nested @import loops. - if (isset($optimize)) { - $_optimize = $optimize; - } - - $contents = ''; - if (file_exists($file)) { - // Load the local CSS stylesheet. - $contents = file_get_contents($file); - - // Change to the current stylesheet's directory. - $cwd = getcwd(); - chdir(dirname($file)); - - // Process the stylesheet. - $contents = drupal_load_stylesheet_content($contents, $_optimize); - - // Change back directory. - chdir($cwd); +function drupal_load_stylesheet($file, $optimize = TRUE, $flatten = TRUE, $return_imports = FALSE) { + if (!is_readable($file)) { + return FALSE; } - return $contents; + $contents = file_get_contents($file); + // Load and process the contents of the stylesheet. + return drupal_load_stylesheet_content($contents, $optimize, $flatten, dirname($file), $return_imports); } /** @@ -3461,45 +3493,187 @@ * @param $contents * The contents of the stylesheet. * @param $optimize - * (optional) Boolean whether CSS contents should be minified. Defaults to - * FALSE. + * (optional) Boolean whether CSS contents should be optimized. Defaults to + * TRUE. + * @param $flatten + * (optional) Boolean whether @import commands should be replaced with the + * actual stylesheet content. Defaults to TRUE. + * @param $base_directory + * (optional) Relative paths will be normalized using this base directory. + * When not provided the drupal base path will be used. + * @param $return_imports + * (internal) When set to TRUE an array with seperate @import rules will be + * returned. The array has two keys: + * - 'imports': All non-replaced @import rules. + * - 'contents': Optimized and/or flattened stylesheet * @return * Contents of the stylesheet including the imported stylesheets. */ -function drupal_load_stylesheet_content($contents, $optimize = FALSE) { +function drupal_load_stylesheet_content($contents, $optimize = TRUE, $flatten = TRUE, $base_directory = NULL, $return_imports = FALSE) { // Remove multiple charset declarations for standards compliance (and fixing Safari problems). $contents = preg_replace('/^@charset\s+[\'"](\S*)\b[\'"];/i', '', $contents); + // Unify all (line)breaks to make the next regular expression easier. + $contents = str_replace(array("\r\n", "\f", "\r"), "\n", $contents); + + // Find all strings and comments in the stylesheet and the chunks in between. + // The regular expressions for strings and comments are modified versions of + // the ones found here: http://www.w3.org/TR/CSS21/syndata.html#tokenization. + $chunks = preg_split('< + ("(?:[^\n"\\\\]|\\\\(?i:\n|[0-9a-f]{1,6}[\040\n\t]?|[^\n0-9a-f]))*") | + (\'(?:[^\n\'\\\\]|\\\\(?i:\n|[0-9a-f]{1,6}[\040\n\t]?|[^\n0-9a-f]))*\') | + (?:(/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)) | + (@import\s[^;]+;) | + (url\([^)]+\)) | + (\s+) + >x', $contents, NULL, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + + $contents = ''; + $imports = ''; + $keep_next_comment = FALSE; + $skip_imports = FALSE; + // Iterate over all the chunks and rebuild contents. + foreach ($chunks as $i => $chunk) { + $start = substr($chunk, 0, 2); + $end = substr($chunk, -2); + $has_prev = isset($chunks[$i-1]); + $has_next = isset($chunks[$i+1]); + + // Process strings. + if (($start{0} == '"' && $end{1} == '"') || + ($start{0} == "'" && $end{1} == "'")) { + $contents .= $chunk; + } + // Process comments. + elseif ($start == '/*' && $end == '*/') { + if (!$optimize) { + $contents .= $chunk; + } + // Keep IE-mac hack start comment + elseif (substr($chunk, -3) == '\*/') { + // Use an empty comment as this won't hurt the hack. + $contents .= '/*\*/'; + $keep_next_comment = TRUE; + } + elseif ($keep_next_comment) { + $contents .= $chunk; + $keep_next_comment = FALSE; + } + // Keep comments that come right before or after ">" or ":" characters + // as those are probably hacks: + // http://www.webdevout.net/css-hacks#in_css-selectors. + elseif (($has_prev && preg_match('/[>:]/', substr($chunks[$i-1], -1))) || + ($has_next && preg_match('/[>:]/', $chunks[$i+1]{0}))) { + // Use an empty comment as this won't hurt the hack. + $contents .= '/**/'; + continue; + } + } + // Process @import rules. + elseif (substr($chunk, 0, 7) == '@import' && $end{1} == ';') { + preg_match('/^(?:url\()?[\'"]?([^\'")]+)[\'"]?\)?(.*)$/i', trim(substr($chunk, 7, -1)), $matches); + $url = $matches[1]; + $media_list = $matches[2]; + $is_external = url_is_external($url) || substr($url, 0, 5) == 'data:'; + if (!$is_external) { + $url = _drupal_build_css_path($url, $base_directory); + $path = substr($url, strlen(base_path())); + } + + // @import rules must precede all other rules. + if ($skip_imports) { + $contents .= $chunk; + } + // Restore @import rule when flattening is disabled, url is external, + // a media list is provided or when drupal_load_stylesheet() fails. + // Per the W3C specification at http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, + // @import rules must proceed any other style, so we keep them seperate. + elseif ((!$flatten || $is_external || !empty($media_list)) || + ($css = drupal_load_stylesheet($path, $optimize, $flatten, TRUE)) === FALSE) { + $imports .= '@import url(' . $url . ')' . $media_list . ";\n"; + } + // Replace @import rules with the actual stylesheet content. + // This happens recursively but omits external files. + else { + $imports .= $css['imports']; + $contents .= $css['contents']; + } + } + // Process URLs. + elseif (substr($chunk, 0, 4) == 'url(' && $end{1} == ')') { + $url = trim(substr($chunk, 4, -1), "'\" \n\t"); + // Paths needs to be normalized as relative URLs may break when aggregating + // this CSS file. + if (!url_is_external($url) && substr($url, 0, 5) != 'data:') { + $url = _drupal_build_css_path($url, $base_directory); + } + $contents .= 'url(' . $url . ')'; + } + // Process whitespace. + elseif ($chunk{0} == " " || $chunk{0} == "\n" || $chunk{0} == "\t") { + if (!$optimize) { + $contents .= $chunk; + } + // Remove whitespace before and after separators. + elseif (($has_prev && preg_match('/[@{}:;,+>~]/', substr($chunks[$i-1], -1))) || + ($has_next && preg_match('/[@{}:;,+>~]/', $chunks[$i+1]{0}))) { + $contents .= ''; + } + // A single space around parentheses is sufficient. + elseif (($has_prev && substr($chunks[$i-1], -1) == ')') || + ($has_next && $chunks[$i+1]{0} == '(')) { + $contents .= $chunk{0}; + } + // Remove whitespace before and after comments. + elseif (($has_prev && substr($chunks[$i-1], -2) == '*/') || + ($has_next && substr($chunks[$i+1], 0, 2) == '/*')) { + $contents .= ''; + } + else { + $contents .= $chunk; + } + } + // Proces anything else. + else { + // From now on don't flatten @import rules, as they must precede all + // other rules (http://www.w3.org/TR/CSS21/cascade.html#at-import). + $skip_imports = TRUE; + + if (!$optimize) { + $contents .= $chunk; + } + else { + // Remove unnecessary last ";" from rulesets. + if ($chunk{0} == '}' && substr($contents, -1) == ';') { + $contents = substr($contents, 0, -1); + } + // Leave at least 1 space between the first-letter and first-line pseudo + // elements and "{" (http://www.crankygeek.com/ie6pebug/). + elseif ($chunk{0} == '{' && (substr($contents, -13) == ':first-letter' || + substr($contents, -11) == ':first-line')) { + $contents .= ' '; + } + + $chunk = str_replace(';}', '}', $chunk); + + $contents .= $chunk; + } + } + } + if ($optimize) { - // Perform some safe CSS optimizations. - $contents = preg_replace('{ - (?<=\\\\\*/)([^/\*]+/\*)([^\*/]+\*/) # Add a backslash also at the end ie-mac hack comment, so the next pass will not touch it. - # The added backshlash does not affect the effectiveness of the hack. - }x', '\1\\\\\2', $contents); - $contents = preg_replace('< - \s*([@{}:;,]|\)\s|\s\()\s* | # Remove whitespace around separators, but keep space around parentheses. - /\*[^*\\\\]*\*+([^/*][^*]*\*+)*/ | # Remove comments that are not CSS hacks. - >x', '\1', $contents); - } - - // Replaces @import commands with the actual stylesheet content. - // This happens recursively but omits external files. - $contents = preg_replace_callback('/@import\s*(?:url\()?[\'"]?(?![a-z]+:)([^\'"\()]+)[\'"]?\)?;/', '_drupal_load_stylesheet', $contents); - return $contents; -} + $contents = trim($contents); + } -/** - * Loads stylesheets recursively and returns contents with corrected paths. - * - * This function is used for recursive loading of stylesheets and - * returns the stylesheet content with all url() paths corrected. - */ -function _drupal_load_stylesheet($matches) { - $filename = $matches[1]; - // Load the imported stylesheet and replace @import commands in there as well. - $file = drupal_load_stylesheet($filename); - // Alter all url() paths, but not external. - return preg_replace('/url\(([\'"]?)(?![a-z]+:)([^\'")]+)[\'"]?\)?;/i', 'url(\1' . dirname($filename) . '/', $file); + if ($return_imports) { + return array( + 'imports' => $imports, + 'contents' => $contents + ); + } + else { + return $imports . $contents; + } } /** Index: modules/color/color.module =================================================================== RCS file: /cvs/drupal/drupal/modules/color/color.module,v retrieving revision 1.77 diff -u -r1.77 color.module --- modules/color/color.module 25 Nov 2009 06:38:16 -0000 1.77 +++ modules/color/color.module 2 Dec 2009 19:47:08 -0000 @@ -337,18 +337,10 @@ } foreach ($files as $file) { - // Aggregate @imports recursively for each configured top level CSS file - // without optimization. Aggregation and optimization will be + // Flatten @import rules recursively for each configured top level CSS + // file without optimization. Aggregation and optimization will be // handled by drupal_build_css_cache() only. - $style = drupal_load_stylesheet($paths['source'] . $file, FALSE); - - // Return the path to where this CSS file originated from, stripping - // off the name of the file at the end of the path. - $base = base_path() . dirname($paths['source'] . $file) . '/'; - _drupal_build_css_path(NULL, $base); - - // Prefix all paths within this CSS file, ignoring absolute paths. - $style = preg_replace_callback('/url\([\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\)/i', '_drupal_build_css_path', $style); + $style = drupal_load_stylesheet($paths['source'] . $file, FALSE, TRUE); // Rewrite stylesheet with new colors. $style = _color_rewrite_stylesheet($theme, $info, $paths, $palette, $style);