juicer 0.2.6 → 1.0.0

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