Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.10% covered (success)
93.10%
27 / 29
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Display_Critical_CSS
93.10% covered (success)
93.10%
27 / 29
75.00% covered (warning)
75.00%
3 / 4
10.03
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 asynchronize_stylesheets
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
6.07
 display_critical_css
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 onload_flip_stylesheets
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Class that's responsible for rendering
4 * Critical CSS on the site front-end.
5 */
6
7namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
8
9class Display_Critical_CSS {
10
11    /**
12     * @var string The Critical CSS to display.
13     */
14    protected $css;
15
16    /**
17     * @param string $css
18     */
19    public function __construct( $css ) {
20        $this->css = $css;
21    }
22
23    /**
24     * Converts existing screen CSS to be asynchronously loaded.
25     *
26     * @param string $html   The link tag for the enqueued style.
27     * @param string $handle The style's registered handle.
28     * @param string $href   The stylesheet's source URL.
29     * @param string $media  The stylesheet's media attribute.
30     *
31     * @return string
32     * @see style_loader_tag
33     */
34    public function asynchronize_stylesheets(
35        $html,
36        $handle,
37        $href,
38        $media
39    ) {
40        // If there is no critical CSS, do not alter the stylesheet loading.
41        if ( ! $this->css ) {
42            return $html;
43        }
44
45        $supported_loading_methods = array( 'async', 'deferred' );
46
47        /**
48         * Loading method for stylesheets.
49         *
50         * Filter the loading method for each stylesheet for the screen with following values:
51         *     async    - Stylesheets are loaded asynchronously.
52         *                Styles are applied once the stylesheet is loaded completely without render blocking.
53         *     deferred - Loading of stylesheets are deferred until the window load event.
54         *                Styles from all the stylesheets are applied at once after the page load.
55         *
56         * Stylesheet loading behaviour is not altered for any other value such as false or 'default'.
57         * Stylesheet loading is instant and the process blocks the page rendering.
58         *     Eg: add_filter( 'jetpack_boost_async_style', '__return_false' );
59         *
60         * @param string $handle The style's registered handle.
61         * @param string $media  The stylesheet's media attribute.
62         *
63         * @see   onload_flip_stylesheets for how stylesheets loading is deferred.
64         *
65         * @todo  Retrieve settings from database, either via auto-configuration or UI option.
66         */
67        $method = apply_filters( 'jetpack_boost_async_style', 'async', $handle, $media );
68
69        // If the loading method is not supported, do not alter the stylesheet loading.
70        if ( ! in_array( $method, $supported_loading_methods, true ) ) {
71            return $html;
72        }
73
74        // Update the stylesheet markup for supported loading methods using WordPress HTML API.
75        $processor = new \WP_HTML_Tag_Processor( $html );
76        if ( ! $processor->next_tag( 'link' ) ) {
77            return $html;
78        }
79
80        // Only process if this is a stylesheet link tag.
81        if ( 'stylesheet' !== $processor->get_attribute( 'rel' ) ) {
82            return $html;
83        }
84
85        // Set the new attributes based on the selected method.
86        $processor->set_attribute( 'media', 'not all' );
87        $processor->set_attribute( 'data-media', $media );
88        if ( 'async' === $method ) {
89            $processor->set_attribute( 'onload', "this.media=this.dataset.media; delete this.dataset.media; this.removeAttribute( 'onload' );" );
90        }
91
92        // Prepend the original HTML stylesheet tag within the noscript tag
93        // to support the rendering of the stylesheet when JavaScript is disabled.
94        return '<noscript>' . $html . '</noscript>' . $processor->get_updated_html();
95    }
96
97    /**
98     * Prints the critical CSS to the page.
99     */
100    public function display_critical_css() {
101        $critical_css = $this->css;
102
103        if ( ! $critical_css ) {
104            // phpcs:ignore Universal.CodeAnalysis.ConstructorDestructorReturn.ReturnValueFound -- This is not a PHP 4 constructor, that only applies to non-namespaced classes.
105            return false;
106        }
107
108        echo '<style id="jetpack-boost-critical-css">';
109
110        // Ensure no </style> tag (or any HTML tags) in output.
111        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
112        echo wp_strip_all_tags( $critical_css );
113
114        echo '</style>';
115    }
116
117    /**
118     * Add a small piece of JavaScript to the footer, which on load flips all
119     * linked stylesheets from media="not all" to "all", and switches the
120     * Critical CSS <style> block to media="not all" to deactivate it.
121     */
122    public function onload_flip_stylesheets() {
123        /*
124            Unminified version of footer script.
125
126        ?>
127            <script>
128                window.addEventListener( 'load', function() {
129
130                    // Flip all media="not all" links to media="all".
131                    document.querySelectorAll( 'link' ).forEach(
132                        function( link ) {
133                            if ( link.media === 'not all' && link.dataset.media ) {
134                                link.media = link.dataset.media;
135                                delete link.dataset.media;
136                            }
137                        }
138                    );
139
140                    // Turn off Critical CSS style block with media="not all".
141                    var element = document.getElementById( 'jetpack-boost-critical-css' );
142                    if ( element ) {
143                        element.media = 'not all';
144                    }
145
146                } );
147            </script>
148        <?php
149        */
150
151        // Minified version of footer script. See above comment for unminified version.
152        ?>
153        <script>window.addEventListener( 'load', function() {
154                document.querySelectorAll( 'link' ).forEach( function( e ) {'not all' === e.media && e.dataset.media && ( e.media = e.dataset.media, delete e.dataset.media );} );
155                var e = document.getElementById( 'jetpack-boost-critical-css' );
156                e && ( e.media = 'not all' );
157            } );</script>
158        <?php
159    }
160}