UNPKG

can

Version:

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

428 lines (366 loc) 11.8 kB
/* jshint maxdepth:7,node:true*/ steal(function(){ function each(items, callback){ for ( var i = 0; i < items.length; i++ ) { callback(items[i], i); } } function makeMap(str){ var obj = {}, items = str.split(","); each(items, function(name){ obj[name] = true; }); return obj; } function handleIntermediate(intermediate, handler){ for(var i = 0, len = intermediate.length; i < len; i++) { var item = intermediate[i]; handler[item.tokenType].apply(handler, item.args); } return intermediate; } var alphaNumeric = "A-Za-z0-9", alphaNumericHU = "-:_"+alphaNumeric, attributeNames = "[^=>\\s\\/]+", spaceEQspace = "\\s*=\\s*", singleCurly = "\\{[^\\}\\{]\\}", doubleCurly = "\\{\\{[^\\}]\\}\\}\\}?", attributeEqAndValue = "(?:"+spaceEQspace+"(?:"+ "(?:"+doubleCurly+")|(?:"+singleCurly+")|(?:\"[^\"]*\")|(?:'[^']*')|[^>\\s]+))?", matchStash = "\\{\\{[^\\}]*\\}\\}\\}?", stash = "\\{\\{([^\\}]*)\\}\\}\\}?", startTag = new RegExp("^<(["+alphaNumeric+"]["+alphaNumericHU+"]*)"+ "(" + "(?:\\s*"+ "(?:(?:"+ "(?:"+attributeNames+")?"+ attributeEqAndValue+")|"+ "(?:"+matchStash+")+)"+ ")*"+ ")\\s*(\\/?)>"), endTag = new RegExp("^<\\/(["+alphaNumericHU+"]+)[^>]*>"), mustache = new RegExp(stash,"g"), txtBreak = /<|\{\{/, space = /\s/; // Empty Elements - HTML 5 var empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed"); // Block Elements - HTML 5 // For an INLINE element which can have BLOCK children, include that element in BOTH lists var block = makeMap("a,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video"); // Inline Elements - HTML 5 var inline = makeMap("a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var"); // Elements for which tag case matters - shouldn't be lowercased. var caseMatters = makeMap("altGlyph,altGlyphDef,altGlyphItem,animateColor,animateMotion,animateTransform,clipPath,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistantLight,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,foreignObject,glyphRef,linearGradient,radialGradient,textPath"); // Elements that you can, intentionally, leave open // (and which close themselves) var closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr"); // Attributes that have their values filled in disabled="disabled" // var fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected"); // Special Elements (can contain anything) var special = makeMap("script"); // Callback names on `handler`. var tokenTypes = "start,end,close,attrStart,attrEnd,attrValue,chars,comment,special,done".split(","); var fn = function(){}; var HTMLParser = function (html, handler, returnIntermediate) { if(typeof html === "object") { return handleIntermediate(html, handler); } var intermediate = []; handler = handler || {}; if(returnIntermediate) { // overwrite handlers so they add to intermediate each(tokenTypes, function(name){ var callback = handler[name] || fn; handler[name] = function(){ if( callback.apply(this, arguments) !== false ) { intermediate.push({tokenType: name, args: [].slice.call(arguments, 0) }); } }; }); } function parseStartTag(tag, tagName, rest, unary) { tagName = caseMatters[tagName] ? tagName : tagName.toLowerCase(); if (block[tagName] && !inline[tagName]) { var last = stack.last(); while (last && inline[last] && !block[last]) { parseEndTag("", last); last = stack.last(); } } if (closeSelf[tagName] && stack.last() === tagName) { parseEndTag("", tagName); } unary = empty[tagName] || !!unary; handler.start(tagName, unary); if (!unary) { stack.push(tagName); } // find attribute or special HTMLParser.parseAttrs(rest, handler); handler.end(tagName,unary); } function parseEndTag(tag, tagName) { // If no tag name is provided, clean shop var pos; if (!tagName) { pos = 0; } // Find the closest opened tag of the same type else { tagName = caseMatters[tagName] ? tagName : tagName.toLowerCase(); for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos] === tagName) { break; } } } if (pos >= 0) { // Close all the open elements, up the stack for (var i = stack.length - 1; i >= pos; i--) { if (handler.close) { handler.close(stack[i]); } } // Remove the open elements from the stack stack.length = pos; } } function parseMustache(mustache, inside){ if(handler.special){ handler.special(inside); } } var callChars = function(){ if(charsText) { if(handler.chars) { handler.chars(charsText); } } charsText = ""; }; var index, chars, match, stack = [], last = html, // an accumulating text for the next .chars callback charsText = ""; stack.last = function () { return this[this.length - 1]; }; while (html) { chars = true; // Make sure we're not in a script or style element if (!stack.last() || !special[stack.last()]) { // Comment if (html.indexOf("<!--") === 0) { index = html.indexOf("-->"); if (index >= 0) { callChars(); if (handler.comment) { handler.comment(html.substring(4, index)); } html = html.substring(index + 3); chars = false; } // end tag } else if (html.indexOf("</") === 0) { match = html.match(endTag); if (match) { callChars(); html = html.substring(match[0].length); match[0].replace(endTag, parseEndTag); chars = false; } // start tag } else if (html.indexOf("<") === 0) { match = html.match(startTag); if (match) { callChars(); html = html.substring(match[0].length); match[0].replace(startTag, parseStartTag); chars = false; } } else if (html.indexOf("{{") === 0 ) { match = html.match(mustache); if (match) { callChars(); html = html.substring(match[0].length); match[0].replace(mustache, parseMustache); } } if (chars) { index = html.search(txtBreak); if(index === 0 && html === last) { charsText += html.charAt(0); html = html.substr(1); index = html.search(txtBreak); } var text = index < 0 ? html : html.substring(0, index); html = index < 0 ? "" : html.substring(index); if (text) { charsText += text; } } } else { html = html.replace(new RegExp("([\\s\\S]*?)<\/" + stack.last() + "[^>]*>"), function (all, text) { text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, "$1$2"); if (handler.chars) { handler.chars(text); } return ""; }); parseEndTag("", stack.last()); } if (html === last) { throw new Error("Parse Error: " + html); } last = html; } callChars(); // Clean up any remaining tags parseEndTag(); handler.done(); return intermediate; }; var callAttrStart = function(state, curIndex, handler, rest){ state.attrStart = rest.substring(typeof state.nameStart === "number" ? state.nameStart : curIndex, curIndex); handler.attrStart(state.attrStart); state.inName = false; }; var callAttrEnd = function(state, curIndex, handler, rest){ if(state.valueStart !== undefined && state.valueStart < curIndex) { handler.attrValue(rest.substring(state.valueStart, curIndex)); } // if this never got to be inValue, like `DISABLED` then send a attrValue else if(!state.inValue){ //handler.attrValue(state.attrStart); } handler.attrEnd(state.attrStart); state.attrStart = undefined; state.valueStart = undefined; state.inValue = false; state.inName = false; state.lookingForEq = false; state.inQuote = false; state.lookingForName = true; }; HTMLParser.parseAttrs = function(rest, handler){ if(!rest) { return; } var i = 0; var curIndex; var state = { inDoubleCurly: false, inName: false, nameStart: undefined, inValue: false, valueStart: undefined, inQuote: false, attrStart: undefined, lookingForName: true, lookingForValue: false, lookingForEq : false }; while(i < rest.length) { curIndex = i; var cur = rest.charAt(i); var next = rest.charAt(i+1); var nextNext = rest.charAt(i+2); i++; //debugger; if(cur === "{" && next === "{") { if(state.inValue && curIndex > state.valueStart) { handler.attrValue(rest.substring(state.valueStart, curIndex)); } // `{{#foo}}DISABLED{{/foo}}` else if(state.inName && state.nameStart < curIndex) { callAttrStart(state, curIndex, handler, rest); callAttrEnd(state, curIndex, handler, rest); } // foo={{bar}} else if(state.lookingForValue){ state.inValue = true; } // a {{bar}} else if(state.lookingForEq && state.attrStart) { callAttrEnd(state, curIndex, handler, rest); } state.inDoubleCurly = true; state.doubleCurlyStart = curIndex+2; i++; } else if(state.inDoubleCurly) { if(cur === "}" && next === "}") { // for `{{{}}}` var isTriple = nextNext === "}" ? 1: 0; handler.special(rest.substring(state.doubleCurlyStart, curIndex)); state.inDoubleCurly = false; if(state.inValue) { state.valueStart = curIndex+2+isTriple; } i += (1+isTriple); } } else if(state.inValue) { if(state.inQuote) { if(cur === state.inQuote) { callAttrEnd(state, curIndex, handler, rest); } } else if(space.test(cur)) { callAttrEnd(state, curIndex, handler, rest); } } // if we hit an = outside a value else if(cur === "=" && (state.lookingForEq || state.lookingForName || state.inName)) { // if we haven't yet started this attribute `{{}}=foo` case: if(!state.attrStart) { callAttrStart(state, curIndex, handler, rest); } state.lookingForValue = true; state.lookingForEq = false; state.lookingForName = false; } // if we are currently in a name, check if we found a space else if(state.inName) { if(space.test(cur)) { callAttrStart(state, curIndex, handler, rest); state.lookingForEq = true; } } else if(state.lookingForName) { if(!space.test(cur)) { // might have just started a name, we need to close it if(state.attrStart) { callAttrEnd(state, curIndex, handler, rest); } state.nameStart = curIndex; state.inName = true; } } else if(state.lookingForValue) { if(!space.test(cur)) { state.lookingForValue = false; state.inValue = true; if(cur === "'" || cur === '"') { state.inQuote = cur; state.valueStart = curIndex+1; } else { state.valueStart = curIndex; } } } } if(state.inName) { callAttrStart(state, curIndex+1, handler, rest); callAttrEnd(state, curIndex+1, handler, rest); } else if(state.lookingForEq) { callAttrEnd(state, curIndex+1, handler, rest); } else if(state.inValue) { callAttrEnd(state, curIndex+1, handler, rest); } }; return HTMLParser; });