Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove Duplicate CSS Selectors from the final content #236

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 84 additions & 39 deletions src/CSS.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ class CSS extends Minify
* @var string[] valid import extensions
*/
protected $importExtensions = array(
'gif' => 'data:image/gif',
'png' => 'data:image/png',
'jpe' => 'data:image/jpeg',
'jpg' => 'data:image/jpeg',
'gif' => 'data:image/gif',
'png' => 'data:image/png',
'jpe' => 'data:image/jpeg',
'jpg' => 'data:image/jpeg',
'jpeg' => 'data:image/jpeg',
'svg' => 'data:image/svg+xml',
'svg' => 'data:image/svg+xml',
'woff' => 'data:application/x-font-woff',
'tif' => 'image/tiff',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'xbm' => 'image/x-xbitmap',
'xbm' => 'image/x-xbitmap',
);

/**
Expand Down Expand Up @@ -93,7 +93,7 @@ protected function moveImportsToTop($content)
}

// add to top
$content = implode(';', $matches[2]).';'.trim($content, ';');
$content = implode(';', $matches[2]) . ';' . trim($content, ';');
}

return $content;
Expand All @@ -105,8 +105,8 @@ protected function moveImportsToTop($content)
* @import's will be loaded and their content merged into the original file,
* to save HTTP requests.
*
* @param string $source The file to combine imports for
* @param string $content The CSS content to combine imports for
* @param string $source The file to combine imports for
* @param string $content The CSS content to combine imports for
* @param string[] $parents Parent paths, for circular reference checks
*
* @return string
Expand Down Expand Up @@ -200,7 +200,7 @@ protected function combineImports($source, $content, $parents)
// loop the matches
foreach ($matches as $match) {
// get the path for the file that will be imported
$importPath = dirname($source).'/'.$match['path'];
$importPath = dirname($source) . '/' . $match['path'];

// only replace the import with the content if we can grab the
// content of the file
Expand All @@ -211,7 +211,7 @@ protected function combineImports($source, $content, $parents)
// check if current file was not imported previously in the same
// import chain.
if (in_array($importPath, $parents)) {
throw new FileImportException('Failed to import file "'.$importPath.'": circular reference detected.');
throw new FileImportException('Failed to import file "' . $importPath . '": circular reference detected.');
}

// grab referenced file & minify it (which may include importing
Expand All @@ -221,7 +221,7 @@ protected function combineImports($source, $content, $parents)

// check if this is only valid for certain media
if (!empty($match['media'])) {
$importContent = '@media '.$match['media'].'{'.$importContent.'}';
$importContent = '@media ' . $match['media'] . '{' . $importContent . '}';
}

// add to replacement array
Expand All @@ -239,7 +239,7 @@ protected function combineImports($source, $content, $parents)
* @url(image.jpg) images will be loaded and their content merged into the
* original file, to save HTTP requests.
*
* @param string $source The file to import files for
* @param string $source The file to import files for
* @param string $content The CSS content to import files for
*
* @return string
Expand All @@ -260,7 +260,7 @@ protected function importFiles($source, $content)

// get the path for the file that will be imported
$path = $match[2];
$path = dirname($source).'/'.$path;
$path = dirname($source) . '/' . $path;

// only replace the import with the content if we're able to get
// the content of the file, and it's relatively small
Expand All @@ -271,7 +271,7 @@ protected function importFiles($source, $content)

// build replacement
$search[] = $match[0];
$replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')';
$replace[] = 'url(' . $this->importExtensions[$extension] . ';base64,' . $importContent . ')';
}
}

Expand All @@ -287,9 +287,10 @@ protected function importFiles($source, $content)
* Perform CSS optimizations.
*
* @param string[optional] $path Path to write the data to
* @param string[] $parents Parent paths, for circular reference checks
* @param string[] $parents Parent paths, for circular reference checks
*
* @return string The minified data
* @throws FileImportException
*/
public function execute($path = null, $parents = array())
{
Expand All @@ -303,15 +304,28 @@ public function execute($path = null, $parents = array())
* we should leave it alone. E.g.:
* p { content: "a test" }
*/

$this->extractStrings();
$this->stripComments();

if ($this->options['css-strip-comments'] === true)
$this->stripComments();

$css = $this->replace($css);

$css = $this->stripWhitespace($css);
$css = $this->shortenHex($css);
$css = $this->shortenZeroes($css);
$css = $this->shortenFontWeights($css);
$css = $this->stripEmptyTags($css);
if ($this->options['css-strip-whitespace'] === true)
$css = $this->stripWhitespace($css);

if ($this->options['css-shorten-hex'] === true)
$css = $this->shortenHex($css);

if ($this->options['css-shorten-zeroes'] === true)
$css = $this->shortenZeroes($css);

if ($this->options['css-shorten-font-weights'] === true)
$css = $this->shortenFontWeights($css);

if ($this->options['css-strip-empty-tags'] === true)
$css = $this->stripEmptyTags($css);

// restore the string we've extracted earlier
$css = $this->restoreExtractedData($css);
Expand All @@ -337,6 +351,37 @@ public function execute($path = null, $parents = array())

$content = $this->moveImportsToTop($content);

/*
* After getting the merged data of all css files
* this method here will remove duplicate css selectors
* for instance, a user might add two css files like default.css & default.css?v=1
* both files contain the same selectors "body {font-family: xxx}"
* this method will deal with those duplicates and remove them from the final content.
*/
$content = $this->removeDuplicates($content);

return $content;
}

/**
* Remove Duplicate CSS Selectors
* for example if there is duplicate body{font-size:13px}
* this method will return one selector
*
* @param $content
* @return string
*/
private function removeDuplicates($content)
{

// Collect Selectors
preg_match_all('/(?ims)([a-z0-9, \s\.\:#_\-@]+)\{([^\}]*)\}/', $content, $selectors);

if (isset($selectors[0]))

// return a unique array of selectors and implode it into a string
return implode(null, array_unique($selectors[0]));

return $content;
}

Expand All @@ -347,7 +392,7 @@ public function execute($path = null, $parents = array())
* (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper).
*
* @param ConverterInterface $converter Relative path converter
* @param string $content The CSS content to update relative urls for
* @param string $content The CSS content to update relative urls for
*
* @return string
*/
Expand Down Expand Up @@ -461,9 +506,9 @@ protected function move(ConverterInterface $converter, $content)
// build replacement
$search[] = $match[0];
if ($type === 'url') {
$replace[] = 'url('.$url.')';
$replace[] = 'url(' . $url . ')';
} elseif ($type === 'import') {
$replace[] = '@import "'.$url.'"';
$replace[] = '@import "' . $url . '"';
}
}

Expand Down Expand Up @@ -503,7 +548,7 @@ protected function shortenHex($content)
'#FFC0CB' => 'pink',
'#DDA0DD' => 'plum',
'#800080' => 'purple',
'#F00' => 'red',
'#F00' => 'red',
'#FA8072' => 'salmon',
'#A0522D' => 'sienna',
'#C0C0C0' => 'silver',
Expand All @@ -515,7 +560,7 @@ protected function shortenHex($content)
);

return preg_replace_callback(
'/(?<=[: ])('.implode(array_keys($colors), '|').')(?=[; }])/i',
'/(?<=[: ])(' . implode(array_keys($colors), '|') . ')(?=[; }])/i',
function ($match) use ($colors) {
return $colors[strtoupper($match[0])];
},
Expand All @@ -534,14 +579,14 @@ protected function shortenFontWeights($content)
{
$weights = array(
'normal' => 400,
'bold' => 700,
'bold' => 700,
);

$callback = function ($match) use ($weights) {
return $match[1].$weights[$match[2]];
return $match[1] . $weights[$match[2]];
};

return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content);
return preg_replace_callback('/(font-weight\s*:\s*)(' . implode('|', array_keys($weights)) . ')(?=[;}])/', $callback, $content);
}

/**
Expand Down Expand Up @@ -577,19 +622,19 @@ protected function shortenZeroes($content)
// practice, Webkit (especially Safari) seems to stumble over at least
// 0%, potentially other units as well. Only stripping 'px' for now.
// @see https://github.com/matthiasmullie/minify/issues/60
$content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content);
$content = preg_replace('/' . $before . '(-?0*(\.0+)?)(?<=0)px' . $after . '/', '\\1', $content);

// strip 0-digits (.0 -> 0)
$content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content);
$content = preg_replace('/' . $before . '\.0+' . $units . '?' . $after . '/', '0\\1', $content);
// strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px
$content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content);
$content = preg_replace('/' . $before . '(-?[0-9]+\.[0-9]+)0+' . $units . '?' . $after . '/', '\\1\\2', $content);
// strip trailing 0: 50.00 -> 50, 50.00px -> 50px
$content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content);
$content = preg_replace('/' . $before . '(-?[0-9]+)\.0+' . $units . '?' . $after . '/', '\\1\\2', $content);
// strip leading 0: 0.1 -> .1, 01.1 -> 1.1
$content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content);
$content = preg_replace('/' . $before . '(-?)0+([0-9]*\.[0-9]+)' . $units . '?' . $after . '/', '\\1\\2\\3', $content);

// strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0)
$content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content);
$content = preg_replace('/' . $before . '-?0+' . $units . '?' . $after . '/', '0\\1', $content);

// IE doesn't seem to understand a unitless flex-basis value (correct -
// it goes against the spec), so let's add it in again (make it `%`,
Expand Down Expand Up @@ -655,7 +700,7 @@ protected function stripWhitespace($content)
// not in things like `calc(3px + 2px)`, shorthands like `3px -2px`, or
// selectors like `div.weird- p`
$pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type');
$content = preg_replace('/:('.implode('|', $pseudos).')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content);
$content = preg_replace('/:(' . implode('|', $pseudos) . ')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content);

// remove semicolon/whitespace followed by closing bracket
$content = str_replace(';}', '}', $content);
Expand Down Expand Up @@ -690,7 +735,7 @@ protected function findCalcs($content)
}
}

$results['calc('.count($results).')'] = 'calc'.$expr;
$results['calc(' . count($results) . ')'] = 'calc' . $expr;
}

return $results;
Expand Down
Loading