extruder 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ Gemfile.lock
3
+ .bundle
4
+ .config
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
@@ -0,0 +1,71 @@
1
+ # A simplified rewrite of Yehuda Katz & Tom Dale's awesome rake-pipeline gem.
2
+
3
+ ...documentation / server / executable / assetfile support / tests etc coming.
4
+
5
+ ## A trip through the extruder looks like this:
6
+
7
+ 1. Define input directories to be processed.
8
+ 2. Define output directory for processed files.
9
+ 3. Define matches with file globs (ie: *.js) to determine which files from the input directories will be processed.
10
+ 4. Define filters within each match block that will process files.
11
+ 5. When processing an Extruder instance, the first step is to create a FileSet of eligible files from the input directories.
12
+ 6. As each match block is opened, the files that matched the glob are removed from the FileSet and passed in.
13
+ 7. As each filter is run, the files created by the previous filter are passed on to the next.
14
+ 4. As each match block is closed, the filter-created/modified files are added to the Extruder's FileSet.
15
+ 5. After the final match block has run, FileSet.create is called, writing the filtered files to disk.
16
+
17
+ Extruder layout:
18
+
19
+ ```ruby
20
+ inputs => {'app/js'=>'*/**','app/css'=>'*/**'},
21
+ output_root => 'build',
22
+ matches => [
23
+ { glob: "*.js", filters: [<ConcatFilter>] },
24
+ { glob: "*.txt", filters: [<ClosureFilter>,<CopyFilter>,<CopyFilter>] }
25
+ ]
26
+ ```
27
+
28
+ ## New concepts
29
+ FileSets contain an array of FileWrappers. A FileSet instance has a glob value (ie: *.js) which is transformed into a regular expression that can be used to determine which files match. An array of matching FileWrappers can be retrieved any time by calling the instance function 'eligible_files'
30
+
31
+ FileWrappers write to the instance variable @contents until the instance function create is called, at which point @contents is dumped to the output_root+path.
32
+
33
+ All filtering occurs in memory.
34
+
35
+ ## Junky proof of concept trial run:
36
+
37
+ 1. Build and install gem
38
+ 2. Grab the emberjs/todos repo
39
+ 3. Load irb in its root
40
+ 4. Run this
41
+
42
+ ```ruby
43
+ require 'extruder'
44
+ require 'json'
45
+ class ClosureFilter < Extruder::Filter
46
+ def generate_output(inputs, output)
47
+ inputs.each do |input|
48
+ output.write "(function() { #{input.read.to_json} })()"
49
+ end
50
+ end
51
+ end
52
+
53
+ extruder = Extruder.build do
54
+ input "app"
55
+ output "output"
56
+ match "*.js" do
57
+ concat "test.txt"
58
+ end
59
+ match "*.txt" do
60
+ filter ClosureFilter
61
+ copy "1.txt"
62
+ copy "2.txt"
63
+ end
64
+ end
65
+
66
+ # show resulting fileset
67
+ extruder.result
68
+
69
+ # save resulting fileset
70
+ extruder.result.create
71
+ ```
@@ -0,0 +1,16 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/extruder/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Yehuda Katz", "Tom Dale", "Tyler Kellen"]
6
+ gem.email = ["tyler@sleekcode.net"]
7
+ gem.description = "A simple and powerful file pipeline."
8
+ gem.summary = "A simple and powerful file pipeline."
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split("\n")
12
+ gem.name = "extruder"
13
+ gem.require_paths = ["lib"]
14
+ gem.version = Extruder::VERSION
15
+
16
+ end
@@ -0,0 +1,8 @@
1
+ require 'extruder/version'
2
+ require 'extruder/main'
3
+ require 'extruder/file_wrapper'
4
+ require 'extruder/file_set'
5
+ require 'extruder/filter'
6
+ require 'extruder/filters/concat_filter'
7
+ require 'extruder/filters/ordering_concat_filter'
8
+ require 'extruder/dsl'
@@ -0,0 +1,155 @@
1
+ module Extruder
2
+
3
+ ##
4
+ #
5
+ # This class exists purely to provide a convenient DSL for
6
+ # configuring an extruder.
7
+ #
8
+ # All instance methods of {DSL} are available in the context
9
+ # the block passed to {Extruder.build}
10
+ #
11
+ class DSL
12
+
13
+ # @return [Extruder::Main] the extruder the DSL should configure
14
+ attr_reader :extruder
15
+
16
+ ##
17
+ #
18
+ # Configure an extruder with a passed block.
19
+ #
20
+ # @param [extruder] extruder the extruder that the DSL
21
+ # should configure.
22
+ # @param [Proc] block the block describing the
23
+ # configuration. This block will be evaluated in
24
+ # the context of a new instance of {DSL}
25
+ # @return [void]
26
+ #
27
+ def self.evaluate(extruder, &block)
28
+ # create instance of self with the pipline passed
29
+ # and evalute the block within it
30
+ new(extruder).instance_eval(&block)
31
+ end
32
+
33
+ ##
34
+ #
35
+ # Create a new {DSL} to configure an extruder.
36
+ #
37
+ # @param [Extruder::Main] extruder the extruder that the DSL
38
+ # should configure.
39
+ # @return [void]
40
+ #
41
+ def initialize(extruder)
42
+ @extruder = extruder
43
+ end
44
+
45
+ ##
46
+ #
47
+ # Define the input location and files for the extruder.
48
+ #
49
+ # @example
50
+ # !!!ruby
51
+ # Extruder.build do
52
+ # input "app/assets", "**/*.js"
53
+ # # ...
54
+ # end
55
+ #
56
+ # @param [String] root the root path where the extruder
57
+ # should find its input files.
58
+ # @param [String] glob a file pattern that represents
59
+ # the list of all files that the extruder should
60
+ # process within +root+. The default is +"**/*"+.
61
+ # @return [void]
62
+ #
63
+ def input(root, pattern='**/*')
64
+ extruder.add_input root, pattern
65
+ end
66
+
67
+ ##
68
+ #
69
+ # Add a match to the pipeline with a passed block
70
+ # containing filters that belong to it.
71
+ #
72
+ # @example
73
+ # !!!ruby
74
+ # Extruder.build do
75
+ # input "app/assets"
76
+ #
77
+ # match "*.js" do
78
+ # concat "app.js"
79
+ # end
80
+ #
81
+ # end
82
+ #
83
+ # @param [String] root the output directory.
84
+ # @return [void]
85
+ #
86
+ def match(pattern, &block)
87
+ extruder.add_match pattern
88
+ block.call
89
+ end
90
+
91
+ ##
92
+ #
93
+ # Add a filter to the pipeline, normally called from
94
+ # within a match block. If a filter is added without
95
+ # a match block, the Extruder will automatically create
96
+ # one that matches all input files.
97
+ #
98
+ # In addition to a filter class, {#filter} takes a
99
+ # block that describes how the filter should map
100
+ # input files to output files.
101
+ #
102
+ # By default, the block maps an input file into
103
+ # an output file with the same name.
104
+ #
105
+ # Any additional arguments passed to {#filter} will
106
+ # be passed on to the filter class's constructor.
107
+ #
108
+ # @see Filter#outputs Filter#output (for an example
109
+ # of how a list of input files gets mapped to
110
+ # its outputs)
111
+ #
112
+ # @param [Class] filter_class the class of the filter.
113
+ # @param [Array] ctor_args a list of arguments to pass
114
+ # to the filter's constructor.
115
+ # @param [Proc] block an output file name generator.
116
+ # @return [void]
117
+ #
118
+ def filter(filter_class, *ctor_args, &block)
119
+ filter = filter_class.new(*ctor_args, &block)
120
+ extruder.add_filter(filter)
121
+ end
122
+
123
+ ##
124
+ #
125
+ # Specify the output directory for the pipeline.
126
+ #
127
+ # @param [String] root the output directory.
128
+ # @return [void]
129
+ #
130
+ def output(root)
131
+ extruder.output_root = root
132
+ end
133
+
134
+ ##
135
+ #
136
+ # A helper method for adding a concat filter to
137
+ # the Extruder.
138
+ # If the first argument is an Array, it adds a new
139
+ # {OrderingConcatFilter}, otherwise it adds a new
140
+ # {ConcatFilter}.
141
+ #
142
+ # @see OrderingConcatFilter#initialize
143
+ # @see ConcatFilter#initialize
144
+ #
145
+ def concat(*args, &block)
146
+ if args.first.kind_of?(Array)
147
+ filter(OrderingConcatFilter, *args, &block)
148
+ else
149
+ filter(ConcatFilter, *args, &block)
150
+ end
151
+ end
152
+ alias_method :copy, :concat
153
+
154
+ end
155
+ end
@@ -0,0 +1,13 @@
1
+ module Extruder
2
+ # The general Extruder error class
3
+ class Error < ::StandardError
4
+ end
5
+ # The error that Extruder uses when it detects
6
+ # that a file uses an improper encoding.
7
+ class EncodingError < Error
8
+ end
9
+ # The error that Extruder uses if you try to
10
+ # write to a FileWrapper before creating it.
11
+ class UnopenedFile < Error
12
+ end
13
+ end
@@ -0,0 +1,141 @@
1
+ require "strscan"
2
+
3
+ module Extruder
4
+
5
+ ##
6
+ #
7
+ # This class contains an array of {FileWrapper}s which can be
8
+ # searched with a glob.
9
+ #
10
+ class FileSet
11
+
12
+ # @return [Array[FileWrapper]] an array of FileWrapped files
13
+ attr_accessor :files
14
+
15
+ # @return [String] a glob that a set of input files are matched against
16
+ attr_accessor :glob
17
+
18
+ # @return [String] regular expression pattern to match against input files
19
+ attr_reader :pattern
20
+
21
+ def initialize(files, glob='**/*')
22
+ @files = files
23
+ self.glob = glob
24
+ end
25
+
26
+ def glob=(glob)
27
+ @glob = glob
28
+ @pattern = build_pattern(glob)
29
+ end
30
+
31
+ # Build a regular expression pattern from a file glob that
32
+ # can be used to narrow a selection of files from a Extruder.
33
+ def build_pattern(glob)
34
+
35
+ scanner = StringScanner.new(glob)
36
+
37
+ output, pos = "", 0
38
+
39
+ # keep scanning until end of String
40
+ until scanner.eos?
41
+
42
+ # look for **/, *, {...}, or the end of the string
43
+ new_chars = scanner.scan_until %r{
44
+ \*\*/
45
+ | /\*\*/
46
+ | \*
47
+ | \{[^\}]*\}
48
+ | $
49
+ }x
50
+
51
+ # get the new part of the string up to the match
52
+ before = new_chars[0, new_chars.size - scanner.matched_size]
53
+
54
+ # get the match and new position
55
+ match = scanner.matched
56
+ pos = scanner.pos
57
+
58
+ # add any literal characters to the output
59
+ output << Regexp.escape(before) if before
60
+
61
+ output << case match
62
+ when "/**/"
63
+ # /**/ matches either a "/" followed by any number
64
+ # of characters or a single "/"
65
+ "(/.*|/)"
66
+ when "**/"
67
+ # **/ matches the beginning of the path or
68
+ # any number of characters followed by a "/"
69
+ "(^|.*/)"
70
+ when "*"
71
+ # * matches any number of non-"/" characters
72
+ "[^/]*"
73
+ when /\{.*\}/
74
+ # {...} is split over "," and glued back together
75
+ # as an or condition
76
+ "(" + match[1...-1].gsub(",", "|") + ")"
77
+ else String
78
+ # otherwise, we've grabbed until the end
79
+ match
80
+ end
81
+ end
82
+
83
+ if glob.include?("/")
84
+ # if the pattern includes a /, it must match the
85
+ # entire input, not just the end.
86
+ @pattern = Regexp.new("^#{output}$", "i")
87
+ else
88
+ # anchor the pattern either at the beginning of the
89
+ # path or at any "/" character
90
+ @pattern = Regexp.new("(^|/)#{output}$", "i")
91
+ end
92
+ end
93
+
94
+ ##
95
+ #
96
+ # A list of eligible files based on the current glob
97
+ #
98
+ # @return [Array<FileWrapper>]
99
+ #
100
+ def eligible_files
101
+ files.select do |file|
102
+ file.path =~ @pattern
103
+ end
104
+ end
105
+
106
+ ##
107
+ #
108
+ # Remove eligible files from this {FileSet} and return them
109
+ #
110
+ # @return [Array<FileWrapper>]
111
+ #
112
+ def extract_eligible
113
+ keep = eligible_files
114
+ @files -= keep
115
+ keep
116
+ end
117
+
118
+ ##
119
+ #
120
+ # Select files in this {FileSet} which will be saved to disk
121
+ # if {#create} is run. The save flag is set true for any file
122
+ # that passed through a filter.
123
+ #
124
+ # @return [Array<FileWrapper>]
125
+ #
126
+ def will_save
127
+ files.select { |file| file.save }
128
+ end
129
+
130
+ ##
131
+ #
132
+ # Write all files in this {FileSet} to their output_root.
133
+ #
134
+ def create
135
+ will_save.each do |file|
136
+ file.create
137
+ end
138
+ end
139
+
140
+ end
141
+ end
@@ -0,0 +1,191 @@
1
+ require 'fileutils'
2
+
3
+ module Extruder
4
+
5
+ ##
6
+ #
7
+ # This class wraps a file for consumption inside of filters. It is
8
+ # initialized with a root and path, and filters usually use the
9
+ # {#read} and {#write} methods to work with these files.
10
+ #
11
+ # The {#root} and +path+ parameters are provided by the {Extruder}
12
+ # class' internal implementation. Individual filters do not need
13
+ # to worry about them.
14
+ #
15
+ # The root of a {FileWrapper} is always an absolute path.
16
+ #
17
+ class FileWrapper
18
+ # @return [String] an absolute path representing this {FileWrapper}'s
19
+ # root directory.
20
+ attr_accessor :root
21
+
22
+ # @return [String] the path to the file represented by the {FileWrapper}
23
+ # relative to its {#root}.
24
+ attr_accessor :path
25
+
26
+ # @return [String] contents of the file represented by the {FileWrapper}
27
+ attr_accessor :contents
28
+
29
+ # @return [String] the encoding that the file represented by this
30
+ # {FileWrapper} is encoded in. Filters set the {#encoding} to
31
+ # +BINARY+ if they are declared as processing binary data.
32
+ attr_accessor :encoding
33
+
34
+ # @return [Boolean] true if created by a filter, otherwise false
35
+ # this flag determines if a file will be written when its container
36
+ # {FileSet} is saved.
37
+ attr_accessor :save
38
+
39
+ # @return [String] A pretty representation of the {FileWrapper}.
40
+ def inspect
41
+ "#<FileWrapper root=#{root.inspect} path=#{path.inspect} encoding=#{encoding.inspect}>"
42
+ end
43
+ alias to_s inspect
44
+
45
+ ##
46
+ #
47
+ # Create a new {FileWrapper}, passing in optional root, path, and
48
+ # encoding. Any of the parameters can be ommitted and supplied later.
49
+ #
50
+ # @return [void]
51
+ #
52
+ def initialize(root=nil, path=nil, encoding="UTF-8")
53
+ @root, @path, @encoding = root, path, encoding
54
+ @created_file = nil
55
+ @save = false
56
+ end
57
+
58
+ ##
59
+ #
60
+ # Create a new {FileWrapper} with the same root and path as
61
+ # this {FileWrapper}, but with a specified encoding.
62
+ #
63
+ # @param [String] encoding the encoding for the new object
64
+ # @return [FileWrapper]
65
+ #
66
+ def with_encoding(encoding)
67
+ self.class.new(@root, @path, encoding)
68
+ end
69
+
70
+ ##
71
+ #
72
+ # A {FileWrapper} is equal to another {FileWrapper} for hashing purposes
73
+ # if they have the same {#root} and {#path}
74
+ #
75
+ # @param [FileWrapper] other another {FileWrapper} to compare.
76
+ # @return [true,false]
77
+ #
78
+ def eql?(other)
79
+ return false unless other.is_a?(self.class)
80
+ root == other.root && path == other.path
81
+ end
82
+ alias == eql?
83
+
84
+ ##
85
+ #
86
+ # Similar to {#eql?}, generate a {FileWrapper}'s {#hash} from its {#root}
87
+ # and {#path}
88
+ #
89
+ # @see #eql?
90
+ # @return [Fixnum] a hash code
91
+ #
92
+ def hash
93
+ [root, path].hash
94
+ end
95
+
96
+ ##
97
+ #
98
+ # The full path of a FileWrapper is its root joined with its path
99
+ #
100
+ # @return [String] the {FileWrapper}'s full path
101
+ #
102
+ def fullpath
103
+ raise "#{root}, #{path}" unless root =~ /^\//
104
+ File.join(root, path)
105
+ end
106
+
107
+ ##
108
+ #
109
+ # Make FileWrappers sortable
110
+ #
111
+ # @param [FileWrapper] other {FileWrapper FileWrapper}
112
+ # @return [Fixnum] -1, 0, or 1
113
+ #
114
+ def <=>(other)
115
+ [root, path, encoding] <=> [other.root, other.path, other.encoding]
116
+ end
117
+
118
+ ##
119
+ #
120
+ # Does the file represented by the {FileWrapper} exist in the file system?
121
+ #
122
+ # @return [true,false]
123
+ #
124
+ def exists?
125
+ File.exists?(fullpath)
126
+ end
127
+
128
+ ##
129
+ #
130
+ # Read the contents of the file represented by the {FileWrapper}.
131
+ #
132
+ # Read the file using the {FileWrapper}'s encoding, which will result in
133
+ # this method returning a +String+ tagged with the {FileWrapper}'s encoding.
134
+ #
135
+ # @return [String] the contents of the file
136
+ # @raise [EncodingError] when the contents of the file are not valid in the
137
+ # expected encoding specified in {#encoding}.
138
+ #
139
+ def read
140
+ if @contents.nil?
141
+
142
+ @contents = if "".respond_to?(:encode)
143
+ File.read(fullpath, :encoding => encoding)
144
+ else
145
+ File.read(fullpath)
146
+ end
147
+
148
+ if "".respond_to?(:encode) && !@contents.valid_encoding?
149
+ raise EncodingError, "The file at the path #{fullpath} is not valid UTF-8. Please save it again as UTF-8."
150
+ end
151
+ end
152
+
153
+ @contents
154
+ end
155
+
156
+ ##
157
+ #
158
+ # Append a string to the cached contents of the file this wrapper
159
+ # represents. This method is called repeatedly by a {Filter}'s
160
+ # +#generate_output+ method.
161
+ #
162
+ def write(string)
163
+ @contents = "" if contents.nil?
164
+ @contents << string
165
+ end
166
+
167
+ ##
168
+ #
169
+ # Create a new file at the {FileWrapper}'s {#fullpath} and fill it
170
+ # with the value of @contents. If the file already exists, it will
171
+ # be overwritten.
172
+ #
173
+ # @return [File]
174
+ #
175
+ def create
176
+ FileUtils.mkdir_p(File.dirname(fullpath))
177
+
178
+ @created_file = if "".respond_to?(:encode)
179
+ File.open(fullpath, "w:#{encoding}")
180
+ else
181
+ File.open(fullpath, "w")
182
+ end
183
+
184
+ @created_file.write(@contents)
185
+ @created_file.close
186
+ @created_file
187
+ end
188
+
189
+ end
190
+
191
+ end
@@ -0,0 +1,172 @@
1
+ module Extruder
2
+
3
+ ##
4
+ #
5
+ # This abstract class is used for building Filters that will be
6
+ # assigned to an Extruder. Their function is to transform one
7
+ # array of FileWrappers to another.
8
+ #
9
+ # @abstract
10
+ #
11
+ class Filter
12
+
13
+ # @return [Array[FileWrapper]] the files that this filter
14
+ # will operate on.
15
+ attr_accessor :files
16
+
17
+ # @return [String] the expanded root path that this
18
+ # filter's created files will eventually be written to.
19
+ attr_accessor :output_root
20
+
21
+ # @return [Proc] a block that returns the relative output
22
+ # filename for a particular input file. If the block accepts
23
+ # just one argument, it will be passed the input's path. If
24
+ # it accepts two, it will also be passed the file contents.
25
+ attr_accessor :output_name_generator
26
+
27
+ ##
28
+ #
29
+ # Invoke this method in a subclass of Filter to declare that
30
+ # it expects to work with BINARY data, and that data that is
31
+ # not valid UTF-8 should be allowed.
32
+ #
33
+ # @return [void]
34
+ #
35
+ def self.processes_binary_files
36
+ define_method(:encoding) { "BINARY" }
37
+ end
38
+
39
+ ##
40
+ #
41
+ # ...more soon
42
+ #
43
+ # @param [Proc] block a block to use as the Filter's
44
+ # {#output_name_generator}.
45
+ #
46
+ def initialize(&block)
47
+ if self.respond_to?(:external_dependencies)
48
+ require_dependencies!
49
+ end
50
+ block ||= proc { |input| input }
51
+ @output_name_generator = block
52
+ @files = []
53
+ end
54
+
55
+ ##
56
+ #
57
+ # A hash of output files pointing at their associated input
58
+ # files. The output names are created by applying the
59
+ # {#output_name_generator} to each input file.
60
+ #
61
+ # For exmaple, if you had the following input files:
62
+ #
63
+ # javascripts/jquery.js
64
+ # javascripts/sproutcore.js
65
+ # stylesheets/sproutcore.css
66
+ #
67
+ # And you had the following {#output_name_generator}:
68
+ #
69
+ # !!!ruby
70
+ # filter.output_name_generator = proc do |filename|
71
+ # # javascripts/jquery.js becomes:
72
+ # # ["javascripts", "jquery", "js"]
73
+ # directory, file, ext = file.split(/[\.\/]/)
74
+ #
75
+ # "#{directory}.#{ext}"
76
+ # end
77
+ #
78
+ # You would end up with the following hash:
79
+ #
80
+ # !!!ruby
81
+ # {
82
+ # #<FileWrapper path="javascripts.js"> => [
83
+ # #<FileWrapper path="javascripts/jquery.js">,
84
+ # #<FileWrapper path="javascripts/sproutcore.js">
85
+ # ],
86
+ # #<FileWrapper path="stylesheets.css"> => [
87
+ # #<FileWrapper path="stylesheets/sproutcore.css">
88
+ # ]
89
+ # }
90
+ #
91
+ # @return [Hash{FileWrapper => Array<FileWrapper>}]
92
+ #
93
+ def outputs
94
+ hash = {}
95
+
96
+ @files.each do |file|
97
+ outputs = output_paths(file)
98
+
99
+ output_wrappers(file).each do |output|
100
+ hash[output] ||= []
101
+ hash[output] << file
102
+ end
103
+ end
104
+
105
+ hash
106
+ end
107
+
108
+ ##
109
+ #
110
+ # For each {FileWrapper} file this filter will create, call
111
+ # generate_output (defined in subclasses of this) to fill the
112
+ # @contents.
113
+ #
114
+ # These files should be flagged as saveable so the {FileSet} that
115
+ # they wind up in will know they should ultimately be written to
116
+ # the output directory.
117
+ #
118
+ # @return [Array{FileWrapper}] An array of FileWrapped ouputs for
119
+ # this filter.
120
+ #
121
+ def process
122
+ outputs.map do |output, inputs|
123
+ generate_output(inputs, output)
124
+ output.save = true
125
+ output
126
+ end
127
+ end
128
+
129
+ private
130
+ def encoding
131
+ "UTF-8"
132
+ end
133
+
134
+ ##
135
+ #
136
+ # ...more soon
137
+ #
138
+ # @return [FileWrapper]
139
+ #
140
+ def output_wrappers(input)
141
+ output_paths(input).map do |path|
142
+ FileWrapper.new(output_root, path, encoding)
143
+ end
144
+ end
145
+
146
+ ##
147
+ #
148
+ # ...more soon
149
+ #
150
+ # @return [Array]
151
+ #
152
+ def output_paths(input)
153
+ args = [ input.path ]
154
+ args << input if output_name_generator.arity == 2
155
+ Array(output_name_generator.call(*args))
156
+ end
157
+
158
+ ##
159
+ #
160
+ # ... more soon
161
+ #
162
+ def require_dependencies!
163
+ external_dependencies.each do |d|
164
+ begin
165
+ require d
166
+ rescue LoadError => error
167
+ raise error, "#{self.class} requires #{d}, but it is not available."
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,139 @@
1
+ module Extruder
2
+
3
+ ##
4
+ #
5
+ # A built-in filter that simply accepts a series
6
+ # of inputs and concatenates them into output files
7
+ # with the output file generator.
8
+ #
9
+ # @example
10
+ # !!!ruby
11
+ # Extruder.build do
12
+ # input "app/assets", "**/*.js"
13
+ # output "public"
14
+ #
15
+ # # create a concatenated output file for each
16
+ # # directory of inputs.
17
+ # filter(Extruder::ConcatFilter) do |input|
18
+ # # input files will look something like:
19
+ # # javascripts/admin/main.js
20
+ # # javascripts/admin/app.js
21
+ # # javascripts/users/main.js
22
+ # #
23
+ # # and the outputs will look like:
24
+ # # javascripts/admin.js
25
+ # # javascripts/users.js
26
+ # directory = File.dirname(input)
27
+ # ext = File.extname(input)
28
+ #
29
+ # "#{directory}#{ext}"
30
+ # end
31
+ # end
32
+ #
33
+ class ConcatFilter < Extruder::Filter
34
+ ##
35
+ #
36
+ # ... more soon
37
+ #
38
+ # @param [String] string the name of the output file to
39
+ # concatenate inputs to.
40
+ # @param [Proc] block a block to use as the Filter's
41
+ # {#output_name_generator}.
42
+ def initialize(string=nil, &block)
43
+ block = proc { string } if string
44
+ super(&block)
45
+ end
46
+
47
+ # @method encoding
48
+ # @return [String] the String +"BINARY"+
49
+ processes_binary_files
50
+
51
+ ###
52
+ #
53
+ # implement the {#generate_output} method required by
54
+ # the {Filter} API. In this case, simply loop through
55
+ # the inputs and write their contents to the output.
56
+ #
57
+ # Recall that this method will be called once for each
58
+ # unique output file.
59
+ #
60
+ # @param [Array<FileWrapper>] inputs an Array of
61
+ # {FileWrapper} objects representing the inputs to
62
+ # this filter.
63
+ # @param [FileWrapper] a single {FileWrapper} object
64
+ # representing the output.
65
+ #
66
+ def generate_output(inputs, output)
67
+ inputs.each do |input|
68
+ output.write input.read
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+
75
+ module Extruder
76
+ # A filter that compiles input files written in SCSS
77
+ # to CSS using the Sass compiler and the Compass CSS
78
+ # framework.
79
+ #
80
+ # Requires {http://rubygems.org/gems/sass sass} and
81
+ # {http://rubygems.org/gems/compass compass}
82
+ #
83
+ # @example
84
+ # !!!ruby
85
+ # Rake::Pipeline.build do
86
+ # input "app/assets", "**/*.scss"
87
+ # output "public"
88
+ #
89
+ # # Compile each SCSS file under the app/assets
90
+ # # directory.
91
+ # filter Rake::Pipeline::Web::Filters::SassFilter
92
+ # end
93
+ class SassFilter < Extruder::Filter
94
+
95
+ # @return [Hash] a hash of options to pass to Sass
96
+ # when compiling.
97
+ attr_reader :options
98
+
99
+ # @param [Hash] options options to pass to the Sass
100
+ # compiler
101
+ # @option options [Array] :additional_load_paths a
102
+ # list of paths to append to Sass's :load_path.
103
+ # @param [Proc] block a block to use as the Filter's
104
+ # {#output_name_generator}.
105
+ def initialize(options={}, &block)
106
+ block ||= proc { |input| input.sub(/\.(scss|sass)$/, '.css') }
107
+ super(&block)
108
+ Compass.add_project_configuration
109
+ @options = Compass.configuration.to_sass_engine_options
110
+ @options[:load_paths].concat(Array(options.delete(:additional_load_paths)))
111
+ @options.merge!(options)
112
+ end
113
+
114
+ # Implement the {#generate_output} method required by
115
+ # the {Filter} API. Compiles each input file with Sass.
116
+ #
117
+ # @param [Array<FileWrapper>] inputs an Array of
118
+ # {FileWrapper} objects representing the inputs to
119
+ # this filter.
120
+ # @param [FileWrapper] output a single {FileWrapper}
121
+ # object representing the output.
122
+ def generate_output(inputs, output)
123
+ inputs.each do |input|
124
+ sass_opts = if input.path.match(/\.sass$/)
125
+ options.merge(:syntax => :sass)
126
+ else
127
+ options
128
+ end
129
+ output.write Sass.compile(input.read, sass_opts)
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ def external_dependencies
136
+ [ 'sass', 'compass' ]
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,50 @@
1
+ module Extruder
2
+
3
+ ##
4
+ #
5
+ # A filter that concats files in a specified order.
6
+ #
7
+ # @example
8
+ # !!!ruby
9
+ # Extruder.build do
10
+ # input "app/assets"
11
+ # output "public"
12
+ #
13
+ # # Concat each file into libs.js but make sure
14
+ # # that jQuery and Ember come first.
15
+ # match "**/*.js"
16
+ # filter Rake::Extruder::OrderingConcatFilter, ["jquery.js", "ember.js"], "libs.js"
17
+ # end
18
+ # end
19
+ #
20
+ class OrderingConcatFilter < ConcatFilter
21
+
22
+ ##
23
+ #
24
+ # @param [Array<String>] ordering an Array of Strings
25
+ # of file names that should come in the specified order
26
+ # @param [String] string the name of the output file to
27
+ # concatenate inputs to.
28
+ # @param [Proc] block a block to use as the Filter's
29
+ # {#output_name_generator}.
30
+ #
31
+ def initialize(ordering, string=nil, &block)
32
+ @ordering = ordering
33
+ super(string, &block)
34
+ end
35
+
36
+ ##
37
+ #
38
+ # Extend the {#generate_output} method supplied by {ConcatFilter}.
39
+ # Re-orders the inputs such that the specified files come first.
40
+ # If a file is not in the list it will come after the specified files.
41
+ #
42
+ def generate_output(inputs, output)
43
+ @ordering.reverse.each do |name|
44
+ file = inputs.find{|i| i.path == name }
45
+ inputs.unshift(inputs.delete(file)) if file
46
+ end
47
+ super
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,166 @@
1
+ module Extruder
2
+
3
+ ##
4
+ #
5
+ # Build a new Extruder taking a block. The block will be evaluated
6
+ # by Extruder::DSL.
7
+ #
8
+ # @example
9
+ # Extruder.build do
10
+ # input "app"
11
+ # output "public"
12
+ #
13
+ # match "*.js"
14
+ # concat "app.js"
15
+ # end
16
+ # end
17
+ #
18
+ # All instance methods of Extruder::DSL are available inside
19
+ # the build block.
20
+ #
21
+ # @return [Extruder] the newly configured Extruder
22
+ #
23
+ def self.build(&block)
24
+ extruder = Main.new
25
+ DSL.evaluate(extruder, &block) if block
26
+ extruder
27
+ end
28
+
29
+ ##
30
+ #
31
+ # The main Extruder class, this is where everything gets tied together.
32
+ #
33
+ class Main
34
+
35
+ # @return [Hash[String, String]] the directory paths for the input files
36
+ # and their matching globs.
37
+ attr_accessor :inputs
38
+
39
+ # @return [[{:glob=>"",:filters=>[Filter,Filter]}]] this Extruder's matches
40
+ # and the filters they contain.
41
+ attr_accessor :matches
42
+
43
+ # @return [String] this Extruder's output directory
44
+ attr_reader :output_root
45
+
46
+ ##
47
+ #
48
+ # Initalize matches and inputs on instantiation.
49
+ #
50
+ def initialize
51
+ @matches = []
52
+ @inputs = {}
53
+ end
54
+
55
+ ##
56
+ #
57
+ # Set the output root of this Extruder and expand its path.
58
+ #
59
+ # @param [String] root this Extruder's output root
60
+ #
61
+ def output_root=(root)
62
+ @output_root = File.expand_path(root)
63
+ end
64
+
65
+ ##
66
+ #
67
+ # Add an input directory, optionally filtering which files within
68
+ # the input directory are included.
69
+ #
70
+ # @param [String] root the input root directory; required
71
+ # @param [String] pattern a pattern to match within +root+;
72
+ # optional; defaults to "**/*"
73
+ #
74
+ def add_input(root, pattern='**/*')
75
+ @inputs[root] = pattern
76
+ end
77
+
78
+ ##
79
+ #
80
+ # Add a match and initialize it's filters to an array.
81
+ #
82
+ # @param [String] glob the file glob to match with
83
+ #
84
+ def add_match(glob)
85
+ @matches.push({:glob=>glob,:filters=>[]})
86
+ end
87
+
88
+ ##
89
+ #
90
+ # Add a filter to the current match.
91
+ #
92
+ # @param [Extruder::Filter] filter a filter to run
93
+ #
94
+ def add_filter(filter)
95
+ # add a match that accepts everything if none are specified
96
+ add_match('**/*') if @matches.empty?
97
+ @matches.last[:filters].push(filter)
98
+ end
99
+
100
+ ##
101
+ #
102
+ # Return a sorted array of FileWrappers representing all files
103
+ # in the input paths.
104
+ #
105
+ # @return [Array[FileWrapper]]
106
+ #
107
+ def input_files
108
+
109
+ if inputs.empty?
110
+ raise Extruder::Error, "You cannot get input files without " \
111
+ "first providing an input root"
112
+ end
113
+
114
+ result = []
115
+
116
+ @inputs.each do |root, glob|
117
+ expanded_root = File.expand_path(root)
118
+ files = Dir[File.join(expanded_root, glob)].select { |f| File.file?(f) }
119
+
120
+ files.each do |file|
121
+ relative_path = file.sub(%r{^#{Regexp.escape(expanded_root)}/}, '')
122
+ result << FileWrapper.new(expanded_root, relative_path)
123
+ end
124
+ end
125
+
126
+ result.sort
127
+ end
128
+
129
+ ##
130
+ #
131
+ # Create a {FileSet} from {#input_files}, iterate through matches,
132
+ # running the filters on the files they contain.
133
+ #
134
+ # @return [FileSet] a {FileSet} containing the filtered files.
135
+ #
136
+ def result
137
+ # create a fileset to process filters with
138
+ fileset = FileSet.new(input_files)
139
+
140
+ matches.each do |match|
141
+
142
+ # set match on fileset for filter
143
+ fileset.glob = match[:glob]
144
+
145
+ # get files for filter (and remove them from fileset)
146
+ files = fileset.extract_eligible
147
+
148
+ # add the results of this match to the fileset
149
+ fileset.files += match[:filters].map do |filter|
150
+
151
+ # hand filter its files
152
+ filter.files = files
153
+
154
+ # tell filter where to save them
155
+ filter.output_root = output_root
156
+
157
+ # process files and return them for another loop
158
+ files = filter.process
159
+ end.flatten
160
+ end
161
+
162
+ fileset
163
+ end
164
+
165
+ end
166
+ end
@@ -0,0 +1,3 @@
1
+ module Extruder
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: extruder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Yehuda Katz
9
+ - Tom Dale
10
+ - Tyler Kellen
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2012-02-07 00:00:00.000000000 Z
15
+ dependencies: []
16
+ description: A simple and powerful file pipeline.
17
+ email:
18
+ - tyler@sleekcode.net
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - .gitignore
24
+ - Gemfile
25
+ - README.md
26
+ - extruder.gemspec
27
+ - lib/extruder.rb
28
+ - lib/extruder/dsl.rb
29
+ - lib/extruder/errors.rb
30
+ - lib/extruder/file_set.rb
31
+ - lib/extruder/file_wrapper.rb
32
+ - lib/extruder/filter.rb
33
+ - lib/extruder/filters/concat_filter.rb
34
+ - lib/extruder/filters/ordering_concat_filter.rb
35
+ - lib/extruder/main.rb
36
+ - lib/extruder/version.rb
37
+ homepage: ''
38
+ licenses: []
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubyforge_project:
57
+ rubygems_version: 1.8.10
58
+ signing_key:
59
+ specification_version: 3
60
+ summary: A simple and powerful file pipeline.
61
+ test_files: []
62
+ has_rdoc: