hexp 0.0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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,74 @@
1
+ module Hexp
2
+ module CssSelector
3
+ # A parser of CSS selectors
4
+ #
5
+ # This is a wrapper around the SASS parser. This way we are isolated from
6
+ # changes in SASS. It also makes things easier should we decide to switch
7
+ # to a different parsing library or roll our own parser. We only use a
8
+ # fraction of the functionality of SASS so this might be worth it, although
9
+ # at this point I want to avoid reinventing that wheel.
10
+ #
11
+ # The classes that make up the parse tree largely mimic the ones from SASS,
12
+ # like CommaSequence, SimpleSequence, Class, Id, etc. By having them in our
13
+ # own namespace however we can easily add Hexp-specific helper functions.
14
+ #
15
+ class Parser
16
+ def initialize(selector)
17
+ @selector = selector.freeze
18
+ end
19
+
20
+ def parse
21
+ rewrite_comma_sequence(SassParser.call(@selector))
22
+ end
23
+
24
+ def self.call(selector)
25
+ new(selector).parse
26
+ end
27
+
28
+ private
29
+
30
+ def rewrite_comma_sequence(comma_sequence)
31
+ CommaSequence.new(comma_sequence.members.map{|sequence| rewrite_sequence(sequence)})
32
+ end
33
+
34
+ def rewrite_sequence(sequence)
35
+ Sequence.new(sequence.members.map{|simple_sequence| rewrite_simple_sequence(simple_sequence)})
36
+ end
37
+
38
+ def rewrite_simple_sequence(simple_sequence)
39
+ SimpleSequence.new(simple_sequence.members.map{|simple| rewrite_simple(simple)})
40
+ end
41
+
42
+ def rewrite_simple(simple)
43
+ case simple
44
+ when ::Sass::Selector::Element # span
45
+ Element.new(simple.name.first)
46
+ when ::Sass::Selector::Class # .foo
47
+ Class.new(simple.name.first)
48
+ when ::Sass::Selector::Id # #main
49
+ Id.new(simple.name.first)
50
+ when ::Sass::Selector::Attribute # [href^="http://"]
51
+ raise "CSS attribute selector flags are curently ignored by Hexp (not implemented)" unless simple.flags.nil?
52
+ 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?
54
+ Attribute.new(
55
+ simple.name.first,
56
+ simple.namespace,
57
+ simple.operator,
58
+ simple.value ? simple.value.first : nil,
59
+ simple.flags
60
+ )
61
+ else
62
+ raise "CSS selectors containing #{simple.class} are not implemented in Hexp"
63
+ end
64
+
65
+ # when ::Sass::Selector::Universal # *
66
+ # when ::Sass::Selector::Parent # & in Sass
67
+ # when ::Sass::Selector::Interpolation # #{} in Sass
68
+ # when ::Sass::Selector::Pseudo # :visited, ::first-line, :nth-child(2n+1)
69
+ # when ::Sass::Selector::SelectorPseudoClass # :not(.foo)
70
+
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,22 @@
1
+ module Hexp
2
+ module CssSelector
3
+ # A CSS Parser that only knows how to parse CSS selectors
4
+ #
5
+ class SassParser < ::Sass::SCSS::CssParser
6
+ def initialize(selector)
7
+ super(selector, '')
8
+ end
9
+
10
+ def parse
11
+ init_scanner!
12
+ result = selector_comma_sequence
13
+ raise "Invalid CSS selector : unconsumed input #{@scanner.rest}" unless @scanner.eos?
14
+ result
15
+ end
16
+
17
+ def self.call(selector)
18
+ self.new(selector).parse
19
+ end
20
+ end
21
+ end
22
+ end
@@ -3,7 +3,5 @@ module Hexp
3
3
  Document = Nokogiri::HTML::Document
4
4
  Node = Nokogiri::XML::Node
5
5
  Text = Nokogiri::XML::Text
6
-
7
-
8
6
  end
9
7
  end
@@ -0,0 +1,27 @@
1
+ module Hexp
2
+ 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)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ module Hexp
2
+ Error = Class.new(StandardError)
3
+
4
+ # Raised when trying to stick things inside a Hexp where they don't belong
5
+ #
6
+ class FormatError < Error
7
+ # Create a new FormatError
8
+ #
9
+ # @api private
10
+ #
11
+ def initialize(msg = 'You have illegal contents in your Hexp')
12
+ super
13
+ end
14
+ end
15
+
16
+ # Raised by {Hexp.parse} when the input can't be converted to a {Hexp::Node}
17
+ #
18
+ class ParseError < Error
19
+ end
20
+
21
+ end
@@ -1,2 +1,5 @@
1
- require 'hexp'
2
- H=Hexp::Node
1
+ if defined?(::H) && ::H != Hexp::Node
2
+ $stderr.puts "WARN: H is already defined, Hexp H[] shorthand not available"
3
+ else
4
+ H=Hexp::Node
5
+ end
@@ -1,25 +1,83 @@
1
1
  module Hexp
2
2
  # A list of nodes
3
3
  #
4
- # @example
5
- # Hexp::List[
6
- # Hexp::Node[:marquee, "Try Hexp for instanst satisfaction!"],
7
- # Hexp::Node[:hr],
8
- # ]
9
- #
10
- class List < SimpleDelegator
11
- include Equalizer.new(:__getobj__)
4
+ class List < DelegateClass(Array)
12
5
 
6
+ # Create new Hexp::List
7
+ #
8
+ # @example
9
+ # Hexp::List.new([H[:p], H[:div]])
10
+ #
11
+ # @param nodes [#to_ary] List of nodes
12
+ #
13
+ # @api public
14
+ #
13
15
  def initialize(nodes)
14
- super Hexp.deep_freeze nodes
16
+ super nodes.to_ary.freeze
15
17
  end
16
18
 
19
+ # Convenience constructor
20
+ #
21
+ # @example
22
+ # Hexp::List[
23
+ # Hexp::Node[:marquee, "Try Hexp for instanst satisfaction!"],
24
+ # Hexp::Node[:hr],
25
+ # ]
26
+ #
27
+ # @param args [Array] individual nodes
28
+ #
29
+ # @return [Hexp::List]
30
+ # @api public
31
+ #
17
32
  def self.[](*args)
18
33
  new(args)
19
34
  end
20
35
 
36
+ # String representation
37
+ #
38
+ # This delegates to the underlying array, so it's not obvious from the output
39
+ # that this is a wrapping class. This is convenient when inspecting nested
40
+ # hexps, but probably something we want to solve differently.
41
+ #
42
+ # @api private
43
+ # @return string
44
+ #
21
45
  def inspect
22
46
  __getobj__.inspect
23
47
  end
48
+
49
+ # Internal coercion to Array
50
+ #
51
+ # @example
52
+ # Hexp::List[ H[:p], H[:span] ].to_ary #=> [H[:p], H[:span]]
53
+ #
54
+ # @return [Array<Hexp::Node>]
55
+ # @api public
56
+ #
57
+ def to_ary
58
+ __getobj__
59
+ end
60
+
61
+ # Value and type equality
62
+ #
63
+ # Hexp::List is mostly interchangeable with a plain Array, and so equality
64
+ # with `==` delegates to the underlying array, making `Hexp::List[] == []`
65
+ # true.
66
+ #
67
+ # If you want a stronger comparison, than this version will compare both
68
+ # the value (in this case : contents), and the type.
69
+ #
70
+ # @example
71
+ # H[:div, [[:span]]].children == [H[:span]] #=> true
72
+ # H[:div, [[:span]]].children.eql? [H[:span]] #=> false
73
+ # H[:div, [[:span]]].children.eql? Hexp::List[H[:span]] #=> true
74
+ #
75
+ # @param other [Object] Object to compare with
76
+ # @api public
77
+ # @return [Boolean]
78
+ #
79
+ def eql?(other)
80
+ self == other && self.class == other.class
81
+ end
24
82
  end
25
83
  end
@@ -1,53 +1,162 @@
1
1
  module Hexp
2
- # A Hexp Node
2
+ # A Hexp Node, or simply 'a hexp'
3
3
  class Node
4
4
  include Equalizer.new(:tag, :attributes, :children)
5
5
  extend Forwardable
6
6
 
7
- attr_reader :tag, :attributes, :children
8
- def_delegators :@children, :empty?
7
+ include Hexp::Node::Attributes
8
+ include Hexp::Node::Children
9
9
 
10
- # Normalize the arguments
10
+ # The HTML tag of this node
11
11
  #
12
- # @param args [Array] args a Hexp node
13
- # @return [Hexp::Node]
12
+ # @example
13
+ # H[:p].tag #=> :p
14
+ #
15
+ # @return [Symbol]
16
+ # @api public
17
+ #
18
+ attr_reader :tag
19
+
20
+ # The attributes of this node
14
21
  #
15
22
  # @example
16
- # Hexp::Node[:p, {'class' => 'foo'}, [[:b, "Hello, World!"]]]
23
+ # H[:p, class: 'foo'].attributes #=> {'class' => 'foo'}
17
24
  #
25
+ # @return [Hash<String, String>]
18
26
  # @api public
19
- def initialize(*args)
20
- @tag, @attributes, @children = Hexp.deep_freeze(
21
- Normalize.new(args).call
22
- )
23
- end
27
+ #
28
+ attr_reader :attributes
29
+
30
+ # The child nodes of this node
31
+ #
32
+ # @example
33
+ # H[:p, [ H[:span], 'hello' ]].children
34
+ # #=> Hexp::List[ H[:span], Hexp::TextNode["hello"] ]
35
+ #
36
+ # @return [Hexp::List]
37
+ # @api public
38
+ #
39
+ attr_reader :children
24
40
 
41
+ # Main entry point for creating literal hexps
42
+ #
43
+ # At the moment this just redirects to #new, and since Hexp::Node is aliased
44
+ # to H this provides a shorthand for the contructor,
45
+ #
46
+ # @example
47
+ # H[:span, {attr: 'value'}].
48
+ #
49
+ # Note that while the H[] form is part of the public API and expected to
50
+ # remain, it's implementation might change. In particular H might become
51
+ # a class or module in its own right, so it is recommended to only use
52
+ # this method in its H[] form.
53
+ #
54
+ # @param args [Array] args a Hexp node components
55
+ # @return [Hexp::Node]
56
+ # @api public
57
+ #
25
58
  def self.[](*args)
26
59
  new(*args)
27
60
  end
28
61
 
62
+ # Normalize the arguments
63
+ #
64
+ # @param args [Array] args a Hexp node components
65
+ # @return [Hexp::Node]
66
+ #
67
+ # @example
68
+ # Hexp::Node.new(:p, {'class' => 'foo'}, [[:b, "Hello, World!"]])
69
+ #
70
+ # @api public
71
+ #
72
+ def initialize(*args)
73
+ @tag, @attributes, @children = Normalize.new(args).call
74
+ end
75
+
76
+ # Standard hexp coercion protocol, return self
77
+ #
78
+ # @example
79
+ # H[:p].to_hexp #=> H[:p]
80
+ #
81
+ # @return [Hexp::Node] self
82
+ # @api public
83
+ #
29
84
  def to_hexp
30
85
  self
31
86
  end
32
87
 
33
- def to_html
34
- to_dom.to_html
88
+ # Serialize this node to HTML
89
+ #
90
+ # @example
91
+ # H[:html, [ H[:body, ["hello, world"] ] ]] .to_html
92
+ # # => "<html><body>hello, world</body></html>"
93
+ #
94
+ # @return [String]
95
+ # @api public
96
+ #
97
+ def to_html(options = {})
98
+ to_dom(options).to_html
35
99
  end
36
100
 
37
- def to_dom
38
- Domize.new(self).call
101
+ # Convert this node into a Nokogiri Document
102
+ #
103
+ # @example
104
+ # H[:p].to_dom
105
+ # #=> #<Nokogiri::HTML::Document name="document"
106
+ # children=[#<Nokogiri::XML::DTD name="html">,
107
+ # #<Nokogiri::XML::Element name="p">]>
108
+ #
109
+ # @return [Nokogiri::HTML::Document]
110
+ # @api private
111
+ #
112
+ def to_dom(options = {})
113
+ Domize.new(self, options).call
39
114
  end
40
115
 
116
+ # Return a string representation that is close to the literal form
117
+ #
118
+ # @example
119
+ # H[:p, {class: 'foo'}].inspect #=> "H[:p, {\"class\"=>\"foo\"}]"
120
+ #
121
+ # @return [String]
122
+ # @api public
123
+ #
41
124
  def inspect
42
- self.class.inspect_name + [tag, attributes, children ].reject(&:empty?).inspect
125
+ self.class.inspect_name + [tag, attributes, children].compact.reject(&:empty?).inspect
43
126
  end
44
127
 
128
+ # Pretty print, a multiline representation with indentation
129
+ #
130
+ # @example
131
+ # H[:p, [[:span], [:div]]].pp # => "H[:p, [\n H[:span],\n H[:div]]]"
132
+ #
133
+ # @return [String]
134
+ # @api public
135
+ #
45
136
  def pp
46
137
  self.class::PP.new(self).call
47
138
  end
48
139
 
49
- # Rewrite a node tree. Since nodes are immutable, this is the main entry point
50
- # for deriving nodes from others.
140
+ # Is this a text node? Returns false
141
+ #
142
+ # @example
143
+ # H[:p].text? #=> false
144
+ #
145
+ # @return [FalseClass]
146
+ # @api public
147
+ #
148
+ def text?
149
+ false
150
+ end
151
+
152
+ def set_tag(tag)
153
+ H[tag.to_sym, attributes, children]
154
+ end
155
+
156
+ # Rewrite a node tree
157
+ #
158
+ # Since nodes are immutable, this is the main entry point for deriving nodes
159
+ # from others.
51
160
  #
52
161
  # Rewrite will pass you each node in the tree, and expects something to replace
53
162
  # it with. A single node, multiple nodes, or no nodes (remove it).
@@ -73,10 +182,12 @@ module Hexp
73
182
  #
74
183
  # Remove all script tags
75
184
  #
185
+ # @example
76
186
  # tree.rewrite{|node| [] if node.tag == :script }
77
187
  #
78
188
  # Wrap each <input> tag into a <p> tag
79
189
  #
190
+ # @example
80
191
  # tree.rewrite do |node|
81
192
  # if node.tag == :input
82
193
  # [ H[:p, [ child ] ]
@@ -85,32 +196,76 @@ module Hexp
85
196
  #
86
197
  # @param blk [Proc] The rewrite action
87
198
  # @return [Hexp::Node] The rewritten tree
88
- def rewrite(&blk)
89
- return to_enum(:rewrite) unless block_given?
90
-
91
- H[self.tag,
92
- self.attributes,
93
- self.children.flat_map {|child| child.rewrite(&blk) }
94
- .flat_map do |child|
95
- response = blk.call(child, self)
96
- if response.instance_of?(Hexp::Node)
97
- [ response ]
98
- elsif response.respond_to?(:to_ary)
99
- if response.first.instance_of?(Symbol)
100
- [ response ]
101
- else
102
- response
103
- end
104
- elsif response.nil?
105
- [ child ]
106
- else
107
- raise FormatError, "invalid rewrite response : #{response.inspect}, expected Hexp::Node or Array, got #{response.class}"
108
- end
199
+ # @api public
200
+ #
201
+ def rewrite(css_selector = nil, &block)
202
+ return Rewriter.new(self, block) if css_selector.nil?
203
+ CssSelection.new(self, css_selector).rewrite(&block)
204
+ end
205
+ alias :replace :rewrite
206
+
207
+ def select(css_selector = nil, &block)
208
+ if css_selector
209
+ CssSelection.new(self, css_selector).each(&block)
210
+ else
211
+ Selector.new(self, block)
212
+ end
213
+ end
214
+
215
+ # Run a number of processors on this node
216
+ #
217
+ # This is pure convenience, but it helps to conceptualize the "processor"
218
+ # idea of a component (be it a lambda or other object), that responds to
219
+ # call, and transform a {Hexp::Node} tree.
220
+ #
221
+ # @example
222
+ # hexp.process(
223
+ # ->(node) { node.replace('.section') {|node| H[:p, class: 'big', node]} },
224
+ # ->(node) { node.add_class 'foo' },
225
+ # InlineAssets.new
226
+ # )
227
+ #
228
+ # @param processors [Array<#call>]
229
+ # @return [Hexp::Node]
230
+ # @api public
231
+ #
232
+ def process(*processors)
233
+ processors.empty? ? self : processors.first.(self).process(*processors.drop(1))
234
+ end
235
+
236
+ private
237
+
238
+ # Set an attribute, used internally by #attr
239
+ #
240
+ # Setting an attribute to nil will delete it
241
+ #
242
+ # @param name [String|Symbol]
243
+ # @param value [String|NilClass]
244
+ # @return [Hexp::Node]
245
+ #
246
+ # @api private
247
+ #
248
+ def set_attr(name, value)
249
+ if value.nil?
250
+ new_attrs = {}
251
+ attributes.each do |nam,val|
252
+ new_attrs[nam] = val unless nam == name.to_s
109
253
  end
110
- ]
254
+ else
255
+ new_attrs = attributes.merge(name.to_s => value.to_s)
256
+ end
257
+ self.class.new(self.tag, new_attrs, self.children)
111
258
  end
112
259
 
113
260
  class << self
261
+
262
+ # Returns the class name for use in creating inspection strings
263
+ #
264
+ # This will return "H" if H == Hexp::Node, or "Hexp::Node" otherwise.
265
+ #
266
+ # @return [String]
267
+ # @api private
268
+ #
114
269
  def inspect_name
115
270
  if defined?(H) && H == self
116
271
  'H'
@@ -118,6 +273,7 @@ module Hexp
118
273
  self.name
119
274
  end
120
275
  end
276
+
121
277
  end
122
278
  end
123
279
  end