/*
 * Projekt Orange - web -
 * Orange Extension for jQuery
 * Bringing even more advanced coding laziness to the developer
 * Version 2.1.8 - beta
 * author:Donovan Walker
 *
 * You may use this code and modify it freely so long as you maintain original credit at the top.
 * thanks!
 *
 * *HTML Snippet Engine (Snippet + JSON = HTML) Put it in the DOM or get a string back. Ideal for server JSON callbacks
 * *Javascript Object Inspector (See ALL of a javascript object at runtime with a single call)
 * *Firebug "beta site" Log (Is your site in beta? Logs to the firebug console if it exists, doesn't crash if not. Optional JOI output)
 * * Form Data (Javascript Object->Form Fields, Form Fields->Javascript Object) for convenience, you know?
 *
 *
 * METHOD REFERENCE
 * $("#elementID").listen(inConfig)
 * $("#elementID").snippet(templateName, data) <- (name of template stored in jQuery, properly formatted data object)
 * $("#elementID").snippetString(templateString, data) <- (the actual template, properly formatted data object)
 * $("#elementID").snippetURL(inURL, data) <- NOT YET IMPLEMENTED (will grab a template from a url, then fill it in with data)
 *
 * FUNCTION REFERENCE
 *
 * $.setTemplateLib(inTemplateHash) <- (a js Object where each named attribute is a template)
 *										sets the templates for this library.  Good if you have a lot of templates and want to keep them handy
 * $.snippetString(templateString, data) <- (the actual template, properly formatted data object)
 *										returns the filled template
 * $.loadURLTemplates(inURLHash) <- (a js Object where each named attribute is the url to a corresponding template)
 *										so this function will retrieve templates from a server and store them in the orange template library.
 *										they can then be used by calling snippet with the name the url was associated with in the original js object
 *										js Object. like so:
 *															$.loadURLTemplates({"my_tpl":"http://supernoggies.com/js_templates/my_template.html",
 *																					"your_tpl":"/js_templates/u_template.html"})
 *															$("#target").snippet("my_tpl", data);
 * $.snippetURL(inURL, data) <- NOT YET IMPLEMENTED (will grab a template from a url, then fill it in with data and return the filled template
 *
 * $.objFromDom(inElementIDArray, inPrefix) <- Creates a Javascript Object from html form elements with the
 * $.fillForm(inObj, inPrefix)
 * $.inspect(inObj, inConfig)
 *
 */
//if(typeof jQuery == "object") {


//jQuery POSITION Methods
jQuery.fn.centerElement = function(inID) {
    var width = parseInt(this.css("width"));
    var height = parseInt(this.css("height"));
    var winHeight = jQuery(document).height();
    var winWidth = jQuery(document).width();
    this.css("top", Math.floor((winHeight - height) /2) + "px");
    this.css("left", Math.floor((winWidth - width) /2) + "px");
    return(this);
}


//jQuery KEYLISTENER Methods
jQuery.fn.listen = function(inConfig) {
    if(this.selector.indexOf("#") == 0 && typeof(inConfig.htmlID) == "undefined") {
        inConfig.htmlID = this.selector.substring(1);
    }
    var keyListener = new KeyListener(inConfig);

    switch(inConfig.keystroke) {
        case "keydown" :
            this.keydown( function(e) {
                keyListener.processKey(e);
            });
            break;
        case "keyup" :
            this.keyup( function(e) {
                keyListener.processKey(e);
            });
            break;
        case "keypress" :
        default :
            this.keypress( function(e) {
                keyListener.element = this;
                keyListener.processKey(e);
            });
    }
    return(this);
}

//jQuery VALIDATE Methods
/**
	 * tests the matched element against a regular expression in the library
	 * inRe = the name of the regular expression
	 *
	 *  returns mixed - false on fail, the value of the element on success
	 */
jQuery.fn.validate = function(inRe) {
    jQuery.makeOrangeVars();
    if(typeof(inRe) == "string") {
        if(jQuery.variables.orange.regex.hasOwnProperty(inRe)) {
            if(jQuery.variables.orange.regex[inRe].test(this.val())) {
                return(this.val());
            }
        }
        return(false);
    }
    return(false);
}


//jQuery VALIDATE Functions
/**
	 * Adds a regular expression to the library, or tests a string on a regular expression already assigned to the library
	 *
	 * Add a regular expression
	 * inReName = the name of the regular expression once added to the library
	 * inRegex 	= the regular expression to be added
	 *
	 * Test a string
	 * inReName = the name of the regular expression already in the library
	 * inRegex  = the string to test against the regular expression
	 *
	 * returns boolean
	 */
jQuery.validate = function(inReName, inRegex) {
    jQuery.makeOrangeVars();
    if(typeof(inRegex) == "string") {
        if(jQuery.variables.orange.regex.hasOwnProperty(inReName))
            return(jQuery.variables.orange.regex[inReName].test(inRegex));
    }
    if(typeof(inRegex) != "undefined" && inRegex.constructor == RegExp) {
        jQuery.variables.orange.regex[inReName] = inRegex;
        return(true);
    }
    return(false);
}


//jQuery TEMPLATE Methods
jQuery.fn.snippet = function(inTPName, inElementHash) {
    var snippet = jQuery.orangeLib.sl.fill(inTPName, inElementHash);
    this.each(function(i, item) {
        if(item.tagName.toLowerCase() == "input" || item.tagName.toLowerCase() == "textarea") item.value = snippet;
        else item.innerHTML = snippet;
    }
    );
    return(this);
}


jQuery.fn.snippetAfter = function(inTPName, inElementHash) {
    var snippet = jQuery.orangeLib.sl.fill(inTPName, inElementHash);
    this.after(snippet);
    return(this);
}


jQuery.fn.snippetAppend = function(inTPName, inElementHash) {
    var snippet = jQuery.orangeLib.sl.fill(inTPName, inElementHash);
    this.append(snippet);
    return(this);
};

jQuery.fn.snippetBefore = function(inTPName, inElementHash) {
    var snippet = jQuery.orangeLib.sl.fill(inTPName, inElementHash);
    this.before(snippet);
    return(this);
}

jQuery.fn.snippetPrepend = function(inTPName, inElementHash) {
    var snippet = jQuery.orangeLib.sl.fill(inTPName, inElementHash);
    this.prepend(snippet);
    return(this);
};

jQuery.fn.snippetString = function(inTemplateString, inElementHash) {
    this.html(jQuery.orangeLib.sl.fillString(inTemplateString, inElementHash));
    return(this);
};

//END jQuery TEMPLATE methods


//jQuery TEMPLATE Functions
jQuery.snippet = function(inTPName, inElementHash) {
    return(jQuery.orangeLib.sl.fill(inTPName, inElementHash));
};

jQuery.snippetString = function(inTemplateString, inElementHash) {
    return(jQuery.orangeLib.sl.fillString(inTemplateString, inElementHash));
};


jQuery.makeOrangeVars = function() {
    if(!jQuery.hasOwnProperty("variables")) {
        jQuery.variables = new Object();
    }
    if(!jQuery.variables.hasOwnProperty("orange")) {
        jQuery.variables.orange = new Object();
    }
    
    if (!jQuery.variables.orange.hasOwnProperty("regex")) {
        jQuery.variables.orange.regex = {};
        jQuery.variables.orange.regex.email = /^([A-Za-z0-9_\+\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;
    }
}

jQuery.hasSnippet = function(inName) {
    return(jQuery.orangeLib.sl.has(inName));
}

jQuery.setSnippetLib = function(inSnippets) {
    jQuery.orangeLib.sl.add(inSnippets)
};


jQuery.addSnippet = function(inName, inStr) {
    jQuery.orangeLib.sl.add(inName, inStr)
};


jQuery.getSnippets = function(inTemplateURLs) {
    for(i in inTemplateURLs) {
        if(inTemplateURLs.hasOwnProperty(i)) {
            eval("jQuery.ajax({type:'GET', url:inTemplateURLs[i], success: function(template) {	jQuery.orangeLib.sl.add(\"" + i + "\", template);}, error: function(XMLHttpRequest, textStatus, errorThrown){ alert('error' + textStatus + ' ' + errorThrown);}});");
        }
    }
};


//END jQuery TEMPLATE Functions


jQuery.attrList = function(inObj) {
    var retList = new Array();
    for(i in inObj) {
        if(inObj.hasOwnProperty(i)) {
            retList.push(i);
        }
    }
    return(retList);
}


//jQuery FORM Functions
jQuery.fillForm = function(inObj, inPrefix) {
    var prefix = "";
    if(typeof(inPrefix) != "undefined") {
        prefix = inPrefix
    };
    for(i in inObj) {
        if(inObj.hasOwnProperty(i)) {
            jQuery("#" +prefix + i).val(inObj[i]);
        }
    }
    delete(prefix);
}

jQuery.objFromDom = function(inFormIDList, inDOMPrefix) {
    if(!inDOMPrefix) inDOMPrefix = "";
    var newObject = {};
    for(i in inFormIDList) {
        if(inFormIDList.hasOwnProperty(i)) {
            newObject[inFormIDList[i]] = jQuery("#" + inDOMPrefix + inFormIDList[i]).val();
        }
    }
    return(newObject);
}


jQuery.ofd = function(inFormIDList, inDOMPrefix) {
    return(jQuery.objFromDom(inFormIDList, inDOMPrefix));
}

//jQuery UTIL functions
jQuery.log = function(e, titleInspectConfig, inspectConfig) {
    var config = {};
    if(typeof(console) != "undefined") {
        if(typeof(console.log) == "function") {
            var logTitle = "\n";
            var inspect = false;
            var config = {};
            switch(typeof inspectConfig) {
                case "object" :
                    var inspect = true;
                    config = inspectConfig;
                    break;
                case "boolean" :
                    inspect = true;
                    break;

            }
            switch(typeof(titleInspectConfig)) {
                case "string" :
                    logTitle = titleInspectConfig + "\n";
                    break;
                case "boolean" :
                    inspect = titleInspectConfig;
                    break;
                case "object" :
                    inspect = true;
                    config = titleInspectConfig;
            }
            if(config.hasOwnProperty("title")) logTitle = config.title;
            if(inspect) console.log(logTitle + jQuery.inspect(e, config));
            else console.log(logTitle + e );

            return(true);
        }
    }
    return(false);
}

jQuery.urlParam = function(param) {
    var regex = '[?&]' + param + '=([^&#]*)';
    var results = (new RegExp(regex)).exec(window.location.href);
    if(results) return results[1];
    return false;
}

//WARNING: RECURSIVE OBJECTS WILL RECURSE INFINITELY!!!!
jQuery.clone = function(inObj, root) {
    jQuery.makeOrangeVars();
    if(typeof(root) == "undefined") {
        //var cleanCloneList = true;
        jQuery.variables.orange.cloneList = [];
    }
    if(typeof (inObj) == "undefined") {
        return(inObj);
    }
    if(inObj.constructor == Array) {
        var retObj = [];
        for (var i = 0; i < inObj.length; i++) {
            if (typeof inObj[i] == 'object') {
                retObj[i] = new jQuery.clone(inObj[i], true);
            }
            else {
                retObj[i] = inObj[i];
            }
        }
    }
    else {
        var retObj = {};
        for (var i in inObj) {
            if(inObj.hasOwnProperty(i)) {
                if (typeof inObj[i] == 'object') {
                    retObj[i] = new jQuery.clone(inObj[i], true);
                }
                else {
                    retObj[i] = inObj[i];
                }
            }
        }
    }
    return(retObj);
}

/**
	* inConfig - optional - options:	string	pathToThis  root label for the object returned. I'd suggest the variable name of the object you're passing in
	*					int	maxRecurse (circular objects could recurse infinitely) level 0 will give you the  object's value, but no attributes. Defaults to 5.
	*					bool	allProps (defaults to has own property),
	*					bool	toJSON (converts the object to JSON string - experimental! -)
	*
	*/
jQuery.inspect = function(inObject, inConfig) {
    var outString = "";
    var indent = "";

    if(typeof(inConfig) == "undefined") var inConfig = {};

    if(!inConfig.hasOwnProperty("recurse"))  inConfig.recurse = 0;
    if(!inConfig.hasOwnProperty("maxRecurse")) inConfig.maxRecurse = 5;
    if(!inConfig.hasOwnProperty("allProps")) inConfig.allProps = false;
    if(!inConfig.hasOwnProperty("toJSON")) inConfig.toJSON = false;
    if(!inConfig.hasOwnProperty("inspectedHash")) inConfig.inspectedHash = {};
    if(!inConfig.hasOwnProperty("indent")) inConfig.indent = "";
    if(!inConfig.hasOwnProperty("indentChars")) inConfig.indentChars = false;
    if(!inConfig.hasOwnProperty("pathToThis")) inConfig.pathToThis = "";

    var inObjType = typeof(inObject);
    if(!inConfig.toJSON) {
        switch(inObjType) {
            case "string":
                return inConfig.pathToThis + " = \"" + inObject + "\"\n";
                break;
            case "boolean":
                return inConfig.pathToThis + " = bool(" + inObject + ")\n";
                break;
            case "number":
                return inConfig.pathToThis + " = " + inObject + "\n";
                break;
            case "undefined":
                return inConfig.pathToThis + " = undefined\n";
                break;
            case "object":
                if(inObject == null) return inConfig.pathToThis + " = null\n";

                for(var m in inConfig.inspectedHash) if(inConfig.inspectedHash.hasOwnProperty(m)) {
                    if(inObject == inConfig.inspectedHash[m]) {
                        if(inConfig.indentChars) {
                            return inConfig.pathToThis + " == - already inspected -\n";
                        }
                        return inConfig.pathToThis + " == " + m +"\n";
                    }
                }
                inConfig.inspectedHash[inConfig.pathToThis] = inObject;

                var pathToThis = inConfig.pathToThis;

                if(inObject.constructor == Array) outString += pathToThis + " = []\n";
                else outString += pathToThis + " = {}\n";
                if(inConfig.recurse >= inConfig.maxRecurse) return outString += pathToThis + " !! at max recurse !!\n"
                for(var key in inObject) {
                    try {
                        if(inObject.hasOwnProperty(key) || inConfig.allProps) {
                            if(inObject.constructor == Array) inConfig.pathToThis = pathToThis + "[" + key + "]";
                            else inConfig.pathToThis = pathToThis + "." + key;
                            inConfig.recurse++;
                            outString += "" + jQuery.inspect(inObject[key], inConfig);
                            inConfig.recurse--;
                        }
                    } catch (e) {
                        return (outString + " ERROR inspecting!: " + e.message);
                    }
                }

                break;
            default :
                outString += inConfig.pathToThis + " = " + inObject.toString() + "\n";
        }
    } else { //if inConfig.toJSON
        switch(inObjType) {
            case "string":
                var regEx = /("|\\)/
                var transStr = "";
                while(inObject.indexOf("\\") > -1) {
                    transStr += inObject.substring(0, inObject.indexOf("\"")) + "\\\\";
                    inObject = inObject.substring(inObject.indexOf("\\") + 1);
                }
                transStr += inObject;
                inObject = transStr;
                transStr = "";
                while(inObject.indexOf("\"") > -1) {
                    transStr += inObject.substring(0, inObject.indexOf("\"")) + "\\\"";
                    inObject = inObject.substring(inObject.indexOf("\"") + 1);
                }
                transStr += inObject;
                outString += "\"" + transStr + "\"";
                break;
            case "boolean":
                outString += inObject;
                break;
            case "number":
                outString += inObject;
                break;
            case "undefined":
                outString += "undefined";
                break;
            case "object":
                if(inObject.constructor == Array) outString += "[";
                else outString +="{";
                for(var key in inObject) {
                    if(key != "parent") {
                        try {
                            if(inObject.hasOwnProperty(key) || inConfig.allProps) {
                                var propType = typeof(inObject[key]);
                                if(inObject.constructor != Array)
                                    outString += key + ":";
                                if(propType == "object") {
                                    if(inConfig.recurse < inConfig.maxRecurse) {
                                        inConfig.recurse++;
                                        outString += jQuery.inspect(inObject[key], inConfig);
                                        inConfig.recurse--;
                                    } else {
                                        if(inObject[key] == null) outString += "null";
                                        else if(inObject[key].constructor == "Array") outString += "[]";
                                        else outString += "{}";
                                    }
                                } else {
                                    outString += jQuery.inspect(inObject[key], inConfig);
                                }
                                outString += ",";
                            }
                        } catch (e) {
                            return (outString + " ERROR inspecting!: " + e.message);
                        }
                    }
                }
                if(inObject.constructor == Array) outString += "]";
                else outString += "}";
                break;
            default :
                outString += inObject.toString();
        }
    }

    return(outString);
}

/****
	* Serializes a javscript object with named attributes. Attribute names will be utilized in the url.  NOTE strings and numbers supported only!
	* @param object inObject			- required -
	* @param string inAddPrefix 		- optional - prefix the names in the returned url with this
	* @param object inFilter 			- optional - filter on this object, only the attribute names are important. values are ignored
	* @param boolean inFilterPositive 	- optional - is the filter positive (allowing only those attributes that exist in the filter to be included)
	*													or negative (any attribute that is in the filter will be excluded)
	*/
jQuery.serializeObj = function(inObject, inAddPrefix, inFilter, inFilterPositive) {
    var urlString = "";
    var prefix = "";
    if(typeof(inAddPrefix) != "undefined") {
        prefix = inAddPrefix;
    }
    if(typeof(inFilter) == "undefined") {
        for(i in inObject) {
            if(inObject.hasOwnProperty(i))
                urlString += "&" + prefix + i + "=" + inObject[i];
        }
    } else if (typeof(inFilterPositive) == "boolean" && !inFilterPositive) {
        for(i in inObject) {
            if(inObject.hasOwnProperty(i))
                if(!inFilter.hasOwnProperty(i))
                    urlString += "&" + prefix + i + "=" + inObject[i];
        }
    } else {
        for(i in inObject) {
            if(inObject.hasOwnProperty(i))
                if(inFilter.hasOwnProperty(i))
                    urlString += "&" + prefix + i + "=" + inObject[i];
        }
    }
    return(urlString);
}


/****
	* Serialize a javascript object into a url list representation
	* @param object inList			- required - the object/list to be serialized
	* @param string inListName 		- required - the name to be used for the serialized list
	*/
jQuery.serializeList = function(inList, inListName) {
    var urlString = "";
    for(i in inList) {
        if(inList.hasOwnProperty(i))
            urlString += "&" + inListName + "[]=" + inList.i;
    }
    return(urlString);
}
//}

/**
 * The snippet library manager.
 * You must use the manager if you wish to use the include function for the snippets.
 */
function SnippetLib(inSnippets){
    this.snippets       = {};
    this.isSnippetLib   = true;
    if(typeof inSnippets == "object") this.add(inSnippets);
}

SnippetLib.prototype.has = function(inName) {
    if(this.snippets.hasOwnProperty(inName))
        return (this.snippets[inName]);
    return(false);
}


SnippetLib.prototype.add = function(inNameOrObj, inStr) {
    if(typeof inNameOrObj == "object") {
        for(var i in inNameOrObj) if(inNameOrObj.hasOwnProperty(i)) {
            this.snippets[i] = new Snippet(inNameOrObj[i], this);
        }
        }
    else if (typeof inStr == "string") {
        this.snippets[inNameOrObj] = new Snippet(inStr, this);
    } else {

}
};

SnippetLib.prototype.fill = function(inName, inObj, inConfig) {
    if(!this.snippets.hasOwnProperty(inName)) {
        var msg = "template:'" + inName + "' not set";
        if(!jQuery.log(msg)) alert(msg);
        return(false);
    }
    if(typeof inConfig == "object") {
        if(inConfig.hasOwnProperty("parent")) { //supporting 'include' function
            this.snippets[inName].parent = inConfig.parent;
            var html = this.snippets[inName].fill(inObj);
            this.snippets[inName].parent = false;
            return(html);

        }

    }
    return(this.snippets[inName].fill(inObj));
};

SnippetLib.prototype.fillString = function(inTPLString, inObj) {
    var snippet = new Snippet(inTPLString, this);
    var retString = snippet.fill(inObj);
    delete(snippet);
    return(retString);
};



///the snippet class!  required for templating functionality
function Snippet(inString, inLib, inParent) {
    //defaults
    this.isSnippet      = true;
    this.config         = {htmlentities:false, maxlen:false, maxend:"", maxnohtml:false, maxchop:0};
    this.cycleInc       = 0;
    this.cycleName      = false;
    this.cycleValues    = [];
    this.elements       = [];
    this.defaultVal     = "";
    this.includeSnippet = false;
    this.inputString    = inString;
    this.key            = null;
    this.parent         = false;
    this.sLib           = false;
    this.tag            = null;
    this.type           = "";

    //this.snippets 	= new Array();
    if(typeof(inParent) == "object" && inParent.isSnippet)
        this.parent = inParent;
    if(typeof(inLib) == "object" && inLib.isSnippetLib)
        this.sLib = inLib;

    //find my tag (always at the beginning of instring)
    var match = this.tagOpen.exec(inString);
    if(!this.parent) { //if I have no parent, I'm the root snippet and don't need to find my tag (because it's implied)
        
        this.tag = "root{}";
        this.type = this.tagType(this.tag);
    } else {

        this.tag = match[0].substring(1, match[0].length - 1); //get the clean tag with no whitespace or opening {
        this.type = this.tagType(this.tag);
        inString = inString.substring(match[0].length-1)
        switch(this.type) {
            case "if" : //we do nothing here because we need the if's 'config' expression for the child.. or do we?
                break;
            case "function" :
                this.parseConfig(inString.substring(1, inString.indexOf("}}")));
                inString = inString.substring(inString.indexOf("}}") + 2);
                break;
            default :
                this.parseConfig(inString.substring(1, inString.indexOf("}")));
                inString = inString.substring(inString.indexOf("}") + 1);
        }
    }

    //get the data element key  (to fill the template)

    //if this is an object, array, or root snippet (an object) look through the 'inString' for child-snippets
    if(this.type == "if") { //if elements and thier else/elseif children have a unique construction
        this.key = "";
        this.constructIf(inString);
    } else if(this.type == "elseif") {
    //since the elseif is built by the parent 'if' we don't need to do anything here.
    } else if(this.type == "object" || this.type == "array" || this.type == "function") {
        this.key =  this.tag.substring(0, this.tag.length - 2); //trim off the [] or {}
        var tag = null;
        var tagSuffix = null;
        var tagType = null;
        var matchString = null;
        var working = inString;
        var key = null;
        var closeTag = null;
        var z = 0;

        while(true) {
            match = this.tagOpen.exec(working);
            if(match == null) break; //this will break us out of the loop when there are no more matches
            this.elements.push(working.substring(0, match.index)); //adding any static string before the match to the list of child elements
            working = working.substring(match.index);

            matchString = match[0];
            tag = matchString.substring(1, matchString.length -1);
            //if the child is an object or array snippet, we need to find the end tag and give it all the characters in-between
            tagType = this.tagType(tag);
            switch(tagType) {
                case "object" :
                case "array" :
                case "function" :
                case "if" :
                    if(tag.indexOf("#") == 0) {
                        key = tag.substring(1);
                        closeTag = "{/" + key + "}";
                    } else {
                        tagSuffix = tag.substring(tag.length - 2);
                        key 	= tag.substring(0, tag.length - 2);

                        closeTag = "{" + tagSuffix + key + "}";
                    }

                    //var newMatchIndex = working.indexOf(matchString, match.index + matchString.length);
                    var newMatchIndex = working.indexOf(matchString, matchString.length);
                    var matchCloseIndex = working.indexOf(closeTag);
                    if(matchCloseIndex == -1) alert("error, no close for " + this.tagType(tag) + " " + tag);
                    //because there may be nested tags of the same type/name
                    var i = 0; //for debugging the templates
                    while(newMatchIndex < matchCloseIndex && newMatchIndex > -1) {
                        newMatchIndex = working.indexOf(matchString, newMatchIndex + matchString.length);
                        matchCloseIndex = working.indexOf(closeTag, matchCloseIndex + closeTag.length);
                        if(matchCloseIndex == -1 && newMatchIndex == -1) alert("error, no close for " + this.tagType(tag) + " " + tag + " iteration:" + i);
                        i++;
                    }
                    //we've found the closing tag for our new object or array Snippet now create it
                    var snippet = new Snippet(working.substring(0, matchCloseIndex), this.sLib, this);
                    //snippet.pre = working.substring(0, match.index);
                    this.elements.push(snippet);
                    working = working.substring(matchCloseIndex + closeTag.length);
                    break;
                default : //if tag is a value
                    matchCloseIndex = working.indexOf("}");
                    var snippetString = working.substring(0, matchCloseIndex + 1);
                    if(tag != "#if") 
                        var snippet = new Snippet(snippetString, this.sLib, this);

                    //snippet.pre = working.substring(0, match.index);
                    this.elements.push(snippet);
                    working = working.substring(matchCloseIndex + 1);
            }
            z++;
        }
        this.elements.push(working);
    } else {
        this.key = this.tag;
    }
    return(this);
}

Snippet.prototype.reg = {
    htmltag:/<(?:.|\s)*?>/
}

Snippet.prototype.constructIf = function(inString) {
    var element = new Snippet("{#elseif" + inString.substring(0, inString.indexOf("}") +1), this.sLib, this.parent);
    var tag = null;
    var tagSuffix = null;
    var tagType = null;
    var match   = null;
    var matchString = null;
    var working = inString.substring(inString.indexOf("}") + 1);
    var key = null;
    var closeTag = null;
    var z = 0;
    while(true) { //we loop over the root if's working string.
        match = this.tagOpen.exec(working);
        if(match == null) break; //this will break us out of the loop when there are no more matches
        element.elements.push(working.substring(0, match.index)); //adding any static string before the match to the list of child elements
        working = working.substring(match.index);

        matchString = match[0];
        if(matchString == "{#else")
            tag = matchString.substring(1);
        else
            tag = matchString.substring(1, matchString.length -1);
        //if the child is an object or array snippet, we need to find the end tag and give it all the characters in-between
        tagType = this.tagType(tag);
        switch(tagType) {
            case "elseif" :
                this.elements.push(element);
                element = new Snippet(working.substring(0, working.indexOf("}") + 1), this.sLib, this.parent);
                working = working.substring(working.indexOf("}") + 1);
                break;
            case "else" :
                this.elements.push(element);
                element = new Snippet("{#elseif true}", this.sLib, this.parent);
                working = working.substring(working.indexOf("}") + 1);
                break;
            case "object" :
            case "array" :
            case "function" :
            case "if" :
                if(tag.indexOf("#") == 0) {
                    key = tag.substring(1);
                    closeTag = "{/" + key + "}";
                } else {
                    tagSuffix = tag.substring(tag.length - 2);
                    key 	= tag.substring(0, tag.length - 2);

                    closeTag = "{" + tagSuffix + key + "}";
                }

                //var newMatchIndex = working.indexOf(matchString, match.index + matchString.length);
                var newMatchIndex = working.indexOf(matchString, matchString.length);
                var matchCloseIndex = working.indexOf(closeTag);
                if(matchCloseIndex == -1) alert("error, no close for " + this.tagType(tag) + " " + tag);
                //because there may be nested tags of the same type/name
                var i = 0; //for debugging the templates
                while(newMatchIndex < matchCloseIndex && newMatchIndex > -1) {
                    newMatchIndex = working.indexOf(matchString, newMatchIndex + matchString.length);
                    matchCloseIndex = working.indexOf(closeTag, matchCloseIndex + closeTag.length);
                    if(matchCloseIndex == -1 && newMatchIndex == -1) alert("error, no close for " + this.tagType(tag) + " " + tag + " iteration:" + i);
                    i++;
                }
                //we've found the closing tag for our new object or array Snippet now create it
                var snippet = new Snippet(working.substring(0, matchCloseIndex), this.sLib, this);
                //snippet.pre = working.substring(0, match.index);
                element.elements.push(snippet);
                working = working.substring(matchCloseIndex + closeTag.length);
                break;
            default : //if tag is a value
                matchCloseIndex = working.indexOf("}");
                var snippetString = working.substring(0, matchCloseIndex + 1);
                if(tag != "#if")
                    var snippet = new Snippet(snippetString, this.sLib, this);

                //snippet.pre = working.substring(0, match.index);
                element.elements.push(snippet);
                working = working.substring(matchCloseIndex + 1);
        }
        z++;
    }
    element.elements.push(working);
    this.elements.push(element);
}

//Snippet.prototype.tagOpen = new RegExp("\{([a-z]|[A-Z]|_)+(\.([a-z]|[A-Z])+)*((\{\})|(\\[\\]|\\(\\)))*( |\})");
Snippet.prototype.tagOpen = /\{(#template |#if |#elseif |#else|#include |([a-z]|[A-Z]|_)+(([0-9]|[a-z]|[A-Z])+)*((\{\})|(\[\]|\(\)))*( |\}))/;
Snippet.prototype.tagOpenCloseBrace = /(^|[^\\])}/;  //not yet used



/**
	* assumes tag has tag delimiters and any attributes removed
	*/
Snippet.prototype.tagType = function(inTag) {
    if(inTag.substring((inTag.length - 2)) == "[]") {
        return("array");
    }
    else if(inTag.substring((inTag.length - 2)) == "{}") {
        return("object");
    }
    else if(inTag.substring((inTag.length - 2)) == "()") {
        return("function");
    } else if(inTag == "#template") {
        return("template");
    } else if(inTag == "#if") {
        return("if");
    } else if(inTag == "#elseif") {
        return("elseif");
    } else if(inTag == "#else") {
        return("else");
    } else if(inTag == "#include") {
        return("include");
    } else if(inTag == "#func") {
        return("func");
    }
    return("value");
}


Snippet.prototype.fill = function(obj) {
    var out = "";
    //var snippet = null;
    //var count = 1;
    //var item = null;
    var myVal = "";
    var objType = typeof obj;
    this.obj = obj;
    if(objType == "undefined" || obj == null) {
        obj = this.getDefaultValue();
        objType = typeof obj;
    }
    if(objType == "undefined") {
        $.log(this, this.tag, true);
    }
    switch (this.type) {
        case "value" :
            obj = obj.toString();
            if(this.config.maxlen && (obj.length > this.config.maxlen)) {
                obj = obj.substring(0, this.config.maxlen - this.config.maxchop);
                /*we perform the htmlentities check and append 'maxend' AFTER because we don't want the maxend string to be transformed (there may be html in it)*/
                 obj = (this.config.htmlentities)? this.htmlentities(obj) + this.config.maxend:obj + this.config.maxend;
            } else if(this.config.htmlentities) {
                obj = this.htmlentities(obj);
            }
            return obj;
        case "include" :
            if(!this.sLib) {
                alert("cannot use include when not using SnippetLib"); return("");
            }
            return this.sLib.fill(this.includeSnippet, obj, {
                parent:this
            });
        case "function" :
            myVal = this.myFunction(obj);
            if(typeof(myVal) == "undefined" || !myVal) return "";
            if(typeof(myVal) == "string" || typeof(myVal) == "number") return(myVal);
            return this.fillSnippets(obj);
        case "if" :
            for(var i = 0; i < this.elements.length; i++) {
                myVal = this.elements[i].fill(obj);
                if(typeof myVal == "string") {
                    out += myVal;
                    i = this.elements.length;
                }
            }
            break;
        case "elseif" :
            if(!this.myFunction(obj)) return false;
            return this.fillSnippets(obj)
        case "object" :
            if(this.tag == "root{}") {
                //return this.fillSnippets(obj);
                if(objType == "number" || objType == "string") 
                    return this.fillSnippets({
                        "val":obj
                    });
                else if(objType == "object" && !(obj instanceof Array))
                    return this.fillSnippets(obj);
            } else {
                return this.fillSnippets(obj);
            }
        case "array" :
            if(typeof(obj.length) == "undefined") {
                if(typeof(obj) == "object") {
                    this.cycleInc = 0;
                    for(var j in obj) if(obj.hasOwnProperty(j)) {
                        this.arrayInc = j;
                        if(this.cycleInc >= this.cycleValues.length) this.cycleInc = 0;
                        if(typeof(obj[j]) == "string" || typeof(obj[j]) == "boolean" || typeof(obj[j]) == "number") {
                            out += this.fillSnippets({
                                "val":obj[j]
                            });
                        } else {
                            out += this.fillSnippets(obj[j]);
                        }
                        this.cycleInc++;
                    }
                } else {
                    return(out + this.inner);
                }
            } else {
                this.cycleInc = 0;
                for(var j = 0; j < obj.length; j++) {
                    if(!this.config.maxlen || this.config.maxlen < j) {
                        this.arrayInc = j;
                        if(this.cycleInc >= this.cycleValues.length) this.cycleInc = 0;
                        if(typeof(obj[j]) == "string" || typeof(obj[j]) == "boolean" || typeof(obj[j]) == "number") {
                            out += this.fillSnippets({
                                "val":obj[j]
                            });
                        } else {
                            out += this.fillSnippets(obj[j]);
                        }
                        this.cycleInc++;
                    } else {
                        out += this.config.maxend;
                        j = obj.length;
                    }
                }
            }
    }
    //}
    this.obj = null;
    delete(this.obj);
    return(out);
}


Snippet.prototype.fillSnippets = function(obj) {
    var out = "";
    var snippet = null;
    for(var i = 0; i < this.elements.length; i++) {
        snippet = this.elements[i];
        if(typeof(snippet) == "string") { //static bit of html
            out += snippet;
        } else { //is a real snippet
            if(this.cycleName && this.cycleName == snippet.tag) {
                out += snippet.fill(this.cycleValues[this.cycleInc]);
            } else {
                switch(snippet.type) {
                    case "function" :
                    case "if" :
                    case "include" :
                        out += snippet.fill(obj); //we do this because ifs & functions operate in the parent namespace
                        break;
                    default :
                        out += snippet.fill(obj[snippet.key]);
                }
            }
        }
    }
    return(out);
}


Snippet.prototype.getDefaultValue = function() {
    if(this.defaultVal.length == 0 && this.parent) {
        var val = this.parent.getObjValue(this.key);
        if(typeof(val) != "undefined") {
            return(val)
        }
    } 
    return(this.defaultVal);
}


Snippet.prototype.getObjValue = function(inKey) {
    if(typeof(this.obj) != "undefined") {
        if(this.obj.hasOwnProperty(inKey)) {
            return(this.obj[inKey]);
        } /*else if(this.type == "array") {
            $.log(this.tag + " is an array at index " + this.arrayInc);
            $.log(this.obj, true);
            $.log(this.obj[this.arrayInc], true);
                if(this.obj[this.arrayInc].hasOwnProperty(inKey)) {
                    return(this.obj[this.arrayInc][inKey]);
                } else if(this.cycleName == inKey) {
                    return(this.cycleValues[this.cycleInc]);
                }
        }*/ else
            return this.parentValue(inKey);
    }
    return("");
}

/* fancy. We'll use it only if  we need it.
Snippet.prototype.htmlentities = function (inHTML){
    //by Micox - elmicoxcodes.blogspot.com - www.ievolutionweb.com
    var i,charCode,html='';
    for(i=0;i < inHTML.length;i++){
        charCode = inHTML[i].charCodeAt(0);
        if( (charCode > 47 && charCode < 58) || (charCode > 62 && charCode < 127) ){
            html += inHTML[i];
        }else{
            html += "&#" + charCode + ";";
        }
    }
    return html;
} */
Snippet.prototype.htmlentities = function (inHTML) {
    return inHTML.
    replace(/&/gmi, '&amp;').
    replace(/"/gmi, '&quot;').
    replace(/>/gmi, '&gt;').
    replace(/</gmi, '&lt;')
}



Snippet.prototype.parseConfig = function(inString) {
    switch(this.type) {
        case "function" :
            inString = inString.substring(1);
            eval("this.myFunction = function(obj) {" + inString + "}");
            break;
        case "elseif" :
            eval("this.myFunction = function(obj) { return(" + inString + ");}");
            break;
        case "include" :
            inString = inString.substring(inString.indexOf("\"") + 1);
            this.includeSnippet = inString.substring(0, inString.indexOf("\""));
            break;
        default : /* value,list*/
            var temp = "";
            var index = inString.indexOf("default=\"");
            if(index > -1) {
                temp = inString.substring(index + 9);
                this.defaultVal = temp.substring(0, temp.indexOf("\""));
                inString = inString.replace("default=\"" + this.defaultVal + "\"", "");
            }
            index = inString.indexOf("maxend=\"");
            if(index > -1) {
                temp = inString.substring(index + 8);
                this.config.maxend = temp.substring(0, temp.indexOf("\""));
                inString = inString.replace("maxend=\"" + this.defaultVal + "\"", "");
            }
            index = inString.indexOf("cycleName=");
            if(index > -1) {
                temp = inString.substring(index + 10);
                this.cycleName = temp.substring(0, temp.indexOf(" "));
                temp = temp.substring(this.cycleName.length + 1);
                temp = temp.substring(0, (temp.indexOf(" ") > 0)? temp.indexOf(" "): temp.length);
                this.cycleValues = temp.split("|");
                inString = inString.replace("cycleName=" + this.cycleName + " " + temp + "", "");
            }
            index = inString.indexOf("maxlen=");
            if(index > -1) {
                temp = inString.substring(index + 7);
                temp = temp.substring(0, (temp.indexOf(" ") > 0)? temp.indexOf(" "): temp.length);
                this.config.maxlen = parseInt(temp);
                inString = inString.replace("maxlen=" + temp, "");
            }
            index = inString.indexOf("maxchop=");
            if(index > -1) {
                temp = inString.substring(index + 8);
                temp = temp.substring(0, (temp.indexOf(" ") > 0)? temp.indexOf(" "): temp.length);
                this.config.maxchop = parseInt(temp);
                inString = inString.replace("maxchop=" + temp, "");
            }
            index = inString.indexOf("htmlentities");
            if(index > -1) {
                temp = inString.substring(index, 12);
                this.config.htmlentities = true;
                inString = inString.replace("htmlentities", "");
            }
            index = inString.indexOf("maxnohtml");
            if(index > -1) {
                temp = inString.substring(index, 12);
                this.config.maxnohtml = true;
                inString = inString.replace("maxnohtml", "");
            }



    }
}




Snippet.prototype.parentValue = function(inKey, inValue) {
    if(this.parent) {
        if(typeof inValue == "undefined")
            return(this.parent.getObjValue(inKey));
        else {
            alert("snippet-setting parent value not implemented!");
            this.parent.setObjValue(inKey, inValue);
        }
    }
    return("");
}


/**
	*	Key event evaluation object (perform javascript on a keystroke event)
	*
	*	NOTE: binding the processKey event using jQuery or in areas where scope/context(namespace) may be ambiguous binding needs to be wrapped in a function
	*	example: $("#groups_display_label").keyup( function(e) {keyWatcher.processKey(e); } );
	*
	* @param inConfig	-required-		//all are optional
	* 	- keycodes -
	*	.27,
	*	.13, etc = (function) || (string)
	*           //the integer number  of a Character keyCode.  Add as many as you want
	*           Each of these can be either a string to be evaluated - eval(string), or a function to be called.
	*           functions will be passed 2 arguments:
	*           1. The event that triggered the call
	*           2. The listener object (where config options including htmlID will be available off of .config[optName]
	*		duplicating this for an eval string would be the string 'somefunc(e, this)'
	*
	*	.defaultAction = (function)||(string)
	*           //the action that is performed when any valid key is pressed (see keyCodes above)
	*           (see .regEx for a definition of a 'valid key')
	*
	* 	.regEx = (/regEx/)
	*           a regular expression that will 'test' the whole input string on each keystroke and will process the default action
        *           *to match on the whole string remember to use ^ and $ at the beginning and end of the string
	*           if this argument is not supplied KeyListener.isKeyDefaultValid() will be used to determine if the key is to be processed
	*
	*	.invalidAction = (function)||(string)
	*           //the action that is performed when any INVALID key is pressed (see keyCodes above)
	*           if absent the listener will attempte to swallow the keystroke.
	*
	*	.defaultActionDelay = (integer)
	*           the amount of time to delay the default action. if another key code is processed
	*           before the time expires, the time is reset
	*
	*	.actionDelay = (integer)
        *           the amount of time to delay all actions but the default action. if another key code is processed before the time expires, the time is reset
	*
	* 	.htmlID (string)
	*           The unique DOM id of the INPUT element to be matched.
	*
	* Additional information
	* 	If you want 'no action' performed on some keystrokes when a default action is specified, simply assign and
	*	empty string to that keycode, the same for the .invalidAction argument
        *       inConfig = {'defaultAction':'myfunction()', 27:"$('#somefield').val('')", 9:''}
	*
	*/
function KeyListener(inConfig){
    this.delayedAction = false;
    this.config = inConfig;
    if(!this.config.hasOwnProperty("keyCode") || !this.config.keyCode) {
        this.config.keyCode = false;
    } else {
        if(!this.config.keyCode.hasOwnProperty("onMatch"))                  this.config.keyCode.onMatch = false;
        if(!this.config.keyCode.hasOwnProperty("onMatchDelay"))             this.config.keyCode.onMatchDelay = false;
        if(!this.config.keyCode.hasOwnProperty("onMatchPreventDefault"))    this.config.keyCode.onMatchPreventDefault = false;
        if(!this.config.keyCode.hasOwnProperty("onFailed"))                 this.config.keyCode.onFailed = false;
        if(!this.config.keyCode.hasOwnProperty("onFailedPreventDefault"))   this.config.keyCode.onFailedPreventDefault = false;
    }
    if(!this.config.hasOwnProperty("chars") || !this.config.chars) {
        this.config.chars = false;
    } else {
        if(!this.config.chars.hasOwnProperty("onMatch"))                this.config.chars.onMatch = false;
        if(!this.config.chars.hasOwnProperty("onMatchDelay"))           this.config.chars.onMatchDelay = false;
        if(!this.config.chars.hasOwnProperty("onMatchPreventDefault"))  this.config.chars.onMatchPreventDefault = false;
        if(!this.config.chars.hasOwnProperty("onFailed"))               this.config.chars.onFailed = false;
        if(!this.config.chars.hasOwnProperty("onFailedPreventDefault")) this.config.chars.onFailedPreventDefault = false;
    }
    if(!this.config.hasOwnProperty("regEx") || !this.config.regEx) {
        this.config.regEx = false;
    } else {
        if(!this.config.regEx.hasOwnProperty("expr")) {
            alert("listener:ERROR: config.regEx.expr is missing. Regular expression actions disabled;");
            this.config.regEx = false;
        }
        if(!this.config.hasOwnProperty("htmlID")) alert("listener:WARNING: config.regEx requires config.htmlID be set to the DOM id of the listened element")
        if(!this.config.regEx.hasOwnProperty("onMatch"))                this.config.keyCode.onMatch = false;
        if(!this.config.regEx.hasOwnProperty("onMatchDelay"))           this.config.keyCode.onMatchDelay = false;
        if(!this.config.regEx.hasOwnProperty("onMatchPreventDefault"))  this.config.keyCode.onMatchPreventDefault = false;
        if(!this.config.regEx.hasOwnProperty("onFailed"))               this.config.keyCode.onFailed = false;
        if(!this.config.regEx.hasOwnProperty("onFailedPreventDefault")) this.config.keyCode.onFailedPreventDefault = false;
    }
}

KeyListener.prototype.executeAction = function(e, inAction) {
    switch(typeof(inAction)) {
        case "string" :
            eval(inAction);
            break;
        case "function" :
            inAction.call(this, e); // this is supposed to be the key listener instance
            break;
    }
}

/**
	* Processes the keyboard input key value passed to it.
	*
	* @param	Key Event	e			Key event we are going to process
	**/
KeyListener.prototype.processKey = function( e ){
    clearTimeout(this.delayedAction);

    var action = false;
    var actionDelay = false;
    var keyCode = (e.keyCode)?e.keyCode:e.which;
    var character = String.fromCharCode(keyCode);
    var matched     = false;
    var preventDefault = false;
    if(this.config.keyCode) {
        if(this.config.keyCode.hasOwnProperty(keyCode)) {
            matched = true;
            action = this.config.keyCode[keyCode];
            actionDelay = this.config.keyCode.onMatchDelay;
            preventDefault = this.config.keyCode.onMatchPreventDefault;
        } else {
            action = this.config.keyCode.onFailed;
            preventDefault = this.config.keyCode.onFailedPreventDefault;
        }
    }

    if(!matched && this.config.chars) {
        if(this.config.chars.hasOwnProperty(character)) {
            matched = true;
            action = this.config.chars[character];
            actionDelay = this.config.chars.onMatchDelay;
            preventDefault = this.config.chars.onMatchPreventDefault;
        } else {
            action = this.config.chars.onFailed;
            preventDefault = this.config.chars.onFailedPreventDefault;
        }
    }

    if(!matched && this.config.regEx) {
        var newval = this.element.value.substring(0, this.element.selectionStart);
        newval += character;
        newval += this.element.value.substring(this.element.selectionEnd);
        if(this.config.regEx.expr.test(newval) ) {
            action = this.config.regEx.onMatch;
            actionDelay = this.config.regEx.onMatchDelay;
            preventDefault = this.config.regEx.onMatchPreventDefault;
        } else {
            if(this.config.regEx.onFailed) action = this.config.regEx.onFailed;
            if(this.config.regEx.onFailedPreventDefault) preventDefault = this.config.regEx.onFailedPreventDefault;
        }
        delete newval;
    }

    if(preventDefault) {
        if (e.preventDefault) e.preventDefault();
        if (e.stopPropagation) e.stopPropagation();
    }

    if(action) {
        if(!actionDelay) {
            this.executeAction(e, action);
        } else {
            var listener = this;
            this.delayedAction = setTimeout(function() {
                    listener.executeAction(e, action);
                }, actionDelay
            );
        }
    }
    delete matched, action,actionDelay,keyCode,character,preventDefault;
}


if(typeof(jQuery) == "function") {
    jQuery.orangeLib = {};
    jQuery.orangeLib.sl = new SnippetLib();
} else {
    sl = new SnippetLib();
}













