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.

508 lines
18 KiB

  1. /**
  2. * Copyright (c) 2010 Mike Kent
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining
  5. * a copy of this software and associated documentation files (the
  6. * "Software"), to deal in the Software without restriction, including
  7. * without limitation the rights to use, copy, modify, merge, publish,
  8. * distribute, sublicense, and/or sell copies of the Software, and to
  9. * permit persons to whom the Software is furnished to do so, subject to
  10. * the following conditions:
  11. *
  12. * The above copyright notice and this permission notice shall be
  13. * included in all copies or substantial portions of the Software.
  14. *
  15. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  16. * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  17. * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  18. * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  19. * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  20. * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  21. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. */
  23. (function($) {
  24. /*
  25. * Calling conventions:
  26. *
  27. * $.zc( ZenCode | ZenObject [, data] )
  28. *
  29. * ZenCode: string to be parsed into HTML
  30. * ZenObject: Collection of ZenCode and ZenObjects. ZenObject.main must
  31. * be defined
  32. */
  33. $.zc = $.zen = function(ZenCode,data) {
  34. if(data !== undefined)
  35. var functions = data.functions;
  36. var el = createHTMLBlock(ZenCode,data,functions);
  37. return el;
  38. };
  39. var regZenTagDfn =
  40. /*
  41. * (
  42. * [#\.@]?[\w!-]+ # tag names, ids, classes, and references
  43. * |
  44. * \[ # attributes
  45. * ([(\w|\-)!?=:"']+ # attribute name
  46. * (="([^"]|\\")+")? # attribute value
  47. * {0,})+ # allow spaces, and look for 1+ attributes
  48. * \]
  49. * |
  50. * \~[\w$]+=[\w$]+ # events in form -event=function
  51. * |
  52. * &[\w$\+(=[\w$]+)? # data in form &data[=variable]
  53. * |
  54. * [#\.\@]? # allow for types to precede dynamic names
  55. * !([^!]|\\!)+!){0,} # contents enclosed by !...!
  56. * |
  57. * (?:[^\\]|^) # find either \ or beginning of line
  58. * !
  59. * ){0,} # 0 or more of the above
  60. * (\{ # contents
  61. * (
  62. * [^\}]
  63. * |
  64. * \\\} # find all before }, but include \}
  65. * )+
  66. * \})?
  67. */
  68. /([#\.\@]?[\w-]+|\[([(\w|\-)!?=:"']+(="([^"]|\\")+")? {0,})+\]|\~[\w$]+=[\w$]+|&[\w$]+(=[\w$]+)?|[#\.\@]?!([^!]|\\!)+!){0,}(\{([^\}]|\\\})+\})?/i,
  69. regTag = /(\w+)/i, //finds only the first word, must check for now word
  70. regId = /#((\-|[\w])+)/i, //finds id name
  71. regTagNotContent = /((([#\.]?[\w-]+)?(\[([\w!]+(="([^"]|\\")+")? {0,})+\])?)+)/i,
  72. regClasses = /(\.[\w-]+)/gi, //finds all classes
  73. regClass = /\.([\w-]+)/i, //finds the class name of each class
  74. //finds reference objects
  75. regReference = /(@[\w$_][\w$_\d]+)/i,
  76. //finds attributes within '[' and ']' of type name or name="value"
  77. regAttrDfn = /(\[([(\w|\-)!]+(="([^"]|\\")+")? {0,})+\])/i,
  78. regAttrs = /([(\w|\-)!]+(="([^"]|\\")+")?)/gi, //finds each attribute
  79. regAttr = /([(\w|\-)!]+)(="(([^"]|\\")+)")?/i, //finds individual attribute and value
  80. //finds content within '{' and '}' while ignoring '\}'
  81. regCBrace = /\{(([^\}]|\\\})+)\}/i,
  82. //finds content within !...! while ignoring '\!' within !...!
  83. regExclamation = /(?:([^\\]|^))!([^!]|\\!)+!/gim,
  84. //finds events in form of -event=function
  85. regEvents = /\~[\w$]+(=[\w$]+)?/gi,
  86. regEvent = /\~([\w$]+)=([\w$]+)/i,
  87. //find data in form &data or &dataname=data
  88. regDatas = /&[\w$]+(=[\w$]+)?/gi,
  89. regData = /&([\w$]+)(=([\w$]+))?/i;
  90. /*
  91. * The magic happens here.
  92. *
  93. * This is the recursive function to break up, parse, and create every
  94. * element.
  95. */
  96. function createHTMLBlock(ZenObject,data,functions,indexes) {
  97. if($.isPlainObject(ZenObject))
  98. var ZenCode = ZenObject.main;
  99. else {
  100. var ZenCode = ZenObject;
  101. ZenObject = {
  102. main: ZenCode
  103. };
  104. }
  105. var origZenCode = ZenCode;
  106. if(indexes === undefined)
  107. indexes = {};
  108. // Take care of !for:...! and !if:...! structure and if $.isArray(data)
  109. if(ZenCode.charAt(0)=='!' || $.isArray(data)) {
  110. // If data is simply an array, then handle loop specially.
  111. // This allows for some shorthand and quick templating.
  112. if($.isArray(data))
  113. var forScope = ZenCode;
  114. // Check to see if an index is specified
  115. else {
  116. var obj = parseEnclosure(ZenCode,'!');
  117. obj = obj.substring(obj.indexOf(':')+1,obj.length-1);
  118. var forScope = parseVariableScope(ZenCode);
  119. }
  120. // Only parse the scope of the !for:! after taking care of references
  121. while(forScope.charAt(0) == '@')
  122. forScope = parseVariableScope(
  123. '!for:!'+parseReferences(forScope, ZenObject));
  124. // setup a zen object with the forScope as main
  125. var zo = ZenObject;
  126. zo.main = forScope;
  127. // initialize el for consistent use
  128. var el = $();
  129. if(ZenCode.substring(0,5)=="!for:" || $.isArray(data)) { //!for:...!
  130. // again, data as an array is handled differently
  131. if(!$.isArray(data) && obj.indexOf(':')>0) {
  132. var indexName = obj.substring(0,obj.indexOf(':'));
  133. obj = obj.substr(obj.indexOf(':')+1);
  134. }
  135. // setup the array to either be data as a whole or an object
  136. // within data. This is the reason for the two special exceptions
  137. // above to handle data as an aray.
  138. var arr = $.isArray(data)?data:data[obj];
  139. var zc = zo.main;
  140. if($.isArray(arr) || $.isPlainObject(arr)) {
  141. $.map(arr, function(value, index) {
  142. zo.main = zc;
  143. // initialize index if it was specified
  144. if(indexName!==undefined)
  145. indexes[indexName] = index;
  146. // allow for array references as "value" by wrapping the array
  147. // element.
  148. if(!$.isPlainObject(value))
  149. value = {value:value};
  150. // create the element based on ZenObject previously created
  151. var next = createHTMLBlock(zo,value,functions,indexes);
  152. if(el.length == 0)
  153. el = next;
  154. // append elements... TODO: is this "if" necessary?
  155. else {
  156. $.each(next, function(index,value) {
  157. el.push(value);
  158. });
  159. }
  160. });
  161. }
  162. // if data is an array, then the whole ZenCode is looped, therefore
  163. // there is nothing left to do.
  164. if(!$.isArray(data))
  165. ZenCode = ZenCode.substr(obj.length+6+forScope.length);
  166. else
  167. ZenCode = '';
  168. } else if(ZenCode.substring(0,4)=="!if:") { //!if:...!
  169. // check result of if contents
  170. var result = parseContents('!'+obj+'!',data,indexes);
  171. // Only execute ZenCode if the result was positive.
  172. if(result!='undefined' || result!='false' || result!='')
  173. el = createHTMLBlock(zo,data,functions,indexes);
  174. ZenCode = ZenCode.substr(obj.length+5+forScope.length);
  175. }
  176. // setup function ZenObject.main to reflect changes in both !for:!
  177. // and !if:!
  178. ZenObject.main = ZenCode;
  179. }
  180. // Take care of nested groups
  181. else if(ZenCode.charAt(0)=='(') {
  182. // get full parenthetical group
  183. var paren = parseEnclosure(ZenCode,'(',')');
  184. // exclude beginning and ending parentheses
  185. var inner = paren.substring(1,paren.length-1);
  186. // update ZenCode for later
  187. ZenCode = ZenCode.substr(paren.length);
  188. var zo = ZenObject;
  189. zo.main = inner;
  190. // create Element(s) based on contents of group
  191. var el = createHTMLBlock(zo,data,functions,indexes);
  192. }
  193. // Everything left should be a regular block
  194. else {
  195. var blocks = ZenCode.match(regZenTagDfn);
  196. var block = blocks[0]; // actual block to create
  197. if(block.length == 0) {
  198. return '';
  199. }
  200. // dereference references if any
  201. // references can drastically change the code in unexpected ways
  202. // so it is required to reparse the whole ZenObject.
  203. if(block.indexOf('@') >= 0) {
  204. ZenCode = parseReferences(ZenCode,ZenObject);
  205. var zo = ZenObject;
  206. zo.main = ZenCode;
  207. return createHTMLBlock(zo,data,functions,indexes);
  208. }
  209. // apply any dynamic content to block ZenCode
  210. block = parseContents(block,data,indexes);
  211. // get all classes
  212. var blockClasses = parseClasses(block);
  213. // get block id if any
  214. if(regId.test(block))
  215. var blockId = regId.exec(block)[1];
  216. // get block attributes
  217. var blockAttrs = parseAttributes(block,data);
  218. // default block tag is div unless block is only {...}, thenspan
  219. var blockTag = block.charAt(0)=='{'?'span':'div';
  220. // get block tag if it is explicitly defined
  221. if(ZenCode.charAt(0)!='#' && ZenCode.charAt(0)!='.' &&
  222. ZenCode.charAt(0)!='{')
  223. blockTag = regTag.exec(block)[1];
  224. // get block HTML contents
  225. if(block.search(regCBrace) != -1)
  226. var blockHTML = block.match(regCBrace)[1];
  227. // create jQuery attribute object with all data
  228. blockAttrs = $.extend(blockAttrs, {
  229. id: blockId,
  230. 'class': blockClasses,
  231. html: blockHTML
  232. });
  233. // create Element based on block
  234. var el = $('<'+blockTag+'>', blockAttrs);
  235. el.attr(blockAttrs); //fixes IE error (issue 2)
  236. // bind created element with any events and data
  237. el = bindEvents(block, el, functions);
  238. el = bindData(block, el, data);
  239. // remove block from ZenCode and update ZenObject
  240. ZenCode = ZenCode.substr(blocks[0].length);
  241. ZenObject.main = ZenCode;
  242. }
  243. // Recurse based on '+' or '>'
  244. if(ZenCode.length > 0) {
  245. // Create children
  246. if(ZenCode.charAt(0) == '>') {
  247. // one or more elements enclosed in a group
  248. if(ZenCode.charAt(1) == '(') {
  249. var zc = parseEnclosure(ZenCode.substr(1),'(',')');
  250. ZenCode = ZenCode.substr(zc.length+1);
  251. }
  252. // dynamically created elements or !for:! or !if:!
  253. else if(ZenCode.charAt(1) == '!') {
  254. var obj = parseEnclosure(ZenCode.substr(1),'!');
  255. var forScope = parseVariableScope(ZenCode.substr(1));
  256. var zc = obj+forScope;
  257. ZenCode = ZenCode.substr(zc.length+1);
  258. }
  259. // a single element that either ends the ZenCode or has siblings
  260. else {
  261. var len = Math.max(ZenCode.indexOf('+'),ZenCode.length);
  262. var zc = ZenCode.substring(1, len);
  263. ZenCode = ZenCode.substr(len);
  264. }
  265. var zo = ZenObject;
  266. zo.main = zc;
  267. // recurse and append
  268. var els = $(
  269. createHTMLBlock(zo,data,functions,indexes)
  270. );
  271. els.appendTo(el);
  272. }
  273. // Create siblings
  274. if(ZenCode.charAt(0) == '+') {
  275. var zo = ZenObject;
  276. zo.main = ZenCode.substr(1);
  277. // recurse and push new elements with current ones
  278. var el2 = createHTMLBlock(zo,data,functions,indexes);
  279. $.each(el2, function(index,value) {
  280. el.push(value);
  281. });
  282. }
  283. }
  284. var ret = el;
  285. return ret;
  286. }
  287. /*
  288. * Binds the appropiate data to the element specified by
  289. * &data=value
  290. * Or in the case of
  291. * &data
  292. * binds data.data to data on the element.
  293. */
  294. function bindData(ZenCode, el, data) {
  295. if(ZenCode.search(regDatas) == 0)
  296. return el;
  297. var datas = ZenCode.match(regDatas);
  298. if(datas === null)
  299. return el;
  300. for(var i=0;i<datas.length;i++) {
  301. var split = regData.exec(datas[i]);
  302. // the data dfn can be either &dfn or &data=dfn
  303. if(split[3] === undefined)
  304. $(el).data(split[1],data[split[1]]);
  305. else
  306. $(el).data(split[1],data[split[3]]);
  307. }
  308. return el;
  309. }
  310. /*
  311. * Binds the appropiate function to the event specified by
  312. * ~event=function
  313. * Or in the case of
  314. * ~event
  315. * binds function.event to event.
  316. */
  317. function bindEvents(ZenCode, el, functions) {
  318. if(ZenCode.search(regEvents) == 0)
  319. return el;
  320. var bindings = ZenCode.match(regEvents);
  321. if(bindings === null)
  322. return el;
  323. for(var i=0;i<bindings.length;i++) {
  324. var split = regEvent.exec(bindings[i]);
  325. // function dfn can be either ~dfn or ~function=dfn
  326. if(split[2] === undefined)
  327. var fn = functions[split[1]];
  328. else
  329. var fn = functions[split[2]];
  330. $(el).bind(split[1],fn);
  331. }
  332. return el;
  333. }
  334. /*
  335. * parses attributes out of a single css element definition
  336. * returns as a space delimited string of attributes and their values
  337. */
  338. function parseAttributes(ZenBlock, data) {
  339. if(ZenBlock.search(regAttrDfn) == -1)
  340. return undefined;
  341. var attrStrs = ZenBlock.match(regAttrDfn);
  342. attrStrs = attrStrs[0].match(regAttrs);
  343. var attrs = {};
  344. for(var i=0;i<attrStrs.length;i++) {
  345. var parts = regAttr.exec(attrStrs[i]);
  346. attrs[parts[1]] = '';
  347. // all attributes must be attr="value"
  348. if(parts[3] !== undefined)
  349. attrs[parts[1]] = parseContents(parts[3],data);
  350. }
  351. return attrs;
  352. }
  353. /*
  354. * parses classes out of a single css element definition
  355. * returns as a space delimited string of classes
  356. */
  357. function parseClasses(ZenBlock) {
  358. ZenBlock = ZenBlock.match(regTagNotContent)[0];
  359. if(ZenBlock.search(regClasses) == -1)
  360. return undefined;
  361. var classes = ZenBlock.match(regClasses);
  362. var clsString = '';
  363. for(var i=0;i<classes.length;i++) {
  364. clsString += ' '+regClass.exec(classes[i])[1];
  365. }
  366. return $.trim(clsString);
  367. }
  368. /*
  369. * Converts !...! into its javascript equivelant.
  370. */
  371. function parseContents(ZenBlock, data, indexes) {
  372. if(indexes===undefined)
  373. indexes = {};
  374. var html = ZenBlock;
  375. if(data===undefined)
  376. return html;
  377. //The while takes care of the issue .!fruit!!fruit=="bla"?:".sd":""!
  378. //aka contigous !...!
  379. while(regExclamation.test(html)) {
  380. html = html.replace(regExclamation, function(str, str2) {
  381. var begChar = '';
  382. // don't process !for:! or !if:!
  383. if(str.indexOf("!for:") > 0 || str.indexOf("!if:") > 0)
  384. return str;
  385. // regex can return either !val! or x!val! where x is a misc char
  386. // begChar takes care of this second possability and saves the
  387. // character to be restored back to the string
  388. if(str.charAt(0) == '!')
  389. str = str.substring(1,str.length-1);
  390. else {
  391. begChar = str.charAt(0);
  392. str = str.substring(2,str.length-1);
  393. }
  394. // wrap a function with dfn to find value in either data or indexes
  395. var fn = new Function('data','indexes',
  396. 'var r=undefined;'+
  397. 'with(data){try{r='+str+';}catch(e){}}'+
  398. 'with(indexes){try{if(r===undefined)r='+str+';}catch(e){}}'+
  399. 'return r;');
  400. var val = unescape(fn(data,indexes));
  401. //var val = fn(data,indexes);
  402. return begChar+val;
  403. });
  404. }
  405. html = html.replace(/\\./g,function (str) {
  406. return str.charAt(1);
  407. });
  408. return unescape(html);
  409. }
  410. /*
  411. * There are actually three forms of this function:
  412. *
  413. * parseEnclosure(ZenCode,open) - use open as both open and close
  414. * parseEnclosure(ZenCode,open,close) - specify both
  415. * parseEnclosure(ZenCode,open,close,count) - specify initial count
  416. */
  417. function parseEnclosure(ZenCode,open,close,count) {
  418. if(close===undefined)
  419. close = open;
  420. var index = 1;
  421. // allow count to be either 1 if the string starts with an open char
  422. // or 0 and then return if it does not.
  423. if(count === undefined)
  424. count = ZenCode.charAt(0)==open?1:0;
  425. if(count==0)
  426. return;
  427. // go through each character to find the end of the enclosure while
  428. // keeping track of how deeply nested the parser currently is
  429. // and ignoring escaped enclosure characters.
  430. for(;count>0 && index<ZenCode.length;index++) {
  431. if(ZenCode.charAt(index)==close && ZenCode.charAt(index-1)!='\\')
  432. count--;
  433. else if(ZenCode.charAt(index)==open && ZenCode.charAt(index-1)!='\\')
  434. count++;
  435. }
  436. var ret = ZenCode.substring(0,index);
  437. return ret;
  438. }
  439. /*
  440. * Parses multiple ZenCode references. The initial ZenCode must be
  441. * declared as ZenObject.main
  442. */
  443. function parseReferences(ZenCode, ZenObject) {
  444. ZenCode = ZenCode.replace(regReference, function(str) {
  445. str = str.substr(1);
  446. // wrap str in a function to find its value in the ZenObject
  447. var fn = new Function('objs',//'reparse',
  448. 'var r="";'+
  449. 'with(objs){try{'+
  450. //'if($.isPlainObject('+str+'))'+
  451. // 'r=reparse('+str+');'+
  452. //'else '+
  453. 'r='+str+';'+
  454. '}catch(e){}}'+
  455. 'return r;');
  456. return fn(ZenObject,parseReferences);
  457. });
  458. return ZenCode;
  459. }
  460. /*
  461. * Parses the scope of a !for:...!
  462. *
  463. * The scope of !for:...! is:
  464. * If the tag has no children, then only immeiately following tag
  465. * Tag and its children
  466. */
  467. function parseVariableScope(ZenCode) {
  468. // only parse !for:! or !if:!
  469. if(ZenCode.substring(0,5)!="!for:" &&
  470. ZenCode.substring(0,4)!="!if:")
  471. return undefined;
  472. // find the enclosure and remove it from the string
  473. var forCode = parseEnclosure(ZenCode,'!');
  474. ZenCode = ZenCode.substr(forCode.length);
  475. // scope of !for:! and !if:! can only be one (if any) group of elements
  476. if(ZenCode.charAt(0) == '(') {
  477. return parseEnclosure(ZenCode,'(',')');
  478. }
  479. var tag = ZenCode.match(regZenTagDfn)[0];
  480. ZenCode = ZenCode.substr(tag.length);
  481. // scope of !for:! and !if:! is the single element and its children
  482. if(ZenCode.length==0 || ZenCode.charAt(0)=='+') {
  483. return tag;
  484. }
  485. else if(ZenCode.charAt(0)=='>') {
  486. var rest = '';
  487. rest = parseEnclosure(ZenCode.substr(1),'(',')',1);
  488. return tag+'>'+rest;
  489. }
  490. return undefined;
  491. }
  492. })(jQuery);