polymer 1.0.0.beta.3

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 (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