All Downloads are FREE. Search and download functionalities are using the official Maven repository.

haml-3.0.25.lib.haml.exec.rb Maven / Gradle / Ivy

The newest version!
require 'optparse'
require 'fileutils'

module Haml
  # This module handles the various Haml executables (`haml`, `sass`, `sass-convert`, etc).
  module Exec
    # An abstract class that encapsulates the executable code for all three executables.
    class Generic
      # @param args [Array] The command-line arguments
      def initialize(args)
        @args = args
        @options = {}
      end

      # Parses the command-line arguments and runs the executable.
      # Calls `Kernel#exit` at the end, so it never returns.
      #
      # @see #parse
      def parse!
        begin
          parse
        rescue Exception => e
          raise e if @options[:trace] || e.is_a?(SystemExit)

          $stderr.print "#{e.class}: " unless e.class == RuntimeError
          $stderr.puts "#{e.message}"
          $stderr.puts "  Use --trace for backtrace."
          exit 1
        end
        exit 0
      end

      # Parses the command-line arguments and runs the executable.
      # This does not handle exceptions or exit the program.
      #
      # @see #parse!
      def parse
        @opts = OptionParser.new(&method(:set_opts))
        @opts.parse!(@args)

        process_result

        @options
      end

      # @return [String] A description of the executable
      def to_s
        @opts.to_s
      end

      protected

      # Finds the line of the source template
      # on which an exception was raised.
      #
      # @param exception [Exception] The exception
      # @return [String] The line number
      def get_line(exception)
        # SyntaxErrors have weird line reporting
        # when there's trailing whitespace,
        # which there is for Haml documents.
        return (exception.message.scan(/:(\d+)/).first || ["??"]).first if exception.is_a?(::SyntaxError)
        (exception.backtrace[0].scan(/:(\d+)/).first || ["??"]).first
      end

      # Tells optparse how to parse the arguments
      # available for all executables.
      #
      # This is meant to be overridden by subclasses
      # so they can add their own options.
      #
      # @param opts [OptionParser]
      def set_opts(opts)
        opts.on('-s', '--stdin', :NONE, 'Read input from standard input instead of an input file') do
          @options[:input] = $stdin
        end

        opts.on('--trace', :NONE, 'Show a full traceback on error') do
          @options[:trace] = true
        end

        opts.on('--unix-newlines', 'Use Unix-style newlines in written files.') do
          @options[:unix_newlines] = true if ::Haml::Util.windows?
        end

        opts.on_tail("-?", "-h", "--help", "Show this message") do
          puts opts
          exit
        end

        opts.on_tail("-v", "--version", "Print version") do
          puts("Haml/Sass #{::Haml.version[:string]}")
          exit
        end
      end

      # Processes the options set by the command-line arguments.
      # In particular, sets `@options[:input]` and `@options[:output]`
      # to appropriate IO streams.
      #
      # This is meant to be overridden by subclasses
      # so they can run their respective programs.
      def process_result
        input, output = @options[:input], @options[:output]
        args = @args.dup
        input ||=
          begin
            filename = args.shift
            @options[:filename] = filename
            open_file(filename) || $stdin
          end
        output ||= open_file(args.shift, 'w') || $stdout

        @options[:input], @options[:output] = input, output
      end

      COLORS = { :red => 31, :green => 32, :yellow => 33 }

      # Prints a status message about performing the given action,
      # colored using the given color (via terminal escapes) if possible.
      #
      # @param name [#to_s] A short name for the action being performed.
      #   Shouldn't be longer than 11 characters.
      # @param color [Symbol] The name of the color to use for this action.
      #   Can be `:red`, `:green`, or `:yellow`.
      def puts_action(name, color, arg)
        return if @options[:for_engine][:quiet]
        printf color(color, "%11s %s\n"), name, arg
      end

      # Same as \{Kernel.puts}, but doesn't print anything if the `--quiet` option is set.
      #
      # @param args [Array] Passed on to \{Kernel.puts}
      def puts(*args)
        return if @options[:for_engine][:quiet]
        Kernel.puts(*args)
      end

      # Wraps the given string in terminal escapes
      # causing it to have the given color.
      # If terminal esapes aren't supported on this platform,
      # just returns the string instead.
      #
      # @param color [Symbol] The name of the color to use.
      #   Can be `:red`, `:green`, or `:yellow`.
      # @param str [String] The string to wrap in the given color.
      # @return [String] The wrapped string.
      def color(color, str)
        raise "[BUG] Unrecognized color #{color}" unless COLORS[color]

        # Almost any real Unix terminal will support color,
        # so we just filter for Windows terms (which don't set TERM)
        # and not-real terminals, which aren't ttys.
        return str if ENV["TERM"].nil? || ENV["TERM"].empty? || !STDOUT.tty?
        return "\e[#{COLORS[color]}m#{str}\e[0m"
      end

      private

      def open_file(filename, flag = 'r')
        return if filename.nil?
        flag = 'wb' if @options[:unix_newlines] && flag == 'w'
        File.open(filename, flag)
      end

      def handle_load_error(err)
        dep = err.message[/^no such file to load -- (.*)/, 1]
        raise err if @options[:trace] || dep.nil? || dep.empty?
        $stderr.puts <] The command-line arguments
      def initialize(args)
        super
        @options[:for_engine] = {}
      end

      protected

      # Tells optparse how to parse the arguments
      # available for the `haml` and `sass` executables.
      #
      # This is meant to be overridden by subclasses
      # so they can add their own options.
      #
      # @param opts [OptionParser]
      def set_opts(opts)
        opts.banner = <] The command-line arguments
      def initialize(args)
        super
        @name = "Sass"
        @options[:for_engine][:load_paths] = ['.'] + (ENV['SASSPATH'] || '').split(File::PATH_SEPARATOR)
      end

      protected

      # Tells optparse how to parse the arguments.
      #
      # @param opts [OptionParser]
      def set_opts(opts)
        super

        opts.on('--scss',
                'Use the CSS-superset SCSS syntax.') do
          @options[:for_engine][:syntax] = :scss
        end
        opts.on('--watch', 'Watch files or directories for changes.',
                           'The location of the generated CSS can be set using a colon:',
                           '  sass --watch input.sass:output.css',
                           '  sass --watch input-dir:output-dir') do
          @options[:watch] = true
        end
        opts.on('--update', 'Compile files or directories to CSS.',
                            'Locations are set like --watch.') do
          @options[:update] = true
        end
        opts.on('--stop-on-error', 'If a file fails to compile, exit immediately.',
                                   'Only meaningful for --watch and --update.') do
          @options[:stop_on_error] = true
        end
        opts.on('-t', '--style NAME',
                'Output style. Can be nested (default), compact, compressed, or expanded.') do |name|
          @options[:for_engine][:style] = name.to_sym
        end
        opts.on('-q', '--quiet', 'Silence warnings and status messages during compilation.') do
          @options[:for_engine][:quiet] = true
        end
        opts.on('-g', '--debug-info',
                'Emit extra information in the generated CSS that can be used by the FireSass Firebug plugin.') do
          @options[:for_engine][:debug_info] = true
        end
        opts.on('-l', '--line-numbers', '--line-comments',
                'Emit comments in the generated CSS indicating the corresponding sass line.') do
          @options[:for_engine][:line_numbers] = true
        end
        opts.on('-i', '--interactive',
                'Run an interactive SassScript shell.') do
          @options[:interactive] = true
        end
        opts.on('-I', '--load-path PATH', 'Add a sass import path.') do |path|
          @options[:for_engine][:load_paths] << path
        end
        opts.on('-r', '--require LIB', 'Require a Ruby library before running Sass.') do |lib|
          require lib
        end
        opts.on('--cache-location PATH', 'The path to put cached Sass files. Defaults to .sass-cache.') do |loc|
          @options[:for_engine][:cache_location] = loc
        end
        opts.on('-C', '--no-cache', "Don't cache to sassc files.") do
          @options[:for_engine][:cache] = false
        end

        unless ::Haml::Util.ruby1_8?
          opts.on('-E encoding', 'Specify the default encoding for Sass files.') do |encoding|
            Encoding.default_external = encoding
          end
        end
      end

      # Processes the options set by the command-line arguments,
      # and runs the Sass compiler appropriately.
      def process_result
        if !@options[:update] && !@options[:watch] &&
            @args.first && colon_path?(@args.first)
          if @args.size == 1
            @args = split_colon_path(@args.first)
          else
            @options[:update] = true
          end
        end

        return interactive if @options[:interactive]
        return watch_or_update if @options[:watch] || @options[:update]
        super

        begin
          input = @options[:input]
          output = @options[:output]

          @options[:for_engine][:syntax] ||= :scss if input.is_a?(File) && input.path =~ /\.scss$/
          tree =
            if input.is_a?(File) && !@options[:check_syntax]
              ::Sass::Files.tree_for(input.path, @options[:for_engine])
            else
              # We don't need to do any special handling of @options[:check_syntax] here,
              # because the Sass syntax checking happens alongside evaluation
              # and evaluation doesn't actually evaluate any code anyway.
              ::Sass::Engine.new(input.read(), @options[:for_engine]).to_tree
            end

          input.close() if input.is_a?(File)

          output.write(tree.render)
          output.close() if output.is_a? File
        rescue ::Sass::SyntaxError => e
          raise e if @options[:trace]
          raise e.sass_backtrace_str("standard input")
        end
      end

      private

      def interactive
        require 'sass'
        require 'sass/repl'
        ::Sass::Repl.new(@options).run
      end

      def watch_or_update
        require 'sass'
        require 'sass/plugin'
        ::Sass::Plugin.options.merge! @options[:for_engine]
        ::Sass::Plugin.options[:unix_newlines] = @options[:unix_newlines]

        raise <>> Sass is watching for changes. Press Ctrl-C to stop."

        ::Sass::Plugin.on_template_modified {|template| puts ">>> Change detected to: #{template}"}
        ::Sass::Plugin.on_template_created {|template| puts ">>> New template detected: #{template}"}
        ::Sass::Plugin.on_template_deleted {|template| puts ">>> Deleted template detected: #{template}"}

        ::Sass::Plugin.watch(files)
      end

      def colon_path?(path)
        !split_colon_path(path)[1].nil?
      end

      def split_colon_path(path)
        one, two = path.split(':', 2)
        if one && two && ::Haml::Util.windows? &&
            one =~ /\A[A-Za-z]\Z/ && two =~ /\A[\/\\]/
          # If we're on Windows and we were passed a drive letter path,
          # don't split on that colon.
          one2, two = two.split(':', 2)
          one = one + ':' + one2
        end
        return one, two
      end

      # Whether path is likely to be meant as the destination
      # in a source:dest pair.
      def probably_dest_dir?(path)
        return false unless path
        return false if colon_path?(path)
        return Dir.glob(File.join(path, "*.s[ca]ss")).empty?
      end
    end

    # The `haml` executable.
    class Haml < HamlSass
      # @param args [Array] The command-line arguments
      def initialize(args)
        super
        @name = "Haml"
        @options[:requires] = []
        @options[:load_paths] = []
      end

      # Tells optparse how to parse the arguments.
      #
      # @param opts [OptionParser]
      def set_opts(opts)
        super

        opts.on('-t', '--style NAME',
                'Output style. Can be indented (default) or ugly.') do |name|
          @options[:for_engine][:ugly] = true if name.to_sym == :ugly
        end

        opts.on('-f', '--format NAME',
                'Output format. Can be xhtml (default), html4, or html5.') do |name|
          @options[:for_engine][:format] = name.to_sym
        end

        opts.on('-e', '--escape-html',
                'Escape HTML characters (like ampersands and angle brackets) by default.') do
          @options[:for_engine][:escape_html] = true
        end

        opts.on('-q', '--double-quote-attributes',
                'Set attribute wrapper to double-quotes (default is single).') do
          @options[:for_engine][:attr_wrapper] = '"'
        end

        opts.on('-r', '--require FILE', "Same as 'ruby -r'.") do |file|
          @options[:requires] << file
        end

        opts.on('-I', '--load-path PATH', "Same as 'ruby -I'.") do |path|
          @options[:load_paths] << path
        end

        unless ::Haml::Util.ruby1_8?
          opts.on('-E ex[:in]', 'Specify the default external and internal character encodings.') do |encoding|
            external, internal = encoding.split(':')
            Encoding.default_external = external if external && !external.empty?
            Encoding.default_internal = internal if internal && !internal.empty?
          end
        end

        opts.on('--debug', "Print out the precompiled Ruby source.") do
          @options[:debug] = true
        end
      end

      # Processes the options set by the command-line arguments,
      # and runs the Haml compiler appropriately.
      def process_result
        super
        input = @options[:input]
        output = @options[:output]

        template = input.read()
        input.close() if input.is_a? File

        begin
          engine = ::Haml::Engine.new(template, @options[:for_engine])
          if @options[:check_syntax]
            puts "Syntax OK"
            return
          end

          @options[:load_paths].each {|p| $LOAD_PATH << p}
          @options[:requires].each {|f| require f}

          if @options[:debug]
            puts engine.precompiled
            puts '=' * 100
          end

          result = engine.to_html
        rescue Exception => e
          raise e if @options[:trace]

          case e
          when ::Haml::SyntaxError; raise "Syntax error on line #{get_line e}: #{e.message}"
          when ::Haml::Error;       raise "Haml error on line #{get_line e}: #{e.message}"
          else raise "Exception on line #{get_line e}: #{e.message}\n  Use --trace for backtrace."
          end
        end

        output.write(result)
        output.close() if output.is_a? File
      end
    end

    # The `html2haml` executable.
    class HTML2Haml < Generic
      # @param args [Array] The command-line arguments
      def initialize(args)
        super
        @module_opts = {}
      end

      # Tells optparse how to parse the arguments.
      #
      # @param opts [OptionParser]
      def set_opts(opts)
        opts.banner = < e
        raise "#{e.is_a?(::Haml::SyntaxError) ? "Syntax error" : "Error"} on line " +
          "#{get_line e}: #{e.message}"
      rescue LoadError => err
        handle_load_error(err)
      end
    end

    # The `sass-convert` executable.
    class SassConvert < Generic
      # @param args [Array] The command-line arguments
      def initialize(args)
        super
        require 'sass'
        @options[:for_tree] = {}
        @options[:for_engine] = {:cache => false, :read_cache => true}
      end

      # Tells optparse how to parse the arguments.
      #
      # @param opts [OptionParser]
      def set_opts(opts)
        opts.banner = < e
        raise e if @options[:trace]
        file = " of #{e.sass_filename}" if e.sass_filename
        raise "Error on line #{e.sass_line}#{file}: #{e.message}\n  Use --trace for backtrace"
      rescue LoadError => err
        handle_load_error(err)
      end

      @@less_note_printed = false
      def try_less_note
        return if @@less_note_printed
        @@less_note_printed = true
        warn <




© 2015 - 2025 Weber Informatics LLC | Privacy Policy