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 everything: letters, my resume, pages on this site, even forms.

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.

Haml combines a set of valuable features:

  1. A notation that mirrors HTML
  2. Blocks of text can be interpreted using custom filters
  3. Ad-hoc scripting

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

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)


Built-in Tests

Builtin self tests are an excellent means of writing filters. Haml filters are simple 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

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

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