Eric Radman : a Journal

Programming Documents with Haml

Haml is an templating language for writing HTML. Over the past decade the capabilities of HTML+CSS have become good enough for nearly any kind of document to be rendred in a Web browser.

Haml combines a set of valuable features:

  1. A notation that mirrors HTML
  2. Inline scripting and control flow
  3. Blocks of text can be interpreted using custom filters

Haml 6 introduced many breaking changes in the library and command line. To work around this set the gem install to version 5.2.2.

Mirroring HTML

Notwithstanding the name, Haml is not very abstract. It is mostly a 1:1 representation of the HTML it generates

!!!
%html
  %head
    %title A Letter
    %link{:href=>"main.css", :rel=>"stylesheet", :type=>"text/css"}/
  %body
    = Haml::Engine.new('home.haml').render
    %hr/
    %p.timestamp
      - require 'date'
      = "Last updated on " + Time.now().strftime("%B %d, %Y")

Setting a class and id properties have their own syntax

/ class="first"
%p.first

/ id="content"
%p#content

Ruby code can be run directly, and Haml::Engine can be used directly to process another template.

Building

In Haml, Ruby itself is also not abstracted, which gives us the ability to invent new mechanisms. One simple scheme I have used is to read extra arguments on the command line

= Haml::Engine.new(File.read(ARGV[-1])).render

This means Makefile can easily communicate a source file to read

.SUFFIXES: .haml .html

SOURCES != ls posts/*.haml | awk 'sub("\.haml$$", ".html")'

haml.html:
    haml template.haml $@ $<

Filters

Perhaps the most important feature of template language is the ability to process and transform blocks of text

# link.rb
module Haml::Filters::Link
  include Haml::Filters::Base

  def render(text)
    %q|<a href="#{text}">#{text}</a>|
  end
end

This function processes a block of text as the input

%p
  Visit my home page at
  :link
    http://eradman.com/

And to compile add an include path and a reference to the library

haml -I . -r link my.haml

Other Haml functions may also be used by initializing haml_helpers

module Haml::Filters::Cite
  include Haml::Filters::Base
  include Haml::Helpers

  init_haml_helpers
end

Utility Functions

Modules may include any kind of functionality: here is a function that find the next date that lands on a Saturday

# utils.rb
require 'date'

def next_weekend(offset)
  today = Date.today
  today = today.next_day(offset)
  while ! today.saturday?
    today = today.next_day
  end
  today.strftime("%B %d, %Y")
end

Relative imports may be used to make functions available

- require_relative 'utils'

%p
  = next_weekend(1)

Building a DOM can be accomplished using html_tag()

def cite(author, link, title)
  capture_haml do
    haml_tag :cite do
      haml_tag :span, author + ','
      haml_tag :a, :href=>link do
        haml_tag :em, title
      end
    end
  end
end

Whitespace

Nearly every other templating system seems to have difficulty controlling whitespace, and Haml is no exception.

The normal way to add a comma or a period after another tag is to use the and succeed method. (See also surround). In this example I want a period immediately following the hyperlink

Send comments to
= succeed '.' do
  %a{:href=>"mailto:ericshane@eradman.com"} ericshane@eradman.com

Another technique for controlling whitespace is to append > to trim whitespace and then add non-breaking spaces where needed

%pre
  %span.Prompt> $&nbsp;
  ls *.css *.html | entr reload-browser Firefox

Haml allows HTML to be mixed in directly. This is a good option whenever raw HTML provides a readable or correct solution to formatting.

Built-in Tests

Haml filters are Ruby modules, so we can build simple self-tests without having to instantiate an object

require 'haml'

module Haml::Filters::Link
    def render(text)
        # ...
    end
end

if __FILE__ == $0
    include Haml::Filters::Link

    out = render("http://eradman.com")
    expected = %q|<a href="http://eradman.com">http://eradman.com</a>|

    if expected == out
        puts "PASS"
    else
        puts <<~MESSAGE
          FAIL
          expected: #{expected}
          got     : #{out}
        MESSAGE
    end
end