hexp 0.2.0 → 0.3.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.
@@ -10,35 +10,75 @@ module Hexp
10
10
  #
11
11
  # The classes that make up the parse tree largely mimic the ones from SASS,
12
12
  # like CommaSequence, SimpleSequence, Class, Id, etc. By having them in our
13
- # own namespace however we can easily add Hexp-specific helper functions.
13
+ # own namespace however we can easily add Hexp-specific functionality to them.
14
14
  #
15
15
  class Parser
16
+ # Initialize the parser with the selector to parse
17
+ #
18
+ # @param [String] selector
19
+ #
20
+ # @api private
16
21
  def initialize(selector)
17
22
  @selector = selector.freeze
18
23
  end
19
24
 
25
+ # Parse the selector
26
+ #
27
+ # @return [Hexp::CssSelector::CommaSequence]
28
+ #
29
+ # @api private
20
30
  def parse
21
31
  rewrite_comma_sequence(SassParser.call(@selector))
22
32
  end
23
33
 
34
+ # Parse a CSS selector in one go
35
+ #
36
+ # @param [String] selector
37
+ # @return [Hexp::CssSelector::CommaSequence]
38
+ #
39
+ # @api private
24
40
  def self.call(selector)
25
41
  new(selector).parse
26
42
  end
27
43
 
28
44
  private
29
45
 
46
+ # Map CommaSequence from the SASS namespace to our own
47
+ #
48
+ # @param [Sass::Selector::CommaSequence] comma_sequence
49
+ # @return [Hexp::CssSelector::CommaSequence]
50
+ #
51
+ # @api private
30
52
  def rewrite_comma_sequence(comma_sequence)
31
53
  CommaSequence.new(comma_sequence.members.map{|sequence| rewrite_sequence(sequence)})
32
54
  end
33
55
 
56
+ # Map Sequence from the SASS namespace to our own
57
+ #
58
+ # @param [Sass::Selector::Sequence] comma_sequence
59
+ # @return [Hexp::CssSelector::Sequence]
60
+ #
61
+ # @api private
34
62
  def rewrite_sequence(sequence)
35
63
  Sequence.new(sequence.members.map{|simple_sequence| rewrite_simple_sequence(simple_sequence)})
36
64
  end
37
65
 
66
+ # Map SimpleSequence from the SASS namespace to our own
67
+ #
68
+ # @param [Sass::Selector::SimpleSequence] comma_sequence
69
+ # @return [Hexp::CssSelector::SimpleSequence]
70
+ #
71
+ # @api private
38
72
  def rewrite_simple_sequence(simple_sequence)
39
73
  SimpleSequence.new(simple_sequence.members.map{|simple| rewrite_simple(simple)})
40
74
  end
41
75
 
76
+ # Map Simple from the SASS namespace to our own
77
+ #
78
+ # @param [Sass::Selector::Simple] comma_sequence
79
+ # @return [Hexp::CssSelector::Simple]
80
+ #
81
+ # @api private
42
82
  def rewrite_simple(simple)
43
83
  case simple
44
84
  when ::Sass::Selector::Element # span
@@ -50,7 +90,7 @@ module Hexp
50
90
  when ::Sass::Selector::Attribute # [href^="http://"]
51
91
  raise "CSS attribute selector flags are curently ignored by Hexp (not implemented)" unless simple.flags.nil?
52
92
  raise "CSS attribute namespaces are curently ignored by Hexp (not implemented)" unless simple.namespace.nil?
53
- raise "CSS attribute operator #{simple.operator} not understood by Hexp" unless %w[= ~= ^=].include?(simple.operator) || simple.operator.nil?
93
+ raise "CSS attribute operator #{simple.operator} not understood by Hexp" unless %w[= ~= ^= |= $= *=].include?(simple.operator) || simple.operator.nil?
54
94
  Attribute.new(
55
95
  simple.name.first,
56
96
  simple.namespace,
@@ -58,10 +98,13 @@ module Hexp
58
98
  simple.value ? simple.value.first : nil,
59
99
  simple.flags
60
100
  )
101
+ when ::Sass::Selector::Universal # *
102
+ Universal.new
61
103
  else
62
104
  raise "CSS selectors containing #{simple.class} are not implemented in Hexp"
63
105
  end
64
106
 
107
+ # As of yet unimplemented
65
108
  # when ::Sass::Selector::Universal # *
66
109
  # when ::Sass::Selector::Parent # & in Sass
67
110
  # when ::Sass::Selector::Interpolation # #{} in Sass
@@ -3,10 +3,20 @@ module Hexp
3
3
  # A CSS Parser that only knows how to parse CSS selectors
4
4
  #
5
5
  class SassParser < ::Sass::SCSS::CssParser
6
+ # Initialize the parser with the selector to parse
7
+ #
8
+ # @param [String] selector
9
+ #
10
+ # @api private
6
11
  def initialize(selector)
7
12
  super(selector, '')
8
13
  end
9
14
 
15
+ # Parse the selector
16
+ #
17
+ # @return [Sass::Selector::CommaSequence]
18
+ #
19
+ # @api private
10
20
  def parse
11
21
  init_scanner!
12
22
  result = selector_comma_sequence
@@ -14,6 +24,12 @@ module Hexp
14
24
  result
15
25
  end
16
26
 
27
+ # Parse a CSS selector in one go
28
+ #
29
+ # @param [String] selector
30
+ # @return [Sass::Selector::CommaSequence]
31
+ #
32
+ # @api private
17
33
  def self.call(selector)
18
34
  self.new(selector).parse
19
35
  end
data/lib/hexp/dom.rb CHANGED
@@ -2,6 +2,8 @@ module Hexp
2
2
  module DOM
3
3
  Document = Nokogiri::HTML::Document
4
4
  Node = Nokogiri::XML::Node
5
+ NodeSet = Nokogiri::XML::NodeSet
6
+ DTD = Nokogiri::XML::DTD
5
7
  Text = Nokogiri::XML::Text
6
8
  end
7
9
  end
data/lib/hexp/dsl.rb CHANGED
@@ -1,26 +1,25 @@
1
1
  module Hexp
2
+ # Make the Hexp::Node DSL available to objects that implement `to_hexp`
3
+ #
4
+ # Mixing in {Hexp} has the same effect as mixing in {Hexp::DSL}, and is the
5
+ # recommended way.
6
+ #
2
7
  module DSL
3
- [ :tag,
4
- :attributes,
5
- :children,
6
- :attr,
7
- :rewrite,
8
- :replace,
9
- :select,
10
- :to_html,
11
- :class?,
12
- :add_class,
13
- :add_child,
14
- :add,
15
- :<<,
16
- :process,
17
- :%,
18
- :text,
19
- :remove_attr,
20
- :set_attributes,
21
- ].each do |meth|
22
- define_method meth do |*args, &blk|
23
- to_hexp.public_send(meth, *args, &blk)
8
+ # The names of methods related to manipulating the list of children of a node
9
+ CHILDREN_METHODS = Hexp::Node::Children.public_instance_methods.freeze
10
+
11
+ # The names of methods related to a node's attributes
12
+ ATTRIBUTES_METHODS = Hexp::Node::Attributes.public_instance_methods.freeze
13
+
14
+ # Methods that are defined directly in {Hexp::Node}
15
+ NODE_METHODS = Hexp::Node.public_instance_methods(false) - [
16
+ :to_hexp,
17
+ :inspect
18
+ ]
19
+
20
+ [CHILDREN_METHODS, ATTRIBUTES_METHODS, NODE_METHODS].flatten.each do |method|
21
+ define_method method do |*args, &blk|
22
+ to_hexp.public_send(method, *args, &blk)
24
23
  end
25
24
  end
26
25
  end
data/lib/hexp/errors.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  module Hexp
2
+ # Base class for exceptions raised by Hexp
3
+ #
2
4
  Error = Class.new(StandardError)
3
5
 
4
6
  # Raised when trying to stick things inside a Hexp where they don't belong
@@ -7,7 +9,6 @@ module Hexp
7
9
  # Create a new FormatError
8
10
  #
9
11
  # @api private
10
- #
11
12
  def initialize(msg = 'You have illegal contents in your Hexp')
12
13
  super
13
14
  end
@@ -17,5 +18,4 @@ module Hexp
17
18
  #
18
19
  class ParseError < Error
19
20
  end
20
-
21
21
  end
data/lib/hexp/list.rb CHANGED
@@ -8,10 +8,10 @@ module Hexp
8
8
  # @example
9
9
  # Hexp::List.new([H[:p], H[:div]])
10
10
  #
11
- # @param nodes [#to_ary] List of nodes
11
+ # @param [#to_ary] nodes
12
+ # List of nodes
12
13
  #
13
14
  # @api public
14
- #
15
15
  def initialize(nodes)
16
16
  super nodes.to_ary.freeze
17
17
  end
@@ -24,11 +24,12 @@ module Hexp
24
24
  # Hexp::Node[:hr],
25
25
  # ]
26
26
  #
27
- # @param args [Array] individual nodes
27
+ # @param [Array] args
28
+ # individual nodes
28
29
  #
29
30
  # @return [Hexp::List]
30
- # @api public
31
31
  #
32
+ # @api public
32
33
  def self.[](*args)
33
34
  new(args)
34
35
  end
@@ -39,21 +40,21 @@ module Hexp
39
40
  # that this is a wrapping class. This is convenient when inspecting nested
40
41
  # hexps, but probably something we want to solve differently.
41
42
  #
42
- # @api private
43
- # @return string
43
+ # @return [String]
44
44
  #
45
+ # @api private
45
46
  def inspect
46
47
  __getobj__.inspect
47
48
  end
48
49
 
49
- # Internal coercion to Array
50
+ # Implicit conversion to Array
50
51
  #
51
52
  # @example
52
53
  # Hexp::List[ H[:p], H[:span] ].to_ary #=> [H[:p], H[:span]]
53
54
  #
54
55
  # @return [Array<Hexp::Node>]
55
- # @api public
56
56
  #
57
+ # @api public
57
58
  def to_ary
58
59
  __getobj__
59
60
  end
@@ -72,10 +73,12 @@ module Hexp
72
73
  # H[:div, [[:span]]].children.eql? [H[:span]] #=> false
73
74
  # H[:div, [[:span]]].children.eql? Hexp::List[H[:span]] #=> true
74
75
  #
75
- # @param other [Object] Object to compare with
76
- # @api public
77
- # @return [Boolean]
76
+ # @param [Object] other
77
+ # Object to compare with
78
78
  #
79
+ # @return [true,false]
80
+ #
81
+ # @api public
79
82
  def eql?(other)
80
83
  self == other && self.class == other.class
81
84
  end
data/lib/hexp/node.rb CHANGED
@@ -1,5 +1,56 @@
1
1
  module Hexp
2
- # A Hexp Node, or simply 'a hexp'
2
+ # A +Hexp::Node+ represents a single element in a HTML syntax tree. It
3
+ # consists of three parts : the {#tag}, the {#attributes} and the {#children}.
4
+ #
5
+ # Instances of +Hexp::Node+ are immutable. Because of this all methods that
6
+ # "alter" the node actually return a new instance, leaving the old one
7
+ # untouched.
8
+ #
9
+ # @example Immutable nodes : the old one is untouched
10
+ # node = Hexp::Node.new(:div)
11
+ # node2 = node.add_class('items')
12
+ # p node # => H[:div]
13
+ # p node2 # => H[:div, 'class' => 'items']
14
+ #
15
+ # The +Hexp::Node+ constructor takes a +Symbol+, a +Hash+ and an +Array+ for
16
+ # the {#tag}, {#attributes} and {#children} respectively. However the
17
+ # attributes and children can each be ommitted if they are empty.
18
+ #
19
+ # If the node contains a single child node, then it is not necessary to wrap
20
+ # that child node in an array. Just put the single node in place of the list
21
+ # of children.
22
+ #
23
+ # One can use +H[tag, attrs, children]+ as a
24
+ # shorthand syntax. Finally one can use {Hexp.build Hexp.build} to construct nodes using
25
+ # Ruby blocks, not unlike the Builder or Nokogiri gems.
26
+ #
27
+ # @example Creating Hexp : syntax alternatives and optional parameters
28
+ # Hexp::Node.new(:div, class: 'foo')
29
+ # Hexp::Node.new(:div, {class: 'foo'}, "A text node")
30
+ # Hexp::Node.new(:div, {class: 'foo'}, ["A text node"])
31
+ # H[:div, {class: 'foo'}, H[:span, {class: 'big'}, "good stuff"]]
32
+ # H[:div, {class: 'foo'}, [
33
+ # H[:span, {class: 'big'}, "good stuff"],
34
+ # H[:a, {href: '/index'}, "go home"]
35
+ # ]
36
+ # ]
37
+ # Hexp.build { div.strong { "Hello, world!" } }
38
+ #
39
+ # Methods that read or alter the attributes Hash are defined in
40
+ # {Hexp::Node::Attributes}. Methods that read or alter the list of child nodes
41
+ # are defined in {Hexp::Node::Children}
42
+ #
43
+ # == CSS selectors
44
+ #
45
+ # When working with large trees of {Hexp::Node} objects, it is convenient to
46
+ # be able to target specific nodes using CSS selector syntax. Hexp supports a
47
+ # subset of the CSS 3 selector syntax, see {Hexp::Node::CssSelection} for info
48
+ # on the supported syntax.
49
+ #
50
+ # For changing, replacing or removing specific nodes based on a CSS selector
51
+ # string, see {Hexp::Node#replace}. To iterate over nodes, see
52
+ # {Hexp::Node#select}.
53
+ #
3
54
  class Node
4
55
  include Equalizer.new(:tag, :attributes, :children)
5
56
  extend Forwardable
@@ -149,66 +200,66 @@ module Hexp
149
200
  false
150
201
  end
151
202
 
203
+ # Return a new node, with a different tag
204
+ #
205
+ # @example
206
+ # H[:div, class: 'foo'].set_tag(:bar)
207
+ # # => H[:bar, class: 'foo']
208
+ #
209
+ # @param tag [#to_sym] The new tag
210
+ # @return [Hexp::Node]
211
+ #
212
+ # @api public
152
213
  def set_tag(tag)
153
214
  H[tag.to_sym, attributes, children]
154
215
  end
155
216
 
156
- # Rewrite a node tree
157
- #
158
- # Since nodes are immutable, this is the main entry point for deriving nodes
159
- # from others.
217
+ # Replace nodes in a tree
160
218
  #
161
- # Rewrite will pass you each node in the tree, and expects something to replace
162
- # it with. A single node, multiple nodes, or no nodes (remove it).
219
+ # With a CSS selector string like +"form.checkout"+ you specify the nodes
220
+ # you want to operate on. These will be passed one by one into the block.
221
+ # The block returns the {Hexp::Node} that will replace the old node, or it
222
+ # can replace an +Array+ of nodes to fill the place of the old node.
163
223
  #
164
- # Rewrite will not pass in the root node, since rewrite always returns a single
165
- # node, so it doesn't allow you to replace the root node with zero or multiple
166
- # nodes.
224
+ # Because of this you can add one or more nodes, or remove nodes by
225
+ # returning an empty array.
167
226
  #
168
- # The block can take one or two parameters, the first being the node that is
169
- # being replaced, the second (optional) its parent. As a response the block
170
- # needs to return one of these
227
+ # @example Remove all script tags
228
+ # tree.replace('script') {|_| [] }
171
229
  #
172
- # * a single Hexp::Node
173
- # * a Hexp::NodeList, or Array of Hexp::Node
174
- # * an Array representation of a Hexp::Node, [:tag, {attributes}, [children]]
175
- # * nil
230
+ # @example Wrap each +<input>+ tag into a +<p>+ tag
231
+ # tree.replace('input') do |input|
232
+ # H[:p, input]
233
+ # end
176
234
  #
177
- # The last case (returning nil) means "do nothing", it will simply keep the
178
- # currently referenced node where it is. This is very handy when you only want
179
- # to act on certain nodes, just return nothing if you want to do nothing.
235
+ # @param [String] css_selector
180
236
  #
181
- # Some examples :
237
+ # @yieldparam [Hexp::Node]
238
+ # The matching nodes
182
239
  #
183
- # Remove all script tags
184
- #
185
- # @example
186
- # tree.rewrite{|node| [] if node.tag == :script }
187
- #
188
- # Wrap each <input> tag into a <p> tag
240
+ # @return [Hexp::Node]
241
+ # The rewritten tree
189
242
  #
190
- # @example
191
- # tree.rewrite do |node|
192
- # if node.tag == :input
193
- # [ H[:p, [ child ] ]
194
- # end
195
- # end
196
- #
197
- # @param blk [Proc] The rewrite action
198
- # @return [Hexp::Node] The rewritten tree
199
243
  # @api public
200
- #
201
244
  def rewrite(css_selector = nil, &block)
202
245
  return Rewriter.new(self, block) if css_selector.nil?
203
246
  CssSelection.new(self, css_selector).rewrite(&block)
204
247
  end
205
248
  alias :replace :rewrite
206
249
 
250
+ # Select nodes based on a css selector
251
+ #
252
+ # @param [String] css_selector
253
+ # @yieldparam [Hexp::Node]
254
+ #
255
+ # @return [Hexp::Selector,Hexp::CssSelector]
256
+ #
257
+ # @api public
207
258
  def select(css_selector = nil, &block)
208
259
  if css_selector
209
260
  CssSelection.new(self, css_selector).each(&block)
210
261
  else
211
- Selector.new(self, block)
262
+ Selection.new(self, block)
212
263
  end
213
264
  end
214
265
 
@@ -14,9 +14,10 @@ module Hexp
14
14
  # H[:p, class: 'hello'].attr('id', 'para1') # => H[:p, {"class"=>"hello", "id"=>"para1"}]
15
15
  # H[:p, class: 'hello'].attr('class', nil) # => H[:p]
16
16
  #
17
+ # @param [Array<#to_s>] args
17
18
  # @return [String|Hexp::Node]
18
- # @api private
19
19
  #
20
+ # @api private
20
21
  def attr(*args)
21
22
  arity = args.count
22
23
  attr_name = args[0].to_s
@@ -38,10 +39,12 @@ module Hexp
38
39
  # @example
39
40
  # H[:option].has_attr?('selected') #=> false
40
41
  #
41
- # @param name [String|Symbol] the name of the attribute
42
- # @return [Boolean]
43
- # @api public
42
+ # @param [String|Symbol] name
43
+ # The name of the attribute
44
44
  #
45
+ # @return [true,false]
46
+ #
47
+ # @api public
45
48
  def has_attr?(name)
46
49
  attributes.has_key? name.to_s
47
50
  end
@@ -51,10 +54,13 @@ module Hexp
51
54
  # @example
52
55
  # H[:span, class: "banner strong"].class?("strong") #=> true
53
56
  #
54
- # @param klass [String] the name of the class to check for
55
- # @return [Boolean] true if the class is present, false otherwise
56
- # @api public
57
+ # @param [String] klass
58
+ # The name of the class to check for
59
+ #
60
+ # @return [Boolean]
61
+ # True if the class is present, false otherwise
57
62
  #
63
+ # @api public
58
64
  def class?(klass)
59
65
  attr('class') && attr('class').split(' ').include?(klass.to_s)
60
66
  end
@@ -64,10 +70,12 @@ module Hexp
64
70
  # @example
65
71
  # H[:div].add_class('foo') #=> H[:div, class: 'foo']
66
72
  #
67
- # @param klass [#to_s] The class to add
73
+ # @param [#to_s] klass
74
+ # The class to add
75
+ #
68
76
  # @return [Hexp::Node]
69
- # @api public
70
77
  #
78
+ # @api public
71
79
  def add_class(klass)
72
80
  attr('class', [attr('class'), klass].compact.join(' '))
73
81
  end
@@ -77,8 +85,8 @@ module Hexp
77
85
  # Convenience method so you don't have to split the class list yourself.
78
86
  #
79
87
  # @return [Array<String>]
80
- # @api public
81
88
  #
89
+ # @api public
82
90
  def class_list
83
91
  @class_list ||= (attr('class') || '').split(' ').freeze
84
92
  end
@@ -93,11 +101,12 @@ module Hexp
93
101
  # Calling this on a node with a class attribute that is equal to an
94
102
  # empty string will result in the class attribute being removed.
95
103
  #
96
- # @param klass [#to_s] The class to be removed
97
- # @return [Hexp::Node] A node that is identical to this one, but with
98
- # the given class removed
99
- # @api public
104
+ # @param [#to_s] klass
105
+ # The class to be removed
106
+ # @return [Hexp::Node]
107
+ # A node that is identical to this one, but with the given class removed
100
108
  #
109
+ # @api public
101
110
  def remove_class(klass)
102
111
  return self unless has_attr?('class')
103
112
  new_list = class_list - [klass.to_s]
@@ -107,10 +116,11 @@ module Hexp
107
116
 
108
117
  # Set or override multiple attributes using a hash syntax
109
118
  #
110
- # @param attrs[Hash]
119
+ # @param [Hash<#to_s,#to_s>] attrs
120
+ #
111
121
  # @return [Hexp::Node]
112
- # @api public
113
122
  #
123
+ # @api public
114
124
  def set_attrs(attrs)
115
125
  H[
116
126
  self.tag,
@@ -123,10 +133,13 @@ module Hexp
123
133
 
124
134
  # Remove an attribute by name
125
135
  #
126
- # @param name [#to_s] The attribute to be removed
127
- # @return [Hexp::Node] a new node with the attribute removed
128
- # @api public
136
+ # @param [#to_s] name
137
+ # The attribute to be removed
138
+ #
139
+ # @return [Hexp::Node]
140
+ # A new node with the attribute removed
129
141
  #
142
+ # @api public
130
143
  def remove_attr(name)
131
144
  H[
132
145
  self.tag,
@@ -137,10 +150,13 @@ module Hexp
137
150
 
138
151
  # Attribute accessor
139
152
  #
140
- # @param attribute_name [#to_s] The name of the attribute
141
- # @return [String] The value of the attribute
142
- # @api public
153
+ # @param_name [#to_s] attr
154
+ # The name of the attribute
143
155
  #
156
+ # @return [String]
157
+ # The value of the attribute
158
+ #
159
+ # @api public
144
160
  def [](attr_name)
145
161
  self.attributes[attr_name.to_s]
146
162
  end
@@ -148,16 +164,17 @@ module Hexp
148
164
  # Merge attributes into this Hexp
149
165
  #
150
166
  # Class attributes are treated special : the class lists are merged, rather
151
- # than being overwritten. See {set_attrs} for a more basic version.
167
+ # than being overwritten. See {#set_attrs} for a more basic version.
152
168
  #
153
- # This method is analoguous with {Hash#merge}. As argument it can take a
169
+ # This method is analoguous with `Hash#merge`. As argument it can take a
154
170
  # Hash, or another Hexp element, in which case that element's attributes
155
171
  # are used.
156
172
  #
157
- # @param node_or_hash [#to_hexp|Hash]
173
+ # @param_or_hash [#to_hexp|Hash] node
174
+ #
158
175
  # @return [Hexp::Node]
159
- # @api public
160
176
  #
177
+ # @api public
161
178
  def merge_attrs(node_or_hash)
162
179
  hash = node_or_hash.respond_to?(:to_hexp) ?
163
180
  node_or_hash.to_hexp.attributes : node_or_hash