Catrope has submitted this change and it was merged.

Change subject: Work around IE's normalization of style attributes by abusing 
XML parsers
......................................................................


Work around IE's normalization of style attributes by abusing XML parsers

ve.parseXhtml() takes HTML that is also valid XML, and abuses
an XML parser to shadow the style and bgcolor attributes with
data-ve-style and data-ve-bgcolor.

ve.serializeXhtml() takes a document created with ve.parseXhtml()
and undoes the shadowing, restoring the attributes' original
values.

This is necessary because IE corrupts the values of the style
and bgcolor attributes by normalizing them. The original values
of these attributes are not accessible in any way that I could
find other than by using an XML parser.

These functions detect whether the browser bug is present,
so shadowing is not performed in compliant browsers.

Change-Id: I0fb47f7c91f6130788611466f72aa9bdff249b5f
(cherry picked from commit 80dd016d8e84a682ee40853d4cd752e4c68f5f85)
---
M src/ve.js
M tests/ve.test.js
2 files changed, 213 insertions(+), 1 deletion(-)

Approvals:
  Catrope: Verified; Looks good to me, approved



diff --git a/src/ve.js b/src/ve.js
index 1bbd0fe..b1b0310 100644
--- a/src/ve.js
+++ b/src/ve.js
@@ -722,6 +722,9 @@
         *
         * To create an empty document, pass the empty string.
         *
+        * If your input is both valid HTML and valid XML, and you need to work 
around style
+        * normalization bugs in Internet Explorer, use #parseXhtml and 
#serializeXhtml.
+        *
         * @param {string} html HTML string
         * @returns {HTMLDocument} Document constructed from the HTML string
         */
@@ -850,7 +853,7 @@
        };
 
        /**
-        * Helper function for ve#properInnerHtml and #properOuterHtml.
+        * Helper function for #properInnerHtml, #properOuterHtml and 
#serializeXhtml.
         *
         * Detect whether the browser has broken `<pre>` serialization, and if 
so return a clone
         * of the node with extra newlines added to make it serialize properly. 
If the browser is not
@@ -891,6 +894,125 @@
                return $element.get( 0 );
        };
 
+       /**
+        * Helper function for #transformStyleAttributes.
+        *
+        * Normalize an attribute value. In compliant browsers, this should be
+        * a no-op, but in IE style attributes are normalized on all elements 
and
+        * bgcolor attributes are normalized on some elements (like `<tr>`).
+        *
+        * @param {string} name Attribute name
+        * @param {string} value Attribute value
+        * @param {string} [nodeName='div'] Element name
+        * @return {string} Normalized attribute value
+        */
+       ve.normalizeAttributeValue = function ( name, value, nodeName ) {
+               var node = document.createElement( nodeName || 'div' );
+               node.setAttribute( name, value );
+               // IE normalizes invalid CSS to empty string, then if you 
normalize
+               // an empty string again it becomes null. Return an empty string
+               // instead of null to make this function idempotent.
+               return node.getAttribute( name ) || '';
+       };
+
+       /**
+        * Helper function for #parseXhtml and #serializeXhtml.
+        *
+        * Detect whether the browser normalizes style attributes (IE does this)
+        * and if so, map broken attributes to attributes prefixed with data-ve-
+        * or vice versa.
+        *
+        * @param {string} html HTML string. Must also be valid XML
+        * @param {boolean} unmask Map the masked attributes back to their 
originals
+        * @returns {string} HTML string, possibly modified to mask broken 
attributes
+        */
+       ve.transformStyleAttributes = function ( html, unmask ) {
+               var xmlDoc, fromAttr, toAttr, i, len,
+                       maskAttrs = [
+                               'style', // IE normalizes 'color:#ffd' to 
'color: rgb(255, 255, 221);'
+                               'bgcolor' // IE normalizes '#FFDEAD' to 
'#ffdead'
+                       ];
+
+               // Feature-detect style attribute breakage in IE
+               if ( ve.isStyleAttributeBroken === undefined ) {
+                       ve.isStyleAttributeBroken = ve.normalizeAttributeValue( 
'style', 'color:#ffd' ) !== 'color:#ffd';
+               }
+               if ( !ve.isStyleAttributeBroken ) {
+                       // Nothing to do
+                       return html;
+               }
+
+               // Parse the HTML into an XML DOM
+               xmlDoc = new DOMParser().parseFromString( html, 'text/xml' );
+
+               // Go through and mask/unmask each attribute on all elements 
that have it
+               for ( i = 0, len = maskAttrs.length; i < len; i++ ) {
+                       fromAttr = unmask ? 'data-ve-' + maskAttrs[i] : 
maskAttrs[i];
+                       toAttr = unmask ? maskAttrs[i] : 'data-ve-' + 
maskAttrs[i];
+                       /*jshint loopfunc:true */
+                       $( xmlDoc ).find( '[' + fromAttr + ']' ).each( function 
() {
+                               var toAttrValue, fromAttrNormalized,
+                                       fromAttrValue = this.getAttribute( 
fromAttr );
+
+                               if ( unmask ) {
+                                       this.removeAttribute( fromAttr );
+
+                                       // If the data-ve- version doesn't 
normalize to the same value,
+                                       // the attribute must have changed, so 
don't overwrite it
+                                       fromAttrNormalized = 
ve.normalizeAttributeValue( toAttr, fromAttrValue, this.nodeName );
+                                       // toAttr can't not be set, but IE 
returns null if the value was ''
+                                       toAttrValue = this.getAttribute( toAttr 
) || '';
+                                       if ( toAttrValue !== fromAttrNormalized 
) {
+                                               return;
+                                       }
+                               }
+
+                               this.setAttribute( toAttr, fromAttrValue );
+                       } );
+               }
+
+               // HACK: Inject empty text nodes into empty non-void tags to 
prevent
+               // things like <a></a> from being serialized as <a /> and 
wreaking havoc
+               $( xmlDoc ).find( ':empty:not(' + ve.elementTypes.void.join( 
',' ) + ')' ).each( function () {
+                       this.appendChild( xmlDoc.createTextNode( '' ) );
+               } );
+
+               // Serialize back to a string
+               return new XMLSerializer().serializeToString( xmlDoc );
+       };
+
+       /**
+        * Parse an HTML string into an HTML DOM, while masking attributes 
affected by
+        * normalization bugs if a broken browser is detected.
+        * Since this process uses an XML parser, the input must be valid XML 
as well as HTML.
+        *
+        * @param {string} html HTML string. Must also be valid XML
+        * @return {HTMLDocument} HTML DOM
+        */
+       ve.parseXhtml = function ( html ) {
+               return ve.createDocumentFromHtml( ve.transformStyleAttributes( 
html, false ) );
+       };
+
+       /**
+        * Serialize an HTML DOM created with #parseXhtml back to an HTML 
string, unmasking any
+        * attributes that were masked.
+        *
+        * @param {HTMLDocument} doc HTML DOM
+        * @return {string} Serialized HTML string
+        */
+       ve.serializeXhtml = function ( doc ) {
+               var xml = new XMLSerializer().serializeToString( 
ve.fixupPreBug( doc.documentElement ) );
+               // HACK: strip out xmlns
+               xml = xml.replace( '<html 
xmlns="http://www.w3.org/1999/xhtml";', '<html' );
+               return ve.transformStyleAttributes( xml, true );
+       };
+
+       /**
+        * Wrapper for node.normalize(). The native implementation is broken in 
IE,
+        * so we use our own implementation in that case.
+        *
+        * @param {Node} node Node to normalize
+        */
        ve.normalizeNode = function ( node ) {
                var p, nodeIterator, textNode;
                if ( ve.isNormalizeBroken === undefined ) {
diff --git a/tests/ve.test.js b/tests/ve.test.js
index 9ad2ba4..bc740b5 100644
--- a/tests/ve.test.js
+++ b/tests/ve.test.js
@@ -352,6 +352,96 @@
        }
 } );
 
+QUnit.test( 'transformStyleAttributes', function ( assert ) {
+       var i, wasStyleAttributeBroken, oldNormalizeAttributeValue,
+               normalizeColor = function ( name, value ) {
+                       if ( name === 'style' && value === 'color:#ffd' ) {
+                               return 'color: rgb(255, 255, 221);';
+                       }
+                       return value;
+               },
+               normalizeBgcolor = function ( name, value ) {
+                       if ( name === 'bgcolor' ) {
+                               return value && value.toLowerCase();
+                       }
+                       return value;
+               },
+               cases = [
+                       {
+                               msg: 'Empty tags are not changed self-closing 
tags',
+                               before: '<html><head></head><body>Hello <a 
href="foo"></a> world</body></html>'
+                       },
+                       {
+                               msg: 'HTML string with doctype is parsed 
correctly',
+                               before: '<!DOCTYPE 
html><html><head><title>Foo</title></head><body>Hello</body></html>'
+                       },
+                       {
+                               msg: 'Style attributes are masked then 
unmasked',
+                               before: '<body><div 
style="color:#ffd">Hello</div></body>',
+                               masked: '<body><div style="color:#ffd" 
data-ve-style="color:#ffd">Hello</div></body>'
+                       },
+                       {
+                               msg: 'Style attributes that differ but 
normalize the same are overwritten when unmasked',
+                               masked: '<body><div style="color: rgb(255, 255, 
221);" data-ve-style="color:#ffd">Hello</div></body>',
+                               after: '<body><div 
style="color:#ffd">Hello</div></body>',
+                               normalize: normalizeColor
+                       },
+                       {
+                               msg: 'Style attributes that do not normalize 
the same are not overwritten when unmasked',
+                               masked: '<body><div style="color: rgb(0, 0, 
0);" data-ve-style="color:#ffd">Hello</div></body>',
+                               after: '<body><div style="color: rgb(0, 0, 
0);">Hello</div></body>',
+                               normalize: normalizeColor
+                       },
+                       {
+                               msg: 'bgcolor attributes are masked then 
unmasked',
+                               before: '<body><table><tr 
bgcolor="#FFDEAD"></tr></table></body>',
+                               masked: '<body><table><tr bgcolor="#FFDEAD" 
data-ve-bgcolor="#FFDEAD"></tr></table></body>'
+                       },
+                       {
+                               msg: 'bgcolor attributes that differ but 
normalize the same are overwritten when unmasked',
+                               masked: '<body><table><tr bgcolor="#ffdead" 
data-ve-bgcolor="#FFDEAD"></tr></table></body>',
+                               after: '<body><table><tr 
bgcolor="#FFDEAD"></tr></table></body>',
+                               normalize: normalizeBgcolor
+                       },
+                       {
+                               msg: 'bgcolor attributes that do not normalize 
the same are not overwritten when unmasked',
+                               masked: '<body><table><tr bgcolor="#fffffa" 
data-ve-bgcolor="#FFDEAD"></tr></table></body>',
+                               after: '<body><table><tr 
bgcolor="#fffffa"></tr></table></body>',
+                               normalize: normalizeBgcolor
+                       }
+               ];
+       QUnit.expect( 2 * cases.length );
+
+       // Force transformStyleAttributes to think that we're in a broken 
browser
+       wasStyleAttributeBroken = ve.isStyleAttributeBroken;
+       ve.isStyleAttributeBroken = true;
+
+       for ( i = 0; i < cases.length; i++ ) {
+               if ( cases[i].normalize ) {
+                       oldNormalizeAttributeValue = ve.normalizeAttributeValue;
+                       ve.normalizeAttributeValue = cases[i].normalize;
+               }
+               if ( cases[i].before ) {
+                       assert.strictEqual(
+                               ve.transformStyleAttributes( cases[i].before, 
false ),
+                               cases[i].masked || cases[i].before,
+                               cases[i].msg + ' (masking)'
+                       );
+               } else {
+                       assert.ok( true, cases[i].msg + ' (no masking test)' );
+               }
+               assert.strictEqual(
+                       ve.transformStyleAttributes( cases[i].masked || 
cases[i].before, true ),
+                       cases[i].after || cases[i].before,
+                       cases[i].msg + ' (unmasking)'
+               );
+
+               if ( cases[i].normalize ) {
+                       ve.normalizeAttributeValue = oldNormalizeAttributeValue;
+               }
+       }
+} );
+
 QUnit.test( 'normalizeNode', function ( assert ) {
        var i, actual, expected, wasNormalizeBroken,
                cases = [

-- 
To view, visit https://gerrit.wikimedia.org/r/160594
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I0fb47f7c91f6130788611466f72aa9bdff249b5f
Gerrit-PatchSet: 1
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: wmf/1.24wmf20
Gerrit-Owner: Jforrester <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to