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

Extend CSS tree shaking to remove selectors for non-existent IDs and elements #1142

Merged
merged 2 commits into from
May 11, 2018
Merged
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
86 changes: 76 additions & 10 deletions includes/sanitizers/class-amp-style-sanitizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer {
*/
private $used_class_names = array();

/**
* Tag names used in document.
*
* @since 1.0
* @var array
*/
private $used_tag_names = array();

/**
* XPath.
*
Expand Down Expand Up @@ -245,6 +253,24 @@ private function get_used_class_names() {
return $this->used_class_names;
}


/**
* Get list of all the tag names used in the document.
*
* @since 1.0
* @return array Used tag names.
*/
private function get_used_tag_names() {
if ( empty( $this->used_tag_names ) ) {
$used_tag_names = array();
foreach ( $this->dom->getElementsByTagName( '*' ) as $el ) {
$used_tag_names[ $el->tagName ] = true;
}
$this->used_tag_names = array_keys( $used_tag_names );
}
return $this->used_tag_names;
}

/**
* Sanitize CSS styles within the HTML contained in this instance's DOMDocument.
*
Expand Down Expand Up @@ -531,7 +557,7 @@ private function process_stylesheet( $stylesheet, $node, $options = array() ) {

$cache_key = md5( $stylesheet . serialize( $cache_impacting_options ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize

$cache_group = 'amp-parsed-stylesheet-v2';
$cache_group = 'amp-parsed-stylesheet-v4';
if ( wp_using_ext_object_cache() ) {
$parsed = wp_cache_get( $cache_key, $cache_group );
} else {
Expand Down Expand Up @@ -614,6 +640,7 @@ function ( $value ) {
$validation_errors = $this->process_css_list( $css_document, $options );

$output_format = Sabberworm\CSS\OutputFormat::createCompact();
$output_format->setSemicolonAfterLastRule( false );

$before_declaration_block = '/*AMP_WP_BEFORE_DECLARATION_BLOCK*/';
$between_selectors = '/*AMP_WP_BETWEEN_SELECTORS*/';
Expand Down Expand Up @@ -644,18 +671,34 @@ function ( $value ) {

$selectors_parsed = array();
foreach ( $selectors as $selector ) {
$classes = array();
$selectors_parsed[ $selector ] = array();

// Remove :not() to eliminate false negatives, such as with `body:not(.title-tagline-hidden) .site-branding-text`.
$reduced_selector = preg_replace( '/:not\(.+?\)/', '', $selector );
// Remove :not() and pseudo selectors to eliminate false negatives, such as with `body:not(.title-tagline-hidden) .site-branding-text`.
$reduced_selector = preg_replace( '/:[a-zA-Z0-9_-]+(\(.+?\))?/', '', $selector );

// Remove attribute selectors to eliminate false negative, such as with `.social-navigation a[href*="example.com"]:before`.
$reduced_selector = preg_replace( '/\[\w.*?\]/', '', $reduced_selector );

if ( preg_match_all( '/(?<=\.)([a-zA-Z0-9_-]+)/', $reduced_selector, $matches ) ) {
$classes = $matches[0];
$reduced_selector = preg_replace_callback(
'/\.([a-zA-Z0-9_-]+)/',
function( $matches ) use ( $selector, &$selectors_parsed ) {
$selectors_parsed[ $selector ]['classes'][] = $matches[1];
return '';
},
$reduced_selector
);
$reduced_selector = preg_replace_callback(
'/#([a-zA-Z0-9_-]+)/',
function( $matches ) use ( $selector, &$selectors_parsed ) {
$selectors_parsed[ $selector ]['ids'][] = $matches[1];
return '';
},
$reduced_selector
);

if ( preg_match_all( '/[a-zA-Z0-9_-]+/', $reduced_selector, $matches ) ) {
$selectors_parsed[ $selector ]['tags'] = $matches[0];
}
$selectors_parsed[ $selector ] = $classes;
}

// Restore calc() functions that were replaced with placeholders.
Expand Down Expand Up @@ -1403,6 +1446,7 @@ function( $selector ) {
$stylesheet_set['processed_nodes'] = array();

$final_size = 0;
$dom = $this->dom;
foreach ( $stylesheet_set['pending_stylesheets'] as &$pending_stylesheet ) {
$stylesheet = '';
foreach ( $pending_stylesheet['stylesheet'] as $stylesheet_part ) {
Expand All @@ -1412,12 +1456,34 @@ function( $selector ) {
list( $selectors_parsed, $declaration_block ) = $stylesheet_part;
if ( $should_tree_shake ) {
$selectors = array();
foreach ( $selectors_parsed as $selector => $class_names ) {
foreach ( $selectors_parsed as $selector => $parsed_selector ) {
$should_include = (
( $dynamic_selector_pattern && preg_match( $dynamic_selector_pattern, $selector ) )
||
// If all class names are used in the doc.
0 === count( array_diff( $class_names, $this->get_used_class_names() ) )
(
// If all class names are used in the doc.
(
empty( $parsed_selector['classes'] )
||
0 === count( array_diff( $parsed_selector['classes'], $this->get_used_class_names() ) )
)
&&
// If all IDs are used in the doc.
(
empty( $parsed_selector['ids'] )
||
0 === count( array_filter( $parsed_selector['ids'], function( $id ) use ( $dom ) {
return ! $dom->getElementById( $id );
} ) )
)
&&
// If tag names are present in the doc.
(
empty( $parsed_selector['tags'] )
||
0 === count( array_diff( $parsed_selector['tags'], $this->get_used_tag_names() ) )
)
)
);
if ( $should_include ) {
$selectors[] = $selector;
Expand Down
Loading