markup.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. /*
  2. Markup.js v1.5.21: http://github.com/adammark/Markup.js
  3. MIT License
  4. (c) 2011 - 2014 Adam Mark
  5. */
  6. var Mark = {
  7. // Templates to include, by name. A template is a string.
  8. includes: {},
  9. // Global variables, by name. Global variables take precedence over context variables.
  10. globals: {},
  11. // The delimiter to use in pipe expressions, e.g. {{if color|like>red}}.
  12. delimiter: ">",
  13. // Collapse white space between HTML elements in the resulting string.
  14. compact: false,
  15. // Shallow-copy an object.
  16. _copy: function (a, b) {
  17. b = b || [];
  18. for (var i in a) {
  19. b[i] = a[i];
  20. }
  21. return b;
  22. },
  23. // Get the value of a number or size of an array. This is a helper function for several pipes.
  24. _size: function (a) {
  25. return a instanceof Array ? a.length : (a || 0);
  26. },
  27. // This object represents an iteration. It has an index and length.
  28. _iter: function (idx, size) {
  29. this.idx = idx;
  30. this.size = size;
  31. this.length = size;
  32. this.sign = "#";
  33. // Print the index if "#" or the count if "##".
  34. this.toString = function () {
  35. return this.idx + this.sign.length - 1;
  36. };
  37. },
  38. // Pass a value through a series of pipe expressions, e.g. _pipe(123, ["add>10","times>5"]).
  39. _pipe: function (val, expressions) {
  40. var expression, parts, fn, result;
  41. // If we have expressions, pull out the first one, e.g. "add>10".
  42. if ((expression = expressions.shift())) {
  43. // Split the expression into its component parts, e.g. ["add", "10"].
  44. parts = expression.split(this.delimiter);
  45. // Pull out the function name, e.g. "add".
  46. fn = parts.shift().trim();
  47. try {
  48. // Run the function, e.g. add(123, 10) ...
  49. result = Mark.pipes[fn].apply(null, [val].concat(parts));
  50. // ... then pipe again with remaining expressions.
  51. val = this._pipe(result, expressions);
  52. }
  53. catch (e) {
  54. }
  55. }
  56. // Return the piped value.
  57. return val;
  58. },
  59. // TODO doc
  60. _eval: function (context, filters, child) {
  61. var result = this._pipe(context, filters),
  62. ctx = result,
  63. i = -1,
  64. j,
  65. opts;
  66. if (result instanceof Array) {
  67. result = "";
  68. j = ctx.length;
  69. while (++i < j) {
  70. opts = {
  71. iter: new this._iter(i, j)
  72. };
  73. result += child ? Mark.up(child, ctx[i], opts) : ctx[i];
  74. }
  75. }
  76. else if (result instanceof Object) {
  77. result = Mark.up(child, ctx);
  78. }
  79. return result;
  80. },
  81. // Process the contents of an IF or IF/ELSE block.
  82. _test: function (bool, child, context, options) {
  83. // Process the child string, then split it into the IF and ELSE parts.
  84. var str = Mark.up(child, context, options).split(/\{\{\s*else\s*\}\}/);
  85. // Return the IF or ELSE part. If no ELSE, return an empty string.
  86. return (bool === false ? str[1] : str[0]) || "";
  87. },
  88. // Determine the extent of a block expression, e.g. "{{foo}}...{{/foo}}"
  89. _bridge: function (tpl, tkn) {
  90. tkn = tkn == "." ? "\\." : tkn.replace(/\$/g, "\\$");
  91. var exp = "{{\\s*" + tkn + "([^/}]+\\w*)?}}|{{/" + tkn + "\\s*}}",
  92. re = new RegExp(exp, "g"),
  93. tags = tpl.match(re) || [],
  94. t,
  95. i,
  96. a = 0,
  97. b = 0,
  98. c = -1,
  99. d = 0;
  100. for (i = 0; i < tags.length; i++) {
  101. t = i;
  102. c = tpl.indexOf(tags[t], c + 1);
  103. if (tags[t].indexOf("{{/") > -1) {
  104. b++;
  105. }
  106. else {
  107. a++;
  108. }
  109. if (a === b) {
  110. break;
  111. }
  112. }
  113. a = tpl.indexOf(tags[0]);
  114. b = a + tags[0].length;
  115. d = c + tags[t].length;
  116. // Return the block, e.g. "{{foo}}bar{{/foo}}" and its child, e.g. "bar".
  117. return [tpl.substring(a, d), tpl.substring(b, c)];
  118. }
  119. };
  120. // Inject a template string with contextual data and return a new string.
  121. Mark.up = function (template, context, options) {
  122. context = context || {};
  123. options = options || {};
  124. // Match all tags like "{{...}}".
  125. var re = /\{\{(.+?)\}\}/g,
  126. // All tags in the template.
  127. tags = template.match(re) || [],
  128. // The tag being evaluated, e.g. "{{hamster|dance}}".
  129. tag,
  130. // The expression to evaluate inside the tag, e.g. "hamster|dance".
  131. prop,
  132. // The token itself, e.g. "hamster".
  133. token,
  134. // An array of pipe expressions, e.g. ["more>1", "less>2"].
  135. filters = [],
  136. // Does the tag close itself? e.g. "{{stuff/}}".
  137. selfy,
  138. // Is the tag an "if" statement?
  139. testy,
  140. // The contents of a block tag, e.g. "{{aa}}bb{{/aa}}" -> "bb".
  141. child,
  142. // The resulting string.
  143. result,
  144. // The global variable being evaluated, or undefined.
  145. global,
  146. // The included template being evaluated, or undefined.
  147. include,
  148. // A placeholder variable.
  149. ctx,
  150. // Iterators.
  151. i = 0,
  152. j = 0;
  153. // Set custom pipes, if provided.
  154. if (options.pipes) {
  155. this._copy(options.pipes, this.pipes);
  156. }
  157. // Set templates to include, if provided.
  158. if (options.includes) {
  159. this._copy(options.includes, this.includes);
  160. }
  161. // Set global variables, if provided.
  162. if (options.globals) {
  163. this._copy(options.globals, this.globals);
  164. }
  165. // Optionally override the delimiter.
  166. if (options.delimiter) {
  167. this.delimiter = options.delimiter;
  168. }
  169. // Optionally collapse white space.
  170. if (options.compact !== undefined) {
  171. this.compact = options.compact;
  172. }
  173. // Loop through tags, e.g. {{a}}, {{b}}, {{c}}, {{/c}}.
  174. while ((tag = tags[i++])) {
  175. result = undefined;
  176. child = "";
  177. selfy = tag.indexOf("/}}") > -1;
  178. prop = tag.substr(2, tag.length - (selfy ? 5 : 4));
  179. prop = prop.replace(/`(.+?)`/g, function (s, p1) {
  180. return Mark.up("{{" + p1 + "}}", context);
  181. });
  182. testy = prop.trim().indexOf("if ") === 0;
  183. filters = prop.split("|");
  184. filters.shift(); // instead of splice(1)
  185. prop = prop.replace(/^\s*if/, "").split("|").shift().trim();
  186. token = testy ? "if" : prop.split("|")[0];
  187. ctx = context[prop];
  188. // If an "if" statement without filters, assume "{{if foo|notempty}}"
  189. if (testy && !filters.length) {
  190. filters = ["notempty"];
  191. }
  192. // Does the tag have a corresponding closing tag? If so, find it and move the cursor.
  193. if (!selfy && template.indexOf("{{/" + token) > -1) {
  194. result = this._bridge(template, token);
  195. tag = result[0];
  196. child = result[1];
  197. i += tag.match(re).length - 1; // fast forward
  198. }
  199. // Skip "else" tags. These are pulled out in _test().
  200. if (/^\{\{\s*else\s*\}\}$/.test(tag)) {
  201. continue;
  202. }
  203. // Evaluating a global variable.
  204. else if ((global = this.globals[prop]) !== undefined) {
  205. result = this._eval(global, filters, child);
  206. }
  207. // Evaluating an included template.
  208. else if ((include = this.includes[prop])) {
  209. if (include instanceof Function) {
  210. include = include();
  211. }
  212. result = this._pipe(Mark.up(include, context, options), filters);
  213. }
  214. // Evaluating a loop counter ("#" or "##").
  215. else if (prop.indexOf("#") > -1) {
  216. options.iter.sign = prop;
  217. result = this._pipe(options.iter, filters);
  218. }
  219. // Evaluating the current context.
  220. else if (prop === ".") {
  221. result = this._pipe(context, filters);
  222. }
  223. // Evaluating a variable with dot notation, e.g. "a.b.c"
  224. else if (prop.indexOf(".") > -1) {
  225. prop = prop.split(".");
  226. ctx = Mark.globals[prop[0]];
  227. if (ctx) {
  228. j = 1;
  229. }
  230. else {
  231. j = 0;
  232. ctx = context;
  233. }
  234. // Get the actual context
  235. while (ctx && j < prop.length) {
  236. ctx = ctx[prop[j++]];
  237. }
  238. result = this._eval(ctx, filters, child);
  239. }
  240. // Evaluating an "if" statement.
  241. else if (testy) {
  242. result = this._pipe(ctx, filters);
  243. }
  244. // Evaluating an array, which might be a block expression.
  245. else if (ctx instanceof Array) {
  246. result = this._eval(ctx, filters, child);
  247. }
  248. // Evaluating a block expression.
  249. else if (child) {
  250. result = ctx ? Mark.up(child, ctx) : undefined;
  251. }
  252. // Evaluating anything else.
  253. else if (context.hasOwnProperty(prop)) {
  254. result = this._pipe(ctx, filters);
  255. }
  256. // Evaluating special case: if the resulting context is actually an Array
  257. if (result instanceof Array) {
  258. result = this._eval(result, filters, child);
  259. }
  260. // Evaluating an "if" statement.
  261. if (testy) {
  262. result = this._test(result, child, context, options);
  263. }
  264. // Replace the tag, e.g. "{{name}}", with the result, e.g. "Adam".
  265. template = template.replace(tag, result === undefined ? "???" : result);
  266. }
  267. return this.compact ? template.replace(/>\s+</g, "><") : template;
  268. };
  269. // Freebie pipes. See usage in README.md
  270. Mark.pipes = {
  271. empty: function (obj) {
  272. return !obj || (obj + "").trim().length === 0 ? obj : false;
  273. },
  274. notempty: function (obj) {
  275. return obj && (obj + "").trim().length ? obj : false;
  276. },
  277. blank: function (str, val) {
  278. return !!str || str === 0 ? str : val;
  279. },
  280. more: function (a, b) {
  281. return Mark._size(a) > b ? a : false;
  282. },
  283. less: function (a, b) {
  284. return Mark._size(a) < b ? a : false;
  285. },
  286. ormore: function (a, b) {
  287. return Mark._size(a) >= b ? a : false;
  288. },
  289. orless: function (a, b) {
  290. return Mark._size(a) <= b ? a : false;
  291. },
  292. between: function (a, b, c) {
  293. a = Mark._size(a);
  294. return a >= b && a <= c ? a : false;
  295. },
  296. equals: function (a, b) {
  297. return a == b ? a : false;
  298. },
  299. notequals: function (a, b) {
  300. return a != b ? a : false;
  301. },
  302. like: function (str, pattern) {
  303. return new RegExp(pattern, "i").test(str) ? str : false;
  304. },
  305. notlike: function (str, pattern) {
  306. return !Mark.pipes.like(str, pattern) ? str : false;
  307. },
  308. upcase: function (str) {
  309. return String(str).toUpperCase();
  310. },
  311. downcase: function (str) {
  312. return String(str).toLowerCase();
  313. },
  314. capcase: function (str) {
  315. return str.replace(/(?:^|\s)\S/g, function (a) { return a.toUpperCase(); });
  316. },
  317. chop: function (str, n) {
  318. return str.length > n ? str.substr(0, n) + "..." : str;
  319. },
  320. tease: function (str, n) {
  321. var a = str.split(/\s+/);
  322. return a.slice(0, n).join(" ") + (a.length > n ? "..." : "");
  323. },
  324. trim: function (str) {
  325. return str.trim();
  326. },
  327. pack: function (str) {
  328. return str.trim().replace(/\s{2,}/g, " ");
  329. },
  330. round: function (num) {
  331. return Math.round(+num);
  332. },
  333. clean: function (str) {
  334. return String(str).replace(/<\/?[^>]+>/gi, "");
  335. },
  336. size: function (obj) {
  337. return obj.length;
  338. },
  339. length: function (obj) {
  340. return obj.length;
  341. },
  342. reverse: function (arr) {
  343. return [].concat(arr).reverse();
  344. },
  345. join: function (arr, separator) {
  346. return arr.join(separator);
  347. },
  348. limit: function (arr, count, idx) {
  349. return arr.slice(+idx || 0, +count + (+idx || 0));
  350. },
  351. split: function (str, separator) {
  352. return str.split(separator || ",");
  353. },
  354. choose: function (bool, iffy, elsy) {
  355. return !!bool ? iffy : (elsy || "");
  356. },
  357. toggle: function (obj, csv1, csv2, str) {
  358. return csv2.split(",")[csv1.match(/\w+/g).indexOf(obj + "")] || str;
  359. },
  360. sort: function (arr, prop) {
  361. var fn = function (a, b) {
  362. return a[prop] > b[prop] ? 1 : -1;
  363. };
  364. return [].concat(arr).sort(prop ? fn : undefined);
  365. },
  366. fix: function (num, n) {
  367. return (+num).toFixed(n);
  368. },
  369. mod: function (num, n) {
  370. return (+num) % (+n);
  371. },
  372. divisible: function (num, n) {
  373. return num && (+num % n) === 0 ? num : false;
  374. },
  375. even: function (num) {
  376. return num && (+num & 1) === 0 ? num : false;
  377. },
  378. odd: function (num) {
  379. return num && (+num & 1) === 1 ? num : false;
  380. },
  381. number: function (str) {
  382. return parseFloat(str.replace(/[^\-\d\.]/g, ""));
  383. },
  384. url: function (str) {
  385. return encodeURI(str);
  386. },
  387. bool: function (obj) {
  388. return !!obj;
  389. },
  390. falsy: function (obj) {
  391. return !obj;
  392. },
  393. first: function (iter) {
  394. return iter.idx === 0;
  395. },
  396. last: function (iter) {
  397. return iter.idx === iter.size - 1;
  398. },
  399. call: function (obj, fn) {
  400. return obj[fn].apply(obj, [].slice.call(arguments, 2));
  401. },
  402. set: function (obj, key) {
  403. Mark.globals[key] = obj; return "";
  404. },
  405. log: function (obj) {
  406. console.log(obj);
  407. return obj;
  408. }
  409. };
  410. // Shim for IE.
  411. if (typeof String.prototype.trim !== "function") {
  412. String.prototype.trim = function() {
  413. return this.replace(/^\s+|\s+$/g, "");
  414. }
  415. }
  416. // Export for Node.js and AMD.
  417. if (typeof module !== "undefined" && module.exports) {
  418. module.exports = Mark;
  419. }
  420. else if (typeof define === "function" && define.amd) {
  421. define(function() {
  422. return Mark;
  423. });
  424. }