/** * Copyright (c) 2010 Mike Kent * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function($) { /* * Calling conventions: * * $.zc( ZenCode | ZenObject [, data] ) * * ZenCode: string to be parsed into HTML * ZenObject: Collection of ZenCode and ZenObjects. ZenObject.main must * be defined */ $.zc = $.zen = function(ZenCode,data) { if(data !== undefined) var functions = data.functions; var el = createHTMLBlock(ZenCode,data,functions); return el; }; var regZenTagDfn = /* * ( * [#\.@]?[\w!-]+ # tag names, ids, classes, and references * | * \[ # attributes * ([(\w|\-)!?=:"']+ # attribute name * (="([^"]|\\")+")? # attribute value * {0,})+ # allow spaces, and look for 1+ attributes * \] * | * \~[\w$]+=[\w$]+ # events in form -event=function * | * &[\w$\+(=[\w$]+)? # data in form &data[=variable] * | * [#\.\@]? # allow for types to precede dynamic names * !([^!]|\\!)+!){0,} # contents enclosed by !...! * | * (?:[^\\]|^) # find either \ or beginning of line * ! * ){0,} # 0 or more of the above * (\{ # contents * ( * [^\}] * | * \\\} # find all before }, but include \} * )+ * \})? */ /([#\.\@]?[\w-]+|\[([(\w|\-)!?=:"']+(="([^"]|\\")+")? {0,})+\]|\~[\w$]+=[\w$]+|&[\w$]+(=[\w$]+)?|[#\.\@]?!([^!]|\\!)+!){0,}(\{([^\}]|\\\})+\})?/i, regTag = /(\w+)/i, //finds only the first word, must check for now word regId = /#((\-|[\w])+)/i, //finds id name regTagNotContent = /((([#\.]?[\w-]+)?(\[([\w!]+(="([^"]|\\")+")? {0,})+\])?)+)/i, regClasses = /(\.[\w-]+)/gi, //finds all classes regClass = /\.([\w-]+)/i, //finds the class name of each class //finds reference objects regReference = /(@[\w$_][\w$_\d]+)/i, //finds attributes within '[' and ']' of type name or name="value" regAttrDfn = /(\[([(\w|\-)!]+(="([^"]|\\")+")? {0,})+\])/i, regAttrs = /([(\w|\-)!]+(="([^"]|\\")+")?)/gi, //finds each attribute regAttr = /([(\w|\-)!]+)(="(([^"]|\\")+)")?/i, //finds individual attribute and value //finds content within '{' and '}' while ignoring '\}' regCBrace = /\{(([^\}]|\\\})+)\}/i, //finds content within !...! while ignoring '\!' within !...! regExclamation = /(?:([^\\]|^))!([^!]|\\!)+!/gim, //finds events in form of -event=function regEvents = /\~[\w$]+(=[\w$]+)?/gi, regEvent = /\~([\w$]+)=([\w$]+)/i, //find data in form &data or &dataname=data regDatas = /&[\w$]+(=[\w$]+)?/gi, regData = /&([\w$]+)(=([\w$]+))?/i; /* * The magic happens here. * * This is the recursive function to break up, parse, and create every * element. */ function createHTMLBlock(ZenObject,data,functions,indexes) { if($.isPlainObject(ZenObject)) var ZenCode = ZenObject.main; else { var ZenCode = ZenObject; ZenObject = { main: ZenCode }; } var origZenCode = ZenCode; if(indexes === undefined) indexes = {}; // Take care of !for:...! and !if:...! structure and if $.isArray(data) if(ZenCode.charAt(0)=='!' || $.isArray(data)) { // If data is simply an array, then handle loop specially. // This allows for some shorthand and quick templating. if($.isArray(data)) var forScope = ZenCode; // Check to see if an index is specified else { var obj = parseEnclosure(ZenCode,'!'); obj = obj.substring(obj.indexOf(':')+1,obj.length-1); var forScope = parseVariableScope(ZenCode); } // Only parse the scope of the !for:! after taking care of references while(forScope.charAt(0) == '@') forScope = parseVariableScope( '!for:!'+parseReferences(forScope, ZenObject)); // setup a zen object with the forScope as main var zo = ZenObject; zo.main = forScope; // initialize el for consistent use var el = $(); if(ZenCode.substring(0,5)=="!for:" || $.isArray(data)) { //!for:...! // again, data as an array is handled differently if(!$.isArray(data) && obj.indexOf(':')>0) { var indexName = obj.substring(0,obj.indexOf(':')); obj = obj.substr(obj.indexOf(':')+1); } // setup the array to either be data as a whole or an object // within data. This is the reason for the two special exceptions // above to handle data as an aray. var arr = $.isArray(data)?data:data[obj]; var zc = zo.main; if($.isArray(arr) || $.isPlainObject(arr)) { $.map(arr, function(value, index) { zo.main = zc; // initialize index if it was specified if(indexName!==undefined) indexes[indexName] = index; // allow for array references as "value" by wrapping the array // element. if(!$.isPlainObject(value)) value = {value:value}; // create the element based on ZenObject previously created var next = createHTMLBlock(zo,value,functions,indexes); if(el.length == 0) el = next; // append elements... TODO: is this "if" necessary? else { $.each(next, function(index,value) { el.push(value); }); } }); } // if data is an array, then the whole ZenCode is looped, therefore // there is nothing left to do. if(!$.isArray(data)) ZenCode = ZenCode.substr(obj.length+6+forScope.length); else ZenCode = ''; } else if(ZenCode.substring(0,4)=="!if:") { //!if:...! // check result of if contents var result = parseContents('!'+obj+'!',data,indexes); // Only execute ZenCode if the result was positive. if(result!='undefined' || result!='false' || result!='') el = createHTMLBlock(zo,data,functions,indexes); ZenCode = ZenCode.substr(obj.length+5+forScope.length); } // setup function ZenObject.main to reflect changes in both !for:! // and !if:! ZenObject.main = ZenCode; } // Take care of nested groups else if(ZenCode.charAt(0)=='(') { // get full parenthetical group var paren = parseEnclosure(ZenCode,'(',')'); // exclude beginning and ending parentheses var inner = paren.substring(1,paren.length-1); // update ZenCode for later ZenCode = ZenCode.substr(paren.length); var zo = ZenObject; zo.main = inner; // create Element(s) based on contents of group var el = createHTMLBlock(zo,data,functions,indexes); } // Everything left should be a regular block else { var blocks = ZenCode.match(regZenTagDfn); var block = blocks[0]; // actual block to create if(block.length == 0) { return ''; } // dereference references if any // references can drastically change the code in unexpected ways // so it is required to reparse the whole ZenObject. if(block.indexOf('@') >= 0) { ZenCode = parseReferences(ZenCode,ZenObject); var zo = ZenObject; zo.main = ZenCode; return createHTMLBlock(zo,data,functions,indexes); } // apply any dynamic content to block ZenCode block = parseContents(block,data,indexes); // get all classes var blockClasses = parseClasses(block); // get block id if any if(regId.test(block)) var blockId = regId.exec(block)[1]; // get block attributes var blockAttrs = parseAttributes(block,data); // default block tag is div unless block is only {...}, thenspan var blockTag = block.charAt(0)=='{'?'span':'div'; // get block tag if it is explicitly defined if(ZenCode.charAt(0)!='#' && ZenCode.charAt(0)!='.' && ZenCode.charAt(0)!='{') blockTag = regTag.exec(block)[1]; // get block HTML contents if(block.search(regCBrace) != -1) var blockHTML = block.match(regCBrace)[1]; // create jQuery attribute object with all data blockAttrs = $.extend(blockAttrs, { id: blockId, 'class': blockClasses, html: blockHTML }); // create Element based on block var el = $('<'+blockTag+'>', blockAttrs); el.attr(blockAttrs); //fixes IE error (issue 2) // bind created element with any events and data el = bindEvents(block, el, functions); el = bindData(block, el, data); // remove block from ZenCode and update ZenObject ZenCode = ZenCode.substr(blocks[0].length); ZenObject.main = ZenCode; } // Recurse based on '+' or '>' if(ZenCode.length > 0) { // Create children if(ZenCode.charAt(0) == '>') { // one or more elements enclosed in a group if(ZenCode.charAt(1) == '(') { var zc = parseEnclosure(ZenCode.substr(1),'(',')'); ZenCode = ZenCode.substr(zc.length+1); } // dynamically created elements or !for:! or !if:! else if(ZenCode.charAt(1) == '!') { var obj = parseEnclosure(ZenCode.substr(1),'!'); var forScope = parseVariableScope(ZenCode.substr(1)); var zc = obj+forScope; ZenCode = ZenCode.substr(zc.length+1); } // a single element that either ends the ZenCode or has siblings else { var len = Math.max(ZenCode.indexOf('+'),ZenCode.length); var zc = ZenCode.substring(1, len); ZenCode = ZenCode.substr(len); } var zo = ZenObject; zo.main = zc; // recurse and append var els = $( createHTMLBlock(zo,data,functions,indexes) ); els.appendTo(el); } // Create siblings if(ZenCode.charAt(0) == '+') { var zo = ZenObject; zo.main = ZenCode.substr(1); // recurse and push new elements with current ones var el2 = createHTMLBlock(zo,data,functions,indexes); $.each(el2, function(index,value) { el.push(value); }); } } var ret = el; return ret; } /* * Binds the appropiate data to the element specified by * &data=value * Or in the case of * &data * binds data.data to data on the element. */ function bindData(ZenCode, el, data) { if(ZenCode.search(regDatas) == 0) return el; var datas = ZenCode.match(regDatas); if(datas === null) return el; for(var i=0;i 0 || str.indexOf("!if:") > 0) return str; // regex can return either !val! or x!val! where x is a misc char // begChar takes care of this second possability and saves the // character to be restored back to the string if(str.charAt(0) == '!') str = str.substring(1,str.length-1); else { begChar = str.charAt(0); str = str.substring(2,str.length-1); } // wrap a function with dfn to find value in either data or indexes var fn = new Function('data','indexes', 'var r=undefined;'+ 'with(data){try{r='+str+';}catch(e){}}'+ 'with(indexes){try{if(r===undefined)r='+str+';}catch(e){}}'+ 'return r;'); var val = unescape(fn(data,indexes)); //var val = fn(data,indexes); return begChar+val; }); } html = html.replace(/\\./g,function (str) { return str.charAt(1); }); return unescape(html); } /* * There are actually three forms of this function: * * parseEnclosure(ZenCode,open) - use open as both open and close * parseEnclosure(ZenCode,open,close) - specify both * parseEnclosure(ZenCode,open,close,count) - specify initial count */ function parseEnclosure(ZenCode,open,close,count) { if(close===undefined) close = open; var index = 1; // allow count to be either 1 if the string starts with an open char // or 0 and then return if it does not. if(count === undefined) count = ZenCode.charAt(0)==open?1:0; if(count==0) return; // go through each character to find the end of the enclosure while // keeping track of how deeply nested the parser currently is // and ignoring escaped enclosure characters. for(;count>0 && index') { var rest = ''; rest = parseEnclosure(ZenCode.substr(1),'(',')',1); return tag+'>'+rest; } return undefined; } })(jQuery);