html_surgeon 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 184ce38f2ca801e35b52003637cdba997487557c
4
- data.tar.gz: 23741ef09a0cf4e12150171fcbb93417c86ceef2
3
+ metadata.gz: 94d817ba0e3871c050f1d56fd80034cfc389562c
4
+ data.tar.gz: aca28c4bcc429fd9e7ea1294c3f81896f641cda2
5
5
  SHA512:
6
- metadata.gz: e567d65954530da7c2f33c562f0890813280b7274d68978d6419552aa933fe49e4cac1d5e5110ac454c99c1393381fade85b73d374750e96e92ebc96f7d8cc38
7
- data.tar.gz: a25d072d6c15964317a440e5857d24f60ced9ce620b87acfd5f5f48af3b82c507daa8a4edb3c2382fb8078669a6123e2c1d608d3ff34537386ff80ee0a311a25
6
+ metadata.gz: ebe07e4c500cdaa9b95b852abb1e3b894eb3805c6ebfa9fac34fa0a56d34f3488700a025ac3f0cb1a5df1d6f194634b9724afea8c9769a09428148ea7219e8e1
7
+ data.tar.gz: c348f793ae6186dceb2f00db9269b45a0477d1e816b2084b672eb431b09d6d8e8a7d3147cff48ecb34bfc55ce35f12ec854ee6d28756eafb8f4bb068bcb3035e
data/README.md CHANGED
@@ -110,7 +110,7 @@ If we have enabled audit, we'll get the changes applied to an element in an data
110
110
  It will store, in JSON, an array with all the changes.
111
111
 
112
112
  ```ruby
113
- surgeon = HtmlService.for(GIVEN_HTML)
113
+ surgeon = HtmlService.for(GIVEN_HTML, audit: true)
114
114
  surgeon.css('.lol').replace_tag_name('span').add_css_class('hey').run
115
115
  surgeon.html # =>
116
116
  # <div>
@@ -123,7 +123,7 @@ surgeon.html # =>
123
123
  # <li>2</li>
124
124
  # </ul>
125
125
  # </div>
126
- # </div>#
126
+ # </div>
127
127
  ```
128
128
 
129
129
  the attribute's value (formatted) is:
@@ -178,7 +178,24 @@ surgeon.css('div.to-be-changed').replace_name_tag('article')
178
178
  surgeon.css('div.to-be-changed').add_css_class('applied-some-stuff')
179
179
  ```
180
180
 
181
+ ## Rollback
182
+
183
+ the surgen can be used to revert any audited rollback. We can select what changes to rollback based on:
181
184
 
185
+ - `change_set`: The change_set UUID
186
+ - `changed_at`: The change timestamp
187
+ - `changed_from`: All changes which timestamp is more recent than the given time
188
+
189
+ We can also revert all audited changes.
190
+
191
+ ```ruby
192
+ surgeon = HtmlSurgeon.for(GIVEN_HTML)
193
+
194
+ surgeon.rollback.html # => returns the html with all events reverted
195
+ surgeon.rollback(change_set: uuid).html # => returns the html with only the given change set reverted
196
+ surgeon.rollback(changed_at: changed_at).html # => returns the html with only the change set with timestamp reverted
197
+ surgeon.rollback(changed_from: changed_from).html # => returns the html with any change sets with a timestamp more recent than `changed_from` reverted
198
+ ```
182
199
 
183
200
  ## Installation
184
201
 
@@ -207,3 +224,8 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
207
224
 
208
225
  Bug reports and pull requests are welcome on GitHub at https://github.com/eturino/html_surgeon.
209
226
 
227
+
228
+ ## CHANGESET
229
+
230
+ ### v 0.2.0
231
+ - added `rollback support
data/html_surgeon.gemspec CHANGED
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_dependency 'activesupport'
31
31
  spec.add_dependency 'oj'
32
32
  spec.add_dependency 'oj_mimic_json'
33
+ spec.add_dependency 'naught'
33
34
  spec.add_development_dependency 'bundler', '~> 1.10'
34
35
  spec.add_development_dependency 'rake', '~> 10.0'
35
36
  spec.add_development_dependency 'rspec'
@@ -0,0 +1,49 @@
1
+ module HtmlSurgeon
2
+ class Auditor
3
+ attr_reader :node
4
+
5
+ def initialize(node)
6
+ @node = node
7
+ end
8
+
9
+ def add_change(change_definition)
10
+ changes << change_definition
11
+ end
12
+
13
+ def apply(change_list = nil)
14
+ change_list ||= changes
15
+ if change_list.empty?
16
+ node.xpath(".//@#{DATA_CHANGE_AUDIT_ATTRIBUTE}").remove
17
+ else
18
+ node[DATA_CHANGE_AUDIT_ATTRIBUTE] = Oj.dump change_list
19
+ end
20
+ end
21
+
22
+ def changes
23
+ @changes ||= load_changes_from_node
24
+ end
25
+
26
+ private
27
+
28
+ def load_changes_from_node
29
+ current = node[DATA_CHANGE_AUDIT_ATTRIBUTE].presence
30
+ return [] unless current
31
+
32
+ Oj.load current, symbol_keys: true
33
+ end
34
+
35
+ NullAuditor = Naught.build do
36
+ def initialize(*)
37
+ super
38
+ end
39
+
40
+ def add_change(change_definition)
41
+ false
42
+ end
43
+
44
+ def apply
45
+ false
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,50 +1,37 @@
1
1
  module HtmlSurgeon
2
2
 
3
3
  class Change
4
- DATA_CHANGE_AUDIT_ATTRIBUTE = 'data-surgeon-audit'.freeze
4
+ def self.inherited(klass)
5
+ Changes.add_change_class(klass)
6
+ end
5
7
 
6
8
  attr_reader :change_set
9
+ delegate :audit?, to: :change_set
10
+ delegate :uuid, :run_time, to: :change_set, prefix: true
7
11
 
8
12
  def initialize(change_set:)
9
13
  @change_set = change_set
10
14
  end
11
15
 
12
- delegate :audit?, to: :change_set
13
- delegate :uuid, :run_time, to: :change_set, prefix: true
16
+ def apply(node)
17
+ auditor = auditor_class.new(node)
14
18
 
15
- def apply(element)
16
- prepare_audit_change(element) if audit?
17
-
18
- apply_in element
19
-
20
- apply_audit_change(element) if audit?
19
+ auditor.add_change(audit_data(node))
20
+ apply_in node
21
+ auditor.apply
21
22
 
22
23
  self
23
24
  end
24
25
 
26
+ def auditor_class
27
+ audit? ? Auditor : Auditor::NullAuditor
28
+ end
29
+
25
30
  def log
26
31
  raise AbstractMethodError, "a lazy developer has not implemented this method in #{self.class}"
27
32
  end
28
33
 
29
34
  private
30
-
31
- def prepare_audit_change(element)
32
- @audit_data = audit_data(element)
33
- end
34
-
35
- def apply_audit_change(element)
36
- current = current_audit_data(element)
37
- current << @audit_data
38
- element[DATA_CHANGE_AUDIT_ATTRIBUTE] = Oj.dump current
39
- end
40
-
41
- def current_audit_data(element)
42
- current = element[DATA_CHANGE_AUDIT_ATTRIBUTE].presence
43
- return [] unless current
44
-
45
- Oj.load current
46
- end
47
-
48
35
  def basic_audit_data
49
36
  {
50
37
  change_set: change_set_uuid,
@@ -52,11 +39,11 @@ module HtmlSurgeon
52
39
  }
53
40
  end
54
41
 
55
- def audit_data(element)
42
+ def audit_data(node)
56
43
  raise AbstractMethodError, "a lazy developer has not implemented this method in #{self.class}"
57
44
  end
58
45
 
59
- def apply_in(_element)
46
+ def apply_in(_node)
60
47
  raise AbstractMethodError, "a lazy developer has not implemented this method in #{self.class}"
61
48
  end
62
49
 
@@ -3,12 +3,24 @@ module HtmlSurgeon
3
3
  class ChangeSet
4
4
  attr_reader :node_set, :base, :change_list, :uuid, :run_time
5
5
 
6
+ def self.create(node_set, base)
7
+ new_class.new node_set, base
8
+ end
9
+
10
+ def self.new_class
11
+ Class.new(ChangeSet) do
12
+ Changes.change_classes.each do |klass|
13
+ include klass::ChangeSetMethods
14
+ end
15
+ end
16
+ end
17
+
6
18
  def initialize(node_set, base)
7
19
  @node_set = node_set
8
20
  @base = base
9
21
  @change_list = []
10
- @uuid = SecureRandom.uuid
11
- @run_time = nil
22
+ @uuid = SecureRandom.uuid
23
+ @run_time = nil
12
24
  end
13
25
 
14
26
  delegate :audit?, :html, to: :base
@@ -18,8 +30,8 @@ module HtmlSurgeon
18
30
  def run
19
31
  @run_time = Time.now.utc
20
32
 
21
- node_set.each do |element|
22
- apply_on_element(element)
33
+ node_set.each do |node|
34
+ apply_on_node(node)
23
35
  end
24
36
 
25
37
  self
@@ -31,21 +43,16 @@ module HtmlSurgeon
31
43
 
32
44
  # CHANGES
33
45
 
34
- def replace_tag_name(new_tag_name)
35
- change_list << Changes::ReplaceTagName.new(change_set: self, new_tag_name: new_tag_name)
36
- self
37
- end
46
+ private
38
47
 
39
- def add_css_class(css_class)
40
- change_list << Changes::AddCssClass.new(change_set: self, css_class: css_class)
48
+ def chain_add_change(change)
49
+ change_list << change
41
50
  self
42
51
  end
43
52
 
44
- private
45
-
46
- def apply_on_element(element)
53
+ def apply_on_node(node)
47
54
  change_list.each do |change|
48
- change.apply(element)
55
+ change.apply(node)
49
56
  end
50
57
  end
51
58
  end
@@ -1,9 +1,13 @@
1
1
  module HtmlSurgeon
2
2
  module Changes
3
3
  class AddCssClass < Change
4
- attr_reader :css_class
4
+ AUDIT_TYPE = :add_css_class
5
+
5
6
  CLASS_ATTRIBUTE = 'class'.freeze
6
7
  CLASS_SEPARATOR = ' '.freeze
8
+ CLASS_XPATH = './/@class'
9
+
10
+ attr_reader :css_class
7
11
 
8
12
  def initialize(css_class:, **other)
9
13
  @css_class = css_class
@@ -16,24 +20,53 @@ module HtmlSurgeon
16
20
  end
17
21
 
18
22
  private
19
- def apply_in(element)
20
- classes = element_classes element
21
- classes << css_class
22
- element.set_attribute(CLASS_ATTRIBUTE, classes.join(CLASS_SEPARATOR))
23
+ def apply_in(node)
24
+ self.class.add_class_to_node css_class, node
23
25
  end
24
26
 
25
- def audit_data(element)
26
- basic_audit_data.merge type: :add_css_class,
27
- existed_before: had_class?(element),
27
+ def audit_data(node)
28
+ basic_audit_data.merge type: AUDIT_TYPE,
29
+ existed_before: self.class.had_class?(node, css_class),
28
30
  class: css_class
29
31
  end
30
32
 
31
- def element_classes(element)
32
- element.get_attribute(CLASS_ATTRIBUTE).to_s.split(CLASS_SEPARATOR)
33
+ def self.add_class_to_node(css_class, node)
34
+ classes = node_classes node
35
+ classes << css_class unless classes.include? css_class
36
+ set_classes_in_node(classes, node)
37
+ end
38
+
39
+ def self.remove_class_to_node(css_class, node)
40
+ classes = node_classes node
41
+ classes.delete css_class
42
+ if classes.empty?
43
+ node.xpath(CLASS_XPATH).remove
44
+ else
45
+ set_classes_in_node(classes, node)
46
+ end
47
+ end
48
+
49
+ def self.set_classes_in_node(classes, node)
50
+ node.set_attribute(CLASS_ATTRIBUTE, classes.join(CLASS_SEPARATOR))
51
+ end
52
+
53
+ def self.node_classes(node)
54
+ node.get_attribute(CLASS_ATTRIBUTE).to_s.split(CLASS_SEPARATOR)
55
+ end
56
+
57
+ def self.had_class?(node, css_class)
58
+ node_classes(node).include? css_class
59
+ end
60
+
61
+ def self.revert(node, change_definition)
62
+ return if change_definition[:existed_before]
63
+ remove_class_to_node(change_definition[:class], node)
33
64
  end
34
65
 
35
- def had_class?(element)
36
- element_classes(element).include? css_class
66
+ module ChangeSetMethods
67
+ def add_css_class(css_class)
68
+ chain_add_change Changes::AddCssClass.new(change_set: self, css_class: css_class)
69
+ end
37
70
  end
38
71
  end
39
72
  end
@@ -1,6 +1,8 @@
1
1
  module HtmlSurgeon
2
2
  module Changes
3
3
  class ReplaceTagName < Change
4
+ AUDIT_TYPE = :replace_tag_name
5
+
4
6
  attr_reader :new_tag_name
5
7
 
6
8
  def initialize(new_tag_name:, **other)
@@ -14,15 +16,25 @@ module HtmlSurgeon
14
16
  end
15
17
 
16
18
  private
17
- def apply_in(element)
18
- element.name = new_tag_name
19
+ def apply_in(node)
20
+ node.name = new_tag_name
19
21
  end
20
22
 
21
- def audit_data(element)
22
- basic_audit_data.merge type: :replace_tag_name,
23
- old: element.name,
23
+ def audit_data(node)
24
+ basic_audit_data.merge type: AUDIT_TYPE,
25
+ old: node.name,
24
26
  new: new_tag_name
25
27
  end
28
+
29
+ def self.revert(node, change_definition)
30
+ node.name = change_definition[:old]
31
+ end
32
+
33
+ module ChangeSetMethods
34
+ def replace_tag_name(new_tag_name)
35
+ chain_add_change Changes::ReplaceTagName.new(change_set: self, new_tag_name: new_tag_name)
36
+ end
37
+ end
26
38
  end
27
39
  end
28
40
  end
@@ -0,0 +1,16 @@
1
+ module HtmlSurgeon
2
+ module Changes
3
+ def self.change_classes
4
+ @change_classes ||= []
5
+ end
6
+
7
+ def self.add_change_class(klass)
8
+ change_classes << klass
9
+ end
10
+
11
+ def self.change_class_by_type(type)
12
+ type = type.to_s
13
+ change_classes.find { |klass| klass::AUDIT_TYPE.to_s == type }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,79 @@
1
+ module HtmlSurgeon
2
+ class NodeReverser
3
+ attr_reader :node, :change_set, :changed_at, :changed_from
4
+
5
+ def initialize(node:, change_set: nil, changed_at: nil, changed_from: nil)
6
+ @node = node
7
+ @change_set = change_set
8
+ @changed_at = changed_at
9
+ @changed_from = changed_from
10
+ end
11
+
12
+ def call
13
+ changes_to_revert.each do |change_definition|
14
+ revert_change change_definition
15
+ end
16
+
17
+ write_remaining_changes
18
+ end
19
+
20
+ private
21
+
22
+ def revert_change(change_definition)
23
+ klass = Changes.change_class_by_type change_definition[:type]
24
+ klass.revert(node, change_definition)
25
+ end
26
+
27
+ def write_remaining_changes
28
+ auditor.apply remaining_changes
29
+ end
30
+
31
+ def changes
32
+ auditor.changes
33
+ end
34
+
35
+ def remaining_changes
36
+ changes - changes_to_revert
37
+ end
38
+
39
+ def changes_to_revert
40
+ @changes_to_revert ||= load_changes_to_revert
41
+ end
42
+
43
+ def auditor
44
+ @auditor ||= Auditor.new node
45
+ end
46
+
47
+ def load_changes_to_revert
48
+ changes.dup.tap do |rev_changes|
49
+ remove_by_change_set(rev_changes)
50
+ remove_by_changed_at(rev_changes)
51
+ remove_by_changed_from(rev_changes)
52
+ end
53
+ end
54
+
55
+ def remove_by_changed_from(rev_changes)
56
+ return unless changed_from.present?
57
+
58
+ changed_from_time = changed_from.to_time.utc
59
+ rev_changes.reject! do |change|
60
+ change[:changed_at].to_time.utc < changed_from_time
61
+ end
62
+ end
63
+
64
+ def remove_by_changed_at(rev_changes)
65
+ return unless changed_at.present?
66
+
67
+ changed_at_time = changed_at.to_time.utc
68
+ rev_changes.reject! do |change|
69
+ change[:changed_at].to_time.utc != changed_at_time
70
+ end
71
+ end
72
+
73
+ def remove_by_change_set(rev_changes)
74
+ return unless change_set.present?
75
+
76
+ rev_changes.reject! { |change| change[:change_set] != change_set }
77
+ end
78
+ end
79
+ end
@@ -18,7 +18,15 @@ module HtmlSurgeon
18
18
 
19
19
  def css(css_selector)
20
20
  node_set = doc.css(css_selector)
21
- ChangeSet.new(node_set, self)
21
+ ChangeSet.create(node_set, self)
22
+ end
23
+
24
+ def rollback(change_set: nil, changed_at: nil, changed_from: nil)
25
+ doc.css("[#{DATA_CHANGE_AUDIT_ATTRIBUTE}]").each do |node|
26
+ NodeReverser.new(node: node, change_set: change_set, changed_at: changed_at, changed_from: changed_from).call
27
+ end
28
+
29
+ self
22
30
  end
23
31
 
24
32
  private
@@ -1,3 +1,3 @@
1
1
  module HtmlSurgeon
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/html_surgeon.rb CHANGED
@@ -1,15 +1,22 @@
1
+ require 'naught'
2
+ require 'oj'
1
3
  require 'oj_mimic_json'
2
4
  require 'nokogiri'
3
5
  require 'active_support/all'
4
6
  require 'html_surgeon/version'
5
7
  require 'html_surgeon/abstract_method_error'
8
+ require 'html_surgeon/auditor'
9
+ require 'html_surgeon/node_reverser'
6
10
  require 'html_surgeon/service'
7
11
  require 'html_surgeon/change_set'
8
12
  require 'html_surgeon/change'
13
+ require 'html_surgeon/changes'
9
14
  require 'html_surgeon/changes/add_css_class'
10
15
  require 'html_surgeon/changes/replace_tag_name'
11
16
 
12
17
  module HtmlSurgeon
18
+ DATA_CHANGE_AUDIT_ATTRIBUTE = 'data-surgeon-audit'.freeze
19
+
13
20
  def self.for(html_string, **options)
14
21
  Service.new html_string, **options
15
22
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: html_surgeon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eduardo Turiño
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: naught
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: bundler
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -126,10 +140,13 @@ files:
126
140
  - html_surgeon.gemspec
127
141
  - lib/html_surgeon.rb
128
142
  - lib/html_surgeon/abstract_method_error.rb
143
+ - lib/html_surgeon/auditor.rb
129
144
  - lib/html_surgeon/change.rb
130
145
  - lib/html_surgeon/change_set.rb
146
+ - lib/html_surgeon/changes.rb
131
147
  - lib/html_surgeon/changes/add_css_class.rb
132
148
  - lib/html_surgeon/changes/replace_tag_name.rb
149
+ - lib/html_surgeon/node_reverser.rb
133
150
  - lib/html_surgeon/service.rb
134
151
  - lib/html_surgeon/version.rb
135
152
  homepage: https://github.com/eturino/html_surgeon