You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
509 lines
18 KiB
509 lines
18 KiB
/**
|
|
* 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<datas.length;i++) {
|
|
var split = regData.exec(datas[i]);
|
|
// the data dfn can be either &dfn or &data=dfn
|
|
if(split[3] === undefined)
|
|
$(el).data(split[1],data[split[1]]);
|
|
else
|
|
$(el).data(split[1],data[split[3]]);
|
|
}
|
|
return el;
|
|
}
|
|
|
|
/*
|
|
* Binds the appropiate function to the event specified by
|
|
* ~event=function
|
|
* Or in the case of
|
|
* ~event
|
|
* binds function.event to event.
|
|
*/
|
|
function bindEvents(ZenCode, el, functions) {
|
|
if(ZenCode.search(regEvents) == 0)
|
|
return el;
|
|
var bindings = ZenCode.match(regEvents);
|
|
if(bindings === null)
|
|
return el;
|
|
for(var i=0;i<bindings.length;i++) {
|
|
var split = regEvent.exec(bindings[i]);
|
|
// function dfn can be either ~dfn or ~function=dfn
|
|
if(split[2] === undefined)
|
|
var fn = functions[split[1]];
|
|
else
|
|
var fn = functions[split[2]];
|
|
$(el).bind(split[1],fn);
|
|
}
|
|
return el;
|
|
}
|
|
|
|
/*
|
|
* parses attributes out of a single css element definition
|
|
* returns as a space delimited string of attributes and their values
|
|
*/
|
|
function parseAttributes(ZenBlock, data) {
|
|
if(ZenBlock.search(regAttrDfn) == -1)
|
|
return undefined;
|
|
var attrStrs = ZenBlock.match(regAttrDfn);
|
|
attrStrs = attrStrs[0].match(regAttrs);
|
|
var attrs = {};
|
|
for(var i=0;i<attrStrs.length;i++) {
|
|
var parts = regAttr.exec(attrStrs[i]);
|
|
attrs[parts[1]] = '';
|
|
// all attributes must be attr="value"
|
|
if(parts[3] !== undefined)
|
|
attrs[parts[1]] = parseContents(parts[3],data);
|
|
}
|
|
return attrs;
|
|
}
|
|
|
|
/*
|
|
* parses classes out of a single css element definition
|
|
* returns as a space delimited string of classes
|
|
*/
|
|
function parseClasses(ZenBlock) {
|
|
ZenBlock = ZenBlock.match(regTagNotContent)[0];
|
|
if(ZenBlock.search(regClasses) == -1)
|
|
return undefined;
|
|
var classes = ZenBlock.match(regClasses);
|
|
var clsString = '';
|
|
for(var i=0;i<classes.length;i++) {
|
|
clsString += ' '+regClass.exec(classes[i])[1];
|
|
}
|
|
return $.trim(clsString);
|
|
}
|
|
|
|
/*
|
|
* Converts !...! into its javascript equivelant.
|
|
*/
|
|
function parseContents(ZenBlock, data, indexes) {
|
|
if(indexes===undefined)
|
|
indexes = {};
|
|
var html = ZenBlock;
|
|
if(data===undefined)
|
|
return html;
|
|
//The while takes care of the issue .!fruit!!fruit=="bla"?:".sd":""!
|
|
//aka contigous !...!
|
|
while(regExclamation.test(html)) {
|
|
html = html.replace(regExclamation, function(str, str2) {
|
|
var begChar = '';
|
|
// don't process !for:! or !if:!
|
|
if(str.indexOf("!for:") > 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<ZenCode.length;index++) {
|
|
if(ZenCode.charAt(index)==close && ZenCode.charAt(index-1)!='\\')
|
|
count--;
|
|
else if(ZenCode.charAt(index)==open && ZenCode.charAt(index-1)!='\\')
|
|
count++;
|
|
}
|
|
var ret = ZenCode.substring(0,index);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Parses multiple ZenCode references. The initial ZenCode must be
|
|
* declared as ZenObject.main
|
|
*/
|
|
function parseReferences(ZenCode, ZenObject) {
|
|
ZenCode = ZenCode.replace(regReference, function(str) {
|
|
str = str.substr(1);
|
|
// wrap str in a function to find its value in the ZenObject
|
|
var fn = new Function('objs',//'reparse',
|
|
'var r="";'+
|
|
'with(objs){try{'+
|
|
//'if($.isPlainObject('+str+'))'+
|
|
// 'r=reparse('+str+');'+
|
|
//'else '+
|
|
'r='+str+';'+
|
|
'}catch(e){}}'+
|
|
'return r;');
|
|
return fn(ZenObject,parseReferences);
|
|
});
|
|
return ZenCode;
|
|
}
|
|
|
|
/*
|
|
* Parses the scope of a !for:...!
|
|
*
|
|
* The scope of !for:...! is:
|
|
* If the tag has no children, then only immeiately following tag
|
|
* Tag and its children
|
|
*/
|
|
function parseVariableScope(ZenCode) {
|
|
// only parse !for:! or !if:!
|
|
if(ZenCode.substring(0,5)!="!for:" &&
|
|
ZenCode.substring(0,4)!="!if:")
|
|
return undefined;
|
|
// find the enclosure and remove it from the string
|
|
var forCode = parseEnclosure(ZenCode,'!');
|
|
ZenCode = ZenCode.substr(forCode.length);
|
|
// scope of !for:! and !if:! can only be one (if any) group of elements
|
|
if(ZenCode.charAt(0) == '(') {
|
|
return parseEnclosure(ZenCode,'(',')');
|
|
}
|
|
var tag = ZenCode.match(regZenTagDfn)[0];
|
|
ZenCode = ZenCode.substr(tag.length);
|
|
// scope of !for:! and !if:! is the single element and its children
|
|
if(ZenCode.length==0 || ZenCode.charAt(0)=='+') {
|
|
return tag;
|
|
}
|
|
else if(ZenCode.charAt(0)=='>') {
|
|
var rest = '';
|
|
rest = parseEnclosure(ZenCode.substr(1),'(',')',1);
|
|
return tag+'>'+rest;
|
|
}
|
|
return undefined;
|
|
}
|
|
})(jQuery);
|