README.md in hexp-0.4.1 vs README.md in hexp-0.4.2

- old
+ new

@@ -10,116 +10,222 @@ [codeclimate]: https://codeclimate.com/github/plexus/hexp [coveralls]: https://coveralls.io/r/plexus/hexp # Hexp -**Hexp** (pronounced [ˈɦækspi:]) is a Ruby API for creating and manipulating HTML syntax trees. It enables a web application architecture where HTML is only ever represented as structured data, rather than as plain text. +**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. -Only when the data needs to be serialized and sent over the network is it converted to a string representation. This has a number of advantages. +## Fundamentals -* Single responsibility : HTML generation is not mixed with business logic -* Security : Protection from XSS (cross-site scripting) -* Productivity : components that create or alter fragments of a HTML become generic, reusable parts +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. -For a more in-depth explanation please see the slides of talk [Web Linguistics, Towards Higher Fluency](http://arnebrasseur.net/talks/eurucamp2013/presentation.html) given at Eurucamp 2013. (the video is not available yet.) +Take this bit of HTML -**Creating Hexps** +``` html +<nav id="menu"> + <ul> + <li>Home</li> + <li>Lolcats</li> + <li>Games</li> + </ul> +</nav> +``` -Hexps are basically snippets of HTML written in nothing but Ruby, here's an example. +If we would spell out all the objects, it would be represented as -````ruby -@message = "Hexps are fun for the whole family, from 9 to 99 years old." -@hexp = H[:div, {class: 'hexp-intro'}, [ - [:p, @message] - ] -] -```` +``` ruby +include Hexp -For more info to get you up and running have a look at the API documentation for [Hexp::Node](http://plexus.github.io/hexp/Hexp/Node.html). +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")])) + ]) + ) + ]) +) +``` -**Don't people use templates for this kind of thing?** +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. -They do, this is an alternative approach. With templates you need to think about which parts need to be HTML-escaped, or you can make errors like forgetting a closing tag. With hexps you no longer need to think about escaping. +The above simplifies to: -**Wait how is that?** +``` ruby +Node.new(:nav, {"id"=>"menu"}, + Node.new(:ul, + Node.new(:li, "Home"), + Node.new(:li, "lolcats"), + Node.new(:li, "Games") + ) +) +``` -With traditional approaches you can insert plain text in your template, or snippets of HTML. The first must be escaped, the second should not. For your template they are all just strings, so you, the programmer, need to distinguish between the two in a way. For example by using `html_escape` on one (explicit escaping), or `html_safe` on the other (implicit escaping). +There's also a shorthand syntax: -When using hexps you never deal with strings that actually contain HTML. Helper methods would return hexps instead, and you can combine those into bigger hexps. Strings inside a hexp are always just that, so they will always be escaped without you thinking about it. +``` ruby +node = H[:nav, {"id"=>"menu"}, + H[:ul, + H[:li, "Home"], + H[:li, "Lolcats"], + H[:li, "Games"]]] -**So that's it, easier escaping?** +puts node.to_html +``` -Well that's not all, by having a simple lightweight representation of HTML that is _a real data structure_, you can really start programming your HTML. If you have an object that responds to `to_hexp`, you can use that object inside a hexp, so you can use Object Orientation for your user interface. Like so +If the first argument to `H[...]` is a Symbol, then the result is a `Node`, otherwise it's a `List`. -````ruby -class ProfileLink < Struct.new(:user) - def to_hexp - H[:a, {class: "profile-link", href: "/user/#{user.id}"}, user.name] - end -end +You can parse exisiting HTML to Hexp with `Hexp.parse(...)`. -class Layout < Struct.new(:content) - def to_hexp - H[:html, [ - [:body, [content]] - ] - end -end +### Hexp::Node -render inline: Layout.new(ProfileLink.new(@user)).to_html -```` +A `Node` has a `#tag`, `#attrs` and `#children`. The methods `#set_tag`, `#set_attrs` and `#set_children` return a new updated instance. -**Does it get any better?** +``` ruby +node = H[:p, { class: 'bold' }, "A lovely paragraph"] +node.tag # => :p +node.attrs # => {"class"=>"bold"} +node.children # => ["A lovely paragraph"] -It does! The really neat part is having filters that process this HTML tree before it gets serialized to text. This could be good for +node.set_tag(:div) +# => H[:div, {"class"=>"bold"}, ["A lovely paragraph"]] -- populating form fields -- adding extra admin buttons when logged in -- cleanly separate aspects of your app (e.g. discount codes) from 'core' implementation -- becoming filthy rich and/or ridiculously happy +node.set_attrs({id: 'para-1'}) +# => H[:p, {"id"=>"para-1"}, ["A lovely paragraph"]] -**What's up with the funny name?** +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"]] +``` -Hexp stands for HTML expressions. It's a reference to s-expressions as they are known in LISP languages, a simple way to represent data as nested lists. +#### Predicates -How to use it -------------- +``` ruby +node.tag?(:p) # => true +node.text? # => false +node.children.first.text? # => true +``` -Hexp objects come in two flavors : `Hexp::Node` and `Hexp::List`. A `Node` consists of three parts : its `tag`, `attributes` and `children`. A `List` is just that, a list (of nodes). +#### Attributes -To construct a `Node` use `H[tag, attributes, children]`. Use a `Symbol` for the `tag`, a `Hash` for the `attributes`, and an `Array` for the `children`. Attributes or children can be omitted when they are empty. +``` ruby +# [] : As in Nokogiri/Hpricot, attributes can be accessed with hash syntax +node['class'] # => "bold" -The list of children will automatically be converted to `Hexp::List`, similarly for any nested nodes you can simply use `[tag, attributes, children]` without the `H`, all nodes in the tree will be converted to proper `Hexp::Node` objects. +# 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"]] -The entire API is centered around these two classes, and one of them you can think of as essentially just an `Array`, in other words Hexp is super easy to learn. Try it out in `irb`, have a look at the examples, and *build cool stuff*! +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"]] -A note on immutability ----------------------- +# 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"]] +``` -All Hexp objects are frozen on creation, you can never alter them afterwards. Operations always return a new `Hexp::Node` rather than working in place. +#### Children -This might seem stringent when you are not used to this style of coding, but it's a pattern that generally promotes good code. +``` 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"]] +``` -Can I already use it --------------------- +#### CSS Selectors -Yes, but there are some things to keep in mind. +``` ruby +node.select('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') } +``` -For the 0.x line of versions Hexp is not restricted to [semantic versioning](http://semver.org). We are still designing the API, and small backwards incompatible changes may occur. However given that the project's aim is to only provide the lowest level of DOM manipulation upon which others can built, it is already quite feature complete. It shouldn't be too long before we release a 1.0.0, after which we will commit to semantic versioning. +#### The rest -Another thing is that Hexp is young. It hasn't been battle tested yet, and the ecosystem which will make this approach truly attractive is yet to emerge. Therefore better try it out on smaller, non-critical projects first, and give us your feedback. +``` ruby +puts node.pp +node.to_html +node.to_dom # => Convert to Nokogiri +``` -Is it any good? ---------------- +### Hexp::List -Yes +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. -How to install --------------- +Additionally `Hexp::List` implements `to_html`, `append`, and `+`. Just like built-in collections, the class implements `[]` as an alternative constructor. -At this point you're best off grabbing the Git repo, e.g. with bundler +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. -````sh -# Gemfile +``` ruby +list = Hexp::List[H[:p, "hello, world!"]] +list.append("what", "a", "nice", "day") +#=> [H[:p, ["hello, world!"]], "what", "a", "nice", "day"] +``` -gem 'hexp', github: 'plexus/hexp' -```` +## 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.