PageRenderTime 125ms CodeModel.GetById 14ms app.highlight 102ms RepoModel.GetById 1ms app.codeStats 1ms

/scalate-jruby/src/main/resources/haml-3.0.25/lib/haml/exec.rb

http://github.com/scalate/scalate
Ruby | 871 lines | 689 code | 95 blank | 87 comment | 88 complexity | d5fb709456c0c9a5812b6705d10ec7a5 MD5 | raw file
  1require 'optparse'
  2require 'fileutils'
  3
  4module Haml
  5  # This module handles the various Haml executables (`haml`, `sass`, `sass-convert`, etc).
  6  module Exec
  7    # An abstract class that encapsulates the executable code for all three executables.
  8    class Generic
  9      # @param args [Array<String>] The command-line arguments
 10      def initialize(args)
 11        @args = args
 12        @options = {}
 13      end
 14
 15      # Parses the command-line arguments and runs the executable.
 16      # Calls `Kernel#exit` at the end, so it never returns.
 17      #
 18      # @see #parse
 19      def parse!
 20        begin
 21          parse
 22        rescue Exception => e
 23          raise e if @options[:trace] || e.is_a?(SystemExit)
 24
 25          $stderr.print "#{e.class}: " unless e.class == RuntimeError
 26          $stderr.puts "#{e.message}"
 27          $stderr.puts "  Use --trace for backtrace."
 28          exit 1
 29        end
 30        exit 0
 31      end
 32
 33      # Parses the command-line arguments and runs the executable.
 34      # This does not handle exceptions or exit the program.
 35      #
 36      # @see #parse!
 37      def parse
 38        @opts = OptionParser.new(&method(:set_opts))
 39        @opts.parse!(@args)
 40
 41        process_result
 42
 43        @options
 44      end
 45
 46      # @return [String] A description of the executable
 47      def to_s
 48        @opts.to_s
 49      end
 50
 51      protected
 52
 53      # Finds the line of the source template
 54      # on which an exception was raised.
 55      #
 56      # @param exception [Exception] The exception
 57      # @return [String] The line number
 58      def get_line(exception)
 59        # SyntaxErrors have weird line reporting
 60        # when there's trailing whitespace,
 61        # which there is for Haml documents.
 62        return (exception.message.scan(/:(\d+)/).first || ["??"]).first if exception.is_a?(::SyntaxError)
 63        (exception.backtrace[0].scan(/:(\d+)/).first || ["??"]).first
 64      end
 65
 66      # Tells optparse how to parse the arguments
 67      # available for all executables.
 68      #
 69      # This is meant to be overridden by subclasses
 70      # so they can add their own options.
 71      #
 72      # @param opts [OptionParser]
 73      def set_opts(opts)
 74        opts.on('-s', '--stdin', :NONE, 'Read input from standard input instead of an input file') do
 75          @options[:input] = $stdin
 76        end
 77
 78        opts.on('--trace', :NONE, 'Show a full traceback on error') do
 79          @options[:trace] = true
 80        end
 81
 82        opts.on('--unix-newlines', 'Use Unix-style newlines in written files.') do
 83          @options[:unix_newlines] = true if ::Haml::Util.windows?
 84        end
 85
 86        opts.on_tail("-?", "-h", "--help", "Show this message") do
 87          puts opts
 88          exit
 89        end
 90
 91        opts.on_tail("-v", "--version", "Print version") do
 92          puts("Haml/Sass #{::Haml.version[:string]}")
 93          exit
 94        end
 95      end
 96
 97      # Processes the options set by the command-line arguments.
 98      # In particular, sets `@options[:input]` and `@options[:output]`
 99      # to appropriate IO streams.
100      #
101      # This is meant to be overridden by subclasses
102      # so they can run their respective programs.
103      def process_result
104        input, output = @options[:input], @options[:output]
105        args = @args.dup
106        input ||=
107          begin
108            filename = args.shift
109            @options[:filename] = filename
110            open_file(filename) || $stdin
111          end
112        output ||= open_file(args.shift, 'w') || $stdout
113
114        @options[:input], @options[:output] = input, output
115      end
116
117      COLORS = { :red => 31, :green => 32, :yellow => 33 }
118
119      # Prints a status message about performing the given action,
120      # colored using the given color (via terminal escapes) if possible.
121      #
122      # @param name [#to_s] A short name for the action being performed.
123      #   Shouldn't be longer than 11 characters.
124      # @param color [Symbol] The name of the color to use for this action.
125      #   Can be `:red`, `:green`, or `:yellow`.
126      def puts_action(name, color, arg)
127        return if @options[:for_engine][:quiet]
128        printf color(color, "%11s %s\n"), name, arg
129      end
130
131      # Same as \{Kernel.puts}, but doesn't print anything if the `--quiet` option is set.
132      #
133      # @param args [Array] Passed on to \{Kernel.puts}
134      def puts(*args)
135        return if @options[:for_engine][:quiet]
136        Kernel.puts(*args)
137      end
138
139      # Wraps the given string in terminal escapes
140      # causing it to have the given color.
141      # If terminal esapes aren't supported on this platform,
142      # just returns the string instead.
143      #
144      # @param color [Symbol] The name of the color to use.
145      #   Can be `:red`, `:green`, or `:yellow`.
146      # @param str [String] The string to wrap in the given color.
147      # @return [String] The wrapped string.
148      def color(color, str)
149        raise "[BUG] Unrecognized color #{color}" unless COLORS[color]
150
151        # Almost any real Unix terminal will support color,
152        # so we just filter for Windows terms (which don't set TERM)
153        # and not-real terminals, which aren't ttys.
154        return str if ENV["TERM"].nil? || ENV["TERM"].empty? || !STDOUT.tty?
155        return "\e[#{COLORS[color]}m#{str}\e[0m"
156      end
157
158      private
159
160      def open_file(filename, flag = 'r')
161        return if filename.nil?
162        flag = 'wb' if @options[:unix_newlines] && flag == 'w'
163        File.open(filename, flag)
164      end
165
166      def handle_load_error(err)
167        dep = err.message[/^no such file to load -- (.*)/, 1]
168        raise err if @options[:trace] || dep.nil? || dep.empty?
169        $stderr.puts <<MESSAGE
170Required dependency #{dep} not found!
171    Run "gem install #{dep}" to get it.
172  Use --trace for backtrace.
173MESSAGE
174        exit 1
175      end
176    end
177
178    # An abstrac class that encapsulates the code
179    # specific to the `haml` and `sass` executables.
180    class HamlSass < Generic
181      # @param args [Array<String>] The command-line arguments
182      def initialize(args)
183        super
184        @options[:for_engine] = {}
185      end
186
187      protected
188
189      # Tells optparse how to parse the arguments
190      # available for the `haml` and `sass` executables.
191      #
192      # This is meant to be overridden by subclasses
193      # so they can add their own options.
194      #
195      # @param opts [OptionParser]
196      def set_opts(opts)
197        opts.banner = <<END
198Usage: #{@name.downcase} [options] [INPUT] [OUTPUT]
199
200Description:
201  Uses the #{@name} engine to parse the specified template
202  and outputs the result to the specified file.
203
204Options:
205END
206
207        opts.on('--rails RAILS_DIR', "Install Haml and Sass from the Gem to a Rails project") do |dir|
208          original_dir = dir
209
210          env = File.join(dir, "config", "environment.rb")
211          if File.exists?(File.join(dir, "Gemfile"))
212            puts("haml --rails isn't needed for Rails 3 or greater.",
213              "Add 'gem \"haml\"' to your Gemfile instead.", "",
214              "haml --rails will no longer work in the next version of #{@name}.", "")
215          elsif File.exists?(env) && File.open(env) {|env| env.grep(/config\.gem/)}
216            puts("haml --rails isn't needed for Rails 2.1 or greater.",
217              "Add 'config.gem \"haml\"' to config/environment.rb instead.", "",
218              "haml --rails will no longer work in the next version of #{@name}.", "")
219          end
220
221          dir = File.join(dir, 'vendor', 'plugins')
222
223          unless File.exists?(dir)
224            puts "Directory #{dir} doesn't exist"
225            exit 1
226          end
227
228          dir = File.join(dir, 'haml')
229
230          if File.exists?(dir)
231            print "Directory #{dir} already exists, overwrite [y/N]? "
232            exit 2 if gets !~ /y/i
233            FileUtils.rm_rf(dir)
234          end
235
236          begin
237            Dir.mkdir(dir)
238          rescue SystemCallError
239            puts "Cannot create #{dir}"
240            exit 1
241          end
242
243          File.open(File.join(dir, 'init.rb'), 'w') do |file|
244            file << File.read(File.dirname(__FILE__) + "/../../init.rb")
245          end
246
247          puts "Haml plugin added to #{original_dir}"
248          exit
249        end
250
251        opts.on('-c', '--check', "Just check syntax, don't evaluate.") do
252          require 'stringio'
253          @options[:check_syntax] = true
254          @options[:output] = StringIO.new
255        end
256
257        super
258      end
259
260      # Processes the options set by the command-line arguments.
261      # In particular, sets `@options[:for_engine][:filename]` to the input filename
262      # and requires the appropriate file.
263      #
264      # This is meant to be overridden by subclasses
265      # so they can run their respective programs.
266      def process_result
267        super
268        @options[:for_engine][:filename] = @options[:filename] if @options[:filename]
269        require File.dirname(__FILE__) + "/../#{@name.downcase}"
270      end
271    end
272
273    # The `sass` executable.
274    class Sass < HamlSass
275      # @param args [Array<String>] The command-line arguments
276      def initialize(args)
277        super
278        @name = "Sass"
279        @options[:for_engine][:load_paths] = ['.'] + (ENV['SASSPATH'] || '').split(File::PATH_SEPARATOR)
280      end
281
282      protected
283
284      # Tells optparse how to parse the arguments.
285      #
286      # @param opts [OptionParser]
287      def set_opts(opts)
288        super
289
290        opts.on('--scss',
291                'Use the CSS-superset SCSS syntax.') do
292          @options[:for_engine][:syntax] = :scss
293        end
294        opts.on('--watch', 'Watch files or directories for changes.',
295                           'The location of the generated CSS can be set using a colon:',
296                           '  sass --watch input.sass:output.css',
297                           '  sass --watch input-dir:output-dir') do
298          @options[:watch] = true
299        end
300        opts.on('--update', 'Compile files or directories to CSS.',
301                            'Locations are set like --watch.') do
302          @options[:update] = true
303        end
304        opts.on('--stop-on-error', 'If a file fails to compile, exit immediately.',
305                                   'Only meaningful for --watch and --update.') do
306          @options[:stop_on_error] = true
307        end
308        opts.on('-t', '--style NAME',
309                'Output style. Can be nested (default), compact, compressed, or expanded.') do |name|
310          @options[:for_engine][:style] = name.to_sym
311        end
312        opts.on('-q', '--quiet', 'Silence warnings and status messages during compilation.') do
313          @options[:for_engine][:quiet] = true
314        end
315        opts.on('-g', '--debug-info',
316                'Emit extra information in the generated CSS that can be used by the FireSass Firebug plugin.') do
317          @options[:for_engine][:debug_info] = true
318        end
319        opts.on('-l', '--line-numbers', '--line-comments',
320                'Emit comments in the generated CSS indicating the corresponding sass line.') do
321          @options[:for_engine][:line_numbers] = true
322        end
323        opts.on('-i', '--interactive',
324                'Run an interactive SassScript shell.') do
325          @options[:interactive] = true
326        end
327        opts.on('-I', '--load-path PATH', 'Add a sass import path.') do |path|
328          @options[:for_engine][:load_paths] << path
329        end
330        opts.on('-r', '--require LIB', 'Require a Ruby library before running Sass.') do |lib|
331          require lib
332        end
333        opts.on('--cache-location PATH', 'The path to put cached Sass files. Defaults to .sass-cache.') do |loc|
334          @options[:for_engine][:cache_location] = loc
335        end
336        opts.on('-C', '--no-cache', "Don't cache to sassc files.") do
337          @options[:for_engine][:cache] = false
338        end
339
340        unless ::Haml::Util.ruby1_8?
341          opts.on('-E encoding', 'Specify the default encoding for Sass files.') do |encoding|
342            Encoding.default_external = encoding
343          end
344        end
345      end
346
347      # Processes the options set by the command-line arguments,
348      # and runs the Sass compiler appropriately.
349      def process_result
350        if !@options[:update] && !@options[:watch] &&
351            @args.first && colon_path?(@args.first)
352          if @args.size == 1
353            @args = split_colon_path(@args.first)
354          else
355            @options[:update] = true
356          end
357        end
358
359        return interactive if @options[:interactive]
360        return watch_or_update if @options[:watch] || @options[:update]
361        super
362
363        begin
364          input = @options[:input]
365          output = @options[:output]
366
367          @options[:for_engine][:syntax] ||= :scss if input.is_a?(File) && input.path =~ /\.scss$/
368          tree =
369            if input.is_a?(File) && !@options[:check_syntax]
370              ::Sass::Files.tree_for(input.path, @options[:for_engine])
371            else
372              # We don't need to do any special handling of @options[:check_syntax] here,
373              # because the Sass syntax checking happens alongside evaluation
374              # and evaluation doesn't actually evaluate any code anyway.
375              ::Sass::Engine.new(input.read(), @options[:for_engine]).to_tree
376            end
377
378          input.close() if input.is_a?(File)
379
380          output.write(tree.render)
381          output.close() if output.is_a? File
382        rescue ::Sass::SyntaxError => e
383          raise e if @options[:trace]
384          raise e.sass_backtrace_str("standard input")
385        end
386      end
387
388      private
389
390      def interactive
391        require 'sass'
392        require 'sass/repl'
393        ::Sass::Repl.new(@options).run
394      end
395
396      def watch_or_update
397        require 'sass'
398        require 'sass/plugin'
399        ::Sass::Plugin.options.merge! @options[:for_engine]
400        ::Sass::Plugin.options[:unix_newlines] = @options[:unix_newlines]
401
402        raise <<MSG if @args.empty?
403What files should I watch? Did you mean something like:
404    sass --watch input.sass:output.css
405    sass --watch input-dir:output-dir
406MSG
407
408        if !colon_path?(@args[0]) && probably_dest_dir?(@args[1])
409          flag = @options[:update] ? "--update" : "--watch"
410          err =
411            if !File.exist?(@args[1])
412              "doesn't exist"
413            elsif @args[1] =~ /\.css$/
414              "is a CSS file"
415            end
416          raise <<MSG if err
417File #{@args[1]} #{err}.
418    Did you mean: sass #{flag} #{@args[0]}:#{@args[1]}
419MSG
420        end
421
422        dirs, files = @args.map {|name| split_colon_path(name)}.
423          partition {|i, _| File.directory? i}
424        files.map! {|from, to| [from, to || from.gsub(/\..*?$/, '.css')]}
425        dirs.map! {|from, to| [from, to || from]}
426        ::Sass::Plugin.options[:template_location] = dirs
427
428        ::Sass::Plugin.on_updating_stylesheet do |_, css|
429          if File.exists? css
430            puts_action :overwrite, :yellow, css
431          else
432            puts_action :create, :green, css
433          end
434        end
435
436        had_error = false
437        ::Sass::Plugin.on_creating_directory {|dirname| puts_action :directory, :green, dirname}
438        ::Sass::Plugin.on_deleting_css {|filename| puts_action :delete, :yellow, filename}
439        ::Sass::Plugin.on_compilation_error do |error, _, _|
440          raise error unless error.is_a?(::Sass::SyntaxError) && !@options[:stop_on_error]
441          had_error = true
442          puts_action :error, :red, "#{error.sass_filename} (Line #{error.sass_line}: #{error.message})"
443        end
444
445        if @options[:update]
446          ::Sass::Plugin.update_stylesheets(files)
447          exit 1 if had_error
448          return
449        end
450
451        puts ">>> Sass is watching for changes. Press Ctrl-C to stop."
452
453        ::Sass::Plugin.on_template_modified {|template| puts ">>> Change detected to: #{template}"}
454        ::Sass::Plugin.on_template_created {|template| puts ">>> New template detected: #{template}"}
455        ::Sass::Plugin.on_template_deleted {|template| puts ">>> Deleted template detected: #{template}"}
456
457        ::Sass::Plugin.watch(files)
458      end
459
460      def colon_path?(path)
461        !split_colon_path(path)[1].nil?
462      end
463
464      def split_colon_path(path)
465        one, two = path.split(':', 2)
466        if one && two && ::Haml::Util.windows? &&
467            one =~ /\A[A-Za-z]\Z/ && two =~ /\A[\/\\]/
468          # If we're on Windows and we were passed a drive letter path,
469          # don't split on that colon.
470          one2, two = two.split(':', 2)
471          one = one + ':' + one2
472        end
473        return one, two
474      end
475
476      # Whether path is likely to be meant as the destination
477      # in a source:dest pair.
478      def probably_dest_dir?(path)
479        return false unless path
480        return false if colon_path?(path)
481        return Dir.glob(File.join(path, "*.s[ca]ss")).empty?
482      end
483    end
484
485    # The `haml` executable.
486    class Haml < HamlSass
487      # @param args [Array<String>] The command-line arguments
488      def initialize(args)
489        super
490        @name = "Haml"
491        @options[:requires] = []
492        @options[:load_paths] = []
493      end
494
495      # Tells optparse how to parse the arguments.
496      #
497      # @param opts [OptionParser]
498      def set_opts(opts)
499        super
500
501        opts.on('-t', '--style NAME',
502                'Output style. Can be indented (default) or ugly.') do |name|
503          @options[:for_engine][:ugly] = true if name.to_sym == :ugly
504        end
505
506        opts.on('-f', '--format NAME',
507                'Output format. Can be xhtml (default), html4, or html5.') do |name|
508          @options[:for_engine][:format] = name.to_sym
509        end
510
511        opts.on('-e', '--escape-html',
512                'Escape HTML characters (like ampersands and angle brackets) by default.') do
513          @options[:for_engine][:escape_html] = true
514        end
515
516        opts.on('-q', '--double-quote-attributes',
517                'Set attribute wrapper to double-quotes (default is single).') do
518          @options[:for_engine][:attr_wrapper] = '"'
519        end
520
521        opts.on('-r', '--require FILE', "Same as 'ruby -r'.") do |file|
522          @options[:requires] << file
523        end
524
525        opts.on('-I', '--load-path PATH', "Same as 'ruby -I'.") do |path|
526          @options[:load_paths] << path
527        end
528
529        unless ::Haml::Util.ruby1_8?
530          opts.on('-E ex[:in]', 'Specify the default external and internal character encodings.') do |encoding|
531            external, internal = encoding.split(':')
532            Encoding.default_external = external if external && !external.empty?
533            Encoding.default_internal = internal if internal && !internal.empty?
534          end
535        end
536
537        opts.on('--debug', "Print out the precompiled Ruby source.") do
538          @options[:debug] = true
539        end
540      end
541
542      # Processes the options set by the command-line arguments,
543      # and runs the Haml compiler appropriately.
544      def process_result
545        super
546        input = @options[:input]
547        output = @options[:output]
548
549        template = input.read()
550        input.close() if input.is_a? File
551
552        begin
553          engine = ::Haml::Engine.new(template, @options[:for_engine])
554          if @options[:check_syntax]
555            puts "Syntax OK"
556            return
557          end
558
559          @options[:load_paths].each {|p| $LOAD_PATH << p}
560          @options[:requires].each {|f| require f}
561
562          if @options[:debug]
563            puts engine.precompiled
564            puts '=' * 100
565          end
566
567          result = engine.to_html
568        rescue Exception => e
569          raise e if @options[:trace]
570
571          case e
572          when ::Haml::SyntaxError; raise "Syntax error on line #{get_line e}: #{e.message}"
573          when ::Haml::Error;       raise "Haml error on line #{get_line e}: #{e.message}"
574          else raise "Exception on line #{get_line e}: #{e.message}\n  Use --trace for backtrace."
575          end
576        end
577
578        output.write(result)
579        output.close() if output.is_a? File
580      end
581    end
582
583    # The `html2haml` executable.
584    class HTML2Haml < Generic
585      # @param args [Array<String>] The command-line arguments
586      def initialize(args)
587        super
588        @module_opts = {}
589      end
590
591      # Tells optparse how to parse the arguments.
592      #
593      # @param opts [OptionParser]
594      def set_opts(opts)
595        opts.banner = <<END
596Usage: html2haml [options] [INPUT] [OUTPUT]
597
598Description: Transforms an HTML file into corresponding Haml code.
599
600Options:
601END
602
603        opts.on('-e', '--erb', 'Parse ERb tags.') do
604          @module_opts[:erb] = true
605        end
606
607        opts.on('--no-erb', "Don't parse ERb tags.") do
608          @options[:no_erb] = true
609        end
610
611        opts.on('-r', '--rhtml', 'Deprecated; same as --erb.') do
612          @module_opts[:erb] = true
613        end
614
615        opts.on('--no-rhtml', "Deprecated; same as --no-erb.") do
616          @options[:no_erb] = true
617        end
618
619        opts.on('-x', '--xhtml', 'Parse the input using the more strict XHTML parser.') do
620          @module_opts[:xhtml] = true
621        end
622
623        super
624      end
625
626      # Processes the options set by the command-line arguments,
627      # and runs the HTML compiler appropriately.
628      def process_result
629        super
630
631        require 'haml/html'
632
633        input = @options[:input]
634        output = @options[:output]
635
636        @module_opts[:erb] ||= input.respond_to?(:path) && input.path =~ /\.(rhtml|erb)$/
637        @module_opts[:erb] &&= @options[:no_erb] != false
638
639        output.write(::Haml::HTML.new(input, @module_opts).render)
640      rescue ::Haml::Error => e
641        raise "#{e.is_a?(::Haml::SyntaxError) ? "Syntax error" : "Error"} on line " +
642          "#{get_line e}: #{e.message}"
643      rescue LoadError => err
644        handle_load_error(err)
645      end
646    end
647
648    # The `sass-convert` executable.
649    class SassConvert < Generic
650      # @param args [Array<String>] The command-line arguments
651      def initialize(args)
652        super
653        require 'sass'
654        @options[:for_tree] = {}
655        @options[:for_engine] = {:cache => false, :read_cache => true}
656      end
657
658      # Tells optparse how to parse the arguments.
659      #
660      # @param opts [OptionParser]
661      def set_opts(opts)
662        opts.banner = <<END
663Usage: sass-convert [options] [INPUT] [OUTPUT]
664
665Description:
666  Converts between CSS, Sass, and SCSS files.
667  E.g. converts from SCSS to Sass,
668  or converts from CSS to SCSS (adding appropriate nesting).
669
670Options:
671END
672
673        opts.on('-F', '--from FORMAT',
674          'The format to convert from. Can be css, scss, sass, less, or sass2.',
675          'sass2 is the same as sass, but updates more old syntax to new.',
676          'By default, this is inferred from the input filename.',
677          'If there is none, defaults to css.') do |name|
678          @options[:from] = name.downcase.to_sym
679          unless [:css, :scss, :sass, :less, :sass2].include?(@options[:from])
680            raise "Unknown format for sass-convert --from: #{name}"
681          end
682          try_less_note if @options[:from] == :less
683        end
684
685        opts.on('-T', '--to FORMAT',
686          'The format to convert to. Can be scss or sass.',
687          'By default, this is inferred from the output filename.',
688          'If there is none, defaults to sass.') do |name|
689          @options[:to] = name.downcase.to_sym
690          unless [:scss, :sass].include?(@options[:to])
691            raise "Unknown format for sass-convert --to: #{name}"
692          end
693        end
694
695        opts.on('-R', '--recursive',
696          'Convert all the files in a directory. Requires --from and --to.') do
697          @options[:recursive] = true
698        end
699
700        opts.on('-i', '--in-place',
701          'Convert a file to its own syntax.',
702          'This can be used to update some deprecated syntax.') do
703          @options[:in_place] = true
704        end
705
706        opts.on('--dasherize', 'Convert underscores to dashes') do
707          @options[:for_tree][:dasherize] = true
708        end
709
710        opts.on('--old', 'Output the old-style ":prop val" property syntax.',
711                         'Only meaningful when generating Sass.') do
712          @options[:for_tree][:old] = true
713        end
714
715        opts.on('-C', '--no-cache', "Don't cache to sassc files.") do
716          @options[:for_engine][:read_cache] = false
717        end
718
719        unless ::Haml::Util.ruby1_8?
720          opts.on('-E encoding', 'Specify the default encoding for Sass and CSS files.') do |encoding|
721            Encoding.default_external = encoding
722          end
723        end
724
725        super
726      end
727
728      # Processes the options set by the command-line arguments,
729      # and runs the CSS compiler appropriately.
730      def process_result
731        require 'sass'
732
733        if @options[:recursive]
734          process_directory
735          return
736        end
737
738        super
739        input = @options[:input]
740        raise "Error: '#{input.path}' is a directory (did you mean to use --recursive?)" if File.directory?(input)
741        output = @options[:output]
742        output = input if @options[:in_place]
743        process_file(input, output)
744      end
745
746      private
747
748      def process_directory
749        unless input = @options[:input] = @args.shift
750          raise "Error: directory required when using --recursive."
751        end
752
753        output = @options[:output] = @args.shift
754        raise "Error: --from required when using --recursive." unless @options[:from]
755        raise "Error: --to required when using --recursive." unless @options[:to]
756        raise "Error: '#{@options[:input]}' is not a directory" unless File.directory?(@options[:input])
757        if @options[:output] && File.exists?(@options[:output]) && !File.directory?(@options[:output])
758          raise "Error: '#{@options[:output]}' is not a directory"
759        end
760        @options[:output] ||= @options[:input]
761
762        from = @options[:from]
763        from = :sass if from == :sass2
764        if @options[:to] == @options[:from] && !@options[:in_place]
765          fmt = @options[:from]
766          raise "Error: converting from #{fmt} to #{fmt} without --in-place"
767        end
768
769        ext = @options[:from]
770        ext = :sass if ext == :sass2
771        Dir.glob("#{@options[:input]}/**/*.#{ext}") do |f|
772          output =
773            if @options[:in_place]
774              f
775            elsif @options[:output]
776              output_name = f.gsub(/\.(c|sa|sc|le)ss$/, ".#{@options[:to]}")
777              output_name[0...@options[:input].size] = @options[:output]
778              output_name
779            else
780              f.gsub(/\.(c|sa|sc|le)ss$/, ".#{@options[:to]}")
781            end
782
783          unless File.directory?(File.dirname(output))
784            puts_action :directory, :green, File.dirname(output)
785            FileUtils.mkdir_p(File.dirname(output))
786          end
787          puts_action :convert, :green, f
788          if File.exists?(output)
789            puts_action :overwrite, :yellow, output
790          else
791            puts_action :create, :green, output
792          end
793
794          input = open_file(f)
795          output = @options[:in_place] ? input : open_file(output, "w")
796          process_file(input, output)
797        end
798      end
799
800      def process_file(input, output)
801        if input.is_a?(File)
802          @options[:from] ||=
803            case input.path
804            when /\.scss$/; :scss
805            when /\.sass$/; :sass
806            when /\.less$/; :less
807            when /\.css$/; :css
808            end
809        elsif @options[:in_place]
810          raise "Error: the --in-place option requires a filename."
811        end
812
813        if output.is_a?(File)
814          @options[:to] ||=
815            case output.path
816            when /\.scss$/; :scss
817            when /\.sass$/; :sass
818            end
819        end
820
821        if @options[:from] == :sass2
822          @options[:from] = :sass
823          @options[:for_engine][:sass2] = true
824        end
825
826        @options[:from] ||= :css
827        @options[:to] ||= :sass
828        @options[:for_engine][:syntax] = @options[:from]
829
830        out =
831          ::Haml::Util.silence_haml_warnings do
832            if @options[:from] == :css
833              require 'sass/css'
834              ::Sass::CSS.new(input.read, @options[:for_tree]).render(@options[:to])
835            elsif @options[:from] == :less
836              require 'sass/less'
837              try_less_note
838              input = input.read if input.is_a?(IO) && !input.is_a?(File) # Less is dumb
839              Less::Engine.new(input).to_tree.to_sass_tree.send("to_#{@options[:to]}", @options[:for_tree])
840            else
841              if input.is_a?(File)
842                ::Sass::Files.tree_for(input.path, @options[:for_engine])
843              else
844                ::Sass::Engine.new(input.read, @options[:for_engine]).to_tree
845              end.send("to_#{@options[:to]}", @options[:for_tree])
846            end
847          end
848
849        output = File.open(input.path, 'w') if @options[:in_place]
850        output.write(out)
851      rescue ::Sass::SyntaxError => e
852        raise e if @options[:trace]
853        file = " of #{e.sass_filename}" if e.sass_filename
854        raise "Error on line #{e.sass_line}#{file}: #{e.message}\n  Use --trace for backtrace"
855      rescue LoadError => err
856        handle_load_error(err)
857      end
858
859      @@less_note_printed = false
860      def try_less_note
861        return if @@less_note_printed
862        @@less_note_printed = true
863        warn <<NOTE
864* NOTE: Sass and Less are different languages, and they work differently.
865* I'll do my best to translate, but some features -- especially mixins --
866* should be checked by hand.
867NOTE
868      end
869    end
870  end
871end