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