iron-web 1.0.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.
- data/.rspec +1 -0
- data/History.txt +8 -0
- data/LICENSE +20 -0
- data/README.rdoc +91 -0
- data/Version.txt +1 -0
- data/lib/iron/web.rb +8 -0
- data/lib/iron/web/color.rb +267 -0
- data/lib/iron/web/html.rb +150 -0
- data/lib/iron/web/html/element.rb +154 -0
- data/lib/iron/web/string.rb +60 -0
- data/lib/iron/web/url.rb +228 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/web/color_spec.rb +58 -0
- data/spec/web/element_spec.rb +69 -0
- data/spec/web/html_safe_string_spec.rb +25 -0
- data/spec/web/html_spec.rb +20 -0
- data/spec/web/string_spec.rb +16 -0
- data/spec/web/url_spec.rb +101 -0
- metadata +87 -0
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require <%= File.join(File.expand_path(File.dirname(__FILE__)), 'spec', 'spec_helper.rb') %>
|
data/History.txt
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
== 1.0.0 / 2012-03-02
|
2
|
+
|
3
|
+
* Broke out extensions from older irongaze gem
|
4
|
+
* Updated Url and Color classes with misc fixes
|
5
|
+
* Added Url and Color spec coverage
|
6
|
+
* Major Html::Element spec work, basic Html spec coverage
|
7
|
+
* Add html_safe support for non-Rails environments
|
8
|
+
* Added to GitHub
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Irongaze Consulting LLC
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
'Software'), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
18
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
19
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
= GEM: iron-web
|
2
|
+
|
3
|
+
Written by Rob Morris @ Irongaze Consulting LLC (http://irongaze.com)
|
4
|
+
|
5
|
+
== DESCRIPTION
|
6
|
+
|
7
|
+
A set of classes useful in the generation of HTML content.
|
8
|
+
|
9
|
+
== CLASSES
|
10
|
+
|
11
|
+
* Url - a url parsing and manipulation class
|
12
|
+
|
13
|
+
>> url = Url.build('/home')
|
14
|
+
>> url.to_s
|
15
|
+
=> '/home'
|
16
|
+
>> url.params[:key] = 'some value'
|
17
|
+
>> url.to_s
|
18
|
+
=> '/home?key=some+value'
|
19
|
+
|
20
|
+
* Color - an RGB/A color manipulation class, useful in generating CSS etc.
|
21
|
+
|
22
|
+
>> rgb('#f00').to_s # Parse a color string into a Color then render it
|
23
|
+
=> '#FF0000'
|
24
|
+
>> rgb('#f00').darken(0.5).to_s # Darken bright red 50%
|
25
|
+
=> '#800000'
|
26
|
+
>> rgb('#f00').blend('#0f0', 0.5).to_s # Blend pure red and pure green, get medium yellow
|
27
|
+
=> "#807F00"
|
28
|
+
|
29
|
+
* Html / Html::Element - a builder-syntax HTML generation class set
|
30
|
+
|
31
|
+
>> html = Html.build do |html|
|
32
|
+
>> html.div(:id => 'primary') {
|
33
|
+
>> html.h1('Title-town!')
|
34
|
+
>> }
|
35
|
+
>> html.div(:id => 'secondary') {|div|
|
36
|
+
>> div.style = 'text-align: center;'
|
37
|
+
>> div.text! "My body text..."
|
38
|
+
>> }
|
39
|
+
>> end
|
40
|
+
>> puts html
|
41
|
+
=> <div id="primary">
|
42
|
+
=> <h1>
|
43
|
+
=> Title-town!
|
44
|
+
=> </h1>
|
45
|
+
=> </div>
|
46
|
+
=> <div id="secondary" style="text-align: center;">
|
47
|
+
=> My body text...
|
48
|
+
=> </div>
|
49
|
+
|
50
|
+
== SYNOPSIS
|
51
|
+
|
52
|
+
To use:
|
53
|
+
|
54
|
+
require 'iron/web'
|
55
|
+
|
56
|
+
Sample usage of all components:
|
57
|
+
|
58
|
+
Html.build do |html|
|
59
|
+
html.h1('Hello World') {|h1|
|
60
|
+
h1.style = "color: #{rgb('#f00').darken};"
|
61
|
+
}
|
62
|
+
|
63
|
+
html.hr
|
64
|
+
|
65
|
+
homepage = Url.parse('http://irongaze.com')
|
66
|
+
homepage.fragment = 'incoming'
|
67
|
+
html.a('Say hello to my log file', :href => homepage)
|
68
|
+
end
|
69
|
+
|
70
|
+
Which would result in:
|
71
|
+
|
72
|
+
<h1 style="color: #CC0000;">
|
73
|
+
Hello World
|
74
|
+
</h1>
|
75
|
+
<hr>
|
76
|
+
<a href="http://irongaze.com#incoming">Say hello to my log file</a>
|
77
|
+
|
78
|
+
Notice the darkened and correctly formatted CSS color value and the newly fragment-ized url.
|
79
|
+
HTML elements are build by calling their tag name on the Html instance. Similarly, element
|
80
|
+
attributes are added by calling the attribute's name on the element instance, or by simply
|
81
|
+
setting them during construction.
|
82
|
+
|
83
|
+
== REQUIREMENTS
|
84
|
+
|
85
|
+
* iron-extensions gem
|
86
|
+
|
87
|
+
== INSTALL
|
88
|
+
|
89
|
+
To install, simply run:
|
90
|
+
|
91
|
+
sudo gem install iron-web
|
data/Version.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/lib/iron/web.rb
ADDED
@@ -0,0 +1,267 @@
|
|
1
|
+
|
2
|
+
# Implements a color (r,g,b + a) with conversion to/from web format (eg #aabbcc), and
|
3
|
+
# with a number of utilities to lighten, darken and blend values.
|
4
|
+
class Color
|
5
|
+
|
6
|
+
# Basic attributes - holds each channel as 0-255 fixnum
|
7
|
+
attr_reader :r, :g, :b, :a
|
8
|
+
|
9
|
+
# Table for conversion to hex
|
10
|
+
HEXVAL = (('0'..'9').to_a).concat(('A'..'F').to_a).freeze
|
11
|
+
# Default value for #darken, #lighten etc.
|
12
|
+
BRIGHTNESS_DEFAULT = 0.2
|
13
|
+
|
14
|
+
# Construct ourselves from whatever is passed in
|
15
|
+
def initialize(*args)
|
16
|
+
@r = 255
|
17
|
+
@g = 255
|
18
|
+
@b = 255
|
19
|
+
@a = 255
|
20
|
+
|
21
|
+
if args.size.between?(3,4)
|
22
|
+
self.r = args[0]
|
23
|
+
self.g = args[1]
|
24
|
+
self.b = args[2]
|
25
|
+
self.a = args[3] if args[3]
|
26
|
+
else
|
27
|
+
set(*args)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# All-purpose setter - pass in another Color, '#000000', rgb vals... whatever
|
32
|
+
def set(*args)
|
33
|
+
val = Color.parse(*args)
|
34
|
+
unless val.nil?
|
35
|
+
self.r = val.r
|
36
|
+
self.g = val.g
|
37
|
+
self.b = val.b
|
38
|
+
self.a = val.a
|
39
|
+
end
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# Test for equality, accepts string vals as well, eg Color.new('aaa') == '#AAAAAA' => true
|
44
|
+
def ==(val)
|
45
|
+
val = Color.parse(val)
|
46
|
+
return false if val.nil?
|
47
|
+
return r == val.r && g == val.g && b == val.b && a == val.a
|
48
|
+
end
|
49
|
+
|
50
|
+
# Setters for individual channels - take 0-255 or '00'-'FF' values
|
51
|
+
def r=(val); @r = from_hex(val); end
|
52
|
+
def g=(val); @g = from_hex(val); end
|
53
|
+
def b=(val); @b = from_hex(val); end
|
54
|
+
def a=(val); @a = from_hex(val); end
|
55
|
+
|
56
|
+
# Assigns a callback lambda to convert symbols into
|
57
|
+
# parse-able values. Sample usage:
|
58
|
+
# Color.lookup_callback = lambda {|val| Settings[:site][:colors].get!(val) }
|
59
|
+
# rgb(:off_white) # => '#f8f0ee'
|
60
|
+
def self.lookup_callback=(callback)
|
61
|
+
@lookup_callback = callback
|
62
|
+
end
|
63
|
+
|
64
|
+
# Get the lookup callback, if any
|
65
|
+
def self.lookup_callback
|
66
|
+
@lookup_callback
|
67
|
+
end
|
68
|
+
|
69
|
+
# Attempt to read in a string and parse it into values
|
70
|
+
def self.parse(*args)
|
71
|
+
case args.size
|
72
|
+
|
73
|
+
when 0 then
|
74
|
+
return nil
|
75
|
+
|
76
|
+
when 1 then
|
77
|
+
val = args[0]
|
78
|
+
|
79
|
+
# Trivial parse... :-)
|
80
|
+
return val if val.is_a?(Color)
|
81
|
+
|
82
|
+
# Lookup site settings if symbol
|
83
|
+
if val.is_a?(Symbol)
|
84
|
+
callback = Color.lookup_callback
|
85
|
+
return callback.nil? ? nil : self.parse(callback.call(val))
|
86
|
+
end
|
87
|
+
|
88
|
+
# Single value, assume grayscale
|
89
|
+
return Color.new(val, val, val) if val.is_a?(Fixnum)
|
90
|
+
|
91
|
+
# Assume string
|
92
|
+
str = val.to_s.upcase
|
93
|
+
str = str[/[0-9A-F]{3,8}/] || ''
|
94
|
+
case str.size
|
95
|
+
when 3, 4 then
|
96
|
+
r, g, b, a = str.scan(/[0-9A-F]/)
|
97
|
+
when 6, 8 then
|
98
|
+
r, g, b, a = str.scan(/[0-9A-F]{2}/)
|
99
|
+
else
|
100
|
+
return nil
|
101
|
+
end
|
102
|
+
a = 255 if a.nil?
|
103
|
+
|
104
|
+
return Color.new(r,g,b,a)
|
105
|
+
|
106
|
+
when 3,4 then
|
107
|
+
return Color.new(*args)
|
108
|
+
|
109
|
+
end
|
110
|
+
nil
|
111
|
+
end
|
112
|
+
|
113
|
+
def inspect
|
114
|
+
to_s
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_s(add_hash = true)
|
118
|
+
trans? ? to_rgba(add_hash) : to_rgb(add_hash)
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_rgb(add_hash = true)
|
122
|
+
(add_hash ? '#' : '') + to_hex(r) + to_hex(g) + to_hex(b)
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_rgba(add_hash = true)
|
126
|
+
to_rgb(add_hash) + to_hex(a)
|
127
|
+
end
|
128
|
+
|
129
|
+
def opaque?
|
130
|
+
@a == 255
|
131
|
+
end
|
132
|
+
|
133
|
+
def trans?
|
134
|
+
@a != 255
|
135
|
+
end
|
136
|
+
|
137
|
+
def grayscale?
|
138
|
+
@r == @g && @g == @b
|
139
|
+
end
|
140
|
+
|
141
|
+
# Lighten color by amt
|
142
|
+
def lighten(amt = BRIGHTNESS_DEFAULT)
|
143
|
+
return self if amt <= 0
|
144
|
+
return WHITE if amt >= 1.0
|
145
|
+
val = Color.new(self)
|
146
|
+
val.r += ((255-val.r) * amt).to_i
|
147
|
+
val.g += ((255-val.g) * amt).to_i
|
148
|
+
val.b += ((255-val.b) * amt).to_i
|
149
|
+
val
|
150
|
+
end
|
151
|
+
|
152
|
+
def lighten!(amt = BRIGHTNESS_DEFAULT)
|
153
|
+
set(lighten(amt))
|
154
|
+
self
|
155
|
+
end
|
156
|
+
|
157
|
+
# Darken color by amt
|
158
|
+
def darken(amt = BRIGHTNESS_DEFAULT)
|
159
|
+
return self if amt <= 0
|
160
|
+
return BLACK if amt >= 1.0
|
161
|
+
val = Color.new(self)
|
162
|
+
val.r -= (val.r * amt).to_i
|
163
|
+
val.g -= (val.g * amt).to_i
|
164
|
+
val.b -= (val.b * amt).to_i
|
165
|
+
val
|
166
|
+
end
|
167
|
+
|
168
|
+
def darken!(amt = BRIGHTNESS_DEFAULT)
|
169
|
+
set(darken(amt))
|
170
|
+
self
|
171
|
+
end
|
172
|
+
|
173
|
+
# Go towards middle of brightness scale, based on whether color is dark or light.
|
174
|
+
def contrast(amt = BRIGHTNESS_DEFAULT)
|
175
|
+
dark? ? lighten(amt) : darken(amt)
|
176
|
+
end
|
177
|
+
|
178
|
+
def contrast!(amt = BRIGHTNESS_DEFAULT)
|
179
|
+
set(contrast(amt))
|
180
|
+
self
|
181
|
+
end
|
182
|
+
|
183
|
+
# Convert to grayscale
|
184
|
+
def grayscale
|
185
|
+
Color.new(self.brightness)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Compute our overall brightness, using perception-based weighting, from 0.0 to 1.0
|
189
|
+
def brightness
|
190
|
+
(0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b)
|
191
|
+
end
|
192
|
+
|
193
|
+
def dark?
|
194
|
+
brightness < 0.5
|
195
|
+
end
|
196
|
+
|
197
|
+
def light?
|
198
|
+
!dark?
|
199
|
+
end
|
200
|
+
|
201
|
+
def grayscale!
|
202
|
+
set(grayscale)
|
203
|
+
self
|
204
|
+
end
|
205
|
+
|
206
|
+
# Blend to a color amt % towards another color value
|
207
|
+
def blend(other, amt)
|
208
|
+
other = Color.parse(other)
|
209
|
+
return Color.new(self) if amt <= 0 || other.nil?
|
210
|
+
return Color.new(other) if amt >= 1.0
|
211
|
+
val = Color.new(self)
|
212
|
+
val.r += ((other.r - val.r)*amt).to_i
|
213
|
+
val.g += ((other.g - val.g)*amt).to_i
|
214
|
+
val.b += ((other.b - val.b)*amt).to_i
|
215
|
+
val
|
216
|
+
end
|
217
|
+
|
218
|
+
def blend!(other, amt)
|
219
|
+
set(blend(other, amt))
|
220
|
+
self
|
221
|
+
end
|
222
|
+
|
223
|
+
def self.blend(col1, col2, amt)
|
224
|
+
col1 = Color.parse(col1)
|
225
|
+
col2 = Color.parse(col2)
|
226
|
+
col1.blend(col2, amt)
|
227
|
+
end
|
228
|
+
|
229
|
+
def self.average(col1, col2)
|
230
|
+
self.blend(col1, col2, 0.5)
|
231
|
+
end
|
232
|
+
|
233
|
+
protected
|
234
|
+
|
235
|
+
# Convert int to string hex, eg 255 => 'FF'
|
236
|
+
def to_hex(val)
|
237
|
+
HEXVAL[val / 16] + HEXVAL[val % 16]
|
238
|
+
end
|
239
|
+
|
240
|
+
# Convert int or string to int, eg 80 => 80, 'FF' => 255, '7' => 119
|
241
|
+
def from_hex(val)
|
242
|
+
if val.is_a?(String)
|
243
|
+
# Double up if single char form
|
244
|
+
val = val + val if val.size == 1
|
245
|
+
# Convert to integer
|
246
|
+
val = val.hex
|
247
|
+
end
|
248
|
+
# Clamp
|
249
|
+
val = 0 if val < 0
|
250
|
+
val = 255 if val > 255
|
251
|
+
val
|
252
|
+
end
|
253
|
+
|
254
|
+
public
|
255
|
+
|
256
|
+
# Some constants for general use
|
257
|
+
WHITE = Color.new(255,255,255).freeze
|
258
|
+
BLACK = Color.new(0,0,0).freeze
|
259
|
+
|
260
|
+
end
|
261
|
+
|
262
|
+
# "Global" method for creating Color objects, eg:
|
263
|
+
# new_color = rgb(params[:new_color])
|
264
|
+
# style="border: 1px solid <%= rgb(10,50,80).lighten %>"
|
265
|
+
def rgb(*args)
|
266
|
+
Color.parse(*args)
|
267
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'iron/web/html/element'
|
2
|
+
|
3
|
+
# == Html Creation and Rendering
|
4
|
+
#
|
5
|
+
# This class, combined with the Html::Element class, provides a DSL for html creation, similar to the XmlBuilder class, but tailored for HTML generation.
|
6
|
+
#
|
7
|
+
# An Html class instance is an ordered collection of Html::Elements and Strings, that together can be rendered out as HTML.
|
8
|
+
#
|
9
|
+
# Usage:
|
10
|
+
#
|
11
|
+
# Html.build do |html|
|
12
|
+
# html.div(:id => 'some-div') {
|
13
|
+
# html.em('HTML is neat!')
|
14
|
+
# }
|
15
|
+
# end
|
16
|
+
class Html
|
17
|
+
|
18
|
+
# Constants
|
19
|
+
HTML_ESCAPE = {"&"=>"&", ">"=>">", "<"=>"<", "\""=>"""}.freeze
|
20
|
+
|
21
|
+
# Remove everything that would normally come from Object and Kernel etc. so our keys can be anything
|
22
|
+
instance_methods.each do |m|
|
23
|
+
keepers = [] #['inspect']
|
24
|
+
undef_method m if m =~ /^[a-z]+[0-9]?$/ && !keepers.include?(m)
|
25
|
+
end
|
26
|
+
|
27
|
+
# So we can behave as a collection
|
28
|
+
include Enumerable
|
29
|
+
undef_method :select #This is an HTML tag, dammit
|
30
|
+
|
31
|
+
# Primary entry point for HTML generation using these tools.
|
32
|
+
def self.build
|
33
|
+
builder = Html.new
|
34
|
+
yield builder if block_given?
|
35
|
+
builder.render.html_safe
|
36
|
+
end
|
37
|
+
|
38
|
+
# Ripped from Rails...
|
39
|
+
def self.escape_once(html)
|
40
|
+
return html if html.html_safe?
|
41
|
+
html.to_s.gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| HTML_ESCAPE[special] }.html_safe
|
42
|
+
end
|
43
|
+
|
44
|
+
# Sets up internal state, natch, and accepts a block that customizes the resulting object.
|
45
|
+
def initialize
|
46
|
+
@items = []
|
47
|
+
@item_stack = []
|
48
|
+
yield self if block_given?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Inserts an HTML comment (eg <!-- yo -->)
|
52
|
+
def comment!(str)
|
53
|
+
if str.include? "\n"
|
54
|
+
text! "<!--\n#{str}\n-->\n"
|
55
|
+
else
|
56
|
+
text! "<!-- #{str} -->\n"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Inserts raw text
|
61
|
+
def text!(str)
|
62
|
+
self << str
|
63
|
+
end
|
64
|
+
|
65
|
+
# Allow pushing new elements
|
66
|
+
def <<(new_item)
|
67
|
+
if @item_stack.empty?
|
68
|
+
@items << new_item
|
69
|
+
else
|
70
|
+
@item_stack.last.html << new_item
|
71
|
+
end
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
# Implement enumerable
|
76
|
+
def each
|
77
|
+
@items.each {|v| yield v} if block_given?
|
78
|
+
end
|
79
|
+
|
80
|
+
def count
|
81
|
+
@items.count
|
82
|
+
end
|
83
|
+
|
84
|
+
def empty?
|
85
|
+
@items.empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
def blank?
|
89
|
+
empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
# Create a new element explicitly
|
93
|
+
def tag(tag, *args, &block)
|
94
|
+
item = Html::Element.new(tag, *args)
|
95
|
+
self << item
|
96
|
+
if block
|
97
|
+
@item_stack.push item
|
98
|
+
block.call(item)
|
99
|
+
@item_stack.pop
|
100
|
+
end
|
101
|
+
return self
|
102
|
+
end
|
103
|
+
|
104
|
+
# Creates a new element on any method missing calls.
|
105
|
+
# Returns self, so you can chain calls (eg html.div('foo').span('bar') )
|
106
|
+
def method_missing(method, *args, &block)
|
107
|
+
parts = method.to_s.match(/^([a-z]+[0-9]?)$/)
|
108
|
+
if parts
|
109
|
+
# Assume it's a new element, create the tag
|
110
|
+
tag(parts[1], *args, &block)
|
111
|
+
else
|
112
|
+
# There really is no method...
|
113
|
+
super
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Renders out as html - accepts depth param to indicate level of indentation
|
118
|
+
def render(depth = 0, inblock = true)
|
119
|
+
# Convert elements to strings
|
120
|
+
@items.collect do |item|
|
121
|
+
if item.is_a?(String)
|
122
|
+
if inblock
|
123
|
+
inblock = false
|
124
|
+
' '*depth + item
|
125
|
+
else
|
126
|
+
item
|
127
|
+
end
|
128
|
+
elsif item.nil?
|
129
|
+
''
|
130
|
+
else
|
131
|
+
item.render(depth,inblock)
|
132
|
+
end
|
133
|
+
end.join('')
|
134
|
+
end
|
135
|
+
|
136
|
+
# Alias for #render
|
137
|
+
def to_s
|
138
|
+
render
|
139
|
+
end
|
140
|
+
|
141
|
+
# Alias for #render
|
142
|
+
def inspect
|
143
|
+
render
|
144
|
+
end
|
145
|
+
|
146
|
+
def is_a?(other)
|
147
|
+
return other == Html
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|