arbre2 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|