bunch 0.2.2 → 1.0.0pre1

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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -6
  3. data/Gemfile +3 -1
  4. data/Guardfile +5 -0
  5. data/LICENSE.txt +22 -0
  6. data/Rakefile +7 -12
  7. data/bin/bunch +2 -5
  8. data/bunch.gemspec +30 -23
  9. data/lib/bunch.rb +37 -81
  10. data/lib/bunch/cli.rb +40 -74
  11. data/lib/bunch/combiner.rb +121 -0
  12. data/lib/bunch/compiler.rb +52 -0
  13. data/lib/bunch/compilers/coffee_script.rb +23 -0
  14. data/lib/bunch/compilers/ejs.rb +28 -0
  15. data/lib/bunch/compilers/jade.rb +28 -0
  16. data/lib/bunch/compilers/jst.rb +38 -0
  17. data/lib/bunch/compilers/null.rb +19 -0
  18. data/lib/bunch/compilers/sass.rb +55 -0
  19. data/lib/bunch/content_hash.rb +37 -0
  20. data/lib/bunch/css_minifier.rb +121 -0
  21. data/lib/bunch/file.rb +18 -0
  22. data/lib/bunch/file_cache.rb +159 -0
  23. data/lib/bunch/file_tree.rb +153 -0
  24. data/lib/bunch/ignorer.rb +38 -0
  25. data/lib/bunch/js_minifier.rb +38 -0
  26. data/lib/bunch/middleware.rb +16 -67
  27. data/lib/bunch/pipeline.rb +30 -0
  28. data/lib/bunch/server.rb +56 -0
  29. data/lib/bunch/simple_cache.rb +36 -0
  30. data/lib/bunch/tree_merge.rb +29 -0
  31. data/lib/bunch/version.rb +3 -1
  32. data/spec/bunch/cli_spec.rb +85 -0
  33. data/spec/bunch/combiner_spec.rb +107 -0
  34. data/spec/bunch/compiler_spec.rb +73 -0
  35. data/spec/bunch/compilers/coffee_script_spec.rb +23 -0
  36. data/spec/bunch/compilers/ejs_spec.rb +27 -0
  37. data/spec/bunch/compilers/jade_spec.rb +28 -0
  38. data/spec/bunch/compilers/sass_spec.rb +120 -0
  39. data/spec/bunch/css_minifier_spec.rb +31 -0
  40. data/spec/bunch/file_cache_spec.rb +151 -0
  41. data/spec/bunch/file_tree_spec.rb +127 -0
  42. data/spec/bunch/ignorer_spec.rb +26 -0
  43. data/spec/bunch/js_minifier_spec.rb +35 -0
  44. data/spec/bunch/middleware_spec.rb +41 -0
  45. data/spec/bunch/pipeline_spec.rb +31 -0
  46. data/spec/bunch/server_spec.rb +90 -0
  47. data/spec/bunch/simple_cache_spec.rb +55 -0
  48. data/spec/bunch/tree_merge_spec.rb +30 -0
  49. data/spec/bunch_spec.rb +6 -0
  50. data/spec/example_tree/directory/_combine +2 -0
  51. data/{example/js/test1.js → spec/example_tree/directory/file1} +0 -0
  52. data/{example/js/test2/test2a.js → spec/example_tree/directory/file2} +0 -0
  53. data/{example/js/test2/test2c.js → spec/example_tree/file3} +0 -0
  54. data/spec/spec_helper.rb +38 -0
  55. metadata +224 -102
  56. data/.yardopts +0 -1
  57. data/README.md +0 -4
  58. data/config.ru +0 -6
  59. data/example/config.ru +0 -6
  60. data/example/css/test1.css +0 -1
  61. data/example/css/test2/test2a.scss +0 -1
  62. data/example/css/test2/test2b.css +0 -1
  63. data/example/js/.bunchignore +0 -1
  64. data/example/js/test2/_.yml +0 -2
  65. data/example/js/test2/foo.js +0 -1
  66. data/example/js/test2/test2b.js +0 -1
  67. data/example/js/test3/test3a.js +0 -1
  68. data/example/js/test3/test3b/_.yml +0 -1
  69. data/example/js/test3/test3b/test3bi.js +0 -1
  70. data/example/js/test3/test3b/test3bii.js +0 -1
  71. data/example/js/test4/_.yml +0 -1
  72. data/example/js/test4/test4a.js +0 -1
  73. data/example/js/test4/test4b.coffee +0 -1
  74. data/example/js/test4/test4c.coffee +0 -1
  75. data/example/js/test5/test5a.jst.ejs +0 -1
  76. data/lib/bunch/abstract_node.rb +0 -25
  77. data/lib/bunch/cache.rb +0 -40
  78. data/lib/bunch/coffee_node.rb +0 -39
  79. data/lib/bunch/directory_node.rb +0 -82
  80. data/lib/bunch/ejs_node.rb +0 -50
  81. data/lib/bunch/file_node.rb +0 -25
  82. data/lib/bunch/jade_node.rb +0 -50
  83. data/lib/bunch/null_node.rb +0 -11
  84. data/lib/bunch/rack.rb +0 -38
  85. data/lib/bunch/sass_node.rb +0 -39
  86. data/test/middleware_test.rb +0 -26
  87. data/test/rack_test.rb +0 -93
  88. data/test/test_helper.rb +0 -21
@@ -0,0 +1,121 @@
1
+ # encoding: utf-8
2
+
3
+ module Bunch
4
+ class Combiner
5
+ def initialize(tree)
6
+ @input = tree
7
+ @output = FileTree.new
8
+ end
9
+
10
+ def result
11
+ @path = []
12
+ @contexts = []
13
+ @input.accept(self)
14
+ @output
15
+ end
16
+
17
+ def enter_tree(tree)
18
+ if tree.name
19
+ @path << tree.name
20
+
21
+ combine_file = tree.get("_combine")
22
+
23
+ if combine_file || combining?
24
+ ordering = combine_file ? combine_file.content : ""
25
+ push_context Context.new(@path, ordering)
26
+ end
27
+ end
28
+ end
29
+
30
+ def leave_tree(tree)
31
+ if tree.name
32
+ @path.pop
33
+ end
34
+
35
+ if combining?
36
+ this_context = pop_context
37
+
38
+ if this_context.empty?
39
+ # do nothing
40
+ elsif combining?
41
+ context.add tree.path, this_context.content, this_context.extension
42
+ else
43
+ write_file tree.path, this_context.content, this_context.extension
44
+ end
45
+ end
46
+ end
47
+
48
+ def visit_file(file)
49
+ return if file.path == "_combine"
50
+
51
+ if combining?
52
+ context.add file.path, file.content, file.extension
53
+ else
54
+ write_file file.path, file.content
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def write_file(relative_path, content, extension = "")
61
+ path = absolute_path(relative_path) + extension
62
+ @output.write path, content
63
+ end
64
+
65
+ def absolute_path(relative_path)
66
+ [*@path, relative_path].join("/")
67
+ end
68
+
69
+ def push_context(hash)
70
+ @contexts.push hash
71
+ end
72
+
73
+ def pop_context
74
+ @contexts.pop
75
+ end
76
+
77
+ def context
78
+ @contexts.last
79
+ end
80
+
81
+ def combining?
82
+ @contexts.any?
83
+ end
84
+
85
+ class Context
86
+ attr_accessor :ordering, :extension
87
+
88
+ def initialize(path, ordering_file_contents)
89
+ @path = path.join("/")
90
+ @content = {}
91
+ @ordering = ordering_file_contents.split("\n")
92
+ @extension = nil
93
+ @empty = true
94
+ end
95
+
96
+ def add(path, content, extension)
97
+ @content[path.chomp(extension)] = content
98
+ @extension ||= extension
99
+ @empty = false
100
+
101
+ if @extension != extension
102
+ message = "Incompatible types ('#{extension}' vs '#{@path}/#{path}')"
103
+ message << "\n #{@content.keys.join(', ')}"
104
+ raise message
105
+ end
106
+ end
107
+
108
+ def content
109
+ ordered, unordered = \
110
+ @content.partition { |fn, _| @ordering.include?(fn) }
111
+ ordered.sort_by! { |fn, _| @ordering.index(fn) }
112
+ unordered.sort_by! { |fn, _| fn }
113
+ (ordered + unordered).map(&:last).join("\n")
114
+ end
115
+
116
+ def empty?
117
+ @empty
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,52 @@
1
+ # encoding: UTF-8
2
+
3
+ module Bunch
4
+ class Compiler
5
+ def self.register(extension, klass)
6
+ compilers[extension] = klass
7
+ end
8
+
9
+ def self.compilers
10
+ @compilers ||= {}
11
+ end
12
+
13
+ def initialize(tree)
14
+ @input = tree
15
+ @output = FileTree.new
16
+ end
17
+
18
+ def result
19
+ @path = []
20
+ @input.accept(self)
21
+ @output
22
+ end
23
+
24
+ def enter_tree(tree)
25
+ @path << tree.name if tree.name
26
+ end
27
+
28
+ def leave_tree(tree)
29
+ @path.pop if tree.name
30
+ end
31
+
32
+ def visit_file(file)
33
+ compiler = compiler_for file
34
+ write_file compiler.path, compiler.content
35
+ end
36
+
37
+ private
38
+
39
+ def compiler_for(file)
40
+ klass = self.class.compilers.fetch file.extension, Compilers::Null
41
+ klass.new file, @input, absolute_path(file.path)
42
+ end
43
+
44
+ def write_file(relative_path, content)
45
+ @output.write absolute_path(relative_path), content
46
+ end
47
+
48
+ def absolute_path(relative_path)
49
+ (@path + [relative_path]).join("/")
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: UTF-8
2
+
3
+ module Bunch
4
+ module Compilers
5
+ class CoffeeScript
6
+ def initialize(file, *)
7
+ require "coffee-script"
8
+ @file = file
9
+ rescue LoadError => e
10
+ raise "'gem install coffee-script' to compile .coffee files."
11
+ end
12
+
13
+ def path
14
+ @file.path.chomp(".coffee") + ".js"
15
+ end
16
+
17
+ def content
18
+ ::CoffeeScript.compile(@file.content)
19
+ end
20
+ end
21
+ end
22
+ Compiler.register ".coffee", Compilers::CoffeeScript
23
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: UTF-8
2
+
3
+ require "bunch/compilers/jst"
4
+
5
+ module Bunch
6
+ module Compilers
7
+ class EJS < JST
8
+ def initialize(*)
9
+ super
10
+ require "ejs"
11
+ rescue LoadError => e
12
+ raise "'gem install ejs' to compile .ejs files."
13
+ end
14
+
15
+ private
16
+
17
+ def compile(content)
18
+ ::EJS.compile content
19
+ end
20
+
21
+ def extension
22
+ ".ejs"
23
+ end
24
+ end
25
+ end
26
+ Compiler.register ".jst.ejs", Compilers::EJS
27
+ Compiler.register ".ejs", Compilers::EJS
28
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: UTF-8
2
+
3
+ require "bunch/compilers/jst"
4
+
5
+ module Bunch
6
+ module Compilers
7
+ class Jade < JST
8
+ def initialize(*)
9
+ require "ruby-jade"
10
+ super
11
+ rescue LoadError => e
12
+ raise "'gem install ruby-jade' to compile .jade files."
13
+ end
14
+
15
+ private
16
+
17
+ def compile(content)
18
+ ::Jade.compile content
19
+ end
20
+
21
+ def extension
22
+ ".jade"
23
+ end
24
+ end
25
+ end
26
+ Compiler.register ".jst.jade", Compilers::Jade
27
+ Compiler.register ".jade", Compilers::Jade
28
+ end
@@ -0,0 +1,38 @@
1
+ # encoding: UTF-8
2
+
3
+ module Bunch
4
+ module Compilers
5
+ class JST
6
+ def initialize(file, _, path)
7
+ @file, @path = file, path
8
+ end
9
+
10
+ def path
11
+ "#{template_name}.js"
12
+ end
13
+
14
+ def content
15
+ @content ||= <<-JAVASCRIPT
16
+ (function() {
17
+ this.JST || (this.JST = {});
18
+ this.JST['#{template_name}'] = #{compile(@file.content)};
19
+ })();
20
+ JAVASCRIPT
21
+ end
22
+
23
+ private
24
+
25
+ def compile(content)
26
+ # override in subclasses
27
+ end
28
+
29
+ def extension
30
+ # override in subclasses
31
+ end
32
+
33
+ def template_name
34
+ @path.chomp(extension).chomp(".jst")
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: UTF-8
2
+
3
+ module Bunch
4
+ module Compilers
5
+ class Null
6
+ def initialize(file, *)
7
+ @file = file
8
+ end
9
+
10
+ def path
11
+ @file.path
12
+ end
13
+
14
+ def content
15
+ @file.content
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,55 @@
1
+ # encoding: UTF-8
2
+
3
+ module Bunch
4
+ module Compilers
5
+ class Sass
6
+ def initialize(file, tree, path)
7
+ require "sass"
8
+ @file, @tree, @abs_path = file, tree, path
9
+ rescue LoadError => e
10
+ raise "'gem install sass' to compile .sass and .scss files."
11
+ end
12
+
13
+ def path
14
+ @file.path.sub(/\.s[ca]ss$/, "") + ".css"
15
+ end
16
+
17
+ def content
18
+ new_engine(@file.content, @abs_path).render
19
+ end
20
+
21
+ def new_engine(content, path)
22
+ syntax = path.end_with?("scss") ? :scss : :sass
23
+ ::Sass::Engine.new(
24
+ content, syntax: syntax, filename: path, importer: self)
25
+ end
26
+
27
+ ## Sass importer interface ##
28
+
29
+ def find_relative(path, base, options)
30
+ abs = ::File.expand_path("../#{path}", "/#{base}")[1..-1]
31
+ find(abs, options)
32
+ end
33
+
34
+ def find(path, options)
35
+ path = path.chomp(".scss").chomp(".sass")
36
+ path_with_underscore = path.sub(/(.*)\//, '\1/_')
37
+ file = @tree.get_fuzzy(path) || @tree.get_fuzzy(path_with_underscore)
38
+ if file
39
+ new_engine(file.content, file.path)
40
+ else
41
+ raise "Couldn't find '#{path}' to import! (#{options[:original_filename]}:#{options[:_line]})"
42
+ end
43
+ end
44
+
45
+ def mtime(path, options)
46
+ end
47
+
48
+ def key(path, options)
49
+ ["Bunch:#{::File.dirname(path)}", ::File.basename(path)]
50
+ end
51
+ end
52
+ end
53
+ Compiler.register ".sass", Compilers::Sass
54
+ Compiler.register ".scss", Compilers::Sass
55
+ end
@@ -0,0 +1,37 @@
1
+ # encoding: UTF-8
2
+
3
+ require "digest/md5"
4
+
5
+ module Bunch
6
+ class ContentHash
7
+ def initialize(file_or_tree)
8
+ @input = file_or_tree
9
+ end
10
+
11
+ def result
12
+ @path = []
13
+ @hash = 0
14
+ @input.accept(self)
15
+ @hash
16
+ end
17
+
18
+ def enter_tree(tree)
19
+ @path << tree.name if tree.name
20
+ end
21
+
22
+ def leave_tree(tree)
23
+ @path.pop if tree.name
24
+ end
25
+
26
+ def visit_file(file)
27
+ file_path = [*@path, file.path].join("/")
28
+ @hash ^= truncated_md5("#{file_path}:#{file.content}")
29
+ end
30
+
31
+ private
32
+
33
+ def truncated_md5(string)
34
+ Digest::MD5.digest(string)[0..4].unpack("L")[0]
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,121 @@
1
+ # encoding: UTF-8
2
+
3
+ # Includes a copy of CSSMin, written by Ryan Grove and based on work by
4
+ # Julien Lecomte and Isaac Schlueter.
5
+ #
6
+ # CSSMin License:
7
+ # Copyright (c) 2008 Ryan Grove <ryan@wonko.com>
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of this project nor the names of its contributors may be
19
+ # used to endorse or promote products derived from this software without
20
+ # specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
26
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
+
33
+ module Bunch
34
+ class CssMinifier
35
+ def initialize(tree)
36
+ @input = tree
37
+ @output = FileTree.new
38
+ end
39
+
40
+ def result
41
+ @path = []
42
+ @input.accept(self)
43
+ @output
44
+ end
45
+
46
+ def enter_tree(tree)
47
+ @path << tree.name if tree.name
48
+ end
49
+
50
+ def leave_tree(tree)
51
+ @path.pop if tree.name
52
+ end
53
+
54
+ def visit_file(file)
55
+ file_path = [*@path, file.path].join("/")
56
+ content = (file.extension == ".css") ? minify(file.content) : file.content
57
+ @output.write file_path, content
58
+ end
59
+
60
+ private
61
+
62
+ def minify(css)
63
+ # Remove comments.
64
+ css.gsub!(/\/\*[\s\S]*?\*\//, '')
65
+
66
+ # Compress all runs of whitespace to a single space to make things easier
67
+ # to work with.
68
+ css.gsub!(/\s+/, ' ')
69
+
70
+ # Replace box model hacks with placeholders.
71
+ css.gsub!(/"\\"\}\\""/, '___BMH___')
72
+
73
+ # Remove unnecessary spaces, but be careful not to turn "p :link {...}"
74
+ # into "p:link{...}".
75
+ css.gsub!(/(?:^|\})[^\{:]+\s+:+[^\{]*\{/) do |match|
76
+ match.gsub(':', '___PSEUDOCLASSCOLON___')
77
+ end
78
+ css.gsub!(/\s+([!\{\};:>+\(\)\],])/, '\1')
79
+ css.gsub!('___PSEUDOCLASSCOLON___', ':')
80
+ css.gsub!(/([!\{\}:;>+\(\[,])\s+/, '\1')
81
+
82
+ # Add missing semicolons.
83
+ css.gsub!(/([^;\}])\}/, '\1;}')
84
+
85
+ # Replace 0(%, em, ex, px, in, cm, mm, pt, pc) with just 0.
86
+ css.gsub!(/([\s:])([+-]?0)(?:%|em|ex|px|in|cm|mm|pt|pc)/i, '\1\2')
87
+
88
+ # Replace 0 0 0 0; with 0.
89
+ css.gsub!(/:(?:0 )+0;/, ':0;')
90
+
91
+ # Replace background-position:0; with background-position:0 0;
92
+ css.gsub!('background-position:0;', 'background-position:0 0;')
93
+
94
+ # Replace 0.6 with .6, but only when preceded by : or a space.
95
+ css.gsub!(/(:|\s)0+\.(\d+)/, '\1.\2')
96
+
97
+ # Convert rgb color values to hex values.
98
+ css.gsub!(/rgb\s*\(\s*([0-9,\s]+)\s*\)/) do |match|
99
+ '#' << $1.scan(/\d+/).map{|n| n.to_i.to_s(16).rjust(2, '0') }.join
100
+ end
101
+
102
+ # Compress color hex values, making sure not to touch values used in IE
103
+ # filters, since they would break.
104
+ css.gsub!(/([^"'=\s])(\s?)\s*#([0-9a-f])\3([0-9a-f])\4([0-9a-f])\5/i, '\1\2#\3\4\5')
105
+
106
+ # Remove empty rules.
107
+ css.gsub!(/[^\}]+\{;\}\n/, '')
108
+
109
+ # Re-insert box model hacks.
110
+ css.gsub!('___BMH___', '"\"}\""')
111
+
112
+ # Put the space back in for media queries
113
+ css.gsub!(/\band\(/, 'and (')
114
+
115
+ # Prevent redundant semicolons.
116
+ css.gsub!(/;+\}/, '}')
117
+
118
+ css.strip
119
+ end
120
+ end
121
+ end