/armza/Scripts/lib/jazor.js

http://armza.codeplex.com · JavaScript · 599 lines · 444 code · 104 blank · 51 comment · 214 complexity · 73a6ef0cddfbb40fcccd5b9099b8171b MD5 · raw file

  1. var jazor = (function ($) {
  2. if (!window.console) {
  3. window.console = {
  4. log: function(message) {
  5. }
  6. };
  7. }
  8. // Determine if we are running with jQuery.
  9. var jQ = (typeof ($) !== "undefined");
  10. // Represents a block of literal text.
  11. var literalBlock = function (content) {
  12. this.content = content;
  13. };
  14. literalBlock.prototype = {
  15. toString: function () { return "Literal"; },
  16. // Render the block.
  17. render: function (arrName) {
  18. var c = this.content
  19. .replace('\\', '\\\\', 'g')
  20. .replace('\"', '\\\"', 'g')
  21. .replace('\'', '\\\'', 'g')
  22. .replace('\n', '\\n', 'g');
  23. return (arrName + ".push(\"" + c + "\");");
  24. }
  25. };
  26. // Represents an expression.
  27. var expressionBlock = function (content) {
  28. this.content = content;
  29. };
  30. expressionBlock.prototype = {
  31. toString: function () { return "Expression"; },
  32. // Render the block.
  33. render: function (arrName) {
  34. return (arrName + ".push(" + this.content + ");");
  35. }
  36. };
  37. // Represents a code block.
  38. var codeBlock = function (content) {
  39. this.content = content;
  40. };
  41. codeBlock.prototype = {
  42. toString: function () { return "Code"; },
  43. // Render the block.
  44. render: function () {
  45. return this.content;
  46. }
  47. };
  48. // Defines the coordinating parser that manages the code and markup parsers.
  49. var parser = function (codeParser, markupParser) {
  50. this.codeParser = codeParser; codeParser.parser = this;
  51. this.markupParser = markupParser; markupParser.parser = this;
  52. // Our output array of template blocks
  53. this.blocks = [];
  54. // Our array of helper blocks
  55. this.helpers = [];
  56. };
  57. parser.prototype = {
  58. // Parses the next code block in the stream.
  59. parseCodeBlock: function (stream) {
  60. this.codeParser.parseBlock(stream);
  61. },
  62. // Parses the next markup block in the stream.
  63. parseMarkupBlock: function (stream) {
  64. this.markupParser.parseBlock(stream);
  65. },
  66. // Parses a template.
  67. parse: function (stream) {
  68. // Start at the markup parser, begin parsing the document (template).
  69. this.markupParser.parseDocument(stream);
  70. return { blocks: this.blocks, helpers: this.helpers };
  71. },
  72. // Pushes a finalised block (part of template) into the result.
  73. pushBlock: function (blockType, content) {
  74. var block = null;
  75. switch (blockType) {
  76. case "literal": {
  77. block = new literalBlock(content);
  78. break;
  79. }
  80. case "expression": {
  81. block = new expressionBlock(content);
  82. break;
  83. }
  84. case "code": {
  85. block = new codeBlock(content);
  86. break;
  87. }
  88. }
  89. if (block == null) throw "Unexpected block type: " + blockType;
  90. this.blocks.push(block);
  91. }
  92. };
  93. // Defines a markup parser for parsing xml-style markup.
  94. var markupParser = function () { };
  95. markupParser.prototype = {
  96. // Determines if the specified character is a valid for an email address.
  97. isValidEmailChar: function (chr) {
  98. var code = chr.charCodeAt(0);
  99. if (code >= 48 && code <= 57) return true;
  100. if (code >= 65 && code <= 90) return true;
  101. if (code >= 97 && code <= 122) return true;
  102. return false;
  103. },
  104. // Determines if the specified index represents a valid transition.
  105. isValidTransition: function (stream, index) {
  106. if (index == 0) return true;
  107. if (index == (stream.length - 1)) return false;
  108. if (this.isValidEmailChar(stream[index - 1])
  109. && this.isValidEmailChar(stream[index + 1])) return false;
  110. if (stream[index - 1] == "@"
  111. || stream[index + 1] == "@") return false;
  112. return true;
  113. },
  114. // Scans forward for the next transition.
  115. nextTransition: function (stream) {
  116. for (var i = 0; i < stream.length; i++) {
  117. if (stream[i] == "@" && this.isValidTransition(stream, i))
  118. return i;
  119. }
  120. return -1;
  121. },
  122. // Parses markup.
  123. parseBlock: function (stream) {
  124. if (stream == null || stream.length == 0) return;
  125. var next = this.nextTransition(stream);
  126. if (next == -1) {
  127. this.parser.pushBlock("literal", stream.join(""));
  128. return;
  129. }
  130. var markup = stream.slice(0, next).join("");
  131. this.parser.pushBlock("literal", markup);
  132. this.parser.parseCodeBlock(stream.slice(next));
  133. },
  134. // Parses a document (template).
  135. parseDocument: function (doc) {
  136. var stream = doc.split("");
  137. this.parseBlock(stream);
  138. }
  139. };
  140. // Defines a code parser for parsing javascript-style code.
  141. var codeParser = function () {
  142. this.keywords = ["if", "for", "with", "while", "helper"];
  143. };
  144. codeParser.prototype = {
  145. // Accepts all content up to the end of a set of braces.
  146. acceptBrace: function (stream, brace) {
  147. if (stream == null || stream.length == 0) return null;
  148. var output = [], qchr = 0, sbrace = brace.charCodeAt(0), ebrace = 0;
  149. if (sbrace == 40) ebrace = 41;
  150. if (sbrace == 91) ebrace = 93;
  151. if (stream[0] != brace) return null;
  152. var scopes = 0;
  153. for (var i = 0; i < stream.length; i++) {
  154. var cur = stream[i];
  155. var cde = cur.charCodeAt(0);
  156. if (cde == sbrace) {
  157. if (qchr == 0) {
  158. scopes++;
  159. }
  160. output.push(cur);
  161. } else if (cde == ebrace) {
  162. if (qchr == 0) {
  163. scopes--;
  164. }
  165. output.push(cur);
  166. if (scopes == 0) break;
  167. } else {
  168. if (qchr == cde) {
  169. qchr = 0;
  170. } else if (cde == 34 || cde == 39) {
  171. qchr = cde;
  172. }
  173. output.push(cur);
  174. }
  175. }
  176. return output.join("");
  177. },
  178. // Accepts all content up to the end of an identifier.
  179. acceptIdentifier: function (stream) {
  180. if (stream == null || stream.length == 0) return null;
  181. var output = [];
  182. for (var i = 0; i < stream.length; i++) {
  183. var cur = stream[i];
  184. var cde = cur.charCodeAt(0);
  185. if (i == 0) {
  186. if (cde == 36 || cde == 95 || (cde >= 65 && cde <= 90) || (cde >= 97 && cde <= 122)) { // $_A-Za-z
  187. output.push(cur);
  188. } else {
  189. return null;
  190. }
  191. } else {
  192. if (cde == 36 || cde == 95 || (cde >= 65 && cde <= 90) || (cde >= 97 && cde <= 122) || (cde >= 48 && cde <= 57)) { // $_A-Za-z0-9
  193. output.push(cur);
  194. } else {
  195. break;
  196. }
  197. }
  198. }
  199. return output.join("");
  200. },
  201. // Scans forward to the end of a block.
  202. endBlock: function (stream, startChar, endChar) {
  203. var scope = 0, cur;
  204. var quoteChar = null;
  205. for (var i = 0; i < stream.length; i++) {
  206. cur = stream[i];
  207. if (cur == "\"" || cur == "\'") {
  208. if (quoteChar == null) quoteChar = cur;
  209. else if (quoteChar == cur) quoteChar = null;
  210. }
  211. if (cur === startChar && quoteChar == null) scope++;
  212. if (cur === endChar && quoteChar == null) {
  213. scope--;
  214. if (scope == 0) return i;
  215. }
  216. }
  217. return -1;
  218. },
  219. // Scans forward to the end of a code block.
  220. endCodeBlock: function (stream) {
  221. return this.endBlock(stream, "{", "}");
  222. },
  223. // Scans forward to the end of an explicit block.
  224. endExplicitBlock: function (stream) {
  225. return this.endBlock(stream, "(", ")");
  226. },
  227. // Determines if the specified identifier is a keyword.
  228. isKeyword: function (identifier) {
  229. for (var i = 0; i < this.keywords.length; i++) {
  230. if (identifier == this.keywords[i]) return true;
  231. }
  232. return false;
  233. },
  234. // Scans forward to the next instance of the character to find.
  235. nextChar: function (stream, findChar) {
  236. for (var i = 0; i < stream.length; i++) {
  237. if (stream[i] == findChar) return i;
  238. }
  239. return -1;
  240. },
  241. // Parses code.
  242. parseBlock: function (stream) {
  243. if (stream == null || stream.length == 0) return;
  244. // Make sure we are starting with code.
  245. if (stream[0] != "@") {
  246. this.parser.parseMarkupBlock(stream);
  247. return;
  248. }
  249. var next = stream[1];
  250. if (next == ":") {
  251. // Parse a line, e.g. @: <div></div>
  252. this.parseLine(stream);
  253. return;
  254. }
  255. if (next == "(") {
  256. // Parse an explicit expression, e.g. @(model.name)
  257. this.parseExplicitExpression(stream);
  258. return;
  259. }
  260. if (next == "{") {
  261. // Parse a code block, e.g. @{ var age = 27; }
  262. this.parseCodeBlock(stream);
  263. return;
  264. }
  265. this.parseExpressionBlock(stream);
  266. },
  267. // Parses a code block.
  268. parseCodeBlock: function (stream) {
  269. if (stream == null || stream.length == -1) return;
  270. var end = this.endCodeBlock(stream);
  271. if (end == -1) throw "Unterminated code block.";
  272. var code = stream.slice(2, end).join("");
  273. this.parser.pushBlock("code", code);
  274. this.parser.parseMarkupBlock(stream.slice(end + 1));
  275. },
  276. // Parses an expression.
  277. parseExpression: function (stream) {
  278. var block = stream.slice(1);
  279. var expression = this.readExpression(block);
  280. if (expression == null) {
  281. this.parser.parseMarkupBlock(block);
  282. } else {
  283. if (expression == "helper") {
  284. this.parseHelper(stream);
  285. } else {
  286. this.parser.pushBlock("expression", expression);
  287. this.parser.parseMarkupBlock(stream.slice(expression.length + 1));
  288. }
  289. }
  290. },
  291. // Parses an expression block or language construct.
  292. parseExpressionBlock: function (stream) {
  293. if (stream == null || stream.length == -1) return;
  294. var nextBrace = this.nextChar(stream, "("),
  295. nextScope = this.nextChar(stream, "{");
  296. var next = (nextBrace < nextScope) ? nextBrace : nextScope;
  297. if (next > -1) {
  298. var identifier = this.trim(stream.slice(1, next).join(""));
  299. if (this.isKeyword(identifier)) {
  300. this.parseKeyword(identifier, stream);
  301. return;
  302. }
  303. }
  304. this.parseExpression(stream);
  305. },
  306. // Parses an explicit expression.
  307. parseExplicitExpression: function (stream) {
  308. if (stream == null || stream.length == -1) return;
  309. var end = this.endExplicitBlock(stream);
  310. if (end == -1) throw "Untermined explicit expression.";
  311. var expr = stream.slice(2, end).join("");
  312. this.parser.pushBlock("expression", expr);
  313. this.parser.parseMarkupBlock(stream.slice(end + 1));
  314. },
  315. // Parses a helper function.
  316. parseHelper: function (stream) {
  317. if (stream == null || stream.length == -1) return;
  318. var end = this.endCodeBlock(stream);
  319. if (end == -1) throw "Unterminated helper block.";
  320. var start = this.nextChar(stream, "{");
  321. var name = stream.slice(7, start + 1),
  322. len = this.parser.blocks.length;
  323. this.parser.pushBlock("code", "function" + name.join(""));
  324. var innerBlock = stream.slice(start + 1, end);
  325. this.parser.parseMarkupBlock(innerBlock);
  326. this.parser.pushBlock("code", "}");
  327. var len2 = this.parser.blocks.length;
  328. var helper = this.parser.blocks.slice(len, len2);
  329. this.parser.helpers.push(helper);
  330. this.parser.blocks = this.parser.blocks.slice(0, len);
  331. this.parser.parseMarkupBlock(stream.slice(end + 1));
  332. },
  333. // Parses an conditional block
  334. parseIfBlock: function (stream) {
  335. if (stream == null || stream.length == -1) return;
  336. var end = this.endCodeBlock(stream);
  337. if (end == -1) throw "Unterminated code block.";
  338. var start = this.nextChar(stream, "{");
  339. var statement = stream.slice(1, start + 1).join("");
  340. this.parser.pushBlock("code", statement);
  341. var innerBlock = stream.slice(start + 1, end);
  342. this.parser.parseMarkupBlock(innerBlock);
  343. this.parser.pushBlock("code", "}");
  344. this.parser.parseMarkupBlock(stream.slice(end + 1));
  345. },
  346. // Parses a keyword and code block.
  347. parseKeyword: function (keyword, stream) {
  348. if (stream == null || stream.length == -1) return;
  349. switch (keyword) {
  350. case "if": {
  351. this.parseIfBlock(stream);
  352. break;
  353. }
  354. case "for":
  355. case "with":
  356. case "while": {
  357. this.parseSimpleBlock(stream);
  358. break;
  359. }
  360. default: {
  361. this.parseMarkupBlock(stream.slice(1));
  362. break;
  363. }
  364. }
  365. },
  366. // Parses a line.
  367. parseLine: function (stream) {
  368. if (stream == null || stream.length == -1) return;
  369. var end = this.nextChar(stream, '\n');
  370. if (end == -1) end = (stream.length - 1);
  371. var line = stream.slice(2, end);
  372. this.parser.parseMarkupBlock(line);
  373. if (end != -1)
  374. this.parser.parseMarkupBlock(stream.slice(end));
  375. },
  376. // Parses a simple block.
  377. parseSimpleBlock: function (stream) {
  378. if (stream == null || stream.length == -1) return;
  379. var end = this.endCodeBlock(stream);
  380. if (end == -1) throw "Unterminated code block.";
  381. var start = this.nextChar(stream, "{");
  382. var statement = stream.slice(1, start + 1).join("");
  383. this.parser.pushBlock("code", statement);
  384. var innerBlock = stream.slice(start + 1, end);
  385. this.parser.parseMarkupBlock(innerBlock);
  386. this.parser.pushBlock("code", "}");
  387. this.parser.parseMarkupBlock(stream.slice(end + 1));
  388. },
  389. // Reads an expression from the stream.
  390. readExpression: function (stream) {
  391. if (stream == null || stream.length == 0) return null;
  392. var output = [], state = 1, i = 0;
  393. while (true) {
  394. if (state == 1) {
  395. var id = this.acceptIdentifier(stream.slice(i));
  396. if (id == null)
  397. break;
  398. output = output.concat(id);
  399. i += id.length;
  400. state = 2;
  401. } else if (state == 2) {
  402. if (stream[i] == "(" || stream[i] == "[") {
  403. var brace = this.acceptBrace(stream.slice(i), stream[i]);
  404. if (brace == null) break;
  405. output = output.concat(brace);
  406. i += brace.length;
  407. } else {
  408. state = 3;
  409. }
  410. } else if (state == 3) {
  411. if (stream[i] == ".") {
  412. state = 4;
  413. i++;
  414. continue;
  415. }
  416. break;
  417. } else if (state == 4) {
  418. var id = this.acceptIdentifier(stream.slice(i));
  419. if (id == null)
  420. break;
  421. output.push(".");
  422. state = 1;
  423. }
  424. }
  425. return output.join("");
  426. },
  427. // Trims a string.
  428. trim: function (str) {
  429. str = str.replace(/^\s+/, "");
  430. for (var i = str.length - 1; i >= 0; i--) {
  431. if (/\S/.test(str.charAt(i))) {
  432. str = str.substring(0, i + 1);
  433. break;
  434. }
  435. }
  436. return str;
  437. }
  438. };
  439. var runner = function (blocks, helpers) {
  440. this.blocks = blocks;
  441. this.helpers = helpers;
  442. this.template = null;
  443. this.prepare();
  444. };
  445. runner.prototype = {
  446. // Prepares the template runner by evaluating the template.
  447. prepare: function () {
  448. var render = [];
  449. render.push("(function(model) {");
  450. for (var i in this.helpers) {
  451. var helper = this.helpers[i];
  452. render.push(helper[0].render());
  453. render.push("var hr = [];");
  454. for (var j = 1; j < (helper.length - 1) ; j++) {
  455. render.push(helper[j].render("hr"));
  456. }
  457. render.push("return hr.join(\"\");");
  458. render.push(helper[helper.length - 1].render());
  459. };
  460. render.push("var r = [];");
  461. for (var i in this.blocks) {
  462. render.push(this.blocks[i].render("r"));
  463. }
  464. render.push("return r.join(\"\");");
  465. render.push("});");
  466. var tmp = render.join("\n");
  467. console.log(tmp);
  468. this.template = eval(tmp);
  469. },
  470. // Runs the template using the specified model.
  471. run: function (model) {
  472. return this.template(model);
  473. }
  474. };
  475. // Represents the base jazor object.
  476. var jazor = {
  477. // Parses the given template and executes it with the specified model.
  478. parse: function(template, model) {
  479. var p = new parser(new codeParser(), new markupParser());
  480. var result = p.parse(template);
  481. var r = new runner(result.blocks, result.helpers);
  482. return r.run(model);
  483. }
  484. };
  485. /* Register our jQuery function. */
  486. if (jQ) {
  487. // Parses the text values of the query results using the specified model.
  488. $.fn.jazor = function (model) {
  489. var text = $(this).text();
  490. return jazor.parse(text, model);
  491. };
  492. }
  493. return jazor;
  494. });
  495. jazor = (typeof (jQuery) === "undefined") ? jazor() : jazor(jQuery);