Skip to content

Commit

Permalink
Closes #3539 Combine @import rules into the minified CSS files (PR #3603
Browse files Browse the repository at this point in the history
)
  • Loading branch information
engahmeds3ed authored and iCaspar committed Mar 23, 2021
1 parent bcc54d0 commit 6b64d61
Show file tree
Hide file tree
Showing 6 changed files with 441 additions and 6 deletions.
283 changes: 282 additions & 1 deletion inc/Engine/Optimization/CSSTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use WP_Rocket\Dependencies\PathConverter\ConverterInterface;
use WP_Rocket\Dependencies\PathConverter\Converter;
use WP_Rocket\Logger\Logger;

trait CSSTrait {
/**
Expand Down Expand Up @@ -35,6 +36,10 @@ public function rewrite_paths( $source, $target, $content ) {
*/
$target = apply_filters( 'rocket_css_asset_target_path', $target );

$content = $this->move( $this->get_converter( $source, $target ), $content, $source );

$content = $this->combine_imports( $content, $target );

/**
* Filters the content of a CSS file
*
Expand All @@ -44,7 +49,7 @@ public function rewrite_paths( $source, $target, $content ) {
* @param string $source Source filepath.
* @param string $target Target filepath.
*/
return apply_filters( 'rocket_css_content', $this->move( $this->get_converter( $source, $target ), $content, $source ), $source, $target );
return apply_filters( 'rocket_css_content', $content, $source, $target );
}

/**
Expand Down Expand Up @@ -191,6 +196,141 @@ protected function move( ConverterInterface $converter, $content, $source ) {
return str_replace( $search, $replace, $content );
}

/**
* Replace local imports with their contents recursively.
*
* @since 3.8.6
*
* @param string $content CSS Content.
* @param string $target Target CSS file path.
*
* @return string
*/
protected function combine_imports( $content, $target ) {
$import_regexes = [
// @import url(xxx)
'/
# import statement
@import
# whitespace
\s+
# open url()
url\(
# (optional) open path enclosure
(?P<quotes>["\']?)
# fetch path
(?P<path>.+?)
# (optional) close path enclosure
(?P=quotes)
# close url()
\)
# (optional) trailing whitespace
\s*
# (optional) media statement(s)
(?P<media>[^;]*)
# (optional) trailing whitespace
\s*
# (optional) closing semi-colon
;?
/ix',

// @import 'xxx'
'/
# import statement
@import
# whitespace
\s+
# open path enclosure
(?P<quotes>["\'])
# fetch path
(?P<path>.+?)
# close path enclosure
(?P=quotes)
# (optional) trailing whitespace
\s*
# (optional) media statement(s)
(?P<media>[^;]*)
# (optional) trailing whitespace
\s*
# (optional) closing semi-colon
;?
/ix',
];

// find all relative imports in css.
$matches = [];
foreach ( $import_regexes as $import_regexe ) {
if ( preg_match_all( $import_regexe, $content, $regex_matches, PREG_SET_ORDER ) ) {
$matches = array_merge( $matches, $regex_matches );
}
}

if ( empty( $matches ) ) {
return $content;
}

$search = [];
$replace = [];

// loop the matches.
foreach ( $matches as $match ) {
/**
* Filter Skip import replacement for one file.
*
* @since 3.8.6
*
* @param bool Skipped or not (Default not skipped).
* @param string $file_path Matched import path.
* @param string $import_match Full import match.
*/
if ( apply_filters( 'rocket_skip_import_replacement', false, $match['path'], $match ) ) {
continue;
}

list( $import_path, $import_content ) = $this->get_internal_file_contents( $match['path'], dirname( $target ) );

if ( empty( $import_content ) ) {
continue;
}

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

// Use recursion to rewrite paths and combine imports again for imported content.
$import_content = $this->rewrite_paths( $import_path, $target, $import_content );

// add to replacement array.
$search[] = $match[0];
$replace[] = $import_content;
}

// replace the import statements.
return str_replace( $search, $replace, $content );
}

/**
* Applies font-display:swap to all font-family rules without a previously set font-display property.
*
Expand Down Expand Up @@ -219,4 +359,145 @@ function ( $matches ) {
$css_file_content
);
}

/**
* Get internal file full path and contents.
*
* @since 3.8.6
*
* @param string $file Internal file path (maybe external url or relative path).
* @param string $base_path Base path as reference for relative paths.
*
* @return array Array of two values ( full path, contents )
*/
private function get_internal_file_contents( $file, $base_path ) {
if ( $this->is_external_path( $file ) && wp_http_validate_url( $file ) ) {
return [ $file, false ];
}

// Remove query strings.
$file = str_replace( '?' . wp_parse_url( $file, PHP_URL_QUERY ), '', $file );

// Check if this file is readable or it's relative path so we add base_path at it's start.
if ( ! rocket_direct_filesystem()->is_readable( $this->get_local_path( $file ) ) ) {
$ds = rocket_get_constant( 'DIRECTORY_SEPARATOR' );
$file = $base_path . $ds . str_replace( '/', $ds, $file );
}else {
$file = $this->get_local_path( $file );
}

$file_type = wp_check_filetype( $file, [ 'css' => 'text/css' ] );

if ( 'css' !== $file_type['ext'] ) {
return [ $file, null ];
}

$import_content = rocket_direct_filesystem()->get_contents( $file );

return [ $file, $import_content ];
}

/**
* Determines if the file is external.
*
* @since 3.8.6
*
* @param string $url URL of the file.
* @return bool True if external, false otherwise.
*/
protected function is_external_path( $url ) {
$file = get_rocket_parse_url( $url );

if ( empty( $file['path'] ) ) {
return true;
}

$parsed_site_url = wp_parse_url( site_url() );

if ( empty( $parsed_site_url['host'] ) ) {
return true;
}

// This filter is documented in inc/Engine/Admin/Settings/Settings.php.
$hosts = (array) apply_filters( 'rocket_cdn_hosts', [], [ 'all' ] );
$hosts[] = $parsed_site_url['host'];
$langs = get_rocket_i18n_uri();

// Get host for all langs.
foreach ( $langs as $lang ) {
$url_host = wp_parse_url( $lang, PHP_URL_HOST );

if ( ! isset( $url_host ) ) {
continue;
}

$hosts[] = $url_host;
}

$hosts = array_unique( $hosts );

if ( empty( $hosts ) ) {
return true;
}

// URL has domain and domain is part of the internal domains.
if ( ! empty( $file['host'] ) ) {
foreach ( $hosts as $host ) {
if ( false !== strpos( $url, $host ) ) {
return false;
}
}

return true;
}

return false;
}

/**
* Get local absolute path for image.
*
* @since 3.8.6
*
* @param string $url Image url.
*
* @return string Image absolute local path.
*/
private function get_local_path( $url ) {
$url = $this->normalize_url( $url );

$path = rocket_url_to_path( $url );
if ( $path ) {
return $path;
}

$relative_url = ltrim( wp_make_link_relative( $url ), '/' );
$ds = rocket_get_constant( 'DIRECTORY_SEPARATOR' );
$base_path = isset( $_SERVER['DOCUMENT_ROOT'] ) ? ( sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) . $ds ) : '';

return $base_path . str_replace( '/', $ds, $relative_url );
}

/**
* Normalize relative url to full url.
*
* @since 3.8.6
*
* @param string $url Url to be normalized.
*
* @return string Normalized url.
*/
private function normalize_url( $url ) {
$url_host = wp_parse_url( $url, PHP_URL_HOST );

if ( ! empty( $url_host ) ) {
return $url;
}

$relative_url = ltrim( wp_make_link_relative( $url ), '/' );
$site_url_components = wp_parse_url( site_url( '/' ) );

return $site_url_components['scheme'] . '://' . $site_url_components['host'] . '/' . $relative_url;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,53 @@
'wp-content/cache/min/1/b6dcf622d68835c7b1cd01e3cb339560.css',
'wp-content/cache/min/1/b6dcf622d68835c7b1cd01e3cb339560.css.gz',
],
'css' => '@import url(vfs://public/wp-content/themes/twentytwenty/style.css);body{font-family:Helvetica,Arial,sans-serif;text-align:center}body{font-family:Helvetica,Arial,sans-serif;text-align:center}',
'css' => 'body{font-family:Helvetica,Arial,sans-serif;text-align:center}body{font-family:Helvetica,Arial,sans-serif;text-align:center}body{font-family:Helvetica,Arial,sans-serif;text-align:center}',
],

'cdn_host' => [],
'cdn_url' => 'http://example.org',
'site_url' => 'http://example.org',
],

'combineCssFilesWithImportJSFile' => [
'original' =>
'<html><head><title>Sample Page</title>' .
'<link rel="stylesheet" href="http://example.org/wp-content/themes/twentytwenty/style-import-jsfile.css" type="text/css" media="all">' .
'</head><body></body></html>',

'expected' => [
'html' => '<html><head><title>Sample Page</title>' .
'<link rel="stylesheet" href="http://example.org/wp-content/cache/min/1/c78ec42dba4ed16fe23582b1e3d03895.css" media="all" data-minify="1" />' .
'</head><body></body></html>',
'files' => [
'wp-content/cache/min/1/c78ec42dba4ed16fe23582b1e3d03895.css',
'wp-content/cache/min/1/c78ec42dba4ed16fe23582b1e3d03895.css.gz',
],
'css' => '@import url(vfs://public/wp-content/themes/twentytwenty/assets/script.js);',
],

'cdn_host' => [],
'cdn_url' => 'http://example.org',
'site_url' => 'http://example.org',
],

'combineCssFilesWithNestedImport' => [
'original' =>
'<html><head><title>Sample Page</title>' .
'<link rel="stylesheet" href="http://example.org/wp-content/themes/twentytwenty/style-import2.css" type="text/css" media="all">' .
'<link rel="stylesheet" href="http://example.org/wp-content/plugins/hello-dolly/style.css">' .
'<link rel="stylesheet" href="http://example.org/wp-includes/css/dashicons.min.css">' .
'</head><body></body></html>',

'expected' => [
'html' => '<html><head><title>Sample Page</title>' .
'<link rel="stylesheet" href="http://example.org/wp-content/cache/min/1/a41edef8114680bb60b530fa32be3ca5.css" media="all" data-minify="1" />' .
'</head><body></body></html>',
'files' => [
'wp-content/cache/min/1/a41edef8114680bb60b530fa32be3ca5.css',
'wp-content/cache/min/1/a41edef8114680bb60b530fa32be3ca5.css.gz',
],
'css' => '@import "http://www.google.com/style.css";.style-import-external{color:green}.style-another-import2{color:green}.style-another-import{color:red}body{font-family:Helvetica,Arial,sans-serif;text-align:center}body{font-family:Helvetica,Arial,sans-serif;text-align:center}',
],

'cdn_host' => [],
Expand All @@ -88,7 +134,7 @@
'wp-content/cache/min/1/afeb29591023f7eb6314ad594ca01138.css',
'wp-content/cache/min/1/afeb29591023f7eb6314ad594ca01138.css.gz',
],
'css' => '@import url(vfs://public/wp-content/themes/twentytwenty/style.css);body{font-family:Helvetica,Arial,sans-serif;text-align:center}body{font-family:Helvetica,Arial,sans-serif;text-align:center}',
'css' => 'body{font-family:Helvetica,Arial,sans-serif;text-align:center}body{font-family:Helvetica,Arial,sans-serif;text-align:center}body{font-family:Helvetica,Arial,sans-serif;text-align:center}',
],

'cdn_host' => [],
Expand Down Expand Up @@ -247,7 +293,7 @@
'wp-content/cache/min/1/074f89d1546ea3c6df831e874538e908.css',
'wp-content/cache/min/1/074f89d1546ea3c6df831e874538e908.css.gz',
],
'css' => "@import url(vfs://public/wp-content/themes/twentytwenty/style.css);@font-face{font-display:swap;font-family:'FontAwesome';src:url(https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/../fonts/fontawesome-webfont.eot?v=4.7.0);src:url('https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url(https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/../fonts/fontawesome-webfont.woff2?v=4.7.0) format('woff2'),url(https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/../fonts/fontawesome-webfont.woff?v=4.7.0) format('woff'),url(https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/../fonts/fontawesome-webfont.ttf?v=4.7.0) format('truetype'),url('https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:400;font-style:normal}body{font-family:Helvetica,Arial,sans-serif;text-align:center}body{font-family:Helvetica,Arial,sans-serif;text-align:center}body{font-family:Helvetica,Arial,sans-serif;text-align:center}@font-face{font-display:swap;font-family:Helvetica}footer{color:red}",
'css' => "@font-face{font-display:swap;font-family:'FontAwesome';src:url(https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/../fonts/fontawesome-webfont.eot?v=4.7.0);src:url('https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url(https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/../fonts/fontawesome-webfont.woff2?v=4.7.0) format('woff2'),url(https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/../fonts/fontawesome-webfont.woff?v=4.7.0) format('woff'),url(https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/../fonts/fontawesome-webfont.ttf?v=4.7.0) format('truetype'),url('https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:400;font-style:normal}body{font-family:Helvetica,Arial,sans-serif;text-align:center}body{font-family:Helvetica,Arial,sans-serif;text-align:center}body{font-family:Helvetica,Arial,sans-serif;text-align:center}@font-face{font-display:swap;font-family:Helvetica}body{font-family:Helvetica,Arial,sans-serif;text-align:center}footer{color:red}",
],

'cdn_host' => [],
Expand Down
Loading

0 comments on commit 6b64d61

Please sign in to comment.