extruder 0.0.1

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.
@@ -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: