hexp 0.0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/.travis.yml +12 -3
  2. data/Changelog.md +9 -0
  3. data/Gemfile +3 -5
  4. data/Gemfile.devtools +20 -18
  5. data/Gemfile.lock +97 -84
  6. data/Rakefile +16 -0
  7. data/config/flay.yml +2 -2
  8. data/config/flog.yml +1 -1
  9. data/config/reek.yml +42 -18
  10. data/config/rubocop.yml +31 -0
  11. data/config/yardstick.yml +39 -1
  12. data/examples/from_nokogiri.rb +77 -0
  13. data/examples/selector_rewriter_chaining.rb +14 -0
  14. data/examples/todo.rb +138 -0
  15. data/examples/widget.rb +64 -0
  16. data/hexp.gemspec +8 -3
  17. data/lib/hexp.rb +103 -2
  18. data/lib/hexp/builder.rb +256 -0
  19. data/lib/hexp/css_selector.rb +205 -0
  20. data/lib/hexp/css_selector/parser.rb +74 -0
  21. data/lib/hexp/css_selector/sass_parser.rb +22 -0
  22. data/lib/hexp/dom.rb +0 -2
  23. data/lib/hexp/dsl.rb +27 -0
  24. data/lib/hexp/errors.rb +21 -0
  25. data/lib/hexp/h.rb +5 -2
  26. data/lib/hexp/list.rb +67 -9
  27. data/lib/hexp/node.rb +197 -41
  28. data/lib/hexp/node/attributes.rb +176 -0
  29. data/lib/hexp/node/children.rb +44 -0
  30. data/lib/hexp/node/css_selection.rb +73 -0
  31. data/lib/hexp/node/domize.rb +52 -6
  32. data/lib/hexp/node/normalize.rb +19 -9
  33. data/lib/hexp/node/pp.rb +32 -0
  34. data/lib/hexp/node/rewriter.rb +52 -0
  35. data/lib/hexp/node/selector.rb +59 -0
  36. data/lib/hexp/nokogiri/equality.rb +61 -0
  37. data/lib/hexp/nokogiri/reader.rb +27 -0
  38. data/lib/hexp/sass/selector_parser.rb +4 -0
  39. data/lib/hexp/text_node.rb +129 -9
  40. data/lib/hexp/version.rb +1 -1
  41. data/notes +34 -0
  42. data/spec/shared_helper.rb +6 -0
  43. data/spec/spec_helper.rb +2 -6
  44. data/spec/unit/hexp/builder_spec.rb +101 -0
  45. data/spec/unit/hexp/css_selector/attribute_spec.rb +137 -0
  46. data/spec/unit/hexp/css_selector/class_spec.rb +15 -0
  47. data/spec/unit/hexp/css_selector/comma_sequence_spec.rb +20 -0
  48. data/spec/unit/hexp/css_selector/element_spec.rb +11 -0
  49. data/spec/unit/hexp/css_selector/parser_spec.rb +51 -0
  50. data/spec/unit/hexp/css_selector/simple_sequence_spec.rb +48 -0
  51. data/spec/unit/hexp/dsl_spec.rb +55 -0
  52. data/spec/unit/hexp/h_spec.rb +38 -0
  53. data/spec/unit/hexp/list_spec.rb +19 -0
  54. data/spec/unit/hexp/node/attr_spec.rb +55 -0
  55. data/spec/unit/hexp/node/attributes_spec.rb +125 -0
  56. data/spec/unit/hexp/node/children_spec.rb +33 -0
  57. data/spec/unit/hexp/node/class_spec.rb +37 -0
  58. data/spec/unit/hexp/node/css_selection_spec.rb +86 -0
  59. data/spec/unit/hexp/node/normalize_spec.rb +12 -6
  60. data/spec/unit/hexp/node/rewrite_spec.rb +67 -30
  61. data/spec/unit/hexp/node/selector_spec.rb +78 -0
  62. data/spec/unit/hexp/node/text_spec.rb +7 -0
  63. data/spec/unit/hexp/node/to_dom_spec.rb +1 -1
  64. data/spec/unit/hexp/nokogiri/reader_spec.rb +8 -0
  65. data/spec/unit/hexp/parse_spec.rb +23 -0
  66. data/spec/unit/hexp/text_node_spec.rb +25 -0
  67. data/spec/unit/hexp_spec.rb +33 -0
  68. metadata +129 -16
  69. data/lib/hexp/format_error.rb +0 -8
@@ -0,0 +1,52 @@
1
+ module Hexp
2
+ class Node
3
+ # Create a new Hexp node based on an existing node
4
+ #
5
+ # Rewriting in this case means iterating over the whole Hexp tree, and for
6
+ # each element providing zero or more elements to replace it with.
7
+ #
8
+ class Rewriter
9
+ include Hexp
10
+
11
+ def initialize(node, block)
12
+ @node, @block = node, block
13
+ end
14
+
15
+ def to_hexp
16
+ @hexp ||= H[
17
+ @node.tag,
18
+ @node.attributes,
19
+ @block ? rewrite_children : @node.children
20
+ ]
21
+ end
22
+
23
+ private
24
+
25
+ # Helper for rewrite
26
+ #
27
+ # @param blk [Proc] the block for rewriting
28
+ # @return [Array<Hexp::Node>]
29
+ # @api private
30
+ #
31
+ def rewrite_children
32
+ @node.children
33
+ .flat_map {|child| child.rewrite &@block }
34
+ .flat_map {|child| coerce_rewrite_response(@block.(child.to_hexp, @node)) || [child] }
35
+ end
36
+
37
+ def coerce_rewrite_response(response)
38
+ return [] if response.nil?
39
+
40
+ return [response.to_hexp] if response.respond_to? :to_hexp
41
+ return [response.to_str] if response.respond_to? :to_str
42
+
43
+ if response.respond_to? :to_ary
44
+ return [response] if response.first.is_a? Symbol
45
+ return response.to_ary
46
+ end
47
+
48
+ raise FormatError, "invalid rewrite response : #{response.inspect}, expected #{self.class} or Array, got #{response.class}"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,59 @@
1
+ module Hexp
2
+ class Node
3
+ # Select nodes from a Hexp tree
4
+ #
5
+ # This is what is backing the {Hexp::Node#select} method. It serves a double
6
+ # purpose. At it's core it's an Enumerable for iterating over nodes that
7
+ # match a criterium.
8
+ #
9
+ # @example
10
+ # # Loop over the nodes with class="big"
11
+ # hexp.select {|el| el.class? 'big' }.each { ... }
12
+ #
13
+ # It also integrates with {Hexp::Node::Rewriter} for selective rewriting of
14
+ # a Hexp tree.
15
+ #
16
+ # @example
17
+ # # stick all links inside a <span class="link> ... </span>
18
+ # hexp.select {|el| el.tag == 'a' }.wrap(:span, class: 'link')
19
+ #
20
+ class Selector
21
+ include Enumerable
22
+
23
+ def initialize(node, block)
24
+ @node, @select_block = node, block
25
+ end
26
+
27
+ def rewrite(&block)
28
+ @node.rewrite do |node, parent|
29
+ if @select_block.(node)
30
+ block.(node, parent)
31
+ else
32
+ node
33
+ end
34
+ end
35
+ end
36
+
37
+ def attr(name, value)
38
+ rewrite do |node|
39
+ node.attr(name, value)
40
+ end
41
+ end
42
+
43
+ def wrap(tag, attributes = {})
44
+ rewrite do |node|
45
+ H[tag, attributes, [node]]
46
+ end
47
+ end
48
+
49
+ def each(&block)
50
+ return to_enum(:each) unless block_given?
51
+
52
+ @node.children.each do |child|
53
+ child.select(&@select_block).each(&block)
54
+ end
55
+ yield @node if @select_block.(@node)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -2,6 +2,7 @@ module Hexp
2
2
  module Nokogiri
3
3
  # Used in test to see if two Nokogiri objects have the same content,
4
4
  # i.e. are equivalent as far as we are concerned
5
+ #
5
6
  class Equality
6
7
  CLASSES = [
7
8
  ::Nokogiri::HTML::Document,
@@ -14,6 +15,22 @@ module Hexp
14
15
  ::Nokogiri::XML::DTD,
15
16
  ]
16
17
 
18
+ # Create a new equality tester for two Nokogiri objects
19
+ #
20
+ # (see Hexp::Nokogiri::Equality::CLASSES for all possible objects that can
21
+ # be passed in)
22
+ #
23
+ # @example
24
+ # doc = Nokogiri::HTML::Document.new
25
+ # this_node = Nokogiri::XML::Element.new("div", doc)
26
+ # that_node = Nokogiri::XML::Element.new("span", doc)
27
+ # Hexp::Nokogiri::Equality.new(this_node, that_node).call #=> false
28
+ #
29
+ # @param this [Object] The first object to compare
30
+ # @param that [Object] The second object to compare
31
+ #
32
+ # @api public
33
+ #
17
34
  def initialize(this, that)
18
35
  @this, @that = this, that
19
36
  [this, that].each do |input|
@@ -21,6 +38,12 @@ module Hexp
21
38
  end
22
39
  end
23
40
 
41
+ # Perform the comparison
42
+ #
43
+ # @return [Boolean]
44
+ #
45
+ # @api public
46
+ #
24
47
  def call
25
48
  [ equal_class?,
26
49
  equal_name?,
@@ -29,26 +52,56 @@ module Hexp
29
52
  equal_text? ].all?
30
53
  end
31
54
 
55
+ # Are the two elements instances of the same class
56
+ #
57
+ # @return [Boolean]
58
+ #
59
+ # @api public
60
+ #
32
61
  def equal_class?
33
62
  @this.class == @that.class
34
63
  end
35
64
 
65
+ # Do both elements have the same tag name
66
+ #
67
+ # @return [Boolean]
68
+ #
69
+ # @api public
70
+ #
36
71
  def equal_name?
37
72
  @this.name == @that.name
38
73
  end
39
74
 
75
+ # Do the elements under comparison have the same child elements
76
+ #
77
+ # @return [Boolean]
78
+ #
79
+ # @api public
80
+ #
40
81
  def equal_children?
41
82
  return true unless @this.respond_to? :children
42
83
  @this.children.count == @that.children.count &&
43
84
  compare_children.all?
44
85
  end
45
86
 
87
+ # Compare the child elements, assuming both elements respond_to? :children
88
+ #
89
+ # @return [Boolean]
90
+ #
91
+ # @api public
92
+ #
46
93
  def compare_children
47
94
  @this.children.map.with_index do |child, idx|
48
95
  self.class.new(child, @that.children[idx]).call
49
96
  end
50
97
  end
51
98
 
99
+ # Do the elements under comparison have the same attributes
100
+ #
101
+ # @return [Boolean]
102
+ #
103
+ # @api public
104
+ #
52
105
  def equal_attributes?
53
106
  return true unless @this.respond_to? :attributes
54
107
  @this.attributes.keys.all? do |key|
@@ -56,6 +109,14 @@ module Hexp
56
109
  end
57
110
  end
58
111
 
112
+ # Compare the text of text elements
113
+ #
114
+ # If the elements are not of type Nokogiri::XML::Text, return true
115
+ #
116
+ # @return [Boolean]
117
+ #
118
+ # @api public
119
+ #
59
120
  def equal_text?
60
121
  return true unless @this.instance_of?(::Nokogiri::XML::Text)
61
122
  @this.text == @that.text
@@ -0,0 +1,27 @@
1
+ module Hexp
2
+ module Nokogiri
3
+ # Read Nokogiri, turning it into Hexp
4
+ #
5
+ class Reader
6
+ # Take a Nokogiri root node and convert it to Hexp
7
+ #
8
+ # @param node [Nokogiri::XML::Element]
9
+ # @return [Hexp::Node]
10
+ # @api public
11
+ #
12
+ def call(node)
13
+ return node.text if node.text?
14
+
15
+ unless node.attributes.empty?
16
+ attrs = node.attributes.map do |key, value|
17
+ [key.to_sym, value.value]
18
+ end
19
+ attrs = Hash[attrs]
20
+ end
21
+
22
+ recurse = ->(node) { call(node) }
23
+ H[node.name.to_sym, attrs, node.children.map(&recurse)]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,4 @@
1
+ module Hexp
2
+ module Sass
3
+ end
4
+ end
@@ -1,26 +1,146 @@
1
1
  module Hexp
2
- # Represents text inside HTML, at the moment a wrapper
3
- # around a plain String. Needs work
4
- class TextNode < SimpleDelegator
2
+ # Represents text inside HTML. Instances behave like Strings, but also support
3
+ # the most of `Hexp::Node` interface, so you don't have to treat them differently
4
+ # when traversing across trees.
5
+ #
6
+ # Strings used inside Hexp literals like `H[:span, "Hi!"]` automatically get
7
+ # converted to `TextNode` instances, so there is usually no reason to instantiate
8
+ # these yourself.
9
+ class TextNode < DelegateClass(String)
10
+ # Inspect the TextNode
11
+ #
12
+ # This delegates to the underlying String, making it
13
+ # non-obvious that you're dealing with something else. However, a TextNode
14
+ # supports the full API of String, so this might not be a big problem.
15
+ # The benefit is that inspection of complete nodes containing text looks
16
+ # nice
17
+ #
18
+ # @example
19
+ # Hexp::TextNode.new("hello, world").inspect #=> "\"hello, world\""
20
+ #
21
+ # @return [String]
22
+ #
23
+ # @api public
24
+ #
5
25
  def inspect
6
26
  __getobj__.inspect
7
27
  end
8
28
 
9
- def tree_walk
10
- yield self
11
- end
12
-
29
+ # The attributes of this Node
30
+ #
31
+ # Text nodes can not have attributes, so this always returns an empty Hash.
32
+ #
33
+ # @return [Hash]
34
+ #
35
+ # @api public
36
+ #
13
37
  def attributes
14
38
  {}.freeze
15
39
  end
16
40
 
41
+ # Same as inspect, used by `Hexp::Node#pp`
42
+ #
43
+ # @example
44
+ # Hexp::TextNode.new("hello, world").pp #=> "\"hello, world\""
45
+ #
46
+ # @return [String]
47
+ #
48
+ # @api public
49
+ #
17
50
  def pp
18
51
  inspect
19
52
  end
20
53
 
21
- def to_a
22
- [:text, self, Hexp::List[]]
54
+ # The tag of this node
55
+ #
56
+ # A text node does not have a tag, so this returns nil
57
+ #
58
+ # @example
59
+ # Hexp::TextNode.new("hello, world").tag #=> nil
60
+ #
61
+ # @return [NilClass]
62
+ #
63
+ # @api public
64
+ #
65
+ def tag
66
+ end
67
+
68
+ # Standard conversion protocol, returns self
69
+ #
70
+ # @example
71
+ # Hexp::TextNode.new("hello, world").to_hexp #=> #<Hexp::TextNode "hello, world">
72
+ #
73
+ # @return [Hexp::TextNode]
74
+ #
75
+ # @api public
76
+ #
77
+ def to_hexp
78
+ self
23
79
  end
24
80
 
81
+ # Children of the node
82
+ #
83
+ # A text node has no children, this always returns an empty array.
84
+ #
85
+ # @example
86
+ # Hexp::TextNode.new("hello, world").children #=> []
87
+ #
88
+ # @return [Array]
89
+ #
90
+ # @api public
91
+ #
92
+ def children
93
+ [].freeze
94
+ end
95
+
96
+ # Is this a text node?
97
+ #
98
+ # @example
99
+ # Hexp::TextNode.new('foo').text? #=> true
100
+ # H[:p].text? #=> false
101
+ #
102
+ # @return [TrueClass]
103
+ #
104
+ # @api public
105
+ #
106
+ def text?
107
+ true
108
+ end
109
+
110
+ # Is a certain CSS class present on this node?
111
+ #
112
+ # Text nodes have no attributes, so this always returns false.
113
+ #
114
+ # @example
115
+ # Hexp::TextNode.new('foo').class?('bar') #=> false
116
+ #
117
+ # @return [FalseClass]
118
+ #
119
+ # @api public
120
+ #
121
+ def class?(klz)
122
+ false
123
+ end
124
+
125
+ # Rewrite a node
126
+ #
127
+ # See Hexp::Node#rewrite for more info. On a TextNode this simply return self.
128
+ #
129
+ # @example
130
+ # tree.rewrite do |node|
131
+ # H[:div, {class: 'wrap'}, node]
132
+ # end
133
+ #
134
+ # @return [Hexp::TextNode]
135
+ #
136
+ # @api public
137
+ #
138
+ def rewrite(&blk)
139
+ self
140
+ end
141
+
142
+ def select(&block)
143
+ Node::Selector.new(self, block)
144
+ end
25
145
  end
26
146
  end
@@ -1,3 +1,3 @@
1
1
  module Hexp
2
- VERSION = '0.0.1'
2
+ VERSION = '0.2.0'
3
3
  end
data/notes ADDED
@@ -0,0 +1,34 @@
1
+ http://begriffs.github.io/showpiece/
2
+
3
+ TODO
4
+ ====
5
+ * Rename Hexp::Node to Hexp::Element
6
+ * Rename Selector to Selection
7
+
8
+ Issues
9
+ ======
10
+
11
+ Root elements should not be treated separately
12
+
13
+ A `rewrite` operation currently yields all nodes except the root node. Rewrite allows you to replace a node with zero, one or more nodes. My thinking at the time was that I wanted to make sure a single Hexp was returned, so if one could rewrite the root node that would be problematic.
14
+
15
+ This however makes for a special case that is not very intuitive. When implementing `Hexp::Node::Selector` I came across this again. One can use a Selector strictly as an Enumerable
16
+
17
+ ```ruby
18
+ # Find the first child element of all elements with class="strong"
19
+ hexp.select {|el| el.class? 'strong' }.map {|el| el.children.first }
20
+ ```
21
+
22
+ In this case there is no reason why that shouldn't iterate the whole tree, including the node. Other operations on `Selector` however do a Rewrite of the selected elements, and in this case the "all-except-the-root-node" limitation applies.
23
+
24
+ ``` ruby
25
+ # Add class="strong" to all divs
26
+ H[:div, [
27
+ [:div, 'one'],
28
+ [:div, 'two']
29
+ ]
30
+ ].select{|node| node.tag==:div}.attr('class', 'strong').to_hexp
31
+ #=> H[:div, [H[:div, {"class"=>"strong"}, ["one"]], H[:div, {"class"=>"strong"}, ["two"]]]]
32
+ ```
33
+
34
+ The top-level node is unaffected. To remove this limitation `Rewriter` will have to return a `Hexp::List` when zero or multiple elements are returned. Hexp::Node and Hexp::List will also have to implement as much as possible the same interface, so they can be largely used intechangably. This should especially be possible for all select/rewrite operations.