/* jshint maxdepth:7*/
steal('can/view', './elements', "can/view/callbacks",function (can, elements, viewCallbacks) {
/**
* Helper(s)
*/
var newLine = /(\r|\n)+/g,
notEndTag = /\//,
/* 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
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
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;
});