Project

hiccdown

0.0
A long-lived project that still receives updates
Generates an HTML string from a Hiccup structure and improves Rails views.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

Runtime

>= 7.0, < 9
>= 7.0, < 9
 Project Readme

Hiccdown

Hiccdown is a simple Ruby gem that parses arrays and turns them into HTML strings.

In Rails, it solves a major problem nobody talks about.

The name is a variation on the popular Clojure package Hiccup. Hiccdown introduces the same basic functionality in Ruby (with some extensions, see below).

The problem

If you’re used to writing embedded Ruby (those pesky .erb files), you may not realize how bad it is.

Consider this template:

<ul>
  <% [1, 2, 3].each do |i| %>
    <li><%= i %></li>
  <% end %>
</ul>

This is gross. Embedded Ruby makes you mix your template and your logic. Rails is big on separation of concerns, and the above example is the opposite of that. It’s “programming in strings”, as a former colleague of mine calls it.

The fundamental mistake is that of forcing the language in charge of assembling and rendering the template – in this case, Ruby – into the template itself. Ruby should be in control; ‘above’ the template, as it were. Instead, it’s demoted to living inside its own creation, resurfacing only through strange interpolative outgrowths.

This problem is well known in the Clojure world. Logic should be taken care of before rendering, not during.

Hiccdown takes a datastructure representing your template – which you’re free to build up programmatically in any way you like, using the full power of Ruby (map, filter, reduce etc) – and then turns that datastructure into HTML at the end. All of this still happens on the server, so you still get all the benefits of pre-processing.

Compare the above erb syntax with this simple but functionally equivalent Hiccdown syntax:

[:ul, [1, 2, 3].map { |i| [:li, i] }]

Benefits

Here are some of the benefits of Hiccdown:

  1. Clean separation of logic and presentation – never write HTML again
  2. More concise, Ruby-native syntax
  3. Easier programmatic manipulation of content – data is easier to traverse and manipulate than HTML strings
  4. Simplified post-processing without additional parsing libraries
  5. Reduced risk of HTML-injection vulnerabilities
  6. Better composability and reusability of components
  7. Easier to generate dynamic structures
  8. Enhanced static analysis capabilities

Once you understand these benefits, you’ll realize, for example, that Rails having both helper methods and view partials has always been a code smell – see below.

Installation

In your Gemfile:

gem 'hiccdown'

Then $ bundle.

Hiccdown supports Rails 7.0 through 8.1. See the changelog for what changed between versions, including what to look out for when upgrading to 2.0.0.

Usage in Ruby

The original Hiccup explanation applies:

The first [item] of the [array] is used as the element name. The second [item] can optionally be a map, in which case it is used to supply the element's attributes. Every other [item] is considered part of the [element]'s body.

# plain
Hiccdown.to_html [:h1, 'hello world']
# => '<h1>hello world</h1>'

# nested elements
Hiccdown.to_html [:div, [:h1, 'hello world']]
# => '<div><h1>hello world</h1></div>'

# nested siblings
Hiccdown.to_html [:div, [:h1, 'hello world'], [:h2, 'hello again']]
# => '<div><h1>hello world</h1><h2>hello again</h2></div>'

# attributes
Hiccdown.to_html [:h1, {class: 'heading big'}, 'hello world']
# => '<h1 class="heading big">hello world</h1>'

# ids and classes in the tag, in any order
Hiccdown.to_html [:'h1#title.heading.big', 'hello world']
# => '<h1 id="title" class="heading big">hello world</h1>'

# an id in the attributes wins, and their classes are added
Hiccdown.to_html [:'h1#title.heading', {id: 'main', class: 'big'}, 'hello world']
# => '<h1 id="main" class="heading big">hello world</h1>'

# children as arrays
Hiccdown.to_html [:ul, [[:li, 'first'], [:li, 'second']]]
# => '<ul><li>first</li><li>second</li></ul>'
#
# This is equivalent to writing:
Hiccdown.to_html [:ul, [:li, 'first'], [:li, 'second']]
# So why use it? So you can use methods that return arrays inside your hiccdown
# structure without having to use the splat operator every time:
Hiccdown.to_html [:ul, ['first', 'second'].map { |i| [:li, i] }]
# => '<ul><li>first</li><li>second</li></ul>'

Components

Methods that return Hiccdown act as components:

def component
  [:h1, 'hello world']
end

Programmatically nest Hiccdown for rich components:

def component text, *children
  [:div, text, *children]
end

component('hello world', [:p, 'hello america'], [:p, 'hello europe'])
# => [:div, 'hello world', [:p, 'hello america'], [:p, 'hello europe']]

Usage in Rails

Include the following module in your ApplicationHelper:

module ApplicationHelper
  include Hiccdown::ViewHelpers
end

View replacement

Hiccdown replaces view files. It intercepts render to point to helper methods instead.

For instance, picture a ProductsController with an index and a show action:

class ProductsController < ApplicationController
  def index
    @products = Product.all
  end

  def show
    @product = Product.find(params[:id])
  end
end

Hiccdown then calls the index and show methods on the ProductsHelper, turns the return value into HTML, and renders it in the browser, inside the application layout, just as you would expect for an erb template. Helper methods thus become Hiccdown components:

module ProductsHelper
  def index
    [:ul, @products.map { |p| [:li, p.title] }]
    # => Renders '<ul><li>…</li>…</ul>'
  end

  def show
    [:div
      [:h1, @product.title]
      [:span, @product.description]]
    # => Renders '<div><h1>…</h1><span>…</span></div>'
  end
end

You can also render Hiccdown directly in your controller:

class FooController < ApplicationController
  def bar
    render hiccdown: [:h1, 'hello world!']
  end
end

Hiccdown can be used inside .erb templates, but that’s discouraged:

<!-- bar.html.erb -->
<%= Hiccdown.to_html([:h1, @text]).html_safe %>

(Be careful with html_safe.)

Layouts

Layouts live in helper methods called layout, too. yield gives you the page’s content and yield(:title) whatever was set with content_for(:title), just like in an erb layout:

module ApplicationHelper
  def layout
    [:html,
      [:head,
        [:title, yield(:title)],
        csrf_meta_tags],
      [:body, yield]]
  end
end

Hiccdown adds <!DOCTYPE html> to layouts whose root is :html.

Layouts are found the way Rails finds layout files: a layout named products lives in ProductsHelper#layout instead of app/views/layouts/products.html.erb. So ProductsController uses ProductsHelper#layout if there is one, and ApplicationHelper#layout otherwise, and layout 'admin' uses AdminHelper#layout.

Mailers

Emails work the same way. A mailer's helper is named after it, so the HTML part of UserMailer#welcome lives in UserMailerHelper#welcome:

module UserMailerHelper
  def welcome
    [:p, "Welcome, ", @user.name]
  end
end

A text part still comes from a template, like app/views/user_mailer/welcome.text.erb, if there is one. Mailer layouts are found as in controllers, so layout 'mailer' uses MailerHelper#layout for HTML and app/views/layouts/mailer.text.erb for text.

Mailers don't include ApplicationHelper, so include Hiccdown::ViewHelpers in a helper they use too, such as MailerHelper, for Rails helpers with blocks, like content_for(:footer) { [...] }, to take Hiccdown.

Usage with additional helper methods

Since Hiccdown code lives inside helpers anyway, simply use additional helper methods in your Hiccdown code:

module ProductsHelper
  def index
    [:ul, @products.map { |p| product(p) }] # calls `product` method below
  end

  def show
    [:div
      [:h1, @product.title]
      [:span, @product.description]]
  end

  # This would traditionally live in a _product.html.erb partial
  def product p
    [:li, p.title]
  end
end

As you can see, Hiccdown eliminates the need for view partials, as well. Again, that both partials and helper methods exist in Rails has always been a code smell – it’s a consequence of the wider problem that Rails does not properly separate logic and rendering. This fudge leads to situations where, for instance, you’re not sure if you should make a partial that calls helper methods or create a helper method that uses content_tags.

Using existing Rails helpers

You can continue using Rails’s built-in helper methods such as link_to:

module ProductsHelper
  def product p
    [:li,
      link_to(p.title, p)] # functionally equivalent to
  end                      # [:a, { href: url_for(p) }, p.title]
end

Built-in helper methods can process Hiccdown returned by blocks:

module ProductsHelper
  def product p
    [:li,
      link_to(p) do
        [:h2, p.title]
      end]
  end
end

However, rather than use built-in helpers that render HTML, you are encouraged to just use Hiccdown replacements whenever possible. In many cases, nesting Hiccdown structures lets you avoid blocks altogether:

[:a, { href: url_for(p) }, # instead of link_to
  [:h2, p.title]]

link_to, button_to, content_tag, and all other built-in helper methods for rendering markup should support Hiccdown blocks. Form helpers support them as well:

module ProductsHelper
  def form p
    form_with(model: p) do |f|
      [:div,
        f.text_field(:title),
        f.text_area(:description),
        [:button, { type: :submit }, 'Submit']]
    end
  end
end

They take Hiccdown as content, too, so helpers of your own can yield into them:

link_to([:span, p.title], p)

def card
  content_tag(:div, yield, class: 'card')
end

card { [:h2, p.title] }

Scoping

Computations should generally precede the building of a Hiccdown structure itself. Remember, these are all just helper methods, so as long as your method returns Hiccdown, any valid Ruby code works:

def product p
  total_sold = product.sales.map(&:total).reduce(&:+)

  [:li, p.title,
    [:strong, 'Sold: ', total_sold]] # total_sold can also be a separate method altogether
end

But sometimes, you want to perform computations within a Hiccdown structure. Hiccdown ships with a simple method called scope:

def product p
  [:li, p.title,
    scope do
      total_sold = product.sales.map(&:total).reduce(&:+)

      [:strong, 'Sold: ', total_sold]
    end]
end

scope accepts arbitrary arguments for easy variable setup:

scope(1, 2, 3) do |a, b, c|
  # Instead of
  # a = 1
  # b = 2
  # c = 3
end

Outside of Rails, scope is available on the Hiccdown module: Hiccdown.scope

Gradual rollout

You don’t need to replace your views all at once. When there’s no helper method corresponding to a controller action, Rails will render the erb template as it normally would. Once you’ve migrated a template, simply delete it.

In addition, you can still call render in your helpers. So, when a view renders a partial, you can continue to render it in your helper until you’ve migrated the partial itself. For example:

# ProductsHelper
def index
  [:div,
    [:h1, 'Products'],
    render(@products)]
end
<!-- app/views/products/_product.html.erb -->
<h1><%= product.title %></h1>

Then, migrate the _product.html.erb partial into a helper method called product in the same module and update the index method accordingly:

def index
  [:div,
    [:h1, 'Products'],
    # Invoke `product` instead of `render`
    @products.map { |p| product(p) }]
end

# This replaces _product.html.erb
def product p
  [:h1, p.title]
end

Lastly, delete _product.html.erb.

The same goes for layouts. Helper layouts wrap erb views and erb layouts wrap Hiccdown views. Rails looks for a layout named after the controller first, then after each of its parents, so a products.html.erb layout file still comes before ApplicationHelper#layout. Where a layout exists both as a helper method and as a file, the helper method is used and a warning is logged, so you can delete the file.

Usage with turbo streams

Where previously you might render a turbo stream like this:

turbo_stream.update(@product, partial: 'products/product', locals: { product: @product })

You now pass Hiccdown by invoking the helper method that replaces the partial:

turbo_stream.update(@product, hiccdown: product(@product))

Or pass a Hiccdown structure directly:

turbo_stream.update(@product, hiccdown: [:h1, @product.title])

Since turbo streams are just glorified HTML templates, you can also just add the following method to your ApplicationHelper:

def ts action, target, *children
  [:'turbo-stream', {action: action, target: target},
    [:template, *children]]
end

# Easy creation of a hiccdown turbo stream
ts(:update, dom_id(@product), product(@product))

An action's helper method can answer with turbo streams, too. When it returns one or more, in Hiccdown or from turbo_stream, and the request takes turbo streams, as Turbo's form submissions do, Hiccdown sends them as such, without a layout:

module ProductsHelper
  def update
    if @product.valid?
      ts(:update, dom_id(@product), product(@product))
    else
      edit # A page, in its layout
    end
  end
end

HTML escape

Hiccdown escapes HTML characters in attribute values and primitive children. You can override this behavior by passing false as the second parameter:

Hiccdown.to_html([:h1, '<script>alert("pwned");</script>'], false)

Hiccdown does not escape strings marked as html_safe. This can be useful when rendering HTML entities:

[:p,
  'foo',
  ' &middot '.html_safe,
  'bar']

# => Browser renders this as 'foo · bar'

Hiccup extensions

For convenience, Hiccdown extends Hiccup in four ways:

  1. Deeply nested attribute hashes result in hyphenated attribute keys. This is useful for constructing data attributes. For example:

    Hiccdown.to_html([:div, { data: { foo: { bar: 'baz' }, fuzz: 'buzz' } }])
    # => '<div data-foo-bar="baz" data-fuzz="buzz"></div>'
  2. Array attribute values are concatenated with a space (after each being cast to a string and escaped). nil and empty strings are ignored. This is useful for programmatically building class attributes:

    Hiccdown.to_html([:div, { class: ['foo', :bar, nil, '', 1] }])
    # => '<div class="foo bar 1"></div>'

Of course, these first two extensions can be mixed:

Hiccdown.to_html([:div, { data: { foo: ['bar', :baz] } }])
# => '<div data-foo="bar baz"></div>'
  1. To get top-level siblings, ie elements without a parent, wrap them in an array. The elements can be arrays and/or strings and will simply be concatenated:

    Hiccdown.to_html([[:div, 'foo'], [:div, 'bar']])
    # => '<div>foo</div><div>bar</div>'
    
    Hiccdown.to_html(['foo', [:div, 'bar']])
    # => 'foo<div>bar</div>'
  2. As in Rails, attributes that are nil are left out, and so are boolean attributes, like disabled or checked, that are false, so they can be set conditionally. For other attributes, like draggable or data attributes, false is a value:

    Hiccdown.to_html([:button, { disabled: false, draggable: false, data: { open: false } }, 'Open'])
    # => '<button draggable="false" data-open="false">Open</button>'

Contributing

Report issues on the Veritula issue tracker. Do not submit issues on GitHub.

$ rake test runs the tests against the latest Rails, and $ rake test:rails against each supported version, with the Gemfiles in gemfiles.

test/dummy is a small Rails app for trying Hiccdown out in a browser ($ test/dummy/bin/rails server). $ rake test:system runs the system tests, which drive it in headless Brave, or in another Chromium-based browser at BROWSER_PATH. They’re slower, so rake test leaves them out.

Bug bounty program

There’s a Veritula bug bounty program for Hiccdown. Report bugs there, not on GitHub.