$.fn.XMLToJSON = function(storer, iterationNodePath, remap) {
	
	
	//prep
	
	var xml = $(this);
	var root = $(this).children();
	var afterTransferRules = [];
	var iterationNodeName = root.children().get(0).tagName;
	var error = false;
	
	//validation - return if stuff not passed
	
	if (!storer || typeof storer != 'object')
		error = "No storer array passed in first argument, i.e. to hold the converted JSON in";
	else if (!iterationNodePath || typeof iterationNodePath != 'string')
		error = "No iteration node path specified, e.g. 'root item' or, if RSS, 'rss channel item'";
	else if (remap && typeof remap != 'object')
		error = "Remap parameter must be an object of XML node paths to JSON node paths";
	if (error) { console.log("XML-to-JSON error: "+error); return; }
	
	
	//iterate over node and transfer data into JSON object. If node contains children, call iterate() on itself.
	
    function iterate(node) {
        var temp = [];
        node.children().each(function() {
            var propName = getXPath($(this))+$(this).get(0).tagName;
            if (remap && remap[propName]) {
            	propName = remap[propName];
            	xml.find(getXPath($(this)).replace(/\./g, ' ')+$(this).get(0).tagName).attr('remapped_xPathShouldBe', propName);;
            	propName = propName.split('.');
        	} else {
	            propName = propName.split('.');
	            propName = [propName[propName.length-1]];
	        }
            var brackets = ''; for(var i in propName) brackets += '(';
            var evalStr = brackets+"temp['";
            for(var d in propName) {
            	var pathToThisPoint = d > 0 ? propName.slice(0, d) : [propName[0]];
            	evalStr += propName[d]+(d < propName.length-1 ? "'] = temp."+pathToThisPoint.join('.')+" ? temp."+pathToThisPoint.join('.')+": {})['" : '');
            }
            evalStr += "'])";
            evalStr += " = "+($(this).children().length == 0 ? "$(this).text()" : "$.extend("+evalStr+", iterate($(this)))")+";";
            eval(evalStr);
        });
        return temp;
    }
    
    
    //get XPath from given node up to (but not including) iteration node
    
    function getXPath(node) {
    	return function() {
    		var j=[]; node.parentsUntil(iterationNodePath).each(function() {
    			j.unshift($(this).get(0).tagName);
    		});
    		return j.length > 0 ? j.join('.')+'.' : '';
    	}();
	}
	
	
	//start ball rolling by calling iterate() on each iteration node (i.e. each child of root node)
    $(this).find(iterationNodePath).each(function() { 
      storer.push(iterate($(this))); 
    });

    
    //lastly, if any remapped properties need their XPath updating, fix here
    
    xml.find('[remapped_xPathShouldBe]').each(function() {
    	var attr_split = $(this).attr('remapped_xPathShouldBe').split('.');
    	var thisIterationNodeIndex = $(this).closest(iterationNodeName).prevAll().length;
    	var brackets = ''; for(var i=0; i<attr_split.length; i++) brackets += '(';
    	var evalStr = brackets+"storer["+thisIterationNodeIndex+"]['";
    	for(var d in attr_split) {
        	var pathToThisPoint = d > 0 ? attr_split.slice(0, d) : [attr_split[0]];
        	evalStr += attr_split[d]+(d < attr_split.length-1 ? "'] = storer["+thisIterationNodeIndex+"]."+pathToThisPoint.join('.')+" ? storer["+thisIterationNodeIndex+"]."+pathToThisPoint.join('.')+": {})['" : '');
    	}
    	evalStr += "'])";
    	if (eval(evalStr) == undefined) {
    		eval(evalStr+" = $(this).text();");
    	}
	});
    
}
