diff --git a/lib/haml/exec.rb b/lib/haml/exec.rb index 42df9cc7..f6e86a6b 100644 --- a/lib/haml/exec.rb +++ b/lib/haml/exec.rb @@ -180,6 +180,10 @@ END 'Output style. Can be nested (default), compact, compressed, or expanded.') do |name| @options[:for_engine][:style] = name.to_sym end + opts.on('-l', '--line-comments', + 'Line Comments. Emit comments in the generated CSS indicating the corresponding sass line.') do + @options[:for_engine][:line_comments] = true + end end def process_result diff --git a/lib/sass.rb b/lib/sass.rb index 890c67a1..d87a1b90 100644 --- a/lib/sass.rb +++ b/lib/sass.rb @@ -846,12 +846,18 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir) # Defaults to false in production mode, true otherwise. # Only has meaning within Ruby on Rails or Merb. # -# [:template_location] The directory where Sass templates should be read from. +# [:template_location] A path to the root sass template directory for you application. +# If a hash, :css_location is ignored and this option designates +# both a mapping between input and output directories. +# May also be given a list of 2-element lists, instead of a hash. # Defaults to RAILS_ROOT + "/public/stylesheets/sass" # or MERB_ROOT + "/public/stylesheets/sass". # Only has meaning within Ruby on Rails or Merb. +# This will be derived from the :css_location path list if not provided +# by appending a folder of "sass" to each corresponding css location. # -# [:css_location] The directory where CSS output should be written to. +# [:css_location] The path where CSS output should be written to. +# This option is ignored when :template_location is a Hash. # Defaults to RAILS_ROOT + "/public/stylesheets" # or MERB_ROOT + "/public/stylesheets". # Only has meaning within Ruby on Rails or Merb. diff --git a/lib/sass/engine.rb b/lib/sass/engine.rb index 37653e51..bd5b7409 100644 --- a/lib/sass/engine.rb +++ b/lib/sass/engine.rb @@ -438,6 +438,12 @@ END raise SyntaxError.new("Unbalanced brackets.", @line) end + def import_paths + paths = @options[:load_paths] || [] + paths.unshift(File.dirname(@options[:filename])) if @options[:filename] + paths + end + def import(files) nodes = [] @@ -445,7 +451,7 @@ END engine = nil begin - filename = self.class.find_file_to_import(filename, @options[:load_paths]) + filename = self.class.find_file_to_import(filename, import_paths) rescue Exception => e raise SyntaxError.new(e.message, @line) end @@ -503,7 +509,9 @@ END def self.find_full_path(filename, load_paths) load_paths.each do |path| - ["_#{filename}", filename].each do |name| + segments = filename.split(File::SEPARATOR) + segments.push "_#{segments.pop}" + [segments.join(File::SEPARATOR), filename].each do |name| full_path = File.join(path, name) if File.readable?(full_path) return full_path diff --git a/lib/sass/plugin.rb b/lib/sass/plugin.rb index 11176f2d..b6564a7d 100644 --- a/lib/sass/plugin.rb +++ b/lib/sass/plugin.rb @@ -8,7 +8,6 @@ module Sass module Plugin class << self @@options = { - :template_location => './public/stylesheets/sass', :css_location => './public/stylesheets', :always_update => false, :always_check => true, @@ -35,44 +34,20 @@ module Sass @@options.merge!(value) end - # Checks each stylesheet in options[:css_location] - # to see if it needs updating, - # and updates it using the corresponding template - # from options[:templates] - # if it does. + # Checks each css stylesheet to see if it needs updating, + # and updates it using the corresponding sass template if it does. def update_stylesheets return if options[:never_update] @@checked_for_updates = true - Dir.glob(File.join(options[:template_location], "**", "*.sass")).entries.each do |file| + template_locations.zip(css_locations).each do |template_location, css_location| - # Get the relative path to the file with no extension - name = file.sub(options[:template_location] + "/", "")[0...-5] + Dir.glob(File.join(template_location, "**", "*.sass")).each do |file| + # Get the relative path to the file with no extension + name = file.sub(template_location + "/", "")[0...-5] - if !forbid_update?(name) && (options[:always_update] || stylesheet_needs_update?(name)) - css = css_filename(name) - File.delete(css) if File.exists?(css) - - filename = template_filename(name) - l_options = @@options.dup - l_options[:css_filename] = css - l_options[:filename] = filename - l_options[:load_paths] = load_paths - engine = Engine.new(File.read(filename), l_options) - result = begin - engine.render - rescue Exception => e - exception_string(e) - end - - # Create any directories that might be necessary - dirs = [l_options[:css_location]] - name.split("/")[0...-1].each { |dir| dirs << "#{dirs[-1]}/#{dir}" } - dirs.each { |dir| Dir.mkdir(dir) unless File.exist?(dir) } - - # Finally, write the file - File.open(css, 'w') do |file| - file.print(result) + if !forbid_update?(name) && (options[:always_update] || stylesheet_needs_update?(name, template_location, css_location)) + update_stylesheet(name, template_location, css_location) end end end @@ -80,8 +55,57 @@ module Sass private + def update_stylesheet(name, template_location, css_location) + css = css_filename(name, css_location) + File.delete(css) if File.exists?(css) + + filename = template_filename(name, template_location) + l_options = @@options.dup + l_options[:css_filename] = css + l_options[:filename] = filename + l_options[:load_paths] = load_paths + engine = Engine.new(File.read(filename), l_options) + result = begin + engine.render + rescue Exception => e + exception_string(e) + end + + # Create any directories that might be necessary + mkpath(css_location, name) + + # Finally, write the file + File.open(css, 'w') do |file| + file.print(result) + end + end + + # Create any successive directories required to be able to write a file to: File.join(base,name) + def mkpath(base, name) + dirs = [base] + name.split(File::SEPARATOR)[0...-1].each { |dir| dirs << File.join(dirs[-1],dir) } + dirs.each { |dir| Dir.mkdir(dir) unless File.exist?(dir) } + end + def load_paths - (options[:load_paths] || []) + [options[:template_location]] + (options[:load_paths] || []) + template_locations + end + + def template_locations + location = (options[:template_location] || File.join(options[:css_location],'sass')) + if location.is_a?(String) + [location] + else + location.to_a.map { |l| l.first } + end + end + + def css_locations + if options[:template_location] && !options[:template_location].is_a?(String) + options[:template_location].to_a.map { |l| l.last } + else + [options[:css_location]] + end end def exception_string(e) @@ -98,10 +122,14 @@ module Sass e_string << "\n\n" min = [e.sass_line - 5, 0].max - File.read(e.sass_filename).rstrip.split("\n")[ - min .. e.sass_line + 5 - ].each_with_index do |line, i| - e_string << "#{min + i + 1}: #{line}\n" + begin + File.read(e.sass_filename).rstrip.split("\n")[ + min .. e.sass_line + 5 + ].each_with_index do |line, i| + e_string << "#{min + i + 1}: #{line}\n" + end + rescue + e_string << "Couldn't read sass file: #{e.sass_filename}" end end end @@ -123,25 +151,27 @@ END end end - def template_filename(name) - "#{options[:template_location]}/#{name}.sass" + def template_filename(name, path) + "#{path}/#{name}.sass" end - def css_filename(name) - "#{options[:css_location]}/#{name}.css" + def css_filename(name, path) + "#{path}/#{name}.css" end def forbid_update?(name) name.sub(/^.*\//, '')[0] == ?_ end - def stylesheet_needs_update?(name) - if !File.exists?(css_filename(name)) + def stylesheet_needs_update?(name, template_path, css_path) + css_file = css_filename(name, css_path) + template_file = template_filename(name, template_path) + if !File.exists?(css_file) return true else - css_mtime = File.mtime(css_filename(name)) - File.mtime(template_filename(name)) > css_mtime || - dependencies(template_filename(name)).any?(&dependency_updated?(css_mtime)) + css_mtime = File.mtime(css_file) + File.mtime(template_file) > css_mtime || + dependencies(template_file).any?(&dependency_updated?(css_mtime)) end end diff --git a/lib/sass/tree/rule_node.rb b/lib/sass/tree/rule_node.rb index 1711958b..ad900d60 100644 --- a/lib/sass/tree/rule_node.rb +++ b/lib/sass/tree/rule_node.rb @@ -68,8 +68,16 @@ module Sass::Tree if @options[:line_comments] && @style != :compressed to_return << "#{old_spaces}/* line #{line}" - if filename && @options[:css_filename] - relative_filename = Pathname.new(filename).relative_path_from(Pathname.new(File.dirname(@options[:css_filename]))).to_s + if filename + relative_filename = if @options[:css_filename] + begin + Pathname.new(filename).relative_path_from( + Pathname.new(File.dirname(@options[:css_filename]))).to_s + rescue ArgumentError + nil + end + end + relative_filename ||= filename to_return << ", #{relative_filename}" end diff --git a/test/sass/more_results/more1.css b/test/sass/more_results/more1.css new file mode 100644 index 00000000..b0d1182f --- /dev/null +++ b/test/sass/more_results/more1.css @@ -0,0 +1,9 @@ +body { font: Arial; background: blue; } + +#page { width: 700px; height: 100; } +#page #header { height: 300px; } +#page #header h1 { font-size: 50px; color: blue; } + +#content.user.show #container.top #column.left { width: 100px; } +#content.user.show #container.top #column.right { width: 600px; } +#content.user.show #container.bottom { background: brown; } diff --git a/test/sass/more_results/more1_with_line_comments.css b/test/sass/more_results/more1_with_line_comments.css new file mode 100644 index 00000000..f31dbca2 --- /dev/null +++ b/test/sass/more_results/more1_with_line_comments.css @@ -0,0 +1,26 @@ +/* line 3, ../more_templates/more1.sass */ +body { + font: Arial; + background: blue; } + +/* line 7, ../more_templates/more1.sass */ +#page { + width: 700px; + height: 100; } + /* line 10, ../more_templates/more1.sass */ + #page #header { + height: 300px; } + /* line 12, ../more_templates/more1.sass */ + #page #header h1 { + font-size: 50px; + color: blue; } + +/* line 18, ../more_templates/more1.sass */ +#content.user.show #container.top #column.left { + width: 100px; } +/* line 20, ../more_templates/more1.sass */ +#content.user.show #container.top #column.right { + width: 600px; } +/* line 22, ../more_templates/more1.sass */ +#content.user.show #container.bottom { + background: brown; } diff --git a/test/sass/more_results/more_import.css b/test/sass/more_results/more_import.css new file mode 100644 index 00000000..97c4797e --- /dev/null +++ b/test/sass/more_results/more_import.css @@ -0,0 +1,29 @@ +imported { otherconst: hello; myconst: goodbye; pre-mixin: here; } + +body { font: Arial; background: blue; } + +#page { width: 700px; height: 100; } +#page #header { height: 300px; } +#page #header h1 { font-size: 50px; color: blue; } + +#content.user.show #container.top #column.left { width: 100px; } +#content.user.show #container.top #column.right { width: 600px; } +#content.user.show #container.bottom { background: brown; } + +midrule { inthe: middle; } + +body { font: Arial; background: blue; } + +#page { width: 700px; height: 100; } +#page #header { height: 300px; } +#page #header h1 { font-size: 50px; color: blue; } + +#content.user.show #container.top #column.left { width: 100px; } +#content.user.show #container.top #column.right { width: 600px; } +#content.user.show #container.bottom { background: brown; } + +@import url(basic.css); +@import url(../results/complex.css); +#foo { background-color: #baf; } + +nonimported { myconst: hello; otherconst: goodbye; post-mixin: here; } diff --git a/test/sass/more_templates/_more_partial.sass b/test/sass/more_templates/_more_partial.sass new file mode 100644 index 00000000..bef627d2 --- /dev/null +++ b/test/sass/more_templates/_more_partial.sass @@ -0,0 +1,2 @@ +#foo + :background-color #baf diff --git a/test/sass/more_templates/more1.sass b/test/sass/more_templates/more1.sass new file mode 100644 index 00000000..71117bf5 --- /dev/null +++ b/test/sass/more_templates/more1.sass @@ -0,0 +1,23 @@ + + +body + :font Arial + :background blue + +#page + :width 700px + :height 100 + #header + :height 300px + h1 + :font-size 50px + :color blue + +#content.user.show + #container.top + #column.left + :width 100px + #column.right + :width 600px + #container.bottom + :background brown \ No newline at end of file diff --git a/test/sass/more_templates/more_import.sass b/test/sass/more_templates/more_import.sass new file mode 100644 index 00000000..fa329b32 --- /dev/null +++ b/test/sass/more_templates/more_import.sass @@ -0,0 +1,11 @@ +!preconst = hello + +=premixin + pre-mixin: here + +@import importee, basic, basic.css, ../results/complex.css, more_partial + +nonimported + :myconst = !preconst + :otherconst = !postconst + +postmixin diff --git a/test/sass/plugin_test.rb b/test/sass/plugin_test.rb index f1a379e0..ef025669 100644 --- a/test/sass/plugin_test.rb +++ b/test/sass/plugin_test.rb @@ -14,13 +14,15 @@ class SassPluginTest < Test::Unit::TestCase } def setup - FileUtils.mkdir File.dirname(__FILE__) + '/tmp' + FileUtils.mkdir tempfile_loc + FileUtils.mkdir tempfile_loc(nil,"more_") set_plugin_opts Sass::Plugin.update_stylesheets end def teardown - FileUtils.rm_r File.dirname(__FILE__) + '/tmp' + FileUtils.rm_r tempfile_loc + FileUtils.rm_r tempfile_loc(nil,"more_") end def test_templates_should_render_correctly @@ -29,32 +31,32 @@ class SassPluginTest < Test::Unit::TestCase def test_no_update File.delete(tempfile_loc('basic')) - assert Sass::Plugin.stylesheet_needs_update?('basic') + assert Sass::Plugin.stylesheet_needs_update?('basic', template_loc, tempfile_loc) Sass::Plugin.update_stylesheets - assert !Sass::Plugin.stylesheet_needs_update?('basic') + assert !Sass::Plugin.stylesheet_needs_update?('basic', template_loc, tempfile_loc) end def test_update_needed_when_modified sleep(1) FileUtils.touch(template_loc('basic')) - assert Sass::Plugin.stylesheet_needs_update?('basic') + assert Sass::Plugin.stylesheet_needs_update?('basic', template_loc, tempfile_loc) Sass::Plugin.update_stylesheets - assert !Sass::Plugin.stylesheet_needs_update?('basic') + assert !Sass::Plugin.stylesheet_needs_update?('basic', template_loc, tempfile_loc) end def test_update_needed_when_dependency_modified sleep(1) FileUtils.touch(template_loc('basic')) - assert Sass::Plugin.stylesheet_needs_update?('import') + assert Sass::Plugin.stylesheet_needs_update?('import', template_loc, tempfile_loc) Sass::Plugin.update_stylesheets - assert !Sass::Plugin.stylesheet_needs_update?('import') + assert !Sass::Plugin.stylesheet_needs_update?('import', template_loc, tempfile_loc) end def test_full_exception_handling File.delete(tempfile_loc('bork')) Sass::Plugin.update_stylesheets File.open(tempfile_loc('bork')) do |file| - assert_equal("/*\nSass::SyntaxError: Undefined constant: \"!bork\".\non line 2 of #{File.dirname(__FILE__) + '/templates/bork.sass'}\n\n1: bork\n2: :bork= !bork", file.read.split("\n")[0...6].join("\n")) + assert_equal("/*\nSass::SyntaxError: Undefined constant: \"!bork\".\non line 2 of #{template_loc('bork')}\n\n1: bork\n2: :bork= !bork", file.read.split("\n")[0...6].join("\n")) end File.delete(tempfile_loc('bork')) end @@ -69,14 +71,34 @@ class SassPluginTest < Test::Unit::TestCase Sass::Plugin.options[:full_exception] = true end + + def test_two_template_directories + set_plugin_opts :template_location => { + template_loc => tempfile_loc, + template_loc(nil,'more_') => tempfile_loc(nil,'more_') + } + Sass::Plugin.update_stylesheets + ['more1', 'more_import'].each { |name| assert_renders_correctly(name, :prefix => 'more_') } + end + + def test_two_template_directories_with_line_annotations + set_plugin_opts :line_comments => true, + :style => :nested, + :template_location => { + template_loc => tempfile_loc, + template_loc(nil,'more_') => tempfile_loc(nil,'more_') + } + Sass::Plugin.update_stylesheets + assert_renders_correctly('more1_with_line_comments', 'more1', :prefix => 'more_') + end def test_rails_update File.delete(tempfile_loc('basic')) - assert Sass::Plugin.stylesheet_needs_update?('basic') + assert Sass::Plugin.stylesheet_needs_update?('basic', template_loc, tempfile_loc) ActionController::Base.new.process - assert !Sass::Plugin.stylesheet_needs_update?('basic') + assert !Sass::Plugin.stylesheet_needs_update?('basic', template_loc, tempfile_loc) end def test_merb_update @@ -105,33 +127,58 @@ class SassPluginTest < Test::Unit::TestCase private - def assert_renders_correctly(name) - File.read(result_loc(name)).split("\n").zip(File.read(tempfile_loc(name)).split("\n")).each_with_index do |pair, line| - message = "template: #{name}\nline: #{line + 1}" + def assert_renders_correctly(*arguments) + options = arguments.last.is_a?(Hash) ? arguments.pop : {} + prefix = options[:prefix] + result_name = arguments.shift + tempfile_name = arguments.shift || result_name + expected_lines = File.read(result_loc(result_name, prefix)).split("\n") + actual_lines = File.read(tempfile_loc(tempfile_name, prefix)).split("\n") + expected_lines.zip(actual_lines).each_with_index do |pair, line| + message = "template: #{result_name}\nline: #{line + 1}" assert_equal(pair.first, pair.last, message) end + if expected_lines.size < actual_lines.size + assert(false, "#{actual_lines.size - expected_lines.size} Trailing lines found in #{tempfile_name}.css: #{actual_lines[expected_lines.size..-1].join('\n')}") + end end - def template_loc(name) - File.dirname(__FILE__) + "/templates/#{name}.sass" + def template_loc(name = nil, prefix = nil) + if name + absolutize "#{prefix}templates/#{name}.sass" + else + absolutize "#{prefix}templates" + end end - def tempfile_loc(name) - File.dirname(__FILE__) + "/tmp/#{name}.css" + def tempfile_loc(name = nil, prefix = nil) + if name + absolutize "#{prefix}tmp/#{name}.css" + else + absolutize "#{prefix}tmp" + end end - def result_loc(name) - File.dirname(__FILE__) + "/results/#{name}.css" + def result_loc(name = nil, prefix = nil) + if name + absolutize "#{prefix}results/#{name}.css" + else + absolutize "#{prefix}results" + end end - def set_plugin_opts + def absolutize(file) + "#{File.dirname(__FILE__)}/#{file}" + end + + def set_plugin_opts(overrides = {}) Sass::Plugin.options = { - :template_location => File.dirname(__FILE__) + '/templates', - :css_location => File.dirname(__FILE__) + '/tmp', + :template_location => template_loc, + :css_location => tempfile_loc, :style => :compact, - :load_paths => [File.dirname(__FILE__) + '/results'], + :load_paths => [result_loc], :always_update => true, - } + }.merge(overrides) end end