Skip to content

Preserve Inline Styles#470

Merged
mjangda merged 14 commits intoampproject:masterfrom
coreymckrill:master
Sep 28, 2016
Merged

Preserve Inline Styles#470
mjangda merged 14 commits intoampproject:masterfrom
coreymckrill:master

Conversation

@coreymckrill
Copy link
Copy Markdown
Contributor

Adds the AMP_Style_Sanitizer sanitizer class, which runs before the Blacklist class, and finds all of the element nodes within the content that have a style attribute. For each one, it stores the contents of style and generates a unique class name, which it adds to the element. Then, during template rendering, it outputs these class names and their styles in the amp-custom stylesheet 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.

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.
private function wrap_style( $array ) {
ob_start(); ?>

/* Inline Styles */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Fixed with 28092c4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

private function generate_class_name( $string ) {
return 'amp-inline-style-' . md5( $string );
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to prefix this as amp-wp- for consistency

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with 43c6972

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

return '';
}

return implode( ";\n\t", $arr ) . ';';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should worry about whitespace.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with a952797

$this->collect_styles_recursive( $body );

if ( ! empty( $this->styles ) ) {
add_action( 'amp_post_template_css', array( $this, 'append_styles' ), 99 );
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we just added get_styles to the Base Sanitizer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mjangda is this what you're thinking of? bcc1a85

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, exactly!

$new_class = trim( $class . ' ' . $class_name );

$node->setAttribute( 'class', $new_class );
$this->styles[ $class_name ] = $style;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we just remove the style attribute as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in this particular case, it's probably fine for the Style Sanitizer to remove the attribute once it's done.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Fixed with 7288395

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 );
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: let's remove -style from the class name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with 4d8fc50

}

public function get_styles() {
if ( ! empty( $this->styles ) ) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this does sound a bit cleaner. Ok, stay tuned.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`.
@mjangda
Copy link
Copy Markdown
Contributor

mjangda commented Sep 28, 2016

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
@coreymckrill
Copy link
Copy Markdown
Contributor Author

Cool! Here are the changes: 5d69505

@mjangda
Copy link
Copy Markdown
Contributor

mjangda commented Sep 28, 2016

💥 💥 💥

Thanks @coreymckrill!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants