diff --git a/Examples/Content/HelloRactive.umap b/Examples/Content/HelloRactive.umap
new file mode 100644
index 0000000..9328b13
--- /dev/null
+++ b/Examples/Content/HelloRactive.umap
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6f865a06d454c41b762cbc9b97d7c6169ed47e52a0d328d3379c845d1204a093
+size 632800
diff --git a/Examples/Content/Scripts/helloRactive.js b/Examples/Content/Scripts/helloRactive.js
new file mode 100644
index 0000000..c92d770
--- /dev/null
+++ b/Examples/Content/Scripts/helloRactive.js
@@ -0,0 +1,186 @@
+/// <reference path="typings/ue.d.ts">/>
+// ; typing info for auto-completion in Visual Studio Code
+let UMG = require('UMG')
+let instantiator = require('instantiator')
+let _ = require('lodash')
+
+function GetPC() {
+    return PlayerController.C(GWorld.GetAllActorsOfClass(PlayerController).OutActors[0])
+}
+
+(function (global) {
+    "use strict"
+
+    function dyn_wrap(ractive,prop,bindings,design) {            
+        let elem
+        let observe
+        function update(value) {
+            let bag = {}
+            _.each(bindings,(v,k) => {
+                bag[k] = _.get(v(value), prop)
+            })
+            elem.set_attrs(bag)
+        }
+        function listener(cur, prev, keyPath) {
+            update(ractive.get())
+        }
+        function link() {
+            observe = ractive.observe(prop, listener)                
+        }
+        function unlink() {
+            observe.cancel()
+        }           
+        
+        let old = design.$link
+        design.$link = _elem => {
+            elem = _elem
+            old && old()        
+            update(ractive)
+            link()
+        }
+        let old_unlink = design.$unlink
+        design.$unlink = _ => {
+            old_unlink && old_unlink()
+            unlink()
+        }
+        return design
+    }
+    
+    function dyn_object(ractive, prop, fn) {
+        let elem
+        let observe
+        let old = _.clone(ractive.get(prop))
+
+        function update(data) {
+            elem.remove_children()
+            let widget = fn(data)
+            if(widget) {
+                elem.add_child(widget)            
+            }
+        }
+
+        function listener(cur, prev, keyPath) {
+            if(ractive.get(prop) != old) {
+                update(ractive.get(prop))                
+            }
+            old = _.clone(ractive.get(prop))
+        }                           
+        function link(_elem) {
+            elem = _elem   
+            update(ractive.get(prop))
+            observe = ractive.observe(prop, listener)                      
+        }
+        function unlink() {
+            observe.cancel()
+        }
+        return UMG.div({$link:link,$unlink:unlink})
+        
+    }
+    
+    function dyn_array(ractive,array,prop,fn) {
+        let elem
+        let observe
+        let map = new Map()
+
+        let old = [...ractive.get(array)]
+        function add(added) {
+            added.forEach(x => {
+                let data = _.get(x, prop)
+                map.set(data,elem.add_child(fn(data)))
+            })
+        }
+        function remove(removed) {
+            removed.forEach(x => {
+                let data = _.get(x, prop)
+                elem.remove_child(map.get(data))
+                map.delete(x)
+            })
+        }
+        function listener(cur, prev, keyPath) {
+            let added = _.difference(ractive.get(array),old)
+            let removed = _.difference(old,ractive.get(array))
+            old = [...ractive.get(array)]
+            add(added)
+            remove(removed)
+        }                           
+        function link(_elem) {
+            elem = _elem                
+            add(ractive.get(array))
+            observe = ractive.observe(array, listener)                      
+        }
+        function unlink() {
+            observe.cancel()
+        }
+        return UMG.div({$link:link,$unlink:unlink})
+    }
+
+
+    function main() {
+        let content = {hello: 'hello'}
+        let people =  [
+            {name: 'Rich Harris'},
+            {name: 'Marty Nelson'}
+        ]
+        let obj = { a: { b: { c: 1 } } };
+
+        let ractive = new Ractive({
+            data : {
+                content: content,
+                people: people,
+                obj : obj
+            }})
+            
+        let PC = GetPC()
+
+        let design = UMG.div({},
+            dyn_object(ractive, 'content.hello', x => {
+                return UMG.text({}, `${x}`)
+            }),
+            dyn_array(ractive, 'people', 'name', x => {
+                return UMG.text({}, `${x}`)
+            }, UMG.span({})),
+            dyn_wrap(ractive, 'obj.a.b.c', {Text:x => x}, UMG.text({}, ""))
+        )
+        let i = 0
+        let j = 100
+        
+        function tick() {
+            people.push({name: `h ${i++}`})
+            people.splice(0, 1)
+            obj.a.b.c = `${j--}`
+            content.hello += `${i}`
+            ractive.update('content.hello')
+            ractive.update('obj.a.b.c')
+            setTimeout(__ => tick(), 1000)
+        }
+        setTimeout(__ => tick(), 1000)
+
+        let page = instantiator(design)
+
+        let widget = GWorld.CreateWidget(JavascriptWidget, PC)
+        
+        widget.SetRootWidget(page)
+        widget.AddToViewport()
+
+        // clean up the mess
+        return function () {
+            widget.RemoveFromViewport()
+        }
+    }
+
+    // bootstrap to initiate live-reloading dev env.
+    try {
+        module.exports = () => {
+            let cleanup = null
+
+            // wait for map to be loaded.
+            process.nextTick(() => cleanup = main());
+
+            // live-reloadable function should return its cleanup function
+            return () => cleanup()
+        }
+    }
+    catch (e) {
+        require('bootstrap')('helloRactive')
+    }
+})(this)
diff --git a/Examples/Content/Scripts/ractive.js b/Examples/Content/Scripts/ractive.js
new file mode 100644
index 0000000..7eb5778
--- /dev/null
+++ b/Examples/Content/Scripts/ractive.js
@@ -0,0 +1,16619 @@
+/*
+active.js v0.7.3
+	Sat Apr 25 2015 13:52:38 GMT-0400 (EDT) - commit da40f81c660ba2f09c45a09a9c20fdd34ee36d80
+
+	http://ractivejs.org
+	http://twitter.com/RactiveJS
+
+	Released under the MIT License.
+*/
+
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+  typeof define === 'function' && define.amd ? define(factory) :
+  global.Ractive = factory()
+}(this, function () { 'use strict';
+
+  var TEMPLATE_VERSION = 3;
+
+  var defaultOptions = {
+
+  	// render placement:
+  	el: void 0,
+  	append: false,
+
+  	// template:
+  	template: { v: TEMPLATE_VERSION, t: [] },
+
+  	// parse:     // TODO static delimiters?
+  	preserveWhitespace: false,
+  	sanitize: false,
+  	stripComments: true,
+  	delimiters: ["{{", "}}"],
+  	tripleDelimiters: ["{{{", "}}}"],
+  	interpolate: false,
+
+  	// data & binding:
+  	data: {},
+  	computed: {},
+  	magic: false,
+  	modifyArrays: true,
+  	adapt: [],
+  	isolated: false,
+  	twoway: true,
+  	lazy: false,
+
+  	// transitions:
+  	noIntro: false,
+  	transitionsEnabled: true,
+  	complete: void 0,
+
+  	// css:
+  	css: null,
+  	noCssTransform: false
+  };
+
+  var config_defaults = defaultOptions;
+
+  // These are a subset of the easing equations found at
+  // https://raw.github.com/danro/easing-js - license info
+  // follows:
+
+  // --------------------------------------------------
+  // easing.js v0.5.4
+  // Generic set of easing functions with AMD support
+  // https://github.com/danro/easing-js
+  // This code may be freely distributed under the MIT license
+  // http://danro.mit-license.org/
+  // --------------------------------------------------
+  // All functions adapted from Thomas Fuchs & Jeremy Kahn
+  // Easing Equations (c) 2003 Robert Penner, BSD license
+  // https://raw.github.com/danro/easing-js/master/LICENSE
+  // --------------------------------------------------
+
+  // In that library, the functions named easeIn, easeOut, and
+  // easeInOut below are named easeInCubic, easeOutCubic, and
+  // (you guessed it) easeInOutCubic.
+  //
+  // You can add additional easing functions to this list, and they
+  // will be globally available.
+
+  var static_easing = {
+  	linear: function (pos) {
+  		return pos;
+  	},
+  	easeIn: function (pos) {
+  		return Math.pow(pos, 3);
+  	},
+  	easeOut: function (pos) {
+  		return Math.pow(pos - 1, 3) + 1;
+  	},
+  	easeInOut: function (pos) {
+  		if ((pos /= 0.5) < 1) {
+  			return 0.5 * Math.pow(pos, 3);
+  		}
+  		return 0.5 * (Math.pow(pos - 2, 3) + 2);
+  	}
+  };
+
+  /*global console, navigator */
+  var isClient, isJsdom, hasConsole, environment__magic, namespaces, svg, vendors;
+
+  isClient = typeof document === "object";
+
+  isJsdom = typeof navigator !== "undefined" && /jsDom/.test(navigator.appName);
+
+  hasConsole = typeof console !== "undefined" && typeof console.warn === "function" && typeof console.warn.apply === "function";
+
+  try {
+  	Object.defineProperty({}, "test", { value: 0 });
+  	environment__magic = true;
+  } catch (e) {
+  	environment__magic = false;
+  }
+
+  namespaces = {
+  	html: "http://www.w3.org/1999/xhtml",
+  	mathml: "http://www.w3.org/1998/Math/MathML",
+  	svg: "http://www.w3.org/2000/svg",
+  	xlink: "http://www.w3.org/1999/xlink",
+  	xml: "http://www.w3.org/XML/1998/namespace",
+  	xmlns: "http://www.w3.org/2000/xmlns/"
+  };
+
+  if (typeof document === "undefined") {
+  	svg = false;
+  } else {
+  	svg = document && document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1");
+  }
+
+  vendors = ["o", "ms", "moz", "webkit"];
+
+  var createElement, matches, dom__div, methodNames, unprefixed, prefixed, dom__i, j, makeFunction;
+
+  // Test for SVG support
+  if (!svg) {
+  	createElement = function (type, ns) {
+  		if (ns && ns !== namespaces.html) {
+  			throw "This browser does not support namespaces other than http://www.w3.org/1999/xhtml. The most likely cause of this error is that you're trying to render SVG in an older browser. See http://docs.ractivejs.org/latest/svg-and-older-browsers for more information";
+  		}
+
+  		return document.createElement(type);
+  	};
+  } else {
+  	createElement = function (type, ns) {
+  		if (!ns || ns === namespaces.html) {
+  			return document.createElement(type);
+  		}
+
+  		return document.createElementNS(ns, type);
+  	};
+  }
+
+  function getElement(input) {
+  	var output;
+
+  	if (!input || typeof input === "boolean") {
+  		return;
+  	}
+
+  	if (typeof window === "undefined" || !document || !input) {
+  		return null;
+  	}
+
+  	// We already have a DOM node - no work to do. (Duck typing alert!)
+  	if (input.nodeType) {
+  		return input;
+  	}
+
+  	// Get node from string
+  	if (typeof input === "string") {
+  		// try ID first
+  		output = document.getElementById(input);
+
+  		// then as selector, if possible
+  		if (!output && document.querySelector) {
+  			output = document.querySelector(input);
+  		}
+
+  		// did it work?
+  		if (output && output.nodeType) {
+  			return output;
+  		}
+  	}
+
+  	// If we've been given a collection (jQuery, Zepto etc), extract the first item
+  	if (input[0] && input[0].nodeType) {
+  		return input[0];
+  	}
+
+  	return null;
+  }
+
+  if (!isClient) {
+  	matches = null;
+  } else {
+  	dom__div = createElement("div");
+  	methodNames = ["matches", "matchesSelector"];
+
+  	makeFunction = function (methodName) {
+  		return function (node, selector) {
+  			return node[methodName](selector);
+  		};
+  	};
+
+  	dom__i = methodNames.length;
+
+  	while (dom__i-- && !matches) {
+  		unprefixed = methodNames[dom__i];
+
+  		if (dom__div[unprefixed]) {
+  			matches = makeFunction(unprefixed);
+  		} else {
+  			j = vendors.length;
+  			while (j--) {
+  				prefixed = vendors[dom__i] + unprefixed.substr(0, 1).toUpperCase() + unprefixed.substring(1);
+
+  				if (dom__div[prefixed]) {
+  					matches = makeFunction(prefixed);
+  					break;
+  				}
+  			}
+  		}
+  	}
+
+  	// IE8...
+  	if (!matches) {
+  		matches = function (node, selector) {
+  			var nodes, parentNode, i;
+
+  			parentNode = node.parentNode;
+
+  			if (!parentNode) {
+  				// empty dummy <div>
+  				dom__div.innerHTML = "";
+
+  				parentNode = dom__div;
+  				node = node.cloneNode();
+
+  				dom__div.appendChild(node);
+  			}
+
+  			nodes = parentNode.querySelectorAll(selector);
+
+  			i = nodes.length;
+  			while (i--) {
+  				if (nodes[i] === node) {
+  					return true;
+  				}
+  			}
+
+  			return false;
+  		};
+  	}
+  }
+
+  function detachNode(node) {
+  	if (node && typeof node.parentNode !== "unknown" && node.parentNode) {
+  		node.parentNode.removeChild(node);
+  	}
+
+  	return node;
+  }
+
+  function safeToStringValue(value) {
+  	return value == null || !value.toString ? "" : value;
+  }
+
+  var legacy = null;
+
+  var create, defineProperty, defineProperties;
+
+  try {
+  	Object.defineProperty({}, "test", { value: 0 });
+
+  	if (isClient) {
+  		Object.defineProperty(document.createElement("div"), "test", { value: 0 });
+  	}
+
+  	defineProperty = Object.defineProperty;
+  } catch (err) {
+  	// Object.defineProperty doesn't exist, or we're in IE8 where you can
+  	// only use it with DOM objects (what were you smoking, MSFT?)
+  	defineProperty = function (obj, prop, desc) {
+  		obj[prop] = desc.value;
+  	};
+  }
+
+  try {
+  	try {
+  		Object.defineProperties({}, { test: { value: 0 } });
+  	} catch (err) {
+  		// TODO how do we account for this? noMagic = true;
+  		throw err;
+  	}
+
+  	if (isClient) {
+  		Object.defineProperties(createElement("div"), { test: { value: 0 } });
+  	}
+
+  	defineProperties = Object.defineProperties;
+  } catch (err) {
+  	defineProperties = function (obj, props) {
+  		var prop;
+
+  		for (prop in props) {
+  			if (props.hasOwnProperty(prop)) {
+  				defineProperty(obj, prop, props[prop]);
+  			}
+  		}
+  	};
+  }
+
+  try {
+  	Object.create(null);
+
+  	create = Object.create;
+  } catch (err) {
+  	// sigh
+  	create = (function () {
+  		var F = function () {};
+
+  		return function (proto, props) {
+  			var obj;
+
+  			if (proto === null) {
+  				return {};
+  			}
+
+  			F.prototype = proto;
+  			obj = new F();
+
+  			if (props) {
+  				Object.defineProperties(obj, props);
+  			}
+
+  			return obj;
+  		};
+  	})();
+  }
+
+  function utils_object__extend(target) {
+  	for (var _len = arguments.length, sources = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+  		sources[_key - 1] = arguments[_key];
+  	}
+
+  	var prop, source;
+
+  	while (source = sources.shift()) {
+  		for (prop in source) {
+  			if (hasOwn.call(source, prop)) {
+  				target[prop] = source[prop];
+  			}
+  		}
+  	}
+
+  	return target;
+  }
+
+  function fillGaps(target) {
+  	for (var _len = arguments.length, sources = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+  		sources[_key - 1] = arguments[_key];
+  	}
+
+  	sources.forEach(function (s) {
+  		for (var key in s) {
+  			if (s.hasOwnProperty(key) && !(key in target)) {
+  				target[key] = s[key];
+  			}
+  		}
+  	});
+
+  	return target;
+  }
+
+  var hasOwn = Object.prototype.hasOwnProperty;
+
+  // thanks, http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/
+  var is__toString = Object.prototype.toString,
+      arrayLikePattern = /^\[object (?:Array|FileList)\]$/;
+  function isArray(thing) {
+  	return is__toString.call(thing) === "[object Array]";
+  }
+
+  function isArrayLike(obj) {
+  	return arrayLikePattern.test(is__toString.call(obj));
+  }
+
+  function isEqual(a, b) {
+  	if (a === null && b === null) {
+  		return true;
+  	}
+
+  	if (typeof a === "object" || typeof b === "object") {
+  		return false;
+  	}
+
+  	return a === b;
+  }
+
+  function is__isNumeric(thing) {
+  	return !isNaN(parseFloat(thing)) && isFinite(thing);
+  }
+
+  function isObject(thing) {
+  	return thing && is__toString.call(thing) === "[object Object]";
+  }
+
+  var noop = function () {};
+
+  /* global console */
+  var alreadyWarned = {},
+      log,
+      printWarning,
+      welcome;
+
+  if (hasConsole) {
+  	(function () {
+  		var welcomeIntro = ["%cRactive.js %c0.7.3 %cin debug mode, %cmore...", "color: rgb(114, 157, 52); font-weight: normal;", "color: rgb(85, 85, 85); font-weight: normal;", "color: rgb(85, 85, 85); font-weight: normal;", "color: rgb(82, 140, 224); font-weight: normal; text-decoration: underline;"];
+  		var welcomeMessage = "You're running Ractive 0.7.3 in debug mode - messages will be printed to the console to help you fix problems and optimise your application.\n\nTo disable debug mode, add this line at the start of your app:\n  Ractive.DEBUG = false;\n\nTo disable debug mode when your app is minified, add this snippet:\n  Ractive.DEBUG = /unminified/.test(function(){/*unminified*/});\n\nGet help and support:\n  http://docs.ractivejs.org\n  http://stackoverflow.com/questions/tagged/ractivejs\n  http://groups.google.com/forum/#!forum/ractive-js\n  http://twitter.com/ractivejs\n\nFound a bug? Raise an issue:\n  https://github.com/ractivejs/ractive/issues\n\n";
+
+  		welcome = function () {
+  			var hasGroup = !!console.groupCollapsed;
+  			console[hasGroup ? "groupCollapsed" : "log"].apply(console, welcomeIntro);
+  			console.log(welcomeMessage);
+  			if (hasGroup) {
+  				console.groupEnd(welcomeIntro);
+  			}
+
+  			welcome = noop;
+  		};
+
+  		printWarning = function (message, args) {
+  			welcome();
+
+  			// extract information about the instance this message pertains to, if applicable
+  			if (typeof args[args.length - 1] === "object") {
+  				var options = args.pop();
+  				var ractive = options ? options.ractive : null;
+
+  				if (ractive) {
+  					// if this is an instance of a component that we know the name of, add
+  					// it to the message
+  					var _name = undefined;
+  					if (ractive.component && (_name = ractive.component.name)) {
+  						message = "<" + _name + "> " + message;
+  					}
+
+  					var node = undefined;
+  					if (node = options.node || ractive.fragment && ractive.fragment.rendered && ractive.find("*")) {
+  						args.push(node);
+  					}
+  				}
+  			}
+
+  			console.warn.apply(console, ["%cRactive.js: %c" + message, "color: rgb(114, 157, 52);", "color: rgb(85, 85, 85);"].concat(args));
+  		};
+
+  		log = function () {
+  			console.log.apply(console, arguments);
+  		};
+  	})();
+  } else {
+  	printWarning = log = welcome = noop;
+  }
+
+  function format(message, args) {
+  	return message.replace(/%s/g, function () {
+  		return args.shift();
+  	});
+  }
+
+  function fatal(message) {
+  	for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+  		args[_key - 1] = arguments[_key];
+  	}
+
+  	message = format(message, args);
+  	throw new Error(message);
+  }
+
+  function logIfDebug() {
+  	if (_Ractive.DEBUG) {
+  		log.apply(null, arguments);
+  	}
+  }
+
+  function warn(message) {
+  	for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+  		args[_key - 1] = arguments[_key];
+  	}
+
+  	message = format(message, args);
+  	printWarning(message, args);
+  }
+
+  function warnOnce(message) {
+  	for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+  		args[_key - 1] = arguments[_key];
+  	}
+
+  	message = format(message, args);
+
+  	if (alreadyWarned[message]) {
+  		return;
+  	}
+
+  	alreadyWarned[message] = true;
+  	printWarning(message, args);
+  }
+
+  function warnIfDebug() {
+  	if (_Ractive.DEBUG) {
+  		warn.apply(null, arguments);
+  	}
+  }
+
+  function warnOnceIfDebug() {
+  	if (_Ractive.DEBUG) {
+  		warnOnce.apply(null, arguments);
+  	}
+  }
+
+  // Error messages that are used (or could be) in multiple places
+  var badArguments = "Bad arguments";
+  var noRegistryFunctionReturn = "A function was specified for \"%s\" %s, but no %s was returned";
+  var missingPlugin = function (name, type) {
+    return "Missing \"" + name + "\" " + type + " plugin. You may need to download a plugin via http://docs.ractivejs.org/latest/plugins#" + type + "s";
+  };
+
+  function findInViewHierarchy(registryName, ractive, name) {
+  	var instance = findInstance(registryName, ractive, name);
+  	return instance ? instance[registryName][name] : null;
+  }
+
+  function findInstance(registryName, ractive, name) {
+  	while (ractive) {
+  		if (name in ractive[registryName]) {
+  			return ractive;
+  		}
+
+  		if (ractive.isolated) {
+  			return null;
+  		}
+
+  		ractive = ractive.parent;
+  	}
+  }
+
+  var interpolate = function (from, to, ractive, type) {
+  	if (from === to) {
+  		return snap(to);
+  	}
+
+  	if (type) {
+
+  		var interpol = findInViewHierarchy("interpolators", ractive, type);
+  		if (interpol) {
+  			return interpol(from, to) || snap(to);
+  		}
+
+  		fatal(missingPlugin(type, "interpolator"));
+  	}
+
+  	return static_interpolators.number(from, to) || static_interpolators.array(from, to) || static_interpolators.object(from, to) || snap(to);
+  };
+
+  var shared_interpolate = interpolate;
+
+  function snap(to) {
+  	return function () {
+  		return to;
+  	};
+  }
+
+  var interpolators = {
+  	number: function (from, to) {
+  		var delta;
+
+  		if (!is__isNumeric(from) || !is__isNumeric(to)) {
+  			return null;
+  		}
+
+  		from = +from;
+  		to = +to;
+
+  		delta = to - from;
+
+  		if (!delta) {
+  			return function () {
+  				return from;
+  			};
+  		}
+
+  		return function (t) {
+  			return from + t * delta;
+  		};
+  	},
+
+  	array: function (from, to) {
+  		var intermediate, interpolators, len, i;
+
+  		if (!isArray(from) || !isArray(to)) {
+  			return null;
+  		}
+
+  		intermediate = [];
+  		interpolators = [];
+
+  		i = len = Math.min(from.length, to.length);
+  		while (i--) {
+  			interpolators[i] = shared_interpolate(from[i], to[i]);
+  		}
+
+  		// surplus values - don't interpolate, but don't exclude them either
+  		for (i = len; i < from.length; i += 1) {
+  			intermediate[i] = from[i];
+  		}
+
+  		for (i = len; i < to.length; i += 1) {
+  			intermediate[i] = to[i];
+  		}
+
+  		return function (t) {
+  			var i = len;
+
+  			while (i--) {
+  				intermediate[i] = interpolators[i](t);
+  			}
+
+  			return intermediate;
+  		};
+  	},
+
+  	object: function (from, to) {
+  		var properties, len, interpolators, intermediate, prop;
+
+  		if (!isObject(from) || !isObject(to)) {
+  			return null;
+  		}
+
+  		properties = [];
+  		intermediate = {};
+  		interpolators = {};
+
+  		for (prop in from) {
+  			if (hasOwn.call(from, prop)) {
+  				if (hasOwn.call(to, prop)) {
+  					properties.push(prop);
+  					interpolators[prop] = shared_interpolate(from[prop], to[prop]);
+  				} else {
+  					intermediate[prop] = from[prop];
+  				}
+  			}
+  		}
+
+  		for (prop in to) {
+  			if (hasOwn.call(to, prop) && !hasOwn.call(from, prop)) {
+  				intermediate[prop] = to[prop];
+  			}
+  		}
+
+  		len = properties.length;
+
+  		return function (t) {
+  			var i = len,
+  			    prop;
+
+  			while (i--) {
+  				prop = properties[i];
+
+  				intermediate[prop] = interpolators[prop](t);
+  			}
+
+  			return intermediate;
+  		};
+  	}
+  };
+
+  var static_interpolators = interpolators;
+
+  // This function takes a keypath such as 'foo.bar.baz', and returns
+  // all the variants of that keypath that include a wildcard in place
+  // of a key, such as 'foo.bar.*', 'foo.*.baz', 'foo.*.*' and so on.
+  // These are then checked against the dependants map (ractive.viewmodel.depsMap)
+  // to see if any pattern observers are downstream of one or more of
+  // these wildcard keypaths (e.g. 'foo.bar.*.status')
+  var utils_getPotentialWildcardMatches = getPotentialWildcardMatches;
+
+  var starMaps = {};
+  function getPotentialWildcardMatches(keypath) {
+  	var keys, starMap, mapper, i, result, wildcardKeypath;
+
+  	keys = keypath.split(".");
+  	if (!(starMap = starMaps[keys.length])) {
+  		starMap = getStarMap(keys.length);
+  	}
+
+  	result = [];
+
+  	mapper = function (star, i) {
+  		return star ? "*" : keys[i];
+  	};
+
+  	i = starMap.length;
+  	while (i--) {
+  		wildcardKeypath = starMap[i].map(mapper).join(".");
+
+  		if (!result.hasOwnProperty(wildcardKeypath)) {
+  			result.push(wildcardKeypath);
+  			result[wildcardKeypath] = true;
+  		}
+  	}
+
+  	return result;
+  }
+
+  // This function returns all the possible true/false combinations for
+  // a given number - e.g. for two, the possible combinations are
+  // [ true, true ], [ true, false ], [ false, true ], [ false, false ].
+  // It does so by getting all the binary values between 0 and e.g. 11
+  function getStarMap(num) {
+  	var ones = "",
+  	    max,
+  	    binary,
+  	    starMap,
+  	    mapper,
+  	    i,
+  	    j,
+  	    l,
+  	    map;
+
+  	if (!starMaps[num]) {
+  		starMap = [];
+
+  		while (ones.length < num) {
+  			ones += 1;
+  		}
+
+  		max = parseInt(ones, 2);
+
+  		mapper = function (digit) {
+  			return digit === "1";
+  		};
+
+  		for (i = 0; i <= max; i += 1) {
+  			binary = i.toString(2);
+  			while (binary.length < num) {
+  				binary = "0" + binary;
+  			}
+
+  			map = [];
+  			l = binary.length;
+  			for (j = 0; j < l; j++) {
+  				map.push(mapper(binary[j]));
+  			}
+  			starMap[i] = map;
+  		}
+
+  		starMaps[num] = starMap;
+  	}
+
+  	return starMaps[num];
+  }
+
+  var refPattern = /\[\s*(\*|[0-9]|[1-9][0-9]+)\s*\]/g;
+  var patternPattern = /\*/;
+  var keypathCache = {};
+
+  var Keypath = function (str) {
+  	var keys = str.split(".");
+
+  	this.str = str;
+
+  	if (str[0] === "@") {
+  		this.isSpecial = true;
+  		this.value = decodeKeypath(str);
+  	}
+
+  	this.firstKey = keys[0];
+  	this.lastKey = keys.pop();
+
+  	this.isPattern = patternPattern.test(str);
+
+  	this.parent = str === "" ? null : getKeypath(keys.join("."));
+  	this.isRoot = !str;
+  };
+
+  Keypath.prototype = {
+  	equalsOrStartsWith: function (keypath) {
+  		return keypath === this || this.startsWith(keypath);
+  	},
+
+  	join: function (str) {
+  		return getKeypath(this.isRoot ? String(str) : this.str + "." + str);
+  	},
+
+  	replace: function (oldKeypath, newKeypath) {
+  		if (this === oldKeypath) {
+  			return newKeypath;
+  		}
+
+  		if (this.startsWith(oldKeypath)) {
+  			return newKeypath === null ? newKeypath : getKeypath(this.str.replace(oldKeypath.str + ".", newKeypath.str + "."));
+  		}
+  	},
+
+  	startsWith: function (keypath) {
+  		if (!keypath) {
+  			// TODO under what circumstances does this happen?
+  			return false;
+  		}
+
+  		return keypath && this.str.substr(0, keypath.str.length + 1) === keypath.str + ".";
+  	},
+
+  	toString: function () {
+  		throw new Error("Bad coercion");
+  	},
+
+  	valueOf: function () {
+  		throw new Error("Bad coercion");
+  	},
+
+  	wildcardMatches: function () {
+  		return this._wildcardMatches || (this._wildcardMatches = utils_getPotentialWildcardMatches(this.str));
+  	}
+  };
+  function assignNewKeypath(target, property, oldKeypath, newKeypath) {
+  	var existingKeypath = target[property];
+
+  	if (existingKeypath && (existingKeypath.equalsOrStartsWith(newKeypath) || !existingKeypath.equalsOrStartsWith(oldKeypath))) {
+  		return;
+  	}
+
+  	target[property] = existingKeypath ? existingKeypath.replace(oldKeypath, newKeypath) : newKeypath;
+  	return true;
+  }
+
+  function decodeKeypath(keypath) {
+  	var value = keypath.slice(2);
+
+  	if (keypath[1] === "i") {
+  		return is__isNumeric(value) ? +value : value;
+  	} else {
+  		return value;
+  	}
+  }
+
+  function getKeypath(str) {
+  	if (str == null) {
+  		return str;
+  	}
+
+  	// TODO it *may* be worth having two versions of this function - one where
+  	// keypathCache inherits from null, and one for IE8. Depends on how
+  	// much of an overhead hasOwnProperty is - probably negligible
+  	if (!keypathCache.hasOwnProperty(str)) {
+  		keypathCache[str] = new Keypath(str);
+  	}
+
+  	return keypathCache[str];
+  }
+
+  function getMatchingKeypaths(ractive, keypath) {
+  	var keys, key, matchingKeypaths;
+
+  	keys = keypath.str.split(".");
+  	matchingKeypaths = [rootKeypath];
+
+  	while (key = keys.shift()) {
+  		if (key === "*") {
+  			// expand to find all valid child keypaths
+  			matchingKeypaths = matchingKeypaths.reduce(expand, []);
+  		} else {
+  			if (matchingKeypaths[0] === rootKeypath) {
+  				// first key
+  				matchingKeypaths[0] = getKeypath(key);
+  			} else {
+  				matchingKeypaths = matchingKeypaths.map(concatenate(key));
+  			}
+  		}
+  	}
+
+  	return matchingKeypaths;
+
+  	function expand(matchingKeypaths, keypath) {
+  		var wrapper, value, keys;
+
+  		if (keypath.isRoot) {
+  			keys = [].concat(Object.keys(ractive.viewmodel.data), Object.keys(ractive.viewmodel.mappings), Object.keys(ractive.viewmodel.computations));
+  		} else {
+  			wrapper = ractive.viewmodel.wrapped[keypath.str];
+  			value = wrapper ? wrapper.get() : ractive.viewmodel.get(keypath);
+
+  			keys = value ? Object.keys(value) : null;
+  		}
+
+  		if (keys) {
+  			keys.forEach(function (key) {
+  				if (key !== "_ractive" || !isArray(value)) {
+  					matchingKeypaths.push(keypath.join(key));
+  				}
+  			});
+  		}
+
+  		return matchingKeypaths;
+  	}
+  }
+
+  function concatenate(key) {
+  	return function (keypath) {
+  		return keypath.join(key);
+  	};
+  }
+  function normalise(ref) {
+  	return ref ? ref.replace(refPattern, ".$1") : "";
+  }
+
+  var rootKeypath = getKeypath("");
+
+  var shared_add = add;
+  var shared_add__errorMessage = "Cannot add to a non-numeric value";
+  function add(root, keypath, d) {
+  	if (typeof keypath !== "string" || !is__isNumeric(d)) {
+  		throw new Error("Bad arguments");
+  	}
+
+  	var value = undefined,
+  	    changes = undefined;
+
+  	if (/\*/.test(keypath)) {
+  		changes = {};
+
+  		getMatchingKeypaths(root, getKeypath(normalise(keypath))).forEach(function (keypath) {
+  			var value = root.viewmodel.get(keypath);
+
+  			if (!is__isNumeric(value)) {
+  				throw new Error(shared_add__errorMessage);
+  			}
+
+  			changes[keypath.str] = value + d;
+  		});
+
+  		return root.set(changes);
+  	}
+
+  	value = root.get(keypath);
+
+  	if (!is__isNumeric(value)) {
+  		throw new Error(shared_add__errorMessage);
+  	}
+
+  	return root.set(keypath, +value + d);
+  }
+
+  var prototype_add = Ractive$add;
+  function Ractive$add(keypath, d) {
+  	return shared_add(this, keypath, d === undefined ? 1 : +d);
+  }
+
+  var requestAnimationFrame;
+
+  // If window doesn't exist, we don't need requestAnimationFrame
+  if (typeof window === "undefined") {
+  	requestAnimationFrame = null;
+  } else {
+  	// https://gist.github.com/paulirish/1579671
+  	(function (vendors, lastTime, window) {
+
+  		var x, setTimeout;
+
+  		if (window.requestAnimationFrame) {
+  			return;
+  		}
+
+  		for (x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+  			window.requestAnimationFrame = window[vendors[x] + "RequestAnimationFrame"];
+  		}
+
+  		if (!window.requestAnimationFrame) {
+  			setTimeout = window.setTimeout;
+
+  			window.requestAnimationFrame = function (callback) {
+  				var currTime, timeToCall, id;
+
+  				currTime = Date.now();
+  				timeToCall = Math.max(0, 16 - (currTime - lastTime));
+  				id = setTimeout(function () {
+  					callback(currTime + timeToCall);
+  				}, timeToCall);
+
+  				lastTime = currTime + timeToCall;
+  				return id;
+  			};
+  		}
+  	})(vendors, 0, window);
+
+  	requestAnimationFrame = window.requestAnimationFrame;
+  }
+
+  var rAF = requestAnimationFrame;
+
+  var getTime;
+
+  if (typeof window !== "undefined" && window.performance && typeof window.performance.now === "function") {
+  	getTime = function () {
+  		return window.performance.now();
+  	};
+  } else {
+  	getTime = function () {
+  		return Date.now();
+  	};
+  }
+
+  var utils_getTime = getTime;
+
+  var deprecations = {
+  	construct: {
+  		deprecated: "beforeInit",
+  		replacement: "onconstruct"
+  	},
+  	render: {
+  		deprecated: "init",
+  		message: "The \"init\" method has been deprecated " + "and will likely be removed in a future release. " + "You can either use the \"oninit\" method which will fire " + "only once prior to, and regardless of, any eventual ractive " + "instance being rendered, or if you need to access the " + "rendered DOM, use \"onrender\" instead. " + "See http://docs.ractivejs.org/latest/migrating for more information."
+  	},
+  	complete: {
+  		deprecated: "complete",
+  		replacement: "oncomplete"
+  	}
+  };
+
+  function Hook(event) {
+  	this.event = event;
+  	this.method = "on" + event;
+  	this.deprecate = deprecations[event];
+  }
+
+  Hook.prototype.fire = function (ractive, arg) {
+  	function call(method) {
+  		if (ractive[method]) {
+  			arg ? ractive[method](arg) : ractive[method]();
+  			return true;
+  		}
+  	}
+
+  	call(this.method);
+
+  	if (!ractive[this.method] && this.deprecate && call(this.deprecate.deprecated)) {
+  		if (this.deprecate.message) {
+  			warnIfDebug(this.deprecate.message);
+  		} else {
+  			warnIfDebug("The method \"%s\" has been deprecated in favor of \"%s\" and will likely be removed in a future release. See http://docs.ractivejs.org/latest/migrating for more information.", this.deprecate.deprecated, this.deprecate.replacement);
+  		}
+  	}
+
+  	arg ? ractive.fire(this.event, arg) : ractive.fire(this.event);
+  };
+
+  var hooks_Hook = Hook;
+
+  function addToArray(array, value) {
+  	var index = array.indexOf(value);
+
+  	if (index === -1) {
+  		array.push(value);
+  	}
+  }
+
+  function arrayContains(array, value) {
+  	for (var i = 0, c = array.length; i < c; i++) {
+  		if (array[i] == value) {
+  			return true;
+  		}
+  	}
+
+  	return false;
+  }
+
+  function arrayContentsMatch(a, b) {
+  	var i;
+
+  	if (!isArray(a) || !isArray(b)) {
+  		return false;
+  	}
+
+  	if (a.length !== b.length) {
+  		return false;
+  	}
+
+  	i = a.length;
+  	while (i--) {
+  		if (a[i] !== b[i]) {
+  			return false;
+  		}
+  	}
+
+  	return true;
+  }
+
+  function ensureArray(x) {
+  	if (typeof x === "string") {
+  		return [x];
+  	}
+
+  	if (x === undefined) {
+  		return [];
+  	}
+
+  	return x;
+  }
+
+  function lastItem(array) {
+  	return array[array.length - 1];
+  }
+
+  function removeFromArray(array, member) {
+  	var index = array.indexOf(member);
+
+  	if (index !== -1) {
+  		array.splice(index, 1);
+  	}
+  }
+
+  function toArray(arrayLike) {
+  	var array = [],
+  	    i = arrayLike.length;
+  	while (i--) {
+  		array[i] = arrayLike[i];
+  	}
+
+  	return array;
+  }
+
+  var _Promise,
+      PENDING = {},
+      FULFILLED = {},
+      REJECTED = {};
+
+  if (typeof Promise === "function") {
+  	// use native Promise
+  	_Promise = Promise;
+  } else {
+  	_Promise = function (callback) {
+  		var fulfilledHandlers = [],
+  		    rejectedHandlers = [],
+  		    state = PENDING,
+  		    result,
+  		    dispatchHandlers,
+  		    makeResolver,
+  		    fulfil,
+  		    reject,
+  		    promise;
+
+  		makeResolver = function (newState) {
+  			return function (value) {
+  				if (state !== PENDING) {
+  					return;
+  				}
+
+  				result = value;
+  				state = newState;
+
+  				dispatchHandlers = makeDispatcher(state === FULFILLED ? fulfilledHandlers : rejectedHandlers, result);
+
+  				// dispatch onFulfilled and onRejected handlers asynchronously
+  				wait(dispatchHandlers);
+  			};
+  		};
+
+  		fulfil = makeResolver(FULFILLED);
+  		reject = makeResolver(REJECTED);
+
+  		try {
+  			callback(fulfil, reject);
+  		} catch (err) {
+  			reject(err);
+  		}
+
+  		promise = {
+  			// `then()` returns a Promise - 2.2.7
+  			then: function (onFulfilled, onRejected) {
+  				var promise2 = new _Promise(function (fulfil, reject) {
+
+  					var processResolutionHandler = function (handler, handlers, forward) {
+
+  						// 2.2.1.1
+  						if (typeof handler === "function") {
+  							handlers.push(function (p1result) {
+  								var x;
+
+  								try {
+  									x = handler(p1result);
+  									utils_Promise__resolve(promise2, x, fulfil, reject);
+  								} catch (err) {
+  									reject(err);
+  								}
+  							});
+  						} else {
+  							// Forward the result of promise1 to promise2, if resolution handlers
+  							// are not given
+  							handlers.push(forward);
+  						}
+  					};
+
+  					// 2.2
+  					processResolutionHandler(onFulfilled, fulfilledHandlers, fulfil);
+  					processResolutionHandler(onRejected, rejectedHandlers, reject);
+
+  					if (state !== PENDING) {
+  						// If the promise has resolved already, dispatch the appropriate handlers asynchronously
+  						wait(dispatchHandlers);
+  					}
+  				});
+
+  				return promise2;
+  			}
+  		};
+
+  		promise["catch"] = function (onRejected) {
+  			return this.then(null, onRejected);
+  		};
+
+  		return promise;
+  	};
+
+  	_Promise.all = function (promises) {
+  		return new _Promise(function (fulfil, reject) {
+  			var result = [],
+  			    pending,
+  			    i,
+  			    processPromise;
+
+  			if (!promises.length) {
+  				fulfil(result);
+  				return;
+  			}
+
+  			processPromise = function (promise, i) {
+  				if (promise && typeof promise.then === "function") {
+  					promise.then(function (value) {
+  						result[i] = value;
+  						--pending || fulfil(result);
+  					}, reject);
+  				} else {
+  					result[i] = promise;
+  					--pending || fulfil(result);
+  				}
+  			};
+
+  			pending = i = promises.length;
+  			while (i--) {
+  				processPromise(promises[i], i);
+  			}
+  		});
+  	};
+
+  	_Promise.resolve = function (value) {
+  		return new _Promise(function (fulfil) {
+  			fulfil(value);
+  		});
+  	};
+
+  	_Promise.reject = function (reason) {
+  		return new _Promise(function (fulfil, reject) {
+  			reject(reason);
+  		});
+  	};
+  }
+
+  var utils_Promise = _Promise;
+
+  // TODO use MutationObservers or something to simulate setImmediate
+  function wait(callback) {
+  	setTimeout(callback, 0);
+  }
+
+  function makeDispatcher(handlers, result) {
+  	return function () {
+  		var handler;
+
+  		while (handler = handlers.shift()) {
+  			handler(result);
+  		}
+  	};
+  }
+
+  function utils_Promise__resolve(promise, x, fulfil, reject) {
+  	// Promise Resolution Procedure
+  	var then;
+
+  	// 2.3.1
+  	if (x === promise) {
+  		throw new TypeError("A promise's fulfillment handler cannot return the same promise");
+  	}
+
+  	// 2.3.2
+  	if (x instanceof _Promise) {
+  		x.then(fulfil, reject);
+  	}
+
+  	// 2.3.3
+  	else if (x && (typeof x === "object" || typeof x === "function")) {
+  		try {
+  			then = x.then; // 2.3.3.1
+  		} catch (e) {
+  			reject(e); // 2.3.3.2
+  			return;
+  		}
+
+  		// 2.3.3.3
+  		if (typeof then === "function") {
+  			var called, resolvePromise, rejectPromise;
+
+  			resolvePromise = function (y) {
+  				if (called) {
+  					return;
+  				}
+  				called = true;
+  				utils_Promise__resolve(promise, y, fulfil, reject);
+  			};
+
+  			rejectPromise = function (r) {
+  				if (called) {
+  					return;
+  				}
+  				called = true;
+  				reject(r);
+  			};
+
+  			try {
+  				then.call(x, resolvePromise, rejectPromise);
+  			} catch (e) {
+  				if (!called) {
+  					// 2.3.3.3.4.1
+  					reject(e); // 2.3.3.3.4.2
+  					called = true;
+  					return;
+  				}
+  			}
+  		} else {
+  			fulfil(x);
+  		}
+  	} else {
+  		fulfil(x);
+  	}
+  }
+
+  var getInnerContext = function (fragment) {
+  	do {
+  		if (fragment.context !== undefined) {
+  			return fragment.context;
+  		}
+  	} while (fragment = fragment.parent);
+
+  	return rootKeypath;
+  };
+
+  var shared_resolveRef = resolveRef;
+
+  function resolveRef(ractive, ref, fragment) {
+  	var keypath;
+
+  	ref = normalise(ref);
+
+  	// If a reference begins '~/', it's a top-level reference
+  	if (ref.substr(0, 2) === "~/") {
+  		keypath = getKeypath(ref.substring(2));
+  		createMappingIfNecessary(ractive, keypath.firstKey, fragment);
+  	}
+
+  	// If a reference begins with '.', it's either a restricted reference or
+  	// an ancestor reference...
+  	else if (ref[0] === ".") {
+  		keypath = resolveAncestorRef(getInnerContext(fragment), ref);
+
+  		if (keypath) {
+  			createMappingIfNecessary(ractive, keypath.firstKey, fragment);
+  		}
+  	}
+
+  	// ...otherwise we need to figure out the keypath based on context
+  	else {
+  		keypath = resolveAmbiguousReference(ractive, getKeypath(ref), fragment);
+  	}
+
+  	return keypath;
+  }
+
+  function resolveAncestorRef(baseContext, ref) {
+  	var contextKeys;
+
+  	// TODO...
+  	if (baseContext != undefined && typeof baseContext !== "string") {
+  		baseContext = baseContext.str;
+  	}
+
+  	// {{.}} means 'current context'
+  	if (ref === ".") return getKeypath(baseContext);
+
+  	contextKeys = baseContext ? baseContext.split(".") : [];
+
+  	// ancestor references (starting "../") go up the tree
+  	if (ref.substr(0, 3) === "../") {
+  		while (ref.substr(0, 3) === "../") {
+  			if (!contextKeys.length) {
+  				throw new Error("Could not resolve reference - too many \"../\" prefixes");
+  			}
+
+  			contextKeys.pop();
+  			ref = ref.substring(3);
+  		}
+
+  		contextKeys.push(ref);
+  		return getKeypath(contextKeys.join("."));
+  	}
+
+  	// not an ancestor reference - must be a restricted reference (prepended with "." or "./")
+  	if (!baseContext) {
+  		return getKeypath(ref.replace(/^\.\/?/, ""));
+  	}
+
+  	return getKeypath(baseContext + ref.replace(/^\.\//, "."));
+  }
+
+  function resolveAmbiguousReference(ractive, ref, fragment, isParentLookup) {
+  	var context, key, parentValue, hasContextChain, parentKeypath;
+
+  	if (ref.isRoot) {
+  		return ref;
+  	}
+
+  	key = ref.firstKey;
+
+  	while (fragment) {
+  		context = fragment.context;
+  		fragment = fragment.parent;
+
+  		if (!context) {
+  			continue;
+  		}
+
+  		hasContextChain = true;
+  		parentValue = ractive.viewmodel.get(context);
+
+  		if (parentValue && (typeof parentValue === "object" || typeof parentValue === "function") && key in parentValue) {
+  			return context.join(ref.str);
+  		}
+  	}
+
+  	// Root/computed/mapped property?
+  	if (isRootProperty(ractive.viewmodel, key)) {
+  		return ref;
+  	}
+
+  	// If this is an inline component, and it's not isolated, we
+  	// can try going up the scope chain
+  	if (ractive.parent && !ractive.isolated) {
+  		hasContextChain = true;
+  		fragment = ractive.component.parentFragment;
+
+  		key = getKeypath(key);
+
+  		if (parentKeypath = resolveAmbiguousReference(ractive.parent, key, fragment, true)) {
+  			// We need to create an inter-component binding
+  			ractive.viewmodel.map(key, {
+  				origin: ractive.parent.viewmodel,
+  				keypath: parentKeypath
+  			});
+
+  			return ref;
+  		}
+  	}
+
+  	// If there's no context chain, and the instance is either a) isolated or
+  	// b) an orphan, then we know that the keypath is identical to the reference
+  	if (!isParentLookup && !hasContextChain) {
+  		// the data object needs to have a property by this name,
+  		// to prevent future failed lookups
+  		ractive.viewmodel.set(ref, undefined);
+  		return ref;
+  	}
+  }
+
+  function createMappingIfNecessary(ractive, key) {
+  	var parentKeypath;
+
+  	if (!ractive.parent || ractive.isolated || isRootProperty(ractive.viewmodel, key)) {
+  		return;
+  	}
+
+  	key = getKeypath(key);
+
+  	if (parentKeypath = resolveAmbiguousReference(ractive.parent, key, ractive.component.parentFragment, true)) {
+  		ractive.viewmodel.map(key, {
+  			origin: ractive.parent.viewmodel,
+  			keypath: parentKeypath
+  		});
+  	}
+  }
+
+  function isRootProperty(viewmodel, key) {
+  	// special case for reference to root
+  	return key === "" || key in viewmodel.data || key in viewmodel.computations || key in viewmodel.mappings;
+  }
+
+  function teardown(x) {
+    x.teardown();
+  }
+
+  function methodCallers__unbind(x) {
+    x.unbind();
+  }
+
+  function methodCallers__unrender(x) {
+    x.unrender();
+  }
+
+  function cancel(x) {
+    x.cancel();
+  }
+
+  var TransitionManager = function (callback, parent) {
+  	this.callback = callback;
+  	this.parent = parent;
+
+  	this.intros = [];
+  	this.outros = [];
+
+  	this.children = [];
+  	this.totalChildren = this.outroChildren = 0;
+
+  	this.detachQueue = [];
+  	this.decoratorQueue = [];
+  	this.outrosComplete = false;
+
+  	if (parent) {
+  		parent.addChild(this);
+  	}
+  };
+
+  TransitionManager.prototype = {
+  	addChild: function (child) {
+  		this.children.push(child);
+
+  		this.totalChildren += 1;
+  		this.outroChildren += 1;
+  	},
+
+  	decrementOutros: function () {
+  		this.outroChildren -= 1;
+  		check(this);
+  	},
+
+  	decrementTotal: function () {
+  		this.totalChildren -= 1;
+  		check(this);
+  	},
+
+  	add: function (transition) {
+  		var list = transition.isIntro ? this.intros : this.outros;
+  		list.push(transition);
+  	},
+
+  	addDecorator: function (decorator) {
+  		this.decoratorQueue.push(decorator);
+  	},
+
+  	remove: function (transition) {
+  		var list = transition.isIntro ? this.intros : this.outros;
+  		removeFromArray(list, transition);
+  		check(this);
+  	},
+
+  	init: function () {
+  		this.ready = true;
+  		check(this);
+  	},
+
+  	detachNodes: function () {
+  		this.decoratorQueue.forEach(teardown);
+  		this.detachQueue.forEach(detach);
+  		this.children.forEach(detachNodes);
+  	}
+  };
+
+  function detach(element) {
+  	element.detach();
+  }
+
+  function detachNodes(tm) {
+  	tm.detachNodes();
+  }
+
+  function check(tm) {
+  	if (!tm.ready || tm.outros.length || tm.outroChildren) return;
+
+  	// If all outros are complete, and we haven't already done this,
+  	// we notify the parent if there is one, otherwise
+  	// start detaching nodes
+  	if (!tm.outrosComplete) {
+  		if (tm.parent) {
+  			tm.parent.decrementOutros(tm);
+  		} else {
+  			tm.detachNodes();
+  		}
+
+  		tm.outrosComplete = true;
+  	}
+
+  	// Once everything is done, we can notify parent transition
+  	// manager and call the callback
+  	if (!tm.intros.length && !tm.totalChildren) {
+  		if (typeof tm.callback === "function") {
+  			tm.callback();
+  		}
+
+  		if (tm.parent) {
+  			tm.parent.decrementTotal();
+  		}
+  	}
+  }
+
+  var global_TransitionManager = TransitionManager;
+
+  var batch,
+      runloop,
+      unresolved = [],
+      changeHook = new hooks_Hook("change");
+
+  runloop = {
+  	start: function (instance, returnPromise) {
+  		var promise, fulfilPromise;
+
+  		if (returnPromise) {
+  			promise = new utils_Promise(function (f) {
+  				return fulfilPromise = f;
+  			});
+  		}
+
+  		batch = {
+  			previousBatch: batch,
+  			transitionManager: new global_TransitionManager(fulfilPromise, batch && batch.transitionManager),
+  			views: [],
+  			tasks: [],
+  			ractives: [],
+  			instance: instance
+  		};
+
+  		if (instance) {
+  			batch.ractives.push(instance);
+  		}
+
+  		return promise;
+  	},
+
+  	end: function () {
+  		flushChanges();
+
+  		batch.transitionManager.init();
+  		if (!batch.previousBatch && !!batch.instance) batch.instance.viewmodel.changes = [];
+  		batch = batch.previousBatch;
+  	},
+
+  	addRactive: function (ractive) {
+  		if (batch) {
+  			addToArray(batch.ractives, ractive);
+  		}
+  	},
+
+  	registerTransition: function (transition) {
+  		transition._manager = batch.transitionManager;
+  		batch.transitionManager.add(transition);
+  	},
+
+  	registerDecorator: function (decorator) {
+  		batch.transitionManager.addDecorator(decorator);
+  	},
+
+  	addView: function (view) {
+  		batch.views.push(view);
+  	},
+
+  	addUnresolved: function (thing) {
+  		unresolved.push(thing);
+  	},
+
+  	removeUnresolved: function (thing) {
+  		removeFromArray(unresolved, thing);
+  	},
+
+  	// synchronise node detachments with transition ends
+  	detachWhenReady: function (thing) {
+  		batch.transitionManager.detachQueue.push(thing);
+  	},
+
+  	scheduleTask: function (task, postRender) {
+  		var _batch;
+
+  		if (!batch) {
+  			task();
+  		} else {
+  			_batch = batch;
+  			while (postRender && _batch.previousBatch) {
+  				// this can't happen until the DOM has been fully updated
+  				// otherwise in some situations (with components inside elements)
+  				// transitions and decorators will initialise prematurely
+  				_batch = _batch.previousBatch;
+  			}
+
+  			_batch.tasks.push(task);
+  		}
+  	}
+  };
+
+  var global_runloop = runloop;
+
+  function flushChanges() {
+  	var i, thing, changeHash;
+
+  	while (batch.ractives.length) {
+  		thing = batch.ractives.pop();
+  		changeHash = thing.viewmodel.applyChanges();
+
+  		if (changeHash) {
+  			changeHook.fire(thing, changeHash);
+  		}
+  	}
+
+  	attemptKeypathResolution();
+
+  	// Now that changes have been fully propagated, we can update the DOM
+  	// and complete other tasks
+  	for (i = 0; i < batch.views.length; i += 1) {
+  		batch.views[i].update();
+  	}
+  	batch.views.length = 0;
+
+  	for (i = 0; i < batch.tasks.length; i += 1) {
+  		batch.tasks[i]();
+  	}
+  	batch.tasks.length = 0;
+
+  	// If updating the view caused some model blowback - e.g. a triple
+  	// containing <option> elements caused the binding on the <select>
+  	// to update - then we start over
+  	if (batch.ractives.length) return flushChanges();
+  }
+
+  function attemptKeypathResolution() {
+  	var i, item, keypath, resolved;
+
+  	i = unresolved.length;
+
+  	// see if we can resolve any unresolved references
+  	while (i--) {
+  		item = unresolved[i];
+
+  		if (item.keypath) {
+  			// it resolved some other way. TODO how? two-way binding? Seems
+  			// weird that we'd still end up here
+  			unresolved.splice(i, 1);
+  			continue; // avoid removing the wrong thing should the next condition be true
+  		}
+
+  		if (keypath = shared_resolveRef(item.root, item.ref, item.parentFragment)) {
+  			(resolved || (resolved = [])).push({
+  				item: item,
+  				keypath: keypath
+  			});
+
+  			unresolved.splice(i, 1);
+  		}
+  	}
+
+  	if (resolved) {
+  		resolved.forEach(global_runloop__resolve);
+  	}
+  }
+
+  function global_runloop__resolve(resolved) {
+  	resolved.item.resolve(resolved.keypath);
+  }
+
+  var queue = [];
+
+  var animations = {
+  	tick: function () {
+  		var i, animation, now;
+
+  		now = utils_getTime();
+
+  		global_runloop.start();
+
+  		for (i = 0; i < queue.length; i += 1) {
+  			animation = queue[i];
+
+  			if (!animation.tick(now)) {
+  				// animation is complete, remove it from the stack, and decrement i so we don't miss one
+  				queue.splice(i--, 1);
+  			}
+  		}
+
+  		global_runloop.end();
+
+  		if (queue.length) {
+  			rAF(animations.tick);
+  		} else {
+  			animations.running = false;
+  		}
+  	},
+
+  	add: function (animation) {
+  		queue.push(animation);
+
+  		if (!animations.running) {
+  			animations.running = true;
+  			rAF(animations.tick);
+  		}
+  	},
+
+  	// TODO optimise this
+  	abort: function (keypath, root) {
+  		var i = queue.length,
+  		    animation;
+
+  		while (i--) {
+  			animation = queue[i];
+
+  			if (animation.root === root && animation.keypath === keypath) {
+  				animation.stop();
+  			}
+  		}
+  	}
+  };
+
+  var shared_animations = animations;
+
+  var Animation = function (options) {
+  	var key;
+
+  	this.startTime = Date.now();
+
+  	// from and to
+  	for (key in options) {
+  		if (options.hasOwnProperty(key)) {
+  			this[key] = options[key];
+  		}
+  	}
+
+  	this.interpolator = shared_interpolate(this.from, this.to, this.root, this.interpolator);
+  	this.running = true;
+
+  	this.tick();
+  };
+
+  Animation.prototype = {
+  	tick: function () {
+  		var elapsed, t, value, timeNow, index, keypath;
+
+  		keypath = this.keypath;
+
+  		if (this.running) {
+  			timeNow = Date.now();
+  			elapsed = timeNow - this.startTime;
+
+  			if (elapsed >= this.duration) {
+  				if (keypath !== null) {
+  					global_runloop.start(this.root);
+  					this.root.viewmodel.set(keypath, this.to);
+  					global_runloop.end();
+  				}
+
+  				if (this.step) {
+  					this.step(1, this.to);
+  				}
+
+  				this.complete(this.to);
+
+  				index = this.root._animations.indexOf(this);
+
+  				// TODO investigate why this happens
+  				if (index === -1) {
+  					warnIfDebug("Animation was not found");
+  				}
+
+  				this.root._animations.splice(index, 1);
+
+  				this.running = false;
+  				return false; // remove from the stack
+  			}
+
+  			t = this.easing ? this.easing(elapsed / this.duration) : elapsed / this.duration;
+
+  			if (keypath !== null) {
+  				value = this.interpolator(t);
+  				global_runloop.start(this.root);
+  				this.root.viewmodel.set(keypath, value);
+  				global_runloop.end();
+  			}
+
+  			if (this.step) {
+  				this.step(t, value);
+  			}
+
+  			return true; // keep in the stack
+  		}
+
+  		return false; // remove from the stack
+  	},
+
+  	stop: function () {
+  		var index;
+
+  		this.running = false;
+
+  		index = this.root._animations.indexOf(this);
+
+  		// TODO investigate why this happens
+  		if (index === -1) {
+  			warnIfDebug("Animation was not found");
+  		}
+
+  		this.root._animations.splice(index, 1);
+  	}
+  };
+
+  var animate_Animation = Animation;
+
+  var prototype_animate = Ractive$animate;
+
+  var noAnimation = { stop: noop };
+  function Ractive$animate(keypath, to, options) {
+  	var promise, fulfilPromise, k, animation, animations, easing, duration, step, complete, makeValueCollector, currentValues, collectValue, dummy, dummyOptions;
+
+  	promise = new utils_Promise(function (fulfil) {
+  		return fulfilPromise = fulfil;
+  	});
+
+  	// animate multiple keypaths
+  	if (typeof keypath === "object") {
+  		options = to || {};
+  		easing = options.easing;
+  		duration = options.duration;
+
+  		animations = [];
+
+  		// we don't want to pass the `step` and `complete` handlers, as they will
+  		// run for each animation! So instead we'll store the handlers and create
+  		// our own...
+  		step = options.step;
+  		complete = options.complete;
+
+  		if (step || complete) {
+  			currentValues = {};
+
+  			options.step = null;
+  			options.complete = null;
+
+  			makeValueCollector = function (keypath) {
+  				return function (t, value) {
+  					currentValues[keypath] = value;
+  				};
+  			};
+  		}
+
+  		for (k in keypath) {
+  			if (keypath.hasOwnProperty(k)) {
+  				if (step || complete) {
+  					collectValue = makeValueCollector(k);
+  					options = { easing: easing, duration: duration };
+
+  					if (step) {
+  						options.step = collectValue;
+  					}
+  				}
+
+  				options.complete = complete ? collectValue : noop;
+  				animations.push(animate(this, k, keypath[k], options));
+  			}
+  		}
+
+  		// Create a dummy animation, to facilitate step/complete
+  		// callbacks, and Promise fulfilment
+  		dummyOptions = { easing: easing, duration: duration };
+
+  		if (step) {
+  			dummyOptions.step = function (t) {
+  				return step(t, currentValues);
+  			};
+  		}
+
+  		if (complete) {
+  			promise.then(function (t) {
+  				return complete(t, currentValues);
+  			});
+  		}
+
+  		dummyOptions.complete = fulfilPromise;
+
+  		dummy = animate(this, null, null, dummyOptions);
+  		animations.push(dummy);
+
+  		promise.stop = function () {
+  			var animation;
+
+  			while (animation = animations.pop()) {
+  				animation.stop();
+  			}
+
+  			if (dummy) {
+  				dummy.stop();
+  			}
+  		};
+
+  		return promise;
+  	}
+
+  	// animate a single keypath
+  	options = options || {};
+
+  	if (options.complete) {
+  		promise.then(options.complete);
+  	}
+
+  	options.complete = fulfilPromise;
+  	animation = animate(this, keypath, to, options);
+
+  	promise.stop = function () {
+  		return animation.stop();
+  	};
+  	return promise;
+  }
+
+  function animate(root, keypath, to, options) {
+  	var easing, duration, animation, from;
+
+  	if (keypath) {
+  		keypath = getKeypath(normalise(keypath));
+  	}
+
+  	if (keypath !== null) {
+  		from = root.viewmodel.get(keypath);
+  	}
+
+  	// cancel any existing animation
+  	// TODO what about upstream/downstream keypaths?
+  	shared_animations.abort(keypath, root);
+
+  	// don't bother animating values that stay the same
+  	if (isEqual(from, to)) {
+  		if (options.complete) {
+  			options.complete(options.to);
+  		}
+
+  		return noAnimation;
+  	}
+
+  	// easing function
+  	if (options.easing) {
+  		if (typeof options.easing === "function") {
+  			easing = options.easing;
+  		} else {
+  			easing = root.easing[options.easing];
+  		}
+
+  		if (typeof easing !== "function") {
+  			easing = null;
+  		}
+  	}
+
+  	// duration
+  	duration = options.duration === undefined ? 400 : options.duration;
+
+  	// TODO store keys, use an internal set method
+  	animation = new animate_Animation({
+  		keypath: keypath,
+  		from: from,
+  		to: to,
+  		root: root,
+  		duration: duration,
+  		easing: easing,
+  		interpolator: options.interpolator,
+
+  		// TODO wrap callbacks if necessary, to use instance as context
+  		step: options.step,
+  		complete: options.complete
+  	});
+
+  	shared_animations.add(animation);
+  	root._animations.push(animation);
+
+  	return animation;
+  }
+
+  var prototype_detach = Ractive$detach;
+  var prototype_detach__detachHook = new hooks_Hook("detach");
+  function Ractive$detach() {
+  	if (this.detached) {
+  		return this.detached;
+  	}
+
+  	if (this.el) {
+  		removeFromArray(this.el.__ractive_instances__, this);
+  	}
+  	this.detached = this.fragment.detach();
+  	prototype_detach__detachHook.fire(this);
+  	return this.detached;
+  }
+
+  var prototype_find = Ractive$find;
+
+  function Ractive$find(selector) {
+  	if (!this.el) {
+  		return null;
+  	}
+
+  	return this.fragment.find(selector);
+  }
+
+  var test = Query$test;
+  function Query$test(item, noDirty) {
+  	var itemMatches;
+
+  	if (this._isComponentQuery) {
+  		itemMatches = !this.selector || item.name === this.selector;
+  	} else {
+  		itemMatches = item.node ? matches(item.node, this.selector) : null;
+  	}
+
+  	if (itemMatches) {
+  		this.push(item.node || item.instance);
+
+  		if (!noDirty) {
+  			this._makeDirty();
+  		}
+
+  		return true;
+  	}
+  }
+
+  var makeQuery_cancel = function () {
+  	var liveQueries, selector, index;
+
+  	liveQueries = this._root[this._isComponentQuery ? "liveComponentQueries" : "liveQueries"];
+  	selector = this.selector;
+
+  	index = liveQueries.indexOf(selector);
+
+  	if (index !== -1) {
+  		liveQueries.splice(index, 1);
+  		liveQueries[selector] = null;
+  	}
+  };
+
+  var sortByItemPosition = function (a, b) {
+  	var ancestryA, ancestryB, oldestA, oldestB, mutualAncestor, indexA, indexB, fragments, fragmentA, fragmentB;
+
+  	ancestryA = getAncestry(a.component || a._ractive.proxy);
+  	ancestryB = getAncestry(b.component || b._ractive.proxy);
+
+  	oldestA = lastItem(ancestryA);
+  	oldestB = lastItem(ancestryB);
+
+  	// remove items from the end of both ancestries as long as they are identical
+  	// - the final one removed is the closest mutual ancestor
+  	while (oldestA && oldestA === oldestB) {
+  		ancestryA.pop();
+  		ancestryB.pop();
+
+  		mutualAncestor = oldestA;
+
+  		oldestA = lastItem(ancestryA);
+  		oldestB = lastItem(ancestryB);
+  	}
+
+  	// now that we have the mutual ancestor, we can find which is earliest
+  	oldestA = oldestA.component || oldestA;
+  	oldestB = oldestB.component || oldestB;
+
+  	fragmentA = oldestA.parentFragment;
+  	fragmentB = oldestB.parentFragment;
+
+  	// if both items share a parent fragment, our job is easy
+  	if (fragmentA === fragmentB) {
+  		indexA = fragmentA.items.indexOf(oldestA);
+  		indexB = fragmentB.items.indexOf(oldestB);
+
+  		// if it's the same index, it means one contains the other,
+  		// so we see which has the longest ancestry
+  		return indexA - indexB || ancestryA.length - ancestryB.length;
+  	}
+
+  	// if mutual ancestor is a section, we first test to see which section
+  	// fragment comes first
+  	if (fragments = mutualAncestor.fragments) {
+  		indexA = fragments.indexOf(fragmentA);
+  		indexB = fragments.indexOf(fragmentB);
+
+  		return indexA - indexB || ancestryA.length - ancestryB.length;
+  	}
+
+  	throw new Error("An unexpected condition was met while comparing the position of two components. Please file an issue at https://github.com/RactiveJS/Ractive/issues - thanks!");
+  };
+
+  function getParent(item) {
+  	var parentFragment;
+
+  	if (parentFragment = item.parentFragment) {
+  		return parentFragment.owner;
+  	}
+
+  	if (item.component && (parentFragment = item.component.parentFragment)) {
+  		return parentFragment.owner;
+  	}
+  }
+
+  function getAncestry(item) {
+  	var ancestry, ancestor;
+
+  	ancestry = [item];
+
+  	ancestor = getParent(item);
+
+  	while (ancestor) {
+  		ancestry.push(ancestor);
+  		ancestor = getParent(ancestor);
+  	}
+
+  	return ancestry;
+  }
+
+  var sortByDocumentPosition = function (node, otherNode) {
+  	var bitmask;
+
+  	if (node.compareDocumentPosition) {
+  		bitmask = node.compareDocumentPosition(otherNode);
+  		return bitmask & 2 ? 1 : -1;
+  	}
+
+  	// In old IE, we can piggy back on the mechanism for
+  	// comparing component positions
+  	return sortByItemPosition(node, otherNode);
+  };
+
+  var sort = function () {
+  	this.sort(this._isComponentQuery ? sortByItemPosition : sortByDocumentPosition);
+  	this._dirty = false;
+  };
+
+  var makeQuery_dirty = function () {
+  	var _this = this;
+
+  	if (!this._dirty) {
+  		this._dirty = true;
+
+  		// Once the DOM has been updated, ensure the query
+  		// is correctly ordered
+  		global_runloop.scheduleTask(function () {
+  			_this._sort();
+  		});
+  	}
+  };
+
+  var remove = function (nodeOrComponent) {
+  	var index = this.indexOf(this._isComponentQuery ? nodeOrComponent.instance : nodeOrComponent);
+
+  	if (index !== -1) {
+  		this.splice(index, 1);
+  	}
+  };
+
+  var _makeQuery = makeQuery;
+  function makeQuery(ractive, selector, live, isComponentQuery) {
+  	var query = [];
+
+  	defineProperties(query, {
+  		selector: { value: selector },
+  		live: { value: live },
+
+  		_isComponentQuery: { value: isComponentQuery },
+  		_test: { value: test }
+  	});
+
+  	if (!live) {
+  		return query;
+  	}
+
+  	defineProperties(query, {
+  		cancel: { value: makeQuery_cancel },
+
+  		_root: { value: ractive },
+  		_sort: { value: sort },
+  		_makeDirty: { value: makeQuery_dirty },
+  		_remove: { value: remove },
+
+  		_dirty: { value: false, writable: true }
+  	});
+
+  	return query;
+  }
+
+  var prototype_findAll = Ractive$findAll;
+  function Ractive$findAll(selector, options) {
+  	var liveQueries, query;
+
+  	if (!this.el) {
+  		return [];
+  	}
+
+  	options = options || {};
+  	liveQueries = this._liveQueries;
+
+  	// Shortcut: if we're maintaining a live query with this
+  	// selector, we don't need to traverse the parallel DOM
+  	if (query = liveQueries[selector]) {
+
+  		// Either return the exact same query, or (if not live) a snapshot
+  		return options && options.live ? query : query.slice();
+  	}
+
+  	query = _makeQuery(this, selector, !!options.live, false);
+
+  	// Add this to the list of live queries Ractive needs to maintain,
+  	// if applicable
+  	if (query.live) {
+  		liveQueries.push(selector);
+  		liveQueries["_" + selector] = query;
+  	}
+
+  	this.fragment.findAll(selector, query);
+  	return query;
+  }
+
+  var prototype_findAllComponents = Ractive$findAllComponents;
+  function Ractive$findAllComponents(selector, options) {
+  	var liveQueries, query;
+
+  	options = options || {};
+  	liveQueries = this._liveComponentQueries;
+
+  	// Shortcut: if we're maintaining a live query with this
+  	// selector, we don't need to traverse the parallel DOM
+  	if (query = liveQueries[selector]) {
+
+  		// Either return the exact same query, or (if not live) a snapshot
+  		return options && options.live ? query : query.slice();
+  	}
+
+  	query = _makeQuery(this, selector, !!options.live, true);
+
+  	// Add this to the list of live queries Ractive needs to maintain,
+  	// if applicable
+  	if (query.live) {
+  		liveQueries.push(selector);
+  		liveQueries["_" + selector] = query;
+  	}
+
+  	this.fragment.findAllComponents(selector, query);
+  	return query;
+  }
+
+  var prototype_findComponent = Ractive$findComponent;
+
+  function Ractive$findComponent(selector) {
+  	return this.fragment.findComponent(selector);
+  }
+
+  var findContainer = Ractive$findContainer;
+
+  function Ractive$findContainer(selector) {
+  	if (this.container) {
+  		if (this.container.component && this.container.component.name === selector) {
+  			return this.container;
+  		} else {
+  			return this.container.findContainer(selector);
+  		}
+  	}
+
+  	return null;
+  }
+
+  var findParent = Ractive$findParent;
+
+  function Ractive$findParent(selector) {
+
+  	if (this.parent) {
+  		if (this.parent.component && this.parent.component.name === selector) {
+  			return this.parent;
+  		} else {
+  			return this.parent.findParent(selector);
+  		}
+  	}
+
+  	return null;
+  }
+
+  var eventStack = {
+  	enqueue: function (ractive, event) {
+  		if (ractive.event) {
+  			ractive._eventQueue = ractive._eventQueue || [];
+  			ractive._eventQueue.push(ractive.event);
+  		}
+  		ractive.event = event;
+  	},
+  	dequeue: function (ractive) {
+  		if (ractive._eventQueue && ractive._eventQueue.length) {
+  			ractive.event = ractive._eventQueue.pop();
+  		} else {
+  			delete ractive.event;
+  		}
+  	}
+  };
+
+  var shared_eventStack = eventStack;
+
+  var shared_fireEvent = fireEvent;
+
+  function fireEvent(ractive, eventName) {
+  	var options = arguments[2] === undefined ? {} : arguments[2];
+
+  	if (!eventName) {
+  		return;
+  	}
+
+  	if (!options.event) {
+  		options.event = {
+  			name: eventName,
+  			// until event not included as argument default
+  			_noArg: true
+  		};
+  	} else {
+  		options.event.name = eventName;
+  	}
+
+  	var eventNames = getKeypath(eventName).wildcardMatches();
+  	fireEventAs(ractive, eventNames, options.event, options.args, true);
+  }
+
+  function fireEventAs(ractive, eventNames, event, args) {
+  	var initialFire = arguments[4] === undefined ? false : arguments[4];
+
+  	var subscribers,
+  	    i,
+  	    bubble = true;
+
+  	shared_eventStack.enqueue(ractive, event);
+
+  	for (i = eventNames.length; i >= 0; i--) {
+  		subscribers = ractive._subs[eventNames[i]];
+
+  		if (subscribers) {
+  			bubble = notifySubscribers(ractive, subscribers, event, args) && bubble;
+  		}
+  	}
+
+  	shared_eventStack.dequeue(ractive);
+
+  	if (ractive.parent && bubble) {
+
+  		if (initialFire && ractive.component) {
+  			var fullName = ractive.component.name + "." + eventNames[eventNames.length - 1];
+  			eventNames = getKeypath(fullName).wildcardMatches();
+
+  			if (event) {
+  				event.component = ractive;
+  			}
+  		}
+
+  		fireEventAs(ractive.parent, eventNames, event, args);
+  	}
+  }
+
+  function notifySubscribers(ractive, subscribers, event, args) {
+  	var originalEvent = null,
+  	    stopEvent = false;
+
+  	if (event && !event._noArg) {
+  		args = [event].concat(args);
+  	}
+
+  	// subscribers can be modified inflight, e.g. "once" functionality
+  	// so we need to copy to make sure everyone gets called
+  	subscribers = subscribers.slice();
+
+  	for (var i = 0, len = subscribers.length; i < len; i += 1) {
+  		if (subscribers[i].apply(ractive, args) === false) {
+  			stopEvent = true;
+  		}
+  	}
+
+  	if (event && !event._noArg && stopEvent && (originalEvent = event.original)) {
+  		originalEvent.preventDefault && originalEvent.preventDefault();
+  		originalEvent.stopPropagation && originalEvent.stopPropagation();
+  	}
+
+  	return !stopEvent;
+  }
+
+  var prototype_fire = Ractive$fire;
+  function Ractive$fire(eventName) {
+
+  	var options = {
+  		args: Array.prototype.slice.call(arguments, 1)
+  	};
+
+  	shared_fireEvent(this, eventName, options);
+  }
+
+  var prototype_get = Ractive$get;
+  var options = {
+  	capture: true, // top-level calls should be intercepted
+  	noUnwrap: true, // wrapped values should NOT be unwrapped
+  	fullRootGet: true // root get should return mappings
+  };
+  function Ractive$get(keypath) {
+  	var value;
+
+  	keypath = getKeypath(normalise(keypath));
+  	value = this.viewmodel.get(keypath, options);
+
+  	// Create inter-component binding, if necessary
+  	if (value === undefined && this.parent && !this.isolated) {
+  		if (shared_resolveRef(this, keypath.str, this.component.parentFragment)) {
+  			// creates binding as side-effect, if appropriate
+  			value = this.viewmodel.get(keypath);
+  		}
+  	}
+
+  	return value;
+  }
+
+  var insert = Ractive$insert;
+
+  var insertHook = new hooks_Hook("insert");
+  function Ractive$insert(target, anchor) {
+  	if (!this.fragment.rendered) {
+  		// TODO create, and link to, documentation explaining this
+  		throw new Error("The API has changed - you must call `ractive.render(target[, anchor])` to render your Ractive instance. Once rendered you can use `ractive.insert()`.");
+  	}
+
+  	target = getElement(target);
+  	anchor = getElement(anchor) || null;
+
+  	if (!target) {
+  		throw new Error("You must specify a valid target to insert into");
+  	}
+
+  	target.insertBefore(this.detach(), anchor);
+  	this.el = target;
+
+  	(target.__ractive_instances__ || (target.__ractive_instances__ = [])).push(this);
+  	this.detached = null;
+
+  	fireInsertHook(this);
+  }
+
+  function fireInsertHook(ractive) {
+  	insertHook.fire(ractive);
+
+  	ractive.findAllComponents("*").forEach(function (child) {
+  		fireInsertHook(child.instance);
+  	});
+  }
+
+  var prototype_merge = Ractive$merge;
+  function Ractive$merge(keypath, array, options) {
+  	var currentArray, promise;
+
+  	keypath = getKeypath(normalise(keypath));
+  	currentArray = this.viewmodel.get(keypath);
+
+  	// If either the existing value or the new value isn't an
+  	// array, just do a regular set
+  	if (!isArray(currentArray) || !isArray(array)) {
+  		return this.set(keypath, array, options && options.complete);
+  	}
+
+  	// Manage transitions
+  	promise = global_runloop.start(this, true);
+  	this.viewmodel.merge(keypath, currentArray, array, options);
+  	global_runloop.end();
+
+  	return promise;
+  }
+
+  var Observer = function (ractive, keypath, callback, options) {
+  	this.root = ractive;
+  	this.keypath = keypath;
+  	this.callback = callback;
+  	this.defer = options.defer;
+
+  	// default to root as context, but allow it to be overridden
+  	this.context = options && options.context ? options.context : ractive;
+  };
+
+  Observer.prototype = {
+  	init: function (immediate) {
+  		this.value = this.root.get(this.keypath.str);
+
+  		if (immediate !== false) {
+  			this.update();
+  		} else {
+  			this.oldValue = this.value;
+  		}
+  	},
+
+  	setValue: function (value) {
+  		var _this = this;
+
+  		if (!isEqual(value, this.value)) {
+  			this.value = value;
+
+  			if (this.defer && this.ready) {
+  				global_runloop.scheduleTask(function () {
+  					return _this.update();
+  				});
+  			} else {
+  				this.update();
+  			}
+  		}
+  	},
+
+  	update: function () {
+  		// Prevent infinite loops
+  		if (this.updating) {
+  			return;
+  		}
+
+  		this.updating = true;
+
+  		this.callback.call(this.context, this.value, this.oldValue, this.keypath.str);
+  		this.oldValue = this.value;
+
+  		this.updating = false;
+  	}
+  };
+
+  var observe_Observer = Observer;
+
+  var observe_getPattern = getPattern;
+  function getPattern(ractive, pattern) {
+  	var matchingKeypaths, values;
+
+  	matchingKeypaths = getMatchingKeypaths(ractive, pattern);
+
+  	values = {};
+  	matchingKeypaths.forEach(function (keypath) {
+  		values[keypath.str] = ractive.get(keypath.str);
+  	});
+
+  	return values;
+  }
+
+  var PatternObserver,
+      slice = Array.prototype.slice;
+
+  PatternObserver = function (ractive, keypath, callback, options) {
+  	this.root = ractive;
+
+  	this.callback = callback;
+  	this.defer = options.defer;
+
+  	this.keypath = keypath;
+  	this.regex = new RegExp("^" + keypath.str.replace(/\./g, "\\.").replace(/\*/g, "([^\\.]+)") + "$");
+  	this.values = {};
+
+  	if (this.defer) {
+  		this.proxies = [];
+  	}
+
+  	// default to root as context, but allow it to be overridden
+  	this.context = options && options.context ? options.context : ractive;
+  };
+
+  PatternObserver.prototype = {
+  	init: function (immediate) {
+  		var values, keypath;
+
+  		values = observe_getPattern(this.root, this.keypath);
+
+  		if (immediate !== false) {
+  			for (keypath in values) {
+  				if (values.hasOwnProperty(keypath)) {
+  					this.update(getKeypath(keypath));
+  				}
+  			}
+  		} else {
+  			this.values = values;
+  		}
+  	},
+
+  	update: function (keypath) {
+  		var _this = this;
+
+  		var values;
+
+  		if (keypath.isPattern) {
+  			values = observe_getPattern(this.root, keypath);
+
+  			for (keypath in values) {
+  				if (values.hasOwnProperty(keypath)) {
+  					this.update(getKeypath(keypath));
+  				}
+  			}
+
+  			return;
+  		}
+
+  		// special case - array mutation should not trigger `array.*`
+  		// pattern observer with `array.length`
+  		if (this.root.viewmodel.implicitChanges[keypath.str]) {
+  			return;
+  		}
+
+  		if (this.defer && this.ready) {
+  			global_runloop.scheduleTask(function () {
+  				return _this.getProxy(keypath).update();
+  			});
+  			return;
+  		}
+
+  		this.reallyUpdate(keypath);
+  	},
+
+  	reallyUpdate: function (keypath) {
+  		var keypathStr, value, keys, args;
+
+  		keypathStr = keypath.str;
+  		value = this.root.viewmodel.get(keypath);
+
+  		// Prevent infinite loops
+  		if (this.updating) {
+  			this.values[keypathStr] = value;
+  			return;
+  		}
+
+  		this.updating = true;
+
+  		if (!isEqual(value, this.values[keypathStr]) || !this.ready) {
+  			keys = slice.call(this.regex.exec(keypathStr), 1);
+  			args = [value, this.values[keypathStr], keypathStr].concat(keys);
+
+  			this.values[keypathStr] = value;
+  			this.callback.apply(this.context, args);
+  		}
+
+  		this.updating = false;
+  	},
+
+  	getProxy: function (keypath) {
+  		var _this = this;
+
+  		if (!this.proxies[keypath.str]) {
+  			this.proxies[keypath.str] = {
+  				update: function () {
+  					return _this.reallyUpdate(keypath);
+  				}
+  			};
+  		}
+
+  		return this.proxies[keypath.str];
+  	}
+  };
+
+  var observe_PatternObserver = PatternObserver;
+
+  var observe_getObserverFacade = getObserverFacade;
+  var emptyObject = {};
+  function getObserverFacade(ractive, keypath, callback, options) {
+  	var observer, isPatternObserver, cancelled;
+
+  	keypath = getKeypath(normalise(keypath));
+  	options = options || emptyObject;
+
+  	// pattern observers are treated differently
+  	if (keypath.isPattern) {
+  		observer = new observe_PatternObserver(ractive, keypath, callback, options);
+  		ractive.viewmodel.patternObservers.push(observer);
+  		isPatternObserver = true;
+  	} else {
+  		observer = new observe_Observer(ractive, keypath, callback, options);
+  	}
+
+  	observer.init(options.init);
+  	ractive.viewmodel.register(keypath, observer, isPatternObserver ? "patternObservers" : "observers");
+
+  	// This flag allows observers to initialise even with undefined values
+  	observer.ready = true;
+
+  	var facade = {
+  		cancel: function () {
+  			var index;
+
+  			if (cancelled) {
+  				return;
+  			}
+
+  			if (isPatternObserver) {
+  				index = ractive.viewmodel.patternObservers.indexOf(observer);
+
+  				ractive.viewmodel.patternObservers.splice(index, 1);
+  				ractive.viewmodel.unregister(keypath, observer, "patternObservers");
+  			} else {
+  				ractive.viewmodel.unregister(keypath, observer, "observers");
+  			}
+  			cancelled = true;
+  		}
+  	};
+
+  	ractive._observers.push(facade);
+  	return facade;
+  }
+
+  var observe = Ractive$observe;
+  function Ractive$observe(keypath, callback, options) {
+
+  	var observers, map, keypaths, i;
+
+  	// Allow a map of keypaths to handlers
+  	if (isObject(keypath)) {
+  		options = callback;
+  		map = keypath;
+
+  		observers = [];
+
+  		for (keypath in map) {
+  			if (map.hasOwnProperty(keypath)) {
+  				callback = map[keypath];
+  				observers.push(this.observe(keypath, callback, options));
+  			}
+  		}
+
+  		return {
+  			cancel: function () {
+  				while (observers.length) {
+  					observers.pop().cancel();
+  				}
+  			}
+  		};
+  	}
+
+  	// Allow `ractive.observe( callback )` - i.e. observe entire model
+  	if (typeof keypath === "function") {
+  		options = callback;
+  		callback = keypath;
+  		keypath = "";
+
+  		return observe_getObserverFacade(this, keypath, callback, options);
+  	}
+
+  	keypaths = keypath.split(" ");
+
+  	// Single keypath
+  	if (keypaths.length === 1) {
+  		return observe_getObserverFacade(this, keypath, callback, options);
+  	}
+
+  	// Multiple space-separated keypaths
+  	observers = [];
+
+  	i = keypaths.length;
+  	while (i--) {
+  		keypath = keypaths[i];
+
+  		if (keypath) {
+  			observers.push(observe_getObserverFacade(this, keypath, callback, options));
+  		}
+  	}
+
+  	return {
+  		cancel: function () {
+  			while (observers.length) {
+  				observers.pop().cancel();
+  			}
+  		}
+  	};
+  }
+
+  var observeOnce = Ractive$observeOnce;
+
+  function Ractive$observeOnce(property, callback, options) {
+
+  	var observer = this.observe(property, function () {
+  		callback.apply(this, arguments);
+  		observer.cancel();
+  	}, { init: false, defer: options && options.defer });
+
+  	return observer;
+  }
+
+  var shared_trim = function (str) {
+    return str.trim();
+  };
+
+  var notEmptyString = function (str) {
+    return str !== "";
+  };
+
+  var off = Ractive$off;
+  function Ractive$off(eventName, callback) {
+  	var _this = this;
+
+  	var eventNames;
+
+  	// if no arguments specified, remove all callbacks
+  	if (!eventName) {
+  		// TODO use this code instead, once the following issue has been resolved
+  		// in PhantomJS (tests are unpassable otherwise!)
+  		// https://github.com/ariya/phantomjs/issues/11856
+  		// defineProperty( this, '_subs', { value: create( null ), configurable: true });
+  		for (eventName in this._subs) {
+  			delete this._subs[eventName];
+  		}
+  	} else {
+  		// Handle multiple space-separated event names
+  		eventNames = eventName.split(" ").map(shared_trim).filter(notEmptyString);
+
+  		eventNames.forEach(function (eventName) {
+  			var subscribers, index;
+
+  			// If we have subscribers for this event...
+  			if (subscribers = _this._subs[eventName]) {
+  				// ...if a callback was specified, only remove that
+  				if (callback) {
+  					index = subscribers.indexOf(callback);
+  					if (index !== -1) {
+  						subscribers.splice(index, 1);
+  					}
+  				}
+
+  				// ...otherwise remove all callbacks
+  				else {
+  					_this._subs[eventName] = [];
+  				}
+  			}
+  		});
+  	}
+
+  	return this;
+  }
+
+  var on = Ractive$on;
+  function Ractive$on(eventName, callback) {
+  	var _this = this;
+
+  	var listeners, n, eventNames;
+
+  	// allow mutliple listeners to be bound in one go
+  	if (typeof eventName === "object") {
+  		listeners = [];
+
+  		for (n in eventName) {
+  			if (eventName.hasOwnProperty(n)) {
+  				listeners.push(this.on(n, eventName[n]));
+  			}
+  		}
+
+  		return {
+  			cancel: function () {
+  				var listener;
+
+  				while (listener = listeners.pop()) {
+  					listener.cancel();
+  				}
+  			}
+  		};
+  	}
+
+  	// Handle multiple space-separated event names
+  	eventNames = eventName.split(" ").map(shared_trim).filter(notEmptyString);
+
+  	eventNames.forEach(function (eventName) {
+  		(_this._subs[eventName] || (_this._subs[eventName] = [])).push(callback);
+  	});
+
+  	return {
+  		cancel: function () {
+  			return _this.off(eventName, callback);
+  		}
+  	};
+  }
+
+  var once = Ractive$once;
+
+  function Ractive$once(eventName, handler) {
+
+  	var listener = this.on(eventName, function () {
+  		handler.apply(this, arguments);
+  		listener.cancel();
+  	});
+
+  	// so we can still do listener.cancel() manually
+  	return listener;
+  }
+
+  // This function takes an array, the name of a mutator method, and the
+  // arguments to call that mutator method with, and returns an array that
+  // maps the old indices to their new indices.
+
+  // So if you had something like this...
+  //
+  //     array = [ 'a', 'b', 'c', 'd' ];
+  //     array.push( 'e' );
+  //
+  // ...you'd get `[ 0, 1, 2, 3 ]` - in other words, none of the old indices
+  // have changed. If you then did this...
+  //
+  //     array.unshift( 'z' );
+  //
+  // ...the indices would be `[ 1, 2, 3, 4, 5 ]` - every item has been moved
+  // one higher to make room for the 'z'. If you removed an item, the new index
+  // would be -1...
+  //
+  //     array.splice( 2, 2 );
+  //
+  // ...this would result in [ 0, 1, -1, -1, 2, 3 ].
+  //
+  // This information is used to enable fast, non-destructive shuffling of list
+  // sections when you do e.g. `ractive.splice( 'items', 2, 2 );
+
+  var shared_getNewIndices = getNewIndices;
+
+  function getNewIndices(array, methodName, args) {
+  	var spliceArguments,
+  	    len,
+  	    newIndices = [],
+  	    removeStart,
+  	    removeEnd,
+  	    balance,
+  	    i;
+
+  	spliceArguments = getSpliceEquivalent(array, methodName, args);
+
+  	if (!spliceArguments) {
+  		return null; // TODO support reverse and sort?
+  	}
+
+  	len = array.length;
+  	balance = spliceArguments.length - 2 - spliceArguments[1];
+
+  	removeStart = Math.min(len, spliceArguments[0]);
+  	removeEnd = removeStart + spliceArguments[1];
+
+  	for (i = 0; i < removeStart; i += 1) {
+  		newIndices.push(i);
+  	}
+
+  	for (; i < removeEnd; i += 1) {
+  		newIndices.push(-1);
+  	}
+
+  	for (; i < len; i += 1) {
+  		newIndices.push(i + balance);
+  	}
+
+  	// there is a net shift for the rest of the array starting with index + balance
+  	if (balance !== 0) {
+  		newIndices.touchedFrom = spliceArguments[0];
+  	} else {
+  		newIndices.touchedFrom = array.length;
+  	}
+
+  	return newIndices;
+  }
+
+  // The pop, push, shift an unshift methods can all be represented
+  // as an equivalent splice
+  function getSpliceEquivalent(array, methodName, args) {
+  	switch (methodName) {
+  		case "splice":
+  			if (args[0] !== undefined && args[0] < 0) {
+  				args[0] = array.length + Math.max(args[0], -array.length);
+  			}
+
+  			while (args.length < 2) {
+  				args.push(0);
+  			}
+
+  			// ensure we only remove elements that exist
+  			args[1] = Math.min(args[1], array.length - args[0]);
+
+  			return args;
+
+  		case "sort":
+  		case "reverse":
+  			return null;
+
+  		case "pop":
+  			if (array.length) {
+  				return [array.length - 1, 1];
+  			}
+  			return [0, 0];
+
+  		case "push":
+  			return [array.length, 0].concat(args);
+
+  		case "shift":
+  			return [0, array.length ? 1 : 0];
+
+  		case "unshift":
+  			return [0, 0].concat(args);
+  	}
+  }
+
+  var arrayProto = Array.prototype;
+
+  var makeArrayMethod = function (methodName) {
+  	return function (keypath) {
+  		for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+  			args[_key - 1] = arguments[_key];
+  		}
+
+  		var array,
+  		    newIndices = [],
+  		    len,
+  		    promise,
+  		    result;
+
+  		keypath = getKeypath(normalise(keypath));
+
+  		array = this.viewmodel.get(keypath);
+  		len = array.length;
+
+  		if (!isArray(array)) {
+  			throw new Error("Called ractive." + methodName + "('" + keypath.str + "'), but '" + keypath.str + "' does not refer to an array");
+  		}
+
+  		newIndices = shared_getNewIndices(array, methodName, args);
+
+  		result = arrayProto[methodName].apply(array, args);
+  		promise = global_runloop.start(this, true).then(function () {
+  			return result;
+  		});
+
+  		if (!!newIndices) {
+  			this.viewmodel.smartUpdate(keypath, array, newIndices);
+  		} else {
+  			this.viewmodel.mark(keypath);
+  		}
+
+  		global_runloop.end();
+
+  		return promise;
+  	};
+  };
+
+  var pop = makeArrayMethod("pop");
+
+  var push = makeArrayMethod("push");
+
+  var css,
+      update,
+      styleElement,
+      head,
+      styleSheet,
+      inDom,
+      global_css__prefix = "/* Ractive.js component styles */\n",
+      styles = [],
+      dirty = false;
+
+  if (!isClient) {
+  	// TODO handle encapsulated CSS in server-rendered HTML!
+  	css = {
+  		add: noop,
+  		apply: noop
+  	};
+  } else {
+  	styleElement = document.createElement("style");
+  	styleElement.type = "text/css";
+
+  	head = document.getElementsByTagName("head")[0];
+
+  	inDom = false;
+
+  	// Internet Exploder won't let you use styleSheet.innerHTML - we have to
+  	// use styleSheet.cssText instead
+  	styleSheet = styleElement.styleSheet;
+
+  	update = function () {
+  		var css = global_css__prefix + styles.map(function (s) {
+  			return "\n/* {" + s.id + "} */\n" + s.styles;
+  		}).join("\n");
+
+  		if (styleSheet) {
+  			styleSheet.cssText = css;
+  		} else {
+  			styleElement.innerHTML = css;
+  		}
+
+  		if (!inDom) {
+  			head.appendChild(styleElement);
+  			inDom = true;
+  		}
+  	};
+
+  	css = {
+  		add: function (s) {
+  			styles.push(s);
+  			dirty = true;
+  		},
+
+  		apply: function () {
+  			if (dirty) {
+  				update();
+  				dirty = false;
+  			}
+  		}
+  	};
+  }
+
+  var global_css = css;
+
+  var prototype_render = Ractive$render;
+
+  var renderHook = new hooks_Hook("render"),
+      completeHook = new hooks_Hook("complete");
+  function Ractive$render(target, anchor) {
+  	var _this = this;
+
+  	var promise, instances, transitionsEnabled;
+
+  	// if `noIntro` is `true`, temporarily disable transitions
+  	transitionsEnabled = this.transitionsEnabled;
+  	if (this.noIntro) {
+  		this.transitionsEnabled = false;
+  	}
+
+  	promise = global_runloop.start(this, true);
+  	global_runloop.scheduleTask(function () {
+  		return renderHook.fire(_this);
+  	}, true);
+
+  	if (this.fragment.rendered) {
+  		throw new Error("You cannot call ractive.render() on an already rendered instance! Call ractive.unrender() first");
+  	}
+
+  	target = getElement(target) || this.el;
+  	anchor = getElement(anchor) || this.anchor;
+
+  	this.el = target;
+  	this.anchor = anchor;
+
+  	if (!this.append && target) {
+  		// Teardown any existing instances *before* trying to set up the new one -
+  		// avoids certain weird bugs
+  		var others = target.__ractive_instances__;
+  		if (others && others.length) {
+  			removeOtherInstances(others);
+  		}
+
+  		// make sure we are the only occupants
+  		target.innerHTML = ""; // TODO is this quicker than removeChild? Initial research inconclusive
+  	}
+
+  	if (this.cssId) {
+  		// ensure encapsulated CSS is up-to-date
+  		global_css.apply();
+  	}
+
+  	if (target) {
+  		if (!(instances = target.__ractive_instances__)) {
+  			target.__ractive_instances__ = [this];
+  		} else {
+  			instances.push(this);
+  		}
+
+  		if (anchor) {
+  			target.insertBefore(this.fragment.render(), anchor);
+  		} else {
+  			target.appendChild(this.fragment.render());
+  		}
+  	}
+
+  	global_runloop.end();
+
+  	this.transitionsEnabled = transitionsEnabled;
+
+  	return promise.then(function () {
+  		return completeHook.fire(_this);
+  	});
+  }
+
+  function removeOtherInstances(others) {
+  	others.splice(0, others.length).forEach(teardown);
+  }
+
+  var adaptConfigurator = {
+  	extend: function (Parent, proto, options) {
+  		proto.adapt = custom_adapt__combine(proto.adapt, ensureArray(options.adapt));
+  	},
+
+  	init: function () {}
+  };
+
+  var custom_adapt = adaptConfigurator;
+
+  function custom_adapt__combine(a, b) {
+  	var c = a.slice(),
+  	    i = b.length;
+
+  	while (i--) {
+  		if (! ~c.indexOf(b[i])) {
+  			c.push(b[i]);
+  		}
+  	}
+
+  	return c;
+  }
+
+  var transform = transformCss;
+
+  var selectorsPattern = /(?:^|\})?\s*([^\{\}]+)\s*\{/g,
+      commentsPattern = /\/\*.*?\*\//g,
+      selectorUnitPattern = /((?:(?:\[[^\]+]\])|(?:[^\s\+\>\~:]))+)((?::[^\s\+\>\~\(]+(?:\([^\)]+\))?)?\s*[\s\+\>\~]?)\s*/g,
+      mediaQueryPattern = /^@media/,
+      dataRvcGuidPattern = /\[data-ractive-css~="\{[a-z0-9-]+\}"]/g;
+  function transformCss(css, id) {
+  	var transformed, dataAttr, addGuid;
+
+  	dataAttr = "[data-ractive-css~=\"{" + id + "}\"]";
+
+  	addGuid = function (selector) {
+  		var selectorUnits,
+  		    match,
+  		    unit,
+  		    base,
+  		    prepended,
+  		    appended,
+  		    i,
+  		    transformed = [];
+
+  		selectorUnits = [];
+
+  		while (match = selectorUnitPattern.exec(selector)) {
+  			selectorUnits.push({
+  				str: match[0],
+  				base: match[1],
+  				modifiers: match[2]
+  			});
+  		}
+
+  		// For each simple selector within the selector, we need to create a version
+  		// that a) combines with the id, and b) is inside the id
+  		base = selectorUnits.map(extractString);
+
+  		i = selectorUnits.length;
+  		while (i--) {
+  			appended = base.slice();
+
+  			// Pseudo-selectors should go after the attribute selector
+  			unit = selectorUnits[i];
+  			appended[i] = unit.base + dataAttr + unit.modifiers || "";
+
+  			prepended = base.slice();
+  			prepended[i] = dataAttr + " " + prepended[i];
+
+  			transformed.push(appended.join(" "), prepended.join(" "));
+  		}
+
+  		return transformed.join(", ");
+  	};
+
+  	if (dataRvcGuidPattern.test(css)) {
+  		transformed = css.replace(dataRvcGuidPattern, dataAttr);
+  	} else {
+  		transformed = css.replace(commentsPattern, "").replace(selectorsPattern, function (match, $1) {
+  			var selectors, transformed;
+
+  			// don't transform media queries!
+  			if (mediaQueryPattern.test($1)) return match;
+
+  			selectors = $1.split(",").map(trim);
+  			transformed = selectors.map(addGuid).join(", ") + " ";
+
+  			return match.replace($1, transformed);
+  		});
+  	}
+
+  	return transformed;
+  }
+
+  function trim(str) {
+  	if (str.trim) {
+  		return str.trim();
+  	}
+
+  	return str.replace(/^\s+/, "").replace(/\s+$/, "");
+  }
+
+  function extractString(unit) {
+  	return unit.str;
+  }
+
+  var css_css__uid = 1;
+
+  var cssConfigurator = {
+  	name: "css",
+
+  	extend: function (Parent, proto, options) {
+  		if (options.css) {
+  			var id = css_css__uid++;
+  			var styles = options.noCssTransform ? options.css : transform(options.css, id);
+
+  			proto.cssId = id;
+  			global_css.add({ id: id, styles: styles });
+  		}
+  	},
+
+  	init: function () {}
+  };
+
+  var css_css = cssConfigurator;
+
+  function validate(data) {
+  	// Warn if userOptions.data is a non-POJO
+  	if (data && data.constructor !== Object) {
+  		if (typeof data === "function") {} else if (typeof data !== "object") {
+  			fatal("data option must be an object or a function, `" + data + "` is not valid");
+  		} else {
+  			warnIfDebug("If supplied, options.data should be a plain JavaScript object - using a non-POJO as the root object may work, but is discouraged");
+  		}
+  	}
+  }
+
+  var dataConfigurator = {
+  	name: "data",
+
+  	extend: function (Parent, proto, options) {
+  		var key = undefined,
+  		    value = undefined;
+
+  		// check for non-primitives, which could cause mutation-related bugs
+  		if (options.data && isObject(options.data)) {
+  			for (key in options.data) {
+  				value = options.data[key];
+
+  				if (value && typeof value === "object") {
+  					if (isObject(value) || isArray(value)) {
+  						warnIfDebug("Passing a `data` option with object and array properties to Ractive.extend() is discouraged, as mutating them is likely to cause bugs. Consider using a data function instead:\n\n  // this...\n  data: function () {\n    return {\n      myObject: {}\n    };\n  })\n\n  // instead of this:\n  data: {\n    myObject: {}\n  }");
+  					}
+  				}
+  			}
+  		}
+
+  		proto.data = custom_data__combine(proto.data, options.data);
+  	},
+
+  	init: function (Parent, ractive, options) {
+  		var result = custom_data__combine(Parent.prototype.data, options.data);
+
+  		if (typeof result === "function") {
+  			result = result.call(ractive);
+  		}
+
+  		return result || {};
+  	},
+
+  	reset: function (ractive) {
+  		var result = this.init(ractive.constructor, ractive, ractive.viewmodel);
+
+  		ractive.viewmodel.reset(result);
+  		return true;
+  	}
+  };
+
+  var custom_data = dataConfigurator;
+
+  function custom_data__combine(parentValue, childValue) {
+  	validate(childValue);
+
+  	var parentIsFn = typeof parentValue === "function";
+  	var childIsFn = typeof childValue === "function";
+
+  	// Very important, otherwise child instance can become
+  	// the default data object on Ractive or a component.
+  	// then ractive.set() ends up setting on the prototype!
+  	if (!childValue && !parentIsFn) {
+  		childValue = {};
+  	}
+
+  	// Fast path, where we just need to copy properties from
+  	// parent to child
+  	if (!parentIsFn && !childIsFn) {
+  		return fromProperties(childValue, parentValue);
+  	}
+
+  	return function () {
+  		var child = childIsFn ? callDataFunction(childValue, this) : childValue;
+  		var parent = parentIsFn ? callDataFunction(parentValue, this) : parentValue;
+
+  		return fromProperties(child, parent);
+  	};
+  }
+
+  function callDataFunction(fn, context) {
+  	var data = fn.call(context);
+
+  	if (!data) return;
+
+  	if (typeof data !== "object") {
+  		fatal("Data function must return an object");
+  	}
+
+  	if (data.constructor !== Object) {
+  		warnOnceIfDebug("Data function returned something other than a plain JavaScript object. This might work, but is strongly discouraged");
+  	}
+
+  	return data;
+  }
+
+  function fromProperties(primary, secondary) {
+  	if (primary && secondary) {
+  		for (var key in secondary) {
+  			if (!(key in primary)) {
+  				primary[key] = secondary[key];
+  			}
+  		}
+
+  		return primary;
+  	}
+
+  	return primary || secondary;
+  }
+
+  // TODO do we need to support this in the new Ractive() case?
+
+  var Parser,
+      ParseError,
+      parse_Parser__leadingWhitespace = /^\s+/;
+
+  ParseError = function (message) {
+  	this.name = "ParseError";
+  	this.message = message;
+  	try {
+  		throw new Error(message);
+  	} catch (e) {
+  		this.stack = e.stack;
+  	}
+  };
+
+  ParseError.prototype = Error.prototype;
+
+  Parser = function (str, options) {
+  	var items,
+  	    item,
+  	    lineStart = 0;
+
+  	this.str = str;
+  	this.options = options || {};
+  	this.pos = 0;
+
+  	this.lines = this.str.split("\n");
+  	this.lineEnds = this.lines.map(function (line) {
+  		var lineEnd = lineStart + line.length + 1; // +1 for the newline
+
+  		lineStart = lineEnd;
+  		return lineEnd;
+  	}, 0);
+
+  	// Custom init logic
+  	if (this.init) this.init(str, options);
+
+  	items = [];
+
+  	while (this.pos < this.str.length && (item = this.read())) {
+  		items.push(item);
+  	}
+
+  	this.leftover = this.remaining();
+  	this.result = this.postProcess ? this.postProcess(items, options) : items;
+  };
+
+  Parser.prototype = {
+  	read: function (converters) {
+  		var pos, i, len, item;
+
+  		if (!converters) converters = this.converters;
+
+  		pos = this.pos;
+
+  		len = converters.length;
+  		for (i = 0; i < len; i += 1) {
+  			this.pos = pos; // reset for each attempt
+
+  			if (item = converters[i](this)) {
+  				return item;
+  			}
+  		}
+
+  		return null;
+  	},
+
+  	getLinePos: function (char) {
+  		var lineNum = 0,
+  		    lineStart = 0,
+  		    columnNum;
+
+  		while (char >= this.lineEnds[lineNum]) {
+  			lineStart = this.lineEnds[lineNum];
+  			lineNum += 1;
+  		}
+
+  		columnNum = char - lineStart;
+  		return [lineNum + 1, columnNum + 1, char]; // line/col should be one-based, not zero-based!
+  	},
+
+  	error: function (message) {
+  		var pos = this.getLinePos(this.pos);
+  		var lineNum = pos[0];
+  		var columnNum = pos[1];
+
+  		var line = this.lines[pos[0] - 1];
+  		var numTabs = 0;
+  		var annotation = line.replace(/\t/g, function (match, char) {
+  			if (char < pos[1]) {
+  				numTabs += 1;
+  			}
+
+  			return "  ";
+  		}) + "\n" + new Array(pos[1] + numTabs).join(" ") + "^----";
+
+  		var error = new ParseError("" + message + " at line " + lineNum + " character " + columnNum + ":\n" + annotation);
+
+  		error.line = pos[0];
+  		error.character = pos[1];
+  		error.shortMessage = message;
+
+  		throw error;
+  	},
+
+  	matchString: function (string) {
+  		if (this.str.substr(this.pos, string.length) === string) {
+  			this.pos += string.length;
+  			return string;
+  		}
+  	},
+
+  	matchPattern: function (pattern) {
+  		var match;
+
+  		if (match = pattern.exec(this.remaining())) {
+  			this.pos += match[0].length;
+  			return match[1] || match[0];
+  		}
+  	},
+
+  	allowWhitespace: function () {
+  		this.matchPattern(parse_Parser__leadingWhitespace);
+  	},
+
+  	remaining: function () {
+  		return this.str.substring(this.pos);
+  	},
+
+  	nextChar: function () {
+  		return this.str.charAt(this.pos);
+  	}
+  };
+
+  Parser.extend = function (proto) {
+  	var Parent = this,
+  	    Child,
+  	    key;
+
+  	Child = function (str, options) {
+  		Parser.call(this, str, options);
+  	};
+
+  	Child.prototype = create(Parent.prototype);
+
+  	for (key in proto) {
+  		if (hasOwn.call(proto, key)) {
+  			Child.prototype[key] = proto[key];
+  		}
+  	}
+
+  	Child.extend = Parser.extend;
+  	return Child;
+  };
+
+  var parse_Parser = Parser;
+
+  var TEXT = 1;
+  var INTERPOLATOR = 2;
+  var TRIPLE = 3;
+  var SECTION = 4;
+  var INVERTED = 5;
+  var CLOSING = 6;
+  var ELEMENT = 7;
+  var PARTIAL = 8;
+  var COMMENT = 9;
+  var DELIMCHANGE = 10;
+  var ATTRIBUTE = 13;
+  var CLOSING_TAG = 14;
+  var COMPONENT = 15;
+  var YIELDER = 16;
+  var INLINE_PARTIAL = 17;
+  var DOCTYPE = 18;
+
+  var NUMBER_LITERAL = 20;
+  var STRING_LITERAL = 21;
+  var ARRAY_LITERAL = 22;
+  var OBJECT_LITERAL = 23;
+  var BOOLEAN_LITERAL = 24;
+  var REGEXP_LITERAL = 25;
+
+  var GLOBAL = 26;
+  var KEY_VALUE_PAIR = 27;
+
+  var REFERENCE = 30;
+  var REFINEMENT = 31;
+  var MEMBER = 32;
+  var PREFIX_OPERATOR = 33;
+  var BRACKETED = 34;
+  var CONDITIONAL = 35;
+  var INFIX_OPERATOR = 36;
+
+  var INVOCATION = 40;
+
+  var SECTION_IF = 50;
+  var SECTION_UNLESS = 51;
+  var SECTION_EACH = 52;
+  var SECTION_WITH = 53;
+  var SECTION_IF_WITH = 54;
+
+  var ELSE = 60;
+  var ELSEIF = 61;
+
+  var mustache_readDelimiterChange = readDelimiterChange;
+  var delimiterChangePattern = /^[^\s=]+/,
+      whitespacePattern = /^\s+/;
+  function readDelimiterChange(parser) {
+  	var start, opening, closing;
+
+  	if (!parser.matchString("=")) {
+  		return null;
+  	}
+
+  	start = parser.pos;
+
+  	// allow whitespace before new opening delimiter
+  	parser.allowWhitespace();
+
+  	opening = parser.matchPattern(delimiterChangePattern);
+  	if (!opening) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	// allow whitespace (in fact, it's necessary...)
+  	if (!parser.matchPattern(whitespacePattern)) {
+  		return null;
+  	}
+
+  	closing = parser.matchPattern(delimiterChangePattern);
+  	if (!closing) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	// allow whitespace before closing '='
+  	parser.allowWhitespace();
+
+  	if (!parser.matchString("=")) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	return [opening, closing];
+  }
+
+  var readRegexpLiteral = readRegexpLiteral__readNumberLiteral;
+  var regexpPattern = /^(\/(?:[^\n\r\u2028\u2029/\\[]|\\.|\[(?:[^\n\r\u2028\u2029\]\\]|\\.)*])+\/(?:([gimuy])(?![a-z]*\2))*(?![a-zA-Z_$0-9]))/;
+  function readRegexpLiteral__readNumberLiteral(parser) {
+  	var result;
+
+  	if (result = parser.matchPattern(regexpPattern)) {
+  		return {
+  			t: REGEXP_LITERAL,
+  			v: result
+  		};
+  	}
+
+  	return null;
+  }
+
+  var converters_readMustache = readMustache;
+
+  var delimiterChangeToken = { t: DELIMCHANGE, exclude: true };
+  function readMustache(parser) {
+  	var mustache, i;
+
+  	// If we're inside a <script> or <style> tag, and we're not
+  	// interpolating, bug out
+  	if (parser.interpolate[parser.inside] === false) {
+  		return null;
+  	}
+
+  	for (i = 0; i < parser.tags.length; i += 1) {
+  		if (mustache = readMustacheOfType(parser, parser.tags[i])) {
+  			return mustache;
+  		}
+  	}
+  }
+
+  function readMustacheOfType(parser, tag) {
+  	var start, mustache, reader, i;
+
+  	start = parser.pos;
+
+  	if (parser.matchString("\\" + tag.open)) {
+  		if (start === 0 || parser.str[start - 1] !== "\\") {
+  			return tag.open;
+  		}
+  	} else if (!parser.matchString(tag.open)) {
+  		return null;
+  	}
+
+  	// delimiter change?
+  	if (mustache = mustache_readDelimiterChange(parser)) {
+  		// find closing delimiter or abort...
+  		if (!parser.matchString(tag.close)) {
+  			return null;
+  		}
+
+  		// ...then make the switch
+  		tag.open = mustache[0];
+  		tag.close = mustache[1];
+  		parser.sortMustacheTags();
+
+  		return delimiterChangeToken;
+  	}
+
+  	parser.allowWhitespace();
+
+  	// illegal section closer
+  	if (parser.matchString("/")) {
+  		parser.pos -= 1;
+  		var rewind = parser.pos;
+  		if (!readRegexpLiteral(parser)) {
+  			parser.pos = rewind - tag.close.length;
+  			parser.error("Attempted to close a section that wasn't open");
+  		} else {
+  			parser.pos = rewind;
+  		}
+  	}
+
+  	for (i = 0; i < tag.readers.length; i += 1) {
+  		reader = tag.readers[i];
+
+  		if (mustache = reader(parser, tag)) {
+  			if (tag.isStatic) {
+  				mustache.s = true; // TODO make this `1` instead - more compact
+  			}
+
+  			if (parser.includeLinePositions) {
+  				mustache.p = parser.getLinePos(start);
+  			}
+
+  			return mustache;
+  		}
+  	}
+
+  	parser.pos = start;
+  	return null;
+  }
+
+  var expectedExpression = "Expected a JavaScript expression";
+  var expectedParen = "Expected closing paren";
+
+  var literal_readNumberLiteral = literal_readNumberLiteral__readNumberLiteral;
+  var literal_readNumberLiteral__numberPattern = /^(?:[+-]?)0*(?:(?:(?:[1-9]\d*)?\.\d+)|(?:(?:0|[1-9]\d*)\.)|(?:0|[1-9]\d*))(?:[eE][+-]?\d+)?/;
+  function literal_readNumberLiteral__readNumberLiteral(parser) {
+  	var result;
+
+  	if (result = parser.matchPattern(literal_readNumberLiteral__numberPattern)) {
+  		return {
+  			t: NUMBER_LITERAL,
+  			v: result
+  		};
+  	}
+
+  	return null;
+  }
+
+  var literal_readBooleanLiteral = readBooleanLiteral;
+  function readBooleanLiteral(parser) {
+  	var remaining = parser.remaining();
+
+  	if (remaining.substr(0, 4) === "true") {
+  		parser.pos += 4;
+  		return {
+  			t: BOOLEAN_LITERAL,
+  			v: "true"
+  		};
+  	}
+
+  	if (remaining.substr(0, 5) === "false") {
+  		parser.pos += 5;
+  		return {
+  			t: BOOLEAN_LITERAL,
+  			v: "false"
+  		};
+  	}
+
+  	return null;
+  }
+
+  var stringMiddlePattern, escapeSequencePattern, lineContinuationPattern;
+
+  // Match one or more characters until: ", ', \, or EOL/EOF.
+  // EOL/EOF is written as (?!.) (meaning there's no non-newline char next).
+  stringMiddlePattern = /^(?=.)[^"'\\]+?(?:(?!.)|(?=["'\\]))/;
+
+  // Match one escape sequence, including the backslash.
+  escapeSequencePattern = /^\\(?:['"\\bfnrt]|0(?![0-9])|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|(?=.)[^ux0-9])/;
+
+  // Match one ES5 line continuation (backslash + line terminator).
+  lineContinuationPattern = /^\\(?:\r\n|[\u000A\u000D\u2028\u2029])/;
+
+  // Helper for defining getDoubleQuotedString and getSingleQuotedString.
+  var makeQuotedStringMatcher = function (okQuote) {
+  	return function (parser) {
+  		var start, literal, done, next;
+
+  		start = parser.pos;
+  		literal = "\"";
+  		done = false;
+
+  		while (!done) {
+  			next = parser.matchPattern(stringMiddlePattern) || parser.matchPattern(escapeSequencePattern) || parser.matchString(okQuote);
+  			if (next) {
+  				if (next === "\"") {
+  					literal += "\\\"";
+  				} else if (next === "\\'") {
+  					literal += "'";
+  				} else {
+  					literal += next;
+  				}
+  			} else {
+  				next = parser.matchPattern(lineContinuationPattern);
+  				if (next) {
+  					// convert \(newline-like) into a \u escape, which is allowed in JSON
+  					literal += "\\u" + ("000" + next.charCodeAt(1).toString(16)).slice(-4);
+  				} else {
+  					done = true;
+  				}
+  			}
+  		}
+
+  		literal += "\"";
+
+  		// use JSON.parse to interpret escapes
+  		return JSON.parse(literal);
+  	};
+  };
+
+  var getSingleQuotedString = makeQuotedStringMatcher("\"");
+  var getDoubleQuotedString = makeQuotedStringMatcher("'");
+
+  var readStringLiteral = function (parser) {
+  	var start, string;
+
+  	start = parser.pos;
+
+  	if (parser.matchString("\"")) {
+  		string = getDoubleQuotedString(parser);
+
+  		if (!parser.matchString("\"")) {
+  			parser.pos = start;
+  			return null;
+  		}
+
+  		return {
+  			t: STRING_LITERAL,
+  			v: string
+  		};
+  	}
+
+  	if (parser.matchString("'")) {
+  		string = getSingleQuotedString(parser);
+
+  		if (!parser.matchString("'")) {
+  			parser.pos = start;
+  			return null;
+  		}
+
+  		return {
+  			t: STRING_LITERAL,
+  			v: string
+  		};
+  	}
+
+  	return null;
+  };
+
+  var patterns__name = /^[a-zA-Z_$][a-zA-Z_$0-9]*/;
+
+  // http://mathiasbynens.be/notes/javascript-properties
+  // can be any name, string literal, or number literal
+  var shared_readKey = readKey;
+  var identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
+  function readKey(parser) {
+  	var token;
+
+  	if (token = readStringLiteral(parser)) {
+  		return identifier.test(token.v) ? token.v : "\"" + token.v.replace(/"/g, "\\\"") + "\"";
+  	}
+
+  	if (token = literal_readNumberLiteral(parser)) {
+  		return token.v;
+  	}
+
+  	if (token = parser.matchPattern(patterns__name)) {
+  		return token;
+  	}
+  }
+
+  var keyValuePair = readKeyValuePair;
+  function readKeyValuePair(parser) {
+  	var start, key, value;
+
+  	start = parser.pos;
+
+  	// allow whitespace between '{' and key
+  	parser.allowWhitespace();
+
+  	key = shared_readKey(parser);
+  	if (key === null) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	// allow whitespace between key and ':'
+  	parser.allowWhitespace();
+
+  	// next character must be ':'
+  	if (!parser.matchString(":")) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	// allow whitespace between ':' and value
+  	parser.allowWhitespace();
+
+  	// next expression must be a, well... expression
+  	value = converters_readExpression(parser);
+  	if (value === null) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	return {
+  		t: KEY_VALUE_PAIR,
+  		k: key,
+  		v: value
+  	};
+  }
+
+  var objectLiteral_keyValuePairs = readKeyValuePairs;
+  function readKeyValuePairs(parser) {
+  	var start, pairs, pair, keyValuePairs;
+
+  	start = parser.pos;
+
+  	pair = keyValuePair(parser);
+  	if (pair === null) {
+  		return null;
+  	}
+
+  	pairs = [pair];
+
+  	if (parser.matchString(",")) {
+  		keyValuePairs = readKeyValuePairs(parser);
+
+  		if (!keyValuePairs) {
+  			parser.pos = start;
+  			return null;
+  		}
+
+  		return pairs.concat(keyValuePairs);
+  	}
+
+  	return pairs;
+  }
+
+  var readObjectLiteral = function (parser) {
+  	var start, keyValuePairs;
+
+  	start = parser.pos;
+
+  	// allow whitespace
+  	parser.allowWhitespace();
+
+  	if (!parser.matchString("{")) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	keyValuePairs = objectLiteral_keyValuePairs(parser);
+
+  	// allow whitespace between final value and '}'
+  	parser.allowWhitespace();
+
+  	if (!parser.matchString("}")) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	return {
+  		t: OBJECT_LITERAL,
+  		m: keyValuePairs
+  	};
+  };
+
+  var shared_readExpressionList = readExpressionList;
+  function readExpressionList(parser) {
+  	var start, expressions, expr, next;
+
+  	start = parser.pos;
+
+  	parser.allowWhitespace();
+
+  	expr = converters_readExpression(parser);
+
+  	if (expr === null) {
+  		return null;
+  	}
+
+  	expressions = [expr];
+
+  	// allow whitespace between expression and ','
+  	parser.allowWhitespace();
+
+  	if (parser.matchString(",")) {
+  		next = readExpressionList(parser);
+  		if (next === null) {
+  			parser.error(expectedExpression);
+  		}
+
+  		next.forEach(append);
+  	}
+
+  	function append(expression) {
+  		expressions.push(expression);
+  	}
+
+  	return expressions;
+  }
+
+  var readArrayLiteral = function (parser) {
+  	var start, expressionList;
+
+  	start = parser.pos;
+
+  	// allow whitespace before '['
+  	parser.allowWhitespace();
+
+  	if (!parser.matchString("[")) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	expressionList = shared_readExpressionList(parser);
+
+  	if (!parser.matchString("]")) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	return {
+  		t: ARRAY_LITERAL,
+  		m: expressionList
+  	};
+  };
+
+  var primary_readLiteral = readLiteral;
+  function readLiteral(parser) {
+  	return literal_readNumberLiteral(parser) || literal_readBooleanLiteral(parser) || readStringLiteral(parser) || readObjectLiteral(parser) || readArrayLiteral(parser) || readRegexpLiteral(parser);
+  }
+
+  var primary_readReference = readReference;
+  var prefixPattern = /^(?:~\/|(?:\.\.\/)+|\.\/(?:\.\.\/)*|\.)/,
+      globals,
+      keywords;
+
+  // if a reference is a browser global, we don't deference it later, so it needs special treatment
+  globals = /^(?:Array|console|Date|RegExp|decodeURIComponent|decodeURI|encodeURIComponent|encodeURI|isFinite|isNaN|parseFloat|parseInt|JSON|Math|NaN|undefined|null)\b/;
+
+  // keywords are not valid references, with the exception of `this`
+  keywords = /^(?:break|case|catch|continue|debugger|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|throw|try|typeof|var|void|while|with)$/;
+
+  var legalReference = /^[a-zA-Z$_0-9]+(?:(?:\.[a-zA-Z$_0-9]+)|(?:\[[0-9]+\]))*/;
+  var relaxedName = /^[a-zA-Z_$][-a-zA-Z_$0-9]*/;
+  function readReference(parser) {
+  	var startPos, prefix, name, global, reference, lastDotIndex;
+
+  	startPos = parser.pos;
+
+  	name = parser.matchPattern(/^@(?:keypath|index|key)/);
+
+  	if (!name) {
+  		prefix = parser.matchPattern(prefixPattern) || "";
+  		name = !prefix && parser.relaxedNames && parser.matchPattern(relaxedName) || parser.matchPattern(legalReference);
+
+  		if (!name && prefix === ".") {
+  			prefix = "";
+  			name = ".";
+  		}
+  	}
+
+  	if (!name) {
+  		return null;
+  	}
+
+  	// bug out if it's a keyword (exception for ancestor/restricted refs - see https://github.com/ractivejs/ractive/issues/1497)
+  	if (!prefix && !parser.relaxedNames && keywords.test(name)) {
+  		parser.pos = startPos;
+  		return null;
+  	}
+
+  	// if this is a browser global, stop here
+  	if (!prefix && globals.test(name)) {
+  		global = globals.exec(name)[0];
+  		parser.pos = startPos + global.length;
+
+  		return {
+  			t: GLOBAL,
+  			v: global
+  		};
+  	}
+
+  	reference = (prefix || "") + normalise(name);
+
+  	if (parser.matchString("(")) {
+  		// if this is a method invocation (as opposed to a function) we need
+  		// to strip the method name from the reference combo, else the context
+  		// will be wrong
+  		lastDotIndex = reference.lastIndexOf(".");
+  		if (lastDotIndex !== -1) {
+  			reference = reference.substr(0, lastDotIndex);
+  			parser.pos = startPos + reference.length;
+  		} else {
+  			parser.pos -= 1;
+  		}
+  	}
+
+  	return {
+  		t: REFERENCE,
+  		n: reference.replace(/^this\./, "./").replace(/^this$/, ".")
+  	};
+  }
+
+  var primary_readBracketedExpression = readBracketedExpression;
+  function readBracketedExpression(parser) {
+  	var start, expr;
+
+  	start = parser.pos;
+
+  	if (!parser.matchString("(")) {
+  		return null;
+  	}
+
+  	parser.allowWhitespace();
+
+  	expr = converters_readExpression(parser);
+  	if (!expr) {
+  		parser.error(expectedExpression);
+  	}
+
+  	parser.allowWhitespace();
+
+  	if (!parser.matchString(")")) {
+  		parser.error(expectedParen);
+  	}
+
+  	return {
+  		t: BRACKETED,
+  		x: expr
+  	};
+  }
+
+  var readPrimary = function (parser) {
+  	return primary_readLiteral(parser) || primary_readReference(parser) || primary_readBracketedExpression(parser);
+  };
+
+  var shared_readRefinement = readRefinement;
+  function readRefinement(parser) {
+  	var start, name, expr;
+
+  	start = parser.pos;
+
+  	parser.allowWhitespace();
+
+  	// "." name
+  	if (parser.matchString(".")) {
+  		parser.allowWhitespace();
+
+  		if (name = parser.matchPattern(patterns__name)) {
+  			return {
+  				t: REFINEMENT,
+  				n: name
+  			};
+  		}
+
+  		parser.error("Expected a property name");
+  	}
+
+  	// "[" expression "]"
+  	if (parser.matchString("[")) {
+  		parser.allowWhitespace();
+
+  		expr = converters_readExpression(parser);
+  		if (!expr) {
+  			parser.error(expectedExpression);
+  		}
+
+  		parser.allowWhitespace();
+
+  		if (!parser.matchString("]")) {
+  			parser.error("Expected ']'");
+  		}
+
+  		return {
+  			t: REFINEMENT,
+  			x: expr
+  		};
+  	}
+
+  	return null;
+  }
+
+  var readMemberOrInvocation = function (parser) {
+  	var current, expression, refinement, expressionList;
+
+  	expression = readPrimary(parser);
+
+  	if (!expression) {
+  		return null;
+  	}
+
+  	while (expression) {
+  		current = parser.pos;
+
+  		if (refinement = shared_readRefinement(parser)) {
+  			expression = {
+  				t: MEMBER,
+  				x: expression,
+  				r: refinement
+  			};
+  		} else if (parser.matchString("(")) {
+  			parser.allowWhitespace();
+  			expressionList = shared_readExpressionList(parser);
+
+  			parser.allowWhitespace();
+
+  			if (!parser.matchString(")")) {
+  				parser.error(expectedParen);
+  			}
+
+  			expression = {
+  				t: INVOCATION,
+  				x: expression
+  			};
+
+  			if (expressionList) {
+  				expression.o = expressionList;
+  			}
+  		} else {
+  			break;
+  		}
+  	}
+
+  	return expression;
+  };
+
+  var readTypeOf, makePrefixSequenceMatcher;
+
+  makePrefixSequenceMatcher = function (symbol, fallthrough) {
+  	return function (parser) {
+  		var expression;
+
+  		if (expression = fallthrough(parser)) {
+  			return expression;
+  		}
+
+  		if (!parser.matchString(symbol)) {
+  			return null;
+  		}
+
+  		parser.allowWhitespace();
+
+  		expression = converters_readExpression(parser);
+  		if (!expression) {
+  			parser.error(expectedExpression);
+  		}
+
+  		return {
+  			s: symbol,
+  			o: expression,
+  			t: PREFIX_OPERATOR
+  		};
+  	};
+  };
+
+  // create all prefix sequence matchers, return readTypeOf
+  (function () {
+  	var i, len, matcher, prefixOperators, fallthrough;
+
+  	prefixOperators = "! ~ + - typeof".split(" ");
+
+  	fallthrough = readMemberOrInvocation;
+  	for (i = 0, len = prefixOperators.length; i < len; i += 1) {
+  		matcher = makePrefixSequenceMatcher(prefixOperators[i], fallthrough);
+  		fallthrough = matcher;
+  	}
+
+  	// typeof operator is higher precedence than multiplication, so provides the
+  	// fallthrough for the multiplication sequence matcher we're about to create
+  	// (we're skipping void and delete)
+  	readTypeOf = fallthrough;
+  })();
+
+  var readTypeof = readTypeOf;
+
+  var readLogicalOr, makeInfixSequenceMatcher;
+
+  makeInfixSequenceMatcher = function (symbol, fallthrough) {
+  	return function (parser) {
+  		var start, left, right;
+
+  		left = fallthrough(parser);
+  		if (!left) {
+  			return null;
+  		}
+
+  		// Loop to handle left-recursion in a case like `a * b * c` and produce
+  		// left association, i.e. `(a * b) * c`.  The matcher can't call itself
+  		// to parse `left` because that would be infinite regress.
+  		while (true) {
+  			start = parser.pos;
+
+  			parser.allowWhitespace();
+
+  			if (!parser.matchString(symbol)) {
+  				parser.pos = start;
+  				return left;
+  			}
+
+  			// special case - in operator must not be followed by [a-zA-Z_$0-9]
+  			if (symbol === "in" && /[a-zA-Z_$0-9]/.test(parser.remaining().charAt(0))) {
+  				parser.pos = start;
+  				return left;
+  			}
+
+  			parser.allowWhitespace();
+
+  			// right operand must also consist of only higher-precedence operators
+  			right = fallthrough(parser);
+  			if (!right) {
+  				parser.pos = start;
+  				return left;
+  			}
+
+  			left = {
+  				t: INFIX_OPERATOR,
+  				s: symbol,
+  				o: [left, right]
+  			};
+
+  			// Loop back around.  If we don't see another occurrence of the symbol,
+  			// we'll return left.
+  		}
+  	};
+  };
+
+  // create all infix sequence matchers, and return readLogicalOr
+  (function () {
+  	var i, len, matcher, infixOperators, fallthrough;
+
+  	// All the infix operators on order of precedence (source: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/Operator_Precedence)
+  	// Each sequence matcher will initially fall through to its higher precedence
+  	// neighbour, and only attempt to match if one of the higher precedence operators
+  	// (or, ultimately, a literal, reference, or bracketed expression) already matched
+  	infixOperators = "* / % + - << >> >>> < <= > >= in instanceof == != === !== & ^ | && ||".split(" ");
+
+  	// A typeof operator is higher precedence than multiplication
+  	fallthrough = readTypeof;
+  	for (i = 0, len = infixOperators.length; i < len; i += 1) {
+  		matcher = makeInfixSequenceMatcher(infixOperators[i], fallthrough);
+  		fallthrough = matcher;
+  	}
+
+  	// Logical OR is the fallthrough for the conditional matcher
+  	readLogicalOr = fallthrough;
+  })();
+
+  var expressions_readLogicalOr = readLogicalOr;
+
+  // The conditional operator is the lowest precedence operator, so we start here
+  var readConditional = getConditional;
+  function getConditional(parser) {
+  	var start, expression, ifTrue, ifFalse;
+
+  	expression = expressions_readLogicalOr(parser);
+  	if (!expression) {
+  		return null;
+  	}
+
+  	start = parser.pos;
+
+  	parser.allowWhitespace();
+
+  	if (!parser.matchString("?")) {
+  		parser.pos = start;
+  		return expression;
+  	}
+
+  	parser.allowWhitespace();
+
+  	ifTrue = converters_readExpression(parser);
+  	if (!ifTrue) {
+  		parser.error(expectedExpression);
+  	}
+
+  	parser.allowWhitespace();
+
+  	if (!parser.matchString(":")) {
+  		parser.error("Expected \":\"");
+  	}
+
+  	parser.allowWhitespace();
+
+  	ifFalse = converters_readExpression(parser);
+  	if (!ifFalse) {
+  		parser.error(expectedExpression);
+  	}
+
+  	return {
+  		t: CONDITIONAL,
+  		o: [expression, ifTrue, ifFalse]
+  	};
+  }
+
+  var converters_readExpression = readExpression;
+  function readExpression(parser) {
+  	// The conditional operator is the lowest precedence operator (except yield,
+  	// assignment operators, and commas, none of which are supported), so we
+  	// start there. If it doesn't match, it 'falls through' to progressively
+  	// higher precedence operators, until it eventually matches (or fails to
+  	// match) a 'primary' - a literal or a reference. This way, the abstract syntax
+  	// tree has everything in its proper place, i.e. 2 + 3 * 4 === 14, not 20.
+  	return readConditional(parser);
+  }
+
+  var utils_flattenExpression = flattenExpression;
+
+  function flattenExpression(expression) {
+  	var refs;
+
+  	extractRefs(expression, refs = []);
+
+  	return {
+  		r: refs,
+  		s: stringify(expression)
+  	};
+
+  	function stringify(node) {
+  		switch (node.t) {
+  			case BOOLEAN_LITERAL:
+  			case GLOBAL:
+  			case NUMBER_LITERAL:
+  			case REGEXP_LITERAL:
+  				return node.v;
+
+  			case STRING_LITERAL:
+  				return JSON.stringify(String(node.v));
+
+  			case ARRAY_LITERAL:
+  				return "[" + (node.m ? node.m.map(stringify).join(",") : "") + "]";
+
+  			case OBJECT_LITERAL:
+  				return "{" + (node.m ? node.m.map(stringify).join(",") : "") + "}";
+
+  			case KEY_VALUE_PAIR:
+  				return node.k + ":" + stringify(node.v);
+
+  			case PREFIX_OPERATOR:
+  				return (node.s === "typeof" ? "typeof " : node.s) + stringify(node.o);
+
+  			case INFIX_OPERATOR:
+  				return stringify(node.o[0]) + (node.s.substr(0, 2) === "in" ? " " + node.s + " " : node.s) + stringify(node.o[1]);
+
+  			case INVOCATION:
+  				return stringify(node.x) + "(" + (node.o ? node.o.map(stringify).join(",") : "") + ")";
+
+  			case BRACKETED:
+  				return "(" + stringify(node.x) + ")";
+
+  			case MEMBER:
+  				return stringify(node.x) + stringify(node.r);
+
+  			case REFINEMENT:
+  				return node.n ? "." + node.n : "[" + stringify(node.x) + "]";
+
+  			case CONDITIONAL:
+  				return stringify(node.o[0]) + "?" + stringify(node.o[1]) + ":" + stringify(node.o[2]);
+
+  			case REFERENCE:
+  				return "_" + refs.indexOf(node.n);
+
+  			default:
+  				throw new Error("Expected legal JavaScript");
+  		}
+  	}
+  }
+
+  // TODO maybe refactor this?
+  function extractRefs(node, refs) {
+  	var i, list;
+
+  	if (node.t === REFERENCE) {
+  		if (refs.indexOf(node.n) === -1) {
+  			refs.unshift(node.n);
+  		}
+  	}
+
+  	list = node.o || node.m;
+  	if (list) {
+  		if (isObject(list)) {
+  			extractRefs(list, refs);
+  		} else {
+  			i = list.length;
+  			while (i--) {
+  				extractRefs(list[i], refs);
+  			}
+  		}
+  	}
+
+  	if (node.x) {
+  		extractRefs(node.x, refs);
+  	}
+
+  	if (node.r) {
+  		extractRefs(node.r, refs);
+  	}
+
+  	if (node.v) {
+  		extractRefs(node.v, refs);
+  	}
+  }
+
+  var utils_refineExpression = refineExpression;
+
+  var arrayMemberPattern = /^[0-9][1-9]*$/;
+  function refineExpression(expression, mustache) {
+  	var referenceExpression;
+
+  	if (expression) {
+  		while (expression.t === BRACKETED && expression.x) {
+  			expression = expression.x;
+  		}
+
+  		// special case - integers should be treated as array members references,
+  		// rather than as expressions in their own right
+  		if (expression.t === REFERENCE) {
+  			mustache.r = expression.n;
+  		} else {
+  			if (expression.t === NUMBER_LITERAL && arrayMemberPattern.test(expression.v)) {
+  				mustache.r = expression.v;
+  			} else if (referenceExpression = getReferenceExpression(expression)) {
+  				mustache.rx = referenceExpression;
+  			} else {
+  				mustache.x = utils_flattenExpression(expression);
+  			}
+  		}
+
+  		return mustache;
+  	}
+  }
+
+  // TODO refactor this! it's bewildering
+  function getReferenceExpression(expression) {
+  	var members = [],
+  	    refinement;
+
+  	while (expression.t === MEMBER && expression.r.t === REFINEMENT) {
+  		refinement = expression.r;
+
+  		if (refinement.x) {
+  			if (refinement.x.t === REFERENCE) {
+  				members.unshift(refinement.x);
+  			} else {
+  				members.unshift(utils_flattenExpression(refinement.x));
+  			}
+  		} else {
+  			members.unshift(refinement.n);
+  		}
+
+  		expression = expression.x;
+  	}
+
+  	if (expression.t !== REFERENCE) {
+  		return null;
+  	}
+
+  	return {
+  		r: expression.n,
+  		m: members
+  	};
+  }
+
+  var mustache_readTriple = readTriple;
+  function readTriple(parser, tag) {
+  	var expression = converters_readExpression(parser),
+  	    triple;
+
+  	if (!expression) {
+  		return null;
+  	}
+
+  	if (!parser.matchString(tag.close)) {
+  		parser.error("Expected closing delimiter '" + tag.close + "'");
+  	}
+
+  	triple = { t: TRIPLE };
+  	utils_refineExpression(expression, triple); // TODO handle this differently - it's mysterious
+
+  	return triple;
+  }
+
+  var mustache_readUnescaped = readUnescaped;
+  function readUnescaped(parser, tag) {
+  	var expression, triple;
+
+  	if (!parser.matchString("&")) {
+  		return null;
+  	}
+
+  	parser.allowWhitespace();
+
+  	expression = converters_readExpression(parser);
+
+  	if (!expression) {
+  		return null;
+  	}
+
+  	if (!parser.matchString(tag.close)) {
+  		parser.error("Expected closing delimiter '" + tag.close + "'");
+  	}
+
+  	triple = { t: TRIPLE };
+  	utils_refineExpression(expression, triple); // TODO handle this differently - it's mysterious
+
+  	return triple;
+  }
+
+  var mustache_readPartial = readPartial;
+  function readPartial(parser, tag) {
+  	var start, nameStart, expression, context, partial;
+
+  	start = parser.pos;
+
+  	if (!parser.matchString(">")) {
+  		return null;
+  	}
+
+  	parser.allowWhitespace();
+  	nameStart = parser.pos;
+
+  	// Partial names can include hyphens, so we can't use readExpression
+  	// blindly. Instead, we use the `relaxedNames` flag to indicate that
+  	// `foo-bar` should be read as a single name, rather than 'subtract
+  	// bar from foo'
+  	parser.relaxedNames = true;
+  	expression = converters_readExpression(parser);
+  	parser.relaxedNames = false;
+
+  	parser.allowWhitespace();
+  	context = converters_readExpression(parser);
+  	parser.allowWhitespace();
+
+  	if (!expression) {
+  		return null;
+  	}
+
+  	partial = { t: PARTIAL };
+  	utils_refineExpression(expression, partial); // TODO...
+
+  	parser.allowWhitespace();
+
+  	// if we have another expression - e.g. `{{>foo bar}}` - then
+  	// we turn it into `{{#with bar}}{{>foo}}{{/with}}`
+  	if (context) {
+  		partial = {
+  			t: SECTION,
+  			n: SECTION_WITH,
+  			f: [partial]
+  		};
+
+  		utils_refineExpression(context, partial);
+  	}
+
+  	if (!parser.matchString(tag.close)) {
+  		parser.error("Expected closing delimiter '" + tag.close + "'");
+  	}
+
+  	return partial;
+  }
+
+  var readMustacheComment = readComment;
+  function readComment(parser, tag) {
+  	var index;
+
+  	if (!parser.matchString("!")) {
+  		return null;
+  	}
+
+  	index = parser.remaining().indexOf(tag.close);
+
+  	if (index !== -1) {
+  		parser.pos += index + tag.close.length;
+  		return { t: COMMENT };
+  	}
+  }
+
+  var converters_readExpressionOrReference = readExpressionOrReference;
+  function readExpressionOrReference(parser, expectedFollowers) {
+  	var start, expression, i;
+
+  	start = parser.pos;
+  	expression = converters_readExpression(parser);
+
+  	if (!expression) {
+  		return null;
+  	}
+
+  	for (i = 0; i < expectedFollowers.length; i += 1) {
+  		if (parser.remaining().substr(0, expectedFollowers[i].length) === expectedFollowers[i]) {
+  			return expression;
+  		}
+  	}
+
+  	parser.pos = start;
+  	return primary_readReference(parser);
+  }
+
+  var mustache_readInterpolator = readInterpolator;
+  function readInterpolator(parser, tag) {
+  	var start, expression, interpolator, err;
+
+  	start = parser.pos;
+
+  	// TODO would be good for perf if we could do away with the try-catch
+  	try {
+  		expression = converters_readExpressionOrReference(parser, [tag.close]);
+  	} catch (e) {
+  		err = e;
+  	}
+
+  	if (!expression) {
+  		if (parser.str.charAt(start) === "!") {
+  			// special case - comment
+  			parser.pos = start;
+  			return null;
+  		}
+
+  		if (err) {
+  			throw err;
+  		}
+  	}
+
+  	if (!parser.matchString(tag.close)) {
+  		parser.error("Expected closing delimiter '" + tag.close + "' after reference");
+
+  		if (!expression) {
+  			// special case - comment
+  			if (parser.nextChar() === "!") {
+  				return null;
+  			}
+
+  			parser.error("Expected expression or legal reference");
+  		}
+  	}
+
+  	interpolator = { t: INTERPOLATOR };
+  	utils_refineExpression(expression, interpolator); // TODO handle this differently - it's mysterious
+
+  	return interpolator;
+  }
+
+  var mustache_readYielder = readYielder;
+  var yieldPattern = /^yield\s*/;
+  function readYielder(parser, tag) {
+  	var start, name, yielder;
+
+  	if (!parser.matchPattern(yieldPattern)) {
+  		return null;
+  	}
+
+  	start = parser.pos;
+  	name = parser.matchPattern(/^[a-zA-Z_$][a-zA-Z_$0-9\-]*/);
+
+  	parser.allowWhitespace();
+
+  	if (!parser.matchString(tag.close)) {
+  		parser.error("expected legal partial name");
+  	}
+
+  	yielder = { t: YIELDER };
+
+  	if (name) {
+  		yielder.n = name;
+  	}
+
+  	return yielder;
+  }
+
+  var section_readClosing = readClosing;
+  function readClosing(parser, tag) {
+  	var start, remaining, index, closing;
+
+  	start = parser.pos;
+
+  	if (!parser.matchString(tag.open)) {
+  		return null;
+  	}
+
+  	parser.allowWhitespace();
+
+  	if (!parser.matchString("/")) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	parser.allowWhitespace();
+
+  	remaining = parser.remaining();
+  	index = remaining.indexOf(tag.close);
+
+  	if (index !== -1) {
+  		closing = {
+  			t: CLOSING,
+  			r: remaining.substr(0, index).split(" ")[0]
+  		};
+
+  		parser.pos += index;
+
+  		if (!parser.matchString(tag.close)) {
+  			parser.error("Expected closing delimiter '" + tag.close + "'");
+  		}
+
+  		return closing;
+  	}
+
+  	parser.pos = start;
+  	return null;
+  }
+
+  var section_readElse = section_readElse__readElse;
+  var section_readElse__elsePattern = /^\s*else\s*/;
+  function section_readElse__readElse(parser, tag) {
+  	var start = parser.pos;
+
+  	if (!parser.matchString(tag.open)) {
+  		return null;
+  	}
+
+  	if (!parser.matchPattern(section_readElse__elsePattern)) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	if (!parser.matchString(tag.close)) {
+  		parser.error("Expected closing delimiter '" + tag.close + "'");
+  	}
+
+  	return {
+  		t: ELSE
+  	};
+  }
+
+  var readElseIf = readElseIf__readElse;
+  var readElseIf__elsePattern = /^\s*elseif\s+/;
+  function readElseIf__readElse(parser, tag) {
+  	var start = parser.pos,
+  	    expression;
+
+  	if (!parser.matchString(tag.open)) {
+  		return null;
+  	}
+
+  	if (!parser.matchPattern(readElseIf__elsePattern)) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	expression = converters_readExpression(parser);
+
+  	if (!parser.matchString(tag.close)) {
+  		parser.error("Expected closing delimiter '" + tag.close + "'");
+  	}
+
+  	return {
+  		t: ELSEIF,
+  		x: expression
+  	};
+  }
+
+  var handlebarsBlockCodes = {
+  	each: SECTION_EACH,
+  	"if": SECTION_IF,
+  	"if-with": SECTION_IF_WITH,
+  	"with": SECTION_WITH,
+  	unless: SECTION_UNLESS
+  };
+
+  var mustache_readSection = readSection;
+
+  var indexRefPattern = /^\s*:\s*([a-zA-Z_$][a-zA-Z_$0-9]*)/,
+      keyIndexRefPattern = /^\s*,\s*([a-zA-Z_$][a-zA-Z_$0-9]*)/,
+      handlebarsBlockPattern = new RegExp("^(" + Object.keys(handlebarsBlockCodes).join("|") + ")\\b");
+  function readSection(parser, tag) {
+  	var start, expression, section, child, children, hasElse, block, unlessBlock, conditions, closed, i, expectedClose;
+
+  	start = parser.pos;
+
+  	if (parser.matchString("^")) {
+  		section = { t: SECTION, f: [], n: SECTION_UNLESS };
+  	} else if (parser.matchString("#")) {
+  		section = { t: SECTION, f: [] };
+
+  		if (parser.matchString("partial")) {
+  			parser.pos = start - parser.standardDelimiters[0].length;
+  			parser.error("Partial definitions can only be at the top level of the template, or immediately inside components");
+  		}
+
+  		if (block = parser.matchPattern(handlebarsBlockPattern)) {
+  			expectedClose = block;
+  			section.n = handlebarsBlockCodes[block];
+  		}
+  	} else {
+  		return null;
+  	}
+
+  	parser.allowWhitespace();
+
+  	expression = converters_readExpression(parser);
+
+  	if (!expression) {
+  		parser.error("Expected expression");
+  	}
+
+  	// optional index and key references
+  	if (i = parser.matchPattern(indexRefPattern)) {
+  		var extra = undefined;
+
+  		if (extra = parser.matchPattern(keyIndexRefPattern)) {
+  			section.i = i + "," + extra;
+  		} else {
+  			section.i = i;
+  		}
+  	}
+
+  	parser.allowWhitespace();
+
+  	if (!parser.matchString(tag.close)) {
+  		parser.error("Expected closing delimiter '" + tag.close + "'");
+  	}
+
+  	parser.sectionDepth += 1;
+  	children = section.f;
+
+  	conditions = [];
+
+  	do {
+  		if (child = section_readClosing(parser, tag)) {
+  			if (expectedClose && child.r !== expectedClose) {
+  				parser.error("Expected " + tag.open + "/" + expectedClose + "" + tag.close);
+  			}
+
+  			parser.sectionDepth -= 1;
+  			closed = true;
+  		} else if (child = readElseIf(parser, tag)) {
+  			if (section.n === SECTION_UNLESS) {
+  				parser.error("{{else}} not allowed in {{#unless}}");
+  			}
+
+  			if (hasElse) {
+  				parser.error("illegal {{elseif...}} after {{else}}");
+  			}
+
+  			if (!unlessBlock) {
+  				unlessBlock = createUnlessBlock(expression, section.n);
+  			}
+
+  			unlessBlock.f.push({
+  				t: SECTION,
+  				n: SECTION_IF,
+  				x: utils_flattenExpression(mustache_readSection__combine(conditions.concat(child.x))),
+  				f: children = []
+  			});
+
+  			conditions.push(invert(child.x));
+  		} else if (child = section_readElse(parser, tag)) {
+  			if (section.n === SECTION_UNLESS) {
+  				parser.error("{{else}} not allowed in {{#unless}}");
+  			}
+
+  			if (hasElse) {
+  				parser.error("there can only be one {{else}} block, at the end of a section");
+  			}
+
+  			hasElse = true;
+
+  			// use an unless block if there's no elseif
+  			if (!unlessBlock) {
+  				unlessBlock = createUnlessBlock(expression, section.n);
+  				children = unlessBlock.f;
+  			} else {
+  				unlessBlock.f.push({
+  					t: SECTION,
+  					n: SECTION_IF,
+  					x: utils_flattenExpression(mustache_readSection__combine(conditions)),
+  					f: children = []
+  				});
+  			}
+  		} else {
+  			child = parser.read(READERS);
+
+  			if (!child) {
+  				break;
+  			}
+
+  			children.push(child);
+  		}
+  	} while (!closed);
+
+  	if (unlessBlock) {
+  		// special case - `with` should become `if-with` (TODO is this right?
+  		// seems to me that `with` ought to behave consistently, regardless
+  		// of the presence/absence of `else`. In other words should always
+  		// be `if-with`
+  		if (section.n === SECTION_WITH) {
+  			section.n = SECTION_IF_WITH;
+  		}
+
+  		section.l = unlessBlock;
+  	}
+
+  	utils_refineExpression(expression, section);
+
+  	// TODO if a section is empty it should be discarded. Don't do
+  	// that here though - we need to clean everything up first, as
+  	// it may contain removeable whitespace. As a temporary measure,
+  	// to pass the existing tests, remove empty `f` arrays
+  	if (!section.f.length) {
+  		delete section.f;
+  	}
+
+  	return section;
+  }
+
+  function createUnlessBlock(expression, sectionType) {
+  	var unlessBlock;
+
+  	if (sectionType === SECTION_WITH) {
+  		// special case - a `{{#with foo}}` section will render if `foo` is
+  		// truthy, so the `{{else}}` section needs to render if `foo` is falsy,
+  		// rather than adhering to the normal `{{#unless foo}}` logic (which
+  		// treats empty arrays/objects as falsy)
+  		unlessBlock = {
+  			t: SECTION,
+  			n: SECTION_IF,
+  			f: []
+  		};
+
+  		utils_refineExpression(invert(expression), unlessBlock);
+  	} else {
+  		unlessBlock = {
+  			t: SECTION,
+  			n: SECTION_UNLESS,
+  			f: []
+  		};
+
+  		utils_refineExpression(expression, unlessBlock);
+  	}
+
+  	return unlessBlock;
+  }
+
+  function invert(expression) {
+  	if (expression.t === PREFIX_OPERATOR && expression.s === "!") {
+  		return expression.o;
+  	}
+
+  	return {
+  		t: PREFIX_OPERATOR,
+  		s: "!",
+  		o: parensIfNecessary(expression)
+  	};
+  }
+
+  function mustache_readSection__combine(expressions) {
+  	if (expressions.length === 1) {
+  		return expressions[0];
+  	}
+
+  	return {
+  		t: INFIX_OPERATOR,
+  		s: "&&",
+  		o: [parensIfNecessary(expressions[0]), parensIfNecessary(mustache_readSection__combine(expressions.slice(1)))]
+  	};
+  }
+
+  function parensIfNecessary(expression) {
+  	// TODO only wrap if necessary
+  	return {
+  		t: BRACKETED,
+  		x: expression
+  	};
+  }
+
+  var converters_readHtmlComment = readHtmlComment;
+  var OPEN_COMMENT = "<!--",
+      CLOSE_COMMENT = "-->";
+  function readHtmlComment(parser) {
+  	var start, content, remaining, endIndex, comment;
+
+  	start = parser.pos;
+
+  	if (!parser.matchString(OPEN_COMMENT)) {
+  		return null;
+  	}
+
+  	remaining = parser.remaining();
+  	endIndex = remaining.indexOf(CLOSE_COMMENT);
+
+  	if (endIndex === -1) {
+  		parser.error("Illegal HTML - expected closing comment sequence ('-->')");
+  	}
+
+  	content = remaining.substr(0, endIndex);
+  	parser.pos += endIndex + 3;
+
+  	comment = {
+  		t: COMMENT,
+  		c: content
+  	};
+
+  	if (parser.includeLinePositions) {
+  		comment.p = parser.getLinePos(start);
+  	}
+
+  	return comment;
+  }
+
+  var booleanAttributes, voidElementNames, htmlEntities, controlCharacters, entityPattern, lessThan, greaterThan, amp;
+
+  // https://github.com/kangax/html-minifier/issues/63#issuecomment-37763316
+  booleanAttributes = /^(allowFullscreen|async|autofocus|autoplay|checked|compact|controls|declare|default|defaultChecked|defaultMuted|defaultSelected|defer|disabled|enabled|formNoValidate|hidden|indeterminate|inert|isMap|itemScope|loop|multiple|muted|noHref|noResize|noShade|noValidate|noWrap|open|pauseOnExit|readOnly|required|reversed|scoped|seamless|selected|sortable|translate|trueSpeed|typeMustMatch|visible)$/i;
+  voidElementNames = /^(?:area|base|br|col|command|doctype|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
+
+  htmlEntities = { quot: 34, amp: 38, apos: 39, lt: 60, gt: 62, nbsp: 160, iexcl: 161, cent: 162, pound: 163, curren: 164, yen: 165, brvbar: 166, sect: 167, uml: 168, copy: 169, ordf: 170, laquo: 171, not: 172, shy: 173, reg: 174, macr: 175, deg: 176, plusmn: 177, sup2: 178, sup3: 179, acute: 180, micro: 181, para: 182, middot: 183, cedil: 184, sup1: 185, ordm: 186, raquo: 187, frac14: 188, frac12: 189, frac34: 190, iquest: 191, Agrave: 192, Aacute: 193, Acirc: 194, Atilde: 195, Auml: 196, Aring: 197, AElig: 198, Ccedil: 199, Egrave: 200, Eacute: 201, Ecirc: 202, Euml: 203, Igrave: 204, Iacute: 205, Icirc: 206, Iuml: 207, ETH: 208, Ntilde: 209, Ograve: 210, Oacute: 211, Ocirc: 212, Otilde: 213, Ouml: 214, times: 215, Oslash: 216, Ugrave: 217, Uacute: 218, Ucirc: 219, Uuml: 220, Yacute: 221, THORN: 222, szlig: 223, agrave: 224, aacute: 225, acirc: 226, atilde: 227, auml: 228, aring: 229, aelig: 230, ccedil: 231, egrave: 232, eacute: 233, ecirc: 234, euml: 235, igrave: 236, iacute: 237, icirc: 238, iuml: 239, eth: 240, ntilde: 241, ograve: 242, oacute: 243, ocirc: 244, otilde: 245, ouml: 246, divide: 247, oslash: 248, ugrave: 249, uacute: 250, ucirc: 251, uuml: 252, yacute: 253, thorn: 254, yuml: 255, OElig: 338, oelig: 339, Scaron: 352, scaron: 353, Yuml: 376, fnof: 402, circ: 710, tilde: 732, Alpha: 913, Beta: 914, Gamma: 915, Delta: 916, Epsilon: 917, Zeta: 918, Eta: 919, Theta: 920, Iota: 921, Kappa: 922, Lambda: 923, Mu: 924, Nu: 925, Xi: 926, Omicron: 927, Pi: 928, Rho: 929, Sigma: 931, Tau: 932, Upsilon: 933, Phi: 934, Chi: 935, Psi: 936, Omega: 937, alpha: 945, beta: 946, gamma: 947, delta: 948, epsilon: 949, zeta: 950, eta: 951, theta: 952, iota: 953, kappa: 954, lambda: 955, mu: 956, nu: 957, xi: 958, omicron: 959, pi: 960, rho: 961, sigmaf: 962, sigma: 963, tau: 964, upsilon: 965, phi: 966, chi: 967, psi: 968, omega: 969, thetasym: 977, upsih: 978, piv: 982, ensp: 8194, emsp: 8195, thinsp: 8201, zwnj: 8204, zwj: 8205, lrm: 8206, rlm: 8207, ndash: 8211, mdash: 8212, lsquo: 8216, rsquo: 8217, sbquo: 8218, ldquo: 8220, rdquo: 8221, bdquo: 8222, dagger: 8224, Dagger: 8225, bull: 8226, hellip: 8230, permil: 8240, prime: 8242, Prime: 8243, lsaquo: 8249, rsaquo: 8250, oline: 8254, frasl: 8260, euro: 8364, image: 8465, weierp: 8472, real: 8476, trade: 8482, alefsym: 8501, larr: 8592, uarr: 8593, rarr: 8594, darr: 8595, harr: 8596, crarr: 8629, lArr: 8656, uArr: 8657, rArr: 8658, dArr: 8659, hArr: 8660, forall: 8704, part: 8706, exist: 8707, empty: 8709, nabla: 8711, isin: 8712, notin: 8713, ni: 8715, prod: 8719, sum: 8721, minus: 8722, lowast: 8727, radic: 8730, prop: 8733, infin: 8734, ang: 8736, and: 8743, or: 8744, cap: 8745, cup: 8746, int: 8747, there4: 8756, sim: 8764, cong: 8773, asymp: 8776, ne: 8800, equiv: 8801, le: 8804, ge: 8805, sub: 8834, sup: 8835, nsub: 8836, sube: 8838, supe: 8839, oplus: 8853, otimes: 8855, perp: 8869, sdot: 8901, lceil: 8968, rceil: 8969, lfloor: 8970, rfloor: 8971, lang: 9001, rang: 9002, loz: 9674, spades: 9824, clubs: 9827, hearts: 9829, diams: 9830 };
+  controlCharacters = [8364, 129, 8218, 402, 8222, 8230, 8224, 8225, 710, 8240, 352, 8249, 338, 141, 381, 143, 144, 8216, 8217, 8220, 8221, 8226, 8211, 8212, 732, 8482, 353, 8250, 339, 157, 382, 376];
+  entityPattern = new RegExp("&(#?(?:x[\\w\\d]+|\\d+|" + Object.keys(htmlEntities).join("|") + "));?", "g");
+
+  function decodeCharacterReferences(html) {
+  	return html.replace(entityPattern, function (match, entity) {
+  		var code;
+
+  		// Handle named entities
+  		if (entity[0] !== "#") {
+  			code = htmlEntities[entity];
+  		} else if (entity[1] === "x") {
+  			code = parseInt(entity.substring(2), 16);
+  		} else {
+  			code = parseInt(entity.substring(1), 10);
+  		}
+
+  		if (!code) {
+  			return match;
+  		}
+
+  		return String.fromCharCode(validateCode(code));
+  	});
+  }
+
+  // some code points are verboten. If we were inserting HTML, the browser would replace the illegal
+  // code points with alternatives in some cases - since we're bypassing that mechanism, we need
+  // to replace them ourselves
+  //
+  // Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
+  function validateCode(code) {
+  	if (!code) {
+  		return 65533;
+  	}
+
+  	// line feed becomes generic whitespace
+  	if (code === 10) {
+  		return 32;
+  	}
+
+  	// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
+  	if (code < 128) {
+  		return code;
+  	}
+
+  	// code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need
+  	// to correct the mistake or we'll end up with missing â‚¬ signs and so on
+  	if (code <= 159) {
+  		return controlCharacters[code - 128];
+  	}
+
+  	// basic multilingual plane
+  	if (code < 55296) {
+  		return code;
+  	}
+
+  	// UTF-16 surrogate halves
+  	if (code <= 57343) {
+  		return 65533;
+  	}
+
+  	// rest of the basic multilingual plane
+  	if (code <= 65535) {
+  		return code;
+  	}
+
+  	return 65533;
+  }
+
+  lessThan = /</g;
+  greaterThan = />/g;
+  amp = /&/g;
+
+  function escapeHtml(str) {
+  	return str.replace(amp, "&amp;").replace(lessThan, "&lt;").replace(greaterThan, "&gt;");
+  }
+
+  var leadingLinebreak = /^\s*\r?\n/,
+      trailingLinebreak = /\r?\n\s*$/;
+
+  var stripStandalones = function (items) {
+  	var i, current, backOne, backTwo, lastSectionItem;
+
+  	for (i = 1; i < items.length; i += 1) {
+  		current = items[i];
+  		backOne = items[i - 1];
+  		backTwo = items[i - 2];
+
+  		// if we're at the end of a [text][comment][text] sequence...
+  		if (isString(current) && isComment(backOne) && isString(backTwo)) {
+
+  			// ... and the comment is a standalone (i.e. line breaks either side)...
+  			if (trailingLinebreak.test(backTwo) && leadingLinebreak.test(current)) {
+
+  				// ... then we want to remove the whitespace after the first line break
+  				items[i - 2] = backTwo.replace(trailingLinebreak, "\n");
+
+  				// and the leading line break of the second text token
+  				items[i] = current.replace(leadingLinebreak, "");
+  			}
+  		}
+
+  		// if the current item is a section, and it is preceded by a linebreak, and
+  		// its first item is a linebreak...
+  		if (isSection(current) && isString(backOne)) {
+  			if (trailingLinebreak.test(backOne) && isString(current.f[0]) && leadingLinebreak.test(current.f[0])) {
+  				items[i - 1] = backOne.replace(trailingLinebreak, "\n");
+  				current.f[0] = current.f[0].replace(leadingLinebreak, "");
+  			}
+  		}
+
+  		// if the last item was a section, and it is followed by a linebreak, and
+  		// its last item is a linebreak...
+  		if (isString(current) && isSection(backOne)) {
+  			lastSectionItem = lastItem(backOne.f);
+
+  			if (isString(lastSectionItem) && trailingLinebreak.test(lastSectionItem) && leadingLinebreak.test(current)) {
+  				backOne.f[backOne.f.length - 1] = lastSectionItem.replace(trailingLinebreak, "\n");
+  				items[i] = current.replace(leadingLinebreak, "");
+  			}
+  		}
+  	}
+
+  	return items;
+  };
+
+  function isString(item) {
+  	return typeof item === "string";
+  }
+
+  function isComment(item) {
+  	return item.t === COMMENT || item.t === DELIMCHANGE;
+  }
+
+  function isSection(item) {
+  	return (item.t === SECTION || item.t === INVERTED) && item.f;
+  }
+
+  var trimWhitespace = function (items, leadingPattern, trailingPattern) {
+  	var item;
+
+  	if (leadingPattern) {
+  		item = items[0];
+  		if (typeof item === "string") {
+  			item = item.replace(leadingPattern, "");
+
+  			if (!item) {
+  				items.shift();
+  			} else {
+  				items[0] = item;
+  			}
+  		}
+  	}
+
+  	if (trailingPattern) {
+  		item = lastItem(items);
+  		if (typeof item === "string") {
+  			item = item.replace(trailingPattern, "");
+
+  			if (!item) {
+  				items.pop();
+  			} else {
+  				items[items.length - 1] = item;
+  			}
+  		}
+  	}
+  };
+
+  var utils_cleanup = cleanup;
+  var contiguousWhitespace = /[ \t\f\r\n]+/g;
+  var preserveWhitespaceElements = /^(?:pre|script|style|textarea)$/i;
+  var utils_cleanup__leadingWhitespace = /^[ \t\f\r\n]+/;
+  var trailingWhitespace = /[ \t\f\r\n]+$/;
+  var leadingNewLine = /^(?:\r\n|\r|\n)/;
+  var trailingNewLine = /(?:\r\n|\r|\n)$/;
+  function cleanup(items, stripComments, preserveWhitespace, removeLeadingWhitespace, removeTrailingWhitespace) {
+  	var i, item, previousItem, nextItem, preserveWhitespaceInsideFragment, removeLeadingWhitespaceInsideFragment, removeTrailingWhitespaceInsideFragment, key;
+
+  	// First pass - remove standalones and comments etc
+  	stripStandalones(items);
+
+  	i = items.length;
+  	while (i--) {
+  		item = items[i];
+
+  		// Remove delimiter changes, unsafe elements etc
+  		if (item.exclude) {
+  			items.splice(i, 1);
+  		}
+
+  		// Remove comments, unless we want to keep them
+  		else if (stripComments && item.t === COMMENT) {
+  			items.splice(i, 1);
+  		}
+  	}
+
+  	// If necessary, remove leading and trailing whitespace
+  	trimWhitespace(items, removeLeadingWhitespace ? utils_cleanup__leadingWhitespace : null, removeTrailingWhitespace ? trailingWhitespace : null);
+
+  	i = items.length;
+  	while (i--) {
+  		item = items[i];
+
+  		// Recurse
+  		if (item.f) {
+  			var isPreserveWhitespaceElement = item.t === ELEMENT && preserveWhitespaceElements.test(item.e);
+  			preserveWhitespaceInsideFragment = preserveWhitespace || isPreserveWhitespaceElement;
+
+  			if (!preserveWhitespace && isPreserveWhitespaceElement) {
+  				trimWhitespace(item.f, leadingNewLine, trailingNewLine);
+  			}
+
+  			if (!preserveWhitespaceInsideFragment) {
+  				previousItem = items[i - 1];
+  				nextItem = items[i + 1];
+
+  				// if the previous item was a text item with trailing whitespace,
+  				// remove leading whitespace inside the fragment
+  				if (!previousItem || typeof previousItem === "string" && trailingWhitespace.test(previousItem)) {
+  					removeLeadingWhitespaceInsideFragment = true;
+  				}
+
+  				// and vice versa
+  				if (!nextItem || typeof nextItem === "string" && utils_cleanup__leadingWhitespace.test(nextItem)) {
+  					removeTrailingWhitespaceInsideFragment = true;
+  				}
+  			}
+
+  			cleanup(item.f, stripComments, preserveWhitespaceInsideFragment, removeLeadingWhitespaceInsideFragment, removeTrailingWhitespaceInsideFragment);
+  		}
+
+  		// Split if-else blocks into two (an if, and an unless)
+  		if (item.l) {
+  			cleanup(item.l.f, stripComments, preserveWhitespace, removeLeadingWhitespaceInsideFragment, removeTrailingWhitespaceInsideFragment);
+
+  			items.splice(i + 1, 0, item.l);
+  			delete item.l; // TODO would be nice if there was a way around this
+  		}
+
+  		// Clean up element attributes
+  		if (item.a) {
+  			for (key in item.a) {
+  				if (item.a.hasOwnProperty(key) && typeof item.a[key] !== "string") {
+  					cleanup(item.a[key], stripComments, preserveWhitespace, removeLeadingWhitespaceInsideFragment, removeTrailingWhitespaceInsideFragment);
+  				}
+  			}
+  		}
+
+  		// Clean up conditional attributes
+  		if (item.m) {
+  			cleanup(item.m, stripComments, preserveWhitespace, removeLeadingWhitespaceInsideFragment, removeTrailingWhitespaceInsideFragment);
+  		}
+
+  		// Clean up event handlers
+  		if (item.v) {
+  			for (key in item.v) {
+  				if (item.v.hasOwnProperty(key)) {
+  					// clean up names
+  					if (isArray(item.v[key].n)) {
+  						cleanup(item.v[key].n, stripComments, preserveWhitespace, removeLeadingWhitespaceInsideFragment, removeTrailingWhitespaceInsideFragment);
+  					}
+
+  					// clean up params
+  					if (isArray(item.v[key].d)) {
+  						cleanup(item.v[key].d, stripComments, preserveWhitespace, removeLeadingWhitespaceInsideFragment, removeTrailingWhitespaceInsideFragment);
+  					}
+  				}
+  			}
+  		}
+  	}
+
+  	// final pass - fuse text nodes together
+  	i = items.length;
+  	while (i--) {
+  		if (typeof items[i] === "string") {
+  			if (typeof items[i + 1] === "string") {
+  				items[i] = items[i] + items[i + 1];
+  				items.splice(i + 1, 1);
+  			}
+
+  			if (!preserveWhitespace) {
+  				items[i] = items[i].replace(contiguousWhitespace, " ");
+  			}
+
+  			if (items[i] === "") {
+  				items.splice(i, 1);
+  			}
+  		}
+  	}
+  }
+
+  var element_readClosingTag = readClosingTag;
+  var closingTagPattern = /^([a-zA-Z]{1,}:?[a-zA-Z0-9\-]*)\s*\>/;
+  function readClosingTag(parser) {
+  	var start, tag;
+
+  	start = parser.pos;
+
+  	// are we looking at a closing tag?
+  	if (!parser.matchString("</")) {
+  		return null;
+  	}
+
+  	if (tag = parser.matchPattern(closingTagPattern)) {
+  		if (parser.inside && tag !== parser.inside) {
+  			parser.pos = start;
+  			return null;
+  		}
+
+  		return {
+  			t: CLOSING_TAG,
+  			e: tag
+  		};
+  	}
+
+  	// We have an illegal closing tag, report it
+  	parser.pos -= 2;
+  	parser.error("Illegal closing tag");
+  }
+
+  var getLowestIndex = function (haystack, needles) {
+  	var i, index, lowest;
+
+  	i = needles.length;
+  	while (i--) {
+  		index = haystack.indexOf(needles[i]);
+
+  		// short circuit
+  		if (!index) {
+  			return 0;
+  		}
+
+  		if (index === -1) {
+  			continue;
+  		}
+
+  		if (!lowest || index < lowest) {
+  			lowest = index;
+  		}
+  	}
+
+  	return lowest || -1;
+  };
+
+  var element_readAttribute = readAttribute;
+
+  var attributeNamePattern = /^[^\s"'>\/=]+/,
+      unquotedAttributeValueTextPattern = /^[^\s"'=<>`]+/;
+  function readAttribute(parser) {
+  	var attr, name, value;
+
+  	parser.allowWhitespace();
+
+  	name = parser.matchPattern(attributeNamePattern);
+  	if (!name) {
+  		return null;
+  	}
+
+  	attr = { name: name };
+
+  	value = readAttributeValue(parser);
+  	if (value != null) {
+  		// not null/undefined
+  		attr.value = value;
+  	}
+
+  	return attr;
+  }
+
+  function readAttributeValue(parser) {
+  	var start, valueStart, startDepth, value;
+
+  	start = parser.pos;
+
+  	// next character must be `=`, `/`, `>` or whitespace
+  	if (!/[=\/>\s]/.test(parser.nextChar())) {
+  		parser.error("Expected `=`, `/`, `>` or whitespace");
+  	}
+
+  	parser.allowWhitespace();
+
+  	if (!parser.matchString("=")) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	parser.allowWhitespace();
+
+  	valueStart = parser.pos;
+  	startDepth = parser.sectionDepth;
+
+  	value = readQuotedAttributeValue(parser, "'") || readQuotedAttributeValue(parser, "\"") || readUnquotedAttributeValue(parser);
+
+  	if (value === null) {
+  		parser.error("Expected valid attribute value");
+  	}
+
+  	if (parser.sectionDepth !== startDepth) {
+  		parser.pos = valueStart;
+  		parser.error("An attribute value must contain as many opening section tags as closing section tags");
+  	}
+
+  	if (!value.length) {
+  		return "";
+  	}
+
+  	if (value.length === 1 && typeof value[0] === "string") {
+  		return decodeCharacterReferences(value[0]);
+  	}
+
+  	return value;
+  }
+
+  function readUnquotedAttributeValueToken(parser) {
+  	var start, text, haystack, needles, index;
+
+  	start = parser.pos;
+
+  	text = parser.matchPattern(unquotedAttributeValueTextPattern);
+
+  	if (!text) {
+  		return null;
+  	}
+
+  	haystack = text;
+  	needles = parser.tags.map(function (t) {
+  		return t.open;
+  	}); // TODO refactor... we do this in readText.js as well
+
+  	if ((index = getLowestIndex(haystack, needles)) !== -1) {
+  		text = text.substr(0, index);
+  		parser.pos = start + text.length;
+  	}
+
+  	return text;
+  }
+
+  function readUnquotedAttributeValue(parser) {
+  	var tokens, token;
+
+  	parser.inAttribute = true;
+
+  	tokens = [];
+
+  	token = converters_readMustache(parser) || readUnquotedAttributeValueToken(parser);
+  	while (token !== null) {
+  		tokens.push(token);
+  		token = converters_readMustache(parser) || readUnquotedAttributeValueToken(parser);
+  	}
+
+  	if (!tokens.length) {
+  		return null;
+  	}
+
+  	parser.inAttribute = false;
+  	return tokens;
+  }
+
+  function readQuotedAttributeValue(parser, quoteMark) {
+  	var start, tokens, token;
+
+  	start = parser.pos;
+
+  	if (!parser.matchString(quoteMark)) {
+  		return null;
+  	}
+
+  	parser.inAttribute = quoteMark;
+
+  	tokens = [];
+
+  	token = converters_readMustache(parser) || readQuotedStringToken(parser, quoteMark);
+  	while (token !== null) {
+  		tokens.push(token);
+  		token = converters_readMustache(parser) || readQuotedStringToken(parser, quoteMark);
+  	}
+
+  	if (!parser.matchString(quoteMark)) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	parser.inAttribute = false;
+
+  	return tokens;
+  }
+
+  function readQuotedStringToken(parser, quoteMark) {
+  	var start, index, haystack, needles;
+
+  	start = parser.pos;
+  	haystack = parser.remaining();
+
+  	needles = parser.tags.map(function (t) {
+  		return t.open;
+  	}); // TODO refactor... we do this in readText.js as well
+  	needles.push(quoteMark);
+
+  	index = getLowestIndex(haystack, needles);
+
+  	if (index === -1) {
+  		parser.error("Quoted attribute value must have a closing quote");
+  	}
+
+  	if (!index) {
+  		return null;
+  	}
+
+  	parser.pos += index;
+  	return haystack.substr(0, index);
+  }
+
+  var JsonParser, specials, specialsPattern, parseJSON__numberPattern, placeholderPattern, placeholderAtStartPattern, onlyWhitespace;
+
+  specials = {
+  	"true": true,
+  	"false": false,
+  	undefined: undefined,
+  	"null": null
+  };
+
+  specialsPattern = new RegExp("^(?:" + Object.keys(specials).join("|") + ")");
+  parseJSON__numberPattern = /^(?:[+-]?)(?:(?:(?:0|[1-9]\d*)?\.\d+)|(?:(?:0|[1-9]\d*)\.)|(?:0|[1-9]\d*))(?:[eE][+-]?\d+)?/;
+  placeholderPattern = /\$\{([^\}]+)\}/g;
+  placeholderAtStartPattern = /^\$\{([^\}]+)\}/;
+  onlyWhitespace = /^\s*$/;
+
+  JsonParser = parse_Parser.extend({
+  	init: function (str, options) {
+  		this.values = options.values;
+  		this.allowWhitespace();
+  	},
+
+  	postProcess: function (result) {
+  		if (result.length !== 1 || !onlyWhitespace.test(this.leftover)) {
+  			return null;
+  		}
+
+  		return { value: result[0].v };
+  	},
+
+  	converters: [function getPlaceholder(parser) {
+  		var placeholder;
+
+  		if (!parser.values) {
+  			return null;
+  		}
+
+  		placeholder = parser.matchPattern(placeholderAtStartPattern);
+
+  		if (placeholder && parser.values.hasOwnProperty(placeholder)) {
+  			return { v: parser.values[placeholder] };
+  		}
+  	}, function getSpecial(parser) {
+  		var special;
+
+  		if (special = parser.matchPattern(specialsPattern)) {
+  			return { v: specials[special] };
+  		}
+  	}, function getNumber(parser) {
+  		var number;
+
+  		if (number = parser.matchPattern(parseJSON__numberPattern)) {
+  			return { v: +number };
+  		}
+  	}, function getString(parser) {
+  		var stringLiteral = readStringLiteral(parser),
+  		    values;
+
+  		if (stringLiteral && (values = parser.values)) {
+  			return {
+  				v: stringLiteral.v.replace(placeholderPattern, function (match, $1) {
+  					return $1 in values ? values[$1] : $1;
+  				})
+  			};
+  		}
+
+  		return stringLiteral;
+  	}, function getObject(parser) {
+  		var result, pair;
+
+  		if (!parser.matchString("{")) {
+  			return null;
+  		}
+
+  		result = {};
+
+  		parser.allowWhitespace();
+
+  		if (parser.matchString("}")) {
+  			return { v: result };
+  		}
+
+  		while (pair = getKeyValuePair(parser)) {
+  			result[pair.key] = pair.value;
+
+  			parser.allowWhitespace();
+
+  			if (parser.matchString("}")) {
+  				return { v: result };
+  			}
+
+  			if (!parser.matchString(",")) {
+  				return null;
+  			}
+  		}
+
+  		return null;
+  	}, function getArray(parser) {
+  		var result, valueToken;
+
+  		if (!parser.matchString("[")) {
+  			return null;
+  		}
+
+  		result = [];
+
+  		parser.allowWhitespace();
+
+  		if (parser.matchString("]")) {
+  			return { v: result };
+  		}
+
+  		while (valueToken = parser.read()) {
+  			result.push(valueToken.v);
+
+  			parser.allowWhitespace();
+
+  			if (parser.matchString("]")) {
+  				return { v: result };
+  			}
+
+  			if (!parser.matchString(",")) {
+  				return null;
+  			}
+
+  			parser.allowWhitespace();
+  		}
+
+  		return null;
+  	}]
+  });
+
+  function getKeyValuePair(parser) {
+  	var key, valueToken, pair;
+
+  	parser.allowWhitespace();
+
+  	key = shared_readKey(parser);
+
+  	if (!key) {
+  		return null;
+  	}
+
+  	pair = { key: key };
+
+  	parser.allowWhitespace();
+  	if (!parser.matchString(":")) {
+  		return null;
+  	}
+  	parser.allowWhitespace();
+
+  	valueToken = parser.read();
+  	if (!valueToken) {
+  		return null;
+  	}
+
+  	pair.value = valueToken.v;
+
+  	return pair;
+  }
+
+  var parseJSON = function (str, values) {
+  	var parser = new JsonParser(str, {
+  		values: values
+  	});
+
+  	return parser.result;
+  };
+
+  // TODO clean this up, it's shocking
+  var element_processDirective = processDirective;
+  var methodCallPattern = /^([a-zA-Z_$][a-zA-Z_$0-9]*)\(/,
+      methodCallExcessPattern = /\)\s*$/,
+      ExpressionParser;
+
+  ExpressionParser = parse_Parser.extend({
+  	converters: [converters_readExpression]
+  });
+  function processDirective(tokens, parentParser) {
+  	var result, match, parser, args, token, colonIndex, directiveName, directiveArgs, parsed;
+
+  	if (typeof tokens === "string") {
+  		if (match = methodCallPattern.exec(tokens)) {
+  			var end = tokens.lastIndexOf(")");
+
+  			// check for invalid method calls
+  			if (!methodCallExcessPattern.test(tokens)) {
+  				parentParser.error("Invalid input after method call expression '" + tokens.slice(end + 1) + "'");
+  			}
+
+  			result = { m: match[1] };
+  			args = "[" + tokens.slice(result.m.length + 1, end) + "]";
+
+  			parser = new ExpressionParser(args);
+  			result.a = utils_flattenExpression(parser.result[0]);
+
+  			return result;
+  		}
+
+  		if (tokens.indexOf(":") === -1) {
+  			return tokens.trim();
+  		}
+
+  		tokens = [tokens];
+  	}
+
+  	result = {};
+
+  	directiveName = [];
+  	directiveArgs = [];
+
+  	if (tokens) {
+  		while (tokens.length) {
+  			token = tokens.shift();
+
+  			if (typeof token === "string") {
+  				colonIndex = token.indexOf(":");
+
+  				if (colonIndex === -1) {
+  					directiveName.push(token);
+  				} else {
+
+  					// is the colon the first character?
+  					if (colonIndex) {
+  						// no
+  						directiveName.push(token.substr(0, colonIndex));
+  					}
+
+  					// if there is anything after the colon in this token, treat
+  					// it as the first token of the directiveArgs fragment
+  					if (token.length > colonIndex + 1) {
+  						directiveArgs[0] = token.substring(colonIndex + 1);
+  					}
+
+  					break;
+  				}
+  			} else {
+  				directiveName.push(token);
+  			}
+  		}
+
+  		directiveArgs = directiveArgs.concat(tokens);
+  	}
+
+  	if (!directiveName.length) {
+  		result = "";
+  	} else if (directiveArgs.length || typeof directiveName !== "string") {
+  		result = {
+  			// TODO is this really necessary? just use the array
+  			n: directiveName.length === 1 && typeof directiveName[0] === "string" ? directiveName[0] : directiveName
+  		};
+
+  		if (directiveArgs.length === 1 && typeof directiveArgs[0] === "string") {
+  			parsed = parseJSON("[" + directiveArgs[0] + "]");
+  			result.a = parsed ? parsed.value : directiveArgs[0].trim();
+  		} else {
+  			result.d = directiveArgs;
+  		}
+  	} else {
+  		result = directiveName;
+  	}
+
+  	return result;
+  }
+
+  var tagNamePattern = /^[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/,
+      validTagNameFollower = /^[\s\n\/>]/,
+      onPattern = /^on/,
+      proxyEventPattern = /^on-([a-zA-Z\\*\\.$_][a-zA-Z\\*\\.$_0-9\-]+)$/,
+      reservedEventNames = /^(?:change|reset|teardown|update|construct|config|init|render|unrender|detach|insert)$/,
+      directives = { "intro-outro": "t0", intro: "t1", outro: "t2", decorator: "o" },
+      exclude = { exclude: true },
+      disallowedContents;
+
+  // based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
+  disallowedContents = {
+  	li: ["li"],
+  	dt: ["dt", "dd"],
+  	dd: ["dt", "dd"],
+  	p: "address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul".split(" "),
+  	rt: ["rt", "rp"],
+  	rp: ["rt", "rp"],
+  	optgroup: ["optgroup"],
+  	option: ["option", "optgroup"],
+  	thead: ["tbody", "tfoot"],
+  	tbody: ["tbody", "tfoot"],
+  	tfoot: ["tbody"],
+  	tr: ["tr", "tbody"],
+  	td: ["td", "th", "tr"],
+  	th: ["td", "th", "tr"]
+  };
+
+  var converters_readElement = readElement;
+
+  function readElement(parser) {
+  	var start, element, directiveName, match, addProxyEvent, attribute, directive, selfClosing, children, partials, hasPartials, child, closed, pos, remaining, closingTag;
+
+  	start = parser.pos;
+
+  	if (parser.inside || parser.inAttribute) {
+  		return null;
+  	}
+
+  	if (!parser.matchString("<")) {
+  		return null;
+  	}
+
+  	// if this is a closing tag, abort straight away
+  	if (parser.nextChar() === "/") {
+  		return null;
+  	}
+
+  	element = {};
+  	if (parser.includeLinePositions) {
+  		element.p = parser.getLinePos(start);
+  	}
+
+  	if (parser.matchString("!")) {
+  		element.t = DOCTYPE;
+  		if (!parser.matchPattern(/^doctype/i)) {
+  			parser.error("Expected DOCTYPE declaration");
+  		}
+
+  		element.a = parser.matchPattern(/^(.+?)>/);
+  		return element;
+  	}
+
+  	element.t = ELEMENT;
+
+  	// element name
+  	element.e = parser.matchPattern(tagNamePattern);
+  	if (!element.e) {
+  		return null;
+  	}
+
+  	// next character must be whitespace, closing solidus or '>'
+  	if (!validTagNameFollower.test(parser.nextChar())) {
+  		parser.error("Illegal tag name");
+  	}
+
+  	addProxyEvent = function (name, directive) {
+  		var directiveName = directive.n || directive;
+
+  		if (reservedEventNames.test(directiveName)) {
+  			parser.pos -= directiveName.length;
+  			parser.error("Cannot use reserved event names (change, reset, teardown, update, construct, config, init, render, unrender, detach, insert)");
+  		}
+
+  		element.v[name] = directive;
+  	};
+
+  	parser.allowWhitespace();
+
+  	// directives and attributes
+  	while (attribute = converters_readMustache(parser) || element_readAttribute(parser)) {
+  		// regular attributes
+  		if (attribute.name) {
+  			// intro, outro, decorator
+  			if (directiveName = directives[attribute.name]) {
+  				element[directiveName] = element_processDirective(attribute.value, parser);
+  			}
+
+  			// on-click etc
+  			else if (match = proxyEventPattern.exec(attribute.name)) {
+  				if (!element.v) element.v = {};
+  				directive = element_processDirective(attribute.value, parser);
+  				addProxyEvent(match[1], directive);
+  			} else {
+  				if (!parser.sanitizeEventAttributes || !onPattern.test(attribute.name)) {
+  					if (!element.a) element.a = {};
+  					element.a[attribute.name] = attribute.value || (attribute.value === "" ? "" : 0);
+  				}
+  			}
+  		}
+
+  		// {{#if foo}}class='foo'{{/if}}
+  		else {
+  			if (!element.m) element.m = [];
+  			element.m.push(attribute);
+  		}
+
+  		parser.allowWhitespace();
+  	}
+
+  	// allow whitespace before closing solidus
+  	parser.allowWhitespace();
+
+  	// self-closing solidus?
+  	if (parser.matchString("/")) {
+  		selfClosing = true;
+  	}
+
+  	// closing angle bracket
+  	if (!parser.matchString(">")) {
+  		return null;
+  	}
+
+  	var lowerCaseName = element.e.toLowerCase();
+  	var preserveWhitespace = parser.preserveWhitespace;
+
+  	if (!selfClosing && !voidElementNames.test(element.e)) {
+  		parser.elementStack.push(lowerCaseName);
+
+  		// Special case - if we open a script element, further tags should
+  		// be ignored unless they're a closing script element
+  		if (lowerCaseName === "script" || lowerCaseName === "style") {
+  			parser.inside = lowerCaseName;
+  		}
+
+  		children = [];
+  		partials = create(null);
+
+  		do {
+  			pos = parser.pos;
+  			remaining = parser.remaining();
+
+  			// if for example we're in an <li> element, and we see another
+  			// <li> tag, close the first so they become siblings
+  			if (!canContain(lowerCaseName, remaining)) {
+  				closed = true;
+  			}
+
+  			// closing tag
+  			else if (closingTag = element_readClosingTag(parser)) {
+  				closed = true;
+
+  				var closingTagName = closingTag.e.toLowerCase();
+
+  				// if this *isn't* the closing tag for the current element...
+  				if (closingTagName !== lowerCaseName) {
+  					// rewind parser
+  					parser.pos = pos;
+
+  					// if it doesn't close a parent tag, error
+  					if (! ~parser.elementStack.indexOf(closingTagName)) {
+  						var errorMessage = "Unexpected closing tag";
+
+  						// add additional help for void elements, since component names
+  						// might clash with them
+  						if (voidElementNames.test(closingTagName)) {
+  							errorMessage += " (<" + closingTagName + "> is a void element - it cannot contain children)";
+  						}
+
+  						parser.error(errorMessage);
+  					}
+  				}
+  			}
+
+  			// implicit close by closing section tag. TODO clean this up
+  			else if (child = section_readClosing(parser, { open: parser.standardDelimiters[0], close: parser.standardDelimiters[1] })) {
+  				closed = true;
+  				parser.pos = pos;
+  			} else {
+  				if (child = parser.read(PARTIAL_READERS)) {
+  					if (partials[child.n]) {
+  						parser.pos = pos;
+  						parser.error("Duplicate partial definition");
+  					}
+
+  					utils_cleanup(child.f, parser.stripComments, preserveWhitespace, !preserveWhitespace, !preserveWhitespace);
+
+  					partials[child.n] = child.f;
+  					hasPartials = true;
+  				} else {
+  					if (child = parser.read(READERS)) {
+  						children.push(child);
+  					} else {
+  						closed = true;
+  					}
+  				}
+  			}
+  		} while (!closed);
+
+  		if (children.length) {
+  			element.f = children;
+  		}
+
+  		if (hasPartials) {
+  			element.p = partials;
+  		}
+
+  		parser.elementStack.pop();
+  	}
+
+  	parser.inside = null;
+
+  	if (parser.sanitizeElements && parser.sanitizeElements.indexOf(lowerCaseName) !== -1) {
+  		return exclude;
+  	}
+
+  	return element;
+  }
+
+  function canContain(name, remaining) {
+  	var match, disallowed;
+
+  	match = /^<([a-zA-Z][a-zA-Z0-9]*)/.exec(remaining);
+  	disallowed = disallowedContents[name];
+
+  	if (!match || !disallowed) {
+  		return true;
+  	}
+
+  	return ! ~disallowed.indexOf(match[1].toLowerCase());
+  }
+
+  var converters_readText = readText;
+  function readText(parser) {
+  	var index, remaining, disallowed, barrier;
+
+  	remaining = parser.remaining();
+
+  	barrier = parser.inside ? "</" + parser.inside : "<";
+
+  	if (parser.inside && !parser.interpolate[parser.inside]) {
+  		index = remaining.indexOf(barrier);
+  	} else {
+  		disallowed = parser.tags.map(function (t) {
+  			return t.open;
+  		});
+  		disallowed = disallowed.concat(parser.tags.map(function (t) {
+  			return "\\" + t.open;
+  		}));
+
+  		// http://developers.whatwg.org/syntax.html#syntax-attributes
+  		if (parser.inAttribute === true) {
+  			// we're inside an unquoted attribute value
+  			disallowed.push("\"", "'", "=", "<", ">", "`");
+  		} else if (parser.inAttribute) {
+  			// quoted attribute value
+  			disallowed.push(parser.inAttribute);
+  		} else {
+  			disallowed.push(barrier);
+  		}
+
+  		index = getLowestIndex(remaining, disallowed);
+  	}
+
+  	if (!index) {
+  		return null;
+  	}
+
+  	if (index === -1) {
+  		index = remaining.length;
+  	}
+
+  	parser.pos += index;
+
+  	return parser.inside ? remaining.substr(0, index) : decodeCharacterReferences(remaining.substr(0, index));
+  }
+
+  var utils_escapeRegExp = escapeRegExp;
+  var utils_escapeRegExp__pattern = /[-/\\^$*+?.()|[\]{}]/g;
+  function escapeRegExp(str) {
+  	return str.replace(utils_escapeRegExp__pattern, "\\$&");
+  }
+
+  var converters_readPartialDefinitionComment = readPartialDefinitionComment;
+
+  var startPattern = /^<!--\s*/,
+      namePattern = /s*>\s*([a-zA-Z_$][-a-zA-Z_$0-9]*)\s*/,
+      finishPattern = /\s*-->/,
+      child;
+
+  function readPartialDefinitionComment(parser) {
+  	var firstPos = parser.pos,
+  	    open = parser.standardDelimiters[0],
+  	    close = parser.standardDelimiters[1],
+  	    content = undefined,
+  	    closed = undefined;
+
+  	if (!parser.matchPattern(startPattern) || !parser.matchString(open)) {
+  		parser.pos = firstPos;
+  		return null;
+  	}
+
+  	var name = parser.matchPattern(namePattern);
+
+  	warnOnceIfDebug("Inline partial comments are deprecated.\nUse this...\n  {{#partial " + name + "}} ... {{/partial}}\n\n...instead of this:\n  <!-- {{>" + name + "}} --> ... <!-- {{/" + name + "}} -->'");
+
+  	// make sure the rest of the comment is in the correct place
+  	if (!parser.matchString(close) || !parser.matchPattern(finishPattern)) {
+  		parser.pos = firstPos;
+  		return null;
+  	}
+
+  	content = [];
+
+  	var endPattern = new RegExp("^<!--\\s*" + utils_escapeRegExp(open) + "\\s*\\/\\s*" + name + "\\s*" + utils_escapeRegExp(close) + "\\s*-->");
+
+  	do {
+  		if (parser.matchPattern(endPattern)) {
+  			closed = true;
+  		} else {
+  			child = parser.read(READERS);
+  			if (!child) {
+  				parser.error("expected closing comment ('<!-- " + open + "/" + name + "" + close + " -->')");
+  			}
+
+  			content.push(child);
+  		}
+  	} while (!closed);
+
+  	return {
+  		t: INLINE_PARTIAL,
+  		f: content,
+  		n: name
+  	};
+  }
+
+  var converters_readPartialDefinitionSection = readPartialDefinitionSection;
+  var partialDefinitionSectionPattern = /^#\s*partial\s+/;
+  function readPartialDefinitionSection(parser) {
+  	var start, name, content, child, closed;
+
+  	start = parser.pos;
+
+  	var delimiters = parser.standardDelimiters;
+
+  	if (!parser.matchString(delimiters[0])) {
+  		return null;
+  	}
+
+  	if (!parser.matchPattern(partialDefinitionSectionPattern)) {
+  		parser.pos = start;
+  		return null;
+  	}
+
+  	name = parser.matchPattern(/^[a-zA-Z_$][a-zA-Z_$0-9\-]*/);
+
+  	if (!name) {
+  		parser.error("expected legal partial name");
+  	}
+
+  	if (!parser.matchString(delimiters[1])) {
+  		parser.error("Expected closing delimiter '" + delimiters[1] + "'");
+  	}
+
+  	content = [];
+
+  	do {
+  		// TODO clean this up
+  		if (child = section_readClosing(parser, { open: parser.standardDelimiters[0], close: parser.standardDelimiters[1] })) {
+  			if (!child.r === "partial") {
+  				parser.error("Expected " + delimiters[0] + "/partial" + delimiters[1]);
+  			}
+
+  			closed = true;
+  		} else {
+  			child = parser.read(READERS);
+
+  			if (!child) {
+  				parser.error("Expected " + delimiters[0] + "/partial" + delimiters[1]);
+  			}
+
+  			content.push(child);
+  		}
+  	} while (!closed);
+
+  	return {
+  		t: INLINE_PARTIAL,
+  		n: name,
+  		f: content
+  	};
+  }
+
+  var converters_readTemplate = readTemplate;
+  function readTemplate(parser) {
+  	var fragment = [];
+  	var partials = create(null);
+  	var hasPartials = false;
+
+  	var preserveWhitespace = parser.preserveWhitespace;
+
+  	while (parser.pos < parser.str.length) {
+  		var pos = parser.pos,
+  		    item = undefined,
+  		    partial = undefined;
+
+  		if (partial = parser.read(PARTIAL_READERS)) {
+  			if (partials[partial.n]) {
+  				parser.pos = pos;
+  				parser.error("Duplicated partial definition");
+  			}
+
+  			utils_cleanup(partial.f, parser.stripComments, preserveWhitespace, !preserveWhitespace, !preserveWhitespace);
+
+  			partials[partial.n] = partial.f;
+  			hasPartials = true;
+  		} else if (item = parser.read(READERS)) {
+  			fragment.push(item);
+  		} else {
+  			parser.error("Unexpected template content");
+  		}
+  	}
+
+  	var result = {
+  		v: TEMPLATE_VERSION,
+  		t: fragment
+  	};
+
+  	if (hasPartials) {
+  		result.p = partials;
+  	}
+
+  	return result;
+  }
+
+  var _parse = parse;
+
+  var STANDARD_READERS = [mustache_readPartial, mustache_readUnescaped, mustache_readSection, mustache_readYielder, mustache_readInterpolator, readMustacheComment];
+  var TRIPLE_READERS = [mustache_readTriple];
+  var STATIC_READERS = [mustache_readUnescaped, mustache_readSection, mustache_readInterpolator]; // TODO does it make sense to have a static section?
+
+  var StandardParser = undefined;
+  function parse(template, options) {
+  	return new StandardParser(template, options || {}).result;
+  }
+
+  var READERS = [converters_readMustache, converters_readHtmlComment, converters_readElement, converters_readText];
+  var PARTIAL_READERS = [converters_readPartialDefinitionComment, converters_readPartialDefinitionSection];
+
+  StandardParser = parse_Parser.extend({
+  	init: function (str, options) {
+  		var tripleDelimiters = options.tripleDelimiters || ["{{{", "}}}"],
+  		    staticDelimiters = options.staticDelimiters || ["[[", "]]"],
+  		    staticTripleDelimiters = options.staticTripleDelimiters || ["[[[", "]]]"];
+
+  		this.standardDelimiters = options.delimiters || ["{{", "}}"];
+
+  		this.tags = [{ isStatic: false, isTriple: false, open: this.standardDelimiters[0], close: this.standardDelimiters[1], readers: STANDARD_READERS }, { isStatic: false, isTriple: true, open: tripleDelimiters[0], close: tripleDelimiters[1], readers: TRIPLE_READERS }, { isStatic: true, isTriple: false, open: staticDelimiters[0], close: staticDelimiters[1], readers: STATIC_READERS }, { isStatic: true, isTriple: true, open: staticTripleDelimiters[0], close: staticTripleDelimiters[1], readers: TRIPLE_READERS }];
+
+  		this.sortMustacheTags();
+
+  		this.sectionDepth = 0;
+  		this.elementStack = [];
+
+  		this.interpolate = {
+  			script: !options.interpolate || options.interpolate.script !== false,
+  			style: !options.interpolate || options.interpolate.style !== false
+  		};
+
+  		if (options.sanitize === true) {
+  			options.sanitize = {
+  				// blacklist from https://code.google.com/p/google-caja/source/browse/trunk/src/com/google/caja/lang/html/html4-elements-whitelist.json
+  				elements: "applet base basefont body frame frameset head html isindex link meta noframes noscript object param script style title".split(" "),
+  				eventAttributes: true
+  			};
+  		}
+
+  		this.stripComments = options.stripComments !== false;
+  		this.preserveWhitespace = options.preserveWhitespace;
+  		this.sanitizeElements = options.sanitize && options.sanitize.elements;
+  		this.sanitizeEventAttributes = options.sanitize && options.sanitize.eventAttributes;
+  		this.includeLinePositions = options.includeLinePositions;
+  	},
+
+  	postProcess: function (result) {
+  		// special case - empty string
+  		if (!result.length) {
+  			return { t: [], v: TEMPLATE_VERSION };
+  		}
+
+  		if (this.sectionDepth > 0) {
+  			this.error("A section was left open");
+  		}
+
+  		utils_cleanup(result[0].t, this.stripComments, this.preserveWhitespace, !this.preserveWhitespace, !this.preserveWhitespace);
+
+  		return result[0];
+  	},
+
+  	converters: [converters_readTemplate],
+
+  	sortMustacheTags: function () {
+  		// Sort in order of descending opening delimiter length (longer first),
+  		// to protect against opening delimiters being substrings of each other
+  		this.tags.sort(function (a, b) {
+  			return b.open.length - a.open.length;
+  		});
+  	}
+  });
+
+  var parseOptions = ["preserveWhitespace", "sanitize", "stripComments", "delimiters", "tripleDelimiters", "interpolate"];
+
+  var parser = {
+  	fromId: fromId, isHashedId: isHashedId, isParsed: isParsed, getParseOptions: getParseOptions, createHelper: template_parser__createHelper,
+  	parse: doParse
+  };
+
+  function template_parser__createHelper(parseOptions) {
+  	var helper = create(parser);
+  	helper.parse = function (template, options) {
+  		return doParse(template, options || parseOptions);
+  	};
+  	return helper;
+  }
+
+  function doParse(template, parseOptions) {
+  	if (!_parse) {
+  		throw new Error("Missing Ractive.parse - cannot parse template. Either preparse or use the version that includes the parser");
+  	}
+
+  	return _parse(template, parseOptions || this.options);
+  }
+
+  function fromId(id, options) {
+  	var template;
+
+  	if (!isClient) {
+  		if (options && options.noThrow) {
+  			return;
+  		}
+  		throw new Error("Cannot retrieve template #" + id + " as Ractive is not running in a browser.");
+  	}
+
+  	if (isHashedId(id)) {
+  		id = id.substring(1);
+  	}
+
+  	if (!(template = document.getElementById(id))) {
+  		if (options && options.noThrow) {
+  			return;
+  		}
+  		throw new Error("Could not find template element with id #" + id);
+  	}
+
+  	if (template.tagName.toUpperCase() !== "SCRIPT") {
+  		if (options && options.noThrow) {
+  			return;
+  		}
+  		throw new Error("Template element with id #" + id + ", must be a <script> element");
+  	}
+
+  	return "textContent" in template ? template.textContent : template.innerHTML;
+  }
+
+  function isHashedId(id) {
+  	return id && id[0] === "#";
+  }
+
+  function isParsed(template) {
+  	return !(typeof template === "string");
+  }
+
+  function getParseOptions(ractive) {
+  	// Could be Ractive or a Component
+  	if (ractive.defaults) {
+  		ractive = ractive.defaults;
+  	}
+
+  	return parseOptions.reduce(function (val, key) {
+  		val[key] = ractive[key];
+  		return val;
+  	}, {});
+  }
+
+  var template_parser = parser;
+
+  var templateConfigurator = {
+  	name: "template",
+
+  	extend: function extend(Parent, proto, options) {
+  		var template;
+
+  		// only assign if exists
+  		if ("template" in options) {
+  			template = options.template;
+
+  			if (typeof template === "function") {
+  				proto.template = template;
+  			} else {
+  				proto.template = parseIfString(template, proto);
+  			}
+  		}
+  	},
+
+  	init: function init(Parent, ractive, options) {
+  		var template, fn;
+
+  		// TODO because of prototypal inheritance, we might just be able to use
+  		// ractive.template, and not bother passing through the Parent object.
+  		// At present that breaks the test mocks' expectations
+  		template = "template" in options ? options.template : Parent.prototype.template;
+
+  		if (typeof template === "function") {
+  			fn = template;
+  			template = getDynamicTemplate(ractive, fn);
+
+  			ractive._config.template = {
+  				fn: fn,
+  				result: template
+  			};
+  		}
+
+  		template = parseIfString(template, ractive);
+
+  		// TODO the naming of this is confusing - ractive.template refers to [...],
+  		// but Component.prototype.template refers to {v:1,t:[],p:[]}...
+  		// it's unnecessary, because the developer never needs to access
+  		// ractive.template
+  		ractive.template = template.t;
+
+  		if (template.p) {
+  			extendPartials(ractive.partials, template.p);
+  		}
+  	},
+
+  	reset: function (ractive) {
+  		var result = resetValue(ractive),
+  		    parsed;
+
+  		if (result) {
+  			parsed = parseIfString(result, ractive);
+
+  			ractive.template = parsed.t;
+  			extendPartials(ractive.partials, parsed.p, true);
+
+  			return true;
+  		}
+  	}
+  };
+
+  function resetValue(ractive) {
+  	var initial = ractive._config.template,
+  	    result;
+
+  	// If this isn't a dynamic template, there's nothing to do
+  	if (!initial || !initial.fn) {
+  		return;
+  	}
+
+  	result = getDynamicTemplate(ractive, initial.fn);
+
+  	// TODO deep equality check to prevent unnecessary re-rendering
+  	// in the case of already-parsed templates
+  	if (result !== initial.result) {
+  		initial.result = result;
+  		result = parseIfString(result, ractive);
+  		return result;
+  	}
+  }
+
+  function getDynamicTemplate(ractive, fn) {
+  	var helper = template_template__createHelper(template_parser.getParseOptions(ractive));
+  	return fn.call(ractive, helper);
+  }
+
+  function template_template__createHelper(parseOptions) {
+  	var helper = create(template_parser);
+  	helper.parse = function (template, options) {
+  		return template_parser.parse(template, options || parseOptions);
+  	};
+  	return helper;
+  }
+
+  function parseIfString(template, ractive) {
+  	if (typeof template === "string") {
+  		// ID of an element containing the template?
+  		if (template[0] === "#") {
+  			template = template_parser.fromId(template);
+  		}
+
+  		template = _parse(template, template_parser.getParseOptions(ractive));
+  	}
+
+  	// Check that the template even exists
+  	else if (template == undefined) {
+  		throw new Error("The template cannot be " + template + ".");
+  	}
+
+  	// Check the parsed template has a version at all
+  	else if (typeof template.v !== "number") {
+  		throw new Error("The template parser was passed a non-string template, but the template doesn't have a version.  Make sure you're passing in the template you think you are.");
+  	}
+
+  	// Check we're using the correct version
+  	else if (template.v !== TEMPLATE_VERSION) {
+  		throw new Error("Mismatched template version (expected " + TEMPLATE_VERSION + ", got " + template.v + ") Please ensure you are using the latest version of Ractive.js in your build process as well as in your app");
+  	}
+
+  	return template;
+  }
+
+  function extendPartials(existingPartials, newPartials, overwrite) {
+  	if (!newPartials) return;
+
+  	// TODO there's an ambiguity here - we need to overwrite in the `reset()`
+  	// case, but not initially...
+
+  	for (var key in newPartials) {
+  		if (overwrite || !existingPartials.hasOwnProperty(key)) {
+  			existingPartials[key] = newPartials[key];
+  		}
+  	}
+  }
+
+  var template_template = templateConfigurator;
+
+  var config_registries__registryNames, Registry, registries;
+
+  config_registries__registryNames = ["adaptors", "components", "computed", "decorators", "easing", "events", "interpolators", "partials", "transitions"];
+
+  Registry = function (name, useDefaults) {
+  	this.name = name;
+  	this.useDefaults = useDefaults;
+  };
+
+  Registry.prototype = {
+  	constructor: Registry,
+
+  	extend: function (Parent, proto, options) {
+  		this.configure(this.useDefaults ? Parent.defaults : Parent, this.useDefaults ? proto : proto.constructor, options);
+  	},
+
+  	init: function () {},
+
+  	configure: function (Parent, target, options) {
+  		var name = this.name,
+  		    option = options[name],
+  		    registry;
+
+  		registry = create(Parent[name]);
+
+  		for (var key in option) {
+  			registry[key] = option[key];
+  		}
+
+  		target[name] = registry;
+  	},
+
+  	reset: function (ractive) {
+  		var registry = ractive[this.name];
+  		var changed = false;
+  		Object.keys(registry).forEach(function (key) {
+  			var item = registry[key];
+  			if (item._fn) {
+  				if (item._fn.isOwner) {
+  					registry[key] = item._fn;
+  				} else {
+  					delete registry[key];
+  				}
+  				changed = true;
+  			}
+  		});
+  		return changed;
+  	}
+  };
+
+  registries = config_registries__registryNames.map(function (name) {
+  	return new Registry(name, name === "computed");
+  });
+
+  var config_registries = registries;
+
+  /*this.configure(
+  	this.useDefaults ? Parent.defaults : Parent,
+  	ractive,
+  	options );*/
+
+  var wrapPrototype = wrap;
+
+  function wrap(parent, name, method) {
+  	if (!/_super/.test(method)) {
+  		return method;
+  	}
+
+  	var wrapper = function wrapSuper() {
+  		var superMethod = getSuperMethod(wrapper._parent, name),
+  		    hasSuper = ("_super" in this),
+  		    oldSuper = this._super,
+  		    result;
+
+  		this._super = superMethod;
+
+  		result = method.apply(this, arguments);
+
+  		if (hasSuper) {
+  			this._super = oldSuper;
+  		} else {
+  			delete this._super;
+  		}
+
+  		return result;
+  	};
+
+  	wrapper._parent = parent;
+  	wrapper._method = method;
+
+  	return wrapper;
+  }
+
+  function getSuperMethod(parent, name) {
+  	var value, method;
+
+  	if (name in parent) {
+  		value = parent[name];
+
+  		if (typeof value === "function") {
+  			method = value;
+  		} else {
+  			method = function returnValue() {
+  				return value;
+  			};
+  		}
+  	} else {
+  		method = noop;
+  	}
+
+  	return method;
+  }
+
+  var config_deprecate = deprecate;
+  function getMessage(deprecated, correct, isError) {
+  	return "options." + deprecated + " has been deprecated in favour of options." + correct + "." + (isError ? " You cannot specify both options, please use options." + correct + "." : "");
+  }
+
+  function deprecateOption(options, deprecatedOption, correct) {
+  	if (deprecatedOption in options) {
+  		if (!(correct in options)) {
+  			warnIfDebug(getMessage(deprecatedOption, correct));
+  			options[correct] = options[deprecatedOption];
+  		} else {
+  			throw new Error(getMessage(deprecatedOption, correct, true));
+  		}
+  	}
+  }
+  function deprecate(options) {
+  	deprecateOption(options, "beforeInit", "onconstruct");
+  	deprecateOption(options, "init", "onrender");
+  	deprecateOption(options, "complete", "oncomplete");
+  	deprecateOption(options, "eventDefinitions", "events");
+
+  	// Using extend with Component instead of options,
+  	// like Human.extend( Spider ) means adaptors as a registry
+  	// gets copied to options. So we have to check if actually an array
+  	if (isArray(options.adaptors)) {
+  		deprecateOption(options, "adaptors", "adapt");
+  	}
+  }
+
+  var config, order, defaultKeys, custom, isBlacklisted, isStandardKey;
+
+  custom = {
+  	adapt: custom_adapt,
+  	css: css_css,
+  	data: custom_data,
+  	template: template_template
+  };
+
+  defaultKeys = Object.keys(config_defaults);
+
+  isStandardKey = makeObj(defaultKeys.filter(function (key) {
+  	return !custom[key];
+  }));
+
+  // blacklisted keys that we don't double extend
+  isBlacklisted = makeObj(defaultKeys.concat(config_registries.map(function (r) {
+  	return r.name;
+  })));
+
+  order = [].concat(defaultKeys.filter(function (key) {
+  	return !config_registries[key] && !custom[key];
+  }), config_registries, custom.data, custom.template, custom.css);
+
+  config = {
+  	extend: function (Parent, proto, options) {
+  		return configure("extend", Parent, proto, options);
+  	},
+
+  	init: function (Parent, ractive, options) {
+  		return configure("init", Parent, ractive, options);
+  	},
+
+  	reset: function (ractive) {
+  		return order.filter(function (c) {
+  			return c.reset && c.reset(ractive);
+  		}).map(function (c) {
+  			return c.name;
+  		});
+  	},
+
+  	// this defines the order. TODO this isn't used anywhere in the codebase,
+  	// only in the test suite - should get rid of it
+  	order: order };
+
+  function configure(method, Parent, target, options) {
+  	config_deprecate(options);
+
+  	for (var key in options) {
+  		if (isStandardKey.hasOwnProperty(key)) {
+  			var value = options[key];
+
+  			// warn the developer if they passed a function and ignore its value
+
+  			// NOTE: we allow some functions on "el" because we duck type element lists
+  			// and some libraries or ef'ed-up virtual browsers (phantomJS) return a
+  			// function object as the result of querySelector methods
+  			if (key !== "el" && typeof value === "function") {
+  				warnIfDebug("" + key + " is a Ractive option that does not expect a function and will be ignored", method === "init" ? target : null);
+  			} else {
+  				target[key] = value;
+  			}
+  		}
+  	}
+
+  	config_registries.forEach(function (registry) {
+  		registry[method](Parent, target, options);
+  	});
+
+  	custom_adapt[method](Parent, target, options);
+  	template_template[method](Parent, target, options);
+  	css_css[method](Parent, target, options);
+
+  	extendOtherMethods(Parent.prototype, target, options);
+  }
+
+  function extendOtherMethods(parent, target, options) {
+  	for (var key in options) {
+  		if (!isBlacklisted[key] && options.hasOwnProperty(key)) {
+  			var member = options[key];
+
+  			// if this is a method that overwrites a method, wrap it:
+  			if (typeof member === "function") {
+  				member = wrapPrototype(parent, key, member);
+  			}
+
+  			target[key] = member;
+  		}
+  	}
+  }
+
+  function makeObj(array) {
+  	var obj = {};
+  	array.forEach(function (x) {
+  		return obj[x] = true;
+  	});
+  	return obj;
+  }
+
+  var config_config = config;
+
+  var prototype_bubble = Fragment$bubble;
+
+  function Fragment$bubble() {
+  	this.dirtyValue = this.dirtyArgs = true;
+
+  	if (this.bound && typeof this.owner.bubble === "function") {
+  		this.owner.bubble();
+  	}
+  }
+
+  var Fragment_prototype_detach = Fragment$detach;
+
+  function Fragment$detach() {
+  	var docFrag;
+
+  	if (this.items.length === 1) {
+  		return this.items[0].detach();
+  	}
+
+  	docFrag = document.createDocumentFragment();
+
+  	this.items.forEach(function (item) {
+  		var node = item.detach();
+
+  		// TODO The if {...} wasn't previously required - it is now, because we're
+  		// forcibly detaching everything to reorder sections after an update. That's
+  		// a non-ideal brute force approach, implemented to get all the tests to pass
+  		// - as soon as it's replaced with something more elegant, this should
+  		// revert to `docFrag.appendChild( item.detach() )`
+  		if (node) {
+  			docFrag.appendChild(node);
+  		}
+  	});
+
+  	return docFrag;
+  }
+
+  var Fragment_prototype_find = Fragment$find;
+
+  function Fragment$find(selector) {
+  	var i, len, item, queryResult;
+
+  	if (this.items) {
+  		len = this.items.length;
+  		for (i = 0; i < len; i += 1) {
+  			item = this.items[i];
+
+  			if (item.find && (queryResult = item.find(selector))) {
+  				return queryResult;
+  			}
+  		}
+
+  		return null;
+  	}
+  }
+
+  var Fragment_prototype_findAll = Fragment$findAll;
+
+  function Fragment$findAll(selector, query) {
+  	var i, len, item;
+
+  	if (this.items) {
+  		len = this.items.length;
+  		for (i = 0; i < len; i += 1) {
+  			item = this.items[i];
+
+  			if (item.findAll) {
+  				item.findAll(selector, query);
+  			}
+  		}
+  	}
+
+  	return query;
+  }
+
+  var Fragment_prototype_findAllComponents = Fragment$findAllComponents;
+
+  function Fragment$findAllComponents(selector, query) {
+  	var i, len, item;
+
+  	if (this.items) {
+  		len = this.items.length;
+  		for (i = 0; i < len; i += 1) {
+  			item = this.items[i];
+
+  			if (item.findAllComponents) {
+  				item.findAllComponents(selector, query);
+  			}
+  		}
+  	}
+
+  	return query;
+  }
+
+  var Fragment_prototype_findComponent = Fragment$findComponent;
+
+  function Fragment$findComponent(selector) {
+  	var len, i, item, queryResult;
+
+  	if (this.items) {
+  		len = this.items.length;
+  		for (i = 0; i < len; i += 1) {
+  			item = this.items[i];
+
+  			if (item.findComponent && (queryResult = item.findComponent(selector))) {
+  				return queryResult;
+  			}
+  		}
+
+  		return null;
+  	}
+  }
+
+  var prototype_findNextNode = Fragment$findNextNode;
+
+  function Fragment$findNextNode(item) {
+  	var index = item.index,
+  	    node;
+
+  	if (this.items[index + 1]) {
+  		node = this.items[index + 1].firstNode();
+  	}
+
+  	// if this is the root fragment, and there are no more items,
+  	// it means we're at the end...
+  	else if (this.owner === this.root) {
+  		if (!this.owner.component) {
+  			// TODO but something else could have been appended to
+  			// this.root.el, no?
+  			node = null;
+  		}
+
+  		// ...unless this is a component
+  		else {
+  			node = this.owner.component.findNextNode();
+  		}
+  	} else {
+  		node = this.owner.findNextNode(this);
+  	}
+
+  	return node;
+  }
+
+  var prototype_firstNode = Fragment$firstNode;
+
+  function Fragment$firstNode() {
+  	if (this.items && this.items[0]) {
+  		return this.items[0].firstNode();
+  	}
+
+  	return null;
+  }
+
+  var shared_processItems = processItems;
+
+  function processItems(items, values, guid, counter) {
+  	counter = counter || 0;
+
+  	return items.map(function (item) {
+  		var placeholderId, wrapped, value;
+
+  		if (item.text) {
+  			return item.text;
+  		}
+
+  		if (item.fragments) {
+  			return item.fragments.map(function (fragment) {
+  				return processItems(fragment.items, values, guid, counter);
+  			}).join("");
+  		}
+
+  		placeholderId = guid + "-" + counter++;
+
+  		if (item.keypath && (wrapped = item.root.viewmodel.wrapped[item.keypath.str])) {
+  			value = wrapped.value;
+  		} else {
+  			value = item.getValue();
+  		}
+
+  		values[placeholderId] = value;
+
+  		return "${" + placeholderId + "}";
+  	}).join("");
+  }
+
+  var getArgsList = Fragment$getArgsList;
+  function Fragment$getArgsList() {
+  	var values, source, parsed, result;
+
+  	if (this.dirtyArgs) {
+  		source = shared_processItems(this.items, values = {}, this.root._guid);
+  		parsed = parseJSON("[" + source + "]", values);
+
+  		if (!parsed) {
+  			result = [this.toString()];
+  		} else {
+  			result = parsed.value;
+  		}
+
+  		this.argsList = result;
+  		this.dirtyArgs = false;
+  	}
+
+  	return this.argsList;
+  }
+
+  var getNode = Fragment$getNode;
+
+  function Fragment$getNode() {
+  	var fragment = this;
+
+  	do {
+  		if (fragment.pElement) {
+  			return fragment.pElement.node;
+  		}
+  	} while (fragment = fragment.parent);
+
+  	return this.root.detached || this.root.el;
+  }
+
+  var prototype_getValue = Fragment$getValue;
+  function Fragment$getValue() {
+  	var values, source, parsed, result;
+
+  	if (this.dirtyValue) {
+  		source = shared_processItems(this.items, values = {}, this.root._guid);
+  		parsed = parseJSON(source, values);
+
+  		if (!parsed) {
+  			result = this.toString();
+  		} else {
+  			result = parsed.value;
+  		}
+
+  		this.value = result;
+  		this.dirtyValue = false;
+  	}
+
+  	return this.value;
+  }
+
+  var shared_detach = function () {
+  	return detachNode(this.node);
+  };
+
+  var Text = function (options) {
+  	this.type = TEXT;
+  	this.text = options.template;
+  };
+
+  Text.prototype = {
+  	detach: shared_detach,
+
+  	firstNode: function () {
+  		return this.node;
+  	},
+
+  	render: function () {
+  		if (!this.node) {
+  			this.node = document.createTextNode(this.text);
+  		}
+
+  		return this.node;
+  	},
+
+  	toString: function (escape) {
+  		return escape ? escapeHtml(this.text) : this.text;
+  	},
+
+  	unrender: function (shouldDestroy) {
+  		if (shouldDestroy) {
+  			return this.detach();
+  		}
+  	}
+  };
+
+  var items_Text = Text;
+
+  var shared_unbind = shared_unbind__unbind;
+
+  function shared_unbind__unbind() {
+  	if (this.registered) {
+  		// this was registered as a dependant
+  		this.root.viewmodel.unregister(this.keypath, this);
+  	}
+
+  	if (this.resolver) {
+  		this.resolver.unbind();
+  	}
+  }
+
+  var Mustache_getValue = Mustache$getValue;
+
+  function Mustache$getValue() {
+  	return this.value;
+  }
+
+  var ReferenceResolver = function (owner, ref, callback) {
+  	var keypath;
+
+  	this.ref = ref;
+  	this.resolved = false;
+
+  	this.root = owner.root;
+  	this.parentFragment = owner.parentFragment;
+  	this.callback = callback;
+
+  	keypath = shared_resolveRef(owner.root, ref, owner.parentFragment);
+  	if (keypath != undefined) {
+  		this.resolve(keypath);
+  	} else {
+  		global_runloop.addUnresolved(this);
+  	}
+  };
+
+  ReferenceResolver.prototype = {
+  	resolve: function (keypath) {
+  		if (this.keypath && !keypath) {
+  			// it was resolved, and now it's not. Can happen if e.g. `bar` in
+  			// `{{foo[bar]}}` becomes undefined
+  			global_runloop.addUnresolved(this);
+  		}
+
+  		this.resolved = true;
+
+  		this.keypath = keypath;
+  		this.callback(keypath);
+  	},
+
+  	forceResolution: function () {
+  		this.resolve(getKeypath(this.ref));
+  	},
+
+  	rebind: function (oldKeypath, newKeypath) {
+  		var keypath;
+
+  		if (this.keypath != undefined) {
+  			keypath = this.keypath.replace(oldKeypath, newKeypath);
+  			// was a new keypath created?
+  			if (keypath !== undefined) {
+  				// resolve it
+  				this.resolve(keypath);
+  			}
+  		}
+  	},
+
+  	unbind: function () {
+  		if (!this.resolved) {
+  			global_runloop.removeUnresolved(this);
+  		}
+  	}
+  };
+
+  var Resolvers_ReferenceResolver = ReferenceResolver;
+
+  var SpecialResolver = function (owner, ref, callback) {
+  	this.parentFragment = owner.parentFragment;
+  	this.ref = ref;
+  	this.callback = callback;
+
+  	this.rebind();
+  };
+
+  var props = {
+  	"@keypath": { prefix: "c", prop: ["context"] },
+  	"@index": { prefix: "i", prop: ["index"] },
+  	"@key": { prefix: "k", prop: ["key", "index"] }
+  };
+
+  function getProp(target, prop) {
+  	var value;
+  	for (var i = 0; i < prop.prop.length; i++) {
+  		if ((value = target[prop.prop[i]]) !== undefined) {
+  			return value;
+  		}
+  	}
+  }
+
+  SpecialResolver.prototype = {
+  	rebind: function () {
+  		var ref = this.ref,
+  		    fragment = this.parentFragment,
+  		    prop = props[ref],
+  		    value;
+
+  		if (!prop) {
+  			throw new Error("Unknown special reference \"" + ref + "\" - valid references are @index, @key and @keypath");
+  		}
+
+  		// have we already found the nearest parent?
+  		if (this.cached) {
+  			return this.callback(getKeypath("@" + prop.prefix + getProp(this.cached, prop)));
+  		}
+
+  		// special case for indices, which may cross component boundaries
+  		if (prop.prop.indexOf("index") !== -1 || prop.prop.indexOf("key") !== -1) {
+  			while (fragment) {
+  				if (fragment.owner.currentSubtype === SECTION_EACH && (value = getProp(fragment, prop)) !== undefined) {
+  					this.cached = fragment;
+
+  					fragment.registerIndexRef(this);
+
+  					return this.callback(getKeypath("@" + prop.prefix + value));
+  				}
+
+  				// watch for component boundaries
+  				if (!fragment.parent && fragment.owner && fragment.owner.component && fragment.owner.component.parentFragment && !fragment.owner.component.instance.isolated) {
+  					fragment = fragment.owner.component.parentFragment;
+  				} else {
+  					fragment = fragment.parent;
+  				}
+  			}
+  		} else {
+  			while (fragment) {
+  				if ((value = getProp(fragment, prop)) !== undefined) {
+  					return this.callback(getKeypath("@" + prop.prefix + value.str));
+  				}
+
+  				fragment = fragment.parent;
+  			}
+  		}
+  	},
+
+  	unbind: function () {
+  		if (this.cached) {
+  			this.cached.unregisterIndexRef(this);
+  		}
+  	}
+  };
+
+  var Resolvers_SpecialResolver = SpecialResolver;
+
+  var IndexResolver = function (owner, ref, callback) {
+  	this.parentFragment = owner.parentFragment;
+  	this.ref = ref;
+  	this.callback = callback;
+
+  	ref.ref.fragment.registerIndexRef(this);
+
+  	this.rebind();
+  };
+
+  IndexResolver.prototype = {
+  	rebind: function () {
+  		var index,
+  		    ref = this.ref.ref;
+
+  		if (ref.ref.t === "k") {
+  			index = "k" + ref.fragment.key;
+  		} else {
+  			index = "i" + ref.fragment.index;
+  		}
+
+  		if (index !== undefined) {
+  			this.callback(getKeypath("@" + index));
+  		}
+  	},
+
+  	unbind: function () {
+  		this.ref.ref.fragment.unregisterIndexRef(this);
+  	}
+  };
+
+  var Resolvers_IndexResolver = IndexResolver;
+
+  var Resolvers_findIndexRefs = findIndexRefs;
+
+  function findIndexRefs(fragment, refName) {
+  	var result = {},
+  	    refs,
+  	    fragRefs,
+  	    ref,
+  	    i,
+  	    owner,
+  	    hit = false;
+
+  	if (!refName) {
+  		result.refs = refs = {};
+  	}
+
+  	while (fragment) {
+  		if ((owner = fragment.owner) && (fragRefs = owner.indexRefs)) {
+
+  			// we're looking for a particular ref, and it's here
+  			if (refName && (ref = owner.getIndexRef(refName))) {
+  				result.ref = {
+  					fragment: fragment,
+  					ref: ref
+  				};
+  				return result;
+  			}
+
+  			// we're collecting refs up-tree
+  			else if (!refName) {
+  				for (i in fragRefs) {
+  					ref = fragRefs[i];
+
+  					// don't overwrite existing refs - they should shadow parents
+  					if (!refs[ref.n]) {
+  						hit = true;
+  						refs[ref.n] = {
+  							fragment: fragment,
+  							ref: ref
+  						};
+  					}
+  				}
+  			}
+  		}
+
+  		// watch for component boundaries
+  		if (!fragment.parent && fragment.owner && fragment.owner.component && fragment.owner.component.parentFragment && !fragment.owner.component.instance.isolated) {
+  			result.componentBoundary = true;
+  			fragment = fragment.owner.component.parentFragment;
+  		} else {
+  			fragment = fragment.parent;
+  		}
+  	}
+
+  	if (!hit) {
+  		return undefined;
+  	} else {
+  		return result;
+  	}
+  }
+
+  findIndexRefs.resolve = function resolve(indices) {
+  	var refs = {},
+  	    k,
+  	    ref;
+
+  	for (k in indices.refs) {
+  		ref = indices.refs[k];
+  		refs[ref.ref.n] = ref.ref.t === "k" ? ref.fragment.key : ref.fragment.index;
+  	}
+
+  	return refs;
+  };
+
+  var Resolvers_createReferenceResolver = createReferenceResolver;
+  function createReferenceResolver(owner, ref, callback) {
+  	var indexRef;
+
+  	if (ref.charAt(0) === "@") {
+  		return new Resolvers_SpecialResolver(owner, ref, callback);
+  	}
+
+  	if (indexRef = Resolvers_findIndexRefs(owner.parentFragment, ref)) {
+  		return new Resolvers_IndexResolver(owner, indexRef, callback);
+  	}
+
+  	return new Resolvers_ReferenceResolver(owner, ref, callback);
+  }
+
+  var shared_getFunctionFromString = getFunctionFromString;
+  var cache = {};
+  function getFunctionFromString(str, i) {
+  	var fn, args;
+
+  	if (cache[str]) {
+  		return cache[str];
+  	}
+
+  	args = [];
+  	while (i--) {
+  		args[i] = "_" + i;
+  	}
+
+  	fn = new Function(args.join(","), "return(" + str + ")");
+
+  	cache[str] = fn;
+  	return fn;
+  }
+
+  var ExpressionResolver,
+      Resolvers_ExpressionResolver__bind = Function.prototype.bind;
+
+  ExpressionResolver = function (owner, parentFragment, expression, callback) {
+  	var _this = this;
+
+  	var ractive;
+
+  	ractive = owner.root;
+
+  	this.root = ractive;
+  	this.parentFragment = parentFragment;
+  	this.callback = callback;
+  	this.owner = owner;
+  	this.str = expression.s;
+  	this.keypaths = [];
+
+  	// Create resolvers for each reference
+  	this.pending = expression.r.length;
+  	this.refResolvers = expression.r.map(function (ref, i) {
+  		return Resolvers_createReferenceResolver(_this, ref, function (keypath) {
+  			_this.resolve(i, keypath);
+  		});
+  	});
+
+  	this.ready = true;
+  	this.bubble();
+  };
+
+  ExpressionResolver.prototype = {
+  	bubble: function () {
+  		if (!this.ready) {
+  			return;
+  		}
+
+  		this.uniqueString = getUniqueString(this.str, this.keypaths);
+  		this.keypath = createExpressionKeypath(this.uniqueString);
+
+  		this.createEvaluator();
+  		this.callback(this.keypath);
+  	},
+
+  	unbind: function () {
+  		var resolver;
+
+  		while (resolver = this.refResolvers.pop()) {
+  			resolver.unbind();
+  		}
+  	},
+
+  	resolve: function (index, keypath) {
+  		this.keypaths[index] = keypath;
+  		this.bubble();
+  	},
+
+  	createEvaluator: function () {
+  		var _this = this;
+
+  		var computation, valueGetters, signature, keypath, fn;
+
+  		keypath = this.keypath;
+  		computation = this.root.viewmodel.computations[keypath.str];
+
+  		// only if it doesn't exist yet!
+  		if (!computation) {
+  			fn = shared_getFunctionFromString(this.str, this.refResolvers.length);
+
+  			valueGetters = this.keypaths.map(function (keypath) {
+  				var value;
+
+  				if (keypath === "undefined") {
+  					return function () {
+  						return undefined;
+  					};
+  				}
+
+  				// 'special' keypaths encode a value
+  				if (keypath.isSpecial) {
+  					value = keypath.value;
+  					return function () {
+  						return value;
+  					};
+  				}
+
+  				return function () {
+  					var value = _this.root.viewmodel.get(keypath, { noUnwrap: true, fullRootGet: true });
+  					if (typeof value === "function") {
+  						value = wrapFunction(value, _this.root);
+  					}
+  					return value;
+  				};
+  			});
+
+  			signature = {
+  				deps: this.keypaths.filter(isValidDependency),
+  				getter: function () {
+  					var args = valueGetters.map(call);
+  					return fn.apply(null, args);
+  				}
+  			};
+
+  			computation = this.root.viewmodel.compute(keypath, signature);
+  		} else {
+  			this.root.viewmodel.mark(keypath);
+  		}
+  	},
+
+  	rebind: function (oldKeypath, newKeypath) {
+  		// TODO only bubble once, no matter how many references are affected by the rebind
+  		this.refResolvers.forEach(function (r) {
+  			return r.rebind(oldKeypath, newKeypath);
+  		});
+  	}
+  };
+
+  var Resolvers_ExpressionResolver = ExpressionResolver;
+
+  function call(value) {
+  	return value.call();
+  }
+
+  function getUniqueString(str, keypaths) {
+  	// get string that is unique to this expression
+  	return str.replace(/_([0-9]+)/g, function (match, $1) {
+  		var keypath, value;
+
+  		// make sure we're not replacing a non-keypath _[0-9]
+  		if (+$1 >= keypaths.length) {
+  			return "_" + $1;
+  		}
+
+  		keypath = keypaths[$1];
+
+  		if (keypath === undefined) {
+  			return "undefined";
+  		}
+
+  		if (keypath.isSpecial) {
+  			value = keypath.value;
+  			return typeof value === "number" ? value : "\"" + value + "\"";
+  		}
+
+  		return keypath.str;
+  	});
+  }
+
+  function createExpressionKeypath(uniqueString) {
+  	// Sanitize by removing any periods or square brackets. Otherwise
+  	// we can't split the keypath into keys!
+  	// Remove asterisks too, since they mess with pattern observers
+  	return getKeypath("${" + uniqueString.replace(/[\.\[\]]/g, "-").replace(/\*/, "#MUL#") + "}");
+  }
+
+  function isValidDependency(keypath) {
+  	return keypath !== undefined && keypath[0] !== "@";
+  }
+
+  function wrapFunction(fn, ractive) {
+  	var wrapped, prop, key;
+
+  	if (fn.__ractive_nowrap) {
+  		return fn;
+  	}
+
+  	prop = "__ractive_" + ractive._guid;
+  	wrapped = fn[prop];
+
+  	if (wrapped) {
+  		return wrapped;
+  	} else if (/this/.test(fn.toString())) {
+  		defineProperty(fn, prop, {
+  			value: Resolvers_ExpressionResolver__bind.call(fn, ractive),
+  			configurable: true
+  		});
+
+  		// Add properties/methods to wrapped function
+  		for (key in fn) {
+  			if (fn.hasOwnProperty(key)) {
+  				fn[prop][key] = fn[key];
+  			}
+  		}
+
+  		ractive._boundFunctions.push({
+  			fn: fn,
+  			prop: prop
+  		});
+
+  		return fn[prop];
+  	}
+
+  	defineProperty(fn, "__ractive_nowrap", {
+  		value: fn
+  	});
+
+  	return fn.__ractive_nowrap;
+  }
+
+  var MemberResolver = function (template, resolver, parentFragment) {
+  	var _this = this;
+
+  	this.resolver = resolver;
+  	this.root = resolver.root;
+  	this.parentFragment = parentFragment;
+  	this.viewmodel = resolver.root.viewmodel;
+
+  	if (typeof template === "string") {
+  		this.value = template;
+  	}
+
+  	// Simple reference?
+  	else if (template.t === REFERENCE) {
+  		this.refResolver = Resolvers_createReferenceResolver(this, template.n, function (keypath) {
+  			_this.resolve(keypath);
+  		});
+  	}
+
+  	// Otherwise we have an expression in its own right
+  	else {
+  		new Resolvers_ExpressionResolver(resolver, parentFragment, template, function (keypath) {
+  			_this.resolve(keypath);
+  		});
+  	}
+  };
+
+  MemberResolver.prototype = {
+  	resolve: function (keypath) {
+  		if (this.keypath) {
+  			this.viewmodel.unregister(this.keypath, this);
+  		}
+
+  		this.keypath = keypath;
+  		this.value = this.viewmodel.get(keypath);
+
+  		this.bind();
+
+  		this.resolver.bubble();
+  	},
+
+  	bind: function () {
+  		this.viewmodel.register(this.keypath, this);
+  	},
+
+  	rebind: function (oldKeypath, newKeypath) {
+  		if (this.refResolver) {
+  			this.refResolver.rebind(oldKeypath, newKeypath);
+  		}
+  	},
+
+  	setValue: function (value) {
+  		this.value = value;
+  		this.resolver.bubble();
+  	},
+
+  	unbind: function () {
+  		if (this.keypath) {
+  			this.viewmodel.unregister(this.keypath, this);
+  		}
+
+  		if (this.refResolver) {
+  			this.refResolver.unbind();
+  		}
+  	},
+
+  	forceResolution: function () {
+  		if (this.refResolver) {
+  			this.refResolver.forceResolution();
+  		}
+  	}
+  };
+
+  var ReferenceExpressionResolver_MemberResolver = MemberResolver;
+
+  var ReferenceExpressionResolver = function (mustache, template, callback) {
+  	var _this = this;
+
+  	var ractive, ref, keypath, parentFragment;
+
+  	this.parentFragment = parentFragment = mustache.parentFragment;
+  	this.root = ractive = mustache.root;
+  	this.mustache = mustache;
+
+  	this.ref = ref = template.r;
+  	this.callback = callback;
+
+  	this.unresolved = [];
+
+  	// Find base keypath
+  	if (keypath = shared_resolveRef(ractive, ref, parentFragment)) {
+  		this.base = keypath;
+  	} else {
+  		this.baseResolver = new Resolvers_ReferenceResolver(this, ref, function (keypath) {
+  			_this.base = keypath;
+  			_this.baseResolver = null;
+  			_this.bubble();
+  		});
+  	}
+
+  	// Find values for members, or mark them as unresolved
+  	this.members = template.m.map(function (template) {
+  		return new ReferenceExpressionResolver_MemberResolver(template, _this, parentFragment);
+  	});
+
+  	this.ready = true;
+  	this.bubble(); // trigger initial resolution if possible
+  };
+
+  ReferenceExpressionResolver.prototype = {
+  	getKeypath: function () {
+  		var values = this.members.map(ReferenceExpressionResolver_ReferenceExpressionResolver__getValue);
+
+  		if (!values.every(isDefined) || this.baseResolver) {
+  			return null;
+  		}
+
+  		return this.base.join(values.join("."));
+  	},
+
+  	bubble: function () {
+  		if (!this.ready || this.baseResolver) {
+  			return;
+  		}
+
+  		this.callback(this.getKeypath());
+  	},
+
+  	unbind: function () {
+  		this.members.forEach(methodCallers__unbind);
+  	},
+
+  	rebind: function (oldKeypath, newKeypath) {
+  		var changed;
+
+  		if (this.base) {
+  			var newBase = this.base.replace(oldKeypath, newKeypath);
+  			if (newBase && newBase !== this.base) {
+  				this.base = newBase;
+  				changed = true;
+  			}
+  		}
+
+  		this.members.forEach(function (members) {
+  			if (members.rebind(oldKeypath, newKeypath)) {
+  				changed = true;
+  			}
+  		});
+
+  		if (changed) {
+  			this.bubble();
+  		}
+  	},
+
+  	forceResolution: function () {
+  		if (this.baseResolver) {
+  			this.base = getKeypath(this.ref);
+
+  			this.baseResolver.unbind();
+  			this.baseResolver = null;
+  		}
+
+  		this.members.forEach(forceResolution);
+  		this.bubble();
+  	}
+  };
+
+  function ReferenceExpressionResolver_ReferenceExpressionResolver__getValue(member) {
+  	return member.value;
+  }
+
+  function isDefined(value) {
+  	return value != undefined;
+  }
+
+  function forceResolution(member) {
+  	member.forceResolution();
+  }
+
+  var ReferenceExpressionResolver_ReferenceExpressionResolver = ReferenceExpressionResolver;
+
+  var Mustache_initialise = Mustache$init;
+  function Mustache$init(mustache, options) {
+
+  	var ref, parentFragment, template;
+
+  	parentFragment = options.parentFragment;
+  	template = options.template;
+
+  	mustache.root = parentFragment.root;
+  	mustache.parentFragment = parentFragment;
+  	mustache.pElement = parentFragment.pElement;
+
+  	mustache.template = options.template;
+  	mustache.index = options.index || 0;
+  	mustache.isStatic = options.template.s;
+
+  	mustache.type = options.template.t;
+
+  	mustache.registered = false;
+
+  	// if this is a simple mustache, with a reference, we just need to resolve
+  	// the reference to a keypath
+  	if (ref = template.r) {
+  		mustache.resolver = Resolvers_createReferenceResolver(mustache, ref, resolve);
+  	}
+
+  	// if it's an expression, we have a bit more work to do
+  	if (options.template.x) {
+  		mustache.resolver = new Resolvers_ExpressionResolver(mustache, parentFragment, options.template.x, resolveAndRebindChildren);
+  	}
+
+  	if (options.template.rx) {
+  		mustache.resolver = new ReferenceExpressionResolver_ReferenceExpressionResolver(mustache, options.template.rx, resolveAndRebindChildren);
+  	}
+
+  	// Special case - inverted sections
+  	if (mustache.template.n === SECTION_UNLESS && !mustache.hasOwnProperty("value")) {
+  		mustache.setValue(undefined);
+  	}
+
+  	function resolve(keypath) {
+  		mustache.resolve(keypath);
+  	}
+
+  	function resolveAndRebindChildren(newKeypath) {
+  		var oldKeypath = mustache.keypath;
+
+  		if (newKeypath != oldKeypath) {
+  			mustache.resolve(newKeypath);
+
+  			if (oldKeypath !== undefined) {
+  				mustache.fragments && mustache.fragments.forEach(function (f) {
+  					f.rebind(oldKeypath, newKeypath);
+  				});
+  			}
+  		}
+  	}
+  }
+
+  var Mustache_resolve = Mustache$resolve;
+
+  function Mustache$resolve(keypath) {
+  	var wasResolved, value, twowayBinding;
+
+  	// 'Special' keypaths, e.g. @foo or @7, encode a value
+  	if (keypath && keypath.isSpecial) {
+  		this.keypath = keypath;
+  		this.setValue(keypath.value);
+  		return;
+  	}
+
+  	// If we resolved previously, we need to unregister
+  	if (this.registered) {
+  		// undefined or null
+  		this.root.viewmodel.unregister(this.keypath, this);
+  		this.registered = false;
+
+  		wasResolved = true;
+  	}
+
+  	this.keypath = keypath;
+
+  	// If the new keypath exists, we need to register
+  	// with the viewmodel
+  	if (keypath != undefined) {
+  		// undefined or null
+  		value = this.root.viewmodel.get(keypath);
+  		this.root.viewmodel.register(keypath, this);
+
+  		this.registered = true;
+  	}
+
+  	// Either way we need to queue up a render (`value`
+  	// will be `undefined` if there's no keypath)
+  	this.setValue(value);
+
+  	// Two-way bindings need to point to their new target keypath
+  	if (wasResolved && (twowayBinding = this.twowayBinding)) {
+  		twowayBinding.rebound();
+  	}
+  }
+
+  var Mustache_rebind = Mustache$rebind;
+
+  function Mustache$rebind(oldKeypath, newKeypath) {
+  	// Children first
+  	if (this.fragments) {
+  		this.fragments.forEach(function (f) {
+  			return f.rebind(oldKeypath, newKeypath);
+  		});
+  	}
+
+  	// Expression mustache?
+  	if (this.resolver) {
+  		this.resolver.rebind(oldKeypath, newKeypath);
+  	}
+  }
+
+  var Mustache = {
+  	getValue: Mustache_getValue,
+  	init: Mustache_initialise,
+  	resolve: Mustache_resolve,
+  	rebind: Mustache_rebind
+  };
+
+  var Interpolator = function (options) {
+  	this.type = INTERPOLATOR;
+  	Mustache.init(this, options);
+  };
+
+  Interpolator.prototype = {
+  	update: function () {
+  		this.node.data = this.value == undefined ? "" : this.value;
+  	},
+  	resolve: Mustache.resolve,
+  	rebind: Mustache.rebind,
+  	detach: shared_detach,
+
+  	unbind: shared_unbind,
+
+  	render: function () {
+  		if (!this.node) {
+  			this.node = document.createTextNode(safeToStringValue(this.value));
+  		}
+
+  		return this.node;
+  	},
+
+  	unrender: function (shouldDestroy) {
+  		if (shouldDestroy) {
+  			detachNode(this.node);
+  		}
+  	},
+
+  	getValue: Mustache.getValue,
+
+  	// TEMP
+  	setValue: function (value) {
+  		var wrapper;
+
+  		// TODO is there a better way to approach this?
+  		if (this.keypath && (wrapper = this.root.viewmodel.wrapped[this.keypath.str])) {
+  			value = wrapper.get();
+  		}
+
+  		if (!isEqual(value, this.value)) {
+  			this.value = value;
+  			this.parentFragment.bubble();
+
+  			if (this.node) {
+  				global_runloop.addView(this);
+  			}
+  		}
+  	},
+
+  	firstNode: function () {
+  		return this.node;
+  	},
+
+  	toString: function (escape) {
+  		var string = "" + safeToStringValue(this.value);
+  		return escape ? escapeHtml(string) : string;
+  	}
+  };
+
+  var items_Interpolator = Interpolator;
+
+  var Section_prototype_bubble = Section$bubble;
+
+  function Section$bubble() {
+  	this.parentFragment.bubble();
+  }
+
+  var Section_prototype_detach = Section$detach;
+
+  function Section$detach() {
+  	var docFrag;
+
+  	if (this.fragments.length === 1) {
+  		return this.fragments[0].detach();
+  	}
+
+  	docFrag = document.createDocumentFragment();
+
+  	this.fragments.forEach(function (item) {
+  		docFrag.appendChild(item.detach());
+  	});
+
+  	return docFrag;
+  }
+
+  var find = Section$find;
+
+  function Section$find(selector) {
+  	var i, len, queryResult;
+
+  	len = this.fragments.length;
+  	for (i = 0; i < len; i += 1) {
+  		if (queryResult = this.fragments[i].find(selector)) {
+  			return queryResult;
+  		}
+  	}
+
+  	return null;
+  }
+
+  var findAll = Section$findAll;
+
+  function Section$findAll(selector, query) {
+  	var i, len;
+
+  	len = this.fragments.length;
+  	for (i = 0; i < len; i += 1) {
+  		this.fragments[i].findAll(selector, query);
+  	}
+  }
+
+  var findAllComponents = Section$findAllComponents;
+
+  function Section$findAllComponents(selector, query) {
+  	var i, len;
+
+  	len = this.fragments.length;
+  	for (i = 0; i < len; i += 1) {
+  		this.fragments[i].findAllComponents(selector, query);
+  	}
+  }
+
+  var findComponent = Section$findComponent;
+
+  function Section$findComponent(selector) {
+  	var i, len, queryResult;
+
+  	len = this.fragments.length;
+  	for (i = 0; i < len; i += 1) {
+  		if (queryResult = this.fragments[i].findComponent(selector)) {
+  			return queryResult;
+  		}
+  	}
+
+  	return null;
+  }
+
+  var findNextNode = Section$findNextNode;
+
+  function Section$findNextNode(fragment) {
+  	if (this.fragments[fragment.index + 1]) {
+  		return this.fragments[fragment.index + 1].firstNode();
+  	}
+
+  	return this.parentFragment.findNextNode(this);
+  }
+
+  var firstNode = Section$firstNode;
+
+  function Section$firstNode() {
+  	var len, i, node;
+
+  	if (len = this.fragments.length) {
+  		for (i = 0; i < len; i += 1) {
+  			if (node = this.fragments[i].firstNode()) {
+  				return node;
+  			}
+  		}
+  	}
+
+  	return this.parentFragment.findNextNode(this);
+  }
+
+  var shuffle = Section$shuffle;
+
+  function Section$shuffle(newIndices) {
+  	var _this = this;
+
+  	var parentFragment, firstChange, i, newLength, reboundFragments, fragmentOptions, fragment;
+
+  	// short circuit any double-updates, and ensure that this isn't applied to
+  	// non-list sections
+  	if (this.shuffling || this.unbound || this.currentSubtype !== SECTION_EACH) {
+  		return;
+  	}
+
+  	this.shuffling = true;
+  	global_runloop.scheduleTask(function () {
+  		return _this.shuffling = false;
+  	});
+
+  	parentFragment = this.parentFragment;
+
+  	reboundFragments = [];
+
+  	// TODO: need to update this
+  	// first, rebind existing fragments
+  	newIndices.forEach(function (newIndex, oldIndex) {
+  		var fragment, by, oldKeypath, newKeypath, deps;
+
+  		if (newIndex === oldIndex) {
+  			reboundFragments[newIndex] = _this.fragments[oldIndex];
+  			return;
+  		}
+
+  		fragment = _this.fragments[oldIndex];
+
+  		if (firstChange === undefined) {
+  			firstChange = oldIndex;
+  		}
+
+  		// does this fragment need to be torn down?
+  		if (newIndex === -1) {
+  			_this.fragmentsToUnrender.push(fragment);
+  			fragment.unbind();
+  			return;
+  		}
+
+  		// Otherwise, it needs to be rebound to a new index
+  		by = newIndex - oldIndex;
+  		oldKeypath = _this.keypath.join(oldIndex);
+  		newKeypath = _this.keypath.join(newIndex);
+
+  		fragment.index = newIndex;
+
+  		// notify any registered index refs directly
+  		if (deps = fragment.registeredIndexRefs) {
+  			deps.forEach(shuffle__blindRebind);
+  		}
+
+  		fragment.rebind(oldKeypath, newKeypath);
+  		reboundFragments[newIndex] = fragment;
+  	});
+
+  	newLength = this.root.viewmodel.get(this.keypath).length;
+
+  	// If nothing changed with the existing fragments, then we start adding
+  	// new fragments at the end...
+  	if (firstChange === undefined) {
+  		// ...unless there are no new fragments to add
+  		if (this.length === newLength) {
+  			return;
+  		}
+
+  		firstChange = this.length;
+  	}
+
+  	this.length = this.fragments.length = newLength;
+
+  	if (this.rendered) {
+  		global_runloop.addView(this);
+  	}
+
+  	// Prepare new fragment options
+  	fragmentOptions = {
+  		template: this.template.f,
+  		root: this.root,
+  		owner: this
+  	};
+
+  	// Add as many new fragments as we need to, or add back existing
+  	// (detached) fragments
+  	for (i = firstChange; i < newLength; i += 1) {
+  		fragment = reboundFragments[i];
+
+  		if (!fragment) {
+  			this.fragmentsToCreate.push(i);
+  		}
+
+  		this.fragments[i] = fragment;
+  	}
+  }
+
+  function shuffle__blindRebind(dep) {
+  	// the keypath doesn't actually matter here as it won't have changed
+  	dep.rebind("", "");
+  }
+
+  var prototype_rebind = function (oldKeypath, newKeypath) {
+  	Mustache.rebind.call(this, oldKeypath, newKeypath);
+  };
+
+  var Section_prototype_render = Section$render;
+
+  function Section$render() {
+  	var _this = this;
+
+  	this.docFrag = document.createDocumentFragment();
+
+  	this.fragments.forEach(function (f) {
+  		return _this.docFrag.appendChild(f.render());
+  	});
+
+  	this.renderedFragments = this.fragments.slice();
+  	this.fragmentsToRender = [];
+
+  	this.rendered = true;
+  	return this.docFrag;
+  }
+
+  var setValue = Section$setValue;
+
+  function Section$setValue(value) {
+  	var _this = this;
+
+  	var wrapper, fragmentOptions;
+
+  	if (this.updating) {
+  		// If a child of this section causes a re-evaluation - for example, an
+  		// expression refers to a function that mutates the array that this
+  		// section depends on - we'll end up with a double rendering bug (see
+  		// https://github.com/ractivejs/ractive/issues/748). This prevents it.
+  		return;
+  	}
+
+  	this.updating = true;
+
+  	// with sections, we need to get the fake value if we have a wrapped object
+  	if (this.keypath && (wrapper = this.root.viewmodel.wrapped[this.keypath.str])) {
+  		value = wrapper.get();
+  	}
+
+  	// If any fragments are awaiting creation after a splice,
+  	// this is the place to do it
+  	if (this.fragmentsToCreate.length) {
+  		fragmentOptions = {
+  			template: this.template.f || [],
+  			root: this.root,
+  			pElement: this.pElement,
+  			owner: this
+  		};
+
+  		this.fragmentsToCreate.forEach(function (index) {
+  			var fragment;
+
+  			fragmentOptions.context = _this.keypath.join(index);
+  			fragmentOptions.index = index;
+
+  			fragment = new virtualdom_Fragment(fragmentOptions);
+  			_this.fragmentsToRender.push(_this.fragments[index] = fragment);
+  		});
+
+  		this.fragmentsToCreate.length = 0;
+  	} else if (reevaluateSection(this, value)) {
+  		this.bubble();
+
+  		if (this.rendered) {
+  			global_runloop.addView(this);
+  		}
+  	}
+
+  	this.value = value;
+  	this.updating = false;
+  }
+
+  function changeCurrentSubtype(section, value, obj) {
+  	if (value === SECTION_EACH) {
+  		// make sure ref type is up to date for key or value indices
+  		if (section.indexRefs && section.indexRefs[0]) {
+  			var ref = section.indexRefs[0];
+
+  			// when switching flavors, make sure the section gets updated
+  			if (obj && ref.t === "i" || !obj && ref.t === "k") {
+  				// if switching from object to list, unbind all of the old fragments
+  				if (!obj) {
+  					section.length = 0;
+  					section.fragmentsToUnrender = section.fragments.slice(0);
+  					section.fragmentsToUnrender.forEach(function (f) {
+  						return f.unbind();
+  					});
+  				}
+  			}
+
+  			ref.t = obj ? "k" : "i";
+  		}
+  	}
+
+  	section.currentSubtype = value;
+  }
+
+  function reevaluateSection(section, value) {
+  	var fragmentOptions = {
+  		template: section.template.f || [],
+  		root: section.root,
+  		pElement: section.parentFragment.pElement,
+  		owner: section
+  	};
+
+  	section.hasContext = true;
+
+  	// If we already know the section type, great
+  	// TODO can this be optimised? i.e. pick an reevaluateSection function during init
+  	// and avoid doing this each time?
+  	if (section.subtype) {
+  		switch (section.subtype) {
+  			case SECTION_IF:
+  				section.hasContext = false;
+  				return reevaluateConditionalSection(section, value, false, fragmentOptions);
+
+  			case SECTION_UNLESS:
+  				section.hasContext = false;
+  				return reevaluateConditionalSection(section, value, true, fragmentOptions);
+
+  			case SECTION_WITH:
+  				return reevaluateContextSection(section, fragmentOptions);
+
+  			case SECTION_IF_WITH:
+  				return reevaluateConditionalContextSection(section, value, fragmentOptions);
+
+  			case SECTION_EACH:
+  				if (isObject(value)) {
+  					changeCurrentSubtype(section, section.subtype, true);
+  					return reevaluateListObjectSection(section, value, fragmentOptions);
+  				}
+
+  				// Fallthrough - if it's a conditional or an array we need to continue
+  		}
+  	}
+
+  	// Otherwise we need to work out what sort of section we're dealing with
+  	section.ordered = !!isArrayLike(value);
+
+  	// Ordered list section
+  	if (section.ordered) {
+  		changeCurrentSubtype(section, SECTION_EACH, false);
+  		return reevaluateListSection(section, value, fragmentOptions);
+  	}
+
+  	// Unordered list, or context
+  	if (isObject(value) || typeof value === "function") {
+  		// Index reference indicates section should be treated as a list
+  		if (section.template.i) {
+  			changeCurrentSubtype(section, SECTION_EACH, true);
+  			return reevaluateListObjectSection(section, value, fragmentOptions);
+  		}
+
+  		// Otherwise, object provides context for contents
+  		changeCurrentSubtype(section, SECTION_WITH, false);
+  		return reevaluateContextSection(section, fragmentOptions);
+  	}
+
+  	// Conditional section
+  	changeCurrentSubtype(section, SECTION_IF, false);
+  	section.hasContext = false;
+  	return reevaluateConditionalSection(section, value, false, fragmentOptions);
+  }
+
+  function reevaluateListSection(section, value, fragmentOptions) {
+  	var i, length, fragment;
+
+  	length = value.length;
+
+  	if (length === section.length) {
+  		// Nothing to do
+  		return false;
+  	}
+
+  	// if the array is shorter than it was previously, remove items
+  	if (length < section.length) {
+  		section.fragmentsToUnrender = section.fragments.splice(length, section.length - length);
+  		section.fragmentsToUnrender.forEach(methodCallers__unbind);
+  	}
+
+  	// otherwise...
+  	else {
+  		if (length > section.length) {
+  			// add any new ones
+  			for (i = section.length; i < length; i += 1) {
+  				// append list item to context stack
+  				fragmentOptions.context = section.keypath.join(i);
+  				fragmentOptions.index = i;
+
+  				fragment = new virtualdom_Fragment(fragmentOptions);
+  				section.fragmentsToRender.push(section.fragments[i] = fragment);
+  			}
+  		}
+  	}
+
+  	section.length = length;
+  	return true;
+  }
+
+  function reevaluateListObjectSection(section, value, fragmentOptions) {
+  	var id, i, hasKey, fragment, changed, deps;
+
+  	hasKey = section.hasKey || (section.hasKey = {});
+
+  	// remove any fragments that should no longer exist
+  	i = section.fragments.length;
+  	while (i--) {
+  		fragment = section.fragments[i];
+
+  		if (!(fragment.key in value)) {
+  			changed = true;
+
+  			fragment.unbind();
+  			section.fragmentsToUnrender.push(fragment);
+  			section.fragments.splice(i, 1);
+
+  			hasKey[fragment.key] = false;
+  		}
+  	}
+
+  	// notify any dependents about changed indices
+  	i = section.fragments.length;
+  	while (i--) {
+  		fragment = section.fragments[i];
+
+  		if (fragment.index !== i) {
+  			fragment.index = i;
+  			if (deps = fragment.registeredIndexRefs) {
+  				deps.forEach(setValue__blindRebind);
+  			}
+  		}
+  	}
+
+  	// add any that haven't been created yet
+  	i = section.fragments.length;
+  	for (id in value) {
+  		if (!hasKey[id]) {
+  			changed = true;
+
+  			fragmentOptions.context = section.keypath.join(id);
+  			fragmentOptions.key = id;
+  			fragmentOptions.index = i++;
+
+  			fragment = new virtualdom_Fragment(fragmentOptions);
+
+  			section.fragmentsToRender.push(fragment);
+  			section.fragments.push(fragment);
+  			hasKey[id] = true;
+  		}
+  	}
+
+  	section.length = section.fragments.length;
+  	return changed;
+  }
+
+  function reevaluateConditionalContextSection(section, value, fragmentOptions) {
+  	if (value) {
+  		return reevaluateContextSection(section, fragmentOptions);
+  	} else {
+  		return removeSectionFragments(section);
+  	}
+  }
+
+  function reevaluateContextSection(section, fragmentOptions) {
+  	var fragment;
+
+  	// ...then if it isn't rendered, render it, adding section.keypath to the context stack
+  	// (if it is already rendered, then any children dependent on the context stack
+  	// will update themselves without any prompting)
+  	if (!section.length) {
+  		// append this section to the context stack
+  		fragmentOptions.context = section.keypath;
+  		fragmentOptions.index = 0;
+
+  		fragment = new virtualdom_Fragment(fragmentOptions);
+
+  		section.fragmentsToRender.push(section.fragments[0] = fragment);
+  		section.length = 1;
+
+  		return true;
+  	}
+  }
+
+  function reevaluateConditionalSection(section, value, inverted, fragmentOptions) {
+  	var doRender, emptyArray, emptyObject, fragment, name;
+
+  	emptyArray = isArrayLike(value) && value.length === 0;
+  	emptyObject = false;
+  	if (!isArrayLike(value) && isObject(value)) {
+  		emptyObject = true;
+  		for (name in value) {
+  			emptyObject = false;
+  			break;
+  		}
+  	}
+
+  	if (inverted) {
+  		doRender = emptyArray || emptyObject || !value;
+  	} else {
+  		doRender = value && !emptyArray && !emptyObject;
+  	}
+
+  	if (doRender) {
+  		if (!section.length) {
+  			// no change to context stack
+  			fragmentOptions.index = 0;
+
+  			fragment = new virtualdom_Fragment(fragmentOptions);
+  			section.fragmentsToRender.push(section.fragments[0] = fragment);
+  			section.length = 1;
+
+  			return true;
+  		}
+
+  		if (section.length > 1) {
+  			section.fragmentsToUnrender = section.fragments.splice(1);
+  			section.fragmentsToUnrender.forEach(methodCallers__unbind);
+
+  			return true;
+  		}
+  	} else {
+  		return removeSectionFragments(section);
+  	}
+  }
+
+  function removeSectionFragments(section) {
+  	if (section.length) {
+  		section.fragmentsToUnrender = section.fragments.splice(0, section.fragments.length).filter(isRendered);
+  		section.fragmentsToUnrender.forEach(methodCallers__unbind);
+  		section.length = section.fragmentsToRender.length = 0;
+  		return true;
+  	}
+  }
+
+  function isRendered(fragment) {
+  	return fragment.rendered;
+  }
+
+  function setValue__blindRebind(dep) {
+  	// the keypath doesn't actually matter here as it won't have changed
+  	dep.rebind("", "");
+  }
+
+  var prototype_toString = Section$toString;
+
+  function Section$toString(escape) {
+  	var str, i, len;
+
+  	str = "";
+
+  	i = 0;
+  	len = this.length;
+
+  	for (i = 0; i < len; i += 1) {
+  		str += this.fragments[i].toString(escape);
+  	}
+
+  	return str;
+  }
+
+  var prototype_unbind = Section$unbind;
+  function Section$unbind() {
+  	var _this = this;
+
+  	this.fragments.forEach(methodCallers__unbind);
+  	this.fragmentsToRender.forEach(function (f) {
+  		return removeFromArray(_this.fragments, f);
+  	});
+  	this.fragmentsToRender = [];
+  	shared_unbind.call(this);
+
+  	this.length = 0;
+  	this.unbound = true;
+  }
+
+  var prototype_unrender = Section$unrender;
+
+  function Section$unrender(shouldDestroy) {
+  	this.fragments.forEach(shouldDestroy ? unrenderAndDestroy : prototype_unrender__unrender);
+  	this.renderedFragments = [];
+  	this.rendered = false;
+  }
+
+  function unrenderAndDestroy(fragment) {
+  	fragment.unrender(true);
+  }
+
+  function prototype_unrender__unrender(fragment) {
+  	fragment.unrender(false);
+  }
+
+  var prototype_update = Section$update;
+
+  function Section$update() {
+  	var fragment, renderIndex, renderedFragments, anchor, target, i, len;
+
+  	// `this.renderedFragments` is in the order of the previous render.
+  	// If fragments have shuffled about, this allows us to quickly
+  	// reinsert them in the correct place
+  	renderedFragments = this.renderedFragments;
+
+  	// Remove fragments that have been marked for destruction
+  	while (fragment = this.fragmentsToUnrender.pop()) {
+  		fragment.unrender(true);
+  		renderedFragments.splice(renderedFragments.indexOf(fragment), 1);
+  	}
+
+  	// Render new fragments (but don't insert them yet)
+  	while (fragment = this.fragmentsToRender.shift()) {
+  		fragment.render();
+  	}
+
+  	if (this.rendered) {
+  		target = this.parentFragment.getNode();
+  	}
+
+  	len = this.fragments.length;
+  	for (i = 0; i < len; i += 1) {
+  		fragment = this.fragments[i];
+  		renderIndex = renderedFragments.indexOf(fragment, i); // search from current index - it's guaranteed to be the same or higher
+
+  		if (renderIndex === i) {
+  			// already in the right place. insert accumulated nodes (if any) and carry on
+  			if (this.docFrag.childNodes.length) {
+  				anchor = fragment.firstNode();
+  				target.insertBefore(this.docFrag, anchor);
+  			}
+
+  			continue;
+  		}
+
+  		this.docFrag.appendChild(fragment.detach());
+
+  		// update renderedFragments
+  		if (renderIndex !== -1) {
+  			renderedFragments.splice(renderIndex, 1);
+  		}
+  		renderedFragments.splice(i, 0, fragment);
+  	}
+
+  	if (this.rendered && this.docFrag.childNodes.length) {
+  		anchor = this.parentFragment.findNextNode(this);
+  		target.insertBefore(this.docFrag, anchor);
+  	}
+
+  	// Save the rendering order for next time
+  	this.renderedFragments = this.fragments.slice();
+  }
+
+  var Section = function (options) {
+  	this.type = SECTION;
+  	this.subtype = this.currentSubtype = options.template.n;
+  	this.inverted = this.subtype === SECTION_UNLESS;
+
+  	this.pElement = options.pElement;
+
+  	this.fragments = [];
+  	this.fragmentsToCreate = [];
+  	this.fragmentsToRender = [];
+  	this.fragmentsToUnrender = [];
+
+  	if (options.template.i) {
+  		this.indexRefs = options.template.i.split(",").map(function (k, i) {
+  			return { n: k, t: i === 0 ? "k" : "i" };
+  		});
+  	}
+
+  	this.renderedFragments = [];
+
+  	this.length = 0; // number of times this section is rendered
+
+  	Mustache.init(this, options);
+  };
+
+  Section.prototype = {
+  	bubble: Section_prototype_bubble,
+  	detach: Section_prototype_detach,
+  	find: find,
+  	findAll: findAll,
+  	findAllComponents: findAllComponents,
+  	findComponent: findComponent,
+  	findNextNode: findNextNode,
+  	firstNode: firstNode,
+  	getIndexRef: function (name) {
+  		if (this.indexRefs) {
+  			var i = this.indexRefs.length;
+  			while (i--) {
+  				var ref = this.indexRefs[i];
+  				if (ref.n === name) {
+  					return ref;
+  				}
+  			}
+  		}
+  	},
+  	getValue: Mustache.getValue,
+  	shuffle: shuffle,
+  	rebind: prototype_rebind,
+  	render: Section_prototype_render,
+  	resolve: Mustache.resolve,
+  	setValue: setValue,
+  	toString: prototype_toString,
+  	unbind: prototype_unbind,
+  	unrender: prototype_unrender,
+  	update: prototype_update
+  };
+
+  var _Section = Section;
+
+  var Triple_prototype_detach = Triple$detach;
+
+  function Triple$detach() {
+  	var len, i;
+
+  	if (this.docFrag) {
+  		len = this.nodes.length;
+  		for (i = 0; i < len; i += 1) {
+  			this.docFrag.appendChild(this.nodes[i]);
+  		}
+
+  		return this.docFrag;
+  	}
+  }
+
+  var Triple_prototype_find = Triple$find;
+  function Triple$find(selector) {
+  	var i, len, node, queryResult;
+
+  	len = this.nodes.length;
+  	for (i = 0; i < len; i += 1) {
+  		node = this.nodes[i];
+
+  		if (node.nodeType !== 1) {
+  			continue;
+  		}
+
+  		if (matches(node, selector)) {
+  			return node;
+  		}
+
+  		if (queryResult = node.querySelector(selector)) {
+  			return queryResult;
+  		}
+  	}
+
+  	return null;
+  }
+
+  var Triple_prototype_findAll = Triple$findAll;
+  function Triple$findAll(selector, queryResult) {
+  	var i, len, node, queryAllResult, numNodes, j;
+
+  	len = this.nodes.length;
+  	for (i = 0; i < len; i += 1) {
+  		node = this.nodes[i];
+
+  		if (node.nodeType !== 1) {
+  			continue;
+  		}
+
+  		if (matches(node, selector)) {
+  			queryResult.push(node);
+  		}
+
+  		if (queryAllResult = node.querySelectorAll(selector)) {
+  			numNodes = queryAllResult.length;
+  			for (j = 0; j < numNodes; j += 1) {
+  				queryResult.push(queryAllResult[j]);
+  			}
+  		}
+  	}
+  }
+
+  var Triple_prototype_firstNode = Triple$firstNode;
+
+  function Triple$firstNode() {
+  	if (this.rendered && this.nodes[0]) {
+  		return this.nodes[0];
+  	}
+
+  	return this.parentFragment.findNextNode(this);
+  }
+
+  var elementCache = {},
+      ieBug,
+      ieBlacklist;
+
+  try {
+  	createElement("table").innerHTML = "foo";
+  } catch (err) {
+  	ieBug = true;
+
+  	ieBlacklist = {
+  		TABLE: ["<table class=\"x\">", "</table>"],
+  		THEAD: ["<table><thead class=\"x\">", "</thead></table>"],
+  		TBODY: ["<table><tbody class=\"x\">", "</tbody></table>"],
+  		TR: ["<table><tr class=\"x\">", "</tr></table>"],
+  		SELECT: ["<select class=\"x\">", "</select>"]
+  	};
+  }
+
+  var insertHtml = function (html, node, docFrag) {
+  	var container,
+  	    nodes = [],
+  	    wrapper,
+  	    selectedOption,
+  	    child,
+  	    i;
+
+  	// render 0 and false
+  	if (html != null && html !== "") {
+  		if (ieBug && (wrapper = ieBlacklist[node.tagName])) {
+  			container = element("DIV");
+  			container.innerHTML = wrapper[0] + html + wrapper[1];
+  			container = container.querySelector(".x");
+
+  			if (container.tagName === "SELECT") {
+  				selectedOption = container.options[container.selectedIndex];
+  			}
+  		} else if (node.namespaceURI === namespaces.svg) {
+  			container = element("DIV");
+  			container.innerHTML = "<svg class=\"x\">" + html + "</svg>";
+  			container = container.querySelector(".x");
+  		} else {
+  			container = element(node.tagName);
+  			container.innerHTML = html;
+
+  			if (container.tagName === "SELECT") {
+  				selectedOption = container.options[container.selectedIndex];
+  			}
+  		}
+
+  		while (child = container.firstChild) {
+  			nodes.push(child);
+  			docFrag.appendChild(child);
+  		}
+
+  		// This is really annoying. Extracting <option> nodes from the
+  		// temporary container <select> causes the remaining ones to
+  		// become selected. So now we have to deselect them. IE8, you
+  		// amaze me. You really do
+  		// ...and now Chrome too
+  		if (node.tagName === "SELECT") {
+  			i = nodes.length;
+  			while (i--) {
+  				if (nodes[i] !== selectedOption) {
+  					nodes[i].selected = false;
+  				}
+  			}
+  		}
+  	}
+
+  	return nodes;
+  };
+
+  function element(tagName) {
+  	return elementCache[tagName] || (elementCache[tagName] = createElement(tagName));
+  }
+
+  var helpers_updateSelect = updateSelect;
+
+  function updateSelect(parentElement) {
+  	var selectedOptions, option, value;
+
+  	if (!parentElement || parentElement.name !== "select" || !parentElement.binding) {
+  		return;
+  	}
+
+  	selectedOptions = toArray(parentElement.node.options).filter(isSelected);
+
+  	// If one of them had a `selected` attribute, we need to sync
+  	// the model to the view
+  	if (parentElement.getAttribute("multiple")) {
+  		value = selectedOptions.map(function (o) {
+  			return o.value;
+  		});
+  	} else if (option = selectedOptions[0]) {
+  		value = option.value;
+  	}
+
+  	if (value !== undefined) {
+  		parentElement.binding.setValue(value);
+  	}
+
+  	parentElement.bubble();
+  }
+
+  function isSelected(option) {
+  	return option.selected;
+  }
+
+  var Triple_prototype_render = Triple$render;
+  function Triple$render() {
+  	if (this.rendered) {
+  		throw new Error("Attempted to render an item that was already rendered");
+  	}
+
+  	this.docFrag = document.createDocumentFragment();
+  	this.nodes = insertHtml(this.value, this.parentFragment.getNode(), this.docFrag);
+
+  	// Special case - we're inserting the contents of a <select>
+  	helpers_updateSelect(this.pElement);
+
+  	this.rendered = true;
+  	return this.docFrag;
+  }
+
+  var prototype_setValue = Triple$setValue;
+  function Triple$setValue(value) {
+  	var wrapper;
+
+  	// TODO is there a better way to approach this?
+  	if (wrapper = this.root.viewmodel.wrapped[this.keypath.str]) {
+  		value = wrapper.get();
+  	}
+
+  	if (value !== this.value) {
+  		this.value = value;
+  		this.parentFragment.bubble();
+
+  		if (this.rendered) {
+  			global_runloop.addView(this);
+  		}
+  	}
+  }
+
+  var Triple_prototype_toString = Triple$toString;
+  function Triple$toString() {
+  	return this.value != undefined ? decodeCharacterReferences("" + this.value) : "";
+  }
+
+  var Triple_prototype_unrender = Triple$unrender;
+  function Triple$unrender(shouldDestroy) {
+  	if (this.rendered && shouldDestroy) {
+  		this.nodes.forEach(detachNode);
+  		this.rendered = false;
+  	}
+
+  	// TODO update live queries
+  }
+
+  var Triple_prototype_update = Triple$update;
+  function Triple$update() {
+  	var node, parentNode;
+
+  	if (!this.rendered) {
+  		return;
+  	}
+
+  	// Remove existing nodes
+  	while (this.nodes && this.nodes.length) {
+  		node = this.nodes.pop();
+  		node.parentNode.removeChild(node);
+  	}
+
+  	// Insert new nodes
+  	parentNode = this.parentFragment.getNode();
+
+  	this.nodes = insertHtml(this.value, parentNode, this.docFrag);
+  	parentNode.insertBefore(this.docFrag, this.parentFragment.findNextNode(this));
+
+  	// Special case - we're inserting the contents of a <select>
+  	helpers_updateSelect(this.pElement);
+  }
+
+  var Triple = function (options) {
+  	this.type = TRIPLE;
+  	Mustache.init(this, options);
+  };
+
+  Triple.prototype = {
+  	detach: Triple_prototype_detach,
+  	find: Triple_prototype_find,
+  	findAll: Triple_prototype_findAll,
+  	firstNode: Triple_prototype_firstNode,
+  	getValue: Mustache.getValue,
+  	rebind: Mustache.rebind,
+  	render: Triple_prototype_render,
+  	resolve: Mustache.resolve,
+  	setValue: prototype_setValue,
+  	toString: Triple_prototype_toString,
+  	unbind: shared_unbind,
+  	unrender: Triple_prototype_unrender,
+  	update: Triple_prototype_update
+  };
+
+  var _Triple = Triple;
+
+  var Element_prototype_bubble = function () {
+  	this.parentFragment.bubble();
+  };
+
+  var Element_prototype_detach = Element$detach;
+
+  function Element$detach() {
+  	var node = this.node,
+  	    parentNode;
+
+  	if (node) {
+  		// need to check for parent node - DOM may have been altered
+  		// by something other than Ractive! e.g. jQuery UI...
+  		if (parentNode = node.parentNode) {
+  			parentNode.removeChild(node);
+  		}
+
+  		return node;
+  	}
+  }
+
+  var Element_prototype_find = function (selector) {
+  	if (!this.node) {
+  		// this element hasn't been rendered yet
+  		return null;
+  	}
+
+  	if (matches(this.node, selector)) {
+  		return this.node;
+  	}
+
+  	if (this.fragment && this.fragment.find) {
+  		return this.fragment.find(selector);
+  	}
+  };
+
+  var Element_prototype_findAll = function (selector, query) {
+  	// Add this node to the query, if applicable, and register the
+  	// query on this element
+  	if (query._test(this, true) && query.live) {
+  		(this.liveQueries || (this.liveQueries = [])).push(query);
+  	}
+
+  	if (this.fragment) {
+  		this.fragment.findAll(selector, query);
+  	}
+  };
+
+  var Element_prototype_findAllComponents = function (selector, query) {
+  	if (this.fragment) {
+  		this.fragment.findAllComponents(selector, query);
+  	}
+  };
+
+  var Element_prototype_findComponent = function (selector) {
+  	if (this.fragment) {
+  		return this.fragment.findComponent(selector);
+  	}
+  };
+
+  var Element_prototype_findNextNode = Element$findNextNode;
+
+  function Element$findNextNode() {
+  	return null;
+  }
+
+  var Element_prototype_firstNode = Element$firstNode;
+
+  function Element$firstNode() {
+  	return this.node;
+  }
+
+  var getAttribute = Element$getAttribute;
+
+  function Element$getAttribute(name) {
+  	if (!this.attributes || !this.attributes[name]) {
+  		return;
+  	}
+
+  	return this.attributes[name].value;
+  }
+
+  var truthy = /^true|on|yes|1$/i;
+  var processBindingAttributes__isNumeric = /^[0-9]+$/;
+
+  var processBindingAttributes = function (element, template) {
+  	var val, attrs, attributes;
+
+  	attributes = template.a || {};
+  	attrs = {};
+
+  	// attributes that are present but don't have a value (=)
+  	// will be set to the number 0, which we condider to be true
+  	// the string '0', however is false
+
+  	val = attributes.twoway;
+  	if (val !== undefined) {
+  		attrs.twoway = val === 0 || truthy.test(val);
+  	}
+
+  	val = attributes.lazy;
+  	if (val !== undefined) {
+  		// check for timeout value
+  		if (val !== 0 && processBindingAttributes__isNumeric.test(val)) {
+  			attrs.lazy = parseInt(val);
+  		} else {
+  			attrs.lazy = val === 0 || truthy.test(val);
+  		}
+  	}
+
+  	return attrs;
+  };
+
+  var Attribute_prototype_bubble = Attribute$bubble;
+  function Attribute$bubble() {
+  	var value = this.useProperty || !this.rendered ? this.fragment.getValue() : this.fragment.toString();
+
+  	// TODO this can register the attribute multiple times (see render test
+  	// 'Attribute with nested mustaches')
+  	if (!isEqual(value, this.value)) {
+
+  		// Need to clear old id from ractive.nodes
+  		if (this.name === "id" && this.value) {
+  			delete this.root.nodes[this.value];
+  		}
+
+  		this.value = value;
+
+  		if (this.name === "value" && this.node) {
+  			// We need to store the value on the DOM like this so we
+  			// can retrieve it later without it being coerced to a string
+  			this.node._ractive.value = value;
+  		}
+
+  		if (this.rendered) {
+  			global_runloop.addView(this);
+  		}
+  	}
+  }
+
+  var svgCamelCaseElements, svgCamelCaseAttributes, createMap, map;
+  svgCamelCaseElements = "altGlyph altGlyphDef altGlyphItem animateColor animateMotion animateTransform clipPath feBlend feColorMatrix feComponentTransfer feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset fePointLight feSpecularLighting feSpotLight feTile feTurbulence foreignObject glyphRef linearGradient radialGradient textPath vkern".split(" ");
+  svgCamelCaseAttributes = "attributeName attributeType baseFrequency baseProfile calcMode clipPathUnits contentScriptType contentStyleType diffuseConstant edgeMode externalResourcesRequired filterRes filterUnits glyphRef gradientTransform gradientUnits kernelMatrix kernelUnitLength keyPoints keySplines keyTimes lengthAdjust limitingConeAngle markerHeight markerUnits markerWidth maskContentUnits maskUnits numOctaves pathLength patternContentUnits patternTransform patternUnits pointsAtX pointsAtY pointsAtZ preserveAlpha preserveAspectRatio primitiveUnits refX refY repeatCount repeatDur requiredExtensions requiredFeatures specularConstant specularExponent spreadMethod startOffset stdDeviation stitchTiles surfaceScale systemLanguage tableValues targetX targetY textLength viewBox viewTarget xChannelSelector yChannelSelector zoomAndPan".split(" ");
+
+  createMap = function (items) {
+  	var map = {},
+  	    i = items.length;
+  	while (i--) {
+  		map[items[i].toLowerCase()] = items[i];
+  	}
+  	return map;
+  };
+
+  map = createMap(svgCamelCaseElements.concat(svgCamelCaseAttributes));
+
+  var enforceCase = function (elementName) {
+  	var lowerCaseElementName = elementName.toLowerCase();
+  	return map[lowerCaseElementName] || lowerCaseElementName;
+  };
+
+  var determineNameAndNamespace = function (attribute, name) {
+  	var colonIndex, namespacePrefix;
+
+  	// are we dealing with a namespaced attribute, e.g. xlink:href?
+  	colonIndex = name.indexOf(":");
+  	if (colonIndex !== -1) {
+
+  		// looks like we are, yes...
+  		namespacePrefix = name.substr(0, colonIndex);
+
+  		// ...unless it's a namespace *declaration*, which we ignore (on the assumption
+  		// that only valid namespaces will be used)
+  		if (namespacePrefix !== "xmlns") {
+  			name = name.substring(colonIndex + 1);
+
+  			attribute.name = enforceCase(name);
+  			attribute.namespace = namespaces[namespacePrefix.toLowerCase()];
+  			attribute.namespacePrefix = namespacePrefix;
+
+  			if (!attribute.namespace) {
+  				throw "Unknown namespace (\"" + namespacePrefix + "\")";
+  			}
+
+  			return;
+  		}
+  	}
+
+  	// SVG attribute names are case sensitive
+  	attribute.name = attribute.element.namespace !== namespaces.html ? enforceCase(name) : name;
+  };
+
+  var helpers_getInterpolator = getInterpolator;
+  function getInterpolator(attribute) {
+  	var items = attribute.fragment.items;
+
+  	if (items.length !== 1) {
+  		return;
+  	}
+
+  	if (items[0].type === INTERPOLATOR) {
+  		return items[0];
+  	}
+  }
+
+  var prototype_init = Attribute$init;
+  function Attribute$init(options) {
+  	this.type = ATTRIBUTE;
+  	this.element = options.element;
+  	this.root = options.root;
+
+  	determineNameAndNamespace(this, options.name);
+  	this.isBoolean = booleanAttributes.test(this.name);
+
+  	// if it's an empty attribute, or just a straight key-value pair, with no
+  	// mustache shenanigans, set the attribute accordingly and go home
+  	if (!options.value || typeof options.value === "string") {
+  		this.value = this.isBoolean ? true : options.value || "";
+  		return;
+  	}
+
+  	// otherwise we need to do some work
+
+  	// share parentFragment with parent element
+  	this.parentFragment = this.element.parentFragment;
+
+  	this.fragment = new virtualdom_Fragment({
+  		template: options.value,
+  		root: this.root,
+  		owner: this
+  	});
+
+  	// TODO can we use this.fragment.toString() in some cases? It's quicker
+  	this.value = this.fragment.getValue();
+
+  	// Store a reference to this attribute's interpolator, if its fragment
+  	// takes the form `{{foo}}`. This is necessary for two-way binding and
+  	// for correctly rendering HTML later
+  	this.interpolator = helpers_getInterpolator(this);
+  	this.isBindable = !!this.interpolator && !this.interpolator.isStatic;
+
+  	// mark as ready
+  	this.ready = true;
+  }
+
+  var Attribute_prototype_rebind = Attribute$rebind;
+
+  function Attribute$rebind(oldKeypath, newKeypath) {
+  	if (this.fragment) {
+  		this.fragment.rebind(oldKeypath, newKeypath);
+  	}
+  }
+
+  var Attribute_prototype_render = Attribute$render;
+  var propertyNames = {
+  	"accept-charset": "acceptCharset",
+  	accesskey: "accessKey",
+  	bgcolor: "bgColor",
+  	"class": "className",
+  	codebase: "codeBase",
+  	colspan: "colSpan",
+  	contenteditable: "contentEditable",
+  	datetime: "dateTime",
+  	dirname: "dirName",
+  	"for": "htmlFor",
+  	"http-equiv": "httpEquiv",
+  	ismap: "isMap",
+  	maxlength: "maxLength",
+  	novalidate: "noValidate",
+  	pubdate: "pubDate",
+  	readonly: "readOnly",
+  	rowspan: "rowSpan",
+  	tabindex: "tabIndex",
+  	usemap: "useMap"
+  };
+  function Attribute$render(node) {
+  	var propertyName;
+
+  	this.node = node;
+
+  	// should we use direct property access, or setAttribute?
+  	if (!node.namespaceURI || node.namespaceURI === namespaces.html) {
+  		propertyName = propertyNames[this.name] || this.name;
+
+  		if (node[propertyName] !== undefined) {
+  			this.propertyName = propertyName;
+  		}
+
+  		// is attribute a boolean attribute or 'value'? If so we're better off doing e.g.
+  		// node.selected = true rather than node.setAttribute( 'selected', '' )
+  		if (this.isBoolean || this.isTwoway) {
+  			this.useProperty = true;
+  		}
+
+  		if (propertyName === "value") {
+  			node._ractive.value = this.value;
+  		}
+  	}
+
+  	this.rendered = true;
+  	this.update();
+  }
+
+  var Attribute_prototype_toString = Attribute$toString;
+
+  function Attribute$toString() {
+  	var _ref = this;
+
+  	var name = _ref.name;
+  	var namespacePrefix = _ref.namespacePrefix;
+  	var value = _ref.value;
+  	var interpolator = _ref.interpolator;
+  	var fragment = _ref.fragment;
+
+  	// Special case - select and textarea values (should not be stringified)
+  	if (name === "value" && (this.element.name === "select" || this.element.name === "textarea")) {
+  		return;
+  	}
+
+  	// Special case - content editable
+  	if (name === "value" && this.element.getAttribute("contenteditable") !== undefined) {
+  		return;
+  	}
+
+  	// Special case - radio names
+  	if (name === "name" && this.element.name === "input" && interpolator) {
+  		return "name={{" + (interpolator.keypath.str || interpolator.ref) + "}}";
+  	}
+
+  	// Boolean attributes
+  	if (this.isBoolean) {
+  		return value ? name : "";
+  	}
+
+  	if (fragment) {
+  		// special case - this catches undefined/null values (#1211)
+  		if (fragment.items.length === 1 && fragment.items[0].value == null) {
+  			return "";
+  		}
+
+  		value = fragment.toString();
+  	}
+
+  	if (namespacePrefix) {
+  		name = namespacePrefix + ":" + name;
+  	}
+
+  	return value ? name + "=\"" + Attribute_prototype_toString__escape(value) + "\"" : name;
+  }
+
+  function Attribute_prototype_toString__escape(value) {
+  	return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
+  }
+
+  var Attribute_prototype_unbind = Attribute$unbind;
+
+  function Attribute$unbind() {
+  	// ignore non-dynamic attributes
+  	if (this.fragment) {
+  		this.fragment.unbind();
+  	}
+
+  	if (this.name === "id") {
+  		delete this.root.nodes[this.value];
+  	}
+  }
+
+  var updateSelectValue = Attribute$updateSelect;
+
+  function Attribute$updateSelect() {
+  	var value = this.value,
+  	    options,
+  	    option,
+  	    optionValue,
+  	    i;
+
+  	if (!this.locked) {
+  		this.node._ractive.value = value;
+
+  		options = this.node.options;
+  		i = options.length;
+
+  		while (i--) {
+  			option = options[i];
+  			optionValue = option._ractive ? option._ractive.value : option.value; // options inserted via a triple don't have _ractive
+
+  			if (optionValue == value) {
+  				// double equals as we may be comparing numbers with strings
+  				option.selected = true;
+  				break;
+  			}
+  		}
+  	}
+
+  	// if we're still here, it means the new value didn't match any of the options...
+  	// TODO figure out what to do in this situation
+  }
+
+  var updateMultipleSelectValue = Attribute$updateMultipleSelect;
+  function Attribute$updateMultipleSelect() {
+  	var value = this.value,
+  	    options,
+  	    i,
+  	    option,
+  	    optionValue;
+
+  	if (!isArray(value)) {
+  		value = [value];
+  	}
+
+  	options = this.node.options;
+  	i = options.length;
+
+  	while (i--) {
+  		option = options[i];
+  		optionValue = option._ractive ? option._ractive.value : option.value; // options inserted via a triple don't have _ractive
+  		option.selected = arrayContains(value, optionValue);
+  	}
+  }
+
+  var updateRadioName = Attribute$updateRadioName;
+
+  function Attribute$updateRadioName() {
+  	var _ref = this;
+
+  	var node = _ref.node;
+  	var value = _ref.value;
+
+  	node.checked = value == node._ractive.value;
+  }
+
+  var updateRadioValue = Attribute$updateRadioValue;
+  function Attribute$updateRadioValue() {
+  	var wasChecked,
+  	    node = this.node,
+  	    binding,
+  	    bindings,
+  	    i;
+
+  	wasChecked = node.checked;
+
+  	node.value = this.element.getAttribute("value");
+  	node.checked = this.element.getAttribute("value") === this.element.getAttribute("name");
+
+  	// This is a special case - if the input was checked, and the value
+  	// changed so that it's no longer checked, the twoway binding is
+  	// most likely out of date. To fix it we have to jump through some
+  	// hoops... this is a little kludgy but it works
+  	if (wasChecked && !node.checked && this.element.binding) {
+  		bindings = this.element.binding.siblings;
+
+  		if (i = bindings.length) {
+  			while (i--) {
+  				binding = bindings[i];
+
+  				if (!binding.element.node) {
+  					// this is the initial render, siblings are still rendering!
+  					// we'll come back later...
+  					return;
+  				}
+
+  				if (binding.element.node.checked) {
+  					global_runloop.addRactive(binding.root);
+  					return binding.handleChange();
+  				}
+  			}
+
+  			this.root.viewmodel.set(binding.keypath, undefined);
+  		}
+  	}
+  }
+
+  var updateCheckboxName = Attribute$updateCheckboxName;
+  function Attribute$updateCheckboxName() {
+  	var _ref = this;
+
+  	var element = _ref.element;
+  	var node = _ref.node;
+  	var value = _ref.value;var binding = element.binding;var valueAttribute;var i;
+
+  	valueAttribute = element.getAttribute("value");
+
+  	if (!isArray(value)) {
+  		binding.isChecked = node.checked = value == valueAttribute;
+  	} else {
+  		i = value.length;
+  		while (i--) {
+  			if (valueAttribute == value[i]) {
+  				binding.isChecked = node.checked = true;
+  				return;
+  			}
+  		}
+  		binding.isChecked = node.checked = false;
+  	}
+  }
+
+  var updateClassName = Attribute$updateClassName;
+  function Attribute$updateClassName() {
+  	this.node.className = safeToStringValue(this.value);
+  }
+
+  var updateIdAttribute = Attribute$updateIdAttribute;
+
+  function Attribute$updateIdAttribute() {
+  	var _ref = this;
+
+  	var node = _ref.node;
+  	var value = _ref.value;
+
+  	this.root.nodes[value] = node;
+  	node.id = value;
+  }
+
+  var updateIEStyleAttribute = Attribute$updateIEStyleAttribute;
+
+  function Attribute$updateIEStyleAttribute() {
+  	var node, value;
+
+  	node = this.node;
+  	value = this.value;
+
+  	if (value === undefined) {
+  		value = "";
+  	}
+
+  	node.style.setAttribute("cssText", value);
+  }
+
+  var updateContentEditableValue = Attribute$updateContentEditableValue;
+
+  function Attribute$updateContentEditableValue() {
+  	var value = this.value;
+
+  	if (value === undefined) {
+  		value = "";
+  	}
+
+  	if (!this.locked) {
+  		this.node.innerHTML = value;
+  	}
+  }
+
+  var updateValue = Attribute$updateValue;
+
+  function Attribute$updateValue() {
+  	var _ref = this;
+
+  	var node = _ref.node;
+  	var value = _ref.value;
+
+  	// store actual value, so it doesn't get coerced to a string
+  	node._ractive.value = value;
+
+  	// with two-way binding, only update if the change wasn't initiated by the user
+  	// otherwise the cursor will often be sent to the wrong place
+  	if (!this.locked) {
+  		node.value = value == undefined ? "" : value;
+  	}
+  }
+
+  var updateBoolean = Attribute$updateBooleanAttribute;
+
+  function Attribute$updateBooleanAttribute() {
+  	// with two-way binding, only update if the change wasn't initiated by the user
+  	// otherwise the cursor will often be sent to the wrong place
+  	if (!this.locked) {
+  		this.node[this.propertyName] = this.value;
+  	}
+  }
+
+  var updateEverythingElse = Attribute$updateEverythingElse;
+
+  function Attribute$updateEverythingElse() {
+  	var _ref = this;
+
+  	var node = _ref.node;
+  	var namespace = _ref.namespace;
+  	var name = _ref.name;
+  	var value = _ref.value;
+  	var fragment = _ref.fragment;
+
+  	if (namespace) {
+  		node.setAttributeNS(namespace, name, (fragment || value).toString());
+  	} else if (!this.isBoolean) {
+  		if (value == null) {
+  			node.removeAttribute(name);
+  		} else {
+  			node.setAttribute(name, (fragment || value).toString());
+  		}
+  	}
+
+  	// Boolean attributes - truthy becomes '', falsy means 'remove attribute'
+  	else {
+  		if (value) {
+  			node.setAttribute(name, "");
+  		} else {
+  			node.removeAttribute(name);
+  		}
+  	}
+  }
+
+  // There are a few special cases when it comes to updating attributes. For this reason,
+  // the prototype .update() method points to this method, which waits until the
+  // attribute has finished initialising, then replaces the prototype method with a more
+  // suitable one. That way, we save ourselves doing a bunch of tests on each call
+  var Attribute_prototype_update = Attribute$update;
+  function Attribute$update() {
+  	var _ref = this;
+
+  	var name = _ref.name;
+  	var element = _ref.element;
+  	var node = _ref.node;var type;var updateMethod;
+
+  	if (name === "id") {
+  		updateMethod = updateIdAttribute;
+  	} else if (name === "value") {
+  		// special case - selects
+  		if (element.name === "select" && name === "value") {
+  			updateMethod = element.getAttribute("multiple") ? updateMultipleSelectValue : updateSelectValue;
+  		} else if (element.name === "textarea") {
+  			updateMethod = updateValue;
+  		}
+
+  		// special case - contenteditable
+  		else if (element.getAttribute("contenteditable") != null) {
+  			updateMethod = updateContentEditableValue;
+  		}
+
+  		// special case - <input>
+  		else if (element.name === "input") {
+  			type = element.getAttribute("type");
+
+  			// type='file' value='{{fileList}}'>
+  			if (type === "file") {
+  				updateMethod = noop; // read-only
+  			}
+
+  			// type='radio' name='{{twoway}}'
+  			else if (type === "radio" && element.binding && element.binding.name === "name") {
+  				updateMethod = updateRadioValue;
+  			} else {
+  				updateMethod = updateValue;
+  			}
+  		}
+  	}
+
+  	// special case - <input type='radio' name='{{twoway}}' value='foo'>
+  	else if (this.isTwoway && name === "name") {
+  		if (node.type === "radio") {
+  			updateMethod = updateRadioName;
+  		} else if (node.type === "checkbox") {
+  			updateMethod = updateCheckboxName;
+  		}
+  	}
+
+  	// special case - style attributes in Internet Exploder
+  	else if (name === "style" && node.style.setAttribute) {
+  		updateMethod = updateIEStyleAttribute;
+  	}
+
+  	// special case - class names. IE fucks things up, again
+  	else if (name === "class" && (!node.namespaceURI || node.namespaceURI === namespaces.html)) {
+  		updateMethod = updateClassName;
+  	} else if (this.useProperty) {
+  		updateMethod = updateBoolean;
+  	}
+
+  	if (!updateMethod) {
+  		updateMethod = updateEverythingElse;
+  	}
+
+  	this.update = updateMethod;
+  	this.update();
+  }
+
+  var Attribute = function (options) {
+  	this.init(options);
+  };
+
+  Attribute.prototype = {
+  	bubble: Attribute_prototype_bubble,
+  	init: prototype_init,
+  	rebind: Attribute_prototype_rebind,
+  	render: Attribute_prototype_render,
+  	toString: Attribute_prototype_toString,
+  	unbind: Attribute_prototype_unbind,
+  	update: Attribute_prototype_update
+  };
+
+  var _Attribute = Attribute;
+
+  var createAttributes = function (element, attributes) {
+  	var name,
+  	    attribute,
+  	    result = [];
+
+  	for (name in attributes) {
+  		// skip binding attributes
+  		if (name === "twoway" || name === "lazy") {
+  			continue;
+  		}
+
+  		if (attributes.hasOwnProperty(name)) {
+  			attribute = new _Attribute({
+  				element: element,
+  				name: name,
+  				value: attributes[name],
+  				root: element.root
+  			});
+
+  			result[name] = attribute;
+
+  			if (name !== "value") {
+  				result.push(attribute);
+  			}
+  		}
+  	}
+
+  	// value attribute goes last. This is because it
+  	// may get clamped on render otherwise, e.g. in
+  	// `<input type='range' value='999' min='0' max='1000'>`
+  	// since default max is 100
+  	if (attribute = result.value) {
+  		result.push(attribute);
+  	}
+
+  	return result;
+  };
+
+  var _ConditionalAttribute__div;
+
+  if (typeof document !== "undefined") {
+  	_ConditionalAttribute__div = createElement("div");
+  }
+
+  var ConditionalAttribute = function (element, template) {
+  	this.element = element;
+  	this.root = element.root;
+  	this.parentFragment = element.parentFragment;
+
+  	this.attributes = [];
+
+  	this.fragment = new virtualdom_Fragment({
+  		root: element.root,
+  		owner: this,
+  		template: [template]
+  	});
+  };
+
+  ConditionalAttribute.prototype = {
+  	bubble: function () {
+  		if (this.node) {
+  			this.update();
+  		}
+
+  		this.element.bubble();
+  	},
+
+  	rebind: function (oldKeypath, newKeypath) {
+  		this.fragment.rebind(oldKeypath, newKeypath);
+  	},
+
+  	render: function (node) {
+  		this.node = node;
+  		this.isSvg = node.namespaceURI === namespaces.svg;
+
+  		this.update();
+  	},
+
+  	unbind: function () {
+  		this.fragment.unbind();
+  	},
+
+  	update: function () {
+  		var _this = this;
+
+  		var str, attrs;
+
+  		str = this.fragment.toString();
+  		attrs = parseAttributes(str, this.isSvg);
+
+  		// any attributes that previously existed but no longer do
+  		// must be removed
+  		this.attributes.filter(function (a) {
+  			return notIn(attrs, a);
+  		}).forEach(function (a) {
+  			_this.node.removeAttribute(a.name);
+  		});
+
+  		attrs.forEach(function (a) {
+  			_this.node.setAttribute(a.name, a.value);
+  		});
+
+  		this.attributes = attrs;
+  	},
+
+  	toString: function () {
+  		return this.fragment.toString();
+  	}
+  };
+
+  var _ConditionalAttribute = ConditionalAttribute;
+
+  function parseAttributes(str, isSvg) {
+  	var tag = isSvg ? "svg" : "div";
+  	_ConditionalAttribute__div.innerHTML = "<" + tag + " " + str + "></" + tag + ">";
+
+  	return toArray(_ConditionalAttribute__div.childNodes[0].attributes);
+  }
+
+  function notIn(haystack, needle) {
+  	var i = haystack.length;
+
+  	while (i--) {
+  		if (haystack[i].name === needle.name) {
+  			return false;
+  		}
+  	}
+
+  	return true;
+  }
+
+  var createConditionalAttributes = function (element, attributes) {
+  	if (!attributes) {
+  		return [];
+  	}
+
+  	return attributes.map(function (a) {
+  		return new _ConditionalAttribute(element, a);
+  	});
+  };
+
+  var Binding = function (element) {
+  	var interpolator, keypath, value, parentForm;
+
+  	this.element = element;
+  	this.root = element.root;
+  	this.attribute = element.attributes[this.name || "value"];
+
+  	interpolator = this.attribute.interpolator;
+  	interpolator.twowayBinding = this;
+
+  	if (keypath = interpolator.keypath) {
+  		if (keypath.str.slice(-1) === "}") {
+  			warnOnceIfDebug("Two-way binding does not work with expressions (`%s` on <%s>)", interpolator.resolver.uniqueString, element.name, { ractive: this.root });
+  			return false;
+  		}
+
+  		if (keypath.isSpecial) {
+  			warnOnceIfDebug("Two-way binding does not work with %s", interpolator.resolver.ref, { ractive: this.root });
+  			return false;
+  		}
+  	} else {
+  		// A mustache may be *ambiguous*. Let's say we were given
+  		// `value="{{bar}}"`. If the context was `foo`, and `foo.bar`
+  		// *wasn't* `undefined`, the keypath would be `foo.bar`.
+  		// Then, any user input would result in `foo.bar` being updated.
+  		//
+  		// If, however, `foo.bar` *was* undefined, and so was `bar`, we would be
+  		// left with an unresolved partial keypath - so we are forced to make an
+  		// assumption. That assumption is that the input in question should
+  		// be forced to resolve to `bar`, and any user input would affect `bar`
+  		// and not `foo.bar`.
+  		//
+  		// Did that make any sense? No? Oh. Sorry. Well the moral of the story is
+  		// be explicit when using two-way data-binding about what keypath you're
+  		// updating. Using it in lists is probably a recipe for confusion...
+  		var ref = interpolator.template.r ? "'" + interpolator.template.r + "' reference" : "expression";
+  		warnIfDebug("The %s being used for two-way binding is ambiguous, and may cause unexpected results. Consider initialising your data to eliminate the ambiguity", ref, { ractive: this.root });
+  		interpolator.resolver.forceResolution();
+  		keypath = interpolator.keypath;
+  	}
+
+  	this.attribute.isTwoway = true;
+  	this.keypath = keypath;
+
+  	// initialise value, if it's undefined
+  	value = this.root.viewmodel.get(keypath);
+
+  	if (value === undefined && this.getInitialValue) {
+  		value = this.getInitialValue();
+
+  		if (value !== undefined) {
+  			this.root.viewmodel.set(keypath, value);
+  		}
+  	}
+
+  	if (parentForm = findParentForm(element)) {
+  		this.resetValue = value;
+  		parentForm.formBindings.push(this);
+  	}
+  };
+
+  Binding.prototype = {
+  	handleChange: function () {
+  		var _this = this;
+
+  		global_runloop.start(this.root);
+  		this.attribute.locked = true;
+  		this.root.viewmodel.set(this.keypath, this.getValue());
+  		global_runloop.scheduleTask(function () {
+  			return _this.attribute.locked = false;
+  		});
+  		global_runloop.end();
+  	},
+
+  	rebound: function () {
+  		var bindings, oldKeypath, newKeypath;
+
+  		oldKeypath = this.keypath;
+  		newKeypath = this.attribute.interpolator.keypath;
+
+  		// The attribute this binding is linked to has already done the work
+  		if (oldKeypath === newKeypath) {
+  			return;
+  		}
+
+  		removeFromArray(this.root._twowayBindings[oldKeypath.str], this);
+
+  		this.keypath = newKeypath;
+
+  		bindings = this.root._twowayBindings[newKeypath.str] || (this.root._twowayBindings[newKeypath.str] = []);
+  		bindings.push(this);
+  	},
+
+  	unbind: function () {}
+  };
+
+  Binding.extend = function (properties) {
+  	var Parent = this,
+  	    SpecialisedBinding;
+
+  	SpecialisedBinding = function (element) {
+  		Binding.call(this, element);
+
+  		if (this.init) {
+  			this.init();
+  		}
+  	};
+
+  	SpecialisedBinding.prototype = create(Parent.prototype);
+  	utils_object__extend(SpecialisedBinding.prototype, properties);
+
+  	SpecialisedBinding.extend = Binding.extend;
+
+  	return SpecialisedBinding;
+  };
+
+  var Binding_Binding = Binding;
+
+  function findParentForm(element) {
+  	while (element = element.parent) {
+  		if (element.name === "form") {
+  			return element;
+  		}
+  	}
+  }
+
+  // this is called when the element is unbound.
+  // Specialised bindings can override it
+
+  // This is the handler for DOM events that would lead to a change in the model
+  // (i.e. change, sometimes, input, and occasionally click and keyup)
+  var handleDomEvent = handleChange;
+
+  function handleChange() {
+  	this._ractive.binding.handleChange();
+  }
+
+  var GenericBinding;
+
+  GenericBinding = Binding_Binding.extend({
+  	getInitialValue: function () {
+  		return "";
+  	},
+
+  	getValue: function () {
+  		return this.element.node.value;
+  	},
+
+  	render: function () {
+  		var node = this.element.node,
+  		    lazy,
+  		    timeout = false;
+  		this.rendered = true;
+
+  		// any lazy setting for this element overrides the root
+  		// if the value is a number, it's a timeout
+  		lazy = this.root.lazy;
+  		if (this.element.lazy === true) {
+  			lazy = true;
+  		} else if (this.element.lazy === false) {
+  			lazy = false;
+  		} else if (is__isNumeric(this.element.lazy)) {
+  			lazy = false;
+  			timeout = +this.element.lazy;
+  		} else if (is__isNumeric(lazy || "")) {
+  			timeout = +lazy;
+  			lazy = false;
+
+  			// make sure the timeout is available to the handler
+  			this.element.lazy = timeout;
+  		}
+
+  		this.handler = timeout ? handleDelay : handleDomEvent;
+
+  		node.addEventListener("change", handleDomEvent, false);
+
+  		if (!lazy) {
+  			node.addEventListener("input", this.handler, false);
+
+  			if (node.attachEvent) {
+  				node.addEventListener("keyup", this.handler, false);
+  			}
+  		}
+
+  		node.addEventListener("blur", handleBlur, false);
+  	},
+
+  	unrender: function () {
+  		var node = this.element.node;
+  		this.rendered = false;
+
+  		node.removeEventListener("change", handleDomEvent, false);
+  		node.removeEventListener("input", this.handler, false);
+  		node.removeEventListener("keyup", this.handler, false);
+  		node.removeEventListener("blur", handleBlur, false);
+  	}
+  });
+
+  var Binding_GenericBinding = GenericBinding;
+
+  function handleBlur() {
+  	var value;
+
+  	handleDomEvent.call(this);
+
+  	value = this._ractive.root.viewmodel.get(this._ractive.binding.keypath);
+  	this.value = value == undefined ? "" : value;
+  }
+
+  function handleDelay() {
+  	var binding = this._ractive.binding,
+  	    el = this;
+
+  	if (!!binding._timeout) clearTimeout(binding._timeout);
+
+  	binding._timeout = setTimeout(function () {
+  		if (binding.rendered) handleDomEvent.call(el);
+  		binding._timeout = undefined;
+  	}, binding.element.lazy);
+  }
+
+  var ContentEditableBinding = Binding_GenericBinding.extend({
+  	getInitialValue: function () {
+  		return this.element.fragment ? this.element.fragment.toString() : "";
+  	},
+
+  	getValue: function () {
+  		return this.element.node.innerHTML;
+  	}
+  });
+
+  var Binding_ContentEditableBinding = ContentEditableBinding;
+
+  var shared_getSiblings = getSiblings;
+  var sets = {};
+  function getSiblings(id, group, keypath) {
+  	var hash = id + group + keypath;
+  	return sets[hash] || (sets[hash] = []);
+  }
+
+  var RadioBinding = Binding_Binding.extend({
+  	name: "checked",
+
+  	init: function () {
+  		this.siblings = shared_getSiblings(this.root._guid, "radio", this.element.getAttribute("name"));
+  		this.siblings.push(this);
+  	},
+
+  	render: function () {
+  		var node = this.element.node;
+
+  		node.addEventListener("change", handleDomEvent, false);
+
+  		if (node.attachEvent) {
+  			node.addEventListener("click", handleDomEvent, false);
+  		}
+  	},
+
+  	unrender: function () {
+  		var node = this.element.node;
+
+  		node.removeEventListener("change", handleDomEvent, false);
+  		node.removeEventListener("click", handleDomEvent, false);
+  	},
+
+  	handleChange: function () {
+  		global_runloop.start(this.root);
+
+  		this.siblings.forEach(function (binding) {
+  			binding.root.viewmodel.set(binding.keypath, binding.getValue());
+  		});
+
+  		global_runloop.end();
+  	},
+
+  	getValue: function () {
+  		return this.element.node.checked;
+  	},
+
+  	unbind: function () {
+  		removeFromArray(this.siblings, this);
+  	}
+  });
+
+  var Binding_RadioBinding = RadioBinding;
+
+  var RadioNameBinding = Binding_Binding.extend({
+  	name: "name",
+
+  	init: function () {
+  		this.siblings = shared_getSiblings(this.root._guid, "radioname", this.keypath.str);
+  		this.siblings.push(this);
+
+  		this.radioName = true; // so that ractive.updateModel() knows what to do with this
+  	},
+
+  	getInitialValue: function () {
+  		if (this.element.getAttribute("checked")) {
+  			return this.element.getAttribute("value");
+  		}
+  	},
+
+  	render: function () {
+  		var node = this.element.node;
+
+  		node.name = "{{" + this.keypath.str + "}}";
+  		node.checked = this.root.viewmodel.get(this.keypath) == this.element.getAttribute("value");
+
+  		node.addEventListener("change", handleDomEvent, false);
+
+  		if (node.attachEvent) {
+  			node.addEventListener("click", handleDomEvent, false);
+  		}
+  	},
+
+  	unrender: function () {
+  		var node = this.element.node;
+
+  		node.removeEventListener("change", handleDomEvent, false);
+  		node.removeEventListener("click", handleDomEvent, false);
+  	},
+
+  	getValue: function () {
+  		var node = this.element.node;
+  		return node._ractive ? node._ractive.value : node.value;
+  	},
+
+  	handleChange: function () {
+  		// If this <input> is the one that's checked, then the value of its
+  		// `name` keypath gets set to its value
+  		if (this.element.node.checked) {
+  			Binding_Binding.prototype.handleChange.call(this);
+  		}
+  	},
+
+  	rebound: function (oldKeypath, newKeypath) {
+  		var node;
+
+  		Binding_Binding.prototype.rebound.call(this, oldKeypath, newKeypath);
+
+  		if (node = this.element.node) {
+  			node.name = "{{" + this.keypath.str + "}}";
+  		}
+  	},
+
+  	unbind: function () {
+  		removeFromArray(this.siblings, this);
+  	}
+  });
+
+  var Binding_RadioNameBinding = RadioNameBinding;
+
+  var CheckboxNameBinding = Binding_Binding.extend({
+  	name: "name",
+
+  	getInitialValue: function () {
+  		// This only gets called once per group (of inputs that
+  		// share a name), because it only gets called if there
+  		// isn't an initial value. By the same token, we can make
+  		// a note of that fact that there was no initial value,
+  		// and populate it using any `checked` attributes that
+  		// exist (which users should avoid, but which we should
+  		// support anyway to avoid breaking expectations)
+  		this.noInitialValue = true;
+  		return [];
+  	},
+
+  	init: function () {
+  		var existingValue, bindingValue;
+
+  		this.checkboxName = true; // so that ractive.updateModel() knows what to do with this
+
+  		// Each input has a reference to an array containing it and its
+  		// siblings, as two-way binding depends on being able to ascertain
+  		// the status of all inputs within the group
+  		this.siblings = shared_getSiblings(this.root._guid, "checkboxes", this.keypath.str);
+  		this.siblings.push(this);
+
+  		if (this.noInitialValue) {
+  			this.siblings.noInitialValue = true;
+  		}
+
+  		// If no initial value was set, and this input is checked, we
+  		// update the model
+  		if (this.siblings.noInitialValue && this.element.getAttribute("checked")) {
+  			existingValue = this.root.viewmodel.get(this.keypath);
+  			bindingValue = this.element.getAttribute("value");
+
+  			existingValue.push(bindingValue);
+  		}
+  	},
+
+  	unbind: function () {
+  		removeFromArray(this.siblings, this);
+  	},
+
+  	render: function () {
+  		var node = this.element.node,
+  		    existingValue,
+  		    bindingValue;
+
+  		existingValue = this.root.viewmodel.get(this.keypath);
+  		bindingValue = this.element.getAttribute("value");
+
+  		if (isArray(existingValue)) {
+  			this.isChecked = arrayContains(existingValue, bindingValue);
+  		} else {
+  			this.isChecked = existingValue == bindingValue;
+  		}
+
+  		node.name = "{{" + this.keypath.str + "}}";
+  		node.checked = this.isChecked;
+
+  		node.addEventListener("change", handleDomEvent, false);
+
+  		// in case of IE emergency, bind to click event as well
+  		if (node.attachEvent) {
+  			node.addEventListener("click", handleDomEvent, false);
+  		}
+  	},
+
+  	unrender: function () {
+  		var node = this.element.node;
+
+  		node.removeEventListener("change", handleDomEvent, false);
+  		node.removeEventListener("click", handleDomEvent, false);
+  	},
+
+  	changed: function () {
+  		var wasChecked = !!this.isChecked;
+  		this.isChecked = this.element.node.checked;
+  		return this.isChecked === wasChecked;
+  	},
+
+  	handleChange: function () {
+  		this.isChecked = this.element.node.checked;
+  		Binding_Binding.prototype.handleChange.call(this);
+  	},
+
+  	getValue: function () {
+  		return this.siblings.filter(isChecked).map(Binding_CheckboxNameBinding__getValue);
+  	}
+  });
+
+  function isChecked(binding) {
+  	return binding.isChecked;
+  }
+
+  function Binding_CheckboxNameBinding__getValue(binding) {
+  	return binding.element.getAttribute("value");
+  }
+
+  var Binding_CheckboxNameBinding = CheckboxNameBinding;
+
+  var CheckboxBinding = Binding_Binding.extend({
+  	name: "checked",
+
+  	render: function () {
+  		var node = this.element.node;
+
+  		node.addEventListener("change", handleDomEvent, false);
+
+  		if (node.attachEvent) {
+  			node.addEventListener("click", handleDomEvent, false);
+  		}
+  	},
+
+  	unrender: function () {
+  		var node = this.element.node;
+
+  		node.removeEventListener("change", handleDomEvent, false);
+  		node.removeEventListener("click", handleDomEvent, false);
+  	},
+
+  	getValue: function () {
+  		return this.element.node.checked;
+  	}
+  });
+
+  var Binding_CheckboxBinding = CheckboxBinding;
+
+  var SelectBinding = Binding_Binding.extend({
+  	getInitialValue: function () {
+  		var options = this.element.options,
+  		    len,
+  		    i,
+  		    value,
+  		    optionWasSelected;
+
+  		if (this.element.getAttribute("value") !== undefined) {
+  			return;
+  		}
+
+  		i = len = options.length;
+
+  		if (!len) {
+  			return;
+  		}
+
+  		// take the final selected option...
+  		while (i--) {
+  			if (options[i].getAttribute("selected")) {
+  				value = options[i].getAttribute("value");
+  				optionWasSelected = true;
+  				break;
+  			}
+  		}
+
+  		// or the first non-disabled option, if none are selected
+  		if (!optionWasSelected) {
+  			while (++i < len) {
+  				if (!options[i].getAttribute("disabled")) {
+  					value = options[i].getAttribute("value");
+  					break;
+  				}
+  			}
+  		}
+
+  		// This is an optimisation (aka hack) that allows us to forgo some
+  		// other more expensive work
+  		if (value !== undefined) {
+  			this.element.attributes.value.value = value;
+  		}
+
+  		return value;
+  	},
+
+  	render: function () {
+  		this.element.node.addEventListener("change", handleDomEvent, false);
+  	},
+
+  	unrender: function () {
+  		this.element.node.removeEventListener("change", handleDomEvent, false);
+  	},
+
+  	// TODO this method is an anomaly... is it necessary?
+  	setValue: function (value) {
+  		this.root.viewmodel.set(this.keypath, value);
+  	},
+
+  	getValue: function () {
+  		var options, i, len, option, optionValue;
+
+  		options = this.element.node.options;
+  		len = options.length;
+
+  		for (i = 0; i < len; i += 1) {
+  			option = options[i];
+
+  			if (options[i].selected) {
+  				optionValue = option._ractive ? option._ractive.value : option.value;
+  				return optionValue;
+  			}
+  		}
+  	},
+
+  	forceUpdate: function () {
+  		var _this = this;
+
+  		var value = this.getValue();
+
+  		if (value !== undefined) {
+  			this.attribute.locked = true;
+  			global_runloop.scheduleTask(function () {
+  				return _this.attribute.locked = false;
+  			});
+  			this.root.viewmodel.set(this.keypath, value);
+  		}
+  	}
+  });
+
+  var Binding_SelectBinding = SelectBinding;
+
+  var MultipleSelectBinding = Binding_SelectBinding.extend({
+  	getInitialValue: function () {
+  		return this.element.options.filter(function (option) {
+  			return option.getAttribute("selected");
+  		}).map(function (option) {
+  			return option.getAttribute("value");
+  		});
+  	},
+
+  	render: function () {
+  		var valueFromModel;
+
+  		this.element.node.addEventListener("change", handleDomEvent, false);
+
+  		valueFromModel = this.root.viewmodel.get(this.keypath);
+
+  		if (valueFromModel === undefined) {
+  			// get value from DOM, if possible
+  			this.handleChange();
+  		}
+  	},
+
+  	unrender: function () {
+  		this.element.node.removeEventListener("change", handleDomEvent, false);
+  	},
+
+  	setValue: function () {
+  		throw new Error("TODO not implemented yet");
+  	},
+
+  	getValue: function () {
+  		var selectedValues, options, i, len, option, optionValue;
+
+  		selectedValues = [];
+  		options = this.element.node.options;
+  		len = options.length;
+
+  		for (i = 0; i < len; i += 1) {
+  			option = options[i];
+
+  			if (option.selected) {
+  				optionValue = option._ractive ? option._ractive.value : option.value;
+  				selectedValues.push(optionValue);
+  			}
+  		}
+
+  		return selectedValues;
+  	},
+
+  	handleChange: function () {
+  		var attribute, previousValue, value;
+
+  		attribute = this.attribute;
+  		previousValue = attribute.value;
+
+  		value = this.getValue();
+
+  		if (previousValue === undefined || !arrayContentsMatch(value, previousValue)) {
+  			Binding_SelectBinding.prototype.handleChange.call(this);
+  		}
+
+  		return this;
+  	},
+
+  	forceUpdate: function () {
+  		var _this = this;
+
+  		var value = this.getValue();
+
+  		if (value !== undefined) {
+  			this.attribute.locked = true;
+  			global_runloop.scheduleTask(function () {
+  				return _this.attribute.locked = false;
+  			});
+  			this.root.viewmodel.set(this.keypath, value);
+  		}
+  	},
+
+  	updateModel: function () {
+  		if (this.attribute.value === undefined || !this.attribute.value.length) {
+  			this.root.viewmodel.set(this.keypath, this.initialValue);
+  		}
+  	}
+  });
+
+  var Binding_MultipleSelectBinding = MultipleSelectBinding;
+
+  var FileListBinding = Binding_Binding.extend({
+  	render: function () {
+  		this.element.node.addEventListener("change", handleDomEvent, false);
+  	},
+
+  	unrender: function () {
+  		this.element.node.removeEventListener("change", handleDomEvent, false);
+  	},
+
+  	getValue: function () {
+  		return this.element.node.files;
+  	}
+  });
+
+  var Binding_FileListBinding = FileListBinding;
+
+  var NumericBinding = Binding_GenericBinding.extend({
+  	getInitialValue: function () {
+  		return undefined;
+  	},
+
+  	getValue: function () {
+  		var value = parseFloat(this.element.node.value);
+  		return isNaN(value) ? undefined : value;
+  	}
+  });
+
+  var init_createTwowayBinding = createTwowayBinding;
+
+  function createTwowayBinding(element) {
+  	var attributes = element.attributes,
+  	    type,
+  	    Binding,
+  	    bindName,
+  	    bindChecked,
+  	    binding;
+
+  	// if this is a late binding, and there's already one, it
+  	// needs to be torn down
+  	if (element.binding) {
+  		element.binding.teardown();
+  		element.binding = null;
+  	}
+
+  	// contenteditable
+  	if (
+  	// if the contenteditable attribute is true or is bindable and may thus become true
+  	(element.getAttribute("contenteditable") || !!attributes.contenteditable && isBindable(attributes.contenteditable)) && isBindable(attributes.value)) {
+  		Binding = Binding_ContentEditableBinding;
+  	}
+
+  	// <input>
+  	else if (element.name === "input") {
+  		type = element.getAttribute("type");
+
+  		if (type === "radio" || type === "checkbox") {
+  			bindName = isBindable(attributes.name);
+  			bindChecked = isBindable(attributes.checked);
+
+  			// we can either bind the name attribute, or the checked attribute - not both
+  			if (bindName && bindChecked) {
+  				warnIfDebug("A radio input can have two-way binding on its name attribute, or its checked attribute - not both", { ractive: element.root });
+  			}
+
+  			if (bindName) {
+  				Binding = type === "radio" ? Binding_RadioNameBinding : Binding_CheckboxNameBinding;
+  			} else if (bindChecked) {
+  				Binding = type === "radio" ? Binding_RadioBinding : Binding_CheckboxBinding;
+  			}
+  		} else if (type === "file" && isBindable(attributes.value)) {
+  			Binding = Binding_FileListBinding;
+  		} else if (isBindable(attributes.value)) {
+  			Binding = type === "number" || type === "range" ? NumericBinding : Binding_GenericBinding;
+  		}
+  	}
+
+  	// <select>
+  	else if (element.name === "select" && isBindable(attributes.value)) {
+  		Binding = element.getAttribute("multiple") ? Binding_MultipleSelectBinding : Binding_SelectBinding;
+  	}
+
+  	// <textarea>
+  	else if (element.name === "textarea" && isBindable(attributes.value)) {
+  		Binding = Binding_GenericBinding;
+  	}
+
+  	if (Binding && (binding = new Binding(element)) && binding.keypath) {
+  		return binding;
+  	}
+  }
+
+  function isBindable(attribute) {
+  	return attribute && attribute.isBindable;
+  }
+
+  // and this element also has a value attribute to bind
+
+  var EventHandler_prototype_bubble = EventHandler$bubble;
+
+  function EventHandler$bubble() {
+  	var hasAction = this.getAction();
+
+  	if (hasAction && !this.hasListener) {
+  		this.listen();
+  	} else if (!hasAction && this.hasListener) {
+  		this.unrender();
+  	}
+  }
+
+  // This function may be overwritten, if the event directive
+  // includes parameters
+  var EventHandler_prototype_fire = EventHandler$fire;
+  function EventHandler$fire(event) {
+  	shared_fireEvent(this.root, this.getAction(), { event: event });
+  }
+
+  var getAction = EventHandler$getAction;
+
+  function EventHandler$getAction() {
+  	return this.action.toString().trim();
+  }
+
+  var EventHandler_prototype_init = EventHandler$init;
+
+  var eventPattern = /^event(?:\.(.+))?/;
+  function EventHandler$init(element, name, template) {
+  	var _this = this;
+
+  	var action, refs, ractive;
+
+  	this.element = element;
+  	this.root = element.root;
+  	this.parentFragment = element.parentFragment;
+  	this.name = name;
+
+  	if (name.indexOf("*") !== -1) {
+  		fatal("Only component proxy-events may contain \"*\" wildcards, <%s on-%s=\"...\"/> is not valid", element.name, name);
+  		this.invalid = true;
+  	}
+
+  	if (template.m) {
+  		refs = template.a.r;
+
+  		// This is a method call
+  		this.method = template.m;
+  		this.keypaths = [];
+  		this.fn = shared_getFunctionFromString(template.a.s, refs.length);
+
+  		this.parentFragment = element.parentFragment;
+  		ractive = this.root;
+
+  		// Create resolvers for each reference
+  		this.refResolvers = [];
+  		refs.forEach(function (ref, i) {
+  			var match = undefined;
+
+  			// special case - the `event` object
+  			if (match = eventPattern.exec(ref)) {
+  				_this.keypaths[i] = {
+  					eventObject: true,
+  					refinements: match[1] ? match[1].split(".") : []
+  				};
+  			} else {
+  				_this.refResolvers.push(Resolvers_createReferenceResolver(_this, ref, function (keypath) {
+  					return _this.resolve(i, keypath);
+  				}));
+  			}
+  		});
+
+  		this.fire = fireMethodCall;
+  	} else {
+  		// Get action ('foo' in 'on-click='foo')
+  		action = template.n || template;
+  		if (typeof action !== "string") {
+  			action = new virtualdom_Fragment({
+  				template: action,
+  				root: this.root,
+  				owner: this
+  			});
+  		}
+
+  		this.action = action;
+
+  		// Get parameters
+  		if (template.d) {
+  			this.dynamicParams = new virtualdom_Fragment({
+  				template: template.d,
+  				root: this.root,
+  				owner: this.element
+  			});
+
+  			this.fire = fireEventWithDynamicParams;
+  		} else if (template.a) {
+  			this.params = template.a;
+  			this.fire = fireEventWithParams;
+  		}
+  	}
+  }
+
+  function fireMethodCall(event) {
+  	var ractive, values, args;
+
+  	ractive = this.root;
+
+  	if (typeof ractive[this.method] !== "function") {
+  		throw new Error("Attempted to call a non-existent method (\"" + this.method + "\")");
+  	}
+
+  	values = this.keypaths.map(function (keypath) {
+  		var value, len, i;
+
+  		if (keypath === undefined) {
+  			// not yet resolved
+  			return undefined;
+  		}
+
+  		// TODO the refinements stuff would be better handled at parse time
+  		if (keypath.eventObject) {
+  			value = event;
+
+  			if (len = keypath.refinements.length) {
+  				for (i = 0; i < len; i += 1) {
+  					value = value[keypath.refinements[i]];
+  				}
+  			}
+  		} else {
+  			value = ractive.viewmodel.get(keypath);
+  		}
+
+  		return value;
+  	});
+
+  	shared_eventStack.enqueue(ractive, event);
+
+  	args = this.fn.apply(null, values);
+  	ractive[this.method].apply(ractive, args);
+
+  	shared_eventStack.dequeue(ractive);
+  }
+
+  function fireEventWithParams(event) {
+  	shared_fireEvent(this.root, this.getAction(), { event: event, args: this.params });
+  }
+
+  function fireEventWithDynamicParams(event) {
+  	var args = this.dynamicParams.getArgsList();
+
+  	// need to strip [] from ends if a string!
+  	if (typeof args === "string") {
+  		args = args.substr(1, args.length - 2);
+  	}
+
+  	shared_fireEvent(this.root, this.getAction(), { event: event, args: args });
+  }
+
+  var shared_genericHandler = genericHandler;
+  function genericHandler(event) {
+  	var storage,
+  	    handler,
+  	    indices,
+  	    index = {};
+
+  	storage = this._ractive;
+  	handler = storage.events[event.type];
+
+  	if (indices = Resolvers_findIndexRefs(handler.element.parentFragment)) {
+  		index = Resolvers_findIndexRefs.resolve(indices);
+  	}
+
+  	handler.fire({
+  		node: this,
+  		original: event,
+  		index: index,
+  		keypath: storage.keypath.str,
+  		context: storage.root.viewmodel.get(storage.keypath)
+  	});
+  }
+
+  var listen = EventHandler$listen;
+
+  var customHandlers = {},
+      touchEvents = {
+  	touchstart: true,
+  	touchmove: true,
+  	touchend: true,
+  	touchcancel: true,
+  	//not w3c, but supported in some browsers
+  	touchleave: true
+  };
+  function EventHandler$listen() {
+  	var definition,
+  	    name = this.name;
+
+  	if (this.invalid) {
+  		return;
+  	}
+
+  	if (definition = findInViewHierarchy("events", this.root, name)) {
+  		this.custom = definition(this.node, getCustomHandler(name));
+  	} else {
+  		// Looks like we're dealing with a standard DOM event... but let's check
+  		if (!("on" + name in this.node) && !(window && "on" + name in window) && !isJsdom) {
+
+  			// okay to use touch events if this browser doesn't support them
+  			if (!touchEvents[name]) {
+  				warnOnceIfDebug(missingPlugin(name, "event"), { node: this.node });
+  			}
+
+  			return;
+  		}
+
+  		this.node.addEventListener(name, shared_genericHandler, false);
+  	}
+
+  	this.hasListener = true;
+  }
+
+  function getCustomHandler(name) {
+  	if (!customHandlers[name]) {
+  		customHandlers[name] = function (event) {
+  			var storage = event.node._ractive;
+
+  			event.index = storage.index;
+  			event.keypath = storage.keypath.str;
+  			event.context = storage.root.viewmodel.get(storage.keypath);
+
+  			storage.events[name].fire(event);
+  		};
+  	}
+
+  	return customHandlers[name];
+  }
+
+  var EventHandler_prototype_rebind = EventHandler$rebind;
+
+  function EventHandler$rebind(oldKeypath, newKeypath) {
+  	var fragment;
+  	if (this.method) {
+  		fragment = this.element.parentFragment;
+  		this.refResolvers.forEach(rebind);
+
+  		return;
+  	}
+
+  	if (typeof this.action !== "string") {
+  		rebind(this.action);
+  	}
+
+  	if (this.dynamicParams) {
+  		rebind(this.dynamicParams);
+  	}
+
+  	function rebind(thing) {
+  		thing && thing.rebind(oldKeypath, newKeypath);
+  	}
+  }
+
+  var EventHandler_prototype_render = EventHandler$render;
+
+  function EventHandler$render() {
+  	this.node = this.element.node;
+  	// store this on the node itself, so it can be retrieved by a
+  	// universal handler
+  	this.node._ractive.events[this.name] = this;
+
+  	if (this.method || this.getAction()) {
+  		this.listen();
+  	}
+  }
+
+  var prototype_resolve = EventHandler$resolve;
+
+  function EventHandler$resolve(index, keypath) {
+  	this.keypaths[index] = keypath;
+  }
+
+  var EventHandler_prototype_unbind = EventHandler$unbind;
+  function EventHandler$unbind() {
+  	if (this.method) {
+  		this.refResolvers.forEach(methodCallers__unbind);
+  		return;
+  	}
+
+  	// Tear down dynamic name
+  	if (typeof this.action !== "string") {
+  		this.action.unbind();
+  	}
+
+  	// Tear down dynamic parameters
+  	if (this.dynamicParams) {
+  		this.dynamicParams.unbind();
+  	}
+  }
+
+  var EventHandler_prototype_unrender = EventHandler$unrender;
+  function EventHandler$unrender() {
+
+  	if (this.custom) {
+  		this.custom.teardown();
+  	} else {
+  		this.node.removeEventListener(this.name, shared_genericHandler, false);
+  	}
+
+  	this.hasListener = false;
+  }
+
+  var EventHandler = function (element, name, template) {
+  	this.init(element, name, template);
+  };
+
+  EventHandler.prototype = {
+  	bubble: EventHandler_prototype_bubble,
+  	fire: EventHandler_prototype_fire,
+  	getAction: getAction,
+  	init: EventHandler_prototype_init,
+  	listen: listen,
+  	rebind: EventHandler_prototype_rebind,
+  	render: EventHandler_prototype_render,
+  	resolve: prototype_resolve,
+  	unbind: EventHandler_prototype_unbind,
+  	unrender: EventHandler_prototype_unrender
+  };
+
+  var _EventHandler = EventHandler;
+
+  var createEventHandlers = function (element, template) {
+  	var i,
+  	    name,
+  	    names,
+  	    handler,
+  	    result = [];
+
+  	for (name in template) {
+  		if (template.hasOwnProperty(name)) {
+  			names = name.split("-");
+  			i = names.length;
+
+  			while (i--) {
+  				handler = new _EventHandler(element, names[i], template[name]);
+  				result.push(handler);
+  			}
+  		}
+  	}
+
+  	return result;
+  };
+
+  var Decorator = function (element, template) {
+  	var self = this,
+  	    ractive,
+  	    name,
+  	    fragment;
+
+  	this.element = element;
+  	this.root = ractive = element.root;
+
+  	name = template.n || template;
+
+  	if (typeof name !== "string") {
+  		fragment = new virtualdom_Fragment({
+  			template: name,
+  			root: ractive,
+  			owner: element
+  		});
+
+  		name = fragment.toString();
+  		fragment.unbind();
+
+  		if (name === "") {
+  			// empty string okay, just no decorator
+  			return;
+  		}
+  	}
+
+  	if (template.a) {
+  		this.params = template.a;
+  	} else if (template.d) {
+  		this.fragment = new virtualdom_Fragment({
+  			template: template.d,
+  			root: ractive,
+  			owner: element
+  		});
+
+  		this.params = this.fragment.getArgsList();
+
+  		this.fragment.bubble = function () {
+  			this.dirtyArgs = this.dirtyValue = true;
+  			self.params = this.getArgsList();
+
+  			if (self.ready) {
+  				self.update();
+  			}
+  		};
+  	}
+
+  	this.fn = findInViewHierarchy("decorators", ractive, name);
+
+  	if (!this.fn) {
+  		fatal(missingPlugin(name, "decorator"));
+  	}
+  };
+
+  Decorator.prototype = {
+  	init: function () {
+  		var node, result, args;
+
+  		node = this.element.node;
+
+  		if (this.params) {
+  			args = [node].concat(this.params);
+  			result = this.fn.apply(this.root, args);
+  		} else {
+  			result = this.fn.call(this.root, node);
+  		}
+
+  		if (!result || !result.teardown) {
+  			throw new Error("Decorator definition must return an object with a teardown method");
+  		}
+
+  		// TODO does this make sense?
+  		this.actual = result;
+  		this.ready = true;
+  	},
+
+  	update: function () {
+  		if (this.actual.update) {
+  			this.actual.update.apply(this.root, this.params);
+  		} else {
+  			this.actual.teardown(true);
+  			this.init();
+  		}
+  	},
+
+  	rebind: function (oldKeypath, newKeypath) {
+  		if (this.fragment) {
+  			this.fragment.rebind(oldKeypath, newKeypath);
+  		}
+  	},
+
+  	teardown: function (updating) {
+  		this.torndown = true;
+  		if (this.ready) {
+  			this.actual.teardown();
+  		}
+
+  		if (!updating && this.fragment) {
+  			this.fragment.unbind();
+  		}
+  	}
+  };
+
+  var _Decorator = Decorator;
+
+  function select__bubble() {
+  	var _this = this;
+
+  	if (!this.dirty) {
+  		this.dirty = true;
+
+  		global_runloop.scheduleTask(function () {
+  			sync(_this);
+  			_this.dirty = false;
+  		});
+  	}
+
+  	this.parentFragment.bubble(); // default behaviour
+  }
+
+  function sync(selectElement) {
+  	var selectNode, selectValue, isMultiple, options, optionWasSelected;
+
+  	selectNode = selectElement.node;
+
+  	if (!selectNode) {
+  		return;
+  	}
+
+  	options = toArray(selectNode.options);
+
+  	selectValue = selectElement.getAttribute("value");
+  	isMultiple = selectElement.getAttribute("multiple");
+
+  	// If the <select> has a specified value, that should override
+  	// these options
+  	if (selectValue !== undefined) {
+  		options.forEach(function (o) {
+  			var optionValue, shouldSelect;
+
+  			optionValue = o._ractive ? o._ractive.value : o.value;
+  			shouldSelect = isMultiple ? valueContains(selectValue, optionValue) : selectValue == optionValue;
+
+  			if (shouldSelect) {
+  				optionWasSelected = true;
+  			}
+
+  			o.selected = shouldSelect;
+  		});
+
+  		if (!optionWasSelected) {
+  			if (options[0]) {
+  				options[0].selected = true;
+  			}
+
+  			if (selectElement.binding) {
+  				selectElement.binding.forceUpdate();
+  			}
+  		}
+  	}
+
+  	// Otherwise the value should be initialised according to which
+  	// <option> element is selected, if twoway binding is in effect
+  	else if (selectElement.binding) {
+  		selectElement.binding.forceUpdate();
+  	}
+  }
+
+  function valueContains(selectValue, optionValue) {
+  	var i = selectValue.length;
+  	while (i--) {
+  		if (selectValue[i] == optionValue) {
+  			return true;
+  		}
+  	}
+  }
+
+  function special_option__init(option, template) {
+  	option.select = findParentSelect(option.parent);
+
+  	// we might be inside a <datalist> element
+  	if (!option.select) {
+  		return;
+  	}
+
+  	option.select.options.push(option);
+
+  	// If the value attribute is missing, use the element's content
+  	if (!template.a) {
+  		template.a = {};
+  	}
+
+  	// ...as long as it isn't disabled
+  	if (template.a.value === undefined && !template.a.hasOwnProperty("disabled")) {
+  		template.a.value = template.f;
+  	}
+
+  	// If there is a `selected` attribute, but the <select>
+  	// already has a value, delete it
+  	if ("selected" in template.a && option.select.getAttribute("value") !== undefined) {
+  		delete template.a.selected;
+  	}
+  }
+
+  function special_option__unbind(option) {
+  	if (option.select) {
+  		removeFromArray(option.select.options, option);
+  	}
+  }
+
+  function findParentSelect(element) {
+  	if (!element) {
+  		return;
+  	}
+
+  	do {
+  		if (element.name === "select") {
+  			return element;
+  		}
+  	} while (element = element.parent);
+  }
+
+  var Element_prototype_init = Element$init;
+  function Element$init(options) {
+  	var parentFragment, template, ractive, binding, bindings, twoway, bindingAttrs;
+
+  	this.type = ELEMENT;
+
+  	// stuff we'll need later
+  	parentFragment = this.parentFragment = options.parentFragment;
+  	template = this.template = options.template;
+
+  	this.parent = options.pElement || parentFragment.pElement;
+
+  	this.root = ractive = parentFragment.root;
+  	this.index = options.index;
+  	this.key = options.key;
+
+  	this.name = enforceCase(template.e);
+
+  	// Special case - <option> elements
+  	if (this.name === "option") {
+  		special_option__init(this, template);
+  	}
+
+  	// Special case - <select> elements
+  	if (this.name === "select") {
+  		this.options = [];
+  		this.bubble = select__bubble; // TODO this is a kludge
+  	}
+
+  	// Special case - <form> elements
+  	if (this.name === "form") {
+  		this.formBindings = [];
+  	}
+
+  	// handle binding attributes first (twoway, lazy)
+  	bindingAttrs = processBindingAttributes(this, template);
+
+  	// create attributes
+  	this.attributes = createAttributes(this, template.a);
+  	this.conditionalAttributes = createConditionalAttributes(this, template.m);
+
+  	// append children, if there are any
+  	if (template.f) {
+  		this.fragment = new virtualdom_Fragment({
+  			template: template.f,
+  			root: ractive,
+  			owner: this,
+  			pElement: this,
+  			cssIds: null
+  		});
+  	}
+
+  	// the element setting should override the ractive setting
+  	twoway = ractive.twoway;
+  	if (bindingAttrs.twoway === false) twoway = false;else if (bindingAttrs.twoway === true) twoway = true;
+
+  	this.twoway = twoway;
+  	this.lazy = bindingAttrs.lazy;
+
+  	// create twoway binding
+  	if (twoway && (binding = init_createTwowayBinding(this, template.a))) {
+  		this.binding = binding;
+
+  		// register this with the root, so that we can do ractive.updateModel()
+  		bindings = this.root._twowayBindings[binding.keypath.str] || (this.root._twowayBindings[binding.keypath.str] = []);
+  		bindings.push(binding);
+  	}
+
+  	// create event proxies
+  	if (template.v) {
+  		this.eventHandlers = createEventHandlers(this, template.v);
+  	}
+
+  	// create decorator
+  	if (template.o) {
+  		this.decorator = new _Decorator(this, template.o);
+  	}
+
+  	// create transitions
+  	this.intro = template.t0 || template.t1;
+  	this.outro = template.t0 || template.t2;
+  }
+
+  var Element_prototype_rebind = Element$rebind;
+  function Element$rebind(oldKeypath, newKeypath) {
+  	var i, storage, liveQueries, ractive;
+
+  	if (this.attributes) {
+  		this.attributes.forEach(rebind);
+  	}
+
+  	if (this.conditionalAttributes) {
+  		this.conditionalAttributes.forEach(rebind);
+  	}
+
+  	if (this.eventHandlers) {
+  		this.eventHandlers.forEach(rebind);
+  	}
+
+  	if (this.decorator) {
+  		rebind(this.decorator);
+  	}
+
+  	// rebind children
+  	if (this.fragment) {
+  		rebind(this.fragment);
+  	}
+
+  	// Update live queries, if necessary
+  	if (liveQueries = this.liveQueries) {
+  		ractive = this.root;
+
+  		i = liveQueries.length;
+  		while (i--) {
+  			liveQueries[i]._makeDirty();
+  		}
+  	}
+
+  	if (this.node && (storage = this.node._ractive)) {
+
+  		// adjust keypath if needed
+  		assignNewKeypath(storage, "keypath", oldKeypath, newKeypath);
+  	}
+
+  	function rebind(thing) {
+  		thing.rebind(oldKeypath, newKeypath);
+  	}
+  }
+
+  function special_img__render(img) {
+  	var loadHandler;
+
+  	// if this is an <img>, and we're in a crap browser, we may need to prevent it
+  	// from overriding width and height when it loads the src
+  	if (img.attributes.width || img.attributes.height) {
+  		img.node.addEventListener("load", loadHandler = function () {
+  			var width = img.getAttribute("width"),
+  			    height = img.getAttribute("height");
+
+  			if (width !== undefined) {
+  				img.node.setAttribute("width", width);
+  			}
+
+  			if (height !== undefined) {
+  				img.node.setAttribute("height", height);
+  			}
+
+  			img.node.removeEventListener("load", loadHandler, false);
+  		}, false);
+  	}
+  }
+
+  function form__render(element) {
+  	element.node.addEventListener("reset", handleReset, false);
+  }
+
+  function form__unrender(element) {
+  	element.node.removeEventListener("reset", handleReset, false);
+  }
+
+  function handleReset() {
+  	var element = this._ractive.proxy;
+
+  	global_runloop.start();
+  	element.formBindings.forEach(updateModel);
+  	global_runloop.end();
+  }
+
+  function updateModel(binding) {
+  	binding.root.viewmodel.set(binding.keypath, binding.resetValue);
+  }
+
+  var Transition_prototype_init = Transition$init;
+  function Transition$init(element, template, isIntro) {
+  	var ractive, name, fragment;
+
+  	this.element = element;
+  	this.root = ractive = element.root;
+  	this.isIntro = isIntro;
+
+  	name = template.n || template;
+
+  	if (typeof name !== "string") {
+  		fragment = new virtualdom_Fragment({
+  			template: name,
+  			root: ractive,
+  			owner: element
+  		});
+
+  		name = fragment.toString();
+  		fragment.unbind();
+
+  		if (name === "") {
+  			// empty string okay, just no transition
+  			return;
+  		}
+  	}
+
+  	this.name = name;
+
+  	if (template.a) {
+  		this.params = template.a;
+  	} else if (template.d) {
+  		// TODO is there a way to interpret dynamic arguments without all the
+  		// 'dependency thrashing'?
+  		fragment = new virtualdom_Fragment({
+  			template: template.d,
+  			root: ractive,
+  			owner: element
+  		});
+
+  		this.params = fragment.getArgsList();
+  		fragment.unbind();
+  	}
+
+  	this._fn = findInViewHierarchy("transitions", ractive, name);
+
+  	if (!this._fn) {
+  		warnOnceIfDebug(missingPlugin(name, "transition"), { ractive: this.root });
+  	}
+  }
+
+  var camelCase = function (hyphenatedStr) {
+  	return hyphenatedStr.replace(/-([a-zA-Z])/g, function (match, $1) {
+  		return $1.toUpperCase();
+  	});
+  };
+
+  var helpers_prefix__prefix, prefixCache, helpers_prefix__testStyle;
+
+  if (!isClient) {
+  	helpers_prefix__prefix = null;
+  } else {
+  	prefixCache = {};
+  	helpers_prefix__testStyle = createElement("div").style;
+
+  	helpers_prefix__prefix = function (prop) {
+  		var i, vendor, capped;
+
+  		prop = camelCase(prop);
+
+  		if (!prefixCache[prop]) {
+  			if (helpers_prefix__testStyle[prop] !== undefined) {
+  				prefixCache[prop] = prop;
+  			} else {
+  				// test vendors...
+  				capped = prop.charAt(0).toUpperCase() + prop.substring(1);
+
+  				i = vendors.length;
+  				while (i--) {
+  					vendor = vendors[i];
+  					if (helpers_prefix__testStyle[vendor + capped] !== undefined) {
+  						prefixCache[prop] = vendor + capped;
+  						break;
+  					}
+  				}
+  			}
+  		}
+
+  		return prefixCache[prop];
+  	};
+  }
+
+  var helpers_prefix = helpers_prefix__prefix;
+
+  var getStyle, prototype_getStyle__getComputedStyle;
+
+  if (!isClient) {
+  	getStyle = null;
+  } else {
+  	prototype_getStyle__getComputedStyle = window.getComputedStyle || legacy.getComputedStyle;
+
+  	getStyle = function (props) {
+  		var computedStyle, styles, i, prop, value;
+
+  		computedStyle = prototype_getStyle__getComputedStyle(this.node);
+
+  		if (typeof props === "string") {
+  			value = computedStyle[helpers_prefix(props)];
+  			if (value === "0px") {
+  				value = 0;
+  			}
+  			return value;
+  		}
+
+  		if (!isArray(props)) {
+  			throw new Error("Transition$getStyle must be passed a string, or an array of strings representing CSS properties");
+  		}
+
+  		styles = {};
+
+  		i = props.length;
+  		while (i--) {
+  			prop = props[i];
+  			value = computedStyle[helpers_prefix(prop)];
+  			if (value === "0px") {
+  				value = 0;
+  			}
+  			styles[prop] = value;
+  		}
+
+  		return styles;
+  	};
+  }
+
+  var prototype_getStyle = getStyle;
+
+  var setStyle = function (style, value) {
+  	var prop;
+
+  	if (typeof style === "string") {
+  		this.node.style[helpers_prefix(style)] = value;
+  	} else {
+  		for (prop in style) {
+  			if (style.hasOwnProperty(prop)) {
+  				this.node.style[helpers_prefix(prop)] = style[prop];
+  			}
+  		}
+  	}
+
+  	return this;
+  };
+
+  var Ticker = function (options) {
+  	var easing;
+
+  	this.duration = options.duration;
+  	this.step = options.step;
+  	this.complete = options.complete;
+
+  	// easing
+  	if (typeof options.easing === "string") {
+  		easing = options.root.easing[options.easing];
+
+  		if (!easing) {
+  			warnOnceIfDebug(missingPlugin(options.easing, "easing"));
+  			easing = linear;
+  		}
+  	} else if (typeof options.easing === "function") {
+  		easing = options.easing;
+  	} else {
+  		easing = linear;
+  	}
+
+  	this.easing = easing;
+
+  	this.start = utils_getTime();
+  	this.end = this.start + this.duration;
+
+  	this.running = true;
+  	shared_animations.add(this);
+  };
+
+  Ticker.prototype = {
+  	tick: function (now) {
+  		var elapsed, eased;
+
+  		if (!this.running) {
+  			return false;
+  		}
+
+  		if (now > this.end) {
+  			if (this.step) {
+  				this.step(1);
+  			}
+
+  			if (this.complete) {
+  				this.complete(1);
+  			}
+
+  			return false;
+  		}
+
+  		elapsed = now - this.start;
+  		eased = this.easing(elapsed / this.duration);
+
+  		if (this.step) {
+  			this.step(eased);
+  		}
+
+  		return true;
+  	},
+
+  	stop: function () {
+  		if (this.abort) {
+  			this.abort();
+  		}
+
+  		this.running = false;
+  	}
+  };
+
+  var shared_Ticker = Ticker;
+  function linear(t) {
+  	return t;
+  }
+
+  var unprefixPattern = new RegExp("^-(?:" + vendors.join("|") + ")-");
+
+  var unprefix = function (prop) {
+  	return prop.replace(unprefixPattern, "");
+  };
+
+  var vendorPattern = new RegExp("^(?:" + vendors.join("|") + ")([A-Z])");
+
+  var hyphenate = function (str) {
+  	var hyphenated;
+
+  	if (!str) {
+  		return ""; // edge case
+  	}
+
+  	if (vendorPattern.test(str)) {
+  		str = "-" + str;
+  	}
+
+  	hyphenated = str.replace(/[A-Z]/g, function (match) {
+  		return "-" + match.toLowerCase();
+  	});
+
+  	return hyphenated;
+  };
+
+  var createTransitions,
+      animateStyle_createTransitions__testStyle,
+      TRANSITION,
+      TRANSITIONEND,
+      CSS_TRANSITIONS_ENABLED,
+      TRANSITION_DURATION,
+      TRANSITION_PROPERTY,
+      TRANSITION_TIMING_FUNCTION,
+      canUseCssTransitions = {},
+      cannotUseCssTransitions = {};
+
+  if (!isClient) {
+  	createTransitions = null;
+  } else {
+  	animateStyle_createTransitions__testStyle = createElement("div").style;
+
+  	// determine some facts about our environment
+  	(function () {
+  		if (animateStyle_createTransitions__testStyle.transition !== undefined) {
+  			TRANSITION = "transition";
+  			TRANSITIONEND = "transitionend";
+  			CSS_TRANSITIONS_ENABLED = true;
+  		} else if (animateStyle_createTransitions__testStyle.webkitTransition !== undefined) {
+  			TRANSITION = "webkitTransition";
+  			TRANSITIONEND = "webkitTransitionEnd";
+  			CSS_TRANSITIONS_ENABLED = true;
+  		} else {
+  			CSS_TRANSITIONS_ENABLED = false;
+  		}
+  	})();
+
+  	if (TRANSITION) {
+  		TRANSITION_DURATION = TRANSITION + "Duration";
+  		TRANSITION_PROPERTY = TRANSITION + "Property";
+  		TRANSITION_TIMING_FUNCTION = TRANSITION + "TimingFunction";
+  	}
+
+  	createTransitions = function (t, to, options, changedProperties, resolve) {
+
+  		// Wait a beat (otherwise the target styles will be applied immediately)
+  		// TODO use a fastdom-style mechanism?
+  		setTimeout(function () {
+
+  			var hashPrefix, jsTransitionsComplete, cssTransitionsComplete, checkComplete, transitionEndHandler;
+
+  			checkComplete = function () {
+  				if (jsTransitionsComplete && cssTransitionsComplete) {
+  					// will changes to events and fire have an unexpected consequence here?
+  					t.root.fire(t.name + ":end", t.node, t.isIntro);
+  					resolve();
+  				}
+  			};
+
+  			// this is used to keep track of which elements can use CSS to animate
+  			// which properties
+  			hashPrefix = (t.node.namespaceURI || "") + t.node.tagName;
+
+  			t.node.style[TRANSITION_PROPERTY] = changedProperties.map(helpers_prefix).map(hyphenate).join(",");
+  			t.node.style[TRANSITION_TIMING_FUNCTION] = hyphenate(options.easing || "linear");
+  			t.node.style[TRANSITION_DURATION] = options.duration / 1000 + "s";
+
+  			transitionEndHandler = function (event) {
+  				var index;
+
+  				index = changedProperties.indexOf(camelCase(unprefix(event.propertyName)));
+  				if (index !== -1) {
+  					changedProperties.splice(index, 1);
+  				}
+
+  				if (changedProperties.length) {
+  					// still transitioning...
+  					return;
+  				}
+
+  				t.node.removeEventListener(TRANSITIONEND, transitionEndHandler, false);
+
+  				cssTransitionsComplete = true;
+  				checkComplete();
+  			};
+
+  			t.node.addEventListener(TRANSITIONEND, transitionEndHandler, false);
+
+  			setTimeout(function () {
+  				var i = changedProperties.length,
+  				    hash,
+  				    originalValue,
+  				    index,
+  				    propertiesToTransitionInJs = [],
+  				    prop,
+  				    suffix;
+
+  				while (i--) {
+  					prop = changedProperties[i];
+  					hash = hashPrefix + prop;
+
+  					if (CSS_TRANSITIONS_ENABLED && !cannotUseCssTransitions[hash]) {
+  						t.node.style[helpers_prefix(prop)] = to[prop];
+
+  						// If we're not sure if CSS transitions are supported for
+  						// this tag/property combo, find out now
+  						if (!canUseCssTransitions[hash]) {
+  							originalValue = t.getStyle(prop);
+
+  							// if this property is transitionable in this browser,
+  							// the current style will be different from the target style
+  							canUseCssTransitions[hash] = t.getStyle(prop) != to[prop];
+  							cannotUseCssTransitions[hash] = !canUseCssTransitions[hash];
+
+  							// Reset, if we're going to use timers after all
+  							if (cannotUseCssTransitions[hash]) {
+  								t.node.style[helpers_prefix(prop)] = originalValue;
+  							}
+  						}
+  					}
+
+  					if (!CSS_TRANSITIONS_ENABLED || cannotUseCssTransitions[hash]) {
+  						// we need to fall back to timer-based stuff
+  						if (originalValue === undefined) {
+  							originalValue = t.getStyle(prop);
+  						}
+
+  						// need to remove this from changedProperties, otherwise transitionEndHandler
+  						// will get confused
+  						index = changedProperties.indexOf(prop);
+  						if (index === -1) {
+  							warnIfDebug("Something very strange happened with transitions. Please raise an issue at https://github.com/ractivejs/ractive/issues - thanks!", { node: t.node });
+  						} else {
+  							changedProperties.splice(index, 1);
+  						}
+
+  						// TODO Determine whether this property is animatable at all
+
+  						suffix = /[^\d]*$/.exec(to[prop])[0];
+
+  						// ...then kick off a timer-based transition
+  						propertiesToTransitionInJs.push({
+  							name: helpers_prefix(prop),
+  							interpolator: shared_interpolate(parseFloat(originalValue), parseFloat(to[prop])),
+  							suffix: suffix
+  						});
+  					}
+  				}
+
+  				// javascript transitions
+  				if (propertiesToTransitionInJs.length) {
+  					new shared_Ticker({
+  						root: t.root,
+  						duration: options.duration,
+  						easing: camelCase(options.easing || ""),
+  						step: function (pos) {
+  							var prop, i;
+
+  							i = propertiesToTransitionInJs.length;
+  							while (i--) {
+  								prop = propertiesToTransitionInJs[i];
+  								t.node.style[prop.name] = prop.interpolator(pos) + prop.suffix;
+  							}
+  						},
+  						complete: function () {
+  							jsTransitionsComplete = true;
+  							checkComplete();
+  						}
+  					});
+  				} else {
+  					jsTransitionsComplete = true;
+  				}
+
+  				if (!changedProperties.length) {
+  					// We need to cancel the transitionEndHandler, and deal with
+  					// the fact that it will never fire
+  					t.node.removeEventListener(TRANSITIONEND, transitionEndHandler, false);
+  					cssTransitionsComplete = true;
+  					checkComplete();
+  				}
+  			}, 0);
+  		}, options.delay || 0);
+  	};
+  }
+
+  var animateStyle_createTransitions = createTransitions;
+
+  var hidden, vendor, animateStyle_visibility__prefix, animateStyle_visibility__i, visibility;
+
+  if (typeof document !== "undefined") {
+  	hidden = "hidden";
+
+  	visibility = {};
+
+  	if (hidden in document) {
+  		animateStyle_visibility__prefix = "";
+  	} else {
+  		animateStyle_visibility__i = vendors.length;
+  		while (animateStyle_visibility__i--) {
+  			vendor = vendors[animateStyle_visibility__i];
+  			hidden = vendor + "Hidden";
+
+  			if (hidden in document) {
+  				animateStyle_visibility__prefix = vendor;
+  			}
+  		}
+  	}
+
+  	if (animateStyle_visibility__prefix !== undefined) {
+  		document.addEventListener(animateStyle_visibility__prefix + "visibilitychange", onChange);
+
+  		// initialise
+  		onChange();
+  	} else {
+  		// gah, we're in an old browser
+  		if ("onfocusout" in document) {
+  			document.addEventListener("focusout", onHide);
+  			document.addEventListener("focusin", onShow);
+  		} else {
+  			window.addEventListener("pagehide", onHide);
+  			window.addEventListener("blur", onHide);
+
+  			window.addEventListener("pageshow", onShow);
+  			window.addEventListener("focus", onShow);
+  		}
+
+  		visibility.hidden = false; // until proven otherwise. Not ideal but hey
+  	}
+  }
+
+  function onChange() {
+  	visibility.hidden = document[hidden];
+  }
+
+  function onHide() {
+  	visibility.hidden = true;
+  }
+
+  function onShow() {
+  	visibility.hidden = false;
+  }
+
+  var animateStyle_visibility = visibility;
+
+  var animateStyle, _animateStyle__getComputedStyle, resolved;
+
+  if (!isClient) {
+  	animateStyle = null;
+  } else {
+  	_animateStyle__getComputedStyle = window.getComputedStyle || legacy.getComputedStyle;
+
+  	animateStyle = function (style, value, options) {
+  		var _this = this;
+
+  		var to;
+
+  		if (arguments.length === 4) {
+  			throw new Error("t.animateStyle() returns a promise - use .then() instead of passing a callback");
+  		}
+
+  		// Special case - page isn't visible. Don't animate anything, because
+  		// that way you'll never get CSS transitionend events
+  		if (animateStyle_visibility.hidden) {
+  			this.setStyle(style, value);
+  			return resolved || (resolved = utils_Promise.resolve());
+  		}
+
+  		if (typeof style === "string") {
+  			to = {};
+  			to[style] = value;
+  		} else {
+  			to = style;
+
+  			// shuffle arguments
+  			options = value;
+  		}
+
+  		// As of 0.3.9, transition authors should supply an `option` object with
+  		// `duration` and `easing` properties (and optional `delay`), plus a
+  		// callback function that gets called after the animation completes
+
+  		// TODO remove this check in a future version
+  		if (!options) {
+  			warnOnceIfDebug("The \"%s\" transition does not supply an options object to `t.animateStyle()`. This will break in a future version of Ractive. For more info see https://github.com/RactiveJS/Ractive/issues/340", this.name);
+  			options = this;
+  		}
+
+  		var promise = new utils_Promise(function (resolve) {
+  			var propertyNames, changedProperties, computedStyle, current, from, i, prop;
+
+  			// Edge case - if duration is zero, set style synchronously and complete
+  			if (!options.duration) {
+  				_this.setStyle(to);
+  				resolve();
+  				return;
+  			}
+
+  			// Get a list of the properties we're animating
+  			propertyNames = Object.keys(to);
+  			changedProperties = [];
+
+  			// Store the current styles
+  			computedStyle = _animateStyle__getComputedStyle(_this.node);
+
+  			from = {};
+  			i = propertyNames.length;
+  			while (i--) {
+  				prop = propertyNames[i];
+  				current = computedStyle[helpers_prefix(prop)];
+
+  				if (current === "0px") {
+  					current = 0;
+  				}
+
+  				// we need to know if we're actually changing anything
+  				if (current != to[prop]) {
+  					// use != instead of !==, so we can compare strings with numbers
+  					changedProperties.push(prop);
+
+  					// make the computed style explicit, so we can animate where
+  					// e.g. height='auto'
+  					_this.node.style[helpers_prefix(prop)] = current;
+  				}
+  			}
+
+  			// If we're not actually changing anything, the transitionend event
+  			// will never fire! So we complete early
+  			if (!changedProperties.length) {
+  				resolve();
+  				return;
+  			}
+
+  			animateStyle_createTransitions(_this, to, options, changedProperties, resolve);
+  		});
+
+  		return promise;
+  	};
+  }
+
+  var _animateStyle = animateStyle;
+
+  var processParams = function (params, defaults) {
+  	if (typeof params === "number") {
+  		params = { duration: params };
+  	} else if (typeof params === "string") {
+  		if (params === "slow") {
+  			params = { duration: 600 };
+  		} else if (params === "fast") {
+  			params = { duration: 200 };
+  		} else {
+  			params = { duration: 400 };
+  		}
+  	} else if (!params) {
+  		params = {};
+  	}
+
+  	return fillGaps({}, params, defaults);
+  };
+
+  var prototype_start = Transition$start;
+
+  function Transition$start() {
+  	var _this = this;
+
+  	var node, originalStyle, completed;
+
+  	node = this.node = this.element.node;
+  	originalStyle = node.getAttribute("style");
+
+  	// create t.complete() - we don't want this on the prototype,
+  	// because we don't want `this` silliness when passing it as
+  	// an argument
+  	this.complete = function (noReset) {
+  		if (completed) {
+  			return;
+  		}
+
+  		if (!noReset && _this.isIntro) {
+  			resetStyle(node, originalStyle);
+  		}
+
+  		node._ractive.transition = null;
+  		_this._manager.remove(_this);
+
+  		completed = true;
+  	};
+
+  	// If the transition function doesn't exist, abort
+  	if (!this._fn) {
+  		this.complete();
+  		return;
+  	}
+
+  	this._fn.apply(this.root, [this].concat(this.params));
+  }
+
+  function resetStyle(node, style) {
+  	if (style) {
+  		node.setAttribute("style", style);
+  	} else {
+
+  		// Next line is necessary, to remove empty style attribute!
+  		// See http://stackoverflow.com/a/7167553
+  		node.getAttribute("style");
+  		node.removeAttribute("style");
+  	}
+  }
+
+  var Transition = function (owner, template, isIntro) {
+  	this.init(owner, template, isIntro);
+  };
+
+  Transition.prototype = {
+  	init: Transition_prototype_init,
+  	start: prototype_start,
+  	getStyle: prototype_getStyle,
+  	setStyle: setStyle,
+  	animateStyle: _animateStyle,
+  	processParams: processParams
+  };
+
+  var _Transition = Transition;
+
+  var Element_prototype_render = Element$render;
+
+  var updateCss, updateScript;
+
+  updateCss = function () {
+  	var node = this.node,
+  	    content = this.fragment.toString(false);
+
+  	// IE8 has no styleSheet unless there's a type text/css
+  	if (window && window.appearsToBeIELessEqual8) {
+  		node.type = "text/css";
+  	}
+
+  	if (node.styleSheet) {
+  		node.styleSheet.cssText = content;
+  	} else {
+
+  		while (node.hasChildNodes()) {
+  			node.removeChild(node.firstChild);
+  		}
+
+  		node.appendChild(document.createTextNode(content));
+  	}
+  };
+
+  updateScript = function () {
+  	if (!this.node.type || this.node.type === "text/javascript") {
+  		warnIfDebug("Script tag was updated. This does not cause the code to be re-evaluated!", { ractive: this.root });
+  		// As it happens, we ARE in a position to re-evaluate the code if we wanted
+  		// to - we could eval() it, or insert it into a fresh (temporary) script tag.
+  		// But this would be a terrible idea with unpredictable results, so let's not.
+  	}
+
+  	this.node.text = this.fragment.toString(false);
+  };
+  function Element$render() {
+  	var _this = this;
+
+  	var root = this.root,
+  	    namespace,
+  	    node,
+  	    transition;
+
+  	namespace = getNamespace(this);
+  	node = this.node = createElement(this.name, namespace);
+
+  	// Is this a top-level node of a component? If so, we may need to add
+  	// a data-ractive-css attribute, for CSS encapsulation
+  	if (this.parentFragment.cssIds) {
+  		this.node.setAttribute("data-ractive-css", this.parentFragment.cssIds.map(function (x) {
+  			return "{" + x + "}";
+  		}).join(" "));
+  	}
+
+  	// Add _ractive property to the node - we use this object to store stuff
+  	// related to proxy events, two-way bindings etc
+  	defineProperty(this.node, "_ractive", {
+  		value: {
+  			proxy: this,
+  			keypath: getInnerContext(this.parentFragment),
+  			events: create(null),
+  			root: root
+  		}
+  	});
+
+  	// Render attributes
+  	this.attributes.forEach(function (a) {
+  		return a.render(node);
+  	});
+  	this.conditionalAttributes.forEach(function (a) {
+  		return a.render(node);
+  	});
+
+  	// Render children
+  	if (this.fragment) {
+  		// Special case - <script> element
+  		if (this.name === "script") {
+  			this.bubble = updateScript;
+  			this.node.text = this.fragment.toString(false); // bypass warning initially
+  			this.fragment.unrender = noop; // TODO this is a kludge
+  		}
+
+  		// Special case - <style> element
+  		else if (this.name === "style") {
+  			this.bubble = updateCss;
+  			this.bubble();
+  			this.fragment.unrender = noop;
+  		}
+
+  		// Special case - contenteditable
+  		else if (this.binding && this.getAttribute("contenteditable")) {
+  			this.fragment.unrender = noop;
+  		} else {
+  			this.node.appendChild(this.fragment.render());
+  		}
+  	}
+
+  	// deal with two-way bindings
+  	if (this.binding) {
+  		this.binding.render();
+  		this.node._ractive.binding = this.binding;
+  	}
+
+  	// Add proxy event handlers
+  	if (this.eventHandlers) {
+  		this.eventHandlers.forEach(function (h) {
+  			return h.render();
+  		});
+  	}
+
+  	if (this.name === "option") {
+  		processOption(this);
+  	}
+
+  	// Special cases
+  	if (this.name === "img") {
+  		// if this is an <img>, and we're in a crap browser, we may
+  		// need to prevent it from overriding width and height when
+  		// it loads the src
+  		special_img__render(this);
+  	} else if (this.name === "form") {
+  		// forms need to keep track of their bindings, in case of reset
+  		form__render(this);
+  	} else if (this.name === "input" || this.name === "textarea") {
+  		// inputs and textareas should store their initial value as
+  		// `defaultValue` in case of reset
+  		this.node.defaultValue = this.node.value;
+  	} else if (this.name === "option") {
+  		// similarly for option nodes
+  		this.node.defaultSelected = this.node.selected;
+  	}
+
+  	// apply decorator(s)
+  	if (this.decorator && this.decorator.fn) {
+  		global_runloop.scheduleTask(function () {
+  			if (!_this.decorator.torndown) {
+  				_this.decorator.init();
+  			}
+  		}, true);
+  	}
+
+  	// trigger intro transition
+  	if (root.transitionsEnabled && this.intro) {
+  		transition = new _Transition(this, this.intro, true);
+  		global_runloop.registerTransition(transition);
+  		global_runloop.scheduleTask(function () {
+  			return transition.start();
+  		}, true);
+
+  		this.transition = transition;
+  	}
+
+  	if (this.node.autofocus) {
+  		// Special case. Some browsers (*cough* Firefix *cough*) have a problem
+  		// with dynamically-generated elements having autofocus, and they won't
+  		// allow you to programmatically focus the element until it's in the DOM
+  		global_runloop.scheduleTask(function () {
+  			return _this.node.focus();
+  		}, true);
+  	}
+
+  	updateLiveQueries(this);
+  	return this.node;
+  }
+
+  function getNamespace(element) {
+  	var namespace, xmlns, parent;
+
+  	// Use specified namespace...
+  	if (xmlns = element.getAttribute("xmlns")) {
+  		namespace = xmlns;
+  	}
+
+  	// ...or SVG namespace, if this is an <svg> element
+  	else if (element.name === "svg") {
+  		namespace = namespaces.svg;
+  	} else if (parent = element.parent) {
+  		// ...or HTML, if the parent is a <foreignObject>
+  		if (parent.name === "foreignObject") {
+  			namespace = namespaces.html;
+  		}
+
+  		// ...or inherit from the parent node
+  		else {
+  			namespace = parent.node.namespaceURI;
+  		}
+  	} else {
+  		namespace = element.root.el.namespaceURI;
+  	}
+
+  	return namespace;
+  }
+
+  function processOption(option) {
+  	var optionValue, selectValue, i;
+
+  	if (!option.select) {
+  		return;
+  	}
+
+  	selectValue = option.select.getAttribute("value");
+  	if (selectValue === undefined) {
+  		return;
+  	}
+
+  	optionValue = option.getAttribute("value");
+
+  	if (option.select.node.multiple && isArray(selectValue)) {
+  		i = selectValue.length;
+  		while (i--) {
+  			if (optionValue == selectValue[i]) {
+  				option.node.selected = true;
+  				break;
+  			}
+  		}
+  	} else {
+  		option.node.selected = optionValue == selectValue;
+  	}
+  }
+
+  function updateLiveQueries(element) {
+  	var instance, liveQueries, i, selector, query;
+
+  	// Does this need to be added to any live queries?
+  	instance = element.root;
+
+  	do {
+  		liveQueries = instance._liveQueries;
+
+  		i = liveQueries.length;
+  		while (i--) {
+  			selector = liveQueries[i];
+  			query = liveQueries["_" + selector];
+
+  			if (query._test(element)) {
+  				// keep register of applicable selectors, for when we teardown
+  				(element.liveQueries || (element.liveQueries = [])).push(query);
+  			}
+  		}
+  	} while (instance = instance.parent);
+  }
+
+  var Element_prototype_toString = function () {
+  	var str, escape;
+
+  	if (this.template.y) {
+  		// DOCTYPE declaration
+  		return "<!DOCTYPE" + this.template.dd + ">";
+  	}
+
+  	str = "<" + this.template.e;
+
+  	str += this.attributes.map(stringifyAttribute).join("") + this.conditionalAttributes.map(stringifyAttribute).join("");
+
+  	// Special case - selected options
+  	if (this.name === "option" && optionIsSelected(this)) {
+  		str += " selected";
+  	}
+
+  	// Special case - two-way radio name bindings
+  	if (this.name === "input" && inputIsCheckedRadio(this)) {
+  		str += " checked";
+  	}
+
+  	str += ">";
+
+  	// Special case - textarea
+  	if (this.name === "textarea" && this.getAttribute("value") !== undefined) {
+  		str += escapeHtml(this.getAttribute("value"));
+  	}
+
+  	// Special case - contenteditable
+  	else if (this.getAttribute("contenteditable") !== undefined) {
+  		str += this.getAttribute("value") || "";
+  	}
+
+  	if (this.fragment) {
+  		escape = this.name !== "script" && this.name !== "style";
+  		str += this.fragment.toString(escape);
+  	}
+
+  	// add a closing tag if this isn't a void element
+  	if (!voidElementNames.test(this.template.e)) {
+  		str += "</" + this.template.e + ">";
+  	}
+
+  	return str;
+  };
+
+  function optionIsSelected(element) {
+  	var optionValue, selectValue, i;
+
+  	optionValue = element.getAttribute("value");
+
+  	if (optionValue === undefined || !element.select) {
+  		return false;
+  	}
+
+  	selectValue = element.select.getAttribute("value");
+
+  	if (selectValue == optionValue) {
+  		return true;
+  	}
+
+  	if (element.select.getAttribute("multiple") && isArray(selectValue)) {
+  		i = selectValue.length;
+  		while (i--) {
+  			if (selectValue[i] == optionValue) {
+  				return true;
+  			}
+  		}
+  	}
+  }
+
+  function inputIsCheckedRadio(element) {
+  	var attributes, typeAttribute, valueAttribute, nameAttribute;
+
+  	attributes = element.attributes;
+
+  	typeAttribute = attributes.type;
+  	valueAttribute = attributes.value;
+  	nameAttribute = attributes.name;
+
+  	if (!typeAttribute || typeAttribute.value !== "radio" || !valueAttribute || !nameAttribute.interpolator) {
+  		return;
+  	}
+
+  	if (valueAttribute.value === nameAttribute.interpolator.value) {
+  		return true;
+  	}
+  }
+
+  function stringifyAttribute(attribute) {
+  	var str = attribute.toString();
+  	return str ? " " + str : "";
+  }
+
+  var Element_prototype_unbind = Element$unbind;
+  function Element$unbind() {
+  	if (this.fragment) {
+  		this.fragment.unbind();
+  	}
+
+  	if (this.binding) {
+  		this.binding.unbind();
+  	}
+
+  	if (this.eventHandlers) {
+  		this.eventHandlers.forEach(methodCallers__unbind);
+  	}
+
+  	// Special case - <option>
+  	if (this.name === "option") {
+  		special_option__unbind(this);
+  	}
+
+  	this.attributes.forEach(methodCallers__unbind);
+  	this.conditionalAttributes.forEach(methodCallers__unbind);
+  }
+
+  var Element_prototype_unrender = Element$unrender;
+
+  function Element$unrender(shouldDestroy) {
+  	var binding, bindings, transition;
+
+  	if (transition = this.transition) {
+  		transition.complete();
+  	}
+
+  	// Detach as soon as we can
+  	if (this.name === "option") {
+  		// <option> elements detach immediately, so that
+  		// their parent <select> element syncs correctly, and
+  		// since option elements can't have transitions anyway
+  		this.detach();
+  	} else if (shouldDestroy) {
+  		global_runloop.detachWhenReady(this);
+  	}
+
+  	// Children first. that way, any transitions on child elements will be
+  	// handled by the current transitionManager
+  	if (this.fragment) {
+  		this.fragment.unrender(false);
+  	}
+
+  	if (binding = this.binding) {
+  		this.binding.unrender();
+
+  		this.node._ractive.binding = null;
+  		bindings = this.root._twowayBindings[binding.keypath.str];
+  		bindings.splice(bindings.indexOf(binding), 1);
+  	}
+
+  	// Remove event handlers
+  	if (this.eventHandlers) {
+  		this.eventHandlers.forEach(methodCallers__unrender);
+  	}
+
+  	if (this.decorator) {
+  		global_runloop.registerDecorator(this.decorator);
+  	}
+
+  	// trigger outro transition if necessary
+  	if (this.root.transitionsEnabled && this.outro) {
+  		transition = new _Transition(this, this.outro, false);
+  		global_runloop.registerTransition(transition);
+  		global_runloop.scheduleTask(function () {
+  			return transition.start();
+  		});
+  	}
+
+  	// Remove this node from any live queries
+  	if (this.liveQueries) {
+  		removeFromLiveQueries(this);
+  	}
+
+  	if (this.name === "form") {
+  		form__unrender(this);
+  	}
+  }
+
+  function removeFromLiveQueries(element) {
+  	var query, selector, i;
+
+  	i = element.liveQueries.length;
+  	while (i--) {
+  		query = element.liveQueries[i];
+  		selector = query.selector;
+
+  		query._remove(element.node);
+  	}
+  }
+
+  var Element = function (options) {
+  	this.init(options);
+  };
+
+  Element.prototype = {
+  	bubble: Element_prototype_bubble,
+  	detach: Element_prototype_detach,
+  	find: Element_prototype_find,
+  	findAll: Element_prototype_findAll,
+  	findAllComponents: Element_prototype_findAllComponents,
+  	findComponent: Element_prototype_findComponent,
+  	findNextNode: Element_prototype_findNextNode,
+  	firstNode: Element_prototype_firstNode,
+  	getAttribute: getAttribute,
+  	init: Element_prototype_init,
+  	rebind: Element_prototype_rebind,
+  	render: Element_prototype_render,
+  	toString: Element_prototype_toString,
+  	unbind: Element_prototype_unbind,
+  	unrender: Element_prototype_unrender
+  };
+
+  var _Element = Element;
+
+  var deIndent__empty = /^\s*$/,
+      deIndent__leadingWhitespace = /^\s*/;
+
+  var deIndent = function (str) {
+  	var lines, firstLine, lastLine, minIndent;
+
+  	lines = str.split("\n");
+
+  	// remove first and last line, if they only contain whitespace
+  	firstLine = lines[0];
+  	if (firstLine !== undefined && deIndent__empty.test(firstLine)) {
+  		lines.shift();
+  	}
+
+  	lastLine = lastItem(lines);
+  	if (lastLine !== undefined && deIndent__empty.test(lastLine)) {
+  		lines.pop();
+  	}
+
+  	minIndent = lines.reduce(reducer, null);
+
+  	if (minIndent) {
+  		str = lines.map(function (line) {
+  			return line.replace(minIndent, "");
+  		}).join("\n");
+  	}
+
+  	return str;
+  };
+
+  function reducer(previous, line) {
+  	var lineIndent = deIndent__leadingWhitespace.exec(line)[0];
+
+  	if (previous === null || lineIndent.length < previous.length) {
+  		return lineIndent;
+  	}
+
+  	return previous;
+  }
+
+  var Partial_getPartialTemplate = getPartialTemplate;
+
+  function getPartialTemplate(ractive, name, parentFragment) {
+  	var partial;
+
+  	// If the partial in instance or view heirarchy instances, great
+  	if (partial = getPartialFromRegistry(ractive, name, parentFragment || {})) {
+  		return partial;
+  	}
+
+  	// Does it exist on the page as a script tag?
+  	partial = template_parser.fromId(name, { noThrow: true });
+
+  	if (partial) {
+  		// is this necessary?
+  		partial = deIndent(partial);
+
+  		// parse and register to this ractive instance
+  		var parsed = template_parser.parse(partial, template_parser.getParseOptions(ractive));
+
+  		// register (and return main partial if there are others in the template)
+  		return ractive.partials[name] = parsed.t;
+  	}
+  }
+
+  function getPartialFromRegistry(ractive, name, parentFragment) {
+  	var fn = undefined,
+  	    partial = findParentPartial(name, parentFragment.owner);
+
+  	// if there was an instance up-hierarchy, cool
+  	if (partial) return partial;
+
+  	// find first instance in the ractive or view hierarchy that has this partial
+  	var instance = findInstance("partials", ractive, name);
+
+  	if (!instance) {
+  		return;
+  	}
+
+  	partial = instance.partials[name];
+
+  	// partial is a function?
+  	if (typeof partial === "function") {
+  		fn = partial.bind(instance);
+  		fn.isOwner = instance.partials.hasOwnProperty(name);
+  		partial = fn.call(ractive, template_parser);
+  	}
+
+  	if (!partial && partial !== "") {
+  		warnIfDebug(noRegistryFunctionReturn, name, "partial", "partial", { ractive: ractive });
+  		return;
+  	}
+
+  	// If this was added manually to the registry,
+  	// but hasn't been parsed, parse it now
+  	if (!template_parser.isParsed(partial)) {
+
+  		// use the parseOptions of the ractive instance on which it was found
+  		var parsed = template_parser.parse(partial, template_parser.getParseOptions(instance));
+
+  		// Partials cannot contain nested partials!
+  		// TODO add a test for this
+  		if (parsed.p) {
+  			warnIfDebug("Partials ({{>%s}}) cannot contain nested inline partials", name, { ractive: ractive });
+  		}
+
+  		// if fn, use instance to store result, otherwise needs to go
+  		// in the correct point in prototype chain on instance or constructor
+  		var target = fn ? instance : findOwner(instance, name);
+
+  		// may be a template with partials, which need to be registered and main template extracted
+  		target.partials[name] = partial = parsed.t;
+  	}
+
+  	// store for reset
+  	if (fn) {
+  		partial._fn = fn;
+  	}
+
+  	return partial.v ? partial.t : partial;
+  }
+
+  function findOwner(ractive, key) {
+  	return ractive.partials.hasOwnProperty(key) ? ractive : findConstructor(ractive.constructor, key);
+  }
+
+  function findConstructor(constructor, key) {
+  	if (!constructor) {
+  		return;
+  	}
+  	return constructor.partials.hasOwnProperty(key) ? constructor : findConstructor(constructor._Parent, key);
+  }
+
+  function findParentPartial(name, parent) {
+  	if (parent) {
+  		if (parent.template && parent.template.p && parent.template.p[name]) {
+  			return parent.template.p[name];
+  		} else if (parent.parentFragment && parent.parentFragment.owner) {
+  			return findParentPartial(name, parent.parentFragment.owner);
+  		}
+  	}
+  }
+
+  var applyIndent = function (string, indent) {
+  	var indented;
+
+  	if (!indent) {
+  		return string;
+  	}
+
+  	indented = string.split("\n").map(function (line, notFirstLine) {
+  		return notFirstLine ? indent + line : line;
+  	}).join("\n");
+
+  	return indented;
+  };
+
+  var missingPartialMessage = "Could not find template for partial \"%s\"";
+
+  var Partial = function (options) {
+  	var parentFragment, template;
+
+  	parentFragment = this.parentFragment = options.parentFragment;
+
+  	this.root = parentFragment.root;
+  	this.type = PARTIAL;
+  	this.index = options.index;
+  	this.name = options.template.r;
+  	this.rendered = false;
+
+  	this.fragment = this.fragmentToRender = this.fragmentToUnrender = null;
+
+  	Mustache.init(this, options);
+
+  	// If this didn't resolve, it most likely means we have a named partial
+  	// (i.e. `{{>foo}}` means 'use the foo partial', not 'use the partial
+  	// whose name is the value of `foo`')
+  	if (!this.keypath) {
+  		if (template = Partial_getPartialTemplate(this.root, this.name, parentFragment)) {
+  			shared_unbind.call(this); // prevent any further changes
+  			this.isNamed = true;
+  			this.setTemplate(template);
+  		} else {
+  			warnOnceIfDebug(missingPartialMessage, this.name);
+  		}
+  	}
+  };
+
+  Partial.prototype = {
+  	bubble: function () {
+  		this.parentFragment.bubble();
+  	},
+
+  	detach: function () {
+  		return this.fragment.detach();
+  	},
+
+  	find: function (selector) {
+  		return this.fragment.find(selector);
+  	},
+
+  	findAll: function (selector, query) {
+  		return this.fragment.findAll(selector, query);
+  	},
+
+  	findComponent: function (selector) {
+  		return this.fragment.findComponent(selector);
+  	},
+
+  	findAllComponents: function (selector, query) {
+  		return this.fragment.findAllComponents(selector, query);
+  	},
+
+  	firstNode: function () {
+  		return this.fragment.firstNode();
+  	},
+
+  	findNextNode: function () {
+  		return this.parentFragment.findNextNode(this);
+  	},
+
+  	getPartialName: function () {
+  		if (this.isNamed && this.name) return this.name;else if (this.value === undefined) return this.name;else return this.value;
+  	},
+
+  	getValue: function () {
+  		return this.fragment.getValue();
+  	},
+
+  	rebind: function (oldKeypath, newKeypath) {
+  		// named partials aren't bound, so don't rebind
+  		if (!this.isNamed) {
+  			Mustache_rebind.call(this, oldKeypath, newKeypath);
+  		}
+
+  		if (this.fragment) {
+  			this.fragment.rebind(oldKeypath, newKeypath);
+  		}
+  	},
+
+  	render: function () {
+  		this.docFrag = document.createDocumentFragment();
+  		this.update();
+
+  		this.rendered = true;
+  		return this.docFrag;
+  	},
+
+  	resolve: Mustache.resolve,
+
+  	setValue: function (value) {
+  		var template;
+
+  		if (value !== undefined && value === this.value) {
+  			// nothing has changed, so no work to be done
+  			return;
+  		}
+
+  		if (value !== undefined) {
+  			template = Partial_getPartialTemplate(this.root, "" + value, this.parentFragment);
+  		}
+
+  		// we may be here if we have a partial like `{{>foo}}` and `foo` is the
+  		// name of both a data property (whose value ISN'T the name of a partial)
+  		// and a partial. In those cases, this becomes a named partial
+  		if (!template && this.name && (template = Partial_getPartialTemplate(this.root, this.name, this.parentFragment))) {
+  			shared_unbind.call(this);
+  			this.isNamed = true;
+  		}
+
+  		if (!template) {
+  			warnOnceIfDebug(missingPartialMessage, this.name, { ractive: this.root });
+  		}
+
+  		this.value = value;
+
+  		this.setTemplate(template || []);
+
+  		this.bubble();
+
+  		if (this.rendered) {
+  			global_runloop.addView(this);
+  		}
+  	},
+
+  	setTemplate: function (template) {
+  		if (this.fragment) {
+  			this.fragment.unbind();
+  			if (this.rendered) {
+  				this.fragmentToUnrender = this.fragment;
+  			}
+  		}
+
+  		this.fragment = new virtualdom_Fragment({
+  			template: template,
+  			root: this.root,
+  			owner: this,
+  			pElement: this.parentFragment.pElement
+  		});
+
+  		this.fragmentToRender = this.fragment;
+  	},
+
+  	toString: function (toString) {
+  		var string, previousItem, lastLine, match;
+
+  		string = this.fragment.toString(toString);
+
+  		previousItem = this.parentFragment.items[this.index - 1];
+
+  		if (!previousItem || previousItem.type !== TEXT) {
+  			return string;
+  		}
+
+  		lastLine = previousItem.text.split("\n").pop();
+
+  		if (match = /^\s+$/.exec(lastLine)) {
+  			return applyIndent(string, match[0]);
+  		}
+
+  		return string;
+  	},
+
+  	unbind: function () {
+  		if (!this.isNamed) {
+  			// dynamic partial - need to unbind self
+  			shared_unbind.call(this);
+  		}
+
+  		if (this.fragment) {
+  			this.fragment.unbind();
+  		}
+  	},
+
+  	unrender: function (shouldDestroy) {
+  		if (this.rendered) {
+  			if (this.fragment) {
+  				this.fragment.unrender(shouldDestroy);
+  			}
+  			this.rendered = false;
+  		}
+  	},
+
+  	update: function () {
+  		var target, anchor;
+
+  		if (this.fragmentToUnrender) {
+  			this.fragmentToUnrender.unrender(true);
+  			this.fragmentToUnrender = null;
+  		}
+
+  		if (this.fragmentToRender) {
+  			this.docFrag.appendChild(this.fragmentToRender.render());
+  			this.fragmentToRender = null;
+  		}
+
+  		if (this.rendered) {
+  			target = this.parentFragment.getNode();
+  			anchor = this.parentFragment.findNextNode(this);
+  			target.insertBefore(this.docFrag, anchor);
+  		}
+  	}
+  };
+
+  var _Partial = Partial;
+
+  // finds the component constructor in the registry or view hierarchy registries
+
+  var Component_getComponent = getComponent;
+  function getComponent(ractive, name) {
+
+  	var Component,
+  	    instance = findInstance("components", ractive, name);
+
+  	if (instance) {
+  		Component = instance.components[name];
+
+  		// best test we have for not Ractive.extend
+  		if (!Component._Parent) {
+  			// function option, execute and store for reset
+  			var fn = Component.bind(instance);
+  			fn.isOwner = instance.components.hasOwnProperty(name);
+  			Component = fn();
+
+  			if (!Component) {
+  				warnIfDebug(noRegistryFunctionReturn, name, "component", "component", { ractive: ractive });
+
+  				return;
+  			}
+
+  			if (typeof Component === "string") {
+  				// allow string lookup
+  				Component = getComponent(ractive, Component);
+  			}
+
+  			Component._fn = fn;
+  			instance.components[name] = Component;
+  		}
+  	}
+
+  	return Component;
+  }
+
+  var Component_prototype_detach = Component$detach;
+  var Component_prototype_detach__detachHook = new hooks_Hook("detach");
+  function Component$detach() {
+  	var detached = this.instance.fragment.detach();
+  	Component_prototype_detach__detachHook.fire(this.instance);
+  	return detached;
+  }
+
+  var Component_prototype_find = Component$find;
+
+  function Component$find(selector) {
+  	return this.instance.fragment.find(selector);
+  }
+
+  var Component_prototype_findAll = Component$findAll;
+
+  function Component$findAll(selector, query) {
+  	return this.instance.fragment.findAll(selector, query);
+  }
+
+  var Component_prototype_findAllComponents = Component$findAllComponents;
+
+  function Component$findAllComponents(selector, query) {
+  	query._test(this, true);
+
+  	if (this.instance.fragment) {
+  		this.instance.fragment.findAllComponents(selector, query);
+  	}
+  }
+
+  var Component_prototype_findComponent = Component$findComponent;
+
+  function Component$findComponent(selector) {
+  	if (!selector || selector === this.name) {
+  		return this.instance;
+  	}
+
+  	if (this.instance.fragment) {
+  		return this.instance.fragment.findComponent(selector);
+  	}
+
+  	return null;
+  }
+
+  var Component_prototype_findNextNode = Component$findNextNode;
+
+  function Component$findNextNode() {
+  	return this.parentFragment.findNextNode(this);
+  }
+
+  var Component_prototype_firstNode = Component$firstNode;
+
+  function Component$firstNode() {
+  	if (this.rendered) {
+  		return this.instance.fragment.firstNode();
+  	}
+
+  	return null;
+  }
+
+  var processWrapper = function (wrapper, array, methodName, newIndices) {
+  	var root = wrapper.root;
+  	var keypath = wrapper.keypath;
+
+  	if (!!newIndices) {
+  		root.viewmodel.smartUpdate(keypath, array, newIndices);
+  	} else {
+  		// If this is a sort or reverse, we just do root.set()...
+  		// TODO use merge logic?
+  		root.viewmodel.mark(keypath);
+  	}
+  };
+
+  var patchedArrayProto = [],
+      mutatorMethods = ["pop", "push", "reverse", "shift", "sort", "splice", "unshift"],
+      testObj,
+      patchArrayMethods,
+      unpatchArrayMethods;
+
+  mutatorMethods.forEach(function (methodName) {
+  	var method = function () {
+  		for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
+  			args[_key] = arguments[_key];
+  		}
+
+  		var newIndices, result, wrapper, i;
+
+  		newIndices = shared_getNewIndices(this, methodName, args);
+
+  		// apply the underlying method
+  		result = Array.prototype[methodName].apply(this, arguments);
+
+  		// trigger changes
+  		global_runloop.start();
+
+  		this._ractive.setting = true;
+  		i = this._ractive.wrappers.length;
+  		while (i--) {
+  			wrapper = this._ractive.wrappers[i];
+
+  			global_runloop.addRactive(wrapper.root);
+  			processWrapper(wrapper, this, methodName, newIndices);
+  		}
+
+  		global_runloop.end();
+
+  		this._ractive.setting = false;
+  		return result;
+  	};
+
+  	defineProperty(patchedArrayProto, methodName, {
+  		value: method
+  	});
+  });
+
+  // can we use prototype chain injection?
+  // http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/#wrappers_prototype_chain_injection
+  testObj = {};
+
+  if (testObj.__proto__) {
+  	// yes, we can
+  	patchArrayMethods = function (array) {
+  		array.__proto__ = patchedArrayProto;
+  	};
+
+  	unpatchArrayMethods = function (array) {
+  		array.__proto__ = Array.prototype;
+  	};
+  } else {
+  	// no, we can't
+  	patchArrayMethods = function (array) {
+  		var i, methodName;
+
+  		i = mutatorMethods.length;
+  		while (i--) {
+  			methodName = mutatorMethods[i];
+  			defineProperty(array, methodName, {
+  				value: patchedArrayProto[methodName],
+  				configurable: true
+  			});
+  		}
+  	};
+
+  	unpatchArrayMethods = function (array) {
+  		var i;
+
+  		i = mutatorMethods.length;
+  		while (i--) {
+  			delete array[mutatorMethods[i]];
+  		}
+  	};
+  }
+
+  patchArrayMethods.unpatch = unpatchArrayMethods;
+  var patch = patchArrayMethods;
+
+  var arrayAdaptor,
+
+  // helpers
+  ArrayWrapper, array_index__errorMessage;
+
+  arrayAdaptor = {
+  	filter: function (object) {
+  		// wrap the array if a) b) it's an array, and b) either it hasn't been wrapped already,
+  		// or the array didn't trigger the get() itself
+  		return isArray(object) && (!object._ractive || !object._ractive.setting);
+  	},
+  	wrap: function (ractive, array, keypath) {
+  		return new ArrayWrapper(ractive, array, keypath);
+  	}
+  };
+
+  ArrayWrapper = function (ractive, array, keypath) {
+  	this.root = ractive;
+  	this.value = array;
+  	this.keypath = getKeypath(keypath);
+
+  	// if this array hasn't already been ractified, ractify it
+  	if (!array._ractive) {
+
+  		// define a non-enumerable _ractive property to store the wrappers
+  		defineProperty(array, "_ractive", {
+  			value: {
+  				wrappers: [],
+  				instances: [],
+  				setting: false
+  			},
+  			configurable: true
+  		});
+
+  		patch(array);
+  	}
+
+  	// store the ractive instance, so we can handle transitions later
+  	if (!array._ractive.instances[ractive._guid]) {
+  		array._ractive.instances[ractive._guid] = 0;
+  		array._ractive.instances.push(ractive);
+  	}
+
+  	array._ractive.instances[ractive._guid] += 1;
+  	array._ractive.wrappers.push(this);
+  };
+
+  ArrayWrapper.prototype = {
+  	get: function () {
+  		return this.value;
+  	},
+  	teardown: function () {
+  		var array, storage, wrappers, instances, index;
+
+  		array = this.value;
+  		storage = array._ractive;
+  		wrappers = storage.wrappers;
+  		instances = storage.instances;
+
+  		// if teardown() was invoked because we're clearing the cache as a result of
+  		// a change that the array itself triggered, we can save ourselves the teardown
+  		// and immediate setup
+  		if (storage.setting) {
+  			return false; // so that we don't remove it from this.root.viewmodel.wrapped
+  		}
+
+  		index = wrappers.indexOf(this);
+  		if (index === -1) {
+  			throw new Error(array_index__errorMessage);
+  		}
+
+  		wrappers.splice(index, 1);
+
+  		// if nothing else depends on this array, we can revert it to its
+  		// natural state
+  		if (!wrappers.length) {
+  			delete array._ractive;
+  			patch.unpatch(this.value);
+  		} else {
+  			// remove ractive instance if possible
+  			instances[this.root._guid] -= 1;
+  			if (!instances[this.root._guid]) {
+  				index = instances.indexOf(this.root);
+
+  				if (index === -1) {
+  					throw new Error(array_index__errorMessage);
+  				}
+
+  				instances.splice(index, 1);
+  			}
+  		}
+  	}
+  };
+
+  array_index__errorMessage = "Something went wrong in a rather interesting way";
+  var array_index = arrayAdaptor;
+
+  var numeric = /^\s*[0-9]+\s*$/;
+
+  var createBranch = function (key) {
+  	return numeric.test(key) ? [] : {};
+  };
+
+  var magicAdaptor, MagicWrapper;
+
+  try {
+  	Object.defineProperty({}, "test", { value: 0 });
+
+  	magicAdaptor = {
+  		filter: function (object, keypath, ractive) {
+  			var parentWrapper, parentValue;
+
+  			if (!keypath) {
+  				return false;
+  			}
+
+  			keypath = getKeypath(keypath);
+
+  			// If the parent value is a wrapper, other than a magic wrapper,
+  			// we shouldn't wrap this property
+  			if ((parentWrapper = ractive.viewmodel.wrapped[keypath.parent.str]) && !parentWrapper.magic) {
+  				return false;
+  			}
+
+  			parentValue = ractive.viewmodel.get(keypath.parent);
+
+  			// if parentValue is an array that doesn't include this member,
+  			// we should return false otherwise lengths will get messed up
+  			if (isArray(parentValue) && /^[0-9]+$/.test(keypath.lastKey)) {
+  				return false;
+  			}
+
+  			return parentValue && (typeof parentValue === "object" || typeof parentValue === "function");
+  		},
+  		wrap: function (ractive, property, keypath) {
+  			return new MagicWrapper(ractive, property, keypath);
+  		}
+  	};
+
+  	MagicWrapper = function (ractive, value, keypath) {
+  		var objKeypath, template, siblings;
+
+  		keypath = getKeypath(keypath);
+
+  		this.magic = true;
+
+  		this.ractive = ractive;
+  		this.keypath = keypath;
+  		this.value = value;
+
+  		this.prop = keypath.lastKey;
+
+  		objKeypath = keypath.parent;
+  		this.obj = objKeypath.isRoot ? ractive.viewmodel.data : ractive.viewmodel.get(objKeypath);
+
+  		template = this.originalDescriptor = Object.getOwnPropertyDescriptor(this.obj, this.prop);
+
+  		// Has this property already been wrapped?
+  		if (template && template.set && (siblings = template.set._ractiveWrappers)) {
+
+  			// Yes. Register this wrapper to this property, if it hasn't been already
+  			if (siblings.indexOf(this) === -1) {
+  				siblings.push(this);
+  			}
+
+  			return; // already wrapped
+  		}
+
+  		// No, it hasn't been wrapped
+  		createAccessors(this, value, template);
+  	};
+
+  	MagicWrapper.prototype = {
+  		get: function () {
+  			return this.value;
+  		},
+  		reset: function (value) {
+  			if (this.updating) {
+  				return;
+  			}
+
+  			this.updating = true;
+  			this.obj[this.prop] = value; // trigger set() accessor
+  			global_runloop.addRactive(this.ractive);
+  			this.ractive.viewmodel.mark(this.keypath, { keepExistingWrapper: true });
+  			this.updating = false;
+  			return true;
+  		},
+  		set: function (key, value) {
+  			if (this.updating) {
+  				return;
+  			}
+
+  			if (!this.obj[this.prop]) {
+  				this.updating = true;
+  				this.obj[this.prop] = createBranch(key);
+  				this.updating = false;
+  			}
+
+  			this.obj[this.prop][key] = value;
+  		},
+  		teardown: function () {
+  			var template, set, value, wrappers, index;
+
+  			// If this method was called because the cache was being cleared as a
+  			// result of a set()/update() call made by this wrapper, we return false
+  			// so that it doesn't get torn down
+  			if (this.updating) {
+  				return false;
+  			}
+
+  			template = Object.getOwnPropertyDescriptor(this.obj, this.prop);
+  			set = template && template.set;
+
+  			if (!set) {
+  				// most likely, this was an array member that was spliced out
+  				return;
+  			}
+
+  			wrappers = set._ractiveWrappers;
+
+  			index = wrappers.indexOf(this);
+  			if (index !== -1) {
+  				wrappers.splice(index, 1);
+  			}
+
+  			// Last one out, turn off the lights
+  			if (!wrappers.length) {
+  				value = this.obj[this.prop];
+
+  				Object.defineProperty(this.obj, this.prop, this.originalDescriptor || {
+  					writable: true,
+  					enumerable: true,
+  					configurable: true
+  				});
+
+  				this.obj[this.prop] = value;
+  			}
+  		}
+  	};
+  } catch (err) {
+  	magicAdaptor = false; // no magic in this browser
+  }
+
+  var adaptors_magic = magicAdaptor;
+
+  function createAccessors(originalWrapper, value, template) {
+
+  	var object, property, oldGet, oldSet, get, set;
+
+  	object = originalWrapper.obj;
+  	property = originalWrapper.prop;
+
+  	// Is this template configurable?
+  	if (template && !template.configurable) {
+  		// Special case - array length
+  		if (property === "length") {
+  			return;
+  		}
+
+  		throw new Error("Cannot use magic mode with property \"" + property + "\" - object is not configurable");
+  	}
+
+  	// Time to wrap this property
+  	if (template) {
+  		oldGet = template.get;
+  		oldSet = template.set;
+  	}
+
+  	get = oldGet || function () {
+  		return value;
+  	};
+
+  	set = function (v) {
+  		if (oldSet) {
+  			oldSet(v);
+  		}
+
+  		value = oldGet ? oldGet() : v;
+  		set._ractiveWrappers.forEach(updateWrapper);
+  	};
+
+  	function updateWrapper(wrapper) {
+  		var keypath, ractive;
+
+  		wrapper.value = value;
+
+  		if (wrapper.updating) {
+  			return;
+  		}
+
+  		ractive = wrapper.ractive;
+  		keypath = wrapper.keypath;
+
+  		wrapper.updating = true;
+  		global_runloop.start(ractive);
+
+  		ractive.viewmodel.mark(keypath);
+
+  		global_runloop.end();
+  		wrapper.updating = false;
+  	}
+
+  	// Create an array of wrappers, in case other keypaths/ractives depend on this property.
+  	// Handily, we can store them as a property of the set function. Yay JavaScript.
+  	set._ractiveWrappers = [originalWrapper];
+  	Object.defineProperty(object, property, { get: get, set: set, enumerable: true, configurable: true });
+  }
+
+  var magicArrayAdaptor, MagicArrayWrapper;
+
+  if (adaptors_magic) {
+  	magicArrayAdaptor = {
+  		filter: function (object, keypath, ractive) {
+  			return adaptors_magic.filter(object, keypath, ractive) && array_index.filter(object);
+  		},
+
+  		wrap: function (ractive, array, keypath) {
+  			return new MagicArrayWrapper(ractive, array, keypath);
+  		}
+  	};
+
+  	MagicArrayWrapper = function (ractive, array, keypath) {
+  		this.value = array;
+
+  		this.magic = true;
+
+  		this.magicWrapper = adaptors_magic.wrap(ractive, array, keypath);
+  		this.arrayWrapper = array_index.wrap(ractive, array, keypath);
+  	};
+
+  	MagicArrayWrapper.prototype = {
+  		get: function () {
+  			return this.value;
+  		},
+  		teardown: function () {
+  			this.arrayWrapper.teardown();
+  			this.magicWrapper.teardown();
+  		},
+  		reset: function (value) {
+  			return this.magicWrapper.reset(value);
+  		}
+  	};
+  }
+
+  var magicArray = magicArrayAdaptor;
+
+  var prototype_adapt = Viewmodel$adapt;
+
+  var prefixers = {};
+  function Viewmodel$adapt(keypath, value) {
+  	var len, i, adaptor, wrapped;
+
+  	if (!this.adaptors) return;
+
+  	// Do we have an adaptor for this value?
+  	len = this.adaptors.length;
+  	for (i = 0; i < len; i += 1) {
+  		adaptor = this.adaptors[i];
+
+  		if (adaptor.filter(value, keypath, this.ractive)) {
+  			wrapped = this.wrapped[keypath] = adaptor.wrap(this.ractive, value, keypath, getPrefixer(keypath));
+  			wrapped.value = value;
+  			return;
+  		}
+  	}
+  }
+
+  function prefixKeypath(obj, prefix) {
+  	var prefixed = {},
+  	    key;
+
+  	if (!prefix) {
+  		return obj;
+  	}
+
+  	prefix += ".";
+
+  	for (key in obj) {
+  		if (obj.hasOwnProperty(key)) {
+  			prefixed[prefix + key] = obj[key];
+  		}
+  	}
+
+  	return prefixed;
+  }
+
+  function getPrefixer(rootKeypath) {
+  	var rootDot;
+
+  	if (!prefixers[rootKeypath]) {
+  		rootDot = rootKeypath ? rootKeypath + "." : "";
+
+  		prefixers[rootKeypath] = function (relativeKeypath, value) {
+  			var obj;
+
+  			if (typeof relativeKeypath === "string") {
+  				obj = {};
+  				obj[rootDot + relativeKeypath] = value;
+  				return obj;
+  			}
+
+  			if (typeof relativeKeypath === "object") {
+  				// 'relativeKeypath' is in fact a hash, not a keypath
+  				return rootDot ? prefixKeypath(relativeKeypath, rootKeypath) : relativeKeypath;
+  			}
+  		};
+  	}
+
+  	return prefixers[rootKeypath];
+  }
+
+  // TEMP
+
+  var helpers_getUpstreamChanges = getUpstreamChanges;
+  function getUpstreamChanges(changes) {
+  	var upstreamChanges = [rootKeypath],
+  	    i,
+  	    keypath;
+
+  	i = changes.length;
+  	while (i--) {
+  		keypath = changes[i].parent;
+
+  		while (keypath && !keypath.isRoot) {
+  			if (changes.indexOf(keypath) === -1) {
+  				addToArray(upstreamChanges, keypath);
+  			}
+  			keypath = keypath.parent;
+  		}
+  	}
+
+  	return upstreamChanges;
+  }
+
+  var applyChanges_notifyPatternObservers = notifyPatternObservers;
+
+  function notifyPatternObservers(viewmodel, keypath, onlyDirect) {
+  	var potentialWildcardMatches;
+
+  	updateMatchingPatternObservers(viewmodel, keypath);
+
+  	if (onlyDirect) {
+  		return;
+  	}
+
+  	potentialWildcardMatches = keypath.wildcardMatches();
+  	potentialWildcardMatches.forEach(function (upstreamPattern) {
+  		cascade(viewmodel, upstreamPattern, keypath);
+  	});
+  }
+
+  function cascade(viewmodel, upstreamPattern, keypath) {
+  	var group, map, actualChildKeypath;
+
+  	// TODO should be one or the other
+  	upstreamPattern = upstreamPattern.str || upstreamPattern;
+
+  	group = viewmodel.depsMap.patternObservers;
+  	map = group && group[upstreamPattern];
+
+  	if (!map) {
+  		return;
+  	}
+
+  	map.forEach(function (childKeypath) {
+  		actualChildKeypath = keypath.join(childKeypath.lastKey); // 'foo.bar.baz'
+
+  		updateMatchingPatternObservers(viewmodel, actualChildKeypath);
+  		cascade(viewmodel, childKeypath, actualChildKeypath);
+  	});
+  }
+
+  function updateMatchingPatternObservers(viewmodel, keypath) {
+  	viewmodel.patternObservers.forEach(function (observer) {
+  		if (observer.regex.test(keypath.str)) {
+  			observer.update(keypath);
+  		}
+  	});
+  }
+
+  var applyChanges = Viewmodel$applyChanges;
+
+  function Viewmodel$applyChanges() {
+  	var _this = this;
+
+  	var self = this,
+  	    changes,
+  	    upstreamChanges,
+  	    hash = {},
+  	    bindings;
+
+  	changes = this.changes;
+
+  	if (!changes.length) {
+  		// TODO we end up here on initial render. Perhaps we shouldn't?
+  		return;
+  	}
+
+  	function invalidateComputation(computation) {
+  		var key = computation.key;
+
+  		if (computation.viewmodel === self) {
+  			self.clearCache(key.str);
+  			computation.invalidate();
+
+  			changes.push(key);
+  			cascade(key);
+  		} else {
+  			computation.viewmodel.mark(key);
+  		}
+  	}
+
+  	function cascade(keypath) {
+  		var map, computations;
+
+  		if (self.noCascade.hasOwnProperty(keypath.str)) {
+  			return;
+  		}
+
+  		if (computations = self.deps.computed[keypath.str]) {
+  			computations.forEach(invalidateComputation);
+  		}
+
+  		if (map = self.depsMap.computed[keypath.str]) {
+  			map.forEach(cascade);
+  		}
+  	}
+
+  	changes.slice().forEach(cascade);
+
+  	upstreamChanges = helpers_getUpstreamChanges(changes);
+  	upstreamChanges.forEach(function (keypath) {
+  		var computations;
+
+  		// make sure we haven't already been down this particular keypath in this turn
+  		if (changes.indexOf(keypath) === -1 && (computations = self.deps.computed[keypath.str])) {
+  			computations.forEach(invalidateComputation);
+  		}
+  	});
+
+  	this.changes = [];
+
+  	// Pattern observers are a weird special case
+  	if (this.patternObservers.length) {
+  		upstreamChanges.forEach(function (keypath) {
+  			return applyChanges_notifyPatternObservers(_this, keypath, true);
+  		});
+  		changes.forEach(function (keypath) {
+  			return applyChanges_notifyPatternObservers(_this, keypath);
+  		});
+  	}
+
+  	if (this.deps.observers) {
+  		upstreamChanges.forEach(function (keypath) {
+  			return notifyUpstreamDependants(_this, null, keypath, "observers");
+  		});
+  		notifyAllDependants(this, changes, "observers");
+  	}
+
+  	if (this.deps["default"]) {
+  		bindings = [];
+  		upstreamChanges.forEach(function (keypath) {
+  			return notifyUpstreamDependants(_this, bindings, keypath, "default");
+  		});
+
+  		if (bindings.length) {
+  			notifyBindings(this, bindings, changes);
+  		}
+
+  		notifyAllDependants(this, changes, "default");
+  	}
+
+  	// Return a hash of keypaths to updated values
+  	changes.forEach(function (keypath) {
+  		hash[keypath.str] = _this.get(keypath);
+  	});
+
+  	this.implicitChanges = {};
+  	this.noCascade = {};
+
+  	return hash;
+  }
+
+  function notifyUpstreamDependants(viewmodel, bindings, keypath, groupName) {
+  	var dependants, value;
+
+  	if (dependants = findDependants(viewmodel, keypath, groupName)) {
+  		value = viewmodel.get(keypath);
+
+  		dependants.forEach(function (d) {
+  			// don't "set" the parent value, refine it
+  			// i.e. not data = value, but data[foo] = fooValue
+  			if (bindings && d.refineValue) {
+  				bindings.push(d);
+  			} else {
+  				d.setValue(value);
+  			}
+  		});
+  	}
+  }
+
+  function notifyBindings(viewmodel, bindings, changes) {
+
+  	bindings.forEach(function (binding) {
+  		var useSet = false,
+  		    i = 0,
+  		    length = changes.length,
+  		    refinements = [];
+
+  		while (i < length) {
+  			var keypath = changes[i];
+
+  			if (keypath === binding.keypath) {
+  				useSet = true;
+  				break;
+  			}
+
+  			if (keypath.slice(0, binding.keypath.length) === binding.keypath) {
+  				refinements.push(keypath);
+  			}
+
+  			i++;
+  		}
+
+  		if (useSet) {
+  			binding.setValue(viewmodel.get(binding.keypath));
+  		}
+
+  		if (refinements.length) {
+  			binding.refineValue(refinements);
+  		}
+  	});
+  }
+
+  function notifyAllDependants(viewmodel, keypaths, groupName) {
+  	var queue = [];
+
+  	addKeypaths(keypaths);
+  	queue.forEach(dispatch);
+
+  	function addKeypaths(keypaths) {
+  		keypaths.forEach(addKeypath);
+  		keypaths.forEach(cascade);
+  	}
+
+  	function addKeypath(keypath) {
+  		var deps = findDependants(viewmodel, keypath, groupName);
+
+  		if (deps) {
+  			queue.push({
+  				keypath: keypath,
+  				deps: deps
+  			});
+  		}
+  	}
+
+  	function cascade(keypath) {
+  		var childDeps;
+
+  		if (childDeps = viewmodel.depsMap[groupName][keypath.str]) {
+  			addKeypaths(childDeps);
+  		}
+  	}
+
+  	function dispatch(set) {
+  		var value = viewmodel.get(set.keypath);
+  		set.deps.forEach(function (d) {
+  			return d.setValue(value);
+  		});
+  	}
+  }
+
+  function findDependants(viewmodel, keypath, groupName) {
+  	var group = viewmodel.deps[groupName];
+  	return group ? group[keypath.str] : null;
+  }
+
+  var capture = Viewmodel$capture;
+
+  function Viewmodel$capture() {
+  	this.captureGroups.push([]);
+  }
+
+  var clearCache = Viewmodel$clearCache;
+
+  function Viewmodel$clearCache(keypath, keepExistingWrapper) {
+  	var cacheMap, wrapper;
+
+  	if (!keepExistingWrapper) {
+  		// Is there a wrapped property at this keypath?
+  		if (wrapper = this.wrapped[keypath]) {
+  			// Did we unwrap it?
+  			if (wrapper.teardown() !== false) {
+  				// Is this right?
+  				// What's the meaning of returning false from teardown?
+  				// Could there be a GC ramification if this is a "real" ractive.teardown()?
+  				this.wrapped[keypath] = null;
+  			}
+  		}
+  	}
+
+  	this.cache[keypath] = undefined;
+
+  	if (cacheMap = this.cacheMap[keypath]) {
+  		while (cacheMap.length) {
+  			this.clearCache(cacheMap.pop());
+  		}
+  	}
+  }
+
+  var UnresolvedDependency = function (computation, ref) {
+  	this.computation = computation;
+  	this.viewmodel = computation.viewmodel;
+  	this.ref = ref;
+
+  	// TODO this seems like a red flag!
+  	this.root = this.viewmodel.ractive;
+  	this.parentFragment = this.root.component && this.root.component.parentFragment;
+  };
+
+  UnresolvedDependency.prototype = {
+  	resolve: function (keypath) {
+  		this.computation.softDeps.push(keypath);
+  		this.computation.unresolvedDeps[keypath.str] = null;
+  		this.viewmodel.register(keypath, this.computation, "computed");
+  	}
+  };
+
+  var Computation_UnresolvedDependency = UnresolvedDependency;
+
+  var Computation = function (key, signature) {
+  	this.key = key;
+
+  	this.getter = signature.getter;
+  	this.setter = signature.setter;
+
+  	this.hardDeps = signature.deps || [];
+  	this.softDeps = [];
+  	this.unresolvedDeps = {};
+
+  	this.depValues = {};
+
+  	this._dirty = this._firstRun = true;
+  };
+
+  Computation.prototype = {
+  	constructor: Computation,
+
+  	init: function (viewmodel) {
+  		var _this = this;
+
+  		var initial;
+
+  		this.viewmodel = viewmodel;
+  		this.bypass = true;
+
+  		initial = viewmodel.get(this.key);
+  		viewmodel.clearCache(this.key.str);
+
+  		this.bypass = false;
+
+  		if (this.setter && initial !== undefined) {
+  			this.set(initial);
+  		}
+
+  		if (this.hardDeps) {
+  			this.hardDeps.forEach(function (d) {
+  				return viewmodel.register(d, _this, "computed");
+  			});
+  		}
+  	},
+
+  	invalidate: function () {
+  		this._dirty = true;
+  	},
+
+  	get: function () {
+  		var _this = this;
+
+  		var newDeps,
+  		    dependenciesChanged,
+  		    dependencyValuesChanged = false;
+
+  		if (this.getting) {
+  			// prevent double-computation (e.g. caused by array mutation inside computation)
+  			var msg = "The " + this.key.str + " computation indirectly called itself. This probably indicates a bug in the computation. It is commonly caused by `array.sort(...)` - if that's the case, clone the array first with `array.slice().sort(...)`";
+  			warnOnce(msg);
+  			return this.value;
+  		}
+
+  		this.getting = true;
+
+  		if (this._dirty) {
+  			// determine whether the inputs have changed, in case this depends on
+  			// other computed values
+  			if (this._firstRun || !this.hardDeps.length && !this.softDeps.length) {
+  				dependencyValuesChanged = true;
+  			} else {
+  				[this.hardDeps, this.softDeps].forEach(function (deps) {
+  					var keypath, value, i;
+
+  					if (dependencyValuesChanged) {
+  						return;
+  					}
+
+  					i = deps.length;
+  					while (i--) {
+  						keypath = deps[i];
+  						value = _this.viewmodel.get(keypath);
+
+  						if (!isEqual(value, _this.depValues[keypath.str])) {
+  							_this.depValues[keypath.str] = value;
+  							dependencyValuesChanged = true;
+
+  							return;
+  						}
+  					}
+  				});
+  			}
+
+  			if (dependencyValuesChanged) {
+  				this.viewmodel.capture();
+
+  				try {
+  					this.value = this.getter();
+  				} catch (err) {
+  					warnIfDebug("Failed to compute \"%s\"", this.key.str);
+  					logIfDebug(err.stack || err);
+
+  					this.value = void 0;
+  				}
+
+  				newDeps = this.viewmodel.release();
+  				dependenciesChanged = this.updateDependencies(newDeps);
+
+  				if (dependenciesChanged) {
+  					[this.hardDeps, this.softDeps].forEach(function (deps) {
+  						deps.forEach(function (keypath) {
+  							_this.depValues[keypath.str] = _this.viewmodel.get(keypath);
+  						});
+  					});
+  				}
+  			}
+
+  			this._dirty = false;
+  		}
+
+  		this.getting = this._firstRun = false;
+  		return this.value;
+  	},
+
+  	set: function (value) {
+  		if (this.setting) {
+  			this.value = value;
+  			return;
+  		}
+
+  		if (!this.setter) {
+  			throw new Error("Computed properties without setters are read-only. (This may change in a future version of Ractive!)");
+  		}
+
+  		this.setter(value);
+  	},
+
+  	updateDependencies: function (newDeps) {
+  		var i, oldDeps, keypath, dependenciesChanged, unresolved;
+
+  		oldDeps = this.softDeps;
+
+  		// remove dependencies that are no longer used
+  		i = oldDeps.length;
+  		while (i--) {
+  			keypath = oldDeps[i];
+
+  			if (newDeps.indexOf(keypath) === -1) {
+  				dependenciesChanged = true;
+  				this.viewmodel.unregister(keypath, this, "computed");
+  			}
+  		}
+
+  		// create references for any new dependencies
+  		i = newDeps.length;
+  		while (i--) {
+  			keypath = newDeps[i];
+
+  			if (oldDeps.indexOf(keypath) === -1 && (!this.hardDeps || this.hardDeps.indexOf(keypath) === -1)) {
+  				dependenciesChanged = true;
+
+  				// if this keypath is currently unresolved, we need to mark
+  				// it as such. TODO this is a bit muddy...
+  				if (isUnresolved(this.viewmodel, keypath) && !this.unresolvedDeps[keypath.str]) {
+  					unresolved = new Computation_UnresolvedDependency(this, keypath.str);
+  					newDeps.splice(i, 1);
+
+  					this.unresolvedDeps[keypath.str] = unresolved;
+  					global_runloop.addUnresolved(unresolved);
+  				} else {
+  					this.viewmodel.register(keypath, this, "computed");
+  				}
+  			}
+  		}
+
+  		if (dependenciesChanged) {
+  			this.softDeps = newDeps.slice();
+  		}
+
+  		return dependenciesChanged;
+  	}
+  };
+
+  function isUnresolved(viewmodel, keypath) {
+  	var key = keypath.firstKey;
+
+  	return !(key in viewmodel.data) && !(key in viewmodel.computations) && !(key in viewmodel.mappings);
+  }
+
+  var Computation_Computation = Computation;
+
+  var compute = Viewmodel$compute;
+  function Viewmodel$compute(key, signature) {
+  	var computation = new Computation_Computation(key, signature);
+
+  	if (this.ready) {
+  		computation.init(this);
+  	}
+
+  	return this.computations[key.str] = computation;
+  }
+
+  var FAILED_LOOKUP = { FAILED_LOOKUP: true };
+
+  var viewmodel_prototype_get = Viewmodel$get;
+
+  var viewmodel_prototype_get__empty = {};
+  function Viewmodel$get(keypath, options) {
+  	var cache = this.cache,
+  	    value,
+  	    computation,
+  	    wrapped,
+  	    captureGroup,
+  	    keypathStr = keypath.str,
+  	    key;
+
+  	options = options || viewmodel_prototype_get__empty;
+
+  	// capture the keypath, if we're inside a computation
+  	if (options.capture && (captureGroup = lastItem(this.captureGroups))) {
+  		if (! ~captureGroup.indexOf(keypath)) {
+  			captureGroup.push(keypath);
+  		}
+  	}
+
+  	if (hasOwn.call(this.mappings, keypath.firstKey)) {
+  		return this.mappings[keypath.firstKey].get(keypath, options);
+  	}
+
+  	if (keypath.isSpecial) {
+  		return keypath.value;
+  	}
+
+  	if (cache[keypathStr] === undefined) {
+
+  		// Is this a computed property?
+  		if ((computation = this.computations[keypathStr]) && !computation.bypass) {
+  			value = computation.get();
+  			this.adapt(keypathStr, value);
+  		}
+
+  		// Is this a wrapped property?
+  		else if (wrapped = this.wrapped[keypathStr]) {
+  			value = wrapped.value;
+  		}
+
+  		// Is it the root?
+  		else if (keypath.isRoot) {
+  			this.adapt("", this.data);
+  			value = this.data;
+  		}
+
+  		// No? Then we need to retrieve the value one key at a time
+  		else {
+  			value = retrieve(this, keypath);
+  		}
+
+  		cache[keypathStr] = value;
+  	} else {
+  		value = cache[keypathStr];
+  	}
+
+  	if (!options.noUnwrap && (wrapped = this.wrapped[keypathStr])) {
+  		value = wrapped.get();
+  	}
+
+  	if (keypath.isRoot && options.fullRootGet) {
+  		for (key in this.mappings) {
+  			value[key] = this.mappings[key].getValue();
+  		}
+  	}
+
+  	return value === FAILED_LOOKUP ? void 0 : value;
+  }
+
+  function retrieve(viewmodel, keypath) {
+
+  	var parentValue, cacheMap, value, wrapped;
+
+  	parentValue = viewmodel.get(keypath.parent);
+
+  	if (wrapped = viewmodel.wrapped[keypath.parent.str]) {
+  		parentValue = wrapped.get();
+  	}
+
+  	if (parentValue === null || parentValue === undefined) {
+  		return;
+  	}
+
+  	// update cache map
+  	if (!(cacheMap = viewmodel.cacheMap[keypath.parent.str])) {
+  		viewmodel.cacheMap[keypath.parent.str] = [keypath.str];
+  	} else {
+  		if (cacheMap.indexOf(keypath.str) === -1) {
+  			cacheMap.push(keypath.str);
+  		}
+  	}
+
+  	// If this property doesn't exist, we return a sentinel value
+  	// so that we know to query parent scope (if such there be)
+  	if (typeof parentValue === "object" && !(keypath.lastKey in parentValue)) {
+  		return viewmodel.cache[keypath.str] = FAILED_LOOKUP;
+  	}
+
+  	value = parentValue[keypath.lastKey];
+
+  	// Do we have an adaptor for this value?
+  	viewmodel.adapt(keypath.str, value, false);
+
+  	// Update cache
+  	viewmodel.cache[keypath.str] = value;
+  	return value;
+  }
+
+  var viewmodel_prototype_init = Viewmodel$init;
+
+  function Viewmodel$init() {
+  	var key;
+
+  	for (key in this.computations) {
+  		this.computations[key].init(this);
+  	}
+  }
+
+  var prototype_map = Viewmodel$map;
+
+  function Viewmodel$map(key, options) {
+  	var mapping = this.mappings[key.str] = new Mapping(key, options);
+  	mapping.initViewmodel(this);
+  	return mapping;
+  }
+
+  var Mapping = function (localKey, options) {
+  	this.localKey = localKey;
+  	this.keypath = options.keypath;
+  	this.origin = options.origin;
+
+  	this.deps = [];
+  	this.unresolved = [];
+
+  	this.resolved = false;
+  };
+
+  Mapping.prototype = {
+  	forceResolution: function () {
+  		// TODO warn, as per #1692?
+  		this.keypath = this.localKey;
+  		this.setup();
+  	},
+
+  	get: function (keypath, options) {
+  		if (!this.resolved) {
+  			return undefined;
+  		}
+  		return this.origin.get(this.map(keypath), options);
+  	},
+
+  	getValue: function () {
+  		if (!this.keypath) {
+  			return undefined;
+  		}
+  		return this.origin.get(this.keypath);
+  	},
+
+  	initViewmodel: function (viewmodel) {
+  		this.local = viewmodel;
+  		this.setup();
+  	},
+
+  	map: function (keypath) {
+  		if (typeof this.keypath === undefined) {
+  			return this.localKey;
+  		}
+  		return keypath.replace(this.localKey, this.keypath);
+  	},
+
+  	register: function (keypath, dependant, group) {
+  		this.deps.push({ keypath: keypath, dep: dependant, group: group });
+
+  		if (this.resolved) {
+  			this.origin.register(this.map(keypath), dependant, group);
+  		}
+  	},
+
+  	resolve: function (keypath) {
+  		if (this.keypath !== undefined) {
+  			this.unbind(true);
+  		}
+
+  		this.keypath = keypath;
+  		this.setup();
+  	},
+
+  	set: function (keypath, value) {
+  		if (!this.resolved) {
+  			this.forceResolution();
+  		}
+
+  		this.origin.set(this.map(keypath), value);
+  	},
+
+  	setup: function () {
+  		var _this = this;
+
+  		if (this.keypath === undefined) {
+  			return;
+  		}
+
+  		this.resolved = true;
+
+  		// accumulated dependants can now be registered
+  		if (this.deps.length) {
+  			this.deps.forEach(function (d) {
+  				var keypath = _this.map(d.keypath);
+  				_this.origin.register(keypath, d.dep, d.group);
+
+  				// TODO this is a bit of a red flag... all deps should be the same?
+  				if (d.dep.setValue) {
+  					d.dep.setValue(_this.origin.get(keypath));
+  				} else if (d.dep.invalidate) {
+  					d.dep.invalidate();
+  				} else {
+  					throw new Error("An unexpected error occurred. Please raise an issue at https://github.com/ractivejs/ractive/issues - thanks!");
+  				}
+  			});
+
+  			this.origin.mark(this.keypath);
+  		}
+  	},
+
+  	setValue: function (value) {
+  		if (!this.keypath) {
+  			throw new Error("Mapping does not have keypath, cannot set value. Please raise an issue at https://github.com/ractivejs/ractive/issues - thanks!");
+  		}
+
+  		this.origin.set(this.keypath, value);
+  	},
+
+  	unbind: function (keepLocal) {
+  		var _this = this;
+
+  		if (!keepLocal) {
+  			delete this.local.mappings[this.localKey];
+  		}
+
+  		if (!this.resolved) {
+  			return;
+  		}
+
+  		this.deps.forEach(function (d) {
+  			_this.origin.unregister(_this.map(d.keypath), d.dep, d.group);
+  		});
+
+  		if (this.tracker) {
+  			this.origin.unregister(this.keypath, this.tracker);
+  		}
+  	},
+
+  	unregister: function (keypath, dependant, group) {
+  		var deps, i;
+
+  		if (!this.resolved) {
+  			return;
+  		}
+
+  		deps = this.deps;
+  		i = deps.length;
+
+  		while (i--) {
+  			if (deps[i].dep === dependant) {
+  				deps.splice(i, 1);
+  				break;
+  			}
+  		}
+  		this.origin.unregister(this.map(keypath), dependant, group);
+  	}
+  };
+
+  var mark = Viewmodel$mark;
+
+  function Viewmodel$mark(keypath, options) {
+  	var computation,
+  	    keypathStr = keypath.str;
+
+  	// implicit changes (i.e. `foo.length` on `ractive.push('foo',42)`)
+  	// should not be picked up by pattern observers
+  	if (options) {
+  		if (options.implicit) {
+  			this.implicitChanges[keypathStr] = true;
+  		}
+  		if (options.noCascade) {
+  			this.noCascade[keypathStr] = true;
+  		}
+  	}
+
+  	if (computation = this.computations[keypathStr]) {
+  		computation.invalidate();
+  	}
+
+  	if (this.changes.indexOf(keypath) === -1) {
+  		this.changes.push(keypath);
+  	}
+
+  	// pass on keepExistingWrapper, if we can
+  	var keepExistingWrapper = options ? options.keepExistingWrapper : false;
+
+  	this.clearCache(keypathStr, keepExistingWrapper);
+
+  	if (this.ready) {
+  		this.onchange();
+  	}
+  }
+
+  var mapOldToNewIndex = function (oldArray, newArray) {
+  	var usedIndices, firstUnusedIndex, newIndices, changed;
+
+  	usedIndices = {};
+  	firstUnusedIndex = 0;
+
+  	newIndices = oldArray.map(function (item, i) {
+  		var index, start, len;
+
+  		start = firstUnusedIndex;
+  		len = newArray.length;
+
+  		do {
+  			index = newArray.indexOf(item, start);
+
+  			if (index === -1) {
+  				changed = true;
+  				return -1;
+  			}
+
+  			start = index + 1;
+  		} while (usedIndices[index] && start < len);
+
+  		// keep track of the first unused index, so we don't search
+  		// the whole of newArray for each item in oldArray unnecessarily
+  		if (index === firstUnusedIndex) {
+  			firstUnusedIndex += 1;
+  		}
+
+  		if (index !== i) {
+  			changed = true;
+  		}
+
+  		usedIndices[index] = true;
+  		return index;
+  	});
+
+  	return newIndices;
+  };
+
+  var merge = Viewmodel$merge;
+
+  var comparators = {};
+  function Viewmodel$merge(keypath, currentArray, array, options) {
+  	var oldArray, newArray, comparator, newIndices;
+
+  	this.mark(keypath);
+
+  	if (options && options.compare) {
+
+  		comparator = getComparatorFunction(options.compare);
+
+  		try {
+  			oldArray = currentArray.map(comparator);
+  			newArray = array.map(comparator);
+  		} catch (err) {
+  			// fallback to an identity check - worst case scenario we have
+  			// to do more DOM manipulation than we thought...
+  			warnIfDebug("merge(): \"%s\" comparison failed. Falling back to identity checking", keypath);
+
+  			oldArray = currentArray;
+  			newArray = array;
+  		}
+  	} else {
+  		oldArray = currentArray;
+  		newArray = array;
+  	}
+
+  	// find new indices for members of oldArray
+  	newIndices = mapOldToNewIndex(oldArray, newArray);
+
+  	this.smartUpdate(keypath, array, newIndices, currentArray.length !== array.length);
+  }
+
+  function stringify(item) {
+  	return JSON.stringify(item);
+  }
+
+  function getComparatorFunction(comparator) {
+  	// If `compare` is `true`, we use JSON.stringify to compare
+  	// objects that are the same shape, but non-identical - i.e.
+  	// { foo: 'bar' } !== { foo: 'bar' }
+  	if (comparator === true) {
+  		return stringify;
+  	}
+
+  	if (typeof comparator === "string") {
+  		if (!comparators[comparator]) {
+  			comparators[comparator] = function (item) {
+  				return item[comparator];
+  			};
+  		}
+
+  		return comparators[comparator];
+  	}
+
+  	if (typeof comparator === "function") {
+  		return comparator;
+  	}
+
+  	throw new Error("The `compare` option must be a function, or a string representing an identifying field (or `true` to use JSON.stringify)");
+  }
+
+  var register = Viewmodel$register;
+
+  function Viewmodel$register(keypath, dependant) {
+  	var group = arguments[2] === undefined ? "default" : arguments[2];
+
+  	var mapping, depsByKeypath, deps;
+
+  	if (dependant.isStatic) {
+  		return; // TODO we should never get here if a dependant is static...
+  	}
+
+  	if (mapping = this.mappings[keypath.firstKey]) {
+  		mapping.register(keypath, dependant, group);
+  	} else {
+  		depsByKeypath = this.deps[group] || (this.deps[group] = {});
+  		deps = depsByKeypath[keypath.str] || (depsByKeypath[keypath.str] = []);
+
+  		deps.push(dependant);
+
+  		if (!this.depsMap[group]) {
+  			this.depsMap[group] = {};
+  		}
+
+  		if (!keypath.isRoot) {
+  			register__updateDependantsMap(this, keypath, group);
+  		}
+  	}
+  }
+
+  function register__updateDependantsMap(viewmodel, keypath, group) {
+  	var map, parent, keypathStr;
+
+  	// update dependants map
+  	while (!keypath.isRoot) {
+  		map = viewmodel.depsMap[group];
+  		parent = map[keypath.parent.str] || (map[keypath.parent.str] = []);
+
+  		keypathStr = keypath.str;
+
+  		// TODO find an alternative to this nasty approach
+  		if (parent["_" + keypathStr] === undefined) {
+  			parent["_" + keypathStr] = 0;
+  			parent.push(keypath);
+  		}
+
+  		parent["_" + keypathStr] += 1;
+  		keypath = keypath.parent;
+  	}
+  }
+
+  var release = Viewmodel$release;
+
+  function Viewmodel$release() {
+  	return this.captureGroups.pop();
+  }
+
+  var reset = Viewmodel$reset;
+
+  function Viewmodel$reset(data) {
+  	this.data = data;
+  	this.clearCache("");
+  }
+
+  var prototype_set = Viewmodel$set;
+
+  function Viewmodel$set(keypath, value) {
+  	var options = arguments[2] === undefined ? {} : arguments[2];
+
+  	var mapping, computation, wrapper, keepExistingWrapper;
+
+  	// unless data is being set for data tracking purposes
+  	if (!options.noMapping) {
+  		// If this data belongs to a different viewmodel,
+  		// pass the change along
+  		if (mapping = this.mappings[keypath.firstKey]) {
+  			return mapping.set(keypath, value);
+  		}
+  	}
+
+  	computation = this.computations[keypath.str];
+  	if (computation) {
+  		if (computation.setting) {
+  			// let the other computation set() handle things...
+  			return;
+  		}
+  		computation.set(value);
+  		value = computation.get();
+  	}
+
+  	if (isEqual(this.cache[keypath.str], value)) {
+  		return;
+  	}
+
+  	wrapper = this.wrapped[keypath.str];
+
+  	// If we have a wrapper with a `reset()` method, we try and use it. If the
+  	// `reset()` method returns false, the wrapper should be torn down, and
+  	// (most likely) a new one should be created later
+  	if (wrapper && wrapper.reset) {
+  		keepExistingWrapper = wrapper.reset(value) !== false;
+
+  		if (keepExistingWrapper) {
+  			value = wrapper.get();
+  		}
+  	}
+
+  	if (!computation && !keepExistingWrapper) {
+  		resolveSet(this, keypath, value);
+  	}
+
+  	if (!options.silent) {
+  		this.mark(keypath);
+  	} else {
+  		// We're setting a parent of the original target keypath (i.e.
+  		// creating a fresh branch) - we need to clear the cache, but
+  		// not mark it as a change
+  		this.clearCache(keypath.str);
+  	}
+  }
+
+  function resolveSet(viewmodel, keypath, value) {
+  	var wrapper, parentValue, wrapperSet, valueSet;
+
+  	wrapperSet = function () {
+  		if (wrapper.set) {
+  			wrapper.set(keypath.lastKey, value);
+  		} else {
+  			parentValue = wrapper.get();
+  			valueSet();
+  		}
+  	};
+
+  	valueSet = function () {
+  		if (!parentValue) {
+  			parentValue = createBranch(keypath.lastKey);
+  			viewmodel.set(keypath.parent, parentValue, { silent: true });
+  		}
+  		parentValue[keypath.lastKey] = value;
+  	};
+
+  	wrapper = viewmodel.wrapped[keypath.parent.str];
+
+  	if (wrapper) {
+  		wrapperSet();
+  	} else {
+  		parentValue = viewmodel.get(keypath.parent);
+
+  		// may have been wrapped via the above .get()
+  		// call on viewmodel if this is first access via .set()!
+  		if (wrapper = viewmodel.wrapped[keypath.parent.str]) {
+  			wrapperSet();
+  		} else {
+  			valueSet();
+  		}
+  	}
+  }
+
+  var smartUpdate = Viewmodel$smartUpdate;
+
+  var implicitOption = { implicit: true },
+      noCascadeOption = { noCascade: true };
+  function Viewmodel$smartUpdate(keypath, array, newIndices) {
+  	var _this = this;
+
+  	var dependants, oldLength, i;
+
+  	oldLength = newIndices.length;
+
+  	// Indices that are being removed should be marked as dirty
+  	newIndices.forEach(function (newIndex, oldIndex) {
+  		if (newIndex === -1) {
+  			_this.mark(keypath.join(oldIndex), noCascadeOption);
+  		}
+  	});
+
+  	// Update the model
+  	// TODO allow existing array to be updated in place, rather than replaced?
+  	this.set(keypath, array, { silent: true });
+
+  	if (dependants = this.deps["default"][keypath.str]) {
+  		dependants.filter(canShuffle).forEach(function (d) {
+  			return d.shuffle(newIndices, array);
+  		});
+  	}
+
+  	if (oldLength !== array.length) {
+  		this.mark(keypath.join("length"), implicitOption);
+
+  		for (i = newIndices.touchedFrom; i < array.length; i += 1) {
+  			this.mark(keypath.join(i));
+  		}
+
+  		// don't allow removed indexes beyond end of new array to trigger recomputations
+  		// TODO is this still necessary, now that computations are lazy?
+  		for (i = array.length; i < oldLength; i += 1) {
+  			this.mark(keypath.join(i), noCascadeOption);
+  		}
+  	}
+  }
+
+  function canShuffle(dependant) {
+  	return typeof dependant.shuffle === "function";
+  }
+
+  var prototype_teardown = Viewmodel$teardown;
+
+  function Viewmodel$teardown() {
+  	var _this = this;
+
+  	var unresolvedImplicitDependency;
+
+  	// Clear entire cache - this has the desired side-effect
+  	// of unwrapping adapted values (e.g. arrays)
+  	Object.keys(this.cache).forEach(function (keypath) {
+  		return _this.clearCache(keypath);
+  	});
+
+  	// Teardown any failed lookups - we don't need them to resolve any more
+  	while (unresolvedImplicitDependency = this.unresolvedImplicitDependencies.pop()) {
+  		unresolvedImplicitDependency.teardown();
+  	}
+  }
+
+  var unregister = Viewmodel$unregister;
+
+  function Viewmodel$unregister(keypath, dependant) {
+  	var group = arguments[2] === undefined ? "default" : arguments[2];
+
+  	var mapping, deps, index;
+
+  	if (dependant.isStatic) {
+  		return;
+  	}
+
+  	if (mapping = this.mappings[keypath.firstKey]) {
+  		return mapping.unregister(keypath, dependant, group);
+  	}
+
+  	deps = this.deps[group][keypath.str];
+  	index = deps.indexOf(dependant);
+
+  	if (index === -1) {
+  		throw new Error("Attempted to remove a dependant that was no longer registered! This should not happen. If you are seeing this bug in development please raise an issue at https://github.com/RactiveJS/Ractive/issues - thanks");
+  	}
+
+  	deps.splice(index, 1);
+
+  	if (keypath.isRoot) {
+  		return;
+  	}
+
+  	unregister__updateDependantsMap(this, keypath, group);
+  }
+
+  function unregister__updateDependantsMap(viewmodel, keypath, group) {
+  	var map, parent;
+
+  	// update dependants map
+  	while (!keypath.isRoot) {
+  		map = viewmodel.depsMap[group];
+  		parent = map[keypath.parent.str];
+
+  		parent["_" + keypath.str] -= 1;
+
+  		if (!parent["_" + keypath.str]) {
+  			// remove from parent deps map
+  			removeFromArray(parent, keypath);
+  			parent["_" + keypath.str] = undefined;
+  		}
+
+  		keypath = keypath.parent;
+  	}
+  }
+
+  var Viewmodel = function (options) {
+  	var adapt = options.adapt;
+  	var data = options.data;
+  	var ractive = options.ractive;
+  	var computed = options.computed;
+  	var mappings = options.mappings;
+  	var key;
+  	var mapping;
+
+  	// TODO is it possible to remove this reference?
+  	this.ractive = ractive;
+
+  	this.adaptors = adapt;
+  	this.onchange = options.onchange;
+
+  	this.cache = {}; // we need to be able to use hasOwnProperty, so can't inherit from null
+  	this.cacheMap = create(null);
+
+  	this.deps = {
+  		computed: create(null),
+  		"default": create(null)
+  	};
+  	this.depsMap = {
+  		computed: create(null),
+  		"default": create(null)
+  	};
+
+  	this.patternObservers = [];
+
+  	this.specials = create(null);
+
+  	this.wrapped = create(null);
+  	this.computations = create(null);
+
+  	this.captureGroups = [];
+  	this.unresolvedImplicitDependencies = [];
+
+  	this.changes = [];
+  	this.implicitChanges = {};
+  	this.noCascade = {};
+
+  	this.data = data;
+
+  	// set up explicit mappings
+  	this.mappings = create(null);
+  	for (key in mappings) {
+  		this.map(getKeypath(key), mappings[key]);
+  	}
+
+  	if (data) {
+  		// if data exists locally, but is missing on the parent,
+  		// we transfer ownership to the parent
+  		for (key in data) {
+  			if ((mapping = this.mappings[key]) && mapping.getValue() === undefined) {
+  				mapping.setValue(data[key]);
+  			}
+  		}
+  	}
+
+  	for (key in computed) {
+  		if (mappings && key in mappings) {
+  			fatal("Cannot map to a computed property ('%s')", key);
+  		}
+
+  		this.compute(getKeypath(key), computed[key]);
+  	}
+
+  	this.ready = true;
+  };
+
+  Viewmodel.prototype = {
+  	adapt: prototype_adapt,
+  	applyChanges: applyChanges,
+  	capture: capture,
+  	clearCache: clearCache,
+  	compute: compute,
+  	get: viewmodel_prototype_get,
+  	init: viewmodel_prototype_init,
+  	map: prototype_map,
+  	mark: mark,
+  	merge: merge,
+  	register: register,
+  	release: release,
+  	reset: reset,
+  	set: prototype_set,
+  	smartUpdate: smartUpdate,
+  	teardown: prototype_teardown,
+  	unregister: unregister
+  };
+
+  var viewmodel_Viewmodel = Viewmodel;
+
+  function HookQueue(event) {
+  	this.hook = new hooks_Hook(event);
+  	this.inProcess = {};
+  	this.queue = {};
+  }
+
+  HookQueue.prototype = {
+
+  	constructor: HookQueue,
+
+  	begin: function (ractive) {
+  		this.inProcess[ractive._guid] = true;
+  	},
+
+  	end: function (ractive) {
+
+  		var parent = ractive.parent;
+
+  		// If this is *isn't* a child of a component that's in process,
+  		// it should call methods or fire at this point
+  		if (!parent || !this.inProcess[parent._guid]) {
+  			fire(this, ractive);
+  		}
+  		// elsewise, handoff to parent to fire when ready
+  		else {
+  			getChildQueue(this.queue, parent).push(ractive);
+  		}
+
+  		delete this.inProcess[ractive._guid];
+  	}
+  };
+
+  function getChildQueue(queue, ractive) {
+  	return queue[ractive._guid] || (queue[ractive._guid] = []);
+  }
+
+  function fire(hookQueue, ractive) {
+
+  	var childQueue = getChildQueue(hookQueue.queue, ractive);
+
+  	hookQueue.hook.fire(ractive);
+
+  	// queue is "live" because components can end up being
+  	// added while hooks fire on parents that modify data values.
+  	while (childQueue.length) {
+  		fire(hookQueue, childQueue.shift());
+  	}
+
+  	delete hookQueue.queue[ractive._guid];
+  }
+
+  var hooks_HookQueue = HookQueue;
+
+  var helpers_getComputationSignatures = getComputationSignatures;
+
+  var helpers_getComputationSignatures__pattern = /\$\{([^\}]+)\}/g;
+  function getComputationSignatures(ractive, computed) {
+  	var signatures = {},
+  	    key;
+
+  	for (key in computed) {
+  		signatures[key] = getComputationSignature(ractive, key, computed[key]);
+  	}
+
+  	return signatures;
+  }
+
+  function getComputationSignature(ractive, key, signature) {
+  	var getter, setter;
+
+  	if (typeof signature === "function") {
+  		getter = helpers_getComputationSignatures__bind(signature, ractive);
+  	}
+
+  	if (typeof signature === "string") {
+  		getter = createFunctionFromString(ractive, signature);
+  	}
+
+  	if (typeof signature === "object") {
+  		if (typeof signature.get === "string") {
+  			getter = createFunctionFromString(ractive, signature.get);
+  		} else if (typeof signature.get === "function") {
+  			getter = helpers_getComputationSignatures__bind(signature.get, ractive);
+  		} else {
+  			fatal("`%s` computation must have a `get()` method", key);
+  		}
+
+  		if (typeof signature.set === "function") {
+  			setter = helpers_getComputationSignatures__bind(signature.set, ractive);
+  		}
+  	}
+
+  	return { getter: getter, setter: setter };
+  }
+
+  function createFunctionFromString(ractive, str) {
+  	var functionBody, hasThis, fn;
+
+  	functionBody = "return (" + str.replace(helpers_getComputationSignatures__pattern, function (match, keypath) {
+  		hasThis = true;
+  		return "__ractive.get(\"" + keypath + "\")";
+  	}) + ");";
+
+  	if (hasThis) {
+  		functionBody = "var __ractive = this; " + functionBody;
+  	}
+
+  	fn = new Function(functionBody);
+  	return hasThis ? fn.bind(ractive) : fn;
+  }
+
+  function helpers_getComputationSignatures__bind(fn, context) {
+  	return /this/.test(fn.toString()) ? fn.bind(context) : fn;
+  }
+
+  var constructHook = new hooks_Hook("construct");
+  var configHook = new hooks_Hook("config");
+  var initHook = new hooks_HookQueue("init");
+  var initialise__uid = 0;
+
+  var initialise__registryNames = ["adaptors", "components", "decorators", "easing", "events", "interpolators", "partials", "transitions"];
+
+  var initialise = initialiseRactiveInstance;
+
+  function initialiseRactiveInstance(ractive) {
+  	var userOptions = arguments[1] === undefined ? {} : arguments[1];
+  	var options = arguments[2] === undefined ? {} : arguments[2];
+
+  	var el, viewmodel;
+
+  	if (_Ractive.DEBUG) {
+  		welcome();
+  	}
+
+  	initialiseProperties(ractive, options);
+
+  	// TODO remove this, eventually
+  	defineProperty(ractive, "data", { get: deprecateRactiveData });
+
+  	// TODO don't allow `onconstruct` with `new Ractive()`, there's no need for it
+  	constructHook.fire(ractive, userOptions);
+
+  	// Add registries
+  	initialise__registryNames.forEach(function (name) {
+  		ractive[name] = utils_object__extend(create(ractive.constructor[name] || null), userOptions[name]);
+  	});
+
+  	// Create a viewmodel
+  	viewmodel = new viewmodel_Viewmodel({
+  		adapt: getAdaptors(ractive, ractive.adapt, userOptions),
+  		data: custom_data.init(ractive.constructor, ractive, userOptions),
+  		computed: helpers_getComputationSignatures(ractive, utils_object__extend(create(ractive.constructor.prototype.computed), userOptions.computed)),
+  		mappings: options.mappings,
+  		ractive: ractive,
+  		onchange: function () {
+  			return global_runloop.addRactive(ractive);
+  		}
+  	});
+
+  	ractive.viewmodel = viewmodel;
+
+  	// This can't happen earlier, because computed properties may call `ractive.get()`, etc
+  	viewmodel.init();
+
+  	// init config from Parent and options
+  	config_config.init(ractive.constructor, ractive, userOptions);
+
+  	configHook.fire(ractive);
+  	initHook.begin(ractive);
+
+  	// // If this is a component with a function `data` property, call the function
+  	// // with `ractive` as context (unless the child was also a function)
+  	// if ( typeof ractive.constructor.prototype.data === 'function' && typeof userOptions.data !== 'function' ) {
+  	// 	viewmodel.reset( ractive.constructor.prototype.data.call( ractive ) || fatal( '`data` functions must return a data object' ) );
+  	// }
+
+  	// Render virtual DOM
+  	if (ractive.template) {
+  		var cssIds = undefined;
+
+  		if (options.cssIds || ractive.cssId) {
+  			cssIds = options.cssIds ? options.cssIds.slice() : [];
+
+  			if (ractive.cssId) {
+  				cssIds.push(ractive.cssId);
+  			}
+  		}
+
+  		ractive.fragment = new virtualdom_Fragment({
+  			template: ractive.template,
+  			root: ractive,
+  			owner: ractive, // saves doing `if ( this.parent ) { /*...*/ }` later on
+  			cssIds: cssIds
+  		});
+  	}
+
+  	initHook.end(ractive);
+
+  	// render automatically ( if `el` is specified )
+  	if (el = getElement(ractive.el)) {
+  		var promise = ractive.render(el, ractive.append);
+
+  		if (_Ractive.DEBUG_PROMISES) {
+  			promise["catch"](function (err) {
+  				warnOnceIfDebug("Promise debugging is enabled, to help solve errors that happen asynchronously. Some browsers will log unhandled promise rejections, in which case you can safely disable promise debugging:\n  Ractive.DEBUG_PROMISES = false;");
+  				warnIfDebug("An error happened during rendering", { ractive: ractive });
+  				err.stack && logIfDebug(err.stack);
+
+  				throw err;
+  			});
+  		}
+  	}
+  }
+
+  function getAdaptors(ractive, protoAdapt, userOptions) {
+  	var adapt, magic, modifyArrays;
+
+  	protoAdapt = protoAdapt.map(lookup);
+  	adapt = ensureArray(userOptions.adapt).map(lookup);
+
+  	adapt = initialise__combine(protoAdapt, adapt);
+
+  	magic = "magic" in userOptions ? userOptions.magic : ractive.magic;
+  	modifyArrays = "modifyArrays" in userOptions ? userOptions.modifyArrays : ractive.modifyArrays;
+
+  	if (magic) {
+  		if (!environment__magic) {
+  			throw new Error("Getters and setters (magic mode) are not supported in this browser");
+  		}
+
+  		if (modifyArrays) {
+  			adapt.push(magicArray);
+  		}
+
+  		adapt.push(adaptors_magic);
+  	}
+
+  	if (modifyArrays) {
+  		adapt.push(array_index);
+  	}
+
+  	return adapt;
+
+  	function lookup(adaptor) {
+  		if (typeof adaptor === "string") {
+  			adaptor = findInViewHierarchy("adaptors", ractive, adaptor);
+
+  			if (!adaptor) {
+  				fatal(missingPlugin(adaptor, "adaptor"));
+  			}
+  		}
+
+  		return adaptor;
+  	}
+  }
+
+  function initialise__combine(a, b) {
+  	var c = a.slice(),
+  	    i = b.length;
+
+  	while (i--) {
+  		if (! ~c.indexOf(b[i])) {
+  			c.push(b[i]);
+  		}
+  	}
+
+  	return c;
+  }
+
+  function initialiseProperties(ractive, options) {
+  	// Generate a unique identifier, for places where you'd use a weak map if it
+  	// existed
+  	ractive._guid = "r-" + initialise__uid++;
+
+  	// events
+  	ractive._subs = create(null);
+
+  	// storage for item configuration from instantiation to reset,
+  	// like dynamic functions or original values
+  	ractive._config = {};
+
+  	// two-way bindings
+  	ractive._twowayBindings = create(null);
+
+  	// animations (so we can stop any in progress at teardown)
+  	ractive._animations = [];
+
+  	// nodes registry
+  	ractive.nodes = {};
+
+  	// live queries
+  	ractive._liveQueries = [];
+  	ractive._liveComponentQueries = [];
+
+  	// bound data functions
+  	ractive._boundFunctions = [];
+
+  	// observers
+  	ractive._observers = [];
+
+  	// properties specific to inline components
+  	if (options.component) {
+  		ractive.parent = options.parent;
+  		ractive.container = options.container || null;
+  		ractive.root = ractive.parent.root;
+
+  		ractive.component = options.component;
+  		options.component.instance = ractive;
+
+  		// for hackability, this could be an open option
+  		// for any ractive instance, but for now, just
+  		// for components and just for ractive...
+  		ractive._inlinePartials = options.inlinePartials;
+  	} else {
+  		ractive.root = ractive;
+  		ractive.parent = ractive.container = null;
+  	}
+  }
+
+  function deprecateRactiveData() {
+  	throw new Error("Using `ractive.data` is no longer supported - you must use the `ractive.get()` API instead");
+  }
+
+  function ComplexParameter(component, template, callback) {
+  	this.parentFragment = component.parentFragment;
+  	this.callback = callback;
+
+  	this.fragment = new virtualdom_Fragment({
+  		template: template,
+  		root: component.root,
+  		owner: this
+  	});
+
+  	this.update();
+  }
+
+  var initialise_ComplexParameter = ComplexParameter;
+
+  ComplexParameter.prototype = {
+  	bubble: function () {
+  		if (!this.dirty) {
+  			this.dirty = true;
+  			global_runloop.addView(this);
+  		}
+  	},
+
+  	update: function () {
+  		this.callback(this.fragment.getValue());
+  		this.dirty = false;
+  	},
+
+  	rebind: function (oldKeypath, newKeypath) {
+  		this.fragment.rebind(oldKeypath, newKeypath);
+  	},
+
+  	unbind: function () {
+  		this.fragment.unbind();
+  	}
+  };
+
+  var createInstance = function (component, Component, attributes, yieldTemplate, partials) {
+  	var instance,
+  	    parentFragment,
+  	    ractive,
+  	    fragment,
+  	    container,
+  	    inlinePartials = {},
+  	    data = {},
+  	    mappings = {},
+  	    ready,
+  	    resolvers = [];
+
+  	parentFragment = component.parentFragment;
+  	ractive = component.root;
+
+  	partials = partials || {};
+  	utils_object__extend(inlinePartials, partials);
+
+  	// Make contents available as a {{>content}} partial
+  	partials.content = yieldTemplate || [];
+
+  	// set a default partial for yields with no name
+  	inlinePartials[""] = partials.content;
+
+  	if (Component.defaults.el) {
+  		warnIfDebug("The <%s/> component has a default `el` property; it has been disregarded", component.name);
+  	}
+
+  	// find container
+  	fragment = parentFragment;
+  	while (fragment) {
+  		if (fragment.owner.type === YIELDER) {
+  			container = fragment.owner.container;
+  			break;
+  		}
+
+  		fragment = fragment.parent;
+  	}
+
+  	// each attribute represents either a) data or b) a mapping
+  	if (attributes) {
+  		Object.keys(attributes).forEach(function (key) {
+  			var attribute = attributes[key],
+  			    parsed,
+  			    resolver;
+
+  			if (typeof attribute === "string") {
+  				// it's static data
+  				parsed = parseJSON(attribute);
+  				data[key] = parsed ? parsed.value : attribute;
+  			} else if (attribute === 0) {
+  				// it had no '=', so we'll call it true
+  				data[key] = true;
+  			} else if (isArray(attribute)) {
+  				// this represents dynamic data
+  				if (isSingleInterpolator(attribute)) {
+  					mappings[key] = {
+  						origin: component.root.viewmodel,
+  						keypath: undefined
+  					};
+
+  					resolver = createResolver(component, attribute[0], function (keypath) {
+  						if (keypath.isSpecial) {
+  							if (ready) {
+  								instance.set(key, keypath.value); // TODO use viewmodel?
+  							} else {
+  								data[key] = keypath.value;
+
+  								// TODO errr.... would be better if we didn't have to do this
+  								delete mappings[key];
+  							}
+  						} else {
+  							if (ready) {
+  								instance.viewmodel.mappings[key].resolve(keypath);
+  							} else {
+  								// resolved immediately
+  								mappings[key].keypath = keypath;
+  							}
+  						}
+  					});
+  				} else {
+  					resolver = new initialise_ComplexParameter(component, attribute, function (value) {
+  						if (ready) {
+  							instance.set(key, value); // TODO use viewmodel?
+  						} else {
+  							data[key] = value;
+  						}
+  					});
+  				}
+
+  				resolvers.push(resolver);
+  			} else {
+  				throw new Error("erm wut");
+  			}
+  		});
+  	}
+
+  	instance = create(Component.prototype);
+
+  	initialise(instance, {
+  		el: null,
+  		append: true,
+  		data: data,
+  		partials: partials,
+  		magic: ractive.magic || Component.defaults.magic,
+  		modifyArrays: ractive.modifyArrays,
+  		// need to inherit runtime parent adaptors
+  		adapt: ractive.adapt
+  	}, {
+  		parent: ractive,
+  		component: component,
+  		container: container,
+  		mappings: mappings,
+  		inlinePartials: inlinePartials,
+  		cssIds: parentFragment.cssIds
+  	});
+
+  	ready = true;
+  	component.resolvers = resolvers;
+
+  	return instance;
+  };
+
+  function createResolver(component, template, callback) {
+  	var resolver;
+
+  	if (template.r) {
+  		resolver = Resolvers_createReferenceResolver(component, template.r, callback);
+  	} else if (template.x) {
+  		resolver = new Resolvers_ExpressionResolver(component, component.parentFragment, template.x, callback);
+  	} else if (template.rx) {
+  		resolver = new ReferenceExpressionResolver_ReferenceExpressionResolver(component, template.rx, callback);
+  	}
+
+  	return resolver;
+  }
+
+  function isSingleInterpolator(template) {
+  	return template.length === 1 && template[0].t === INTERPOLATOR;
+  }
+
+  // TODO how should event arguments be handled? e.g.
+  // <widget on-foo='bar:1,2,3'/>
+  // The event 'bar' will be fired on the parent instance
+  // when 'foo' fires on the child, but the 1,2,3 arguments
+  // will be lost
+
+  var initialise_propagateEvents = propagateEvents;
+
+  function propagateEvents(component, eventsDescriptor) {
+  	var eventName;
+
+  	for (eventName in eventsDescriptor) {
+  		if (eventsDescriptor.hasOwnProperty(eventName)) {
+  			propagateEvent(component.instance, component.root, eventName, eventsDescriptor[eventName]);
+  		}
+  	}
+  }
+
+  function propagateEvent(childInstance, parentInstance, eventName, proxyEventName) {
+  	if (typeof proxyEventName !== "string") {
+  		fatal("Components currently only support simple events - you cannot include arguments. Sorry!");
+  	}
+
+  	childInstance.on(eventName, function () {
+  		var event, args;
+
+  		// semi-weak test, but what else? tag the event obj ._isEvent ?
+  		if (arguments.length && arguments[0] && arguments[0].node) {
+  			event = Array.prototype.shift.call(arguments);
+  		}
+
+  		args = Array.prototype.slice.call(arguments);
+
+  		shared_fireEvent(parentInstance, proxyEventName, { event: event, args: args });
+
+  		// cancel bubbling
+  		return false;
+  	});
+  }
+
+  var initialise_updateLiveQueries = function (component) {
+  	var ancestor, query;
+
+  	// If there's a live query for this component type, add it
+  	ancestor = component.root;
+  	while (ancestor) {
+  		if (query = ancestor._liveComponentQueries["_" + component.name]) {
+  			query.push(component.instance);
+  		}
+
+  		ancestor = ancestor.parent;
+  	}
+  };
+
+  var Component_prototype_init = Component$init;
+  function Component$init(options, Component) {
+  	var parentFragment, root;
+
+  	if (!Component) {
+  		throw new Error("Component \"" + this.name + "\" not found");
+  	}
+
+  	parentFragment = this.parentFragment = options.parentFragment;
+  	root = parentFragment.root;
+
+  	this.root = root;
+  	this.type = COMPONENT;
+  	this.name = options.template.e;
+  	this.index = options.index;
+  	this.indexRefBindings = {};
+  	this.yielders = {};
+  	this.resolvers = [];
+
+  	createInstance(this, Component, options.template.a, options.template.f, options.template.p);
+  	initialise_propagateEvents(this, options.template.v);
+
+  	// intro, outro and decorator directives have no effect
+  	if (options.template.t0 || options.template.t1 || options.template.t2 || options.template.o) {
+  		warnIfDebug("The \"intro\", \"outro\" and \"decorator\" directives have no effect on components", { ractive: this.instance });
+  	}
+
+  	initialise_updateLiveQueries(this);
+  }
+
+  var Component_prototype_rebind = Component$rebind;
+
+  function Component$rebind(oldKeypath, newKeypath) {
+  	var query;
+
+  	this.resolvers.forEach(rebind);
+
+  	for (var k in this.yielders) {
+  		if (this.yielders[k][0]) {
+  			rebind(this.yielders[k][0]);
+  		}
+  	}
+
+  	if (query = this.root._liveComponentQueries["_" + this.name]) {
+  		query._makeDirty();
+  	}
+
+  	function rebind(x) {
+  		x.rebind(oldKeypath, newKeypath);
+  	}
+  }
+
+  var Component_prototype_render = Component$render;
+
+  function Component$render() {
+  	var instance = this.instance;
+
+  	instance.render(this.parentFragment.getNode());
+
+  	this.rendered = true;
+  	return instance.fragment.detach();
+  }
+
+  var Component_prototype_toString = Component$toString;
+
+  function Component$toString() {
+  	return this.instance.fragment.toString();
+  }
+
+  var Component_prototype_unbind = Component$unbind;
+
+  var Component_prototype_unbind__teardownHook = new hooks_Hook("teardown");
+  function Component$unbind() {
+  	var instance = this.instance;
+
+  	this.resolvers.forEach(methodCallers__unbind);
+
+  	removeFromLiveComponentQueries(this);
+
+  	instance._observers.forEach(cancel);
+
+  	// teardown the instance
+  	instance.fragment.unbind();
+  	instance.viewmodel.teardown();
+
+  	if (instance.fragment.rendered && instance.el.__ractive_instances__) {
+  		removeFromArray(instance.el.__ractive_instances__, instance);
+  	}
+
+  	Component_prototype_unbind__teardownHook.fire(instance);
+  }
+
+  function removeFromLiveComponentQueries(component) {
+  	var instance, query;
+
+  	instance = component.root;
+
+  	do {
+  		if (query = instance._liveComponentQueries["_" + component.name]) {
+  			query._remove(component);
+  		}
+  	} while (instance = instance.parent);
+  }
+
+  var Component_prototype_unrender = Component$unrender;
+
+  function Component$unrender(shouldDestroy) {
+  	this.shouldDestroy = shouldDestroy;
+  	this.instance.unrender();
+  }
+
+  var Component = function (options, Constructor) {
+  	this.init(options, Constructor);
+  };
+
+  Component.prototype = {
+  	detach: Component_prototype_detach,
+  	find: Component_prototype_find,
+  	findAll: Component_prototype_findAll,
+  	findAllComponents: Component_prototype_findAllComponents,
+  	findComponent: Component_prototype_findComponent,
+  	findNextNode: Component_prototype_findNextNode,
+  	firstNode: Component_prototype_firstNode,
+  	init: Component_prototype_init,
+  	rebind: Component_prototype_rebind,
+  	render: Component_prototype_render,
+  	toString: Component_prototype_toString,
+  	unbind: Component_prototype_unbind,
+  	unrender: Component_prototype_unrender
+  };
+
+  var _Component = Component;
+
+  var Comment = function (options) {
+  	this.type = COMMENT;
+  	this.value = options.template.c;
+  };
+
+  Comment.prototype = {
+  	detach: shared_detach,
+
+  	firstNode: function () {
+  		return this.node;
+  	},
+
+  	render: function () {
+  		if (!this.node) {
+  			this.node = document.createComment(this.value);
+  		}
+
+  		return this.node;
+  	},
+
+  	toString: function () {
+  		return "<!--" + this.value + "-->";
+  	},
+
+  	unrender: function (shouldDestroy) {
+  		if (shouldDestroy) {
+  			this.node.parentNode.removeChild(this.node);
+  		}
+  	}
+  };
+
+  var items_Comment = Comment;
+
+  var Yielder = function (options) {
+  	var container, component;
+
+  	this.type = YIELDER;
+
+  	this.container = container = options.parentFragment.root;
+  	this.component = component = container.component;
+
+  	this.container = container;
+  	this.containerFragment = options.parentFragment;
+  	this.parentFragment = component.parentFragment;
+
+  	var name = this.name = options.template.n || "";
+
+  	var template = container._inlinePartials[name];
+
+  	if (!template) {
+  		warnIfDebug("Could not find template for partial \"" + name + "\"", { ractive: options.root });
+  		template = [];
+  	}
+
+  	this.fragment = new virtualdom_Fragment({
+  		owner: this,
+  		root: container.parent,
+  		template: template,
+  		pElement: this.containerFragment.pElement
+  	});
+
+  	// even though only one yielder is allowed, we need to have an array of them
+  	// as it's possible to cause a yielder to be created before the last one
+  	// was destroyed in the same turn of the runloop
+  	if (!isArray(component.yielders[name])) {
+  		component.yielders[name] = [this];
+  	} else {
+  		component.yielders[name].push(this);
+  	}
+
+  	global_runloop.scheduleTask(function () {
+  		if (component.yielders[name].length > 1) {
+  			throw new Error("A component template can only have one {{yield" + (name ? " " + name : "") + "}} declaration at a time");
+  		}
+  	});
+  };
+
+  Yielder.prototype = {
+  	detach: function () {
+  		return this.fragment.detach();
+  	},
+
+  	find: function (selector) {
+  		return this.fragment.find(selector);
+  	},
+
+  	findAll: function (selector, query) {
+  		return this.fragment.findAll(selector, query);
+  	},
+
+  	findComponent: function (selector) {
+  		return this.fragment.findComponent(selector);
+  	},
+
+  	findAllComponents: function (selector, query) {
+  		return this.fragment.findAllComponents(selector, query);
+  	},
+
+  	findNextNode: function () {
+  		return this.containerFragment.findNextNode(this);
+  	},
+
+  	firstNode: function () {
+  		return this.fragment.firstNode();
+  	},
+
+  	getValue: function (options) {
+  		return this.fragment.getValue(options);
+  	},
+
+  	render: function () {
+  		return this.fragment.render();
+  	},
+
+  	unbind: function () {
+  		this.fragment.unbind();
+  	},
+
+  	unrender: function (shouldDestroy) {
+  		this.fragment.unrender(shouldDestroy);
+  		removeFromArray(this.component.yielders[this.name], this);
+  	},
+
+  	rebind: function (oldKeypath, newKeypath) {
+  		this.fragment.rebind(oldKeypath, newKeypath);
+  	},
+
+  	toString: function () {
+  		return this.fragment.toString();
+  	}
+  };
+
+  var items_Yielder = Yielder;
+
+  var Doctype = function (options) {
+  	this.declaration = options.template.a;
+  };
+
+  Doctype.prototype = {
+  	init: noop,
+  	render: noop,
+  	unrender: noop,
+  	teardown: noop,
+  	toString: function () {
+  		return "<!DOCTYPE" + this.declaration + ">";
+  	}
+  };
+
+  var items_Doctype = Doctype;
+
+  var Fragment_prototype_init = Fragment$init;
+
+  function Fragment$init(options) {
+  	var _this = this;
+
+  	this.owner = options.owner; // The item that owns this fragment - an element, section, partial, or attribute
+  	this.parent = this.owner.parentFragment;
+
+  	// inherited properties
+  	this.root = options.root;
+  	this.pElement = options.pElement;
+  	this.context = options.context;
+  	this.index = options.index;
+  	this.key = options.key;
+  	this.registeredIndexRefs = [];
+
+  	// encapsulated styles should be inherited until they get applied by an element
+  	this.cssIds = "cssIds" in options ? options.cssIds : this.parent ? this.parent.cssIds : null;
+
+  	this.items = options.template.map(function (template, i) {
+  		return createItem({
+  			parentFragment: _this,
+  			pElement: options.pElement,
+  			template: template,
+  			index: i
+  		});
+  	});
+
+  	this.value = this.argsList = null;
+  	this.dirtyArgs = this.dirtyValue = true;
+
+  	this.bound = true;
+  }
+
+  function createItem(options) {
+  	if (typeof options.template === "string") {
+  		return new items_Text(options);
+  	}
+
+  	switch (options.template.t) {
+  		case YIELDER:
+  			return new items_Yielder(options);
+  		case INTERPOLATOR:
+  			return new items_Interpolator(options);
+  		case SECTION:
+  			return new _Section(options);
+  		case TRIPLE:
+  			return new _Triple(options);
+  		case ELEMENT:
+  			var constructor = undefined;
+  			if (constructor = Component_getComponent(options.parentFragment.root, options.template.e)) {
+  				return new _Component(options, constructor);
+  			}
+  			return new _Element(options);
+  		case PARTIAL:
+  			return new _Partial(options);
+  		case COMMENT:
+  			return new items_Comment(options);
+  		case DOCTYPE:
+  			return new items_Doctype(options);
+
+  		default:
+  			throw new Error("Something very strange happened. Please file an issue at https://github.com/ractivejs/ractive/issues. Thanks!");
+  	}
+  }
+
+  var Fragment_prototype_rebind = Fragment$rebind;
+  function Fragment$rebind(oldKeypath, newKeypath) {
+
+  	// assign new context keypath if needed
+  	if (!this.owner || this.owner.hasContext) {
+  		assignNewKeypath(this, "context", oldKeypath, newKeypath);
+  	}
+
+  	this.items.forEach(function (item) {
+  		if (item.rebind) {
+  			item.rebind(oldKeypath, newKeypath);
+  		}
+  	});
+  }
+
+  var Fragment_prototype_render = Fragment$render;
+
+  function Fragment$render() {
+  	var result;
+
+  	if (this.items.length === 1) {
+  		result = this.items[0].render();
+  	} else {
+  		result = document.createDocumentFragment();
+
+  		this.items.forEach(function (item) {
+  			result.appendChild(item.render());
+  		});
+  	}
+
+  	this.rendered = true;
+  	return result;
+  }
+
+  var Fragment_prototype_toString = Fragment$toString;
+
+  function Fragment$toString(escape) {
+  	if (!this.items) {
+  		return "";
+  	}
+
+  	return this.items.map(escape ? toEscapedString : Fragment_prototype_toString__toString).join("");
+  }
+
+  function Fragment_prototype_toString__toString(item) {
+  	return item.toString();
+  }
+
+  function toEscapedString(item) {
+  	return item.toString(true);
+  }
+
+  var Fragment_prototype_unbind = Fragment$unbind;
+
+  function Fragment$unbind() {
+  	if (!this.bound) {
+  		return;
+  	}
+
+  	this.items.forEach(unbindItem);
+  	this.bound = false;
+  }
+
+  function unbindItem(item) {
+  	if (item.unbind) {
+  		item.unbind();
+  	}
+  }
+
+  var Fragment_prototype_unrender = Fragment$unrender;
+
+  function Fragment$unrender(shouldDestroy) {
+  	if (!this.rendered) {
+  		throw new Error("Attempted to unrender a fragment that was not rendered");
+  	}
+
+  	this.items.forEach(function (i) {
+  		return i.unrender(shouldDestroy);
+  	});
+  	this.rendered = false;
+  }
+
+  var Fragment = function (options) {
+  	this.init(options);
+  };
+
+  Fragment.prototype = {
+  	bubble: prototype_bubble,
+  	detach: Fragment_prototype_detach,
+  	find: Fragment_prototype_find,
+  	findAll: Fragment_prototype_findAll,
+  	findAllComponents: Fragment_prototype_findAllComponents,
+  	findComponent: Fragment_prototype_findComponent,
+  	findNextNode: prototype_findNextNode,
+  	firstNode: prototype_firstNode,
+  	getArgsList: getArgsList,
+  	getNode: getNode,
+  	getValue: prototype_getValue,
+  	init: Fragment_prototype_init,
+  	rebind: Fragment_prototype_rebind,
+  	registerIndexRef: function (idx) {
+  		var idxs = this.registeredIndexRefs;
+  		if (idxs.indexOf(idx) === -1) {
+  			idxs.push(idx);
+  		}
+  	},
+  	render: Fragment_prototype_render,
+  	toString: Fragment_prototype_toString,
+  	unbind: Fragment_prototype_unbind,
+  	unregisterIndexRef: function (idx) {
+  		var idxs = this.registeredIndexRefs;
+  		idxs.splice(idxs.indexOf(idx), 1);
+  	},
+  	unrender: Fragment_prototype_unrender
+  };
+
+  var virtualdom_Fragment = Fragment;
+
+  var prototype_reset = Ractive$reset;
+  var shouldRerender = ["template", "partials", "components", "decorators", "events"],
+      resetHook = new hooks_Hook("reset");
+  function Ractive$reset(data) {
+  	var promise, wrapper, changes, i, rerender;
+
+  	data = data || {};
+
+  	if (typeof data !== "object") {
+  		throw new Error("The reset method takes either no arguments, or an object containing new data");
+  	}
+
+  	// If the root object is wrapped, try and use the wrapper's reset value
+  	if ((wrapper = this.viewmodel.wrapped[""]) && wrapper.reset) {
+  		if (wrapper.reset(data) === false) {
+  			// reset was rejected, we need to replace the object
+  			this.viewmodel.reset(data);
+  		}
+  	} else {
+  		this.viewmodel.reset(data);
+  	}
+
+  	// reset config items and track if need to rerender
+  	changes = config_config.reset(this);
+
+  	i = changes.length;
+  	while (i--) {
+  		if (shouldRerender.indexOf(changes[i]) > -1) {
+  			rerender = true;
+  			break;
+  		}
+  	}
+
+  	if (rerender) {
+  		var component = undefined;
+
+  		this.viewmodel.mark(rootKeypath);
+
+  		// Is this is a component, we need to set the `shouldDestroy`
+  		// flag, otherwise it will assume by default that a parent node
+  		// will be detached, and therefore it doesn't need to bother
+  		// detaching its own nodes
+  		if (component = this.component) {
+  			component.shouldDestroy = true;
+  		}
+
+  		this.unrender();
+
+  		if (component) {
+  			component.shouldDestroy = false;
+  		}
+
+  		// If the template changed, we need to destroy the parallel DOM
+  		// TODO if we're here, presumably it did?
+  		if (this.fragment.template !== this.template) {
+  			this.fragment.unbind();
+
+  			this.fragment = new virtualdom_Fragment({
+  				template: this.template,
+  				root: this,
+  				owner: this
+  			});
+  		}
+
+  		promise = this.render(this.el, this.anchor);
+  	} else {
+  		promise = global_runloop.start(this, true);
+  		this.viewmodel.mark(rootKeypath);
+  		global_runloop.end();
+  	}
+
+  	resetHook.fire(this, data);
+
+  	return promise;
+  }
+
+  var resetPartial = function (name, partial) {
+  	var promise,
+  	    collection = [];
+
+  	function collect(source, dest, ractive) {
+  		// if this is a component and it has its own partial, bail
+  		if (ractive && ractive.partials[name]) return;
+
+  		source.forEach(function (item) {
+  			// queue to rerender if the item is a partial and the current name matches
+  			if (item.type === PARTIAL && item.getPartialName() === name) {
+  				dest.push(item);
+  			}
+
+  			// if it has a fragment, process its items
+  			if (item.fragment) {
+  				collect(item.fragment.items, dest, ractive);
+  			}
+
+  			// or if it has fragments
+  			if (isArray(item.fragments)) {
+  				collect(item.fragments, dest, ractive);
+  			}
+
+  			// or if it is itself a fragment, process its items
+  			else if (isArray(item.items)) {
+  				collect(item.items, dest, ractive);
+  			}
+
+  			// or if it is a component, step in and process its items
+  			else if (item.type === COMPONENT && item.instance) {
+  				collect(item.instance.fragment.items, dest, item.instance);
+  			}
+
+  			// if the item is an element, process its attributes too
+  			if (item.type === ELEMENT) {
+  				if (isArray(item.attributes)) {
+  					collect(item.attributes, dest, ractive);
+  				}
+
+  				if (isArray(item.conditionalAttributes)) {
+  					collect(item.conditionalAttributes, dest, ractive);
+  				}
+  			}
+  		});
+  	}
+
+  	collect(this.fragment.items, collection);
+  	this.partials[name] = partial;
+
+  	promise = global_runloop.start(this, true);
+
+  	collection.forEach(function (item) {
+  		item.value = undefined;
+  		item.setValue(name);
+  	});
+
+  	global_runloop.end();
+
+  	return promise;
+  };
+
+  // TODO should resetTemplate be asynchronous? i.e. should it be a case
+  // of outro, update template, intro? I reckon probably not, since that
+  // could be achieved with unrender-resetTemplate-render. Also, it should
+  // conceptually be similar to resetPartial, which couldn't be async
+
+  var resetTemplate = Ractive$resetTemplate;
+  function Ractive$resetTemplate(template) {
+  	var transitionsEnabled, component;
+
+  	template_template.init(null, this, { template: template });
+
+  	transitionsEnabled = this.transitionsEnabled;
+  	this.transitionsEnabled = false;
+
+  	// Is this is a component, we need to set the `shouldDestroy`
+  	// flag, otherwise it will assume by default that a parent node
+  	// will be detached, and therefore it doesn't need to bother
+  	// detaching its own nodes
+  	if (component = this.component) {
+  		component.shouldDestroy = true;
+  	}
+
+  	this.unrender();
+
+  	if (component) {
+  		component.shouldDestroy = false;
+  	}
+
+  	// remove existing fragment and create new one
+  	this.fragment.unbind();
+  	this.fragment = new virtualdom_Fragment({
+  		template: this.template,
+  		root: this,
+  		owner: this
+  	});
+
+  	this.render(this.el, this.anchor);
+
+  	this.transitionsEnabled = transitionsEnabled;
+  }
+
+  var reverse = makeArrayMethod("reverse");
+
+  var Ractive_prototype_set = Ractive$set;
+
+  function Ractive$set(keypath, value) {
+  	var map, promise;
+
+  	promise = global_runloop.start(this, true);
+
+  	// Set multiple keypaths in one go
+  	if (isObject(keypath)) {
+  		map = keypath;
+
+  		for (keypath in map) {
+  			if (map.hasOwnProperty(keypath)) {
+  				value = map[keypath];
+  				set(this, keypath, value);
+  			}
+  		}
+  	}
+
+  	// Set a single keypath
+  	else {
+  		set(this, keypath, value);
+  	}
+
+  	global_runloop.end();
+
+  	return promise;
+  }
+
+  function set(ractive, keypath, value) {
+  	keypath = getKeypath(normalise(keypath));
+
+  	if (keypath.isPattern) {
+  		getMatchingKeypaths(ractive, keypath).forEach(function (keypath) {
+  			ractive.viewmodel.set(keypath, value);
+  		});
+  	} else {
+  		ractive.viewmodel.set(keypath, value);
+  	}
+  }
+
+  var shift = makeArrayMethod("shift");
+
+  var prototype_sort = makeArrayMethod("sort");
+
+  var splice = makeArrayMethod("splice");
+
+  var subtract = Ractive$subtract;
+  function Ractive$subtract(keypath, d) {
+  	return shared_add(this, keypath, d === undefined ? -1 : -d);
+  }
+
+  // Teardown. This goes through the root fragment and all its children, removing observers
+  // and generally cleaning up after itself
+
+  var Ractive_prototype_teardown = Ractive$teardown;
+
+  var Ractive_prototype_teardown__teardownHook = new hooks_Hook("teardown");
+  function Ractive$teardown() {
+  	var promise;
+
+  	this.fragment.unbind();
+  	this.viewmodel.teardown();
+
+  	this._observers.forEach(cancel);
+
+  	if (this.fragment.rendered && this.el.__ractive_instances__) {
+  		removeFromArray(this.el.__ractive_instances__, this);
+  	}
+
+  	this.shouldDestroy = true;
+  	promise = this.fragment.rendered ? this.unrender() : utils_Promise.resolve();
+
+  	Ractive_prototype_teardown__teardownHook.fire(this);
+
+  	this._boundFunctions.forEach(deleteFunctionCopy);
+
+  	return promise;
+  }
+
+  function deleteFunctionCopy(bound) {
+  	delete bound.fn[bound.prop];
+  }
+
+  var toggle = Ractive$toggle;
+  function Ractive$toggle(keypath) {
+  	var _this = this;
+
+  	if (typeof keypath !== "string") {
+  		throw new TypeError(badArguments);
+  	}
+
+  	var changes = undefined;
+
+  	if (/\*/.test(keypath)) {
+  		changes = {};
+
+  		getMatchingKeypaths(this, getKeypath(normalise(keypath))).forEach(function (keypath) {
+  			changes[keypath.str] = !_this.viewmodel.get(keypath);
+  		});
+
+  		return this.set(changes);
+  	}
+
+  	return this.set(keypath, !this.get(keypath));
+  }
+
+  var toHTML = Ractive$toHTML;
+
+  function Ractive$toHTML() {
+  	return this.fragment.toString(true);
+  }
+
+  var Ractive_prototype_unrender = Ractive$unrender;
+  var unrenderHook = new hooks_Hook("unrender");
+  function Ractive$unrender() {
+  	var promise, shouldDestroy;
+
+  	if (!this.fragment.rendered) {
+  		warnIfDebug("ractive.unrender() was called on a Ractive instance that was not rendered");
+  		return utils_Promise.resolve();
+  	}
+
+  	promise = global_runloop.start(this, true);
+
+  	// If this is a component, and the component isn't marked for destruction,
+  	// don't detach nodes from the DOM unnecessarily
+  	shouldDestroy = !this.component || this.component.shouldDestroy || this.shouldDestroy;
+
+  	// Cancel any animations in progress
+  	while (this._animations[0]) {
+  		this._animations[0].stop(); // it will remove itself from the index
+  	}
+
+  	this.fragment.unrender(shouldDestroy);
+
+  	removeFromArray(this.el.__ractive_instances__, this);
+
+  	unrenderHook.fire(this);
+
+  	global_runloop.end();
+  	return promise;
+  }
+
+  var unshift = makeArrayMethod("unshift");
+
+  var Ractive_prototype_update = Ractive$update;
+  var updateHook = new hooks_Hook("update");
+  function Ractive$update(keypath) {
+  	var promise;
+
+  	keypath = getKeypath(keypath) || rootKeypath;
+
+  	promise = global_runloop.start(this, true);
+  	this.viewmodel.mark(keypath);
+  	global_runloop.end();
+
+  	updateHook.fire(this, keypath);
+
+  	return promise;
+  }
+
+  var prototype_updateModel = Ractive$updateModel;
+
+  function Ractive$updateModel(keypath, cascade) {
+  	var values, key, bindings;
+
+  	if (typeof keypath === "string" && !cascade) {
+  		bindings = this._twowayBindings[keypath];
+  	} else {
+  		bindings = [];
+
+  		for (key in this._twowayBindings) {
+  			if (!keypath || getKeypath(key).equalsOrStartsWith(keypath)) {
+  				// TODO is this right?
+  				bindings.push.apply(bindings, this._twowayBindings[key]);
+  			}
+  		}
+  	}
+
+  	values = consolidate(this, bindings);
+  	return this.set(values);
+  }
+
+  function consolidate(ractive, bindings) {
+  	var values = {},
+  	    checkboxGroups = [];
+
+  	bindings.forEach(function (b) {
+  		var oldValue, newValue;
+
+  		// special case - radio name bindings
+  		if (b.radioName && !b.element.node.checked) {
+  			return;
+  		}
+
+  		// special case - checkbox name bindings come in groups, so
+  		// we want to get the value once at most
+  		if (b.checkboxName) {
+  			if (!checkboxGroups[b.keypath.str] && !b.changed()) {
+  				checkboxGroups.push(b.keypath);
+  				checkboxGroups[b.keypath.str] = b;
+  			}
+
+  			return;
+  		}
+
+  		oldValue = b.attribute.value;
+  		newValue = b.getValue();
+
+  		if (arrayContentsMatch(oldValue, newValue)) {
+  			return;
+  		}
+
+  		if (!isEqual(oldValue, newValue)) {
+  			values[b.keypath.str] = newValue;
+  		}
+  	});
+
+  	// Handle groups of `<input type='checkbox' name='{{foo}}' ...>`
+  	if (checkboxGroups.length) {
+  		checkboxGroups.forEach(function (keypath) {
+  			var binding, oldValue, newValue;
+
+  			binding = checkboxGroups[keypath.str]; // one to represent the entire group
+  			oldValue = binding.attribute.value;
+  			newValue = binding.getValue();
+
+  			if (!arrayContentsMatch(oldValue, newValue)) {
+  				values[keypath.str] = newValue;
+  			}
+  		});
+  	}
+
+  	return values;
+  }
+
+  var prototype = {
+  	add: prototype_add,
+  	animate: prototype_animate,
+  	detach: prototype_detach,
+  	find: prototype_find,
+  	findAll: prototype_findAll,
+  	findAllComponents: prototype_findAllComponents,
+  	findComponent: prototype_findComponent,
+  	findContainer: findContainer,
+  	findParent: findParent,
+  	fire: prototype_fire,
+  	get: prototype_get,
+  	insert: insert,
+  	merge: prototype_merge,
+  	observe: observe,
+  	observeOnce: observeOnce,
+  	off: off,
+  	on: on,
+  	once: once,
+  	pop: pop,
+  	push: push,
+  	render: prototype_render,
+  	reset: prototype_reset,
+  	resetPartial: resetPartial,
+  	resetTemplate: resetTemplate,
+  	reverse: reverse,
+  	set: Ractive_prototype_set,
+  	shift: shift,
+  	sort: prototype_sort,
+  	splice: splice,
+  	subtract: subtract,
+  	teardown: Ractive_prototype_teardown,
+  	toggle: toggle,
+  	toHTML: toHTML,
+  	toHtml: toHTML,
+  	unrender: Ractive_prototype_unrender,
+  	unshift: unshift,
+  	update: Ractive_prototype_update,
+  	updateModel: prototype_updateModel
+  };
+
+  var wrapMethod = function (method, superMethod, force) {
+
+  	if (force || needsSuper(method, superMethod)) {
+
+  		return function () {
+
+  			var hasSuper = ("_super" in this),
+  			    _super = this._super,
+  			    result;
+
+  			this._super = superMethod;
+
+  			result = method.apply(this, arguments);
+
+  			if (hasSuper) {
+  				this._super = _super;
+  			}
+
+  			return result;
+  		};
+  	} else {
+  		return method;
+  	}
+  };
+
+  function needsSuper(method, superMethod) {
+  	return typeof superMethod === "function" && /_super/.test(method);
+  }
+
+  var unwrapExtended = unwrap;
+
+  function unwrap(Child) {
+  	var options = {};
+
+  	while (Child) {
+  		addRegistries(Child, options);
+  		addOtherOptions(Child, options);
+
+  		if (Child._Parent !== _Ractive) {
+  			Child = Child._Parent;
+  		} else {
+  			Child = false;
+  		}
+  	}
+
+  	return options;
+  }
+
+  function addRegistries(Child, options) {
+  	config_registries.forEach(function (r) {
+  		addRegistry(r.useDefaults ? Child.prototype : Child, options, r.name);
+  	});
+  }
+
+  function addRegistry(target, options, name) {
+  	var registry,
+  	    keys = Object.keys(target[name]);
+
+  	if (!keys.length) {
+  		return;
+  	}
+
+  	if (!(registry = options[name])) {
+  		registry = options[name] = {};
+  	}
+
+  	keys.filter(function (key) {
+  		return !(key in registry);
+  	}).forEach(function (key) {
+  		return registry[key] = target[name][key];
+  	});
+  }
+
+  function addOtherOptions(Child, options) {
+  	Object.keys(Child.prototype).forEach(function (key) {
+  		if (key === "computed") {
+  			return;
+  		}
+
+  		var value = Child.prototype[key];
+
+  		if (!(key in options)) {
+  			options[key] = value._method ? value._method : value;
+  		}
+
+  		// is it a wrapped function?
+  		else if (typeof options[key] === "function" && typeof value === "function" && options[key]._method) {
+
+  			var result = undefined,
+  			    needsSuper = value._method;
+
+  			if (needsSuper) {
+  				value = value._method;
+  			}
+
+  			// rewrap bound directly to parent fn
+  			result = wrapMethod(options[key]._method, value);
+
+  			if (needsSuper) {
+  				result._method = result;
+  			}
+
+  			options[key] = result;
+  		}
+  	});
+  }
+
+  var _extend = _extend__extend;
+
+  function _extend__extend() {
+  	for (var _len = arguments.length, options = Array(_len), _key = 0; _key < _len; _key++) {
+  		options[_key] = arguments[_key];
+  	}
+
+  	if (!options.length) {
+  		return extendOne(this);
+  	} else {
+  		return options.reduce(extendOne, this);
+  	}
+  }
+
+  function extendOne(Parent) {
+  	var options = arguments[1] === undefined ? {} : arguments[1];
+
+  	var Child, proto;
+
+  	// if we're extending with another Ractive instance...
+  	//
+  	//   var Human = Ractive.extend(...), Spider = Ractive.extend(...);
+  	//   var Spiderman = Human.extend( Spider );
+  	//
+  	// ...inherit prototype methods and default options as well
+  	if (options.prototype instanceof _Ractive) {
+  		options = unwrapExtended(options);
+  	}
+
+  	Child = function (options) {
+  		if (!(this instanceof Child)) return new Child(options);
+  		initialise(this, options);
+  	};
+
+  	proto = create(Parent.prototype);
+  	proto.constructor = Child;
+
+  	// Static properties
+  	defineProperties(Child, {
+  		// alias prototype as defaults
+  		defaults: { value: proto },
+
+  		// extendable
+  		extend: { value: _extend__extend, writable: true, configurable: true },
+
+  		// Parent - for IE8, can't use Object.getPrototypeOf
+  		_Parent: { value: Parent }
+  	});
+
+  	// extend configuration
+  	config_config.extend(Parent, proto, options);
+
+  	custom_data.extend(Parent, proto, options);
+
+  	if (options.computed) {
+  		proto.computed = utils_object__extend(create(Parent.prototype.computed), options.computed);
+  	}
+
+  	Child.prototype = proto;
+
+  	return Child;
+  }
+
+  var getNodeInfo = function (node) {
+  	var info = {},
+  	    priv,
+  	    indices;
+
+  	if (!node || !(priv = node._ractive)) {
+  		return info;
+  	}
+
+  	info.ractive = priv.root;
+  	info.keypath = priv.keypath.str;
+  	info.index = {};
+
+  	// find all index references and resolve them
+  	if (indices = Resolvers_findIndexRefs(priv.proxy.parentFragment)) {
+  		info.index = Resolvers_findIndexRefs.resolve(indices);
+  	}
+
+  	return info;
+  };
+
+  var Ractive, properties;
+
+  // Main Ractive required object
+  Ractive = function (options) {
+  	if (!(this instanceof Ractive)) return new Ractive(options);
+  	initialise(this, options);
+  };
+
+  // Ractive properties
+  properties = {
+
+  	// debug flag
+  	DEBUG: { writable: true, value: true },
+  	DEBUG_PROMISES: { writable: true, value: true },
+
+  	// static methods:
+  	extend: { value: _extend },
+  	getNodeInfo: { value: getNodeInfo },
+  	parse: { value: _parse },
+
+  	// Namespaced constructors
+  	Promise: { value: utils_Promise },
+
+  	// support
+  	svg: { value: svg },
+  	magic: { value: environment__magic },
+
+  	// version
+  	VERSION: { value: "0.7.3" },
+
+  	// Plugins
+  	adaptors: { writable: true, value: {} },
+  	components: { writable: true, value: {} },
+  	decorators: { writable: true, value: {} },
+  	easing: { writable: true, value: static_easing },
+  	events: { writable: true, value: {} },
+  	interpolators: { writable: true, value: static_interpolators },
+  	partials: { writable: true, value: {} },
+  	transitions: { writable: true, value: {} }
+  };
+
+  // Ractive properties
+  defineProperties(Ractive, properties);
+
+  Ractive.prototype = utils_object__extend(prototype, config_defaults);
+
+  Ractive.prototype.constructor = Ractive;
+
+  // alias prototype as defaults
+  Ractive.defaults = Ractive.prototype;
+
+  // Ractive.js makes liberal use of things like Array.prototype.indexOf. In
+  // older browsers, these are made available via a shim - here, we do a quick
+  // pre-flight check to make sure that either a) we're not in a shit browser,
+  // or b) we're using a Ractive-legacy.js build
+  var FUNCTION = "function";
+
+  if (typeof Date.now !== FUNCTION || typeof String.prototype.trim !== FUNCTION || typeof Object.keys !== FUNCTION || typeof Array.prototype.indexOf !== FUNCTION || typeof Array.prototype.forEach !== FUNCTION || typeof Array.prototype.map !== FUNCTION || typeof Array.prototype.filter !== FUNCTION || typeof window !== "undefined" && typeof window.addEventListener !== FUNCTION) {
+  	throw new Error("It looks like you're attempting to use Ractive.js in an older browser. You'll need to use one of the 'legacy builds' in order to continue - see http://docs.ractivejs.org/latest/legacy-builds for more information.");
+  }
+
+  var _Ractive = Ractive;
+
+  return _Ractive;
+
+}));
+//# sourceMappingURL=ractive.js.map
