arbre2 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +30 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +6 -0
  6. data/CHANGELOG.md +75 -0
  7. data/Gemfile +2 -0
  8. data/Gemfile.lock +93 -0
  9. data/LICENSE +20 -0
  10. data/README.md +92 -0
  11. data/Rakefile +7 -0
  12. data/arbre.gemspec +28 -0
  13. data/lib/arbre/child_element_collection.rb +86 -0
  14. data/lib/arbre/container.rb +20 -0
  15. data/lib/arbre/context.rb +83 -0
  16. data/lib/arbre/element/building.rb +151 -0
  17. data/lib/arbre/element.rb +194 -0
  18. data/lib/arbre/element_collection.rb +93 -0
  19. data/lib/arbre/html/attributes.rb +91 -0
  20. data/lib/arbre/html/class_list.rb +53 -0
  21. data/lib/arbre/html/comment.rb +47 -0
  22. data/lib/arbre/html/document.rb +93 -0
  23. data/lib/arbre/html/html_tags.rb +67 -0
  24. data/lib/arbre/html/querying.rb +256 -0
  25. data/lib/arbre/html/tag.rb +317 -0
  26. data/lib/arbre/rails/layouts.rb +126 -0
  27. data/lib/arbre/rails/legacy_document.rb +29 -0
  28. data/lib/arbre/rails/rendering.rb +76 -0
  29. data/lib/arbre/rails/rspec/arbre_support.rb +61 -0
  30. data/lib/arbre/rails/rspec.rb +2 -0
  31. data/lib/arbre/rails/template_handler.rb +32 -0
  32. data/lib/arbre/rails.rb +35 -0
  33. data/lib/arbre/rspec/be_rendered_as_matcher.rb +103 -0
  34. data/lib/arbre/rspec/be_scripted_as_matcher.rb +68 -0
  35. data/lib/arbre/rspec/contain_script_matcher.rb +64 -0
  36. data/lib/arbre/rspec.rb +3 -0
  37. data/lib/arbre/text_node.rb +35 -0
  38. data/lib/arbre/version.rb +3 -0
  39. data/lib/arbre.rb +27 -0
  40. data/spec/arbre/integration/html_document_spec.rb +90 -0
  41. data/spec/arbre/integration/html_spec.rb +283 -0
  42. data/spec/arbre/integration/querying_spec.rb +187 -0
  43. data/spec/arbre/integration/rails_spec.rb +183 -0
  44. data/spec/arbre/rails/rspec/arbre_support_spec.rb +75 -0
  45. data/spec/arbre/rspec/be_rendered_as_matcher_spec.rb +80 -0
  46. data/spec/arbre/rspec/be_scripted_as_matcher_spec.rb +61 -0
  47. data/spec/arbre/rspec/contain_script_matcher_spec.rb +40 -0
  48. data/spec/arbre/support/arbre_example_group.rb +0 -0
  49. data/spec/arbre/unit/child_element_collection_spec.rb +146 -0
  50. data/spec/arbre/unit/container_spec.rb +23 -0
  51. data/spec/arbre/unit/context_spec.rb +95 -0
  52. data/spec/arbre/unit/element/building_spec.rb +300 -0
  53. data/spec/arbre/unit/element_collection_spec.rb +169 -0
  54. data/spec/arbre/unit/element_spec.rb +297 -0
  55. data/spec/arbre/unit/html/attributes_spec.rb +219 -0
  56. data/spec/arbre/unit/html/class_list_spec.rb +109 -0
  57. data/spec/arbre/unit/html/comment_spec.rb +42 -0
  58. data/spec/arbre/unit/html/querying_spec.rb +32 -0
  59. data/spec/arbre/unit/html/tag_spec.rb +300 -0
  60. data/spec/arbre/unit/rails/layouts_spec.rb +127 -0
  61. data/spec/arbre/unit/text_node_spec.rb +40 -0
  62. data/spec/rails/app/controllers/example_controller.rb +18 -0
  63. data/spec/rails/app/views/example/_arbre_partial.html.arb +7 -0
  64. data/spec/rails/app/views/example/_erb_partial.html.erb +1 -0
  65. data/spec/rails/app/views/example/arbre.html.arb +1 -0
  66. data/spec/rails/app/views/example/arbre_partial_result.html.arb +3 -0
  67. data/spec/rails/app/views/example/erb.html.erb +5 -0
  68. data/spec/rails/app/views/example/erb_partial_result.html.arb +3 -0
  69. data/spec/rails/app/views/example/partials.html.arb +11 -0
  70. data/spec/rails/app/views/layouts/empty.html.arb +1 -0
  71. data/spec/rails/app/views/layouts/with_title.html.arb +5 -0
  72. data/spec/rails/config/routes.rb +4 -0
  73. data/spec/rails_spec_helper.rb +13 -0
  74. data/spec/spec_helper.rb +20 -0
  75. data/spec/support/arbre_example_group.rb +19 -0
  76. metadata +254 -0
@@ -0,0 +1,67 @@
1
+ module Arbre
2
+ module Html
3
+
4
+ # This file creates a class for all known HTML 5 tags. You can derive
5
+ # from these classes to build specialized versions.
6
+
7
+ SELF_CLOSING_TAGS = %w[
8
+ input img col br meta link
9
+ ]
10
+ OTHER_TAGS = %w[
11
+ a abbr address area article aside audio b base
12
+ bdo blockquote body button canvas caption cite
13
+ code colgroup command datalist dd del details
14
+ dfn div dl dt em embed fieldset figcaption figure
15
+ footer form h1 h2 h3 h4 h5 h6 head header hgroup
16
+ hr html i iframe ins keygen kbd label
17
+ legend li map mark menu meter nav noscript
18
+ object ol optgroup option output pre progress q
19
+ s samp script section select small source span
20
+ strong style sub summary sup table tbody td
21
+ textarea tfoot th thead time title tr ul var video
22
+ ]
23
+
24
+ def self.create_tag_class(tag, builder_method = tag.to_sym, self_closing: false)
25
+ self_closing_method = self_closing ? 'def self_closing_tag?() true end' : ''
26
+
27
+ module_eval <<-RUBY, __FILE__, __LINE__+1
28
+ class #{tag.camelize} < Tag
29
+ builder_method #{builder_method.inspect}
30
+ tag #{tag.inspect}
31
+
32
+ #{self_closing_method}
33
+ end
34
+ RUBY
35
+ end
36
+
37
+ SELF_CLOSING_TAGS.each do |tag|
38
+ create_tag_class tag, self_closing: true
39
+ end
40
+ OTHER_TAGS.each do |tag|
41
+ create_tag_class tag
42
+ end
43
+
44
+ create_tag_class 'p', :para
45
+
46
+ Input.class_eval do
47
+ attribute :type
48
+ end
49
+
50
+ Table.class_eval do
51
+ def initialize(*)
52
+ super
53
+ set_table_tag_defaults
54
+ end
55
+
56
+ protected
57
+
58
+ # Set some good defaults for tables
59
+ def set_table_tag_defaults
60
+ set_attribute :border, 0
61
+ set_attribute :cellspacing, 0
62
+ set_attribute :cellpadding, 0
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,256 @@
1
+ require 'active_support/core_ext/object/blank'
2
+
3
+ module Arbre
4
+ module Html
5
+
6
+ ######
7
+ # Querying mixin
8
+
9
+ # Adds querying to the Arbre elements.
10
+ module Querying
11
+
12
+ # Finds elements by running a query.
13
+ def find(query)
14
+ Query.new(self).execute(query)
15
+ end
16
+
17
+ # Finds the first element from the given query.
18
+ def find_first(query)
19
+ find(query).first
20
+ end
21
+
22
+ # Finds all child tags of this element. This operation sees through all elements that
23
+ # are not a tag.
24
+ # @return [ElementCollection]
25
+ def child_tags
26
+ result = ElementCollection.new
27
+
28
+ children.each do |child|
29
+ if child.is_a?(Tag)
30
+ result << child
31
+ else
32
+ result.concat child.child_tags
33
+ end
34
+ end
35
+
36
+ result
37
+ end
38
+
39
+ # Finds all descendant tags of this element. This operation sees through all elements that
40
+ # are not a tag.
41
+ # @return [ElementCollection]
42
+ def descendant_tags
43
+ result = ElementCollection.new
44
+
45
+ children.each do |child|
46
+ result << child if child.is_a?(Tag)
47
+ result.concat child.descendant_tags
48
+ end
49
+
50
+ result
51
+ end
52
+
53
+ # Finds elements by a combination of tag and / or classes.
54
+ def find_by_tag_and_classes(tag = nil, classes = nil)
55
+ tag_matches = ->(el) { tag.nil? || el.tag_name == tag }
56
+ classes_match = ->(el) { classes.nil? || classes.all? { |cls| el.has_class?(cls) } }
57
+
58
+ found = []
59
+ children.each do |child|
60
+ if child.is_a?(Tag)
61
+ found << child if tag_matches[child] && classes_match[child]
62
+ end
63
+
64
+ found += child.find_by_tag_and_classes(tag, classes)
65
+ end
66
+
67
+ ElementCollection.new(found)
68
+ end
69
+
70
+ def find_by_tag(tag)
71
+ find_by_tag_and_classes tag
72
+ end
73
+
74
+ def find_by_classes(classes)
75
+ find_by_tag_and_classes nil, classes
76
+ end
77
+
78
+ # Finds an element by an ID. Note that only the first element with the specified ID
79
+ # is retrieved.
80
+ def find_by_id(id)
81
+ children.each do |child|
82
+ found = if child.is_a?(Tag) && child.id == id
83
+ child
84
+ else
85
+ child.find_by_id(id)
86
+ end
87
+ return found if found
88
+ end
89
+
90
+ nil
91
+ end
92
+
93
+ end
94
+
95
+ ######
96
+ # Query class
97
+
98
+ # Class to find tags from a given root tag / element based on a CSS-like query.
99
+ class Query
100
+
101
+ ######
102
+ # Initialization
103
+
104
+ def initialize(root)
105
+ @root = root
106
+ end
107
+
108
+ attr_reader :root
109
+
110
+ ######
111
+ # Constants
112
+
113
+ # @api private
114
+ CSS_IDENTIFIER = /[-_a-z][-_a-z0-9]*/
115
+
116
+ # @api private
117
+ CSS_SCAN = %r[
118
+
119
+ # Child node operator
120
+ (>)?
121
+
122
+ \s*
123
+
124
+ (?:
125
+
126
+ (\*)
127
+
128
+ |
129
+
130
+ # Tag name
131
+ (#{CSS_IDENTIFIER})?
132
+
133
+ # ID
134
+ (?:\#(#{CSS_IDENTIFIER}))?
135
+
136
+ # Class
137
+ ((?:\.#{CSS_IDENTIFIER})+)?
138
+
139
+ # Pseudo
140
+ ((?::#{CSS_IDENTIFIER})+)?
141
+
142
+ # Attributes
143
+ ((?:\[.+?\])+)?
144
+
145
+ )
146
+
147
+ ]x
148
+
149
+ ######
150
+ # Execution
151
+
152
+ # Executes the given query.
153
+ def execute(query)
154
+ # Sanitize the query for processing.
155
+ query = query.downcase.squeeze(' ')
156
+
157
+ result = []
158
+
159
+ selectors = query.split(',').map(&:strip).reject(&:blank?)
160
+
161
+ selectors.map do |selector|
162
+ tags = [ root ]
163
+
164
+ # Run through all segments in the selector and process them one by one.
165
+ selector.scan CSS_SCAN do |operator, all, tag, id, classes, pseudos, attributes|
166
+ next unless all || tag || id || classes || pseudos || attributes
167
+
168
+ classes = classes.split('.').reject(&:blank?) if classes
169
+ pseudos = pseudos.split(':').reject(&:blank?) if pseudos
170
+
171
+ # First process combinations of operator, all and id.
172
+ tags = case operator
173
+ when '>' then find_children(tags, tag, id, classes)
174
+ else find_descendants(tags, tag, id, classes)
175
+ end
176
+
177
+ filter_by_pseudos tags, pseudos if pseudos
178
+ filter_by_attributes tags, attributes if attributes
179
+ end
180
+
181
+ result.concat tags
182
+ end
183
+
184
+ # Convert to an element collection.
185
+ ElementCollection.new(result)
186
+ end
187
+
188
+ ######
189
+ # Internal methods
190
+
191
+ private
192
+
193
+ def find_children(tags, tag_name, id, classes)
194
+ children = tags.inject([]) { |result, tag| result += tag.child_tags }
195
+
196
+ children.select! { |tag| tag.tag_name == tag_name } if tag_name
197
+ children.select! { |tag| classes.all? { |cls| tag.has_class?(cls) } } if classes
198
+ children.select! { |tag| tag.id == id } if id
199
+
200
+ children
201
+ end
202
+
203
+ def find_descendants(tags, tag_name, id, classes)
204
+ if id
205
+ # Find all children by ID.
206
+ children = tags.map{ |tag| tag.find_by_id(id) }.compact
207
+
208
+ # If a tag or classes are specified as well, filter the children.
209
+ children.select! { |tag| tag.tag_name == tag_name } if tag_name
210
+ children.select! { |tag| classes.all? { |cls| tag.has_class?(cls) } } if classes
211
+
212
+ children
213
+ elsif tag_name || classes
214
+ # All descendants matching tag and/or classes.
215
+ tags.inject([]) { |result, tag| result += tag.find_by_tag_and_classes(tag_name, classes) }
216
+ else
217
+ # All descendants.
218
+ tags.inject([]) { |result, tag| result += tag.descendant_tags }
219
+ end
220
+ end
221
+
222
+ def filter_by_pseudos(tags, pseudos)
223
+ pseudos.each do |pseudo|
224
+ case pseudo
225
+ when 'first'
226
+ tags.slice! 1..-1
227
+ when 'last'
228
+ tags.slice! 0..-2
229
+ when 'first-child'
230
+ tags.select! do |tag|
231
+ tag == tag.parent.children.first
232
+ end
233
+ when 'last-child'
234
+ tags.select! do |tag|
235
+ tag == tag.parent.children.last
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ def filter_by_attributes(tags, attributes)
242
+ attributes.scan(/\[(.+?=".+?")\]|\[(.+?)\]/).each do |quoted, simple|
243
+ key, value = (quoted||simple).split('=')
244
+ value = $1 if value =~ /^"(.+?)"$/
245
+ if value
246
+ tags.select! { |tag| tag[key] == value }
247
+ else
248
+ tags.select! { |tag| tag.has_attribute?(key) }
249
+ end
250
+ end
251
+ end
252
+
253
+ end
254
+
255
+ end
256
+ end
@@ -0,0 +1,317 @@
1
+ require 'erb'
2
+
3
+ module Arbre
4
+ module Html
5
+
6
+ # HTML tag element. Has attributes and is rendered as a HTML tag.
7
+ class Tag < Element
8
+
9
+ ######
10
+ # Initialization
11
+
12
+ def initialize(*)
13
+ super
14
+
15
+ @attributes = Attributes.new
16
+ end
17
+
18
+ ######
19
+ # Attributes
20
+
21
+ # Override this to provide a proper tag name.
22
+ # You can also use method {.tag} for the same purpose.
23
+ def tag_name
24
+ raise NotImplementedError, "method `tag_name' not implemented for #{self.class.name}"
25
+ end
26
+
27
+ # Override this if you want to give your tag a default ID.
28
+ # You can also use method {.id} for the same purpose.
29
+ def tag_id
30
+ end
31
+
32
+ # Override this if you want to give your tag some default classes.
33
+ # You can also use method {.classes} for the same purpose.
34
+ def tag_classes
35
+ end
36
+
37
+ attr_reader :attributes
38
+
39
+ ######
40
+ # Building
41
+
42
+ # Builds a tag.
43
+ #
44
+ # Any remaining keyword arguments that are received by this method are merged
45
+ # into the attributes array. This means that in your subclass, you can use
46
+ # keyword arguments, if you always end with +**extra+ which you pass on to this
47
+ # method.
48
+ #
49
+ # @param [String] content
50
+ # Any raw content for in the tag.
51
+ # @param [Hash] attributes
52
+ # HTML attributes to render.
53
+ def build!(*args, **extra)
54
+ attributes = args.extract_options!
55
+
56
+ self.content = args.first unless args.empty?
57
+ self.id ||= tag_id
58
+
59
+ attributes.update extra
60
+
61
+ # Take out attributes that have a corresponding '<attribute>=' method, so that
62
+ # they can be processed better.
63
+ attributes.keys.each do |name|
64
+ next if name.to_s == 'content'
65
+ next if helpers && helpers.respond_to?(:"#{name}=")
66
+
67
+ send :"#{name}=", attributes.delete(name) if respond_to?(:"#{name}=")
68
+ end
69
+
70
+ # Set all other attributes normally.
71
+ self.attributes.update attributes
72
+
73
+ # Add classes now, so as to not overwrite these with a :class argument.
74
+ add_class tag_classes.join(' ') if tag_classes.present?
75
+
76
+ super()
77
+ end
78
+
79
+ ######
80
+ # Attributes
81
+
82
+ class << self
83
+
84
+ # Defines an HTML attribute accessor.
85
+ #
86
+ # == Example
87
+ #
88
+ # class CheckBox < Tag
89
+ #
90
+ # def tag_name
91
+ # 'input'
92
+ # end
93
+ #
94
+ # attribute :value
95
+ # attribute :checked, boolean: true
96
+ #
97
+ # def build
98
+ # self[:type] = 'checkbox'
99
+ # self.value = '1' # equivalent to self[:value] = '1'
100
+ # self.checked = true # equivalent to self[:checked] = 'checked'
101
+ # self.checked = false # equivalent to self[:checked] = nil, i.e. removes the attribute
102
+ # end
103
+ #
104
+ # end
105
+ def attribute(*attributes, boolean: false)
106
+ attributes.each do |attribute|
107
+ if boolean
108
+ class_eval <<-RUBY, __FILE__, __LINE__+1
109
+ def #{attribute}
110
+ has_attribute? :#{attribute}
111
+ end
112
+ def #{attribute}=(value)
113
+ self[:#{attribute}] = !!value
114
+ end
115
+ RUBY
116
+ else
117
+ class_eval <<-RUBY, __FILE__, __LINE__+1
118
+ def #{attribute}
119
+ self[:#{attribute}]
120
+ end
121
+ def #{attribute}=(value)
122
+ self[:#{attribute}] = value
123
+ end
124
+ RUBY
125
+ end
126
+ end
127
+ end
128
+
129
+ # Defines the tag name for this class and derived classes. This is a DSL-alternative to
130
+ # defining method tag_name.
131
+ #
132
+ # == Usage
133
+ #
134
+ # The following two are equivalent:
135
+ #
136
+ # tag 'div'
137
+ #
138
+ # and
139
+ #
140
+ # def tag_name
141
+ # 'div'
142
+ # end
143
+ def tag(tag)
144
+ class_eval <<-RUBY, __FILE__, __LINE__+1
145
+ def tag_name
146
+ #{tag.to_s.inspect}
147
+ end
148
+ RUBY
149
+ end
150
+
151
+ # Defines the tag ID attribute for this class and derived classes.
152
+ #
153
+ # == Usage
154
+ #
155
+ # The following two are equivalent:
156
+ #
157
+ # id 'my-div'
158
+ #
159
+ # and
160
+ #
161
+ # def build!(*)
162
+ # super
163
+ # self.id = 'my-div'
164
+ # end
165
+ def id(id)
166
+ class_eval <<-RUBY, __FILE__, __LINE__+1
167
+ def tag_id
168
+ #{id.to_s.inspect}
169
+ end
170
+ RUBY
171
+ end
172
+
173
+ # Defines the tag (CSS) classes for this class and derived classes.
174
+ #
175
+ # == Usage
176
+ #
177
+ # The following two are equivalent:
178
+ #
179
+ # classes 'dashboard', 'floatright'
180
+ #
181
+ # and
182
+ #
183
+ # def build!(*)
184
+ # super
185
+ # add_class 'dashboard'
186
+ # add_class 'floatright'
187
+ # end
188
+ def classes(*classes)
189
+ classes = classes.flatten.map(&:to_s)
190
+ class_eval <<-RUBY, __FILE__, __LINE__+1
191
+ def tag_classes
192
+ #{classes.inspect}
193
+ end
194
+ RUBY
195
+ end
196
+
197
+ end
198
+
199
+ def [](attribute)
200
+ attributes[attribute]
201
+ end
202
+ alias_method :get_attribute, :[]
203
+
204
+ def []=(attribute, value)
205
+ attributes[attribute] = value
206
+ end
207
+ alias_method :set_attribute, :[]=
208
+
209
+ def has_attribute?(name)
210
+ attributes.has_key? name
211
+ end
212
+
213
+ ######
214
+ # ID, class, style
215
+
216
+ attribute :id
217
+ def generate_id!
218
+ self.id = object_id
219
+ end
220
+
221
+ def add_class(classes)
222
+ self[:class].add classes if classes.present?
223
+ end
224
+
225
+ def remove_class(classes)
226
+ self[:class].remove classes
227
+ self[:class] = nil if self[:class].empty?
228
+ end
229
+
230
+ def classes=(classes)
231
+ self[:class] = classes.present? ? classes : nil
232
+ end
233
+
234
+ def classes
235
+ self[:class]
236
+ end
237
+
238
+ def has_class?(klass)
239
+ klass.split(' ').all? { |cls| classes.include?(cls) }
240
+ end
241
+
242
+ ######
243
+ # Rendering
244
+
245
+ def to_s
246
+ indent opening_tag, content, closing_tag
247
+ end
248
+
249
+ private
250
+
251
+ def opening_tag
252
+ attrs = " #{attributes}" unless attributes.empty?
253
+ "<#{tag_name}#{attrs}>".html_safe
254
+ end
255
+
256
+ def closing_tag
257
+ "</#{tag_name}>".html_safe
258
+ end
259
+
260
+ def self_closing_tag
261
+ attrs = " #{attributes}" unless attributes.empty?
262
+ "<#{tag_name}#{attrs}/>".html_safe
263
+ end
264
+
265
+ INDENT_SIZE = 2
266
+
267
+ def indent(open_tag, child_content, close_tag)
268
+ spaces = (' ' * indent_level * INDENT_SIZE)
269
+
270
+ html = ActiveSupport::SafeBuffer.new
271
+
272
+ if empty? && self_closing_tag?
273
+ html << spaces << self_closing_tag
274
+ elsif empty? || one_line?
275
+ html << spaces << open_tag << child_content << close_tag
276
+ else
277
+ html << spaces << open_tag << "\n"
278
+ html << child_content << "\n"
279
+ html << spaces << close_tag
280
+ end
281
+
282
+ html
283
+ end
284
+
285
+ public
286
+
287
+ def empty?
288
+ children.empty?
289
+ end
290
+
291
+ def one_line?
292
+ children.length == 1 &&
293
+ children.first.is_a?(TextNode) &&
294
+ !children.first.text.include?("\n")
295
+ end
296
+
297
+ def self_closing_tag?
298
+ false
299
+ end
300
+
301
+ ######
302
+ # Misc
303
+
304
+ def inspect
305
+ tag_desc = tag_name
306
+ tag_desc << "(#{self.class.name})" if self.class.name.demodulize != tag_name.camelize
307
+ tag_desc << "##{id}" if id
308
+ tag_desc << classes.map{ |cls| ".#{cls}" }.join
309
+ tag_desc << "[type=#{self[:type]}]" if has_attribute?(:type)
310
+
311
+ "<#{tag_desc}>"
312
+ end
313
+
314
+ end
315
+
316
+ end
317
+ end