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,256 @@
1
+ module Hexp
2
+ # Build Hexps using the builder pattern
3
+ #
4
+ class Builder < BasicObject
5
+ include ::Hexp
6
+
7
+ # def inspect
8
+ # ::Kernel.puts ::Kernel.caller ; ::Kernel.exit
9
+ # end
10
+
11
+ # Construct a new builder, and start building
12
+ #
13
+ # The recommended way to call this is through `Hexp.build`.
14
+ #
15
+ # @param tag [Symbol] The tag of the outermost element (optional)
16
+ # @param args [Array<Hash|String>] Extra arguments, a String for a text
17
+ # node, a Hash for attributes
18
+ # @param block [Proc] The block containing builder directives, can be with
19
+ # or without an argument.
20
+ #
21
+ # @api private
22
+ #
23
+ def initialize(tag = nil, *args, &block)
24
+ @stack = []
25
+ if tag
26
+ tag!(tag, *args, &block)
27
+ else
28
+ _process(&block) if block
29
+ end
30
+ end
31
+
32
+ # Add a tag (HTML element)
33
+ #
34
+ # Typically this is called implicitly through method missing, but in case of
35
+ # name clashes or dynamically generated tags you can call this directly.
36
+ #
37
+ # @example
38
+ # hexp = Hexp.build :div do
39
+ # tag!(:p, "Oh the code, such sweet joy it brings")
40
+ # end
41
+ # hexp.to_html #=> "<div><p>Oh the code, such sweet joy it brings</p></div>"
42
+ #
43
+ # @param tag [Symbol] The tag name, like 'div' or 'head'
44
+ # @param args [Array<Hash|String>] A hash of attributes, or a string to use
45
+ # inside the tag, or both. Multiple occurences of each can be
46
+ # specified
47
+ # @param block [Proc] Builder directives for the contents of the tag
48
+ # @return [NilClass]
49
+ #
50
+ # @api public
51
+ #
52
+ def tag!(tag, *args, &block)
53
+ text, attributes = nil, {}
54
+ args.each do |arg|
55
+ case arg
56
+ when ::Hash
57
+ attributes.merge!(arg)
58
+ when ::String
59
+ text ||= ''
60
+ text << arg
61
+ end
62
+ end
63
+ @stack << [tag, attributes, text ? [text] : []]
64
+ if block
65
+ _process(&block)
66
+ end
67
+ if @stack.length > 1
68
+ node = @stack.pop
69
+ @stack.last[2] << node
70
+ NodeBuilder.new(node, self)
71
+ else
72
+ NodeBuilder.new(@stack.last, self)
73
+ end
74
+ end
75
+
76
+ alias method_missing tag!
77
+
78
+ # Add a text node to the tree
79
+ #
80
+ # @example
81
+ # hexp = Hexp.build do
82
+ # span do
83
+ # text! 'Not all who wander are lost'
84
+ # end
85
+ # end
86
+ #
87
+ # @param text [String] the text to add
88
+ # @return [Hexp::Builder] self
89
+ # @api public
90
+ #
91
+ def text!(text)
92
+ _raise_if_empty! "Hexp::Builder needs a root element to add text elements to"
93
+ @stack.last[2] << text.to_s
94
+ self
95
+ end
96
+
97
+ # Add Hexp objects to the current tag
98
+ #
99
+ # Any Hexp::Node or other object implementing to_hexp can be added with
100
+ # this operator. Multiple objects can be specified in one call.
101
+ #
102
+ # Nokogiri and Builder allow inserting of strings containing HTML through
103
+ # this operator. Since this would violate the core philosophy of Hexp, and
104
+ # open the door for XSS vulnerabilities, we do not support that usage.
105
+ #
106
+ # If you really want to insert HTML that is already in serialized form,
107
+ # consider parsing it to Hexps first
108
+ #
109
+ # @example
110
+ # widget = H[:button, "click me!"]
111
+ # node = Hexp.build :div do |h|
112
+ # h << widget
113
+ # end
114
+ # node.to_html #=> <div><button>click me!</button></div>
115
+ #
116
+ # @params args [Array<:to_hexp>] Hexpable objects to add to the current tag
117
+ # @return [Hexp::Builder]
118
+ #
119
+ # @api public
120
+ #
121
+ def <<(*args)
122
+ args.each do |arg|
123
+ if arg.respond_to?(:to_hexp)
124
+ @stack.last[2] << arg
125
+ self
126
+ else
127
+ ::Kernel.raise ::Hexp::FormatError, "Inserting literal HTML into a builder with << is deliberately not supported by Hexp"
128
+ end
129
+ end
130
+ end
131
+
132
+ # Implement the standard Hexp coercion protocol
133
+ #
134
+ # By implementing this a Builder is interchangeable for a regular node, so
135
+ # you can use it inside other nodes transparently. But you can call this
136
+ # method if you really, really just want the plain {Hexp::Node}
137
+ #
138
+ # @example
139
+ # Hexp.build { div { text! 'hello' } }.to_hexp # => H[:div, ["hello"]]
140
+ #
141
+ # @return [Hexp::Node]
142
+ # @api public
143
+ #
144
+ def to_hexp
145
+ _raise_if_empty!
146
+ ::Hexp::Node[*@stack.last]
147
+ end
148
+
149
+ # Call the block, with a specific value of 'self'
150
+ #
151
+ # If the block takes an argument, then we pass ourselves (the builder) to
152
+ # the block, and call it as a closure. This way 'self' refers to the calling
153
+ # object, and it can reference its own methods and ivars.
154
+ #
155
+ # If the block does not take an argument, then we evaluate it in the context
156
+ # of ourselves (the builder), so unqualified method calls are seen as
157
+ # builder calls.
158
+ #
159
+ # @param block [Proc]
160
+ # @return [NilClass]
161
+ # @api private
162
+ #
163
+ def _process(&block)
164
+ if block.arity == 1
165
+ block.call(self)
166
+ else
167
+ self.instance_eval(&block)
168
+ end
169
+ end
170
+
171
+ # Allow setting HTML classes through method calls
172
+ #
173
+ # @example
174
+ # Hexp.build do
175
+ # div.miraculous.wondrous do
176
+ # hr
177
+ # end
178
+ # end
179
+ #
180
+ # @api private
181
+ #
182
+ class NodeBuilder
183
+ # Create new NodeBuilder
184
+ #
185
+ # @param node [Array] (tag, attrs, children) triplet
186
+ # @param builder [Hexp::Builder] The parent builder to delegate back
187
+ # @api private
188
+ #
189
+ def initialize(node, builder)
190
+ @node, @builder = node, builder
191
+ end
192
+
193
+ # Used for specifying CSS class names
194
+ #
195
+ # @example
196
+ # Hexp.build { div.strong.warn }.to_hexp
197
+ # # => H[:div, class: 'strong warn']
198
+ #
199
+ # @param sym [Symbol] the class to add
200
+ # @return [Hexp::Builder::NodeBuilder] self
201
+ # @api public
202
+ #
203
+ def method_missing(sym, &block)
204
+ attrs = @node[1]
205
+ @node[1] = attrs.merge class: [attrs[:class], sym.to_s].compact.join(' ')
206
+ @builder._process &block if block
207
+ self
208
+ end
209
+ end
210
+
211
+ # Return a debugging representation
212
+ #
213
+ # Hexp is intended for HTML, so it shouldn't be a problem that this is an
214
+ # actual method. It really helps for debugging or when playing around in
215
+ # irb. If you really want an `<inspect>` tag, use `tag!(:inspect)`.
216
+ #
217
+ # @example
218
+ # p Hexp.build { div }
219
+ #
220
+ # @return [String]
221
+ # @api public
222
+ #
223
+ def inspect
224
+ "#<Hexp::Builder #{@stack.empty? ? '[]' :to_hexp.inspect}>"
225
+ end
226
+
227
+ # Gratefully borrowed from Builder.
228
+ # I'd like to benchmark this singleton class based version vs
229
+ # adding the methods to the class directly, before putting this in.
230
+ #
231
+ # @param sym [Symbol] Name of the method to define
232
+ # @api private
233
+ #
234
+ # def _cache_method_call(sym)
235
+ # class << self; self; end.class_eval do
236
+ # unless method_defined?(sym)
237
+ # define_method(sym) do |*args, &block|
238
+ # tag!(sym, *args, &block)
239
+ # end
240
+ # end
241
+ # end
242
+ # end
243
+
244
+ private
245
+
246
+ # Raise an exception if nothing has been built yet
247
+ #
248
+ # @param text [String] The error message
249
+ # @raises {Hexp::FormatError}
250
+ # @api private
251
+ #
252
+ def _raise_if_empty!(text = 'Hexp::Builder is lacking a root element.')
253
+ ::Kernel.raise ::Hexp::FormatError, text if @stack.empty?
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,205 @@
1
+ module Hexp
2
+ module CssSelector
3
+ # Common behavior for parse tree nodes based on a list of members
4
+ #
5
+ module Members
6
+ include Equalizer.new(:members)
7
+
8
+ extend Forwardable
9
+ def_delegator :@members, :empty?
10
+
11
+ # Member nodes
12
+ #
13
+ attr_reader :members
14
+
15
+ # Shared initializer for parse tree nodes with children (members)
16
+ #
17
+ def initialize(members)
18
+ @members = Hexp.deep_freeze(members)
19
+ end
20
+
21
+ # Create a class level collection constructor
22
+ #
23
+ # @example
24
+ # CommaSequence[member1, member2]
25
+ #
26
+ # @param klass [Class]
27
+ # @api private
28
+ #
29
+ def self.included(klass)
30
+ def klass.[](*members)
31
+ new(members)
32
+ end
33
+ end
34
+
35
+ # Return a debugging representation
36
+ #
37
+ # @return [String]
38
+ # @api private
39
+ #
40
+ def inspect
41
+ "#{self.class.name.split('::').last}[#{self.members.map(&:inspect).join(', ')}]"
42
+ end
43
+ end
44
+
45
+ # Common behavior for parse tree elements that have a name
46
+ #
47
+ module Named
48
+ include Equalizer.new(:name)
49
+ attr_reader :name
50
+
51
+ def initialize(name)
52
+ @name = name.freeze
53
+ end
54
+
55
+ def inspect
56
+ "<#{self.class.name.split('::').last} name=#{name}>"
57
+ end
58
+ end
59
+
60
+ # Top level parse tree node of a CSS selector
61
+ #
62
+ # Contains a number of {Sequence} objects
63
+ #
64
+ # For example : `span .big, a'
65
+ #
66
+ class CommaSequence
67
+ include Members
68
+
69
+ # def inspect
70
+ # members.map(&:inspect).join(', ')
71
+ # end
72
+
73
+ # Does any sequence in this comma sequence fully match the given element
74
+ #
75
+ # This method does not recurse, it only checks if any of the sequences in
76
+ # this CommaSequence with a length of one can fully match the given
77
+ # element.
78
+ #
79
+ # @param element [Hexp::Node]
80
+ # @return [Boolean]
81
+ #
82
+ def matches?(element)
83
+ members.any? do |sequence|
84
+ sequence.members.count == 1 &&
85
+ sequence.head_matches?(element)
86
+ end
87
+ end
88
+ end
89
+
90
+ # A single CSS sequence like 'div span .foo'
91
+ #
92
+ class Sequence
93
+
94
+ include Members
95
+
96
+ def head_matches?(element)
97
+ members.first.matches?(element)
98
+ end
99
+
100
+ def drop_head
101
+ self.class.new(members.drop(1))
102
+ end
103
+
104
+ # def inspect
105
+ # members.map(&:inspect).join(' ')
106
+ # end
107
+ end
108
+
109
+ # A CSS sequence that relates to a single element, like 'div.caption:first'
110
+ #
111
+ class SimpleSequence
112
+ include Members
113
+
114
+ def matches?(element)
115
+ members.all? do |simple|
116
+ simple.matches?(element)
117
+ end
118
+ end
119
+
120
+ # def inspect
121
+ # members.map(&:inspect).join
122
+ # end
123
+ end
124
+
125
+ # A CSS element declaration, like 'div'
126
+ class Element
127
+ include Named
128
+
129
+ def matches?(element)
130
+ element.tag.to_s == name
131
+ end
132
+
133
+ # def inspect
134
+ # name
135
+ # end
136
+ end
137
+
138
+ # A CSS class declaration, like '.foo'
139
+ #
140
+ class Class
141
+ include Named
142
+
143
+ def matches?(element)
144
+ element.class?(name)
145
+ end
146
+ end
147
+
148
+ # A CSS id declaration, like '#section-14'
149
+ #
150
+ class Id
151
+ include Named
152
+
153
+ def matches?(element)
154
+ element.attr('id') == name
155
+ end
156
+ end
157
+
158
+ # An attribute selector, like [href^="http://"]
159
+ #
160
+ class Attribute
161
+ include Equalizer.new(:name, :namespace, :operator, :value, :flags)
162
+ attr_reader :name, :namespace, :operator, :value, :flags
163
+
164
+ def initialize(name, namespace, operator, value, flags)
165
+ @name = name.freeze
166
+ @namespace = namespace.freeze
167
+ @operator = operator.freeze
168
+ @value = value.freeze
169
+ @flag = flags.freeze
170
+ end
171
+
172
+ def inspect
173
+ "<#{self.class.name.split('::').last} name=#{name} namespace=#{namespace.inspect} operator=#{operator.inspect} value=#{value.inspect} flags=#{flags.inspect}>"
174
+ end
175
+
176
+ def matches?(element)
177
+ return false unless element[name]
178
+ attribute = element[name]
179
+
180
+ case operator
181
+ # CSS 2
182
+ when nil
183
+ true
184
+ when '=' # exact match
185
+ attribute == value
186
+ when '~=' # space separated list contains
187
+ attribute.split(' ').include?(value)
188
+ when '|=' # equal to, or starts with followed by a dash
189
+ attribute =~ /\A#{Regexp.escape(value)}(-|\z)/
190
+
191
+ # CSS 3
192
+ when '^=' # starts with
193
+ attribute.index(value) == 0
194
+ when '$=' # ends with
195
+ attribute =~ /#{Regexp.escape(value)}\z/
196
+ when '*=' # contains
197
+ !!(attribute =~ /#{Regexp.escape(value)}/)
198
+
199
+ else
200
+ raise "Unknown operator : #{operator}"
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end