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:

express vs sinatraexpress sinatra requests per second

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.

javascript haml vs ruby hamlrequests per second

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.

sass js vs ruby sasssass benchmarks

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()