• Jump To … +
    elements.js render.js scanner.js view.js
  • scanner.js

  • ¶
    /* jshint maxdepth:7*/
    steal('can/view', './elements', "can/view/callbacks",function (can, elements, viewCallbacks) {
    
    	/**
    	 * Helper(s)
    	 */
    	var newLine = /(\r|\n)+/g,
    		notEndTag = /\//,
  • ¶

    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] === "<" && !notEndTag.test(tokens[i + 1])) {
    						return elements.reverseTagMap[tokens[i + 1]] || 'span';
    					}
    					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];
    		},
    		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>}}
    	 */
  • ¶
    	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");
    	};
    
    	/**
    	 * 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: [],
  • ¶

    last tag hooked up

    					lastTagHookup: ''
    				},
  • ¶

    Helper function for removing tagHookups from the hookup stack

    				popTagHookup = function() {
  • ¶

    The length of tagHookups is the nested depth which can be used to uniquely identify custom tags of the same type

    					specialStates.lastTagHookup = specialStates.tagHookups.pop() + specialStates.tagHookups.length;
    				},
  • ¶

    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;
    						/* falls through */
    					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 comparison to lastTagHookup makes sure the same custom tags can be nested

    						if ((tagName + specialStates.tagHookups.length) !== specialStates.lastTagHookup && tagName === top(specialStates.tagHookups)) {
  • ¶

    If it’s a self closing tag (like ) 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.pending({tagName:'" + tagName + "'," + (attrs) + "scope: " + (this.text.scope || "this") + this.text.options);
  • ¶

    if it’s a self closing tag (like ) close and end the tag

    							if (emptyElement) {
    								buff.push("}));");
    								content = "/>";
    								popTagHookup();
    							}
  • ¶

    if it’s an empty tag

    							else if (tokens[i] === "<" && tokens[i + 1] === "/" + tagName) {
    								buff.push("}));");
    								content = token;
    								popTagHookup();
    							} 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

    						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 (viewCallbacks.attr(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;
    								}
    
    							}
    						}
    						/* falls through */
    					default:
  • ¶

    Track the current tag

    						if (lastToken === '<') {
    
    							tagName = token.substr(0, 3) === "!--" ?
    								"!--" : token.split(/\s/)[0];
    
    							var isClosingTag = false,
    								cleanedTagName;
    
    							if (tagName.indexOf("/") === 0) {
    								isClosingTag = true;
    								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 = "><";
    									popTagHookup();
    								}
    
    							} else {
    								if (tagName.lastIndexOf("/") === tagName.length - 1) {
    									tagName = tagName.substr(0, tagName.length - 1);
    
    								}
    
    								if (tagName !== "!--" && (viewCallbacks.tag(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.startTxt && content.end && specialAttribute) {
    									buff.push(insert_cmd, "can.view.toStr( ",content.content, '() ) );');
    
    								} else {
    
    									if (content.startTxt) {
    										buff.push(insert_cmd, "can.view.txt(\n" +
    											(typeof status() === "string" || (content.escaped != null ? content.escaped : escaped)) + ",\n'" + tagName + "',\n" + status() + ",\nthis,\n");
    									} else if (content.startOnlyTxt) {
    										buff.push(insert_cmd, 'can.view.onlytxt(this,\n');
    									}
    									buff.push(content.content);
    									if (content.end) {
    										buff.push('));');
    									}
    
    								}
    
    							} 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" + (typeof status() === "string" || 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"
    								);
    
    								/*buff.push(insert_cmd, "can.view.txt(\n" +
    									  + ",\n'" +
    									  tagName + "',\n" +
    									  status() +",\n" +
    									  "this,\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.attr

  • ¶

    This is called when there is a special tag

    	can.view.pending = function (viewData) {
  • ¶

    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);
    			});
    			viewData.templateType = "legacy";
    			if (viewData.tagName) {
    				viewCallbacks.tagHandler(el, viewData.tagName, viewData);
    			}
    
    			can.each(viewData && viewData.attrs || [], function (attributeName) {
    				viewData.attributeName = attributeName;
    				var callback = viewCallbacks.attr(attributeName);
    				if(callback) {
    					callback(el, viewData);
    				}
    			});
    
    		});
    
    	};
    
    	can.view.tag("content", function (el, tagData) {
    		return tagData.scope;
    	});
    
    	can.view.Scanner = Scanner;
    
    	return Scanner;
    });