From 08f991b8f12f0a79e9d29f04077535160535df32 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 10 May 2018 17:51:58 -0700 Subject: [PATCH 1/2] Add CSS tree shaking based on extant IDs and tag names --- .../sanitizers/class-amp-style-sanitizer.php | 86 ++++++++++++++++--- tests/test-amp-style-sanitizer.php | 14 ++- 2 files changed, 86 insertions(+), 14 deletions(-) diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 961ead0b663..0075f8ca0b7 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -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. * @@ -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. * @@ -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 { @@ -613,6 +639,7 @@ function ( $value ) { $validation_errors = $this->process_css_list( $css_document, $options ); + // @todo Remove trailing semicolons. $output_format = Sabberworm\CSS\OutputFormat::createCompact(); $before_declaration_block = '/*AMP_WP_BEFORE_DECLARATION_BLOCK*/'; @@ -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. @@ -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 ) { @@ -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; diff --git a/tests/test-amp-style-sanitizer.php b/tests/test-amp-style-sanitizer.php index d5f82cd0d5e..a7b53311faf 100644 --- a/tests/test-amp-style-sanitizer.php +++ b/tests/test-amp-style-sanitizer.php @@ -222,7 +222,7 @@ public function test_body_style_attribute_sanitizer( $source, $expected_content, public function get_link_and_style_test_data() { return array( 'multiple_amp_custom_and_other_styles' => array( - '', + '1ius', array( ':root:not(#_):not(#_) b{color:red;}', 'i{color:blue;}', @@ -472,8 +472,10 @@ public function test_large_custom_css_and_rule_removal() { $html = ''; $html .= ''; - $html .= ''; - $html .= '...'; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= '...'; $dom = AMP_DOM_Utils::get_dom( $html ); $error_codes = array(); @@ -486,7 +488,11 @@ public function test_large_custom_css_and_rule_removal() { $sanitizer->sanitize(); $this->assertEquals( - array( '.b{color:blue;}' ), + array( + '.b{color:blue;}', + '#exists{color:white;}', + 'span{color:white;}', + ), array_values( $sanitizer->get_stylesheets() ) ); From 231de1910e2e3c55b4990c8f8f8c24e036409d1a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 10 May 2018 18:15:38 -0700 Subject: [PATCH 2/2] Remove trailing semicolons after last rules to save additional bytes --- .../sanitizers/class-amp-style-sanitizer.php | 2 +- tests/test-amp-style-sanitizer.php | 102 +++++++++--------- tests/test-class-amp-theme-support.php | 2 +- 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 0075f8ca0b7..f99ad2bceb2 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -639,8 +639,8 @@ function ( $value ) { $validation_errors = $this->process_css_list( $css_document, $options ); - // @todo Remove trailing semicolons. $output_format = Sabberworm\CSS\OutputFormat::createCompact(); + $output_format->setSemicolonAfterLastRule( false ); $before_declaration_block = '/*AMP_WP_BEFORE_DECLARATION_BLOCK*/'; $between_selectors = '/*AMP_WP_BETWEEN_SELECTORS*/'; diff --git a/tests/test-amp-style-sanitizer.php b/tests/test-amp-style-sanitizer.php index a7b53311faf..7726fba5a8d 100644 --- a/tests/test-amp-style-sanitizer.php +++ b/tests/test-amp-style-sanitizer.php @@ -29,7 +29,7 @@ public function get_body_style_attribute_data() { 'This is green.', 'This is green.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-bb01159{color:#0f0;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-bb01159{color:#0f0}', ), ), @@ -37,7 +37,7 @@ public function get_body_style_attribute_data() { 'This is green.', 'This is green.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-0837823{color:#0f0;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-0837823{color:#0f0}', ), ), @@ -45,7 +45,7 @@ public function get_body_style_attribute_data() { 'This is green.', 'This is green.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-c71affe{color:#0f0;background-color:#000;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-c71affe{color:#0f0;background-color:#000}', ), ), @@ -53,7 +53,7 @@ public function get_body_style_attribute_data() { 'Kses-banned properties are allowed since Kses will have already applied if user does not have unfiltered_html.', 'Kses-banned properties are allowed since Kses will have already applied if user does not have unfiltered_html.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-224b51a{display:none;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-224b51a{display:none}', ), ), @@ -61,7 +61,7 @@ public function get_body_style_attribute_data() { '!important is converted.', '!important is converted.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-6a75598{padding:1px;outline:3px;}:root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-6a75598{margin:2px;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-6a75598{padding:1px;outline:3px}:root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-6a75598{margin:2px}', ), ), @@ -69,7 +69,7 @@ public function get_body_style_attribute_data() { '!important is converted.', '!important is converted.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-952600b{color:red;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-952600b{color:red}', ), ), @@ -77,7 +77,7 @@ public function get_body_style_attribute_data() { '!important is converted.', '!important is converted.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-1e2bfaa{color:red;background:blue;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-1e2bfaa{color:red;background:blue}', ), ), @@ -85,8 +85,8 @@ public function get_body_style_attribute_data() { 'This is red.', 'This is red.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-bb01159{color:#0f0;}', - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-cc68ddc{color:#f00;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-bb01159{color:#0f0}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-cc68ddc{color:#f00}', ), ), @@ -94,7 +94,7 @@ public function get_body_style_attribute_data() { '
', '
', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-2864855{background:#000;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-2864855{background:#000}', ), ), @@ -102,7 +102,7 @@ public function get_body_style_attribute_data() { '
bold!
', '
bold!
', array( - 'div > span{font-style:italic;}@media screen and ( max-width: 640px ){div > span{font-style:normal;}:root:not(#_):not(#_) div > span{font-weight:normal;}}:root:not(#_):not(#_) div > span{font-weight:bold;}', + 'div > span{font-style:italic}@media screen and ( max-width: 640px ){div > span{font-style:normal}:root:not(#_):not(#_) div > span{font-weight:normal}}:root:not(#_):not(#_) div > span{font-weight:bold}', ), ), @@ -110,7 +110,7 @@ public function get_body_style_attribute_data() { '', '', array( - 'button{font-weight:bold;}@media screen{button{font-weight:bold;}}', + 'button{font-weight:bold}@media screen{button{font-weight:bold}}', ), array( 'illegal_css_property', 'illegal_css_property', 'illegal_css_property', 'illegal_css_property' ), ), @@ -126,7 +126,7 @@ public function get_body_style_attribute_data() { '', '', array( - 'body{color:black;}', + 'body{color:black}', ), array( 'illegal_css_at_rule', 'illegal_css_at_rule', 'illegal_css_at_rule', 'illegal_css_at_rule', 'illegal_css_at_rule' ), ), @@ -135,17 +135,17 @@ public function get_body_style_attribute_data() { '', '', array( - '@media screen and ( max-width: 640px ){body{font-size:small;}}@font-face{font-family:"Open Sans";src:url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2");}@supports (display: grid){div{display:grid;}}@-moz-keyframes appear{from{opacity:0;}to{opacity:1;}}@keyframes appear{from{opacity:0;}to{opacity:1;}}', + '@media screen and ( max-width: 640px ){body{font-size:small}}@font-face{font-family:"Open Sans";src:url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2")}@supports (display: grid){div{display:grid}}@-moz-keyframes appear{from{opacity:0}to{opacity:1}}@keyframes appear{from{opacity:0}to{opacity:1}}', ), ), 'selector_specificity' => array( - '
onetwothree
', + '
onetwothree
', '
onetwothree
', array( - ':root:not(#_) #child{color:red;}:root:not(#_):not(#_) #parent #child{color:pink;}:root:not(#_) .foo{color:blue;}:root:not(#_):not(#_) #me .foo{color:green;}', - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-64b4fd4{color:yellow;}', - ':root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-ab79d9e{color:purple;}', + ':root:not(#_) #child{color:red}:root:not(#_):not(#_) #parent #child{color:pink}:root:not(#_) .foo{color:blue}:root:not(#_):not(#_) #me .foo{color:green}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-64b4fd4{color:yellow}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-ab79d9e{color:purple}', ), ), @@ -153,7 +153,7 @@ public function get_body_style_attribute_data() { '
', '
', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-cbcb5c2{width:253px;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-cbcb5c2{width:253px}', ), ), @@ -161,7 +161,7 @@ public function get_body_style_attribute_data() { '
', '
', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-cd7753e{width:50%;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-cd7753e{width:50%}', ), ), @@ -175,7 +175,7 @@ public function get_body_style_attribute_data() { '
', '
', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-c8aa9e9{width:50px;width:60px;background-color:red;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-c8aa9e9{width:50px;width:60px;background-color:red}', ), ), ); @@ -222,12 +222,12 @@ public function test_body_style_attribute_sanitizer( $source, $expected_content, public function get_link_and_style_test_data() { return array( 'multiple_amp_custom_and_other_styles' => array( - '1ius', + '1ius', array( - ':root:not(#_):not(#_) b{color:red;}', - 'i{color:blue;}', - 'u{color:green;}:root:not(#_):not(#_) u{text-decoration:underline;}', - 's{color:yellow;}', + ':root:not(#_):not(#_) b{color:red}', + 'i{color:blue}', + 'u{color:green}:root:not(#_):not(#_) u{text-decoration:underline}', + 's{color:yellow}', ), array(), ), @@ -240,36 +240,36 @@ public function get_link_and_style_test_data() { 'strong.before-dashicon', '.dashicons-dashboard:before', 'strong.after-dashicon', - ':root:not(#_):not(#_) s{color:yellow;}', + ':root:not(#_):not(#_) s{color:yellow}', ), array(), ), 'style_with_no_head' => array( - 'Not good!', + 'Not good!', array( - 'body{color:red;}', + 'body{color:red}', ), array(), ), 'style_with_not_selectors' => array( '

Hello

', array( - 'body.foo:not(.bar) > p{color:blue;}body.foo:not(.bar) p:not(.baz){color:green;}body.foo p{color:yellow;}', + 'body.foo:not(.bar) > p{color:blue}body.foo:not(.bar) p:not(.baz){color:green}body.foo p{color:yellow}', ), array(), ), 'style_with_attribute_selectors' => array( '', array( - '.social-navigation a[href*="example.com"]{color:red;}', + '.social-navigation a[href*="example.com"]{color:red}', ), array(), ), 'style_on_root_element' => array( 'Hi', array( - 'html:not(#_):not(#_){background-color:blue;}', - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-10b06ba{color:red;}', + 'html:not(#_):not(#_){background-color:blue}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-10b06ba{color:red}', ), array(), ), @@ -286,9 +286,9 @@ public function get_link_and_style_test_data() { '', ) ), array( - 'form [submit-success] b,div[submit-failure] b{color:green;}', - 'amp-live-list li .highlighted{background:yellow;}', - 'body amp-list .portland{color:blue;}', + 'form [submit-success] b,div[submit-failure] b{color:green}', + 'amp-live-list li .highlighted{background:yellow}', + 'body amp-list .portland{color:blue}', ), array(), ), @@ -297,13 +297,13 @@ public function get_link_and_style_test_data() { '', '', '', - '', // Test unbalanced parentheses. + '', // Test unbalanced parentheses. '
', ) ), array( - 'body{color:red;width:-webkit-calc( 1px + 2vh * 3pt - ( 4em / 5 ) );outline:solid 1px blue;}', - '.alignwide{max-width:calc(50% + 22.5rem);border:solid 1px red;}', - '.alignwide{color:red;content:")";}', + 'body{color:red;width:-webkit-calc( 1px + 2vh * 3pt - ( 4em / 5 ) );outline:solid 1px blue}', + '.alignwide{max-width:calc(50% + 22.5rem);border:solid 1px red}', + '.alignwide{color:red;content:")"}', ), array(), ), @@ -448,10 +448,10 @@ public function test_class_amp_bind_preservation() { $sanitizer->sanitize(); $this->assertEquals( array(), $error_codes ); $actual_stylesheets = array_values( $sanitizer->get_stylesheets() ); - $this->assertEquals( '.sidebar1{display:none;}', $actual_stylesheets[0] ); - $this->assertEquals( '.sidebar1.expanded{display:block;}', $actual_stylesheets[1] ); - $this->assertEquals( '.sidebar2{visibility:hidden;}', $actual_stylesheets[2] ); - $this->assertEquals( '.sidebar2.visible{display:block;}', $actual_stylesheets[3] ); + $this->assertEquals( '.sidebar1{display:none}', $actual_stylesheets[0] ); + $this->assertEquals( '.sidebar1.expanded{display:block}', $actual_stylesheets[1] ); + $this->assertEquals( '.sidebar2{visibility:hidden}', $actual_stylesheets[2] ); + $this->assertEquals( '.sidebar2.visible{display:block}', $actual_stylesheets[3] ); $this->assertEmpty( $actual_stylesheets[4] ); } @@ -489,9 +489,9 @@ public function test_large_custom_css_and_rule_removal() { $this->assertEquals( array( - '.b{color:blue;}', - '#exists{color:white;}', - 'span{color:white;}', + '.b{color:blue}', + '#exists{color:white}', + 'span{color:white}', ), array_values( $sanitizer->get_stylesheets() ) ); @@ -521,7 +521,7 @@ public function test_relative_background_url_handling() { $stylesheet = $actual_stylesheets[0]; $this->assertNotContains( '../images/spinner', $stylesheet ); - $this->assertContains( sprintf( '.spinner{background-image:url("%s");', admin_url( 'images/spinner-2x.gif' ) ), $stylesheet ); + $this->assertContains( sprintf( '.spinner{background-image:url("%s")', admin_url( 'images/spinner-2x.gif' ) ), $stylesheet ); } /** @@ -542,7 +542,7 @@ public function get_keyframe_data() { return array( 'style_amp_keyframes' => array( '', - '', + '', array(), ), @@ -560,13 +560,13 @@ public function get_keyframe_data() { 'blacklisted_and_whitelisted_keyframe_properties' => array( '', - '', + '', array( 'illegal_css_property', 'illegal_css_property', 'illegal_css_property' ), ), 'style_amp_keyframes_with_disallowed_rules' => array( '', - '', + '', array( 'unrecognized_css', 'illegal_css_important', 'illegal_css_at_rule' ), ), ); diff --git a/tests/test-class-amp-theme-support.php b/tests/test-class-amp-theme-support.php index 7548a8a3ddd..1260242420d 100644 --- a/tests/test-class-amp-theme-support.php +++ b/tests/test-class-amp-theme-support.php @@ -1029,7 +1029,7 @@ public function test_prepare_response() { $this->assertContains( '', $sanitized_html ); $this->assertContains( '', $sanitized_html ); $this->assertContains( '#s', $sanitized_html ); + $this->assertRegExp( '##s', $sanitized_html ); $this->assertContains( '', $sanitized_html ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript $this->assertContains( '', $sanitized_html ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript $this->assertContains( '', $sanitized_html ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript