sprite-factory 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.
Files changed (71) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +207 -0
  3. data/Rakefile +67 -0
  4. data/bin/sf +46 -0
  5. data/lib/sprite_factory.rb +51 -0
  6. data/lib/sprite_factory/layout.rb +89 -0
  7. data/lib/sprite_factory/library/chunky_png.rb +31 -0
  8. data/lib/sprite_factory/library/rmagick.rb +32 -0
  9. data/lib/sprite_factory/runner.rb +204 -0
  10. data/lib/sprite_factory/style.rb +58 -0
  11. data/sprite_factory.gemspec +24 -0
  12. data/test/images/custom/custom.css +4 -0
  13. data/test/images/custom/running.png +0 -0
  14. data/test/images/custom/stopped.png +0 -0
  15. data/test/images/empty/readme.txt +1 -0
  16. data/test/images/formats/alice.gif +0 -0
  17. data/test/images/formats/monkey.gif +0 -0
  18. data/test/images/formats/spies.jpg +0 -0
  19. data/test/images/formats/thief.png +0 -0
  20. data/test/images/irregular/irregular1.png +0 -0
  21. data/test/images/irregular/irregular2.png +0 -0
  22. data/test/images/irregular/irregular3.png +0 -0
  23. data/test/images/irregular/irregular4.png +0 -0
  24. data/test/images/irregular/irregular5.png +0 -0
  25. data/test/images/irregular/readme.txt +2 -0
  26. data/test/images/reference/custom.css +22 -0
  27. data/test/images/reference/custom.png +0 -0
  28. data/test/images/reference/formats.css +22 -0
  29. data/test/images/reference/formats.png +0 -0
  30. data/test/images/reference/index.html +135 -0
  31. data/test/images/reference/irregular.css +24 -0
  32. data/test/images/reference/irregular.fixed.css +24 -0
  33. data/test/images/reference/irregular.fixed.png +0 -0
  34. data/test/images/reference/irregular.horizontal.css +24 -0
  35. data/test/images/reference/irregular.horizontal.png +0 -0
  36. data/test/images/reference/irregular.padded.css +24 -0
  37. data/test/images/reference/irregular.padded.png +0 -0
  38. data/test/images/reference/irregular.png +0 -0
  39. data/test/images/reference/irregular.sassy.css +38 -0
  40. data/test/images/reference/irregular.sassy.png +0 -0
  41. data/test/images/reference/irregular.sassy.sass +40 -0
  42. data/test/images/reference/irregular.vertical.css +24 -0
  43. data/test/images/reference/irregular.vertical.png +0 -0
  44. data/test/images/reference/regular.css +24 -0
  45. data/test/images/reference/regular.custom.css +24 -0
  46. data/test/images/reference/regular.custom.png +0 -0
  47. data/test/images/reference/regular.fixed.css +24 -0
  48. data/test/images/reference/regular.fixed.png +0 -0
  49. data/test/images/reference/regular.horizontal.css +24 -0
  50. data/test/images/reference/regular.horizontal.png +0 -0
  51. data/test/images/reference/regular.padded.css +24 -0
  52. data/test/images/reference/regular.padded.png +0 -0
  53. data/test/images/reference/regular.png +0 -0
  54. data/test/images/reference/regular.sassy.css +38 -0
  55. data/test/images/reference/regular.sassy.png +0 -0
  56. data/test/images/reference/regular.sassy.sass +40 -0
  57. data/test/images/reference/regular.vertical.css +24 -0
  58. data/test/images/reference/regular.vertical.png +0 -0
  59. data/test/images/reference/s.gif +0 -0
  60. data/test/images/regular/regular1.png +0 -0
  61. data/test/images/regular/regular2.png +0 -0
  62. data/test/images/regular/regular3.png +0 -0
  63. data/test/images/regular/regular4.png +0 -0
  64. data/test/images/regular/regular5.png +0 -0
  65. data/test/integration_test.rb +100 -0
  66. data/test/layout_test.rb +228 -0
  67. data/test/library_test.rb +57 -0
  68. data/test/runner_test.rb +156 -0
  69. data/test/style_test.rb +64 -0
  70. data/test/test_case.rb +127 -0
  71. metadata +159 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Jake Gordon and contributors
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
20
+
@@ -0,0 +1,207 @@
1
+ Sprite Factory
2
+ ==============
3
+
4
+ The sprite factory is a ruby library that can be used to generate
5
+ [CSS sprites](http://www.alistapart.com/articles/sprites). It combines
6
+ individual image files from a directory into a single unified sprite image
7
+ and creates an appropriate CSS stylesheet for use in your web application.
8
+
9
+ The library provides:
10
+
11
+ * both a ruby API and a command line script
12
+ * many customizable options
13
+ * support for any stylesheet syntax, including [CSS](http://www.w3.org/Style/CSS/) and [Sass](http://sass-lang.com/).
14
+ * support for any image library, including [RMagick](http://rmagick.rubyforge.org/) and [ChunkyPNG](https://github.com/wvanbergen/chunky_png).
15
+
16
+
17
+ Installation
18
+ ============
19
+
20
+ $ gem install sprite-factory
21
+
22
+ An image library is also required. SpriteFactory comes with built in support for
23
+ [RMagick](http://rmagick.rubyforge.org/) or
24
+ [ChunkyPng](https://github.com/wvanbergen/chunky_png) and is easily extensible to
25
+ use any image library of your choice.
26
+
27
+ _(see below for instructions to install an image library if you don't already have one.)_
28
+
29
+ Usage
30
+ =====
31
+
32
+ Use the `sf` command line script specifying the location of your images.
33
+
34
+ $ sf images/icons
35
+
36
+ This will combine the individual image files within that directory and generate:
37
+
38
+ * images/icons.png
39
+ * images/icons.css
40
+
41
+ You can also use the SpriteFactory class directly from your own code:
42
+
43
+ require 'sprite_factory'
44
+
45
+ SpriteFactory.run!('images/icons')
46
+
47
+ The original image name is used for the CSS class to show that image in HTML:
48
+
49
+ <img src='s.gif' class='high'>
50
+ <img src='s.gif' class='medium'>
51
+ <img src='s.gif' class='low'>
52
+
53
+ When using a framework such as Rails, you would usually DRY this up with a helper method:
54
+
55
+ def sprite_tag(name)
56
+ image_tag('s.gif', :class => name)
57
+ end
58
+
59
+ Customization
60
+ =============
61
+
62
+ Much of the behavior can be customized by overriding the following options:
63
+
64
+ - `:output` - specify output location for generated files
65
+ - `:layout` - specify layout algorithm (horizontal or vertical)
66
+ - `:style` - specify output style (css or sass)
67
+ - `:library` - specify image library to use (rmagick or chunkypng)
68
+ - `:selector` - specify custom css selector (see below)
69
+ - `:csspath` - specify custom path for css image url (see below)
70
+ - `:padding` - add padding to each sprite
71
+ - `:width` - fix width of each sprite to a specific size
72
+ - `:height` - fix height of each sprite to a specific size
73
+
74
+ Options can be passed as command line arguments to the `sf` script:
75
+
76
+ $ sf images/icons --style sass --layout vertical
77
+
78
+ Options can also be passed as the 2nd argument to the `#run!` method:
79
+
80
+ SpriteFactory.run!('images/icons', :style => :sass, :layout => :vertical)
81
+
82
+ You can see the results of many of these options by viewing the sample page that
83
+ comes with the gem in `test/images/reference/index.html`.
84
+
85
+ Customizing the CSS Selector
86
+ ============================
87
+
88
+ By default, the CSS generated is fairly simple. It assumes you will be using `<img>`
89
+ elements for your sprites, and that the basename of each individual file is suitable for
90
+ use as a CSS classname. For example:
91
+
92
+ img.high { width: 16px; height: 16px; background: url(images/icons.png) 0px 0px no-repeat; }
93
+ img.medium { width: 16px; height: 16px; background: url(images/icons.png) -16px 0px no-repeat; }
94
+ img.low { width: 16px; height: 16px; background: url(images/icons.png) -32px 0px no-repeat; }
95
+
96
+ If you want to use different selectors for your rules, you can provide the `:selector` option. For
97
+ example:
98
+
99
+ SpriteFactory.run!('images/icons', :selector => 'span.icon_')
100
+
101
+ will generate:
102
+
103
+ span.icon_high { width: 16px; height: 16px; background: url(images/icons.png) 0px 0px no-repeat; }
104
+ span.icon_medium { width: 16px; height: 16px; background: url(images/icons.png) -16px 0px no-repeat; }
105
+ span.icon_low { width: 16px; height: 16px; background: url(images/icons.png) -32px 0px no-repeat; }
106
+
107
+ Customizing the CSS Image Path
108
+ ==============================
109
+
110
+ Within the generated CSS file, it can be tricky to get the correct path to your unified
111
+ sprite image. For example, you might be hosting your images on Amazon S3, or if you are
112
+ building a Ruby on Rails application you might need to generate URL's using the `#image_path`
113
+ helper method to ensure it gets the appopriate cache-busting query parameter.
114
+
115
+ By default, the SpriteFactory generates simple url's that contain only the basename of the
116
+ unified sprite image, but you can control the generation of these url's using the :csspath
117
+ option:
118
+
119
+ For most CDN's, you can prepend a simple string to the image name:
120
+
121
+ SpriteFactory.run('images/icons',
122
+ :csspath => "http://s3.amazonaws.com/")
123
+
124
+ # generates: url(http://s3.amazonaws.com/icons.png)
125
+
126
+ For more control, you can provide a lambda function and generate your own paths:
127
+
128
+ SpriteFactory.run('images/icons',
129
+ :csspath => lambda{|image| image_path(image)})
130
+
131
+ # generates: url(/images/icons.png?v123456)
132
+
133
+ Customizing the entire CSS output
134
+ =================================
135
+
136
+ If you want **complete** control over the generated styles, you can pass a block to the `run!` method.
137
+
138
+ The block will be provided with information about each image, including the generated css attributes.
139
+ Whatever content the block returns will be inserted into the generated css file.
140
+
141
+ SpriteFactory.run!('images/timer') do |images|
142
+ rules = []
143
+ rules << "div.running img.button { cursor: pointer; #{images[:running][:style]} }"
144
+ rules << "div.stopped img.button { cursor: pointer; #{images[:stopped][:style]} }"
145
+ rules.join("\n")
146
+ end
147
+
148
+ The `images` argument is a hash, where each key is the basename of an image file, and the
149
+ value is a hash of image metadata that includes the following:
150
+
151
+ * `:style` - the default generated style
152
+ * `:cssx` - the css sprite x position
153
+ * `:cssy` - the css sprite y position
154
+ * `:cssw` - the css sprite width
155
+ * `:cssh` - the css sprite height
156
+ * `:x` - the image x position
157
+ * `:y` - the image y position
158
+ * `:width` - the image width
159
+ * `:height` - the image height
160
+
161
+ (*NOTE*: the image coords can differ form the css sprite coords when padding or fixed width/height options are specified)
162
+
163
+ Extending the Library
164
+ =====================
165
+
166
+ The sprite factory library can also be extended in a number of other ways.
167
+
168
+ * provide a custom layout algorithm in the `SpriteFactory::Layout` module.
169
+ * provide a custom style generator in the `SpriteFactory::Style` module.
170
+ * provide a custom image library in the `SpriteFactory::Library` module.
171
+
172
+ _(see existing code for examples of each)._
173
+
174
+ Installing an Image Library
175
+ ===========================
176
+
177
+ SpriteFactory comes with built in support for
178
+ [RMagick](http://rmagick.rubyforge.org/) or
179
+ [ChunkyPng](https://github.com/wvanbergen/chunky_png)
180
+
181
+ RMagick is the most flexible image libary to use, but requires ImageMagick
182
+ binaries, installation instructions for ubuntu:
183
+
184
+ $ sudo aptitude install imageMagick libMagickWand-dev
185
+ $ sudo gem install rmagick
186
+
187
+ ChunkyPng is lighter weight and has no binary requirements, but only supports
188
+ .png format. Installation is a simple gem install:
189
+
190
+ $ gem install chunky_png
191
+
192
+ SpriteFactory can also be easily extended to use the image library of your choice.
193
+
194
+ License
195
+ =======
196
+
197
+ See LICENSE file.
198
+
199
+ Contact
200
+ =======
201
+
202
+ You can reach me at [jake@codeincomplete.com](mailto:jake@codeincomplete.com), or via
203
+ my website: [Code inComplete](http://codeincomplete.com).
204
+
205
+
206
+
207
+
@@ -0,0 +1,67 @@
1
+ require 'rake/testtask'
2
+
3
+ #------------------------------------------------------------------------------
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ #------------------------------------------------------------------------------
11
+
12
+ desc "run a console with SpriteFactory loaded"
13
+ task :console do
14
+ system "irb -r #{File.expand_path('lib/sprite_factory', File.dirname(__FILE__))}"
15
+ end
16
+
17
+ #------------------------------------------------------------------------------
18
+
19
+ desc "regenerate test reference images"
20
+ task :reference do
21
+
22
+ require File.expand_path('lib/sprite_factory', File.dirname(__FILE__))
23
+
24
+ regenerate = lambda do |input, options = {}, &block|
25
+ output = options[:output] || input
26
+ SpriteFactory.run!(input, {:report => true}.merge(options), &block)
27
+ FileUtils.mv(output + "." + ( :png).to_s, 'test/images/reference')
28
+ FileUtils.mv(output + "." + (options[:style] || :css).to_s, 'test/images/reference')
29
+ end
30
+
31
+ regenerate.call('test/images/regular')
32
+ regenerate.call('test/images/regular', :output => 'test/images/regular.horizontal', :selector => 'img.horizontal_', :layout => :horizontal)
33
+ regenerate.call('test/images/regular', :output => 'test/images/regular.vertical', :selector => 'img.vertical_', :layout => :vertical)
34
+ regenerate.call('test/images/regular', :output => 'test/images/regular.padded', :selector => 'img.padded_', :padding => 10)
35
+ regenerate.call('test/images/regular', :output => 'test/images/regular.fixed', :selector => 'img.fixed_', :width => 100, :height => 100)
36
+ regenerate.call('test/images/regular', :output => 'test/images/regular.sassy', :selector => 'img.sassy_', :style => :sass)
37
+
38
+ regenerate.call('test/images/irregular')
39
+ regenerate.call('test/images/irregular', :output => 'test/images/irregular.horizontal', :selector => 'img.horizontal_', :layout => :horizontal)
40
+ regenerate.call('test/images/irregular', :output => 'test/images/irregular.vertical', :selector => 'img.vertical_', :layout => :vertical)
41
+ regenerate.call('test/images/irregular', :output => 'test/images/irregular.padded', :selector => 'img.padded_', :padding => 10)
42
+ regenerate.call('test/images/irregular', :output => 'test/images/irregular.fixed', :selector => 'img.fixed_', :width => 100, :height => 100)
43
+ regenerate.call('test/images/irregular', :output => 'test/images/irregular.sassy', :selector => 'img.sassy_', :style => :sass)
44
+
45
+ regenerate.call('test/images/custom', :output => 'test/images/custom') do |images|
46
+ rules = []
47
+ rules << "div.running img.button { cursor: pointer; #{images[:running][:style]} }"
48
+ rules << "div.stopped img.button { cursor: pointer; #{images[:stopped][:style]} }"
49
+ rules.join("\n")
50
+ end
51
+
52
+ regenerate.call('test/images/formats', :library => :rmagick)
53
+
54
+ end
55
+
56
+ #------------------------------------------------------------------------------
57
+
58
+ desc "convert reference test sass files to css"
59
+ task :sass do
60
+
61
+ `sass 'test/images/reference/regular.sassy.sass' 'test/images/reference/regular.sassy.css'`
62
+ `sass 'test/images/reference/irregular.sassy.sass' 'test/images/reference/irregular.sassy.css'`
63
+
64
+ end
65
+
66
+ #------------------------------------------------------------------------------
67
+
data/bin/sf ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.push File.expand_path("../lib", File.dirname(__FILE__)) # add sprite factory library to load path
4
+
5
+ require 'sprite_factory'
6
+ require 'optparse'
7
+
8
+ options = { :report => true }
9
+ op = OptionParser.new
10
+ op.banner = "#{SpriteFactory::DESCRIPTION}\nUsage: sprite <dir> [options]"
11
+
12
+ op.on("-h", "--help") do
13
+ puts op.to_s
14
+ exit
15
+ end
16
+
17
+ op.on("-v", "--version") do
18
+ puts SpriteFactory::VERSION
19
+ exit
20
+ end
21
+
22
+ output_help = "specify output location, without any extension"
23
+ layout_help = "specify layout orientation ( horizontal, vertical )"
24
+ style_help = "specify output style format ( css, sass )"
25
+ library_help = "specify image library to use ( rmagic, chunkypng )"
26
+ selector_help = "specify custom selector to use for each css rule ( default: 'img.' )"
27
+ csspath_help = "specify custom path to use for css image urls ( default: output file's basename )"
28
+
29
+ op.on("--output [PATH]", output_help) {|value| options[:output] = value }
30
+ op.on("--layout [ORIENTATION]", layout_help) {|value| options[:layout] = value }
31
+ op.on("--style [STYLE]", style_help) {|value| options[:style] = value }
32
+ op.on("--library [LIBRARY]", library_help) {|value| options[:library] = value }
33
+ op.on("--selector [SELECTOR]", selector_help) {|value| options[:selector] = value }
34
+ op.on("--csspath [CSSPATH]", csspath_help) {|value| options[:csspath] = value }
35
+
36
+ begin
37
+ op.parse!(ARGV)
38
+ raise "a single argument must be specified containing images to be sprited" if ARGV.empty?
39
+ SpriteFactory.run!(ARGV[0], options)
40
+ rescue Exception => ex
41
+ puts ex.message
42
+ puts op.to_s
43
+ exit
44
+ end
45
+
46
+
@@ -0,0 +1,51 @@
1
+ module SpriteFactory
2
+
3
+ #----------------------------------------------------------------------------
4
+
5
+ VERSION = "1.0.0"
6
+ SUMMARY = "Automatic CSS sprite generator"
7
+ DESCRIPTION = "Combines individual images from a directory into a single sprite image file and creates an appropriate CSS stylesheet"
8
+ LIB = File.dirname(__FILE__)
9
+
10
+ autoload :Runner, File.join(LIB, 'sprite_factory/runner') # controller that glues everything together
11
+ autoload :Layout, File.join(LIB, 'sprite_factory/layout') # layout calculations
12
+ autoload :Style, File.join(LIB, 'sprite_factory/style') # style generators
13
+
14
+ def self.run!(input, config = {}, &block)
15
+ Runner.new(input, config).run!(&block)
16
+ end
17
+
18
+ #
19
+ # fallback defaults for some options can be set at module level to
20
+ # avoid having to pass them to #run! every single time
21
+ #
22
+ class << self
23
+ attr_accessor :report
24
+ attr_accessor :style
25
+ attr_accessor :layout
26
+ attr_accessor :library
27
+ attr_accessor :selector
28
+ attr_accessor :csspath
29
+ end
30
+
31
+ #----------------------------------------------------------------------------
32
+
33
+ module Library # abstract module for using various image libraries
34
+
35
+ autoload :RMagick, File.join(LIB, 'sprite_factory/library/rmagick') # concrete module for using RMagick (loaded on demand)
36
+ autoload :ChunkyPng, File.join(LIB, 'sprite_factory/library/chunky_png') # concrete module for using ChunkyPng (ditto)
37
+
38
+ def self.rmagick
39
+ RMagick
40
+ end
41
+
42
+ def self.chunkypng
43
+ ChunkyPng
44
+ end
45
+
46
+ end
47
+
48
+ #----------------------------------------------------------------------------
49
+
50
+ end
51
+
@@ -0,0 +1,89 @@
1
+ module SpriteFactory
2
+ module Layout
3
+
4
+ #--------------------------------------------------------------------------
5
+
6
+ def self.horizontal(images, options = {})
7
+ width = options[:width]
8
+ height = options[:height]
9
+ hpadding = options[:hpadding] || 0
10
+ vpadding = options[:vpadding] || 0
11
+ max_height = height || ((2 * vpadding) + images.map{|i| i[:height]}.max)
12
+ x = 0
13
+ images.each do |i|
14
+
15
+ if width
16
+ i[:cssw] = width
17
+ i[:cssx] = x
18
+ i[:x] = x + (width - i[:width]) / 2
19
+ else
20
+ i[:cssw] = i[:width] + (2 * hpadding) # image width plus padding
21
+ i[:cssx] = x # anchored at x
22
+ i[:x] = i[:cssx] + hpadding # image drawn offset to account for padding
23
+ end
24
+
25
+ if height
26
+ i[:cssh] = height
27
+ i[:cssy] = 0
28
+ i[:y] = 0 + (height - i[:height]) / 2
29
+ else
30
+ i[:cssh] = i[:height] + (2 * vpadding) # image height plus padding
31
+ i[:cssy] = (max_height - i[:cssh]) / 2 # centered vertically
32
+ i[:y] = i[:cssy] + vpadding # image drawn offset to account for padding
33
+ end
34
+
35
+ x = x + i[:cssw]
36
+
37
+ end
38
+ { :width => x, :height => max_height }
39
+ end
40
+
41
+ #--------------------------------------------------------------------------
42
+
43
+ def self.vertical(images, options = {})
44
+ width = options[:width]
45
+ height = options[:height]
46
+ hpadding = options[:hpadding] || 0
47
+ vpadding = options[:vpadding] || 0
48
+ max_width = width || ((2 * hpadding) + images.map{|i| i[:width]}.max)
49
+ y = 0
50
+ images.each do |i|
51
+
52
+ if width
53
+ i[:cssw] = width
54
+ i[:cssx] = 0
55
+ i[:x] = 0 + (width - i[:width]) / 2
56
+ else
57
+ i[:cssw] = i[:width] + (2 * hpadding) # image width plus padding
58
+ i[:cssx] = (max_width - i[:cssw]) / 2 # centered horizontally
59
+ i[:x] = i[:cssx] + hpadding # image drawn offset to account for padding
60
+ end
61
+
62
+ if height
63
+ i[:cssh] = height
64
+ i[:cssy] = y
65
+ i[:y] = y + (height - i[:height]) / 2
66
+ else
67
+ i[:cssh] = i[:height] + (2 * vpadding) # image height plus padding
68
+ i[:cssy] = y # anchored at y
69
+ i[:y] = i[:cssy] + vpadding # image drawn offset to account for padding
70
+ end
71
+
72
+ y = y + i[:cssh]
73
+
74
+ end
75
+ { :width => max_width, :height => y }
76
+ end
77
+
78
+ #--------------------------------------------------------------------------
79
+
80
+ def self.knapsack(images)
81
+
82
+ raise NotImplementedError, "one day, when I have time, I'll do some kind of 'best-fit' algorithm"
83
+
84
+ end
85
+
86
+ #--------------------------------------------------------------------------
87
+
88
+ end
89
+ end