todomvc
Version:
> Helping you select an MV\* framework
693 lines (616 loc) • 19.6 kB
JavaScript
/*!
* CanJS - 2.0.3
* http://canjs.us/
* Copyright (c) 2013 Bitovi
* Tue, 26 Nov 2013 18:21:22 GMT
* Licensed MIT
* Includes: CanJS default build
* Download from: http://canjs.us/
*/
define(["can/view", "can/view/elements"], function(can, elements){
/**
* Helper(s)
*/
var newLine = /(\r|\n)+/g,
// Escapes characters starting with `\`.
clean = function( content ) {
return content
.split('\\').join("\\\\")
.split("\n").join("\\n")
.split('"').join('\\"')
.split("\t").join("\\t");
},
// Returns a tagName to use as a temporary placeholder for live content
// looks forward ... could be slow, but we only do it when necessary
getTag = function(tagName, tokens, i){
// if a tagName is provided, use that
if(tagName){
return tagName;
} else {
// otherwise go searching for the next two tokens like "<",TAG
while(i < tokens.length){
if(tokens[i] == "<" && elements.reverseTagMap[tokens[i+1]]){
return elements.reverseTagMap[tokens[i+1]];
}
i++;
}
}
return '';
},
bracketNum = function(content){
return (--content.split("{").length) - (--content.split("}").length);
},
myEval = function( script ) {
eval(script);
},
attrReg = /([^\s]+)[\s]*=[\s]*$/,
// Commands for caching.
startTxt = 'var ___v1ew = [];',
finishTxt = "return ___v1ew.join('')",
put_cmd = "___v1ew.push(\n",
insert_cmd = put_cmd,
// Global controls (used by other functions to know where we are).
// Are we inside a tag?
htmlTag = null,
// Are we within a quote within a tag?
quote = null,
// What was the text before the current quote? (used to get the `attr` name)
beforeQuote = null,
// Whether a rescan is in progress
rescan = null,
getAttrName = function(){
var matches = beforeQuote.match(attrReg);
return matches && matches[1];
},
// Used to mark where the element is.
status = function(){
// `t` - `1`.
// `h` - `0`.
// `q` - String `beforeQuote`.
return quote ? "'"+getAttrName()+"'" : (htmlTag ? 1 : 0);
},
// returns the top of a stack
top = function(stack){
return stack[stack.length-1]
},
// characters that automatically mean a custom element
automaticCustomElementCharacters = /[-\:]/,
Scanner;
/**
* @constructor can.view.Scanner
*
* can.view.Scanner is used to convert a template into a JavaScript function. That
* function is called to produce a rendered result as a string. Often
* the rendered result will include data-view-id attributes on elements that
* will be processed after the template is used to create a document fragment.
*
*
* @param {{text: can.view.Scanner.text, tokens: Array<can.view.Scanner.token>, helpers: Array<can.view.Scanner.helpers>}}
*/
//
/**
* @typedef {{0:String,}}
*/
can.view.Scanner = Scanner = function( options ) {
// Set options on self
can.extend(this, {
/**
* @typedef {{start: String, escape: String, scope: String, options: String}} can.view.Scanner.text
*/
text: {},
tokens: []
}, options);
// make sure it's an empty string if it's not
this.text.options = this.text.options || ""
// Cache a token lookup
this.tokenReg = [];
this.tokenSimple = { "<": "<", ">": ">", '"': '"', "'": "'" };
this.tokenComplex = [];
this.tokenMap = {};
for (var i = 0, token; token = this.tokens[i]; i++) {
/**
* Token data structure (complex token and rescan function are optional):
* [
* "token name",
* "simple token or abbreviation",
* /complex token regexp/,
* function(content) {
* // Rescan Function
* return {
* before: '\n',
* content: content.trim(),
* after: '\n'
* }
* ]
*/
// Save complex mappings (custom regexp)
if (token[2]) {
this.tokenReg.push(token[2]);
this.tokenComplex.push({ abbr: token[1], re: new RegExp(token[2]), rescan: token[3] });
}
// Save simple mappings (string only, no regexp)
else {
this.tokenReg.push(token[1]);
this.tokenSimple[token[1]] = token[0];
}
this.tokenMap[token[0]] = token[1];
}
// Cache the token registry.
this.tokenReg = new RegExp("(" + this.tokenReg.slice(0).concat(["<", ">", '"', "'"]).join("|") + ")","g");
};
Scanner.attributes = {};
Scanner.regExpAttributes = {};
Scanner.attribute = function(attribute, callback){
if(typeof attribute == "string"){
Scanner.attributes[attribute] = callback;
} else {
Scanner.regExpAttributes[attribute] = {
match: attribute,
callback: callback
};
}
}
Scanner.hookupAttributes = function(options, el){
can.each(options && options.attrs || [], function(attr){
options.attr = attr;
if(Scanner.attributes[attr]) {
Scanner.attributes[attr](options,el);
} else {
can.each(Scanner.regExpAttributes,function(attrMatcher){
if(attrMatcher.match.test(attr)){
attrMatcher.callback(options, el)
}
})
}
})
}
Scanner.tag = function( tagName, callback){
// if we have html5shive ... re-generate
if(window.html5){
html5.elements += " "+tagName
html5.shivDocument();
}
Scanner.tags[tagName.toLowerCase()] = callback;
}
Scanner.tags = {};
// This is called when there is a special tag
Scanner.hookupTag = function(hookupOptions){
// we need to call any live hookups
// so get that and return the hook
// a better system will always be called with the same stuff
var hooks = can.view.getHooks();
return can.view.hook(function(el){
can.each(hooks, function(fn){
fn(el);
});
var helperTags = hookupOptions.options.read('helpers._tags',{}).value,
tagName= hookupOptions.tagName,
tagCallback = ( helperTags && helperTags[tagName] ) || Scanner.tags[tagName]
// if this was an element like <foo-bar> that doesn't have a component, just render its content
var scope = hookupOptions.scope,
res = tagCallback ? tagCallback(el, hookupOptions) : scope;
// If the tagCallback gave us something to render with, and there is content within that element
// render it!
if(res && hookupOptions.subtemplate){
if(scope !== res){
scope = scope.add(res)
}
var frag = can.view.frag( hookupOptions.subtemplate(scope, hookupOptions.options) );
can.appendChild(el, frag);
}
can.view.Scanner.hookupAttributes(hookupOptions, el);
});
}
/**
* Extend can.View to add scanner support.
*/
Scanner.prototype = {
// a default that can be overwritten
helpers: [],
scan: function(source, name){
var tokens = [],
last = 0,
simple = this.tokenSimple,
complex = this.tokenComplex;
source = source.replace(newLine, "\n");
if (this.transform) {
source = this.transform(source);
}
source.replace(this.tokenReg, function(whole, part){
// offset is the second to last argument
var offset = arguments[arguments.length-2];
// if the next token starts after the last token ends
// push what's in between
if(offset > last){
tokens.push( source.substring(last, offset) );
}
// push the simple token (if there is one)
if (simple[whole]) {
tokens.push(whole);
}
// otherwise lookup complex tokens
else {
for (var i = 0, token; token = complex[i]; i++) {
if (token.re.test(whole)) {
tokens.push(token.abbr);
// Push a rescan function if one exists
if (token.rescan) {
tokens.push(token.rescan(part));
}
break;
}
}
}
// update the position of the last part of the last token
last = offset+part.length;
});
// if there's something at the end, add it
if(last < source.length){
tokens.push(source.substr(last));
}
var content = '',
buff = [startTxt + (this.text.start || '')],
// Helper `function` for putting stuff in the view concat.
put = function( content, bonus ) {
buff.push(put_cmd, '"', clean(content), '"'+(bonus||'')+');');
},
// A stack used to keep track of how we should end a bracket
// `}`.
// Once we have a `<%= %>` with a `leftBracket`,
// we store how the file should end here (either `))` or `;`).
endStack =[],
// The last token, used to remember which tag we are in.
lastToken,
// The corresponding magic tag.
startTag = null,
// Was there a magic tag inside an html tag?
magicInTag = false,
// was there a special state
specialStates = {
attributeHookups: [],
// a stack of tagHookups
tagHookups: []
},
// The current tag name.
tagName = '',
// stack of tagNames
tagNames = [],
// Pop from tagNames?
popTagName = false,
// Declared here.
bracketCount,
// in a special attr like src= or style=
specialAttribute = false,
i = 0,
token,
tmap = this.tokenMap,
attrName;
// Reinitialize the tag state goodness.
htmlTag = quote = beforeQuote = null;
for (; (token = tokens[i++]) !== undefined;) {
if ( startTag === null ) {
switch ( token ) {
case tmap.left:
case tmap.escapeLeft:
case tmap.returnLeft:
magicInTag = htmlTag && 1;
case tmap.commentLeft:
// A new line -- just add whatever content within a clean.
// Reset everything.
startTag = token;
if ( content.length ) {
put(content);
}
content = '';
break;
case tmap.escapeFull:
// This is a full line escape (a line that contains only whitespace and escaped logic)
// Break it up into escape left and right
magicInTag = htmlTag && 1;
rescan = 1;
startTag = tmap.escapeLeft;
if ( content.length ) {
put(content);
}
rescan = tokens[i++];
content = rescan.content || rescan;
if ( rescan.before ) {
put(rescan.before);
}
tokens.splice(i, 0, tmap.right);
break;
case tmap.commentFull:
// Ignore full line comments.
break;
case tmap.templateLeft:
content += tmap.left;
break;
case '<':
// Make sure we are not in a comment.
if(tokens[i].indexOf("!--") !== 0) {
htmlTag = 1;
magicInTag = 0;
}
content += token;
break;
case '>':
htmlTag = 0;
// content.substr(-1) doesn't work in IE7/8
var emptyElement = (content.substr(content.length-1) == "/" || content.substr(content.length-2) == "--"),
attrs = "";
// if there was a magic tag
// or it's an element that has text content between its tags,
// but content is not other tags add a hookup
// TODO: we should only add `can.EJS.pending()` if there's a magic tag
// within the html tags.
if(specialStates.attributeHookups.length) {
attrs = "attrs: ['"+specialStates.attributeHookups.join("','")+"'], ";
specialStates.attributeHookups = [];
}
// this is the > of a special tag
if(tagName === top(specialStates.tagHookups) ){
// If it's a self closing tag (like <content/>) make sure we put the / at the end.
if(emptyElement) {
content = content.substr(0,content.length-1)
}
// Put the start of the end
buff.push(put_cmd,
'"', clean(content), '"',
",can.view.Scanner.hookupTag({tagName:'"+tagName+"',"+(attrs)+"scope: "+(this.text.scope || "this")+this.text.options)
// if it's a self closing tag (like <content/>) close and end the tag
if(emptyElement) {
buff.push("}));");
content = "/>";
specialStates.tagHookups.pop()
}
// if it's an empty tag
else if( tokens[i] === "<" && tokens[i+1] === "/"+tagName ){
buff.push("}));");
content = token;
specialStates.tagHookups.pop()
} else {
// it has content
buff.push(",subtemplate: function("+this.text.argNames+"){\n"+ startTxt+(this.text.start || '') );
content = '';
}
} else if(magicInTag || (!popTagName && elements.tagToContentPropMap[ tagNames[tagNames.length -1] ] ) || attrs ){
// make sure / of /> is on the right of pending
var pendingPart = ",can.view.pending({"+attrs+"scope: "+(this.text.scope || "this")+this.text.options+"}),\"";
if(emptyElement){
put(content.substr(0,content.length-1),pendingPart+ "/>\"");
} else {
put(content, pendingPart+">\"");
}
content = '';
magicInTag = 0;
} else {
content += token;
}
// if it's a tag like <input/>
if(emptyElement || popTagName){
// remove the current tag in the stack
tagNames.pop();
// set the current tag to the previous parent
tagName = tagNames[tagNames.length-1];
// Don't pop next time
popTagName = false;
}
specialStates.attributeHookups = [];
break;
case "'":
case '"':
// If we are in an html tag, finding matching quotes.
if(htmlTag){
// We have a quote and it matches.
if(quote && quote === token){
// We are exiting the quote.
quote = null;
// Otherwise we are creating a quote.
// TODO: does this handle `\`?
var attr = getAttrName();
if(Scanner.attributes[attr]){
specialStates.attributeHookups.push(attr);
} else {
can.each(Scanner.regExpAttributes,function(attrMatcher){
if( attrMatcher.match.test(attr) ) {
specialStates.attributeHookups.push(attr);
}
});
}
if(specialAttribute) {
content += token;
put(content);
buff.push(finishTxt, "}));\n")
content = ""
specialAttribute = false;
break;
}
} else if(quote === null){
quote = token;
beforeQuote = lastToken;
attrName = getAttrName()
// TODO: check if there's magic!!!!
if( (tagName == "img" && attrName == "src") || attrName === "style" ) {
// put content that was before the attr name, but don't include the src=
put(content.replace(attrReg,""));
content = "";
specialAttribute = true;
buff.push(insert_cmd, "can.view.txt(2,'"+getTag(tagName,tokens, i)+"'," + status() + ",this,function(){", startTxt);
put(attrName+"="+token);
break;
}
}
}
default:
// Track the current tag
if(lastToken === '<'){
tagName = token.split(/\s/)[0];
var isClosingTag = false;
if( tagName.indexOf("/") === 0 ) {
isClosingTag = true;
var cleanedTagName = tagName.substr(1);
}
if( isClosingTag ) { // </tag>
// when we enter a new tag, pop the tag name stack
if( top( tagNames ) === cleanedTagName ) {
// set tagName to the last tagName
// if there are no more tagNames, we'll rely on getTag.
tagName = cleanedTagName;
popTagName = true;
}
// if we are in a closing tag of a custom tag
if( top( specialStates.tagHookups) == cleanedTagName ) {
// remove the last < from the content
put(content.substr(0, content.length-1));
// finish the "section"
buff.push(finishTxt+"}}) );" );
// the < belongs to the outside
content = "><"
specialStates.tagHookups.pop()
}
} else {
if( tagName.lastIndexOf("/") === tagName.length -1 ) {
tagName = tagName.substr(0, tagName.length -1);
}
if(tagName !== "!--" && ( Scanner.tags[tagName] || automaticCustomElementCharacters.test(tagName) )){
// if the content tag is inside something it doesn't belong ...
if(tagName === "content" && elements.tagMap[top(tagNames)]){
// convert it to an element that will work
token = token.replace("content",elements.tagMap[top(tagNames)])
}
// we will hookup at the ending tag>
specialStates.tagHookups.push(tagName);
}
tagNames.push(tagName);
}
}
content += token;
break;
}
} else {
// We have a start tag.
switch ( token ) {
case tmap.right:
case tmap.returnRight:
switch ( startTag ) {
case tmap.left:
// Get the number of `{ minus }`
bracketCount = bracketNum(content);
// We are ending a block.
if (bracketCount == 1) {
// We are starting on.
buff.push(insert_cmd, "can.view.txt(0,'"+getTag(tagName,tokens, i)+"'," + status() + ",this,function(){", startTxt, content);
endStack.push({
before: "",
after: finishTxt+"}));\n"
});
}
else {
// How are we ending this statement?
last = // If the stack has value and we are ending a block...
endStack.length && bracketCount == -1 ? // Use the last item in the block stack.
endStack.pop() : // Or use the default ending.
{
after: ";"
};
// If we are ending a returning block,
// add the finish text which returns the result of the
// block.
if (last.before) {
buff.push(last.before);
}
// Add the remaining content.
buff.push(content, ";",last.after);
}
break;
case tmap.escapeLeft:
case tmap.returnLeft:
// We have an extra `{` -> `block`.
// Get the number of `{ minus }`.
bracketCount = bracketNum(content);
// If we have more `{`, it means there is a block.
if( bracketCount ){
// When we return to the same # of `{` vs `}` end with a `doubleParent`.
endStack.push({
before : finishTxt,
after: "}));\n"
});
}
var escaped = startTag === tmap.escapeLeft ? 1 : 0,
commands = {
insert: insert_cmd,
tagName: getTag(tagName, tokens, i),
status: status(),
specialAttribute: specialAttribute
};
for(var ii = 0; ii < this.helpers.length;ii++){
// Match the helper based on helper
// regex name value
var helper = this.helpers[ii];
if(helper.name.test(content)){
content = helper.fn(content, commands);
// dont escape partials
if(helper.name.source == /^>[\s]*\w*/.source){
escaped = 0;
}
break;
}
}
// Handle special cases
if (typeof content == 'object') {
if (content.raw) {
buff.push(content.raw);
}
} else if (specialAttribute) {
buff.push(insert_cmd, content, ');');
} else {
// If we have `<%== a(function(){ %>` then we want
// `can.EJS.text(0,this, function(){ return a(function(){ var _v1ew = [];`.
buff.push(insert_cmd, "can.view.txt(\n" + escaped + ",\n'"+tagName+"',\n" + status() +",\nthis,\nfunction(){ " + (this.text.escape || '') + "return ", content,
// If we have a block.
bracketCount ?
// Start with startTxt `"var _v1ew = [];"`.
startTxt :
// If not, add `doubleParent` to close push and text.
"}));\n"
);
}
if (rescan && rescan.after && rescan.after.length) {
put(rescan.after.length);
rescan = null;
}
break;
}
startTag = null;
content = '';
break;
case tmap.templateLeft:
content += tmap.left;
break;
default:
content += token;
break;
}
}
lastToken = token;
}
// Put it together...
if ( content.length ) {
// Should be `content.dump` in Ruby.
put(content);
}
buff.push(";");
var template = buff.join(''),
out = {
out: (this.text.outStart||"") + template + " "+finishTxt+(this.text.outEnd || "")
};
// Use `eval` instead of creating a function, because it is easier to debug.
myEval.call(out, 'this.fn = (function('+this.text.argNames+'){' + out.out + '});\r\n//@ sourceURL=' + name + ".js");
return out;
}
};
can.view.Scanner.tag("content",function(el, options){
return options.scope;
})
return Scanner;
});