From 22ad8723ce07569a9b039c7901f29e86ad14523c Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 4 Apr 2012 23:00:58 -0400 Subject: [PATCH] Fix #11325: smaller/stronger domManip/buildFragment/clean --- src/core.js | 2 +- src/manipulation.js | 213 +++++++++++++++++++------------------------- 2 files changed, 95 insertions(+), 120 deletions(-) diff --git a/src/core.js b/src/core.js index 9c8314c91..c29815249 100644 --- a/src/core.js +++ b/src/core.js @@ -132,7 +132,7 @@ jQuery.fn = jQuery.prototype = { } } else { - ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); + ret = jQuery.buildFragment( [ match[1] ], doc ); selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; } diff --git a/src/manipulation.js b/src/manipulation.js index 1fac92065..91502b572 100644 --- a/src/manipulation.js +++ b/src/manipulation.js @@ -25,6 +25,7 @@ var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figca rnoInnerhtml = /<(?:script|style)/i, rnocache = /<(?:script|object|embed|option|style)/i, rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), + rcheckableType = /^(?:checkbox|radio)$/, // checked="checked" or checked rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, rscriptType = /\/(java|ecma)script/i, @@ -287,21 +288,23 @@ jQuery.fn.extend({ }, domManip: function( args, table, callback ) { - var results, first, fragment, + var results, first, fragment, iNoClone, + i = 0, value = args[0], - scripts = []; + scripts = [], + l = this.length; // We can't cloneNode fragments that contain checked, in WebKit - if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) { + if ( !jQuery.support.checkClone && l > 1 && typeof value === "string" && rchecked.test( value ) ) { return this.each(function() { - jQuery(this).domManip( args, table, callback, true ); + jQuery(this).domManip( args, table, callback ); }); } if ( jQuery.isFunction(value) ) { return this.each(function(i) { var self = jQuery(this); - args[0] = value.call(this, i, table ? self.html() : undefined); + args[0] = value.call( this, i, table ? self.html() : undefined ); self.domManip( args, table, callback ); }); } @@ -310,30 +313,25 @@ jQuery.fn.extend({ results = jQuery.buildFragment( args, this, scripts ); fragment = results.fragment; - if ( fragment.childNodes.length === 1 ) { - first = fragment = fragment.firstChild; - } else { first = fragment.firstChild; + if ( fragment.childNodes.length === 1 ) { + fragment = first; } if ( first ) { table = table && jQuery.nodeName( first, "tr" ); - for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) { + // Use the original fragment for the last item instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + // Fragments from the fragment cache must always be cloned and never used in place. + for ( iNoClone = results.cacheable || l - 1; i < l; i++ ) { callback.call( - table ? - root(this[i], first) : + table && jQuery.nodeName( this[i], "table" ) ? + findOrAppend( this[i], "tbody" ) : this[i], - // Make sure that we do not leak memory by inadvertently discarding - // the original fragment (which might have attached data) instead of - // using it; in addition, use the original fragment object for the last - // item instead of first because it can end up being emptied incorrectly - // in certain situations (Bug #8070). - // Fragments from the fragment cache must always be cloned and never used - // in place. - results.cacheable || ( l > 1 && i < lastIndex ) ? - jQuery.clone( fragment, true, true ) : - fragment + i === iNoClone ? + fragment : + jQuery.clone( fragment, true, true ) ); } } @@ -363,11 +361,8 @@ jQuery.fn.extend({ } }); -function root( elem, cur ) { - return jQuery.nodeName(elem, "table") ? - (elem.getElementsByTagName("tbody")[0] || - elem.appendChild(elem.ownerDocument.createElement("tbody"))) : - elem; +function findOrAppend( elem, tag ) { + return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) ); } function cloneCopyEvent( src, dest ) { @@ -426,7 +421,7 @@ function cloneFixAttributes( src, dest ) { if ( nodeName === "object" ) { dest.outerHTML = src.outerHTML; - } else if ( nodeName === "input" && (src.type === "checkbox" || src.type === "radio") ) { + } else if ( nodeName === "input" && rcheckableType.test( src.type ) ) { // IE6-8 fails to persist the checked state of a cloned checkbox // or radio button. Worse, IE6-7 fail to give the cloned element // a checked appearance if the defaultChecked value isn't also set @@ -465,22 +460,19 @@ function cloneFixAttributes( src, dest ) { dest.removeAttribute( "_change_attached" ); } -jQuery.buildFragment = function( args, nodes, scripts ) { - var fragment, cacheable, cacheresults, doc, +jQuery.buildFragment = function( args, context, scripts ) { + var fragment, cacheable, cachehit, first = args[ 0 ]; - // nodes may contain either an explicit document object, - // a jQuery collection or context object. - // If nodes[0] contains a valid object to assign to doc - if ( nodes && nodes[0] ) { - doc = nodes[0].ownerDocument || nodes[0]; - } + // Set context from what may come in as undefined or a jQuery collection or a node + context = context || document; + context = (context[0] || context).ownerDocument || context[0] || context; // Ensure that an attr object doesn't incorrectly stand in as a document object // Chrome and Firefox seem to allow this to occur and will throw exception // Fixes #8950 - if ( !doc.createDocumentFragment ) { - doc = document; + if ( typeof context.createDocumentFragment === "undefined" ) { + context = document; } // Only cache "small" (1/2 KB) HTML strings that are associated with the main document @@ -488,26 +480,25 @@ jQuery.buildFragment = function( args, nodes, scripts ) { // IE 6 doesn't like it when you put or elements in a fragment // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache // Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501 - if ( args.length === 1 && typeof first === "string" && first.length < 512 && doc === document && + if ( args.length === 1 && typeof first === "string" && first.length < 512 && context === document && first.charAt(0) === "<" && !rnocache.test( first ) && (jQuery.support.checkClone || !rchecked.test( first )) && (jQuery.support.html5Clone || !rnoshimcache.test( first )) ) { + // Mark cacheable and look for a hit cacheable = true; - - cacheresults = jQuery.fragments[ first ]; - if ( cacheresults && cacheresults !== 1 ) { - fragment = cacheresults; + fragment = jQuery.fragments[ first ]; + cachehit = fragment !== undefined; } - } if ( !fragment ) { - fragment = doc.createDocumentFragment(); - jQuery.clean( args, doc, fragment, scripts ); - } - + fragment = context.createDocumentFragment(); + jQuery.clean( args, context, fragment, scripts ); if ( cacheable ) { - jQuery.fragments[ first ] = cacheresults ? fragment : 1; + // Update the cache, but only store false + // unless this is a second parsing of the same content + jQuery.fragments[ first ] = cachehit && fragment; + } } return { fragment: fragment, cacheable: cacheable }; @@ -557,20 +548,10 @@ function getAll( elem ) { // Used in clean, fixes the defaultChecked property function fixDefaultChecked( elem ) { - if ( elem.type === "checkbox" || elem.type === "radio" ) { + if ( rcheckableType.test( elem.type ) ) { elem.defaultChecked = elem.checked; } } -// Finds all inputs and passes them to fixDefaultChecked -function findInputs( elem ) { - var nodeName = ( elem.nodeName || "" ).toLowerCase(); - if ( nodeName === "input" ) { - fixDefaultChecked( elem ); - // Skip scripts, get other children - } else if ( nodeName !== "script" && typeof elem.getElementsByTagName !== "undefined" ) { - jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked ); - } -} // Derived From: http://www.iecss.com/shimprove/javascript/shimprove.1-0-1.js function shimCloneNode( elem ) { @@ -637,17 +618,17 @@ jQuery.extend({ }, clean: function( elems, context, fragment, scripts ) { - var checkScriptType, script, j, + var j, safe, elem, tag, wrap, depth, div, hasBody, tbody, len, handleScript, jsTags, + i = 0, ret = []; - context = context || document; - - // !context.createElement fails in IE with an error but returns typeof 'object' - if ( typeof context.createElement === "undefined" ) { - context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + // Ensure that context is a document + if ( !context || typeof context.createDocumentFragment === "undefined" ) { + context = document; } - for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + // Use the already-created safe fragment if context permits + for ( safe = context === document && safeFragment; (elem = elems[i]) != null; i++ ) { if ( typeof elem === "number" ) { elem += ""; } @@ -661,27 +642,17 @@ jQuery.extend({ if ( !rhtml.test( elem ) ) { elem = context.createTextNode( elem ); } else { + // Ensure a safe container in which to render the html + safe = safe || createSafeFragment( context ); + div = div || safe.appendChild( context.createElement("div") ); + // Fix "XHTML"-style tags in all browsers elem = elem.replace(rxhtmlTag, "<$1>"); - // Trim whitespace, otherwise indexOf won't work as expected - var tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(), - wrap = wrapMap[ tag ] || wrapMap._default, - depth = wrap[0], - div = context.createElement("div"), - safeChildNodes = safeFragment.childNodes, - remove; - - // Append wrapper element to unknown element safe doc fragment - if ( context === document ) { - // Use the fragment we've already created for this document - safeFragment.appendChild( div ); - } else { - // Use a fragment created with the owner document - createSafeFragment( context ).appendChild( div ); - } - // Go to html and back, then peel off extra wrappers + tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + depth = wrap[0]; div.innerHTML = wrap[1] + elem + wrap[2]; // Move to the right depth @@ -693,7 +664,7 @@ jQuery.extend({ if ( !jQuery.support.tbody ) { // String was a , *may* have spurious - var hasBody = rtbody.test(elem), + hasBody = rtbody.test(elem); tbody = tag === "table" && !hasBody ? div.firstChild && div.firstChild.childNodes : @@ -716,59 +687,63 @@ jQuery.extend({ elem = div.childNodes; - // Clear elements from DocumentFragment (safeFragment or otherwise) - // to avoid hoarding elements. Fixes #11356 - if ( div ) { - div.parentNode.removeChild( div ); - - // Guard against -1 index exceptions in FF3.6 - if ( safeChildNodes.length > 0 ) { - remove = safeChildNodes[ safeChildNodes.length - 1 ]; - - if ( remove && remove.parentNode ) { - remove.parentNode.removeChild( remove ); + // Remember the top-level container for proper cleanup + div = safe.lastChild; } } + + if ( elem.nodeType ) { + ret.push( elem ); + } else { + ret = jQuery.merge( ret, elem ); } } + + // Fix #11356: Clear elements from safeFragment + if ( div ) { + safe.removeChild( div ); + div = safe = null; } - // Resets defaultChecked for any radios and checkboxes + // Reset defaultChecked for any radios and checkboxes // about to be appended to the DOM in IE 6/7 (#8060) - var len; if ( !jQuery.support.appendChecked ) { - if ( elem[0] && typeof (len = elem.length) === "number" ) { - for ( j = 0; j < len; j++ ) { - findInputs( elem[j] ); + for ( i = 0; (elem = ret[i]) != null; i++ ) { + if ( jQuery.nodeName( elem, "input" ) ) { + fixDefaultChecked( elem ); + } else if ( typeof elem.getElementsByTagName !== "undefined" ) { + jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked ); } - } else { - findInputs( elem ); } } - if ( elem.nodeType ) { - ret.push( elem ); - } else { - ret = jQuery.merge( ret, elem ); - } - } - + // Append elements to a provided document fragment if ( fragment ) { - checkScriptType = function( elem ) { - return !elem.type || rscriptType.test( elem.type ); + // Special handling of each script element + handleScript = function( elem ) { + // Check if we consider it executable + if ( !elem.type || rscriptType.test( elem.type ) ) { + // Detach the script and store it in the scripts array (if provided) or the fragment + // Return truthy to indicate that it has been handled + return scripts ? + scripts.push( elem.parentNode ? elem.parentNode.removeChild( elem ) : elem ) : + fragment.appendChild( elem ); + } }; - for ( i = 0; ret[i]; i++ ) { - script = ret[i]; - if ( scripts && jQuery.nodeName( script, "script" ) && (!script.type || rscriptType.test( script.type )) ) { - scripts.push( script.parentNode ? script.parentNode.removeChild( script ) : script ); - } else { - if ( script.nodeType === 1 ) { - var jsTags = jQuery.grep( script.getElementsByTagName( "script" ), checkScriptType ); + for ( i = 0; (elem = ret[i]) != null; i++ ) { + // Check if we're done after handling an executable script + if ( !( jQuery.nodeName( elem, "script" ) && handleScript( elem ) ) ) { + // Append to fragment and handle embedded scripts + fragment.appendChild( elem ); + if ( typeof elem.getElementsByTagName !== "undefined" ) { + // handleScript alters the DOM, so use jQuery.merge to ensure snapshot iteration + jsTags = jQuery.grep( jQuery.merge( [], elem.getElementsByTagName("script") ), handleScript ); + // Splice the scripts into ret after their former ancestor and advance our index beyond them ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) ); + i += jsTags.length; } - fragment.appendChild( script ); } } } -- 2.39.5