[![Gem Version](https://badge.fury.io/rb/hexp.png)][gem] [![Build Status](https://secure.travis-ci.org/plexus/hexp.png?branch=master)][travis] [![Dependency Status](https://gemnasium.com/plexus/hexp.png)][gemnasium] [![Code Climate](https://codeclimate.com/github/plexus/hexp.png)][codeclimate] [![Coverage Status](https://coveralls.io/repos/plexus/hexp/badge.png?branch=master)][coveralls] [gem]: https://rubygems.org/gems/hexp [travis]: https://travis-ci.org/plexus/hexp [gemnasium]: https://gemnasium.com/plexus/hexp [codeclimate]: https://codeclimate.com/github/plexus/hexp [coveralls]: https://coveralls.io/r/plexus/hexp # Hexp **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 to clean up your helpers and presenters. ## Fundamentals 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 ``` html ``` If we would spell out all the objects, it would be represented as ``` ruby include Hexp Node.new(:nav, {"id"=>"menu"}, List.new([ Node.new(:ul, {}, List.new([ Node.new(:li, {}, List.new([TextNode.new("Home")])), Node.new(:li, {}, List.new([TextNode.new("lolcats")])), Node.new(:li, {}, List.new([TextNode.new("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: ``` ruby Node.new(:nav, {"id"=>"menu"}, Node.new(:ul, Node.new(:li, "Home"), Node.new(:li, "lolcats"), Node.new(:li, "Games") ) ) ``` There's also a shorthand syntax: ``` ruby node = H[:nav, {"id"=>"menu"}, H[:ul, 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(...)`. ### Hexp::Node A `Node` has a `#tag`, `#attrs` and `#children`. The methods `#set_tag`, `#set_attrs` and `#set_children` return a new updated instance. ``` ruby node = H[:p, { class: 'bold' }, "A lovely paragraph"] node.tag # => :p node.attrs # => {"class"=>"bold"} node.children # => ["A lovely paragraph"] node.set_tag(:div) # => 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"]] ``` #### Predicates ``` ruby node.tag?(:p) # => true node.text? # => false node.children.first.text? # => true ``` #### Attributes ``` ruby # [] : 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"]] ``` #### Children ``` ruby 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 ``` ruby node.select('p') # => #"bold"}, ["A lovely paragraph"]] @css_selector="p" matches=true>:each> node.replace('.warn') {|warning| warning.add_class('bold') } ``` #### The rest ``` ruby puts node.pp node.to_html node.to_dom # => Convert to Nokogiri ``` ### Hexp::List 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. ``` ruby list = Hexp::List[H[:p, "hello, world!"]] list.append("what", "a", "nice", "day") #=> [H[:p, ["hello, world!"]], "what", "a", "nice", "day"] ``` ## hexp-rails 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. ``` erb <%= H[:p, legacy_helper] %> ``` You need to explicitly opt-in to this behaviour. The easiest is to add a 'require' to your Gemfile ``` ruby gem 'hexp', require: 'hexp-rails' ``` ## Builder If you like the Builder syntax available in other gems like Builder and Hpricot, you can use `Hexp.build` to achieve the same ``` ruby Hexp.build 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" end end end # 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"]]]] ``` ## to_hexp 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 ``` ruby H[:p, 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](https://github.com/plexus/hexp-kramdown) Convert Markdown documents of various flavors to Hexp * [Slippery](https://github.com/plexus/slippery) Generate HTML/JS slides from Markdown. Supports backends for Reveal.js, Deck.js, and Impress.js. * [Yaks-HTML](https://github.com/plexus/slippery) Uses Hexp to render hypermedia API resources to HTML * [AssetPacker](https://github.com/plexus/asset_packer) 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.