Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

MDL-39673 css: implemented chunking of large sheets

Cherry-picked from MDL-38441
  • Loading branch information...
commit 713b818c40035ab4e0f4bf9a48944eb35a526346 1 parent 4453f32
@samhemelryk samhemelryk authored damyon committed
Showing with 134 additions and 11 deletions.
  1. +111 −7 lib/csslib.php
  2. +23 −4 theme/styles.php
View
118 lib/csslib.php
@@ -35,8 +35,12 @@
* @param theme_config $theme The theme that the CSS belongs to.
* @param string $csspath The path to store the CSS at.
* @param array $cssfiles The CSS files to store.
+ * @param bool $chunk If set to true these files will be chunked to ensure
+ * that no one file contains more than 4095 selectors.
+ * @param string $chunkurl If the CSS is be chunked then we need to know the URL
+ * to use for the chunked files.
*/
-function css_store_css(theme_config $theme, $csspath, array $cssfiles) {
+function css_store_css(theme_config $theme, $csspath, array $cssfiles, $chunk = false, $chunkurl = null) {
global $CFG;
// Check if both the CSS optimiser is enabled and the theme supports it.
@@ -72,6 +76,13 @@ function css_store_css(theme_config $theme, $csspath, array $cssfiles) {
$css = $theme->post_process(css_minify_css($cssfiles));
}
+ if ($chunk) {
+ // Chunk the CSS if requried.
+ $css = css_chunk_by_selector_count($css, $chunkurl);
+ } else {
+ $css = array($css);
+ }
+
clearstatcache();
if (!file_exists(dirname($csspath))) {
@mkdir(dirname($csspath), $CFG->directorypermissions, true);
@@ -80,13 +91,26 @@ function css_store_css(theme_config $theme, $csspath, array $cssfiles) {
// Prevent serving of incomplete file from concurrent request,
// the rename() should be more atomic than fwrite().
ignore_user_abort(true);
- if ($fp = fopen($csspath.'.tmp', 'xb')) {
- fwrite($fp, $css);
- fclose($fp);
- rename($csspath.'.tmp', $csspath);
- @chmod($csspath, $CFG->filepermissions);
- @unlink($csspath.'.tmp'); // just in case anything fails
+
+ $files = count($css);
+ $count = 0;
+ foreach ($css as $content) {
+ if ($files > 1 && ($count+1) !== $files) {
+ // If there is more than one file and this is not the last file.
+ $filename = preg_replace('#\.css$#', '.'.$count.'.css', $csspath);
+ $count++;
+ } else {
+ $filename = $csspath;
+ }
+ if ($fp = fopen($filename.'.tmp', 'xb')) {
+ fwrite($fp, $content);
+ fclose($fp);
+ rename($filename.'.tmp', $filename);
+ @chmod($filename, $CFG->filepermissions);
+ @unlink($filename.'.tmp'); // just in case anything fails
+ }
}
+
ignore_user_abort(false);
if (connection_aborted()) {
die;
@@ -94,6 +118,86 @@ function css_store_css(theme_config $theme, $csspath, array $cssfiles) {
}
/**
+ * Takes CSS and chunks it if the number of selectors within it exceeds $maxselectors.
+ *
+ * @param string $css The CSS to chunk.
+ * @param string $importurl The URL to use for import statements.
+ * @param int $maxselectors The number of selectors to limit a chunk to.
+ * @param int $buffer The buffer size to use when chunking. You shouldn't need to reduce this
+ * unless you are lowering the maximum selectors.
+ * @return array An array of CSS chunks.
+ */
+function css_chunk_by_selector_count($css, $importurl, $maxselectors = 4095, $buffer = 50) {
+ // Check if we need to chunk this CSS file.
+ $count = substr_count($css, ',') + substr_count($css, '{');
+ if ($count < $maxselectors) {
+ // The number of selectors is less then the max - we're fine.
+ return array($css);
+ }
+
+ // Chunk time ?!
+ // Split the CSS by array, making sure to save the delimiter in the process.
+ $parts = preg_split('#([,\}])#', $css, null, PREG_SPLIT_DELIM_CAPTURE + PREG_SPLIT_NO_EMPTY);
+ // We need to chunk the array. Each delimiter is stored separately so we multiple by 2.
+ // We also subtract 100 to give us a small buffer just in case.
+ $parts = array_chunk($parts, $maxselectors * 2 - $buffer * 2);
+ $css = array();
+ $partcount = count($parts);
+ foreach ($parts as $key => $chunk) {
+ if (end($chunk) === ',') {
+ // Damn last element was a comma.
+ // Pretty much the only way to deal with this is to take the styles from the end of the
+ // comma separated chain of selectors and apply it to the last selector we have here in place
+ // of the comma.
+ // Unit tests are essential for making sure this works.
+ $styles = false;
+ $i = $key;
+ while ($styles === false && $i < ($partcount - 1)) {
+ $i++;
+ $nextpart = $parts[$i];
+ foreach ($nextpart as $style) {
+ if (strpos($style, '{') !== false) {
+ $styles = preg_replace('#^[^\{]+#', '', $style);
+ break;
+ }
+ }
+ }
+ if ($styles === false) {
+ $styles = '/** Error chunking CSS **/';
+ } else {
+ $styles .= '}';
+ }
+ array_pop($chunk);
+ array_push($chunk, $styles);
+ }
+ $css[] = join('', $chunk);
+ }
+ // The array $css now contains CSS split into perfect sized chunks.
+ // Import statements can only appear at the very top of a CSS file.
+ // Imported sheets are applied in the the order they are imported and
+ // are followed by the contents of the CSS.
+ // This is terrible for performance.
+ // It means we must put the import statements at the top of the last chunk
+ // to ensure that things are always applied in the correct order.
+ // This way the chunked files are included in the order they were chunked
+ // followed by the contents of the final chunk in the actual sheet.
+ $importcss = '';
+ $slashargs = strpos($importurl, '.php?') === false;
+ $parts = count($css);
+ for ($i = 0; $i < $parts - 1; $i++) {
+ if ($slashargs) {
+ $importcss .= "@import url({$importurl}/chunk{$i});\n";
+ } else {
+ $importcss .= "@import url({$importurl}&chunk={$i});\n";
+ }
+ }
+ $importcss .= end($css);
+ $css[key($css)] = $importcss;
+
+ return $css;
+}
+
+/**
* Sends IE specific CSS
*
* In writing the CSS parser I have a theory that we could optimise the CSS
View
27 theme/styles.php
@@ -36,7 +36,7 @@
if ($slashargument = min_get_slash_argument()) {
$slashargument = ltrim($slashargument, '/');
if (substr_count($slashargument, '/') < 2) {
- image_not_found();
+ css_send_css_not_found();
}
if (strpos($slashargument, '_s/') === 0) {
@@ -47,7 +47,12 @@
$usesvg = true;
}
- // image must be last because it may contain "/"
+ $chunk = null;
+ if (preg_match('#/(chunk(\d+)(/|$))#', $slashargument, $matches)) {
+ $chunk = (int)$matches[2];
+ $slashargument = str_replace($matches[1], '', $slashargument);
+ }
+
list($themename, $rev, $type) = explode('/', $slashargument, 3);
$themename = min_clean_param($themename, 'SAFEDIR');
$rev = min_clean_param($rev, 'INT');
@@ -57,6 +62,7 @@
$themename = min_optional_param('theme', 'standard', 'SAFEDIR');
$rev = min_optional_param('rev', 0, 'INT');
$type = min_optional_param('type', 'all', 'SAFEDIR');
+ $chunk = min_optional_param('chunk', null, 'INT');
$usesvg = (bool)min_optional_param('svg', '1', 'INT');
}
@@ -80,12 +86,17 @@
$candidatedir = "$CFG->cachedir/theme/$themename/css";
$etag = "$themename/$rev/$type";
+$candidatename = $type;
if (!$usesvg) {
// Add to the sheet name, one day we'll be able to just drop this.
$candidatedir .= '/nosvg';
$etag .= '/nosvg';
}
-$candidatesheet = "$candidatedir/$type.css";
+if ($chunk !== null) {
+ $etag .= '/chunk'.$chunk;
+ $candidatename .= '.'.$chunk;
+}
+$candidatesheet = "$candidatedir/$candidatename.css";
$etag = sha1($etag);
if (file_exists($candidatesheet)) {
@@ -122,13 +133,21 @@
$cssfiles = $theme->editor_css_files();
css_store_css($theme, $candidatesheet, $cssfiles);
} else {
+ // IE requests plugins/parents/theme instead of all at once.
+ $chunk = in_array($type, array('plugins', 'parents', 'theme'));
$basedir = "$CFG->cachedir/theme/$themename/css";
if (!$usesvg) {
$basedir .= '/nosvg';
}
$css = $theme->css_files();
$allfiles = array();
+ $relroot = preg_replace('|^http.?://[^/]+|', '', $CFG->wwwroot);
foreach ($css as $key=>$value) {
+ if (!empty($slashargument)) {
+ $chunkurl = "{$relroot}/theme/styles.php/{$themename}/{$rev}/{$key}";
+ } else {
+ $chunkurl = "{$relroot}/theme/styles.php?theme={$themename}&rev={$rev}&type={$key}";
+ }
$cssfiles = array();
foreach($value as $val) {
if (is_array($val)) {
@@ -140,7 +159,7 @@
}
}
$cssfile = "$basedir/$key.css";
- css_store_css($theme, $cssfile, $cssfiles);
+ css_store_css($theme, $cssfile, $cssfiles, true, $chunkurl);
$allfiles = array_merge($allfiles, $cssfiles);
}
$cssfile = "$basedir/all.css";
Please sign in to comment.
Something went wrong with that request. Please try again.