Jade - Haml killer for nodejs
Last night I open sourced my latest project Jade, a template engine for node, focusing on readability, error handling and performance. Jade spawned from several needs, first I was tired of debugging poor JavaScript template implementations, or finding work-arounds for common issues, secondly I love haml’s syntax, however I feel it could be revamped to provide a more enjoyable experience, and lastly … I like parsers!
Examples
So what does this so called “Jade” look like? like this:
!!! 5
html(lang="en")
head
title= pageTitle
:javascript
| if (foo) {
| bar()
| }
body
h1 Jade - node template engine
#container
- if (youAreUsingJade)
p You are amazing
- else
p Get on it!
If you are at all familiar with Haml you will see tons of similarities,
however with a few important changes. First of all inverting the responsibility of whitespace we can remove the % tag prefix, leading words are now simply tags, and text blocks now contain a margin as indicated by the pipe |. I am sure people will have varying opinions on this, but I find it much easier to read, and more enjoyable to type since your main focus is building a layout, not writing text blocks.
Features
Below are some of the highlights of 0.0.1:
- high performance parser
- great readability
- code is escaped by default for security
- contextual error reporting at compile & run time
- executable for compiling jade templates via the command line
- html 5 mode (using the !!! 5 doctype)
- optional memory caching
- combine dynamic and static tag classes
- no tag prefix
- filters
- :sass
- :markdown
- :cdata
- :javascript
Implementations
As of this moment JavaScript (node specifically) is the only Jade implementation, however as with Haml this could easily be ported to other host languages, and I encourage it greatly! Even if you are not a JavaScript guy feel free to try it out, and implement it in your language of choice :)
More Information
Head over to the Github repo, or visit the Jade site for additional examples, installation guides and more.
Express vs Sinatra Benchmarks
So I have been setting up benchmark scripts for Express today, and so far some of the results have been quite interesting! The numbers shown should be taken lightly, however they consistently show that Express is quite fast.
If you are interested in benchmarking your own web applications you might want to read my last article ApacheBench Gnuplot Graphing Benchmarks.
All of the following benchmarks were generated using ApacheBench with a concurrency of 50 and performs 2000 requests. Keep in mind that Thin is used to serve Sinatra requests.
Express vs Sinatra
For those who dont know Express is a NodeJS framework inspired by Ruby’s Sinatra. Below we have the benchmark results for a typical “Hello World” response, which include nodejs benchmarks without the overhead of features provided by Express:


Haml.js vs Ruby Haml
Next up is my JavaScript Haml implementation. Below are the results of running Express & haml.js vs Sinatra & Haml. Both serve a layout template, as well as a page specific template.


Sass.js vs Ruby Sass
Finally we have JavaScript Sass vs the regular Ruby implementation packaged with haml. Both implementations serve the same 80 line stylesheet.


Stay tuned for more benchmarks!
Express 0.9.0 released
A few minutes ago Express was released, packed with tons of bug fixes and features.
Installation
As always Express can be installed via the Kiwi package manager for NodeJS using one simple command:
$ kiwi install express
Alternatively you can Download a tarball, or install via Git:
$ git clone git://github.com/visionmedia/express.git
$ cd express && git submodule update --init
New Haml Support
I wrote another JavaScript implementation of Haml simply because haml-js was a little clunky and did not comply with the original Haml implementation. My implementation is about 4 times faster (not that it really matters once caching is involved), and more compliant.
“max upload size” setting
We added a simple “max upload size” setting which checks multipart post body Content-Length and raises an error when the specified size is exceeded.
set('max upload size', (5).megabytes)
Request#render() callbacks
The Request#render() method now accepts a trailing callback function, which passes an exception (if a problem occurs), and the final view rendered.
self.render('user.html.haml', function(err, contents){
// perform caching etc
})
Map Route Parameters With param()
The param() DSL-level function allows mapping of parameter names to preprocessor functions. In the example shown below we want to prevent users from typing an invalid path such as /user/rawr, as we need a user id. Our function passed to param() accepts the path segment, which when returning false will be disregarded as a valid route, and the router will continue searching for a matching route. Otherwise if parseInt() succeeds the id variable passed to our route is now a Number and not a String.
param('id', function(val){
val = parseInt(val)
return isNaN(val) ? false : val
})
get('/user/:id', function(id){
return 'User ' + id
})
In the near future I plan on having parameter preprocessing async-friendly, so that for example :user_id could be used to load an User record from the database.
Unified Exception Handling With error()
The error() DSL-level function is passed the exception thrown. Keep in mind
that with async environments like node we loose context if an exception bubbles,
so we pass them to Request#error() to allow for the unified error handler shown below.
error(function(err){
this.contentType('html')
this.halt(200, '<p><strong>Error:</strong> ' + err.message + '</p>')
})
Handling Missing Pages With notFound()
The request level method Request#notFound() is now used in place where you
would normally call halt(404), and supports deferring to the DSL-level function notFound() as shown below:
notFound(function(){
this.halt(404, 'your lame')
})
Changelog
- Added DSL level error() route support
- Added DSL level notFound() route support
- Added Request#error()
- Added Request#notFound()
- Added Request#render() callback function. Closes #258
- Added “max upload size” setting
- Added “magic” variables to collection partials (__index__, __length__, __isFirst__, __isLast__). Closes #254
- Added haml.js submodule; removed haml-js
- Added callback function support to Request#halt() as 3rd/4th arg
- Added preprocessing of route param wildcards using param(). Closes #251
- Added view partial support (with collections etc)
- Fixed bug preventing falsey params (such as ?page=0). Closes #286
- Fixed setting of multiple cookies. Closes #199
- Changed; view naming convention is now NAME.TYPE.ENGINE (for example page.html.haml)
- Changed; session cookie is now httpOnly
- Changed; Request is no longer global
- Changed; Event is no longer global
- Changed; “sys” module is no longer global
- Changed; moved Request#download to Static plugin where it belongs
- Changed; Request instance created before body parsing. Closes #262
- Changed; Pre-caching views in memory when “cache view contents” is enabled. Closes #253
- Changed; Pre-caching view partials in memory when “cache view partials” is enabled
- Updated support to node —version 0.1.90
- Updated dependencies
- Removed set(“session cookie”) in favour of use(Session, { cookie: { … }})
- Removed utils.mixin(); use Object#mergeDeep()