hexp 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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