/*

* jQuery Templating Plugin
*   NOTE: Created for demonstration purposes.
* Copyright 2010, John Resig
* Dual licensed under the MIT or GPL Version 2 licenses.
*/

(function( jQuery, undefined ){

var oldManip = jQuery.fn.domManip, tmplItmAtt = "_tmplitem",
        newTmplItems = {}, appendToTmplItems, topTmplItem = { key: 0, data: {} }, itemKey = 0, cloneIndex = 0;

function newTmplItem( options, parentItem, fn, data ) {
        // Returns a template item data structure for a new rendered instance of a template (a 'template item'). 
        // The content field is a hierarchical array of strings and nested items (to be
        // removed and replaced by nodes field of dom elements, once inserted in DOM).
        var newItem = { 
                data: data || (parentItem ? parentItem.data : {}),
                tmpl: null,
                parent: parentItem || null,
                nodes: [],
                nest: nest
        };
        if ( options ) {
                jQuery.extend( newItem, options, { nodes: [], parent: parentItem } );
                fn = fn || (typeof options.tmpl === "function" ? options.tmpl : null);
        }
        if ( fn ) {
                // Build the hierarchical content to be used during insertion into DOM
                newItem.tmpl = fn;
                newItem.content = newItem.tmpl( jQuery, newItem );
                newItem.key = ++itemKey;
                // Keep track of new template item, until it is stored as jQuery Data on DOM element
                newTmplItems[itemKey] = newItem;
        }
        return newItem;
}

// Override appendTo etc., in order to provide support for targeting multiple elements. (This code would disappear if integrated in jquery core).
jQuery.each({
        appendTo: "append",
        prependTo: "prepend",
        insertBefore: "before",
        insertAfter: "after",
        replaceAll: "replaceWith"
}, function( name, original ) {
        jQuery.fn[ name ] = function( selector ) {
                var ret = [], insert = jQuery( selector ),
                        parent = this.length === 1 && this[0].parentNode;

                appendToTmplItems = newTmplItems || {};
                if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
                        insert[ original ]( this[0] );
                        ret = this;
                } else {
                        for ( var i = 0, l = insert.length; i < l; i++ ) {
                                cloneIndex = i;
                                var elems = (i > 0 ? this.clone(true) : this).get();
                                jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
                                ret = ret.concat( elems );
                        }
                        cloneIndex = 0;
                        ret = this.pushStack( ret, name, insert.selector );
                }
                var tmplItems = appendToTmplItems;
                appendToTmplItems = null;
                jQuery.tmpl.complete( tmplItems );
                return ret;
        };
});

jQuery.fn.extend({
        // Use first wrapped element as template markup.
        // Return wrapped set of template items, obtained by rendering template against data.
        tmpl: function( data, options, parentItem ) {
                return jQuery.tmpl( this[0], data, options, parentItem, parentItem === undefined );
        },

        // Find which rendered template item the first wrapped DOM element belongs to 
        tmplItem: function() {
                return jQuery.tmplItem( this[0] );
        },

        // Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template. 
        templates: function( name ) {
                return jQuery.templates( name, this[0] );
        },

        domManip: function( args, table, callback, options ) {
                // This appears to be a bug in the appendTo, etc. implementation
                // it should be doing .call() instead of .apply(). See #6227
                if ( args[0].nodeType ) {
                        var dmArgs = jQuery.makeArray( arguments ), argsLength = args.length, i = 0, tmplItem;
                        while ( i < argsLength && !(tmplItem = jQuery.data( args[i++], "tmplItem" ))) {};
                        if ( argsLength > 1 ) {
                                dmArgs[0] = [jQuery.makeArray( args )];
                        }
                        if ( tmplItem && cloneIndex ) {
                                dmArgs[2] = function( fragClone ) {
                                        // Handler called by oldManip when rendered template has been inserted into DOM.
                                        jQuery.tmpl.afterManip( this, fragClone, callback );
                                }
                        }
                        oldManip.apply( this, dmArgs );
                } else {
                        oldManip.apply( this, arguments );
                }
                cloneIndex = 0;
                if ( !appendToTmplItems ) {
                        jQuery.tmpl.complete( newTmplItems );
                }
                return this;
        }
});

jQuery.extend({
        // Return wrapped set of template items, obtained by rendering template against data.
        tmpl: function( tmpl, data, options, parentItem ) {
                var ret, topLevel = !parentItem;
                if ( topLevel ) {
                        // This is a top-level tmpl call (not from a nested template using {{tmpl}})
                        parentItem = topTmplItem;
                        tmpl = jQuery.templates[tmpl] || jQuery.templates( null, tmpl );
                } else if ( !tmpl ) {
                        // The template item is already associated with DOM - this is a refresh.
                        // Re-evaluate rendered template for the parentItem
                        tmpl =  parentItem.tmpl;
                        newTmplItems[parentItem.key] = parentItem;
                        parentItem.nodes = [];
                        // Rebuild, without creating a new template item
                        return jQuery( build( parentItem, null, parentItem.tmpl( jQuery, parentItem ) ));
                } 
                if ( !tmpl ) {
                        return []; // Could throw...
                }
                if ( typeof data === "function" ) {
                        data = data.call( parentItem.data || {}, parentItem );
                }
                ret = jQuery.isArray( data ) ? 
                        jQuery.map( data, function( dataItem ) {
                                return newTmplItem( options, parentItem, tmpl, dataItem );
                        }) :
                        [ newTmplItem( options, parentItem, tmpl, data ) ];

                return topLevel ? jQuery( build( parentItem, null, ret ) ) : ret;
        },

        // Return rendered template item for an element.
        tmplItem: function( elem ) {
                var tmplItem;
                if ( elem instanceof jQuery ) {
                        elem = tmpl[0];
                }
                while ( elem && elem.nodeType === 1 && !(tmplItem = jQuery.data( elem, "tmplItem" )) && (elem = elem.parentNode) ) {}
                return tmplItem || topTmplItem;
        },

        // Set: 
        // Use $.templates( name, tmpl ) to cache a named template, 
        // where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc.
        // Use $( "selector" ).templates( name ) to provide access by name to a script block template declaration.

        // Get: 
        // Use $.templates( name ) to access a cached template.
        // Also $( selectorToScriptBlock ).templates(), or $.templates( null, templateString ) 
        // will return the compiled template, without adding a name reference.  
        templates: function( name, tmpl ) {
                if (tmpl) {
                        // Compile template and associate with name
                        if ( typeof tmpl === "string" ) {
                                // This is an HTML string being passed directly in.
                                tmpl = buildTmplFn( tmpl )
                        } else if ( tmpl instanceof jQuery ) {
                                tmpl = tmpl[0] || {};
                        } 
                        if ( tmpl.nodeType ) {
                                // If this is a template block, use cached copy, or generate tmpl function and cache.
                                tmpl = jQuery.data( tmpl, "tmpl" ) || jQuery.data( tmpl, "tmpl", buildTmplFn( tmpl.innerHTML ));
                        }
                        return name ? (jQuery.templates[name] = tmpl) : tmpl;
                }
                // Return named compiled template
                return typeof name !== "string" ? null : 
                        (jQuery.templates[name] || 
                                // If not in map, treat as a selector. 
                                jQuery.templates( null, jQuery( name ))); 
        },

        encode: function( text ) {
                // Do HTML encoding replacing < > & and ' and " by corresponding entities.
                return ("" + text).split("<").join("&lt;").split(">").join("&gt;").split('"').join("&#34;").split("'").join("&#39;");
        }
});

jQuery.extend( jQuery.tmpl, {
        tags: {
                "tmpl": {
                        _default: { $2: "null" },
                        prefix: "if($notnull_1){_=_.concat($item.nest($1,$2));}"
                        // tmpl target parameter can be of type function, so use $1, not $1a (so not auto detection of functions)
                        // This means that {{tmpl foo}} treats foo as a template (which IS a function). 
                        // Explicit parens can be used if foo is a function that returns a template: {{tmpl foo()}}.
                },
                "each": {
                        _default: { $2: "$index, $value" },
                        prefix: "if($notnull_1){$.each($1a,function($2){with(this){",
                        suffix: "}});}"
                },
                "if": {
                        prefix: "if(($notnull_1) && $1a){",
                        suffix: "}"
                },
                "else": {
                        prefix: "}else{"
                },
                "html": {
                        prefix: "if($notnull_1){_.push($1a);}"
                },
                "=": {
                        _default: { $1: "$data" },
                        prefix: "if($notnull_1){_.push($.encode($1a));}"
                }
        },

        // This stub can be overridden, e.g. in jquery.tmplPlus for providing rendered events
        complete: function( items ) {
                newTmplItems = {};
        },

        // Call this from code which overrides domManip, or equivalent
        // Manage cloning/storing template items etc.
        afterManip: function afterManip( elem, fragClone, callback ) {
                // Provides cloned fragment ready for fixup prior to and after insertion into DOM
                var content = fragClone.nodeType === 11 ?
                        jQuery.makeArray(fragClone.childNodes) :
                        fragClone.nodeType === 1 ? [fragClone] : [];

                // Return fragment to original caller (e.g. append) for DOM insertion
                callback.call( elem, fragClone );

                // Fragment has been inserted:- Add inserted nodes to tmplItem data structure. Replace inserted element annotations by jQuery.data.
                storeTmplItems( content );
                cloneIndex++;
        }
});

//========================== Private helper functions, used by code above ========================== 

function build( tmplItem, parent, content ) {
        // Convert hierarchical content into flat string array 
        // and finally return array of fragments ready for DOM insertion
        var frag, ret = jQuery.map( content, function( item ) {
                return (typeof item === "string") ? 
                        // Insert template item annotations, to be converted to jQuery.data( "tmplItem" ) when elems are inserted into DOM.
                        item.replace( /(<\w+)([^>]*)/g, "$1 " + tmplItmAtt + "=\"" + tmplItem.key + "\" $2" ) : 
                        // This is a child template item. Build nested template.
                        build( item, tmplItem, item.content );
        });
        if ( parent ) {
                // nested template
                return ret;
        }
        // top-level template
        ret = ret.join("");

        // Support templates which have initial or final text nodes, or consist only of text
        // Also support HTML entities within the HTML markup.
        ret.replace( /^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/, function( all, before, middle, after) {
                frag = jQuery( middle ).get(); // For now use get(), since buildFragment is not current public

// frag = jQuery.buildFragment( [middle] ); // If buildFragment was public, could do these two lines instead // frag = frag.cacheable ? frag.fragment.cloneNode(true) : frag.fragment;

                storeTmplItems( frag );
                if ( before ) {
                        frag = unencode( before ).concat(frag);
                }
                if ( after ) {
                        frag = frag.concat(unencode( after ));
                }
        });
        return frag ? frag : unencode( ret );
}

function unencode( text ) {
        // createTextNode will not render HTML entities correctly
        var el = document.createElement( "div" );
        el.innerHTML = text;
        return jQuery.makeArray(el.childNodes);
}

// Generate a reusable function that will serve to render a template against data
function buildTmplFn( markup ) {
        return new Function("jQuery","$item",
                "var $=jQuery,_=[],$data=$item.data;" +

                // Introduce the data as local variables using with(){}
                "with($data){_.push('" +

                // Convert the template into pure JavaScript
                $.trim(markup)
                        .replace( /([\\'])/g, "\\$1" )
                        .replace( /[\r\t\n]/g, " " )
                        .replace( /\${([^}]*)}/g, "{{= $1}}" )
                        .replace( /{{(\/?)(\w+|.)(?:\(((?:.(?!}}))*?)?\))?(?:\s+(.*?)?)?(\((.*?)\))?\s*}}/g,
                        function( all, slash, type, fnargs, target, parens, args ) {
                                var cmd = jQuery.tmpl.tags[ type ], def, expr;
                                if ( !cmd ) {
                                        throw "Template command not found: " + type;
                                }
                                def = cmd._default || [];
                                if ( target ) {
                                        target = unescape( target ); 
                                        args = args ? ("," + unescape( args ) + ")") : (parens ? ")" : "");
                                        if ( parens && target.indexOf(".") > -1 ) {
                                                // Support for target being things like a.toLowerCase(); 
                                                // In that case don't call with template item as 'this' pointer. Just evaluate...
                                                target += parens;
                                                args = "";
                                        }
                                        expr = args ? ("(" + target + ").call($item" + args) : target;
                                        exprAutoFnDetect = args ? expr: "(typeof(" +  target + ")==='function'?(" +  target + ").call($item):(" +  target + "))";
                                } else {
                                        expr = def["$1"] || "null";
                                }
                                fnargs = unescape( fnargs );
                                return "');" + 
                                        cmd[ slash ? "suffix" : "prefix" ]
                                                .split( "$notnull_1" ).join( "typeof(" + target + ")!=='undefined' && (" + target + ")!=null" )
                                                .split( "$1a" ).join( exprAutoFnDetect )
                                                .split( "$1" ).join( expr )
                                                .split( "$2" ).join( fnargs ?
                                                        fnargs.replace( /\s*([^\(]+)\s*(\((.*?)\))?/g, function( all, name, parens, params ) {
                                                                params = params ? ("," + params + ")") : (parens ? ")" : "");
                                                                return params ? ("(" + name + ").call($item" + params) : all;
                                                        })
                                                        : (def["$2"]||"")
                                                ) +
                                        "_.push('";
                        }) +
                "');}return _;"
        );
}

function unescape( args ) {
        return args ? args.replace( /\\'/g, "'").replace(/\\\\/g, "\\" ) : null;
}

function nest( tmpl, data, options ) {
        // nested template, using {{tmpl}} tag
        return jQuery.tmpl( 
                typeof tmpl === "string" ? jQuery.templates( tmpl ) : tmpl, 
                data, 
                options, 
                this 
        );
}

// Store template items in jQuery.data(), ensuring a unique tmplItem data data structure for each rendered template instance. 
function storeTmplItems( content ) {
        var keySuffix = "_" + cloneIndex, elem, elems, newClonedItems = {};
        for ( var i = 0, l = content.length; i < l; i++ ) {
                if ( (elem = content[i]).nodeType !== 1 ) {
                        continue;
                }
                elems = elem.getElementsByTagName("*");
                for ( var j = 0, m = elems.length; j < m; j++) {
                        processItemKey( elems[j] );
                }
                processItemKey( elem );
        }

        function processItemKey( el ) {
                var pntKey, pntNode = el, pntItem, pntNodeItem, tmplItem, key;
                // Ensure that each rendered template inserted into the DOM has its own template item,
                if ( key = el.getAttribute( tmplItmAtt )) {
                        while ((pntNode = pntNode.parentNode).nodeType === 1 && !(pntKey = pntNode.getAttribute( tmplItmAtt ))) { }
                        if ( pntKey !== key ) {
                                // The next ancestor with a _tmplitem expando is on a different key than this one.
                                // So this is a top-level element within this template item
                                tmplItem = newTmplItems[key];
                                if ( cloneIndex ) {
                                        cloneTmplItem( key );
                                }
                                pntNodeItem = el.parentNode;
                                pntNodeItem = pntNodeItem.nodeType === 11 ? 0 : (pntNodeItem.getAttribute( tmplItmAtt ) || 0);
                        }
                        el.removeAttribute( tmplItmAtt );
                } else if ( cloneIndex && (tmplItem = jQuery.data( el, "tmplItem" )) ) {
                        // This was a rendered element, cloned during append or appendTo etc.
                        // TmplItem stored in jQuery data has already been cloned in cloneCopyEvent. We must replace it with a fresh cloned tmplItem. 
                        cloneTmplItem( tmplItem.key );
                        newTmplItems[tmplItem.key] = tmplItem;
                        pntNodeItem = jQuery.data( el.parentNode, "tmplItem" );
                        pntNodeItem = pntNodeItem ? pntNodeItem.key : 0;
                }
                if ( tmplItem ) {
                        pntItem = tmplItem;
                        // Find the template item of the parent element
                        while ( pntItem && pntItem.key != pntNodeItem ) {
                                // Add this element as a top-level node for this rendered template item, as well as for any
                                // ancestor items between this item and the item of its parent element
                                pntItem.nodes.push( el );
                                pntItem = pntItem.parent;
                        }
                        delete tmplItem.content; // Could keep this available. Currently deleting to reduce API surface area, and memory use...
                        // Store template item as jQuery data on the element
                        jQuery.data( el, "tmplItem", tmplItem );
                }
                function cloneTmplItem( key ) {
                        key = key + keySuffix;
                        tmplItem = newClonedItems[key] 
                        = (newClonedItems[key] || newTmplItem( tmplItem, newTmplItems[tmplItem.parent.key + keySuffix] || tmplItem.parent, null, true ));
                }
        }
}

})(jQuery);