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