Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1082
diff -u -p -r1.1082 common.inc
--- includes/common.inc 8 Jan 2010 06:07:03 -0000 1.1082
+++ includes/common.inc 8 Jan 2010 18:57:58 -0000
@@ -3250,23 +3250,19 @@ 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);
+ // Allow modules to alter the css items. Also allow them to override the
+ // default handlers that will be used to group, aggregate, and render the css.
+ $handlers = array();
+ drupal_alter('css', $css, $handlers);
+ $handlers += array(
+ 'group' => '_drupal_css_group',
+ 'aggregate' => '_drupal_css_aggregate',
+ 'render' => '_drupal_css_render',
+ );
// Sort css items according to their weights.
uasort($css, 'drupal_sort_weight');
@@ -3284,75 +3280,349 @@ 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(
+ // Group the items, aggregate the groups, and render the tags.
+ $css_groups = $handlers['group']($css);
+ $handlers['aggregate']($css_groups);
+ return $handlers['render']($css_groups);
+}
+
+/**
+ * Default handler to group CSS items.
+ *
+ * Once drupal_get_css() has an array of css items, the items then need to be
+ * grouped. Grouping allows files to be aggregated into fewer files which
+ * significantly reduces network traffic and improves page loading speed.
+ * When aggregation is disabled, grouping allows multiple files to be loaded
+ * from a single STYLE tag, enabling us to stay within IE's limit of 31 CSS
+ * inclusion tags.
+ *
+ * The grouping logic implemented by this function is to group all files that
+ * are eligible for aggregation and are for the same media type into a single
+ * group. This results in the fewest possible aggregate files when aggregation
+ * is enabled. However, in some cases, it can result in a change to the
+ * relative order of css items from what is specified in the $css array (for
+ * example, when a file that is ineligible for aggregation is between two files
+ * that are). A module wanting to always preserve relative order or wanting more
+ * control of which files get aggregated together can register an alternate
+ * group handler in hook_css_alter().
+ *
+ * Although grouping and aggregation are related, they are different. Grouping
+ * should be performed the same way whether or not aggregation is enabled. This
+ * ensures that if CSS order is changed when aggregation is enabled, that it is
+ * also changed when aggregation is disabled. The site administrator should be
+ * free to enable aggregation without worrying that it will change the look of
+ * the website.
+ *
+ * @param $css
+ * An array of css items, as returned by drupal_add_css(), but after
+ * alteration performed by drupal_get_css().
+ *
+ * @return
+ * An array of css groups. Each group contains the same properties as a css
+ * item, where the property value applies to the group as a whole. Each group
+ * also contains an 'items' property which is an array of css items, like
+ * $css, but only with the items in the group.
+ *
+ * @see drupal_get_css()
+ * @see _drupal_css_aggregate()
+ */
+function _drupal_css_group($css) {
+ // 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 Why? We need a comment explaining why we do this. It's not at all
+ // clear what the benefit is.
+ $file_groups = array();
+ $inline_groups = array();
+ $external_groups = array();
+ foreach ($css as $item) {
+ switch ($item['type']) {
+ // If a file can be aggregated (whether or not the site-wide aggregation
+ // setting is enabled), put the item into the group for its media type.
+ // Otherwise, put it into its own group.
+ case 'file':
+ 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;
+ }
+ }
+ return array_merge($file_groups, $external_groups, $inline_groups);
+}
+
+/**
+ * Default handler to aggregate CSS files and inline content.
+ *
+ * Having the browser load fewer CSS files results in much faster page loads
+ * than when it loads many small files. This function aggregates files within
+ * the same group into a single file unless the site-wide setting to do so is
+ * disabled (commonly the case during site development). It also aggregates
+ * inline content together. This function also compresses the aggregate files
+ * (removes comments, whitespace, and other unnecessary content) for faster
+ * download.
+ *
+ * A module wanting to implement custom aggregation functionality can register
+ * an alternate aggregate handler in hook_css_alter().
+ *
+ * @param $css_groups
+ * An array of css groups as returned by _drupal_css_group() or a function
+ * that overrides it. This function modifies the group's 'data' property for
+ * each group that is aggregated.
+ *
+ * @see _drupal_css_group()
+ */
+function _drupal_css_aggregate(&$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);
+
+ // For non-aggregated files, a dummy query string is added to the URL (@see
+ // _drupal_css_render()). For aggregated files, it is instead added to the
+ // data from which an md5 hash is generated, so that a changed query string
+ // triggers a new aggregate file to be created.
+ $query_string = '?' . substr(variable_get('css_js_query_string', '0'), 0, 1);
+
+ // For each group that needs aggregation, aggregate its items.
+ foreach ($css_groups as $key => $group) {
+ switch ($group['type']) {
+ // If a file group can be aggregated into a single file, do so, and set
+ // the group's data property to the file path of the aggregate file.
+ case 'file':
+ if ($group['preprocess'] && $preprocess_css && $is_writable) {
+ $filename = 'css_' . md5(serialize($group['items']) . $query_string) . '.css';
+ $css_groups[$key]['data'] = drupal_build_css_cache($group['items'], $filename);
+ }
+ break;
+ // Aggregate all inline CSS content into the group's data property.
+ case 'inline':
+ $css_groups[$key]['data'] = '';
+ foreach ($group['items'] as $item) {
+ $css_groups[$key]['data'] .= drupal_load_stylesheet_content($item['data'], $item['preprocess']);
+ }
+ break;
+ }
+ }
+}
+
+/**
+ * Default handler to render CSS tags.
+ *
+ * For production websites, LINK tags are preferable to STYLE tags with @import
+ * statements, because:
+ * - They are the standard tag intended for linking to a resource.
+ * - On Firefox 2 and perhaps other browsers, CSS files included with @import
+ * statements don't get saved when saving the complete web page for offline
+ * use: @see http://drupal.org/node/145218.
+ * - On IE, if only LINK tags and no @import statements are used, all the CSS
+ * files are downloaded in parallel, resulting in faster page load, but if
+ * @import statements are used and span across multiple STYLE tags, all the
+ * ones from one STYLE tag must be downloaded before downloading begins for
+ * the next STYLE tag. Furthermore, IE7 does not support media declaration on
+ * the @import statement, so multiple STYLE tags must be used when different
+ * files are for different media types. Non-IE browsers always download in
+ * parallel, so this is an IE-specific performance quirk. @see
+ * http://www.stevesouders.com/blog/2009/04/09/dont-use-import/.
+ *
+ * However, IE has an annoying limit of 31 total CSS inclusion tags (@see
+ * http://drupal.org/node/228818) and LINK tags are limited to one file per tag,
+ * whereas STYLE tags can contain multiple @import statements allowing multiple
+ * files to be loaded per tag. When CSS aggregation is disabled, a Drupal site
+ * can easily have more than 31 CSS files that need to be loaded, so using LINK
+ * tags exclusively would result in a site that would display incorrectly in IE.
+ * Depending on different needs, different strategies can be employed to deal
+ * with this problem. This function implements the default strategy. A module
+ * wanting to implement a different strategy can register its own render handler
+ * in hook_css_alter().
+ *
+ * The strategy employed by this function is to use LINK tags for all files when
+ * aggregation is enabled, since this is the optimal choice, and when
+ * aggregation is enabled, is extremely unlikely to encroach on IE's tag limit.
+ * When aggregation is disabled, for files that would be aggregated if it were
+ * enabled, this function outputs a STYLE tag for each set of files that would
+ * be aggregated together (or if necessary, multiple STYLE tags per set, but
+ * with an attempt to output as few STYLE tags as possible). This results in
+ * either exactly the same number of tags (or possibly just 1 or 2 more)
+ * output when aggregation is disabled relative to when it's enabled, so in the
+ * unlikely event that the site uses many files that are ineligible for
+ * aggregation or can only be aggregated in small groups and is therefore in
+ * danger of hitting IE's tag limit even with aggregation enabled, it would be
+ * noticed when aggregation is disabled as well, providing the site builder an
+ * opportunity to fix the problem prior to site launch.
+ *
+ * This function evaluates the aggregation enabled/disabled condition on a group
+ * by group basis, so that modules can implement more sophisticated aggregation
+ * logic than the site-wide setting provided by default. Since disabling
+ * aggregation is primarily targeted for developers, this approach also has the
+ * side-benefit of allowing developers to easily see from the HTML source which
+ * files will and will not be aggregated (and in which groups) when aggregation
+ * gets enabled.
+ *
+ * @param $css_groups
+ * An array of css groups as returned by _drupal_css_group() or a function
+ * that overrides it.
+ * @return
+ * A string of XHTML CSS tags.
+ *
+ * @see _drupal_css_group()
+ * @see _drupal_css_aggregate()
+ */
+function _drupal_css_render($css_groups) {
+ $output = '';
+
+ // 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);
+
+ // Defaults for LINK and STYLE elements.
+ $link_element_defaults = array(
'#tag' => 'link',
'#attributes' => array(
'type' => 'text/css',
'rel' => 'stylesheet',
),
);
- $rendered_css = array();
- $inline_css = '';
- $external_css = '';
- $preprocess_items = array();
- foreach ($css as $data => $item) {
- // Loop through each of the stylesheets, including them appropriately based
- // on their type.
- switch ($item['type']) {
+ $style_element_defaults = array(
+ '#tag' => 'style',
+ '#attributes' => array(
+ 'type' => 'text/css',
+ ),
+ );
+
+ // Loop through each group.
+ foreach ($css_groups as $group) {
+ switch ($group['type']) {
+ // For file items, there are three possibilites.
+ // - The group has been aggregated: in this case, output a LINK tag for
+ // the aggregate file.
+ // - The group can be aggregated but has not been (most likely because
+ // the site administrator disabled the site-wide setting): in this case,
+ // output as few STYLE tags for the group as possible, using @import
+ // statement for each file in the group. This enables us to stay within
+ // IE's limit of 31 total CSS inclusion tags.
+ // - The group contains items not eligible for aggregation (their
+ // 'preprocess' flag has been set to FALSE): in this case, output a LINK
+ // tag for each file.
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));
+ // The group has been aggregated into a single file: output a LINK tag
+ // for the aggregate file.
+ if (isset($group['data'])) {
+ $element = $link_element_defaults;
+ // The aggregate file name already incorporates the dummy query
+ // string information. The query string does not need to be added to
+ // the URL.
+ $element['#attributes']['href'] = file_create_url($group['data']);
+ $element['#attributes']['media'] = $group['media'];
+ $output .= theme('html_tag', array('element' => $element));
+ }
+ // The group can be aggregated, but hasn't been: combine multiple items
+ // into as few STYLE tags as possible.
+ elseif ($group['preprocess']) {
+ $import = array();
+ foreach ($group['items'] as $item) {
+ // The dummy query string needs to be added to the URL to control
+ // browser-caching. IE7 does not support a media type on the @import
+ // statement, so we instead specify the media for the group on the
+ // STYLE tag.
+ $import[] = '@import url("' . file_create_url($item['data']) . $query_string . '");';
+ }
+ // In addition to IE's limit of 31 total CSS inclusion tags, it also
+ // has a limit of 31 @import statements per STYLE tag.
+ while (!empty($import)) {
+ $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));
+ }
}
+ // The group contains items ineligible for aggregation: output a LINK
+ // tag for each file.
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) {
+ $element = $link_element_defaults;
+ // The dummy query string needs to be added to the URL to control
+ // browser-caching.
+ $element['#attributes']['href'] = file_create_url($item['data']) . $query_string;
+ $element['#attributes']['media'] = $item['media'];
+ $output .= theme('html_tag', array('element' => $element));
+ }
}
break;
+ // For inline content, the 'data' property contains the CSS content. If
+ // the group's 'data' property is set, then output it in a single STYLE
+ // tag. Otherwise, output a separate STYLE tag for each item.
case 'inline':
- // Include inline stylesheets.
- $inline_css .= drupal_load_stylesheet_content($item['data'], $item['preprocess']);
+ 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));
+ }
+ 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));
+ }
+ }
break;
+ // Output a LINK tag for each external item. The item's 'data' property
+ // contains the full URL.
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));
+ foreach ($group['items'] as $item) {
+ $element = $link_element_defaults;
+ $element['#attributes']['href'] = $item['data'];
+ $element['#attributes']['media'] = $item['media'];
+ $output .= theme('html_tag', array('element' => $element));
+ }
break;
}
}
- 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));
- }
-
- // 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.100
diff -u -p -r1.100 common.test
--- modules/simpletest/tests/common.test 8 Jan 2010 06:07:03 -0000 1.100
+++ modules/simpletest/tests/common.test 8 Jan 2010 18:58:01 -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.116
diff -u -p -r1.116 system.api.php
--- modules/system/system.api.php 8 Jan 2010 11:07:01 -0000 1.116
+++ modules/system/system.api.php 8 Jan 2010 18:58:01 -0000
@@ -551,13 +551,28 @@ function hook_library_alter(&$libraries,
* Alter CSS files before they are output on the page.
*
* @param $css
- * An array of all CSS items (files and inline CSS) being requested on the page.
+ * An array of all CSS items (files and inline CSS) being requested on the
+ * page.
+ * @param $handlers
+ * An array of handlers to override Drupal's default implementation of how
+ * to group CSS items, aggregate the files within a group, and render the CSS
+ * tags. Can contain the following keys:
+ * - group: A function to run instead of _drupal_css_group().
+ * - aggregate: A function to run instead of _drupal_css_aggregate().
+ * - render: A function to run instead of _drupal_css_render().
+ *
* @see drupal_add_css()
* @see drupal_get_css()
*/
-function hook_css_alter(&$css) {
+function hook_css_alter(&$css, &$handlers) {
// Remove defaults.css file.
unset($css[drupal_get_path('module', 'system') . '/defaults.css']);
+
+ // This site will definitely have few enough CSS files to not encroach on IE's
+ // limit, but for some reason needs to run with aggregation disabled, and we
+ // want all CSS files included with LINK tags instead @import statements, so
+ // override _drupal_css_render().
+ $handlers['render'] = 'mymodule_css_render';
}
/**