Comparing version 0.6.3 to 0.7.0
0.7.0 / 2011-03-04 | ||
================== | ||
* Removed `:javascript` filter (it doesn't really do anything special, use `script` tags) | ||
* Added pipe-less text support. Tags that only accept text nodes (`script`, `textarea`, etc) do not require `|`. | ||
* Added `:text` filter for ad-hoc pipe-less | ||
* Added flexible indentation. Tabs, arbitrary number of spaces etc | ||
* Added conditional-comment support. Closes #146 | ||
* Added block comment support | ||
* Added rss example | ||
* Added `:stylus` filter | ||
* Added `:discount` filter | ||
* Fixed; auto-detect xml and do not self-close tags. Closes #147 | ||
* Fixed whitespace issue. Closes #118 | ||
* Fixed attrs. `,`, `=`, and `:` within attr value strings are valid Closes #133 | ||
* Fixed; only output "" when code == null. Ex: `span.name= user.name` when undefined or null will not output "undefined". Closes #130 | ||
* Fixed; throw on unexpected token instead of hanging | ||
0.6.3 / 2011-02-02 | ||
@@ -3,0 +21,0 @@ ================== |
@@ -12,7 +12,7 @@ | ||
var nodes = require('./nodes'), | ||
filters = require('./filters'), | ||
doctypes = require('./doctypes'), | ||
selfClosing = require('./self-closing'), | ||
utils = require('./utils'); | ||
var nodes = require('./nodes') | ||
, filters = require('./filters') | ||
, doctypes = require('./doctypes') | ||
, selfClosing = require('./self-closing') | ||
, utils = require('./utils'); | ||
@@ -28,4 +28,4 @@ /** | ||
var Compiler = module.exports = function Compiler(node, options) { | ||
this.options = options = options || {}; | ||
this.node = node; | ||
this.options = options = options || {}; | ||
this.node = node; | ||
}; | ||
@@ -38,250 +38,272 @@ | ||
Compiler.prototype = { | ||
/** | ||
* Compile parse tree to JavaScript. | ||
* | ||
* @api public | ||
*/ | ||
compile: function(){ | ||
this.buf = []; | ||
this.visit(this.node); | ||
return this.buf.join('\n'); | ||
}, | ||
/** | ||
* Buffer the given `str` optionally escaped. | ||
* | ||
* @param {String} str | ||
* @param {Boolean} esc | ||
* @api public | ||
*/ | ||
buffer: function(str, esc){ | ||
if (esc) str = utils.escape(str); | ||
this.buf.push("buf.push('" + str + "');"); | ||
}, | ||
/** | ||
* Buffer the given `node`'s lineno. | ||
* | ||
* @param {Node} node | ||
* @api public | ||
*/ | ||
line: function(node){ | ||
if (node.instrumentLineNumber === false) return; | ||
this.buf.push('__.lineno = ' + node.line + ';'); | ||
}, | ||
/** | ||
* Visit `node`. | ||
* | ||
* @param {Node} node | ||
* @api public | ||
*/ | ||
visit: function(node){ | ||
this.line(node); | ||
return this.visitNode(node); | ||
}, | ||
/** | ||
* Visit `node`. | ||
* | ||
* @param {Node} node | ||
* @api public | ||
*/ | ||
visitNode: function(node){ | ||
return this['visit' + node.constructor.name](node); | ||
}, | ||
/** | ||
* Visit all nodes in `block`. | ||
* | ||
* @param {Block} block | ||
* @api public | ||
*/ | ||
/** | ||
* Compile parse tree to JavaScript. | ||
* | ||
* @api public | ||
*/ | ||
compile: function(){ | ||
this.buf = []; | ||
this.visit(this.node); | ||
return this.buf.join('\n'); | ||
}, | ||
/** | ||
* Buffer the given `str` optionally escaped. | ||
* | ||
* @param {String} str | ||
* @param {Boolean} esc | ||
* @api public | ||
*/ | ||
buffer: function(str, esc){ | ||
if (esc) str = utils.escape(str); | ||
this.buf.push("buf.push('" + str + "');"); | ||
}, | ||
/** | ||
* Buffer the given `node`'s lineno. | ||
* | ||
* @param {Node} node | ||
* @api public | ||
*/ | ||
line: function(node){ | ||
if (node.instrumentLineNumber === false) return; | ||
this.buf.push('__.lineno = ' + node.line + ';'); | ||
}, | ||
/** | ||
* Visit `node`. | ||
* | ||
* @param {Node} node | ||
* @api public | ||
*/ | ||
visit: function(node){ | ||
this.line(node); | ||
return this.visitNode(node); | ||
}, | ||
/** | ||
* Visit `node`. | ||
* | ||
* @param {Node} node | ||
* @api public | ||
*/ | ||
visitNode: function(node){ | ||
return this['visit' + node.constructor.name](node); | ||
}, | ||
/** | ||
* Visit all nodes in `block`. | ||
* | ||
* @param {Block} block | ||
* @api public | ||
*/ | ||
visitBlock: function(block){ | ||
for (var i = 0, len = block.length; i < len; ++i) { | ||
this.visit(block[i]); | ||
} | ||
}, | ||
/** | ||
* Visit `doctype`. Sets terse mode to `true` when html 5 | ||
* is used, causing self-closing tags to end with ">" vs "/>", | ||
* and boolean attributes are not mirrored. | ||
* | ||
* @param {Doctype} doctype | ||
* @api public | ||
*/ | ||
visitDoctype: function(doctype){ | ||
var name = doctype.val; | ||
if ('5' == name) this.terse = true; | ||
doctype = doctypes[name || 'default']; | ||
if (!doctype) throw new Error('unknown doctype "' + name + '"'); | ||
this.buffer(doctype); | ||
}, | ||
/** | ||
* Visit `tag` buffering tag markup, generating | ||
* attributes, visiting the `tag`'s code and block. | ||
* | ||
* @param {Tag} tag | ||
* @api public | ||
*/ | ||
visitTag: function(tag){ | ||
var name = tag.name; | ||
visitBlock: function(block){ | ||
for (var i = 0, len = block.length; i < len; ++i) { | ||
this.visit(block[i]); | ||
} | ||
}, | ||
/** | ||
* Visit `doctype`. Sets terse mode to `true` when html 5 | ||
* is used, causing self-closing tags to end with ">" vs "/>", | ||
* and boolean attributes are not mirrored. | ||
* | ||
* @param {Doctype} doctype | ||
* @api public | ||
*/ | ||
visitDoctype: function(doctype){ | ||
var name = doctype.val; | ||
if ('5' == name) this.terse = true; | ||
doctype = doctypes[name || 'default']; | ||
this.xml = 0 == doctype.indexOf('<?xml'); | ||
if (!doctype) throw new Error('unknown doctype "' + name + '"'); | ||
this.buffer(doctype); | ||
}, | ||
/** | ||
* Visit `tag` buffering tag markup, generating | ||
* attributes, visiting the `tag`'s code and block. | ||
* | ||
* @param {Tag} tag | ||
* @api public | ||
*/ | ||
visitTag: function(tag){ | ||
var name = tag.name; | ||
if (~selfClosing.indexOf(name)) { | ||
this.buffer('<' + name); | ||
this.visitAttributes(tag.attrs); | ||
this.terse | ||
? this.buffer('>') | ||
: this.buffer('/>'); | ||
} else { | ||
// Optimize attributes buffering | ||
if (tag.attrs.length) { | ||
this.buffer('<' + name); | ||
if (tag.attrs.length) this.visitAttributes(tag.attrs); | ||
this.buffer('>'); | ||
} else { | ||
this.buffer('<' + name + '>'); | ||
} | ||
if (tag.code) this.visitCode(tag.code); | ||
if (tag.text) this.buffer(utils.text(tag.text.join('\\n').trimLeft())); | ||
this.visit(tag.block); | ||
this.buffer('</' + name + '>'); | ||
} | ||
}, | ||
/** | ||
* Visit `filter`, throwing when the filter does not exist. | ||
* | ||
* @param {Filter} filter | ||
* @api public | ||
*/ | ||
visitFilter: function(filter){ | ||
var fn = filters[filter.name]; | ||
if (!fn) throw new Error('unknown filter ":' + filter.name + '"'); | ||
if (filter.block instanceof nodes.Block) { | ||
this.buf.push(fn(filter.block, this, filter.attrs)); | ||
} else { | ||
this.buffer(fn(utils.text(filter.block.join('\\n')), filter.attrs)); | ||
} | ||
}, | ||
/** | ||
* Visit `text` node. | ||
* | ||
* @param {Text} text | ||
* @api public | ||
*/ | ||
visitText: function(text){ | ||
this.buffer(utils.text(text.join('\\n'), true)); | ||
}, | ||
/** | ||
* Visit a `comment`, only buffering when the buffer flag is set. | ||
* | ||
* @param {Comment} comment | ||
* @api public | ||
*/ | ||
visitComment: function(comment){ | ||
if (!comment.buffer) return; | ||
this.buffer('<!--' + utils.escape(comment.val) + '-->'); | ||
}, | ||
/** | ||
* Visit `code`, respecting buffer / escape flags. | ||
* If the code is followed by a block, wrap it in | ||
* a self-calling function. | ||
* | ||
* @param {Code} code | ||
* @api public | ||
*/ | ||
visitCode: function(code){ | ||
// Wrap code blocks with {}. | ||
// we only wrap unbuffered code blocks ATM | ||
// since they are usually flow control | ||
if (~selfClosing.indexOf(name) && !this.xml) { | ||
this.buffer('<' + name); | ||
this.visitAttributes(tag.attrs); | ||
this.terse | ||
? this.buffer('>') | ||
: this.buffer('/>'); | ||
} else { | ||
// Optimize attributes buffering | ||
if (tag.attrs.length) { | ||
this.buffer('<' + name); | ||
if (tag.attrs.length) this.visitAttributes(tag.attrs); | ||
this.buffer('>'); | ||
} else { | ||
this.buffer('<' + name + '>'); | ||
} | ||
if (tag.code) this.visitCode(tag.code); | ||
if (tag.text) this.buffer(utils.text(tag.text[0].trimLeft())); | ||
this.visit(tag.block); | ||
this.buffer('</' + name + '>'); | ||
} | ||
}, | ||
/** | ||
* Visit `filter`, throwing when the filter does not exist. | ||
* | ||
* @param {Filter} filter | ||
* @api public | ||
*/ | ||
visitFilter: function(filter){ | ||
var fn = filters[filter.name]; | ||
if (!fn) throw new Error('unknown filter ":' + filter.name + '"'); | ||
if (filter.block instanceof nodes.Block) { | ||
this.buf.push(fn(filter.block, this, filter.attrs)); | ||
} else { | ||
this.buffer(fn(utils.text(filter.block.join('\\n')), filter.attrs)); | ||
} | ||
}, | ||
/** | ||
* Visit `text` node. | ||
* | ||
* @param {Text} text | ||
* @api public | ||
*/ | ||
visitText: function(text){ | ||
this.buffer(utils.text(text.join('\\n'))); | ||
this.buffer('\\n'); | ||
}, | ||
/** | ||
* Visit a `comment`, only buffering when the buffer flag is set. | ||
* | ||
* @param {Comment} comment | ||
* @api public | ||
*/ | ||
visitComment: function(comment){ | ||
if (!comment.buffer) return; | ||
this.buffer('<!--' + utils.escape(comment.val) + '-->'); | ||
}, | ||
/** | ||
* Visit a `BlockComment`. | ||
* | ||
* @param {Comment} comment | ||
* @api public | ||
*/ | ||
visitBlockComment: function(comment){ | ||
if (0 == comment.val.indexOf('if')) { | ||
this.buffer('<!--[' + comment.val + ']>'); | ||
this.visit(comment.block); | ||
this.buffer('<![endif]-->'); | ||
} else { | ||
this.buffer('<!--' + comment.val); | ||
this.visit(comment.block); | ||
this.buffer('-->'); | ||
} | ||
}, | ||
/** | ||
* Visit `code`, respecting buffer / escape flags. | ||
* If the code is followed by a block, wrap it in | ||
* a self-calling function. | ||
* | ||
* @param {Code} code | ||
* @api public | ||
*/ | ||
visitCode: function(code){ | ||
// Wrap code blocks with {}. | ||
// we only wrap unbuffered code blocks ATM | ||
// since they are usually flow control | ||
// Buffer code | ||
if (code.buffer) { | ||
var val = code.val.trimLeft(); | ||
if (code.escape) val = 'escape(' + val + ')'; | ||
this.buf.push("buf.push(" + val + ");"); | ||
} else { | ||
this.buf.push(code.val); | ||
} | ||
// Buffer code | ||
if (code.buffer) { | ||
var val = code.val.trimLeft(); | ||
val = '(' + val + ') == null ? "" : (' + val + ')'; | ||
if (code.escape) val = 'escape(' + val + ')'; | ||
this.buf.push("buf.push(" + val + ");"); | ||
} else { | ||
this.buf.push(code.val); | ||
} | ||
// Block support | ||
if (code.block) { | ||
if (!code.buffer) this.buf.push('{'); | ||
this.visit(code.block); | ||
if (!code.buffer) this.buf.push('}'); | ||
} | ||
}, | ||
/** | ||
* Visit `each` block. | ||
* | ||
* @param {Each} each | ||
* @api public | ||
*/ | ||
visitEach: function(each){ | ||
this.buf.push('' | ||
+ '// iterate ' + each.obj + '\n' | ||
+ '(function(){\n' | ||
+ ' if (\'number\' == typeof ' + each.obj + '.length) {\n' | ||
+ ' for (var ' + each.key + ' = 0, $$l = ' + each.obj + '.length; ' + each.key + ' < $$l; ' + each.key + '++) {\n' | ||
+ ' var ' + each.val + ' = ' + each.obj + '[' + each.key + '];\n'); | ||
// Block support | ||
if (code.block) { | ||
if (!code.buffer) this.buf.push('{'); | ||
this.visit(code.block); | ||
if (!code.buffer) this.buf.push('}'); | ||
} | ||
}, | ||
/** | ||
* Visit `each` block. | ||
* | ||
* @param {Each} each | ||
* @api public | ||
*/ | ||
visitEach: function(each){ | ||
this.buf.push('' | ||
+ '// iterate ' + each.obj + '\n' | ||
+ '(function(){\n' | ||
+ ' if (\'number\' == typeof ' + each.obj + '.length) {\n' | ||
+ ' for (var ' + each.key + ' = 0, $$l = ' + each.obj + '.length; ' + each.key + ' < $$l; ' + each.key + '++) {\n' | ||
+ ' var ' + each.val + ' = ' + each.obj + '[' + each.key + '];\n'); | ||
this.visit(each.block); | ||
this.visit(each.block); | ||
this.buf.push('' | ||
+ ' }\n' | ||
+ ' } else {\n' | ||
+ ' for (var ' + each.key + ' in ' + each.obj + ') {\n' | ||
+ ' var ' + each.val + ' = ' + each.obj + '[' + each.key + '];\n'); | ||
this.buf.push('' | ||
+ ' }\n' | ||
+ ' } else {\n' | ||
+ ' for (var ' + each.key + ' in ' + each.obj + ') {\n' | ||
+ ' var ' + each.val + ' = ' + each.obj + '[' + each.key + '];\n'); | ||
this.visit(each.block); | ||
this.visit(each.block); | ||
this.buf.push(' }\n }\n}).call(this);\n'); | ||
}, | ||
/** | ||
* Visit `attrs`. | ||
* | ||
* @param {Array} attrs | ||
* @api public | ||
*/ | ||
visitAttributes: function(attrs){ | ||
var buf = [], | ||
classes = []; | ||
if (this.terse) buf.push('terse: true'); | ||
attrs.forEach(function(attr){ | ||
if (attr.name == 'class') { | ||
classes.push('(' + attr.val + ')'); | ||
} else { | ||
var pair = "'" + attr.name + "':(" + attr.val + ')'; | ||
buf.push(pair); | ||
} | ||
}); | ||
if (classes.length) { | ||
classes = classes.join(" + ' ' + "); | ||
buf.push("class: " + classes); | ||
} | ||
this.buf.push("buf.push(attrs({ " + buf.join(', ') + " }));"); | ||
this.buf.push(' }\n }\n}).call(this);\n'); | ||
}, | ||
/** | ||
* Visit `attrs`. | ||
* | ||
* @param {Array} attrs | ||
* @api public | ||
*/ | ||
visitAttributes: function(attrs){ | ||
var buf = [] | ||
, classes = []; | ||
if (this.terse) buf.push('terse: true'); | ||
attrs.forEach(function(attr){ | ||
if (attr.name == 'class') { | ||
classes.push('(' + attr.val + ')'); | ||
} else { | ||
var pair = "'" + attr.name + "':(" + attr.val + ')'; | ||
buf.push(pair); | ||
} | ||
}); | ||
if (classes.length) { | ||
classes = classes.join(" + ' ' + "); | ||
buf.push("class: " + classes); | ||
} | ||
this.buf.push("buf.push(attrs({ " + buf.join(', ') + " }));"); | ||
} | ||
}; |
@@ -9,11 +9,11 @@ | ||
module.exports = { | ||
'5': '<!DOCTYPE html>', | ||
'xml': '<?xml version="1.0" encoding="utf-8" ?>', | ||
'default': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">', | ||
'transitional': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">', | ||
'strict': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">', | ||
'frameset': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">', | ||
'1.1': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">', | ||
'basic': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">', | ||
'mobile': '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">' | ||
'5': '<!DOCTYPE html>' | ||
, 'xml': '<?xml version="1.0" encoding="utf-8" ?>' | ||
, 'default': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' | ||
, 'transitional': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' | ||
, 'strict': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' | ||
, 'frameset': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">' | ||
, '1.1': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">' | ||
, 'basic': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">' | ||
, 'mobile': '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">' | ||
}; |
@@ -9,63 +9,79 @@ | ||
module.exports = { | ||
/** | ||
* Wrap text with CDATA block. | ||
*/ | ||
cdata: function(str){ | ||
return '<![CDATA[\\n' + str + '\\n]]>'; | ||
}, | ||
/** | ||
* Wrap text with script and CDATA tags. | ||
*/ | ||
javascript: function(str){ | ||
return '<script type="text/javascript">\\n' + str + '</script>'; | ||
}, | ||
/** | ||
* Transform sass to css, wrapped in style tags. | ||
*/ | ||
sass: function(str){ | ||
str = str.replace(/\\n/g, '\n'); | ||
var sass = require('sass').render(str).replace(/\n/g, '\\n'); | ||
return '<style>' + sass + '</style>'; | ||
}, | ||
/** | ||
* Transform sass to css, wrapped in style tags. | ||
*/ | ||
less: function(str){ | ||
var less; | ||
str = str.replace(/\\n/g, '\n'); | ||
require('less').render(str, function(err, css){ | ||
less = '<style>' + css.replace(/\n/g, '\\n') + '</style>'; | ||
}); | ||
return less; | ||
}, | ||
/** | ||
* Transform markdown to html. | ||
*/ | ||
markdown: function(str){ | ||
var md = require('markdown'); | ||
str = str.replace(/\\n/g, '\n'); | ||
return (md.toHTML | ||
? md.toHTML(str) | ||
: md.parse(str)).replace(/\n/g, '\\n').replace(/'/g,'''); | ||
}, | ||
/** | ||
* Wrap text with CDATA block. | ||
*/ | ||
cdata: function(str){ | ||
return '<![CDATA[\\n' + str + '\\n]]>'; | ||
}, | ||
/** | ||
* Transform sass to css, wrapped in style tags. | ||
*/ | ||
sass: function(str){ | ||
str = str.replace(/\\n/g, '\n'); | ||
var sass = require('sass').render(str).replace(/\n/g, '\\n'); | ||
return '<style>' + sass + '</style>'; | ||
}, | ||
/** | ||
* Transform stylus to css, wrapped in style tags. | ||
*/ | ||
stylus: function(str){ | ||
var ret; | ||
str = str.replace(/\\n/g, '\n'); | ||
var stylus = require('stylus'); | ||
stylus(str).render(function(err, css){ | ||
if (err) throw err; | ||
ret = css.replace(/\n/g, '\\n'); | ||
}); | ||
return '<style>' + ret + '</style>'; | ||
}, | ||
/** | ||
* Transform sass to css, wrapped in style tags. | ||
*/ | ||
less: function(str){ | ||
var ret; | ||
str = str.replace(/\\n/g, '\n'); | ||
require('less').render(str, function(err, css){ | ||
if (err) throw err; | ||
ret = '<style>' + css.replace(/\n/g, '\\n') + '</style>'; | ||
}); | ||
return ret; | ||
}, | ||
/** | ||
* Transform markdown to html. | ||
*/ | ||
markdown: function(str){ | ||
var md = require('markdown'); | ||
str = str.replace(/\\n/g, '\n'); | ||
return md.parse(str).replace(/\n/g, '\\n').replace(/'/g,'''); | ||
}, | ||
/** | ||
* Transform markdown to html. | ||
*/ | ||
discount: function(str){ | ||
var md = require('discount'); | ||
str = str.replace(/\\n/g, '\n'); | ||
return md.parse(str).replace(/\n/g, '\\n').replace(/'/g,'''); | ||
}, | ||
/** | ||
* Transform coffeescript to javascript. | ||
*/ | ||
/** | ||
* Transform coffeescript to javascript. | ||
*/ | ||
coffeescript: function(str){ | ||
str = str.replace(/\\n/g, '\n'); | ||
var js = require('coffee-script').compile(str).replace(/\n/g, '\\n'); | ||
return '<script type="text/javascript">\\n' + js + '</script>'; | ||
} | ||
coffeescript: function(str){ | ||
str = str.replace(/\\n/g, '\n'); | ||
var js = require('coffee-script').compile(str).replace(/\n/g, '\\n'); | ||
return '<script type="text/javascript">\\n' + js + '</script>'; | ||
} | ||
}; |
264
lib/jade.js
@@ -12,5 +12,5 @@ | ||
var Parser = require('./parser'), | ||
Compiler = require('./compiler'), | ||
fs = require('fs'); | ||
var Parser = require('./parser') | ||
, Compiler = require('./compiler') | ||
, fs = require('fs'); | ||
@@ -21,3 +21,3 @@ /** | ||
exports.version = '0.6.3'; | ||
exports.version = '0.7.0'; | ||
@@ -89,24 +89,24 @@ /** | ||
function attrs(obj){ | ||
var buf = [], | ||
terse = obj.terse; | ||
delete obj.terse; | ||
var keys = Object.keys(obj), | ||
len = keys.length; | ||
if (len) { | ||
buf.push(''); | ||
for (var i = 0; i < len; ++i) { | ||
var key = keys[i], | ||
val = obj[key]; | ||
if (typeof val === 'boolean' || val === '' || val == null) { | ||
if (val) { | ||
terse | ||
? buf.push(key) | ||
: buf.push(key + '="' + key + '"'); | ||
} | ||
} else { | ||
buf.push(key + '="' + escape(val) + '"'); | ||
} | ||
var buf = [] | ||
, terse = obj.terse; | ||
delete obj.terse; | ||
var keys = Object.keys(obj) | ||
, len = keys.length; | ||
if (len) { | ||
buf.push(''); | ||
for (var i = 0; i < len; ++i) { | ||
var key = keys[i] | ||
, val = obj[key]; | ||
if (typeof val === 'boolean' || val === '' || val == null) { | ||
if (val) { | ||
terse | ||
? buf.push(key) | ||
: buf.push(key + '="' + key + '"'); | ||
} | ||
} else { | ||
buf.push(key + '="' + escape(val) + '"'); | ||
} | ||
} | ||
return buf.join(' '); | ||
} | ||
return buf.join(' '); | ||
} | ||
@@ -123,7 +123,7 @@ | ||
function escape(html){ | ||
return String(html) | ||
.replace(/&(?!\w+;)/g, '&') | ||
.replace(/</g, '<') | ||
.replace(/>/g, '>') | ||
.replace(/"/g, '"'); | ||
return String(html) | ||
.replace(/&(?!\w+;)/g, '&') | ||
.replace(/</g, '<') | ||
.replace(/>/g, '>') | ||
.replace(/"/g, '"'); | ||
} | ||
@@ -143,20 +143,20 @@ | ||
function rethrow(err, str, filename, lineno){ | ||
var start = lineno - 3 > 0 | ||
? lineno - 3 | ||
: 0; | ||
var start = lineno - 3 > 0 | ||
? lineno - 3 | ||
: 0; | ||
// Error context | ||
var context = str.split('\n').slice(start, lineno).map(function(line, i){ | ||
return ' ' | ||
+ (i + start + 1) | ||
+ ". '" | ||
+ line.replace("'", "\\'") | ||
+ "'"; | ||
}).join('\n'); | ||
// Error context | ||
var context = str.split('\n').slice(start, lineno).map(function(line, i){ | ||
return ' ' | ||
+ (i + start + 1) | ||
+ ". '" | ||
+ line.replace("'", "\\'") | ||
+ "'"; | ||
}).join('\n'); | ||
// Alter exception message | ||
err.path = filename; | ||
err.message = (filename || 'Jade') + ':' + lineno | ||
+ '\n' + context + '\n\n' + err.message; | ||
throw err; | ||
// Alter exception message | ||
err.path = filename; | ||
err.message = (filename || 'Jade') + ':' + lineno | ||
+ '\n' + context + '\n\n' + err.message; | ||
throw err; | ||
} | ||
@@ -174,31 +174,31 @@ | ||
function parse(str, options){ | ||
var filename = options.filename; | ||
try { | ||
// Parse | ||
var parser = new Parser(str, filename); | ||
if (options.debug) parser.debug(); | ||
var filename = options.filename; | ||
try { | ||
// Parse | ||
var parser = new Parser(str, filename); | ||
if (options.debug) parser.debug(); | ||
// Compile | ||
var compiler = new (options.compiler || Compiler)(parser.parse(), options), | ||
js = compiler.compile(); | ||
// Compile | ||
var compiler = new (options.compiler || Compiler)(parser.parse(), options) | ||
, js = compiler.compile(); | ||
// Debug compiler | ||
if (options.debug) { | ||
console.log('\n\x1b[1mCompiled Function\x1b[0m:\n\n%s', js.replace(/^/gm, ' ')); | ||
} | ||
// Debug compiler | ||
if (options.debug) { | ||
console.log('\n\x1b[1mCompiled Function\x1b[0m:\n\n%s', js.replace(/^/gm, ' ')); | ||
} | ||
try { | ||
return '' | ||
+ attrs.toString() + '\n\n' | ||
+ escape.toString() + '\n\n' | ||
+ 'var buf = [];\n' | ||
+ 'with (locals || {}) {' + js + '}' | ||
+ 'return buf.join("");'; | ||
} catch (err) { | ||
process.compile(js, filename || 'Jade'); | ||
return; | ||
} | ||
try { | ||
return '' | ||
+ attrs.toString() + '\n\n' | ||
+ escape.toString() + '\n\n' | ||
+ 'var buf = [];\n' | ||
+ 'with (locals || {}) {' + js + '}' | ||
+ 'return buf.join("");'; | ||
} catch (err) { | ||
rethrow(err, str, filename, parser.lexer.lineno); | ||
process.compile(js, filename || 'Jade'); | ||
return; | ||
} | ||
} catch (err) { | ||
rethrow(err, str, filename, parser.lexer.lineno); | ||
} | ||
} | ||
@@ -216,20 +216,20 @@ | ||
exports.compile = function(str, options){ | ||
var options = options || {}, | ||
input = JSON.stringify(str), | ||
filename = options.filename | ||
? JSON.stringify(options.filename) | ||
: 'undefined'; | ||
var options = options || {} | ||
, input = JSON.stringify(str) | ||
, filename = options.filename | ||
? JSON.stringify(options.filename) | ||
: 'undefined'; | ||
// Reduce closure madness by injecting some locals | ||
var fn = [ | ||
'var __ = { lineno: 1, input: ' + input + ', filename: ' + filename + ' };', | ||
rethrow.toString(), | ||
'try {', | ||
parse(String(str), options || {}), | ||
'} catch (err) {', | ||
' rethrow(err, __.input, __.filename, __.lineno);', | ||
'}' | ||
].join('\n'); | ||
// Reduce closure madness by injecting some locals | ||
var fn = [ | ||
'var __ = { lineno: 1, input: ' + input + ', filename: ' + filename + ' };' | ||
, rethrow.toString() | ||
, 'try {' | ||
, parse(String(str), options || {}) | ||
, '} catch (err) {' | ||
, ' rethrow(err, __.input, __.filename, __.lineno);' | ||
, '}' | ||
].join('\n'); | ||
return new Function('locals', fn); | ||
return new Function('locals', fn); | ||
}; | ||
@@ -255,33 +255,33 @@ | ||
exports.render = function(str, options){ | ||
var fn, | ||
options = options || {}, | ||
filename = options.filename; | ||
var fn | ||
, options = options || {} | ||
, filename = options.filename; | ||
// Accept Buffers | ||
str = String(str); | ||
// Accept Buffers | ||
str = String(str); | ||
// Cache support | ||
if (options.cache) { | ||
if (filename) { | ||
if (cache[filename]) { | ||
fn = cache[filename]; | ||
} else { | ||
fn = cache[filename] = new Function('locals', parse(str, options)); | ||
} | ||
} else { | ||
throw new Error('filename is required when using the cache option'); | ||
} | ||
// Cache support | ||
if (options.cache) { | ||
if (filename) { | ||
if (cache[filename]) { | ||
fn = cache[filename]; | ||
} else { | ||
fn = cache[filename] = new Function('locals', parse(str, options)); | ||
} | ||
} else { | ||
fn = new Function('locals', parse(str, options)); | ||
throw new Error('filename is required when using the cache option'); | ||
} | ||
} else { | ||
fn = new Function('locals', parse(str, options)); | ||
} | ||
// Render the template | ||
try { | ||
var locals = options.locals || {} | ||
, meta = { lineno: 1 }; | ||
locals.__ = meta; | ||
return fn.call(options.scope, locals); | ||
} catch (err) { | ||
rethrow(err, str, filename, meta.lineno); | ||
} | ||
// Render the template | ||
try { | ||
var locals = options.locals || {} | ||
, meta = { lineno: 1 }; | ||
locals.__ = meta; | ||
return fn.call(options.scope, locals); | ||
} catch (err) { | ||
rethrow(err, str, filename, meta.lineno); | ||
} | ||
}; | ||
@@ -299,25 +299,25 @@ | ||
exports.renderFile = function(path, options, fn){ | ||
if (typeof options === 'function') { | ||
fn = options; | ||
options = {}; | ||
} | ||
options.filename = path; | ||
if (typeof options === 'function') { | ||
fn = options; | ||
options = {}; | ||
} | ||
options.filename = path; | ||
// Primed cache | ||
if (options.cache && cache[path]) { | ||
try { | ||
fn(null, exports.render('', options)); | ||
} catch (err) { | ||
fn(err); | ||
} | ||
} else { | ||
fs.readFile(path, 'utf8', function(err, str){ | ||
if (err) return fn(err); | ||
try { | ||
fn(null, exports.render(str, options)); | ||
} catch (err) { | ||
fn(err); | ||
} | ||
}); | ||
// Primed cache | ||
if (options.cache && cache[path]) { | ||
try { | ||
fn(null, exports.render('', options)); | ||
} catch (err) { | ||
fn(err); | ||
} | ||
} else { | ||
fs.readFile(path, 'utf8', function(err, str){ | ||
if (err) return fn(err); | ||
try { | ||
fn(null, exports.render(str, options)); | ||
} catch (err) { | ||
fn(err); | ||
} | ||
}); | ||
} | ||
}; |
749
lib/lexer.js
@@ -16,7 +16,10 @@ | ||
var Lexer = module.exports = function Lexer(str) { | ||
this.input = str.replace(/\r\n|\r/g, '\n').replace(/\t/g, ' '); | ||
this.deferredTokens = []; | ||
this.lastIndents = 0; | ||
this.lineno = 1; | ||
this.stash = []; | ||
this.input = str.replace(/\r\n|\r/g, '\n'); | ||
this.deferredTokens = []; | ||
this.lastIndents = 0; | ||
this.lineno = 1; | ||
this.stash = []; | ||
this.indentStack = []; | ||
this.indentRe = null; | ||
this.textPipe = true; | ||
}; | ||
@@ -29,333 +32,419 @@ | ||
Lexer.prototype = { | ||
/** | ||
* Construct a token with the given `type` and `val`. | ||
* | ||
* @param {String} type | ||
* @param {String} val | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
tok: function(type, val){ | ||
return { | ||
type: type, | ||
line: this.lineno, | ||
val: val | ||
/** | ||
* Construct a token with the given `type` and `val`. | ||
* | ||
* @param {String} type | ||
* @param {String} val | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
tok: function(type, val){ | ||
return { | ||
type: type | ||
, line: this.lineno | ||
, val: val | ||
} | ||
}, | ||
/** | ||
* Consume the given `len` of input. | ||
* | ||
* @param {Number} len | ||
* @api private | ||
*/ | ||
consume: function(len){ | ||
this.input = this.input.substr(len); | ||
}, | ||
/** | ||
* Scan for `type` with the given `regexp`. | ||
* | ||
* @param {String} type | ||
* @param {RegExp} regexp | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
scan: function(regexp, type){ | ||
var captures; | ||
if (captures = regexp.exec(this.input)) { | ||
this.consume(captures[0].length); | ||
return this.tok(type, captures[1]); | ||
} | ||
}, | ||
/** | ||
* Defer the given `tok`. | ||
* | ||
* @param {Object} tok | ||
* @api private | ||
*/ | ||
defer: function(tok){ | ||
this.deferredTokens.push(tok); | ||
}, | ||
/** | ||
* Lookahead `n` tokens. | ||
* | ||
* @param {Number} n | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
lookahead: function(n){ | ||
var fetch = n - this.stash.length; | ||
while (fetch-- > 0) this.stash.push(this.next); | ||
return this.stash[--n]; | ||
}, | ||
/** | ||
* Return the indexOf `start` / `end` delimiters. | ||
* | ||
* @param {String} start | ||
* @param {String} end | ||
* @return {Number} | ||
* @api private | ||
*/ | ||
indexOfDelimiters: function(start, end){ | ||
var str = this.input | ||
, nstart = 0 | ||
, nend = 0 | ||
, pos = 0; | ||
for (var i = 0, len = str.length; i < len; ++i) { | ||
if (start == str[i]) { | ||
++nstart; | ||
} else if (end == str[i]) { | ||
if (++nend == nstart) { | ||
pos = i; | ||
break; | ||
} | ||
}, | ||
/** | ||
* Consume the given `len` of input. | ||
* | ||
* @param {Number} len | ||
* @api private | ||
*/ | ||
consume: function(len){ | ||
this.input = this.input.substr(len); | ||
}, | ||
/** | ||
* Scan for `type` with the given `regexp`. | ||
* | ||
* @param {String} type | ||
* @param {RegExp} regexp | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
scan: function(regexp, type){ | ||
var captures; | ||
if (captures = regexp.exec(this.input)) { | ||
this.consume(captures[0].length); | ||
return this.tok(type, captures[1]); | ||
} | ||
}, | ||
/** | ||
* Defer the given `tok`. | ||
* | ||
* @param {Object} tok | ||
* @api private | ||
*/ | ||
defer: function(tok){ | ||
this.deferredTokens.push(tok); | ||
}, | ||
/** | ||
* Lookahead `n` tokens. | ||
* | ||
* @param {Number} n | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
lookahead: function(n){ | ||
var fetch = n - this.stash.length; | ||
while (fetch-- > 0) this.stash.push(this.next); | ||
return this.stash[--n]; | ||
}, | ||
/** | ||
* Return the indexOf `start` / `end` delimiters. | ||
* | ||
* @param {String} start | ||
* @param {String} end | ||
* @return {Number} | ||
* @api private | ||
*/ | ||
indexOfDelimiters: function(start, end){ | ||
var str = this.input, | ||
nstart = 0, | ||
nend = 0, | ||
pos = 0; | ||
for (var i = 0, len = str.length; i < len; ++i) { | ||
if (start == str[i]) { | ||
++nstart; | ||
} else if (end == str[i]) { | ||
if (++nend == nstart) { | ||
pos = i; | ||
break; | ||
} | ||
} | ||
} | ||
return pos; | ||
}, | ||
/** | ||
* Stashed token. | ||
*/ | ||
get stashed() { | ||
return this.stash.length | ||
&& this.stash.shift(); | ||
}, | ||
/** | ||
* Deferred token. | ||
*/ | ||
get deferred() { | ||
return this.deferredTokens.length | ||
&& this.deferredTokens.shift(); | ||
}, | ||
/** | ||
* end-of-source. | ||
*/ | ||
get eos() { | ||
if (this.input.length) return; | ||
return this.lastIndents-- > 0 | ||
? this.tok('outdent') | ||
: this.tok('eos'); | ||
}, | ||
/** | ||
* Comment. | ||
*/ | ||
get comment() { | ||
var captures; | ||
if (captures = /^ *\/\/(-)?([^\n]+)/.exec(this.input)) { | ||
this.consume(captures[0].length); | ||
var tok = this.tok('comment', captures[2]); | ||
tok.buffer = captures[1] !== '-'; | ||
return tok; | ||
} | ||
}, | ||
/** | ||
* Tag. | ||
*/ | ||
get tag() { | ||
return this.scan(/^(\w[-:\w]*)/, 'tag'); | ||
}, | ||
/** | ||
* Filter. | ||
*/ | ||
get filter() { | ||
return this.scan(/^:(\w+)/, 'filter'); | ||
}, | ||
/** | ||
* Doctype. | ||
*/ | ||
get doctype() { | ||
return this.scan(/^!!! *(\w+)?/, 'doctype'); | ||
}, | ||
/** | ||
* Id. | ||
*/ | ||
get id() { | ||
return this.scan(/^#([\w-]+)/, 'id'); | ||
}, | ||
/** | ||
* Class. | ||
*/ | ||
get className() { | ||
return this.scan(/^\.([\w-]+)/, 'class'); | ||
}, | ||
/** | ||
* Text. | ||
*/ | ||
get text() { | ||
return this.scan(/^(?:\| ?)?([^\n]+)/, 'text'); | ||
}, | ||
} | ||
} | ||
return pos; | ||
}, | ||
/** | ||
* Stashed token. | ||
*/ | ||
get stashed() { | ||
return this.stash.length | ||
&& this.stash.shift(); | ||
}, | ||
/** | ||
* Deferred token. | ||
*/ | ||
get deferred() { | ||
return this.deferredTokens.length | ||
&& this.deferredTokens.shift(); | ||
}, | ||
/** | ||
* end-of-source. | ||
*/ | ||
get eos() { | ||
if (this.input.length) return; | ||
if (this.indentStack.length) { | ||
this.indentStack.shift(); | ||
return this.tok('outdent'); | ||
} else { | ||
return this.tok('eos'); | ||
} | ||
}, | ||
/** | ||
* Each. | ||
*/ | ||
get each() { | ||
var captures; | ||
if (captures = /^- *each *(\w+)(?: *, *(\w+))? * in *([^\n]+)/.exec(this.input)) { | ||
this.consume(captures[0].length); | ||
var tok = this.tok('each', captures[1]); | ||
tok.key = captures[2] || 'index'; | ||
tok.code = captures[3]; | ||
return tok; | ||
} | ||
}, | ||
/** | ||
* Code. | ||
*/ | ||
get code() { | ||
var captures; | ||
if (captures = /^(!?=|-)([^\n]+)/.exec(this.input)) { | ||
this.consume(captures[0].length); | ||
var flags = captures[1]; | ||
captures[1] = captures[2]; | ||
var tok = this.tok('code', captures[1]); | ||
tok.escape = flags[0] === '='; | ||
tok.buffer = flags[0] === '=' || flags[1] === '='; | ||
return tok; | ||
} | ||
}, | ||
/** | ||
* Attributes. | ||
*/ | ||
get attrs() { | ||
if ('(' == this.input[0]) { | ||
var index = this.indexOfDelimiters('(', ')'), | ||
str = this.input.substr(1, index-1); | ||
this.consume(index + 1); | ||
var tok = this.tok('attrs', str), | ||
attrs = tok.val.split(/ *, *(?=['"\w-]+ *[:=]|[\w-]+ *$)/); | ||
tok.attrs = {}; | ||
for (var i = 0, len = attrs.length; i < len; ++i) { | ||
var pair = attrs[i]; | ||
// Support = and : | ||
var colon = pair.indexOf(':'), | ||
equal = pair.indexOf('='); | ||
// Boolean | ||
if (colon < 0 && equal < 0) { | ||
var key = pair, | ||
val = true; | ||
} else { | ||
// Split on first = or : | ||
var split = equal >= 0 | ||
? equal | ||
: colon; | ||
if (colon >= 0 && colon < equal) split = colon; | ||
var key = pair.substr(0, split), | ||
val = pair.substr(++split, pair.length); | ||
} | ||
tok.attrs[key.trim().replace(/^['"]|['"]$/g, '')] = val; | ||
/** | ||
* Block comment | ||
*/ | ||
get blockComment() { | ||
var captures; | ||
if (captures = /^\/([^\n]+)/.exec(this.input)) { | ||
this.consume(captures[0].length); | ||
var tok = this.tok('block-comment', captures[1]); | ||
return tok; | ||
} | ||
}, | ||
/** | ||
* Comment. | ||
*/ | ||
get comment() { | ||
var captures; | ||
if (captures = /^ *\/\/(-)?([^\n]+)/.exec(this.input)) { | ||
this.consume(captures[0].length); | ||
var tok = this.tok('comment', captures[2]); | ||
tok.buffer = '-' != captures[1]; | ||
return tok; | ||
} | ||
}, | ||
/** | ||
* Tag. | ||
*/ | ||
get tag() { | ||
return this.scan(/^(\w[-:\w]*)/, 'tag'); | ||
}, | ||
/** | ||
* Filter. | ||
*/ | ||
get filter() { | ||
return this.scan(/^:(\w+)/, 'filter'); | ||
}, | ||
/** | ||
* Doctype. | ||
*/ | ||
get doctype() { | ||
return this.scan(/^!!! *(\w+)?/, 'doctype'); | ||
}, | ||
/** | ||
* Id. | ||
*/ | ||
get id() { | ||
return this.scan(/^#([\w-]+)/, 'id'); | ||
}, | ||
/** | ||
* Class. | ||
*/ | ||
get className() { | ||
return this.scan(/^\.([\w-]+)/, 'class'); | ||
}, | ||
/** | ||
* Text. | ||
*/ | ||
get text() { | ||
return this.scan(/^(?:\| ?)?([^\n]+)/, 'text'); | ||
}, | ||
/** | ||
* Each. | ||
*/ | ||
get each() { | ||
var captures; | ||
if (captures = /^- *each *(\w+)(?: *, *(\w+))? * in *([^\n]+)/.exec(this.input)) { | ||
this.consume(captures[0].length); | ||
var tok = this.tok('each', captures[1]); | ||
tok.key = captures[2] || 'index'; | ||
tok.code = captures[3]; | ||
return tok; | ||
} | ||
}, | ||
/** | ||
* Code. | ||
*/ | ||
get code() { | ||
var captures; | ||
if (captures = /^(!?=|-)([^\n]+)/.exec(this.input)) { | ||
this.consume(captures[0].length); | ||
var flags = captures[1]; | ||
captures[1] = captures[2]; | ||
var tok = this.tok('code', captures[1]); | ||
tok.escape = flags[0] === '='; | ||
tok.buffer = flags[0] === '=' || flags[1] === '='; | ||
return tok; | ||
} | ||
}, | ||
/** | ||
* Attributes. | ||
*/ | ||
get attrs() { | ||
if ('(' == this.input[0]) { | ||
var index = this.indexOfDelimiters('(', ')') | ||
, str = this.input.substr(1, index-1) | ||
, tok = this.tok('attrs') | ||
, len = str.length | ||
, state = 'key' | ||
, key = '' | ||
, val = '' | ||
, c; | ||
this.consume(index + 1); | ||
tok.attrs = {}; | ||
function parse(c) { | ||
switch (c) { | ||
case ',': | ||
if ('string' == state) { | ||
val += c; | ||
} else { | ||
state = 'key'; | ||
val = val.trim(); | ||
tok.attrs[key.trim().replace(/^['"]|['"]$/g, '')] = '' == val | ||
? true | ||
: val; | ||
key = val = ''; | ||
} | ||
return tok; | ||
} | ||
}, | ||
/** | ||
* Indent. | ||
*/ | ||
get indent() { | ||
var captures; | ||
if (captures = /^\n( *)/.exec(this.input)) { | ||
++this.lineno; | ||
this.consume(captures[0].length); | ||
var tok = this.tok('indent', captures[1]), | ||
indents = tok.val.length / 2; | ||
if (this.input[0] === '\n') { | ||
tok.type = 'newline'; | ||
return tok; | ||
} else if (indents % 1 !== 0) { | ||
throw new Error('Invalid indentation, got ' | ||
+ tok.val.length + ' space' | ||
+ (tok.val.length > 1 ? 's' : '') | ||
+ ', must be a multiple of two.'); | ||
} else if (indents === this.lastIndents) { | ||
tok.type = 'newline'; | ||
} else if (indents > this.lastIndents + 1) { | ||
throw new Error('Invalid indentation, got ' | ||
+ indents + ' expected ' | ||
+ (this.lastIndents + 1) + '.'); | ||
} else if (indents < this.lastIndents) { | ||
var n = this.lastIndents - indents; | ||
tok.type = 'outdent'; | ||
while (--n) { | ||
this.defer(this.tok('outdent')); | ||
} | ||
break; | ||
case ':': | ||
case '=': | ||
if ('string' == state || 'val' == state) { | ||
val += c; | ||
} else { | ||
state = 'val'; | ||
} | ||
this.lastIndents = indents; | ||
return tok; | ||
break; | ||
case '"': | ||
case "'": | ||
if ('key' == state) break; | ||
state = 'string' == state | ||
? 'val' | ||
: 'string'; | ||
val += c; | ||
break; | ||
default: | ||
switch (state) { | ||
case 'key': key += c; break; | ||
case 'val': val += c; break; | ||
case 'string': val += c; break; | ||
} | ||
} | ||
}, | ||
/** | ||
* Return the next token object, or those | ||
* previously stashed by lookahead. | ||
* | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
get advance(){ | ||
return this.stashed | ||
|| this.next; | ||
}, | ||
/** | ||
* Return the next token object. | ||
* | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
get next() { | ||
return this.deferred | ||
|| this.eos | ||
|| this.tag | ||
|| this.filter | ||
|| this.each | ||
|| this.code | ||
|| this.doctype | ||
|| this.id | ||
|| this.className | ||
|| this.attrs | ||
|| this.indent | ||
|| this.comment | ||
|| this.text; | ||
} | ||
for (var i = 0; i < len; ++i) { | ||
parse(str[i]); | ||
} | ||
parse(','); | ||
return tok; | ||
} | ||
}, | ||
/** | ||
* Indent. | ||
*/ | ||
get indent() { | ||
var captures, re; | ||
// established regexp | ||
if (this.indentRe) { | ||
captures = this.indentRe.exec(this.input); | ||
// determine regexp | ||
} else { | ||
// tabs | ||
re = /^\n(\t*) */; | ||
captures = re.exec(this.input); | ||
// spaces | ||
if (captures && !captures[1].length) { | ||
re = /^\n( *)/; | ||
captures = re.exec(this.input); | ||
} | ||
// established | ||
if (captures && captures[1].length) this.indentRe = re; | ||
} | ||
if (captures) { | ||
var tok | ||
, indents = captures[1].length; | ||
++this.lineno; | ||
this.consume(indents + 1); | ||
if (' ' == this.input[0] || '\t' == this.input[0]) { | ||
throw new Error('Invalid indentation, you can use tabs or spaces but not both'); | ||
} | ||
// blank line | ||
if ('\n' == this.input[0]) return this.advance; | ||
// outdent | ||
if (this.indentStack.length && indents < this.indentStack[0]) { | ||
while (this.indentStack.length && this.indentStack[0] > indents) { | ||
this.stash.push(this.tok('outdent')); | ||
this.indentStack.shift(); | ||
} | ||
tok = this.stash.pop(); | ||
// indent | ||
} else if (indents && indents != this.indentStack[0]) { | ||
this.indentStack.unshift(indents); | ||
tok = this.tok('indent'); | ||
// newline | ||
} else { | ||
tok = this.tok('newline'); | ||
} | ||
return tok; | ||
} | ||
}, | ||
/** | ||
* Pipe-less text consumed only when | ||
* textPipe is false; | ||
*/ | ||
get pipelessText() { | ||
if (false === this.textPipe) { | ||
if ('\n' == this.input[0]) return; | ||
var i = this.input.indexOf('\n') | ||
, str = this.input.substr(0, i); | ||
if (-1 == i) return; | ||
this.consume(str.length); | ||
return this.tok('text', str); | ||
} | ||
}, | ||
/** | ||
* Return the next token object, or those | ||
* previously stashed by lookahead. | ||
* | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
get advance(){ | ||
return this.stashed | ||
|| this.next; | ||
}, | ||
/** | ||
* Return the next token object. | ||
* | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
get next() { | ||
return this.deferred | ||
|| this.eos | ||
|| this.pipelessText | ||
|| this.tag | ||
|| this.filter | ||
|| this.each | ||
|| this.code | ||
|| this.doctype | ||
|| this.id | ||
|| this.className | ||
|| this.attrs | ||
|| this.indent | ||
|| this.comment | ||
|| this.blockComment | ||
|| this.text; | ||
} | ||
}; |
@@ -22,3 +22,3 @@ | ||
var Block = module.exports = function Block(node){ | ||
if (node) this.push(node); | ||
if (node) this.push(node); | ||
}; | ||
@@ -25,0 +25,0 @@ |
@@ -25,6 +25,6 @@ | ||
var Code = module.exports = function Code(val, buffer, escape) { | ||
this.val = val; | ||
this.buffer = buffer; | ||
this.escape = escape; | ||
if (/^ *else/.test(val)) this.instrumentLineNumber = false; | ||
this.val = val; | ||
this.buffer = buffer; | ||
this.escape = escape; | ||
if (/^ *else/.test(val)) this.instrumentLineNumber = false; | ||
}; | ||
@@ -31,0 +31,0 @@ |
@@ -24,4 +24,4 @@ | ||
var Comment = module.exports = function Comment(val, buffer) { | ||
this.val = val; | ||
this.buffer = buffer; | ||
this.val = val; | ||
this.buffer = buffer; | ||
}; | ||
@@ -28,0 +28,0 @@ |
@@ -22,3 +22,3 @@ | ||
var Doctype = module.exports = function Doctype(val) { | ||
this.val = val; | ||
this.val = val; | ||
}; | ||
@@ -25,0 +25,0 @@ |
@@ -25,6 +25,6 @@ | ||
var Each = module.exports = function Each(obj, val, key, block) { | ||
this.obj = obj; | ||
this.val = val; | ||
this.key = key; | ||
this.block = block; | ||
this.obj = obj; | ||
this.val = val; | ||
this.key = key; | ||
this.block = block; | ||
}; | ||
@@ -31,0 +31,0 @@ |
@@ -24,5 +24,5 @@ | ||
var Filter = module.exports = function Filter(name, block, attrs) { | ||
this.name = name; | ||
this.block = block; | ||
this.attrs = attrs; | ||
this.name = name; | ||
this.block = block; | ||
this.attrs = attrs; | ||
}; | ||
@@ -29,0 +29,0 @@ |
@@ -16,2 +16,3 @@ | ||
exports.Comment = require('./comment'); | ||
exports.BlockComment = require('./block-comment'); | ||
exports.Doctype = require('./doctype'); |
@@ -24,5 +24,5 @@ | ||
var Tag = module.exports = function Tag(name, block) { | ||
this.name = name; | ||
this.attrs = []; | ||
this.block = block || new Block; | ||
this.name = name; | ||
this.attrs = []; | ||
this.block = block || new Block; | ||
}; | ||
@@ -42,4 +42,4 @@ | ||
Tag.prototype.setAttribute = function(name, val){ | ||
this.attrs.push({ name: name, val: val }); | ||
return this; | ||
this.attrs.push({ name: name, val: val }); | ||
return this; | ||
}; | ||
@@ -55,7 +55,7 @@ | ||
Tag.prototype.removeAttribute = function(name){ | ||
for (var i = 0, len = this.attrs.length; i < len; ++i) { | ||
if (this.attrs[i] && this.attrs[i].name == name) { | ||
delete this.attrs[i]; | ||
} | ||
for (var i = 0, len = this.attrs.length; i < len; ++i) { | ||
if (this.attrs[i] && this.attrs[i].name == name) { | ||
delete this.attrs[i]; | ||
} | ||
} | ||
}; | ||
@@ -72,7 +72,7 @@ | ||
Tag.prototype.getAttribute = function(name){ | ||
for (var i = 0, len = this.attrs.length; i < len; ++i) { | ||
if (this.attrs[i] && this.attrs[i].name == name) { | ||
return this.attrs[i].val; | ||
} | ||
for (var i = 0, len = this.attrs.length; i < len; ++i) { | ||
if (this.attrs[i] && this.attrs[i].name == name) { | ||
return this.attrs[i].val; | ||
} | ||
} | ||
}; | ||
@@ -79,0 +79,0 @@ |
@@ -22,3 +22,3 @@ | ||
var Text = module.exports = function Text(line) { | ||
if ('string' == typeof line) this.push(line); | ||
if ('string' == typeof line) this.push(line); | ||
}; | ||
@@ -25,0 +25,0 @@ |
@@ -12,4 +12,4 @@ | ||
var Lexer = require('./lexer'), | ||
nodes = require('./nodes'); | ||
var Lexer = require('./lexer') | ||
, nodes = require('./nodes'); | ||
@@ -24,9 +24,15 @@ /** | ||
var Parser = module.exports = function Parser(str, filename){ | ||
this.input = str; | ||
this.lexer = new Lexer(str); | ||
this.filename = filename; | ||
var Parser = exports = module.exports = function Parser(str, filename){ | ||
this.input = str; | ||
this.lexer = new Lexer(str); | ||
this.filename = filename; | ||
}; | ||
/** | ||
* Tags that may not contain tags. | ||
*/ | ||
var textOnly = exports.textOnly = ['script', 'textarea', 'style']; | ||
/** | ||
* Parser prototype. | ||
@@ -36,326 +42,365 @@ */ | ||
Parser.prototype = { | ||
/** | ||
* Output parse tree to stdout. | ||
* | ||
* @api public | ||
*/ | ||
debug: function(){ | ||
var lexer = new Lexer(this.input), | ||
tree = require('sys').inspect(this.parse(), false, 12, true); | ||
console.log('\n\x1b[1mParse Tree\x1b[0m:\n'); | ||
console.log(tree); | ||
this.lexer = lexer; | ||
}, | ||
/** | ||
* Return the next token object. | ||
* | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
/** | ||
* Output parse tree to stdout. | ||
* | ||
* @api public | ||
*/ | ||
debug: function(){ | ||
var lexer = new Lexer(this.input) | ||
, tree = require('sys').inspect(this.parse(), false, 12, true); | ||
console.log('\n\x1b[1mParse Tree\x1b[0m:\n'); | ||
console.log(tree); | ||
this.lexer = lexer; | ||
}, | ||
/** | ||
* Return the next token object. | ||
* | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
get advance(){ | ||
return this.lexer.advance; | ||
}, | ||
/** | ||
* Single token lookahead. | ||
* | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
get peek() { | ||
return this.lookahead(1); | ||
}, | ||
/** | ||
* Return lexer lineno. | ||
* | ||
* @return {Number} | ||
* @api private | ||
*/ | ||
get line() { | ||
return this.lexer.lineno; | ||
}, | ||
/** | ||
* `n` token lookahead. | ||
* | ||
* @param {Number} n | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
lookahead: function(n){ | ||
return this.lexer.lookahead(n); | ||
}, | ||
/** | ||
* Parse input returning a string of js for evaluation. | ||
* | ||
* @return {String} | ||
* @api public | ||
*/ | ||
parse: function(){ | ||
var block = new nodes.Block; | ||
block.line = this.line; | ||
while (this.peek.type !== 'eos') { | ||
if (this.peek.type === 'newline') { | ||
this.advance; | ||
} else { | ||
block.push(this.parseExpr()); | ||
} | ||
} | ||
return block; | ||
}, | ||
/** | ||
* Expect the given type, or throw an exception. | ||
* | ||
* @param {String} type | ||
* @api private | ||
*/ | ||
expect: function(type){ | ||
if (this.peek.type === type) { | ||
return this.advance; | ||
} else { | ||
throw new Error('expected "' + type + '", but got "' + this.peek.type + '"'); | ||
} | ||
}, | ||
/** | ||
* Accept the given `type`. | ||
* | ||
* @param {String} type | ||
* @api private | ||
*/ | ||
accept: function(type){ | ||
if (this.peek.type === type) { | ||
return this.advance; | ||
} | ||
}, | ||
/** | ||
* tag | ||
* | doctype | ||
* | filter | ||
* | comment | ||
* | text | ||
* | each | ||
* | code | ||
* | id | ||
* | class | ||
*/ | ||
parseExpr: function(){ | ||
get advance(){ | ||
return this.lexer.advance; | ||
}, | ||
/** | ||
* Single token lookahead. | ||
* | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
get peek() { | ||
return this.lookahead(1); | ||
}, | ||
/** | ||
* Return lexer lineno. | ||
* | ||
* @return {Number} | ||
* @api private | ||
*/ | ||
get line() { | ||
return this.lexer.lineno; | ||
}, | ||
/** | ||
* `n` token lookahead. | ||
* | ||
* @param {Number} n | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
lookahead: function(n){ | ||
return this.lexer.lookahead(n); | ||
}, | ||
/** | ||
* Parse input returning a string of js for evaluation. | ||
* | ||
* @return {String} | ||
* @api public | ||
*/ | ||
parse: function(){ | ||
var block = new nodes.Block; | ||
block.line = this.line; | ||
while (this.peek.type !== 'eos') { | ||
if (this.peek.type === 'newline') { | ||
this.advance; | ||
} else { | ||
block.push(this.parseExpr()); | ||
} | ||
} | ||
return block; | ||
}, | ||
/** | ||
* Expect the given type, or throw an exception. | ||
* | ||
* @param {String} type | ||
* @api private | ||
*/ | ||
expect: function(type){ | ||
if (this.peek.type === type) { | ||
return this.advance; | ||
} else { | ||
throw new Error('expected "' + type + '", but got "' + this.peek.type + '"'); | ||
} | ||
}, | ||
/** | ||
* Accept the given `type`. | ||
* | ||
* @param {String} type | ||
* @api private | ||
*/ | ||
accept: function(type){ | ||
if (this.peek.type === type) { | ||
return this.advance; | ||
} | ||
}, | ||
/** | ||
* tag | ||
* | doctype | ||
* | filter | ||
* | comment | ||
* | text | ||
* | each | ||
* | code | ||
* | id | ||
* | class | ||
*/ | ||
parseExpr: function(){ | ||
switch (this.peek.type) { | ||
case 'tag': | ||
return this.parseTag(); | ||
case 'doctype': | ||
return this.parseDoctype(); | ||
case 'filter': | ||
return this.parseFilter(); | ||
case 'comment': | ||
return this.parseComment(); | ||
case 'block-comment': | ||
return this.parseBlockComment(); | ||
case 'text': | ||
return this.parseText(); | ||
case 'each': | ||
return this.parseEach(); | ||
case 'code': | ||
return this.parseCode(); | ||
case 'id': | ||
case 'class': | ||
var tok = this.advance; | ||
this.lexer.defer(this.lexer.tok('tag', 'div')); | ||
this.lexer.defer(tok); | ||
return this.parseExpr(); | ||
default: | ||
throw new Error('unexpected token "' + this.peek.type + '"'); | ||
} | ||
}, | ||
/** | ||
* Text | ||
*/ | ||
parseText: function(){ | ||
var tok = this.expect('text') | ||
, node = new nodes.Text(tok.val); | ||
node.line = this.line; | ||
return node; | ||
}, | ||
/** | ||
* code | ||
*/ | ||
parseCode: function(){ | ||
var tok = this.expect('code') | ||
, node = new nodes.Code(tok.val, tok.buffer, tok.escape); | ||
node.line = this.line; | ||
if ('indent' == this.peek.type) { | ||
node.block = this.parseBlock(); | ||
} | ||
return node; | ||
}, | ||
/** | ||
* block comment | ||
*/ | ||
parseBlockComment: function(){ | ||
var tok = this.expect('block-comment') | ||
, node = new nodes.BlockComment(tok.val, this.parseBlock()); | ||
node.line = this.line; | ||
return node; | ||
}, | ||
/** | ||
* comment | ||
*/ | ||
parseComment: function(){ | ||
var tok = this.expect('comment') | ||
, node = new nodes.Comment(tok.val, tok.buffer); | ||
node.line = this.line; | ||
return node; | ||
}, | ||
/** | ||
* doctype | ||
*/ | ||
parseDoctype: function(){ | ||
var tok = this.expect('doctype') | ||
, node = new nodes.Doctype(tok.val); | ||
node.line = this.line; | ||
return node; | ||
}, | ||
/** | ||
* filter attrs? (text | block) | ||
*/ | ||
parseFilter: function(){ | ||
var block | ||
, tok = this.expect('filter') | ||
, attrs = this.accept('attrs'); | ||
if ('text' == tok.val) { | ||
this.lexer.textPipe = false; | ||
block = this.parseTextBlock(); | ||
this.lexer.textPipe = true; | ||
return block; | ||
} else if ('text' == this.lookahead(2).type) { | ||
block = this.parseTextBlock(); | ||
} else { | ||
block = this.parseBlock(); | ||
} | ||
var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs); | ||
node.line = this.line; | ||
return node; | ||
}, | ||
/** | ||
* each block | ||
*/ | ||
parseEach: function(){ | ||
var tok = this.expect('each') | ||
, node = new nodes.Each(tok.code, tok.val, tok.key, this.parseBlock()); | ||
node.line = this.line; | ||
return node; | ||
}, | ||
/** | ||
* indent (text | newline)* outdent | ||
*/ | ||
parseTextBlock: function(){ | ||
var text = new nodes.Text; | ||
text.line = this.line; | ||
this.expect('indent'); | ||
while ('outdent' != this.peek.type) { | ||
switch (this.peek.type) { | ||
case 'newline': | ||
this.advance; | ||
break; | ||
case 'indent': | ||
text.push(this.parseTextBlock().map(function(text){ | ||
return ' ' + text; | ||
}).join('\\n')); | ||
break; | ||
default: | ||
text.push(this.advance.val); | ||
} | ||
} | ||
this.expect('outdent'); | ||
return text; | ||
}, | ||
/** | ||
* indent expr* outdent | ||
*/ | ||
parseBlock: function(){ | ||
var block = new nodes.Block; | ||
block.line = this.line; | ||
this.expect('indent'); | ||
while ('outdent' != this.peek.type) { | ||
if ('newline' == this.peek.type) { | ||
this.advance; | ||
} else { | ||
block.push(this.parseExpr()); | ||
} | ||
} | ||
this.expect('outdent'); | ||
return block; | ||
}, | ||
/** | ||
* tag (attrs | class | id)* (text | code)? newline* block? | ||
*/ | ||
parseTag: function(){ | ||
var name = this.advance.val | ||
, tag = new nodes.Tag(name); | ||
tag.line = this.line; | ||
// (attrs | class | id)* | ||
out: | ||
while (true) { | ||
switch (this.peek.type) { | ||
case 'tag': | ||
return this.parseTag(); | ||
case 'doctype': | ||
return this.parseDoctype(); | ||
case 'filter': | ||
return this.parseFilter(); | ||
case 'comment': | ||
return this.parseComment(); | ||
case 'text': | ||
return this.parseText(); | ||
case 'each': | ||
return this.parseEach(); | ||
case 'code': | ||
return this.parseCode(); | ||
case 'id': | ||
case 'class': | ||
var tok = this.advance; | ||
this.lexer.defer(this.lexer.tok('tag', 'div')); | ||
this.lexer.defer(tok); | ||
return this.parseExpr(); | ||
} | ||
}, | ||
/** | ||
* Text | ||
*/ | ||
parseText: function(){ | ||
var tok = this.expect('text'), | ||
node = new nodes.Text(tok.val); | ||
node.line = this.line; | ||
return node; | ||
}, | ||
/** | ||
* code | ||
*/ | ||
parseCode: function(){ | ||
var tok = this.expect('code'), | ||
node = new nodes.Code(tok.val, tok.buffer, tok.escape); | ||
node.line = this.line; | ||
if ('indent' == this.peek.type) { | ||
node.block = this.parseBlock(); | ||
} | ||
return node; | ||
}, | ||
/** | ||
* comment | ||
*/ | ||
parseComment: function(){ | ||
var tok = this.expect('comment'), | ||
node = new nodes.Comment(tok.val, tok.buffer); | ||
node.line = this.line; | ||
return node; | ||
}, | ||
/** | ||
* doctype | ||
*/ | ||
parseDoctype: function(){ | ||
var tok = this.expect('doctype'), | ||
node = new nodes.Doctype(tok.val); | ||
node.line = this.line; | ||
return node; | ||
}, | ||
/** | ||
* filter attrs? (text | block) | ||
*/ | ||
parseFilter: function(){ | ||
var block, | ||
tok = this.expect('filter'), | ||
attrs = this.accept('attrs'); | ||
if (this.lookahead(2).type == 'text') { | ||
block = this.parseTextBlock(); | ||
} else { | ||
block = this.parseBlock(); | ||
} | ||
var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs); | ||
node.line = this.line; | ||
return node; | ||
}, | ||
/** | ||
* each block | ||
*/ | ||
parseEach: function(){ | ||
var tok = this.expect('each'), | ||
node = new nodes.Each(tok.code, tok.val, tok.key, this.parseBlock()); | ||
node.line = this.line; | ||
return node; | ||
}, | ||
/** | ||
* indent (text | newline)* outdent | ||
*/ | ||
parseTextBlock: function(){ | ||
var text = new nodes.Text; | ||
text.line = this.line; | ||
this.expect('indent'); | ||
while (this.peek.type === 'text' || this.peek.type === 'newline') { | ||
if (this.peek.type === 'newline') { | ||
this.advance; | ||
} else { | ||
text.push(this.advance.val); | ||
case 'id': | ||
case 'class': | ||
var tok = this.advance; | ||
tag.setAttribute(tok.type, "'" + tok.val + "'"); | ||
continue; | ||
case 'attrs': | ||
var obj = this.advance.attrs | ||
, names = Object.keys(obj); | ||
for (var i = 0, len = names.length; i < len; ++i) { | ||
var name = names[i] | ||
, val = obj[name]; | ||
tag.setAttribute(name, val); | ||
} | ||
continue; | ||
default: | ||
break out; | ||
} | ||
this.expect('outdent'); | ||
return text; | ||
}, | ||
/** | ||
* indent expr* outdent | ||
*/ | ||
parseBlock: function(){ | ||
var block = new nodes.Block; | ||
block.line = this.line; | ||
this.expect('indent'); | ||
while (this.peek.type !== 'outdent') { | ||
if (this.peek.type === 'newline') { | ||
this.advance; | ||
} else { | ||
block.push(this.parseExpr()); | ||
} | ||
} | ||
this.expect('outdent'); | ||
return block; | ||
}, | ||
/** | ||
* tag (attrs | class | id)* (text | code)? newline* block? | ||
*/ | ||
parseTag: function(){ | ||
var name = this.advance.val, | ||
tag = new nodes.Tag(name); | ||
tag.line = this.line; | ||
} | ||
// (attrs | class | id)* | ||
out: | ||
while (true) { | ||
switch (this.peek.type) { | ||
case 'id': | ||
case 'class': | ||
var tok = this.advance; | ||
tag.setAttribute(tok.type, "'" + tok.val + "'"); | ||
continue; | ||
case 'attrs': | ||
var obj = this.advance.attrs, | ||
names = Object.keys(obj); | ||
for (var i = 0, len = names.length; i < len; ++i) { | ||
var name = names[i], | ||
val = obj[name]; | ||
tag.setAttribute(name, val); | ||
} | ||
continue; | ||
default: | ||
break out; | ||
} | ||
} | ||
// (text | code)? | ||
switch (this.peek.type) { | ||
case 'text': | ||
tag.text = this.parseText(); | ||
break; | ||
case 'code': | ||
tag.code = this.parseCode(); | ||
break; | ||
} | ||
// (text | code)? | ||
switch (this.peek.type) { | ||
case 'text': | ||
tag.text = this.parseText(); | ||
break; | ||
case 'code': | ||
tag.code = this.parseCode(); | ||
break; | ||
} | ||
// newline* | ||
while (this.peek.type === 'newline') this.advance; | ||
// newline* | ||
while (this.peek.type === 'newline') this.advance; | ||
// Assume newline when tag followed by text | ||
if (this.peek.type === 'text') { | ||
if (tag.text) tag.text.push('\\n'); | ||
else tag.text = new nodes.Text('\\n'); | ||
} | ||
// Assume newline when tag followed by text | ||
if (this.peek.type === 'text') { | ||
if (tag.text) tag.text.push('\\n'); | ||
else tag.text = new nodes.Text('\\n'); | ||
} | ||
tag.textOnly = ~textOnly.indexOf(name); | ||
// block? | ||
if (this.peek.type === 'indent') { | ||
var block = this.parseBlock(); | ||
if (tag.block) { | ||
for (var i = 0, len = block.length; i < len; ++i) { | ||
tag.block.push(block[i]); | ||
} | ||
} else { | ||
tag.block = block; | ||
} | ||
// block? | ||
if ('indent' == this.peek.type) { | ||
if (tag.textOnly) { | ||
this.lexer.textPipe = false; | ||
tag.block = this.parseTextBlock(); | ||
this.lexer.textPipe = true; | ||
} else { | ||
var block = this.parseBlock(); | ||
if (tag.block) { | ||
for (var i = 0, len = block.length; i < len; ++i) { | ||
tag.block.push(block[i]); | ||
} | ||
} else { | ||
tag.block = block; | ||
} | ||
return tag; | ||
} | ||
} | ||
return tag; | ||
} | ||
}; |
@@ -17,10 +17,10 @@ | ||
var interpolate = exports.interpolate = function(str){ | ||
return str.replace(/(\\)?([#$!]){(.*?)}/g, function(str, escape, flag, code){ | ||
return escape | ||
? str | ||
: "' + " | ||
+ ('!' == flag ? '' : 'escape') | ||
+ "(" + code.replace(/\\'/g, "'") | ||
+ ") + '"; | ||
}); | ||
return str.replace(/(\\)?([#$!]){(.*?)}/g, function(str, escape, flag, code){ | ||
return escape | ||
? str | ||
: "' + " | ||
+ ('!' == flag ? '' : 'escape') | ||
+ "(" + code.replace(/\\'/g, "'") | ||
+ ") + '"; | ||
}); | ||
}; | ||
@@ -37,3 +37,3 @@ | ||
var escape = exports.escape = function(str) { | ||
return str.replace(/'/g, "\\'"); | ||
return str.replace(/'/g, "\\'"); | ||
}; | ||
@@ -49,4 +49,4 @@ | ||
exports.text = function(str, newline){ | ||
return interpolate(escape(str + (newline ? '\\n' : ''))); | ||
exports.text = function(str){ | ||
return interpolate(escape(str)); | ||
}; |
{ | ||
"name": "jade", | ||
"description": "Jade template engine", | ||
"version": "0.6.3", | ||
"version": "0.7.0", | ||
"author": "TJ Holowaychuk <tj@vision-media.ca>", | ||
@@ -6,0 +6,0 @@ "main": "./index.js", |
@@ -11,2 +11,3 @@ | ||
- great readability | ||
- flexible indentation | ||
- code is escaped by default for security | ||
@@ -21,2 +22,3 @@ - contextual error reporting at compile & run time | ||
- transparent iteration over objects, arrays, and even non-enumerables via `- each` | ||
- block comments | ||
- no tag prefix | ||
@@ -37,3 +39,2 @@ - filters | ||
- [php](http://github.com/everzet/jade.php) | ||
- [client-side js](http://github.com/miksago/jade) | ||
- [scala](http://scalate.fusesource.org/versions/snapshot/documentation/scaml-reference.html) | ||
@@ -61,2 +62,6 @@ - [ruby](http://github.com/stonean/slim) | ||
// Compile a function | ||
var fn = jade.compile('string of jade', options); | ||
fn.call(scope, locals); | ||
### Options | ||
@@ -77,7 +82,2 @@ | ||
### Indentation | ||
Jade is indentation based, however currently only supports a _2 space_ indent. | ||
Tabs are converted to 2 spaces before they hit the lexer. | ||
### Tags | ||
@@ -167,2 +167,28 @@ | ||
Tags that accept _only_ text such as `script`, `style`, and `textarea` do not | ||
need the leading `|` character, for example: | ||
html | ||
head | ||
title Example | ||
script | ||
if (foo) { | ||
bar(); | ||
} else { | ||
baz(); | ||
} | ||
Alternatively if you decide no inline tags are needed, you may use the `:text` filter: | ||
html | ||
head | ||
title Example | ||
p | ||
:text | ||
You can place any text, | ||
that you like here | ||
even if it is indented really | ||
strange | ||
. | ||
### Comments | ||
@@ -194,2 +220,36 @@ | ||
### Block Comments | ||
A block comment is the `/` character followed by a block. | ||
body | ||
/ | ||
#content | ||
h1 Example | ||
outputting | ||
<body> | ||
<!-- | ||
<div id="content"> | ||
<h1>Example</h1> | ||
</div> | ||
--> | ||
</body> | ||
Jade supports conditional-comments as well, for example: | ||
body | ||
/if IE | ||
a(href='http://www.mozilla.com/en-US/firefox/') Get Firefox | ||
outputs: | ||
<body> | ||
<!--[if IE]> | ||
<a href="http://www.mozilla.com/en-US/firefox/">Get Firefox</a> | ||
<![endif]--> | ||
</body> | ||
### Nesting | ||
@@ -369,2 +429,11 @@ | ||
When a property is undefined, Jade will output an empty string. For example: | ||
textarea= user.signature | ||
when undefined would normally output "undefined" in your html, however recent | ||
versions of Jade will simply render: | ||
<textarea></textarea> | ||
## bin/jade | ||
@@ -371,0 +440,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1693
478
64246
31