mirror of
https://github.com/moodle/moodle.git
synced 2025-04-21 00:12:56 +02:00
Merge branch 'MDL-44936-master' of git://github.com/FMCorz/moodle
This commit is contained in:
commit
390365e696
169
lib/csslib.php
169
lib/csslib.php
@ -107,14 +107,29 @@ function css_write_file($filename, $content) {
|
||||
/**
|
||||
* 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) {
|
||||
@ -122,44 +137,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.
|
||||
@ -170,7 +257,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 = 1; $i < $parts; $i++) {
|
||||
if ($slashargs) {
|
||||
$importcss .= "@import url({$importurl}/chunk{$i});\n";
|
||||
@ -178,10 +265,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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1082,7 +1082,7 @@ CSS;
|
||||
public function test_css_chunking() {
|
||||
// Test with an even number of styles.
|
||||
$css = 'a{}b{}c{}d{}e{}f{}';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0);
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2);
|
||||
$this->assertInternalType('array', $chunks);
|
||||
$this->assertCount(3, $chunks);
|
||||
$this->assertArrayHasKey(0, $chunks);
|
||||
@ -1094,7 +1094,7 @@ CSS;
|
||||
|
||||
// Test with an odd number of styles.
|
||||
$css = 'a{}b{}c{}d{}e{}';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0);
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2);
|
||||
$this->assertInternalType('array', $chunks);
|
||||
$this->assertCount(3, $chunks);
|
||||
$this->assertArrayHasKey(0, $chunks);
|
||||
@ -1104,21 +1104,9 @@ CSS;
|
||||
$this->assertSame('c{}d{}', $chunks[1]);
|
||||
$this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne{}", $chunks[2]);
|
||||
|
||||
// Test buffering. Set a buffer that will reduce the effective sheet size back to two.
|
||||
$css = 'a{}b{}c{}d{}e{}f{}';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 6, 4);
|
||||
$this->assertInternalType('array', $chunks);
|
||||
$this->assertCount(3, $chunks);
|
||||
$this->assertArrayHasKey(0, $chunks);
|
||||
$this->assertArrayHasKey(1, $chunks);
|
||||
$this->assertArrayHasKey(2, $chunks);
|
||||
$this->assertSame('a{}b{}', $chunks[0]);
|
||||
$this->assertSame('c{}d{}', $chunks[1]);
|
||||
$this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne{}f{}", $chunks[2]);
|
||||
|
||||
// Test well placed commas.
|
||||
$css = 'a,b{}c,d{}e,f{}';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0);
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2);
|
||||
$this->assertInternalType('array', $chunks);
|
||||
$this->assertCount(3, $chunks);
|
||||
$this->assertArrayHasKey(0, $chunks);
|
||||
@ -1130,56 +1118,117 @@ CSS;
|
||||
|
||||
// Test unfortunately placed commas.
|
||||
$css = 'a{}b,c{color:red;}d{}e{}f{}';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0);
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2);
|
||||
$this->assertInternalType('array', $chunks);
|
||||
$this->assertCount(3, $chunks);
|
||||
$this->assertCount(4, $chunks);
|
||||
$this->assertArrayHasKey(0, $chunks);
|
||||
$this->assertArrayHasKey(1, $chunks);
|
||||
$this->assertArrayHasKey(2, $chunks);
|
||||
$this->assertSame('a{}b{color:red;}', $chunks[0]);
|
||||
$this->assertSame('c{color:red;}d{}', $chunks[1]);
|
||||
$this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne{}f{}", $chunks[2]);
|
||||
$this->assertArrayHasKey(3, $chunks);
|
||||
$this->assertSame('a{}', $chunks[0]);
|
||||
$this->assertSame('b,c{color:red;}', $chunks[1]);
|
||||
$this->assertSame('d{}e{}', $chunks[2]);
|
||||
$this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\n@import url(styles.php?type=test&chunk=3);\nf{}", $chunks[3]);
|
||||
|
||||
// Test unfortunate CSS.
|
||||
$css = 'a,b,c,d,e,f{color:red;}';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0);
|
||||
$this->assertInternalType('array', $chunks);
|
||||
$this->assertCount(3, $chunks);
|
||||
$this->assertCount(1, $chunks);
|
||||
$this->assertArrayHasKey(0, $chunks);
|
||||
$this->assertArrayHasKey(1, $chunks);
|
||||
$this->assertArrayHasKey(2, $chunks);
|
||||
$this->assertSame('a,b{color:red;}', $chunks[0]);
|
||||
$this->assertSame('c,d{color:red;}', $chunks[1]);
|
||||
$this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne,f{color:red;}", $chunks[2]);
|
||||
$this->assertSame('a,b,c,d,e,f{color:red;}', $chunks[0]);
|
||||
$this->assertDebuggingCalled('Could not find a safe place to split at offset(s): 6. Those were ignored.');
|
||||
|
||||
// Test to make sure invalid CSS isn't totally ruined.
|
||||
$css = 'a{},,,e{},';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0);
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2);
|
||||
// Believe it or not we want to care what comes out here as this will be parsed correctly
|
||||
// by a browser.
|
||||
$this->assertInternalType('array', $chunks);
|
||||
$this->assertCount(2, $chunks);
|
||||
$this->assertArrayHasKey(0, $chunks);
|
||||
$this->assertArrayHasKey(1, $chunks);
|
||||
$this->assertSame('a{},{}', $chunks[0]);
|
||||
$this->assertSame("@import url(styles.php?type=test&chunk=1);\n,e{}/** Error chunking CSS **/", $chunks[1]);
|
||||
|
||||
// Test utter crap CSS to make sure we don't loop to our deaths.
|
||||
$css = 'a,b,c,d,e,f';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0);
|
||||
$this->assertInternalType('array', $chunks);
|
||||
$this->assertCount(3, $chunks);
|
||||
$this->assertArrayHasKey(0, $chunks);
|
||||
$this->assertArrayHasKey(1, $chunks);
|
||||
$this->assertArrayHasKey(2, $chunks);
|
||||
$this->assertSame('a,b/** Error chunking CSS **/', $chunks[0]);
|
||||
$this->assertSame('c,d/** Error chunking CSS **/', $chunks[1]);
|
||||
$this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne,f", $chunks[2]);
|
||||
$this->assertSame('a{}', $chunks[0]);
|
||||
$this->assertSame(',,,e{}', $chunks[1]);
|
||||
$this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\n,", $chunks[2]);
|
||||
$this->assertDebuggingCalled('Could not find a safe place to split at offset(s): 6. Those were ignored.');
|
||||
|
||||
// Test utter crap CSS to make sure we don't loop to our deaths.
|
||||
$css = 'a,b,c,d,e,f';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2);
|
||||
$this->assertInternalType('array', $chunks);
|
||||
$this->assertCount(1, $chunks);
|
||||
$this->assertArrayHasKey(0, $chunks);
|
||||
$this->assertSame($css, $chunks[0]);
|
||||
$this->assertDebuggingCalled('Could not find a safe place to split at offset(s): 6. Those were ignored.');
|
||||
|
||||
// Test another death situation to make sure we're invincible.
|
||||
$css = 'a,,,,,e';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0);
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2);
|
||||
$this->assertInternalType('array', $chunks);
|
||||
$this->assertDebuggingCalled('Could not find a safe place to split at offset(s): 4. Those were ignored.');
|
||||
// I don't care what the outcome is, I just want to make sure it doesn't die.
|
||||
|
||||
// Test media queries.
|
||||
$css = '@media (min-width: 980px) { .a,.b{} }';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2);
|
||||
$this->assertCount(1, $chunks);
|
||||
$this->assertSame('@media (min-width: 980px) { .a,.b{} }', $chunks[0]);
|
||||
|
||||
// Test special rules.
|
||||
$css = 'a,b{ background-image: linear-gradient(to bottom, #ffffff, #cccccc);}d,e{}';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2);
|
||||
$this->assertCount(2, $chunks);
|
||||
$this->assertSame('a,b{ background-image: linear-gradient(to bottom, #ffffff, #cccccc);}', $chunks[0]);
|
||||
$this->assertSame("@import url(styles.php?type=test&chunk=1);\nd,e{}", $chunks[1]);
|
||||
|
||||
// Test media queries with too many selectors.
|
||||
$css = '@media (min-width: 980px) { a,b,c,d{} }';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2);
|
||||
$this->assertCount(1, $chunks);
|
||||
$this->assertSame('@media (min-width: 980px) { a,b,c,d{} }', $chunks[0]);
|
||||
$this->assertDebuggingCalled('Could not find a safe place to split at offset(s): 34. Those were ignored.');
|
||||
|
||||
// Complex test.
|
||||
$css = '@media (a) {b{}} c{} d,e{} f,g,h{} i,j{x:a,b,c} k,l{} @media(x){l,m{ y: a,b,c}} n{}';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 3);
|
||||
$this->assertCount(6, $chunks);
|
||||
$this->assertSame('@media (a) {b{}} c{}', $chunks[0]);
|
||||
$this->assertSame(' d,e{}', $chunks[1]);
|
||||
$this->assertSame(' f,g,h{}', $chunks[2]);
|
||||
$this->assertSame(' i,j{x:a,b,c}', $chunks[3]);
|
||||
$this->assertSame(' k,l{}', $chunks[4]);
|
||||
$this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\n@import url(styles.php?type=test&chunk=3);\n@import url(styles.php?type=test&chunk=4);\n@import url(styles.php?type=test&chunk=5);\n @media(x){l,m{ y: a,b,c}} n{}", $chunks[5]);
|
||||
|
||||
// Multiple offset errors.
|
||||
$css = 'a,b,c{} d,e,f{}';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2);
|
||||
$this->assertCount(2, $chunks);
|
||||
$this->assertSame('a,b,c{}', $chunks[0]);
|
||||
$this->assertSame("@import url(styles.php?type=test&chunk=1);\n d,e,f{}", $chunks[1]);
|
||||
$this->assertDebuggingCalled('Could not find a safe place to split at offset(s): 6, 14. Those were ignored.');
|
||||
|
||||
// Test the split according to IE.
|
||||
$css = str_repeat('a{}', 4100);
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test');
|
||||
$this->assertCount(2, $chunks);
|
||||
$this->assertSame(str_repeat('a{}', 4095), $chunks[0]);
|
||||
$this->assertSame("@import url(styles.php?type=test&chunk=1);\n" . str_repeat('a{}', 5), $chunks[1]);
|
||||
|
||||
// Test strip out comments.
|
||||
$css = ".a {/** a\nb\nc */} /** a\nb\nc */ .b{} /** .c,.d{} */ e{}";
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2);
|
||||
$this->assertCount(2, $chunks);
|
||||
$this->assertSame('.a {} .b{}', $chunks[0]);
|
||||
$this->assertSame("@import url(styles.php?type=test&chunk=1);\n e{}", $chunks[1]);
|
||||
|
||||
// Test something with unicode characters.
|
||||
$css = 'a,b{} nav a:hover:after { content: "↓"; } b{ color:test;}';
|
||||
$chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2);
|
||||
$this->assertCount(2, $chunks);
|
||||
$this->assertSame('a,b{}', $chunks[0]);
|
||||
$this->assertSame("@import url(styles.php?type=test&chunk=1);\n nav a:hover:after { content: \"↓\"; } b{ color:test;}", $chunks[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user