UNPKG

can

Version:

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

397 lines (339 loc) 11.4 kB
/* 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; });