Preserve Inline Styles#470
Conversation
This adds a new sanitizer class that collects the contents of style attributes, adds a unique class to each element with a style attribute, and then outputs it all in the `amp-custom` stylesheet.
Filter the styles using `safecss_filter_attr()` to make sure there aren't any funky properties. Also alphabetize the valid properties so that variable order doesn't cause unnecessary duplication.
Make sure that if there is nothing left after the inline styles are processed, no class is added to the element.
tests/test-amp-style-sanitizer.php
Outdated
| private function wrap_style( $array ) { | ||
| ob_start(); ?> | ||
|
|
||
| /* Inline Styles */ |
There was a problem hiding this comment.
We're duplicating the generate_class_name and style output behaviours between the sanitizer and the tests, which means that if the implementation in the sanitizer changes but has a bug, we could end up not catching it.
| } | ||
|
|
||
| private function generate_class_name( $string ) { | ||
| return 'amp-inline-style-' . md5( $string ); |
There was a problem hiding this comment.
It would be nice to prefix this as amp-wp- for consistency
| return ''; | ||
| } | ||
|
|
||
| return implode( ";\n\t", $arr ) . ';'; |
There was a problem hiding this comment.
I don't think we should worry about whitespace.
There was a problem hiding this comment.
The reasoning is that this helps avoid duplication of generated style rules in the stylesheet. Without it, color: #fff; and color : #fff; would count as two separate styles.
It is an edge case, so if you feel strongly that it's not worth the extra processing, I'm happy to remove it.
There was a problem hiding this comment.
Not sure I understand your example. What would the original style attribute look like in this edge case?
AMP has limits on how much CSS you can include (although, not 100% sure if whitespace counts) so we should try to keep it as slim as possible. Plus, no need to send extra bytes if we can avoid it.
There was a problem hiding this comment.
What would the original style attribute look like in this edge case?
Example markup:
<p>I really like the color <span style="color: #00ff00;">green</span>.</p>
<p>Miguel really likes <span style="color: #00ff00;">green</span> also.</p>
Without whitespace normalizing, the resulting amp-custom styles would be:
/* Inline Styles */
.amp-wp-inline-style-04a69db6d1a130e8e077df8a467e51dc {
color: #00ff00;
}
.amp-wp-inline-style-bb01159393134c225a1df0d44226c3d2 {
color: #00ff00;
}
With whitespace normalizing:
/* Inline Styles */
.amp-wp-inline-style-bb01159393134c225a1df0d44226c3d2 {
color: #00ff00;
}
There was a problem hiding this comment.
Oh, I think we're talking about different things :)
My comment (a bit unclear, I realize) was just referring to the addition of \n\t; not the use of trim (which is a good idea as you've outlined).
There was a problem hiding this comment.
Oh! Gotcha. The \n\t just makes it easier to read the style rules when they're printed in the doc header, but I'm fine taking them out.
| $this->collect_styles_recursive( $body ); | ||
|
|
||
| if ( ! empty( $this->styles ) ) { | ||
| add_action( 'amp_post_template_css', array( $this, 'append_styles' ), 99 ); |
There was a problem hiding this comment.
Any ideas on how we could skip the need to hook into actions here and pass back the generated styles some other way? (We've tried to keep Sanitizers pretty focused on just DOM manipulation and kept them free from actions/filters for various reasons.)
There was a problem hiding this comment.
I initially had the same thought. I wanted to pass the data to AMP_Post_Template and add the action there, but I couldn't figure out a way to add data to the class without changing the access of some of the methods from private to public. Do you think it would be worth modifying AMP_Post_Template to make this possible?
There was a problem hiding this comment.
What if we just added get_styles to the Base Sanitizer?
| $new_class = trim( $class . ' ' . $class_name ); | ||
|
|
||
| $node->setAttribute( 'class', $new_class ); | ||
| $this->styles[ $class_name ] = $style; |
There was a problem hiding this comment.
Why don't we just remove the style attribute as well?
There was a problem hiding this comment.
Since the Blacklist sanitizer is already designed to remove attributes, including style, I thought it best followed the single responsibility principle to leave the removal part out of the Style sanitizer.
There could be a scenario where someone changes the order of the sanitizers and the Blacklist runs first, but if that happens, the Style sanitizer won't do anything anyway, since there won't be any remaining style attributes.
There was a problem hiding this comment.
Another approach could be to not remove the style attribute in Blacklist, and just handle everything in the Style class. It seems like this could be confusing to other devs, though, since the Blacklist is designed specifically for removing tags and attributes...
There was a problem hiding this comment.
I think in this particular case, it's probably fine for the Style Sanitizer to remove the attribute once it's done.
Instead of duplicating the Style sanitizer's methods for generating class names and outputting style rules, the expected values are just strings. This helps avoid testing issues in the future if any of the methods change. See #470 (review)
Instead of hooking into `amp_post_template_css` directly from the sanitizer, this creates a generic `get_styles` method in the Base sanitizer class, which is then used to pass the styles as a string back to `AMP_Post_Template`, where it can be output in a post template action. This creates the possibility for other sanitizers to generate and output dynamic styles as well.
| } | ||
|
|
||
| private function generate_class_name( $string ) { | ||
| return 'amp-wp-inline-style-' . md5( $string ); |
There was a problem hiding this comment.
Minor: let's remove -style from the class name.
| } | ||
|
|
||
| public function get_styles() { | ||
| if ( ! empty( $this->styles ) ) { |
There was a problem hiding this comment.
Let's actually move have this just return the style array and have our amp_post_template_add_styles callback handle the rendering. That way, we maintain consistency with how get_scripts() works and avoid the use of ob_ (and likely makes the tests a bit cleaner too).
There was a problem hiding this comment.
Yeah, this does sound a bit cleaner. Ok, stay tuned.
Instead of rendering the styles into a CSS block in the sanitizer, keep them in array form until they are loaded by the function hooked to `amp_post_template_css`.
|
Just two small notes and I think we're good to merge! |
* Use `printf` to make the `amp_post_template_add_styles` function more readable * Output the CSS in a compact format to minimize data footprint and whitespace
|
Cool! Here are the changes: 5d69505 |
|
💥 💥 💥 Thanks @coreymckrill! |
Adds the
AMP_Style_Sanitizersanitizer class, which runs before the Blacklist class, and finds all of the element nodes within the content that have astyleattribute. For each one, it stores the contents ofstyleand generates a unique class name, which it adds to the element. Then, during template rendering, it outputs these class names and their styles in theamp-customstylesheet in the document head.This preserves critical style information that gets stripped from the elements by the Blacklist class, which appears to be the root cause of the problems in #344, #358, #362, and #395.
This also includes unit tests for the new sanitizer class. Testing on the performance impact of this additional sanitizer class was done using this script.