Permalink
Browse files

faster parsing, added pretty printing

  • Loading branch information...
1 parent f3f7699 commit d2f1c4d28ad5f8b371981b367b232a0c6f72da64 @chjj committed May 30, 2011
Showing with 166 additions and 93 deletions.
  1. +20 −12 README.md
  2. +143 −78 lib/liquor.js
  3. 0 test/{test.js → index.js}
  4. +1 −1 test/list
  5. +2 −2 test/list.html
View
@@ -12,6 +12,8 @@ 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.
+Update: Added experimental pretty print option.
+
* * *
This engine has 3 capabilities: __variable interpolation__, __conditionals__,
@@ -52,8 +54,9 @@ 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' });
+
+ var template = liquor.compile(hello);
+ template({ hello: 'hello world' });
Outputs:
@@ -65,7 +68,8 @@ difference.
<div>{!!&:num;<p>hello world!</p>}</div>
- liquor.compile(hello)({ num: 250 });
+ var template = liquor.compile(hello);
+ template({ num: 250 });
Outputs:
@@ -82,18 +86,19 @@ 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>
+ #:col[<td>&:this;</td>];
+ </tr>
+ #:data[<tr>
+ <td>&:this.color;</td>
+ <td>&:this.animal;</td>
</tr>];
</table>
- liquor.compile(table)({
+ var template = liquor.compile(table);
+
+ template({
col: ['color', 'animal'],
data: [
{ color: 'brown', animal: 'bear' },
@@ -105,7 +110,10 @@ look pretty when producing the output of a collection traversal.
The above will output:
<table>
- <tr><td>color</td><td>animal</td></tr>
+ <tr>
+ <td>color</td>
+ <td>animal</td>
+ </tr>
<tr>
<td>brown</td>
<td>bear</td>
View
@@ -1,8 +1,6 @@
-(function() {
// liquor - javascript templates
// Copyright (c) 2011, Christopher Jeffrey (MIT Licensed)
-
-// a very simple lexer
+(function() {
var tokenize = (function() {
var IN_LOOP = 0,
IN_CONDITIONAL = 1;
@@ -11,7 +9,7 @@ var tokenize = (function() {
// to use an object instead
var rules = [
['ESCAPED', /^\\([\s\S])/],
- ['LOOP_START', /^(\s*):([^\s[;]+)\[/],
+ ['LOOP_START', /^#:([^\s[;]+)\[/],
['LOOP_END', /^\];/],
['TMP_VAR', /^(!*)&:([^;]+);/],
['COND_START', /^{/],
@@ -21,67 +19,58 @@ var tokenize = (function() {
// 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(''))
+ new RegExp('^([\\s\\S]+?)(?='
+ + rules.map(function(rule) {
+ return rule[1].source.slice(1);
+ }).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;
- }
- }
- };
+ var cap, type, pos = 0, rule, i, state;
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);
+ while (src.length) {
+ for (i = 0; rule = rules[i++];) {
+ if (cap = src.match(rule[1])) {
+ type = rule[0];
+ tokens.push({ type: type, cap: cap });
+ src = src.slice(cap[0].length);
+
+ state = stack[stack.length-1];
+ 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;
}
- stack.pop();
+ pos += cap[0].length;
+
break;
+ }
}
- pos += cap[0].length;
}
if (stack.length > 0) {
throw new
@@ -94,33 +83,38 @@ var tokenize = (function() {
// recursive descent is my only god.
// this will parse the tokens and output a one-line string expression.
var parse = (function() {
+ var cur, tokens;
+
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 name = function(name) {
+ if (!~name.indexOf('this')) name = '__locals.' + name;
+ return name.replace(/#/g, '.').replace(/\.(\d+)/g, '[$1]');
};
- var cur, tokens;
var next = function() {
cur = tokens.shift();
return cur;
};
var conditional = function() {
- var body = [], checks = [];
+ var body = [], checks = [], bool, variable;
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]) + ')'
- );
+ if (cur.type === 'TMP_VAR') {
+ bool = cur.cap[1] || '';
+ variable = name(cur.cap[2]);
+ } else {
+ bool = '';
+ variable = name(cur.cap[1]);
+ }
+ checks.push(bool + '__help.truthy(' + variable + ')');
}
body.push(tok());
}
@@ -134,14 +128,8 @@ var parse = (function() {
while (next().type !== 'LOOP_END') {
body.push(tok());
}
- return [
- '"+(__help.iterate(',
- name(token.cap[2]),
- ', function() { return "',
- escape(token.cap[1]),
- body.join(''),
- '"; }))+"'
- ].join('');
+ return '"+(__help.iterate(' + name(token.cap[1])
+ + ', function() { return "' + body.join('') + '"; }))+"';
};
var tok = function() {
@@ -180,7 +168,7 @@ var compile = (function() {
// we use this for collection traversal statements
iterate: function(obj, func) {
var str = [];
- if (typeof obj.length === 'number') {
+ if (typeof obj.length === 'number' && typeof obj !== 'function') {
for (var i = 0, l = obj.length; i < l; i++) {
str.push(func.call(obj[i]));
}
@@ -205,15 +193,92 @@ var compile = (function() {
return ((t === 'number' && v === v) || t === 'string') ? v : '';
}
};
- return function(src, debug) {
- if (debug === 'debug') return parse(src);
+
+ return function(src, opt) {
+ if (opt === 'debug') return parse(src);
+ opt = opt || {};
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;
- };
+ // curry on the helpers
+ if (opt.pretty === false) {
+ return function(locals) {
+ return func.call(locals, locals, helpers);
+ };
+ } else {
+ return function(locals) {
+ return pretty(func.call(locals, locals, helpers));
+ };
+ }
+ };
+})();
+
+var pretty = (function() {
+ var indent = function(num) {
+ return Array(num + 1).join(' ');
+ };
+ var closing = {
+ meta: true,
+ link: true,
+ input: true,
+ img: true,
+ hr: true,
+ area: true,
+ base: true,
+ col: true,
+ br: true,
+ wbr: true
+ };
+ return function(text) {
+ // temporarily remove PRE elements before processing
+ var place = [];
+ text = text.replace(
+ /<(pre|textarea|li|a|p)(?:\s[^>]+)?>[\s\S]+?<\/\1>/g,
+ function($0, $1) {
+ if ($1 === 'pre') {
+ $0 = $0.replace(/\r?\n/g, '&#x0A;');
+ } else {
+ $0 = $0.replace(/(>)\s+|\s+(<)/g, '$1$2');
+ }
+ return '<' + (place.push($0)-1) + (Array($0.length-2).join('%')) + '/>';
+ });
+
+ // indent elements
+ var stack = [], tag, cap, num = 0;
+ text = text.replace(/(>)\s+|\s+(<)/g, '$1$2').replace(/[\r\n]/g, '');
+ while (cap = text.match(/^([\s\S]*?)<([^>]+)>/)) {
+ text = text.slice(cap[0].length);
+ tag = cap[2].split(' ')[0];
+ if (cap[1]) stack.push(indent(num) + cap[1]);
+ //if (tag[0] === '!') continue;
+ if (tag[0] !== '/') {
+ stack.push(indent(num) + '<' + cap[2] + '>');
+ if (cap[2].slice(-1) !== '/' && !closing[tag] && tag[0] !== '!') num++;
+ } else {
+ num--;
+ stack.push(indent(num) + '<' + cap[2] + '>');
+ }
+ }
+
+ text = stack.join('\n');
+
+ // restore the PRE elements to their original locations
+ text = text.replace(/<(\d+)%*\/>/g, function($0, $1) {
+ return place[$1];
+ });
+
+ // wrap paragraphs
+ text = text.replace(/([\x20\t]*)<p>([\s\S]+?)<\/p>/g, function($0, $1, $2) {
+ var indent = $1 + ' ', text = $2;
+ text = (indent + text
+ .replace(/[\t\r\n]+/g, '')
+ .replace(/(<\/[^>]+>|\/>)(?=\s*<\w)/g, '$1\n' + indent)
+ .replace(/(.{75,}?\s+(?![^<]+>))/g, '$1\n' + indent)
+ .replace(/([^<>\n]{50,}?)(<[^<]{15,}>)/g, '$1\n' + indent + '$2')
+ .replace(/\s+(\/>)/g, '$1')
+ );
+ return $1 + '<p>\n' + text + '\n' + $1 + '</p>';
+ });
+
+ return text;
};
})();
File renamed without changes.
View
@@ -1 +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>"
+"<ol>\n "+(__help.iterate(__locals.list, function() { return "<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 "+(__help.iterate(__locals.list2, function() { return "<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>"
View
@@ -1,11 +1,11 @@
<ol>
- :list[<li>
+ #:list[<li>
<a href="&:this#href;">&:this#text;</a>
{<time datetime="&:this#datetime;">
{&:this#time;}
</time>}
</li>];
- :list2[<li>
+ #:list2[<li>
<a href="&:this#href;">&:this#text;</a>
{<time datetime="&:this#datetime;">
{&:this#time;}

0 comments on commit d2f1c4d

Please sign in to comment.