Ruby virtual DOM for HTML
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.

Gem Version Build Status Dependency Status Code Climate Coverage Status


Hexp (pronounced [ˈɦækspi:]) is a DOM API for Ruby. It lets you treat HTML in your applications as objects, instead of strings. It is a standalone, framework independent library. You can use it to build full web pages, or just to clean up your helpers and presenters.


The three central classes are Hexp::Node, Hexp::TextNode, and Hexp::List. Instances of these classes are immutable. You can mostly treat TextNode as a String and List as an Array, except that they're immutable, and come with some extra convenience functions.

Take this bit of HTML

<nav id="menu">

If we would spell out all the objects, it would be represented as

include Hexp, {"id"=>"menu"},[, {},[, {},["Home")])),, {},["lolcats")])),, {},["Games")]))

The Hexp::Node constructor is lenient though. It knows how to wrap things in TextNode and List instances, and it will let you omit the attributes Hash if it's empty, so you never actually type all of that out.

The above simplifies to:, {"id"=>"menu"},,, "Home"),, "lolcats"),, "Games")

There's also a shorthand syntax:

node = H[:nav, {"id"=>"menu"},
           H[:li, "Home"],
           H[:li, "Lolcats"],
           H[:li, "Games"]]]

puts node.to_html

If the first argument to H[...] is a Symbol, then the result is a Node, otherwise it's a List.

You can parse exisiting HTML to Hexp with Hexp.parse(...).


A Node has a #tag, #attrs and #children. The methods #set_tag, #set_attrs and #set_children return a new updated instance.

node = H[:p, { class: 'bold' }, "A lovely paragraph"]
node.tag      # => :p
node.attrs    # => {"class"=>"bold"}
node.children # => ["A lovely paragraph"]

# => H[:div, {"class"=>"bold"}, ["A lovely paragraph"]]

node.set_attrs({id: 'para-1'})
# => H[:p, {"id"=>"para-1"}, ["A lovely paragraph"]]

node.set_children(H[:em, "Ginsberg said:"], "The starry dynamo in the machinery of night")
# => H[:p, {"class"=>"bold"}, [H[:em, ["Ginsberg said:"]], "The starry dynamo in the machinery of night"]]


node.tag?(:p) # => true
node.text? # => false
node.children.first.text? # => true


# [] : As in Nokogiri/Hpricot, attributes can be accessed with hash syntax
node['class'] # => "bold"

# attr : Analogues to jQuery's `attr`, read-write based on arity
node.attr('class') # => "bold"
node.attr('class', 'bourgeois') # => H[:p, {"class"=>"bourgeois"}, ["A lovely paragraph"]]

node.has_attr?('class') # => true
node.class?('bold') # => true
node.add_class('daring') # => H[:p, {"class"=>"bold daring"}, ["A lovely paragraph"]]
node.class_list # => ["bold"]
node.remove_class('bold') # => H[:p, ["A lovely paragraph"]]
node.remove_attr('class') # => H[:p, ["A lovely paragraph"]]

# merge_attrs : Does a Hash#merge on the attributes, but the class
# attribute is treated special. aliased to %
node.merge_attrs(class: 'daring', id: 'poem')
# => H[:p, {"class"=>"bold daring", "id"=>"poem"}, ["A lovely paragraph"]]


node.empty? # => false
node.append(H[:blink, "ARRSOME"], H[:p, "bye"]) # => H[:p, {"class"=>"bold"}, ["A lovely paragraph", H[:blink, ["ARRSOME"]], H[:p, ["bye"]]]]
node.text # => "A lovely paragraph"
node.map_children { |ch| ch.text? ? ch.upcase : ch } # => H[:p, {"class"=>"bold"}, ["A LOVELY PARAGRAPH"]]

CSS Selectors'p')
# => #<Enumerator: #<Hexp::Node::CssSelection @node=H[:p, {"class"=>"bold"}, ["A lovely paragraph"]] @css_selector="p" matches=true>:each>
node.replace('.warn') {|warning| warning.add_class('bold') }

The rest

puts node.pp
node.to_dom # => Convert to Nokogiri


A Hexp::List wraps and delegates to a Ruby Array, so it has the same API as Array. Methods which mutate the Array will raise an exception.

Additionally Hexp::List implements to_html, append, and +. Just like built-in collections, the class implements [] as an alternative constructor.

Equality checks with == only compare value equality, so comparing to an Array with the same content returns true. Use eql? for a stronger "type and value" equality.

list = Hexp::List[H[:p, "hello, world!"]]
list.append("what", "a", "nice", "day")
#=> [H[:p, ["hello, world!"]], "what", "a", "nice", "day"]


There is a thin layer of Rails integration included. This makes Hexp aware of the html_safe / html_safe? convention used to distinguish text from markup. It also aliases to_html to to_s, so Hexp nodes and lists can be used transparently in templates.

<%= H[:p, legacy_helper] %>

You need to explicitly opt-in to this behaviour. The easiest is to add a 'require' to your Gemfile

gem 'hexp', require: 'hexp-rails'


If you like the Builder syntax available in other gems like Builder and Hpricot, you can use to achieve the same do
  div id: 'warning-sign' do
    span "It's happening!"
    ul.warn_list do
      li "Cats are taking over the world"
      li "The price of lasagne has continued to rise"

# H[:div, {"id"=>"warning-sign"}, [
#   H[:span, [
#     "It's happening!"]],
#   H[:ul, {"class"=>"warn_list"}],
#   H[:li, [
#     "Cats are taking over the world"]],
#   H[:li, [
#     "The price of lasagne has continued to rise"]]]]


When an object implements to_hexp it can be used where you would otherwise use a node. This can be useful for instance to create components that know how to render themselves.

Yaks does not contain any core extensions, but there is an optional, opt-in, implementation of to_hexp for NilClass, so nils in a list of nodes won't raise an error. This lets you write things like

  some_node if some_condition?,
  other_node if other_condition?

You can use it with require 'hexp/core_ext/nil'. Loading hexp-rails will automatically include this because, let's be honest, if you're using Rails a single monkey patch won't make the difference.

Related projects

  • Hexp-Kramdown Convert Markdown documents of various flavors to Hexp
  • Slippery Generate HTML/JS slides from Markdown. Supports backends for Reveal.js, Deck.js, and Impress.js.
  • Yaks-HTML Uses Hexp to render hypermedia API resources to HTML
  • AssetPacker Find all images, stylesheets, and javascript references in a HTML file, save them to local files, and return an updated HTML file pointing to the local resources.