diff --git a/lib/rake-pipeline-web-filters.rb b/lib/rake-pipeline-web-filters.rb index a7d64c1..f6b15e8 100644 --- a/lib/rake-pipeline-web-filters.rb +++ b/lib/rake-pipeline-web-filters.rb @@ -23,4 +23,5 @@ module Filters require "rake-pipeline-web-filters/chained_filter" require "rake-pipeline-web-filters/less_filter" require "rake-pipeline-web-filters/handlebars_filter" +require "rake-pipeline-web-filters/tilt_yielding_filter" require "rake-pipeline-web-filters/helpers" diff --git a/lib/rake-pipeline-web-filters/helpers.rb b/lib/rake-pipeline-web-filters/helpers.rb index 0c2969c..c1a2820 100644 --- a/lib/rake-pipeline-web-filters/helpers.rb +++ b/lib/rake-pipeline-web-filters/helpers.rb @@ -80,6 +80,12 @@ def less(*args, &block) def handlebars(*args, &block) filter(Rake::Pipeline::Web::Filters::HandlebarsFilter, *args, &block) end + + # Add a new {TiltYieldingFilter} to the pipeline. + # @see TiltYieldingFilter#initialize + def tilt_yielding(*args, &block) + filter(Rake::Pipeline::Web::Filters::TiltYieldingFilter, *args, &block) + end end end diff --git a/lib/rake-pipeline-web-filters/tilt_yielding_filter.rb b/lib/rake-pipeline-web-filters/tilt_yielding_filter.rb new file mode 100644 index 0000000..8598899 --- /dev/null +++ b/lib/rake-pipeline-web-filters/tilt_yielding_filter.rb @@ -0,0 +1,98 @@ +module Rake::Pipeline::Web::Filters + # A filter that generates Tilt templates where the templates may yield + # + # @example + # !!!ruby + # Rake::Pipeline.build do + # output "public" + # + # # Below, the layout yields to the index; i.e., there will be a + # # call to yield inside layout.slim. The resulting file generated + # # will be index.html + # input "assets" do + # match "templates/{layout,index}.slim" do + # tilt_yielding :yields_to => ["layout","index"], :output_name => "index.html" + # end + # end + # + # # If you need to match the same layout again, it needs to be in + # # its own input block + # input "assets" do + # match "templates/{layout,about}.slim" do + # tilt_yielding :yields_to => ["layout","about"], :output_name => "about.html" + # end + # end + # end + class TiltYieldingFilter < Rake::Pipeline::Filter + + include Rake::Pipeline::Web::Filters::FilterWithDependencies + + # @param [Hash] options is used by the filter and the generator + # @option options [String] :output_name is the file name that will be + # used for the resulting file from generation + # @option options [Array] :yields_to is the array in which the + # templates will be ordered upon generation, for instance, given + # [a,b,c], a yields_to b and b yields_to c, which means that the + # result of c is nested in b, and that result is nested in a. + # @option options [Object] :scope is the scope that will be passed + # on each render of the templates + # @option options [Hash] :locals is the Hash of local variables that + # will be passed on each render of the templates + # @param [Proc] block a block to use as the Filter's + # {#output_name_generator}, which by default will group all the + # inputs to the @output_name. Note that if this is proc is + # overridden then it is important that inputs are grouped to the + # result of the proc + def initialize options = {}, &block + @output_name = options.delete(:output_name) || DEFAULT_OUTPUT_NAME + @yields_to = options.delete(:yields_to) || [] + @scope = options.delete(:scope) || Object.new + @locals = options.delete(:locals) || {} + @options = options + super(&block || ->_ {@output_name}) + end + + def generate_output inputs, output + output.write invoke_tilt(order_inputs inputs) + end + + private + DEFAULT_OUTPUT_NAME = "a.out" + def external_dependencies; ["tilt"]; end + + # Determines if the value matches the given input, where the value + # will be an item in the @yields_to array, and the input will be an + # item in the array of inputs. The method returns true when the file + # name of the input without an extension equals the value. + def matches_input? value, input + File.basename(input.path,File.extname(input.path)) == value + end + + # Orders the inputs according to the @yields_to array. This method + # takes a list of inputs and returns the same list of inputs, + # potentially in a different order. The reverse occurs at the end + # because Tile must process the inner-most template first and pass + # it up the yields_to chain. So the user specifies the @yields_to in + # terms of a yields to b, but we process b and pass it in when a is + # processed. + def order_inputs inputs + case @yields_to + when [] then inputs + else + @yields_to. + map {|a| inputs.index {|b| matches_input? a,b}}. + map {|a| inputs[a]} + end.reverse + end + + # Generates the result of invoking tilt on each of the inputs where + # each sequential input is nesteded within the next input. So we + # expect that the ordering of the inputs is bottom to top. + def invoke_tilt inputs + inputs.reduce("") do |b,a| + # Filename, line number, options + Tilt[a.path].new(nil,1,@options) {|_| a.read}.render(@scope,@locals) {b} + end + end + end +end diff --git a/spec/helpers_spec.rb b/spec/helpers_spec.rb index e7f9161..d6f7dbb 100644 --- a/spec/helpers_spec.rb +++ b/spec/helpers_spec.rb @@ -82,11 +82,17 @@ def filter end end - describe "#handlebars" do it "creates a HandlebarsFilter" do dsl.handlebars filter.should be_kind_of(Rake::Pipeline::Web::Filters::HandlebarsFilter) end end + + describe "#tilt_yielding" do + it "creates a TiltYieldingFilter" do + dsl.tilt_yielding + filter.should be_kind_of(Rake::Pipeline::Web::Filters::TiltYieldingFilter) + end + end end diff --git a/spec/tilt_yielding_filter_spec.rb b/spec/tilt_yielding_filter_spec.rb new file mode 100644 index 0000000..b094c20 --- /dev/null +++ b/spec/tilt_yielding_filter_spec.rb @@ -0,0 +1,134 @@ +describe "TiltYieldingFilter" do + TiltYieldingFilter ||= Rake::Pipeline::Web::Filters::TiltYieldingFilter + MemoryFileWrapper ||= Rake::Pipeline::SpecHelpers::MemoryFileWrapper + + before do + @input_path = "/path/to/input" + @output_path = "/path/to/output" + @encoding = "UTF-8" + @default_output = "a.out" + end + + before do + @input_files = [ + ["first.erb","First <%= yield %> /First"], + ["second.erb","Second <%= yield %> /Second"], + ["third.erb","Third"], + ].map{|a| MemoryFileWrapper.new @input_path, a.first, @encoding, a.last} + end + + describe "when output is generated using default options" do + before do + @filter = TiltYieldingFilter.new + @filter.file_wrapper_class = MemoryFileWrapper + @filter.input_files = @input_files + @filter.output_root = @output_path + @filter.rake_application = Rake::Application.new + end + + before do + @expected_output = "First Second Third /Second /First" + end + + it "should group the input files into one rake task" do + tasks = @filter.generate_rake_tasks + tasks.length.should == 1 + tasks.first.prerequisites.should =~ @input_files.map{|a| "#{a.root}/#{a.path}"} + end + + it "should generate the output nesting each input file at the yield" do + tasks = @filter.generate_rake_tasks + tasks.each(&:invoke) + MemoryFileWrapper.files["#{@output_path}/#{@default_output}"].body.should == @expected_output + end + end + + describe "when a yield order is provided for the generated output" do + before do + @yields_to = ["second","first","third"] + end + + before do + @filter = TiltYieldingFilter.new :yields_to => @yields_to + @filter.file_wrapper_class = MemoryFileWrapper + @filter.input_files = @input_files + @filter.output_root = @output_path + @filter.rake_application = Rake::Application.new + end + + before do + @expected_output = "Second First Third /First /Second" + end + + it "should generate the output nesting each input file at the yield with respect to the ordering" do + tasks = @filter.generate_rake_tasks + tasks.each(&:invoke) + MemoryFileWrapper.files["#{@output_path}/#{@default_output}"].body.should == @expected_output + end + end + + describe "when output is generated using a locals or a scope" do + before do + @input_files = [ + ["first.erb","<%= first %> <%= yield %> /<%= first %>"], + ["second.erb","<%= second %> <%= yield %> /<%= second %>"], + ["third.erb","<%= third %>"], + ].map{|a| MemoryFileWrapper.new @input_path, a.first, @encoding, a.last} + end + + before do + @first = "abc" + @second = "xyz" + @third = "oh snap!" + @expected_output = "#{@first} #{@second} #{@third} /#{@second} /#{@first}" + end + + describe "when locals are used to define the variables" do + before do + @filter = TiltYieldingFilter.new :locals => { + :first => @first, + :second => @second, + :third => @third + } + @filter.file_wrapper_class = MemoryFileWrapper + @filter.input_files = @input_files + @filter.output_root = @output_path + @filter.rake_application = Rake::Application.new + end + + it "should generate the output nesting each input file at the yield" do + tasks = @filter.generate_rake_tasks + tasks.each(&:invoke) + MemoryFileWrapper.files["#{@output_path}/#{@default_output}"].body.should == @expected_output + end + end + + describe "when a scope is used to define the variables" do + before do + @scope = Class.new do + def initialize first,second,third + @first = first + @second = second + @third = third + end + def first; @first; end + def second; @second; end + def third; @third; end + end.new(@first,@second,@third) + end + before do + @filter = TiltYieldingFilter.new :scope => @scope + @filter.file_wrapper_class = MemoryFileWrapper + @filter.input_files = @input_files + @filter.output_root = @output_path + @filter.rake_application = Rake::Application.new + end + + it "should generate the output nesting each input file at the yield" do + tasks = @filter.generate_rake_tasks + tasks.each(&:invoke) + MemoryFileWrapper.files["#{@output_path}/#{@default_output}"].body.should == @expected_output + end + end + end +end