Skip to content

Commit

Permalink
getting liquored up
Browse files Browse the repository at this point in the history
  • Loading branch information
chjj committed May 8, 2011
0 parents commit 084e558
Show file tree
Hide file tree
Showing 7 changed files with 404 additions and 0 deletions.
19 changes: 19 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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 changes: 121 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./liquor');
228 changes: 228 additions & 0 deletions liquor.js
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions test/list
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions test/list.html
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit 084e558

Please sign in to comment.