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.
- checksums.yaml +7 -0
- data/.gitignore +30 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +75 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +93 -0
- data/LICENSE +20 -0
- data/README.md +92 -0
- data/Rakefile +7 -0
- data/arbre.gemspec +28 -0
- data/lib/arbre/child_element_collection.rb +86 -0
- data/lib/arbre/container.rb +20 -0
- data/lib/arbre/context.rb +83 -0
- data/lib/arbre/element/building.rb +151 -0
- data/lib/arbre/element.rb +194 -0
- data/lib/arbre/element_collection.rb +93 -0
- data/lib/arbre/html/attributes.rb +91 -0
- data/lib/arbre/html/class_list.rb +53 -0
- data/lib/arbre/html/comment.rb +47 -0
- data/lib/arbre/html/document.rb +93 -0
- data/lib/arbre/html/html_tags.rb +67 -0
- data/lib/arbre/html/querying.rb +256 -0
- data/lib/arbre/html/tag.rb +317 -0
- data/lib/arbre/rails/layouts.rb +126 -0
- data/lib/arbre/rails/legacy_document.rb +29 -0
- data/lib/arbre/rails/rendering.rb +76 -0
- data/lib/arbre/rails/rspec/arbre_support.rb +61 -0
- data/lib/arbre/rails/rspec.rb +2 -0
- data/lib/arbre/rails/template_handler.rb +32 -0
- data/lib/arbre/rails.rb +35 -0
- data/lib/arbre/rspec/be_rendered_as_matcher.rb +103 -0
- data/lib/arbre/rspec/be_scripted_as_matcher.rb +68 -0
- data/lib/arbre/rspec/contain_script_matcher.rb +64 -0
- data/lib/arbre/rspec.rb +3 -0
- data/lib/arbre/text_node.rb +35 -0
- data/lib/arbre/version.rb +3 -0
- data/lib/arbre.rb +27 -0
- data/spec/arbre/integration/html_document_spec.rb +90 -0
- data/spec/arbre/integration/html_spec.rb +283 -0
- data/spec/arbre/integration/querying_spec.rb +187 -0
- data/spec/arbre/integration/rails_spec.rb +183 -0
- data/spec/arbre/rails/rspec/arbre_support_spec.rb +75 -0
- data/spec/arbre/rspec/be_rendered_as_matcher_spec.rb +80 -0
- data/spec/arbre/rspec/be_scripted_as_matcher_spec.rb +61 -0
- data/spec/arbre/rspec/contain_script_matcher_spec.rb +40 -0
- data/spec/arbre/support/arbre_example_group.rb +0 -0
- data/spec/arbre/unit/child_element_collection_spec.rb +146 -0
- data/spec/arbre/unit/container_spec.rb +23 -0
- data/spec/arbre/unit/context_spec.rb +95 -0
- data/spec/arbre/unit/element/building_spec.rb +300 -0
- data/spec/arbre/unit/element_collection_spec.rb +169 -0
- data/spec/arbre/unit/element_spec.rb +297 -0
- data/spec/arbre/unit/html/attributes_spec.rb +219 -0
- data/spec/arbre/unit/html/class_list_spec.rb +109 -0
- data/spec/arbre/unit/html/comment_spec.rb +42 -0
- data/spec/arbre/unit/html/querying_spec.rb +32 -0
- data/spec/arbre/unit/html/tag_spec.rb +300 -0
- data/spec/arbre/unit/rails/layouts_spec.rb +127 -0
- data/spec/arbre/unit/text_node_spec.rb +40 -0
- data/spec/rails/app/controllers/example_controller.rb +18 -0
- data/spec/rails/app/views/example/_arbre_partial.html.arb +7 -0
- data/spec/rails/app/views/example/_erb_partial.html.erb +1 -0
- data/spec/rails/app/views/example/arbre.html.arb +1 -0
- data/spec/rails/app/views/example/arbre_partial_result.html.arb +3 -0
- data/spec/rails/app/views/example/erb.html.erb +5 -0
- data/spec/rails/app/views/example/erb_partial_result.html.arb +3 -0
- data/spec/rails/app/views/example/partials.html.arb +11 -0
- data/spec/rails/app/views/layouts/empty.html.arb +1 -0
- data/spec/rails/app/views/layouts/with_title.html.arb +5 -0
- data/spec/rails/config/routes.rb +4 -0
- data/spec/rails_spec_helper.rb +13 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/arbre_example_group.rb +19 -0
- 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
|