hexp 0.0.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +12 -3
- data/Changelog.md +9 -0
- data/Gemfile +3 -5
- data/Gemfile.devtools +20 -18
- data/Gemfile.lock +97 -84
- data/Rakefile +16 -0
- data/config/flay.yml +2 -2
- data/config/flog.yml +1 -1
- data/config/reek.yml +42 -18
- data/config/rubocop.yml +31 -0
- data/config/yardstick.yml +39 -1
- data/examples/from_nokogiri.rb +77 -0
- data/examples/selector_rewriter_chaining.rb +14 -0
- data/examples/todo.rb +138 -0
- data/examples/widget.rb +64 -0
- data/hexp.gemspec +8 -3
- data/lib/hexp.rb +103 -2
- data/lib/hexp/builder.rb +256 -0
- data/lib/hexp/css_selector.rb +205 -0
- data/lib/hexp/css_selector/parser.rb +74 -0
- data/lib/hexp/css_selector/sass_parser.rb +22 -0
- data/lib/hexp/dom.rb +0 -2
- data/lib/hexp/dsl.rb +27 -0
- data/lib/hexp/errors.rb +21 -0
- data/lib/hexp/h.rb +5 -2
- data/lib/hexp/list.rb +67 -9
- data/lib/hexp/node.rb +197 -41
- data/lib/hexp/node/attributes.rb +176 -0
- data/lib/hexp/node/children.rb +44 -0
- data/lib/hexp/node/css_selection.rb +73 -0
- data/lib/hexp/node/domize.rb +52 -6
- data/lib/hexp/node/normalize.rb +19 -9
- data/lib/hexp/node/pp.rb +32 -0
- data/lib/hexp/node/rewriter.rb +52 -0
- data/lib/hexp/node/selector.rb +59 -0
- data/lib/hexp/nokogiri/equality.rb +61 -0
- data/lib/hexp/nokogiri/reader.rb +27 -0
- data/lib/hexp/sass/selector_parser.rb +4 -0
- data/lib/hexp/text_node.rb +129 -9
- data/lib/hexp/version.rb +1 -1
- data/notes +34 -0
- data/spec/shared_helper.rb +6 -0
- data/spec/spec_helper.rb +2 -6
- data/spec/unit/hexp/builder_spec.rb +101 -0
- data/spec/unit/hexp/css_selector/attribute_spec.rb +137 -0
- data/spec/unit/hexp/css_selector/class_spec.rb +15 -0
- data/spec/unit/hexp/css_selector/comma_sequence_spec.rb +20 -0
- data/spec/unit/hexp/css_selector/element_spec.rb +11 -0
- data/spec/unit/hexp/css_selector/parser_spec.rb +51 -0
- data/spec/unit/hexp/css_selector/simple_sequence_spec.rb +48 -0
- data/spec/unit/hexp/dsl_spec.rb +55 -0
- data/spec/unit/hexp/h_spec.rb +38 -0
- data/spec/unit/hexp/list_spec.rb +19 -0
- data/spec/unit/hexp/node/attr_spec.rb +55 -0
- data/spec/unit/hexp/node/attributes_spec.rb +125 -0
- data/spec/unit/hexp/node/children_spec.rb +33 -0
- data/spec/unit/hexp/node/class_spec.rb +37 -0
- data/spec/unit/hexp/node/css_selection_spec.rb +86 -0
- data/spec/unit/hexp/node/normalize_spec.rb +12 -6
- data/spec/unit/hexp/node/rewrite_spec.rb +67 -30
- data/spec/unit/hexp/node/selector_spec.rb +78 -0
- data/spec/unit/hexp/node/text_spec.rb +7 -0
- data/spec/unit/hexp/node/to_dom_spec.rb +1 -1
- data/spec/unit/hexp/nokogiri/reader_spec.rb +8 -0
- data/spec/unit/hexp/parse_spec.rb +23 -0
- data/spec/unit/hexp/text_node_spec.rb +25 -0
- data/spec/unit/hexp_spec.rb +33 -0
- metadata +129 -16
- data/lib/hexp/format_error.rb +0 -8
data/lib/hexp/builder.rb
ADDED
@@ -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
|