haml-server

haml-server is a dead-simple Sinatra app for serving up a directory of files. There are a few other projects much more ambitious than this (such as serve), but haml-server’s goal is simple: serve haml files. Why? Because writing HTML is a pain — especially when you’re just putting together a prototype, or, say, testing out some javascript behavior.

Installation

This is the easy part! Run this from your default ruby (or default RVM ruby) so you have access to haml-server from wherever you are.

gem install haml-server

You can also grab the code from github.

Usage

Think of haml-server as python -m SimpleHTTPServer… but for HAML!

haml-server # serves the current directory at localhost:4567
haml-server . # ^ same as above
haml-server -p 8000 sub/dir

Visiting http://localhost:4567/ will render index.haml. Visiting /path/to/resource will render path/to/resource.haml. Simple.

Layouts

You can also create a file called layout.haml to apply a layout to each of your pages:

# layout.haml
%html
  %body
    = yield

SASS

If you want to use SASS templates, you can either define them inline using HAML’s filters, like so:

# index.haml
%html
  %head
    :sass
      .content
        width: 10px

Or you can write them to a standalone file, and then link to the same file but with “.css” instead of “.sass”. So if you had /stylesheets/screen.sass, then you might do this in your layout:

# layout.haml
%html
  %head
    :css
      @import url(/stylesheets/screen.css);

Helpers

You can create a file called helpers.rb to define any helpers you might want to make available.

# helpers.rb
require "pygmentize"

def highlight(code, format)
  Pygmentize.process(code, format)
end

alias hi highlight

And then in your views, you can access the helpers:

# index.haml

%h3 Example code
~ hi "puts '1 + 2 # => 3'", :ruby

The helpers are reloaded before each request, so you can add and remove them without restarting the server.

Running on Heroku (cedar stack)

With Heroku’s cedar stack, it’s now super easy to deploy your haml-server site for anyone to see. Here’s a step-by-step walkthrough for deploying to Heroku, with some recommendations for how to structe the site.

Structure

Since we’ll be adding some additional files to get things wired up, I recommend you move your views and static files into a separate folder like site (the walkthrough will assume that you’ve done so). So your site might look like this:

> tree
.
├── helpers.rb
└── site
    ├── index.haml
    ├── layout.haml
    └── screen.css

When working locally, then, you’ll want to run the site with: haml-server site.

Now to get things set up for Heroku, we’ll need to add a few files:

# Gemfile
source :rubygems
gem "haml-server"

# Procfile
web: bundle exec haml-server -p $PORT site

Now the project should look like this:

> tree
.
├── Gemfile
├── Procfile
├── helpers.rb
└── site
    ├── index.haml
    ├── layout.haml
    └── screen.css

Deploy

First we’ll need to get our environment in order by running:

> gem install bundler
> bundle

This will install any dependencies, and store those in a file called Gemfile.lock.

Now we should set up the site as a git repository, and commit our files.

> git init
> git add .
> git commit -m "Initial commit"

We’re almost done! Now we’ll create a new application and deploy it:

> gem install heroku
> heroku apps:create --stack cedar
> git push heroku master
> heroku open

heroku open will open up your site in the browser. Done!

The code

HAML, check!

require "haml"

SASS, check!

require "sass"

Okay. In any other context, this would be pretty evil. Let me explain. Sinatra is a well-behaving citizen, and calls ARGV.dup before parsing its options. Unfortunately for us, that means if we want to easily add additional arguments, we either have to 1) re-parse ARGV (which would mean copying the options from sinatra/main.rb) or 2) just redefine ARGV.dup. Should be clear which one I prefer.

def ARGV.dup
  self
end

Yep. This is a hack, too. Sinatra only automatically runs itself when the file is directly called — but that’s not the case when running through rubygem’s wrapped command. Solution? Just pretend we were executed directly.

$0 = __FILE__

And now we can load sinatra, knowing that the options will be parsed out of ARGV and the server will start automatically.

require "sinatra"

I love Pathname. This line works just like you’d expect it to:

Pathname("/data/foo") + "." # => /data/foo
Pathname("/data/foo") + "../" # => /data
Pathname("/data/foo") + "/data" # => /data
require "pathname"

template_dir = Pathname(Dir.pwd) + (ARGV.shift || ".")

set :public_folder, template_dir
set :views, template_dir

If a favicon wasn’t already matched by the public handler, just return a 404.

get "/favicon.ico" do
  404
end

This will be a container module for user-defined helpers. Helpers.reload! will clean up any existing methods in the module, and then load helpers defined in a helpers.rb file if it exists.

module Helpers
  class << self
    def reload!

First we remove all instance methods in the helper module.

      instance_methods.each do |method|
        remove_method(method)
      end

And now we load any helper methods into the module from the file helpers.rb if it exists.

      module_eval(File.read("helpers.rb")) if File.file?("helpers.rb")
    end
  end
end

Include our module into the Sinatra application.

helpers Helpers

This is our basic handler for all requests that come through to Sinatra. We’ll extract the necessary path information, and then render either a haml or sass file.

handler = lambda do

Reload our helpers for each request.

  Helpers.reload!

  path = request.path_info

  extension = File.extname(path)

This is the most straight-forward way to remove the extension from a while while preserving it’s original path information.

  path = path.chomp(extension)

If the path is empty, it’s a request for the index page.

  path = :index if path == "/"

We’re rendering templates, so we need to convert to a symbol for Sinatra — otherwise it will think we’re rending a string.

  path = path.to_sym

  case extension
  when ".css"

If a request for, say, ‘/stylesheets/screen.css’ wasn’t matched by the public handler, then we try to render a sass template with the same name.

    sass path
  else

Otherwise, we fall back to always rendering haml.

    haml path
  end
end

Now we bind our handler to all of the HTTP verbs. Matching against // is shorthand for matching all request paths: since // always returns true when matched against a string, our handler will run on every request. We could also write get %r{/(.*)}, &handler.. but that just looks ugly!

get    //, &handler
put    //, &handler
post   //, &handler
delete //, &handler

And that’s it! We let Sinatra handle the rest: it will parse out any provided options (see haml-server -h) and then run the server for us.