Better HAML for Tailwind CSS
Published: 2020-11-09 09:27:57

## The Problem
When using Tailwind CSS, I didn't like the fact that classes for different states and different breakpoints (such as hover, focus, sm, etc) were grouped together in one class attribute in a HTML element. It made it hard to figure out which classes would be active on different states.

The better way, I felt, would be to have a `focus`, `hover`, `sm`, `md`, etc attributes for each element so that it was visually easy to figure out what tailwind classes would activate on different states. 

## The Solution

Since HAML is a Ruby library, I solved the above with a bit of monkey patching. I added the following after my `ApplicationHelper` module code in `app/helpers/application_helper.rb`:
``` ruby
module Haml
  # Haml::AttriubuteParser parses Hash literal to { String (key name) => String (value literal) }.
  module AttributeParser
    class << self
      # @param  [String] exp - Old attributes literal or Hash literal generated from new attributes.
      # @return [Hash<String, String>,nil] - Return parsed attribute Hash whose values are Ruby literals, or return nil if argument is not a single Hash literal.
      def parse(exp)
        return nil unless hash_literal?(exp)

        hash = Hash.new{"\"\""}
        each_attribute(exp) do |key, value|
          if key.in?(['sm', 'md', 'lg', 'xl', 'focus', 'hover'])
            # Build up string to insert into class attribute
            tw_classes = value[1..-2]   # Remove the esacped quotes from the original string
                          .split        # Split into array
                          .map{ |clas| "#{key}:#{clas}" }
                          .join(" ")

            # Insert tw classes string before last escape quote
            hash['class'] = hash['class'].insert(-2, " #{tw_classes}")
          elsif key == 'class'
            hash['class'] = hash['class'].insert(-2, " #{value[1..-2]}")
          else
            hash[key] = value
          end
        end
        hash
      rescue UnexpectedTokenError, UnexpectedKeyError
        nil
      end
    end
  end
end
```

Now, in my views I'm able to style my elements with Tailwind CSS like so:
``` haml
%input{class: "bg-gray-900 text-white pl-10 pr-4 py-2 rounded-lg w-full",
       sm: "bg-red-400 pl-2"
       focus: "outline-none bg-white text-gray-900",
       hover: "outline-none bg-white text-gray-900",
       placeholder: "Search by keywords"}
```

## The Solution Process

I figured this was a parsing problem so after a look at the HAML documentation, I figured I would have to monkey patch [`HAML::AttributeParser`](https://github.com/haml/haml/blob/c67e67274353526d5a5da2a8d36cabcc313ed7ab/lib/haml/attribute_parser.rb#L27). Investigating the code further, there was only one method that made sense to patch: the `parse` method.

I copied and pasted the code over to my `application_helper.rb` and then inserted a `raise` to see if my code was affecting the parser and it was.

After a few `puts`, each followed by a `raise`, I found that attribute values were saved in the `hash` with double quotes escaped. Thus, an element with attributes of
``` haml
{class: "bg-white text-black"}
```
was saved in the local `hash` variable with a key of `class` and value of 
``` ruby
"\"bg-white text-black\""
```

Thus, all we have to do was handle the double quotes properly.

When inserting into the key of a hash, we have to insert before the last double quote using `.insert(-2, <string>)`.

When handling the value string of an attribute, we have to remove the escaped double quotes using `[1..-2]`.

I'm sure there's a more efficient way of achieving the above. If you have a better solution, please let me know by creating a PR / Discussion [here](https://github.com/vivkr/haml_tailwind_monkeypatch).