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