asset-pocket 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+