MDL-44936 csslib: Chunking supports @media queries and commas in rules

This commit is contained in:
Frederic Massart
2014-04-11 12:04:47 +08:00
parent d59796af1a
commit 89bcfb88b6
2 changed files with 218 additions and 82 deletions
+127 -40
View File
@@ -120,14 +120,29 @@ function css_store_css(theme_config $theme, $csspath, array $cssfiles, $chunk =
/**
* Takes CSS and chunks it if the number of selectors within it exceeds $maxselectors.
*
* The chunking will not split a group of selectors, or a media query. That means that
* if n > $maxselectors and there are n selectors grouped together,
* they will not be chunked and you could end up with more selectors than desired.
* The same applies for a media query that has more than n selectors.
*
* Also, as we do not split group of selectors or media queries, the chunking might
* not be as optimal as it could be, having files with less selectors than it could
* potentially contain.
*
* String functions used here are not compliant with unicode characters. But that is
* not an issue as the syntax of CSS is using ASCII codes. Even if we have unicode
* characters in comments, or in the property 'content: ""', it will behave correcly.
*
* Please note that this strips out the comments if chunking happens.
*
* @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.
* @param int $buffer Not used any more.
* @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) {
@@ -135,44 +150,116 @@ function css_chunk_by_selector_count($css, $importurl, $maxselectors = 4095, $bu
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;
}
$chunks = array(); // The final chunks.
$offsets = array(); // The indexes to chunk at.
$offset = 0; // The current offset.
$selectorcount = 0; // The number of selectors since the last split.
$lastvalidoffset = 0; // The last valid index to split at.
$lastvalidoffsetselectorcount = 0; // The number of selectors used at the time were could split.
$inrule = 0; // The number of rules we are in, should not be greater than 1.
$inmedia = false; // Whether or not we are in a media query.
$mediacoming = false; // Whether or not we are expeting a media query.
$currentoffseterror = null; // Not null when we have recorded an error for the current split.
$offseterrors = array(); // The offsets where we found errors.
// Remove the comments. Because it's easier, safer and probably a lot of other good reasons.
$css = preg_replace('#/\*(.*?)\*/#s', '', $css);
$strlen = strlen($css);
// Walk through the CSS content character by character.
for ($i = 1; $i <= $strlen; $i++) {
$char = $css[$i - 1];
$offset = $i;
// Is that a media query that I see coming towards us?
if ($char === '@') {
if (!$inmedia && substr($css, $offset, 5) === 'media') {
$mediacoming = true;
}
}
// So we are entering a rule or a media query...
if ($char === '{') {
if ($mediacoming) {
$inmedia = true;
$mediacoming = false;
} else {
$inrule++;
$selectorcount++;
}
}
// Let's count the number of selectors, but only if we are not in a rule as they
// can contain commas too.
if (!$inrule && $char === ',') {
$selectorcount++;
}
// We reached the end of something.
if ($char === '}') {
// Oh, we are in a media query.
if ($inmedia) {
if (!$inrule) {
// This is the end of the media query.
$inmedia = false;
} else {
// We were in a rule, in the media query.
$inrule--;
}
} else {
$inrule--;
}
// We are not in a media query, and there is no pending rule, it is safe to split here.
if (!$inmedia && !$inrule) {
$lastvalidoffset = $offset;
$lastvalidoffsetselectorcount = $selectorcount;
}
}
// Alright, this is splitting time...
if ($selectorcount > $maxselectors) {
if (!$lastvalidoffset) {
// We must have reached more selectors into one set than we were allowed. That means that either
// the chunk size value is too small, or that we have a gigantic group of selectors, or that a media
// query contains more selectors than the chunk size. We have to ignore this because we do not
// support split inside a group of selectors or media query.
if ($currentoffseterror === null) {
$currentoffseterror = $offset;
$offseterrors[] = $currentoffseterror;
}
} else {
// We identify the offset to split at and reset the number of selectors found from there.
$offsets[] = $lastvalidoffset;
$selectorcount = $selectorcount - $lastvalidoffsetselectorcount;
$lastvalidoffset = 0;
$currentoffseterror = null;
}
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.
// Report offset errors.
if (!empty($offseterrors)) {
debugging('Could not find a safe place to split at offset(s): ' . implode(', ', $offseterrors) . '. Those were ignored.',
DEBUG_DEVELOPER);
}
// Now that we have got the offets, we can chunk the CSS.
$offsetcount = count($offsets);
foreach ($offsets as $key => $index) {
$start = 0;
if ($key > 0) {
$start = $offsets[$key - 1];
}
// From somewhere up to the offset.
$chunks[] = substr($css, $start, $index - $start);
}
// Add the last chunk (if there is one), from the last offset to the end of the string.
if (end($offsets) != $strlen) {
$chunks[] = substr($css, end($offsets));
}
// The array $chunks 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.
@@ -183,7 +270,7 @@ function css_chunk_by_selector_count($css, $importurl, $maxselectors = 4095, $bu
// followed by the contents of the final chunk in the actual sheet.
$importcss = '';
$slashargs = strpos($importurl, '.php?') === false;
$parts = count($css);
$parts = count($chunks);
for ($i = 0; $i < $parts - 1; $i++) {
if ($slashargs) {
$importcss .= "@import url({$importurl}/chunk{$i});\n";
@@ -191,10 +278,10 @@ function css_chunk_by_selector_count($css, $importurl, $maxselectors = 4095, $bu
$importcss .= "@import url({$importurl}&chunk={$i});\n";
}
}
$importcss .= end($css);
$css[key($css)] = $importcss;
$importcss .= end($chunks);
$chunks[key($chunks)] = $importcss;
return $css;
return $chunks;
}
/**