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.
- 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.
|