CssCollectionRenderer.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. <?php
  2. namespace Drupal\Core\Asset;
  3. use Drupal\Component\Utility\Html;
  4. use Drupal\Core\State\StateInterface;
  5. /**
  6. * Renders CSS assets.
  7. *
  8. * For production websites, LINK tags are preferable to STYLE tags with @import
  9. * statements, because:
  10. * - They are the standard tag intended for linking to a resource.
  11. * - On Firefox 2 and perhaps other browsers, CSS files included with @import
  12. * statements don't get saved when saving the complete web page for offline
  13. * use: https://www.drupal.org/node/145218.
  14. * - On IE, if only LINK tags and no @import statements are used, all the CSS
  15. * files are downloaded in parallel, resulting in faster page load, but if
  16. * @import statements are used and span across multiple STYLE tags, all the
  17. * ones from one STYLE tag must be downloaded before downloading begins for
  18. * the next STYLE tag. Furthermore, IE7 does not support media declaration on
  19. * the @import statement, so multiple STYLE tags must be used when different
  20. * files are for different media types. Non-IE browsers always download in
  21. * parallel, so this is an IE-specific performance quirk:
  22. * http://www.stevesouders.com/blog/2009/04/09/dont-use-import/.
  23. *
  24. * However, IE has an annoying limit of 31 total CSS inclusion tags
  25. * (https://www.drupal.org/node/228818) and LINK tags are limited to one file
  26. * per tag, whereas STYLE tags can contain multiple @import statements allowing
  27. * multiple files to be loaded per tag. When CSS aggregation is disabled, a
  28. * Drupal site can easily have more than 31 CSS files that need to be loaded, so
  29. * using LINK tags exclusively would result in a site that would display
  30. * incorrectly in IE. Depending on different needs, different strategies can be
  31. * employed to decide when to use LINK tags and when to use STYLE tags.
  32. *
  33. * The strategy employed by this class is to use LINK tags for all aggregate
  34. * files and for all files that cannot be aggregated (e.g., if 'preprocess' is
  35. * set to FALSE or the type is 'external'), and to use STYLE tags for groups
  36. * of files that could be aggregated together but aren't (e.g., if the site-wide
  37. * aggregation setting is disabled). This results in all LINK tags when
  38. * aggregation is enabled, a guarantee that as many or only slightly more tags
  39. * are used with aggregation disabled than enabled (so that if the limit were to
  40. * be crossed with aggregation enabled, the site developer would also notice the
  41. * problem while aggregation is disabled), and an easy way for a developer to
  42. * view HTML source while aggregation is disabled and know what files will be
  43. * aggregated together when aggregation becomes enabled.
  44. *
  45. * This class evaluates the aggregation enabled/disabled condition on a group
  46. * by group basis by testing whether an aggregate file has been made for the
  47. * group rather than by testing the site-wide aggregation setting. This allows
  48. * this class to work correctly even if modules have implemented custom
  49. * logic for grouping and aggregating files.
  50. */
  51. class CssCollectionRenderer implements AssetCollectionRendererInterface {
  52. /**
  53. * The state key/value store.
  54. *
  55. * @var \Drupal\Core\State\StateInterface
  56. */
  57. protected $state;
  58. /**
  59. * Constructs a CssCollectionRenderer.
  60. *
  61. * @param \Drupal\Core\State\StateInterface $state
  62. * The state key/value store.
  63. */
  64. public function __construct(StateInterface $state) {
  65. $this->state = $state;
  66. }
  67. /**
  68. * {@inheritdoc}
  69. */
  70. public function render(array $css_assets) {
  71. $elements = [];
  72. // A dummy query-string is added to filenames, to gain control over
  73. // browser-caching. The string changes on every update or full cache
  74. // flush, forcing browsers to load a new copy of the files, as the
  75. // URL changed.
  76. $query_string = $this->state->get('system.css_js_query_string') ?: '0';
  77. // Defaults for LINK and STYLE elements.
  78. $link_element_defaults = [
  79. '#type' => 'html_tag',
  80. '#tag' => 'link',
  81. '#attributes' => [
  82. 'rel' => 'stylesheet',
  83. ],
  84. ];
  85. $style_element_defaults = [
  86. '#type' => 'html_tag',
  87. '#tag' => 'style',
  88. ];
  89. // For filthy IE hack.
  90. $current_ie_group_keys = NULL;
  91. $get_ie_group_key = function ($css_asset) {
  92. return [$css_asset['type'], $css_asset['preprocess'], $css_asset['group'], $css_asset['media'], $css_asset['browsers']];
  93. };
  94. // Loop through all CSS assets, by key, to allow for the special IE
  95. // workaround.
  96. $css_assets_keys = array_keys($css_assets);
  97. for ($i = 0; $i < count($css_assets_keys); $i++) {
  98. $css_asset = $css_assets[$css_assets_keys[$i]];
  99. switch ($css_asset['type']) {
  100. // For file items, there are three possibilities.
  101. // - There are up to 31 CSS assets on the page (some of which may be
  102. // aggregated). In this case, output a LINK tag for file CSS assets.
  103. // - There are more than 31 CSS assets on the page, yet we must stay
  104. // below IE<10's limit of 31 total CSS inclusion tags, we handle this
  105. // in two ways:
  106. // - file CSS assets that are not eligible for aggregation (their
  107. // 'preprocess' flag has been set to FALSE): in this case, output a
  108. // LINK tag.
  109. // - file CSS assets that can be aggregated (and possibly have been):
  110. // in this case, figure out which subsequent file CSS assets share
  111. // the same key properties ('group', 'media' and 'browsers') and
  112. // output this group into as few STYLE tags as possible (a STYLE
  113. // tag may contain only 31 @import statements).
  114. case 'file':
  115. // The dummy query string needs to be added to the URL to control
  116. // browser-caching.
  117. $query_string_separator = (strpos($css_asset['data'], '?') !== FALSE) ? '&' : '?';
  118. // As long as the current page will not run into IE's limit for CSS
  119. // assets: output a LINK tag for a file CSS asset.
  120. if (count($css_assets) <= 31) {
  121. $element = $link_element_defaults;
  122. $element['#attributes']['href'] = file_url_transform_relative(file_create_url($css_asset['data'])) . $query_string_separator . $query_string;
  123. $element['#attributes']['media'] = $css_asset['media'];
  124. $element['#browsers'] = $css_asset['browsers'];
  125. $elements[] = $element;
  126. }
  127. // The current page will run into IE's limits for CSS assets: work
  128. // around these limits by performing a light form of grouping.
  129. // Once Drupal only needs to support IE10 and later, we can drop this.
  130. else {
  131. // The file CSS asset is ineligible for aggregation: output it in a
  132. // LINK tag.
  133. if (!$css_asset['preprocess']) {
  134. $element = $link_element_defaults;
  135. $element['#attributes']['href'] = file_url_transform_relative(file_create_url($css_asset['data'])) . $query_string_separator . $query_string;
  136. $element['#attributes']['media'] = $css_asset['media'];
  137. $element['#browsers'] = $css_asset['browsers'];
  138. $elements[] = $element;
  139. }
  140. // The file CSS asset can be aggregated, but hasn't been: combine
  141. // multiple items into as few STYLE tags as possible.
  142. else {
  143. $import = [];
  144. // Start with the current CSS asset, iterate over subsequent CSS
  145. // assets and find which ones have the same 'type', 'group',
  146. // 'preprocess', 'media' and 'browsers' properties.
  147. $j = $i;
  148. $next_css_asset = $css_asset;
  149. $current_ie_group_key = $get_ie_group_key($css_asset);
  150. do {
  151. // The dummy query string needs to be added to the URL to
  152. // control browser-caching. IE7 does not support a media type on
  153. // the @import statement, so we instead specify the media for
  154. // the group on the STYLE tag.
  155. $import[] = '@import url("' . Html::escape(file_url_transform_relative(file_create_url($next_css_asset['data'])) . '?' . $query_string) . '");';
  156. // Move the outer for loop skip the next item, since we
  157. // processed it here.
  158. $i = $j;
  159. // Retrieve next CSS asset, unless there is none: then break.
  160. if ($j + 1 < count($css_assets_keys)) {
  161. $j++;
  162. $next_css_asset = $css_assets[$css_assets_keys[$j]];
  163. }
  164. else {
  165. break;
  166. }
  167. } while ($get_ie_group_key($next_css_asset) == $current_ie_group_key);
  168. // In addition to IE's limit of 31 total CSS inclusion tags, it
  169. // also has a limit of 31 @import statements per STYLE tag.
  170. while (!empty($import)) {
  171. $import_batch = array_slice($import, 0, 31);
  172. $import = array_slice($import, 31);
  173. $element = $style_element_defaults;
  174. // This simplifies the JavaScript regex, allowing each line
  175. // (separated by \n) to be treated as a completely different
  176. // string. This means that we can use ^ and $ on one line at a
  177. // time, and not worry about style tags since they'll never
  178. // match the regex.
  179. $element['#value'] = "\n" . implode("\n", $import_batch) . "\n";
  180. $element['#attributes']['media'] = $css_asset['media'];
  181. $element['#browsers'] = $css_asset['browsers'];
  182. $elements[] = $element;
  183. }
  184. }
  185. }
  186. break;
  187. // Output a LINK tag for an external CSS asset. The asset's 'data'
  188. // property contains the full URL.
  189. case 'external':
  190. $element = $link_element_defaults;
  191. $element['#attributes']['href'] = $css_asset['data'];
  192. $element['#attributes']['media'] = $css_asset['media'];
  193. $element['#browsers'] = $css_asset['browsers'];
  194. $elements[] = $element;
  195. break;
  196. default:
  197. throw new \Exception('Invalid CSS asset type.');
  198. }
  199. }
  200. return $elements;
  201. }
  202. }