juicer 0.2.6 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. data/History.txt +28 -0
  2. data/Rakefile +84 -36
  3. data/Readme.rdoc +192 -23
  4. data/VERSION +1 -0
  5. data/bin/juicer +2 -4
  6. data/lib/juicer.rb +9 -10
  7. data/lib/juicer/asset/path.rb +275 -0
  8. data/lib/juicer/asset/path_resolver.rb +79 -0
  9. data/lib/juicer/binary.rb +3 -5
  10. data/lib/juicer/cache_buster.rb +112 -27
  11. data/lib/juicer/command/install.rb +4 -2
  12. data/lib/juicer/command/list.rb +16 -9
  13. data/lib/juicer/command/merge.rb +30 -14
  14. data/lib/juicer/command/verify.rb +1 -1
  15. data/lib/juicer/css_cache_buster.rb +31 -47
  16. data/lib/juicer/datafy/datafy.rb +20 -0
  17. data/lib/juicer/dependency_resolver/css_dependency_resolver.rb +29 -0
  18. data/lib/juicer/dependency_resolver/dependency_resolver.rb +101 -0
  19. data/lib/juicer/dependency_resolver/javascript_dependency_resolver.rb +23 -0
  20. data/lib/juicer/ext/logger.rb +5 -0
  21. data/lib/juicer/ext/string.rb +47 -0
  22. data/lib/juicer/ext/symbol.rb +15 -0
  23. data/lib/juicer/image_embed.rb +129 -0
  24. data/lib/juicer/install/base.rb +2 -2
  25. data/lib/juicer/install/closure_compiler_installer.rb +69 -0
  26. data/lib/juicer/install/jslint_installer.rb +3 -3
  27. data/lib/juicer/install/rhino_installer.rb +3 -2
  28. data/lib/juicer/install/yui_compressor_installer.rb +3 -2
  29. data/lib/juicer/jslint.rb +1 -1
  30. data/lib/juicer/merger/base.rb +1 -1
  31. data/lib/juicer/merger/javascript_merger.rb +3 -4
  32. data/lib/juicer/merger/stylesheet_merger.rb +13 -15
  33. data/lib/juicer/minifyer/closure_compiler.rb +90 -0
  34. data/lib/juicer/minifyer/java_base.rb +77 -0
  35. data/lib/juicer/minifyer/yui_compressor.rb +15 -48
  36. data/test/bin/jslint-1.0.js +523 -0
  37. data/test/bin/jslint.js +523 -0
  38. data/test/bin/rhino1_7R1.zip +0 -0
  39. data/test/bin/rhino1_7R2-RC1.jar +0 -0
  40. data/test/bin/rhino1_7R2-RC1.zip +0 -0
  41. data/test/bin/yuicompressor +0 -0
  42. data/test/bin/yuicompressor-2.3.5.zip +0 -0
  43. data/test/bin/yuicompressor-2.4.2.jar +0 -0
  44. data/test/bin/yuicompressor-2.4.2.zip +0 -0
  45. data/test/data/Changelog.txt +10 -0
  46. data/test/data/a.css +3 -0
  47. data/test/data/a.js +5 -0
  48. data/test/data/a1.css +5 -0
  49. data/test/data/b.css +1 -0
  50. data/test/data/b.js +5 -0
  51. data/test/data/b1.css +5 -0
  52. data/test/data/c1.css +3 -0
  53. data/test/data/css/2.gif +1 -0
  54. data/test/data/css/test.css +11 -0
  55. data/test/data/css/test2.css +1 -0
  56. data/test/data/d1.css +3 -0
  57. data/test/data/images/1.png +1 -0
  58. data/test/data/my_app.js +2 -0
  59. data/test/data/not-ok.js +2 -0
  60. data/test/data/ok.js +3 -0
  61. data/test/data/path_test.css +5 -0
  62. data/test/data/path_test2.css +14 -0
  63. data/test/data/pkg/module/moda.js +2 -0
  64. data/test/data/pkg/module/modb.js +3 -0
  65. data/test/data/pkg/pkg.js +1 -0
  66. data/test/fixtures/yui-download.html +425 -0
  67. data/test/test_helper.rb +36 -7
  68. data/test/unit/juicer/asset/path_resolver_test.rb +76 -0
  69. data/test/unit/juicer/asset/path_test.rb +370 -0
  70. data/test/unit/juicer/cache_buster_test.rb +104 -0
  71. data/test/{juicer/test_chainable.rb → unit/juicer/chainable_test.rb} +1 -1
  72. data/test/unit/juicer/command/install_test.rb +58 -0
  73. data/test/{juicer/command/test_list.rb → unit/juicer/command/list_test.rb} +26 -14
  74. data/test/unit/juicer/command/merge_test.rb +162 -0
  75. data/test/{juicer/command/test_util.rb → unit/juicer/command/util_test.rb} +10 -6
  76. data/test/unit/juicer/command/verify_test.rb +48 -0
  77. data/test/{juicer/test_css_cache_buster.rb → unit/juicer/css_cache_buster_test.rb} +10 -30
  78. data/test/unit/juicer/datafy_test.rb +37 -0
  79. data/test/{juicer/merger/test_css_dependency_resolver.rb → unit/juicer/dependency_resolver/css_dependency_resolver_test.rb} +2 -2
  80. data/test/{juicer/merger/test_javascript_dependency_resolver.rb → unit/juicer/dependency_resolver/javascript_dependency_resolver_test.rb} +13 -2
  81. data/test/unit/juicer/ext/{#string_test.rb# → string_test.rb} +0 -7
  82. data/test/unit/juicer/ext/symbol_test.rb +27 -0
  83. data/test/unit/juicer/image_embed_test.rb +271 -0
  84. data/test/unit/juicer/install/installer_base_test.rb +214 -0
  85. data/test/{juicer/install/test_jslint_installer.rb → unit/juicer/install/jslint_installer_test.rb} +1 -1
  86. data/test/{juicer/install/test_rhino_installer.rb → unit/juicer/install/rhino_installer_test.rb} +1 -1
  87. data/test/{juicer/install/test_yui_compressor_installer.rb → unit/juicer/install/yui_compressor_test.rb} +16 -16
  88. data/test/unit/juicer/jslint_test.rb +60 -0
  89. data/test/{juicer/merger/test_base.rb → unit/juicer/merger/base_test.rb} +1 -1
  90. data/test/{juicer/merger/test_javascript_merger.rb → unit/juicer/merger/javascript_merger_test.rb} +2 -2
  91. data/test/{juicer/merger/test_stylesheet_merger.rb → unit/juicer/merger/stylesheet_merger_test.rb} +15 -13
  92. data/test/unit/juicer/minifyer/closure_compressor_test.rb +107 -0
  93. data/test/{integration → unit}/juicer/minifyer/yui_compressor_test.rb +30 -47
  94. data/test/unit/juicer_test.rb +1 -0
  95. metadata +207 -113
  96. data/lib/juicer/core.rb +0 -61
  97. data/lib/juicer/merger/css_dependency_resolver.rb +0 -25
  98. data/lib/juicer/merger/dependency_resolver.rb +0 -82
  99. data/lib/juicer/merger/javascript_dependency_resolver.rb +0 -21
  100. data/tasks/ann.rake +0 -80
  101. data/tasks/bones.rake +0 -20
  102. data/tasks/gem.rake +0 -201
  103. data/tasks/git.rake +0 -40
  104. data/tasks/notes.rake +0 -27
  105. data/tasks/post_load.rake +0 -34
  106. data/tasks/rdoc.rake +0 -51
  107. data/tasks/rubyforge.rake +0 -55
  108. data/tasks/setup.rb +0 -292
  109. data/tasks/spec.rake +0 -54
  110. data/tasks/svn.rake +0 -47
  111. data/tasks/test.rake +0 -40
  112. data/tasks/test/setup.rake +0 -35
  113. data/tasks/zentest.rake +0 -36
  114. data/test/juicer/command/test_install.rb +0 -53
  115. data/test/juicer/command/test_merge.rb +0 -160
  116. data/test/juicer/command/test_verify.rb +0 -33
  117. data/test/juicer/install/test_installer_base.rb +0 -195
  118. data/test/juicer/minifyer/test_yui_compressor.rb +0 -159
  119. data/test/juicer/test_cache_buster.rb +0 -58
  120. data/test/juicer/test_core.rb +0 -47
  121. data/test/juicer/test_jslint.rb +0 -33
  122. data/test/test_juicer.rb +0 -4
@@ -1,4 +1,4 @@
1
- require File.join(File.dirname(__FILE__), "util")
1
+ require "juicer/command/util"
2
2
  require "cmdparse"
3
3
  require "pathname"
4
4
 
@@ -29,7 +29,9 @@ into Juicer installation directory, usually ~/.juicer
29
29
 
30
30
  # Execute command
31
31
  #
32
- def execute(args)
32
+ def execute(*args)
33
+ args.flatten!
34
+
33
35
  if args.length == 0
34
36
  raise ArgumentError.new('Please provide a library to install')
35
37
  end
@@ -1,4 +1,4 @@
1
- require File.join(File.dirname(__FILE__), "util")
1
+ require "juicer/command/util"
2
2
  require "cmdparse"
3
3
  require "pathname"
4
4
 
@@ -12,9 +12,9 @@ module Juicer
12
12
 
13
13
  # Initializes command
14
14
  #
15
- def initialize(io = STDOUT)
15
+ def initialize(log = nil)
16
16
  super('list', false, true)
17
- @io = io
17
+ @log = log
18
18
  self.short_desc = "Lists all dependencies for all input files/patterns"
19
19
  self.description = <<-EOF
20
20
  Dependencies are looked up recursively. The dependency chain reveals which files
@@ -34,16 +34,23 @@ Input parameters may be:
34
34
  raise ArgumentError.new('Please provide atleast one input file/pattern')
35
35
  end
36
36
 
37
- types = { :js => Juicer::Merger::JavaScriptDependencyResolver.new,
38
- :css => Juicer::Merger::CssDependencyResolver.new }
37
+ types = { :js => Juicer::JavaScriptDependencyResolver.new,
38
+ :css => Juicer::CssDependencyResolver.new }
39
39
 
40
- files(args).each do |file|
40
+ result = files(args).map { |file|
41
41
  type = file.split(".").pop.to_sym
42
42
  raise FileNotFoundError.new("Unable to guess type (CSS/JavaScript) of file #{relative(file)}") unless types[type]
43
43
 
44
- @io.puts "Dependency chain for #{relative file}:"
45
- @io.puts " #{relative(types[type].resolve(file)).join("\n ")}\n\n"
46
- end
44
+ deps = relative types[type].resolve(file)
45
+ # there may only be one dependency, which resolve() returns as a string
46
+ deps = deps.join("\n ") if deps.is_a? Array
47
+
48
+ "Dependency chain for #{relative file}:\n #{deps}"
49
+ }.join("\n\n") + "\n"
50
+
51
+ @log.info result
52
+
53
+ result
47
54
  end
48
55
  end
49
56
  end
@@ -1,5 +1,5 @@
1
- require File.join(File.dirname(__FILE__), "util")
2
- require File.join(File.dirname(__FILE__), "verify")
1
+ require "juicer/command/util"
2
+ require "juicer/command/verify"
3
3
  require "cmdparse"
4
4
  require "pathname"
5
5
 
@@ -25,11 +25,12 @@ module Juicer
25
25
  @ignore = false # Ignore syntax problems if true
26
26
  @cache_buster = :soft # What kind of cache buster to use, :soft or :hard
27
27
  @hosts = nil # Hosts to use when replacing URLs in stylesheets
28
- @web_root = nil # Used to understand absolute paths
28
+ @document_root = nil # Used to understand absolute paths
29
29
  @relative_urls = false # Make the merger use relative URLs
30
30
  @absolute_urls = false # Make the merger use absolute URLs
31
- @local_hosts = [] # Host names that are served from :web_root
31
+ @local_hosts = [] # Host names that are served from :document_root
32
32
  @verify = true # Verify js files with JsLint
33
+ @image_embed_type = :none # Embed images in css files, options are :none, :data_uri
33
34
 
34
35
  @log = log || Logger.new(STDOUT)
35
36
 
@@ -47,16 +48,18 @@ inside the first multi-line comment. The switch is @depend or @depends, your
47
48
  choice.
48
49
 
49
50
  The -m --minifyer switch can be used to select which minifyer to use. Currently
50
- only YUI Compressor is supported, ie -m yui_compressor (default). When using
51
- the YUI Compressor the path should be the path to where the jar file is found.
51
+ only YUI Compressor and Google Closure Compiler is supported, ie -m yui_compressor (default) or -m closure_compiler. When using
52
+ the compressor the path should be the path to where the jar file is found.
52
53
  EOF
53
54
 
54
55
  self.options = CmdParse::OptionParserWrapper.new do |opt|
55
56
  opt.on("-o", "--output file", "Output filename") { |filename| @output = filename }
56
57
  opt.on("-p", "--path path", "Path to compressor binary") { |path| @opts[:bin_path] = path }
57
- opt.on("-m", "--minifyer name", "Which minifer to use. Currently only supports yui_compressor") { |name| @minifyer = name }
58
+ opt.on("-m", "--minifyer name", "Which minifer to use. Currently only supports yui_compressor and closure compiler") { |name| @minifyer = name }
58
59
  opt.on("-f", "--force", "Force overwrite of target file") { @force = true }
59
- opt.on("-a", "--arguments arguments", "Arguments to minifyer, escape with quotes") { |arguments| @arguments = arguments }
60
+ opt.on("-a", "--arguments arguments", "Arguments to minifyer, escape with quotes") { |arguments|
61
+ @arguments = arguments.to_s.gsub(/(^['"]|["']$)/, "")
62
+ }
60
63
  opt.on("-i", "--ignore-problems", "Merge and minify even if verifyer finds problems") { @ignore = true }
61
64
  opt.on("-s", "--skip-verification", "Skip JsLint verification (js files only). Not recomended!") { @verify = false }
62
65
  opt.on("-t", "--type type", "Juicer can only guess type when files have .css or .js extensions. Specify js or\n" +
@@ -70,11 +73,15 @@ the YUI Compressor the path should be the path to where the jar file is found.
70
73
  (" " * 37) + "absolute URLs are used. Only valid for CSS files") { |t| @relative_urls = true }
71
74
  opt.on("-b", "--absolute-urls", "Convert all referenced URLs to absolute URLs. Requires --document-root.\n" +
72
75
  (" " * 37) + "Works with cycled asset hosts. Only valid for CSS files") { |t| @absolute_urls = true }
73
- opt.on("-d", "--document-root dir", "Path to resolve absolute URLs relative to") { |path| @web_root = path }
76
+ opt.on("-d", "--document-root dir", "Path to resolve absolute URLs relative to") { |path| @document_root = path }
74
77
  opt.on("-c", "--cache-buster type", "none, soft or hard. Default is soft, which adds timestamps to referenced\n" +
75
78
  (" " * 37) + "URLs as query parameters. None leaves URLs untouched and hard alters file names") do |type|
76
79
  @cache_buster = [:soft, :hard].include?(type.to_sym) ? type.to_sym : nil
77
80
  end
81
+ opt.on("-e", "--embed-images type", "none or data_uri. Default is none. Data_uri embeds images using Base64 encoding\n" +
82
+ (" " * 37) + "None leaves URLs untouched. Candiate images must be flagged with '?embed=true to be considered") do |embed|
83
+ @image_embed_type = [:none, :data_uri].include?(embed.to_sym) ? embed.to_sym : nil
84
+ end
78
85
  end
79
86
  end
80
87
 
@@ -101,7 +108,7 @@ the YUI Compressor the path should be the path to where the jar file is found.
101
108
  # confused
102
109
  merger = merger(output).new(files, :relative_urls => @relative_urls,
103
110
  :absolute_urls => @absolute_urls,
104
- :web_root => @web_root,
111
+ :document_root => @document_root,
105
112
  :hosts => @hosts)
106
113
 
107
114
  # Fail if syntax trouble (js only)
@@ -112,7 +119,7 @@ the YUI Compressor the path should be the path to where the jar file is found.
112
119
  end
113
120
 
114
121
  # Set command chain and execute
115
- merger.set_next(cache_buster(output)).set_next(minifyer)
122
+ merger.set_next(image_embed(output)).set_next(cache_buster(output)).set_next(minifyer)
116
123
  merger.save(output)
117
124
 
118
125
  # Print report
@@ -120,7 +127,7 @@ the YUI Compressor the path should be the path to where the jar file is found.
120
127
  merger.files.each { |file| @log.info " #{relative file}" }
121
128
  rescue FileNotFoundError => err
122
129
  # Handle missing document-root option
123
- puts err.message.sub(/:web_root/, "--document-root")
130
+ puts err.message.sub(/:document_root/, "--document-root")
124
131
  end
125
132
 
126
133
  private
@@ -137,7 +144,8 @@ the YUI Compressor the path should be the path to where the jar file is found.
137
144
  @log.debug "Using #{@minifyer.camel_case} for minification"
138
145
 
139
146
  return compressor
140
- rescue NameError
147
+ rescue NameError => e
148
+ @log.fatal e.message
141
149
  @log.fatal "No such minifyer '#{@minifyer}', aborting"
142
150
  raise SystemExit.new("No such minifyer '#{@minifyer}', aborting")
143
151
  rescue FileNotFoundError => e
@@ -172,9 +180,17 @@ the YUI Compressor the path should be the path to where the jar file is found.
172
180
  #
173
181
  def cache_buster(file)
174
182
  return nil if !file || file !~ /\.css$/ || @cache_buster.nil?
175
- Juicer::CssCacheBuster.new(:web_root => @web_root, :type => @cache_buster, :hosts => @local_hosts)
183
+ Juicer::CssCacheBuster.new(:document_root => @document_root, :type => @cache_buster, :hosts => @local_hosts)
176
184
  end
177
185
 
186
+ #
187
+ # Load image embed, only available for CSS files
188
+ #
189
+ def image_embed(file)
190
+ return nil if !file || file !~ /\.css$/ || @image_embed_type.nil?
191
+ Juicer::ImageEmbed.new( :type => @image_embed_type )
192
+ end
193
+
178
194
  #
179
195
  # Generate output file name. Optional argument is a filename to base the new
180
196
  # name on. It will prepend the original suffix with ".min"
@@ -1,4 +1,4 @@
1
- require File.join(File.dirname(__FILE__), "util")
1
+ require "juicer/command/util"
2
2
  require "rubygems"
3
3
  require "cmdparse"
4
4
  require "pathname"
@@ -1,5 +1,6 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), "chainable"))
2
- require File.expand_path(File.join(File.dirname(__FILE__), "cache_buster"))
1
+ require "juicer/chainable"
2
+ require "juicer/cache_buster"
3
+ require "juicer/asset/path_resolver"
3
4
 
4
5
  module Juicer
5
6
  #
@@ -12,23 +13,23 @@ module Juicer
12
13
  # work.
13
14
  #
14
15
  # When dealing with CSS files that reference absolute URLs like /images/1.png
15
- # you must specify the :web_root option that these URLs should be resolved
16
+ # you must specify the :document_root option that these URLs should be resolved
16
17
  # against.
17
18
  #
18
19
  # When dealing with full URLs (ie including hosts) you can optionally specify
19
20
  # an array of hosts to recognize as "local", meaning they serve assets from
20
- # the :web_root directory. This way even asset host cycling can benefit from
21
+ # the :document_root directory. This way even asset host cycling can benefit from
21
22
  # cache busters.
22
23
  #
23
24
  class CssCacheBuster
24
25
  include Juicer::Chainable
25
26
 
26
27
  def initialize(options = {})
27
- @web_root = options[:web_root]
28
- @web_root.sub!(%r{/?$}, "") if @web_root # Remove trailing slash
28
+ @document_root = options[:document_root]
29
+ @document_root.sub!(%r{/?$}, "") if @document_root
29
30
  @type = options[:type] || :soft
30
- @hosts = (options[:hosts] || []).collect { |h| h.sub!(%r{/?$}, "") } # Remove trailing slashes
31
- @contents = nil
31
+ @hosts = (options[:hosts] || []).collect { |h| h.sub!(%r{/?$}, "") }
32
+ @contents = @base = nil
32
33
  end
33
34
 
34
35
  #
@@ -36,20 +37,21 @@ module Juicer
36
37
  #
37
38
  def save(file, output = nil)
38
39
  @contents = File.read(file)
40
+ self.base = File.dirname(file)
39
41
  used = []
40
42
 
41
- urls(file).each do |url|
43
+ urls(file).each do |asset|
42
44
  begin
43
- path = resolve(url, file)
44
- next if used.include?(path)
45
-
46
- if path != url
47
- used << path
48
- basename = File.basename(Juicer::CacheBuster.path(path, @type))
49
- @contents.gsub!(url, File.join(File.dirname(url), basename))
50
- end
45
+ next if used.include?(asset.path)
46
+ @contents.gsub!(asset.path, asset.path(:cache_buster_type => @type))
51
47
  rescue Errno::ENOENT
52
- puts "Unable to locate file #{path || url}, skipping cache buster"
48
+ puts "Unable to locate file #{asset.path}, skipping cache buster"
49
+ rescue ArgumentError => e
50
+ if e.message =~ /No document root/
51
+ raise FileNotFoundError.new("Unable to resolve path #{asset.path} without :document_root option")
52
+ else
53
+ raise e
54
+ end
53
55
  end
54
56
  end
55
57
 
@@ -67,40 +69,22 @@ module Juicer
67
69
  @contents = File.read(file) unless @contents
68
70
 
69
71
  @contents.scan(/url\([\s"']*([^\)"'\s]*)[\s"']*\)/m).collect do |match|
70
- match.first
72
+ path_resolver.resolve(match.first)
71
73
  end
72
74
  end
73
75
 
74
- #
75
- # Resolve full path from URL
76
- #
77
- def resolve(target, from)
78
- # If URL is external, check known hosts to see if URL can be treated
79
- # like a local one (ie so we can add cache buster)
80
- catch(:continue) do
81
- if target =~ %r{^[a-z]+\://}
82
- # This could've been a one-liner, but I prefer to be
83
- # able to read my own code ;)
84
- @hosts.each do |host|
85
- if target =~ /^#{host}/
86
- target.sub!(/^#{host}/, "")
87
- throw :continue
88
- end
89
- end
90
-
91
- # No known hosts matched, return
92
- return target
93
- end
94
- end
76
+ protected
77
+ def base=(base)
78
+ @prev_base = @base
79
+ @base = base
80
+ end
95
81
 
96
- # Simply add web root to absolute URLs
97
- if target =~ %r{^/}
98
- raise FileNotFoundError.new("Unable to resolve absolute path #{target} without :web_root option") unless @web_root
99
- return File.expand_path(File.join(@web_root, target))
100
- end
82
+ def path_resolver
83
+ return @path_resolver if @path_resolver && @base == @prev_base
101
84
 
102
- # Resolve relative URLs to full paths
103
- File.expand_path(File.join(File.dirname(File.expand_path(from)), target))
85
+ @path_resolver = Juicer::Asset::PathResolver.new(:document_root => @document_root,
86
+ :hosts => @hosts,
87
+ :base => @base)
104
88
  end
105
89
  end
106
90
  end
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ # Datafy code lifted from http://segment7.net/projects/ruby/datafy/
4
+
5
+ require 'base64'
6
+ require 'cgi'
7
+
8
+ module Datafy
9
+ def Datafy::make_data_uri(content, content_type)
10
+ outuri = 'data:' + content_type
11
+ unless content_type =~ /^text/i # base64 encode if not text
12
+ outuri += ';base64'
13
+ content = Base64.encode64(content).gsub("\n", '')
14
+ else
15
+ content = CGI::escape(content)
16
+ end
17
+ outuri += ",#{content}"
18
+ end
19
+
20
+ end
@@ -0,0 +1,29 @@
1
+ require "juicer/dependency_resolver/dependency_resolver"
2
+
3
+ module Juicer
4
+
5
+ # Resolves @import statements in CSS files and builds a list of all
6
+ # files, in order.
7
+ #
8
+ class CssDependencyResolver < DependencyResolver
9
+ # Regexp borrowed from similar project:
10
+ # http://github.com/cgriego/front-end-blender/tree/master/lib/front_end_architect/blender.rb
11
+ @@import_pattern = /^\s*@import(?:\surl\(|\s)(['"]?)([^\?'"\)\s]+)(\?(?:[^'"\)]+)?)?\1\)?(?:[^?;]+)?;?/im
12
+
13
+ private
14
+ def parse(line, imported_file = nil)
15
+ return $2 if line =~ @@import_pattern
16
+
17
+ # At first sight of actual CSS rules we abort (TODO: This does not take
18
+ # into account the fact that rules may be commented out and that more
19
+ # imports may follow)
20
+ throw :done if imported_file && line =~ %r{/*}
21
+ throw :done if line =~ /^[\.\#a-zA-Z\:]/
22
+ end
23
+
24
+ def extension
25
+ ".css"
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,101 @@
1
+ module Juicer
2
+ class DependencyResolver
3
+ include Enumerable
4
+ attr_reader :files
5
+
6
+ # Constructor
7
+ def initialize(options = {})
8
+ @files = []
9
+ @options = options
10
+ end
11
+
12
+ #
13
+ # Resolve dependencies.
14
+ # This method accepts an optional block. The block will receive each
15
+ # file in succession. The file is included in the returned collection
16
+ # if the block is true for the given file. Without a block every found
17
+ # file is returned.
18
+ #
19
+ def resolve(file, &block)
20
+ @files = []
21
+ _resolve(file, &block)
22
+ end
23
+
24
+ #
25
+ # Yield files recursively. Resolve dependencies first, then call each, or
26
+ # any other enumerable methods.
27
+ #
28
+ def each(&block)
29
+ @files.each(&block)
30
+ end
31
+
32
+ #
33
+ # Resolves a path relative to another. If the path is absolute (ie it
34
+ # starts with a protocol or /) the <tt>:document_root</tt> options has to be
35
+ # set as well.
36
+ #
37
+ def resolve_path(path, reference)
38
+ # Absolute URL
39
+ if path =~ %r{^(/|[a-z]+:)}
40
+ if @options[:document_root].nil?
41
+ msg = "Cannot resolve absolute path '#{path}' without web root option"
42
+ raise ArgumentError.new(msg)
43
+ end
44
+
45
+ path.sub!(%r{^[a-z]+://[^/]+/}, '')
46
+ return File.expand_path(File.join(@options[:document_root], path))
47
+ end
48
+
49
+ File.expand_path(File.join(File.dirname(reference), path))
50
+ end
51
+
52
+ private
53
+ def parse(line)
54
+ raise NotImplementedError.new
55
+ end
56
+
57
+ def extension
58
+ raise NotImplementedError.new
59
+ end
60
+
61
+ #
62
+ # Carries out the actual work of resolve. resolve resets the internal
63
+ # file list and yields control to _resolve for rebuilding the file list.
64
+ #
65
+ def _resolve(file)
66
+ imported_path = nil
67
+
68
+ IO.foreach(file) do |line|
69
+ # Implementing subclasses may throw :done from the parse method when
70
+ # the file is exhausted for dependency declaration possibilities.
71
+ catch(:done) do
72
+ imported_path = parse(line, imported_path)
73
+
74
+ # If a dependency declaration was found
75
+ if imported_path
76
+ # Resolves a path relative to the file that imported it
77
+ imported_path = resolve_path(imported_path, file)
78
+
79
+ if File.directory?(imported_path)
80
+ imported_files = Dir.glob(File.join(imported_path, "**", "*#{extension}"))
81
+ else
82
+ imported_files = [imported_path]
83
+ end
84
+
85
+ imported_files.each do |imported_file|
86
+ # Only keep processing file if it's not already included.
87
+ # Yield to block to allow caller to ignore file
88
+ if !@files.include?(imported_file) && (!block_given? || yield(imported_file))
89
+ # Check this file for imports before adding it to get order right
90
+ _resolve(imported_file) { |f| f != File.expand_path(file) }
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ file = File.expand_path(file)
98
+ @files << file if !@files.include?(file) && (!block_given? || yield(file))
99
+ end
100
+ end
101
+ end