Haml 2.0

Posted May 24, 2008

Haml 2.0 has been released. We’re calling it Friendly Fred because it aims to please. By the time you read this, it’ll probably have hit all the gem mirrors, so you can get it immediately.

So what’s new?

Friendliness

We named this release “Friendly Fred” because it’s much more friendly to users than previous versions. All sorts of Haml 1.8 had all sorts of bugs with handling and displaying errors; error messages weren’t useful, line numbers would be reported incorrectly, and sometimes the error message wouldn’t display at all.

That’s all been fixed. No more white screen of death. I’ve personally tested error handling, both compile-time and runtime, in Rails versions back to 1.2.6. I’ve added line-number tests for every error I could think up.

Sass errors didn’t have nearly as many problems as Haml errors, but there were a few line-number-reporting bugs that are now fixed. In addition, it was always kind of annoying to have to check the generated CSS file for the actual error report. Now when there’s a Sass error, in addition to reporting the error in the CSS file, Sass will add the information to the content property of body:before, so it’ll show up in the actual HTML page.

Error-handling wasn’t the only target of the bugfixes that went into 2.0. Among sundry boring fixes, we finally fixed the long-standing parser bug that confused attribute hashes with brackets within text content. This means that Haml 2.0 is actually the first version released with no known bugs.

Finally, we’re attempting to eliminate whitespace-preservation headaches as much as possible by having literal pre and textarea tags – and Rails’ textarea-generating helpers - properly deal with their contents. You’ll still need to use ~ occasionally, but no longer will you be subjected to the tyranny of

~ "<pre>#{h content}</pre>"

Now you can just do

%pre= h content

or even

%pre&= content

(see below).

Speed

The speed increase for 2.0 is nowhere near as impressive as 1.8, but it’s still interesting. According to my benchmarks1, Haml is now slightly faster than ERB, even when rendering via ActionView.

In addition, we’ve added an :ugly option, thanks to Wincent Colaiuta. This option forgoes pretty output formatting in favor of speed increases, which show up in particular when rendering deeply nested partials.

Dynamic Filters

Now, time for some real features! One of the most useful additions to 2.0 is the ability of filters to access the runtime template context. For most filters, this means you can do variable interpolation, like so:

:textile
  You should all go to *#{h @place.name}*!

For the filters that have Ruby code in them already, though, such as :ruby and :erb, this code is just run in the context of the template.

HTML Escaping

Haml 2.0 has very robust support for HTML escaping, thanks to Andre Arko. This includes both an option for escaping all dynamic input by default and syntactic support for escaping input even when it’s not the default.

To turn on HTML-escaping by default, set the :escape_html option to true in whatever way your framework provides. Then =, ==, and so forth will automatically escape any input they’re given. In order to put in unescaped input, you use != or !== instead.

If you don’t have :escape_html set, then = and friends will work just like they always did. But a new set of commands will be available: &=, &==, and so on. These work like = and friends except they do escape their inputs.

To set options in Rails, use the Haml::Template.options hash in config/environment.rb (after Rails::Initializer.run):

Haml::Template.options[:escape_html] = true

To set them in Merb, use the Merb::Config[:haml] hash in init.rb instead:

Merb::Config[:haml][:escape_html] = true

Whitespace Munching

Getting rid of whitespace has traditionally been tricky with Haml. But thanks to much brainstorming by Evgeny Zislis and Sunny Ripert on my blog post about the subject; Nathan Sutton and Dustin Sallings on #haml on freenode; and various other people on the mailing list, we came up with a very nice syntax for it.

You can think of the syntax as alligators figuratively munching away at whitespace. To get rid of all whitespace outside of a tag, you add > to the end, to munch away all the outer whitespace. To get rid of whitespace within, you add < instead. Thus

%img
%img>
%img
%a{:href => "http://nex-3.com"}<
  %p Foo!

compiles to

<img /><img /><img />
<a href='http://nex-3.com'><p>Foo!</p></a>

Sass Mixins

The only major addition to Sass for 2.02 is the addition of support for mixins in Sass, thanks to Garry Hill3. This allows you to define a collection of attributes or even sub-rules and include those in any rules you might want. To define a mixin named clearfix, you would write

=clearfix
  display: inline-block
  &:after
    content: "." 
    height: 0
    clear: both
    visibility: hidden
  * html &
    height: 1px

and to include it in a rule, you would write

#sidebar
  +clearfix
  border: 1px solid black

Little Things

  • There are three new predefined filters: :javascript, which wraps the content in <script> and CDATA tags; :cdata, which wraps the content in CDATA (thanks to Yehuda Katz and :escaped, which HTML-escapes the content.
  • Haml can output HTML4 or HTML5, thanks to Mislav Marhonić. This is set by setting the :format option to :html4 or :html5, respectively.
  • Boolean attributes conform to standards. For XHTML, %input{:selected => true} generates <input selected='selected' />. For HTML, it generates <input selected>.
  • Sass has a guarded-assignment ||= operator for constants that works just like the same operator in Ruby.
  • css2sass tries to resolve parent-references (like &:hover), and can generate the alternate attribute syntax when the --alternate flag is passed.

Morning-after edit: Also, I forgot to mention, Haml 2.0 is fully compatible with Rails 2.1.

The Future

What’s on the horizon for Haml 2.2? I think the set of Haml features is levelling off. There may be minor additions, but I don’t plan to add any major new functionality.

What I do want to focus on is improving what we’ve already got, both in terms of performance and syntax. It’s possible that Haml will be more lenient in various ways that I’m not willing to specify yet by the time 2.2 rolls around.

Sass, being a younger language then Haml, still has a fair amount of room for new features. There are several that various people have asked to have in Sass, relating in particular to constants. It’s very likely that there will be considerable effort put into that.

1 You can run them for yourself by running rake benchmark in the Haml directory. The benchmarks require Yehdua Katz’s benchwarmer gem.

2 There are more on the way for 2.2, though, don’t worry.

3 I couldn’t find a link to a blog or anything. Garry, if you read this and there’s somewhere you want me to link, let me know.

elliottcable said May 24, 2008:

Tried to digg this, but digg failed epicly.

Anyway, so awesome! Can’t wait to wake up tomorrow and play with the new features! d-:

Anonymous said May 24, 2008:

“To turn on HTML-escaping by default, set the :escape_html option to true in whatever way your framework provides.”

Might be useful to provide at least one example for how to do this. Rails for example. And the :ugly option too. I have no idea where to start looking for this.

Sunny said May 24, 2008:

I’m already loving Friendly Fred. Great job ! \o/

Hampton said May 24, 2008:

Great work by Nathan and all of the contributers for helping us hit this milestone in the haml community! I couldn’t ask for a more passionate and amazing community!

Chris McGrath said May 24, 2008:

Great news, looking forward to trying this out!

Lorin Tackett said May 24, 2008:

This totally thrills me! SASS 2.0 will open up a bunch of doors for me. Looking forward to the progression of this language. Keep it up!

Joe Van Dyk said May 24, 2008:

does it work with Rails edge?

Geoffrey Grosenbach said May 24, 2008:

Sass mixins are a fantastic feature! Thanks for the continued work to improve Haml and Sass!

Nathan said May 24, 2008:

I’m glad everyone’s so pleased :-).

Anonymous: I’ve added a bit about setting options to the HTML Escaping section.

Joe: Yes it does… for now. If Rails edge changes their template-handling mechanisms in the future, only the master and stable branches will be compatible until Haml 2.0.1 is released.

Ryan Mulligan said May 24, 2008:

One thing to consider for the future is to promote HAML in different languages. Maybe HAML doesn’t have to just be a Ruby thing.

Luis Esteban said May 24, 2008:

Thank you for the new features.

I have been reluctant to hack HAML (i.e. 1.8.2) but I eventually relented and added the following code after def surround in lib/haml/helpers.rb (I needed something more than surround). I’ve just discovered the new features that enable me to dispense with my hack, but here’s my hack (for discussion, there may still be a place for these):

def substitute(find,replace, &block)
  output = capture_haml(&block)
  "#{output.chomp.gsub(find,replace).chomp}\n" 
end
def concat(with = '', &block)
  substitute("\n", with, &block)
end

Example:

= concat ' | ' do
  = link_to 'Logout',  :action => 'logout'
  = link_to 'Change',  :action => 'change'
  = link_to 'Refresh', :action => 'refresh'

or even:

= surround '[ ', ' ]' do
  = concat ' | ' do
    = link_to 'Logout',  :action => 'logout'
    = link_to 'Change',  :action => 'change'
    = link_to 'Refresh', :action => 'refresh'
Luis Esteban said May 25, 2008:

Thanks also for the javascript filter. I want to create a slightly more efficient result when using it than from the following (i.e. replace repeated script containers with just one)

- unless @special_elements.blank?
  - @special_elements.each do |element|
    :javascript
      make_special('#{element.first}', #{element.last});

which produces:

<script type='text/javascript'>
  //<![CDATA[
    make_special('panel1', ...);
  //]]>
</script>
<script type='text/javascript'>
  //<![CDATA[
    make_special('panel2', ...);
  //]]>
</script>

How can I achieve (note: lines 2 and 3 are swapped)

- unless @special_elements.blank?
  :javascript
    - @special_elements.each do |element|
      make_special('#{element.first}', #{element.last});

to produce

<script type='text/javascript'>
  //<![CDATA[
    make_special('panel1', ...);
    make_special('panel2', ...);
  //]]>
</script>

I have a feeling you might say, “Do that in the controller”. The thing is that @special_elements is filled in by the various views that are generated (i.e. after the controller has run). If you could make this work, it would be pretty neat. I know it goes against the filter concept somewhat, but that could be a powerful concept if you could achieve such embedded control. Anyway, I look forward to your comments.

Thank you for HAML/SASS. I appreciate the contribution. It is a good philosophy to enforce MVC (well, at least to very strongly encourage MVC), particularly as neatness (elegance and conciseness) is a valuable result.

Luis Esteban said May 25, 2008:

I hope you can tell that I am a fan! Sorry for the multiple comments in a row. Please note that I am holding back!

Anyway, can I suggest or request that

%div[@user, :greeting]
  %bar[290]/
  Hello!

be equivalent to

[@user, :greeting]
  %bar[290]/
  Hello!

because it follows the same pattern with # and . in that if the tag is omitted then it is assumed to be a div (i.e. Implicit Div Elements)?

Luis Esteban said May 25, 2008:

Last one (for now).

I’m not sure if this is a bug, but it is at least a warning if you are doing something similar; otherwise, I’ve stuffed it up somehow. For a login form (i.e. if you use form_tag) in RubyOnRails, you have to put the end in.

... #login_panel
      - make_special('login_panel',...)
      .form
        = form_tag :action => :login do
          = text_field_tag :name, params[:name], :size => 30
          = password_field_tag :password, params[:password], :size => 30
          = submit_tag 'Login'
        - end       <-- doesn't work if left off, HAML doesn't auto close this one

The last end is necessary. This is a Rails thing and might be beyond HAML to account for it.

P.S. While I’ve got this code up there, can I make another feature request (just to make things a little DRYer)? Can you provide a variable which contains the id (if there is one) of the current container? This would be useful to me for many things, but this simple example will give you the idea.

... #login_panel
      - make_special('login_panel',...)
      .form

would become (or something similar, with some tasteful and appropriate escape sequence)

... #login_panel
      - make_special(@@@id,...)
      .form

or a namespace polluting solution such as

... #login_panel
      - make_special(@view.current_dom_id,...)
      .form

If you think that is too yuck, then I understand. I think it is worth talking about. Is there a difference between being DRY and being lazy?

Cheers and thanks again.

Nathan said May 25, 2008:

Ryan: There are already numerous alternate implementations of Haml, including two in PHP (phpHaml and pHaml), one in Python, and one in C#. There may be more that I haven’t heard about yet. I’ve been thinking of 2.0 as a time to start working on unifying these alternate implementations, thinking about what it means to be “Haml,” and so forth.

Luis: I can see how those helpers would be useful. I’m not convinced that it’s not a better idea to do Array#join in a helper.

For the :javascript stuff, filters are really meant to be used with primarily static text. I’d do that with

- unless @special_elements.blank?
  %script{:type => "text/javascript"}
    - @special_elements.each do |element|
      &== make_special('#{element.first}', #{element.last});

We may make [] trigger the default-div behavior in the future. I’ve been able to get form_for to work without an extra end… I can’t imagine why it wouldn’t for you. And finally, I think adding support for class- and id-insertion would either cause too many performance issues or force us to run #gsub on some Ruby code, neither of which I want to do.

Luis said May 26, 2008:

Thank you, Nathan, for your replies.

I do agree to not overlook using a helper and Array#join. I think it is easy for people to overlook them because when MVC is talked about, it is not presented as MVHC (H = Helper) and they kind of get left out until later.

I am tempted to try to convince. I think there does exist a sensible use case, but it’s not going to be very common. I think the best shot at it is that concat is conceptually between surround and list_of, and could/should be included for completeness. substitute could allow for some interesting post-processing of HTML. Those uses maybe a bit pathological but could be useful. I would like a more general form of list_of, but the answer is ultimately to use a helper. I do like that the resultant HTML is neatly indented. A helper can’t always make sure that indentation is correct, which is what is luring me out of a helper somewhat.

One drawback with using

%script{:type => "text/javascript"}

is that the CDATA is not included. I think you would need to do

%script{:type => "text/javascript"}
  !== //<![CDATA[
  make_special('panel1', ...);
  make_special('panel2', ...);
  !== //]]>

but then the Javascript code is not indented. :-( Is there a better solution?

The form_for is OK. It’s form_tag that’s the problem. I’ll have another look and see if I can post some useful information (hopefully it’s not that I’m a dummy!).

That’s quite fair enough to avoid performance issues with adding support for class- and id-insertion. It’s great how fast HAML is. I think an efficient alternative to using a gsub would be to venture into namespace polluting which could be an option

Haml::Template.options[:pollute_dom_ids] = true

or

Haml::Template.options[:store_dom_ids_in] = @dom_id_containment

Then when you go into the new scope do

Haml::Template.options[:store_dom_ids_in].push id

and when you leave the new scope do

Haml::Template.options[:store_dom_ids_in].pop

that way you could access the full containment of elements (even across partials), e.g.

... #login_panel
      - make_special(@dom_id_containment.last,...)
      - @domains.each do |domain|
        [@domain]
          - add_ajax_commands(@dom_id_containment,...)
          ...

I hope this makes sense. Can you see where I am going? I like neatness. ;-) Particularly, when neatness also produces speed!

Dominik said May 26, 2008:

Yehaa! The Whitespace-War is over ;-)

When I was confronted with the whitespace issue the first time, I intuitively added an trailing backslash to the line – but that obviously didn’t work. Have you thought about this solution?

BTW, the URLs in your (atom) feed don’t include the domain :-(

Nathan said May 26, 2008:

Luis: Helpers have various tools available to them for properly indenting their output, including using tab_up/tab_down, haml_tag, puts, or just returning a string that’s auto-indented by Haml. And in general, I’d be very nervous about just doing a gsub on newlines… what if there were newlines where we didn’t expect them? It seems way too dangerous.

In general, if you’re dynamically generating Javascript, you can HTML-escape it yourself (as I did in the example above), thus removing the need for CDATA. If you do need it, though, you could just write a simple helper.

The dom id detection isn’t as simple as that. Haml has distinct compilation and runtime phases, and the id may or may not be known in either of those. If it’s possible to implement this at all, it would require a huge amount of added complexity, and I just don’t think it’s worth it.

Dominik: Haml does have multiline syntax using |, the pipe character. The syntax is intentionally awkward, though; see question three in the Haml FAQ.

My feed has the xml:base attribute set, which provides the domain for domainless URLs. If your feed reader doesn’t process that, I suggest you submit a feature request ;-).

Luis said May 26, 2008:

Thanks Nathan. You are quite right. I think that answers the question about the difference between being DRY and being lazy. I think I was advocating laziness. The Javascript example is probably best done by:

- unless @special_elements.blank?
  = javascript_tag(@special_elements.collect{|element| "make_special('#{element.first}', #{element.last});"}.join("\n")

which, with a helper, would become

= special_elements_javascript

All nice and neat. Yes, much better. Same with the dom id business. Thank you for spending the time responding.

Another counter to my argument would be to write a custom filter. But, I think you’re right: it’s just wrong. It’s best to focus on HAML being fast. Being faster than ERB is a good thing.

Also, I really like SASS errors coming up in the page. Great work!

Trek said June 01, 2008:

I’ve been using the clearfix mixin trick for a while now (http://wonderfullyflawed.com/2008/05/21/clearfix-as-mixin/). Happy to see it make it off edge and into the wild!

Steve said December 13, 2009:

I’d like to see escaped Ruby for use as a comment language, replacing my use of markdown.

Make your comments snazzy with Textile!