Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.1080 diff -u -p -r1.1080 common.inc --- includes/common.inc 7 Jan 2010 07:45:03 -0000 1.1080 +++ includes/common.inc 7 Jan 2010 23:21:57 -0000 @@ -3239,21 +3239,10 @@ function drupal_add_css($data = NULL, $o * A string of XHTML CSS tags. */ function drupal_get_css($css = NULL) { - $output = ''; if (!isset($css)) { $css = drupal_add_css(); } - $preprocess_css = (variable_get('preprocess_css', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')); - $directory = file_directory_path('public'); - $is_writable = is_dir($directory) && is_writable($directory); - - // A dummy query-string is added to filenames, to gain control over - // browser-caching. The string changes on every update or full cache - // flush, forcing browsers to load a new copy of the files, as the - // URL changed. - $query_string = '?' . substr(variable_get('css_js_query_string', '0'), 0, 1); - // Allow modules to alter the css items. drupal_alter('css', $css); @@ -3273,75 +3262,276 @@ function drupal_get_css($css = NULL) { } } - // If CSS preprocessing is off, we still need to output the styles. - // Additionally, go through any remaining styles if CSS preprocessing is on - // and output the non-cached ones. - $css_element = array( - '#tag' => 'link', - '#attributes' => array( - 'type' => 'text/css', - 'rel' => 'stylesheet', - ), - ); - $rendered_css = array(); - $inline_css = ''; - $external_css = ''; - $preprocess_items = array(); + // Loop through each item in the $css array and place it into a group. + // Regardless of the order of the items in the $css array, we order all the + // 'file' types ahead of the 'external' types, and the 'external' types ahead + // of the 'inline' types. + // @todo We need a comment explaining why we do that. There's probably a good + // reason, but it's not at all obvious what it is. + // Also, we put all files that can be aggregated (ones whose 'preprocess' + // flag is TRUE) and that are for the same media type into the same group. + // If files that can be aggregated are interleaved with files that can't be + // (in terms of their order within the $css array), or if files that can be + // aggregated but are for different media types are interleaved with each + // other, then this potentially results in the stylesheet rules being applied + // in a different order than specified by the $css array. This is done to + // optimize aggregation (it enables the fewest possible aggregate files), but + // since aggregation can be enabled/disabled, it is also done even if + // aggregation is disabled, so that enabling/disabling the setting doesn't + // change the order of CSS rules. A module or theme can implement + // hook_css_groups_alter() to change the grouping logic (for example, to + // always preserve the rule order specified in the $css array). + $file_groups = array(); + $inline_groups = array(); + $external_groups = array(); foreach ($css as $data => $item) { - // Loop through each of the stylesheets, including them appropriately based - // on their type. switch ($item['type']) { + // If a file can be aggregated (whether or not the site-wide aggregation + // setting is enabled), put it the item into the group for its media type. + // Otherwise, put it into its own group. case 'file': - // Depending on whether aggregation is desired, include the file. - if (!$item['preprocess'] || !($is_writable && $preprocess_css)) { - $element = $css_element; - $element['#attributes']['media'] = $item['media']; - $element['#attributes']['href'] = file_create_url($item['data']) . $query_string; - $rendered_css[] = theme('html_tag', array('element' => $element)); + if ($item['preprocess']) { + $group_key = 'preprocess_' . $item['media']; + if (!isset($file_groups[$group_key])) { + $file_groups[$group_key] = array( + 'preprocess' => $item['preprocess'], + 'media' => $item['media'], + 'type' => $item['type'], + 'items' => array(), + ); + } + $file_groups[$group_key]['items'][] = $item; + } + else { + $file_groups[] = array( + 'preprocess' => $item['preprocess'], + 'media' => $item['media'], + 'type' => $item['type'], + 'items' => array($item), + ); + } + break; + // Put all inline items of the same media into the same group. + case 'inline': + $group_key = 'inline_' . $item['media']; + if (!isset($inline_groups[$group_key])) { + $inline_groups[$group_key] = array( + 'preprocess' => $item['preprocess'], + 'type' => $item['type'], + 'media' => $item['media'], + 'items' => array(), + ); } + $inline_groups[$group_key]['items'][] = $item; + break; + // Put each external item in its own group. + case 'external': + $external_groups[] = array( + 'preprocess' => $item['preprocess'], + 'media' => $item['media'], + 'type' => $item['type'], + 'items' => array($item), + ); + break; + } + } + $css_groups = array('groups' => array_merge($file_groups, $external_groups, $inline_groups)); + + // Allow modules to alter the groupings and register alternate handlers for + // the remaining steps. + drupal_alter('css_groups', $css_groups, $css); + + // Call the preprocess handler to preprocess the groups (for example, to + // generate aggregate files). + $preprocess_handler = (!empty($css_groups['preprocess handler']) && function_exists($css_groups['preprocess handler'])) ? $css_groups['preprocess handler'] : '_drupal_css_preprocess'; + $preprocess_handler($css_groups); + + // Call the markup handler to generate the needed LINK and STYLE tags. + $markup_handler = (!empty($css_groups['markup handler']) && function_exists($css_groups['markup handler'])) ? $css_groups['markup handler'] : '_drupal_css_markup'; + return $markup_handler($css_groups); +} + +/** + * Default CSS group preprocess handler. + * + * @todo Add documentation. + */ +function _drupal_css_preprocess(&$css_groups) { + $preprocess_css = (variable_get('preprocess_css', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')); + $directory = file_directory_path('public'); + $is_writable = is_dir($directory) && is_writable($directory); + + // A dummy query-string is added to filenames, to gain control over + // browser-caching. The string changes on every update or full cache + // flush, forcing browsers to load a new copy of the files, as the + // URL changed. + $query_string = '?' . substr(variable_get('css_js_query_string', '0'), 0, 1); + + // Loop through each group and prepare the group and its items with + // information that will be needed by the markup handler. + foreach ($css_groups['groups'] as &$group) { + switch ($group['type']) { + case 'file': + // If the group can be aggregated into a single file, do so, and set the + // group's href to the URL of that file. + if ($group['preprocess'] && $preprocess_css && $is_writable) { + $filename = 'css_' . md5(serialize($group['items']) . $query_string) . '.css'; + $group['href'] = file_create_url(drupal_build_css_cache($group['items'], $filename)); + } + // Otherwise, set the href of each item. else { - $preprocess_items[$item['media']][] = $item; - // Mark the position of the preprocess element, - // it should be at the position of the first preprocessed file. - $rendered_css['preprocess'] = ''; + foreach ($group['items'] as &$item) { + $item['href'] = file_create_url($item['data']) . $query_string; + // @todo Not unsetting a foreach "as" reference causes problems on + // subsequent iterations into the loop on at least some setups. + // This needs more investigation. If it's a known PHP bug, the + // bug report for it should be found or filed and linked to. + unset($item); + } } break; case 'inline': - // Include inline stylesheets. - $inline_css .= drupal_load_stylesheet_content($item['data'], $item['preprocess']); + // Merge all item CSS content into the group's data property. + $group['data'] = ''; + foreach ($group['items'] as $item) { + $group['data'] .= drupal_load_stylesheet_content($item['data'], $item['preprocess']); + } break; case 'external': - // Preprocessing for external CSS files is ignored. - $element = $css_element; - $element['#attributes']['media'] = $item['media']; - $element['#attributes']['href'] = $item['data']; - $external_css .= theme('html_tag', array('element' => $element)); + // External files can't be aggregated. Each item's 'data' property + // already contains the full URL. + $group['preprocess'] = FALSE; + foreach ($group['items'] as &$item) { + $item['href'] = $item['data']; + // @todo Not unsetting a foreach "as" reference causes problems on + // subsequent iterations into the loop on at least some setups. This + // needs more investigation. If it's a known PHP bug, the bug report + // for it should be found or filed and linked to. + unset($item); + } break; } + // @todo Not unsetting a foreach "as" reference causes problems on + // subsequent iterations into the loop on at least some setups. This needs + // more investigation. If it's a known PHP bug, the bug report for it + // should be found or filed and linked to. + unset($group); } +} - if (!empty($preprocess_items)) { - foreach ($preprocess_items as $media => $items) { - // Prefix filename to prevent blocking by firewalls which reject files - // starting with "ad*". - $element = $css_element; - $element['#attributes']['media'] = $media; - $filename = 'css_' . md5(serialize($items) . $query_string) . '.css'; - $element['#attributes']['href'] = file_create_url(drupal_build_css_cache($items, $filename)); - $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)); +/** + * Returns a themed representation of all stylesheets that should be attached to the page. + * + * IE has an annoying limit of 31 total LINK (rel=stylesheet) plus STYLE tags. + * LINK tags are preferable, but are limited to only being able to load one + * file per tag. STYLE tags can contain @import statements allowing multiple + * files to be loaded per tag. When aggregation is turned off, a Drupal site + * can easily have more than 31 CSS files that need loading. Depending on + * different needs, different strategies can be employed to deal with this + * problem. This function implements the default strategy. A module or theme can + * implement hook_css_groups_alter() to register an alternate 'markup handler' + * in order to employ an alternate strategy. + * + * The strategy employed by this function is to use LINK tags for all files + * that are ineligible for aggregation regardless of the site setting enabling + * aggregation, and to use LINK tags for aggregated files (when aggregation is + * enabled). This function uses STYLE tags for groups of files eligible for + * aggregation but that have not been aggregated due to aggregation being + * disabled. This results in essentially the same number of tags whether + * aggregation is enabled or not, and should be well under the 31 limit. It also + * results in LINK tags being used exclusively when aggregation is enabled, + * which results in optimal performance. Running a site with aggregation + * disabled is primarily for development only, and the resulting split between + * LINK and STYLE tags enables developers to easily know which files will be + * aggregated when aggregation is enabled. Combining LINK and STYLE tags may + * result in suboptimal performance, but once performance optimization is + * needed, aggregation should be enabled. + * + * @param $css_groups + * @todo Add description of parameter. + * @return + * A string of XHTML CSS tags. + */ +function _drupal_css_markup($css_groups) { + $output = ''; + + // Defaults for LINK and STYLE elements. + $link_element_defaults = array( + '#tag' => 'link', + '#attributes' => array( + 'type' => 'text/css', + 'rel' => 'stylesheet', + ), + ); + $style_element_defaults = array( + '#tag' => 'style', + '#attributes' => array( + 'type' => 'text/css', + ), + ); + + // Loop through each group. + foreach ($css_groups['groups'] as $group) { + // If the group has an href, it means the preprocess handler successfully + // created an aggregate file, so output a single LINK tag for it. + if (isset($group['href'])) { + $element = $link_element_defaults; + $element['#attributes']['href'] = $group['href']; + $element['#attributes']['media'] = $group['media']; + $output .= theme('html_tag', array('element' => $element)); + } + // If the group contains inline content, output 1 or more STYLE tags with + // that content. + elseif ($group['type'] == 'inline') { + // If the group's data property is set, it contains the merged content and + // can be output with a single STYLE tag. + if (isset($group['data'])) { + $element = $style_element_defaults; + $element['#value'] = $group['data']; + $element['#attributes']['media'] = $group['media']; + $output .= "\n" . theme('html_tag', array('element' => $element)); + } + // Otherwise, output each item's content in its own STYLE tag. + else { + foreach ($group['items'] as $item) { + $element = $style_element_defaults; + $element['#value'] = $item['data']; + $element['#attributes']['media'] = $item['media']; + $output .= "\n" . theme('html_tag', array('element' => $element)); + } + } + } + // If the group can be aggregated, but hasn't been (perhaps because the + // site administrator disabled aggregation), output its items in a few + // STYLE tags using @import statements. This helps keep within IE's limit of + // 31 total LINK (rel="stylesheet") + STYLE tags. + elseif ($group['preprocess']) { + $import = array(); + foreach ($group['items'] as $item) { + $import[] = '@import url("' . $item['href'] . '");'; + } + while (!empty($import)) { + // IE also has a limit of 31 @import statements per STYLE tag. + $import_batch = array_slice($import, 0, 31); + $import = array_slice($import, 31); + $element = $style_element_defaults; + $element['#value'] = implode("\n", $import_batch); + $element['#attributes']['media'] = $group['media']; + $output .= theme('html_tag', array('element' => $element)); + } + } + // If the group can't be aggregated, output each item with a LINK tag. + else { + foreach ($group['items'] as $item) { + $element = $link_element_defaults; + $element['#attributes']['href'] = $item['href']; + $element['#attributes']['media'] = $item['media']; + $output .= 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 $output; } /** Index: modules/simpletest/tests/common.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v retrieving revision 1.99 diff -u -p -r1.99 common.test --- modules/simpletest/tests/common.test 7 Jan 2010 07:45:03 -0000 1.99 +++ modules/simpletest/tests/common.test 7 Jan 2010 23:21:59 -0000 @@ -558,7 +558,9 @@ class CascadingStylesheetsTestCase exten $css = 'http://example.com/style.css'; drupal_add_css($css, 'external'); $styles = drupal_get_css(); - $this->assertTrue(strpos($styles, 'href="' . $css) > 0, t('Rendering an external CSS file.')); + // Stylesheet URL may be the href of a LINK tag or in an @import statement + // of a STYLE tag. + $this->assertTrue(strpos($styles, 'href="' . $css) > 0 || strpos($styles, '@import url("' . $css . '")') > 0, t('Rendering an external CSS file.')); } /** @@ -566,7 +568,7 @@ class CascadingStylesheetsTestCase exten */ function testRenderInlinePreprocess() { $css = 'body { padding: 0px; }'; - $css_preprocessed = ''; + $css_preprocessed = ''; drupal_add_css($css, 'inline'); $styles = drupal_get_css(); $this->assertEqual($styles, "\n" . $css_preprocessed . "\n", t('Rendering preprocessed inline CSS adds it to the page.')); @@ -631,8 +633,10 @@ class CascadingStylesheetsTestCase exten $styles = drupal_get_css(); - if (preg_match_all('/href="' . preg_quote($GLOBALS['base_url'] . '/', '/') . '([^?]+)\?/', $styles, $matches)) { - $result = $matches[1]; + // Stylesheet URL may be the href of a LINK tag or in an @import statement + // of a STYLE tag. + if (preg_match_all('/(href="|url\(")' . preg_quote($GLOBALS['base_url'] . '/', '/') . '([^?]+)\?/', $styles, $matches)) { + $result = $matches[2]; } else { $result = array(); Index: modules/system/system.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v retrieving revision 1.115 diff -u -p -r1.115 system.api.php --- modules/system/system.api.php 5 Jan 2010 05:03:32 -0000 1.115 +++ modules/system/system.api.php 7 Jan 2010 23:22:00 -0000 @@ -561,6 +561,15 @@ function hook_css_alter(&$css) { } /** + * Alter CSS file groups before they are output on the page. + * + * @todo Add documentation. + */ +function hook_css_groups_alter(&$css_groups) { + // @todo Add sample implementation. +} + +/** * Add elements to a page before it is rendered. * * Use this hook when you want to add elements at the page level. For your