(function (soma) {

	'use strict';

	soma.template = soma.template || {};
	soma.template.version = '0.2.8';

	soma.template.errors = {
		TEMPLATE_STRING_NO_ELEMENT: 'Error in soma.template, a string template requirement a second parameter: an element target - soma.template.create(\'string\', element)',
		TEMPLATE_NO_PARAM: 'Error in soma.template, a template requires at least 1 parameter - soma.template.create(element)'
	};

	var tokenStart = '{{';
	var tokenEnd = '}}';
	var helpersObject = {};
	var helpersScopeObject = {};

	var settings = soma.template.settings = soma.template.settings || {};

	settings.autocreate = true;

	var tokens = settings.tokens = {
		start: function(value) {
			if (isDefined(value) && value !== '') {
				tokenStart = escapeRegExp(value);
				setRegEX(value, true);
			}
			return tokenStart;
		},
		end: function(value) {
			if (isDefined(value) && value !== '') {
				tokenEnd = escapeRegExp(value);
				setRegEX(value, false);
			}
			return tokenEnd;
		}
	};

	var attributes = settings.attributes = {
		skip: 'data-skip',
		repeat: 'data-repeat',
		src: 'data-src',
		href: 'data-href',
		show: 'data-show',
		hide: 'data-hide',
		cloak: 'data-cloak',
		checked: 'data-checked',
		disabled: 'data-disabled',
		multiple: 'data-multiple',
		readonly: 'data-readonly',
		selected: 'data-selected',
		template: 'data-template',
		html: 'data-html'
	};

	var vars = settings.vars = {
		index: '$index',
		key: '$key',
		element: '$element',
		parentElement: '$parentElement',
		attribute: '$attribute',
		scope: '$scope'
	};

	var events = settings.events = {};
	settings.eventsPrefix = 'data-';
	var eventsString = 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup focus blur change select selectstart scroll copy cut paste mousewheel keypress error contextmenu input textinput drag dragenter dragleave dragover dragend dragstart dragover drop load submit reset search resize beforepaste beforecut beforecopy';
	eventsString += ' touchstart touchend touchmove touchenter touchleave touchcancel gesturestart gesturechange gestureend';
	var eventsArray = eventsString.split(' ');
	var i = -1, l = eventsArray.length;
	while(++i < l) {
		events[settings.eventsPrefix + eventsArray[i]] = eventsArray[i];
	}

	var regex = {
		sequence: null,
		token: null,
		expression: null,
		escape: /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,
		trim: /^[\s+]+|[\s+]+$/g,
		repeat: /(.*)\s+in\s+(.*)/,
		func: /(.*)\((.*)\)/,
		params: /,\s+|,|\s+,\s+/,
		quote: /\"|\'/g,
		content: /[^.|^\s]/gm,
		depth: /..\//g,
		string: /^(\"|\')(.*)(\"|\')$/
	};

	var ie = (function(){
		if (typeof document !== 'object') {
			return undefined;
		}
		var v = 3,
			stop = false,
			div = document.createElement('div');
		while (!stop) {
			div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->';
			if (!div.getElementsByTagName('i')[0]) {
				stop = true;
			}
		}
		return v > 4 ? v : undefined;
	}());
	function isArray(value) {
		return Object.prototype.toString.apply(value) === '[object Array]';
	}
	function isObject(value) {
		return typeof value === 'object';
	}
	function isString(value) {
		return typeof value === 'string';
	}
	function isElement(value) {
		return value ? value.nodeType > 0 : false;
	}
	function isTextNode(el) {
		return el && el.nodeType && el.nodeType === 3;
	}
	function isFunction(value) {
		return value && typeof value === 'function';
	}
	function isDefined(value) {
		return value !== null && value !== undefined;
	}
	function normalizeBoolean(value) {
		if (!isDefined(value)) {
			return false;
		}
		if (value === 'true' || value === '1' || value === true || value === 1) {
			return true;
		}
		if (value === 'false' || value === '0' || value === false || value === 0 || (isString(value) && hasInterpolation(value))) {
			return false;
		}
		return !!value;
	}
	function isExpression(value) {
		return value && isFunction(value.toString) && value.toString() === '[object Expression]';
	}
	function isExpFunction(value) {
		if (!isString(value)) {
			return false;
		}
		return !!value.match(regex.func);
	}
	function childNodeIsTemplate(node) {
		return node && node.parent && templates.get(node.element);
	}
	function escapeRegExp(str) {
		return str.replace(regex.escape, '\\$&');
	}
	function setRegEX(nonEscapedValue, isStartToken) {
		// sequence: \{\{.+?\}\}|[^{]+|\{(?!\{)[^{]*
		var unescapedCurrentStartToken = tokens.start().replace(/\\/g, '');
		var endSequence = '';
		var ts = isStartToken ? nonEscapedValue : unescapedCurrentStartToken;
		if (ts.length > 1) {
			endSequence = '|\\' + ts.substr(0, 1) + '(?!\\' + ts.substr(1, 1) + ')[^' + ts.substr(0, 1) + ']*';
		}
		regex.sequence = new RegExp(tokens.start() + '.+?' + tokens.end() + '|[^' + tokens.start() + ']+' + endSequence, 'g');
		regex.token = new RegExp(tokens.start() + '.*?' + tokens.end(), 'g');
		regex.expression = new RegExp(tokens.start() + '|' + tokens.end(), 'gm');
	}
	function trim(value) {
		return value.replace(regex.trim, '');
	}
	function trimQuotes(value) {
		if (regex.string.test(value)) {
			return value.substr(1, value.length-2);
		}
		return value;
	}
	function trimArray(value) {
		if (value[0] === '') {
			value.shift();
		}
		if (value[value.length-1] === '') {
			value.pop();
		}
		return value;
	}
	function trimTokens(value) {
		return value.replace(regex.expression, '');
	}
	function trimScopeDepth(value) {
		return value.replace(regex.depth, '');
	}
	function insertBefore(referenceNode, newNode) {
		if (!referenceNode.parentNode) {
			return;
		}
		referenceNode.parentNode.insertBefore(newNode, referenceNode);
	}
	function insertAfter(referenceNode, newNode) {
		if (!referenceNode.parentNode) {
			return;
		}
		referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
	}
	function removeClass(elm, className) {
		var rmc;
		if (typeof document === 'object' && document.documentElement.classList) {
			rmc = function (elm, className) {
				elm.classList.remove(className);
			};
		} else {
			rmc = function (elm, className) {
				if (!elm || !elm.className) {
					return false;
				}
				var reg = new RegExp('(^|\\s)' + className + '(\\s|$)', 'g');
				elm.className = elm.className.replace(reg, '$2');
			};
		}
		rmc(elm, className);
	}
	// jquery contains
	var contains = typeof document !== 'object' ? function(){} : document.documentElement.contains ?
		function( a, b ) {
			var adown = a.nodeType === 9 ? a.documentElement : a,
				bup = b && b.parentNode;
			return a === bup || !!( bup && bup.nodeType === 1 && adown.contains && adown.contains(bup) );
		} :
		document.documentElement.compareDocumentPosition ?
			function( a, b ) {
				return b && !!( a.compareDocumentPosition( b ) & 16 );
			} :
			function( a, b ) {
				while ( (b = b.parentNode) ) {
					if ( b === a ) {
						return true;
					}
				}
				return false;
			};

	function HashMap(id) {
		var items = {};
		var count = 0;
		//var uuid = function(a,b){for(b=a='';a++<36;b+=a*51&52?(a^15?8^Math.random()*(a^20?16:4):4).toString(16):'-');return b;}
		function uuid() { return ++count + id; }
		function getKey(target) {
			if (!target) {
				return;
			}
			if (typeof target !== 'object') {
				return target;
			}
			var result;
			try {
				// IE 7-8 needs a try catch, seems like I can't add a property on text nodes
				result = target[id] ? target[id] : target[id] = uuid();
			} catch(err){}
			return result;
		}
		this.remove = function(key) {
			delete items[getKey(key)];
		};
		this.get = function(key) {
			return items[getKey(key)];
		};
		this.put = function(key, value) {
			items[getKey(key)] = value;
		};
		this.has = function(key) {
			return typeof items[getKey(key)] !== 'undefined';
		};
		this.getData = function() {
			return items;
		};
		this.dispose = function() {
			for (var key in items) {
				if (items.hasOwnProperty(key)) {
					delete items[key];
				}
			}
			this.length = 0;
		};
	}

	function getRepeaterData(repeaterValue, scope) {
		var parts = repeaterValue.match(regex.repeat);
		if (!parts) {
			return;
		}
		var source = parts[2];
		var exp = new Expression(source);
		return exp.getValue(scope);
	}

	function updateScopeWithRepeaterData(repeaterValue, scope, data) {
		var parts = repeaterValue.match(regex.repeat);
		if (!parts) {
			return;
		}
		var name = parts[1];
		scope[name] = data;
	}
	function getWatcherValue(exp, newValue) {
		var node = exp.node || exp.attribute.node;
		var watchers = node.template.watchers;
		var nodeTarget = node.element;
		if (!watchers) {
			return newValue;
		}
		var watcherNode = watchers.get(nodeTarget);
		if (!watcherNode && isTextNode(node.element) && node.parent) {
			watcherNode = watchers.get(node.parent.element);
		}
		var watcher = watcherNode ? watcherNode : watchers.get(exp.pattern);
		if (isFunction(watcher)) {
			var watcherValue = watcher(exp.value, newValue, exp.pattern, node.scope, node, exp.attribute);
			if (isDefined(watcherValue)) {
				return watcherValue;
			}
		}
		return newValue;
	}

	function getScopeFromPattern(scope, pattern) {
		var depth = getScopeDepth(pattern);
		var scopeTarget = scope;
		while (depth > 0) {
			scopeTarget = scopeTarget._parent ? scopeTarget._parent : scopeTarget;
			depth--;
		}
		return scopeTarget;
	}

	function getValueFromPattern(scope, pattern, context) {
		var exp = new Expression(pattern);
		return getValue(scope, exp.pattern, exp.path, exp.params, undefined, undefined, undefined, context);
	}

	function getValue(scope, pattern, pathString, params, getFunction, getParams, paramsFound, context) {
		// context
		if (pattern === vars.element) {
			return context[vars.element];
		}
		if (pattern === vars.parentElement) {
			return context[vars.parentElement];
		}
		if (pattern === vars.attribute) {
			return context[vars.attribute];
		}
		if (pattern === vars.scope) {
			return context[vars.scope];
		}
		// string
		if (regex.string.test(pattern)) {
			return trimQuotes(pattern);
		}
		else if(!isNaN(pattern)) {
			return +pattern;
		}
		// find params
		var paramsValues = [];
		if (!paramsFound && params) {
			for (var j = 0, jl = params.length; j < jl; j++) {
				paramsValues.push(getValueFromPattern(scope, params[j], context));
			}
		}
		else {
			paramsValues = paramsFound;
		}
		if (getParams) {
			return paramsValues;
		}
		// find scope
		var scopeTarget = getScopeFromPattern(scope, pattern);
		// remove parent string
		pattern = pattern.replace(/..\//g, '');
		pathString = pathString.replace(/..\//g, '');
		if (!scopeTarget) {
			return undefined;
		}
		// search path
		var path = scopeTarget;
		var pathParts = pathString.split(/\.|\[|\]/g);
		if (pathParts.length > 0) {
			for (var i = 0, l = pathParts.length; i < l; i++) {
				if (pathParts[i] !== '') {
					path = path[pathParts[i]];
				}
				if (!isDefined(path)) {
					// no path, search in parent
					if (scopeTarget._parent) {
						return getValue(scopeTarget._parent, pattern, pathString, params, getFunction, getParams, paramsValues);
					}
					else {
						return undefined;
					}
				}
			}
		}
		// return value
		if (!isFunction(path)) {
			return path;
		}
		else {
			if (getFunction) {
				return path;
			}
			else {
				return path.apply(null, paramsValues);
			}
		}
		return undefined;
	}

	function getExpressionPath(value) {
		var val = value.split('(')[0];
		val = trimScopeDepth(val);
		return val;
	}

	function getParamsFromString(value) {
		return trimArray(value.split(regex.params));
	}

	function getScopeDepth(value) {
		var val = value.split('(')[0];
		var matches = val.match(regex.depth);
		return !matches ? 0 : matches.length;
	}

	function addAttribute(node, name, value) {
		var attr;
		node.attributes = node.attributes || [];
		if (name === settings.attributes.skip) {
			node.skip = normalizeBoolean(value);
		}
		if (name === settings.attributes.html) {
			node.html = normalizeBoolean(value);
		}
		if (name === settings.attributes.repeat && !node.isRepeaterDescendant) {
			node.repeater = value;
		}
		if (
			hasInterpolation(name + ':' + value) ||
				name === settings.attributes.repeat ||
				name === settings.attributes.skip ||
				name === settings.attributes.html ||
				name === settings.attributes.show ||
				name === settings.attributes.hide ||
				name === settings.attributes.href ||
				name === settings.attributes.checked ||
				name === settings.attributes.disabled ||
				name === settings.attributes.multiple ||
				name === settings.attributes.readonly ||
				name === settings.attributes.selected ||
				value.indexOf(settings.attributes.cloak) !== -1
			) {
			attr = new Attribute(name, value, node);
			node.attributes.push(attr);
		}
		if (events[name]) {
			attr = new Attribute(name, value, node);
			node.attributes.push(attr);
		}
		return attr;
	}

	function getNodeFromElement(element, scope) {
		var node = new Node(element, scope);
		node.previousSibling = element.previousSibling;
		node.nextSibling = element.nextSibling;
		var eventsArray = [];
		for (var attr, attrs = element.attributes, j = 0, jj = attrs && attrs.length; j < jj; j++) {
			attr = attrs[j];
			if (attr.specified || attr.name === 'value') {
				var newAttr = addAttribute(node, attr.name, attr.value);
				if (events[attr.name]) {
					if (events[attr.name] && !node.isRepeaterChild) {
						eventsArray.push({name:events[attr.name], value:attr.value, attr: newAttr});
					}
				}
			}
		}
		for (var a=0, b=eventsArray.length; a<b; a++) {
			node.addEvent(eventsArray[a].name, eventsArray[a].value, eventsArray[a].attr);
		}
		return node;
	}

	function hasInterpolation(value) {
		var matches = value.match(regex.token);
		return matches && matches.length > 0;
	}

	function hasContent(value) {
		return regex.content.test(value);
	}

	function isElementValid(element) {
		if (!element) {
			return;
		}
		var type = element.nodeType;
		if (!element || !type) {
			return false;
		}
		// comment
		if (type === 8) {
			return false;
		}
		// empty text node
		if (type === 3 && !hasContent(element.nodeValue) && !hasInterpolation(element.nodeValue)) {
			return false;
		}
		// result
		return true;
	}

	function compile(template, element, parent, nodeTarget) {
		if (!isElementValid(element)) {
			return;
		}
		// get node
		var node;
		if (!nodeTarget) {
			node = getNodeFromElement(element, parent ? parent.scope : new Scope(helpersScopeObject)._createChild());
		}
		else {
			node = nodeTarget;
			node.parent = parent;
		}
		if (parent && (parent.repeater || parent.isRepeaterChild)) {
			node.isRepeaterChild = true;
		}
		node.template = template;
		// children
		if (node.skip) {
			return;
		}
		var child = element.firstChild;
		while (child) {
			var childNode = compile(template, child, node);
			if (childNode) {
				childNode.parent = node;
				node.children.push(childNode);
			}
			child = child.nextSibling;
		}
		return node;
	}

	function updateScopeWithData(scope, data) {
		clearScope(scope);
		for (var d in data) {
			if (data.hasOwnProperty(d)) {
				scope[d] = data[d];
			}
		}
	}

	function clearScope(scope) {
		for (var key in scope) {
			if (scope.hasOwnProperty(key)) {
				if (key.substr(0, 1) !== '_') {
					scope[key] = null;
					delete scope[key];
				}
			}
		}
	}

	function updateNodeChildren(node) {
		if (node.repeater || !node.children || childNodeIsTemplate(node)) {
			return;
		}
		for (var i = 0, l = node.children.length; i < l; i++) {
			node.children[i].update();
		}
	}

	function renderNodeChildren(node) {
		if (!node.children || childNodeIsTemplate(node)) {
			return;
		}
		for (var i = 0, l = node.children.length; i < l; i++) {
			node.children[i].render();
		}
	}

	function renderNodeRepeater(node) {
		var data = getRepeaterData(node.repeater, node.scope);
		var previousElement;
		if (isArray(data)) {
			// process array
			for (var i = 0, l1 = data.length, l2 = node.childrenRepeater.length, l = l1 > l2 ? l1 : l2; i < l; i++) {
				if (i < l1) {
					previousElement = createRepeaterChild(node, i, data[i], vars.index, i, previousElement);
				}
				else {
					node.parent.element.removeChild(node.childrenRepeater[i].element);
					node.childrenRepeater[i].dispose();
				}
			}
			if (node.childrenRepeater.length > data.length) {
				node.childrenRepeater.length = data.length;
			}
		}
		else {
			// process object
			var count = -1;
			for (var o in data) {
				if (data.hasOwnProperty(o)) {
					count++;
					previousElement = createRepeaterChild(node, count, data[o], vars.key, o, previousElement);
				}
			}
			var size = count;
			while (count++ < node.childrenRepeater.length-1) {
				node.parent.element.removeChild(node.childrenRepeater[count].element);
				node.childrenRepeater[count].dispose();
			}
			node.childrenRepeater.length = size+1;
		}
		if (node.element.parentNode) {
			node.element.parentNode.removeChild(node.element);
		}
	}

	function compileClone(node, newNode) {
		if (!isElementValid(newNode.element)) {
			return;
		}
		// create attribute
		if (node.attributes) {
			for (var i= 0, l=node.attributes.length; i<l; i++) {
				var attr = node.attributes[i];
				var newAttr = addAttribute(newNode, attr.name, attr.value);
				if (events[attr.name]) {
					newNode.addEvent(events[attr.name], attr.value, newAttr);
				}
			}
		}
		// children
		var child = node.element.firstChild;
		var newChild = newNode.element.firstChild;
		// loop
		while (child && newChild) {
			var childNode = node.getNode(child);
			var newChildNode = new Node(newChild, newNode.scope);
			newNode.children.push(newChildNode);
			newChildNode.parent = newNode;
			newChildNode.template = newNode.template;
			newChildNode.isRepeaterChild = true;
			var compiledNode = compileClone(childNode, newChildNode);
			if (compiledNode) {
				compiledNode.parent = newChildNode;
				compiledNode.template = newChildNode.template;
				newChildNode.children.push(compiledNode);
			}
			child = child.nextSibling;
			newChild = newChild.nextSibling;
		}
		return newChildNode;
	}

	function cloneRepeaterNode(element, node) {
		var newNode = new Node(element, node.scope._createChild());
		newNode.template = node.template;
		newNode.parent = node;
		newNode.isRepeaterChild = true;
		newNode.isRepeaterDescendant = true;
		compileClone(node, newNode);
		return newNode;
	}

	function appendRepeaterElement(previousElement, node, newElement) {
		if (!previousElement) {
			if (node.element.previousSibling) {
				insertAfter(node.element.previousSibling, newElement);
			}
			else if (node.element.nextSibling) {
				insertBefore(node.element.nextSibling, newElement);
			}
			else {
				node.parent.element.appendChild(newElement);
			}
		}
		else {
			insertAfter(previousElement, newElement);
		}
	}

	function createRepeaterChild(node, count, data, indexVar, indexVarValue, previousElement) {
		var existingChild = node.childrenRepeater[count];
		if (!existingChild) {
			var newElement = node.element.cloneNode(true);
			// need to append the cloned element to the DOM
			// before changing attributes or IE will crash
			appendRepeaterElement(previousElement, node, newElement);
			// can't recreate the node with a cloned element on IE7
			// be cause the attributes are not specified anymore (attribute.specified)
			//var newNode = getNodeFromElement(newElement, node.scope._createChild(), true);
			var newNode = cloneRepeaterNode(newElement, node);
			node.childrenRepeater[count] = newNode;
			updateScopeWithRepeaterData(node.repeater, newNode.scope, data);
			newNode.scope[indexVar] = indexVarValue;
			newNode.update();
			newNode.render();
			return newElement;
		}
		else {
			// existing node
			updateScopeWithRepeaterData(node.repeater, existingChild.scope, data);
			existingChild.scope[indexVar] = indexVarValue;
			existingChild.update();
			existingChild.render();
			return existingChild.element;
		}
	}

	var Scope = function(data) {
		var self;
		function createChild(data) {
			var obj = createObject(data);
			obj._parent = self;
			self._children.push(obj);
			return obj;
		}
		function createObject(data) {
			var obj = data || {};
			obj._parent = null;
			obj._children = [];
			obj._createChild = function() {
				self = obj;
				return createChild.apply(obj, arguments);
			};
			return obj;
		}
		return createObject(data);
	};
	var Node = function(element, scope) {
		this.element = element;
		this.scope = scope;
		this.attributes = null;
		this.value = null;
		this.interpolation = null;
		this.invalidate = false;
		this.skip = false;
		this.repeater = null;
		this.isRepeaterDescendant = false;
		this.isRepeaterChild = false;
		this.parent = null;
		this.children = [];
		this.childrenRepeater = [];
		this.previousSibling = null;
		this.nextSibling = null;
		this.template = null;
		this.eventHandlers = {};
		this.html = false;

		if (isTextNode(this.element)) {
			this.value = this.element.nodeValue;
			this.interpolation = new Interpolation(this.value, this, undefined);
		}

	};
	Node.prototype = {
		toString: function() {
			return '[object Node]';
		},
		dispose: function() {
			this.clearEvents();
			var i, l;
			if (this.children) {
				for (i = 0, l = this.children.length; i < l; i++) {
					this.children[i].dispose();
				}
			}
			if (this.childrenRepeater) {
				for (i = 0, l = this.childrenRepeater.length; i < l; i++) {
					this.childrenRepeater[i].dispose();
				}
			}
			if (this.attributes) {
				for (i = 0, l = this.attributes.length; i < l; i++) {
					this.attributes[i].dispose();
				}
			}
			if (this.interpolation) {
				this.interpolation.dispose();
			}
			this.element = null;
			this.scope = null;
			this.attributes = null;
			this.value = null;
			this.interpolation = null;
			this.repeater = null;
			this.parent = null;
			this.children = null;
			this.childrenRepeater = null;
			this.previousSibling = null;
			this.nextSibling = null;
			this.template = null;
			this.eventHandlers = null;
		},
		getNode: function(element) {
			var node;
			if (element === this.element) {
				return this;
			}
			if (this.childrenRepeater.length > 0) {
				for (var k = 0, kl = this.childrenRepeater.length; k < kl; k++) {
					node = this.childrenRepeater[k].getNode(element);
					if (node) {
						return node;
					}
				}
			}
			for (var i = 0, l = this.children.length; i < l; i++) {
				node = this.children[i].getNode(element);
				if (node) {
					return node;
				}
			}
			return null;
		},
		getAttribute: function(name) {
			if (this.attributes) {
				for (var i = 0, l = this.attributes.length; i < l; i++) {
					var att = this.attributes[i];
					if (att.interpolationName && att.interpolationName.value === name) {
						return att;
					}
				}
			}
		},
		update: function() {
			if (childNodeIsTemplate(this)) {
				return;
			}
			if (isDefined(this.interpolation)) {
				this.interpolation.update();
			}
			if (isDefined(this.attributes)) {
				for (var i = 0, l = this.attributes.length; i < l; i++) {
					this.attributes[i].update();
				}
			}
			updateNodeChildren(this);
		},
		invalidateData: function() {
			if (childNodeIsTemplate(this)) {
				return;
			}
			this.invalidate = true;
			var i, l;
			if (this.attributes) {
				for (i = 0, l = this.attributes.length; i < l; i++) {
					this.attributes[i].invalidate = true;
				}
			}
			for (i = 0, l = this.childrenRepeater.length; i < l; i++) {
				this.childrenRepeater[i].invalidateData();
			}
			for (i = 0, l = this.children.length; i < l; i++) {
				this.children[i].invalidateData();
			}
		},
		addEvent: function(type, pattern, attr) {
			if (this.repeater) {
				return;
			}
			if (this.eventHandlers[type]) {
				this.removeEvent(type);
			}
			var scope = this.scope;
			var node = this;
			var handler = function(event) {
				var exp = new Expression(pattern, node, attr);
				var func = exp.getValue(scope, true);
				var params = exp.getValue(scope, false, true);
				params.unshift(event);
				if (func) {
					func.apply(null, params);
				}
			};
			this.eventHandlers[type] = handler;
			addEvent(this.element, type, handler);
		},
		removeEvent: function(type) {
			removeEvent(this.element, type, this.eventHandlers[type]);
			this.eventHandlers[type] = null;
			delete this.eventHandlers[type];
		},
		clearEvents: function() {
			if (this.eventHandlers) {
				for (var key in this.eventHandlers) {
					if (this.eventHandlers.hasOwnProperty(key)) {
						this.removeEvent(key);
					}
				}
			}
			if (this.children) {
				for (var k = 0, kl = this.children.length; k < kl; k++) {
					this.children[k].clearEvents();
				}
			}
			if (this.childrenRepeater) {
				for (var f = 0, fl = this.childrenRepeater.length; f < fl; f++) {
					this.childrenRepeater[f].clearEvents();
				}
			}
		},
		render: function() {
			if (childNodeIsTemplate(this)) {
				return;
			}
			if (this.invalidate) {
				this.invalidate = false;
				if (isTextNode(this.element)) {
					if (this.parent && this.parent.html) {
						this.value = this.parent.element.innerHTML = this.interpolation.render();
					}
					else {
						this.value = this.element.nodeValue = this.interpolation.render();
					}
				}
			}
			if (this.attributes) {
				for (var i = 0, l = this.attributes.length; i < l; i++) {
					this.attributes[i].render();
				}
			}
			if (this.repeater) {
				renderNodeRepeater(this);
			}
			else {
				renderNodeChildren(this);
			}
		}
	};
	var Attribute = function(name, value, node) {
		this.name = name;
		this.value = value;
		this.node = node;
		this.interpolationName = new Interpolation(this.name, null, this);
		this.interpolationValue = new Interpolation(this.value, null, this);
		this.invalidate = false;
	};
	Attribute.prototype = {
		toString: function() {
			return '[object Attribute]';
		},
		dispose: function() {
			if (this.interpolationName) {
				this.interpolationName.dispose();
			}
			if (this.interpolationValue) {
				this.interpolationValue.dispose();
			}
			this.interpolationName = null;
			this.interpolationValue = null;
			this.node = null;
			this.name = null;
			this.value = null;
			this.previousName = null;
		},
		update: function() {
			if (this.node.repeater) {
				return;
			}
			this.interpolationName.update();
			this.interpolationValue.update();
		},
		render: function() {
			if (this.node.repeater) {
				return;
			}
			// normal attribute
			function renderAttribute(name, value, node) {
				if (name === 'value' && node.element['value'] !== undefined) {
					element.value = value;
				}
				else if (ie === 7 && name === 'class') {
					element.className = value;
				}
				else {
					element.setAttribute(name, value);
				}
			}
			// boolean attribute
			function renderBooleanAttribute(name, value) {
				element.setAttribute(name, value);
			}
			// special attribute
			function renderSpecialAttribute(value, attrName) {
				if (normalizeBoolean(value)) {
					element.setAttribute(attrName, attrName);
				}
				else {
					element.removeAttribute(attrName);
				}
			}
			// src attribute
			function renderSrc(value) {
				element.setAttribute('src', value);
			}
			// href attribute
			function renderHref(value) {
				element.setAttribute('href', value);
			}
			var element = this.node.element;
			if (this.invalidate) {
				this.invalidate = false;
				this.previousName = this.name;
				this.name = isDefined(this.interpolationName.render()) ? this.interpolationName.render() : this.name;
				this.value = isDefined(this.interpolationValue.render()) ? this.interpolationValue.render() : this.value;
				if (this.name === attributes.src) {
					renderSrc(this.value);
				}
				else if (this.name === attributes.href) {
					renderHref(this.value);
				}
				else {
					if (ie !== 7 || (ie === 7 && !this.node.isRepeaterChild)) {
						this.node.element.removeAttribute(this.interpolationName.value);
					}
					if (this.previousName) {
						if (ie === 7 && this.previousName === 'class') {
							// iE
							this.node.element.className = '';
						}
						else {
							if (ie !== 7 || (ie === 7 && !this.node.isRepeaterChild)) {
								this.node.element.removeAttribute(this.previousName);
							}
						}
					}
					renderAttribute(this.name, this.value, this.node);
				}
			}
			// cloak
			if (this.name === 'class' && this.value.indexOf(settings.attributes.cloak) !== -1) {
				removeClass(this.node.element, settings.attributes.cloak);
			}
			// hide
			if (this.name === attributes.hide) {
				var bool = normalizeBoolean(this.value);
				renderAttribute(this.name, bool, this.node);
				element.style.display = bool ? 'none' : '';
			}
			// show
			if (this.name === attributes.show) {
				var bool = normalizeBoolean(this.value);
				renderAttribute(this.name, bool, this.node);
				element.style.display = bool ? '' : 'none';
			}
			// checked
			if (this.name === attributes.checked) {
				renderSpecialAttribute(this.value, 'checked');
				renderAttribute(this.name, normalizeBoolean(this.value) ? true : false, this.node);
                element.checked = normalizeBoolean(this.value) ? true : false;
			}
			// disabled
			if (this.name === attributes.disabled) {
				renderSpecialAttribute(this.value, 'disabled');
				renderAttribute(this.name, normalizeBoolean(this.value) ? true : false, this.node);
			}
			// multiple
			if (this.name === attributes.multiple) {
				renderSpecialAttribute(this.value, 'multiple');
				renderAttribute(this.name, normalizeBoolean(this.value) ? true : false, this.node);
			}
			// readonly
			if (this.name === attributes.readonly) {
				var bool = normalizeBoolean(this.value);
				if (ie === 7) {
					element.readOnly = bool ? true : false;
				}
				else {
					renderSpecialAttribute(this.value, 'readonly');
				}
				renderAttribute(this.name, bool ? true : false, this.node);
			}
			// selected
			if (this.name === attributes.selected) {
				renderSpecialAttribute(this.value, 'selected');
				renderAttribute(this.name, normalizeBoolean(this.value) ? true : false, this.node);
			}
		}
	};

	var Interpolation = function(value, node, attribute) {
		this.value = node && !isTextNode(node.element) ? trim(value) : value;
		this.node = node;
		this.attribute = attribute;
		this.sequence = [];
		this.expressions = [];
		var parts = this.value.match(regex.sequence);
		if (parts) {
			for (var i = 0, l = parts.length; i < l; i++) {
				if (parts[i].match(regex.token)) {
					var exp = new Expression(trimTokens(parts[i]), this.node, this.attribute);
					this.sequence.push(exp);
					this.expressions.push(exp);
				}
				else {
					this.sequence.push(parts[i]);
				}
			}
			trimArray(this.sequence);
		}
	};
	Interpolation.prototype = {
		toString: function() {
			return '[object Interpolation]';
		},
		dispose: function() {
			if (this.expressions) {
				for (var i = 0, l = this.expressions.length; i < l; i++) {
					this.expressions[i].dispose();
				}
			}
			this.value = null;
			this.node = null;
			this.attribute = null;
			this.sequence = null;
			this.expressions = null;
		},
		update: function() {
			var i = -1, l = this.expressions.length;
			while (++i < l) {
				this.expressions[i].update();
			}
		},
		render: function() {
			var rendered = '';
			if (this.sequence) {
				for (var i = 0, l = this.sequence.length; i < l; i++) {
					var val = '';
					if (isExpression(this.sequence[i])) {
						val = this.sequence[i].value;
					}
					else {
						val = this.sequence[i];
					}
					if (!isDefined(val)) {
						val = '';
					}
					rendered += val;
				}
			}
			return rendered;
		}
	};

	var Expression = function(pattern, node, attribute) {
		if (!isDefined(pattern)) {
			return;
		}
		this.pattern = pattern;
		this.isString = regex.string.test(pattern);
		this.node = node;
		this.attribute = attribute;
		this.value = this.isString ? this.pattern : undefined;
		if (this.isString) {
			this.isFunction = false;
			this.depth = null;
			this.path = null;
			this.params = null;
		}
		else {
			this.isFunction = isExpFunction(this.pattern);
			this.depth = getScopeDepth(this.pattern);
			this.path = getExpressionPath(this.pattern);
			this.params = !this.isFunction ? null : getParamsFromString(this.pattern.match(regex.func)[2]);
		}
	};
	Expression.prototype = {
		toString: function() {
			return '[object Expression]';
		},
		dispose: function() {
			this.pattern = null;
			this.node = null;
			this.attribute = null;
			this.path = null;
			this.params = null;
			this.value = null;
		},
		update: function() {
			var node = this.node;
			if (!node && this.attribute) {
				node = this.attribute.node;
			}
			if (!node && node.scope) {
				return;
			}
			var newValue = this.getValue(node.scope);
			newValue = getWatcherValue(this, newValue);
			if (this.value !== newValue) {
				this.value = newValue;
				(this.node || this.attribute).invalidate = true;
			}
		},
		getValue: function(scope, getFunction, getParams) {
			var node = this.node;
			if (!node && this.attribute) {
				node = this.attribute.node;
			}
			var context = {};
			if (node) {
				context[vars.element] = node.element;
				if (node.element) {
					context[vars.parentElement] = node.element.parentNode;
				}
			}
			context[vars.attribute] = this.attribute;
			context[vars.scope] = scope;
			return getValue(scope, this.pattern, this.path, this.params, getFunction, getParams, undefined, context);
		}
	};

	var templates = new HashMap('st');

	var Template = function(element) {
		this.watchers = new HashMap('stw');
		this.node = null;
		this.scope = null;
		this.compile(element);
	};
	Template.prototype = {
		toString: function() {
			return '[object Template]';
		},
		compile: function(element) {
			if (element) {
				this.element = element;
			}
			if (this.node) {
				this.node.dispose();
			}
			this.node = compile(this, this.element);
			this.node.root = true;
			this.scope = this.node.scope;
		},
		update: function(data) {
			if (isDefined(data)) {
				updateScopeWithData(this.node.scope, data);
			}
			if (this.node) {
				this.node.update();
			}
		},
		render: function(data) {
			this.update(data);
			if (this.node) {
				this.node.render();
			}
		},
		invalidate: function() {
			if (this.node) {
				this.node.invalidateData();
			}
		},
		watch: function(target, watcher) {
			if ( (!isString(target) && !isElement(target)) || !isFunction(watcher)) {
				return;
			}
			this.watchers.put(target, watcher);
		},
		unwatch: function(target) {
			this.watchers.remove(target);
		},
		clearWatchers: function() {
			this.watchers.dispose();
		},
		clearEvents: function() {
			this.node.clearEvents();
		},
		getNode: function(element) {
			return this.node.getNode(element);
		},
		dispose: function() {
			templates.remove(this.element);
			if (this.watchers) {
				this.watchers.dispose();
			}
			if (this.node) {
				this.node.dispose();
			}
			this.element = null;
			this.watchers = null;
			this.node = null;
		}
	};

	// written by Dean Edwards, 2005
	// with input from Tino Zijdel, Matthias Miller, Diego Perini
	// http://dean.edwards.name/weblog/2005/10/add-event/
	function addEvent(element, type, handler) {
		if (element.addEventListener) {
			element.addEventListener(type, handler, false);
		} else {
			// assign each event handler a unique ID
			if (!handler.$$guid) {
				handler.$$guid = addEvent.guid++;
			}
			// create a hash table of event types for the element
			if (!element.events) {
				element.events = {};
			}
			// create a hash table of event handlers for each element/event pair
			var handlers = element.events[type];
			if (!handlers) {
				handlers = element.events[type] = {};
				// store the existing event handler (if there is one)
				if (element['on' + type]) {
					handlers[0] = element['on' + type];
				}
			}
			// store the event handler in the hash table
			handlers[handler.$$guid] = handler;
			// assign a global event handler to do all the work
			element['on' + type] = function(event) {
				var returnValue = true;
				// grab the event object (IE uses a global event object)
				event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
				// get a reference to the hash table of event handlers
				var handlers = this.events[event.type];
				// execute each event handler
				for (var i in handlers) {
					if (handlers.hasOwnProperty(i)) {
						this.$$handleEvent = handlers[i];
						if (this.$$handleEvent(event) === false) {
							returnValue = false;
						}
					}
				}
				return returnValue;
			};
		}
	}
	// a counter used to create unique IDs
	addEvent.guid = 1;
	function removeEvent(element, type, handler) {
		if (element.removeEventListener) {
			element.removeEventListener(type, handler, false);
		} else {
			// delete the event handler from the hash table
			if (element.events && element.events[type]) {
				delete element.events[type][handler.$$guid];
			}
		}
	}
	function fixEvent(event) {
		// add W3C standard event methods
		event.preventDefault = fixEvent.preventDefault;
		event.stopPropagation = fixEvent.stopPropagation;
		return event;
	}
	fixEvent.preventDefault = function() {
		this.returnValue = false;
	};
	fixEvent.stopPropagation = function() {
		this.cancelBubble = true;
	};

	var maxDepth;
	var eventStore = [];

	function parseEvents(element, object, depth) {
		maxDepth = depth === undefined ? Number.MAX_VALUE : depth;
		parseNode(element, object, 0, true);
	}

	function parseNode(element, object, depth, isRoot) {
		if (!isElement(element)) {
			throw new Error('Error in soma.template.parseEvents, only a DOM Element can be parsed.');
		}
		if (isRoot) {
			parseAttributes(element, object);
		}
		if (maxDepth === 0) {
			return;
		}
		var child = element.firstChild;
		while (child) {
			if (child.nodeType === 1) {
				if (depth < maxDepth) {
					parseNode(child, object, ++depth);
					parseAttributes(child, object);
				}
			}
			child = child.nextSibling;
		}
	}

	function parseAttributes(element, object) {
		for (var attr, name, value, attrs = element.attributes, j = 0, jj = attrs && attrs.length; j < jj; j++) {
			attr = attrs[j];
			if (attr.specified) {
				name = attr.name;
				value = attr.value;
				if (events[name]) {
					var handler = getHandlerFromPattern(object, value);
					if (handler && isFunction(handler)) {
						addEvent(element, events[name], handler);
						eventStore.push({element:element, type:events[name], handler:handler});
					}
				}
			}
		}
	}

	function getHandlerFromPattern(object, pattern) {
		var parts = pattern.match(regex.func);
		if (parts) {
			var func = parts[1];
			if (isFunction(object[func])) {
				return object[func];
			}
		}
	}

	function clearEvents(element) {
		var i = eventStore.length, l = 0;
		while (--i >= l) {
			var item = eventStore[i];
			if (element === item.element || contains(element, item.element)) {
				removeEvent(item.element, item.type, item.handler);
				eventStore.splice(i, 1);
			}
		}
	}




	var ready;
	if (typeof document === 'object') {
		// https://github.com/ded/domready
		var ready=function(){function l(b){for(k=1;b=a.shift();)b()}var b,a=[],c=!1,d=document,e=d.documentElement,f=e.doScroll,g="DOMContentLoaded",h="addEventListener",i="onreadystatechange",j="readyState",k=/^loade|c/.test(d[j]);return d[h]&&d[h](g,b=function(){d.removeEventListener(g,b,c),l()},c),f&&d.attachEvent(i,b=function(){/^c/.test(d[j])&&(d.detachEvent(i,b),l())}),f?function(b){self!=top?k?b():a.push(b):function(){try{e.doScroll("left")}catch(a){return setTimeout(function(){ready(b)},50)}b()}()}:function(b){k?b():a.push(b)}}();
		if (settings.autocreate) {
			var parse = function(element) {
				var child = !element ? document.body : element.firstChild;
				while (child) {
					if (child.nodeType === 1) {
						parse(child);
						var attrValue = child.getAttribute(attributes.template);
						if (attrValue) {
							var getFunction = new Function('return ' + attrValue + ';');
							try {
								var f = getFunction();
								if (isFunction(f)) {
									soma.template.bootstrap(attrValue, child, f);
								}
							} catch(err){}
						}
					}
					child = child.nextSibling;
				}
			};
			ready(parse);
		}
	}
	function bootstrapTemplate(attrValue, element, func) {
		var tpl = createTemplate(element);
		func(tpl, tpl.scope, tpl.element, tpl.node);
	}

	function createTemplate(source, target) {
		var element;
		if (isString(source)) {
			// string template
			if (!isElement(target)) {
				throw new Error(soma.template.errors.TEMPLATE_STRING_NO_ELEMENT);
			}
			target.innerHTML = source;
			element = target;
		}
		else if (isElement(source)) {
			if (isElement(target)) {
				// element template with target
				target.innerHTML = source.innerHTML;
				element = target;
			}
			else {
				// element template
				element = source;
			}
		}
		else {
			throw new Error(soma.template.errors.TEMPLATE_NO_PARAM);
		}
		// existing template
		if (getTemplate(element)) {
			getTemplate(element).dispose();
			templates.remove(element);
		}
		// create template
		var template = new Template(element);
		templates.put(element, template);
		return template;
	}

	function getTemplate(element) {
		return templates.get(element);
	}

	function renderAllTemplates() {
		var data = templates.getData();
		for (var key in templates.getData()) {
			if (data.hasOwnProperty(key)) {
				templates.get(key).render();
			}
		}
	}

	function appendHelpers(obj) {
		if (obj === null) {
			helpersObject = {};
			helpersScopeObject = {};
		}
		if (isDefined(obj) && isObject(obj)) {
			for (var key in obj) {
				if (obj.hasOwnProperty(key)) {
					helpersObject[key] = helpersScopeObject[key] = obj[key];
				}
			}
		}
		return helpersObject;
	}

	// set regex
	tokens.start(tokenStart);
	tokens.end(tokenEnd);

	// plugins

	soma.plugins = soma.plugins || {};

	var TemplatePlugin = function(instance, injector) {
		instance.constructor.prototype.createTemplate = function(cl, domElement) {
			if (!cl || typeof cl !== 'function') {
				throw new Error('Error creating a template, the first parameter must be a function.');
			}
			if (domElement && isElement(domElement)) {
				var template = soma.template.create(domElement);
				for (var key in template) {
					if (typeof template[key] === 'function') {
						cl.prototype[key] = template[key].bind(template);
					}
				}
				cl.prototype.render = template.render.bind(template);
				var childInjector = injector.createChild();
				childInjector.mapValue('template', template);
				childInjector.mapValue('scope', template.scope);
				childInjector.mapValue('element', template.element);
				return childInjector.createInstance(cl);
			}
			return null;
		};
		soma.template.bootstrap = function(attrValue, element, func) {
			instance.createTemplate(func, element);
		};
	};
	if (soma.plugins && soma.plugins.add) {
		soma.plugins.add(TemplatePlugin);
	}

	soma.template.Plugin = TemplatePlugin;

	// exports
	soma.template.create = createTemplate;
	soma.template.get = getTemplate;
	soma.template.renderAll = renderAllTemplates;
	soma.template.helpers = appendHelpers;
	soma.template.bootstrap = bootstrapTemplate;
	soma.template.addEvent = addEvent;
	soma.template.removeEvent = removeEvent;
	soma.template.parseEvents = parseEvents;
	soma.template.clearEvents = clearEvents;
	soma.template.ready = ready;

	// register for AMD module
	if (typeof define === 'function' && typeof define.amd !== 'undefined') {
		define('soma-template', soma.template);
	}

	// export for node.js
	if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
		module.exports = soma.template;
	}
	if (typeof exports !== 'undefined') {
		exports = soma.template;
	}

})(this['soma'] = this['soma'] || {});