hexp 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.