├── .npmignore ├── LICENSE ├── Makefile ├── README.md ├── index.js ├── lib ├── lexer.js ├── liquor.js ├── liquor_minimal.js └── parser.js ├── package.json └── test ├── index.js ├── list └── list.html /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | test/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2012, Christopher Jeffrey (github.com/chjj) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @cp lib/liquor_minimal.js . 3 | @uglifyjs -o liquor.min.js liquor.js 4 | 5 | clean: 6 | @rm liquor.js 7 | @rm liquor_minimal.min.js 8 | 9 | .PHONY: all clean 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Liquor 2 | 3 | Liquor is a templating engine for node. It's very lightweight. It's essentially 4 | embedded javascript with some shorthand significant whitespace notation 5 | available. This is to discourage use of raw code and make templates look nicer. 6 | 7 | ## Usage 8 | 9 | Backticks are used for evaluation, while `#{}` is used for interpolation. 10 | 11 | ``` html 12 | ?:data 13 |
#{this} | 17 ||
#{this.color} | 21 |#{this.animal} | 22 |
Sorry, there was a problem: #{error}.
29 |Please, try again!
30 | !:error 31 |Sorry, no error message.
32 |#{this} | 43 | `})` 44 ||
#{this.color} | 48 |#{this.animal} | 49 |
Sorry, there was a problem: #{error}.
56 |Please, try again!
57 | `} else {` 58 |Sorry, no error message.
59 | `}` 60 |#{key}: #{message.content}
70 | `})` 71 | ``` 72 | 73 | If you're worried about the notorious "undefined" problem with variables 74 | expressed in raw evaluation of JS, you can access them as properties on a 75 | variable called `$`, which exists within the context of a template, and holds 76 | all of the locals and helpers: 77 | 78 | e.g. 79 | 80 | ``` html 81 | `if ($.messages) {`#{JSON.stringify(messages)}
`}` 82 | ``` 83 | 84 | ## License 85 | (c) Copyright 2011-2012, Christopher Jeffrey. See LICENSE for more info. 86 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/liquor'); -------------------------------------------------------------------------------- /lib/lexer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Liquor - Lexer 3 | * Copyright (c) 2011-2012, Christopher Jeffrey. (MIT Licensed) 4 | */ 5 | 6 | var lexer = function(src, options) { 7 | var i = 0 8 | , l = src.length 9 | , ch 10 | , buff = '' 11 | , key 12 | , line = 1 13 | , offset = 0 14 | , tokens = [] 15 | , stack = [] 16 | , indent 17 | , indents = [] 18 | , newline = true 19 | , size; 20 | 21 | var state = function() { 22 | return stack[stack.length-1]; 23 | }; 24 | 25 | var out = function() { 26 | if (!options.pretty) return buff; 27 | var i = indents.length * size; 28 | if (!i) return buff; 29 | return outdent(buff, i); 30 | }; 31 | 32 | for (; i < l; i++) { 33 | ch = src[i]; 34 | offset++; 35 | 36 | if (ch > ' ' && newline) { 37 | if (!size && indent != null && indent < buff.length) { 38 | size = buff.length - indent; 39 | } 40 | indent = buff.length; 41 | // assert(indent % size === 0); 42 | while (indents[indents.length-1] >= indent) { 43 | tokens.push({ 44 | type: 'end', 45 | line: line 46 | }); 47 | indents.pop(); 48 | } 49 | if (!options.pretty) buff = ''; 50 | newline = false; 51 | } 52 | 53 | switch (ch) { 54 | case '\r': 55 | if (src[i+1] === '\n') break; 56 | ; 57 | case '\n': 58 | line++; 59 | offset = 0; 60 | switch (state()) { 61 | case 'evaluate': 62 | case 'interpolate': 63 | buff += ch; 64 | break; 65 | default: 66 | if (stack.length) { 67 | tokens.push({ 68 | type: stack.pop(), 69 | name: buff, 70 | line: line 71 | }); 72 | } else { 73 | if (options.pretty) buff += '\\n'; 74 | tokens.push({ 75 | type: 'text', 76 | text: out(), 77 | line: line 78 | }); 79 | } 80 | buff = ''; 81 | newline = true; 82 | break; 83 | } 84 | break; 85 | case '@': 86 | case '?': 87 | case '!': 88 | if (src[i+1] === ':' && !buff.trim()) { 89 | switch (ch) { 90 | case '@': 91 | stack.push('iterate'); 92 | break; 93 | case '?': 94 | stack.push('if'); 95 | break; 96 | case '!': 97 | stack.push('not'); 98 | break; 99 | } 100 | indents.push(indent); 101 | i++; 102 | buff = ''; 103 | } else { 104 | buff += ch; 105 | } 106 | break; 107 | case '`': 108 | switch (state()) { 109 | case 'evaluate': 110 | tokens.push({ 111 | type: 'evaluate', 112 | code: buff, 113 | line: line 114 | }); 115 | buff = ''; 116 | stack.pop(); 117 | break; 118 | case 'interpolate': 119 | buff += ch; 120 | break; 121 | default: 122 | tokens.push({ 123 | type: 'text', 124 | text: out(), 125 | line: line 126 | }); 127 | buff = ''; 128 | stack.push('evaluate'); 129 | break; 130 | } 131 | break; 132 | case '#': 133 | if (src[i+1] === '{') { 134 | switch (state()) { 135 | case 'interpolate': 136 | case 'evaluate': 137 | buff += ch; 138 | break; 139 | default: 140 | tokens.push({ 141 | type: 'text', 142 | text: out(), 143 | line: line 144 | }); 145 | buff = ''; 146 | i++; 147 | stack.push('interpolate'); 148 | if (src[i+1] === '{') i++; 149 | break; 150 | } 151 | } else { 152 | buff += ch; 153 | } 154 | break; 155 | case '}': 156 | switch (state()) { 157 | case 'interpolate': 158 | if (src[i+1] === '}') { 159 | i++; 160 | tokens.push({ 161 | type: 'interpolate', 162 | escape: true, 163 | code: buff, 164 | line: line 165 | }); 166 | } else { 167 | tokens.push({ 168 | type: 'interpolate', 169 | code: buff, 170 | line: line 171 | }); 172 | } 173 | buff = ''; 174 | stack.pop(); 175 | break; 176 | default: 177 | buff += ch; 178 | break; 179 | } 180 | break; 181 | case '"': 182 | switch (state()) { 183 | case 'interpolate': 184 | case 'evaluate': 185 | buff += '"'; 186 | break; 187 | case 'if': 188 | case 'not': 189 | case 'iterate': 190 | default: 191 | buff += '\\"'; 192 | break; 193 | } 194 | break; 195 | default: 196 | buff += ch; 197 | break; 198 | } 199 | } 200 | 201 | if (buff) { 202 | tokens.push({ 203 | type: 'text', 204 | text: out(), 205 | line: line 206 | }); 207 | } 208 | 209 | return tokens; 210 | }; 211 | 212 | var outdent = function(src, n) { 213 | if (!n) return src; 214 | return src.replace(new RegExp('^[ \t]{' + n + '}', 'gm'), ''); 215 | }; 216 | 217 | /** 218 | * Expose 219 | */ 220 | 221 | module.exports = lexer; 222 | -------------------------------------------------------------------------------- /lib/liquor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Liquor (https://github.com/chjj/liquor) 3 | * Javascript templates minus the code. 4 | * Copyright (c) 2011-2012, Christopher Jeffrey. (MIT Licensed) 5 | */ 6 | 7 | var lexer = require('./lexer') 8 | , parser = require('./parser'); 9 | 10 | /** 11 | * Compile 12 | */ 13 | 14 | var liquor = function(src, options) { 15 | options = options || liquor.defaults; 16 | 17 | src = parser(lexer(src, options), options); 18 | 19 | if (options === 'debug') return src; 20 | 21 | var func = new Function('$, each, __escape', src); 22 | return function(locals) { 23 | return func(locals || {}, each, escape); 24 | }; 25 | }; 26 | 27 | liquor.defaults = {}; 28 | 29 | /** 30 | * Helper 31 | */ 32 | 33 | var escape = function(html, encode) { 34 | return ((html || '') + '') 35 | .replace(/&/g, '&') 36 | .replace(//g, '>') 38 | .replace(/"/g, '"') 39 | .replace(/'/g, '''); 40 | }; 41 | 42 | var each = function(obj, func) { 43 | if (!obj) return; 44 | 45 | var l = obj.length 46 | , i = 0; 47 | 48 | if (typeof l === 'number' && typeof obj !== 'function') { 49 | for (; i < l; i++) { 50 | if (func.call(obj[i], obj[i], i, obj) === false) { 51 | break; 52 | } 53 | } 54 | } else { 55 | var keys = Object.keys(obj) 56 | , l = keys.length 57 | , key; 58 | 59 | for (; i < l; i++) { 60 | key = keys[i]; 61 | if (func.call(obj[key], obj[key], key, obj) === false) { 62 | break; 63 | } 64 | } 65 | } 66 | }; 67 | 68 | /** 69 | * Expose 70 | */ 71 | 72 | liquor.compile = liquor; 73 | 74 | if (typeof module !== 'undefined') { 75 | module.exports = liquor; 76 | } else { 77 | this.liquor = liquor; 78 | } 79 | -------------------------------------------------------------------------------- /lib/liquor_minimal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Liquor (https://github.com/chjj/liquor) 3 | * A minimal version of liquor, suitable for the client-side. 4 | * Copyright (c) 2011-2012, Christopher Jeffrey. (MIT Licensed) 5 | */ 6 | 7 | ;(function() { 8 | 9 | /** 10 | * Liquor 11 | */ 12 | 13 | var liquor = (function() { 14 | var rules = { 15 | iterate: /^( *)@:([^\s]+) *([^\n]*(?:\n+\1 {2}[^\n]*)*)/, 16 | condition: /^( *)(\?|!):([^\s]+) *([^\n]*(?:\n+\1 {2}[^\n]*)*)/, 17 | evaluate: /^`([^`]+)`/, 18 | interpolate: /^#{([^}]+)}|^#{{([^}]+)}}/, 19 | text: /^[^\0]+?(?= *@:| *\?:| *!:|`|#{|$)/ 20 | }; 21 | 22 | var depth 23 | , size; 24 | 25 | var lexer = function(src) { 26 | var out = '' 27 | , cap; 28 | 29 | depth++; 30 | 31 | while (src) { 32 | if (cap) src = src.substring(cap[0].length); 33 | if (cap = rules.iterate.exec(src)) { 34 | out += '"); each(' 35 | + cap[2] 36 | + ', function(v) { __out.push("' 37 | + lexer(cap[3]) 38 | + '"); }); __out.push("'; 39 | continue; 40 | } 41 | if (cap = rules.condition.exec(src)) { 42 | out += '"); if (' 43 | + (cap[2] === '!' ? '!' : '') 44 | + '(typeof ' 45 | + cap[3] 46 | + ' !== "undefined" && ' 47 | + cap[3] 48 | + ')){ __out.push("' 49 | + lexer(cap[4]) 50 | + '"); } __out.push("'; 51 | continue; 52 | } 53 | if (cap = rules.evaluate.exec(src)) { 54 | out += '"); ' 55 | + cap[1] 56 | + '; __out.push("'; 57 | continue; 58 | } 59 | if (cap = rules.interpolate.exec(src)) { 60 | if (cap[2]) { 61 | out += '", (__escape(' 62 | + cap[1] 63 | + ')), "'; 64 | } else { 65 | out += '", (' 66 | + cap[1] 67 | + '), "'; 68 | } 69 | continue; 70 | } 71 | if (cap = rules.text.exec(src)) { 72 | out += outdent(cap[0], depth * size) 73 | .replace(/\n+$/, '') 74 | .replace(/"/g, '\\"') 75 | .replace(/\n/g, '\\n'); 76 | continue; 77 | } 78 | if (src) { 79 | throw new 80 | Error('Liquor: Error. Please report this as an issue.'); 81 | } 82 | } 83 | 84 | depth--; 85 | 86 | return out; 87 | }; 88 | 89 | return function(src, opt) { 90 | depth = -1; 91 | size = indent(src); 92 | 93 | // normalize whitespace 94 | src = src 95 | .replace(/\r\n|\r/g, '\n') 96 | .replace(/\t/g, ' '); 97 | 98 | // wrap 99 | src = 'with ($) { var __out = []; __out.push("' 100 | + lexer(src) 101 | + '"); return __out.join(""); }'; 102 | 103 | if (opt === 'debug') return src; 104 | 105 | var func = new Function('$, each, __escape', src); 106 | return function(locals) { 107 | return func(locals || {}, each, escape); 108 | }; 109 | }; 110 | })(); 111 | 112 | /** 113 | * Helpers 114 | */ 115 | 116 | var escape = function(html, encode) { 117 | return ((html || '') + '') 118 | .replace(/&/g, '&') 119 | .replace(//g, '>') 121 | .replace(/"/g, '"') 122 | .replace(/'/g, '''); 123 | }; 124 | 125 | var each = function(obj, func) { 126 | if (!obj) return; 127 | 128 | var l = obj.length 129 | , i = 0; 130 | 131 | if (typeof l === 'number' && typeof obj !== 'function') { 132 | for (; i < l; i++) { 133 | if (func.call(obj[i], obj[i], i, obj) === false) { 134 | break; 135 | } 136 | } 137 | } else { 138 | var keys = Object.keys(obj) 139 | , l = keys.length 140 | , key; 141 | 142 | for (; i < l; i++) { 143 | key = keys[i]; 144 | if (func.call(obj[key], obj[key], key, obj) === false) { 145 | break; 146 | } 147 | } 148 | } 149 | }; 150 | 151 | var outdent = function(src, n) { 152 | if (!n) return src; 153 | return src.replace(new RegExp('^ {' + n + '}', 'gm'), ''); 154 | }; 155 | 156 | var indent = function(src) { 157 | var start = /^( +)(?:[^\n]*\n+\1)+\s*$/.exec(src); 158 | if (start) src = outdent(src, start[1].length); 159 | var size = /\n( +)/.exec(src); 160 | return size ? size[1].length : 0; 161 | }; 162 | 163 | /** 164 | * Expose 165 | */ 166 | 167 | liquor.compile = liquor; 168 | 169 | if (typeof module !== 'undefined') { 170 | module.exports = liquor; 171 | } else { 172 | this.liquor = liquor; 173 | } 174 | 175 | /** 176 | * Client-Side Shim 177 | */ 178 | 179 | if (!Object.keys) { 180 | var hop = Object.prototype.hasOwnProperty; 181 | Object.keys = function(o) { 182 | var k, c = []; 183 | if (o) for (k in o) if (hop.call(o, k)) c.push(k); 184 | return c; 185 | }; 186 | } 187 | 188 | }).call(this); 189 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Liquor - Parser 3 | * Copyright (c) 2011-2012, Christopher Jeffrey. (MIT Licensed) 4 | */ 5 | 6 | var parser = (function() { 7 | var token 8 | , tokens; 9 | 10 | var next = function() { 11 | return token = tokens.pop(); 12 | }; 13 | 14 | var tok = function() { 15 | var token_ = token 16 | , body = ''; 17 | 18 | switch (token_.type) { 19 | case 'evaluate': 20 | return '"); ' 21 | + token_.code 22 | + '; __out.push("'; 23 | case 'interpolate': 24 | if (token_.escape) { 25 | return '", (__escape(' 26 | + token_.code 27 | + ')), "'; 28 | } 29 | return '", (' 30 | + token_.code 31 | + '), "'; 32 | case 'iterate': 33 | while (next().type !== 'end') { 34 | body += tok(); 35 | } 36 | return '"); each(' 37 | + token_.name 38 | + ', function() { __out.push("' 39 | + body 40 | + '"); }); __out.push("'; 41 | case 'if': 42 | case 'not': 43 | while (next().type !== 'end') { 44 | body += tok(); 45 | } 46 | return '"); if (' 47 | + (token_.type === 'not' ? '!' : '') 48 | + '(typeof ' 49 | + token_.name 50 | + ' !== "undefined" && ' 51 | + token_.name 52 | + ')) { __out.push("' 53 | + body 54 | + '"); } __out.push("'; 55 | case 'text': 56 | return token_.text; 57 | } 58 | }; 59 | 60 | return function(src, options) { 61 | var out = ''; 62 | 63 | tokens = src.reverse(); 64 | 65 | while (next()) { 66 | out += tok(); 67 | } 68 | 69 | out = 'with ($) { var __out = []; __out.push("' 70 | + out 71 | + '"); return __out.join(""); }'; 72 | 73 | token = null; 74 | tokens = null; 75 | 76 | return out; 77 | }; 78 | })(); 79 | 80 | /** 81 | * Expose 82 | */ 83 | 84 | module.exports = parser; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liquor", 3 | "description": "Templates, minus the code.", 4 | "author": "Christopher Jeffrey", 5 | "version": "0.0.5", 6 | "main": "./lib/liquor.js", 7 | "repository": "git://github.com/chjj/liquor.git", 8 | "homepage": "https://github.com/chjj/liquor", 9 | "bugs": { "url": "https://github.com/chjj/liquor/issues" }, 10 | "keywords": [ "template", "html" ], 11 | "tags": [ "template", "html" ] 12 | } 13 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , fs = require('fs') 3 | , compile = require('../').compile; 4 | 5 | fs.readdirSync(__dirname).forEach(function(file) { 6 | if (file.indexOf('.html') === -1) return; 7 | 8 | var counterpart = file.split('.')[0] 9 | , template = fs.readFileSync(__dirname + '/' + file, 'utf8'); 10 | 11 | template = compile(template, 'debug'); 12 | 13 | try { 14 | var result = fs.readFileSync(__dirname + '/' + counterpart, 'utf8'); 15 | } catch(e) { 16 | fs.writeFileSync(__dirname + '/' + counterpart, template); 17 | return; 18 | } 19 | 20 | assert.ok(template === result); 21 | Function('', template); 22 | console.log(file + ' compiled successfully.'); 23 | }); -------------------------------------------------------------------------------- /test/list: -------------------------------------------------------------------------------- 1 | with ($) { var __out = []; __out.push("