arbre2 2.1.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 (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