README.md
# HtmlSurgeon
[![Gem Version](https://badge.fury.io/rb/html_surgeon.svg)](http://badge.fury.io/rb/html_surgeon)
[![Build Status](https://travis-ci.org/eturino/html_surgeon.svg?branch=master)](https://travis-ci.org/eturino/html_surgeon)
[![Code Climate](https://codeclimate.com/github/eturino/html_surgeon.png)](https://codeclimate.com/github/eturino/html_surgeon)
[![Code Climate Coverage](https://codeclimate.com/github/eturino/html_surgeon/coverage.png)](https://codeclimate.com/github/eturino/html_surgeon)
Make specific changes in a HTML string, optionally adding html attributes with the audit trail of the changes. Uses [Nokogiri](http://www.nokogiri.org/).
## Basic Usage
First, you create a HtmlSurgeon service instance for the given html fragment
```ruby
GIVEN_HTML = <<-HTML
<div>
<h1>Something</h1>
<div id="1" class="lol to-be-changed">1</div>
<span>Other</span>
<div id="2" class="another to-be-changed">
<ul>
<li>1</li>
<li>2</li>
</ul>
</div>
</div>
HTML
surgeon = HtmlSurgeon.for(GIVEN_HTML)
```
if you want to add audit attributes in the HTML tags changed, pass the option in the surgeon service creation
```ruby
surgeon = HtmlSurgeon.for(GIVEN_HTML, audit: true)
```
with the surgeon service, you can prepare several change sets. A change set is defined by a node set and a list of changes to be applied on each selected node.
```ruby
change_set = surgeon.css('div.to-be-changed') # => will return a change_set
change_set.node_set # => will return a Nokogiri's Node Set with the selected nodes (right now it'll get us div ID 1 and div ID 2.
# to prepare a change replace the tag name 'div' for 'article'
change_set.replace_tag_name('article')
# to prepare another change to add a css class in the selected nodes
change_set.add_css_class('added-class')
# we can add a second one
change_set.add_css_class('another-added-class')
```
The changes are not made yet. In order to do it, we call `run` on the change set
```ruby
change_set.run
surgeon.html # => html with the changes applied
# =>
# <div>
# <h1>Something</h1>
# <article id="1" class="lol to-be-changed added-class another-added-class">1</div>
# <span>Other</span>
# <article id="2" class="another to-be-changed added-class another-added-class">
# <ul>
# <li>1</li>
# <li>2</li>
# </ul>
# </div>
# </div>
# original html still in
surgeon.given_html == GIVEN_HTML # => true
# you can review what was changed in the change set
change_set.changes
# =>
# [
# "replace tag name with article",
# "add css class added-class",
# "add css class another-added-class",
# ]
```
You can also review what nodes were changed, or the count of them
```ruby
change_set.run
change_set.changed_nodes # => array with nodes changed (without the skipped nodes)
change_set.changed_nodes_size # => same as change_set.changed_nodes.size
```
We can also chain call the changes in a changeset
```ruby
surgeon = HtmlService.for(GIVEN_HTML)
surgeon.css('.lol').replace_tag_name('span').add_css_class('hey').run
surgeon.html # =>
# <div>
# <h1>Something</h1>
# <span id="1" class="lol to-be-changed hey">1</span>
# <span>Other</span>
# <div id="2" class="another to-be-changed">
# <ul>
# <li>1</li>
# <li>2</li>
# </ul>
# </div>
# </div>
```
If we have enabled audit, we'll get the changes applied to an element in an data attribute.
It will store, in JSON, an array with all the changes.
```ruby
surgeon = HtmlService.for(GIVEN_HTML, audit: true)
surgeon.css('.lol').replace_tag_name('span').add_css_class('hey').run
surgeon.html # =>
# <div>
# <h1>Something</h1>
# <span id="1" class="lol to-be-changed hey" data-surgeon-audit='[{"change_set":"830e96dc-fa07-40ce-8968-ea5c55ec4b84","changed_at":"2015-07-02T12:52:43.874Z","type":"replace_tag_name","old":"div","new":"span"},{"change_set":"830e96dc-fa07-40ce-8968-ea5c55ec4b84","changed_at":"2015-07-02T12:52:43.874Z","type":"add_css_class","class":"hey"}]'>1</span>
# <span>Other</span>
# <div id="2" class="another to-be-changed">
# <ul>
# <li>1</li>
# <li>2</li>
# </ul>
# </div>
# </div>
```
the attribute's value (formatted) is:
```json
[
{
"change_set":"830e96dc-fa07-40ce-8968-ea5c55ec4b84",
"changed_at":"2015-07-02T12:52:43.874Z",
"type":"replace_tag_name",
"old":"div",
"new":"span"
},
{
"change_set":"830e96dc-fa07-40ce-8968-ea5c55ec4b84",
"changed_at":"2015-07-02T12:52:43.874Z",
"type":"add_css_class",
"class":"hey"
}
]
```
it has a `change_set` with the ID of the change set, `changed_at` with the moment it was applied, and the rest define the change.
## Selecting the Node Set
we use Nokogiri's selections.
### using css
```ruby
change_set = surgeon.css('div.to-be-changed')
```
### using xpath
```ruby
change_set = surgeon.xpath("span") # note that we use Nokogiri's HTML Fragment and the use of self is special.
```
### Refining the selection
we can skip some nodes based on callbacks added to the Change Set using `select` and `reject` methods.
```ruby
change_set = surgeon.css('.to-be-changed')
change_set.reject { |node| node.name == 'div' }.select { |node| node.get_attribute('class').to_s.split(' ').include? 'yeah-do-it' }
change_set.run # => nodes skipped if reject callback return truthy or if select callback return falsey
```
## Available Changes
### Replace Tag Name
```ruby
surgeon.css('div.to-be-changed').replace_tag_name('article')
```
### Add CSS Class
```ruby
surgeon.css('div.to-be-changed').add_css_class('applied-some-stuff')
```
### Remove Attribute
```ruby
surgeon.css('div.to-be-changed').remove_attribute('style')
```
## Rollback
the surgeon can be used to revert any audited rollback. We can select what changes to rollback based on:
- `change_set`: The change_set UUID
- `changed_at`: The change timestamp
- `changed_from`: All changes which timestamp is more recent than the given time
We can also revert all audited changes.
```ruby
surgeon = HtmlSurgeon.for(GIVEN_HTML)
surgeon.rollback # => Integer with number of changes reverted
surgeon.html # => returns the html with all events reverted
surgeon.rollback(change_set: uuid) # => Integer with number of changes reverted
surgeon.html # => returns the html with only the given change set reverted
surgeon.rollback(changed_at: changed_at) # => Integer with number of changes reverted
surgeon.html # => returns the html with only the change set with timestamp reverted
surgeon.rollback(changed_from: changed_from) # => Integer with number of changes reverted
surgeon.html # => returns the html with any change sets with a timestamp more recent than `changed_from` reverted
```
## Clear Audit trail
we can clear all audit from the given html with the `clear_audit` method.
```ruby
surgeon = HtmlSurgeon.for(GIVEN_HTML)
surgeon.clear_audit # => returns an Integer with the number of changes
surgeon.html # => returns the html with all audit html attributes removed
```
## Helper Methods
### `HtmlSurgeon.node_has_css_class?(nokogiri_node, css_class)`
it will return true if the given nokogiri node has that css_class
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'html_surgeon'
```
And then execute:
$ bundle
Or install it yourself as:
$ gem install html_surgeon
## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/eturino/html_surgeon.
## CHANGESET
### v0.7.0
- added `remove_attribute` change.
- changes will be skipped if not needed on that node (will not do anything, nor be added to the audit).
### v0.6.0
- *WARNING: BREAKING API CHANGE*: now `rollback` and `clear_audit` return the number of changes performed
### v0.5.2
- added `changed_nodes` and `changed_nodes_size` to Change Set
### v0.5.1
- works with `nil` html, performs a `to_s` to the given html on initialization.
### v0.5.0
- added `node_has_css_class?` helper method to `HtmlSurgeon`
- added `clear_audit` to surgeon
### v0.4.0
- added `select` and `reject` callbacks to Change Set, based on blocks with `node` as single argument
### v0.3.0
- added fluid ChangeSet ID setter
- added change_set xpath support
### v0.2.0
- added rollback support