Fix #11325: smaller/stronger domManip/buildFragment/clean

This commit is contained in:
Richard Gibson 2012-04-04 23:00:58 -04:00 committed by Dave Methvin
parent 7c814f4837
commit 22ad8723ce
2 changed files with 105 additions and 130 deletions

View File

@ -132,7 +132,7 @@ jQuery.fn = jQuery.prototype = {
} }
} else { } else {
ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); ret = jQuery.buildFragment( [ match[1] ], doc );
selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes;
} }

View File

@ -25,6 +25,7 @@ var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figca
rnoInnerhtml = /<(?:script|style)/i, rnoInnerhtml = /<(?:script|style)/i,
rnocache = /<(?:script|object|embed|option|style)/i, rnocache = /<(?:script|object|embed|option|style)/i,
rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"),
rcheckableType = /^(?:checkbox|radio)$/,
// checked="checked" or checked // checked="checked" or checked
rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
rscriptType = /\/(java|ecma)script/i, rscriptType = /\/(java|ecma)script/i,
@ -287,21 +288,23 @@ jQuery.fn.extend({
}, },
domManip: function( args, table, callback ) { domManip: function( args, table, callback ) {
var results, first, fragment, var results, first, fragment, iNoClone,
i = 0,
value = args[0], value = args[0],
scripts = []; scripts = [],
l = this.length;
// We can't cloneNode fragments that contain checked, in WebKit // 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() { return this.each(function() {
jQuery(this).domManip( args, table, callback, true ); jQuery(this).domManip( args, table, callback );
}); });
} }
if ( jQuery.isFunction(value) ) { if ( jQuery.isFunction(value) ) {
return this.each(function(i) { return this.each(function(i) {
var self = jQuery(this); 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 ); self.domManip( args, table, callback );
}); });
} }
@ -310,30 +313,25 @@ jQuery.fn.extend({
results = jQuery.buildFragment( args, this, scripts ); results = jQuery.buildFragment( args, this, scripts );
fragment = results.fragment; fragment = results.fragment;
if ( fragment.childNodes.length === 1 ) {
first = fragment = fragment.firstChild;
} else {
first = fragment.firstChild; first = fragment.firstChild;
if ( fragment.childNodes.length === 1 ) {
fragment = first;
} }
if ( first ) { if ( first ) {
table = table && jQuery.nodeName( first, "tr" ); 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( callback.call(
table ? table && jQuery.nodeName( this[i], "table" ) ?
root(this[i], first) : findOrAppend( this[i], "tbody" ) :
this[i], this[i],
// Make sure that we do not leak memory by inadvertently discarding i === iNoClone ?
// the original fragment (which might have attached data) instead of fragment :
// using it; in addition, use the original fragment object for the last jQuery.clone( fragment, true, true )
// 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
); );
} }
} }
@ -363,11 +361,8 @@ jQuery.fn.extend({
} }
}); });
function root( elem, cur ) { function findOrAppend( elem, tag ) {
return jQuery.nodeName(elem, "table") ? return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) );
(elem.getElementsByTagName("tbody")[0] ||
elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
elem;
} }
function cloneCopyEvent( src, dest ) { function cloneCopyEvent( src, dest ) {
@ -426,7 +421,7 @@ function cloneFixAttributes( src, dest ) {
if ( nodeName === "object" ) { if ( nodeName === "object" ) {
dest.outerHTML = src.outerHTML; 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 // IE6-8 fails to persist the checked state of a cloned checkbox
// or radio button. Worse, IE6-7 fail to give the cloned element // or radio button. Worse, IE6-7 fail to give the cloned element
// a checked appearance if the defaultChecked value isn't also set // a checked appearance if the defaultChecked value isn't also set
@ -465,22 +460,19 @@ function cloneFixAttributes( src, dest ) {
dest.removeAttribute( "_change_attached" ); dest.removeAttribute( "_change_attached" );
} }
jQuery.buildFragment = function( args, nodes, scripts ) { jQuery.buildFragment = function( args, context, scripts ) {
var fragment, cacheable, cacheresults, doc, var fragment, cacheable, cachehit,
first = args[ 0 ]; first = args[ 0 ];
// nodes may contain either an explicit document object, // Set context from what may come in as undefined or a jQuery collection or a node
// a jQuery collection or context object. context = context || document;
// If nodes[0] contains a valid object to assign to doc context = (context[0] || context).ownerDocument || context[0] || context;
if ( nodes && nodes[0] ) {
doc = nodes[0].ownerDocument || nodes[0];
}
// Ensure that an attr object doesn't incorrectly stand in as a document object // 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 // Chrome and Firefox seem to allow this to occur and will throw exception
// Fixes #8950 // Fixes #8950
if ( !doc.createDocumentFragment ) { if ( typeof context.createDocumentFragment === "undefined" ) {
doc = document; context = document;
} }
// Only cache "small" (1/2 KB) HTML strings that are associated with the main 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 <object> or <embed> elements in a fragment // IE 6 doesn't like it when you put <object> or <embed> elements in a fragment
// Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache // 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 // 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 ) && first.charAt(0) === "<" && !rnocache.test( first ) &&
(jQuery.support.checkClone || !rchecked.test( first )) && (jQuery.support.checkClone || !rchecked.test( first )) &&
(jQuery.support.html5Clone || !rnoshimcache.test( first )) ) { (jQuery.support.html5Clone || !rnoshimcache.test( first )) ) {
// Mark cacheable and look for a hit
cacheable = true; cacheable = true;
fragment = jQuery.fragments[ first ];
cacheresults = jQuery.fragments[ first ]; cachehit = fragment !== undefined;
if ( cacheresults && cacheresults !== 1 ) {
fragment = cacheresults;
} }
}
if ( !fragment ) { if ( !fragment ) {
fragment = doc.createDocumentFragment(); fragment = context.createDocumentFragment();
jQuery.clean( args, doc, fragment, scripts ); jQuery.clean( args, context, fragment, scripts );
}
if ( cacheable ) { 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 }; return { fragment: fragment, cacheable: cacheable };
@ -557,20 +548,10 @@ function getAll( elem ) {
// Used in clean, fixes the defaultChecked property // Used in clean, fixes the defaultChecked property
function fixDefaultChecked( elem ) { function fixDefaultChecked( elem ) {
if ( elem.type === "checkbox" || elem.type === "radio" ) { if ( rcheckableType.test( elem.type ) ) {
elem.defaultChecked = elem.checked; 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 // Derived From: http://www.iecss.com/shimprove/javascript/shimprove.1-0-1.js
function shimCloneNode( elem ) { function shimCloneNode( elem ) {
@ -637,17 +618,17 @@ jQuery.extend({
}, },
clean: function( elems, context, fragment, scripts ) { 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 = []; ret = [];
context = context || document; // Ensure that context is a document
if ( !context || typeof context.createDocumentFragment === "undefined" ) {
// !context.createElement fails in IE with an error but returns typeof 'object' context = document;
if ( typeof context.createElement === "undefined" ) {
context = context.ownerDocument || context[0] && context[0].ownerDocument || 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" ) { if ( typeof elem === "number" ) {
elem += ""; elem += "";
} }
@ -661,27 +642,17 @@ jQuery.extend({
if ( !rhtml.test( elem ) ) { if ( !rhtml.test( elem ) ) {
elem = context.createTextNode( elem ); elem = context.createTextNode( elem );
} else { } 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 // Fix "XHTML"-style tags in all browsers
elem = elem.replace(rxhtmlTag, "<$1></$2>"); elem = elem.replace(rxhtmlTag, "<$1></$2>");
// 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 // 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]; div.innerHTML = wrap[1] + elem + wrap[2];
// Move to the right depth // Move to the right depth
@ -693,7 +664,7 @@ jQuery.extend({
if ( !jQuery.support.tbody ) { if ( !jQuery.support.tbody ) {
// String was a <table>, *may* have spurious <tbody> // String was a <table>, *may* have spurious <tbody>
var hasBody = rtbody.test(elem), hasBody = rtbody.test(elem);
tbody = tag === "table" && !hasBody ? tbody = tag === "table" && !hasBody ?
div.firstChild && div.firstChild.childNodes : div.firstChild && div.firstChild.childNodes :
@ -716,59 +687,63 @@ jQuery.extend({
elem = div.childNodes; elem = div.childNodes;
// Clear elements from DocumentFragment (safeFragment or otherwise) // Remember the top-level container for proper cleanup
// to avoid hoarding elements. Fixes #11356 div = safe.lastChild;
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 );
} }
} }
}
}
}
// Resets 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] );
}
} else {
findInputs( elem );
}
}
if ( elem.nodeType ) { if ( elem.nodeType ) {
ret.push( elem ); ret.push( elem );
} else { } else {
ret = jQuery.merge( ret, elem ); ret = jQuery.merge( ret, elem );
}
}
if ( fragment ) {
checkScriptType = function( elem ) {
return !elem.type || rscriptType.test( elem.type );
};
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 );
ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) );
} }
fragment.appendChild( script ); }
// Fix #11356: Clear elements from safeFragment
if ( div ) {
safe.removeChild( div );
div = safe = null;
}
// Reset defaultChecked for any radios and checkboxes
// about to be appended to the DOM in IE 6/7 (#8060)
if ( !jQuery.support.appendChecked ) {
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 );
}
}
}
// Append elements to a provided document fragment
if ( fragment ) {
// 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; (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;
}
} }
} }
} }