mirror of
https://github.com/moodle/moodle.git
synced 2025-01-18 05:58:34 +01:00
MDL-38441 css: implemented chunking of large sheets
This commit is contained in:
parent
3a8c4380c0
commit
e10750279d
118
lib/csslib.php
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,19 +91,112 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
@ -36,7 +36,7 @@ require_once($CFG->dirroot.'/lib/csslib.php');
|
||||
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 @@ if ($slashargument = min_get_slash_argument()) {
|
||||
$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 @@ if ($slashargument = min_get_slash_argument()) {
|
||||
$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 @@ if ($type === 'ie') {
|
||||
|
||||
$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 @@ if ($type === 'editor') {
|
||||
$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 @@ if ($type === 'editor') {
|
||||
}
|
||||
}
|
||||
$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";
|
||||
|
Loading…
x
Reference in New Issue
Block a user