ractive
Version:
Next-generation DOM manipulation
1,436 lines (1,072 loc) • 35.2 kB
JavaScript
/*! anglebars - v0.1.5 - 2013-03-18
* http://rich-harris.github.com/Anglebars/
* Copyright (c) 2013 Rich Harris; Licensed WTFPL */
/*jslint eqeq: true, plusplus: true */
/*global document, HTMLElement */
(function ( global ) {
;var Anglebars = Anglebars || {}; // in case we're not using the runtime
(function ( A ) {
'use strict';
var FragmentStub,
getFragmentStubFromTokens,
TextStub,
ElementStub,
SectionStub,
MustacheStub,
decodeCharacterReferences,
htmlEntities,
getFormatter,
types,
voidElementNames,
allElementNames,
closedByParentClose,
implicitClosersByTagName,
elementIsClosedBy;
A.compile = function ( template, options ) {
var tokens, fragmentStub;
options = options || {};
// If delimiters are specified use them, otherwise reset to defaults
A.delimiters = options.delimiters || [ '{{', '}}' ];
A.tripleDelimiters = options.tripleDelimiters || [ '{{{', '}}}' ];
tokens = A.tokenize( template );
fragmentStub = getFragmentStubFromTokens( tokens );
return fragmentStub.toJson();
};
types = A.types;
voidElementNames = [ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' ];
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', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', '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' ];
closedByParentClose = [ 'li', 'dd', 'rt', 'rp', 'optgroup', 'option', 'tbody', 'tfoot', 'tr', 'td', 'th' ];
implicitClosersByTagName = {
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' ],
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' ]
};
getFormatter = function ( str ) {
var match, name, argsStr, args, openIndex;
openIndex = str.indexOf( '[' );
if ( openIndex !== -1 ) {
name = str.substr( 0, openIndex );
argsStr = str.substring( openIndex + 1, str.length - 1 );
try {
args = JSON.parse( argsStr );
} catch ( err ) {
throw 'Could not parse arguments (' + argsStr + ') using JSON.parse';
}
return {
name: name,
args: args
};
}
return {
name: str
};
};
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 };
decodeCharacterReferences = function ( html ) {
var result;
// named entities
result = html.replace( /&([a-zA-Z]+);/, function ( match, name ) {
if ( htmlEntities[ name ] ) {
return String.fromCharCode( htmlEntities[ name ] );
}
return match;
});
// hex references
result = result.replace( /&#x([0-9]+);/, function ( match, hex ) {
return String.fromCharCode( parseInt( hex, 16 ) );
});
// decimal references
result = result.replace( /&#([0-9]+);/, function ( match, num ) {
return String.fromCharCode( num );
});
return result;
};
TextStub = function ( token ) {
this.text = token.value;
};
TextStub.prototype = {
toJson: function () {
// this will be used as text, so we need to decode things like &
return this.decoded || ( this.decoded = decodeCharacterReferences( this.text) );
},
toString: function () {
// this will be used as straight text
return this.text;
},
decodeCharacterReferences: function () {
}
};
ElementStub = function ( token, parentFragment ) {
var items, attributes, numAttributes, i;
this.type = types.ELEMENT;
this.tag = token.tag;
this.parentFragment = parentFragment;
this.parentElement = parentFragment.parentElement;
items = token.attributes.items;
numAttributes = items.length;
if ( numAttributes ) {
attributes = [];
for ( i=0; i<numAttributes; i+=1 ) {
attributes[i] = {
name: items[i].name.value,
value: getFragmentStubFromTokens( items[i].value.tokens, this.parentFragment.priority + 1 )
};
}
this.attributes = attributes;
}
// if this is a void element, or a self-closing tag, seal the element
if ( token.isSelfClosingTag || voidElementNames.indexOf( token.tag.toLowerCase() ) !== -1 ) {
return;
}
this.fragment = new FragmentStub( this, parentFragment.priority + 1 );
};
ElementStub.prototype = {
read: function ( token ) {
return this.fragment && this.fragment.read( token );
},
toJson: function ( noStringify ) {
var json, attrName, attrValue, str, i, fragStr;
json = {
type: types.ELEMENT,
tag: this.tag
};
if ( this.attributes ) {
json.attrs = {};
for ( i=0; i<this.attributes.length; i+=1 ) {
attrName = this.attributes[i].name;
// can we stringify the value?
str = this.attributes[i].value.toString();
if ( str !== false ) { // need to explicitly check, as '' === false
attrValue = str;
} else {
attrValue = this.attributes[i].value.toJson();
}
json.attrs[ attrName ] = attrValue;
}
}
if ( this.fragment && this.fragment.items.length ) {
json.frag = this.fragment.toJson( noStringify );
}
return json;
},
toString: function () {
var str, i, len, attrStr, attrValueStr, fragStr, itemStr, isVoid;
// if this isn't an HTML element, it can't be stringified (since the only reason to stringify an
// element is to use with innerHTML, and SVG doesn't support that method
if ( allElementNames.indexOf( this.tag.toLowerCase() ) === -1 ) {
return false;
}
// see if children can be stringified (i.e. don't contain mustaches)
fragStr = ( this.fragment ? this.fragment.toString() : '' );
if ( fragStr === false ) {
return false;
}
// is this a void element?
isVoid = ( voidElementNames.indexOf( this.tag.toLowerCase() ) !== -1 );
str = '<' + this.tag;
if ( this.attributes ) {
for ( i=0, len=this.attributes.length; i<len; i+=1 ) {
// does this look like a namespaced attribute? if so we can't stringify it
if ( this.attributes[i].name.indexOf( ':' ) !== -1 ) {
return false;
}
attrStr = ' ' + this.attributes[i].name;
attrValueStr = this.attributes[i].value.toString();
if ( attrValueStr === false ) {
return false;
}
if ( attrValueStr !== '' ) {
attrStr += '=';
// does it need to be quoted?
if ( /[\s"'=<>`]/.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.isSelfClosing && !isVoid ) {
str += '/>';
return str;
}
str += '>';
// void element? we're done
if ( isVoid ) {
return str;
}
// if this has children, add them
str += fragStr;
str += '</' + this.tag + '>';
return str;
}
};
SectionStub = function ( token, parentFragment ) {
this.type = types.SECTION;
this.parentFragment = parentFragment;
this.ref = token.ref;
this.inverted = ( token.type === types.INVERTED );
this.formatters = token.formatters;
this.i = token.i;
this.fragment = new FragmentStub( this, parentFragment.priority + 1 );
};
SectionStub.prototype = {
read: function ( token ) {
return this.fragment.read( token );
},
toJson: function ( noStringify ) {
var json;
json = {
type: types.SECTION,
ref: this.ref,
frag: this.fragment.toJson( noStringify )
};
if ( this.formatters && this.formatters.length ) {
json.fmtrs = this.formatters.map( getFormatter );
}
if ( this.inverted ) {
json.inv = true;
}
if ( this.priority ) {
json.p = this.parentFragment.priority;
}
if ( this.i ) {
json.i = this.i;
}
return json;
},
toString: function () {
// sections cannot be stringified
return false;
}
};
MustacheStub = function ( token, priority ) {
this.type = token.type;
this.priority = priority;
this.ref = token.ref;
this.formatters = token.formatters;
};
MustacheStub.prototype = {
toJson: function () {
var json = {
type: this.type,
ref: this.ref
};
if ( this.formatters ) {
json.fmtrs = this.formatters.map( getFormatter );
}
if ( this.priority ) {
json.p = this.priority;
}
return json;
},
toString: function () {
// mustaches cannot be stringified
return false;
}
};
FragmentStub = function ( owner, priority ) {
this.owner = owner;
this.items = [];
if ( owner ) {
this.parentElement = ( owner.type === types.ELEMENT ? owner : owner.parentElement );
}
this.priority = priority;
};
FragmentStub.prototype = {
read: function ( token ) {
if ( this.sealed ) {
return false;
}
// does this token implicitly close this fragment? (e.g. an <li> without a </li> being closed by another <li>)
if ( this.isImplicitlyClosedBy( token ) ) {
this.seal();
return false;
}
// do we have an open child section/element?
if ( this.currentChild ) {
// can it use this token?
if ( this.currentChild.read( token ) ) {
return true;
}
// if not, we no longer have an open child
this.currentChild = null;
}
// does this token explicitly close this fragment?
if ( this.isExplicitlyClosedBy( token ) ) {
this.seal();
return true;
}
// time to create a new child...
// (...unless this is a section closer or a delimiter change or a comment)
if ( token.type === types.CLOSING || token.type === types.DELIMCHANGE || token.type === types.COMMENT ) {
return false;
}
// section?
if ( token.type === types.SECTION || token.type === types.INVERTED ) {
this.currentChild = new SectionStub( token, this );
this.items[ this.items.length ] = this.currentChild;
return true;
}
// element?
if ( token.type === types.TAG ) {
this.currentChild = new ElementStub( token, this );
this.items[ this.items.length ] = this.currentChild;
return true;
}
// text or attribute value?
if ( token.type === types.TEXT || token.type === types.ATTR_VALUE_TOKEN ) {
this.items[ this.items.length ] = new TextStub( token );
return true;
}
// none of the above? must be a mustache
this.items[ this.items.length ] = new MustacheStub( token, this.priority );
return true;
},
isClosedBy: function ( token ) {
return this.isImplicitlyClosedBy( token ) || this.isExplicitlyClosedBy( token );
},
isImplicitlyClosedBy: function ( token ) {
var implicitClosers, element, parentElement, thisTag, tokenTag;
if ( !token.tag || !this.owner || ( this.owner.type !== types.ELEMENT ) ) {
return false;
}
thisTag = this.owner.tag.toLowerCase();
tokenTag = token.tag.toLowerCase();
element = this.owner;
parentElement = element.parentElement || null;
// if this is an element whose end tag can be omitted if followed by an element
// which is an 'implicit closer', return true
implicitClosers = implicitClosersByTagName[ thisTag ];
if ( implicitClosers ) {
if ( !token.isClosingTag && implicitClosers.indexOf( tokenTag ) !== -1 ) {
return true;
}
}
// if this is an element that is closed when its parent closes, return true
if ( closedByParentClose.indexOf( thisTag ) !== -1 ) {
if ( parentElement && parentElement.fragment.isClosedBy( token ) ) {
return true;
}
}
// special cases
// p element end tag can be omitted when parent closes if it is not an a element
if ( thisTag === 'p' ) {
if ( parentElement && parentElement.tag.toLowerCase() === 'a' && parentElement.fragment.isClosedBy( token ) ) {
return true;
}
}
},
isExplicitlyClosedBy: function ( token ) {
if ( !this.owner ) {
return false;
}
if ( this.owner.type === types.SECTION ) {
if ( token.type === types.CLOSING && token.ref === this.owner.ref ) {
return true;
}
}
if ( this.owner.type === types.ELEMENT && this.owner ) {
if ( token.isClosingTag && ( token.tag.toLowerCase() === this.owner.tag.toLowerCase() ) ) {
return true;
}
}
},
toJson: function ( noStringify ) {
var result = [], i, len, str;
// can we stringify this?
if ( !noStringify ) {
str = this.toString();
if ( str !== false ) {
return str;
}
}
for ( i=0, len=this.items.length; i<len; i+=1 ) {
result[i] = this.items[i].toJson( noStringify );
}
return result;
},
toString: function () {
var str = '', i, len, itemStr;
for ( i=0, len=this.items.length; i<len; i+=1 ) {
itemStr = this.items[i].toString();
// if one of the child items cannot be stringified (i.e. contains a mustache) return false
if ( itemStr === false ) {
return false;
}
str += itemStr;
}
return str;
},
seal: function () {
this.sealed = true;
}
};
getFragmentStubFromTokens = function ( tokens, priority ) {
var fragStub = new FragmentStub( null, priority || 0 ), token;
while ( tokens.length ) {
token = tokens.shift();
fragStub.read( token );
}
return fragStub;
};
}( Anglebars ));
/*global Anglebars */
/*jslint white: true */
(function ( A ) {
'use strict';
var types,
whitespace,
stripHtmlComments,
TokenStream,
MustacheBuffer,
TextToken,
MustacheToken,
TripleToken,
TagToken,
AttributeValueToken,
mustacheTypes,
OpeningBracket,
TagName,
AttributeCollection,
Solidus,
ClosingBracket,
Attribute,
AttributeName,
AttributeValue;
A.tokenize = function ( template ) {
var stream = TokenStream.fromString( stripHtmlComments( template ) );
return stream.tokens;
};
// TokenStream generates an array of tokens from an HTML string
TokenStream = function () {
this.tokens = [];
this.buffer = new MustacheBuffer();
};
TokenStream.prototype = {
read: function ( char ) {
var mustacheToken, bufferValue;
// if we're building a tag, send everything to it including delimiter characters
if ( this.currentToken && this.currentToken.type === types.TAG ) {
if ( this.currentToken.read( char ) ) {
return true;
}
}
// either we're not building a tag, or the character was rejected
// send to buffer. if accepted, we don't need to do anything else
if ( this.buffer.read( char ) ) {
return true;
}
// can we convert the buffer to a mustache or triple?
mustacheToken = this.buffer.convert();
if ( mustacheToken ) {
// if we were building a token, seal it
if ( this.currentToken ) {
this.currentToken.seal();
}
// start building the new mustache instead
this.currentToken = this.tokens[ this.tokens.length ] = mustacheToken;
return true;
}
// could not convert to a mustache. can we append to current token?
bufferValue = this.buffer.release();
if ( this.currentToken ) {
while ( bufferValue.length ) {
while ( bufferValue.length && this.currentToken.read( bufferValue.charAt( 0 ) ) ) {
bufferValue = bufferValue.substring( 1 );
}
// still got something left over? create a new token
if ( bufferValue.length ) {
if ( bufferValue.charAt( 0 ) === '<' ) {
this.currentToken = new TagToken();
this.currentToken.read( '<' );
} else {
this.currentToken = new TextToken();
this.currentToken.read( bufferValue.charAt( 0 ) );
}
this.tokens[ this.tokens.length ] = this.currentToken;
bufferValue = bufferValue.substring( 1 );
}
}
return true;
}
// otherwise we need to create a new token
if ( char === '<' ) {
this.currentToken = new TagToken();
} else {
this.currentToken = new TextToken();
}
this.currentToken.read( char );
this.tokens[ this.tokens.length ] = this.currentToken;
return true;
},
end: function () {
if ( !this.buffer.isEmpty() ) {
this.tokens[ this.tokens.length ] = this.buffer.convert();
}
}
};
TokenStream.fromString = function ( string ) {
var stream, i, len;
stream = new TokenStream();
i = 0;
len = string.length;
while ( i < len ) {
stream.read( string.charAt( i ) );
i += 1;
}
stream.end();
return stream;
};
// MustacheBuffer intercepts characters in the token stream and determines
// whether they could be a mustache/triple delimiter
MustacheBuffer = function () {
this.value = '';
};
MustacheBuffer.prototype = {
read: function ( char ) {
var continueBuffering;
this.value += char;
// if this could turn out to be a tag, a mustache or a triple return true
continueBuffering = ( this.isPartialMatchOf( A.delimiters[0] ) || this.isPartialMatchOf( A.tripleDelimiters[0] ) );
return continueBuffering;
},
convert: function () {
var value, mustache, triple, token, getTriple, getMustache;
// store mustache and triple opening delimiters
mustache = A.delimiters[0];
triple = A.tripleDelimiters[0];
value = this.value;
getTriple = function () {
if ( value.indexOf( triple ) === 0 ) {
return new TripleToken();
}
};
getMustache = function () {
if ( value.indexOf( mustache ) === 0 ) {
return new MustacheToken();
}
};
// out of mustache and triple opening delimiters, try to match longest first.
// if they're the same length then only one will match anyway, unless some
// plonker has set them to the same thing (which should probably throw an error)
if ( triple.length > mustache.length ) {
token = getTriple() || getMustache();
} else {
token = getMustache() || getTriple();
}
if ( token ) {
while ( this.value.length ) {
token.read( this.value.charAt( 0 ) );
this.value = this.value.substring( 1 );
}
return token;
}
return false;
},
release: function () {
var value = this.value;
this.value = '';
return value;
},
isEmpty: function () {
return !this.value.length;
},
isPartialMatchOf: function ( str ) {
// if str begins with this.value, the index will be 0
return str.indexOf( this.value ) === 0;
}
};
TextToken = function () {
this.type = types.TEXT;
this.value = '';
};
TextToken.prototype = {
read: function ( char ) {
if ( this.sealed ) {
return false;
}
// this can be anything except a '<'
if ( char === '<' ) {
return false;
}
this.value += char;
return true;
},
seal: function () {
this.sealed = true;
}
};
MustacheToken = function () {
this.value = '';
this.openingDelimiter = A.delimiters[0];
this.closingDelimiter = A.delimiters[1];
};
TripleToken = function () {
this.value = '';
this.openingDelimiter = A.tripleDelimiters[0];
this.closingDelimiter = A.tripleDelimiters[1];
this.type = types.TRIPLE;
};
MustacheToken.prototype = TripleToken.prototype = {
read: function ( char ) {
if ( this.sealed ) {
return false;
}
this.value += char;
if ( this.value.substr( -this.closingDelimiter.length ) === this.closingDelimiter ) {
this.seal();
}
return true;
},
seal: function () {
var trimmed, firstChar, identifiers, pattern, match;
if ( this.sealed ) {
return;
}
// lop off opening and closing delimiters, and leading/trailing whitespace
trimmed = this.value.replace( this.openingDelimiter, '' ).replace( this.closingDelimiter, '' ).trim();
// are we dealing with a delimiter change?
if ( trimmed.charAt( 0 ) === '=' ) {
this.changeDelimiters( trimmed );
this.type = types.DELIMCHANGE;
}
// if type isn't TRIPLE or DELIMCHANGE, determine from first character
if ( !this.type ) {
firstChar = trimmed.charAt( 0 );
if ( mustacheTypes[ firstChar ] ) {
this.type = mustacheTypes[ firstChar ];
trimmed = trimmed.substring( 1 ).trim();
} else {
this.type = types.INTERPOLATOR;
}
}
// do we have a named index?
if ( this.type === types.SECTION ) {
pattern = /:\s*([a-zA-Z_$][a-zA-Z0-9_$]*)$/;
match = pattern.exec( trimmed );
if ( match ) {
this.i = match[1];
trimmed = trimmed.substr( 0, trimmed.length - match[0].length );
}
}
// get reference and any formatters
identifiers = trimmed.split( '|' );
this.ref = identifiers.shift().trim();
if ( identifiers.length ) {
this.formatters = identifiers.map( function ( name ) {
return name.trim();
});
}
// TODO
this.sealed = true;
},
changeDelimiters: function ( str ) {
var delimiters, newDelimiters;
newDelimiters = /\=([^\s=]+)\s+([^\s=]+)=/.exec( str );
delimiters = ( this.type === types.TRIPLE ? A.tripleDelimiters : A.delimiters );
delimiters[0] = newDelimiters[1];
delimiters[1] = newDelimiters[2];
}
};
TagToken = function () {
this.type = types.TAG;
this.openingBracket = new OpeningBracket();
this.closingTagSolidus = new Solidus();
this.tagName = new TagName();
this.attributes = new AttributeCollection();
this.selfClosingSolidus = new Solidus();
this.closingBracket = new ClosingBracket();
};
TagToken.prototype = {
read: function ( char ) {
var accepted;
if ( this.sealed ) {
return false;
}
// if there is room for this character, read it
accepted = this.openingBracket.read( char ) ||
this.closingTagSolidus.read( char ) ||
this.tagName.read( char ) ||
this.attributes.read( char ) ||
this.selfClosingSolidus.read( char ) ||
this.closingBracket.read( char );
if ( accepted ) {
// if closing bracket is sealed, so are we. save ourselves a trip
if ( this.closingBracket.sealed ) {
this.seal();
}
return true;
}
// otherwise we are done with this token
this.seal();
return false;
},
seal: function () {
// time to figure out some stuff about this tag
// tag name
this.tag = this.tagName.value;
// opening or closing tag?
if ( this.closingTagSolidus.value ) {
this.isClosingTag = true;
}
// self-closing?
if ( this.selfClosingSolidus.value ) {
this.isSelfClosingTag = true;
}
this.sealed = true;
}
};
OpeningBracket = function () {};
OpeningBracket.prototype = {
read: function ( char ) {
if ( this.sealed ) {
return false;
}
if ( char === '<' ) {
this.value = '<';
this.seal();
return true;
}
throw 'Expected "<", saw "' + char + '"';
},
seal: function () {
this.sealed = true;
}
};
TagName = function () {};
TagName.prototype = {
read: function ( char ) {
if ( this.sealed ) {
return false;
}
// first char must be a letter
if ( !this.value ) {
if ( /[a-zA-Z]/.test( char ) ) {
this.value = char;
return true;
}
}
// subsequent characters can be letters, numbers or hyphens
if ( /[a-zA-Z0-9\-]/.test( char ) ) {
this.value += char;
return true;
}
this.seal();
return false;
},
seal: function () {
this.sealed = true;
}
};
AttributeCollection = function () {
this.items = [];
};
AttributeCollection.prototype = {
read: function ( char ) {
if ( this.sealed ) {
return false;
}
// are we currently building an attribute?
if ( this.nextItem ) {
// can it take this character?
if ( this.nextItem.read( char ) ) {
return true;
}
}
// ignore whitespace before attributes
if ( whitespace.test( char ) ) {
return true;
}
// if not, start a new attribute
this.nextItem = new Attribute();
// will it accept this character? if so add the new attribute
if ( this.nextItem.read( char ) ) {
this.items[ this.items.length ] = this.nextItem;
return true;
}
// if not, we're done here
this.seal();
return false;
},
seal: function () {
this.sealed = true;
}
};
Attribute = function () {
this.name = new AttributeName();
this.value = new AttributeValue();
};
Attribute.prototype = {
read: function ( char ) {
if ( this.sealed ) {
return false;
}
// can we append this character to the attribute name?
if ( this.name.read( char ) ) {
return true;
}
// if not, only continue if we had a name in the first place
if ( !this.name.value ) {
this.seal();
return false;
}
// send character to this.value
if ( this.value.read( char ) ) {
return true;
}
// rejected? okay, we're done
this.seal();
return false;
},
seal: function () {
// TODO
this.sealed = true;
}
};
AttributeName = function () {};
AttributeName.prototype = {
read: function ( char ) {
if ( this.sealed ) {
return false;
}
// first char?
if ( !this.value ) {
// first char must be letter, underscore or colon. (It really shouldn't be a colon.)
if ( /[a-zA-Z_:]/.test( char ) ) {
this.value = char;
return true;
}
this.seal();
return false;
}
// subsequent chars can be letters, numbers, underscores, colons, periods, or hyphens. Yeah. Nuts.
if ( /[_:a-zA-Z0-9\.\-]/.test( char ) ) {
this.value += char;
return true;
}
this.seal();
return false;
},
seal: function () {
this.sealed = true;
}
};
AttributeValue = function () {
this.tokens = [];
this.buffer = new MustacheBuffer();
this.expected = false;
};
AttributeValue.prototype = {
read: function ( char ) {
var mustacheToken, bufferValue;
if ( this.sealed ) {
return false;
}
// have we had the = character yet?
if ( !this.expected ) {
// ignore whitespace between name and =
if ( whitespace.test( char ) ) {
return true;
}
// if we have the =, we can read in the value
if ( char === '=' ) {
this.expected = true;
return true;
}
// anything else is an error
return false;
}
if ( !this.tokens.length ) {
// ignore leading whitespace
if ( whitespace.test( char ) ) {
return true;
}
// if we get a " or a ', flag value as quoted
if ( char === '"' || char === "'" ) {
this.quoteMark = char;
return true;
}
}
// send character to buffer
if ( this.buffer.read( char ) ) {
return true;
}
// buffer rejected char. can we convert it to a mustache or triple?
mustacheToken = this.buffer.convert();
if ( mustacheToken ) {
// if we were building a token, seal it
if ( this.currentToken ) {
this.currentToken.seal();
}
// start building the new mustache instead
this.currentToken = this.tokens[ this.tokens.length ] = mustacheToken;
return true;
}
// could not convert to a mustache. can we append to current token?
bufferValue = this.buffer.release();
if ( this.currentToken ) {
while ( bufferValue.length ) {
while ( bufferValue.length && bufferValue.charAt( 0 ) !== this.quoteMark && this.currentToken.read( bufferValue.charAt( 0 ) ) ) {
bufferValue = bufferValue.substring( 1 );
}
// still got something left over? create a new token
if ( bufferValue.length && bufferValue.charAt( 0 ) !== this.quoteMark ) {
this.currentToken = new AttributeValueToken( this.quoteMark );
this.currentToken.read( bufferValue.charAt( 0 ) );
this.tokens[ this.tokens.length ] = this.currentToken;
bufferValue = bufferValue.substring( 1 );
}
// closing quoteMark? seal value
if ( bufferValue.charAt( 0 ) === this.quoteMark ) {
this.currentToken.seal();
this.seal();
return true;
}
}
return true;
}
// otherwise we need to create a new token
this.currentToken = new AttributeValueToken( this.quoteMark );
this.currentToken.read( char );
this.tokens[ this.tokens.length ] = this.currentToken;
return true;
},
seal: function () {
this.sealed = true;
}
};
AttributeValueToken = function ( quoteMark ) {
this.type = types.ATTR_VALUE_TOKEN;
this.quoteMark = quoteMark || '';
this.value = '';
};
AttributeValueToken.prototype = {
read: function ( char ) {
if ( this.sealed ) {
return false;
}
if ( char === this.quoteMark ) {
this.seal();
return true;
}
// within quotemarks, anything goes
if ( this.quoteMark ) {
this.value += char;
return true;
}
// without quotemarks, the following characters are invalid: whitespace, ", ', =, <, >, `
if ( /[\s"'=<>`]/.test( char ) ) {
this.seal();
return false;
}
this.value += char;
return true;
},
seal: function () {
this.sealed = true;
}
};
Solidus = function () {};
Solidus.prototype = {
read: function ( char ) {
if ( this.sealed ) {
return false;
}
if ( char === '/' ) {
this.value = '/';
this.seal();
return true;
}
this.seal();
return false;
},
seal: function () {
this.sealed = true;
}
};
ClosingBracket = function () {};
ClosingBracket.prototype = {
read: function ( char ) {
if ( this.sealed ) {
return false;
}
if ( char === '>' ) {
this.value = '>';
this.seal();
return true;
}
throw 'Expected ">", received "' + char + '"';
},
seal: function () {
this.sealed = true;
}
};
stripHtmlComments = function ( html ) {
var commentStart, commentEnd, processed;
processed = '';
while ( html.length ) {
commentStart = html.indexOf( '<!--' );
commentEnd = html.indexOf( '-->' );
// 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;
};
types = A.types;
whitespace = /\s/;
mustacheTypes = {
'#': types.SECTION,
'^': types.INVERTED,
'/': types.CLOSING,
'>': types.PARTIAL,
'!': types.COMMENT,
'&': types.INTERPOLATOR
};
}( Anglebars ));
// Mustache types, used in various places
Anglebars.types = {
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
};
// export
if ( typeof module !== "undefined" && module.exports ) module.exports = Anglebars // Common JS
else if ( typeof define === "function" && define.amd ) define( function () { return Anglebars } ) // AMD
else { global.Anglebars = Anglebars }
}( this ));