polymer 1.0.0.beta.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/Gemfile +8 -0
  2. data/History.md +126 -0
  3. data/LICENSE +28 -0
  4. data/README.md +229 -0
  5. data/Rakefile +186 -0
  6. data/bin/polymer +10 -0
  7. data/lib/polymer/cache.rb +106 -0
  8. data/lib/polymer/cli.rb +340 -0
  9. data/lib/polymer/core_ext.rb +78 -0
  10. data/lib/polymer/css_generator.rb +32 -0
  11. data/lib/polymer/deviant_finder.rb +76 -0
  12. data/lib/polymer/dsl.rb +283 -0
  13. data/lib/polymer/man/polymer-bond.1 +60 -0
  14. data/lib/polymer/man/polymer-bond.1.txt +66 -0
  15. data/lib/polymer/man/polymer-init.1 +33 -0
  16. data/lib/polymer/man/polymer-init.1.txt +42 -0
  17. data/lib/polymer/man/polymer-optimise.1 +23 -0
  18. data/lib/polymer/man/polymer-optimise.1.txt +25 -0
  19. data/lib/polymer/man/polymer-position.1 +39 -0
  20. data/lib/polymer/man/polymer-position.1.txt +42 -0
  21. data/lib/polymer/man/polymer.1 +50 -0
  22. data/lib/polymer/man/polymer.1.txt +60 -0
  23. data/lib/polymer/man/polymer.5 +130 -0
  24. data/lib/polymer/man/polymer.5.txt +145 -0
  25. data/lib/polymer/optimisation.rb +130 -0
  26. data/lib/polymer/project.rb +164 -0
  27. data/lib/polymer/sass_generator.rb +38 -0
  28. data/lib/polymer/source.rb +55 -0
  29. data/lib/polymer/sprite.rb +130 -0
  30. data/lib/polymer/templates/polymer.tt +28 -0
  31. data/lib/polymer/templates/sass_mixins.erb +29 -0
  32. data/lib/polymer/templates/sources/one/book.png +0 -0
  33. data/lib/polymer/templates/sources/one/box-label.png +0 -0
  34. data/lib/polymer/templates/sources/one/calculator.png +0 -0
  35. data/lib/polymer/templates/sources/one/calendar-month.png +0 -0
  36. data/lib/polymer/templates/sources/one/camera.png +0 -0
  37. data/lib/polymer/templates/sources/one/eraser.png +0 -0
  38. data/lib/polymer/templates/sources/two/inbox-image.png +0 -0
  39. data/lib/polymer/templates/sources/two/magnet.png +0 -0
  40. data/lib/polymer/templates/sources/two/newspaper.png +0 -0
  41. data/lib/polymer/templates/sources/two/television.png +0 -0
  42. data/lib/polymer/templates/sources/two/wand-hat.png +0 -0
  43. data/lib/polymer/templates/sources/two/wooden-box-label.png +0 -0
  44. data/lib/polymer/version.rb +4 -0
  45. data/lib/polymer.rb +49 -0
  46. data/polymer.gemspec +94 -0
  47. metadata +206 -0
@@ -0,0 +1,340 @@
1
+ require 'fileutils'
2
+ require 'thor'
3
+
4
+ module Polymer
5
+ class CLI < Thor
6
+
7
+ include Thor::Actions
8
+
9
+ class_option 'no-color', :type => :boolean, :default => false,
10
+ :desc => 'Disable colours in output', :aliases => '--no-colour'
11
+
12
+ def initialize(*args)
13
+ super
14
+ self.shell = Thor::Shell::Basic.new if options['no-color']
15
+ end
16
+
17
+ # --- bond ---------------------------------------------------------------
18
+
19
+ desc 'bond [SPRITES]',
20
+ 'Creates the sprites specified by your .polymer or polymer.rb file'
21
+
22
+ long_desc <<-DESC
23
+ The bond task reads your project configuration and creates your shiny
24
+ new sprites. If enabled, CSS and/or SCSS will also be written so as to
25
+ make working with your sprites a little easier.
26
+
27
+ You may specify exactly which sprites you want generated, otherwise
28
+ Polymer will generate all sprites defined in your config file. Any
29
+ sprite which has not changed since you last ran this command will not be
30
+ re-generated unless you pass the --force option.
31
+ DESC
32
+
33
+ method_option :force, :type => :boolean, :default => false,
34
+ :desc => 'Re-generates sprites whose sources have not changed'
35
+
36
+ method_option :fast, :type => :boolean, :default => false,
37
+ :desc => "Skip optimisation of images after they are generated"
38
+
39
+ def bond(*sprites)
40
+ project = find_project!
41
+
42
+ # Determine which sprites we'll be working on.
43
+ sprites = project.sprites.select do |sprite|
44
+ if sprites.empty? or sprites.include?(sprite.name)
45
+ # The user specified no sprites, or this sprite was requested.
46
+ if project.cache.stale?(sprite) or options[:force]
47
+ # Digest is different, user is forcing update or sprite file
48
+ # has been deleted.
49
+ project.cache.set(sprite)
50
+ end
51
+ end
52
+ end
53
+
54
+ # There's nothing to generate.
55
+ return if sprites.empty?
56
+
57
+ # Get on with it.
58
+ sprites.each do |sprite|
59
+ next unless sprite.save
60
+
61
+ say_status('generated', sprite.name, :green)
62
+
63
+ unless options[:fast]
64
+ say " optimising #{sprite.name} ... "
65
+ before = sprite.save_path.size
66
+
67
+ reduction = Polymer::Optimisation.optimise_file(sprite.save_path)
68
+
69
+ if reduction > 0
70
+ saved = '- saved %.2fkb (%.1f' %
71
+ [reduction.to_f / 1024, (reduction.to_f / before) * 100]
72
+ say_status "\r\e[0K optimised", "#{sprite.name} #{saved}%)", :green
73
+ else
74
+ print "\r\e[0K"
75
+ end
76
+ end
77
+ end
78
+
79
+ # Stylesheets.
80
+ if SassGenerator.generate(project)
81
+ say_status('written', 'Sass mixin', :green)
82
+ end
83
+
84
+ #process Processors::CSS, project
85
+
86
+ # Find sprites with deviant-width sources.
87
+ sprites.each do |sprite|
88
+ if deviants = DeviantFinder.find_deviants(sprite)
89
+ say DeviantFinder.format_ui_message(sprite, deviants)
90
+ end
91
+ end
92
+
93
+ # Clean up the cache, removing sprites which no longer exist.
94
+ project.cache.remove_all_except(project.sprites)
95
+
96
+ # Finish by writing the new cache.
97
+ project.cache.write
98
+
99
+ rescue Polymer::MissingSource, Polymer::TargetNotWritable => e
100
+ say e.message.compress_lines, :red
101
+ exit 1
102
+ end
103
+
104
+ # --- help ---------------------------------------------------------------
105
+
106
+ # Provides customised help information using the man pages.
107
+ # Nod-of-the-hat to Bundler.
108
+ def help(command = nil)
109
+ page_map = {
110
+ # Main manual page.
111
+ nil => 'polymer.1',
112
+ 'polymer' => 'polymer.1',
113
+
114
+ # Sub-commands.
115
+ 'init' => 'polymer-init.1',
116
+ 'bond' => 'polymer-bond.1',
117
+ 'optimise' => 'polymer-optimise.1',
118
+ 'optimize' => 'polymer-optimise.1',
119
+ 'position' => 'polymer-position.1',
120
+
121
+ # Configuration format.
122
+ 'polymer(5)' => 'polymer.5',
123
+ 'polymer.5' => 'polymer.5',
124
+ '.polymer' => 'polymer.5',
125
+ 'polymer.rb' => 'polymer.5',
126
+ 'config' => 'polymer.5'
127
+ }
128
+
129
+ if page_map.has_key?(command)
130
+ root = File.expand_path('../man', __FILE__)
131
+
132
+ if groff_available?
133
+ groff = 'groff -Wall -mtty-char -mandoc -Tascii'
134
+ pager = ENV['MANPAGER'] || ENV['PAGER'] || 'more'
135
+
136
+ Kernel.exec "#{groff} #{root}/#{page_map[command]} | #{pager}"
137
+ else
138
+ puts File.read("#{root}/#{page_map[command]}.txt")
139
+ end
140
+ else
141
+ super
142
+ end
143
+ end
144
+
145
+ # --- init ---------------------------------------------------------------
146
+
147
+ desc 'init', 'Creates a new Polymer project in the current directory'
148
+
149
+ long_desc <<-DESC
150
+ In order to use Polymer, a .polymer configuration file must be created.
151
+ The init task creates a sample configuration, and also adds a couple of
152
+ example source images to demonstrate how to use Polymer to create your
153
+ own sprite images.
154
+ DESC
155
+
156
+ method_option :sprites, :type => :string, :default => 'public/images',
157
+ :desc => 'Default location to which generated sprites are saved'
158
+
159
+ method_option :sources, :type => :string, :default => '<sprites>/sprites',
160
+ :desc => 'Default location of source images'
161
+
162
+ method_option 'no-examples', :type => :boolean, :default => false,
163
+ :desc => "Disables copying of example source files"
164
+
165
+ method_option :windows, :type => :boolean, :default => false,
166
+ :desc => 'Create polymer.rb instead of .polymer for easier editing on ' \
167
+ 'Windows systems.'
168
+
169
+ def init
170
+ if File.exists?('.polymer')
171
+ say 'A .polymer file already exists in this directory.', :red
172
+ exit 1
173
+ end
174
+
175
+ project_dir = Pathname.new(Dir.pwd)
176
+
177
+ config = {
178
+ :sprites => options[:sprites],
179
+ :sources => options[:sources].gsub(/<sprites>/, options[:sprites]),
180
+ :windows => options[:windows]
181
+ }
182
+
183
+ filename = options[:windows] ? 'polymer.rb' : '.polymer'
184
+ polymerfile = project_dir + filename
185
+
186
+ template 'polymer.tt', polymerfile, config
187
+
188
+ # Clean up the template.
189
+ contents = polymerfile.read.gsub(/\n{3,}/, "\n\n")
190
+ polymerfile.open('w') { |file| file.puts contents }
191
+
192
+ unless options['no-examples']
193
+ directory 'sources', project_dir + config[:sources]
194
+ end
195
+
196
+ say_status '', '-------------------------'
197
+ say_status '', 'Your project was created!'
198
+ end
199
+
200
+ # --- optimise -----------------------------------------------------------
201
+
202
+ desc 'optimise PATHS', 'Optimises PNG images at the given PATHS'
203
+
204
+ long_desc <<-DESC
205
+ Given a path to an image (or multiple images), runs Polymer's optimisers
206
+ on the image. Requires that the paths be images to PNG files. Image
207
+ paths are relative to the current working directory.
208
+ DESC
209
+
210
+ map 'optimize' => :optimise
211
+
212
+ def optimise(*paths)
213
+ dir = Pathname.new(Dir.pwd)
214
+ paths = paths.map { |path| dir + path }
215
+
216
+ paths.each do |path|
217
+ fpath = path.relative_path_from(dir).to_s
218
+
219
+ # Ensure the file is a PNG.
220
+ unless path.to_s =~ /\.png/
221
+ say_status 'skipped', "#{fpath} - not a PNG", :yellow
222
+ next
223
+ end
224
+
225
+ before = path.size
226
+ say " optimising #{fpath} "
227
+ reduction = Polymer::Optimisation.optimise_file(path)
228
+
229
+ if reduction > 0
230
+ saved = '- saved %.2fkb (%.1f' %
231
+ [reduction.to_f / 1024, (reduction.to_f / before) * 100]
232
+ say_status "\r\e[0K optimised", "#{fpath} #{saved}%)", :green
233
+ else
234
+ say_status "\r\e[0K optimised", "#{fpath} - no savings", :green
235
+ end
236
+ end
237
+ end
238
+
239
+ # --- position -----------------------------------------------------------
240
+
241
+ desc 'position SOURCE', 'Shows the position of a source within a sprite'
242
+
243
+ long_desc <<-DESC
244
+ The position task shows you the position of a source image within a
245
+ sprite and also shows the appropriate CSS statement for the source
246
+ should you wish to create your own CSS files.
247
+
248
+ You may supply the name of a source image; if a source image with the
249
+ same name exists in multiple sprites, the positions of each of them will
250
+ be shown to you. If you want a particular source, you may instead
251
+ provide a "sprite/source" pair.
252
+ DESC
253
+
254
+ def position(source)
255
+ project = find_project!
256
+
257
+ if source.index('/')
258
+ # Full sprite/source pair given.
259
+ sprite, source = source.split('/', 2)
260
+
261
+ if project.sprite(sprite)
262
+ sprites = [project.sprite(sprite)]
263
+ else
264
+ say "No such sprite: #{sprite}", :red
265
+ exit 1
266
+ end
267
+ else
268
+ # Only a source name was given.
269
+ sprites = project.sprites
270
+ end
271
+
272
+ # Remove sprites which don't have a matching source.
273
+ sprites.reject! { |sprite| not sprite.source(source) }
274
+ say("No such source: #{source}") && exit(1) if sprites.empty?
275
+
276
+ say ""
277
+
278
+ sprites.each do |sprite|
279
+ say "#{sprite.name}/#{source}: #{sprite.position_of(source)}px", :green
280
+ say " #{Polymer::CSSGenerator.background_statement(sprite, source)}"
281
+ say " - or -"
282
+ say " #{Polymer::CSSGenerator.position_statement(sprite, source)}"
283
+ say ""
284
+ end
285
+ end
286
+
287
+ # --- version ------------------------------------------------------------
288
+
289
+ desc 'version', "Shows the version of Polymer you're using"
290
+ map '--version' => :version
291
+
292
+ def version
293
+ say "Polymer #{Polymer::VERSION}"
294
+ end
295
+
296
+ private # ----------------------------------------------------------------
297
+
298
+ # Returns the Project for the current directory. Exits with a message if
299
+ # no project could be found.
300
+ #
301
+ # @return [Polymer::Project]
302
+ #
303
+ def find_project!
304
+ Polymer::DSL.load Polymer::Project.find_config(Dir.pwd)
305
+ rescue Polymer::MissingProject
306
+ say <<-ERROR.compress_lines, :red
307
+ Couldn't find a Polymer project in the current directory, or any of
308
+ the parent directories. Run "polymer init" if you want to create a new
309
+ project here.
310
+ ERROR
311
+ exit 1
312
+ end
313
+
314
+ # Returns if the current machine has groff available.
315
+ #
316
+ # @return [Boolean]
317
+ #
318
+ def groff_available?
319
+ require 'rbconfig'
320
+
321
+ if RbConfig::CONFIG["host_os"] =~ /(msdos|mswin|djgpp|mingw)/
322
+ `which groff 2>NUL`
323
+ else
324
+ `which groff 2>/dev/null`
325
+ end
326
+
327
+ $? == 0
328
+ end
329
+
330
+ def self.source_root
331
+ File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
332
+ end
333
+
334
+ # Temporary -- until the next Thor release.
335
+ def self.banner(task, namespace = nil, subcommand = false)
336
+ super.gsub(/^.*polymer/, 'polymer')
337
+ end
338
+
339
+ end # CLI
340
+ end # Polymer
@@ -0,0 +1,78 @@
1
+ # Some simple string extensions to make things easier.
2
+ class String
3
+
4
+ # Replace sequences of whitespace (including newlines) with either
5
+ # a single space or remove them entirely (according to param _spaced_)
6
+ #
7
+ # <<QUERY.compress_lines
8
+ # SELECT name
9
+ # FROM users
10
+ # QUERY => "SELECT name FROM users"
11
+ #
12
+ # @return [String] Receiver with whitespace (including newlines) replaced
13
+ #
14
+ def compress_lines
15
+ split($/).map { |line| line.strip }.join(' ')
16
+ end
17
+
18
+ # Removes leading whitespace from each line, such as might be added when
19
+ # using a HEREDOC string.
20
+ #
21
+ # @return [String] Receiver with leading whitespace removed.
22
+ #
23
+ def unindent
24
+ (other = dup) and other.unindent! and other
25
+ end
26
+
27
+ # Bang version of #unindent.
28
+ #
29
+ # @return [String] Receiver with leading whitespace removed.
30
+ #
31
+ def unindent!
32
+ gsub!(/^[ \t]{#{minimum_leading_whitespace}}/, '')
33
+ end
34
+
35
+ private
36
+
37
+ # Checks each line and determines the minimum amount of leading whitespace.
38
+ #
39
+ # @return [Integer] The number of leading whitespace characters.
40
+ #
41
+ def minimum_leading_whitespace
42
+ whitespace = split("\n", -1).inject(0) do |indent, line|
43
+ if line.strip.empty?
44
+ indent # Ignore completely blank lines.
45
+ elsif line =~ /^(\s+)/
46
+ (1.0 / $1.length) > indent ? 1.0 / $1.length : indent
47
+ else
48
+ 1.0
49
+ end
50
+ end
51
+
52
+ whitespace == 1.0 ? 0 : (1.0 / whitespace).to_i
53
+ end
54
+
55
+ end
56
+
57
+ # String#compress_lines is extracted from the extlib gem
58
+ # ------------------------------------------------------
59
+ #
60
+ # Copyright (c) 2009 Dan Kubb
61
+ #
62
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
63
+ # of this software and associated documentation files (the "Software"), to
64
+ # deal in the Software without restriction, including without limitation the
65
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
66
+ # sell copies of the Software, and to permit persons to whom the Software is
67
+ # furnished to do so, subject to the following conditions:
68
+ #
69
+ # The above copyright notice and this permission notice shall be included in
70
+ # all copies or substantial portions of the Software.
71
+ #
72
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
73
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
74
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
75
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
76
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
77
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
78
+ # IN THE SOFTWARE.
@@ -0,0 +1,32 @@
1
+ module Polymer
2
+ class CSSGenerator
3
+
4
+ # --- Class Methods ------------------------------------------------------
5
+
6
+ # Returns a string which may be used as the background statement for the
7
+ # given sprite and source pair.
8
+ #
9
+ # @param [Polymer::Sprite] sprite
10
+ # @param [Polymer::Source] source
11
+ #
12
+ # @return [String]
13
+ #
14
+ def self.background_statement(sprite, source)
15
+ "background: url(#{sprite.url}) 0 " \
16
+ "#{-sprite.position_of(source)}px no-repeat;"
17
+ end
18
+
19
+ # Returns a string which may be used as the background-position statement
20
+ # for the given sprite and source pair.
21
+ #
22
+ # @param [Polymer::Sprite] sprite
23
+ # @param [Polymer::Source] source
24
+ #
25
+ # @return [String]
26
+ #
27
+ def self.position_statement(sprite, source)
28
+ "background-position: 0 #{-sprite.position_of(source)}px;"
29
+ end
30
+
31
+ end # CSSGenerator
32
+ end # Polymer
@@ -0,0 +1,76 @@
1
+ module Polymer
2
+ # Given a Sprite, DeviantFinder checks to see if the sprite has any sources
3
+ # which are significantly wider than the average.
4
+ class DeviantFinder
5
+
6
+ # Given a Sprite, checks to see if the sprite has any sources which are
7
+ # significantly wider than the average. Images with a small standard
8
+ # deviation in source width or fewer than 3 sources will be skipped.
9
+ #
10
+ # @param [Polymer::Sprite] sprite
11
+ # The sprite whose source widths are to be checked.
12
+ #
13
+ # @return [Array<String>]
14
+ # Returns an array of source images whose width is greater than the
15
+ # standard deviation.
16
+ # @return [nil]
17
+ # Returns nil if all of the source images are an approriate width.
18
+ #
19
+ def self.find_deviants(sprite)
20
+ # Need more than two sources to find deviants.
21
+ return false if sprite.sources.size < 2
22
+
23
+ mean, std_dev = standard_deviation(sprite.sources.map do |source|
24
+ source.image.columns
25
+ end)
26
+
27
+ return false if std_dev < 100 # Skip images with a < 100px deviation.
28
+
29
+ deviants = sprite.sources.select do |source|
30
+ width = source.image.columns
31
+ width > mean + std_dev || width < mean - std_dev
32
+ end
33
+
34
+ deviants.any? and deviants
35
+ end
36
+
37
+ # Print a warning if the sprite contains wide sources.
38
+ def self.format_ui_message(sprite, deviants)
39
+ if deviants
40
+ <<-MESSAGE.compress_lines
41
+ Your "#{sprite.name}" sprite contains one or more source images
42
+ which deviate significantly from the average source width. You might
43
+ want to consider removing these sources from the sprite in order to
44
+ reduce the sprite filesize.
45
+
46
+ Wide sources: #{deviants.map(&:name).join(', ')}
47
+ MESSAGE
48
+ end
49
+ end
50
+
51
+ private # ----------------------------------------------------------------
52
+
53
+ # Knuth.
54
+ #
55
+ # @param [Array<Integer>] data
56
+ # An array containing the widths of each source image.
57
+ #
58
+ # @return [Array<Integer, Integer>]
59
+ # Returns a two-element array whose first element is the mean width of
60
+ # the source images; the second element is the standard deviation.
61
+ #
62
+ def self.standard_deviation(data)
63
+ n, mean, m2 = 0, 0, 0
64
+
65
+ data.each do |x|
66
+ n = n + 1
67
+ delta = x - mean
68
+ mean = mean + delta / n
69
+ m2 = m2 + delta * (x - mean)
70
+ end
71
+
72
+ [ mean, Math.sqrt(m2 / (n - 1)) ]
73
+ end
74
+
75
+ end # DeviantFinder
76
+ end # Polymer