ParserOutput: Add 'deduplicateStyles' post-cache transformation
authorBrad Jorsch <bjorsch@wikimedia.org>
Fri, 24 Nov 2017 19:22:25 +0000 (14:22 -0500)
committerGergő Tisza <gtisza@wikimedia.org>
Sun, 11 Feb 2018 05:55:56 +0000 (05:55 +0000)
This transformation will find <style> tag with a "data-mw-deduplicate"
attribute. For each value of the attribute, the first instance will be
kept as-is, while any subsequent tags with the same value will be
replaced by a <link rel="mw-deduplicated-inline-style"> with its href
referring to the "data-mw-deduplicate" value using a custom scheme.

This also adds an $attribs parameter to Html::inlineStyle() so the
data-mw-deduplicate attribute can be added.

Note this doesn't actually depend on Ib931e25c, but action=mobileview
will break if it starts being used without that patch.

Bug: T160563
Change-Id: I055abdf4d73ec65771eaa4fe0999ec907c831568
Depends-On: Ib931e25ce85072000e62c486bbe5907f03372494

RELEASE-NOTES-1.31
includes/Html.php
includes/api/ApiParse.php
includes/api/i18n/en.json
includes/api/i18n/qqq.json
includes/parser/ParserOutput.php
tests/phpunit/includes/parser/ParserOutputTest.php

index 88d927e..cc08b33 100644 (file)
@@ -46,6 +46,9 @@ production.
   initial page text for file uploads.
 * (T181651) The info page for File pages now displays the file's base-16 SHA1
   hash value in the table of basic information.
+* Style tags with a 'data-mw-deduplicate' attribute will be deduplicated as a
+  ParserOutput::getText() post-cache transformation. This may be disabled by
+  passing 'deduplicateStyles' => false to that method.
 
 === External library changes in 1.31 ===
 
index dfd80a8..3bcf131 100644 (file)
@@ -589,9 +589,12 @@ class Html {
         *
         * @param string $contents CSS
         * @param string $media A media type string, like 'screen'
+        * @param array $attribs (since 1.31) Associative array of attributes, e.g., [
+        *   'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for
+        *   further documentation.
         * @return string Raw HTML
         */
-       public static function inlineStyle( $contents, $media = 'all' ) {
+       public static function inlineStyle( $contents, $media = 'all', $attribs = [] ) {
                // Don't escape '>' since that is used
                // as direct child selector.
                // Remember, in css, there is no "x" for hexadecimal escapes, and
@@ -609,7 +612,7 @@ class Html {
 
                return self::rawElement( 'style', [
                        'media' => $media,
-               ], $contents );
+               ] + $attribs, $contents );
        }
 
        /**
index 2839ab9..3326fab 100644 (file)
@@ -345,6 +345,7 @@ class ApiParse extends ApiBase {
                                'allowTOC' => !$params['disabletoc'],
                                'enableSectionEditLinks' => !$params['disableeditsection'],
                                'unwrap' => $params['wrapoutputclass'] === '',
+                               'deduplicateStyles' => !$params['disablestylededuplication'],
                        ] );
                        $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
                }
@@ -877,6 +878,7 @@ class ApiParse extends ApiBase {
                        'disablelimitreport' => false,
                        'disableeditsection' => false,
                        'disabletidy' => false,
+                       'disablestylededuplication' => false,
                        'generatexml' => [
                                ApiBase::PARAM_DFLT => false,
                                ApiBase::PARAM_HELP_MSG => [
index 729c4c7..1689019 100644 (file)
        "apihelp-parse-param-disablepp": "Use <var>$1disablelimitreport</var> instead.",
        "apihelp-parse-param-disableeditsection": "Omit edit section links from the parser output.",
        "apihelp-parse-param-disabletidy": "Do not run HTML cleanup (e.g. tidy) on the parser output.",
+       "apihelp-parse-param-disablestylededuplication": "Do not deduplicate inline stylesheets in the parser output.",
        "apihelp-parse-param-generatexml": "Generate XML parse tree (requires content model <code>$1</code>; replaced by <kbd>$2prop=parsetree</kbd>).",
        "apihelp-parse-param-preview": "Parse in preview mode.",
        "apihelp-parse-param-sectionpreview": "Parse in section preview mode (enables preview mode too).",
index 1e4bfc8..e769880 100644 (file)
        "apihelp-parse-param-disablepp": "{{doc-apihelp-param|parse|disablepp}}",
        "apihelp-parse-param-disableeditsection": "{{doc-apihelp-param|parse|disableeditsection}}",
        "apihelp-parse-param-disabletidy": "{{doc-apihelp-param|parse|disabletidy}}",
+       "apihelp-parse-param-disablestylededuplication": "{{doc-apihelp-param|parse|disablestylededuplication}}",
        "apihelp-parse-param-generatexml": "{{doc-apihelp-param|parse|generatexml|params=* $1 - Value of the constant CONTENT_MODEL_WIKITEXT|paramstart=2}}",
        "apihelp-parse-param-preview": "{{doc-apihelp-param|parse|preview}}",
        "apihelp-parse-param-sectionpreview": "{{doc-apihelp-param|parse|sectionpreview}}",
index 346a151..19375e0 100644 (file)
@@ -21,6 +21,7 @@
  * @file
  * @ingroup Parser
  */
+
 class ParserOutput extends CacheTime {
        /**
         * Feature flags to indicate to extensions that MediaWiki core supports and
@@ -270,6 +271,12 @@ class ParserOutput extends CacheTime {
         *     section edit link tokens are present in the HTML. Default is true,
         *     but might be statefully overridden.
         *  - unwrap: (bool) Remove a wrapping mw-parser-output div. Default is false.
+        *  - deduplicateStyles: (bool) When true, which is the default, `<style>`
+        *    tags with the `data-mw-deduplicate` attribute set are deduplicated by
+        *    value of the attribute: all but the first will be replaced by `<link
+        *    rel="mw-deduplicated-inline-style" href="mw-data:..."/>` tags, where
+        *    the scheme-specific-part of the href is the (percent-encoded) value
+        *    of the `data-mw-deduplicate` attribute.
         * @return string HTML
         */
        public function getText( $options = [] ) {
@@ -290,6 +297,7 @@ class ParserOutput extends CacheTime {
                        'allowTOC' => !empty( $this->mTOCEnabled ),
                        'enableSectionEditLinks' => $this->mEditSectionTokens,
                        'unwrap' => false,
+                       'deduplicateStyles' => true,
                ];
                $text = $this->mText;
 
@@ -350,6 +358,35 @@ class ParserOutput extends CacheTime {
                        );
                }
 
+               if ( $options['deduplicateStyles'] ) {
+                       $seen = [];
+                       $text = preg_replace_callback(
+                               '#<style\s+([^>]*data-mw-deduplicate\s*=[^>]*)>.*?</style>#s',
+                               function ( $m ) use ( &$seen ) {
+                                       $attr = Sanitizer::decodeTagAttributes( $m[1] );
+                                       if ( !isset( $attr['data-mw-deduplicate'] ) ) {
+                                               return $m[0];
+                                       }
+
+                                       $key = $attr['data-mw-deduplicate'];
+                                       if ( !isset( $seen[$key] ) ) {
+                                               $seen[$key] = true;
+                                               return $m[0];
+                                       }
+
+                                       // We were going to use an empty <style> here, but there
+                                       // was concern that would be too much overhead for browsers.
+                                       // So let's hope a <link> with a non-standard rel and href isn't
+                                       // going to be misinterpreted or mangled by any subsequent processing.
+                                       return Html::element( 'link', [
+                                               'rel' => 'mw-deduplicated-inline-style',
+                                               'href' => "mw-data:" . wfUrlencode( $key ),
+                                       ] );
+                               },
+                               $text
+                       );
+               }
+
                return $text;
        }
 
index 1f3ee67..44c1773 100644 (file)
@@ -153,6 +153,19 @@ class ParserOutputTest extends MediaWikiTestCase {
 <h2><span class="mw-headline" id="Section_3">Section 3</span><mw:editsection page="Test Page" section="4">Section 3</mw:editsection></h2>
 <p>Three
 </p></div>
+EOF;
+
+               $dedupText = <<<EOF
+<p>This is a test document.</p>
+<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
+<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
+<style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
+<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
+<style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
+<style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
+<style data-mw-deduplicate="duplicate1">.Same-attribute-different-content {}</style>
+<style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
+<style>.Duplicate1 {}</style>
 EOF;
 
                return [
@@ -354,6 +367,23 @@ EOF
 <!-- Saved in parser cache... -->', '<p>Test document.</p>
 <!-- Saved in parser cache... -->'
                        ],
+                       'Style deduplication' => [
+                               [], [], $dedupText, <<<EOF
+<p>This is a test document.</p>
+<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
+<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/>
+<style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
+<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/>
+<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate2"/>
+<style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
+<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/>
+<style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
+<style>.Duplicate1 {}</style>
+EOF
+                       ],
+                       'Style deduplication disabled' => [
+                               [ 'deduplicateStyles' => false ], [], $dedupText, $dedupText
+                       ],
                ];
                // phpcs:enable
        }