Permalink
Browse files

getting liquored up

  • Loading branch information...
0 parents commit 084e558b1ca491d9160d30937f0167a009325d6a @chjj committed May 8, 2011
Showing with 404 additions and 0 deletions.
  1. +19 −0 LICENSE
  2. +121 −0 README.md
  3. +1 −0 index.js
  4. +228 −0 liquor.js
  5. +1 −0 test/list
  6. +14 −0 test/list.html
  7. +20 −0 test/test.js
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2011, Christopher Jeffrey (http://epsilon-not.net/)
+
+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.
121 README.md
@@ -0,0 +1,121 @@
+# Liquor - a templating engine minus the code
+
+A word of warning: Liquor's idea of a template is that it is separate from
+the code. Liquor follows the philosophy that if you're executing
+raw code within your templates, you might be doing something wrong. This
+is specifically for people who think massive amounts of logic do not belong
+in templates. Liquor doesn't allow for any raw evaluation of code. It tries
+to stay as declarative as possible, while remaining convenient. This is my
+personal templating engine, things may change depending on how I use it.
+
+Liquor is also for nerds who care about their markup to a slightly absurd degree:
+it will maintain whitespace, clean up empty lines, etc. This is all to make
+sure your "style" of markup is preserved.
+
+* * *
+
+This engine has 3 capabilities: __variable interpolation__, __conditionals__,
+and __collection traversal__. Liquor tries to have a very concise notation for
+expressing these statements.
+
+## Variable Interpolation
+
+A variable is represented with an ampersand (`&`) followed by a
+colon (`:`), followed by the variable name and a semicolon (`;`). Similar to an
+HTML entity or character refernce, only with a colon.
+
+ &:data;
+
+Variable names can make use of any character except whitespace characters,
+semicolon, and hash/pound (`#`).
+
+Collection variables can access their members with a hash (`#`).
+
+ &:obj#key;
+
+However, you can also access an object's members the regular JS way:
+
+ &:obj.key;
+
+## Conditionals
+
+A conditional statement is denoted by curly braces, `{` and `}`.
+
+The contents of a conditional will __only__ be included in the output if every
+variable contained within the top level of the containing conditional has a
+truthy value. However, truthy and falsey values differ from those of JS:
+ - Falsey values include `false`, `null`, (and `undefined`, `NaN`).
+ - This means the empty string (`''`) and zero (`0`) are both truthy.
+
+Variables that are booleans (and nulls or any other non-displayable value) will
+not be displayed in any way in the output, however they are taken into account
+when determining the conditional's outcome.
+
+ var hello = '<div>{<p>&:hello;</p>}</div>';
+
+ liquor.compile(hello)({ hello: 'hello world' });
+
+Outputs:
+
+ <div><p>hello world!</p></div>
+
+A variable can be forced into a boolean context with a bang `!`. If this is
+done, the position of the variable within the conditional does not make any
+difference.
+
+ <div>{!!&:num;<p>hello world!</p>}</div>
+
+ liquor.compile(hello)({ num: 250 });
+
+Outputs:
+
+ <div><p>hello world!</p></div>
+
+Whereas using a single exclamation point to check whether the variable is
+false would yield (in this case):
+
+ <div></div>
+
+## Collection Traversal
+
+To traverse through a collection, the contents of the desired output must be
+contained in a wrapper, as demonstrated below. Within the statement,
+the context (`this`) refers to the value/subject of the current iteration.
+
+Note: Liquor will try to duplicate the surrounding whitespace to make things
+look pretty when producing the output of a collection traversal.
+
+ <table>
+ <tr><td>&col[0];</td><td>&col[1];</td></tr>
+ :data[<tr>
+ <td>&:this#color;</td>
+ <td>&:this#animal;</td>
+ </tr>];
+ </table>
+
+ liquor.compile(table)({
+ col: ['color', 'animal'],
+ data: [
+ { color: 'brown', animal: 'bear' },
+ { color: 'black', animal: 'cat' },
+ { color: 'white', animal: 'horse' }
+ ]
+ });
+
+The above will output:
+
+ <table>
+ <tr><td>color</td><td>animal</td></tr>
+ <tr>
+ <td>brown</td>
+ <td>bear</td>
+ </tr>
+ <tr>
+ <td>black</td>
+ <td>cat</td>
+ </tr>
+ <tr>
+ <td>white</td>
+ <td>horse</td>
+ </tr>
+ </table>
1 index.js
@@ -0,0 +1 @@
+module.exports = require('./liquor');
228 liquor.js
@@ -0,0 +1,228 @@
+(function() {
+// liquor - javascript templates
+// Copyright (c) 2011, Christopher Jeffrey (MIT Licensed)
+
+// a very simple lexer
+var tokenize = (function() {
+ var IN_LOOP = 0,
+ IN_CONDITIONAL = 1;
+
+ // i wish i could bring myself
+ // to use an object instead
+ var rules = [
+ ['ESCAPED', /^\\([\s\S])/],
+ ['LOOP_START', /^(\s*):([^\s[;]+)\[/],
+ ['LOOP_END', /^\];/],
+ ['TMP_VAR', /^(!*)&:([^;]+);/],
+ ['COND_START', /^{/],
+ ['COND_END', /^}/]
+ ];
+
+ // basically anything that
+ // isnt the above patterns
+ rules.push(['TEXT',
+ RegExp([
+ '^([\\s\\S]+?)(?=',
+ rules.map(function(r) {
+ return r[1].source.slice(1);
+ }).join('|'),
+ '|$)'
+ ].join(''))
+ ]);
+
+ return function(src) {
+ var stack = [], tokens = [];
+ var cap, type, pos = 0;
+
+ var state = function() {
+ return stack[stack.length-1];
+ };
+
+ var scan = function() {
+ for (var i = 0, l = rules.length; i < l; i++) {
+ if (cap = rules[i][1].exec(src)) {
+ type = rules[i][0];
+ src = src.slice(cap[0].length);
+ return true;
+ }
+ }
+ };
+
+ src = src.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
+
+ // scan for tokens
+ // its easy to do some of the
+ // exception throwing here
+ while (scan()) {
+ tokens.push({
+ type: type,
+ cap: cap
+ });
+ switch (type) {
+ case 'LOOP_START':
+ stack.push(IN_LOOP);
+ break;
+ case 'LOOP_END':
+ if (state() === IN_LOOP) {
+ stack.pop();
+ } else {
+ throw new
+ SyntaxError('Unexpected "];" at: ' + pos);
+ }
+ break;
+ case 'COND_START':
+ stack.push(IN_CONDITIONAL);
+ break;
+ case 'COND_END':
+ if (state() !== IN_CONDITIONAL) {
+ throw new
+ SyntaxError('Unexpected "}" at: ' + pos);
+ }
+ stack.pop();
+ break;
+ }
+ pos += cap[0].length;
+ }
+ if (stack.length > 0) {
+ throw new
+ SyntaxError('Unexpected EOF.');
+ }
+ return tokens;
+ };
+})();
+
+// recursive descent is my only god.
+// this will parse the tokens and output a one-line string expression.
+var parse = (function() {
+ var escape = function(txt) { // escape regular text
+ return txt.replace(/"/g, '\\"').replace(/\n/g, '\\n');
+ };
+
+ // if a `with` statement were used,
+ // this wouldn't be necessary
+ var name = function(n) {
+ if (n.indexOf('this') !== 0) n = '__locals.' + n;
+ return n.replace(/#/g, '.').replace(/\.(\d+)/g, '[$1]');
+ };
+
+ var cur, tokens;
+ var next = function() {
+ cur = tokens.shift();
+ return cur;
+ };
+
+ var conditional = function() {
+ var body = [], checks = [];
+ while (next().type !== 'COND_END') {
+ // need to grab all the variables in the top-level for
+ // conditional checks (this includes collection vars)
+ if (cur.type === 'TMP_VAR' || cur.type === 'LOOP_START') {
+ checks.push(
+ (cur.type === 'TMP_VAR' ? (cur.cap[1] || '') : '')
+ + '__help.truthy(' + name(cur.cap[2]) + ')'
+ );
+ }
+ body.push(tok());
+ }
+ return '"+((' + checks.join(' && ') + ') ? "' + body.join('') + '" : "")+"';
+ };
+
+ // pretty straightforward but need to
+ // remeber to manage the whitespace properly
+ var loop = function() {
+ var body = [], token = cur;
+ while (next().type !== 'LOOP_END') {
+ body.push(tok());
+ }
+ return [
+ '"+(__help.iterate(',
+ name(token.cap[2]),
+ ', function() { return "',
+ escape(token.cap[1]),
+ body.join(''),
+ '"; }))+"'
+ ].join('');
+ };
+
+ var tok = function() {
+ var token = cur;
+ switch (token.type) {
+ case 'TMP_VAR':
+ return !token.cap[1]
+ ? '"+__help.show(' + name(token.cap[2]) + ')+"'
+ : '';
+ case 'LOOP_START':
+ return loop();
+ case 'COND_START':
+ return conditional();
+ case 'ESCAPED':
+ case 'TEXT':
+ return escape(token.cap[1]);
+ default:
+ throw new
+ SyntaxError('Unexpected token: ' + token.cap[0]);
+ }
+ };
+
+ return function(src) {
+ var out = [];
+ tokens = tokenize(src);
+ while (next()) {
+ out.push(tok());
+ }
+ return '"' + out.join('') + '"';
+ };
+})();
+
+var compile = (function() {
+ // helper functions to use inside the compiled template
+ var helpers = {
+ // we use this for collection traversal statements
+ iterate: function(obj, func) {
+ var str = [];
+ if (typeof obj.length === 'number') {
+ for (var i = 0, l = obj.length; i < l; i++) {
+ str.push(func.call(obj[i]));
+ }
+ } else {
+ var k = Object.keys(obj);
+ for (var i = 0, l = k.length; i < l; i++) {
+ str.push(func.call(obj[k[i]]));
+ }
+ }
+ return str.join('');
+ },
+ // custom truthy/falsey checks
+ // basically the same as JS, except a
+ // variable is truthy if it is '' or 0
+ truthy: function(v) {
+ return !(v === undefined || v === false || v === null || v !== v);
+ },
+ // the function to determine whether to actually display a value
+ // display any truthy value except for "true"
+ show: function(v) {
+ return (!helpers.truthy(v) || v === true) ? '' : v;
+ }
+ };
+ return function(src, debug) {
+ if (debug === 'debug') return parse(src);
+ var func = Function('__locals, __help', 'return ' + parse(src) + ';');
+ return function(locals) { // curry on the helpers
+ // do some post-processing here to make whitespace nice and neat
+ var out = func.call(locals, locals, helpers);
+ out = out.replace(/(\n)\n+/g, '$1').replace(/(\n)[\x20\t]+\n/g, '$1');
+ return out;
+ };
+ };
+})();
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = exports = compile;
+ exports.compile = compile;
+ exports.parse = parse;
+ exports.tokenize = tokenize;
+} else {
+ this.liquor = compile;
+}
+
+}).call(this);
1 test/list
@@ -0,0 +1 @@
+"<ol>"+(__help.iterate(__locals.list, function() { return "\n <li>\n <a href=\""+__help.show(this.href)+"\">"+__help.show(this.text)+"</a>\n "+((__help.truthy(this.datetime)) ? "<time datetime=\""+__help.show(this.datetime)+"\">\n "+((__help.truthy(this.time)) ? ""+__help.show(this.time)+"" : "")+"\n </time>" : "")+"\n </li>"; }))+""+(__help.iterate(__locals.list2, function() { return "\n <li>\n <a href=\""+__help.show(this.href)+"\">"+__help.show(this.text)+"</a>\n "+((__help.truthy(this.datetime)) ? "<time datetime=\""+__help.show(this.datetime)+"\">\n "+((__help.truthy(this.time)) ? ""+__help.show(this.time)+"" : "")+"\n </time>" : "")+"\n </li>"; }))+"\n</ol>"
14 test/list.html
@@ -0,0 +1,14 @@
+<ol>
+ :list[<li>
+ <a href="&:this#href;">&:this#text;</a>
+ {<time datetime="&:this#datetime;">
+ {&:this#time;}
+ </time>}
+ </li>];
+ :list2[<li>
+ <a href="&:this#href;">&:this#text;</a>
+ {<time datetime="&:this#datetime;">
+ {&:this#time;}
+ </time>}
+ </li>];
+</ol>
20 test/test.js
@@ -0,0 +1,20 @@
+var assert = require('assert');
+var fs = require('fs');
+
+var compile = require('../').compile;
+
+fs.readdirSync(__dirname).forEach(function(file) {
+ if (file.indexOf('.html') === -1) return;
+ var counterpart = file.split('.')[0];
+ var template = fs.readFileSync('./' + file, 'utf-8');
+ template = compile(template, 'debug');
+ try {
+ var result = fs.readFileSync('./' + counterpart, 'utf-8');
+ } catch(e) {
+ fs.writeFileSync('./' + counterpart, template);
+ return;
+ }
+ assert.ok(template === result);
+ Function('', 'return ' + template + ';');
+ console.log(file + ' compiled successfully.');
+});

0 comments on commit 084e558

Please sign in to comment.