asset-pocket 0.1

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.
@@ -0,0 +1,20 @@
1
+
2
+ Feature: copy files from one directory to another
3
+
4
+ @files
5
+ Scenario: keep the directory tree
6
+ Given a file named "sources/a/one" with "1"
7
+ And a file named "sources/a/two" with "2"
8
+ And a file named "sources/b/three" with "3"
9
+ And a file named "sources/b/d/four" with "4"
10
+ When generate a pocket with:
11
+ """
12
+ files "generated/copied/" do
13
+ base "sources"
14
+ use "sources/**/*"
15
+ end
16
+ """
17
+ Then a file named "generated/copied/a/one" contains "1"
18
+ And a file named "generated/copied/a/two" contains "2"
19
+ And a file named "generated/copied/b/three" contains "3"
20
+ And a file named "generated/copied/b/d/four" contains "4"
@@ -0,0 +1,89 @@
1
+
2
+ Feature: JavaScripts Pocket
3
+
4
+ @javascript
5
+ Scenario: combine several files just concat them
6
+ Given a file named "sources/foo.js" with "foo();"
7
+ And a file named "sources/bar.js" with "bar();"
8
+ When generate a pocket with:
9
+ """
10
+ js "generated/dest.js" do
11
+ use "sources/foo.js"
12
+ use "sources/bar.js"
13
+ end
14
+ """
15
+ Then a file named "generated/dest.js" contains "foo();bar();"
16
+
17
+ @javascript
18
+ Scenario: combine several files with a separator
19
+ Given a file named "sources/foo.js" with "foo();"
20
+ And a file named "sources/bar.js" with "bar();"
21
+ When generate a pocket with:
22
+ """
23
+ js "generated/dest.js" do
24
+ use "sources/foo.js"
25
+ use "sources/bar.js"
26
+
27
+ separator "|"
28
+ end
29
+ """
30
+ Then a file named "generated/dest.js" contains "foo();|bar();"
31
+
32
+
33
+ @javascript
34
+ Scenario: a pattern can be used to append files sorted by name
35
+ Given a file named "sources/a/a.js" with "1"
36
+ And a file named "sources/c/b.js" with "5"
37
+ And a file named "sources/a/b.js" with "2"
38
+ And a file named "sources/b/a.js" with "4"
39
+ And a file named "sources/a/c.js" with "3"
40
+ When generate a pocket with:
41
+ """
42
+ js "generated/everything.js" do
43
+ use "sources/**/*.js"
44
+ end
45
+ """
46
+ Then a file named "generated/everything.js" contains "12345"
47
+
48
+ @javascript
49
+ Scenario: a custom compressor can be defined in the configuration
50
+ Given a file named "sources/foo.js" with "foo"
51
+ And a file named "sources/bar.js" with "bar"
52
+ When generate a pocket with:
53
+ """
54
+ class UpperCompressor
55
+ def compress(content)
56
+ content.upcase
57
+ end
58
+ end
59
+
60
+ compressor :upper, :handler => UpperCompressor.new
61
+ js "generated/upper.js" do
62
+ compress :upper
63
+ use "sources/**/*.js"
64
+ end
65
+ """
66
+ Then a file named "generated/upper.js" contains "BARFOO"
67
+
68
+ @javascript
69
+ Scenario: compressors can receive parameters
70
+ Given a file named "sources/foo.js" with "foo"
71
+ And a file named "sources/bar.js" with "bar"
72
+ When generate a pocket with:
73
+ """
74
+ class AppendContentCompressor
75
+ attr_accessor :extra
76
+ def compress(content)
77
+ content + extra
78
+ end
79
+ end
80
+
81
+ compressor :append, :handler => AppendContentCompressor.new
82
+ compressor :append, :extra => "baz"
83
+ js "generated/append.js" do
84
+ compress :append
85
+ use "sources/**/*.js"
86
+ end
87
+ """
88
+ Then a file named "generated/append.js" contains "barfoobaz"
89
+
@@ -0,0 +1,52 @@
1
+
2
+ Feature: Rails integration
3
+ Rails views have a helper method sprite_tag to create a HTML tag which will
4
+ show the image.
5
+
6
+ The rake task pocket:update will generate the pocket.
7
+
8
+ @rails
9
+ Scenario: create a new application and define a pocket with some files
10
+ Given a rails application in a temporary directory
11
+ And a file named "app/views/css/a.css" with "1"
12
+ And a file named "app/views/css/b.css" with "2"
13
+ And the application has a pocket with:
14
+ """
15
+ css "public/stylesheets/application.css" do
16
+ use "app/views/css/*"
17
+ separator "\n"
18
+ end
19
+ """
20
+ When update the pocket
21
+ Then a file named "public/stylesheets/application.css" contains "1\n2"
22
+
23
+ @rails
24
+ Scenario: create a CSS sprite in a rails application
25
+ Given a rails application in a temporary directory
26
+ And a file named "app/views/images/first.png" with a random image of 10x20
27
+ And a file named "app/views/images/second.png" with a random image of 10x30
28
+ And the application has a pocket with:
29
+ """
30
+ css "public/stylesheets/sprites.css" do
31
+ sprite "icons" do
32
+ layout :vertical
33
+ use "app/views/images/*"
34
+ end
35
+ end
36
+ """
37
+ And the controller "foo" has a view "index" with:
38
+ """
39
+ <%= sprite_tag "icons/first" %>
40
+ generated HTML from rails
41
+ <%= sprite_tag "icons/second" %>
42
+ """
43
+ When update the pocket
44
+ Then the sprite "icons" is generated in "public/stylesheets/sprites.css"
45
+ And this sprite has 2 images
46
+ And this sprite is 10x50
47
+ And this sprite format is PNG
48
+ And this sprite has the image "first"
49
+ And this sprite has the image "second"
50
+ Then the page at "/foo/index" include "generated HTML from rails"
51
+ And this page has a tag matched with ".sprite-icons--first"
52
+ And this page has a tag matched with ".sprite-icons--second"
@@ -0,0 +1,69 @@
1
+
2
+ Feature: Sass integration
3
+
4
+ @sass
5
+ Scenario: filenames ended with .scss are compiled with Sass
6
+ Given a file named "sources/foo.scss" with "$color: blue; div { span { color: $color; } }"
7
+ When generate a pocket with:
8
+ """
9
+ css "generated/base.css" do
10
+ use "sources/foo.scss"
11
+ end
12
+ """
13
+ Then a file named "generated/base.css" contains "div span {\n color: blue; }\n"
14
+
15
+ @sass
16
+ Scenario: Sass options can be modified
17
+ Given a file named "sources/bar.scss" with "div { span { display: block; } }"
18
+ When generate a pocket with:
19
+ """
20
+ sass :style => :compressed
21
+ css "generated/compressed.css" do
22
+ use "sources/bar.scss"
23
+ end
24
+ """
25
+ Then a file named "generated/compressed.css" contains "div span{display:block}\n"
26
+
27
+ @sass
28
+ Scenario: multiple Sass sources can be loaded importing them
29
+ Given a file named "sources/one.scss" with "$color: blue;"
30
+ And a file named "sources/two.scss" with "$background: black;"
31
+ And a file named "sources/three.scss" with "$color: blue !default;"
32
+ And a file named "sources/final.scss" with "div { background: $background; color: $color; }"
33
+ When generate a pocket with:
34
+ """
35
+ sass :style => :compressed
36
+ sass "generated/compressed.css" do
37
+ import "sources/one.scss"
38
+ import "sources/two.scss"
39
+ import "sources/three.scss"
40
+ import "sources/final.scss"
41
+ end
42
+ """
43
+ Then a file named "generated/compressed.css" contains "div{background:black;color:blue}\n"
44
+
45
+ @sass
46
+ Scenario: a bundle can be created with previous bundles
47
+ Given a file named "sources/one.css" with "div {}"
48
+ And a file named "sources/two.css" with "span {}"
49
+ And a file named "sources/three.scss" with "div { span { display: block; } }"
50
+ When generate a pocket with:
51
+ """
52
+ css "generated/first.css" do
53
+ use "sources/one.css"
54
+ use "sources/two.css"
55
+
56
+ separator "\n"
57
+ end
58
+
59
+ sass :style => :compressed
60
+ css "generated/second.css" do
61
+ use "sources/three.scss"
62
+ end
63
+
64
+ css "generated/both.css" do
65
+ use "generated/first.css"
66
+ use "generated/second.css"
67
+ end
68
+ """
69
+ Then a file named "generated/both.css" contains "div {}\nspan {}div span{display:block}\n"
@@ -0,0 +1,94 @@
1
+
2
+ Feature: generate CSS sprites from the pocket
3
+
4
+ @sprites
5
+ Scenario: create a simple sprite with different images
6
+ Given a file named "images/a.png" with a random image of 10x10
7
+ And a file named "images/b.png" with a random image of 10x20
8
+ And a file named "images/c.png" with a random image of 20x20
9
+ When generate a pocket with:
10
+ """
11
+ css "sprites/css/first.css" do
12
+ sprite "all" do
13
+ layout :vertical
14
+ use "images/*"
15
+ end
16
+ end
17
+ """
18
+ Then the sprite "all" is generated in "sprites/css/first.css"
19
+ And this sprite has 3 images
20
+ And this sprite is 20x50
21
+ And this sprite format is PNG
22
+
23
+ @sprites
24
+ Scenario Outline: sprites can be generated with different layoutes
25
+ Given 20 random images at "images/<case>/firstNN.png" with a random image of 10x30
26
+ When generate a pocket with:
27
+ """
28
+ css "sprites/css/<case>.css" do
29
+ sprite "all-<case>" do
30
+ layout <layout>
31
+ use "images/<case>/*"
32
+ end
33
+ end
34
+ """
35
+ Then the sprite "all-<case>" is generated in "sprites/css/<case>.css"
36
+ And this sprite has 20 images
37
+ And this sprite is <size>
38
+ And this sprite format is PNG
39
+
40
+ Scenarios:
41
+ | case | layout | size |
42
+ | vertical | :vertical | 10x600 |
43
+ | horizontal | :horizontal | 200x30 |
44
+ | table | :columns => 5 | 50x120 |
45
+
46
+ @sprites
47
+ Scenario: different formats create different sprites
48
+ Given a file named "images/a.png" with a random image of 10x10
49
+ And a file named "images/b.png" with a random image of 10x20
50
+ And a file named "images/c.gif" with a random image of 20x20
51
+ And 10 random images at "images/firstNN.jpeg" with a random image of 10x30
52
+ When generate a pocket with:
53
+ """
54
+ css "sprites/css/multiformat.css" do
55
+ sprite "multiformat" do
56
+ layout :vertical
57
+ use "images/*"
58
+ end
59
+ end
60
+ """
61
+ Then the sprite "multiformat" encoded in PNG is generated in "sprites/css/multiformat.css"
62
+ And this sprite has 2 images
63
+ And this sprite is 10x30
64
+ And the sprite "multiformat" encoded in GIF is generated in "sprites/css/multiformat.css"
65
+ And this sprite has 1 image
66
+ And this sprite is 20x20
67
+ And the sprite "multiformat" encoded in JPEG is generated in "sprites/css/multiformat.css"
68
+ And this sprite has 10 images
69
+ And this sprite is 10x300
70
+
71
+ @sprites
72
+ Scenario: the sprites can be generated using different quality values
73
+ Given a file named "images/a.jpeg" with a random image of 10x10
74
+ And a file named "images/b.jpeg" with a random image of 10x20
75
+ And a file named "images/c.jpeg" with a random image of 20x20
76
+ When generate a pocket with:
77
+ """
78
+ css "sprites/css/everything.css" do
79
+ sprite "best" do
80
+ layout :vertical
81
+ quality 100
82
+ use "images/*"
83
+ end
84
+
85
+ sprite "worst" do
86
+ layout :vertical
87
+ quality 1
88
+ use "images/*"
89
+ end
90
+ end
91
+ """
92
+ Then the sprite "worst" is generated in "sprites/css/everything.css"
93
+ And the sprite "best" is generated in "sprites/css/everything.css"
94
+ And the size of the sprite "worst" is smaller than the size of the sprite "best"
@@ -0,0 +1,23 @@
1
+
2
+ Given /^a file named "([^"]*)" with "([^"]*)"$/ do |filename, content|
3
+ filename = @temp_dir.join(filename)
4
+ filename.exist?.should be_false
5
+
6
+ @created_files.unshift filename
7
+ filename.dirname.mkpath
8
+ filename.open("w") {|f| f.write content }
9
+ end
10
+
11
+ When /^generate a pocket with:$/ do |pocket_content|
12
+ generator = AssetPocket::Generator.new
13
+ generator.root_path = @temp_dir
14
+ generator.parse_string pocket_content
15
+ generator.run!
16
+ end
17
+
18
+ Then /^a file named "([^"]*)" contains "([^"]*)"$/ do |filename, content|
19
+ filename = @temp_dir.join(filename)
20
+ filename.file?.should be_true
21
+ filename.read.inspect[1..-2].should eql(content)
22
+ end
23
+
@@ -0,0 +1,78 @@
1
+
2
+ Given /^a rails application in a temporary directory$/ do
3
+ @temp_dir = @temp_dir.join("test_pocket")
4
+ case (`rails -v` =~ /rails (\d+)/i && $1).to_i
5
+ when 3
6
+ `rails new #{@temp_dir}`
7
+ @temp_dir.join("config/initializers/rack_test.rb").open("w") {|f| f.puts <<-EOI }
8
+ RackTestApplication = proc { Rails::Application.instance }
9
+ EOI
10
+ when 2
11
+ `rails #{@temp_dir}`
12
+ @temp_dir.join("config/initializers/rack_test.rb").open("w") {|f| f.puts <<-EOI }
13
+ gem "rack-test"
14
+ require 'rack/test'
15
+ RackTestApplication = proc { ActionController::Dispatcher.new }
16
+
17
+ class String
18
+ def html_safe
19
+ self
20
+ end
21
+ end
22
+ EOI
23
+ else
24
+ pending
25
+ end
26
+
27
+ plugin_dir = @temp_dir.join("vendor/plugins")
28
+ plugin_dir.mkpath
29
+ FileUtils.cp_r Pathname.new(__FILE__).dirname.join("../../"), plugin_dir
30
+ end
31
+
32
+ Given /^the application has a pocket with:$/ do |pocket|
33
+ pocket_file = @temp_dir.join("config/pocket.rb")
34
+ pocket_file.exist?.should be_false
35
+ pocket_file.open("w") {|f| f.write pocket }
36
+ end
37
+
38
+ Given /^the controller "([^"]*)" has a view "([^"]*)" with:$/ do |controller, action, erb|
39
+ controller_file = @temp_dir.join("app/controllers/#{controller}_controller.rb")
40
+ if not controller_file.exist?
41
+ controller_file.dirname.mkpath
42
+ controller_file.open("w") {|f| f.puts "class #{"#{controller}_controller".gsub(/(?:^|_)([a-z])/) { $1.upcase }} < ApplicationController\nend" }
43
+
44
+ # This route should works in both Rails 3 and 2
45
+ routes = @temp_dir.join("config/routes.rb")
46
+ current_routes = routes.read
47
+ routes.open("w") {|f| f.write current_routes.sub(/end\s*\Z/, %[map.connect "/#{controller}/:action", :controller => "#{controller}"\nend\n]) }
48
+ end
49
+
50
+ view_file = @temp_dir.join("app/views/#{controller}/#{action}.html.erb")
51
+ view_file.dirname.mkpath
52
+ view_file.open("w") {|f| f.write erb }
53
+ end
54
+
55
+
56
+ Then /^the page at "([^"]*)" include "([^"]*)"$/ do |uri,html|
57
+ if @temp_dir.join("script/rails").exist?
58
+ runner = "rails runner"
59
+ else
60
+ runner = "script/runner"
61
+ end
62
+
63
+ @page_body = `cd #{@temp_dir}; #{runner} 'puts Rack::Test::Session.new(RackTestApplication.call).get("#{uri}").body'`
64
+ @page_body.should include(html)
65
+ end
66
+
67
+ Then /^this page has a tag matched with "([^"]*)"$/ do |selector|
68
+ @page_dom ||= Nokogiri::HTML(@page_body)
69
+ @page_dom.search(selector).should_not be_empty
70
+ end
71
+
72
+ When /^update the pocket$/ do
73
+ Process.wait(fork do
74
+ Dir.chdir @temp_dir
75
+ STDOUT.reopen "/dev/null", "w"
76
+ exec "rake", "pocket:update"
77
+ end)
78
+ end
@@ -0,0 +1,76 @@
1
+
2
+ def random_image(filename, width, height)
3
+ img = Magick::Image.new(width, height)
4
+ img.import_pixels 0, 0, width, height, "RGB", File.read("/dev/urandom", width*height*3)
5
+
6
+ filename = @temp_dir.join(filename)
7
+ filename.dirname.mkpath
8
+ img.write filename.to_s
9
+
10
+ @created_files.unshift filename if @created_files
11
+ end
12
+
13
+ Given /^(\d+) random images at "([^"]*)" with a random image of (\d+)x(\d+)$/ do |count, pattern, width, height|
14
+ width = width.to_i
15
+ height = height.to_i
16
+ count.to_i.times do |iter|
17
+ random_image pattern.sub("NN", iter.to_s), width, height
18
+ end
19
+ end
20
+
21
+ Given /^a file named "([^"]*)" with a random image of (\d+)x(\d+)$/ do |filename, width, height|
22
+ random_image filename, width.to_i, height.to_i
23
+ end
24
+
25
+
26
+ def just_parse_css(content)
27
+ content.scan(/(\S+)\s*\{(.*?)\}/m).inject({}) do |hash, item|
28
+ rule, attributes = item
29
+ hash[rule] = attributes.split(";").map {|attribute| attribute.strip }
30
+ hash
31
+ end
32
+ end
33
+
34
+ Then /^the sprite "([^"]*)" is generated in "([^"]*)"$/ do |sprite, css_file|
35
+ Then %|the sprite "#{sprite}" encoded in \\w+ is generated in "#{css_file}"|
36
+ end
37
+
38
+ Then /^the sprite "([^"]*)" encoded in (\S+) is generated in "([^"]*)"$/ do |sprite, format, css_file|
39
+
40
+ @found_sprites ||= {}
41
+
42
+ @sprite_name = sprite
43
+
44
+ css_file = @temp_dir.join(css_file)
45
+ @css_loaded = just_parse_css(css_file.read)
46
+ @css_loaded.should include_key(/\.sprite-#{sprite}--/)
47
+
48
+ @sprite = @css_loaded.reject {|key, value| key !~ /\.sprite-#{sprite}--/ or value.join !~ /url\([^)]+\.#{format}\)/i }
49
+ @image_sprite = Magick::Image.read(css_file.dirname.join(@sprite.values.to_s.first =~ /url\((.*?\.#{format})\)/i && $1).to_s).first
50
+
51
+ @found_sprites[sprite] = @image_sprite
52
+ end
53
+
54
+ Then /^this sprite has (\d+) images?$/ do |count|
55
+ @sprite.size.should eql(count.to_i)
56
+ end
57
+
58
+ Then /^this sprite is (\d+)x(\d+)$/ do |width, height|
59
+ @image_sprite.columns.should eql(width.to_i)
60
+ @image_sprite.rows.should eql(height.to_i)
61
+ end
62
+
63
+ Then /^this sprite format is (\S+)$/ do |format|
64
+ @image_sprite.format.downcase.should eql(format.downcase)
65
+ end
66
+
67
+ Then /^this sprite has the image "([^"]*)"$/ do |image_name|
68
+ @css_loaded.should include_key(/\.sprite-#{@sprite_name}--#{image_name}\b/)
69
+ end
70
+
71
+ Then /^the size of the sprite "([^"]*)" is smaller than the size of the sprite "([^"]*)"$/ do |big_sprite, small_sprite|
72
+ @found_sprites[big_sprite].should_not be_nil
73
+ @found_sprites[small_sprite].should_not be_nil
74
+ (@found_sprites[big_sprite].to_blob > @found_sprites[small_sprite].to_blob).should be_true
75
+ end
76
+
@@ -0,0 +1,37 @@
1
+
2
+ require 'fileutils'
3
+ require 'nokogiri'
4
+
5
+ $: << File.expand_path("../../../lib/", __FILE__)
6
+ require 'asset_pocket/generator'
7
+
8
+ $temp_directory_count = 0
9
+ def make_temp_directory
10
+ temp_dir = Pathname.new("/tmp/asset_pocket_test/t#$$/#{$temp_directory_count += 1}")
11
+ temp_dir.mkpath
12
+ temp_dir
13
+ end
14
+
15
+ class Pathname
16
+ def rmdir_when_empty
17
+ rmdir
18
+ true
19
+ rescue Errno::ENOTEMPTY
20
+ false
21
+ end
22
+ end
23
+
24
+ Before do
25
+ @created_files = []
26
+ @temp_dir = make_temp_directory
27
+
28
+ AssetPocket::SourceFilter::Sass.default_options[:cache_location] = @temp_dir.join("sass-cache").to_s
29
+ end
30
+
31
+ After do
32
+ @created_files.each do |filename|
33
+ filename.delete
34
+ filename.dirname.rmdir_when_empty
35
+ end
36
+ end
37
+
@@ -0,0 +1,6 @@
1
+
2
+ def include_key(regexp)
3
+ simple_matcher("include a key that matchs #{regexp.inspect}") do |given|
4
+ given.keys.grep(regexp).size > 0
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+
2
+ if defined?(Rails)
3
+ require 'asset_pocket/rails'
4
+ end
@@ -0,0 +1,29 @@
1
+
2
+ module AssetPocket
3
+ class Compressor
4
+
5
+ class Error < StandardError; end
6
+
7
+ class <<self
8
+ attr_accessor :available
9
+
10
+ def parse(name, options = {})
11
+ name = name.to_s
12
+ if not available.has_key?(name)
13
+ if handler = options[:handler]
14
+ available[name] = handler
15
+ else
16
+ raise Error, "Unknown compressor: #{name}"
17
+ end
18
+ end
19
+
20
+ hanlder = available[name]
21
+ options.each_pair {|key, value| hanlder.send("#{key}=", value) unless key == :handler }
22
+
23
+ hanlder
24
+ end
25
+ end
26
+
27
+ self.available = {}
28
+ end
29
+ end
@@ -0,0 +1,141 @@
1
+
2
+ require 'RMagick'
3
+ require 'md5'
4
+
5
+ module AssetPocket
6
+ class Pocket
7
+ class SpriteDefition
8
+ attr_accessor :layout, :quality
9
+ attr_reader :name, :css_block, :images_location
10
+
11
+ def initialize(name, css_block)
12
+ @name = name
13
+ @css_block = css_block
14
+ @files = {}
15
+ @layout = :vertical
16
+
17
+ self.images_location = "../images/"
18
+ end
19
+
20
+ def images_location=(location)
21
+ @images_location = css_block.pocket.generator.root_path.join(css_block.filename).dirname.join(location)
22
+ end
23
+
24
+ def generate!
25
+ @images_location.mkpath
26
+
27
+ @files.each_pair do |format, images|
28
+
29
+ sprite_name = @images_location.join("sprite-#{name}.#{format.downcase}")
30
+ url_css_sprite = sprite_name.relative_path_from(css_block.full_filename.dirname)
31
+
32
+ # Compute the canvas size
33
+ width = 0
34
+ height = 0
35
+
36
+ distribute_images images do |image, cx, cy|
37
+ cx = cx + image.columns
38
+ cy = cy + image.rows
39
+
40
+ height = cy if cy > height
41
+ width = cx if cx > width
42
+ end
43
+
44
+ canvas = Magick::Image.new(width, height)
45
+ canvas.format = format
46
+
47
+ distribute_images images do |image, cx, cy|
48
+ canvas.composite! image, cx, cy, Magick::OverCompositeOp
49
+
50
+ css = ".sprite.sprite-#{name}--#{File.basename(image.filename).gsub(/\.\w+$/, "")} {"
51
+ css << "background: url(#{url_css_sprite}) -#{cx}px -#{cy}px;"
52
+ css << "width: #{image.columns}px;"
53
+ css << "height: #{image.rows}px;"
54
+ css << "} \n"
55
+ css_block.content << [ :string, css]
56
+ end
57
+
58
+ images.clear
59
+ quality = quality() # Force variable creation. Block passed to canvas.write losses instance reference (called with instance_eval?)
60
+ canvas.write(sprite_name.to_s) do |info|
61
+ info.quality = quality if quality
62
+ end
63
+ end
64
+ end
65
+
66
+ def use(pattern)
67
+ @files ||= []
68
+ Dir[css_block.pocket.generator.root_path.join(pattern)].each do |image_file|
69
+ begin
70
+ image = Magick::Image.read(image_file)
71
+ if image.size != 1
72
+ # Animated images can not be sprited
73
+ # TODO log the action
74
+ next
75
+ end
76
+
77
+ image = image.first
78
+
79
+ rescue Magick::ImageMagickError
80
+ # TODO log the error
81
+ next
82
+ end
83
+
84
+ @files[image.format] ||= []
85
+ @files[image.format] << image
86
+ end
87
+ end
88
+
89
+ private
90
+ def distribute_images(images, &block)
91
+ cx = cy = 0
92
+ valid_layout = true
93
+
94
+ case layout
95
+ when :vertical
96
+ images.each do |image|
97
+ block.call image, cx, cy
98
+ cy += image.rows
99
+ end
100
+
101
+ when :horizontal
102
+ images.each do |image|
103
+ block.call image, cx, cy
104
+ cx += image.columns
105
+ end
106
+
107
+ when Hash
108
+ if layout[:columns]
109
+ column = 0
110
+ row_height = 0
111
+ columns_per_row = layout[:columns]
112
+
113
+ images.each do |image|
114
+ column += 1
115
+ if column > columns_per_row
116
+ column = 1
117
+ cx = 0
118
+ cy += row_height
119
+ row_height = 0
120
+ end
121
+
122
+ block.call image, cx, cy
123
+ cx += image.columns
124
+ row_height = image.rows if image.rows > row_height
125
+ end
126
+
127
+ else
128
+ valid_layout = false
129
+ end
130
+
131
+ else
132
+ valid_layout = false
133
+ end
134
+
135
+ unless valid_layout
136
+ raise ArgumentError, "Unknown layout: #{layout} in sprite #{name}"
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,107 @@
1
+
2
+ require 'asset_pocket/pocket'
3
+ require 'asset_pocket/source_filter'
4
+ require 'fileutils'
5
+ require 'pathname'
6
+
7
+ module AssetPocket
8
+ class Generator
9
+ attr_reader :root_path
10
+
11
+ def initialize
12
+ self.root_path = "."
13
+ @pocket = nil
14
+ end
15
+
16
+ def root_path=(path)
17
+ @root_path = Pathname.new(path.to_s)
18
+ end
19
+
20
+ def parse_string(content)
21
+ @pocket = Pocket.new(self)
22
+ @pocket.parse_string content
23
+ end
24
+
25
+ def read_file(filename)
26
+ root_path.join(filename).read
27
+ end
28
+
29
+ def run!
30
+ @pocket.definitions.each do |definition|
31
+ send "process_#{definition.process}", definition
32
+ end
33
+ true
34
+ end
35
+
36
+ def process_create_file(definition)
37
+ generated_filename = root_path.join(definition.filename)
38
+ FileUtils.mkpath File.dirname(generated_filename)
39
+ File.open(generated_filename, "w") do |generated_file|
40
+ generated_content = []
41
+ definition.content.each do |content|
42
+ case content[0]
43
+ when :pattern
44
+ generated_content.concat(
45
+ Dir[root_path.join(content[1])].
46
+ sort!.
47
+ map! {|found_file| SourceFilter.filter found_file })
48
+
49
+ when :string
50
+ generated_content << content[1]
51
+
52
+ else
53
+ raise ArgumentError, "Unknown content type: #{content[0]}"
54
+ end
55
+ end
56
+
57
+ generated_content = generated_content.join(definition.separator)
58
+
59
+ if definition.use_compressor?
60
+ generated_content = definition.compressor.compress(generated_content)
61
+ end
62
+
63
+ generated_content = definition.post_process(generated_content)
64
+ generated_file.write(generated_content)
65
+ end
66
+ end
67
+
68
+ def process_copy_files(definition)
69
+ base = Pathname.new(File.expand_path(definition.base, root_path))
70
+ dest_dir = root_path.join(definition.filename)
71
+ dest_dir.mkpath
72
+
73
+ definition.content.each do |content|
74
+ case content[0]
75
+ when :pattern
76
+ Dir[root_path.join(content[1])].each do |source_file|
77
+ source_file = Pathname.new(source_file)
78
+
79
+ # Only work with regular file
80
+ next unless source_file.file?
81
+
82
+ dest_file = dest_dir.join(source_file.relative_path_from(base))
83
+
84
+ if dest_file.exist?
85
+ # Compare it using timestamps and size
86
+ if source_file.size == dest_file.size and source_file.mtime == dest_file.mtime
87
+ next
88
+ end
89
+
90
+ # Remove it, since we have to recreate
91
+ dest_file.delete
92
+ end
93
+
94
+ # Try to create a hard link (Unix on same mount points).
95
+ # If it fails, copy it
96
+ dest_file.dirname.mkpath
97
+ begin
98
+ dest_file.make_link source_file
99
+ rescue Exception => e
100
+ dest_file.open("w") {|f| f.write source_file.read }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,165 @@
1
+
2
+ require 'asset_pocket/compressor'
3
+ require 'asset_pocket/css_sprites'
4
+
5
+ module AssetPocket
6
+ class Pocket
7
+
8
+ attr_reader :definitions, :generator
9
+
10
+ def initialize(generator)
11
+ @generator = generator
12
+ @definitions = []
13
+ end
14
+
15
+ def parse_string(content)
16
+ instance_eval content
17
+ end
18
+
19
+ def compressor(name, options = {})
20
+ AssetPocket::Compressor.parse name, options
21
+ end
22
+
23
+ def sass(option = {}, &block)
24
+ case option
25
+ when Hash
26
+ SourceFilter::Sass.default_options.merge! option
27
+ when String
28
+ defs = SassDefinitions.new(self, option)
29
+ defs.instance_eval(&block)
30
+ definitions << defs
31
+ else
32
+ raise ArgumentError, "Unknown argument type: #{options.class}"
33
+ end
34
+ end
35
+
36
+ def css(filename, &block)
37
+ defs = CSSDefinitions.new(self, filename)
38
+ defs.instance_eval(&block)
39
+ definitions << defs
40
+ end
41
+
42
+ def js(filename, &block)
43
+ defs = JSDefinitions.new(self, filename)
44
+ defs.instance_eval(&block)
45
+ definitions << defs
46
+ end
47
+
48
+ def files(dirname, &block)
49
+ defs = FilesDefinitions.new(self, dirname)
50
+ defs.instance_eval(&block)
51
+ definitions << defs
52
+ end
53
+
54
+ class Definitions
55
+ attr_reader :content, :compressor, :filename, :pocket
56
+
57
+ def initialize(pocket, filename)
58
+ @pocket = pocket
59
+ @content = []
60
+ @filename = filename
61
+ @compressor = nil
62
+ @separator = ""
63
+ end
64
+
65
+ def full_filename
66
+ pocket.generator.root_path.join filename
67
+ end
68
+
69
+ def process
70
+ :create_file
71
+ end
72
+
73
+ def use_compressor?
74
+ @compressor
75
+ end
76
+
77
+ def use(pattern)
78
+ @content << [ :pattern, pattern ]
79
+ end
80
+
81
+ def compress(name, options = {})
82
+ @compressor = AssetPocket::Compressor.parse name, options
83
+ end
84
+
85
+ def separator(new_value = nil)
86
+ @separator = new_value unless new_value.nil?
87
+ @separator
88
+ end
89
+
90
+ def post_process(generated_content)
91
+ generated_content
92
+ end
93
+
94
+ end
95
+
96
+ class CSSDefinitions < Definitions
97
+
98
+ def sprite(name, &block)
99
+ @current_sprite = SpriteDefition.new(name, self)
100
+ block.call
101
+ @current_sprite.generate!
102
+ ensure
103
+ @current_sprite = nil
104
+ end
105
+
106
+ def layout(value)
107
+ ensure_sprite!
108
+ @current_sprite.layout = value
109
+ end
110
+
111
+ def images_location(value)
112
+ ensure_sprite!
113
+ @current_sprite.images_location = value
114
+ end
115
+
116
+ def quality(value)
117
+ ensure_sprite!
118
+ @current_sprite.quality = value
119
+ end
120
+
121
+ def use(pattern)
122
+ if @current_sprite
123
+ @current_sprite.use pattern
124
+ else
125
+ super
126
+ end
127
+ end
128
+
129
+ private
130
+ def ensure_sprite!
131
+ raise NameError, "this macro has to be used in a sprite block" if @current_sprite.nil?
132
+ end
133
+ end
134
+
135
+ class JSDefinitions < Definitions
136
+ end
137
+
138
+ class SassDefinitions < Definitions
139
+ def import(filename)
140
+ @content << [ :string, "@import \"#{File.expand_path(filename, pocket.generator.root_path)}\";\n" ]
141
+ end
142
+
143
+ def post_process(generated_content)
144
+ SourceFilter::Sass.render("#{filename}.scss", generated_content)
145
+ end
146
+
147
+ undef_method :use
148
+ end
149
+
150
+ class FilesDefinitions < Definitions
151
+ def process
152
+ :copy_files
153
+ end
154
+
155
+ def base(value = nil)
156
+ @base = value if value
157
+ @base || "."
158
+ end
159
+
160
+ undef_method :separator
161
+ undef_method :compress
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,15 @@
1
+
2
+ module AssetPocket
3
+ module ViewHelperMethods
4
+ def sprite_tag(name, options = {})
5
+ # TODO options: tag_name, :class, :style, something like that
6
+ # TODO Generate the CSS class name inside AssetPocket, and not hardcoded
7
+ group, image = name.split("/")
8
+ %[<span class="sprite-#{group}--#{image}">&nbsp;</span>].html_safe
9
+ end
10
+ end
11
+ end
12
+
13
+ ActionView::Base.class_eval do
14
+ include AssetPocket::ViewHelperMethods
15
+ end
@@ -0,0 +1,52 @@
1
+
2
+ module AssetPocket
3
+ module SourceFilter
4
+ extend self
5
+
6
+ module Sass
7
+ extend self
8
+
9
+ attr_accessor :default_options
10
+ self.default_options = {
11
+ :css_location => "./tmp/sass-cache"
12
+ }
13
+
14
+ def render(filename, content)
15
+ options = { :filename => filename }.merge(default_options)
16
+
17
+ if filename =~ /\.sass$/i
18
+ options[:syntax] = :sass
19
+ elsif filename =~ /\.scss$/i
20
+ options[:syntax] = :scss
21
+ else
22
+ return content
23
+ end
24
+
25
+ require 'sass' # Load only when needed
26
+
27
+ begin
28
+ ::Sass::Engine.new(content, options).render
29
+
30
+ rescue ::Sass::SyntaxError => error
31
+ "/* #{File.basename(filename)}: #{error.to_s.gsub("*/", "* /")} */"
32
+ end
33
+ end
34
+ end
35
+
36
+ Filters = [
37
+ [ /.*\.s[ac]ss$/i, Sass.method(:render) ]
38
+ ]
39
+
40
+ def filter(filename)
41
+ content = File.read filename
42
+
43
+ Filters.each do |filter|
44
+ if filename =~ filter[0]
45
+ return filter[1].call filename, content
46
+ end
47
+ end
48
+
49
+ content
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,12 @@
1
+
2
+ namespace :pocket do
3
+
4
+ desc "Update the pocket using AssetPocket"
5
+ task :update => :environment do
6
+ require 'asset_pocket/generator'
7
+ generator = AssetPocket::Generator.new
8
+ generator.root_path = Rails.root
9
+ generator.parse_string Rails.root.join("config/pocket.rb").read
10
+ generator.run!
11
+ end
12
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'asset_pocket/rails'
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: asset-pocket
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ version: "0.1"
9
+ platform: ruby
10
+ authors:
11
+ - Ayose Cazorla
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+
16
+ date: 2010-08-17 00:00:00 +01:00
17
+ default_executable:
18
+ dependencies: []
19
+
20
+ description: Using a config file (pocket) you can create multiple kind of assets groups
21
+ email: setepo@gmail.com
22
+ executables: []
23
+
24
+ extensions: []
25
+
26
+ extra_rdoc_files: []
27
+
28
+ files:
29
+ - features/support/matchers.rb
30
+ - features/support/env.rb
31
+ - features/step_definitions/files.rb
32
+ - features/step_definitions/rails.rb
33
+ - features/step_definitions/sprites.rb
34
+ - features/rails.feature
35
+ - features/images.feature
36
+ - features/sass.feature
37
+ - features/javascripts.feature
38
+ - features/sprites.feature
39
+ - lib/asset_pocket/pocket.rb
40
+ - lib/asset_pocket/css_sprites.rb
41
+ - lib/asset_pocket/source_filter.rb
42
+ - lib/asset_pocket/compressor.rb
43
+ - lib/asset_pocket/generator.rb
44
+ - lib/asset_pocket/rails.rb
45
+ - lib/asset_pocket.rb
46
+ - lib/tasks/pocket.rake
47
+ - rails/init.rb
48
+ has_rdoc: true
49
+ homepage: http://github.com/setepo/asset_pocket
50
+ licenses: []
51
+
52
+ post_install_message:
53
+ rdoc_options: []
54
+
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ segments:
69
+ - 0
70
+ version: "0"
71
+ requirements: []
72
+
73
+ rubyforge_project:
74
+ rubygems_version: 1.3.6
75
+ signing_key:
76
+ specification_version: 3
77
+ summary: Manages assets in a versatile way
78
+ test_files: []
79
+