hexp 0.0.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +12 -3
- data/Changelog.md +9 -0
- data/Gemfile +3 -5
- data/Gemfile.devtools +20 -18
- data/Gemfile.lock +97 -84
- data/Rakefile +16 -0
- data/config/flay.yml +2 -2
- data/config/flog.yml +1 -1
- data/config/reek.yml +42 -18
- data/config/rubocop.yml +31 -0
- data/config/yardstick.yml +39 -1
- data/examples/from_nokogiri.rb +77 -0
- data/examples/selector_rewriter_chaining.rb +14 -0
- data/examples/todo.rb +138 -0
- data/examples/widget.rb +64 -0
- data/hexp.gemspec +8 -3
- data/lib/hexp.rb +103 -2
- data/lib/hexp/builder.rb +256 -0
- data/lib/hexp/css_selector.rb +205 -0
- data/lib/hexp/css_selector/parser.rb +74 -0
- data/lib/hexp/css_selector/sass_parser.rb +22 -0
- data/lib/hexp/dom.rb +0 -2
- data/lib/hexp/dsl.rb +27 -0
- data/lib/hexp/errors.rb +21 -0
- data/lib/hexp/h.rb +5 -2
- data/lib/hexp/list.rb +67 -9
- data/lib/hexp/node.rb +197 -41
- data/lib/hexp/node/attributes.rb +176 -0
- data/lib/hexp/node/children.rb +44 -0
- data/lib/hexp/node/css_selection.rb +73 -0
- data/lib/hexp/node/domize.rb +52 -6
- data/lib/hexp/node/normalize.rb +19 -9
- data/lib/hexp/node/pp.rb +32 -0
- data/lib/hexp/node/rewriter.rb +52 -0
- data/lib/hexp/node/selector.rb +59 -0
- data/lib/hexp/nokogiri/equality.rb +61 -0
- data/lib/hexp/nokogiri/reader.rb +27 -0
- data/lib/hexp/sass/selector_parser.rb +4 -0
- data/lib/hexp/text_node.rb +129 -9
- data/lib/hexp/version.rb +1 -1
- data/notes +34 -0
- data/spec/shared_helper.rb +6 -0
- data/spec/spec_helper.rb +2 -6
- data/spec/unit/hexp/builder_spec.rb +101 -0
- data/spec/unit/hexp/css_selector/attribute_spec.rb +137 -0
- data/spec/unit/hexp/css_selector/class_spec.rb +15 -0
- data/spec/unit/hexp/css_selector/comma_sequence_spec.rb +20 -0
- data/spec/unit/hexp/css_selector/element_spec.rb +11 -0
- data/spec/unit/hexp/css_selector/parser_spec.rb +51 -0
- data/spec/unit/hexp/css_selector/simple_sequence_spec.rb +48 -0
- data/spec/unit/hexp/dsl_spec.rb +55 -0
- data/spec/unit/hexp/h_spec.rb +38 -0
- data/spec/unit/hexp/list_spec.rb +19 -0
- data/spec/unit/hexp/node/attr_spec.rb +55 -0
- data/spec/unit/hexp/node/attributes_spec.rb +125 -0
- data/spec/unit/hexp/node/children_spec.rb +33 -0
- data/spec/unit/hexp/node/class_spec.rb +37 -0
- data/spec/unit/hexp/node/css_selection_spec.rb +86 -0
- data/spec/unit/hexp/node/normalize_spec.rb +12 -6
- data/spec/unit/hexp/node/rewrite_spec.rb +67 -30
- data/spec/unit/hexp/node/selector_spec.rb +78 -0
- data/spec/unit/hexp/node/text_spec.rb +7 -0
- data/spec/unit/hexp/node/to_dom_spec.rb +1 -1
- data/spec/unit/hexp/nokogiri/reader_spec.rb +8 -0
- data/spec/unit/hexp/parse_spec.rb +23 -0
- data/spec/unit/hexp/text_node_spec.rb +25 -0
- data/spec/unit/hexp_spec.rb +33 -0
- metadata +129 -16
- 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
|
data/lib/hexp/text_node.rb
CHANGED
@@ -1,26 +1,146 @@
|
|
1
1
|
module Hexp
|
2
|
-
# Represents text inside HTML
|
3
|
-
#
|
4
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
22
|
-
|
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
|
data/lib/hexp/version.rb
CHANGED
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.
|