can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
397 lines (339 loc) • 11.4 kB
JavaScript
/* jshint undef: false */
steal(
"can/util",
"can/view/parser",
"can/view/target",
"./html_section.js",
"./text_section.js",
"./mustache_core.js",
"./mustache_helpers.js",
"./intermediate_and_imports.js",
"can/view/callbacks",
"can/view/bindings",
function(can, parser, target, HTMLSectionBuilder, TextSectionBuilder, mustacheCore, mustacheHelpers, getIntermediateAndImports, viewCallbacks ){
// Make sure that we can also use our modules with Stache as a plugin
parser = parser || can.view.parser;
can.view.parser = parser;
viewCallbacks = viewCallbacks || can.view.callbacks;
var svgNamespace = "http://www.w3.org/2000/svg";
var namespaces = {
"svg": svgNamespace,
// this allows a partial to start with g.
"g": svgNamespace
},
textContentOnlyTag = {style: true, script: true};
function stache(template){
// Remove line breaks according to mustache's specs.
if(typeof template === "string") {
template = mustacheCore.cleanLineEndings(template);
}
// The HTML section that is the root section for the entire template.
var section = new HTMLSectionBuilder(),
// Tracks the state of the parser.
state = {
node: null,
attr: null,
// A stack of which node / section we are in.
// There is probably a better way of doing this.
sectionElementStack: [],
// If text should be inserted and HTML escaped
text: false,
// which namespace we are in
namespaceStack: [],
// for style and script tags
// we create a special TextSectionBuilder and add things to that
// when the element is done, we compile the text section and
// add it as a callback to `section`.
textContentOnly: null
},
// This function is a catch all for taking a section and figuring out
// how to create a "renderer" that handles the functionality for a
// given section and modify the section to use that renderer.
// For example, if an HTMLSection is passed with mode `#` it knows to
// create a liveBindingBranchRenderer and pass that to section.add.
makeRendererAndUpdateSection = function(section, mode, stache){
if(mode === ">") {
// Partials use liveBindingPartialRenderers
section.add(mustacheCore.makeLiveBindingPartialRenderer(stache, copyState()));
} else if(mode === "/") {
section.endSection();
if(section instanceof HTMLSectionBuilder) {
state.sectionElementStack.pop();
}
} else if(mode === "else") {
section.inverse();
} else {
// If we are an HTMLSection, we will generate a
// a LiveBindingBranchRenderer; otherwise, a StringBranchRenderer.
// A LiveBindingBranchRenderer function processes
// the mustache text, and sets up live binding if an observable is read.
// A StringBranchRenderer function processes the mustache text and returns a
// text value.
var makeRenderer = section instanceof HTMLSectionBuilder ?
mustacheCore.makeLiveBindingBranchRenderer:
mustacheCore.makeStringBranchRenderer;
if(mode === "{" || mode === "&") {
// Adds a renderer function that just reads a value or calls a helper.
section.add( makeRenderer(null,stache, copyState() ));
} else if(mode === "#" || mode === "^") {
// Adds a renderer function and starts a section.
section.startSection(makeRenderer(mode,stache, copyState() ));
// If we are a directly nested section, count how many we are within
if(section instanceof HTMLSectionBuilder) {
state.sectionElementStack.push("section");
}
} else {
// Adds a renderer function that only updates text.
section.add( makeRenderer(null,stache, copyState({text: true}) ));
}
}
},
// Copys the state object for use in renderers.
copyState = function(overwrites){
var lastElement = state.sectionElementStack[state.sectionElementStack.length - 1];
var cur = {
tag: state.node && state.node.tag,
attr: state.attr && state.attr.name,
// <content> elements should be considered direclty nested
directlyNested: state.sectionElementStack.length ?
lastElement === "section" || lastElement === "custom": true,
textContentOnly: !!state.textContentOnly
};
return overwrites ? can.simpleExtend(cur, overwrites) : cur;
},
addAttributesCallback = function(node, callback){
if( !node.attributes ) {
node.attributes = [];
}
node.attributes.unshift(callback);
};
parser(template,{
start: function(tagName, unary){
var matchedNamespace = namespaces[tagName];
if (matchedNamespace && !unary ) {
state.namespaceStack.push(matchedNamespace);
}
state.node = {
tag: tagName,
children: [],
namespace: matchedNamespace || can.last(state.namespaceStack)
};
},
end: function(tagName, unary){
var isCustomTag = viewCallbacks.tag(tagName);
if(unary){
// If it's a custom tag with content, we need a section renderer.
section.add(state.node);
if(isCustomTag) {
addAttributesCallback(state.node, function(scope, options, parentNodeList){
viewCallbacks.tagHandler(this,tagName, {
scope: scope,
options: options,
subtemplate: null,
templateType: "stache",
parentNodeList: parentNodeList
});
});
}
} else {
section.push(state.node);
state.sectionElementStack.push( isCustomTag ? 'custom': tagName );
// If it's a custom tag with content, we need a section renderer.
if( isCustomTag ) {
section.startSubSection();
} else if(textContentOnlyTag[tagName]) {
state.textContentOnly = new TextSectionBuilder();
}
}
state.node =null;
},
close: function( tagName ) {
var matchedNamespace = namespaces[tagName];
if (matchedNamespace ) {
state.namespaceStack.pop();
}
var isCustomTag = viewCallbacks.tag(tagName),
renderer;
if( isCustomTag ) {
renderer = section.endSubSectionAndReturnRenderer();
}
if(textContentOnlyTag[tagName]) {
section.last().add(state.textContentOnly.compile(copyState()));
state.textContentOnly = null;
}
var oldNode = section.pop();
if( isCustomTag ) {
addAttributesCallback(oldNode, function(scope, options, parentNodeList){
viewCallbacks.tagHandler(this,tagName, {
scope: scope,
options: options,
subtemplate: renderer,
templateType: "stache",
parentNodeList: parentNodeList
});
});
}
state.sectionElementStack.pop();
},
attrStart: function(attrName){
if(state.node.section) {
state.node.section.add(attrName+"=\"");
} else {
state.attr = {
name: attrName,
value: ""
};
}
},
attrEnd: function(attrName){
if(state.node.section) {
state.node.section.add("\" ");
} else {
if(!state.node.attrs) {
state.node.attrs = {};
}
state.node.attrs[state.attr.name] =
state.attr.section ? state.attr.section.compile(copyState()) : state.attr.value;
var attrCallback = viewCallbacks.attr(attrName);
if(attrCallback) {
if( !state.node.attributes ) {
state.node.attributes = [];
}
state.node.attributes.push(function(scope, options, nodeList){
attrCallback(this,{
attributeName: attrName,
scope: scope,
options: options,
nodeList: nodeList
});
});
}
state.attr = null;
}
},
attrValue: function(value){
var section = state.node.section || state.attr.section;
if(section){
section.add(value);
} else {
state.attr.value += value;
}
},
chars: function( text ) {
(state.textContentOnly || section).add(text);
},
special: function( text ){
var firstAndText = mustacheCore.splitModeFromExpression(text, state),
mode = firstAndText.mode,
expression = firstAndText.expression;
if(expression === "else") {
var inverseSection;
if(state.attr && state.attr.section) {
inverseSection = state.attr.section;
} else if(state.node && state.node.section ) {
inverseSection = state.node.section;
} else {
inverseSection = state.textContentOnly || section;
}
inverseSection.inverse();
return;
}
if(mode === "!") {
return;
}
if(state.node && state.node.section) {
makeRendererAndUpdateSection(state.node.section, mode, expression);
if(state.node.section.subSectionDepth() === 0){
state.node.attributes.push( state.node.section.compile(copyState()) );
delete state.node.section;
}
}
// `{{}}` in an attribute like `class="{{}}"`
else if(state.attr) {
if(!state.attr.section) {
state.attr.section = new TextSectionBuilder();
if(state.attr.value) {
state.attr.section.add(state.attr.value);
}
}
makeRendererAndUpdateSection(state.attr.section, mode, expression );
}
// `{{}}` in a tag like `<div {{}}>`
else if(state.node) {
if(!state.node.attributes) {
state.node.attributes = [];
}
if(!mode) {
state.node.attributes.push( mustacheCore.makeLiveBindingBranchRenderer( null,expression, copyState() ) );
} else if( mode === "#" || mode === "^" ) {
if(!state.node.section) {
state.node.section = new TextSectionBuilder();
}
makeRendererAndUpdateSection(state.node.section, mode, expression );
} else {
throw new Error(mode+" is currently not supported within a tag.");
}
}
else {
makeRendererAndUpdateSection( state.textContentOnly || section, mode, expression );
}
},
comment: function( text ) {
// create comment node
section.add({
comment: text
});
},
done: function(){}
});
return section.compile();
}
var escMap = {
'\n': "\\n",
'\r': "\\r",
'\u2028': "\\u2028",
'\u2029': "\\u2029"
};
var esc = function(string){
return ('' + string).replace(/["'\\\n\r\u2028\u2029]/g, function (character) {
if("'\"\\".indexOf(character) >= 0) {
return "\\"+character;
} else {
return escMap[character];
}
});
};
can.view.register({
suffix: "stache",
contentType: "x-stache-template",
// Returns a `function` that renders the view.
fragRenderer: function(id, text) {
return stache(text);
},
script: function (id, src) {
return "can.stache(\""+esc(src)+"\")";
}
});
can.view.ext = ".stache";
// At this point, can.stache has been created
can.extend(can.stache, mustacheHelpers);
// Copy helpers on raw stache function too so it can be used by stealing it.
can.extend(stache, mustacheHelpers);
can.stache.safeString = stache.safeString = function(text){
return {
toString: function () {
return text;
}
};
};
can.stache.async = function(source){
var iAi = getIntermediateAndImports(source);
var importPromises = can.map( iAi.imports, function(moduleName){
return can["import"](moduleName);
});
return can.when.apply(can, importPromises ).then(function(){
return stache(iAi.intermediate);
});
};
return stache;
});