UNPKG

can

Version:

MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.

686 lines (616 loc) 20.3 kB
/* 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 <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.pending({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 = "/>"; 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 <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 (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; });