/*! Ractive - v0.3.3 - 2013-07-21 * Next-generation DOM manipulation * http://rich-harris.github.com/Ractive/ * Copyright (c) 2013 Rich Harris; Licensed MIT */ /*jslint eqeq: true, plusplus: true */ /*global document, HTMLElement */ (function ( global ) { 'use strict'; var Ractive, doc = global.document || null, // Ractive prototype proto = {}, // properties of the public Ractive object adaptors = {}, eventDefinitions = {}, easing, extend, parse, interpolate, interpolators, transitions = {}, // internal utils - instance-specific teardown, clearCache, registerDependant, unregisterDependant, notifyDependants, notifyMultipleDependants, notifyDependantsByPriority, registerIndexRef, unregisterIndexRef, resolveRef, processDeferredUpdates, // internal utils splitKeypath, toString, isArray, isObject, isNumeric, isEqual, getEl, insertHtml, reassignFragments, executeTransition, getPartialDescriptor, makeTransitionManager, requestAnimationFrame, defineProperty, defineProperties, create, createFromNull, hasOwn = {}.hasOwnProperty, noop = function () {}, // internally used caches keypathCache = {}, // internally used constructors DomFragment, DomElement, DomAttribute, DomPartial, DomInterpolator, DomTriple, DomSection, DomText, StringFragment, StringPartial, StringInterpolator, StringSection, StringText, ExpressionResolver, Evaluator, Animation, // internally used regexes leadingWhitespace = /^\s+/, trailingWhitespace = /\s+$/, // other bits and pieces render, initMustache, updateMustache, resolveMustache, evaluateMustache, initFragment, updateSection, animationCollection, // array modification registerKeypathToArray, unregisterKeypathFromArray, // parser and tokenizer getFragmentStubFromTokens, getToken, tokenize, stripCommentTokens, stripHtmlComments, stripStandalones, // error messages missingParser = 'Missing Ractive.parse - cannot parse template. Either preparse or use the version that includes the parser', // constants TEXT = 1, INTERPOLATOR = 2, TRIPLE = 3, SECTION = 4, INVERTED = 5, CLOSING = 6, ELEMENT = 7, PARTIAL = 8, COMMENT = 9, DELIMCHANGE = 10, MUSTACHE = 11, TAG = 12, ATTR_VALUE_TOKEN = 13, EXPRESSION = 14, NUMBER_LITERAL = 20, STRING_LITERAL = 21, ARRAY_LITERAL = 22, OBJECT_LITERAL = 23, BOOLEAN_LITERAL = 24, LITERAL = 25, GLOBAL = 26, REFERENCE = 30, REFINEMENT = 31, MEMBER = 32, PREFIX_OPERATOR = 33, BRACKETED = 34, CONDITIONAL = 35, INFIX_OPERATOR = 36, INVOCATION = 40, UNSET = { unset: true }, // namespaces 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/' }, // current version VERSION = '0.3.3'; // we're creating a defineProperty function here - we don't want to add // this to _legacy.js since it's not a polyfill. It won't allow us to set // non-enumerable properties. That shouldn't be a problem, unless you're // using for...in on a (modified) array, in which case you deserve what's // coming anyway try { Object.defineProperty({}, 'test', { value: 0 }); Object.defineProperties({}, { test: { value: 0 } }); defineProperty = Object.defineProperty; defineProperties = Object.defineProperties; } catch ( err ) { // Object.defineProperty doesn't exist, or we're in IE8 where you can // only use it with DOM objects (what the fuck were you smoking, MSFT?) defineProperty = function ( obj, prop, desc ) { obj[ prop ] = desc.value; }; 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; createFromNull = function () { return Object.create( null ); }; } catch ( err ) { // sigh create = (function () { var F = function () {}; return function ( proto, props ) { var obj; F.prototype = proto; obj = new F(); if ( props ) { Object.defineProperties( obj, props ); } return obj; }; }()); createFromNull = function () { return {}; // hope you're not modifying the Object prototype }; } var hyphenate = function ( str ) { return str.replace( /[A-Z]/g, function ( match ) { return '-' + match.toLowerCase(); }); }; // determine some facts about our environment var cssTransitionsEnabled, transition, transitionend; (function () { var testDiv; if ( !doc ) { return; } testDiv = doc.createElement( 'div' ); if ( testDiv.style.transition !== undefined ) { transition = 'transition'; transitionend = 'transitionend'; cssTransitionsEnabled = true; } else if ( testDiv.style.webkitTransition !== undefined ) { transition = 'webkitTransition'; transitionend = 'webkitTransitionEnd'; cssTransitionsEnabled = true; } else { cssTransitionsEnabled = false; } }()); executeTransition = function ( descriptor, root, owner, contextStack, isIntro ) { var transitionName, transitionParams, fragment, transitionManager, transition; if ( !root.transitionsEnabled ) { return; } if ( typeof descriptor === 'string' ) { transitionName = descriptor; } else { transitionName = descriptor.n; if ( descriptor.a ) { transitionParams = descriptor.a; } else if ( descriptor.d ) { fragment = new TextFragment({ descriptor: descriptor.d, root: root, owner: owner, contextStack: parentFragment.contextStack }); transitionParams = fragment.toJson(); fragment.teardown(); } } transition = root.transitions[ transitionName ] || Ractive.transitions[ transitionName ]; if ( transition ) { transitionManager = root._transitionManager; transitionManager.push( owner.node ); transition.call( root, owner.node, function () { transitionManager.pop( owner.node ); }, transitionParams, transitionManager.info, isIntro ); } }; insertHtml = function ( html, docFrag ) { var div, nodes = []; div = doc.createElement( 'div' ); div.innerHTML = html; while ( div.firstChild ) { nodes[ nodes.length ] = div.firstChild; docFrag.appendChild( div.firstChild ); } return nodes; }; (function () { var reassignFragment, reassignElement, reassignMustache; reassignFragments = function ( root, section, start, end, by ) { var fragmentsToReassign, i, fragment, indexRef, oldIndex, newIndex, oldKeypath, newKeypath; indexRef = section.descriptor.i; for ( i=start; i{{/section}}) need to cascade // down the tree if ( fragment.owner.parentFragment ) { parentRefs = fragment.owner.parentFragment.indexRefs; if ( parentRefs ) { fragment.indexRefs = createFromNull(); // avoids need for hasOwnProperty for ( ref in parentRefs ) { fragment.indexRefs[ ref ] = parentRefs[ ref ]; } } // while we're in this branch, inherit priority fragment.priority = fragment.owner.parentFragment.priority + 1; } else { fragment.priority = 0; } if ( options.indexRef ) { if ( !fragment.indexRefs ) { fragment.indexRefs = {}; } fragment.indexRefs[ options.indexRef ] = options.index; } // Time to create this fragment's child items; fragment.items = []; itemOptions = { parentFragment: fragment }; numItems = ( options.descriptor ? options.descriptor.length : 0 ); for ( i=0; i section.length ) { // add any new ones for ( i=section.length; i 1 ) { fragmentsToRemove = section.fragments.splice( 1 ); while ( fragmentsToRemove.length ) { fragmentsToRemove.pop().teardown( true ); } } } else if ( section.length ) { section.teardownFragments( true ); section.length = 0; } }; }()); stripCommentTokens = function ( tokens ) { var i, current, previous, next; for ( i=0; i' ); // no comments? great if ( commentStart === -1 && commentEnd === -1 ) { processed += html; break; } // comment start but no comment end if ( commentStart !== -1 && commentEnd === -1 ) { throw 'Illegal HTML - expected closing comment sequence (\'-->\')'; } // comment end but no comment start, or comment end before comment start if ( ( commentEnd !== -1 && commentStart === -1 ) || ( commentEnd < commentStart ) ) { throw 'Illegal HTML - unexpected closing comment sequence (\'-->\')'; } processed += html.substr( 0, commentStart ); html = html.substring( commentEnd + 3 ); } return processed; }; stripStandalones = function ( tokens ) { var i, current, backOne, backTwo, leadingLinebreak, trailingLinebreak; leadingLinebreak = /^\s*\r?\n/; trailingLinebreak = /\r?\n\s*$/; for ( i=2; i 1 ) { key = accumulated[ accumulated.length ] = keys.shift(); // If this branch doesn't exist yet, create a new one - if the next // key matches /^\s*[0-9]+\s*$/, assume we want an array branch rather // than an object if ( !obj[ key ] ) { // if we're creating a new branch, we may need to clear the upstream // keypath if ( !keypathToClear ) { keypathToClear = accumulated.join( '.' ); } obj[ key ] = ( /^\s*[0-9]+\s*$/.test( keys[0] ) ? [] : {} ); } obj = obj[ key ]; } key = keys[0]; obj[ key ] = value; } else { // if value is a primitive, we don't need to do anything else if ( typeof value !== 'object' ) { return; } } // Clear cache clearCache( root, keypathToClear || keypath ); // add this keypath to the notification queue queue[ queue.length ] = keypath; // add upstream keypaths to the upstream notification queue while ( keysClone.length > 1 ) { keysClone.pop(); keypath = keysClone.join( '.' ); if ( upstreamQueue.indexOf( keypath ) === -1 ) { upstreamQueue[ upstreamQueue.length ] = keypath; } } }; attemptKeypathResolution = function ( root ) { var i, unresolved, keypath; // See if we can resolve any of the unresolved keypaths (if such there be) i = root._pendingResolution.length; while ( i-- ) { // Work backwards, so we don't go in circles! unresolved = root._pendingResolution.splice( i, 1 )[0]; if ( keypath = resolveRef( root, unresolved.ref, unresolved.contextStack ) ) { // If we've resolved the keypath, we can initialise this item unresolved.resolve( keypath ); } else { // If we can't resolve the reference, add to the back of // the queue (this is why we're working backwards) root._pendingResolution[ root._pendingResolution.length ] = unresolved; } } }; }( proto )); // Teardown. This goes through the root fragment and all its children, removing observers // and generally cleaning up after itself proto.teardown = function ( complete ) { var keypath, transitionManager, previousTransitionManager; this.fire( 'teardown' ); previousTransitionManager = this._transitionManager; this._transitionManager = transitionManager = makeTransitionManager( this, complete ); this.fragment.teardown( true ); // Cancel any animations in progress while ( this._animations[0] ) { this._animations[0].stop(); // it will remove itself from the index } // Clear cache - this has the side-effect of unregistering keypaths from modified arrays. for ( keypath in this._cache ) { clearCache( this, keypath ); } // Teardown any bindings while ( this._bound.length ) { this.unbind( this._bound.pop() ); } // transition manager has finished its work this._transitionManager = previousTransitionManager; transitionManager.ready(); }; proto.toggleFullscreen = function () { if ( Ractive.isFullscreen( this.el ) ) { this.cancelFullscreen(); } else { this.requestFullscreen(); } }; proto.unbind = function ( adaptor ) { var bound = this._bound, index; index = bound.indexOf( adaptor ); if ( index !== -1 ) { bound.splice( index, 1 ); adaptor.teardown( this ); } }; proto.update = function ( keypath, complete ) { var transitionManager, previousTransitionManager; if ( typeof keypath === 'function' ) { complete = keypath; } // manage transitions previousTransitionManager = this._transitionManager; this._transitionManager = transitionManager = makeTransitionManager( this, complete ); clearCache( this, keypath || '' ); notifyDependants( this, keypath || '' ); processDeferredUpdates( this ); // transition manager has finished its work this._transitionManager = previousTransitionManager; transitionManager.ready(); if ( typeof keypath === 'string' ) { this.fire( 'update', keypath ); } else { this.fire( 'update' ); } return this; }; adaptors.backbone = function ( model, path ) { var settingModel, settingView, setModel, setView, pathMatcher, pathLength, prefix; if ( path ) { path += '.'; pathMatcher = new RegExp( '^' + path.replace( /\./g, '\\.' ) ); pathLength = path.length; } return { init: function ( view ) { // if no path specified... if ( !path ) { setView = function ( model ) { if ( !settingModel ) { settingView = true; view.set( model.changed ); settingView = false; } }; setModel = function ( keypath, value ) { if ( !settingView ) { settingModel = true; model.set( keypath, value ); settingModel = false; } }; } else { prefix = function ( attrs ) { var attr, result; result = {}; for ( attr in attrs ) { if ( hasOwn.call( attrs, attr ) ) { result[ path + attr ] = attrs[ attr ]; } } return result; }; setView = function ( model ) { if ( !settingModel ) { settingView = true; view.set( prefix( model.changed ) ); settingView = false; } }; setModel = function ( keypath, value ) { if ( !settingView ) { if ( pathMatcher.test( keypath ) ) { settingModel = true; model.set( keypath.substring( pathLength ), value ); settingModel = false; } } }; } model.on( 'change', setView ); view.on( 'set', setModel ); // initialise view.set( path ? prefix( model.attributes ) : model.attributes ); }, teardown: function ( view ) { model.off( 'change', setView ); view.off( 'set', setModel ); } }; }; adaptors.statesman = function ( model, path ) { var settingModel, settingView, setModel, setView, pathMatcher, pathLength, prefix; if ( path ) { path += '.'; pathMatcher = new RegExp( '^' + path.replace( /\./g, '\\.' ) ); pathLength = path.length; prefix = function ( attrs ) { var attr, result; if ( !attrs ) { return; } result = {}; for ( attr in attrs ) { if ( hasOwn.call( attrs, attr ) ) { result[ path + attr ] = attrs[ attr ]; } } return result; }; } return { init: function ( view ) { var data; // if no path specified... if ( !path ) { setView = function ( change ) { if ( !settingModel ) { settingView = true; view.set( change ); settingView = false; } }; if ( view.twoway ) { setModel = function ( keypath, value ) { if ( !settingView ) { settingModel = true; model.set( keypath, value ); settingModel = false; } }; } } else { setView = function ( change ) { if ( !settingModel ) { settingView = true; change = prefix( change ); view.set( change ); settingView = false; } }; if ( view.twoway ) { setModel = function ( keypath, value ) { if ( !settingView ) { if ( pathMatcher.test( keypath ) ) { settingModel = true; model.set( keypath.substring( pathLength ), value ); settingModel = false; } } }; } } model.on( 'change', setView ); if ( view.twoway ) { view.on( 'set', setModel ); } // initialise data = ( path ? prefix( model.get() ) : model.get() ); if ( data ) { view.set( path ? prefix( model.get() ) : model.get() ); } }, teardown: function ( view ) { model.off( 'change', setView ); view.off( 'set', setModel ); } }; }; // 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. 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 ) ); } }; eventDefinitions.hover = function ( node, fire ) { var mouseoverHandler, mouseoutHandler; mouseoverHandler = function ( event ) { fire({ node: node, original: event, hover: true }); }; mouseoutHandler = function ( event ) { fire({ node: node, original: event, hover: false }); }; node.addEventListener( 'mouseover', mouseoverHandler ); node.addEventListener( 'mouseout', mouseoutHandler ); return { teardown: function () { node.removeEventListener( 'mouseover', mouseoverHandler ); node.removeEventListener( 'mouseout', mouseoutHandler ); } }; }; (function () { var makeKeyDefinition = function ( code ) { return function ( node, fire ) { var keydownHandler; node.addEventListener( 'keydown', keydownHandler = function ( event ) { var which = event.which || event.keyCode; if ( which === code ) { event.preventDefault(); fire({ node: node, original: event }); } }); return { teardown: function () { node.removeEventListener( keydownHandler ); } }; }; }; eventDefinitions.enter = makeKeyDefinition( 13 ); eventDefinitions.tab = makeKeyDefinition( 9 ); eventDefinitions.escape = makeKeyDefinition( 27 ); eventDefinitions.space = makeKeyDefinition( 32 ); }()); eventDefinitions.tap = function ( node, fire ) { var mousedown, touchstart, distanceThreshold, timeThreshold; distanceThreshold = 5; // maximum pixels pointer can move before cancel timeThreshold = 400; // maximum milliseconds between down and up before cancel mousedown = function ( event ) { var currentTarget, x, y, up, move, cancel; x = event.clientX; y = event.clientY; currentTarget = this; up = function ( event ) { fire({ node: currentTarget, original: event }); cancel(); }; move = function ( event ) { if ( ( Math.abs( event.clientX - x ) >= distanceThreshold ) || ( Math.abs( event.clientY - y ) >= distanceThreshold ) ) { cancel(); } }; cancel = function () { doc.removeEventListener( 'mousemove', move ); doc.removeEventListener( 'mouseup', up ); }; doc.addEventListener( 'mousemove', move ); doc.addEventListener( 'mouseup', up ); setTimeout( cancel, timeThreshold ); }; node.addEventListener( 'mousedown', mousedown ); touchstart = function ( event ) { var currentTarget, x, y, touch, finger, move, up, cancel; if ( event.touches.length !== 1 ) { return; } touch = event.touches[0]; x = touch.clientX; y = touch.clientY; currentTarget = this; finger = touch.identifier; up = function ( event ) { var touch; touch = event.changedTouches[0]; if ( touch.identifier !== finger ) { cancel(); } event.preventDefault(); // prevent compatibility mouse event fire({ node: currentTarget, original: event }); cancel(); }; move = function ( event ) { var touch; if ( event.touches.length !== 1 || event.touches[0].identifier !== finger ) { cancel(); } touch = event.touches[0]; if ( ( Math.abs( touch.clientX - x ) >= distanceThreshold ) || ( Math.abs( touch.clientY - y ) >= distanceThreshold ) ) { cancel(); } }; cancel = function () { window.removeEventListener( 'touchmove', move ); window.removeEventListener( 'touchend', up ); window.removeEventListener( 'touchcancel', cancel ); }; window.addEventListener( 'touchmove', move ); window.addEventListener( 'touchend', up ); window.addEventListener( 'touchcancel', cancel ); setTimeout( cancel, timeThreshold ); }; node.addEventListener( 'touchstart', touchstart ); return { teardown: function () { node.removeEventListener( 'mousedown', mousedown ); node.removeEventListener( 'touchstart', touchstart ); } }; }; (function () { var fillGaps, clone, augment, inheritFromParent, wrapMethod, inheritFromChildProps, conditionallyParseTemplate, extractInlinePartials, conditionallyParsePartials, initChildInstance, extendable, inheritable, blacklist; extend = function ( childProps ) { var Parent, Child, key, template, partials, partial, member; Parent = this; // create Child constructor Child = function ( options ) { initChildInstance( this, Child, options || {}); }; Child.prototype = create( Parent.prototype ); // inherit options from parent, if we're extending a subclass if ( Parent !== Ractive ) { inheritFromParent( Child, Parent ); } // apply childProps inheritFromChildProps( Child, childProps ); // parse template and any partials that need it conditionallyParseTemplate( Child ); extractInlinePartials( Child, childProps ); conditionallyParsePartials( Child ); Child.extend = Parent.extend; return Child; }; extendable = [ 'data', 'partials', 'transitions', 'eventDefinitions' ]; inheritable = [ 'el', 'template', 'complete', 'modifyArrays', 'twoway', 'lazy', 'append', 'preserveWhitespace', 'sanitize', 'noIntro', 'transitionsEnabled' ]; blacklist = extendable.concat( inheritable ); inheritFromParent = function ( Child, Parent ) { extendable.forEach( function ( property ) { if ( Parent[ property ] ) { Child[ property ] = clone( Parent[ property ] ); } }); inheritable.forEach( function ( property ) { if ( Parent[ property ] !== undefined ) { Child[ property ] = Parent[ property ]; } }); }; wrapMethod = function ( method, superMethod ) { if ( /_super/.test( method ) ) { return function () { var _super = this._super; this._super = superMethod; method.apply( this, arguments ); this._super = _super; }; } else { return method; } }; inheritFromChildProps = function ( Child, childProps ) { var key, member; extendable.forEach( function ( property ) { var value = childProps[ property ]; if ( value ) { if ( Child[ property ] ) { augment( Child[ property ], value ); } else { Child[ property ] = value; } } }); inheritable.forEach( function ( property ) { if ( childProps[ property ] !== undefined ) { Child[ property ] = childProps[ property ]; } }); // Blacklisted properties don't extend the child, as they are part of the initialisation options for ( key in childProps ) { if ( hasOwn.call( childProps, key ) && !hasOwn.call( Child.prototype, key ) && blacklist.indexOf( key ) === -1 ) { member = childProps[ key ]; // if this is a method that overwrites a prototype method, we may need // to wrap it if ( typeof member === 'function' && typeof Child.prototype[ key ] === 'function' ) { Child.prototype[ key ] = wrapMethod( member, Child.prototype[ key ] ); } else { Child.prototype[ key ] = member; } } } }; conditionallyParseTemplate = function ( Child ) { var templateEl; if ( typeof Child.template === 'string' ) { if ( !Ractive.parse ) { throw new Error( missingParser ); } if ( Child.template.charAt( 0 ) === '#' && doc ) { templateEl = doc.getElementById( Child.template.substring( 1 ) ); if ( templateEl && templateEl.tagName === 'SCRIPT' ) { Child.template = Ractive.parse( templateEl.innerHTML, Child ); } else { throw new Error( 'Could not find template element (' + Child.template + ')' ); } } else { Child.template = Ractive.parse( Child.template, Child ); // all the relevant options are on Child } } }; extractInlinePartials = function ( Child, childProps ) { // does our template contain inline partials? if ( isObject( Child.template ) ) { if ( !Child.partials ) { Child.partials = {}; } // get those inline partials augment( Child.partials, Child.template.partials ); // but we also need to ensure that any explicit partials override inline ones if ( childProps.partials ) { augment( Child.partials, childProps.partials ); } // move template to where it belongs Child.template = Child.template.template; } }; conditionallyParsePartials = function ( Child ) { var key, partial; // Parse partials, if necessary if ( Child.partials ) { for ( key in Child.partials ) { if ( hasOwn.call( Child.partials, key ) ) { if ( typeof Child.partials[ key ] === 'string' ) { if ( !Ractive.parse ) { throw new Error( missingParser ); } partial = Ractive.parse( Child.partials[ key ], Child ); } else { partial = Child.partials[ key ]; } Child.partials[ key ] = partial; } } } }; initChildInstance = function ( child, Child, options ) { var key, i, optionName; // Add template to options, if necessary if ( !options.template && Child.template ) { options.template = Child.template; } extendable.forEach( function ( property ) { if ( !options[ property ] ) { if ( Child[ property ] ) { options[ property ] = clone( Child[ property ] ); } } else { fillGaps( options[ property ], Child[ property ] ); } }); inheritable.forEach( function ( property ) { if ( options[ property ] === undefined && Child[ property ] !== undefined ) { options[ property ] = Child[ property ]; } }); Ractive.call( child, options ); if ( child.init ) { child.init.call( child, options ); } }; fillGaps = function ( target, source ) { var key; for ( key in source ) { if ( hasOwn.call( source, key ) && !hasOwn.call( target, key ) ) { target[ key ] = source[ key ]; } } }; clone = function ( source ) { var target = {}, key; for ( key in source ) { if ( hasOwn.call( source, key ) ) { target[ key ] = source[ key ]; } } return target; }; augment = function ( target, source ) { var key; for ( key in source ) { if ( hasOwn.call( source, key ) ) { target[ key ] = source[ key ]; } } }; }()); // TODO short circuit values that stay the same interpolate = function ( from, to ) { if ( isNumeric( from ) && isNumeric( to ) ) { return Ractive.interpolators.number( +from, +to ); } if ( isArray( from ) && isArray( to ) ) { return Ractive.interpolators.array( from, to ); } if ( isObject( from ) && isObject( to ) ) { return Ractive.interpolators.object( from, to ); } return function () { return to; }; }; interpolators = { number: function ( from, to ) { var 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; intermediate = []; interpolators = []; i = len = Math.min( from.length, to.length ); while ( i-- ) { interpolators[i] = Ractive.interpolate( from[i], to[i] ); } // surplus values - don't interpolate, but don't exclude them either for ( i=len; i tag templateEl = doc.getElementById( template.substring( 1 ) ); if ( templateEl ) { parsedTemplate = Ractive.parse( templateEl.innerHTML, options ); } else { throw new Error( 'Could not find template element (' + template + ')' ); } } else { parsedTemplate = Ractive.parse( template, options ); } } else { parsedTemplate = template; } // deal with compound template if ( isObject( parsedTemplate ) ) { this.partials = parsedTemplate.partials; parsedTemplate = parsedTemplate.template; } // If the template was an array with a single string member, that means // we can use innerHTML - we just need to unpack it if ( parsedTemplate && ( parsedTemplate.length === 1 ) && ( typeof parsedTemplate[0] === 'string' ) ) { parsedTemplate = parsedTemplate[0]; } this.template = parsedTemplate; // If we were given unparsed partials, parse them if ( options.partials ) { for ( key in options.partials ) { if ( hasOwn.call( options.partials, key ) ) { partial = options.partials[ key ]; if ( typeof partial === 'string' ) { if ( !Ractive.parse ) { throw new Error( missingParser ); } partial = Ractive.parse( partial, options ); } this.partials[ key ] = partial; } } } // Unpack string-based partials, if necessary for ( key in this.partials ) { if ( hasOwn.call( this.partials, key ) && this.partials[ key ].length === 1 && typeof this.partials[ key ][0] === 'string' ) { this.partials[ key ] = this.partials[ key ][0]; } } // temporarily disable transitions, if noIntro flag is set this.transitionsEnabled = ( options.noIntro ? false : options.transitionsEnabled ); render( this, { el: this.el, append: options.append, complete: options.complete }); // reset transitionsEnabled this.transitionsEnabled = options.transitionsEnabled; }; (function () { var getOriginalComputedStyles, setStyle, augment, makeTransition, transform, transformsEnabled, inside, outside; // no point executing this code on the server if ( !doc ) { return; } getOriginalComputedStyles = function ( computedStyle, properties ) { var original = {}, i; i = properties.length; while ( i-- ) { original[ properties[i] ] = computedStyle[ properties[i] ]; } return original; }; setStyle = function ( node, properties, map, params ) { var i = properties.length, prop; while ( i-- ) { prop = properties[i]; if ( map && map[ prop ] ) { if ( typeof map[ prop ] === 'function' ) { node.style[ prop ] = map[ prop ]( params ); } else { node.style[ prop ] = map[ prop ]; } } else { node.style[ prop ] = 0; } } }; augment = function ( target, source ) { var key; if ( !source ) { return target; } for ( key in source ) { if ( hasOwn.call( source, key ) ) { target[ key ] = source[ key ]; } } return target; }; if ( cssTransitionsEnabled ) { makeTransition = function ( properties, defaults, outside, inside ) { if ( typeof properties === 'string' ) { properties = [ properties ]; } return function ( node, complete, params, info, isIntro ) { var transitionEndHandler, transitionStyle, computedStyle, originalComputedStyles, startTransition, originalStyle, originalOpacity, targetOpacity, duration, delay, start, end, source, target, positionStyle, visibilityStyle, stylesToRemove; params = parseTransitionParams( params ); duration = params.duration || defaults.duration; easing = hyphenate( params.easing || defaults.easing ); delay = ( params.delay || defaults.delay || 0 ) + ( ( params.stagger || defaults.stagger || 0 ) * info.i ); start = ( isIntro ? outside : inside ); end = ( isIntro ? inside : outside ); computedStyle = window.getComputedStyle( node ); originalStyle = node.getAttribute( 'style' ); // if this is an intro, we need to transition TO the original styles if ( isIntro ) { // hide, to avoid flashes positionStyle = node.style.position; visibilityStyle = node.style.visibility; node.style.position = 'absolute'; node.style.visibility = 'hidden'; // we need to wait a beat before we can actually get values from computedStyle. // Yeah, I know, WTF browsers setTimeout( function () { var i, prop; originalComputedStyles = getOriginalComputedStyles( computedStyle, properties ); start = outside; end = augment( originalComputedStyles, inside ); // starting style node.style.position = positionStyle; node.style.visibility = visibilityStyle; setStyle( node, properties, start, params ); setTimeout( startTransition, 0 ); }, delay ); } // otherwise we need to transition FROM them else { setTimeout( function () { var i, prop; originalComputedStyles = getOriginalComputedStyles( computedStyle, properties ); start = augment( originalComputedStyles, inside ); end = outside; // ending style setStyle( node, properties, start, params ); setTimeout( startTransition, 0 ); }, delay ); } startTransition = function () { var i, prop; node.style[ transition + 'Duration' ] = ( duration / 1000 ) + 's'; node.style[ transition + 'Properties' ] = properties.map( hyphenate ).join( ',' ); node.style[ transition + 'TimingFunction' ] = easing; transitionEndHandler = function ( event ) { node.removeEventListener( transitionend, transitionEndHandler ); if ( isIntro ) { node.setAttribute( 'style', originalStyle || '' ); } complete(); }; node.addEventListener( transitionend, transitionEndHandler ); setStyle( node, properties, end, params ); }; }; }; transitions.slide = makeTransition([ 'height', 'borderTopWidth', 'borderBottomWidth', 'paddingTop', 'paddingBottom', 'overflowY' ], { duration: 400, easing: 'easeInOut' }, { overflowY: 'hidden' }, { overflowY: 'hidden' }); transitions.fade = makeTransition( 'opacity', { duration: 300, easing: 'linear' }); transitions.fly = makeTransition([ 'opacity', 'left', 'position' ], { duration: 400, easing: 'easeOut' }, { position: 'relative', left: '-500px' }, { position: 'relative', left: 0 }); } }()); var parseTransitionParams = function ( params ) { if ( params === 'fast' ) { return { duration: 200 }; } if ( params === 'slow' ) { return { duration: 600 }; } if ( isNumeric( params ) ) { return { duration: +params }; } return params || {}; }; (function ( transitions ) { var typewriter, typewriteNode, typewriteTextNode; if ( !doc ) { return; } typewriteNode = function ( node, complete, interval ) { var children, next, hideData; if ( node.nodeType === 3 ) { typewriteTextNode( node, complete, interval ); return; } children = Array.prototype.slice.call( node.childNodes ); next = function () { if ( !children.length ) { complete(); return; } typewriteNode( children.shift(), next, interval ); }; next(); }; typewriteTextNode = function ( node, complete, interval ) { var str, len, loop, i; // text node str = node._hiddenData; len = str.length; if ( !len ) { complete(); return; } i = 0; loop = setInterval( function () { var substr, remaining, match, remainingNonWhitespace, filler; substr = str.substr( 0, i ); remaining = str.substring( i ); match = /^\w+/.exec( remaining ); remainingNonWhitespace = ( match ? match[0].length : 0 ); // add some non-breaking whitespace corresponding to the remaining length of the // current word (only really works with monospace fonts, but better than nothing) filler = new Array( remainingNonWhitespace + 1 ).join( '\u00a0' ); node.data = substr + filler; if ( i === len ) { clearInterval( loop ); delete node._hiddenData; complete(); } i += 1; }, interval ); }; typewriter = function ( node, complete, params, info, isIntro ) { var interval, style, computedStyle, hideData; params = parseTransitionParams( params ); interval = params.interval || ( params.speed ? 1000 / params.speed : ( params.duration ? node.textContent.length / params.duration : 4 ) ); style = node.getAttribute( 'style' ); computedStyle = window.getComputedStyle( node ); node.style.visibility = 'hidden'; setTimeout( function () { var computedHeight, computedWidth, computedVisibility; computedWidth = computedStyle.width; computedHeight = computedStyle.height; computedVisibility = computedStyle.visibility; hideData( node ); setTimeout( function () { node.style.width = computedWidth; node.style.height = computedHeight; node.style.visibility = 'visible'; typewriteNode( node, function () { node.setAttribute( 'style', style || '' ); complete(); }, interval ); }, params.delay || 0 ); }); hideData = function ( node ) { var children, i; if ( node.nodeType === 3 ) { node._hiddenData = '' + node.data; node.data = ''; return; } children = Array.prototype.slice.call( node.childNodes ); i = children.length; while ( i-- ) { hideData( children[i] ); } }; }; transitions.typewriter = typewriter; }( transitions )); (function ( Ractive ) { var requestFullscreen, cancelFullscreen, fullscreenElement, testDiv; if ( !doc ) { return; } Ractive.fullscreenEnabled = doc.fullscreenEnabled || doc.mozFullScreenEnabled || doc.webkitFullscreenEnabled; if ( !Ractive.fullscreenEnabled ) { Ractive.requestFullscreen = Ractive.cancelFullscreen = noop; return; } testDiv = doc.createElement( 'div' ); // get prefixed name of requestFullscreen method if ( testDiv.requestFullscreen ) { requestFullscreen = 'requestFullscreen'; } else if ( testDiv.mozRequestFullScreen ) { requestFullscreen = 'mozRequestFullScreen'; } else if ( testDiv.webkitRequestFullscreen ) { requestFullscreen = 'webkitRequestFullscreen'; } Ractive.requestFullscreen = function ( el ) { if ( el[ requestFullscreen ] ) { el[ requestFullscreen ](); } }; // get prefixed name of cancelFullscreen method if ( doc.cancelFullscreen ) { cancelFullscreen = 'cancelFullscreen'; } else if ( doc.mozCancelFullScreen ) { cancelFullscreen = 'mozCancelFullScreen'; } else if ( doc.webkitCancelFullScreen ) { cancelFullscreen = 'webkitCancelFullScreen'; } Ractive.cancelFullscreen = function () { doc[ cancelFullscreen ](); }; // get prefixed name of fullscreenElement property if ( doc.fullscreenElement !== undefined ) { fullscreenElement = 'fullscreenElement'; } else if ( doc.mozFullScreenElement !== undefined ) { fullscreenElement = 'mozFullScreenElement'; } else if ( doc.webkitFullscreenElement !== undefined ) { fullscreenElement = 'webkitFullscreenElement'; } Ractive.isFullscreen = function ( el ) { return el === doc[ fullscreenElement ]; }; }( Ractive )); Animation = function ( options ) { var key; this.startTime = Date.now(); // from and to for ( key in options ) { if ( hasOwn.call( options, key ) ) { this[ key ] = options[ key ]; } } this.interpolator = Ractive.interpolate( this.from, this.to ); this.running = true; }; Animation.prototype = { tick: function () { var elapsed, t, value, timeNow, index; if ( this.running ) { timeNow = Date.now(); elapsed = timeNow - this.startTime; if ( elapsed >= this.duration ) { this.root.set( this.keypath, this.to ); if ( this.step ) { this.step( 1, this.to ); } if ( this.complete ) { this.complete( 1, this.to ); } index = this.root._animations.indexOf( this ); // TODO remove this check, once we're satisifed this never happens! if ( index === -1 && console && console.warn ) { console.warn( 'Animation was not found' ); } this.root._animations.splice( index, 1 ); this.running = false; return false; } t = this.easing ? this.easing ( elapsed / this.duration ) : ( elapsed / this.duration ); value = this.interpolator( t ); this.root.set( this.keypath, value ); if ( this.step ) { this.step( t, value ); } return true; } return false; }, stop: function () { var index; this.running = false; index = this.root._animations.indexOf( this ); // TODO remove this check, once we're satisifed this never happens! if ( index === -1 && console && console.warn ) { console.warn( 'Animation was not found' ); } this.root._animations.splice( index, 1 ); } }; animationCollection = { animations: [], tick: function () { var i, animation; for ( i=0; i // // // // In this case we want to set `colour` to the value of whichever option // is checked. (We assume that a value attribute has been supplied.) if ( this.propertyName === 'name' ) { // replace actual name attribute node.name = '{{' + this.keypath + '}}'; this.updateViewModel = function () { if ( node.checked ) { self.root.set( self.keypath, node.value ); } }; } // Or, we might have a situation like this: // // // // Here, we want to set `active` to true or false depending on whether // the input is checked. else if ( this.propertyName === 'checked' ) { this.updateViewModel = function () { self.root.set( self.keypath, node.checked ); }; } } else { // Otherwise we've probably got a situation like this: // // // // in which case we just want to set `name` whenever the user enters text. // The same applies to selects and textareas this.updateViewModel = function () { var value; value = node.value; // special cases if ( value === '0' ) { value = 0; } else if ( value !== '' ) { value = +value || value; } // Note: we're counting on `this.root.set` recognising that `value` is // already what it wants it to be, and short circuiting the process. // Rather than triggering an infinite loop... self.root.set( self.keypath, value ); }; } // if we figured out how to bind changes to the viewmodel, add the event listeners if ( this.updateViewModel ) { this.twoway = true; this.boundEvents = [ 'change', 'click', 'blur' ]; // TODO click only in IE? if ( !lazy ) { this.boundEvents[3] = 'input'; // this is a hack to see if we're in IE - if so, we probably need to add // a keyup listener as well, since in IE8 the input event doesn't fire, // and in IE9 it doesn't fire when text is deleted if ( node.attachEvent ) { this.boundEvents[4] = 'keyup'; } } i = this.boundEvents.length; while ( i-- ) { node.addEventListener( this.boundEvents[i], this.updateViewModel ); } } }, updateBindings: function () { // if the fragment this attribute belongs to gets reassigned (as a result of // as section being updated via an array shift, unshift or splice), this // attribute needs to recognise that its keypath has changed this.keypath = this.interpolator.keypath || this.interpolator.r; // if we encounter the special case described above, update the name attribute if ( this.propertyName === 'name' ) { // replace actual name attribute this.parentNode.name = '{{' + this.keypath + '}}'; } }, teardown: function () { var i; if ( this.boundEvents ) { i = this.boundEvents.length; while ( i-- ) { this.parentNode.removeEventListener( this.boundEvents[i], this.updateViewModel ); } } // ignore non-dynamic attributes if ( this.fragment ) { this.fragment.teardown(); } }, bubble: function () { // If an attribute's text fragment contains a single item, we can // update the DOM immediately... if ( this.selfUpdating ) { this.update(); } // otherwise we want to register it as a deferred attribute, to be // updated once all the information is in, to prevent unnecessary // DOM manipulation else if ( !this.deferred && this.ready ) { this.root._defAttrs[ this.root._defAttrs.length ] = this; this.deferred = true; } }, update: function () { var value, lowerCaseName; if ( !this.ready ) { return this; // avoid items bubbling to the surface when we're still initialising } if ( this.twoway ) { // TODO compare against previous? lowerCaseName = this.lcName; value = this.interpolator.value; // special case - if we have an element like this: // // // // and `colour` has been set to 'red', we don't want to change the name attribute // to red, we want to indicate that this is the selected option, by setting // input.checked = true if ( lowerCaseName === 'name' && ( this.parentNode.type === 'checkbox' || this.parentNode.type === 'radio' ) ) { if ( value === this.parentNode.value ) { this.parentNode.checked = true; } else { this.parentNode.checked = false; } return this; } // don't programmatically update focused element if ( doc.activeElement === this.parentNode ) { return this; } } value = this.fragment.getValue(); if ( value === undefined ) { value = ''; } if ( value !== this.value ) { if ( this.useProperty ) { this.parentNode[ this.propertyName ] = value; return this; } if ( this.namespace ) { this.parentNode.setAttributeNS( this.namespace, this.name, value ); return this; } if ( this.lcName === 'id' ) { if ( this.value !== undefined ) { this.root.nodes[ this.value ] = undefined; } this.root.nodes[ value ] = this.parentNode; } this.parentNode.setAttribute( this.name, value ); this.value = value; } return this; }, toString: function () { var str; if ( this.value === null ) { return this.name; } // TODO don't use JSON.stringify? if ( !this.fragment ) { return this.name + '=' + JSON.stringify( this.value ); } // TODO deal with boolean attributes correctly str = this.fragment.toString(); return this.name + '=' + JSON.stringify( str ); } }; // Helper functions 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 = name; attribute.namespace = namespaces[ namespacePrefix ]; if ( !attribute.namespace ) { throw 'Unknown namespace ("' + namespacePrefix + '")'; } return; } } attribute.name = name; }; setStaticAttribute = function ( attribute, options ) { if ( options.parentNode ) { if ( attribute.namespace ) { options.parentNode.setAttributeNS( attribute.namespace, options.name, options.value ); } else { options.parentNode.setAttribute( options.name, options.value ); } if ( options.name.toLowerCase() === 'id' ) { options.root.nodes[ options.value ] = options.parentNode; } } attribute.value = options.value; }; determinePropertyName = function ( attribute, options ) { var lowerCaseName, propertyName; if ( attribute.parentNode && !attribute.namespace && ( !options.parentNode.namespaceURI || options.parentNode.namespaceURI === namespaces.html ) ) { lowerCaseName = attribute.lcName; propertyName = propertyNames[ lowerCaseName ] || lowerCaseName; if ( options.parentNode[ propertyName ] !== undefined ) { attribute.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 ( typeof options.parentNode[ propertyName ] === 'boolean' || propertyName === 'value' ) { attribute.useProperty = true; } } }; isAttributeSelfUpdating = function ( attribute ) { var i, item, containsInterpolator; i = attribute.fragment.items.length; while ( i-- ) { item = attribute.fragment.items[i]; if ( item.type === TEXT ) { continue; } // we can only have one interpolator and still be self-updating if ( item.type === INTERPOLATOR ) { if ( containsInterpolator ) { return false; } else { containsInterpolator = true; continue; } } // anything that isn't text or an interpolator (i.e. a section) // and we can't self-update return false; } return true; }; isAttributeBindable = function ( attribute ) { var tagName, propertyName; if ( !attribute.root.twoway ) { return false; } tagName = attribute.element.descriptor.e.toLowerCase(); propertyName = attribute.propertyName; return ( ( propertyName === 'name' || propertyName === 'value' || propertyName === 'checked' ) && ( tagName === 'input' || tagName === 'textarea' || tagName === 'select' ) ); }; }()); // Element DomElement = function ( options, docFrag ) { var parentFragment, descriptor, namespace, eventName, eventNames, i, attr, attrName, lcName, attrValue, bindable, twowayNameAttr, parentNode, root, transition, transitionName, transitionParams, transitionManager, intro; this.type = ELEMENT; // stuff we'll need later parentFragment = this.parentFragment = options.parentFragment; descriptor = this.descriptor = options.descriptor; this.root = root = parentFragment.root; this.parentNode = parentFragment.parentNode; this.index = options.index; this.eventListeners = []; this.customEventListeners = []; // get namespace, if we're actually rendering (not server-side stringifying) if ( this.parentNode ) { if ( descriptor.a && descriptor.a.xmlns ) { namespace = descriptor.a.xmlns; // check it's a string! if ( typeof namespace !== 'string' ) { throw new Error( 'Namespace attribute cannot contain mustaches' ); } } else { namespace = ( descriptor.e.toLowerCase() === 'svg' ? namespaces.svg : this.parentNode.namespaceURI ); } // create the DOM node this.node = doc.createElementNS( namespace, descriptor.e ); } // append children, if there are any if ( descriptor.f ) { if ( typeof descriptor.f === 'string' && ( !this.node || ( !this.node.namespaceURI || this.node.namespaceURI === namespaces.html ) ) ) { // great! we can use innerHTML this.html = descriptor.f; if ( docFrag ) { this.node.innerHTML = this.html; } } else { // once again, everyone has to suffer because of IE bloody 8 if ( descriptor.e === 'style' && this.node.styleSheet !== undefined ) { this.fragment = new StringFragment({ descriptor: descriptor.f, root: root, contextStack: parentFragment.contextStack, owner: this }); if ( docFrag ) { this.bubble = function () { this.node.styleSheet.cssText = this.fragment.toString(); }; } } else { this.fragment = new DomFragment({ descriptor: descriptor.f, root: root, parentNode: this.node, contextStack: parentFragment.contextStack, owner: this }); if ( docFrag ) { this.node.appendChild( this.fragment.docFrag ); } } } } // create event proxies if ( docFrag && descriptor.v ) { for ( eventName in descriptor.v ) { if ( hasOwn.call( descriptor.v, eventName ) ) { eventNames = eventName.split( '-' ); i = eventNames.length; while ( i-- ) { this.addEventProxy( eventNames[i], descriptor.v[ eventName ], parentFragment.contextStack ); } } } } // set attributes this.attributes = []; bindable = []; // save these till the end for ( attrName in descriptor.a ) { if ( hasOwn.call( descriptor.a, attrName ) ) { attrValue = descriptor.a[ attrName ]; attr = new DomAttribute({ element: this, name: attrName, value: ( attrValue === undefined ? null : attrValue ), root: root, parentNode: this.node, contextStack: parentFragment.contextStack }); this.attributes[ this.attributes.length ] = attr; if ( attr.isBindable ) { bindable.push( attr ); } if ( attr.isTwowayNameAttr ) { twowayNameAttr = attr; } else { attr.update(); } } } // if we're actually rendering (i.e. not server-side stringifying), proceed if ( docFrag ) { while ( bindable.length ) { bindable.pop().bind( this.root.lazy ); } if ( twowayNameAttr ) { twowayNameAttr.updateViewModel(); twowayNameAttr.update(); } docFrag.appendChild( this.node ); // trigger intro transition if ( descriptor.t1 ) { executeTransition( descriptor.t1, root, this, parentFragment.contextStack, true ); } } }; DomElement.prototype = { addEventProxy: function ( triggerEventName, proxyDescriptor, contextStack ) { var self = this, root = this.root, proxyName, proxyArgs, dynamicArgs, reuseable, definition, listener, fragment, handler, comboKey; // Note the current context - this can be useful with event handlers if ( !this.node._ractive ) { defineProperty( this.node, '_ractive', { value: { keypath: ( contextStack.length ? contextStack[ contextStack.length - 1 ] : '' ), index: this.parentFragment.indexRefs } }); } if ( typeof proxyDescriptor === 'string' ) { proxyName = proxyDescriptor; } else { proxyName = proxyDescriptor.n; } // This key uniquely identifies this trigger+proxy name combo on this element comboKey = triggerEventName + '=' + proxyName; if ( proxyDescriptor.a ) { proxyArgs = proxyDescriptor.a; } else if ( proxyDescriptor.d ) { dynamicArgs = true; proxyArgs = new StringFragment({ descriptor: proxyDescriptor.d, root: this.root, owner: this, contextStack: contextStack }); if ( !this.proxyFrags ) { this.proxyFrags = []; } this.proxyFrags[ this.proxyFrags.length ] = proxyArgs; } if ( proxyArgs !== undefined ) { // store arguments on the element, so we can reuse the same handler // with multiple elements if ( this.node._ractive[ comboKey ] ) { throw new Error( 'You cannot have two proxy events with the same trigger event (' + comboKey + ')' ); } this.node._ractive[ comboKey ] = { dynamic: dynamicArgs, payload: proxyArgs }; } // Is this a custom event? if ( definition = ( root.eventDefinitions[ triggerEventName ] || Ractive.eventDefinitions[ triggerEventName ] ) ) { // If the proxy is a string (e.g. {{item}}) then // we can reuse the handler. This eliminates the need for event delegation if ( !root._customProxies[ comboKey ] ) { root._customProxies[ comboKey ] = function ( proxyEvent ) { var args, payload; if ( !proxyEvent.node ) { throw new Error( 'Proxy event definitions must fire events with a `node` property' ); } proxyEvent.keypath = proxyEvent.node._ractive.keypath; proxyEvent.context = root.get( proxyEvent.keypath ); proxyEvent.index = proxyEvent.node._ractive.index; if ( proxyEvent.node._ractive[ comboKey ] ) { args = proxyEvent.node._ractive[ comboKey ]; payload = args.dynamic ? args.payload.toJson() : args.payload; } root.fire( proxyName, proxyEvent, payload ); }; } handler = root._customProxies[ comboKey ]; // Use custom event. Apply definition to this node listener = definition( this.node, handler ); this.customEventListeners[ this.customEventListeners.length ] = listener; return; } // If not, we just need to check it is a valid event for this element // warn about invalid event handlers, if we're in debug mode if ( this.node[ 'on' + triggerEventName ] !== undefined && root.debug ) { if ( console && console.warn ) { console.warn( 'Invalid event handler (' + triggerEventName + ')' ); } } if ( !root._proxies[ comboKey ] ) { root._proxies[ comboKey ] = function ( event ) { var args, payload, proxyEvent = { node: this, original: event, keypath: this._ractive.keypath, context: root.get( this._ractive.keypath ), index: this._ractive.index }; if ( this._ractive && this._ractive[ comboKey ] ) { args = this._ractive[ comboKey ]; payload = args.dynamic ? args.payload.toJson() : args.payload; } root.fire( proxyName, proxyEvent, payload ); }; } handler = root._proxies[ comboKey ]; this.eventListeners[ this.eventListeners.length ] = { n: triggerEventName, h: handler }; this.node.addEventListener( triggerEventName, handler ); }, teardown: function ( detach ) { var self = this, tearThisDown, transitionManager, transitionName, transitionParams, listener, outro; // Children first. that way, any transitions on child elements will be // handled by the current transitionManager if ( self.fragment ) { self.fragment.teardown( false ); } while ( self.attributes.length ) { self.attributes.pop().teardown(); } while ( self.eventListeners.length ) { listener = self.eventListeners.pop(); self.node.removeEventListener( listener.n, listener.h ); } while ( self.customEventListeners.length ) { self.customEventListeners.pop().teardown(); } if ( this.proxyFrags ) { while ( this.proxyFrags.length ) { this.proxyFrags.pop().teardown(); } } if ( this.descriptor.t2 ) { executeTransition( this.descriptor.t2, this.root, this, this.parentFragment.contextStack, false ); } if ( detach ) { this.root._transitionManager.detachWhenReady( this.node ); } }, firstNode: function () { return this.node; }, findNextNode: function ( fragment ) { return null; }, bubble: function () { // noop - just so event proxy and transition fragments have something to call! }, toString: function () { var str, i, len, attr; // TODO void tags str = '' + '<' + this.descriptor.e; len = this.attributes.length; for ( i=0; i'; return str; } }; DomFragment = function ( options ) { if ( options.parentNode ) { this.docFrag = doc.createDocumentFragment(); } // if we have an HTML string, our job is easy. if ( typeof options.descriptor === 'string' ) { this.html = options.descriptor; if ( this.docFrag ) { this.nodes = insertHtml( options.descriptor, this.docFrag ); } return; // prevent the rest of the init sequence } // otherwise we need to make a proper fragment initFragment( this, options ); }; DomFragment.prototype = { createItem: function ( options ) { if ( typeof options.descriptor === 'string' ) { return new DomText( options, this.docFrag ); } switch ( options.descriptor.t ) { case INTERPOLATOR: return new DomInterpolator( options, this.docFrag ); case SECTION: return new DomSection( options, this.docFrag ); case TRIPLE: return new DomTriple( options, this.docFrag ); case ELEMENT: return new DomElement( options, this.docFrag ); case PARTIAL: return new DomPartial( options, this.docFrag ); default: throw new Error( 'WTF? not sure what happened here...' ); } }, teardown: function ( detach ) { var node; // if this was built from HTML, we just need to remove the nodes if ( detach && this.nodes ) { while ( this.nodes.length ) { node = this.nodes.pop(); node.parentNode.removeChild( node ); } return; } // otherwise we need to do a proper teardown if ( !this.items ) { return; } while ( this.items.length ) { this.items.pop().teardown( detach ); } }, firstNode: function () { if ( this.items && this.items[0] ) { return this.items[0].firstNode(); } else if ( this.nodes ) { return this.nodes[0] || null; } return null; }, findNextNode: function ( item ) { var index = item.index; if ( this.items[ index + 1 ] ) { return this.items[ index + 1 ].firstNode(); } // if this is the root fragment, and there are no more items, // it means we're at the end if ( this.owner === this.root ) { return null; } return this.owner.findNextNode( this ); }, toString: function () { var html, i, len, item; if ( this.html ) { return this.html; } html = ''; if ( !this.items ) { return html; } len = this.items.length; for ( i=0; i', '>' ); } }; // Partials DomPartial = function ( options, docFrag ) { var parentFragment = this.parentFragment = options.parentFragment, descriptor; this.type = PARTIAL; this.name = options.descriptor.r; descriptor = getPartialDescriptor( parentFragment.root, options.descriptor.r ); this.fragment = new DomFragment({ descriptor: descriptor, root: parentFragment.root, parentNode: parentFragment.parentNode, contextStack: parentFragment.contextStack, owner: this }); if ( docFrag ) { docFrag.appendChild( this.fragment.docFrag ); } }; DomPartial.prototype = { findNextNode: function () { return this.parentFragment.findNextNode( this ); }, teardown: function ( detach ) { this.fragment.teardown( detach ); }, toString: function () { return this.fragment.toString(); } }; // Section DomSection = function ( options, docFrag ) { this.type = SECTION; this.fragments = []; this.length = 0; // number of times this section is rendered if ( docFrag ) { this.docFrag = doc.createDocumentFragment(); } this.initialising = true; initMustache( this, options ); if ( docFrag ) { docFrag.appendChild( this.docFrag ); } this.initialising = false; }; DomSection.prototype = { update: updateMustache, resolve: resolveMustache, smartUpdate: function ( methodName, args ) { var fragmentOptions, i; if ( methodName === 'push' || methodName === 'unshift' || methodName === 'splice' ) { fragmentOptions = { descriptor: this.descriptor.f, root: this.root, parentNode: this.parentNode, owner: this }; if ( this.descriptor.i ) { fragmentOptions.indexRef = this.descriptor.i; } } if ( this[ methodName ] ) { // if not, it's sort or reverse, which doesn't affect us (i.e. our length) this[ methodName ]( fragmentOptions, args ); } }, pop: function () { // teardown last fragment if ( this.length ) { this.fragments.pop().teardown( true ); this.length -= 1; } }, push: function ( fragmentOptions, args ) { var start, end, i; // append list item to context stack start = this.length; end = start + args.length; for ( i=start; i items.3 - the keypaths, // context stacks and index refs will have changed) reassignStart = ( start + addedItems ); reassignFragments( this.root, this, reassignStart, this.length, balance ); }, teardown: function ( detach ) { this.teardownFragments( detach ); teardown( this ); }, firstNode: function () { if ( this.fragments[0] ) { return this.fragments[0].firstNode(); } return this.parentFragment.findNextNode( this ); }, findNextNode: function ( fragment ) { if ( this.fragments[ fragment.index + 1 ] ) { return this.fragments[ fragment.index + 1 ].firstNode(); } return this.parentFragment.findNextNode( this ); }, teardownFragments: function ( detach ) { while ( this.fragments.length ) { this.fragments.shift().teardown( detach ); } }, render: function ( value ) { updateSection( this, value ); if ( !this.initialising ) { // we need to insert the contents of our document fragment into the correct place this.parentNode.insertBefore( this.docFrag, this.parentFragment.findNextNode( this ) ); } }, createFragment: function ( options ) { var fragment = new DomFragment( options ); if ( this.docFrag ) { this.docFrag.appendChild( fragment.docFrag ); } return fragment; }, toString: function () { var str, i, len; str = ''; i = 0; len = this.length; for ( i=0; i', '>' ); } }; // Triple DomTriple = function ( options, docFrag ) { this.type = TRIPLE; if ( docFrag ) { this.nodes = []; this.docFrag = doc.createDocumentFragment(); } this.initialising = true; initMustache( this, options ); if ( docFrag ) { docFrag.appendChild( this.docFrag ); } this.initialising = false; }; DomTriple.prototype = { update: updateMustache, resolve: resolveMustache, teardown: function ( detach ) { // remove child nodes from DOM if ( detach ) { while ( this.nodes.length ) { this.parentNode.removeChild( this.nodes.pop() ); } } teardown( this ); }, firstNode: function () { if ( this.nodes[0] ) { return this.nodes[0]; } return this.parentFragment.findNextNode( this ); }, render: function ( html ) { // remove existing nodes while ( this.nodes.length ) { this.parentNode.removeChild( this.nodes.pop() ); } if ( html === undefined ) { this.nodes = []; return; } // get new nodes this.nodes = insertHtml( html, this.docFrag ); if ( !this.initialising ) { this.parentNode.insertBefore( this.docFrag, this.parentFragment.findNextNode( this ) ); } }, toString: function () { return ( this.value !== undefined ? this.value : '' ); } }; StringFragment = function ( options ) { initFragment( this, options ); }; StringFragment.prototype = { createItem: function ( options ) { if ( typeof options.descriptor === 'string' ) { return new StringText( options.descriptor ); } switch ( options.descriptor.t ) { case INTERPOLATOR: return new StringInterpolator( options ); case TRIPLE: return new StringInterpolator( options ); case SECTION: return new StringSection( options ); default: throw 'Something went wrong in a rather interesting way'; } }, bubble: function () { this.owner.bubble(); }, teardown: function () { var numItems, i; numItems = this.items.length; for ( i=0; i element, preserve whitespace within preserveWhitespace = ( preserveWhitespace || this.lcTag === 'pre' ); if ( firstToken.attrs ) { filtered = filterAttrs( firstToken.attrs ); attrs = filtered.attrs; proxies = filtered.proxies; // remove event attributes (e.g. onclick='doSomething()') if we're sanitizing if ( parser.options.sanitize && parser.options.sanitize.eventAttributes ) { attrs = attrs.filter( sanitize ); } getFrag = function ( attr ) { var lcName = attr.name.toLowerCase(); return { name: ( svgCamelCaseAttributesMap[ lcName ] ? svgCamelCaseAttributesMap[ lcName ] : lcName ), value: getFragmentStubFromTokens( attr.value ) }; }; processProxy = function ( proxy ) { var processed, domEventName, match, tokens, proxyName, proxyArgs, colonIndex, throwError; throwError = function () { throw new Error( 'Illegal proxy event' ); }; if ( !proxy.name || !proxy.value ) { throwError(); } processed = { domEventName: proxy.name }; tokens = proxy.value; // proxy event names must start with a string (no mustaches) if ( tokens[0].type !== TEXT ) { throwError(); } colonIndex = tokens[0].value.indexOf( ':' ); // if no arguments are specified... if ( colonIndex === -1 ) { // ...the proxy name must be string-only (no mustaches) if ( tokens.length > 1 ) { throwError(); } processed.name = tokens[0].value; } else { processed.name = tokens[0].value.substr( 0, colonIndex ); tokens[0].value = tokens[0].value.substring( colonIndex + 1 ); if ( !tokens[0].value ) { tokens.shift(); } // can we parse it yet? if ( tokens.length === 1 && tokens[0].type === TEXT ) { try { processed.args = JSON.parse( tokens[0].value ); } catch ( err ) { processed.args = tokens[0].value; } } processed.dynamicArgs = getFragmentStubFromTokens( tokens ); } return processed; }; if ( attrs.length ) { this.attributes = attrs.map( getFrag ); } if ( proxies.length ) { this.proxies = proxies.map( processProxy ); } // TODO rename this helper function if ( filtered.intro ) { this.intro = processProxy( filtered.intro ); } if ( filtered.outro ) { this.outro = processProxy( filtered.outro ); } } if ( firstToken.selfClosing ) { this.selfClosing = true; } if ( voidElementNames.indexOf( this.lcTag ) !== -1 ) { this.isVoid = true; } // if self-closing or a void element, close if ( this.selfClosing || this.isVoid ) { return; } this.siblings = siblingsByTagName[ this.lcTag ]; this.items = []; next = parser.next(); while ( next ) { // section closing mustache should also close this element, e.g. //
    {{#items}}
  • {{content}}{{/items}}
if ( next.mustacheType === CLOSING ) { break; } if ( next.type === TAG ) { // closing tag if ( next.closing ) { // it's a closing tag, which means this element is closed... if ( next.name.toLowerCase() === this.lcTag ) { parser.pos += 1; } break; } // sibling element, which closes this element implicitly else if ( this.siblings && ( this.siblings.indexOf( next.name.toLowerCase() ) !== -1 ) ) { break; } } this.items[ this.items.length ] = getItem( parser ); next = parser.next(); } // if we're not preserving whitespace, we can eliminate inner leading and trailing whitespace if ( !preserveWhitespace ) { item = this.items[0]; if ( item && item.type === TEXT ) { item.text = item.text.replace( leadingWhitespace, '' ); if ( !item.text ) { this.items.shift(); } } item = this.items[ this.items.length - 1 ]; if ( item && item.type === TEXT ) { item.text = item.text.replace( trailingWhitespace, '' ); if ( !item.text ) { this.items.pop(); } } } }; Element.prototype = { toJson: function ( noStringify ) { var json, name, value, str, itemStr, proxy, match, i, len; json = { t: ELEMENT, e: this.tag }; if ( this.attributes && this.attributes.length ) { json.a = {}; len = this.attributes.length; for ( i=0; i`]/.test( attrValueStr ) ) { attrStr += '"' + attrValueStr.replace( /"/g, '"' ) + '"'; } else { attrStr += attrValueStr; } } } str += attrStr; } } // if this isn't a void tag, but is self-closing, add a solidus. Aaaaand, we're done if ( this.selfClosing && !isVoid ) { str += '/>'; return ( this.str = str ); } str += '>'; // void element? we're done if ( isVoid ) { return ( this.str = str ); } // if this has children, add them str += fragStr; str += ''; return ( this.str = str ); } }; voidElementNames = 'area base br col command embed hr img input keygen link meta param source track wbr'.split( ' ' ); allElementNames = 'a abbr acronym address applet area b base basefont bdo big blockquote body br button caption center cite code col colgroup dd del dfn dir div dl dt em fieldset font form frame frameset h1 h2 h3 h4 h5 h6 head hr html i iframe img input ins isindex kbd label legend li link map menu meta noframes noscript object ol optgroup option p param pre q s samp script select small span strike strong style sub sup textarea title tt u ul var article aside audio bdi canvas command data datagrid datalist details embed eventsource figcaption figure footer header hgroup keygen mark meter nav output progress ruby rp rt section source summary time track video wbr'.split( ' ' ); closedByParentClose = 'li dd rt rp optgroup option tbody tfoot tr td th'.split( ' ' ); 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 glyphRef gradientTransform gradientTransform gradientUnits gradientUnits kernelMatrix kernelUnitLength kernelUnitLength 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 specularExponent spreadMethod spreadMethod startOffset stdDeviation stitchTiles surfaceScale surfaceScale systemLanguage tableValues targetX targetY textLength textLength viewBox viewTarget xChannelSelector yChannelSelector zoomAndPan'.split( ' ' ); mapToLowerCase = function ( items ) { var map = {}, i = items.length; while ( i-- ) { map[ items[i].toLowerCase() ] = items[i]; } return map; }; svgCamelCaseElementsMap = mapToLowerCase( svgCamelCaseElements ); svgCamelCaseAttributesMap = mapToLowerCase( svgCamelCaseAttributes ); siblingsByTagName = { li: [ 'li' ], dt: [ 'dt', 'dd' ], dd: [ 'dt', 'dd' ], p: 'address article aside blockquote dir div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr menu nav ol p pre section table ul'.split( ' ' ), rt: [ 'rt', 'rp' ], rp: [ 'rp', 'rt' ], optgroup: [ 'optgroup' ], option: [ 'option', 'optgroup' ], thead: [ 'tbody', 'tfoot' ], tbody: [ 'tbody', 'tfoot' ], tr: [ 'tr' ], td: [ 'td', 'th' ], th: [ 'td', 'th' ] }; sanitize = function ( attr ) { return attr.name.substr( 0, 2 ) !== 'on'; }; onlyAttrs = function ( attr ) { return attr.name.substr( 0, 6 ) !== 'proxy-'; }; onlyProxies = function ( attr ) { if ( attr.name.substr( 0, 6 ) === 'proxy-' ) { attr.name = attr.name.substring( 6 ); return true; } return false; }; filterAttrs = function ( items ) { var attrs, proxies, filtered, i, len, item; filtered = {}; attrs = []; proxies = []; len = items.length; for ( i=0; i': PARTIAL, '!': COMMENT, '&': INTERPOLATOR }; getMustacheType = function ( tokenizer ) { var type = mustacheTypes[ tokenizer.str.charAt( tokenizer.pos ) ]; if ( !type ) { return null; } tokenizer.pos += 1; return type; }; getIndexRef = getRegexMatcher( /^\s*:\s*([a-zA-Z_$][a-zA-Z_$0-9]*)/ ); }()); // tag (function () { var getOpeningTag, getClosingTag, getTagName, getAttributes, getAttribute, getAttributeName, getAttributeValue, getUnquotedAttributeValue, getUnquotedAttributeValueToken, getUnquotedAttributeValueText, getSingleQuotedAttributeValue, getSingleQuotedStringToken, getDoubleQuotedAttributeValue, getDoubleQuotedStringToken; getTag = function ( tokenizer ) { return ( getOpeningTag( tokenizer ) || getClosingTag( tokenizer ) ); }; getOpeningTag = function ( tokenizer ) { var start, tag, attrs; start = tokenizer.pos; if ( !getStringMatch( tokenizer, '<' ) ) { return null; } tag = { type: TAG }; // tag name tag.name = getTagName( tokenizer ); if ( !tag.name ) { tokenizer.pos = start; return null; } // attributes attrs = getAttributes( tokenizer ); if ( attrs ) { tag.attrs = attrs; } // self-closing solidus? if ( getStringMatch( tokenizer, '/' ) ) { tag.selfClosing = true; } // closing angle bracket if ( !getStringMatch( tokenizer, '>' ) ) { tokenizer.pos = start; return null; } return tag; }; getClosingTag = function ( tokenizer ) { var start, tag; start = tokenizer.pos; if ( !getStringMatch( tokenizer, '<' ) ) { return null; } tag = { type: TAG, closing: true }; // closing solidus if ( !getStringMatch( tokenizer, '/' ) ) { throw new Error( 'Unexpected character ' + tokenizer.remaining().charAt( 0 ) + ' (expected "/")' ); } // tag name tag.name = getTagName( tokenizer ); if ( !tag.name ) { throw new Error( 'Unexpected character ' + tokenizer.remaining().charAt( 0 ) + ' (expected tag name)' ); } // closing angle bracket if ( !getStringMatch( tokenizer, '>' ) ) { throw new Error( 'Unexpected character ' + tokenizer.remaining().charAt( 0 ) + ' (expected ">")' ); } return tag; }; getTagName = getRegexMatcher( /^[a-zA-Z][a-zA-Z0-9]*/ ); getAttributes = function ( tokenizer ) { var start, attrs, attr; start = tokenizer.pos; allowWhitespace( tokenizer ); attr = getAttribute( tokenizer ); if ( !attr ) { tokenizer.pos = start; return null; } attrs = []; while ( attr !== null ) { attrs[ attrs.length ] = attr; allowWhitespace( tokenizer ); attr = getAttribute( tokenizer ); } return attrs; }; getAttribute = function ( tokenizer ) { var attr, name, value; name = getAttributeName( tokenizer ); if ( !name ) { return null; } attr = { name: name }; value = getAttributeValue( tokenizer ); if ( value ) { attr.value = value; } return attr; }; getAttributeName = getRegexMatcher( /^[^\s"'>\/=]+/ ); getAttributeValue = function ( tokenizer ) { var start, value; start = tokenizer.pos; allowWhitespace( tokenizer ); if ( !getStringMatch( tokenizer, '=' ) ) { tokenizer.pos = start; return null; } value = getSingleQuotedAttributeValue( tokenizer ) || getDoubleQuotedAttributeValue( tokenizer ) || getUnquotedAttributeValue( tokenizer ); if ( value === null ) { tokenizer.pos = start; return null; } return value; }; getUnquotedAttributeValueText = getRegexMatcher( /^[^\s"'=<>`]+/ ); getUnquotedAttributeValueToken = function ( tokenizer ) { var start, text, index; start = tokenizer.pos; text = getUnquotedAttributeValueText( tokenizer ); if ( !text ) { return null; } if ( ( index = text.indexOf( tokenizer.delimiters[0] ) ) !== -1 ) { text = text.substr( 0, index ); tokenizer.pos = start + text.length; } return { type: TEXT, value: text }; }; getUnquotedAttributeValue = function ( tokenizer ) { var tokens, token; tokens = []; token = getMustache( tokenizer ) || getUnquotedAttributeValueToken( tokenizer ); while ( token !== null ) { tokens[ tokens.length ] = token; token = getMustache( tokenizer ) || getUnquotedAttributeValueToken( tokenizer ); } if ( !tokens.length ) { return null; } return tokens; }; getSingleQuotedStringToken = function ( tokenizer ) { var start, text, index; start = tokenizer.pos; text = getSingleQuotedString( tokenizer ); if ( !text ) { return null; } if ( ( index = text.indexOf( tokenizer.delimiters[0] ) ) !== -1 ) { text = text.substr( 0, index ); tokenizer.pos = start + text.length; } return { type: TEXT, value: text }; }; getSingleQuotedAttributeValue = function ( tokenizer ) { var start, tokens, token; start = tokenizer.pos; if ( !getStringMatch( tokenizer, "'" ) ) { return null; } tokens = []; token = getMustache( tokenizer ) || getSingleQuotedStringToken( tokenizer ); while ( token !== null ) { tokens[ tokens.length ] = token; token = getMustache( tokenizer ) || getSingleQuotedStringToken( tokenizer ); } if ( !getStringMatch( tokenizer, "'" ) ) { tokenizer.pos = start; return null; } return tokens; }; getDoubleQuotedStringToken = function ( tokenizer ) { var start, text, index; start = tokenizer.pos; text = getDoubleQuotedString( tokenizer ); if ( !text ) { return null; } if ( ( index = text.indexOf( tokenizer.delimiters[0] ) ) !== -1 ) { text = text.substr( 0, index ); tokenizer.pos = start + text.length; } return { type: TEXT, value: text }; }; getDoubleQuotedAttributeValue = function ( tokenizer ) { var start, tokens, token; start = tokenizer.pos; if ( !getStringMatch( tokenizer, '"' ) ) { return null; } tokens = []; token = getMustache( tokenizer ) || getDoubleQuotedStringToken( tokenizer ); while ( token !== null ) { tokens[ tokens.length ] = token; token = getMustache( tokenizer ) || getDoubleQuotedStringToken( tokenizer ); } if ( !getStringMatch( tokenizer, '"' ) ) { tokenizer.pos = start; return null; } return tokens; }; }()); // text (function () { getText = function ( tokenizer ) { var minIndex, text; minIndex = tokenizer.str.length; // anything goes except opening delimiters or a '<' [ tokenizer.delimiters[0], tokenizer.tripleDelimiters[0], '<' ].forEach( function ( substr ) { var index = tokenizer.str.indexOf( substr, tokenizer.pos ); if ( index !== -1 ) { minIndex = Math.min( index, minIndex ); } }); if ( minIndex === tokenizer.pos ) { return null; } text = tokenizer.str.substring( tokenizer.pos, minIndex ); tokenizer.pos = minIndex; return { type: TEXT, value: text }; }; }()); // expression (function () { var getExpressionList, makePrefixSequenceMatcher, makeInfixSequenceMatcher, getRightToLeftSequenceMatcher, getBracketedExpression, getPrimary, getMember, getInvocation, getTypeOf, getLogicalOr, getConditional, getDigits, getExponent, getFraction, getInteger, getReference, getRefinement, getLiteral, getArrayLiteral, getBooleanLiteral, getNumberLiteral, getStringLiteral, getObjectLiteral, getGlobal, getKeyValuePairs, getKeyValuePair, getKey, globals; getExpression = function ( tokenizer ) { var start, expression, fns, fn, i, len; start = tokenizer.pos; // 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. expression = getConditional( tokenizer ); return expression; }; getExpressionList = function ( tokenizer ) { var start, expressions, expr, next; start = tokenizer.pos; allowWhitespace( tokenizer ); expr = getExpression( tokenizer ); if ( expr === null ) { return null; } expressions = [ expr ]; // allow whitespace between expression and ',' allowWhitespace( tokenizer ); if ( getStringMatch( tokenizer, ',' ) ) { next = getExpressionList( tokenizer ); if ( next === null ) { tokenizer.pos = start; return null; } expressions = expressions.concat( next ); } return expressions; }; getBracketedExpression = function ( tokenizer ) { var start, expr; start = tokenizer.pos; if ( !getStringMatch( tokenizer, '(' ) ) { return null; } allowWhitespace( tokenizer ); expr = getExpression( tokenizer ); if ( !expr ) { tokenizer.pos = start; return null; } allowWhitespace( tokenizer ); if ( !getStringMatch( tokenizer, ')' ) ) { tokenizer.pos = start; return null; } return { t: BRACKETED, x: expr }; }; getPrimary = function ( tokenizer ) { return getLiteral( tokenizer ) || getReference( tokenizer ) || getBracketedExpression( tokenizer ); }; getMember = function ( tokenizer ) { var start, expression, name, refinement, member; expression = getPrimary( tokenizer ); if ( !expression ) { return null; } refinement = getRefinement( tokenizer ); if ( !refinement ) { return expression; } while ( refinement !== null ) { member = { t: MEMBER, x: expression, r: refinement }; expression = member; refinement = getRefinement( tokenizer ); } return member; }; getInvocation = function ( tokenizer ) { var start, expression, expressionList, result; expression = getMember( tokenizer ); if ( !expression ) { return null; } start = tokenizer.pos; if ( !getStringMatch( tokenizer, '(' ) ) { return expression; } allowWhitespace( tokenizer ); expressionList = getExpressionList( tokenizer ); allowWhitespace( tokenizer ); if ( !getStringMatch( tokenizer, ')' ) ) { tokenizer.pos = start; return expression; } result = { t: INVOCATION, x: expression }; if ( expressionList ) { result.o = expressionList; } return result; }; // right-to-left makePrefixSequenceMatcher = function ( symbol, fallthrough ) { return function ( tokenizer ) { var start, expression; if ( !getStringMatch( tokenizer, symbol ) ) { return fallthrough( tokenizer ); } start = tokenizer.pos; allowWhitespace( tokenizer ); expression = getExpression( tokenizer ); if ( !expression ) { fail( tokenizer, 'an expression' ); } return { s: symbol, o: expression, t: PREFIX_OPERATOR }; }; }; // create all prefix sequence matchers (function () { var i, len, matcher, prefixOperators, fallthrough; prefixOperators = '! ~ + - typeof'.split( ' ' ); // An invocation operator is higher precedence than logical-not fallthrough = getInvocation; for ( i=0, len=prefixOperators.length; i\s*([a-zA-Z_$][a-zA-Z_$0-9]*)\s*}\}\s*-->/; inlinePartialEnd = //; parse = function ( template, options ) { var tokens, fragmentStub, json, token; options = options || {}; // does this template include inline partials? if ( inlinePartialStart.test( template ) ) { return parseCompoundTemplate( template, options ); } 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 }; } tokens = tokenize( template, options ); if ( !options.preserveWhitespace ) { // remove first token if it only contains whitespace token = tokens[0]; if ( token && ( token.type === TEXT ) && onlyWhitespace.test( token.value ) ) { tokens.shift(); } // ditto last token token = tokens[ tokens.length - 1 ]; if ( token && ( token.type === TEXT ) && onlyWhitespace.test( token.value ) ) { tokens.pop(); } } fragmentStub = getFragmentStubFromTokens( tokens, options, options.preserveWhitespace ); json = fragmentStub.toJson(); if ( typeof json === 'string' ) { // If we return it as a string, Ractive will attempt to reparse it! // Instead we wrap it in an array. Ractive knows what to do then return [ json ]; } return json; }; parseCompoundTemplate = function ( template, options ) { var mainTemplate, remaining, partials, name, startMatch, endMatch; partials = {}; mainTemplate = ''; remaining = template; while ( startMatch = inlinePartialStart.exec( remaining ) ) { name = startMatch[1]; mainTemplate += remaining.substr( 0, startMatch.index ); remaining = remaining.substring( startMatch.index + startMatch[0].length ); endMatch = inlinePartialEnd.exec( remaining ); if ( !endMatch || endMatch[1] !== name ) { throw new Error( 'Inline partials must have a closing delimiter, and cannot be nested' ); } partials[ name ] = parse( remaining.substr( 0, endMatch.index ), options ); remaining = remaining.substring( endMatch.index + endMatch[0].length ); } return { template: parse( mainTemplate, options ), partials: partials }; }; }()); tokenize = function ( template, options ) { var tokenizer, tokens, token, last20, next20; options = options || {}; tokenizer = { str: stripHtmlComments( template ), pos: 0, delimiters: options.delimiters || [ '{{', '}}' ], tripleDelimiters: options.tripleDelimiters || [ '{{{', '}}}' ], remaining: function () { return tokenizer.str.substring( tokenizer.pos ); } }; tokens = []; while ( tokenizer.pos < tokenizer.str.length ) { token = getToken( tokenizer ); if ( token === null && tokenizer.remaining() ) { last20 = tokenizer.str.substr( 0, tokenizer.pos ).substr( -20 ); if ( last20.length === 20 ) { last20 = '...' + last20; } next20 = tokenizer.remaining().substr( 0, 20 ); if ( next20.length === 20 ) { next20 = next20 + '...'; } throw new Error( 'Could not parse template: ' + ( last20 ? last20 + '<- ' : '' ) + 'failed at character ' + tokenizer.pos + ' ->' + next20 ); } tokens[ tokens.length ] = token; } stripStandalones( tokens ); stripCommentTokens( tokens ); return tokens; }; Ractive.prototype = proto; Ractive.adaptors = adaptors; Ractive.eventDefinitions = eventDefinitions; Ractive.partials = {}; Ractive.easing = easing; Ractive.extend = extend; Ractive.interpolate = interpolate; Ractive.interpolators = interpolators; Ractive.parse = parse; // TODO add some more transitions Ractive.transitions = transitions; Ractive.VERSION = VERSION; // export as Common JS module... if ( typeof module !== "undefined" && module.exports ) { module.exports = Ractive; } // ... or as AMD module else if ( typeof define === "function" && define.amd ) { define( function () { return Ractive; }); } // ... or as browser global else { global.Ractive = Ractive; } }( this ));